A proxy tool that converts normal MCP servers to use lazy-loading pattern with meta-tools
Project description
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:
{
"server": "gitlab-public",
"command_name": "gitlab_list_pipelines",
"parameters": {
"project_id": "gitlab/lazy-mcp",
"ref": "feat/file-secret-expansion"
}
}
Incorrect:
{
"server": "gitlab-public",
"command_name": "gitlab_list_pipelines",
"project_id": "gitlab/lazy-mcp",
"ref": "feat/file-secret-expansion"
}
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):
brew tap gitlab-org/lazy-mcp https://gitlab.com/gitlab-org/ai/lazy-mcp
brew install lazy-mcp
Cargo (if you have Rust installed, no Node.js required):
cargo install lazy-mcp
If you have Python / uv (no Node.js required):
uvx lazy-mcp
If you have Node.js — use npx to always get the latest version:
npx lazy-mcp@latest
Or install globally (locks to specific version):
npm install -g lazy-mcp
Docker / Podman:
docker build -t lazy-mcp .
docker compose up
The image compiles the TypeScript CLI during docker build, so this works from a clean checkout without a prebuilt dist/ directory.
Or with Podman:
podman build -t lazy-mcp .
podman compose up
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:
{
"servers": [
{
"name": "chrome-devtools",
"description": "Chrome DevTools automation",
"command": ["npx", "-y", "chrome-devtools-mcp@latest"]
},
{
"name": "gitlab",
"description": "GitLab MCP server",
"url": "https://gitlab.com/api/v4/mcp"
},
{
"name": "my-remote-server",
"description": "Custom remote MCP server with static token",
"url": "https://api.example.com/mcp",
"headers": {
"Authorization": "Bearer ${API_TOKEN}"
}
},
{
"name": "glean",
"description": "Glean enterprise search (OAuth)",
"url": "https://your-company.glean.com/mcp/default"
}
]
}
Then run:
# Using npx (recommended - always latest version)
npx lazy-mcp@latest --config ~/.config/lazy-mcp/servers.json
# Or via environment variable
LAZY_MCP_CONFIG=~/.config/lazy-mcp/servers.json npx lazy-mcp@latest
# Or if installed globally
lazy-mcp --config ~/.config/lazy-mcp/servers.json
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:
lazy-mcp --config servers.json --transport http --port 3000 --auth-token "my-secret"
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:
{
"mcp": {
"lazy-mcp": {
"type": "local",
"command": ["npx", "lazy-mcp@latest", "--config", "~/.config/lazy-mcp/servers.json"],
"enabled": true
}
}
}
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:
npx lazy-mcp@latest --config ~/.config/lazy-mcp/servers.json
# 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
npm install
npm run build
npm test
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):
npm run dev -- --config ~/.config/lazy-mcp/servers.json
After building — run the compiled output:
npm run build
node dist/cli.js --config ~/.config/lazy-mcp/servers.json
# or equivalently:
npm start -- --config ~/.config/lazy-mcp/servers.json
In an MCP client config — point directly at the local build:
{
"mcp": {
"lazy-mcp": {
"command": "node",
"args": ["/path/to/lazy-mcp/dist/cli.js", "--config", "~/.config/lazy-mcp/servers.json"]
}
}
}
Or with ts-node (no build needed, always reflects latest source):
{
"mcp": {
"lazy-mcp": {
"command": "npx",
"args": ["ts-node", "/path/to/lazy-mcp/src/cli.ts", "--config", "~/.config/lazy-mcp/servers.json"]
}
}
}
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)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file lazy_mcp-2.6.2.tar.gz.
File metadata
- Download URL: lazy_mcp-2.6.2.tar.gz
- Upload date:
- Size: 279.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a4169225c84bf64900126219fc5154317c72a3835e738d042ddfc2b4882c872b
|
|
| MD5 |
62007bb38196613f80f807d065d90964
|
|
| BLAKE2b-256 |
5b3ff98cf4d00c805bcb1c1e165de6446dfbe566fa470512d330718ba555550b
|
File details
Details for the file lazy_mcp-2.6.2-py3-none-any.whl.
File metadata
- Download URL: lazy_mcp-2.6.2-py3-none-any.whl
- Upload date:
- Size: 280.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
339b78860e4c77b8feaf04a786b82775f6a0da1ba869403a1b71566358f4ceef
|
|
| MD5 |
cdbb89ea3501d508a73870f7acb33de6
|
|
| BLAKE2b-256 |
526d1ff60557c41ae09222e1cd585f07c87d161aa5b7b9b45cd8e5d95f2c8428
|