Build, debug, and ship Model Context Protocol servers in Python (FastMCP) and TypeScript — the whole loop on one page.
The Model Context Protocol is a JSON-RPC 2.0 wire protocol that lets a language-model client (Claude Desktop, Claude Code, an agent runtime) talk to an external server that exposes tools, resources, and prompts. The server runs in a separate process. The client launches it or connects to it. They speak JSON over a transport: stdio, Streamable HTTP, or SSE.
MCP is not an LLM API. It does not call models. It is the bridge between a model-facing client and the outside world. A server might wrap a database, a shell, a file system, a web search, a hardware device, or a SaaS API. The client decides which servers to load, and the model decides which tools to call.
| Tools | Functions the model can call. JSON-Schema input, free-form output. The high-impact primitive. |
|---|---|
| Resources | Read-only data the client can attach to the model's context. URI-addressed. Optional. |
| Prompts | Named prompt templates the user can pick from a menu. Optional, often skipped. |
If you only build one primitive, build tools. Resources and prompts are nice; tools are why MCP exists.
initialize with its protocol version and capabilities.notifications/initialized. Session is now live.tools/list, resources/list, prompts/list to enumerate.Both SDKs are first-party. Pick by where your existing code lives.
| Python (FastMCP) | Fastest to write. Decorator-based. Best if your tool wraps a Python library, a pandas pipeline, an ML model, or a CLI. |
|---|---|
| TypeScript | Best if your tool wraps a Node API, a browser automation library, or you want to ship a single-binary stdio server via npx. |
| Other (Go, Rust, C#) | Community SDKs exist. Stable enough for personal use; for distribution prefer the two first-party SDKs until your need is specific. |
Most servers published on the public registries are Python. Most servers shipped inside paid products are TypeScript (smaller image, faster cold-start). For a first server, Python.
mcp[cli] is the official Python SDK. FastMCP is the high-level API on top.
# pyproject: dependencies = ["mcp[cli]>=1.2"]
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather")
@mcp.tool()
def get_forecast(city: str, days: int = 1) -> str:
"""Return a plain-text weather forecast for a city.
Args:
city: City name in English.
days: 1-7. Defaults to 1.
"""
return fetch_forecast(city, days) # your code
@mcp.resource("weather://{city}/current")
def current(city: str) -> str:
return fetch_current(city)
if __name__ == "__main__":
mcp.run() # stdio transport by default
Three things to notice. First, the docstring becomes the tool description the model sees. Write it for the model, not for a human reader. Second, type hints become the JSON Schema. int = 1 means optional with default. Use Literal['a','b'] for enums. Third, mcp.run() blocks on stdio. For HTTP, see section 7.
weather, github-search, polymarket-data.get_forecast, create_issue, place_order. The model picks tools by name; pick names that read like an English imperative.weather://city/current, github://repo/issues/123.FastMCP marshals return values into MCP content blocks. Three rules.
str for plain text. The simplest case.dict or list and FastMCP serializes as JSON inside a text block. The model reads it fine; this is the default for structured data.mcp.types.ImageContent(data=..., mimeType=...) for images. The data is base64. The model can see it if the client supports image content.Raise. FastMCP catches and converts to an MCP error response. The model sees the message; show it something actionable.
@mcp.tool()
def transfer(amount: float, to: str) -> str:
if amount <= 0:
raise ValueError("amount must be > 0")
if amount > balance():
raise ValueError(
f"insufficient balance: have {balance()},"
f" need {amount}"
)
return do_transfer(amount, to)
Good error text reads as an instruction to the caller: what went wrong, what would fix it. Bad error text is a stack trace or "internal error". The model can recover from the first; the second wastes a turn.
Inject an mcp.server.fastmcp.Context parameter into any tool to get access to logging, progress reporting, and the active session.
from mcp.server.fastmcp import FastMCP, Context
@mcp.tool()
async def crawl(url: str, depth: int, ctx: Context) -> str:
pages = enumerate_pages(url, depth)
for i, page in enumerate(pages):
await ctx.report_progress(i, len(pages))
ctx.info(f"fetched {page.url}")
return summarize(pages)
report_progress is shown by graphical clients (Claude Desktop shows a bar). ctx.info / ctx.warning / ctx.error go to the client's log surface; the model does not see them. Use them for post-mortem debugging, not for routing model behavior.
@modelcontextprotocol/sdk is the official TypeScript SDK. The API mirrors the Python one but is more explicit (no decorators).
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", version: "0.1.0",
});
server.tool(
"get_forecast",
"Return a forecast for a city.",
{
city: z.string(),
days: z.number().int().min(1).max(7).default(1),
},
async ({ city, days }) => {
const text = await fetchForecast(city, days);
return { content: [{ type: "text", text }] };
},
);
const transport = new StdioServerTransport();
await server.connect(transport);
Use Zod for input schemas. The SDK converts Zod to JSON Schema for the protocol. Return shape is { content: [...] } with one or more content blocks. Each block has a type (text, image, resource).
package.json bin field points at the entry file (with #!/usr/bin/env node). Users invoke via npx your-server.console.log to stdout. Use console.error for diagnostics so the client doesn't choke parsing JSON.The client launches the server as a subprocess. Requests go in on stdin, responses on stdout, diagnostics on stderr. Zero network. Works everywhere. Default for Claude Desktop and Claude Code server configs.
One POST endpoint. Client sends JSON-RPC requests; server streams responses as Server-Sent Events. Replaces the older SSE transport. Use for any server that runs out-of-process, on a different machine, or behind a load balancer.
# Python (FastMCP) -- HTTP server on :8000
mcp.run(
transport="streamable-http",
host="0.0.0.0",
port=8000,
)
Older two-endpoint design: GET /sse for the event stream, POST /messages for requests. New servers should not implement SSE. Existing servers should migrate when the client matrix permits.
| Local dev / personal use | stdio |
|---|---|
| Shipping a public server / multi-user | Streamable HTTP behind TLS |
| Inside a private LAN service | Streamable HTTP, no auth or simple bearer |
| Connecting to a legacy MCP server | SSE, but migrate it |
Stdio servers do not authenticate; the OS process boundary is the trust boundary. The user trusts the client; the client trusts the server it launches. If a stdio server needs an API key, read it from an environment variable the client injects.
| Bearer token | Simple. Header Authorization: Bearer <token>. Good for personal-use servers and behind-the-firewall deployments. |
|---|---|
| OAuth 2.1 (MCP spec) | The protocol-blessed path for multi-user public servers. Client performs an OAuth flow; resulting token attached to every request. More moving parts but interoperable. |
| mTLS | If you control both ends and operate at scale. Not common in MCP land yet. |
Authorization headers.The model decides whether to call your tool based on the tool name, description, and parameter schema. Treat the description as the tool's marketing copy aimed at the model. Three rules.
Test by reading the description aloud and asking: if I knew nothing else, would I know when to call this? If you have to clarify out loud, the model will also have to guess.
Every wrong-shape call costs the user a turn and the operator credibility. Defensive schema design pays back many times over.
severity: 'low' | 'medium' | 'high' is hit every time; severity: str will get "normal" and "medium-high" and 12 other variants."2026-05-27T10:00:00Z", not "tomorrow at 10".temperature: float (0.0 to 2.0). Without the range, models clamp to their priors and you get 0.7 forever.{ k: v } open-ended, define a list of {key: str, value: str} records. Open dicts produce empty dicts.{ "error": "..." } — the model has no convention for noticing it.The Inspector is the official debugging tool. It is an Electron app that talks to your server like a client would and shows the wire traffic.
# launch your server under the Inspector
npx @modelcontextprotocol/inspector python my_server.py
# then open the URL it prints (usually http://localhost:5173)
Use it to confirm tools/list matches what you intended, to exercise tool calls with hand-crafted arguments, and to watch what your server actually returns.
print, console.log, logging.basicConfig (default writes to stderr — fine; but check).python from its own PATH. If your tool needs a venv, point the client config at the venv's python directly, not at python.chmod +x your_server.py for executables; or have the client invoke the interpreter explicitly.search, the model picks one and ignores the other. Prefix when in doubt.Claude Desktop reads claude_desktop_config.json on launch. macOS path: ~/Library/Application Support/Claude/claude_desktop_config.json. Windows: %APPDATA%\Claude\claude_desktop_config.json.
{
"mcpServers": {
"weather": {
"command": "uvx",
"args": ["weather-mcp"],
"env": { "WEATHER_API_KEY": "..." }
},
"local-py": {
"command": "/Users/me/proj/.venv/bin/python",
"args": ["/Users/me/proj/server.py"]
}
}
}
Restart Claude Desktop after editing. The slider icon (bottom-right of the prompt box) lists loaded servers. A red dot means startup failed; click it for the error.
Claude Code stores MCP server configs in ~/.claude.json under the mcpServers key, with the same shape as Claude Desktop's config. The CLI also accepts ad-hoc additions:
# add a stdio server for the current project
claude mcp add weather -- uvx weather-mcp
# add a remote HTTP server
claude mcp add --transport http example https://mcp.example.com
# list servers visible to the current project
claude mcp list
Project-scoped servers live in .mcp.json at the project root and are committed to the repo, so collaborators get the same toolset. User-scoped servers live in ~/.claude.json and follow you across projects.
If you are shipping a public server, the bare minimum stack is:
GET /health returning 200) so the platform can route traffic only to live instances.id of each request so you can correlate across multi-turn sessions.uvx <pkg> and npx <pkg> are the canonical user invocations. A README with a copy-paste Claude Desktop config block doubles install rate.servers repo on GitHub, Smithery, PulseMCP). Submitting takes ten minutes and is worth it.list_issues with create_issue, read_file with edit_file. The model uses read to ground itself before writing.dry_run: bool = false and return what would happen. Models love being asked to confirm.idempotency_key so re-tries don't duplicate.get_user should not also create a user record if one is missing. The name is a promise.delete_database should never be a single tool call. Pair with confirm_delete_database(token) where the token comes from the first call's output.subprocess.run(shell=True) with model-supplied arguments is a remote-code-execution sink.pathlib.Path(p).resolve().is_relative_to(root) is the cheap test.Three layers, in order of importance:
# Python: in-process integration test
import pytest
from mcp.shared.memory import (
create_connected_server_and_client_session,
)
from my_server import mcp
@pytest.mark.asyncio
async def test_get_forecast():
async with create_connected_server_and_client_session(
mcp._mcp_server
) as (_, client):
result = await client.call_tool(
"get_forecast", {"city": "Mumbai"}
)
assert "Mumbai" in result.content[0].text
async def tools natively.Treat your tool surface like a public API. Models trained against an old surface still work against a new one only if you add, not remove.
| Adding a tool | Safe. Bump minor. |
|---|---|
| Adding an optional parameter | Safe. Bump minor. |
| Adding a required parameter | Breaking. Bump major OR provide a default and keep optional. |
| Renaming a tool | Breaking. Keep the old name as an alias for one major version. |
| Removing a tool | Breaking. Bump major. Announce in CHANGELOG. |
| Changing return shape | Breaking, even if it 'looks compatible' — the model has memorized your old shape. |
If you must break: document the migration in the README, and consider shipping the old and new tool side-by-side for one release so users can update at their pace.
| Client | The model-facing application (Claude Desktop, Claude Code, agent runtime) that loads servers. |
|---|---|
| Server | An external process that exposes tools/resources/prompts via MCP. |
| Tool | A function the model can call. Has a name, description, and JSON-Schema input. |
| Resource | A read-only blob of data the client can attach to the model's context, addressed by URI. |
| Prompt | A named, parameterized prompt template offered to the user. |
| Transport | How bytes move between client and server. Stdio, Streamable HTTP, SSE (deprecated). |
| Capability | A protocol feature a side declares it supports during the initialize handshake. |
| Inspector | The official Electron debugger for MCP servers. |
| FastMCP | The decorator-based high-level API in the Python SDK. |
| JSON-RPC 2.0 | The wire format MCP uses. Requests have id/method/params; responses have id and result-or-error. |
Bookmark all six. You will reference them while building, every time.
Send me your REST or OpenAPI API. I ship a production MCP server — auth, error handling, rate limiting, security-scanned, deployed — in about 5 days for a $900 flat fee. Built by the maintainer of this directory.
Get a production MCP server →An MCP (Model Context Protocol) server is a JSON-RPC 2.0 program that exposes tools, resources, and prompts to an AI client like Claude. The client calls your tools; your server runs the logic and returns content blocks the model reads.
Both SDKs are first-party and equally capable. Pick by where your existing code lives: Python (FastMCP, the mcp[cli] package) if your API/logic is already in Python; TypeScript (@modelcontextprotocol/sdk) if it's in Node.
Add a stdio entry to claude_desktop_config.json with the command and args that launch your server, then restart Claude Desktop. The Deployment: Claude Desktop section above has the exact JSON.
The model only acts on what the description says. Write it for the model, not a human: state what the tool does, when to use it, and the exact format of each argument. Vague descriptions cause the model to call tools wrong or not at all.
Yes. Protodex builds production MCP servers from your existing REST/OpenAPI API — auth, error handling, rate limiting, security-scanned, deployed — for a $900 flat fee in about 5 days. Send your API docs on the build page.