My MCP Server Holds Two API Keys. Every Tool Call Runs in the Same Process as Both. A developer discovered that their single FastMCP server holding both GitHub and DEV.to API keys exposes both credentials to every tool call, creating a security risk where untrusted inputs could access write-capable credentials. The server's 8 tools run in one process with both keys in the environment, meaning any tool—even one that doesn't need a credential—could potentially misuse it. The fix is to split the server into separate processes along credential boundaries. I read a post this week where someone connected three MCP servers to one agent and watched it casually request the same access it'd need to hit production. The comment thread was full of "yeah, that's the whole problem with MCP" takes, and I almost scrolled past it — I don't run three servers, I run one. Then I actually opened server.py to check, and realized my one server has the exact same shape of problem, just folded into a single file instead of spread across three. server.py is a FastMCP server with 8 tools split across two unrelated jobs: GitHub profile/repo reads, and DEV.to article reads and writes. Both credentials get loaded the same way, at import time, into the same process environment: python def load env path=".env" : try: with open path as f: for line in f: line = line.strip if line and not line.startswith " " and "=" in line: k, v = line.split "=", 1 os.environ.setdefault k, v except FileNotFoundError: pass load env and two helper functions read them back out: python def gh path, method="GET", data=None : req = urllib.request.Request f"https://api.github.com{path}", method=method req.add header "Authorization", f"token {os.environ 'GITHUB TOKEN' }" ... def dev path, method="GET", data=None : req = urllib.request.Request f"https://dev.to/api{path}", method=method req.add header "api-key", os.environ "DEV TO API" ... Nothing here is a bug in the sense of "wrong output for some input." Every tool does exactly what it says: get github profile reads GitHub, create article writes to DEV.to. The problem is one level up, in what the process boundary actually protects. I'd been thinking of GITHUB TOKEN and DEV TO API as belonging to different tools , scoped by which function reads them. They don't. They belong to the process . Every one of those 8 tools runs with both credentials sitting in its environment, whether the tool needs one, the other, or neither. generate commit message doesn't touch either API — it shells out to claude -p on a git diff — but it runs in a process that could just as easily reach os.environ "DEV TO API" if a future edit to that function, or a bug in it, ever needed a string from somewhere and grabbed the wrong one. That's the same failure mode as three separate MCP servers wired into one agent — the agent's session becomes the shared trust boundary, and every tool call inherits the union of everything reachable from it — just compressed into a single file where it's easier to miss because there's no server-to-server wire to point at. I read the code for months as "8 tools," never as "1 process holding 2 sets of write-capable credentials." The reason it matters in practice, not just in theory, is that this server's tool inputs aren't all trusted. update article article id, title=None, body markdown=None, published=None takes an arbitrary integer and arbitrary text, and the text sometimes originates from an LLM call summarizing something I fed it — a draft, a trending-topic scrape, eventually maybe a comment thread. If that pipeline ever grows a step where article content is derived from external, untrusted text someone else's dev.to comment, a scraped blog post before being handed to update article , the only thing standing between "update my own draft" and "do something I didn't intend" is that nobody has yet written a code path connecting those two things. There's no process-level wall enforcing it. The credential for one integration isn't scoped away from the tool surface of the other; it's just that no one's called them together yet. The fix isn't clever — it's the boring one nobody wants to do because it means running two processes instead of one. Split the server along credential boundaries, not along "feels like one project" boundaries: github server.py — only GITHUB TOKEN is ever loaded into this process load env mcp = FastMCP "github-tools" @mcp.tool def get github profile - dict: ... devto server.py — only DEV TO API is ever loaded into this process load env mcp = FastMCP "devto-tools" @mcp.tool def create article title: str, body markdown: str, tags: list str = None, published: bool = False - dict: ... Two .env files, two processes, two entries in the MCP client config instead of one. It's more annoying to run locally, and I haven't actually cut mine over yet — this article is me writing down the argument before I let myself talk out of it. But the property you get back is the one that actually matters: if devto-tools gets fed something malicious through a tool argument, the worst it can do is misuse DEV TO API . It cannot touch GITHUB TOKEN , because that string was never in its environment to begin with. That's a guarantee the current single-process version can't make, no matter how carefully I review each tool's implementation, because the guarantee I actually need lives at the OS process boundary, not in the Python. The "vet each MCP server before installing it" advice — the checklist I wrote about a while back — still holds, but it answers a different question than the one this raised. Vetting tells you a single server isn't doing something malicious on its own. It says nothing about what happens once two of them, or two credential domains inside one of them, end up reachable from the same agent session. That composition risk doesn't show up in any one server's source code. It only shows up when you ask "what's actually in this process's environment right now, and which of my 8 tools could theoretically reach all of it" — a question I hadn't asked about my own code until someone else's three-server post made me go check.