What this system is, and its 4 parts
MCP (Model Context Protocol) is a standard way to expose tools and data to an LLM client, so instead of hard-coding tool schemas into every app that needs them, you write one server once and any MCP-aware client, Claude Code, Claude Desktop, or an agent wired through the Messages API MCP connector, can discover and call it. This scenario is that server: a small process that exposes a handful of tools and one read-only resource over the MCP protocol, run locally over stdio during development and swapped to a remote Streamable HTTP endpoint when other people or agents need to share it.

MCP Server
tools/list - tools/call - resources/listThe process that implements the MCP protocol: it registers tools and resources, handles the initialize handshake, and responds to tools/call and resources/read requests from a connected client.
ConfigurationFastMCP('server-name') in Python, or new McpServer({name, version}) in TypeScript, with @mcp.tool() / server.registerTool() per capability.Concept: mcp →Transport
stdio (local) - Streamable HTTP (remote)stdio pipes JSON-RPC over the process's stdin/stdout - the default for a server launched by Claude Code or Claude Desktop as a subprocess. Streamable HTTP exposes the same server as a network endpoint for shared or remote use.
Configurationmcp.run(transport='stdio') for local dev; mcp.run(transport='streamable-http') behind an authenticated reverse proxy for shared use.Concept: mcp →
Client Wiring
.mcp.json - MCP connectorFor local stdio servers: a .mcp.json entry with command + args. For remote HTTP servers consumed from the Messages API: the beta MCP connector's mcp_servers + mcp_toolset fields.
Configuration.mcp.json: {"mcpServers": {"my-tools": {"command": "python", "args": ["server.py"]}}}. Or Messages API: mcp_servers=[{"type":"url","url":...,"name":"my-tools"}].Concept: mcp →Input Validator
server-side re-checkEvery tool handler re-validates its input before acting, treating the JSON schema as a hint to the model rather than a guarantee about what actually arrives.
Configurationif not isinstance(order_id, str) or not order_id: raise ValueError('invalid order_id') - before any lookup or side-effecting call.Concept: guardrails-and-safety-controls →The problem
- Expose internal tools once and reuse them across Claude Code, Claude Desktop, and a custom agent, without rewriting the schema three times.
- Let a client discover available tools and resources dynamically, without a hardcoded list baked into the app.
- Keep tool execution scoped and validated, since the server is invoked from model-generated input.
- Duplicating the same tool schema as a Messages API custom tool in three different apps means schema drift: one app's version diverges from another's.
- Trusting tool_use.input at face value inside the handler means the JSON schema is a hint, not an enforcement mechanism - a malformed or adversarial input reaches your handler unvalidated.
- Running a stdio server exactly as written for a shared/remote use case doesn't work: stdio is a local process pipe, not a network protocol.
- ✓ Server responds correctly to tools/list, tools/call, resources/list, resources/read
- ✓ Every tool handler validates input server-side before acting on it
- ✓ Local dev works over stdio via Claude Code's .mcp.json
- ✓ Remote/shared deployment works over Streamable HTTP with authenticated access
One run, traced end to end


6 steps to production
Scaffold the server and register a tool
Define the tool with a clear docstring/description (the client-side model reads this to decide when to call it) and a typed signature the SDK turns into a JSON schema.
Concept: mcp →from mcp.server.fastmcp import FastMCP
mcp = FastMCP("internal-tools")
@mcp.tool()
def lookup_order(order_id: str) -> str:
"""Look up an order by order_id. Use this when asked about order
status or contents. Returns a not_found message if the id is unknown."""
order = db.orders.get(order_id)
if order is None:
return f"order_id {order_id} not found"
return json.dumps(order)Register a read-only resource
Resources expose content (not actions) the client can read directly, discoverable via resources/list and fetched via resources/read.
Concept: mcp →Validate input server-side before acting
The client-supplied JSON schema shapes what the model tries to send, it does not guarantee what actually arrives. Re-validate inside the handler before any side-effecting action.
Concept: guardrails-and-safety-controls →Run over stdio for local development
stdio is the default transport for a server launched as a subprocess by Claude Code or Claude Desktop - no network config needed for local dev.
Concept: mcp →Wire the stdio server into Claude Code
A .mcp.json entry in the project root tells Claude Code how to launch the server as a subprocess and speak MCP over its stdin/stdout.
Concept: mcp →Switch to Streamable HTTP and wire the Messages API MCP connector
For remote or shared use, run the same server over Streamable HTTP behind auth, then connect it to a Messages API request via the beta MCP connector.
Concept: mcp →7 decisions the exam turns into distractors
Streamable HTTP on localhost for a single-user dev loop
stdio, launched as a subprocess via .mcp.json
stdio, expecting multiple clients to share one process's stdin/stdout
Streamable HTTP behind authentication
trust the JSON schema as sufficient enforcement
inside every tool handler, re-checked server-side
model every capability as a tool, including static reference content
tools for actions, resources for read-only content
Handler calls billing.refund(order_id, amount) directly on the input schema's declared fields.
Re-validate amount against the policy cap and order_id shape inside the handler before calling billing.
Team tries to point multiple developers' Claude Code instances at one running stdio process.
Switch to Streamable HTTP for anything beyond a single local subprocess.
A static reference document (refund policy) is exposed as a get_refund_policy tool.
Expose static, read-only content as a resource; reserve tools for actions with real behavior.

Cost & latency
Local stdio is near-instant; remote HTTP adds normal network latency plus your handler's own work.
Oversized results are written to a file in the sandbox with a truncated preview; no extra handling required in the tool itself.
Check every gate before release
3 exam-pattern questions
Work through one question at a time, check the architecture, then move through the set.
You want a tool available to Claude Code locally and to a production agent hitting the Messages API. Do you write two tool definitions?
Frequently asked
What's the actual difference between an MCP tool and a Messages API custom tool?
A Messages API custom tool is defined per-request in your app code and executed by your app. An MCP tool is defined once in a standalone server and discovered/called over the MCP protocol by any connected client, Claude Code, Claude Desktop, or an agent using the Messages API MCP connector, without redefining the schema in each consumer.
When should I use a resource instead of a tool?
Use a resource for static or slowly-changing read-only content the client can fetch directly (a policy doc, a config file, a schema reference). Use a tool for anything that performs an action or does dynamic work, even a read like lookup_order that queries a live database.
Do I need the MCP connector beta header for every use of an MCP server?
Only when wiring a remote MCP server into a direct Messages API request via mcp_servers/mcp_toolset (mcp-client-2025-11-20). Claude Code and Claude Desktop connect to MCP servers through their own configuration (.mcp.json / claude_desktop_config.json) and don't require you to pass that beta header yourself.
How do I authenticate a remote MCP server?
For a Messages API MCP connector, pass an authorization_token on the server entry. For Managed Agents sessions, credentials live in a vault (mcp_oauth for OAuth with auto-refresh, or static_bearer) attached via vault_ids rather than embedded on the agent config.
