What should your MCP server do when two signed-in users request a workshop with the same ID, but only one may read its private notes? The protocol can carry both calls correctly while the application still gives the wrong person the data.
Before you begin: Understand MCP host/client/server roles, Python functions, authentication, and tool-call outcomes.
Model Context Protocol standardizes how an AI application connects with server capabilities such as tools, resources, and prompts. The server still owns its data access, input validation, operational limits, and truthful results.
Match the SDK generation to the example
Reviewed September 10, 2026: the official Python SDK identifies v2 as its stable release line and uses MCPServer from mcp.server. Older v1 examples commonly use FastMCP. Follow the versioned migration guide instead of combining these imports.
The integration below requires Python 3.10 or later and a compatible v2 installation such as mcp[cli]>=2,<3. Resolve and lock exact dependency versions in your project. This server example is documented integration code; it has not been exercised with an external MCP host in this lesson.
Save it as workshops.py. It exposes fictional public information only, so it does not demonstrate authentication for private records.
from mcp.server import MCPServer
mcp = MCPServer("Public workshop records")
records = {
"pottery": {"day": "Saturday", "time": None},
"drawing": {"day": "Sunday", "time": "14:00"},
}
@mcp.tool()
def get_workshop(workshop_id: str) -> dict:
"""Read a public workshop record by its exact ID."""
if len(workshop_id) > 80:
raise ValueError("Workshop ID is too long")
record = records.get(workshop_id)
if record is None:
return {"found": False, "workshop_id": workshop_id}
return {
"found": True,
"workshop_id": workshop_id,
"record": record,
}
Use the SDK's CLI to load the server, for example mcp dev workshops.py for local inspection. The CLI starts the transport; this file does not start a server merely by defining the functions. Check the SDK documentation for your chosen transport and deployment command.
Test the contract before adding private data
Call get_workshop with pottery, then an unknown ID, then an oversized string. The first result should preserve the missing time as null. The second should distinguish “not found” from a backend exception. The third should reject the invalid input.
Type-derived schemas help communicate inputs. They do not establish ownership, validate every business rule, or prove that returned facts are current. Keep those checks in the handler and data layer.
The stdio transport reserves a channel for protocol messages. Current Python SDK v2 redirects ordinary stdout to stderr while serving, which protects that channel from incidental prints. Use deliberate logging anyway, keep private content out of diagnostics, and check the behavior of other SDKs or older versions.
Change the user while keeping the call identical
Use two fictional accounts, each with a record named pottery. Keep the tool name and arguments identical. Change only the authenticated connection. Predict whether a response cache indexed by tool name and arguments can distinguish the requests.
For this exercise, private notes belong to each account separately. First call the tool as learner A to populate the cache. Then call it as learner B. Inspect the content returned and the backend access check, not just the response's user label. Finally revoke A's access and repeat A's request before the cache expires.
Trace the missing boundary
The same arguments do not imply the same permitted result. A shared cache needs an appropriate authorization scope, or private responses need another caching policy. Even a per-user cache can serve revoked content unless the service rechecks permission or invalidates affected entries. Adding the user's name to a response after fetching shared data does not repair the leak.
The public example above has no private records or authentication. Extend it only after you can explain where a trusted identity enters the service and how that identity constrains each read.
Put identity at the service boundary
For private records, derive the principal from the authenticated connection or request context. Do not accept a model-selected user ID as proof of identity. Apply resource ownership and scope checks before fetching or returning content.
Transport choice changes deployment concerns. A local subprocess and a remotely reachable HTTP service do not have the same exposure. Use the current MCP authorization guidance for a remote service, validate credentials for the intended audience, and limit downstream privileges. A protocol session identifier is not automatically authorization.
Never put server secrets into tool descriptions or model-visible errors. Return enough information for recovery without exposing unrelated records or stack traces.
Bound and observe every operation
Set input-size limits, backend timeouts, concurrency limits, and per-user usage policy. Propagate cancellation where supported. Record a request ID, tool name, duration, and outcome while keeping private content out of ordinary logs.
Write operations require additional care: stable operation identity, atomic storage changes, and an explicit recovery path after uncertain results. Adding a tool called “reserve” to a model's menu does not supply these properties.
Practice a release check
Two users ask for records with the same title. One record is private. What must the test verify?
Compare your answer
Each authenticated user receives only records they may access, including through retries, cached responses, resource reads, and error messages. A shared title must not become a shared authorization scope. Verify this at the service layer before testing how pleasantly the model phrases the result.
The official Python SDK documents v2 and its CLI. Its migration guide explains the changed stdio behavior and v1 migration path. The MCP architecture overview explains protocol roles. Use its linked versioned specification for the transport and authorization details of a real deployment.
Continue: Evaluating agents, where protocol success is separated from task success.