technical case study

Building three MCP servers

I built a local Microsoft 365 assistant, a remote multiplayer game surface and a private signal layer for agents. They all use the Model Context Protocol and they have almost nothing else in common. The protocol was the easy part. Identity, authority, state, recovery, and working out what a model should actually see - that is where the engineering was.

Model Context Protocol OAuth 2.1 Cloudflare Workers Durable Objects Python and TypeScript
01

where I started

My first MCP project was Office Assistant, a Python server that lets an agent work with a Microsoft 365 calendar. Recent coding harnesses and first-party integrations now do much of that out of the box. I wouldn't build the same general-purpose assistant again and pretend the tool list was a product.

It was still worth doing, because it made the useful lesson obvious early. Registering a function as an MCP tool takes minutes. Making it safe to point at somebody's real calendar, a live multiplayer game or a private collection of commercial signals took most of the effort, and nearly all of that went into the domain model and the trust boundary around it.

MCP is an open protocol for connecting language-model applications to context and capabilities. Its server-side primitives are tools, resources and prompts. The common vocabulary is worth having. It doesn't decide what the model sees, what it may change, whose identity it acts under, or what happens when half an operation succeeds. You still build all of that yourself.

02

the three systems

  • Office AssistantA local stdio server using delegated Microsoft Graph access. One person, one machine, one external API, and tools that can change a real calendar.
  • Delta-VA public remote server for AI agents playing a simultaneous-turn strategy game. Many untrusted clients, hidden information, live shared state and actions that race each other.
  • AntennaA hosted and local personal signal layer. Private owner-scoped data, source rights and freshness, cautious mutations, and a browser interface sharing policy with the MCP surface.

Going through them in that order changed how I think about MCP. Office Assistant is mostly an adapter around an established service. Delta-V exposes a machine-facing version of a product designed for humans. Antenna treats trustworthy context as the product itself. The same protocol sits at the edge of all three, and it accounts for very little of what any of them needed.

03

Office Assistant: calendar tools over Microsoft Graph

Office Assistant exposes ten tools for profiles, calendars, events, availability and rooms. It uses the official Python MCP SDK and FastMCP, with an asynchronous Microsoft Graph client held for the server lifespan. The public repository has 179 collected tests, strict type checking, linting and a coverage gate. Almost none of the difficulty is in the tool decorators; it's in what has to happen either side of a Graph request.

  • AuthenticationMSAL device-code login, a local token cache with restrictive file permissions, and different scopes for personal and organisational accounts.
  • RecoveryRate limits and transient failures honour Retry-After where Graph supplies it, then use bounded exponential backoff.
  • Error meaningAn expired login is not the same problem as a forbidden room directory or an invalid event. The server turns Graph failures into errors an agent can act on.
  • TransportThe MCP protocol owns standard output, so an interactive login cannot casually print prompts there or block a tool call indefinitely.

The authentication point is easy to underestimate. A command-line application can stop and ask the user to visit a URL. An MCP server is speaking JSON-RPC over that same output stream, so a human prompt written into it corrupts the transport. Office Assistant reports a typed authentication-required condition instead, and lets the host bring the person back into the loop.

The Graph client also distinguishes a permission error from a token that can be refreshed. Retrying every 403 is wrong, and so is treating every 403 as permanent. The surrounding code records the Graph error code, a safe message, request identifiers and retry guidance, without handing raw upstream responses to the model.

I wouldn't try to sell Office Assistant today. Calendar operations are becoming a standard harness capability, and Microsoft is the natural provider of the broad integration. I'd go back to it only for a narrow workflow with business rules the generic tools know nothing about. It's still public evidence that I can put delegated OAuth, a large API and an agent transport together and have the result hold up.

04

Delta-V: a remote game server for agents

Delta-V was a different problem. It's a simultaneous-turn strategy game with fog of war. Human players use a browser and WebSockets. An AI player needs a compact observation, legal candidate actions, a cheap way to wait, protection against stale turns, and an identity that doesn't hand it the credentials the browser game uses.

