Build
MCP Python SDK
The official Python SDK for MCP wraps the JSON-RPC 2.0 wire mechanics, the
initialize handshake, and capability negotiation. Server authors write
Python that says what their tools do; the SDK handles the protocol plumbing
underneath.
What the SDK gives you
- Decorators for the 3 primitives.
@server.tool()declares a Tool and auto-generates its JSON Schema input definition from the function's Python type hints.@server.resource()declares a Resource with a URI handler.@server.prompt()declares a parameterized prompt template. - Async event loop. The server is built on
asyncio. Multiple concurrent client requests share one process; long-running tool handlers do not block the JSON-RPC read loop. - Transport abstraction. The same server code runs over
stdio(the agent client spawns the server as a child process) orSSE(the server listens on an HTTP endpoint and streams responses). Transport choice is a runtime flag; the server code stays the same. - Standard error responses. Raised exceptions map to JSON-RPC
2.0 error codes (
-32601Method Not Found,-32602Invalid Params,-32603Internal Error). The SDK formats the response envelope; the handler raises an exception with a message.
Minimal server shape
A working server is roughly 10-20 lines. Import the SDK, instantiate a server, declare a Tool with a typed function, run the transport.
from mcp.server import Server
from mcp.server.stdio import stdio_server
server = Server("example-server")
@server.tool()
async def add(a: int, b: int) -> int:
"""Add two integers."""
return a + b
if __name__ == "__main__":
import asyncio
asyncio.run(stdio_server(server))
The type hints on add(a: int, b: int) -> int generate the JSON
Schema the client uses to format the call. The docstring becomes the tool
description the model reads at tools/list time. Switching to SSE
is one import change: from mcp.server.sse import sse_server and
the matching asyncio.run() call.
The trust posture for Python servers
A Python MCP server runs with the host process's privileges. The agent client
(Claude Code, Claude Desktop, Cursor, Windsurf) spawns the Python interpreter
as a child process. Inside that process, the SDK and every package you
pip install have access to the same environment variables, API
tokens, and filesystem paths the user's shell has.
That moves the trust boundary up the stack. Any dependency the server imports
shares the server's permissions. A compromised transitive package can read
~/.aws/credentials or exfiltrate the OAuth token the server uses
to talk to its upstream API. The protocol provides no sandbox.
Three concrete practices close most of the gap. Pin every dependency with a
version hash in requirements.txt or pyproject.toml
and audit the lockfile when it changes. Read the source of any transitive
dependency that touches the filesystem, network, or subprocess module before
adopting it. Run the server in a least-privileged shell environment (no
unrelated API keys in the environment when the server starts) so a compromise
cannot exfiltrate credentials that the server has no business reading.