Runtype
ExploreExplainer

What an MCP server is, and when your product needs one

An MCP server offers tools, resources and prompts over JSON-RPC. What the protocol defines, what it leaves to you, and how to decide whether to build one.

Last updated 7 min read

An MCP server is a program that speaks the Model Context Protocol. It offers tools, resources and prompts over JSON-RPC 2.0, and an AI application connects to it, lists what is available, and calls a tool when its model asks for one. You build one when customers want to use your product from inside an AI tool they already have.

The explanations on offer tend to be one of two things: a specification written for people implementing SDKs, or a vendor page that assumes the decision is already made. Between them sits the question most product teams actually have, which is whether their product should publish a server at all, and what a customer would do with it once it exists. This page covers the model, a minimal exchange on the wire, the decision, and the parts the protocol deliberately leaves to you.

The three roles

MCP names three roles, and they are easy to blur. The host is the application a person is sitting in: Claude Desktop, Cursor, VS Code, or an agent your own product runs. The host creates one MCP client for each server it wants to talk to, and a client connects to exactly one server. The server is the process that answers, and it knows nothing about the model or the conversation beyond the arguments of the calls it receives.

That last point matters more than it sounds. A server never sees the user's prompt, the system prompt, or the other tools in the list. It sees a tool name, a JSON object of arguments, and whatever credential the client presented. Everything else is the host's business.

Tools, resources and prompts

A server can offer three kinds of things, distinguished by who decides to use them.

PrimitiveChosen byShapeExample
ToolThe modelName, description, JSON Schema for inputcreate_ticket, get_invoice, run_query
ResourceThe applicationA URI the host reads and may put into contextfile:///repo/README.md, a table row
PromptThe personA named template the user picks from a menu"Review this PR against our style guide"

Tools are where nearly all of the attention goes, because a tool is the only primitive that acts. Resources are read-only context the host assembles, and prompts are canned starting points a user selects by hand. A server can offer one, two or all three; plenty of useful servers offer only tools.

Traffic runs the other way too. Elicitation lets a server ask the user a question in the middle of a call, either in a form mode that collects structured data through the client or a URL mode that sends the user out of band for credentials and third-party OAuth. It is optional and declared as a client capability on every request, so a server that requires it will fail on clients that do not offer it. Sampling, where the server asked the host's model to complete something, was deprecated in the 2026-07-28 revision, which points servers at LLM provider APIs instead.

How a call runs on the wire

Two transports are defined: stdio and Streamable HTTP. Over stdio the host launches the server as a subprocess and exchanges newline-delimited JSON on stdin and stdout, which is how most local developer-tool servers run. Over Streamable HTTP the server exposes one endpoint that accepts POST requests and answers with either a JSON body or an SSE stream scoped to that request.

Revision 2026-07-28 removed protocol-level sessions from it, along with the Mcp-Session-Id header, the standalone GET stream and resumable streams. The two-endpoint HTTP with SSE transport it replaced has been deprecated since March 2025 and is still carried as deprecated rather than removed.

The lifecycle is short. Revisions through 2025-11-25 opened with an initialize handshake; 2026-07-28 replaced that with per-request metadata, so a client states its protocol version and capabilities in each request's _meta and goes straight to two methods. Listing:

{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "resultType": "complete",
    "tools": [
      {
        "name": "get_invoice",
        "description": "Fetch one invoice by id. Returns status, total in cents, and line items.",
        "inputSchema": {
          "type": "object",
          "properties": { "invoiceId": { "type": "string" } },
          "required": ["invoiceId"]
        }
      }
    ]
  }
}

Calling:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "get_invoice",
    "arguments": { "invoiceId": "inv_7c31" }
  }
}
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "resultType": "complete",
    "content": [
      { "type": "text", "text": "inv_7c31 is paid. Total 48200 cents, 3 line items." }
    ],
    "isError": false
  }
}

A result carries a content array of typed parts (text, image, audio, a resource link or an embedded resource) and an isError flag. The 2026-07-28 revision keeps both and adds structuredContent, a JSON value validated against an optional outputSchema on the tool, with the same data serialized into a text block for clients that ignore the field. A failed tool call is normally a successful JSON-RPC response with isError set, so the model can read the failure and try something else; a JSON-RPC error is reserved for protocol-level problems.

For a remote server, authorization builds on OAuth 2.1. The server must implement OAuth 2.0 Protected Resource Metadata (RFC 9728) and advertise that document through a resource_metadata parameter in the WWW-Authenticate header of its 401. Clients must discover the authorization server from it, and must send an RFC 8707 resource indicator naming the server's canonical URI on both the authorization request and the token request. A bearer token then rides every call.

What is an MCP server and do I need one

Two separate decisions hide behind one acronym: whether to publish a server, and whether to consume somebody else's. They have different answers and different work.

