https://github.com/piotrminkina/mcp-standby-proxy
Lazy-start stdio proxy for MCP servers — serves cached tool schemas instantly, starts backends only on first tools/call.
https://github.com/piotrminkina/mcp-standby-proxy
asyncio mcp model-context-protocol proxy python stdio
Last synced: about 2 months ago
JSON representation
Lazy-start stdio proxy for MCP servers — serves cached tool schemas instantly, starts backends only on first tools/call.
- Host: GitHub
- URL: https://github.com/piotrminkina/mcp-standby-proxy
- Owner: piotrminkina
- License: gpl-3.0
- Created: 2026-04-22T18:13:23.000Z (2 months ago)
- Default Branch: master
- Last Pushed: 2026-04-23T08:59:38.000Z (2 months ago)
- Last Synced: 2026-04-23T10:35:02.685Z (2 months ago)
- Topics: asyncio, mcp, model-context-protocol, proxy, python, stdio
- Language: Python
- Size: 215 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Security: SECURITY.md
Awesome Lists containing this project
README
# mcp-standby-proxy
Lightweight stdio proxy for MCP (Model Context Protocol) servers that eliminates
unnecessary backend startup when using AI agents.
Each proxy instance sits between an MCP client and a real MCP server backend. It
serves cached tool schemas instantly on startup and only starts the backend
infrastructure when the agent makes a real `tools/call` request. Backend lifecycle
is controlled via configurable shell commands — the proxy is agnostic to the
underlying runtime (containers, service managers, bare processes).
## Why
MCP clients connect to **all** registered MCP servers at startup to fetch
`tools/list`. This triggers every backend stack to start immediately — even
when the user has no intention of using those tools in the current session.
| Session start (before) | Session start (after) |
|------------------------|-----------------------|
| 15+ processes, 1.5–3 GB RAM, 30–120 s delay | 0 backend processes, ~25 MB per proxy, instant |
| All backends running regardless of need | Backends start only when the agent calls a tool |
## How it works
```
MCP Client ←stdio→ mcp-standby-proxy ←SSE/HTTP/stdio→ Real MCP Server
│
cache.json (tools/list response)
```
1. MCP client spawns proxy as a stdio subprocess
2. Client sends `tools/list` → proxy responds from local cache (no backend started)
3. Client sends `tools/call` → proxy starts the backend, waits for healthcheck, forwards the request
4. Subsequent `tools/call` requests are forwarded directly (backend already running)
5. On session end (SIGTERM / stdin EOF) → proxy stops the backend
**Result:** Zero backend processes at session start. Backends start only when needed.
## Getting Started
### Prerequisites
- [uv](https://docs.astral.sh/uv/) (manages Python versions automatically)
- A running MCP server backend to proxy (e.g., a Docker Compose stack, a systemd user service, or an `npx` stdio package)
### Installation
```bash
git clone https://github.com/piotrminkina/mcp-standby-proxy.git
cd mcp-standby-proxy
uv sync
```
### Quick Start
1. Copy the example config and edit it for your server:
```bash
cp examples/kroki.yaml my-server.yaml
# Edit my-server.yaml: set server.name, backend.url, and lifecycle commands
```
2. Run the proxy:
```bash
uv run mcp-standby-proxy serve -c my-server.yaml
```
3. Register in your MCP client config (e.g., Claude Desktop, Claude Code):
```json
{
"mcpServers": {
"my-server": {
"command": "uv",
"args": [
"run",
"--project", "/path/to/mcp-standby-proxy",
"mcp-standby-proxy", "serve",
"-c", "/path/to/my-server.yaml"
]
}
}
}
```
## Configuration
Reference configs per transport:
- [`examples/kroki.yaml`](examples/kroki.yaml) — **SSE** backend, systemd user service lifecycle
- [`examples/firecrawl.yaml`](examples/firecrawl.yaml) — **Streamable HTTP** backend, Docker Compose lifecycle
- [`examples/claude-context.yaml`](examples/claude-context.yaml) — **stdio** backend with a dependency service (Milvus) managed via `systemctl`
Each example ships with an opt-in `logging` section enabled by default
(required under Claude Code and other clients that do not persist
child-process stderr — see "File logging" below). Remove the section to
disable file logging.
Minimal schema:
```yaml
version: 1
server:
name: "my-mcp-server" # Reported in MCP initialize response
version: "1.0.0"
backend:
transport: sse # sse | streamable_http | stdio
url: "http://localhost:5090/sse" # Required for sse / streamable_http
# For stdio backends:
# command: "npx"
# args: ["@modelcontextprotocol/server-something"]
# env:
# FOO: "bar"
lifecycle:
start:
command: "systemctl"
args: ["--user", "start", "my-server.socket"]
timeout: 30 # Seconds before start command is killed
stop:
command: "systemctl"
args: ["--user", "stop", "my-server.service"]
timeout: 30
healthcheck:
type: http # http | tcp | command
url: "http://localhost:5090/sse"
interval: 1 # Seconds between polls
max_attempts: 60 # Total attempts before giving up
cache:
path: "./my_server_cache.json" # Resolved against the config file's directory
```
Full schema and path-resolution rules: [Config Spec](docs/plans/config-spec.md).
### Cold cache bootstrap
On first run (no cache file), the proxy starts the backend, fetches
`tools/list`, `resources/list`, and `prompts/list`, saves the results to the
cache file, and serves future `tools/list` requests from cache without starting
the backend.
### Streamable HTTP transport
For backends that use the newer Streamable HTTP protocol instead of SSE:
```yaml
backend:
transport: streamable_http
url: "http://localhost:5100/mcp"
```
### File logging (Claude Code and other agent-mode deployments)
Claude Code does not persist child-process stderr to disk
([anthropics/claude-code#29035](https://github.com/anthropics/claude-code/issues/29035)),
so transport errors and lifecycle events are invisible without extra setup. Add a
`logging.file` section to any config to write the same records to a local rotating
log file:
```yaml
logging:
file:
path: ".logs/kroki.log"
level: info # raise to `debug` to reproduce a specific incident
max_size: "10MB"
backup_count: 3 # max disk usage: ~40 MB (active + 3 backups)
```
**How it works:**
- Absence of the `logging` key disables file logging entirely — no files created,
no disk I/O. Stderr output is unchanged.
- File level is independent of `-v`/`-vv`: you can run stderr at `WARNING` while
the file captures `DEBUG`.
- The proxy creates intermediate directories automatically. If the path cannot be
opened (permission denied, read-only filesystem), a single warning is written to
stderr and the proxy continues with stderr-only logging.
- On startup the proxy prints the resolved log path to stderr:
`file logging enabled: path=/abs/path/to/.logs/kroki.log`.
- Rotation is size-based. `backup_count` must be ≥ 1 (setting it to `0` disables
rotation entirely — the stdlib ignores `max_size` when `backupCount=0`).
- `max_size` format: `` with no spaces.
Accepted units: `B`, `KB`, `MB`, `GB` (decimal) and `KiB`, `MiB`, `GiB` (binary).
Range: 1 KB – 10 GB.
The `logging` section is present in all three example configs under
[`examples/`](examples) — delete it to disable file logging. For the known
deferred bug that motivated this feature (`Write stream closed` on SSE
backends), see [`TODO.md`](TODO.md).
### Notes
- `idle_timeout` and `auto_refresh` are accepted in the config but ignored in
the current version. They are reserved for post-MVP features.
## CLI
| Command | Description |
|---------|-------------|
| `serve -c ` | Run the proxy (stdio transport) |
| `-v` | INFO logging to stderr |
| `-vv` | DEBUG logging to stderr |
Logs always go to stderr. Stdout is reserved exclusively for JSON-RPC traffic
to the MCP client — piping or redirecting stdout will corrupt the protocol
stream.
## Security
The YAML config file is **trusted input**. `lifecycle.start` and
`lifecycle.stop` are executed as arbitrary shell commands with the proxy
process's full privileges, and `backend.env` is merged into the child
process environment for stdio backends.
**Do not run `mcp-standby-proxy` against a config file you did not author or
review.** A hostile YAML can execute any command when the proxy starts or
stops the backend. This includes configs downloaded from third parties,
pasted from chat, or committed to repositories you don't control.
## Project Status
**MVP complete.** All three backend transports (SSE, Streamable HTTP, stdio)
are implemented and tested.
See [PRD](docs/plans/prd.md) for the full capability matrix and requirements,
[Tech Stack](docs/plans/tech-stack.md) for technology choices, and
[Tech Spec](docs/plans/tech-spec.md) for architecture details.
## Roadmap (post-MVP)
- Idle timeout: automatically stop backend after inactivity
- Cache auto-refresh: compare live vs cached on reconnect
- `warm` / `validate` CLI subcommands
- Client capability forwarding + server-to-client request forwarding
- Nuitka binary builds for zero-dependency distribution
## Contributing
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for development environment setup,
testing, and contribution workflow.
## License
Copyright (C) 2026 Piotr Minkina
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your
option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
Public License for more details.
Full license text: [`LICENSE`](LICENSE) or .
### Why GPLv3
`mcp-standby-proxy` is a developer tool that sits between an MCP client and
a backend. Both talk to the proxy through arms-length boundaries (stdin/stdout
pipes, HTTP sockets) — under established FSF interpretation, using the proxy
does NOT impose GPL on the MCP client, the backend, or anything else that
merely communicates with it. GPLv3 covers only the proxy itself and any
**derivative works** (forks, embedded copies). The intent is reciprocity: if
you improve the proxy and distribute that improved version, those improvements
stay open for everyone.