Skip to content

8 min read

Splitting one server into four fixes coupling and not context

On this page

The handover document for the engineering-operations agent listed two known issues, in the order the team had complained about them. The first was that the tool definitions consumed a large share of the context window, which the previous owner had measured at 14,200 tokens out of the 32,000 the agent was budgeted, before the system prompt and before any skills were loaded. The second was that when Slack had a 90-minute API degradation in March, the agent had also been unable to file Jira tickets, cut a GitHub branch, or check a deploy status, and nobody could explain why Slack being unwell had that effect.

Both complaints pointed at the same object: one MCP server, operated by the platform team, exposing 40 tools across Jira, GitHub, Slack and the internal deploy system. The natural reading is that the server is too big and that splitting it by domain solves both. Splitting it solves exactly one of them, and shipping the split while believing otherwise is how a team concludes that architectural work does not pay.

The March outage was a shared-fate problem

The mechanism was in the server's startup path. Several tools took a channel argument, and rather than accepting a free string the server built an enum at boot so the model would only ever name channels that existed.

python
# server/startup.py
async def build_tool_definitions() -> list[Tool]:
    channels = await slack.conversations_list(limit=1000)   # ← boot dependency
    channel_enum = [c["name"] for c in channels if not c["is_archived"]]

    return [
        *jira_tools(),
        *github_tools(),
        *deploy_tools(),
        *slack_tools(channel_enum=channel_enum),
    ]

# main.py
TOOLS = asyncio.run(build_tool_definitions())   # raises → process exits

During the degradation, conversations_list returned 503s past its retry budget, build_tool_definitions raised, and the process exited before binding its port. Kubernetes restarted it, it failed again, and after the fifth failure the deployment sat in CrashLoopBackOff. The old pods had already been rolled out an hour earlier for an unrelated release, so there was nothing serving. Every tool in every domain went away because one upstream in one domain was unavailable at one moment in the process lifecycle.

That specific bug is fixable without any architectural change — build the enum lazily, degrade to a free-string channel argument, cache the last good list — and the team did fix it in an afternoon. What is not fixable that way is the general property that produced it. One process fronting four systems has one fate: one readiness probe that decides whether all 40 tools exist, one deploy that carries changes to all four integrations at once, one memory limit that a runaway GitHub pagination loop can exhaust for everyone, and one set of upstream clients whose failure handling has to be uniformly good or the weakest one governs.

Flowchart: Agent → One server; One server → Jira; One server → GitHub; One server → Slack; One server → Deploy; Agent → Jira server; Jira server → Jira; Agent → GitHub server; GitHub server → GitHub; Agent → Slack server; Slack server → Slack; Agent → Deploy server; Deploy server → Deploy.

Splitting by domain is the direct answer to that property, and it is a real fix rather than a cosmetic one. After a split, a Slack incident removes seven tools from the agent's reach and leaves 33 working, which for this agent means the on-call flow it exists to run continues with the notification step degraded to "tell the human to post it themselves". The failure changes from total to partial, which is the only thing an availability argument is ever really about.

The context complaint does not move at all

The other complaint is about how many tool definitions are resident in the request, and the number of processes serving them is not an input to that calculation. The agent's client fetches definitions from every configured server at session start and puts all of them in the request it sends to the model. Forty definitions loaded from four servers occupy the same 14,200 tokens as forty loaded from one, plus a small amount of extra client-side plumbing.

DomainToolsDefinition tokensConsumersCredentialTokens after a 4-way split
Jira113,9003 agentsplatform-owned service account3,900
GitHub93,1003 agentsGitHub App installation token3,100
Slack72,2002 agentsbot token, chat:write2,200
Deploy135,0001 agentdeploy service role, write scope5,000
Total4014,20014,200

The rightmost column is the entire argument, and it is worth sitting with because the intuition that a smaller server means a smaller footprint is strong and wrong. A server's size affects the operator's deployment and the blast radius; the model's context is charged per definition it is given, wherever it came from. There is even a small movement in the wrong direction, since each additional server contributes its own initialize handshake and its own listing round trip at session start, which cost about 40 milliseconds each here and add a few dozen tokens of protocol scaffolding.

