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) or SSE (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 (-32601 Method Not Found, -32602 Invalid Params, -32603 Internal 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.

Related on MCPowered