Lab Notebook: Choosing stdio over SSE for Embedded MCP Services
The context
I had been building agent tools as flat Python scripts for a while. A script would load a prompt, call an API, and return a string. This worked for simple tasks, but it became messy as the tasks grew. I needed a pattern that would give me code organisation, credential isolation, and the ability to swap transport and client without rewriting the logic. The answer I settled on is the embedded stdio MCP service.
What "embedded stdio" means
The pattern has three pieces:
- The agent sees a thin tool script, like
execute_image_workflow.py. That script is the only entry point the LLM knows about. - The tool script spawns
run_mcp_stdio.py, which is a small stdio bridge. - The bridge starts the real MCP server, which lives in
lib/mcp_specialized_service/. The server and the bridge talk over stdin and stdout.
This keeps the LLM-facing surface tiny while the real implementation stays behind a protocol boundary.
Workspace layout
A typical workspace looks like this:
workspace/
├── .agent_workspace/
│ ├── agents/
│ ├── skills/
│ ├── tools/
│ │ └── execute_complex_workflow.py # thin wrapper
│ └── lib/
│ ├── run_mcp_stdio.py # stdio entrypoint
│ └── mcp_specialized_service/ # real implementation
│ ├── __init__.py
│ ├── server.py
│ └── service_client.pyThe LLM only sees execute_complex_workflow.py. That wrapper spawns the stdio bridge and forwards the MCP session. stdout is reserved for protocol messages; stderr is captured for logs. This avoids port conflicts, firewall rules, and network overhead entirely.
Why not SSE
MCP servers can also run over HTTP with Server-Sent Events. SSE is useful when multiple clients share one long-lived service. For an agent and its local capability, however, SSE introduces failure modes I do not need.
The agent framework already runs the tool in a subprocess; replacing the tool body with an MCP stdio bridge requires no extra network stack. When the tool returns, the subprocess exits and the operating system cleans everything up. The lifetime of the capability matches the lifetime of the tool call exactly.
Credential isolation
Complex workflows need privileged credentials: database passwords, cloud tokens, or high-value API keys. If those credentials live inside the tool script the LLM can execute, a malicious or misguided prompt can leak them through environment dumps, source inspection, or creative file writes.
With the embedded MCP pattern, only the server in lib/ loads the secrets. The agent tool knows how to ask the server to perform work, but it never sees the raw credentials. The MCP protocol returns only sanitised results.
This is not just security theatre. It means I can keep a secret out of the LLM context window while still letting the agent benefit from the service that uses it.
When to use it and when to skip it
I use the pattern when the capability is one or more of:
- complex enough to warrant its own server module,
- long-running or asynchronous,
- credential-sensitive,
- intended to be reused across multiple agent frameworks or MCP clients.
It is overkill for a one-line string transformation. In those cases a direct tool script is simpler and easier to debug.
The main debugging cost is stdout discipline. Any stray print in the MCP server corrupts the protocol. I direct all logs to stderr and test the subprocess boundary separately from the business logic.
Conclusion
For local agent capabilities that must stay secure and self-contained, embedded stdio MCP has become my default approach. It gives me the organisation of a service, the isolation of a subprocess, and the portability of a standard protocol, all without adding a network layer.