Connecting to Claude Desktop, Cursor, Etc.
You built a working server in article 03: Your First MCP Server and watched the tools respond in a terminal. Now you want it inside a real AI app. This is where a lot of people get stuck: the server is fine, but it never appears in the host.
The connection is almost always a few lines of JSON in the right file. This article covers where that file lives per host, the two ways a host reaches your server, and how to fix it when nothing shows up.
The registration is just config
You do not “install” an MCP server into a host the way you install a plugin. You tell the host how to reach it: a command to run for a local server, a URL for a remote one.
Every host stores this in a config file, and they have converged on the same shape: a top-level mcpServers object keyed by a name you pick. Claude Desktop set the pattern and the others copied it.
{
"mcpServers": {
"my-tools": {
"command": "python",
"args": ["/absolute/path/to/server.py"]
}
}
}
The key (my-tools) is a label you choose; it shows up in the host’s UI and logs. The value tells the host what to launch. That is the whole contract for a local server.
stdio: the host launches your server
The config above is a stdio server. When the host starts, it runs command with args as a subprocess and talks over the process’s stdin and stdout. No port, no URL, and nothing running until the host launches it. Your server is a child process of the host.
Two rules save you most of the pain here.
First, use absolute paths and name the interpreter. The host does not run inside your shell, so it has no PATH, no pyenv, no project virtualenv. python server.py works in your terminal and fails in the host because it found a different python or none at all. Point at the exact interpreter and the exact file.
{
"mcpServers": {
"my-tools": {
"command": "/Users/you/project/.venv/bin/python",
"args": ["/Users/you/project/server.py"],
"env": { "API_KEY": "sk-..." }
}
}
}
Second, pass secrets through env, not by hardcoding them in the file you might commit. The host injects env into the subprocess before it starts.
Why absolute paths, when relative paths work in my terminal?
Your terminal runs the command from your current directory, with your shell’s PATH and any environment your shell profile set up. The host has none of that.
The host launches the subprocess from its own working directory (usually not your project) with a minimal environment. A relative server.py resolves against the wrong directory; a bare python resolves against the wrong PATH. Both fail silently: the process never starts, or starts against the wrong interpreter. Absolute paths remove the guesswork.
Remote HTTP: the host connects to a URL
For a server that already runs somewhere, on a VM, in a container, behind your company’s gateway, you give a URL instead of a command. The host connects to the running server over Streamable HTTP. The transport itself is covered in article 02: MCP Architecture; the config difference is just command-versus-URL.
{
"mcpServers": {
"team-api": {
"url": "https://mcp.internal.example.com/mcp",
"headers": { "Authorization": "Bearer ${TEAM_TOKEN}" }
}
}
}
No subprocess is launched. The host opens an HTTP connection and runs the same handshake it would over stdio. Because the server is reachable over the network, auth is now your job: most remote servers expect a bearer token or an OAuth flow, passed in headers.
Which one to reach for is the same call as choosing the transport itself. stdio for a personal tool on your own machine, HTTP for anything shared across people or machines.
flowchart LR
A["Server on<br/>your machine?"] -->|Yes| B["stdio:<br/>command + args"]
A -->|"No, remote"| C["HTTP:<br/>url + headers"]
B --> D["Host launches<br/>a subprocess"]
C --> E["Host connects<br/>to a running URL"]
Where each host keeps its config
The shape is shared. The file location and exact keys drift a little per host. Here is where to look.
| Host | Config location | stdio | Remote URL |
|---|---|---|---|
| Claude Desktop | claude_desktop_config.json (Settings, Developer, Edit Config) | Yes | Yes |
| Cursor | .cursor/mcp.json (project) or ~/.cursor/mcp.json (global) | Yes | Yes |
| Windsurf | ~/.codeium/windsurf/mcp_config.json | Yes | Yes |
| Zed | settings.json, under context_servers | Yes | Yes |
Claude Desktop’s config lives in the app support directory, but you rarely need the raw path: Settings, then Developer, then Edit Config opens it for you. Cursor is worth calling out for its per-project file (.cursor/mcp.json at the repo root), the clean way to give one codebase its own servers without touching your global setup.
The rest follow the same mcpServers object with command/args or url. Zed nests it under a differently named key, but the per-server fields match. When you meet a new host, find its “MCP” or “context servers” settings section and expect the Claude Desktop shape.
Verify the connection
After editing config, do two things every time.
Restart the host. Config is read at startup, so editing the file while the app is open changes nothing until you fully quit and reopen. In Claude Desktop, “fully quit” means quitting the app, not just closing the window.
Look for your server in the host’s UI. Claude Desktop lists connected servers and their tools behind the tools icon in the message box. Cursor shows them in its MCP settings pane with a green or red status dot. If the tools are listed, the handshake from article 02: MCP Architecture completed and you are done.
You can also prove the server is sane before touching the host. Send it a raw initialize request over stdin and watch stdout:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"0"}}}' \
| /Users/you/project/.venv/bin/python /Users/you/project/server.py
You want exactly one clean JSON-RPC response on stdout and nothing else. If you see a stack trace, a log line, or a print output mixed in, you have found your bug before the host ever ran.
”My server does not show up”
This is the classic failure, and it has four usual causes. Work down the list.
- Wrong path or interpreter. The most common by far. The host cannot find
commandor the file inargs. Use absolute paths for both. Confirm the exact command runs from a fresh terminal with no virtualenv active. - Crash during
initialize. The server starts but throws before answering the handshake, so the host gives up. A missing env var or a failed import at startup does this. The terminal test above surfaces it immediately. - stdout contamination. On stdio, stdout is the message channel. A stray
print, a startup banner, or a library that logs to stdout corrupts the stream and the host drops the connection. Send all logging to stderr. Sneaky, because the server “works” when you eyeball it. - Did not restart. You edited the config, the app is still on the old one, nothing changed. Fully quit and reopen.
flowchart TB
A["Server missing<br/>in host"] --> B["Restarted<br/>the host?"]
B -->|No| C["Fully quit<br/>and reopen"]
B -->|Yes| D["Run server<br/>by hand"]
D --> E["Clean JSON<br/>on stdout?"]
E -->|No| F["Fix crash or<br/>stdout logging"]
E -->|Yes| G["Fix path in<br/>config, absolute"]
When all four check out and it still fails, read the host’s own logs. Claude Desktop writes per-server logs you can tail, and they capture your server’s stderr, which is exactly where a startup crash announces itself.
Why does the host capture stderr but not stdout for logs?
On stdio, the host reads JSON-RPC messages off your server’s stdout, one per line, and tries to parse every line as protocol. stdout is not yours to write to freely; it belongs to the connection.
stderr is a separate stream the host does not parse. The host redirects it into a log file so your error output has somewhere to go. That is why every MCP SDK defaults its logger to stderr, and why a startup crash shows up in the host’s log even though the server never connected. When something breaks, that log is the first place to look.
Common beginner mistakes
- Relative path in config:
python server.pyfinds the wrong interpreter or nothing. Absolute paths for command and file. - Editing config without restarting: config is read at startup only. Fully quit and reopen the host.
- Logging to stdout on stdio: any stray output corrupts the stream and the host drops the connection. Log to stderr.
- Assuming the host has your shell env: it does not have your
PATHor virtualenv. Point at the exact interpreter and pass secrets viaenv. - Hardcoding secrets in the config file: use the
envblock (stdio) orheaders(HTTP) so you are not committing tokens. - Trailing-comma JSON: these config files are strict JSON. One trailing comma and the whole file fails to load silently.
Questions you will face in production
“It works in Claude Desktop but not in Cursor. Same config, different result. Why?”
Check the file location and the working directory. Cursor’s per-project .cursor/mcp.json resolves relative paths against the repo root, and each host launches the subprocess from its own directory. If either config used a relative path, one host got lucky and the other did not. Make every path absolute and the difference disappears.
“Can I use the same server on stdio locally and HTTP for my team?”
Yes, and you should build for it. The server logic is transport-agnostic; only the entry point differs. Run it over stdio for your own use with command/args, and deploy the same code behind an HTTP endpoint for the team with url. The handshake and the tools are identical either way.
“Do I have to write config by hand, or is there an installer?” For many published servers there is a one-line install command or a marketplace entry that writes the config for you. For a server you wrote, hand-editing the JSON is the reliable path and worth understanding, because when an installer fails, it fails by writing this exact file wrong.
What to remember
- Registering a server is config, not installation: a command for local, a URL for remote
- Every host uses the same
mcpServersshape; only the file location and a few keys differ - stdio means the host launches your server as a subprocess over stdin/stdout; use absolute paths
- Remote HTTP means the host connects to a running URL; auth is now your job, via
headers - Config is read at startup, so restart the host after every edit
- “Does not show up” is almost always wrong path, a crash during
initialize, stdout contamination, or no restart
What to study next
Once your server connects cleanly in the hosts you care about, the next question is how other people install it without hand-editing JSON. That is article 08: Packaging and Distributing: turning a script on your machine into something a stranger can add to their host in one step. If a connection still misbehaves, go back to the handshake in article 02: MCP Architecture and trace it message by message.
Further reading
- Model Context Protocol: Connect to local servers. The official user quickstart for wiring a server into Claude Desktop.
- Claude Desktop: MCP setup docs. Where the config file lives and how to edit it from the app.
- Cursor: Model Context Protocol. Cursor’s
mcp.json, project vs global, and its server settings pane. - MCP spec: Transports. stdio and streamable HTTP defined precisely, so you know what each config maps to.
Where this article comes from. This is a synthesis of the MCP specification and common practice as of 2026, not a citation of any single paper. The sources above are where the mechanics come from. If you find an error or have a better source for a claim, the article gets fixed within a day, send me a note.