Lazy MCP Proxy
A client-agnostic proxy that converts normal MCP servers to use a lazy-loading pattern, dramatically reducing initial context usage by 90%+ and enabling support for hundreds of commands. It works with any MCP client — Claude Desktop, OpenCode, Cursor, VS Code, and more.
Table of Contents
- Table of Contents
- Features
- How It Works
- Known Issue
- Installation
- Usage
- Integration (Claude Desktop, OpenCode, Cursor, VS Code, and more)
- Example
- Development
- Releases
- Configuration Reference
- Benefits
- Documentation
Features
- Client-Agnostic: Works with any MCP client — Claude Desktop, OpenCode, Cursor, VS Code, and any other MCP-compatible tool. Unlike client-specific solutions, lazy-mcp works everywhere you do.
- Multi-Server Aggregation: Aggregate multiple MCP servers with on-demand discovery
- Lazy Loading: Only discover tools when needed, not upfront
- Batch Discovery: Discover multiple servers in one call
- 90%+ Context Reduction: From ~16K to ~1.5K tokens initially
- Built-in OAuth 2.0 + PKCE: Authenticate with OAuth-protected remote servers without a browser — works in sandboxed agent environments
- Background Health Monitoring: Probes all servers on startup and periodically;
list_serversshows accurate health from the first call - Hot Config Reload: Send
SIGHUPto reload config without restarting — add, remove, or update servers on the fly - Streamable HTTP Transport: Run as an HTTP server — expose lazy-mcp over the network so remote clients can connect via
POST /mcp
How It Works
Aggregates multiple MCP servers and exposes four meta-tools:
list_servers- Lists all configured MCP servers with health status. Response includespidandconfig_fileso an agent can fix broken config and reload viakill -HUP <pid>list_commands- Discovers tools from specific server(s), supports batch discoverydescribe_commands- Gets detailed schemas from a serverinvoke_command- Executes commands from a specific server
Calling invoke_command
invoke_command is a wrapper meta-tool. Its input should contain only server, command_name, and an optional parameters object. All downstream command inputs must be nested inside parameters.
Correct:
Incorrect:
Known Issue
Some weaker LLM models flatten invoke_command inputs and place downstream tool fields beside parameters instead of nesting them inside parameters. This causes invalid requests, repeated retries, and unnecessary token usage.
If your client supports custom instructions, add a hint like:
When calling lazy-mcp's invoke_command tool:
- put only server and command_name at the top level
- put all downstream tool inputs inside parameters
- never place downstream tool fields beside parameters
Installation
Homebrew (macOS and Linux, no runtime dependencies):
Cargo (if you have Rust installed, no Node.js required):
If you have Python / uv (no Node.js required):
If you have Node.js — use npx to always get the latest version:
Or install globally (locks to specific version):
Docker / Podman:
The image compiles the TypeScript CLI during docker build, so this works from a clean checkout without a prebuilt dist/ directory.
Or with Podman:
This runs lazy-mcp in HTTP mode on port 8080 with config mounted from ~/.config/lazy-mcp/servers.json. See docker-compose.yml for configuration options.
Usage
Create a configuration file at ~/.config/lazy-mcp/servers.json:
Then run:
# Using npx (recommended - always latest version)
# Or via environment variable
LAZY_MCP_CONFIG=/.config/lazy-mcp/servers.json
# Or if installed globally
Config directory location (XDG Base Directory)
The config directory defaults to ~/.config/lazy-mcp/, but lazy-mcp honours the
XDG Base Directory Specification:
if $XDG_CONFIG_HOME is set, lazy-mcp uses $XDG_CONFIG_HOME/lazy-mcp/
instead. This applies to:
- the default
servers.jsonlocation, - the OAuth token store (
tokens.json,client-info.json, …), - the OAuth callback PID lock files.
This makes it easy to scope tokens per project (e.g. via direnv or mise)
without having to override $HOME:
# In a project's .envrc / mise.toml:
With that set, lazy-mcp will read ./.config/lazy-mcp/servers.json and store
tokens under ./.config/lazy-mcp/ — completely isolated from your global
lazy-mcp state.
Streamable HTTP Transport
By default, lazy-mcp communicates over stdio. You can also run it as an HTTP server so remote clients can connect over the network:
See doc/HTTP_TRANSPORT.md for full configuration, security guidance (DNS rebinding protection, payload limits, bearer auth), and reverse-proxy setup.
Integration (Claude Desktop, OpenCode, Cursor, VS Code, and more)
Replace multiple MCP server entries in your client with one aggregated lazy-mcp proxy:
All downstream MCP servers live in ~/.config/lazy-mcp/servers.json. Result: ~90% context reduction (from ~16K to ~1.5K tokens initially).
See doc/INTEGRATION.md for before/after examples and HTTP-mode client configuration.
Example
# Configure multiple MCP servers in servers.json, then:
# Exposes: list_servers, list_commands, describe_commands, invoke_command (4 meta-tools)
# Instead of loading all tools from all servers upfront (~16K+ tokens),
# the agent discovers tools on-demand (~1.5K tokens initially)
Development
Running from Local Source
Instead of npx lazy-mcp@latest (which downloads the published package), you can run directly from the cloned repo:
Without building — using ts-node (picks up source changes immediately):
After building — run the compiled output:
# or equivalently:
In an MCP client config — point directly at the local build:
Or with ts-node (no build needed, always reflects latest source):
Releases
Releases are fully automated via semantic-release on every push to main. CI analyzes Conventional Commits, bumps the version, tags, and publishes to npm, PyPI, and crates.io.
See doc/RELEASES.md for the full release pipeline and the required CI/CD variables (GITLAB_RELEASE_TOKEN, NPM_TOKEN, PYPI_TOKEN, CARGO_TOKEN).
Configuration Reference
lazy-mcp reads its configuration from ~/.config/lazy-mcp/servers.json (or --config <path>). At minimum each server needs name, description, and either command (local) or url (remote).
Common top-level blocks:
servers[]— list of MCP servers to aggregate (required)permissions— global and per-server allow/deny rules forinvoke_command(experimental)transport— switch from stdio to HTTP, set port, bind host, bearer auth, etc.logging— structured stderr logging (level, format, body dumps, redaction)healthMonitor— background health probes (activity-driven by default)embedServerSummaries— opt-in: embed configured server names/descriptions inlist_serversdescriptionrequestTimeout— per-server request timeout in ms
You can also expand secrets in any string value with ${VAR} (env var) or {file:/path/to/secret} (file-based, owner-only 0600 recommended).
Send SIGHUP to reload the config without restarting (kill -HUP <pid> — the PID is in list_servers).
For the full reference — every field, OAuth flow, permission rule semantics, glob syntax, HTTP transport security, logging knobs, health-monitor tuning, and SIGHUP reload semantics — see doc/CONFIGURATION.md.
Benefits
- 90%+ context reduction - From ~16K to ~1.5K tokens initially
- Progressive tool discovery - Only load schemas when needed
- Multi-server aggregation - Manage multiple MCP servers in one config
- Batch discovery - Discover multiple servers efficiently
- Scales to hundreds of commands without context bloat
- Flexible configuration - Enable/disable servers on demand
- Environment variable support - Secure credential management via
${VAR}and{file:...}notations - Both local and remote - Support for subprocess and HTTP servers
- Streamable HTTP transport - Run as an HTTP server for remote client access
- Health monitoring - Background probes detect broken servers before you hit them
Documentation
- doc/CONFIGURATION.md - Full configuration reference (servers, permissions, OAuth, transport, logging, health monitoring, SIGHUP reload)
- doc/HTTP_TRANSPORT.md - Streamable HTTP transport setup, security, and reverse-proxy guidance
- doc/INTEGRATION.md - Client integration examples (Claude Desktop, OpenCode, Cursor, VS Code) for stdio and HTTP modes
- doc/RELEASES.md - Release pipeline and required CI/CD variables
- doc/ARCHITECTURE.md - Architecture overview and design patterns
- doc/CONTRIBUTING.md - Contributing guide with common development tasks
- doc/requests/ - Bruno API collection for testing the Streamable HTTP transport. Open the
doc/requests/folder as a collection in Bruno, select thelocalorlocal-with-authenvironment, and run requests against a locally runninglazy-mcp --transport httpinstance. - CHANGELOG.md - Version history and release notes
- AGENTS.md - Development guide for AI coding agents (build commands, code style, testing patterns)