MCP Servers
I built four MCP systems: a local Microsoft 365 assistant, a remote multiplayer game, a private signal layer and a live tabletop campaign authority. 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.
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.
the four 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.
- Shadowdark DMA campaign-scoped authority for a live tabletop game. Private GM knowledge, player-specific views, optimistic mutations, OAuth pairing and real-time publication to connected character sheets.
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. Shadowdark DM uses MCP as the command surface for a live, shared world. The same protocol sits at the edge of all four, and it accounts for very little of what any of them needed.
Office Assistant: calendar tools over Microsoft Graph
Office Assistant exposes 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 a substantial test suite, 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.
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 HTTP on Cloudflare. The hosted endpoint now uses the 2026 per-request Stateless MCP model while retaining the old JSON response path for 2025-era clients. A Durable Object owns each live game and seat buffer, 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.
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.
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.
- 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 per-request Stateless MCP with JSON responses, 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 one JSON response per Stateless MCP request and dispatches shared logic internally. A compatibility route keeps older clients working, while local stdio and both HTTP eras construct the same capabilities from one factory.
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.
Shadowdark DM: authority for a live table
Shadowdark DM made the authority problem explicit. The browser is a live view for the GM and each player; MCP is the campaign command surface. More than a hundred narrow tools cover character creation, live-play vitals, encounters, light, handouts, secrets and session orchestration, but none writes storage directly. Every mutation delegates to the same campaign Durable Object used by the web application.
- Campaign scopeA host starts Connect Codex and approves one OAuth grant for one campaign. The model never receives the browser's GM or player credentials.
- Conflict safetyMutations carry expected campaign, character or presentation versions and a concise audited reason. A stale command must read and reconsider rather than replay blindly.
- AudienceGM facts, party facts and one-player facts are different payloads built by the authority. Revealing widens an audience; it never pretends that delivered knowledge can be withdrawn.
- PublicationA successful command validates, persists and journals before the authoritative update reaches the relevant connected sheets.
- OAuth bindingPKCE, rotating refresh credentials, callback issuer binding and immediate host revocation protect the remote surface.
This is where a large tool count can be justified. The alternative is not a smaller, elegant interface; it is a few broad tools with free-form patches and vague permissions. A command named character_damage_apply can own clamping, dying-state rules, versions, journalling and publication. A generic update tool would ask the model to remember all of that policy on every call.
Shadowdark also sharpened the difference between stateless transport and stateless product design. The MCP handler can be thrown away after every request because campaign state, grants, journal history and live subscriptions already have explicit owners. That is what makes Stateless MCP a good fit, rather than merely changing a protocol header.
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. Shadowdark uses named commands for rules and audience changes rather than a general campaign patch. 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 three didn't. A provider can ship a standard adapter to everyone, but Delta-V's hidden-state observation model, Antenna's source policy and Shadowdark's campaign rules only mean anything inside those products. The protocol makes capabilities portable; it doesn't do the domain work for you.
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 or campaign version means read again and reconsider. 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.
Stateless MCP and 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.
The MCP revision released on 28 July 2026 makes the remote version of that distinction explicit. In Stateless MCP, the client repeats its protocol revision, identity, capabilities and method with each request. There is no initialize handshake and no transport session identifier. A request can land on any healthy server instance.
That does not mean the product has no state. Delta-V keeps matches and seat event buffers in Durable Objects. Antenna keeps collections, grants and source configuration in application storage. Shadowdark keeps campaign state, journal history and scoped grants in its campaign authority. The MCP handlers can be restarted or distributed without losing truth because they never owned it.
All three hosted servers now accept 2026 envelope-based requests and retain a stateless 2025 compatibility path from the same tool factory. That is less glamorous than a flag-day migration, and much safer: modern clients lose a handshake and session affinity while older clients keep their existing JSON contract.
I now start remote MCP design by writing down the identities separately: the person or agent, the OAuth client, any legacy 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.
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 Shadowdark it means constructing a player-safe view instead of redacting a GM object after the fact. For Delta-V, an abbreviated compact observation looks like this:
{
"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: ..."
}
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.
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.
- Shadowdark DMStart with session orchestration and the smallest complete live-play loop before expanding the command catalog. Narrow tools are safer, but their discovery and evaluation need deliberate design once the surface becomes large.
Across all four 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.
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.
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;
- move a live endpoint to Stateless MCP without breaking older clients;
- 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.
source and further reading
- Office Assistantsource code, written in Python with FastMCP, MSAL and Microsoft Graph.
- Delta-Vsource code, MCP architecture and the live agent surface.
- Antennasource code and the live application.
- Shadowdark DMThe live application and its public host and player guide.
- ProtocolThe 28 July 2026 MCP release, SEP-2575: Stateless MCP and the current specification.
- GuidanceCloudflare's MCP guidance and Anthropic's writing tools for agents.
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-08