mcp server meaning, a child process your host forks
Almost every guide on this phrase stops at the protocol abstraction: a program that exposes Tools, Resources, and Prompts. That is correct, but it is not the thing you can point at on your machine.
The OS-level definition is smaller and more useful. An MCP server is a child process your host forks, pipes stdin and stdout to, and speaks JSON-RPC to until one of them dies. I will trace that definition through the Swift source of whatsapp-mcp-macos, an MCP server you can watch boot and die in Activity Monitor.
The three-sentence definition
An MCP server is a program the host runs as a child process. The host binds anonymous pipes to that child's stdin and stdout and uses them as the transport. The server answers initialize, tools/list, and tools/call JSON-RPC frames sent across those pipes until one side closes.
Every other property people associate with an MCP server, capabilities, tools, resources, permission model, flows from that. If you hold the forked-child picture in your head, the rest of the MCP spec stops feeling mysterious and starts feeling like implementation detail.
1. On disk
A compiled executable at .build/release/whatsapp-mcp, ~12 MB of Mach-O bytecode produced by `xcrun swift build -c release`. Inert until a parent process does execve() on it.
2. In memory
A running child process with a parent host as ppid. Its stdin and stdout are anonymous pipes connected to the host; its stderr is typically inherited. No TCP listener, no UNIX socket, no filesystem path you can connect to from outside.
3. On the wire
JSON-RPC 2.0 frames over those stdio pipes. Three method families matter: initialize (handshake), tools/list (capability discovery), tools/call (actual work). Everything else is resources, prompts, and notifications layered on top.
How the child comes to life
Before the host ever talks to your server, it reads a config, forks, and pipes. Nothing below involves networking. Nothing below involves the MCP spec yet. This is ordinary POSIX process plumbing, which is the step every definition skips.
host reads config, forks the binary, and staples pipes to it
After this moment, the server is addressable only through its parent's pipes. Nothing outside the parent can send it a request.
The config the host reads
Here is the real entry for whatsapp-mcp in Claude Code's config. It is not a URL. It is a command and a list of args. The host's job is to launch that command as a subprocess, not to “connect” to anything.
What a server's source actually looks like
Every MCP server, regardless of language, boils down to the same shape: an array of tool definitions, a single Server struct carrying name, version, and capabilities, and a call to start the transport. Here is that shape in whatsapp-mcp-macos. Read the comments carefully; they are line numbers from the real source.
Everything the rest of the spec describes, tools, capabilities, lifecycle, flows from those 20-ish lines. There is no hidden runtime, no reverse proxy, no gateway. The Server struct plus the StdioTransport is the server.
anchor fact
0 tools, one Server struct, a 5.0s AX timeout
The whatsapp-mcp binary registers exactly 11 tools (line 1110 of main.swift) under a single Server declared with name: "WhatsAppMCP", version: "3.0.0", and capabilities: .init(tools: .init(listChanged: true)). On startup it logs setupAndStartServer: defined 11 tools to stderr, then blocks on StdioTransport.
The AX messaging timeout that governs every UI call is hardcoded at 5.0 seconds on line 119, not configurable through any MCP-visible setting. If your host calls whatsapp_search and the WhatsApp app hangs, the MCP-visible error comes back after exactly 5 seconds because that is the budget the child set on its own application element.
Where that timeout lives
The reason a timeout buried inside a child process matters is that the host has no way to override it. The value is set on the AXUIElement itself, which is private state of the server.
The initialize handshake, the one that decides what the server can do
The capability contract lives entirely inside one request/response pair at the start of the session. After this exchange, the server's surface is frozen for the life of the process. The host will not ask about features the server did not advertise here.
initialize, then one tool call
Notice the middle column. The transport is not a connection, it is a pair of pipes, and every “message” is just one side writing bytes the other side eventually reads.
Watching one spawn, live
If the forked-child picture still feels abstract, here is the receipt. Start with no MCP host running, confirm nothing is alive, then launch Claude Code and inspect the new process.
Three things are worth noticing. The server has a parent pid (the host). Its stdin and stdout are anonymous pipes, not sockets. And it exits the moment the host does, because an MCP server has no independent reason to exist.
Hosts that speak this shape
The forked-stdio-child shape is not specific to Claude Code. Almost every tool that calls itself an “MCP host” implements the same spawn model, with minor variations in where the config file lives and which log channel the server's stderr goes to.
If you are evaluating an MCP server's behavior, the question is never “what URL does it expose”. It is “what does the host do when it forks this binary”.
The server lifecycle, end to end
Six phases. None of them involve a port, a URL, or a process manager. The host is both the launcher and the only consumer.
The host reads its MCP config
A JSON file (~/.claude.json, settings.json, mcp.json, depending on host) maps a server name like `whatsapp` to a command. The host never opens a network connection; it stores a process recipe.
The host forks and execs
On startup, the host calls the OS equivalent of fork() + execve("whatsapp-mcp"). The new child inherits the host's user id, working directory, environment, and (this matters later) its Accessibility trust grant.
stdio pipes become the transport
The host binds two anonymous pipes to the child: one for stdin, one for stdout. There is no port number. There is no URL. Lines of JSON-RPC travel through those two file descriptors for the life of the process.
The server answers initialize
The host sends { method: "initialize" }. The server responds with { serverInfo: { name, version }, capabilities }. This is the only moment the server gets to declare what it can do. Capabilities the server does not list here do not exist for this session.
The host calls tools on demand
Once initialized, the host calls tools/list to discover the 11 tools, then tools/call when the model decides to use one. Each call is a single request/response pair over the same two pipes.
The parent dies, the server dies
MCP has no graceful shutdown the user triggers. When the host exits, the pipes close; the server's next read returns EOF; the process exits. The lifetime of an MCP server is strictly a sub-interval of the lifetime of the host that spawned it.
The permission model follows the process tree
Because the server is a child of the host, it inherits the host's OS permissions, not its own. On macOS this is the single most confusing part of MCP for new users, and it is exactly a consequence of the forked-child shape.
When whatsapp-mcp calls AXUIElementCreateApplication to read the WhatsApp app's accessibility tree, macOS does not ask whether the server binary has Accessibility trust. It asks whether the responsible process does, and the responsible process is the one the user directly launched. That is the host. Granting Accessibility to whatsapp-mcp by dragging its binary into System Settings has no effect; you have to check the box next to Claude Code, Cursor, or whatever host will fork it.
Same rule applies to Full Disk Access, Input Monitoring, Screen Recording, and every other TCC-gated capability. The server cannot possess a permission its host does not have.
Shipping an MCP server and the spawn model is tripping you up?
30 minutes with me on how your host will fork, sandbox, and permission your server in the wild. Bring your config.
Frequently asked questions
Is an MCP server a web server?
No. By default, an MCP server has no HTTP listener, no URL, and no port. It is a command-line program the host launches as a child process and pipes JSON-RPC through stdio to. The spec does define an optional HTTP transport for the remote case, but the overwhelmingly common shape, including whatsapp-mcp-macos, Claude Code's file system server, and almost every server on the official registry, is a local stdio child. If you try to `curl` it, nothing answers.
Where does an MCP server actually run?
Inside your host's process tree. Open Activity Monitor after launching Claude Code; the `whatsapp-mcp` process is a direct child of `Claude Code Helper (Plugin)`. Its ppid is the host's pid. Its stdin and stdout are anonymous pipes. There is no separate daemon to start, no service to enable, no port to open in the firewall. Uninstalling the host takes the server down with it.
What is actually inside an MCP server program?
At minimum: one Server struct that declares a name, version, and capabilities; a handler for each tool the server exposes; and a transport (almost always StdioTransport). In whatsapp-mcp-macos the whole surface is 11 tools registered between lines 994 and 1108 of Sources/WhatsAppMCP/main.swift, wrapped in a single Server declared at line 1113 with capabilities.tools.listChanged = true, started on StdioTransport at line 1191. That is the whole server.
What does a 'capability' mean during the MCP handshake?
A capability is the server's own declaration, sent in the initialize response, of which protocol features it supports. The three top-level categories are tools, resources, and prompts. whatsapp-mcp-macos sends `capabilities: .init(tools: .init(listChanged: true))`, meaning it exposes tools and will notify the host if the tool list changes. It explicitly does not advertise resources or prompts, so the host will not ask about them. Capabilities are declared once at handshake and cannot be re-negotiated without restarting the process.
Why do I have to grant Accessibility permission to Claude Code instead of to the MCP server itself?
Because the MCP server inherits its TCC (Transparency, Consent, Control) context from its parent host. When whatsapp-mcp calls AXUIElementCreateApplication or CGEventPost, macOS checks whether the responsible process has Accessibility trust. The responsible process is the one that owns the controlling terminal or was launched by the user, which is the host (Claude Code), not the child. Adding whatsapp-mcp directly to Accessibility settings does nothing. This is why `npm install -g whatsapp-mcp-macos` cannot by itself make anything work; you have to toggle Claude Code in System Settings.
Does the server maintain state between tool calls?
As much as any long-lived process does. The server's Swift heap persists for the life of the host session. whatsapp-mcp keeps track of the active chat implicitly through the WhatsApp app's UI state (because it reads from the accessibility tree rather than holding its own model), but an MCP server that wanted to cache results, hold a DB connection, or remember cursor positions is free to do so. State resets when the host restarts the server, which happens whenever you quit and relaunch Claude Code or Cursor.
Can two hosts share one MCP server process?
Not cleanly, and not the way stdio transport works. Each host spawns its own child, so if you run Claude Code and Cursor simultaneously with the same MCP config, you get two independent `whatsapp-mcp` processes, each with its own 5.0s AX messaging timeout, its own heap, and its own view of the WhatsApp app. They can race when both try to drive WhatsApp at once. The remote HTTP transport does allow one server to serve many clients, but it is a different deployment model and the vast majority of installed servers do not use it.
What is the difference between an MCP server and an MCP client?
The client (also called host) is the parent process: Claude Code, Cursor, Fazm. It decides which servers to spawn, routes JSON-RPC requests to them, and hands tool results back to the model. The server is the child process: whatsapp-mcp-macos, filesystem-mcp, github-mcp. It declares a set of tools and executes them on request. One host can hold many servers open at once, each in its own child process, but a server by itself has no reason to exist; there is nothing listening to it until a host spawns it.
Why is this shape worth understanding?
Because most of the everyday confusion disappears once you see it. Questions like 'which process holds the new binary after npm update', 'why does my auth token only reach one server', 'why does debugging require running the server manually with stdin piped from a file', all fall out of the fact that the server is a forked child speaking JSON-RPC over pipes, not a service you connect to. Treating it as a service leads you to look for ports, logs, and PIDs that do not exist.
Where can I see a real MCP server exit because the host died?
Open Activity Monitor and filter by `whatsapp-mcp`. Launch Claude Code; the process appears. Note its PID and its parent's PID. Quit Claude Code; the process disappears within a second, because its stdin pipe closed and its read loop returned EOF. The same experiment works with any stdio MCP server and any host. `dtruss -p <pid>` against the child during shutdown shows the final `read(0, ...) = 0` and then `exit(0)`.