{"slug": "how-to-build-an-mcp-server-step-by-step", "title": "How to build an MCP server, step by step", "summary": "A developer detailed a step-by-step guide to building a Model Context Protocol (MCP) server, using the Python FastMCP SDK and targeting protocol revision 2025-11-25. The guide covers creating a minimal server, defining tools with typed inputs, and optionally exposing resources and prompts, with a note on the upcoming 2026-07-28 revision that changes the wire format.", "body_md": "**Short answer**\n\n**To build an MCP server: install an official MCP SDK, declare your tools with typed inputs, optionally expose resources and prompts, run the server over stdio or HTTP, then connect an MCP client like Claude and test it.** A minimal Python server is about ten lines; the work is in choosing what to expose and validating every input.\n\nThis is the *build*. For what MCP is, its three primitives, and how it differs from an API, start with [what is the Model Context Protocol](https://aiarch.dev/model-context-protocol) — this page assumes that and goes straight to code.\n\nYou need very little to get a server running locally:\n\n`pip`\n\nto manage the environment.Conceptually a server exposes [three things — tools (model-callable functions), resources (readable data), and prompts (reusable templates)](https://modelcontextprotocol.io). The steps below add them in that order. Exact SDK signatures evolve, so treat the snippets as the current shape and check the live docs before shipping.\n\n**Which spec revision this builds against.** The code here targets MCP revision `2025-11-25`\n\n— the revision the spec's versioning page still names as the current protocol version. Revision `2026-07-28`\n\nis published and reworks the wire format substantially. A server built against `2025-11-25`\n\nstays conformant today; [what the new revision changes for a server author](https://aiarch.dev/how-to-build-an-mcp-server#spec-revision) is set out below, so you can build now and plan the move.\n\nCreate a project, install the SDK, and write the smallest server that runs. With `uv`\n\n:\n\n```\nuv init weather\ncd weather\nuv venv\nsource .venv/bin/activate\n\n# Install the MCP SDK (with the CLI extras) plus anything your tools need\nuv add \"mcp[cli]\" httpx\n```\n\nThen create `server.py`\n\nwith the server object and an entry point. [ FastMCP is the high-level Python API](https://github.com/modelcontextprotocol/python-sdk): you name the server and run it over a transport.\n\n``` python\nfrom mcp.server.fastmcp import FastMCP\n\n# Name the server; clients see this name on connect\nmcp = FastMCP(\"weather\")\n\nif __name__ == \"__main__\":\n    # stdio is the default local transport\n    mcp.run(transport=\"stdio\")\n```\n\nThat already runs — it just exposes nothing yet. In the TypeScript SDK the equivalent is [creating an McpServer from @modelcontextprotocol/sdk and connecting it to a transport](https://github.com/modelcontextprotocol/typescript-sdk); the structure is the same, only the syntax differs.\n\nA tool is a function the model can decide to call. In the Python SDK you decorate a normal typed function: the **type hints become the input JSON schema** and the **docstring becomes the description** the model reads to decide when to call it.\n\n``` php\n@mcp.tool()\ndef add(a: int, b: int) -> int:\n    \"\"\"Add two numbers and return the sum.\"\"\"\n    return a + b\n```\n\nReal tools wrap something useful — a database query, an internal API, a file operation. The handler is just a function, so this is where you call your existing service and return a result the model can use:\n\n``` php\n@mcp.tool()\nasync def get_forecast(latitude: float, longitude: float) -> str:\n    \"\"\"Get the weather forecast for a location.\"\"\"\n    data = await call_weather_api(latitude, longitude)\n    return format_forecast(data)\n```\n\nTwo things matter here, both architectural: the schema is the *contract* the model fills in, so keep argument names and descriptions precise; and the function runs with whatever privileges the process has, so scope it to the minimum it needs. We come back to that in Step 5.\n\nTools take actions; **resources** expose data the model can read, and **prompts** are reusable templates. Not every server needs all three — add what your use case calls for.\n\nA resource is addressed by a URI. It can be static, or a template with parameters the client fills in:\n\n``` php\n@mcp.resource(\"config://app\")\ndef get_config() -> str:\n    \"\"\"Static configuration the model can read.\"\"\"\n    return \"weather-app v1.0\"\n\n@mcp.resource(\"weather://{city}/current\")\ndef current_weather(city: str) -> str:\n    \"\"\"Current conditions for a named city.\"\"\"\n    return load_conditions(city)\n```\n\nA prompt is a parameterized template the server publishes so common workflows are authored once, not re-written in every client:\n\n``` php\n@mcp.prompt()\ndef summarize_forecast(city: str) -> str:\n    return f\"Summarize the weather outlook for {city} in two sentences.\"\n```\n\nThe distinction is deliberate: a client may surface resources and prompts to the user (to pull into context or pick from a menu) while letting the model invoke tools autonomously. Keeping them separate is what makes a server predictable across clients.\n\nThere are two transports you will actually use:\n\n`mcp.run(transport=\"stdio\")`\n\n.`mcp.run(transport=\"streamable-http\")`\n\n. This is the transport revision `2026-07-28`\n\nreworks hardest: protocol-level sessions and the `Mcp-Session-Id`\n\nheader are removed, and `Mcp-Method`\n\n, `Mcp-Name`\n\nand `MCP-Protocol-Version`\n\nbecome required request headers. Your `2025-11-25`\n\nserver keeps working — see To connect a local stdio server to Claude Desktop, point its config at the command that launches your server. The fastest path is to let the SDK install it for you:\n\n```\nuv run mcp install server.py\n```\n\nOr write the entry by hand in `claude_desktop_config.json`\n\n(use an absolute path):\n\n```\n{\n  \"mcpServers\": {\n    \"weather\": {\n      \"command\": \"uv\",\n      \"args\": [\"--directory\", \"/ABSOLUTE/PATH/TO/weather\", \"run\", \"server.py\"]\n    }\n  }\n}\n```\n\nRestart the client and your tools, resources, and prompts appear. The server is now part of the [tools/action layer](https://aiarch.dev/agentic-ai-architecture) of whatever agent connects to it — reusable across any MCP-compatible client, not bound to one app.\n\nTest both halves of the contract — discovery (does the client see your capabilities?) and execution (do calls run and return correctly?). The SDK ships [an inspector for exactly this](https://modelcontextprotocol.io/docs/develop/build-server), with no client wiring needed:\n\n```\nuv run mcp dev server.py\n```\n\nThat opens the MCP Inspector, where you can list the tools, resources, and prompts the server advertises and invoke them with test arguments before any model touches them.\n\nThen treat the server as security-critical, because it executes on behalf of an untrusted caller — the model's arguments can be wrong, malformed, or steered by [prompt-injected](https://aiarch.dev/model-context-protocol) content the model just read. The non-negotiables:\n\nThe tool boundary is the attack surface\n\nThe moment a model can take an action, a manipulated input can take that action too. Validate every tool input against its schema and your own rules, scope each tool to least privilege, and gate irreversible actions behind a human. This is the part a quickstart skips and production cannot.\n\nEverything above targets revision `2025-11-25`\n\n. Revision `2026-07-28`\n\nis published, and the gap worth understanding first is the one between \"published\" and \"current\": the spec repo's schema constant reads `LATEST_PROTOCOL_VERSION = \"2026-07-28\"`\n\n, while the docs site's versioning page still says, verbatim, \"The current protocol version is 2025-11-25\". A server that speaks only the older revision is conformant today. What changes when you move:\n\n`initialize`\n\n/`notifications/initialized`\n\nhandshake to make MCP stateless: \"Every request now carries its protocol version and client capabilities in `_meta`\n\n\", and servers \"MUST NOT rely on prior requests over the same connection\". Protocol-level sessions and the `Mcp-Session-Id`\n\nheader go with it on Streamable HTTP.`server/discover`\n\n.`ttlMs`\n\nand a `cacheScope`\n\non complete results for six operations — `server/discover`\n\n, `tools/list`\n\n, `prompts/list`\n\n, `resources/list`\n\n, `resources/templates/list`\n\nand `resources/read`\n\n. Omit `ttlMs`\n\nand clients assume `0`\n\n, so the cost is lost caching rather than a failed call. All results also gain a required `resultType`\n\n.`ping`\n\n, `logging/setLevel`\n\nand `notifications/roots/list_changed`\n\nare removed outright, the HTTP GET stream and `resources/subscribe`\n\n/`resources/unsubscribe`\n\nare replaced by `subscriptions/listen`\n\n, and tasks move out of the core spec into an extension. Roots, Sampling and Logging are `HeaderMismatch`\n\nmoved `-32001`\n\n→ `-32020`\n\n, `MissingRequiredClientCapability`\n\n`-32003`\n\n→ `-32021`\n\n, and `UnsupportedProtocolVersion`\n\n`-32004`\n\n→ `-32022`\n\n. Error handling written against older notes will emit the wrong ones.None of this obsoletes the server you ship this week. The revision publishes a compatibility matrix, and the row for a legacy server reached by a dual-era client reads \"Works\". Deprecated features \"remain fully functional during the deprecation window\", and Roots, Sampling, Logging and dynamic client registration each carry a published earliest removal of \"First revision released on or after 2027-07-28\" — at least twelve months, and that date is a floor rather than a schedule.\n\nIf you also maintain the client side, the backward-compatibility probe *differs by transport*, and that is the detail most write-ups flatten. On stdio you probe with `server/discover`\n\nand fall back on any error that is not a recognized modern error. On Streamable HTTP there is no such probe: you attempt a modern request and inspect the body of a `400 Bad Request`\n\nbefore falling back, because modern servers also return `400`\n\nfor version and capability errors. Reading that body is the load-bearing half — a client that discards it degrades invisibly. The asymmetry is worth internalizing: a legacy *server* keeps working, a legacy *client* does not. The matrix row for a legacy client against a modern server reads \"Fails\", because legacy clients have \"no fall-forward mechanism\".\n\nFrom here, the natural next step is composing this server into a full agent: how the tools/action layer sits among orchestration, memory, and retrieval is covered in [agentic AI architecture](https://aiarch.dev/agentic-ai-architecture), and the broader skill set is in the [curriculum](https://aiarch.dev/curriculum).\n\nInstall an official MCP SDK, create a named server object, declare tools as typed functions (the type hints become the input schema), optionally expose resources and prompts, run the server over stdio or streamable HTTP, then connect an MCP client such as Claude and test discovery and execution. A minimal server is around ten lines; the real work is choosing what to expose and validating every input.\n\nAny language with an MCP SDK. Python and TypeScript are the most mature and best documented, and there are SDKs for several other languages. MCP is a protocol, not a library, so a server written in one language interoperates with clients written in any other — pick the language your existing tools and APIs already live in.\n\nInstall the SDK with `uv add \"mcp[cli]\"`\n\n, then create a `FastMCP(\"name\")`\n\nserver. Decorate typed functions with `@mcp.tool()`\n\nfor actions, `@mcp.resource(\"uri\")`\n\nfor readable data, and `@mcp.prompt()`\n\nfor templates, and call `mcp.run(transport=\"stdio\")`\n\n. The type hints and docstring become the tool's schema and description automatically. Confirm signatures against the current SDK docs, which evolve.\n\nFor a local stdio server, add it to Claude Desktop's `claude_desktop_config.json`\n\nunder `mcpServers`\n\n, pointing `command`\n\nand `args`\n\nat how your server launches — or run `mcp install server.py`\n\nto have the SDK write that entry for you. Restart the client and it discovers your tools, resources, and prompts. A remote server uses the streamable HTTP transport instead.\n\nUse the MCP Inspector that ships with the SDK — `mcp dev server.py`\n\n— to list what the server advertises and invoke tools with test arguments, before any model is involved. Verify both discovery (the client sees your capabilities) and execution (calls run and return correctly). Then connect a real client like Claude and exercise the same paths end to end.\n\nTreat the server as code that runs on behalf of an untrusted caller. Validate every tool input against its schema and your own constraints, never trusting model-supplied arguments. Apply least privilege so each tool reaches only what it must, and require human approval for irreversible actions. Prompt injection is the concrete threat: content the model reads can try to steer the tools it calls.\n\n`FastMCP`\n\nAPI, and the `mcp`\n\nCLI (Inspector / install): `2026-07-28`\n\n— the stateless model, the removed handshake and sessions, `server/discover`\n\n, the renumbered error codes, and the removed methods: `ttlMs`\n\n/`cacheScope`\n\n: Exact SDK signatures and transport options evolve, and two spec revisions are live at once — check both the current spec and the revision you target before shipping. Corrections: [hello@aiarch.dev](mailto:hello@aiarch.dev).\n\n*Originally published at aiarch.dev/how-to-build-an-mcp-server, where it is kept up to date.*\n\n*Want the skeleton instead of the essay? aiarch-templates has the src/lib/ seams, a threshold-gated eval stub and a cost-model skeleton. It is deliberately empty — it fixes the shape and you write the implementation. Apache-2.0.*", "url": "https://wpnews.pro/news/how-to-build-an-mcp-server-step-by-step", "canonical_source": "https://dev.to/aiarch_wibo/how-to-build-an-mcp-server-step-by-step-3iom", "published_at": "2026-08-02 18:02:00+00:00", "updated_at": "2026-08-02 18:12:40.188690+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence"], "entities": ["FastMCP", "Model Context Protocol", "Python SDK", "TypeScript SDK", "Claude"], "alternates": {"html": "https://wpnews.pro/news/how-to-build-an-mcp-server-step-by-step", "markdown": "https://wpnews.pro/news/how-to-build-an-mcp-server-step-by-step.md", "text": "https://wpnews.pro/news/how-to-build-an-mcp-server-step-by-step.txt", "jsonld": "https://wpnews.pro/news/how-to-build-an-mcp-server-step-by-step.jsonld"}}