{"slug": "build-your-first-mcp-server-in-python-stateless-spec-edition", "title": "Build Your First MCP Server in Python (Stateless Spec Edition)", "summary": "The Model Context Protocol's 2026-07-28 specification makes the protocol core stateless, removing the need for clients to establish a session or servers to rely on Mcp-Session-Id for ordinary requests, and the official Python SDK has moved to v2 as its stable line with a higher-level MCPServer API. A tutorial demonstrates building a stateless developer knowledge-base MCP server in Python 3.10+ that exposes a search_kb tool, a kb://articles resource, and a draft_support_reply prompt over Streamable HTTP, installed via uv add \"mcp[cli]\" or pip install \"mcp[cli]\" and inspectable with MCP Inspector.", "body_md": "# Build Your First MCP Server in Python (Stateless Spec Edition)\n\nMCP just got simpler. Learn how to build a stateless MCP server in Python and expose tools, resources, and prompts over HTTP.\n\nThe **[Model Context Protocol](https://modelcontextprotocol.io/)** changed in an important way this summer. With the **[2026-07-28 MCP specification](https://modelcontextprotocol.io/specification/2026-07-28/changelog)**, the protocol core is now **stateless**: modern clients no longer need to establish a protocol session before making requests, and servers no longer rely on `Mcp-Session-Id` for ordinary requests. That makes MCP servers much easier to scale behind normal HTTP infrastructure.\n\nAt the same time, the official **[Python SDK](https://github.com/modelcontextprotocol/python-sdk)** has moved to **v2** as its current stable line and provides a higher-level `MCPServer` API for defining tools, resources, and prompts with regular Python functions.\n\nIn this tutorial, we will build a small but complete MCP server in Python, run it over Streamable HTTP, inspect it locally, and connect to it with a Python MCP client. Let's get started.\n\n## What Are We Building?\n\nWe will create a small **developer knowledge-base server**.\n\nIt will expose three MCP primitives:\n\n```\nTool:\n    search_kb(query, limit)\n\nResource:\n    kb://articles\n\nPrompt:\n    draft_support_reply(customer_message)\n```\n\nThe server will contain no user session state. Every request will contain everything required to process it, which makes it a good example of the new stateless MCP model.\n\nConceptually:\n\n```\nLLM Host\n   |\n   | MCP request\n   v\n+-----------------------+\n| Python MCP Server     |\n|                       |\n| search_kb()           |\n| kb://articles         |\n| draft_support_reply() |\n+-----------------------+\n```\n\n## Step 1: Creating the Project\n\nThe current Python SDK requires Python 3.10 or newer. The official documentation recommends installing the CLI extra because it gives us the development command and MCP Inspector workflow.\n\nUsing **[uv](https://docs.astral.sh/uv/)**:\n\n```\nmkdir first-mcp-server\ncd first-mcp-server\n\nuv init\nuv add \"mcp[cli]\"\n```\n\nOr with `pip`:\n\n```\npip install \"mcp[cli]\"\n```\n\nYour project can be as small as:\n\n```\nfirst-mcp-server/\n├── server.py\n└── client.py\n```\n\nNo framework boilerplate is required.\n\n## Step 2: Creating Your First MCP Server\n\nCreate `server.py`:\n\n``` python\nfrom mcp.server import MCPServer\n\nmcp = MCPServer(\n    \"Developer Support KB\",\n    instructions=(\n        \"Use the knowledge-base tools to answer support questions. \"\n        \"Prefer retrieved KB information over guessing.\"\n    ),\n)\n```\n\n`MCPServer` is the high-level server API in the current Python SDK. For most servers, this is the API you want. The SDK also exposes a lower-level `Server` class, but that is intended for cases where you need exact control over schemas, protocol metadata, or custom methods.\n\nNow let's give our server some data.\n\n```\nARTICLES = [\n    {\n        \"id\": \"python-env\",\n        \"title\": \"Creating a Python virtual environment\",\n        \"body\": (\n            \"Create a virtual environment with `python -m venv .venv`, \"\n            \"then activate it before installing dependencies.\"\n        ),\n    },\n    {\n        \"id\": \"reset-password\",\n        \"title\": \"Resetting your password\",\n        \"body\": (\n            \"Open Account Settings, choose Security, and select \"\n            \"Reset Password. A verification email will be sent.\"\n        ),\n    },\n    {\n        \"id\": \"api-rate-limit\",\n        \"title\": \"Understanding API rate limits\",\n        \"body\": (\n            \"API rate limits restrict the number of requests allowed \"\n            \"within a time window. Clients should retry using \"\n            \"exponential backoff after receiving a rate-limit response.\"\n        ),\n    },\n]\n```\n\nSo far this is just Python.\n\nThe interesting part begins when we expose functions through MCP.\n\n## Step 3: Adding an MCP Tool\n\nAn MCP **tool** is a function the model can decide to call.\n\nAdd this to `server.py`:\n\n``` php\n@mcp.tool()\ndef search_kb(query: str, limit: int = 3) -> list[dict[str, str]]:\n    \"\"\"Search the support knowledge base.\n\n    Args:\n        query: Words or phrases to search for.\n        limit: Maximum number of articles to return.\n    \"\"\"\n    query = query.lower()\n\n    matches = []\n\n    for article in ARTICLES:\n        searchable_text = (\n            article[\"title\"] + \" \" + article[\"body\"]\n        ).lower()\n\n        if query in searchable_text:\n            matches.append(article)\n\n    return matches[:limit]\n```\n\nNotice what we did **not** write.\n\nThere is no JSON Schema.\n\nThere is no manually written tool manifest.\n\nThere is no argument parser.\n\nThe SDK derives the tool definition from the Python function itself. Its type hints become the MCP input schema, and defaults such as:\n\n```\nlimit: int = 3\n```\n\nmake parameters optional in the generated schema. The official SDK documentation uses exactly this pattern. Conceptually, your function:\n\n``` python\ndef search_kb(\n    query: str,\n    limit: int = 3\n)\n```\n\nbecomes something similar to:\n\n```\n{\n  \"name\": \"search_kb\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"query\": {\n        \"type\": \"string\"\n      },\n      \"limit\": {\n        \"type\": \"integer\",\n        \"default\": 3\n      }\n    },\n    \"required\": [\"query\"]\n  }\n}\n```\n\nThis is one of the reasons MCP development in Python feels pleasantly ordinary: your function signature is effectively your interface definition.\n\n## Step 4: Adding a Resource\n\nTools are actions the **model** can call.\n\nResources are different. They expose information that the host application can load into context.\n\nAdd:\n\n``` php\n@mcp.resource(\"kb://articles\")\ndef list_articles() -> str:\n    \"\"\"Return the available knowledge-base articles.\"\"\"\n    lines = []\n\n    for article in ARTICLES:\n        lines.append(\n            f\"{article['id']}: {article['title']}\"\n        )\n\n    return \"\\n\".join(lines)\n```\n\nThe resource has the URI:\n\n```\nkb://articles\n```\n\nA client can read it without invoking a tool.\n\n```\nResource ≈ data that can be read\nTool     ≈ function that can perform work\n```\n\nThe SDK documentation roughly compares resources with `GET`-like behavior and tools with action-oriented `POST`-like behavior.\n\n## Step 5: Adding an MCP Prompt\n\nWe can also expose a reusable prompt template.\n\n``` php\n@mcp.prompt()\ndef draft_support_reply(customer_message: str) -> str:\n    \"\"\"Create a prompt for drafting a concise support response.\"\"\"\n\n    return f\"\"\"\nYou are a technical support assistant.\n\nWrite a concise and helpful response to this customer message:\n\n{customer_message}\n\nUse the support knowledge base when relevant.\nDo not invent product policies.\n\"\"\".strip()\n```\n\nAgain, this is just a Python function plus a decorator.\n\nPrompts are generally initiated by the user or host rather than autonomously invoked by the model. The current SDK supports tools, resources, and prompts through the same decorator-oriented server interface.\n\nAt this point, `server.py` looks like this:\n\n``` python\nfrom mcp.server import MCPServer\n\nmcp = MCPServer(\n    \"Developer Support KB\",\n    instructions=(\n        \"Use the knowledge-base tools to answer support questions. \"\n        \"Prefer retrieved KB information over guessing.\"\n    ),\n)\n\nARTICLES = [\n    {\n        \"id\": \"python-env\",\n        \"title\": \"Creating a Python virtual environment\",\n        \"body\": (\n            \"Create a virtual environment with `python -m venv .venv`, \"\n            \"then activate it before installing dependencies.\"\n        ),\n    },\n    {\n        \"id\": \"reset-password\",\n        \"title\": \"Resetting your password\",\n        \"body\": (\n            \"Open Account Settings, choose Security, and select \"\n            \"Reset Password. A verification email will be sent.\"\n        ),\n    },\n    {\n        \"id\": \"api-rate-limit\",\n        \"title\": \"Understanding API rate limits\",\n        \"body\": (\n            \"API rate limits restrict the number of requests allowed \"\n            \"within a time window. Clients should retry using \"\n            \"exponential backoff after receiving a rate-limit response.\"\n        ),\n    },\n]\n\n@mcp.tool()\ndef search_kb(\n    query: str,\n    limit: int = 3,\n) -> list[dict[str, str]]:\n    \"\"\"Search the support knowledge base.\"\"\"\n\n    query = query.lower()\n\n    matches = []\n\n    for article in ARTICLES:\n        searchable_text = (\n            article[\"title\"] + \" \" + article[\"body\"]\n        ).lower()\n\n        if query in searchable_text:\n            matches.append(article)\n\n    return matches[:limit]\n\n@mcp.resource(\"kb://articles\")\ndef list_articles() -> str:\n    \"\"\"Return the available knowledge-base articles.\"\"\"\n\n    return \"\\n\".join(\n        f\"{article['id']}: {article['title']}\"\n        for article in ARTICLES\n    )\n\n@mcp.prompt()\ndef draft_support_reply(\n    customer_message: str,\n) -> str:\n    \"\"\"Create a support-response prompt.\"\"\"\n\n    return f\"\"\"\nYou are a technical support assistant.\n\nWrite a concise and helpful response to this customer message:\n\n{customer_message}\n\nUse the support knowledge base when relevant.\nDo not invent product policies.\n\"\"\".strip()\n\nif __name__ == \"__main__\":\n    mcp.run(\"streamable-http\")\n```\n\nThat is a complete network-accessible MCP application.\n\n## Step 6: Running It in Development Mode\n\nFor development, the SDK includes a convenient command:\n\n```\nuv run mcp dev server.py\n```\n\nThe MCP development command launches the server with MCP Inspector support, giving you a UI for listing and invoking tools. The official SDK recommends this as the basic development loop.\n\nOpen the Inspector URL printed in your terminal.\n\nYou should see:\n\n```\nsearch_kb\n```\n\nunder Tools.\n\nTry calling it with:\n\n```\n{\n  \"query\": \"rate limit\"\n}\n```\n\nThe result should contain:\n\n```\n[\n  {\n    \"id\": \"api-rate-limit\",\n    \"title\": \"Understanding API rate limits\",\n    \"body\": \"API rate limits restrict ...\"\n  }\n]\n```\n\nYou now have a working MCP server.\n\n## Step 7: Running It Over Streamable HTTP\n\nFor an actual HTTP server, run:\n\n```\nuv run python server.py\n```\n\nBy default, your MCP endpoint is exposed at:\n\n```\nhttp://127.0.0.1:8000/mcp\n```\n\nThe SDK's Streamable HTTP server uses `/mcp` as its default endpoint.\n\nIf you prefer running it as a normal ASGI application, replace the `__main__` block with:\n\n```\napp = mcp.streamable_http_app()\n```\n\nThen launch it with **[Uvicorn](https://www.uvicorn.org/)**:\n\n```\nuvicorn server:app\n```\n\nThis is particularly useful when MCP is one component inside a larger **[FastAPI](https://fastapi.tiangolo.com/)** or **[Starlette](https://www.starlette.io/)** deployment. `streamable_http_app()` returns a standard Starlette-compatible ASGI app.\n\n## What Changed in the Stateless MCP Update?\n\nThis deserves special attention because a lot of MCP tutorials online now describe the older lifecycle. Under older versions of the protocol, an HTTP client effectively did this:\n\n```\nClient\n  |\n  | initialize\n  v\nServer\n  |\n  | Mcp-Session-Id\n  v\nClient\n  |\n  | later request + session id\n  v\nSame logical session\n```\n\nThis created a multi-instance deployment that often needed sticky routing or shared session infrastructure. The `2026-07-28` protocol changes that.\n\nA modern MCP request is designed to be self-contained:\n\n```\nRequest 1\n   |\n   v\nServer A\n\nRequest 2\n   |\n   v\nServer C\n\nRequest 3\n   |\n   v\nServer B\n```\n\nNo protocol session needs to tie those calls together. The MCP team explicitly describes this as moving from a bidirectional, stateful protocol core to a stateless request/response model. This makes ordinary load balancing much easier.\n\n## Writing a Python Client\n\nLet's verify the server without depending on a third-party AI application.\n\nCreate `client.py`:\n\n``` php\nimport asyncio\n\nfrom mcp import Client\n\nasync def main() -> None:\n    async with Client(\n        \"http://127.0.0.1:8000/mcp\"\n    ) as client:\n\n        print(\n            \"Protocol:\",\n            client.protocol_version,\n        )\n\n        tools = await client.list_tools()\n\n        print(\"\\nAvailable tools:\")\n\n        for tool in tools.tools:\n            print(\"-\", tool.name)\n\n        result = await client.call_tool(\n            \"search_kb\",\n            {\n                \"query\": \"rate limit\",\n                \"limit\": 2,\n            },\n        )\n\n        print(\"\\nTool result:\")\n\n        if result.structured_content:\n            print(result.structured_content)\n        else:\n            print(result.content)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nRun the server in one terminal:\n\n```\nuv run python server.py\n```\n\nThen run the client in another:\n\n```\nuv run python client.py\n```\n\nThe v2 `Client` accepts an HTTP URL directly and automatically uses Streamable HTTP. It also exposes the negotiated protocol version, so with a current client/server pair you should see the modern protocol version reported by the connection.\n\nYour output should be similar to mine:\n\n```\nProtocol: 2026-07-28\n\nAvailable tools:\n- search_kb\n  \n  {'result': [{'id': 'api-rate-limit', 'title': 'Understanding API rate limits', 'body': 'API rate limits restrict the number of requests allowed within a time window. Clients should retry using exponential backoff after receiving a rate-limit response.'}]}\n```\n\n## But What If My Application Actually Needs State?\n\n\"Stateless protocol\" does **not** mean your application can never maintain state.\n\nIt means MCP itself no longer hides application state inside a protocol session.\n\nSuppose you were building a shopping server. Instead of relying on:\n\n```\nMCP session 42 owns this basket\n```\n\nyou could expose:\n\n``` php\n@mcp.tool()\ndef create_basket() -> dict[str, str]:\n    basket_id = create_new_basket()\n\n    return {\n        \"basket_id\": basket_id\n    }\n```\n\nThen later:\n\n``` php\n@mcp.tool()\ndef add_item(\n    basket_id: str,\n    product_id: str,\n) -> dict:\n    return add_product(\n        basket_id,\n        product_id,\n    )\n```\n\nNow the model sees and passes:\n\n```\nbasket_id\n```\n\nexplicitly.\n\nThe MCP maintainers specifically recommend this explicit-handle pattern for application-level state under the new stateless protocol.\n\nIt is a subtle but useful architectural shift:\n\n```\nOld idea:\nprotocol remembers state\n\nNew idea:\napplication owns state\nand identifiers travel explicitly\n```\n\n## Scaling the Server\n\nThe stateless core becomes particularly valuable when you deploy multiple workers.\n\nFor example:\n\n```\nuvicorn server:app --workers 4\n```\n\nA modern MCP request can be handled by any worker because the protocol no longer requires it to return to the worker that handled a previous request.\n\nConceptually:\n\n``` php\n              +--> Worker 1\nClient --> LB +--> Worker 2\n              +--> Worker 3\n              +--> Worker 4\n```\n\nThere are additional considerations for advanced features such as multi-round-trip interactions, shared subscription events, authorization, and distributed state, but those are application architecture concerns rather than a requirement of basic MCP tool execution.\n\n## Wrapping Up\n\nThe practical shift here is smaller than the spec diff makes it look: you still decorate functions with `@mcp.tool()`, you still return dicts and let the framework build the result, and you still run `mcp.run()` to serve it. What's different is what happens underneath. You don't need any handshake to negotiate, no session to keep warm, no sticky routing to configure. Get comfortable with `MCPServer`, keep stdout clean, and the rest of the stateless spec mostly stays out of your way.\n\n \n\n \n\n**[**\\[Kanwal Mehreen\\](https://www.linkedin.com/in/kanwal-mehreen1/)**](https://www.linkedin.com/in/kanwal-mehreen1/)** is a machine learning engineer and a technical writer with a profound passion for data science and the intersection of AI with medicine. She co-authored the ebook \"Maximizing Productivity with ChatGPT\". As a Google Generation Scholar 2022 for APAC, she champions diversity and academic excellence. She's also recognized as a Teradata Diversity in Tech Scholar, Mitacs Globalink Research Scholar, and Harvard WeCode Scholar. Kanwal is an ardent advocate for change, having founded FEMCodes to empower women in STEM fields.", "url": "https://wpnews.pro/news/build-your-first-mcp-server-in-python-stateless-spec-edition", "canonical_source": "https://www.kdnuggets.com/build-your-first-mcp-server-in-python-stateless-spec-edition", "published_at": "2026-10-09 14:00:00+00:00", "updated_at": "2026-10-09 14:25:11.195707+00:00", "lang": "en", "topics": ["agent-protocols", "ai-agents", "developer-tools", "ai-tools"], "entities": ["Model Context Protocol", "Python SDK", "MCPServer", "MCP Inspector", "uv", "Streamable HTTP"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/build-your-first-mcp-server-in-python-stateless-spec-edition", "markdown": "https://wpnews.pro/news/build-your-first-mcp-server-in-python-stateless-spec-edition.md", "text": "https://wpnews.pro/news/build-your-first-mcp-server-in-python-stateless-spec-edition.txt", "jsonld": "https://wpnews.pro/news/build-your-first-mcp-server-in-python-stateless-spec-edition.jsonld"}}