AI infrastructure

MCP server fleet

Three self-hosted MCP servers giving AI agents scoped, auditable access to real systems, with no ability to send anything by design.

  • Python
  • FastMCP
  • Model Context Protocol
  • OAuth 2.0
  • SSE
  • Docker
  • Traefik
  • Cloudflare Tunnel
MCP server fleet Three self-hosted Model Context Protocol servers split by business domain, reached through one authenticated edge. No server can send or publish. AI CLIENTS SERVERS · SPLIT BY DOMAIN AUTH VERSIONED READ Browser chat OAuth connector Coding agent user-scope config Telegram bots per-agent denial EDGE Tunnel + proxy token · password gate Context document store Business the lead funnel Personal calendar · inbox DB Doc store lock + audit log EXT CRM · mail drafts only, no send Authenticated edge Versioned store No send path
Every client crosses one authenticated edge, then reaches only its own domain. The reached systems are the point: the document store is versioned, and the mail integration can save a draft but has no send path to call.

Overview

A fleet of three self-hosted MCP (Model Context Protocol) servers that let AI agents work on real systems instead of guessing. One exposes a shared, versioned document store; one reaches a business funnel (self-hosted CRM, mail, mailing lists, OCR); one reaches a personal calendar and inbox. The same documents are readable from a browser chat, from a terminal coding agent, and from Telegram bots, so context crosses project boundaries instead of dying at the end of a conversation.

Problem

An agent is only useful if it can reach real data, but the obvious approach, one server with every integration, is both a security and a context problem. Every agent inherits credentials for systems its task has nothing to do with, and a single leaked token exposes everything at once. There are also no off-the-shelf connectors for a private document store or a self-hosted CRM, so the integrations had to be built.

Solution

Three servers split by business domain rather than by service type, so an agent doing one job gets exactly the tools that job needs and nothing else, and a compromised credential costs one domain instead of all of them. Each server is independently deployed and independently rotatable. Access is authenticated two ways: machine bearer tokens for programmatic clients, and an OAuth flow gated behind a human-typed password for chat connectors, because dynamic client registration is necessarily public. The same pattern extended cleanly when social publishing was added: agents got tools to queue, approve, and hold a post, while the staging and publishing steps run only from a host-side scheduler, so no tool path reaches the platform.

Architecture

Python with FastMCP, served over SSE, each server an isolated Docker Compose project reached through a tunnel and a reverse proxy. The document-store server wraps a markdown corpus with optimistic concurrency: reads return a version, writes must present it, and a mismatch is rejected with the current state so the caller can merge, plus a file lock and an audit log of who changed what. The business server wraps a self-hosted CRM, an inbox, a transactional-email provider, and vision OCR behind a single coherent tool surface, so a lead can be followed from web form to mailing list to inbox to CRM without switching servers. Consumers are browser chat connectors (OAuth), a terminal coding agent (user-scope config so no token ever lands in a repo), and Telegram bots, with per-agent tool denial for bots that must not see business or personal data.

Key decisions

Split by domain, not by service, to bound the blast radius. Nothing in the fleet can send a message or publish a post, and that is structural, not a convention: no send tool is implemented, the mail integration can only save drafts for human review, and the underlying command allowlist omits the send path entirely, so even a bug in the server cannot reach it. The bulk-email provider’s send endpoint is deliberately left unexposed for the same reason, and so is the publishing step. Optimistic concurrency on the document store because several agents write to it at once; a version guard turns a silent clobber into a mergeable conflict. Each server keeps its own OAuth token home, since a shared one would race on refresh.

Challenges

The most expensive one was a transport property nobody reads until it bites. The original streaming transport keeps per-client session state on the server, so restarting a server to ship a change orphans every connected client, and clients do not renegotiate: they keep calling against a session the server has forgotten. The failure surfaces as an invalid-parameters error naming the tool, which sends you to debug the tool. One server stayed broken for thirty-five hours behind exactly that misreading, while the same operation through the underlying command line worked fine. The fix is a stateless transport, which has no session to lose, added alongside the old one so migration is per-client and reversible. Beyond that: auth had to be raw ASGI middleware, because conventional framework middleware buffers responses and breaks the stream; the library’s DNS-rebinding protection returns an invalid-host error behind a proxy that reads exactly like an auth failure; and list-returning tools arrive as one content block per item, so first-block parsing truncates results without erroring.

Outcome

A working authenticated substrate for agentic work: shared context that survives between sessions and across tools, business systems reachable end-to-end by an agent, and security properties that hold because the capability was never built rather than because a prompt asks nicely. It is the infrastructure layer under the AI-assisted way the rest of this portfolio gets built.

Wondering if Daniel is a fit? Ask his assistant, hiring, contracts, or collaboration.