Connect to a Local Server

Beginner6 min

Connecting over stdio is a process launch, not a connection. Absolute paths, the rule against printing to stdout, and where the logs are.

#implementation

How a stdio connection to a local server works

There is no network involved, and once you know that, most of the failures explain themselves.

The host starts your server as a child process and talks to it over the pipes every process already has. The specification describes it plainly: "the client launches the MCP server as a subprocess", and "The server reads JSON-RPC messages from stdin and writes JSON-RPC messages to stdout." One message per line, newline-delimited, and messages "MUST NOT contain embedded newlines."

So "connecting" is not a connection in the usual sense. It is a process launch. Your configuration is not an address; it is a command line. Almost everything that goes wrong goes wrong the way a command line goes wrong.

Transport layer covers the choice between stdio and streamable HTTP, and why you would pick one. What follows assumes you picked stdio and want it running.

The configuration entry, field by field

Configuration lives in a JSON file the client reads at startup. Here is a filesystem server, from the protocol's own documentation:

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/username/Desktop"]
    }
  }
}

Four things are being specified:

  • The key, filesystem here, is a label for the server in the client's own interface. It is not sent anywhere.
  • command is the executable to launch.
  • args are its arguments. Anything after the package name in this example is the server's own configuration, not the protocol's.
  • env, not shown, supplies environment variables. It is where API keys go, because the server inherits a deliberately sparse environment rather than your shell's.

One caution before you copy this. That file is one client's format, not the protocol's. The example above is Claude Desktop's, which the documentation itself frames as "one of the many clients that support MCP", and it lives at ~/Library/Application Support/Claude/claude_desktop_config.json on macOS or %APPDATA%\Claude\claude_desktop_config.json on Windows. Other clients use different paths and sometimes different key names. The shape (a named entry with a command, arguments, and an environment) is what transfers.

Absolute paths, and why relative ones fail

The most common failure has the least interesting cause.

Your server is not launched from your project directory. It is launched by a desktop application whose working directory is something you did not choose and would not guess. A relative path in command or args resolves against that, and the process dies before it speaks a word of protocol.

The documentation's troubleshooting list says it directly: make sure paths "are valid and that they are absolute and not relative." Write out the full path to your interpreter and to your script. It looks verbose and it removes an entire class of problem.

Windows has a sharper version of the same issue. If a path in the configuration contains an unexpanded ${APPDATA}, the launch fails with ENOENT, and the documented fix is to put the expanded value into the entry's env.

Why your server must not print to stdout

This rule produces the most confusing failure of the set.

The specification: "The server MUST NOT write anything to its stdout that is not a valid MCP message."

Standard output is the wire. A console.log in your server is not a debug message. It is a corrupt frame injected into the middle of a JSON-RPC stream. The client reads a line, fails to parse it, and what you see is a connection that drops or a server that never finishes starting. The print statement you added to debug the problem is now causing a different problem.

Logging goes to standard error, and the specification allows it explicitly: the server "MAY write UTF-8 strings to stderr for any logging purposes including informational, debug, and error messages." Clients are told not to read anything into it. The client "SHOULD NOT assume stderr output indicates error conditions."

In practice: replace console.log with console.error in an MCP server, or configure your logging library to write to stderr, before you write anything else.

Debugging a failed connection in order

When a server does not appear, work forward through the stages rather than guessing. Each stage has a different symptom and a different fix.

Does the process start? Run the exact command from the configuration in your own terminal, with the same arguments. Most failures surface here as a missing interpreter, a bad path, or a stack trace on startup. The documentation recommends this as the first step, and it is the fastest one.

Does it stay running? A server that starts and exits immediately usually threw during initialization. Running it by hand shows you that too; the process returns to your prompt instead of waiting.

Does it speak? A process that runs and stays up but shows no tools is usually the stdout problem above, or a server that never registered anything. Check its stderr.

Are the tools there? If the client lists the server but no tools, the server is connected and the gap is in what it exposes. That is a server question, not a connection one.

Reading the client's MCP logs

The client keeps its own record, which is where to look when running the command by hand works and the client still shows nothing.

For the documented example, mcp.log holds general connection logging and failures, and mcp-server-SERVERNAME.log holds one named server's stderr. Note what follows from the stdout rule: since "Stdio servers may use stderr for all their logging", those per-server files "are not limited to errors." Your server's ordinary logging is in there too.

On macOS both sit in ~/Library/Logs/Claude, and following them while you restart the client is the fastest way to watch a connection fail:

tail -n 20 -f ~/Library/Logs/Claude/mcp*.log

One last behavior worth expecting. When the client shuts down it closes your server's standard input and waits for the process to exit, escalating to a forced kill if it does not. Servers "SHOULD exit promptly when their standard input is closed". A server that ignores end-of-file gets killed instead, every time. And if it dies unexpectedly the client is expected to restart it; because the protocol is stateless, "any in-flight requests are simply lost and the client can retry them against the fresh process."

Further reading

Knowledge check

Question 1 of 4

You add a console.log to your MCP server to debug a problem. The client now fails to connect at all. Why?

Sign in to save your progress and pick up where you left off.