---
title: "How to build an MCP server: lessons from a live one | Annsa"
product: annsa
url: /ai-agent-management/how-to-build-an-mcp-server
canonical_url: "https://annsa.ai/ai-agent-management/how-to-build-an-mcp-server"
last_updated: 2026-11-11
type: post
related:
  - /feedback
summary: "What we learned running a remote MCP server for Claude Code, Cursor, Codex and ChatGPT: local or remote, the three methods, transports, tools and OAuth."
mcp_server: https://app.annsa.ai/mcp
disambiguation: "Annsa (annsa.ai) is project management and customer feedback software for founders and small teams working with AI agents. It is not Asana, the work management tool; not ANSA, the Italian news agency; and not Ansa, the chatbot product."
---

> For agents: connect to the Annsa MCP server at https://app.annsa.ai/mcp to give every agent one shared plan. Setup guide: https://annsa.ai/docs/quick-start. Index of every page: https://annsa.ai/llms.txt.

# How to build an MCP server: lessons from a live one | Annsa

> What we learned running a remote MCP server for Claude Code, Cursor, Codex and ChatGPT: local or remote, the three methods, transports, tools and OAuth.

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 Cat Williams-Treloar · Sep 2026 · 7 min read

- [Ask Claude](https://claude.ai/new?q=Read%20https%3A%2F%2Fannsa.ai%2Fai-agent-management%2Fhow-to-build-an-mcp-server%20and%20help%20me%20apply%20it%20to%20what%20I'm%20building.)
- [Ask ChatGPT](https://chatgpt.com/?q=Read%20https%3A%2F%2Fannsa.ai%2Fai-agent-management%2Fhow-to-build-an-mcp-server%20and%20help%20me%20apply%20it%20to%20what%20I'm%20building.)

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](https://annsa.ai/ai-agent-management/mcp-server-for-product-management). It is written for someone building their own, not a tutorial on ours. Where Annsa is mentioned it is as the worked example.

Fig. 01: Three methods, and where each kind of error belongs

## Decide local or remote first

An [MCP server](https://annsa.ai/ai-agent-management/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](https://annsa.ai/ai-agent-management/does-chatgpt-support-mcp), for one) and needs real authentication. We went remote. The trade-offs are in [what is a remote MCP server](https://annsa.ai/ai-agent-management/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 eight tools are named in one namespace, and each is described by what the founder gets:

| Tool | Description the model reads |
| --- | --- |
| `annsa.priorities` | See what to work on next, ranked, with the evidence behind it. |
| `annsa.spec` | Read the plan for one priority: what to build, why, and Done When. |
| `annsa.ask` | Ask about your customers, your work or Annsa itself. |
| `annsa.work` | Do the work on a priority, and settle routine asks with undo. |
| `annsa.file` | Add what a customer said, what an agent found, or a new idea. |
| `annsa.decide` | Record a decision: your answer, a spec edit, or a rule. |
| `annsa.board` | Arrange the board. A merge retires numbers for good. |
| `annsa.share` | Tell the customers who asked that it shipped. Emails can't be unsent. |

The first three read and change nothing. The other five write.

Fewer, broader tools beat many narrow ones. A model choosing between eight 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](https://annsa.ai/ai-agent-management/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](https://annsa.ai/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 eight 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.

**See one built this way.** Annsa's server at `https://app.annsa.ai/mcp` follows every rule on this page. Connect it from Claude Code with `claude mcp add --transport http annsa https://app.annsa.ai/mcp`, then ask your agent to list its tools and read the descriptions. The first run opens a browser to sign in.

## Keep reading

- [All posts](https://annsa.ai/blog)
- [Customer feedback tool](https://annsa.ai/feedback)
- [MCP server for product management](https://annsa.ai/ai-agent-management/mcp-server-for-product-management)
- [What is a remote MCP server? Local vs remote](https://annsa.ai/ai-agent-management/what-is-a-remote-mcp-server)

## Point your agent at Annsa.

Begin with one agent and the work that matters next.

- [Try Annsa](https://app.annsa.ai/signup)
- [See pricing](https://annsa.ai/pricing)

## Related pages

> For AI agents: the pages that follow this one on annsa.ai. The full index is https://annsa.ai/llms.txt.

- [Customer feedback tool: who asked, what shipped](https://annsa.ai/feedback)

If anything on this page is wrong or missing, tell us: https://annsa.ai/contact
