How an MCP Server Works — explained by someone who published one
An MCP server is a program that exposes capabilities to AI models in a standardized format: tools, data, prompts. MCP (Model Context Protocol) defines how an AI assistant discovers what the server offers and how it invokes each capability. The server translates those invocations for the real system behind it, whether an API, a database or the filesystem. It is what lets someone tell an agent “check whether this PDF was altered” and watch the answer come from your product. Nobody had to write an integration for that specific assistant.
That is the short answer. The long one is more interesting, and this post walks through it with a real example. tamperlens-mcp is the server that packages the Tamperlens API, published to npm and to the official MCP Registry. The packaging decisions behind that server have a post of their own. This one is about the protocol: what happens between the user’s request and your API’s response.
What is MCP, exactly?
MCP is an open protocol, created by Anthropic and published in late 2024. It standardizes the conversation between AI applications and external systems. Before it, every assistant had its own plugin or function-calling format, the model calling functions. Integrating a product with N assistants cost N integrations. With MCP, the product exposes one server, and any client that speaks the protocol consumes it. Over 2025, the major AI clients (chat desktops, IDEs, agent frameworks) came to speak this protocol.
The architecture has three roles:
- Host: the AI application the user sees, such as a chat desktop, an IDE or an agent running on a server.
- Client: the component inside the host that maintains the connection to one specific server (one client per server).
- Server: the program exposing the capabilities. That is the part you write.
Underneath, the messages are JSON-RPC 2.0, a question-and-answer format in JSON: requests with method and params, responses with result or error. Nothing exotic. MCP’s strength is not message engineering, it is the standardization of the lifecycle: how to discover capabilities, how to invoke them, how each side negotiates what it supports.
How does a call work, from request to response?
The full cycle, in the order it happens:
- Handshake. The client connects and sends
initialize. Server and client exchange protocol versions and declare capabilities (“I serve tools”, “I accept notifications”). Only after this does anything useful happen. - Discovery. The client asks for
tools/listand receives each tool with a name, a description and a JSON schema for its parameters, that is, the expected shape of each argument. That text goes into the model’s context, and it is what tells the model what exists. - Choice. The user asks for something (“was this contract edited after it was signed?”). The model, reading the available descriptions, decides to invoke a tool and builds the arguments to match the schema.
- Invocation. The client sends
tools/callwith name and arguments. In interactive clients, this is where the user approves or refuses the call. - Execution. The server validates the arguments and does the real work. In our case, reading the file from disk and sending it to the analysis API.
- Response. The result comes back as content (text, structured data) and enters the model’s context, which uses it to answer the user.
Two practical consequences of this design that only become obvious once you operate a server:
The tool description is a user interface, and the user is the model. At step 3, everything the model has to decide with is the text you wrote. The tamperlens-mcp descriptions state what each tool does not do: “returns risk signals with the raw evidence behind each one — never a verdict on authenticity”. The reason is that the model repeats to the user whatever the description claims. An imprecise description becomes a hallucination with your brand on it.
The payload crosses the context, so binaries cannot go inline. Everything that travels in steps 4 and 6 passes through the model. Documents arrive in megabytes. That is why the tools accept a file path or URL, never base64 content. The binary goes from disk to the API through the server, and the model only sees the path and, later, the report. The packaging post covers that decision and the security counterpart it demands.
What a server exposes: tools, resources and prompts
The protocol defines three primitives, and the distinction matters at design time:
- Tools: actions the model decides to invoke, such as “inspect this document” or “compare these two files”. They are the main primitive for packaging an API.
- Resources: data the application attaches as context, such as a file, a record or an existing report. Not the model acting; context made available.
- Prompts: interaction templates the user picks explicitly (“analyze contract”), pre-filling instructions that orchestrate tools and resources.
tamperlens-mcp exposes only tools: inspect a document, run the cheap intake triage, check whether a redaction did remove the text, compare a candidate against the original. The rule we use: an agent tool is not a mirror of your API. The API has more routes than the server exposes. Every extra tool is one more decision the model can get wrong, and one more description competing for attention in the context. The design question is “what would an agent do with this in a real flow?”, not “which endpoints exist?“.
stdio or HTTP: where does the server run?
The protocol defines two transports, and the choice defines the install experience:
- stdio, the process’s standard input and output. The client spawns the server as a local process and talks over stdin/stdout. That is the
npx tamperlens-mcpmode: nothing to host, and the server runs on the user’s machine, with access to their local files. - HTTP (streamable): the server is a remote service, and the client connects over the network. It makes sense for multi-user servers, central state, or servers that never need to touch anyone’s disk.
For our case the choice was stdio, and the reason is the flow: the document the user wants verified lives on their disk. A local server reads the file and sends it to the API. A remote server would require uploading it somewhere first, recreating the friction MCP was supposed to remove. The general rule: if the tool needs to reach something that only exists on the user’s machine, stdio; if the state lives in your backend, HTTP.
What changes about security when the server touches disk and network?
“A local process reads whatever file the model names” is a sentence that should raise the hair on your neck. This is where a serious MCP server separates itself from a weekend wrapper. In tamperlens-mcp, three guards are part of the product, not a footnote:
- A directory allowlist, the list of what may be read. The server only reads inside the directories the operator listed in
TAMPERLENS_ALLOWED_DIRS. Without it, a confused agent, or a prompt injected into a document, could ask for an inspection of~/.ssh/id_rsa. - Symlinks resolved before deciding. A symbolic link inside an allowed directory pointing outside of it is resolved and refused. An allowlist without symlink resolution is a locked door with the key hanging in the lock.
- An anti-SSRF guard on URLs, against forged requests into the internal network. Remote origins are validated, and the protocol is re-checked at every redirect hop. A server that downloads whatever it is told to is a proxy into your internal network waiting to happen.
And there is a symmetric risk on the content side. The inspected document may contain text written to be read by the model, not by humans: prompt injection inside the file. It is one of the signals the analysis itself reports. And it is a reminder that a reliable agent is a property of the whole system, not of the protocol. MCP standardizes the conversation, but it does not make it safe on its own.
When does your API need an MCP server?
The honest read, which we kept after publishing: MCP registries are not an acquisition channel. Today, nobody discovers a new product by browsing a registry. The real value sits elsewhere:
- A procurement answer, meaning an answer for whoever evaluates the purchase. When someone evaluating the product asks “can our agents use this?”, the answer is a one-line command with an official listing. Not an “it can be integrated”.
- A surface for people who converse. The REST API remains the interface for people who program. The MCP server is the interface for people who operate through agents. Different audiences of the same product.
- Low, well-bounded cost. Packaging an existing API as a server with a few tools is a small project, as long as the API already handles authentication, quotas and limits.
tamperlens-mcpworks without a key because it inherits the anonymous allowance the API already imposed on the free checker.
If your API has no flow an agent would run end to end, the server can wait. If it does, the protocol is the easy part. The hard decisions are the packaging and security ones above.
The summary you take home
- MCP server = standardized capabilities for models: JSON-RPC 2.0, a handshake → discovery → choice → invocation → response cycle.
- The tool description is the interface. Write it so the model decides correctly, including what the tool does not do.
- Tools act, resources contextualize, prompts orchestrate. And fewer tools decide better than many.
- stdio to reach the user’s machine, HTTP for state in your backend.
- A server that touches disk or network has confinement as its main feature: allowlist, resolved symlinks, SSRF guard.
- Calibrated expectations: package it as a procurement answer and an agent surface, not as an acquisition funnel.
Need a custom technical project?
Architecture, TypeScript, APIs and automation, from prototype to production. The person answering your email is the one writing the code, and the deadline I promise is the one I can meet.
Send me a message →