What is an MCP server? A real one, opened up in the editor, not the abstract definition.
Every article on the first page of Google defines MCP the same way. A protocol, a client, a server, three primitives (tools, resources, prompts), the same three example servers (files, GitHub, Slack). All correct, all abstract. What none of them do is open an actual MCP server and show the dispatch function where the name the LLM emits becomes the code that runs. This page does. The server is Terminator's MCP agent, a ~10,900 line Rust binary that turns your AI coding assistant into a desktop agent, and the interesting part fits on one screen.
The short answer
An MCP server is a long-running process that exposes a set of named tools an LLM can call. It speaks JSON-RPC 2.0 over stdio or HTTP, it responds to list_tools with a catalogue of what it can do, and it responds to tools/call by dispatching to a handler keyed on the tool name. That is the whole protocol from the server's perspective. The value of an MCP server is whatever its handlers do.
Terminator's MCP server is worth looking at because its 31 handlers wrap the Windows UI Automation and macOS Accessibility APIs. Which means the LLM it is connected to can click, type, scroll, and read the state of any app on your desktop, not just talk to one SaaS. Same protocol, dramatically different surface area.
How a single tool call flows through an MCP server
Four actors, nine messages. The model never touches the OS directly; it emits a tool name and waits.
tools/call over stdio
Terminator's MCP server, by the numbers
Numbers pulled from the current state of the open-source repo. The first is the count of match arms in dispatch_tool. The second is the count of exposed handler functions via #[tool] macros. The third is the line count of src/server.rs. The fourth is the default per-machine concurrency limit in HTTP mode.
dispatch_tool: the whole server, on one screen
This is the core of Terminator's MCP server. Abbreviated, but shaped exactly like the real file. Open crates/terminator-mcp-agent/src/server.rs at line 9953 to see all 31 arms. Each arm deserializes JSON into a typed Args struct, awaits an async handler, and wraps cancellation via tokio::select. There is no routing framework. No middleware chain. One match block.
That is it. Every MCP tool call the LLM makes, for every tool the server exposes, passes through that match. If you remember one thing about MCP servers from this page, let it be that the protocol is elaborate and the server shape is not.
The anchor fact: build.rs keeps the system prompt in sync with the code
Here is the part no other MCP explainer mentions, because no other server does it. Terminator's build.rs runs at compile time. It opens src/server.rs, walks every line until it hits the string let result = match tool_name, then collects the tool names from the match arms below. The resulting comma-separated list is injected into the binary as an environment variable.
At runtime, prompt.rs reads the baked-in list and pastes it verbatim into the system instructions the server announces on initialize. The LLM's system prompt literally contains the names the match block dispatches against.
You can verify this in 10 seconds: clone the repo and run grep -n MCP_TOOLS crates/terminator-mcp-agent/build.rs crates/terminator-mcp-agent/src/prompt.rs. Three matches come back: one writing the env var in build.rs, one reading it with env!, and one interpolating it into the instructions string. The prompt cannot drift from the implementation, because changing either requires recompiling and the build step parses the match block fresh every time.
The 31 tool names, end to end
This is the exact set the build step extracts, in source order. The LLM sees this list inside its system prompt and picks a name when it wants to act.
Count them: there are 31. Some wrap UI actions (click_element, type_into_element), some wrap OS-level affordances (open_application, run_command), some wrap file I/O (read_file, write_file), and one (execute_sequence) is a workflow DSL that nests the others.
Anatomy of a real MCP server
Every MCP server boils down to these six parts. The names change, the shapes do not. Below is how Terminator implements each one.
A single dispatch function
server.rs exposes one async dispatch_tool(tool_name, arguments) that match-arms on the name and calls the correct typed handler. Every tool call in the protocol lands here first. There is no routing layer beyond the match block.
One handler per tool
click_element, type_into_element, get_window_tree. Each match arm deserializes JSON into a typed Args struct, awaits an async method, and wraps cancellation via tokio::select.
list_tools for discovery
The LLM asks the server what it can do. list_tools returns the full tool set, filtered per client if the client is running in Ask mode with blocked_tools set.
A system prompt that stays in sync
build.rs parses the match block at compile time and bakes the tool list into MCP_TOOLS. prompt.rs reads env!("MCP_TOOLS") and pastes it into the instructions the LLM sees. Add a tool, recompile, the prompt updates itself.
Transport: stdio or HTTP
Started by a local command under your editor's supervision (stdio) or behind a load balancer with GET /status returning 503 when busy. Same dispatch, different plumbing.
Observability that is actually there
Every tool call is wrapped in execution_logger::log_request and log_response_with_logs. Screenshots before and after UI actions, per-tool PostHog timings, captured tracing and stderr all land on disk in executions/.
Every tool call, one hub, one handler per arm
End to end, what happens when an LLM calls a tool
Six steps from cold start to rendered result. No hand-waving, no "the framework handles it" boxes.
Editor starts the server
Claude Code or Cursor spawns npx -y terminator-mcp-agent and opens a pair of pipes. The MCP handshake agrees on a protocol version and the server declares its capabilities.
Client asks list_tools
The LLM's host calls list_tools. Terminator returns every handler name it registered via #[tool] macros, with JSON schemas derived from Rust arg structs.
LLM picks a tool by name
During a user turn, the model decides it needs to type into a text box. It emits a tool call with tool_name: "type_into_element" and JSON arguments for the selector and text.
dispatch_tool matches the name
server.rs match arm at "type_into_element" deserializes arguments into TypeIntoElementArgs, runs tokio::select against the cancellation token from request_context, and awaits the handler.
Handler touches the OS
type_into_element calls into the Windows UIA or macOS AX adapter, finds the element by selector, and sends keystrokes through the accessibility API. No screenshots, no vision model, no pixel math.
Result flows back
The handler returns a CallToolResult. dispatch_tool logs the response with duration and captured logs, restores window focus, and writes screenshots before/after to executions/. The LLM receives the result on the next turn.
Try it against a real server in two minutes
This is the actual installer path the Terminator README ships. Once Claude recognizes the server, the 31 tool names show up in its internal tool picker and your AI can drive your desktop.
What orbits a Terminator MCP server once it is running
Six real tool names from the dispatch match, circling the server that exposes them. These are the verbs an LLM has available the moment the process is up.
“Every file and line number referenced on this page is grep-able in a fresh clone of mediar-ai/terminator.”
github.com/mediar-ai/terminator
MCP server vs a traditional REST API
Both expose capabilities over a network protocol. The audience is the difference. One is built for developers to integrate; the other is built for language models to discover and call.
| Feature | REST API | MCP server |
|---|---|---|
| Transport | HTTP+JSON, fixed URL, one endpoint per verb | stdio or HTTP, framed JSON-RPC 2.0, one dispatch_tool entrypoint |
| Tool discovery | OpenAPI spec the LLM has to be shown manually | list_tools() returns them at runtime, baked from source by build.rs |
| Who writes the schema | Developer hand-writes types for function calling | Schema derived from Rust arg structs via #[tool] macros |
| How tools reach the prompt | Prompt engineer pastes a list into the system prompt | env!("MCP_TOOLS") compile-time injection keeps prompt and code in sync |
| What the tools can touch | Whatever the API wraps (usually one service) | Anything the OS accessibility tree exposes, across all apps |
| Session state | Stateless HTTP calls | Long-lived process, cancellation tokens, in_sequence flag, focus restoration |
| Concurrency limit | Typical web API rate limits | MCP_MAX_CONCURRENT env var, 503 when busy for LB probes (README line 45) |
Why MCP matters for the desktop case specifically
A file system MCP server is nice. A GitHub MCP server is nice. What is new is that the same protocol also lets an LLM drive the UI of arbitrary apps. Calculator, Excel, Chrome, a legacy Win32 accounting system your team still uses. Any of those, via the accessibility tree, through one dispatch_tool match block.
Terminator is that MCP server. It is a developer framework for building desktop automation, not a consumer app. It gives existing AI coding assistants the ability to control your entire OS, not just write code. Like Playwright, but for every app on your desktop, and the surface the LLM sees is the 31-tool list the build step extracts from server.rs.
If you were looking for a definition of MCP server, the short answer is at the top of this page. If you were looking for the thing it is actually good for, this is it.
Install Terminator's MCP server now
Runs over stdio under Claude Code or Cursor, or over HTTP behind a load balancer. MIT-licensed, cross-platform, and the full source of the dispatch function is in one file you can read start to finish in an hour.
claude mcp add terminator →Desktop tools currently exposed
One per match arm in dispatch_tool. Counted fresh at every compile.
Frequently asked questions
What is an MCP server in one sentence?
An MCP server is a process that speaks the Model Context Protocol over stdio or HTTP and exposes a set of named, typed tools an LLM can call. Past the protocol framing, the core is a single dispatch function that takes a tool name plus JSON arguments and returns a structured result. You can see a real one at crates/terminator-mcp-agent/src/server.rs line 9953 in the Terminator repo: the async fn dispatch_tool has one match arm per tool, and Terminator's currently has 31 of them. That dispatch function is the MCP server. Everything else (transport, logging, schema generation) is plumbing around it.
How is an MCP server different from a REST API?
Two things. First, the tools are discovered at runtime via list_tools, not pre-documented in an OpenAPI spec. The LLM asks the server "what can you do" on every connection and the server answers with a schema the model can consume without human intervention. Second, the tool set is designed for an LLM, not a human developer. Typed arguments, clear naming, parameter limits, and a cancellation token so the model can stop a long-running action are first-class. Terminator's implementation goes further: its build.rs parses the dispatch_tool match block at compile time and bakes the tool list into the system prompt via env!("MCP_TOOLS"), so the LLM sees the exact same list the binary dispatches against. A REST API has no mechanism for that.
What does an MCP server actually do, as opposed to the generic definition?
Whatever you put inside dispatch_tool. Terminator's server hosts 31 desktop automation tools: click_element, type_into_element, get_window_tree, navigate_browser, open_application, execute_sequence, run_command, and two dozen more. Each one is an async Rust function that eventually calls into Windows UIA or macOS accessibility APIs. An MCP server for Slack would have post_message, list_channels, mark_read. An MCP server for Postgres would have run_query, list_tables, describe_schema. The protocol is the same; the handlers are whatever makes sense for your system.
What is the uncopyable thing about Terminator's MCP server?
The build.rs trick. Look at crates/terminator-mcp-agent/build.rs starting at line 31. extract_mcp_tools() reads src/server.rs, walks until it hits the line containing "let result = match tool_name", then collects every subsequent line that starts with a quoted string followed by " =>". That list becomes the MCP_TOOLS environment variable via rustc-env, which means the compiled binary contains the literal string of comma-separated tool names. prompt.rs line 10 reads env!("MCP_TOOLS") at compile time and injects it into the system instructions the server returns on initialize. The practical effect: you cannot drift between what the server dispatches and what the LLM sees in its prompt. Every other MCP server I have seen lets those two drift.
Do I need the MCP protocol for my LLM to use tools?
No. OpenAI function calling, Anthropic tool use, and model-specific SDKs all let you define tools inline. MCP matters when you want a tool server that is independent of the LLM, reusable across clients, and launchable as a separate process. Example: Terminator's MCP server runs the same way whether Claude Code, Cursor, or a custom app is talking to it, because the protocol is the contract. If you hardcode function definitions into one client, you pay that cost again for the next client. MCP is worth it when the tool set is either large (Terminator's 31), expensive to maintain (browser automation), or security-sensitive (file system access), and you want one implementation that multiple assistants share.
How does a client like Claude or Cursor connect to an MCP server?
Two common transports. stdio means the client launches the server as a child process and communicates over stdin/stdout, framed as JSON-RPC 2.0. This is what claude mcp add terminator "npx -y terminator-mcp-agent@latest" -s user sets up: Claude spawns npx, the MCP agent writes JSON-RPC frames to stdout, Claude reads them. HTTP is the alternative: the server binds a port and exposes POST /mcp (the main JSON-RPC endpoint), GET /health, and GET /status. Terminator's HTTP mode also exposes GET /events for SSE-based workflow progress. See crates/terminator-mcp-agent/README.md lines 36-45 for the full HTTP surface, including the concurrency-aware 503 behavior that plays nicely with an Azure Load Balancer.
What does Terminator's MCP server let an LLM actually do?
Drive any app on your desktop through the accessibility tree. Concretely: read the UI tree of any Windows or macOS app (get_window_tree), click elements by role and name (click_element with selectors like role:Button && name:Submit), type into text boxes (type_into_element), drive a browser (navigate_browser, execute_browser_script), launch applications, read and write files, run shell commands, and stitch it all together into YAML or JSON workflows (execute_sequence). The LLM does not see pixels unless you ask it to; it reads structured accessibility data the OS already provides. That is why Terminator calls itself Playwright-shaped for the whole desktop. Selectors, locators, typed actions, and the LLM sits on top as a user.
How do I inspect an MCP server's tool list without reading the source?
Two ways. The MCP spec provides list_tools. Any client library (including the official TypeScript and Python SDKs) exposes this as a call. Terminator's CLI shortcut is terminator mcp exec list_tools or terminator mcp exec get_applications to probe a single tool. For local debugging, you can also run the agent with HTTP transport (-t http) and hit the endpoint directly. For the specific case of Terminator, the shortest path to see the full list is to open crates/terminator-mcp-agent/src/server.rs and search for "let result = match tool_name"; every subsequent match arm is a tool. The same list is embedded in the binary at build time, so the binary and the source can never disagree.
Why would I build an MCP server instead of adding features to my existing product?
Because the audience is AI assistants, not humans. A product's web UI is optimized for a human with a mouse; an MCP server is optimized for a model that reads tool schemas, reasons about which tool to call, and does not benefit from visual affordances. A good test: if the work you want an AI to do involves chaining 5+ actions in your product and no single API call does all of it, an MCP server gives the LLM a first-class way to drive that chain with typed tools and explicit cancellation. Terminator's case is the extreme version: the product has no UI, it is entirely an MCP server plus SDKs. The MCP server IS the product.
What is the smallest possible MCP server?
A process that speaks MCP, exposes list_tools returning one tool, and handles one tool call. In practice that is ~50 lines in Python using the official SDK. A useful MCP server is usually larger because it needs schema definitions, error handling, observability, and a real set of tools. Terminator's MCP agent is ~10,900 lines in server.rs alone, plus the rest of the crate, because it is wrapping Windows UIA and macOS accessibility APIs and doing serious cross-process work. But the minimum bar is low; the floor to get an LLM calling a single custom tool is a single afternoon of work with an SDK.