Skip to content

How to build an MCP server

Three JSON-RPC methods, one transport decision, tools written for the model, OAuth like a web service. What we learned building Annsa's remote server.

By Catherine Williams-TreloarSep 2026·7 min read

To build an MCP server you implement a small JSON-RPC 2.0 surface (initialize, tools/list and tools/call) over one of two transports, describe each tool with a name, a description and an input schema, and decide how clients authenticate. That is the whole protocol. Everything that makes the server worth connecting to is in the tools themselves and in the descriptions the model reads before choosing one.

This is what we learned building Annsa's server, which is remote, OAuth-protected and used from Claude Code, Cursor, Codex, ChatGPT and Windsurf. It is written for someone building their own, not a tutorial on ours. Where Annsa is mentioned it is as the worked example.

initializeversion, capabilitiestools/listwhat you offertools/callrun oneWHEN SOMETHING IS WRONGA method you do not implementJSON-RPC error -32601, inside a 200A bad or missing credentialHTTP 401, with a pointer to metadata
Fig. 01: Three methods, and where each kind of error belongs

Decide local or remote first

An MCP server can run as a process the client launches on the user's machine, or as a service at a URL. The choice sets everything else: how you ship it, how you authenticate and which clients can reach it.

Local is simpler to write and harder to distribute: every user installs it, and every update is a re-install. Remote is one deployment and one URL, works from clients that cannot launch a process (ChatGPT, for one) and needs real authentication. We went remote. The trade-offs are in what is a remote MCP server.

The three methods that matter

A client's first message is initialize. Answer with the protocol version you support, your capabilities and your server name:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": { "tools": {} },
    "serverInfo": { "name": "your-server", "version": "1.0.0" }
  }
}

Two things in that reply are easy to get wrong.

Negotiate the version. If the client asks for a version you support, echo it. If not, answer with your newest. Annsa supports 2025-06-18 and 2025-03-26 and picks that way.

Declare only what you implement. capabilities is a promise. If you say resources: {} and then fail resources/list, clients behave unpredictably. Annsa declares tools and nothing else.

Then tools/list returns your tool definitions, and tools/call runs one. That is the protocol.

Say no properly

A client may still call a method you do not implement. The correct answer is a JSON-RPC error with code -32601, method not found, inside a normal 200 response. It is not a transport-level 401 or 404.

We got this wrong once. An unauthenticated resources/list hit an auth allow-list and came back as a 401, which every client reads as "your token is bad" and starts a re-authentication loop. The fix was to answer at the protocol level, and it is the single most useful thing on this page: errors about the method go in the JSON-RPC body; errors about the credential go in the HTTP status.

Pick a transport

Two exist: stdio for local servers, and Streamable HTTP for remote ones. Streamable HTTP is POST /mcp with a JSON-RPC message in and a JSON-RPC message out. The spec also allows a server-initiated event stream on GET; you do not have to offer one. Annsa is JSON-only, no SSE, and answers a GET that asks for text/event-stream with 405 and Allow: POST, which is what the spec says to do when you have no stream.

Notifications (messages with no id) are never answered. Batches are arrays; answer each element that has an id.

Write tools for the model, not the API

The tool description is read by a language model deciding which tool to call. Write it as the question the user would ask, not as documentation of the endpoint.

Annsa's 6 tools are named in one namespace and described as questions:

ToolDescription the model reads
annsa.prioritiesWhat should I work on? Ranked customer priorities with trend data and confidence.
annsa.specThe full engineering spec for a priority, grounded in the connected repository.
annsa.askAsk anything about the feedback data — semantic search plus synthesis.
annsa.actTake action: start building, mark shipped, share back with customers, assign.
annsa.surveysHow the in-product surveys are performing.
annsa.helpHow Annsa itself works — product and how-to questions.

Fewer, broader tools beat many narrow ones. A model choosing between 6 clear verbs makes better calls than one choosing between 40 endpoints.

Authenticate like a web service

A remote server with anyone's data behind it needs OAuth, and MCP clients expect a specific shape: a 401 whose WWW-Authenticate header points at protected-resource metadata, which points at authorization-server metadata, which lists every endpoint. Dynamic client registration so a new client needs no pre-issued id. PKCE, S256 only. The full walk-through is in how to add OAuth to an MCP server.

Rate-limit as well, and say so in headers. Annsa returns RateLimit-* headers on every limited endpoint, so an agent can see it is approaching a limit rather than discovering it by being refused. Policy in the API policy.

Make it findable

Publish a server card so registries and clients can describe you without a human reading your docs. Annsa's is at /.well-known/mcp/server-card.json: name, description, the remote URL and transport, an icon and the 6 tools. Then list the server on the registries your users' clients read.

What to test

  • initialize with a version you do not support returns your newest, not an error
  • Every method you do not implement returns -32601
  • A request with no token returns 401 with a WWW-Authenticate pointer
  • A request with a bad token returns 401, and a valid token calling a missing method returns 200 with an error body
  • GET /mcp asking for an event stream returns 405 if you have no stream
  • Each tool description, read cold, tells a model when to use it

If those pass, the server connects from every client we have tried. The rest is the tools.

annsa

Feedback in. Specs out.

500 extra pieces of feedback in your first month.