Hosts, clients and servers
- Local serverstdio subprocess: files, git
- Remote serverHTTP: your ticketing system
- Vendor serverHTTP: a SaaS product
The three server primitives
A server offers any of three features, and picking the right one is the most commonly tested part of this task statement. The specification describes them as: resources, context and data for the user or the model to use; prompts, templated messages and workflows for users; and tools, functions for the model to execute. The distinction is really about who reaches for the thing.
| Primitive | What it is | Who invokes it | Typical methods |
|---|---|---|---|
| Resource | Readable data, addressed by URI | The host or the user selects it | resources/list, resources/read |
| Prompt | A named, templated workflow | The user picks it, often from a menu | prompts/list, prompts/get |
| Tool | A function with a schema | The model decides to call it | tools/list, tools/call |
A design that makes everything a tool works, and is what most first servers do — but it puts every decision in the model's hands and fills the context with things nobody asked for. A design that makes everything a resource leaves the model unable to act. The useful test: if the model should decide when to do it, it is a tool. If the user is choosing what to bring in, it is a resource. If it is a repeatable piece of work a user starts by name, it is a prompt.
Which primitive is this?
- The model, mid-taskToolschema, side effects, approval
- The user picks dataResourceaddressed by URI, read only
- The user starts a workflowPromptnamed template with arguments
Authoring a server
You almost never write JSON-RPC by hand. The official SDKs — Python, TypeScript, Java, Kotlin, C# and others — handle framing, method routing and schema generation. In the Python SDK a tool is a decorated function: the function name becomes the tool name, the type hints become the input schema, and the docstring becomes the description the model reads. That last point matters more than it looks: everything in 8.1 about writing tool descriptions applies here, and the docstring is the description.
from mcp.server import MCPServer
mcp = MCPServer("weather")
@mcp.tool()
async def get_alerts(state: str) -> str:
"""Get weather alerts for a US state.
Args:
state: Two-letter US state code (e.g. CA, NY)
"""
# The docstring above is what the model reads when choosing this tool,
# and the type hints become the input schema. Both are the interface.
data = await make_nws_request(f"{NWS_API_BASE}/alerts/active/area/{state}")
if not data or not data.get("features"):
return "No active alerts for this state."
return "\n---\n".join(format_alert(f) for f in data["features"])
if __name__ == "__main__":
mcp.run(transport="stdio")The TypeScript SDK is the same idea in a different shape: construct an McpServer, call registerTool with a name, a description and a schema, and connect it to a StdioServerTransport. Whichever language, the authoring checklist is short — name the server, register the primitives, describe each one as though for a competent stranger, and choose a transport at the bottom of the file.
Communication patterns: stdio and Streamable HTTP
The specification defines two standard transports, and stresses that protocol semantics are identical on both: a transport is a binding that decides how messages are framed and delivered, not what they mean.
stdio. The client launches the server as a subprocess and they talk over its standard streams: JSON-RPC messages in on stdin, messages out on stdout, one per line, with no embedded newlines. Two rules cause most real bugs. The server must not write anything to stdout that is not a valid MCP message — a stray print or a library that logs to standard output corrupts the channel. Logging goes to stderr, which the server may write freely and which the client should not treat as an error signal. Shutdown is by closing the server's input stream and waiting for it to exit.
Streamable HTTP. Each message is an HTTP POST to a single MCP endpoint, and the reply comes back either as a JSON object or as a request-scoped SSE stream. This is what a remote server uses, and it brings the whole HTTP apparatus with it: TLS, authentication, load balancers, rate limits. The older HTTP+SSE transport, with its separate GET endpoint, is deprecated and new servers should use Streamable HTTP.
Choosing a transport
stdio
- Client launches the server as a subprocess
- Newline-delimited JSON-RPC on
stdin/stdout - Same machine only — local files, local tools
- No network auth; the OS user is the boundary
Streamable HTTP
- POST to one endpoint; JSON or an SSE stream back
- Reachable by many clients, anywhere
- Needs TLS, authentication, rate limiting
- The only option the MCP connector accepts
Client versus server responsibilities
The division is strict, and the current revision made it stricter. Clients send requests and notifications; servers send responses and notifications. Servers do not initiate JSON-RPC requests, and clients do not send responses. When a server needs something from the user mid-request — a confirmation, a missing value — it does not call back: it returns an InputRequiredResult whose inputRequests field says what it needs, and the client retries the original request carrying inputResponses. That is the Multi Round-Trip Requests pattern, and it replaces the older server-initiated calls.
The other structural change is that the protocol is now stateless. The initialize handshake and the notifications/initialized message are gone, as are protocol-level sessions and the session header on Streamable HTTP. Every request carries its own protocol version and client capabilities in _meta, which is what “per-request capability negotiation” means. A server that needs state across calls mints an explicit handle and passes it back as an ordinary tool argument. Servers must implement server/discover, which advertises supported versions, capabilities and identity, and a client may call it first to choose a version.
Deployment and integration with Claude applications
How a server is consumed depends on the host. Claude Code runs both kinds: a local server launched over stdio and a remote one over HTTP. Its tools appear with a namespaced name of the form mcp__<server>__<tool>, which is also how permission rules address them — so an MCP server's tools can be allowed, asked about or denied exactly like built-in tools, and a tool can be marked as requiring user interaction so that it always prompts.
The Messages API reaches remote servers through the MCP connector, so an application can use MCP without implementing an MCP client. You pass an mcp_servers array — each entry with type of url, an https:// URL, a name, and an optional authorization_token — together with the beta header the documentation specifies, and enable the server's tools through an mcp_toolset entry in tools. Three limits matter: it supports tools only, so prompts and resources are not reachable this way; it reaches remote servers only, since local stdio servers are not supported; and its data is not eligible for zero-data-retention arrangements.
Two ways a Claude application reaches a server
from the application down to the server
- Claude Codelocal stdio or remote HTTP servers
- Messages APIMCP connector — remote HTTP only
mcp_serverstype: url,name,authorization_token- Your servertools reachable; prompts and resources are not
- Your systemauth, scopes and audit live here
Deployment decisions follow from that. A server that touches a developer's local files belongs on stdio and should never be exposed. A server that several applications share belongs on Streamable HTTP with real authentication, and its identity model should be the caller's, not one shared service account — 7.4 covers the credential side. Version it as you would any API: a tool removed or a schema tightened is a breaking change for every host that depends on it.
Security deserves a paragraph of its own, because the specification is explicit. Tools represent arbitrary code execution and must be treated with appropriate caution; descriptions of tool behaviour, including annotations, should be considered untrusted unless the server itself is trusted; and hosts must obtain explicit user consent before invoking a tool or exposing user data to a server. Installing a third-party MCP server is therefore a supply-chain decision: its tool descriptions enter your model's context, which makes a malicious description a prompt-injection vector — 7.1's subject, arriving through this door.
Traps the wrong answers are built from
| Tempting but wrong | Do this instead |
|---|---|
| Writing log lines to standard output from a stdio server | stdout carries only valid MCP messages; logging belongs on stderr, which the specification permits explicitly. |
| Exposing every capability as a tool | Use resources for data a user selects and prompts for named workflows, so the model is only asked to decide what it should decide. |
| Planning to reach a local stdio server from a server-side application | The MCP connector takes remote HTTPS servers only; anything the API must reach has to be deployed, authenticated and addressable. |
Building on the initialize handshake, sessions, sampling or roots | The current revision is stateless with per-request capability negotiation, and those features are removed or deprecated. |
| Trusting a third-party server's tool descriptions because it installed cleanly | Treat descriptions and annotations from an untrusted server as untrusted input, and require consent before tools run. |
You should now be able to
- Choose between a tool, a resource and a prompt from who initiates the interaction.
- Author a small server with an SDK and explain how names, type hints and docstrings become the interface.
- Describe both standard transports and pick one from the deployment's constraints.
- State the client-server rules, including that servers never initiate requests, and how a server asks for input instead.
- Attach a remote server to the Messages API through the MCP connector and name its three main limits.
- Identify the security obligations of hosting or installing an MCP server.