When One Process Becomes Too Much: Splitting a Pipeline into MCP Services

When One Process Becomes Too Much: Splitting a Pipeline into MCP Services

The usual way to build a multi-part pipeline is to put every part in the same process and call between them with function calls. It's the path of least resistance, and for a while it's genuinely fine until the parts stop being small enough to treat as implementation detail. Once each one is really a mini-application in its own right, with its own dependencies, its own failure modes, and its own release cadence, sharing a process with the others stops being convenient and starts being the thing that breaks first. That's the point we'd reached, and it's why we put a real boundary, an MCP server around each one instead.Table of contentsThe usual approach and where it stops workingWhy the boundary matters more than the protocolTwo servers deliberately sharing nothingCalling a tool with nothing but the SDKWiring services into an orchestratorWhat this doesn't solve yet···The usual approach and where it stops workingHere's what we had before one orchestrator class, each service instantiated inside it, everything sharing a process and a virtual environment:Three separate failure modes hide in those seven lines, and none of them are exotic. An unhandled exception in extraction takes risk scoring down with it, because there's no boundary between them, just a call stack and the pipeline doesn't degrade, it stops. A dependency bump for the fraud engine can break the extraction engine's install, because pip doesn't know or care that these are conceptually separate services; it only knows they share one environment. And shipping a bug fix to any single service means redeploying the whole application, because there's only one application to deploy, one process to restart, one blast radius that includes everything.The issues show up months after release, not in the demo, once each engine has grown enough real logic and enough real dependencies that "just import it" stops being free. The failure is usually a version pin that quietly forces a downgrade somewhere else, or a memory-heavy extraction run that starves the risk engine of resources it needs in the same process, or a one-line change to fraud detection that nobody thought to test against extraction because on paper they're unrelated. They share an address space, which is a much tighter coupling than anyone designing them separately intended.···Why the boundary matters more than the protocolThe actual fix isn't MCP specifically, it's a real process boundary around each service so a crash in one doesn't touch the others, a dependency change in one doesn't ripple into another, and each can be deployed, scaled, and restarted on its own schedule. You could get that boundary with plain REST endpoints and hand-rolled clients, and plenty of systems do exactly that, successfully. What MCP adds on top is a single, consistent way for any orchestrator, any agent, any future consumer, to discover what a service can do and call it, without writing a bespoke integration per consumer per service.That distinction is worth holding onto, because it stops you reaching for MCP out of momentum rather than justification. If a service only ever has one caller and that's never going to change, a direct internal API is simpler and a protocol layer buys you nothing. The case for MCP here is specific: an orchestrator that needs a uniform way to reach a growing number of services, calling different ones under different conditions. That's a real reason to use it.···Two servers deliberately sharing nothingWriting an MCP tool isn't the interesting part; decorate a function, type-hint the arguments, and FastMCP handles the schema. If that mechanic is new to you, it's the same three lines regardless of what the tool does. The interesting part, and the part that's easy to get wrong even once you know the syntax, is what you deliberately leave out of each server so the isolation is real instead of cosmetic.Here's the extraction service:And risk scoring:Neither file imports the other, neither has the other's dependencies installed anywhere near it. They run as two separate processes, on two separate ports, and that's not an incidental deployment detail, it's the entire reason this exists. If the risk engine needs a library upgrade next quarter, that's a change to one process, tested and shipped on its own schedule, and extraction_server.py never finds out it happened. Streamable HTTP is the transport that makes sense for this deployment model; stdio ties the server's lifetime to the process that spawned it, which works well for local clients but not for independently deployed services.What's easy to miss is that the boundary only holds if you actually respect it while writing the tools. It's tempting, once both servers exist, to have extraction_server.py import a shared utils.py that also happens to be imported by risk_server.py. Its convenient, but it quietly recreates the exact coupling you built two servers to avoid. If both need the same validation logic, that logic either gets duplicated deliberately in each service, or it becomes its own third service with its own tool. The isolation is a discipline, not a default you get for free by using MCP.···Calling a tool with nothing but the SDKBefore any orchestration framework enters the picture, it's worth seeing what a client actually does, because everything built on top of this is calling the same two methods:Discover what's available, call one of those things by name with arguments. Everything else like schemas, transports, multi-server routing exists to make that pattern work at scale, not to replace it with something more complicated.···Wiring services into an orchestratorThe orchestrator's job isn't picking one tool and calling it, it's computing a path through services, where each step's output becomes the next step's input. langchain-mcp-adapters connects to multiple servers at once, and LangGraph's StateGraph is a reasonable way to express that path explicitly:This is a fixed path: extraction always feeds risk scoring, no decision involved. That's honest for a two-service pipeline, and it's worth building this plainly before reaching for anything smarter. The moment a third or fourth service joins and the path genuinely depends on what a given document contains, add_edge stops being enough and you need add_conditional_edges, where the orchestrator decides which service to call next based on what the previous one returned.···What this doesn't solve yetThis gets you isolation and a clean way to route between services through an orchestrator that decides the order. It says nothing about what happens when that order turns out to be wrong mid-execution when a service three steps into the path discovers it's missing a field that only an earlier service could have supplied, and the orchestrator has to go back rather than forward. A fixed edge from extraction to risk scoring has no way to express that. It also says nothing about what a service failure actually looks like once it's a network call away instead of a Python exception three stack frames up, or what happens once the orchestrator is choosing between more services than a single decision can hold clearly. Those are separate, real problems, and they stay separate rather than getting squeesed into the end of this one.The problem I actually opened with is gone after using MCP. Extraction and risk scoring haven't shared a stack trace since, a dependency bump in one has never once touched the other, and shipping a fix to either no longer means redeploying both. That was true before I'd solved a single one of the harder questions above, which tells you something about how much of the original pain was really about isolation and how little of it was ever about the protocol.This is the first piece to come out of rebuilding this the right way, and it won't be the last. There's enough sitting unresolved in what I've just listed; the path that turns out wrong mid-execution, the failure that looks different over a network than it did in a stack trace, the point where an orchestrator has more services to choose from than one decision can hold cleanly, that a single article was never going to close all of it out, and I'd rather leave it open than force a tidier ending than the system actually has right now.

Original Source

Read the full article at Towardsdatascience →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.