A transport is how MCP messages get between a client and your server. MCP defines two: stdio and Streamable HTTP . Tools, resources, and prompts behave the same way on both, and the code you write for them doesn't change.
stdio: a program the client launches
In the stdio transport, the client launches the MCP server as a subprocess . The server reads JSON-RPC messages from stdin and writes JSON-RPC messages to stdout . Messages are delimited by newlines and must not contain embedded newlines.
Nothing listens on a port. You configure it by telling the client what command to run:
{
"servers": {
"my-server": {
"type": "stdio",
"command": "npx",
"args": ["tsx", "src/stdio-server.ts"]
}
}
} This is the right choice when your server works with local things: files on this machine, a local database, installed CLI tools. It's also the fastest way to develop, which is why this course starts there.
Streamable HTTP: a service clients connect to
With Streamable HTTP, your server exposes one endpoint and accepts POST requests to
it. Each message is its own POST. The reply is either a plain JSON object, or a stream scoped to
that request when the tool reports progress along the way.
{
"servers": {
"my-server": {
"type": "http",
"url": "http://localhost:3000/mcp"
}
}
} This is the right choice when many people need to reach one shared instance, or when the server needs to live somewhere other than each user's laptop.
Picking one
| stdio | Streamable HTTP | |
|---|---|---|
| Started by | The client, as a subprocess | You, as a long-running service |
| Reachable by | The one client that launched it | Anyone who can reach the URL |
| Addressed by | A command to run | A URL |
| Good for | Local files, local tooling, development | Shared or hosted servers |
| Concurrent clients | One per process | Many |
The choice isn't permanent. You can serve the same tools over either transport, and moving from stdio to HTTP later changes the file that serves your server, not the tools themselves.
Registering a stdio server with your assistant
A stdio server is only useful once a client is configured to launch it. Every assistant reads its own configuration, so set up the one you're using before moving on.
If you're using GitHub Copilot, create .vscode/mcp.json in your workspace:
{
"servers": {
"demo-server": {
"type": "stdio",
"command": "npx",
"args": ["tsx", "src/stdio-server.ts"]
}
}
} If you're using Claude Code, run this in the terminal instead:
claude mcp add demo-server --transport stdio -- npx tsx src/stdio-server.ts
Both name a command that starts your server as a subprocess, which is exactly what "stdio" means.
You don't have a server for it to launch yet, that's the next lesson, so it won't connect
successfully. Do it anyway: the config will be sitting there once
src/stdio-server.ts exists.
The one rule about standard output
A server must not write anything to its stdout that is not a valid MCP message.
That stream carries the protocol, and console.log writes to it:
async ({ title }) => {
console.log("adding task:", title); // goes into the protocol stream
tasks.push(title);
return { content: [{ type: "text", text: `Added: ${title}` }] };
} Read the raw stdout of a server doing that and the log text sits alongside the response:
adding task: Buy milk
{"result":{"content":[{"type":"text","text":"Added: Buy milk"}],...},"jsonrpc":"2.0","id":1} In practice the call may still succeed, because the SDK client and MCP Inspector skip any line that doesn't parse as a JSON-RPC message. That tolerance is their own behavior, not something the protocol promises, so don't build on it.
For logging, use stderr instead, which a server may write to for any purpose:
console.error("adding task:", title); // safe Check your understanding
Which transport for a local file tool?
You're building a server that reads and edits files in the user's current project. Which transport fits, and why?