The public MCP design document describes three transports: local stdio, local HTTP for development and hosted Streamable HTTP on Cloudflare. The hosted endpoint is stateless between requests. A Durable Object owns each live game and session, so reconnecting to the MCP transport doesn't invent a second source of truth.

  • Agent identityOAuth 2.1 for compatible remote clients, or a short-lived signed agent token for clients that need an explicit setup path.
  • Match capabilityAn opaque per-match token is bound to the agent identity. The model never needs the raw game code or the browser player's credential.
  • ObservationOne canonical player-filtered contract is shared by the bridge and MCP adapter, including legal candidates and optional tactical or spatial detail.
  • Action safetyExpected turn, expected phase and idempotency keys protect against replay and stale decisions. A dry-run tool validates before committing.
  • WaitingA bounded server-side long poll waits for the next turn, so the agent isn't burning calls and tokens asking whether anything has happened yet.

Keeping the agent identity separate from its match capability is the part I'd defend hardest. Authentication answers who is calling; the match token answers which seat in which game that caller may operate. Binding the second to the first means a leaked match token is no use to anybody else, and the low-level player credential never reaches model context at all.

Diagram of the Delta-V agent token model. An OAuth 2.1 path and a manual agent-token path both lead to a bearer token on the MCP endpoint, which is exchanged for an opaque per-match token. The raw player token is held server-side only and never appears in tool arguments or prompts.
Delta-V's identity model, from the repository documentation. Both identity paths end in an opaque per-match token, and the raw player credential never reaches model context.

The observation builder mattered more than the transport. Sending the whole game object would have been easy, and it would have leaked hidden state and left every agent reverse-engineering my internal structures. Delta-V builds an observation for one player instead. The game rules generate the candidate actions, so the model isn't inventing moves, and a recommended index can ride along without taking the choice away. The implementation is public in the shared observation builder.

Simultaneous turns also mean an apparently valid action can become stale while an agent is thinking. The action tool fills in the current phase and turn guards, attaches an idempotency key, and can wait for both the action result and the next observation. A rejected move is a normal game outcome, not an MCP transport failure. That distinction gives the model a sensible recovery path.

I built a separate unrated sandbox so agent experiments don't pollute human matchmaking or public ratings. The repository includes a sandbox smoke test and a six-agent harness. Building those taught me that a set of valid tool schemas tells you almost nothing. What I wanted to know was whether several agents could join, act, recover from a rejection and actually finish a game under the same timing and visibility rules as the live system.

05

Antenna: a personal signal layer

Antenna started from a problem I kept hitting while using agents: the model could search for facts, but it had no small, maintained set of the signals I actually use to make decisions. A fresh exchange rate, a product-usage trend and a manually curated commercial constraint don't share a source, a freshness or a set of sharing rights. Flatten them into a prompt and you lose the information that made them worth trusting.

Antenna is a personal signal layer, and the source is public. The browser is for curation and inspection. The MCP server gives an agent collections, individual signals, histories, templates and a morning-brief prompt. The read tools are deliberately dull. The writes needed more thought.

An agent may propose a new signal, but proposal and confirmation are separate operations. Confirmation fills in approved configuration; the Worker then resolves connector authority and source policy again on the server. The browser or the model can't simply assert that a private source is public, or turn an unapproved URL into an authorised connector. Removing and reordering signals are owner-scoped, and reordering must name every current signal exactly once.

