Metadata-Version: 2.4
Name: lazy-mcp
Version: 2.6.3
Summary: A proxy tool that converts normal MCP servers to use lazy-loading pattern with meta-tools
Project-URL: Homepage, https://gitlab.com/gitlab-org/ai/lazy-mcp
Project-URL: Repository, https://gitlab.com/gitlab-org/ai/lazy-mcp
Project-URL: Issues, https://gitlab.com/gitlab-org/ai/lazy-mcp/-/issues
Author-email: Dmitry Gruzd <dgruzd@gitlab.com>
License-Expression: MIT
Keywords: ai-tools,lazy-loading,llm,mcp,mcp-server,model-context-protocol,proxy
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.9
Requires-Dist: nodejs-wheel>=20
Description-Content-Type: text/markdown

# Lazy MCP Proxy

![Pipeline Status](https://gitlab.com/gitlab-org/ai/lazy-mcp/badges/main/pipeline.svg)

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

<!-- TOC_START -->
  - [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)
<!-- TOC_END -->

## 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
- **`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)
