Building Your First MCP Server in TypeScript or Python
You need a real MCP server you can install in a host, not a protocol essay.
This recipe scaffolds a minimal stdio server in Python (FastMCP) or TypeScript (@modelcontextprotocol/sdk), registers tools with schemas, and connects a desktop or IDE client.
Summary
Pick one language, install the official SDK, register one or two tools with clear descriptions, run over stdio with stderr-only logging, then point a host config at the absolute command path and verify tools appear.
Recipe
- Choose Python 3.10+ with
mcp[cli]or Node 18+ with@modelcontextprotocol/sdkand Zod 3. - Create a project folder and install dependencies (
uv/pipornpm). - Instantiate a server with a stable
nameandversion(TS) or FastMCP name (Python). - Register tools: name, description, typed inputs, async-safe handler, text content out.
- Wire stdio transport; log only to stderr.
- Run/build the entrypoint; confirm the process waits without printing banners to stdout.
- Add a host config entry with absolute paths; restart the host; list tools; call one tool.
- Commit a smoke client or script that initializes, lists tools, and calls
echo/pingfor CI.
Working Example
Python (FastMCP)
# server.py - verify package APIs at build
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("first-server")
@mcp.tool()
def add(a: float, b: float) -> str:
"""Add two numbers and return a short explanation."""
return f"{a} + {b} = {a + b}"
@mcp.tool()
async def fetch_status(service: str) -> str:
"""Return a fake health status for a named service."""
# Replace with real HTTP later; keep side effects explicit.
return f"{service}: ok"
if __name__ == "__main__":
mcp.run(transport="stdio")pip install "mcp[cli]"
python server.pyHost config sketch:
{
"mcpServers": {
"first-server": {
"command": "python",
"args": ["/ABSOLUTE/PATH/TO/server.py"]
}
}
}TypeScript (official SDK)
// src/index.ts - verify import paths at build
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: "first-server",
version: "1.0.0",
});
server.registerTool(
"add",
{
description: "Add two numbers and return a short explanation",
inputSchema: {
a: z.number().describe("First addend"),
b: z.number().describe("Second addend"),
},
},
async ({ a, b }) => ({
content: [{ type: "text", text: `${a} + ${b} = ${a + b}` }],
}),
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("first-server listening on stdio");
}
main().catch((err) => {
console.error(err);
process.exit(1);
});npm init -y
npm install @modelcontextprotocol/sdk zod@3
npm install -D typescript @types/node
# set "type": "module", build with tsc to build/index.js
node build/index.jsDeep Dive
Why two SDKs look different but mean the same
Both stacks implement the same protocol roles: server identity, tool registry, schema, transport.
Python FastMCP optimizes for decorator ergonomics and docstring-driven descriptions.
TypeScript optimizes for explicit registerTool metadata and Zod validation at the edge.
Agents and hosts only care that tools/list and tools/call behave.
Project layout that survives growth
first-server/
server.py # or src/index.ts
pyproject.toml # or package.json + tsconfig.json
README.md # how to run + host config snippet
tests/ # client harness smoke testsKeep handlers thin. Put HTTP clients, DB access, and policy in modules the tools call.
Descriptions are product UX for models
Write tool descriptions as instructions to a junior teammate: what it does, when to use it, what not to pass.
Field-level .describe(...) / docstring Args sections reduce invalid calls more than retry loops do.
Build vs run for TypeScript
Many hosts execute node path/to/build/index.js.
A forgotten npm run build is a frequent "server won't connect" report.
Pin Node and SDK versions in CI the same way you pin agent frameworks.
Environment variables for secrets
stdio servers inherit env from the host config (env map in many clients).
Do not hardcode API keys in source. Document required variables in README.
When to stop at "first server"
Ship when: tools list, one call succeeds, logs stay on stderr, and a smoke test is green.
Add resources, prompts, HTTP, and OAuth only after that baseline.
Gotchas
- Stdout pollution -
print/console.logon stdio corrupts JSON-RPC. - Relative paths in host config - use absolute command and script paths.
- Wrong Python interpreter - host must use the venv that has
mcpinstalled. - TS not built - host points at missing or stale
build/index.js. - Zod major mismatch - install the Zod major the SDK docs pin (often zod@3 - verify at build).
- Overbroad tools - "do_anything" tools get misused; split by capability.
- Blocking the event loop - long sync work stalls other calls; use async I/O.
- Missing error content - uncaught exceptions may drop the session; return structured error text when possible.
Alternatives
| Approach | When it wins | When it loses |
|---|---|---|
| Python FastMCP | Fast prototypes, data/ML stacks | Team standard is Node |
| TypeScript SDK | Same language as web agents | Python-only ops |
| Low-level Server API | Max control over capabilities | More boilerplate |
| Existing third-party MCP server | Zero build | Wrong domain or trust model |
| Plain REST + custom tool wrappers | Already have OpenAPI agents | No shared host ecosystem |
FAQs
Which language should I pick first?
Pick the language your team already ships. Protocol interoperability means a Python server still works with a TypeScript host.
Do I need async tools?
Use async when the tool awaits network or disk. Sync is fine for pure CPU-light helpers, but avoid long blocks.
How do I version the server?
Bump version in TS server metadata and tag releases. Mention schema changes in the changelog so hosts re-test tool lists.
Can I expose the same tools on HTTP later?
Yes. Keep registration code shared; swap only the transport entrypoint when you deploy remotely.
What is the smallest useful tool set?
One health/echo tool for connectivity plus one real domain tool. Connectivity-only servers never get exercised by agents.
How do I debug a silent host failure?
Run the server under your smoke client, check stderr logs, and confirm absolute paths and env vars in the host config.
Are prompts and resources required?
No for a first server. Tools alone are enough to prove the stack.
Where do I put multi-file business logic?
In plain modules imported by tool handlers. Avoid stuffing DB sessions into transport setup beyond shared factories.
Related
Related: Building & Deploying MCP Basics
Related: Anatomy of an MCP Server: Handlers, Schemas, and Transport
Related: Exposing Resources and Prompts, Not Just Tools, from an MCP Server
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.