Antenna system overview diagram. A Preact browser application and MCP clients both talk to one Cloudflare Worker hosting a Hono API, Better Auth, a planner, source policy, a cron dispatcher and connector adapters, backed by D1, R2 and a Durable Object, with external data APIs on the right.
Antenna's system overview, from the public repository. One Worker owns authentication, source policy, persistence, planning and connector dispatch for the browser and the MCP surface alike.
  • One backendThe web application and MCP tools call the same Worker policy and persistence layer. The protocol is a thin boundary, not a parallel implementation.
  • Source policySignals carry source, freshness, status and sharing constraints. Public and shared access fail closed when rights are uncertain.
  • Human approvalRisky changes use propose and confirm rather than hiding consent inside a broad mutation tool.
  • Hosted authOAuth 2.1 with PKCE, audience-bound access tokens, revocable grants and refresh-token rotation protects the remote endpoint.
  • Deployment realityThe Cloudflare endpoint uses JSON responses rather than a long-lived SSE stream, and self-dispatch avoids a Worker fetching its own public hostname.

The last point came from operating the thing, not reading a quick-start. A long-lived idle stream is a poor fit for this deployment path, and a Worker making a network request back to itself can turn into an opaque platform timeout. The hosted route instead uses Streamable HTTP requests with JSON responses and dispatches shared logic internally. Local stdio and hosted HTTP construct the same server and register the same capabilities.

Antenna also sorted out the difference between an MCP server and an agent skill. MCP exposes capabilities and data; the skill says how to use them - read before writing, check source and freshness, propose, get approval, then confirm. All of that is guidance, and a prompt is not an access-control system, so the server validates every write regardless.

06

designing the tool surface

The easiest way to make an MCP server is to turn every API endpoint into a tool, and it's usually the wrong interface. An API is organised around the implementation. What an agent needs is a smaller set of operations organised around outcomes, carrying enough context to choose safely and enough structure to recover when the outcome isn't available.

Office Assistant combines the Graph calls needed to find a meeting time, so the model never has to reproduce Graph's query language. Delta-V returns legal candidates alongside the observation instead of exposing an arbitrary write to game state. Antenna hands over collections and histories already assembled. Cloudflare's MCP guidance and Anthropic's advice on writing tools for agents both land in the same place: focused, goal-shaped tools tend to beat a mechanical copy of a complete API.

It's also why the generic calendar surface got commoditised and the other two didn't. A provider can ship a standard adapter to everyone, but Delta-V's hidden-state observation model and Antenna's source and approval policy only mean anything inside those products. The protocol makes capabilities portable; it doesn't do the domain work for you.

07

error handling for agents

An error message for an agent should answer three questions: what happened, is retrying sensible, and what can be changed before the next attempt? Raw exceptions rarely do that.

  • Retry laterA Graph rate limit or a Delta-V wait timeout can be retried, ideally after the server-supplied delay.
  • Refresh contextA stale game turn means observe again. Retrying the same action unchanged is not useful.
  • Ask the personAn expired login or an Antenna proposal requiring confirmation needs a human step, not a more determined loop.
  • StopA real permission denial, invalid source policy or closed match is terminal for that operation.

That distinction belongs in the tool results themselves, and in the tests. Without it an agent turns a recoverable domain event into a failed session, or hammers a permanent denial until something gives.

08

where state lives

Local stdio makes it tempting to keep everything in the server process. That's fine for a single-user adapter whose durable state lives in Microsoft Graph. It breaks down as soon as the endpoint is remote, requests land on different isolates, and clients reconnect.

Delta-V keeps game and session state in Durable Objects, including bounded event buffers for each seat. Antenna keeps collections, grants and source configuration in its application storage. In both cases the MCP endpoint can be restarted without losing anything. A transport session is useful for negotiation and delivery, and it shouldn't quietly become the only copy of the data.

I now start remote MCP design by writing down four identities separately: the person or agent, the OAuth client, the transport session and the domain capability being exercised. If those collapse into one token or one in-memory object, reconnects, revocation and auditing become much harder than they need to be.

09

designing the observation

A language model can read JSON, which doesn't make every JSON object a good observation. The view you want is smaller than the database and more explicit than the UI state, and it's shaped around the decision the agent is about to make.

For Antenna that means attaching the source, update time, status and history that qualify a value. For Office Assistant it means stable event identifiers and normalised times rather than the incidental layout of an upstream response. For Delta-V, an abbreviated compact observation looks like this:

