# 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](#table-of-contents)
- [Features](#features)
- [How It Works](#how-it-works)
- [Calling `invoke_command`](#calling-invokecommand)
- [Known Issue](#known-issue)
- [Installation](#installation)
- [Usage](#usage)
- [Config directory location (XDG Base Directory)](#config-directory-location-xdg-base-directory)
- [Streamable HTTP Transport](#streamable-http-transport)
- [Integration (Claude Desktop, OpenCode, Cursor, VS Code, and more)](#integration-claude-desktop-opencode-cursor-vs-code-and-more)
- [Example](#example)
- [Development](#development)
- [Running from Local Source](#running-from-local-source)
- [Releases](#releases)
- [Configuration Reference](#configuration-reference)
- [Benefits](#benefits)
- [Documentation](#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_servers` shows accurate health from the first call
- **Hot Config Reload**: Send `SIGHUP` to 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 includes `pid` and `config_file` so an agent can fix broken config and reload via `kill -HUP <pid>`
- `list_commands` - Discovers tools from specific server(s), supports batch discovery
- `describe_commands` - Gets detailed schemas from a server
- `invoke_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:
```json
{
"server": "gitlab-public",
"command_name": "gitlab_list_pipelines",
"parameters": {
"project_id": "gitlab/lazy-mcp",
"ref": "feat/file-secret-expansion"
}
}
```
Incorrect:
```json
{
"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:
```text
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):
```bash
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):
```bash
cargo install lazy-mcp
```
**If you have Python / uv** (no Node.js required):
```bash
uvx lazy-mcp
```
**If you have Node.js** — use npx to always get the latest version:
```bash
npx lazy-mcp@latest
```
Or install globally (locks to specific version):
```bash
npm install -g lazy-mcp
```
**Docker / Podman**:
```bash
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:
```bash
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`:
```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:
```bash
# 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
```
### Config directory location (XDG Base Directory)
The config directory defaults to `~/.config/lazy-mcp/`, but lazy-mcp honours the
[XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html):
if `$XDG_CONFIG_HOME` is set, lazy-mcp uses `$XDG_CONFIG_HOME/lazy-mcp/`
instead. This applies to:
- the default `servers.json` location,
- 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`:
```bash
# In a project's .envrc / mise.toml:
export XDG_CONFIG_HOME="$PWD/.config"
```
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:
```bash
lazy-mcp --config servers.json --transport http --port 3000 --auth-token "my-secret"
```
See [doc/HTTP_TRANSPORT.md](./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:
```json
{
"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](./doc/INTEGRATION.md) for before/after examples and HTTP-mode client configuration.
## Example
```bash
# 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
```bash
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):
```bash
npm run dev -- --config ~/.config/lazy-mcp/servers.json
```
**After building** — run the compiled output:
```bash
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:
```json
{
"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):
```json
{
"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](https://semantic-release.gitbook.io/) on every push to `main`. CI analyzes [Conventional Commits](https://www.conventionalcommits.org/), bumps the version, tags, and publishes to npm, PyPI, and crates.io.
See [doc/RELEASES.md](./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 for `invoke_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 in `list_servers` description
- **`forwardPrompts`** (per-server) — opt-in: forward a server's MCP prompts through lazy-mcp's aggregated `prompts/list`/`prompts/get`
- **`requestTimeout`** — 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](./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](./doc/CONFIGURATION.md)** - Full configuration reference (servers, permissions, OAuth, transport, logging, health monitoring, SIGHUP reload)
- **[doc/HTTP_TRANSPORT.md](./doc/HTTP_TRANSPORT.md)** - Streamable HTTP transport setup, security, and reverse-proxy guidance
- **[doc/INTEGRATION.md](./doc/INTEGRATION.md)** - Client integration examples (Claude Desktop, OpenCode, Cursor, VS Code) for stdio and HTTP modes
- **[doc/RELEASES.md](./doc/RELEASES.md)** - Release pipeline and required CI/CD variables
- **[doc/ARCHITECTURE.md](./doc/ARCHITECTURE.md)** - Architecture overview and design patterns
- **[doc/CONTRIBUTING.md](./doc/CONTRIBUTING.md)** - Contributing guide with common development tasks
- **[doc/requests/](./doc/requests/)** - [Bruno](https://www.usebruno.com/) API collection for testing the Streamable HTTP transport. Open the `doc/requests/` folder as a collection in Bruno, select the `local` or `local-with-auth` environment, and run requests against a locally running `lazy-mcp --transport http` instance.
- **[CHANGELOG.md](./CHANGELOG.md)** - Version history and release notes
- **[AGENTS.md](./AGENTS.md)** - Development guide for AI coding agents (build commands, code style, testing patterns)