Most MCP tutorials in 2026 are still teaching a deployment pattern that Anthropic itself moved past. They’ll walk you through editing claude_desktop_config.json, adding a stdio subprocess, and calling it “deploying an MCP server.” That’s not a deployment. That’s launching a local script.
A real MCP server deployment in 2026 runs as a containerized HTTP service. The MCP 2026-07-28 spec moves the protocol from a bidirectional stateful model to a request/response one, letting servers deploy on serverless and edge infrastructure. This guide walks through that path: Docker + Streamable HTTP + Python SDK v2, in the order you’ll hit each problem.
Why the stateless spec changes everything for MCP servers
The old MCP world assumed one client, one server, one persistent connection. Cute for a laptop. Useless for anything with more than one user. The June 2025 spec started the shift – it classified MCP servers as OAuth Resource Servers, required RFC 8707 Resource Indicators, and removed JSON-RPC batching entirely. Then 2026-07-28 finished the job.
If you built a server against the 2025-03-26 spec and it relied on batching, it won’t talk to current clients. That’s not a warning – that’s a wall. Rebuild it against SDK v2 or it stays broken.
400M monthly SDK downloads as of early 2026. Anthropic donated MCP to the Agentic AI Foundation under the Linux Foundation in December 2025, co-founded with Block and OpenAI, backed by Google, Microsoft, AWS, Cloudflare, and Bloomberg. This is no longer a Claude thing – it’s a cross-industry protocol with a governance body.
Think of it like the shift from FTP to HTTP for file transfer. Both move data. One was built for a world where you controlled both endpoints. The other was built for a world where you don’t know who’s on the other side or how many of them there are. MCP made the same jump.
System requirements and what you actually need
Nothing exotic. If you can run Docker and Python, you can deploy an MCP server.
| Component | Minimum | Recommended |
|---|---|---|
| OS | Any Linux, macOS 12+, Windows 11 with WSL2 | Ubuntu 22.04 LTS or Debian 12 in a container |
| Python | 3.10 (SDK v2 hard floor) | 3.12 |
| Node.js (if TS SDK) | 18 | 22 LTS |
| RAM | 256 MB per instance | 512 MB (headroom for tool I/O) |
| Docker | Docker Engine 24+ or Docker Desktop | Compose v2 |
The Python SDK requires 3.10 as a hard floor – not a suggestion, it uses syntax that older versions won’t parse. For the TypeScript SDK, v2 is the current stable line (released alongside the 2026-07-28 spec); v1.x gets bug fixes and security patches for at least 6 months post-v2 release, so existing TS deployments aren’t immediately broken.
Here’s the part that doesn’t get written about much: the bar to run an MCP server in production is genuinely lower than most protocol deployments. No persistent database required, no WebSocket infrastructure, no session affinity if you configure it right. Whether that simplicity holds as tool counts and concurrency grow – that’s an open question the ecosystem hasn’t fully answered yet.
Where to get MCP servers (and where not to look)
Two GitHub repos with almost the same name, and most people grab the wrong one. The official servers repo is reference implementations – code that demonstrates SDK features and usage patterns for developers building their own servers. The README says exactly this. Not production-ready.
For real servers to deploy, the canonical source is now the MCP Registry (turns out this shifted after the AAIF donation – the old habit of scrolling through the modelcontextprotocol/servers README is just out of date now). Use the registry. That’s where the official docs point.
To build your own, install the SDK:
# Python - pin the version explicitly
pip install "mcp[cli]>=2.0,<3"
# TypeScript
npm install @modelcontextprotocol/sdk
Pinning matters more than it looks. pip install mcp now pulls 2.x. If you have existing servers on v1, keep a <2 upper bound (mcp>=1.28,<2) until you’ve migrated – otherwise a routine pip install --upgrade silently jumps you a major version, breaks import paths, and you spend an afternoon confused about why nothing works when you changed nothing.
Deploying a Streamable HTTP MCP server with Docker
The stdio transport is fine for local development. Beyond that, use Streamable HTTP. It’s a single endpoint that handles JSON-RPC over POST and can stream results back via SSE within the same response – no separate SSE endpoint like the deprecated 2024 transport required.
Minimal Python server (server.py):
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather-server", stateless_http=True)
@mcp.tool()
async def get_forecast(city: str) -> str:
return f"Forecast for {city}: sunny, 22C"
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)
The stateless_http=True flag is the one line most tutorials still skip. It matters – more on why in the errors section.
Now the Dockerfile:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY server.py .
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/mcp')" || exit 1
CMD ["python", "server.py"]
Build and run:
docker build -t my-mcp-server:1.0.0 .
docker run -d -p 8000:8000 --name mcp my-mcp-server:1.0.0
Verify it actually works
Don’t trust docker ps. It tells you the container is running, not that the MCP protocol is answering. Use the official Inspector – a single package (@modelcontextprotocol/inspector) with a web UI, CLI, and TUI mode, all runnable via npx without a global install.
# Point Inspector at your running HTTP server
npx @modelcontextprotocol/inspector --cli http://localhost:8000/mcp
--method tools/list
If you get back a JSON list containing get_forecast, the deployment works. Connection refused means the container isn’t listening on 0.0.0.0 – 127.0.0.1 inside a container is not reachable from the host. A 200 with zero tools usually means a decorator registration failure; check docker logs mcp for import errors.
Pro tip: Add the Inspector CLI to your CI pipeline. A one-line
tools/listcheck catches 90% of regressions before they hit production – schema breaks, missing env vars, and dropped tools all fail loudly instead of silently.
Common errors and the actual fixes
These are the ones that eat hours if you don’t know about them.
“session not found” when scaled horizontally. Two replicas behind a load balancer, requests randomly fail. You add sticky sessions. Still fails. The reason: Cursor and Claude Code both use fetch() internally and don’t forward Set-Cookie headers – so the load balancer never gets the cookie it needs to route consistently. That’s a client-side HTTP implementation detail, not something load balancer config can fix. The only real fix is to run stateless. Set stateless_http=True and stop trying to be clever.
“Method not found” against a fresh client. Server works against an old client, fails against a new one. Almost always batching. The June 2025 revision removed JSON-RPC batching. If your SDK is pinned to a 2025-03-26-era version, upgrade the SDK – don’t downgrade the client.
Container starts, endpoint returns 404. Default endpoint is /mcp, not /. Curl http://localhost:8000/mcp, not http://localhost:8000/. This bites every first-time deployer.
Health check fails but the server works. Streamable HTTP endpoints don’t accept GET without an Accept header. A bare curl in your HEALTHCHECK fails even though the server is fine. Either add a dedicated /health route or send the right accept header.
Upgrading from v1 SDK to v2
Don’t do this on a Friday afternoon. V2 is a major rework – built to support the 2026-07-28 spec plus every earlier revision, and to fix long-standing architectural issues. There’s a migration guide covering every breaking change in the official Python SDK README.
The safe path: pin your existing deployment to mcp>=1.28,<2, deploy a parallel v2 build on a different port, point the Inspector at both, diff the tools/list output. When they match, cut over. No parallel deployment available? At minimum, run the Inspector CLI against a staging container before pushing to prod.
Cleanup and uninstall
Docker cleanup is one command:
docker stop mcp && docker rm mcp && docker rmi my-mcp-server:1.0.0
Host-side Python SDK: pip uninstall mcp. Client configs pointing at the server will need the URL entry removed – but that’s the client’s cleanup problem, not the server’s. No session storage to drain here, no persistent state to worry about. That’s the whole point of the redesign.
FAQ
Do I still need to edit claude_desktop_config.json?
Not for HTTP servers. The client points at a URL – no local config surgery required.
Can I run multiple MCP servers in one container?
Technically yes. In practice: one crash takes everything down. Try to upgrade one dependency and you’re now negotiating version conflicts across all of them at once. Scaling one server independently becomes impossible. The standard approach is one server per container, composed with docker-compose or a Kubernetes namespace – each service isolated, each scalable on its own terms.
Is Streamable HTTP the same as SSE?
Common misconception. The old SSE transport (two endpoints: /sse and /messages) is deprecated. Streamable HTTP is a single /mcp endpoint – JSON-RPC comes in via POST, responses optionally stream back using SSE within that same HTTP response. One endpoint, not two. Any tutorial telling you to expose separate /sse and /messages routes is describing a spec revision that’s already been retired.
Next step: pick one server from the reference implementations, containerize it with the Dockerfile above, and connect the Inspector CLI against it. If tools/list returns a non-empty array, you have a working MCP server deployment. Everything else is scaling.