If this is trueDo this
Customers ask to pull your data into Claude, Cursor or an agent they builtPublish a server
Your own agent needs a capability someone else already maintainsRegister theirs as tools and move on
The only caller is your backend, on a code path you controlCall the API directly, skip the protocol
You have no per-caller identity or authorization model yetBuild that first, then revisit
Your workflow takes minutes and returns a filePublish, but as a start-and-poll pair of tools

Publish when the value is in your data or your actions and the person wanting them is somewhere you are not. A finance product whose customers keep exporting CSVs into a chat window has a clear case. Publish also when your customers are technical enough to run an agent of their own, because they will otherwise scrape the API and get the descriptions wrong.

Consume when a capability is undifferentiated and maintained by someone with more reason to keep it current than you have. Registering an existing server for a code host, an error tracker or a database costs a URL and a credential, and the alternative is writing and maintaining the same tool wrappers by hand.

Skip it when the caller is your own code. If a function is invoked at a fixed point in a deterministic path, wrapping it in a protocol designed for runtime selection by a model adds a schema, a session and a failure mode for nothing. The trade-offs there are laid out in MCP versus a plain API, and the shape for anything long-running is in exposing an internal workflow as an agent tool.

"Not yet" is a real answer. If your product has no notion of a per-customer credential with a scope, publishing a server means every caller gets whatever the server's own credential can reach, and that is an incident waiting for its first curious customer.

What the protocol does not solve

The reference SDKs make a server that lists and calls tools an afternoon of work. The gap between that and something a customer can connect to is made of four things the spec deliberately says nothing about.

Authorization stops at the token. The spec describes how a client obtains a token and presents it, and stops. Which tools a given principal may call, whether get_invoice may return an invoice from a different account, and what happens when the arguments reach outside the caller's scope are all decisions with no protocol primitive behind them. For a multi-tenant server this is the whole problem, because the argument is a string the model chose.

Nothing bounds call volume. There is no rate-limit primitive. A host's agent loop that reads an empty array as a failure will call the same tool again until the host's own turn limit stops it, and that limit belongs to the host, which is not you. A server fronting a paid API needs its own per-caller budget enforced at tools/call, returning an error the model can read rather than a silent throttle it will retry through.

Cost lands on whoever runs the compute. If a tool triggers a model call, a crawl or a metered API on your side, the caller's host pays for its own tokens and you pay for everything else. Attribution has to be built: which customer called which tool with which arguments, and what that cost.

Selection quality is a property of your tool list. Every tool's name, description and schema is loaded into the model's context on each turn, and selection accuracy falls as the list grows. Descriptions are read and obeyed by a model you do not control, which makes them both documentation and an injection surface. Sizing is covered in how many tools an agent should have, and the symptoms of a list that has grown past its usefulness are in wrong tool selection.

What shipping one actually costs

A local stdio server over an existing internal API is a day for someone who has read the SDK docs. A remote server that customers connect to is a different project: OAuth or key issuance, per-key scopes so tools/list shows a caller only what it may call, rate limits per key, argument-level tenant checks, an audit record per call, and a versioning policy for names, since renaming a tool breaks every client that cached the old list. No published survey reports a median engineering time for that work.

The part teams underestimate is that the tool list becomes a public interface with a model as its reader. You cannot patch a bad description with a system prompt, because the system prompt belongs to the customer's host. Boring, stable names and small, well-scoped tools age better than a faithful mirror of your endpoints. The operational side of running one, from key scoping to audit, is collected on the MCP server overview.

Where this gets easier

Runtype treats both directions as configuration rather than a service to build. An agent, flow or set of tools can be published as an MCP server surface, where each capability becomes a tool with a name and description you set, and keys are issued per customer with an optional per-minute rate limit and a scope that limits what tools/list returns to that caller (setting up an MCP surface). The same product's agents can register external MCP servers as tools, with secrets resolved server-side through {{secret:NAME}} references that never enter the model's context, a 30-second default tool timeout with a 60-second hard cap for MCP calls, and tool search that activates automatically past 20 tools. A team that only needs a local stdio server for its own editor should use the reference SDK and skip all of this.

Frequently asked questions

What is the difference between an MCP host, client and server?
The host is the application a person is using, such as Claude Desktop, Cursor or an agent inside your product. The host creates one MCP client per connection, and each client holds a session with exactly one server. The server is the process that answers, offering tools, resources and prompts.
Does an MCP server need a public URL?
Not always. A server that runs over stdio is launched by the host as a local subprocess and exchanges JSON on stdin and stdout, so it never listens on a port. A server that other people connect to over the network uses the Streamable HTTP transport and does need a reachable endpoint, plus a way to authenticate callers.
Is an MCP server worth building if we already have a REST API?
Only if the caller you want is a model rather than a program. A REST endpoint is called by code that already decided which endpoint it wants; an MCP tool is chosen from a list at runtime by a model reading names and descriptions. Most servers are wrappers over an existing API, and the work is in the naming, the per-caller authorization and the limits, not in the transport.