delta_v_get_observation, compactState: true
{
  "version": 1,
  "gameCode": "QK7M",
  "playerId": 0,
  "state": { "turnNumber": 6, "phase": "astrogation", "activePlayer": 0 },
  "candidates": [
    { "type": "astrogation",
      "orders": [{ "shipId": "p0s0", "burn": 2, "overload": null }] },
    { "type": "astrogation",
      "orders": [{ "shipId": "p0s0", "burn": 0, "overload": null }] }
  ],
  "recommendedIndex": 0,
  "summary": "Turn 6, Phase: astrogation\nActive player: YOU\nYOUR SHIPS: ...\nENEMY SHIPS: ... (undetected)\nCANDIDATES: ..."
}
The shape produced by the shared observation builder. Candidates come from the game rules, the summary is filtered for fog of war, and the full authoritative state can be replaced with the three guard fields to save tokens.

Token cost is part of it, but correctness is the bigger part: a compact observation leaves the model less room to misunderstand the system. The same contract then drives the tests, the evaluation harness and any other agent transport.

10

what I would do differently

  • Office AssistantStart with one valuable organisational workflow, not a general calendar surface. Use provider tools where they are already good enough and put effort into the business-specific decision.
  • Delta-VDesign the canonical agent observation and hosted identity model earlier. Local stdio is useful, but it can conceal assumptions that fail as soon as the server is remote and multi-user.
  • AntennaDefine read-only and write scopes before inviting external users, and make the smallest useful sample collection part of first-run setup rather than relying on private dogfooding data.

Across all three I'd add behavioural evaluations earlier. Unit tests prove that handlers validate their inputs and map their outputs. They say nothing about whether an agent picks the right tool, notices a stale observation, stops at an approval boundary, or gets through the task without a pile of unnecessary calls.

11

Antenna is open source

The source is public at tre-systems/antenna-public under the MIT licence, released for self-hosting and experimentation. Publishing code doesn't create a business. I published it because it's the strongest single demonstration of everything above.

It combines a Preact application, a Cloudflare Worker, D1 and R2 persistence, OAuth 2.1, local and hosted MCP transports, connector policy, source rights, scheduled refreshes and human-approved writes. Publishing the implementation lets an employer or a contributor check claims a screenshot can't. It also makes the hosted service easier to trust: the code is open, and convenience, managed connectors and running the thing are what the service actually sells.

The public repository is not the private one with its visibility flipped. It carries a licence, a security policy, contribution guidance, a code of conduct, example environment files and a documented local first-run path, with my personal deployment assumptions stripped out. The tests and architecture documents are part of the release.

There's still a commercial caveat. Source availability isn't a moat, though neither is keeping an unknown application private. What's defensible is trusted connectors, a well-maintained source registry, operational reliability and the judgement in how signals get curated. Open source helps with employment, consulting and adoption. It doesn't find you people who have the problem.

12

what this work demonstrates

The common thread isn't that I can add an MCP dependency. It's that I can take an agent integration through the parts that start after the tutorial ends:

  • turn an existing API into a smaller outcome-oriented tool surface;
  • design a machine-facing observation for a product with hidden and changing state;
  • separate authentication, transport sessions and domain capabilities;
  • put human approval around consequential writes without trusting the prompt to enforce it;
  • carry source, freshness and rights information with context rather than presenting every value as equal;
  • map external and domain failures into recovery instructions an agent can use;
  • keep durable truth behind a stateless remote transport;
  • test the protocol surface and the behaviour of several agents using it.

Those are ordinary distributed-systems, security and product-design problems wearing a new interface, which is most of why the work interests me. Models and harnesses will keep improving and a lot of generic tools will end up built in. The part that stays hard is giving an agent exactly enough authority and context to do something useful in a real system, and being able to explain what happened when it goes wrong.

13

source and further reading

Working on a difficult agent integration?

I'm interested in hands-on technical leadership and engineering work where models have to operate against real systems, real permissions and real users.

last updated: 2026-07