Skip to main content

A proxy tool that converts normal MCP servers to use lazy-loading pattern with meta-tools

Project description

Lazy MCP Proxy

Pipeline Status

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

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:

{
  "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 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
  • 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.

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 the local or local-with-auth environment, and run requests against a locally running lazy-mcp --transport http instance.
  • CHANGELOG.md - Version history and release notes
  • AGENTS.md - Development guide for AI coding agents (build commands, code style, testing patterns)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

lazy_mcp-2.6.2.tar.gz (279.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

lazy_mcp-2.6.2-py3-none-any.whl (280.9 kB view details)

Uploaded Python 3

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

Hashes for lazy_mcp-2.6.2.tar.gz
Algorithm Hash digest
SHA256 a4169225c84bf64900126219fc5154317c72a3835e738d042ddfc2b4882c872b
MD5 62007bb38196613f80f807d065d90964
BLAKE2b-256 5b3ff98cf4d00c805bcb1c1e165de6446dfbe566fa470512d330718ba555550b

See more details on using hashes here.

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

Hashes for lazy_mcp-2.6.2-py3-none-any.whl
Algorithm Hash digest
SHA256 339b78860e4c77b8feaf04a786b82775f6a0da1ba869403a1b71566358f4ceef
MD5 cdbb89ea3501d508a73870f7acb33de6
BLAKE2b-256 526d1ff60557c41ae09222e1cd585f07c87d161aa5b7b9b45cd8e5d95f2c8428

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page