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, but they have almost nothing else in common. The protocol was the easy part. Identity, authority, state, recovery and giving a model the right view of a problem were where the real engineering started.

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 can now do much of that out of the box. I would not build the same general-purpose assistant again and pretend the tool list was a product.

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

MCP is an open protocol for connecting language-model applications to context and capabilities. Its server-side primitives are tools, resources and prompts. That common vocabulary is valuable, but it does not decide what the model should see, what it may change, which identity it is acting for, or how a partial failure should be recovered. Those decisions remain application engineering.

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.

This progression changed how I think about MCP. Office Assistant is mostly an adapter around an established service. Delta-V exposes a new machine-facing version of a product that was originally designed for humans. Antenna treats trustworthy context itself as the product. The same protocol sits at the edge of all three, but the important architecture is behind it.

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. The interesting part is not the decorators, it is everything that has to happen before and after 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 normally speaks JSON-RPC over its output stream. Mixing a human prompt into that stream corrupts the transport. Office Assistant therefore reports a typed authentication-required condition 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. This is the sort of unglamorous boundary code that makes a tool usable.

I would not 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 would only return to it for a narrow workflow with specific business rules that the generic tools do not understand. The project remains useful public evidence that I can integrate delegated OAuth, a complicated API and an agent transport without treating any of them as a toy.

04

Delta-V: a remote game server for agents

Delta-V was a different problem. It is a simultaneous-turn strategy game with fog of war. Human players use a browser and WebSockets. AI players need a compact observation, legal candidate actions, a way to wait efficiently, protection against stale turns, and an identity that does not expose the credentials used by the browser game.

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 does not 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. The agent does not waste calls and tokens repeatedly asking whether anything changed.

Keeping the agent identity separate from its match capability is the part I would defend hardest. Authentication answers who is calling. The match token answers which seat in which game that identity may operate. Binding the second to the first prevents a leaked match token being useful to a different caller, while still keeping low-level player credentials out of model context.

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 is more important than the transport. Sending the entire game object would have been the easy option, but it would reveal hidden state and force every agent to reverse-engineer internal structures. Delta-V instead produces a purpose-built observation filtered for one player. Candidate actions are generated by the game rules, not hallucinated by the model, and may include a recommended index without taking choice away from the agent. 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 do not pollute human matchmaking or public ratings. The repository includes a sandbox smoke test and a six-agent harness. This taught me that a collection of valid tool schemas is not an evaluation. The real test is whether several agents can join, observe, act, recover and 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 encountering while using agents: the model could search for facts, but it did not have a 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 do not all have the same source, freshness or sharing rights. Flattening them into a prompt loses the information that makes them trustworthy.

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. Read tools are deliberately unsurprising. Write tools are where the design becomes more careful.

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 model cannot 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 clarified the difference between an MCP server and an agent skill. MCP exposes capabilities and data. The skill tells an agent how to use them: read before writing, inspect source and freshness, propose a change, ask for approval, then confirm. The protocol cannot carry the whole operating policy by itself, and a prompt is not an access-control system. The server still validates every write.

06

designing the tool surface

The easiest way to make an MCP server is to convert every API endpoint into a tool. It is also usually the wrong interface. An API is organised around the implementation. An agent needs a small set of operations organised around outcomes, with enough context to choose safely and enough structure to recover when the outcome is not available.

Office Assistant combines the Graph calls needed to find a meeting time rather than asking the model to reproduce Graph's query language. Delta-V returns legal candidates with the observation rather than exposing an arbitrary write to the game state. Antenna gives collections and histories rather than making the agent stitch together rows from storage. Both Cloudflare's MCP guidance and Anthropic's advice on writing tools for agents now say the same thing: focused, goal-shaped tools generally perform better than a mechanical copy of a complete API.

It is also why the generic calendar surface was commoditised while the other two systems were not: a provider can supply a standard adapter broadly, but Delta-V's hidden-state observation model and Antenna's source and approval policy belong to their products. A protocol makes those capabilities portable; it does not make the domain work generic.

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.

This taxonomy belongs in tool results and tests. It keeps an agent from turning a recoverable domain event into a failed session, and it keeps a permanent denial from becoming an expensive retry storm.

08

where state lives

Local stdio makes it tempting to keep everything in the server process. That is fine for a single-user adapter whose durable state lives in Microsoft Graph. It breaks down for a remote endpoint where requests may land on different isolates or a client reconnects.

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 the truth. Transport sessions are useful for negotiation and delivery, but they should not quietly become the only database.

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, but that does not mean every JSON object is a good observation. The right view is smaller than the database, more explicit than the UI state and shaped around the next decision.

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.

This is partly about token cost, but mostly about correctness. A compact observation narrows the space in which the model can misunderstand the system. The same contract can then drive tests, evaluation harnesses and other agent transports.

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 would add behavioural evaluations earlier. Unit tests prove that handlers validate inputs and map outputs. They do not prove that an agent selects the right tool, recognises a stale observation, stops at an approval boundary, or completes the whole task without 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. I did not publish it because code alone creates a business, but because it is the strongest single demonstration of the lessons in this article.

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 contributor inspect claims that a screenshot cannot prove. It also makes the hosted service easier to trust: the code is open while convenience, managed connectors and operation remain services.

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 personal deployment assumptions removed. The tests and architecture documents are part of the release.

There is still a commercial caveat. Source availability is not a moat, but neither is keeping an unknown application private. The defensible parts are trusted connectors, a well-maintained source registry, operational reliability and the judgement encoded in how signals are curated. Open source can support employment, consulting and adoption. It does not remove the need to find people who have the problem.

12

what this work demonstrates

The common thread is not that I know how to add an MCP dependency. It is that I can take an agent integration through the parts that appear after the tutorial:

  • 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 concerns in a new interface. That is why the work interests me. Models and harnesses will keep improving, and many generic tools will become built in. The harder problem remains: giving an agent exactly enough authority and context to do useful work in a real system, then being able to explain what happens when it goes wrong.

13

source and further reading

Working on a difficult agent integration?

I am 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