MCP Server Authoring
Build MCP servers in TypeScript or Python that any MCP-compatible client can consume.
When to Use
- You want to expose an internal API or tool to Claude Desktop / Claude Code / Cursor
- You're building a shareable MCP server (npm, PyPI, or Smithery registry)
- You need to wrap read-only data sources as MCP resources
- You want agents to invoke your service without custom glue code per client
Core Model
An MCP server exposes three primitives:
| Primitive | Purpose | Side effects |
|---|---|---|
| Tool | Action the agent calls | May have side effects (writes, network calls) |
| Resource | Data the agent reads | Read-only, addressed by URI |
| Prompt | Reusable prompt template | No side effects; user-invocable |
Pick the right primitive — exposing read operations as tools wastes context when they should be resources.
Quickstart — TypeScript (stdio)
npm init -y
npm i @modelcontextprotocol/sdk zod
npm i -D typescript tsx @types/node
// src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "weather-server",
version: "1.0.0",
});
server.tool(
"get_forecast",
"Get a weather forecast for a location",
{
location: z.string().describe("City name or lat,lng"),
days: z.number().int().min(1).max(14).default(3),
},
async ({ location, days }) => {
const data = await fetchForecast(location, days);
return {
content: [{ type: "text", text: JSON.stringify(data) }],
};
},
);
server.resource(
"station",
"weather://stations/{id}",
async (uri) => {
const id = uri.pathname.split("/").pop()!;
const station = await fetchStation(id);
return {
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(station) }],
};
},
);
const transport = new StdioServerTransport();
await server.connect(transport);
Run it and register in the client's mcp.json:
{
"mcpServers": {
"weather": {
"command": "npx",
"args": ["-y", "tsx", "src/server.ts"]
}
}
}
Quickstart — Python (stdio)
uv add "mcp[cli]"
# server.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather-server")
@mcp.tool()
async def get_forecast(location: str, days: int = 3) -> dict:
"""Get a weather forecast for a location."""
return await fetch_forecast(location, days)
@mcp.resource("weather://stations/{id}")
async def station(id: str) -> dict:
return await fetch_station(id)
if __name__ == "__main__":
mcp.run()
Tool Contract Rules
- Name: snake_case, verb-led (
search_issues, notIssueSearcher) - Description: one sentence, starts with a verb, mentions key args
- Schema: use Zod / Pydantic with descriptive
.describe()on every field - Return shape: always
{ content: [{ type: "text", text: ... }] }— wrap JSON as text - Errors: throw — the SDK converts to
isError: true. Include remediation in the message - Idempotency: write tools should accept an
idempotency_keywhen possible
Progressive Disclosure Pattern
Don't dump 40 tools on the client. Expose a small stable surface and let tools return follow-up handles:
server.tool("list_projects", "List accessible projects", {}, async () => {
const projects = await api.listProjects();
return {
content: [{
type: "text",
text: `Found ${projects.length} projects. Use get_project with id to read one:\n` +
projects.map(p => `- ${p.id}: ${p.name}`).join("\n"),
}],
};
});
Testing
Use the MCP Inspector during development:
npx @modelcontextprotocol/inspector tsx src/server.ts
It gives you a UI to call every tool, inspect every resource, and replay prompts. Run it in CI against a mock backend to catch schema regressions.
Publishing
- npm: publish the server as a
binentry so users runnpx your-mcp-server - Smithery registry: add
smithery.yamlwith config schema for one-click install - Private: ship as a Docker image and point
commandatdocker run
Best Practices
- Keep your tool surface ≤ 15 tools — more hurts model selection accuracy
- Return text, not binary — for large payloads use resources with URIs
- Stream long-running tools via
server.sendNotificationprogress events - Version your server's
namesuffix (github-v2) when making breaking changes - Never read env vars for secrets at tool-call time — load at startup and fail fast
- Log every tool invocation server-side — agents behave unpredictably, you'll need the audit trail