Vocabulary for this course

The terms the chapters assume, in plain words. You can open this from any chapter.

capability
A named feature one side declares it supports during the handshake, such as tools or resources.
client
The connector a host creates to talk to one server, sending requests and handling replies.
handshake
The opening exchange where both sides agree on a version of the rules and declare what each supports.
host
The application the user runs, which creates a client and decides what a model may reach.
JSON-RPC
A message format of requests, responses and notifications, where each request carries an id so its reply can be matched.
notification
A message that expects no reply, used to report an event or a change.
prompt
A reusable message template a server offers, which a user can choose and fill in.
resource
Read-only data a server exposes, addressed by a name or address and returned as content.
schema
A description of the arguments a tool accepts, used to check a request before it runs.
server
The program that offers tools, resources or prompts and answers requests over a transport.
stdio
A transport that carries messages over the standard input and output of a program, usually on one machine.
tool
A function a server exposes that a model may ask to run, with named arguments and a result.
transport
The channel that carries messages between client and server, such as stdio or HTTP.

Model Context Protocol from the ground up

Transports: stdio and HTTP

stdio is a pipe and HTTP is a request; both carry the same JSON-RPC frames. Choose by where the server lives, and treat the transport as the first security boundary rather than a detail.

Part 4 of 10

Advanced15 min readBack to Courses

stdio is a pipe

In the stdio transport the client starts the server as a child process and writes one JSON-RPC message per line to its stdin. The server writes one message per line to stdout. Logs go to stderr, never to stdout, because stdout is the wire. The connection lives exactly as long as the process does, and one process serves one client.

Newline framing works because JSON escapes a newline inside a string. A tool result containing several paragraphs is still one line on the wire, with the line breaks written as backslash-n. That means a reader can split on newlines without parsing first, which is why the transport is small enough to implement in an afternoon.

The child process inherits the working directory and the environment, which is convenient and dangerous. Pass only the variables the server needs, and set the directory explicitly rather than relying on wherever the host happened to start. An inherited token is a permission nobody granted on purpose.

HTTP is a request with a session

The Streamable HTTP transport uses a single endpoint. The client POSTs a JSON-RPC message; the server answers with JSON or opens a server-sent event stream for a long-running call. On initialize the server returns an Mcp-Session-Id header, and the client sends it on every later request. Later requests also carry the negotiated protocol version in an Mcp-Protocol-Version header.

A GET to the same endpoint opens a stream for messages the server initiates, such as a progress notice or a list-changed hint. Event ids make the stream resumable: a client that reconnects with Last-Event-ID picks up after the last event it saw. The older HTTP plus SSE transport used two endpoints and is deprecated, so a new server should not be built on it.

A POST that starts a long call can return an event stream instead of a JSON body. The client reads events until the response event arrives, then closes. That is the same request with a different content type, not a second protocol, and the handler does not change.

The same client, two transports

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp.client.streamable_http import streamablehttp_client

params = StdioServerParameters(command="python", args=["server.py"])

async with stdio_client(params) as (read, write):
    async with ClientSession(read, write) as session:
        await session.initialize()
        tools = await session.list_tools()

async with streamablehttp_client("https://example.com/mcp") as (read, write, _):
    async with ClientSession(read, write) as session:
        await session.initialize()
        tools = await session.list_tools()

Newline framing, by hand

Write three messages to a buffer and read them back. Confirm the tool argument containing a newline survives as one frame, then add a message with an empty params object.

Output
Click "Run" to execute your code

The transport is the first security boundary

A stdio server is a local process with the user’s privileges. It needs no authentication because the operating system already decided who may start it, but it inherits the environment, including any token that happens to be there. Spawning a server from a configuration file the user did not write is the equivalent of running a script from an email attachment.

An HTTP server is reachable by anything that can route to it. Bind it to localhost unless it is meant to be public, validate the Origin header so a browser page on another site cannot reach it through DNS rebinding, and require a real access token. A session id identifies a conversation; it is not a credential, and it must not be the only thing standing between a caller and a tool.

TLS is table stakes for a remote server, and it is not enough on its own. Authenticate the caller, authorise the tool, and log the decision with the session id, because the transport will happily carry an unauthenticated request to a handler that trusts it.

stdout is the wire. One stray print in a stdio server corrupts the frame stream, and the client sees a parse error rather than the line you were debugging.

Choosing a transport

Use stdio when the server is a local capability: a filesystem, an editor, a database on the same machine. The lifecycle is simple, there is no port to expose and no certificate to renew. Use Streamable HTTP when the server is remote, shared by several users or scaled behind a load balancer. The protocol is the same; the operational shape is not.

The mistake to avoid is putting a local capability behind a public endpoint because HTTP felt more modern. A filesystem server exposed on a port is a remote file read for anyone who finds it. The transport should follow the trust boundary, not the other way round.

A server can support both transports behind one handler, which is often the right answer: stdio for the local developer, HTTP for the hosted product. The handler does not know which one delivered the frame, and it should not. That ignorance is the portability the protocol promised.

A session over HTTP

Initialize, send the initialized notice, then make a call with and without the session header. Confirm the missing-session request is rejected before it reaches a tool.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

Why must a stdio server log to stderr?