camzabriskie.com

Primers › Working with AI

Running an MCP server locally over stdio

02 / 03 updated Oct 13, 2026
On this page
  1. A few terms
  2. How stdio works
  3. Registering a server with a client
  4. The edit, restart, test loop
  5. Testing without a client
  6. Common problems
  7. Trying the HTTP transport on your own machine
  8. Quick reference
  9. References

An MCP server gives an AI assistant tools it doesn’t have on its own, such as querying a database or filing a ticket. A server can run on a shared machine that many people connect to, or on your own laptop, started by the assistant itself. The second way is called stdio, and it is also the quickest way to try a change to a server you’re writing, because nothing has to be deployed first.

A few terms

How stdio works

The examples in this primer use one made-up server called notes-server, a Python program that lets an assistant search a folder of notes. It lives in /Users/example/projects/notes-server.

With stdio, you never start notes-server yourself. You tell the client the command that starts it, and the client runs that command as a subprocess. From then on the client writes requests to the server’s stdin and reads replies from its stdout. The server may write log lines to stderr, which the client can capture, forward, or ignore [2].

Replies and logs travel on separate streams. Only valid MCP messages may go on stdout.

The spec’s rules for stdio [2]:

The second rule explains the most common stdio bug. A stray print("starting up") in the server lands on stdout, the client reads it as a message, and the connection fails. Logging belongs on stderr.

Registering a server with a client

A client needs three things to start a stdio server: the command, its arguments, and any environment variables. The same server looks like this in a portable config file, which is the mcpServers format that several clients read [4]:

{
  "mcpServers": {
    "notes-local": {
      "command": "uv",
      "args": ["run", "--directory", "/Users/example/projects/notes-server", "notes-server"],
      "env": { "NOTES_DIR": "/Users/example/notes" }
    }
  }
}

VS Code reads the same entry from .vscode/mcp.json, where the top-level key is servers and not mcpServers [4]. A server given as command and args runs locally, while one given as a url with "type": "http" is a remote one [4].

Three details in that entry prevent most “it works in my terminal but not in the client” problems:

Many clients also have an “add server” command that writes this entry for you. VS Code’s is MCP: Add Server [4].

Trust: a local server can run any code

A stdio server is a program running on your machine with your permissions. VS Code’s documentation warns that local servers can run arbitrary code and asks you to add only servers from sources you trust [4]. That includes your own half-finished one, if it shells out or deletes files.

The edit, restart, test loop

The client starts the server when a session begins and ends it when the session closes: it closes the server’s stdin, waits for it to exit, and stops it by force only if it doesn’t. A server should exit promptly when its stdin closes [2]. Either way, a running server never sees edits made after it started. The loop for changing a server is:

  1. Edit the server’s code.
  2. Restart the client session, or use the client’s restart command for that one server.
  3. Call the changed tool from the assistant.
  4. Run the project’s own tests before committing.

A session with an assistant exercises one path once, while a unit test exercises the tool’s logic every time, without needing the client at all.

Testing without a client

The MCP Inspector is the reference tool for poking at a server by hand. It takes the same command you would put in the config and starts the server itself [6]:

npx @modelcontextprotocol/inspector uv run --directory /Users/example/projects/notes-server notes-server

It needs Node 22.19.0 or newer. The command opens a browser UI where you can list the server’s tools, call one with arguments, and read the raw JSON-RPC exchange, which is where a stray stdout line shows up [6]. The same package has a scriptable mode that lists tools and exits [6]:

npx @modelcontextprotocol/inspector --cli uv run --directory /Users/example/projects/notes-server notes-server --method tools/list

If a tool works in the Inspector but not in the client, the problem is in the client’s config. If it fails in both, the problem is in the server.

Common problems

SymptomLikely causeFix
Client reports a parse or connection error right after launchSomething other than MCP messages on stdoutSend logs to stderr; remove print calls
“command not found” in the client onlyA graphical client has a shorter PATH than your terminalUse the absolute path from which uv (macOS, Linux) or Get-Command uv (PowerShell)
Server starts but can’t find its filesWorking directory is not the projectPass the directory flag or an absolute path
Every tool appears twiceSame server registered in two config filesRemove one registration
Edits have no effectServer still running from before the editRestart the client session
Works locally, fails against a shared copyLocal runs use your credentials, a shared server uses its ownTest the shared deployment’s permissions separately

Trying the HTTP transport on your own machine

Streamable HTTP runs the server as an independent process that clients connect to by URL [7]. To test that path locally, start the server yourself and register http://localhost:8000/mcp as a url entry (port and path depend on the server). The spec says a server running locally should bind to 127.0.0.1 and not 0.0.0.0, and that it must validate the Origin header, because otherwise a web page could reach it through DNS rebinding [7].

Quick reference

GoalCommand
Run the server by hand (it waits on stdin)the same command the client runs, for example uv run notes-server
Register itadd command, args, env under mcpServers (or servers in .vscode/mcp.json)
Poke at it without a clientnpx @modelcontextprotocol/inspector <command> <args>
Load an editrestart the client session
Go sharedswitch to the HTTP transport and a url entry

References

Show all 7 sourcesHide sources
  1. [1] ↩ a b Model Context Protocol project, “Specification,” Model Context Protocol Specification (2026-07-28). Accessed: Oct. 13, 2026. [Online]. Available: https://modelcontextprotocol.io/specification/2026-07-28
  2. [2] ↩ a b c d Model Context Protocol project, “stdio,” Model Context Protocol Specification (2026-07-28). Accessed: Oct. 13, 2026. [Online]. Available: https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/stdio
  3. [3] ↩ Astral, “Running commands,” uv Documentation. Accessed: Oct. 13, 2026. [Online]. Available: https://docs.astral.sh/uv/concepts/projects/run/
  4. [4] ↩ a b c d e f Microsoft, “Add and manage MCP servers in VS Code,” Visual Studio Code Documentation. Accessed: Oct. 13, 2026. [Online]. Available: https://code.visualstudio.com/docs/agent-customization/mcp-servers
  5. [5] ↩ Astral, “Commands,” uv Documentation (CLI reference). Accessed: Oct. 13, 2026. [Online]. Available: https://docs.astral.sh/uv/reference/cli/
  6. [6] ↩ a b c Model Context Protocol project, “MCP Inspector,” Model Context Protocol Documentation. Accessed: Oct. 13, 2026. [Online]. Available: https://modelcontextprotocol.io/docs/tools/inspector
  7. [7] ↩ a b Model Context Protocol project, “Streamable HTTP,” Model Context Protocol Specification (2026-07-28). Accessed: Oct. 13, 2026. [Online]. Available: https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http

Every source cited across the primers is collected on one page.