The per-request cost is worth stating in money because it is the part that makes the number feel real. Those 14,200 tokens are re-sent on every model call in the loop, and this agent averages 18 calls per conversation. That is 255,600 input tokens of tool definitions per conversation before anything the user said, which at $3 per million input tokens is $0.77 a conversation, against roughly 400 conversations a day. Prompt caching, where the provider keeps the unchanged prefix of a request between calls and re-reads it instead of processing it again, cuts the repeat cost substantially — cached reads bill at a tenth of the input rate, so the steady-state figure lands nearer $0.10 a conversation — and caching does nothing about the other half of the problem, which is that the definitions still occupy 44% of a 32,000-token budget that also has to hold the retention skill, the runbook the agent is following, and the transcript.

Reading the two complaints apart

The discipline that keeps this straight is asking what each complaint is a function of. Context cost is a function of how many tool definitions are resident in the request and how verbose each one is. Coupling is a function of how many independent systems share a process, a deploy, and a probe. They are related only in that a team that adds tools carelessly tends to do both at once, which is why they arrive together and get diagnosed as one thing.

The test that separates them takes ten minutes and no code. Imagine the split has shipped: four servers, four deployments, same 40 tools configured on the same agent. Ask what each complaint looks like afterwards. The March outage becomes seven unavailable tools instead of forty, so that complaint is addressed. The window still has 14,200 tokens of definitions in it, so that complaint is untouched. Any proposal that claims to fix both should survive being described that way, and a proposal that does not survive it is going to disappoint someone in six weeks.

What to change, and what to keep as a server on purpose

The split is worth doing here, and the boundaries should follow failure domains rather than the org chart, which in this case happen to coincide because each upstream is a separate system with its own availability. Four servers, each with its own deploy, its own probe, and its own upstream client, is the shape.

Two of the four deserve individual attention rather than a uniform treatment. The deploy tools are the clearest case for staying a server regardless of anything else on this page, because the credential behind them can write to production, and the reason it lives in the platform team's process is the one established in chapter 2: this agent's workload is not permitted to hold that role. Thirteen tools, one consumer, and no reuse argument at all, and it remains a server because the trust boundary is doing work that no amount of in-process tidiness replaces.

Slack is the opposite case and the one the team had not questioned. Seven tools, two consumers, and a bot token whose entire scope is chat:write to a fixed set of channels — a credential the agent process is allowed to hold, which by chapter 2's test makes the server optional rather than required. Posting a message is one HTTP call. Keeping it as a server buys a shared deployment for two consumers and costs a network hop, a process to run, and a place for the March-style failure to live. It is a reasonable candidate for an in-process tool in each of the two agents, and the reason to keep it a server anyway would be that the platform team wants one place to enforce which channels an agent may post to, which is a real reason and should be stated rather than assumed.

Jira and GitHub sit between those two and stay servers for the reason chapter 3 established rather than for a trust reason. Three agents consume each of them, and the alternative is three implementations of search_tickets and three of open_pull_request, drifting apart at the speed of whichever team last needed a new field. The GitHub App installation token is also a credential that gets meaningfully worse when it is copied: an installation token carries the App's permissions across every repository the App is installed on, so three copies in three processes is three blast radii for one grant. Neither of those arguments is about availability, which is the axis the split is being made on, and that is the point — the four boundaries are drawn by failure domain, and the decision about whether each piece is a server at all is made separately, per piece, on ownership. A team that draws the boundaries and then applies one answer to all four ends up with either a deploy server that should not exist in the agent process or a Slack server nobody needed.

Three of the 40 turned out not to belong in this conversation at all. get_deploy_runbook, get_oncall_rotation_policy and get_incident_severity_matrix were tools that fetched a document and returned it as text, and they consumed 1,100 tokens of definitions between them so that the model could decide to ask for content it needed in nearly every conversation. That is procedural knowledge behind a call, which is the inverse of the mistake in chapter 1 and just as costly: the model has to guess that the guidance exists before it can read it, and it often does not guess. Moved into the agent's skills, they cost their own text when loaded and nothing in tool definitions, and the model no longer has to discover them.

That leaves the context complaint standing at roughly 13,100 tokens after the split and the three deletions, which is progress of the kind you cannot present as a solution. The reviewer who hears the plan will also raise the objection that four deployments is more operational work than one, and that objection has a good answer that is not "it is worth it". Both of those — the window and the deployment count — are the subject of the next chapter, which works on the interface rather than on the topology.

Skill vs tool vs MCP server — read the current version at https://daviddominguez.dev/learn/en/skill-vs-tool-vs-mcp-server/splitting-one-server-into-four-fixes-coupling-and-not-context

Back to top