Guide · AI and GenAI
MCP in Production: Multi-Server Setups, Auth and Logging
25 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- MCP production work means running MCP, the open standard that lets an AI app call tools on separate servers, for a whole company. That means many shared servers, each with real logins, limits and logs.
- Local servers use stdio: the app starts the server as a child process and they talk through its input and output streams. Shared servers use Streamable HTTP, where every message the app sends is its own HTTP POST request.
- Each shared server acts as an OAuth resource server, the side that checks passes rather than issuing them. It accepts only access tokens, short-lived passes from a login service, whose audience (the server named inside) is itself, and never forwards them.
- With many servers, prefix tool names by server, set per-server timeouts and rate limits, and pass one trace id, a tracking number for the request, to every server it touches.
- Beginners can stop after the transport section. Engineers get what the 28 July 2026 revision changed. That means no sessions, headers a gateway (one front door for all servers) can trust, and tested token-checking code. They also get the scaling catch for change notifications, alerts that a server's tools changed.
What goes wrong when one MCP server becomes a company-wide platform?
A setup built for one person on one laptop starts failing when hundreds of people share it. The laptop setup trusted one user, one machine and one config file. MCP production work is about removing those three assumptions.
Picture a common setup. A furniture exporter builds an internal assistant, a chat app its staff use to ask about stock, orders and dispatch. The setup is made up, but the problems are real.
A platform engineer built the first MCP server over a weekend. It ran on a laptop as a local process, with one tool, check_stock, that read the inventory database using a password from a config file. Twelve people in sales tried it and loved it.
Six months later, five teams want servers of their own: inventory, orders, design drawings, dispatch and HR leave. About 400 staff now use the assistant, making roughly 3,000 tool calls a day. Four problems show up within a month.
- The orders server and the design server both expose a tool called search. The assistant sometimes picks the wrong one and returns drawings when someone asked about an order.
- Every server uses one shared database password, copied into five config files. Nobody can say who has seen it.
- A tool cancels an export order by mistake. The logs show that the orders server did it, but not which employee asked.
- One Monday the assistant feels slow. Nobody can tell if the delay is in the model, the dispatch server or something behind it.
Each problem has a fix in this guide. Tool names come first, then logins, then audit logs and traces.
Which words do you need before running MCP in production?
You need six words beyond basic MCP: host, client, transport, remote server, access token and trace. Each gets one plain meaning and one example from the running setup.
Host. The host is the app people actually use. It sends the chat to the model, runs the tool calls the model asks for, and shows the answers. Example: the assistant's chat window is the host.
Client. An MCP client is the part of the host that talks to one MCP server. A host connected to five servers runs five clients. Example: the orders client only ever talks to the orders server.
Transport. A transport is how MCP messages travel between a client and a server. Example: the laptop server used stdio, the standard input and output streams of a local process.
Remote server. A remote server runs on another machine and is reached over the network, usually by HTTPS. Example: the new orders server runs in the company data centre, not on anyone's laptop.
Access token. An access token is a short-lived string that proves a request is allowed. A login service called an authorization server issues it. Example: a token that lets the assistant read orders for one employee.
Trace. A trace is the full record of one request as it passes through several services, tied together by one trace id. Example: one question that touches the host, the dispatch server and a partner system.
What does an MCP production setup look like?
An MCP production setup is one host connected to several remote MCP servers over HTTPS. Around them sit a shared login service, a load balancer and one store for logs and traces.
Each server belongs to the team that owns its data. The inventory team runs the inventory server, and the logistics team runs the dispatch server. The platform team owns the host and the shared plumbing.
A company assistant with many MCP servers
Drag to exploreNotice that no arrow runs from the host to a database. Every data access goes through a server its own team runs, and every hop reports to one trace store.
Diagram connections:
- Staff in a browser to Assistant host
- Assistant host to Company login service (Get a token per server)
- Assistant host to Load balancer (HTTPS with a token)
- Load balancer to Inventory MCP server
- Load balancer to Orders MCP server
- Load balancer to Dispatch MCP server
- Assistant host to Trace and log store (Spans)
- Inventory MCP server to Trace and log store (Spans and audit logs)
- Orders MCP server to Trace and log store (Spans and audit logs)
- Dispatch MCP server to Trace and log store (Spans and audit logs)
- Staff in a browser: About 400 people use the assistant
- Assistant host: Calls the model; one MCP client per server
- Company login service: Issues a separate token for each server
- Load balancer: Sends each path to that server's copies
- Inventory MCP server: Owned by the inventory team
- Orders MCP server: Owned by the sales team
- Dispatch MCP server: Owned by the logistics team
- Trace and log store: Spans and audit records from every hop
Should an MCP server use stdio or Streamable HTTP?
Use stdio when the server runs on the same machine as the host and serves one person. Use Streamable HTTP when many people or apps share the server over a network.
With stdio, the host starts the server as a child process. They exchange JSON messages, one per line, through the process's input and output streams. The server must write nothing else to its output, and nothing listens on the network.
Streamable HTTP is the spec's network transport. The server exposes one HTTP endpoint, such as a path ending in /mcp. The client sends every message as its own POST request. The server replies with plain JSON, or with a stream for that one request.
That stream uses Server-Sent Events (SSE), a standard way for a server to push several messages down one HTTP response. Progress updates travel on it before the final answer.
| stdio (local) | Streamable HTTP (remote) | |
|---|---|---|
| Where it runs | On the same machine as the host | On a server reached over HTTPS |
| Who starts it | The host, as a child process | Its team deploys and runs it |
| Who uses it | One person through one host | Many people through many hosts |
| Credentials | Environment variables on that machine | OAuth access tokens on every request |
| Scaling | One process per user | Many copies behind a load balancer |
| Where logs go | Standard error (stderr) | OpenTelemetry and a central log store |
| Example in this setup | A developer's own file tools | Inventory, orders and dispatch servers |
Streamable HTTP brings web risks with it. If a request carries an Origin header, the browser's note of which website sent it, and the server does not trust that site, the spec says reply HTTP 403. That blocks DNS rebinding, a trick where a website makes your browser talk to a server on your own machine.
For HTTP servers running locally, the spec also says to bind only to 127.0.0.1, not to every network interface. In the running example, two developers' test servers were listening on the whole office network.
Beginners can stop here: you now know why the laptop setup breaks, what the parts are, and which transport to pick. The rest of the guide is for engineers who build the shared servers.
How do you connect one AI app to many MCP servers?
You give the host one MCP client per server, then merge their tool lists into one list the model can use safely. The merging step is where most multi-server trouble starts.
Tool names only have to be unique inside one server. The spec says hosts that combine servers should plan for clashes, for example by prefixing each tool name with a server id.
That is the search clash from the opening. The host renames tools as the model sees them: orders_search and design_search. It keeps a map from each new name back to the right server and the original name.
Pick a separator your model's tool-calling interface accepts. The MCP spec recommends only letters, digits, underscores, hyphens and dots in tool names, but the model side may be stricter.
The platform team's server registry holds four things for every server:
- A fixed id and an HTTPS address chosen by the platform team. The spec warns that the name a server reports about itself may not be unique.
- Which staff roles may use it. HR leave tools appear only for HR staff.
- A timeout. Dispatch gets 10 seconds, while the fast inventory server gets 3.
- A cap on how many tools it may add, so one team cannot flood the model's list.
Most hosts send every tool's name, description and input schema to the model as input. So each extra tool adds text you pay for on every request that includes the list.
How do tool list caching hints work?
In the 2026-07-28 revision, every tool list result must carry ttlMs, a hint in milliseconds for how long the list stays fresh. It also carries cacheScope: public means a shared gateway may reuse the list for anyone, private means only for the same login.
Servers should also return tools in the same order every time, so the host can cache the merged list. A change notification makes a cached list stale at once.
One default is worth knowing. In our test in October 2026, a server built with the official Python SDK 2.3.0 returned ttlMs 0 and cacheScope private, meaning stale at once. For lists that rarely change, set a real hint such as 300000 (five minutes).
In that SDK you pass the hint when you create the server: MCPServer("orders", cache_hints={"tools/list": CacheHint(ttl_ms=300000)}), with CacheHint from mcp.server.caching. Our test then returned ttlMs 300000.
Do you need a gateway in front of all your MCP servers?
A gateway is one front door that every MCP request passes through before it reaches a server. It can check tokens, apply rate limits and write logs in one place.
The price is an extra hop, a single point of failure, and one place where every token passes. The running example starts without one. The load balancer routes by path, and each server checks its own tokens.
If you add a gateway later, the new revision helps. Every request carries an Mcp-Method header, and tool calls also carry Mcp-Name with the tool's name. A gateway can route or rate limit on them without parsing the JSON body.
A server that reads the body must reject a request whose headers and body disagree, with HTTP 400 and error code -32020. Without that rule, a gateway could approve one tool name while the server quietly runs another.
One catch: before the 2026-07-28 revision, no server had to check these headers against the body. So the spec says a gateway enforcing policy on them should reject requests whose MCP-Protocol-Version header is missing or older.
You can watch the rule work. Save this tiny orders server and run it with the official Python SDK 2.3.0 installed (pip install mcp==2.3.0).
import uvicorn
from mcp.server.mcpserver import MCPServer
app = MCPServer("orders")
@app.tool()
def search(query: str) -> str:
"""Search orders by text."""
return f"0 orders match {query}"
uvicorn.run(app.streamable_http_app(json_response=True), host="127.0.0.1", port=8765)Then, in a second terminal, send a GET request and a tool call whose Mcp-Name header claims a different tool than the body:
curl -s -o /dev/null -w "%{http_code}\n" -X GET http://127.0.0.1:8765/mcp \
-H "MCP-Protocol-Version: 2026-07-28"
curl -s http://127.0.0.1:8765/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Protocol-Version: 2026-07-28" \
-H "Mcp-Method: tools/call" \
-H "Mcp-Name: delete_order" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search","arguments":{"query":"5521"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}' \
-w "\nHTTP %{http_code}\n"Pause and predict: the header says delete_order, the body says search. Which one will the server run?
Expect 405 for the GET, because the new revision has no GET stream. The second call should print an error with code -32020 and then HTTP 400. Change the header to search and the same call returns 0 orders match 5521.
How do you scale an MCP server behind a load balancer in 2026?
You run several identical copies of the server behind a load balancer. Since the 2026-07-28 revision made MCP stateless, any copy can answer any request, as long as your own tool state lives outside the process.
Stateless means each request carries everything the server needs. The protocol version and the client's capabilities now travel inside every request, in a field called _meta. The old start-up handshake is gone.
Revisions from March to November 2025 worked differently. A server could hand out a session id in an Mcp-Session-Id header. In practice, every request in that session then had to reach the same copy, or a shared store had to hold the session.
Some tools really do need memory between calls, such as a dispatch plan built over several steps. The spec's advice is an explicit handle. The first tool returns an id like plan_7f3a, and later calls pass it back as an argument.
The team stores dispatch plans in Redis, a shared in-memory store, under that id, so any copy can find them. On every call, the server also checks that this user may use this plan. The spec's tools page puts it this way for servers with login: a handle is a name, not a capability. In other words, knowing the id grants no access by itself.
One more change affects retries. On Streamable HTTP in the new revision, a broken response stream cannot be resumed, and the server treats it as a cancel. The tool may already have done its work. A client that still wants the answer re-sends it as a new request with a new id, so any tool that changes data must be safe to run twice.
What is the catch with change notifications?
Change notifications break this neat picture, because they tie a host to one copy for a long time. A host that wants to hear about new tools sends a subscriptions/listen request, and the reply is a stream that stays open on one copy.
When the orders team deploys a new tool, the copy that notices must reach the copy holding each stream. The official Python SDK 2.3.0 ships an in-memory bus for this, which only reaches listeners in the same process.
Its source code says a deployment with several copies needs a custom bus, for example one backed by Redis publish and subscribe. This is SDK behaviour read from the 2.3.0 source in October 2026, not part of the spec, so pin your SDK version.
How does authorization work for remote MCP servers?
A remote MCP server that uses MCP authorization acts as an OAuth 2.1 resource server. It accepts a request only if it carries a valid access token issued for that exact server.
OAuth is the standard way to let an app act for a person without ever seeing their password. OAuth 2.1 is still an IETF draft, which the MCP spec cites by version. Authorization is optional in the MCP spec. Any server that touches company data should still use it.
For stdio servers, the spec says the opposite. They should skip this flow and take credentials from the environment. That is why the laptop server could read a password from a local setting.
Think of a truck carrying goods between states with an e-way bill, the online permit for moving goods in India. The bill names the goods, the truck and the destination. When a tax officer stops the truck on the road, they compare the bill with this truck, not with any truck.
An access token works the same way. It names its audience, the one server it was issued for. The orders server checks that audience before it does anything else, like the officer at the roadside check.
Where the picture breaks: one e-way bill can be checked by several officers along a route. A token should pass only at the server it names, and every other server must refuse it.
Here is what happens the first time the assistant calls the orders server for an employee. Follow the steps in order.
First call to a protected MCP server
Drag to exploreThe first call fails on purpose. Notice that the 401 reply, not a config file, tells the host where to log in, and the token that comes back names the orders server only.
Diagram connections:
- Call without a token to 401 with a metadata link
- 401 with a metadata link to Read resource metadata
- Read resource metadata to Read login service metadata
- Read login service metadata to User signs in and approves
- User signs in and approves to Token for orders only
- Token for orders only to Call with Bearer token
- Call with Bearer token to Check token, run the tool
- Call without a token: Host → Orders server
- 401 with a metadata link: Orders server → Host
- Read resource metadata: Host → Orders server
- Read login service metadata: Host → Login service
- User signs in and approves: Browser → Login service, naming the orders server
- Token for orders only: Login service → Host
- Call with Bearer token: Host → Orders server
- Check token, run the tool: Orders server → Orders database
The metadata in step three follows RFC 9728, Protected Resource Metadata. It is a small JSON document listing which authorization servers the orders server trusts. MCP servers must publish it.
In step five, the host must send a resource parameter, defined in RFC 8707. It names the exact server the token is for, using that server's address. RFC 8707 says a login service that supports it should then limit the token's audience to that server.
The MCP spec tells clients to send the parameter even when the login service ignores it. So test that yours really sets the audience. If it does not, the audience check below rejects every token.
Scopes say what a token allows, such as orders:read. If a token lacks a scope a tool needs, the server should reply 403 with a WWW-Authenticate header naming the scopes required. The host can then ask the user to approve more.
Why must an MCP server never pass the user's token along?
The spec says an MCP server must accept only tokens issued for itself. Its security page adds a second rule: never pass the client's token through to an upstream API. An upstream API that uses OAuth gets a separate token, issued by that API's own login service.
Back on the road, an e-way bill for one truck cannot cover a second truck that leaves the warehouse later. Likewise, the orders server calls its database with its own credentials and records which user asked.
Here is the check at the top of every orders tool. It assumes the login service issues signed JWT access tokens, a common token format that carries readable fields called claims. Opaque tokens need the login service's lookup endpoint instead.
import jwt # PyJWT 2.x, installed with: pip install "pyjwt[crypto]"
ORDERS_SERVER = "https://mcp.example.com/orders" # this server's own address
LOGIN_SERVICE = "https://login.example.com" # the issuer it trusts
IMPLIES = {"orders:admin": {"orders:read", "orders:write"}}
class AuthError(Exception):
def __init__(self, status, scope=None):
self.status, self.scope = status, scope
def authorize(headers, needed_scope, public_key):
auth = headers.get("authorization", "")
if not auth.startswith("Bearer "):
raise AuthError(401)
try:
claims = jwt.decode(auth[len("Bearer "):], public_key, algorithms=["RS256"],
audience=ORDERS_SERVER, issuer=LOGIN_SERVICE,
options={"require": ["exp", "aud", "iss", "sub"]})
except jwt.InvalidTokenError: # bad signature, expired, or another server's token
raise AuthError(401)
granted = set(claims.get("scope", "").split())
for scope in list(granted):
granted |= IMPLIES.get(scope, set()) # a broad scope covers narrower ones
if needed_scope not in granted:
raise AuthError(403, needed_scope) # ask the user for more scope
return claims["sub"] # the user, for the audit logWhat just happened? PyJWT checks the signature, the expiry, the issuer and the audience in one call. A token for another server fails that call and gets a 401, the same as an expired one, so the host must fetch the right token.
The IMPLIES map exists because the spec says servers must honour scope hierarchies, where a broad scope covers narrower ones. A missing scope gets a 403 that names it. The function returns sub, the user id, which the audit log needs.
To check it, mint test tokens with your own key pair. In our run on PyJWT 2.15.1, a dispatch token gave 401, an orders:read token asking for orders:write gave 403, and an orders:admin token passed.
In the real server, a 401 reply also carries a WWW-Authenticate header with the metadata link from step two. That header is how the host finds its way to login.
How should you log and trace MCP tool calls in production?
Log every tool call with who asked, which tool ran, how long it took and whether it worked. Tie each record to one trace id that crosses every hop.
MCP has its own logging feature, where servers send log messages back to the client. The 2026-07-28 revision deprecated it, and it stays in the spec for at least twelve months after that release. New servers should log to stderr on stdio, or use OpenTelemetry.
OpenTelemetry is an open standard and set of tools for traces, metrics and logs. A span is one timed step inside a trace, such as one tool call on the dispatch server.
MCP carries trace context inside a request's _meta field, under the key traceparent. Its value follows W3C Trace Context, a web standard. A traceparent holds a 32-character trace id and a 16-character parent span id, written in hex.
This is the e-way bill number all over again. Every officer who checks the truck on the route sees the same number. If the truck arrives late, that one number ties together every stop it made.
The host starts a trace for every chat message and copies the traceparent into each tool call. Each server continues the same trace and adds spans for its own slow steps. Follow the one slow question below.
Tracing one slow question
Drag to exploreOne trace id travels with the request. Notice where the time went: the partner call took 7.8 of the 8.4 seconds (made up).
Diagram connections:
- Ask where order 5521 is to Call dispatch.track_order
- Call dispatch.track_order to Ask the transport partner
- Ask the transport partner to Return status and timings
- Return status and timings to Reply in the chat
- Reply in the chat to Spans land in one trace
- Ask where order 5521 is: Browser → Assistant host
- Call dispatch.track_order: Host → Dispatch server, traceparent in _meta
- Ask the transport partner: Dispatch server → Partner system
- Return status and timings: Dispatch server → Host
- Reply in the chat: Host → Browser
- Spans land in one trace: Host and dispatch server → Trace store
That Monday mystery is now one search. The trace shows the transport partner's system taking almost eight seconds. The model and the dispatch server were fine.
You may not need to write this yourself. Servers built on the official Python SDK 2.3.0 ship an OpenTelemetry middleware, switched on by default, that continues the trace found in _meta. It produces spans once you configure an OpenTelemetry exporter. Its source marks this middleware as provisional, liable to change in a 2.x minor release, as of October 2026.
We checked this in October 2026. A tools/call request carrying traceparent 00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01 produced a span named tools/call track_order. Its trace id was 0af7651916cd43dd8448eb211c80319c, and its parent was 00f067aa0ba902b7.
What should an MCP audit log record?
Traces answer what was slow. Audit logs answer who did what. The security team wants one audit record per tool call, kept for a year in a store the server teams cannot edit.
- The time, the trace id and the server id.
- The user id from the token, never the token itself.
- The tool name and its arguments, with secrets, personal data and very large values removed.
- The outcome, success or error, plus how long the call took.
The deprecated logging page has a rule worth copying for any log. MCP log messages must not contain credentials, secrets or personal identifying information. Here is the small wrapper every server in the running example puts around its tools.
import json, sys, time
SECRET_FIELDS = {"password", "token", "phone", "address"}
MAX_ARG_CHARS = 200
def clean(args):
safe = {}
for key, value in args.items():
if key in SECRET_FIELDS:
continue
text = json.dumps(value)
safe[key] = value if len(text) <= MAX_ARG_CHARS else f"{len(text)} chars, not logged"
return safe
def audited(server_id, tool_name, user_id, trace_id, args, run_tool):
started = time.monotonic()
outcome = "ok"
try:
return run_tool(args)
except Exception as err:
outcome = f"error: {type(err).__name__}"
raise
finally:
record = {"ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"server": server_id, "tool": tool_name, "user": user_id,
"trace_id": trace_id, "outcome": outcome,
"ms": round((time.monotonic() - started) * 1000),
"args": clean(args)}
print(json.dumps(record), file=sys.stderr)The finally block is the key. It writes the record whether the tool worked or failed, so a failed cancel_order still leaves a trail. The clean function drops fields named in SECRET_FIELDS and replaces any argument over 200 characters with its length.
It only checks top-level argument names, so a secret nested inside another field needs its own rule. A tool that receives a whole drawing file logs the file's size, not its contents.
The record goes to stderr on purpose. On stdio, standard output carries the MCP messages themselves, so logs must stay out of it. On remote servers, a log collector ships stderr to the central store.
What breaks first when MCP goes to production?
In the running example, the first failures were plain ones: slow tools, floods of calls and version mismatches. Each needs a rule in code, not a hope that the model behaves.
- Slow tools. The spec says clients should set timeouts on tool calls. The host tells the model when a tool timed out, so it can say so instead of waiting.
- Floods. The spec says servers must rate limit tool calls. Once, a confused loop called check_stock 900 times in one minute (made up).
- Version drift. A server that does not support the requested protocol version replies 400 with error code -32022 and lists the versions it supports. The host logs it and alerts that server's team.
- Changing tool lists. Hosts with an open subscriptions/listen stream get a list-changed notice and refresh. Others wait until the list's ttlMs runs out.
- Repeated actions. A broken stream means a resend, so any tool that changes data checks its current state, or a request key, before acting again.
How do you explain MCP in production in a system design interview?
Explain it as three layers on top of basic MCP: transport and scaling, authorization, and observability. Tie each layer to a failure it prevents.
Draw the host and servers
Show one host with one MCP client per server. Each server belongs to the team that owns its data.
Pick transports
Use stdio for local, single-user tools. Use Streamable HTTP behind a load balancer for shared servers, and say that stateless servers scale like any web service.
Add authorization
Each server is an OAuth resource server. Tokens are bound to one server by audience, scopes are checked per tool, and tokens are never forwarded.
Handle many servers
Prefix tool names by server, cap tools per server, and set per-server timeouts and rate limits.
Make it observable
Pass trace context in _meta, send spans to one store, and keep an audit log with user, tool and outcome.
Name the trade-offs
Say when a gateway earns its extra hop, how change notifications reach every copy, and how tools that change data stay safe under retries.
Questions people ask
- Do I need OAuth for a local MCP server?
- No, not for a server that uses stdio. The spec says stdio servers should not use the OAuth flow and should read credentials from the environment instead. Keep those credentials limited to what the one person running the server needs, and never commit them to a repository.
- Can I still use Mcp-Session-Id in my MCP server?
- Only for clients that speak an older revision. The 2026-07-28 revision removed protocol-level sessions, and servers that support only that revision should ignore the header. For state across calls, return an explicit handle from one tool and accept it as an argument in later calls.
- Should every MCP server have its own authorization server?
- Not necessarily. The spec lets the authorization server run inside the MCP server or as a separate service, so many servers can trust one shared login service. That service issues a separate token per server using the resource parameter, and each server still checks that the token's audience is itself.
- How many MCP servers can one AI app connect to?
- The spec's tool and transport pages set no number. The practical limit is the model's tool list, because hosts usually send every tool's name, description and schema as input. Cap tools per server, and show each user only the servers their role needs.
- Is MCP's built-in logging still worth using?
- Not for new servers. It still works, but it is deprecated as of the 2026-07-28 revision and can be removed after at least twelve months. New servers should log to stderr on stdio or send data through OpenTelemetry. Your own audit log, with user, tool and outcome, is a separate need either way.
- What is the difference between a 401 and a 403 from an MCP server?
- A 401 means the token is missing, invalid, expired or meant for another server, so the client needs a new one. A 403 means the token is valid but lacks a needed scope. The server should then name the required scopes in its WWW-Authenticate header.
Sources and further reading
- MCP specification 2026-07-28: Overview (statelessness, _meta fields, traceparent)
- MCP specification 2026-07-28: Key changes since 2025-11-25
- MCP specification 2026-07-28: Streamable HTTP transport (Origin check, headers, cancellation)
- MCP specification 2026-07-28: stdio transport
- MCP specification 2026-07-28: Authorization
- MCP specification 2026-07-28: Authorization security considerations (no token passthrough)
- MCP specification 2026-07-28: Tools (name collisions, stateful handles, security)
- MCP specification 2026-07-28: Caching (ttlMs and cacheScope)
- MCP specification 2026-07-28: Logging (deprecated)
- MCP specification 2026-07-28: Versioning and compatibility
- RFC 9728: OAuth 2.0 Protected Resource Metadata
- RFC 8707: Resource Indicators for OAuth 2.0
- W3C Recommendation: Trace Context Level 1
- PyPI: mcp 2.3.0, the official MCP Python SDK