lazy-mcp 2.5.0 → 2.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -499
- package/dist/cli.js +9 -10
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +14 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +88 -0
- package/dist/config.js.map +1 -1
- package/dist/multi-proxy.d.ts.map +1 -1
- package/dist/multi-proxy.js +21 -0
- package/dist/multi-proxy.js.map +1 -1
- package/dist/oauth-manager.d.ts +44 -4
- package/dist/oauth-manager.d.ts.map +1 -1
- package/dist/oauth-manager.js +125 -39
- package/dist/oauth-manager.js.map +1 -1
- package/dist/paths.d.ts +26 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +76 -0
- package/dist/paths.js.map +1 -0
- package/dist/permissions.d.ts +49 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +83 -0
- package/dist/permissions.js.map +1 -0
- package/dist/pid-lock.d.ts +3 -2
- package/dist/pid-lock.d.ts.map +1 -1
- package/dist/pid-lock.js +8 -7
- package/dist/pid-lock.js.map +1 -1
- package/dist/server-manager.d.ts +22 -3
- package/dist/server-manager.d.ts.map +1 -1
- package/dist/server-manager.js +38 -5
- package/dist/server-manager.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +17 -0
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +47 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,28 +14,14 @@ A client-agnostic proxy that converts normal MCP servers to use a lazy-loading p
|
|
|
14
14
|
- [Known Issue](#known-issue)
|
|
15
15
|
- [Installation](#installation)
|
|
16
16
|
- [Usage](#usage)
|
|
17
|
+
- [Config directory location (XDG Base Directory)](#config-directory-location-xdg-base-directory)
|
|
17
18
|
- [Streamable HTTP Transport](#streamable-http-transport)
|
|
18
19
|
- [Integration (Claude Desktop, OpenCode, Cursor, VS Code, and more)](#integration-claude-desktop-opencode-cursor-vs-code-and-more)
|
|
19
20
|
- [Example](#example)
|
|
20
21
|
- [Development](#development)
|
|
21
22
|
- [Running from Local Source](#running-from-local-source)
|
|
22
23
|
- [Releases](#releases)
|
|
23
|
-
- [How It Works](#how-it-works)
|
|
24
|
-
- [Required CI/CD Variables](#required-cicd-variables)
|
|
25
|
-
- [`GITLAB_RELEASE_TOKEN`](#gitlabreleasetoken)
|
|
26
|
-
- [`NPM_TOKEN`](#npmtoken)
|
|
27
|
-
- [`PYPI_TOKEN`](#pypitoken)
|
|
28
|
-
- [`CARGO_TOKEN`](#cargotoken)
|
|
29
24
|
- [Configuration Reference](#configuration-reference)
|
|
30
|
-
- [Server Configuration Fields](#server-configuration-fields)
|
|
31
|
-
- [OAuth 2.0 Authentication](#oauth-20-authentication)
|
|
32
|
-
- [Command Format](#command-format)
|
|
33
|
-
- [Environment Variables](#environment-variables)
|
|
34
|
-
- [Embed Server Summaries](#embed-server-summaries)
|
|
35
|
-
- [Transport Configuration](#transport-configuration)
|
|
36
|
-
- [Logging Configuration](#logging-configuration)
|
|
37
|
-
- [Health Monitoring](#health-monitoring)
|
|
38
|
-
- [Config Reload (SIGHUP)](#config-reload-sighup)
|
|
39
25
|
- [Benefits](#benefits)
|
|
40
26
|
- [Documentation](#documentation)
|
|
41
27
|
<!-- TOC_END -->
|
|
@@ -200,104 +186,43 @@ LAZY_MCP_CONFIG=~/.config/lazy-mcp/servers.json npx lazy-mcp@latest
|
|
|
200
186
|
lazy-mcp --config ~/.config/lazy-mcp/servers.json
|
|
201
187
|
```
|
|
202
188
|
|
|
203
|
-
###
|
|
189
|
+
### Config directory location (XDG Base Directory)
|
|
204
190
|
|
|
205
|
-
|
|
191
|
+
The config directory defaults to `~/.config/lazy-mcp/`, but lazy-mcp honours the
|
|
192
|
+
[XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html):
|
|
193
|
+
if `$XDG_CONFIG_HOME` is set, lazy-mcp uses `$XDG_CONFIG_HOME/lazy-mcp/`
|
|
194
|
+
instead. This applies to:
|
|
206
195
|
|
|
207
|
-
|
|
196
|
+
- the default `servers.json` location,
|
|
197
|
+
- the OAuth token store (`tokens.json`, `client-info.json`, …),
|
|
198
|
+
- the OAuth callback PID lock files.
|
|
208
199
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
"servers": [
|
|
212
|
-
{ "name": "my-server", "description": "My MCP server", "command": ["python", "server.py"] }
|
|
213
|
-
],
|
|
214
|
-
"transport": {
|
|
215
|
-
"type": "http",
|
|
216
|
-
"port": 3000,
|
|
217
|
-
"host": "localhost",
|
|
218
|
-
"path": "/mcp",
|
|
219
|
-
"authToken": "${MY_API_KEY}"
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
**Via CLI flags** (override config values):
|
|
200
|
+
This makes it easy to scope tokens per project (e.g. via `direnv` or `mise`)
|
|
201
|
+
without having to override `$HOME`:
|
|
225
202
|
|
|
226
203
|
```bash
|
|
227
|
-
#
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
# With custom host, path, and auth
|
|
231
|
-
lazy-mcp --config servers.json --transport http --port 3000 --host 127.0.0.1 --path /mcp --auth-token "my-secret"
|
|
204
|
+
# In a project's .envrc / mise.toml:
|
|
205
|
+
export XDG_CONFIG_HOME="$PWD/.config"
|
|
232
206
|
```
|
|
233
207
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
> lazy-mcp's own socket plus the incoming `Host` header. If you run lazy-mcp
|
|
238
|
-
> behind a TLS-terminating reverse proxy, the backend hop is usually plain
|
|
239
|
-
> HTTP, so the implicit same-origin shortcut may reject public HTTPS origins.
|
|
240
|
-
> In that setup, configure `transport.allowedOrigins` explicitly. If your
|
|
241
|
-
> proxy preserves the public `Host` header, configure `transport.allowedHosts`
|
|
242
|
-
> too.
|
|
243
|
-
>
|
|
244
|
-
> Example:
|
|
245
|
-
> ```json
|
|
246
|
-
> {
|
|
247
|
-
> "transport": {
|
|
248
|
-
> "type": "http",
|
|
249
|
-
> "host": "127.0.0.1",
|
|
250
|
-
> "port": 8080,
|
|
251
|
-
> "path": "/mcp",
|
|
252
|
-
> "allowedHosts": ["mcp.example.com"],
|
|
253
|
-
> "allowedOrigins": ["https://mcp.example.com"]
|
|
254
|
-
> }
|
|
255
|
-
> }
|
|
256
|
-
> ```
|
|
257
|
-
>
|
|
258
|
-
> lazy-mcp intentionally does not trust `Forwarded` / `X-Forwarded-*` headers
|
|
259
|
-
> by default. If proxy-aware origin reconstruction is ever added, it should be
|
|
260
|
-
> behind an explicit trusted-proxy setting.
|
|
261
|
-
|
|
262
|
-
**Via environment variables** (lowest precedence):
|
|
263
|
-
|
|
264
|
-
```bash
|
|
265
|
-
LAZY_MCP_TRANSPORT=http LAZY_MCP_PORT=3000 LAZY_MCP_AUTH_TOKEN=my-secret lazy-mcp
|
|
266
|
-
```
|
|
208
|
+
With that set, lazy-mcp will read `./.config/lazy-mcp/servers.json` and store
|
|
209
|
+
tokens under `./.config/lazy-mcp/` — completely isolated from your global
|
|
210
|
+
lazy-mcp state.
|
|
267
211
|
|
|
268
|
-
|
|
212
|
+
### Streamable HTTP Transport
|
|
269
213
|
|
|
270
|
-
|
|
214
|
+
By default, lazy-mcp communicates over **stdio**. You can also run it as an **HTTP server** so remote clients can connect over the network:
|
|
271
215
|
|
|
272
216
|
```bash
|
|
273
|
-
|
|
274
|
-
curl -X POST http://localhost:3000/mcp \
|
|
275
|
-
-H "Content-Type: application/json" \
|
|
276
|
-
-H "Accept: application/json, text/event-stream" \
|
|
277
|
-
-H "Authorization: Bearer ${MY_API_KEY}" \
|
|
278
|
-
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
|
|
217
|
+
lazy-mcp --config servers.json --transport http --port 3000 --auth-token "my-secret"
|
|
279
218
|
```
|
|
280
219
|
|
|
281
|
-
|
|
220
|
+
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.
|
|
282
221
|
|
|
283
222
|
## Integration (Claude Desktop, OpenCode, Cursor, VS Code, and more)
|
|
284
223
|
|
|
285
|
-
Replace multiple MCP server entries with one aggregated proxy:
|
|
224
|
+
Replace multiple MCP server entries in your client with one aggregated lazy-mcp proxy:
|
|
286
225
|
|
|
287
|
-
**Before** (5 separate MCP servers):
|
|
288
|
-
```json
|
|
289
|
-
{
|
|
290
|
-
"mcp": {
|
|
291
|
-
"chrome-devtools": { "command": ["npx", "lazy-mcp@latest", "npx", "-y", "chrome-devtools-mcp@latest"] },
|
|
292
|
-
"gitlab": { "command": ["npx", "lazy-mcp@latest", "npx", "mcp-remote@latest", "https://..."] },
|
|
293
|
-
"grepai": { "command": ["npx", "lazy-mcp@latest", "grepai", "mcp-serve"] },
|
|
294
|
-
"context7": { "command": ["npx", "-y", "@upstash/context7-mcp"] },
|
|
295
|
-
"perplexity": { "command": ["npx", "-y", "@perplexity-ai/mcp-server"] }
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
**After** (Consolidated into 1 multi-server proxy):
|
|
301
226
|
```json
|
|
302
227
|
{
|
|
303
228
|
"mcp": {
|
|
@@ -310,38 +235,9 @@ Replace multiple MCP server entries with one aggregated proxy:
|
|
|
310
235
|
}
|
|
311
236
|
```
|
|
312
237
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
Start lazy-mcp in HTTP mode (e.g. `lazy-mcp --config servers.json --transport http --port 3000`), then configure your client:
|
|
316
|
-
|
|
317
|
-
```json
|
|
318
|
-
{
|
|
319
|
-
"mcp": {
|
|
320
|
-
"lazy-mcp": {
|
|
321
|
-
"type": "remote",
|
|
322
|
-
"url": "http://localhost:3000/mcp",
|
|
323
|
-
"headers": {
|
|
324
|
-
"Authorization": "Bearer ${LAZY_MCP_AUTH_TOKEN}"
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
Where `~/.config/lazy-mcp/servers.json` contains all 5 servers:
|
|
332
|
-
```json
|
|
333
|
-
{
|
|
334
|
-
"servers": [
|
|
335
|
-
{ "name": "chrome-devtools", "description": "Chrome DevTools automation", "command": ["npx", "-y", "chrome-devtools-mcp@latest"] },
|
|
336
|
-
{ "name": "gitlab", "description": "GitLab API integration", "command": ["npx", "mcp-remote@latest", "https://..."] },
|
|
337
|
-
{ "name": "grepai", "description": "Search codebase", "command": ["grepai", "mcp-serve"] },
|
|
338
|
-
{ "name": "context7", "description": "Library documentation", "command": ["npx", "-y", "@upstash/context7-mcp"] },
|
|
339
|
-
{ "name": "perplexity", "description": "Web search", "command": ["npx", "-y", "@perplexity-ai/mcp-server"] }
|
|
340
|
-
]
|
|
341
|
-
}
|
|
342
|
-
```
|
|
238
|
+
All downstream MCP servers live in `~/.config/lazy-mcp/servers.json`. **Result**: ~90% context reduction (from ~16K to ~1.5K tokens initially).
|
|
343
239
|
|
|
344
|
-
|
|
240
|
+
See [doc/INTEGRATION.md](./doc/INTEGRATION.md) for before/after examples and HTTP-mode client configuration.
|
|
345
241
|
|
|
346
242
|
## Example
|
|
347
243
|
|
|
@@ -408,384 +304,29 @@ Or with `ts-node` (no build needed, always reflects latest source):
|
|
|
408
304
|
|
|
409
305
|
## Releases
|
|
410
306
|
|
|
411
|
-
Releases are fully automated via [semantic-release](https://semantic-release.gitbook.io/) on every push to `main`.
|
|
412
|
-
|
|
413
|
-
### How It Works
|
|
414
|
-
|
|
415
|
-
1. CI analyzes all commits since the last tag using [Conventional Commits](https://www.conventionalcommits.org/)
|
|
416
|
-
2. Determines the next version (`feat` → minor bump, `fix`/`perf`/`chore`/`refactor`/`test` → patch bump)
|
|
417
|
-
3. Updates `package.json`, `CHANGELOG.md`, `VERSION`, and `RELEASE_NOTES.md`
|
|
418
|
-
4. Commits those files as `chore(release): X.Y.Z` and pushes a `vX.Y.Z` tag
|
|
419
|
-
5. Creates a GitLab Release with the generated release notes
|
|
420
|
-
6. The `vX.Y.Z` tag triggers a separate pipeline that publishes the package to npm automatically
|
|
421
|
-
|
|
422
|
-
No manual version bumping or tagging needed — just merge to `main` with conventional commit messages.
|
|
423
|
-
|
|
424
|
-
### Required CI/CD Variables
|
|
425
|
-
|
|
426
|
-
Four CI/CD variables must be configured (GitLab → Project → Settings → CI/CD → Variables):
|
|
307
|
+
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.
|
|
427
308
|
|
|
428
|
-
|
|
429
|
-
|----------|-------------|
|
|
430
|
-
| `GITLAB_RELEASE_TOKEN` | Project access token — pushes the release commit + tag to `main` and creates the GitLab Release |
|
|
431
|
-
| `NPM_TOKEN` | npm automation token — publishes the package to the npm registry on tag pipelines |
|
|
432
|
-
| `PYPI_TOKEN` | PyPI API token — publishes the package to PyPI on tag pipelines |
|
|
433
|
-
| `CARGO_TOKEN` | crates.io API token — publishes the crate to crates.io on tag pipelines |
|
|
434
|
-
|
|
435
|
-
#### `GITLAB_RELEASE_TOKEN`
|
|
436
|
-
|
|
437
|
-
**Creating the token** (GitLab → Project → Settings → Access Tokens):
|
|
438
|
-
|
|
439
|
-
| Setting | Value |
|
|
440
|
-
|---------|-------|
|
|
441
|
-
| Token name | `semantic-release-bot` (or any name) |
|
|
442
|
-
| Role | **Developer** (push to a protected branch should be configured separately) |
|
|
443
|
-
| Scopes | `api`, `write_repository` |
|
|
444
|
-
|
|
445
|
-
**Adding the variable:**
|
|
446
|
-
|
|
447
|
-
| Setting | Value |
|
|
448
|
-
|---------|-------|
|
|
449
|
-
| Key | `GITLAB_RELEASE_TOKEN` |
|
|
450
|
-
| Masked | ✅ Yes |
|
|
451
|
-
| Protected | ❌ No (must be available on the unprotected `main` pipeline) |
|
|
452
|
-
|
|
453
|
-
> **Note**: If `main` is a protected branch with push restrictions, the token's bot user must be added to the "Allowed to push" list under GitLab → Project → Settings → Repository → Protected branches.
|
|
454
|
-
|
|
455
|
-
#### `NPM_TOKEN`
|
|
456
|
-
|
|
457
|
-
**Creating the token** (npmjs.com → Account → Access Tokens → Generate New Token):
|
|
458
|
-
|
|
459
|
-
| Setting | Value |
|
|
460
|
-
|---------|-------|
|
|
461
|
-
| Token type | **Automation** (bypasses 2FA, suitable for CI) |
|
|
462
|
-
|
|
463
|
-
**Adding the variable:**
|
|
464
|
-
|
|
465
|
-
| Setting | Value |
|
|
466
|
-
|---------|-------|
|
|
467
|
-
| Key | `NPM_TOKEN` |
|
|
468
|
-
| Masked | ✅ Yes |
|
|
469
|
-
| Protected | ✅ Yes (only needed on tag pipelines, which are protected) |
|
|
470
|
-
|
|
471
|
-
#### `PYPI_TOKEN`
|
|
472
|
-
|
|
473
|
-
**Creating the token** (pypi.org → Account Settings → API tokens → Add API token):
|
|
474
|
-
|
|
475
|
-
| Setting | Value |
|
|
476
|
-
|---------|-------|
|
|
477
|
-
| Token name | `lazy-mcp-ci` (or any name) |
|
|
478
|
-
| Scope | **Project: lazy-mcp** (restrict to this project after first publish; use "Entire account" for the very first publish) |
|
|
479
|
-
|
|
480
|
-
**Adding the variable:**
|
|
481
|
-
|
|
482
|
-
| Setting | Value |
|
|
483
|
-
|---------|-------|
|
|
484
|
-
| Key | `PYPI_TOKEN` |
|
|
485
|
-
| Masked | ✅ Yes |
|
|
486
|
-
| Protected | ✅ Yes (only needed on tag pipelines, which are protected) |
|
|
487
|
-
|
|
488
|
-
#### `CARGO_TOKEN`
|
|
489
|
-
|
|
490
|
-
**Creating the token** (crates.io → Account Settings → API Tokens → New Token):
|
|
491
|
-
|
|
492
|
-
| Setting | Value |
|
|
493
|
-
|---------|-------|
|
|
494
|
-
| Token name | `lazy-mcp-ci` (or any name) |
|
|
495
|
-
| Scopes | `publish-new`, `publish-update` |
|
|
496
|
-
|
|
497
|
-
**Adding the variable:**
|
|
498
|
-
|
|
499
|
-
| Setting | Value |
|
|
500
|
-
|---------|-------|
|
|
501
|
-
| Key | `CARGO_TOKEN` |
|
|
502
|
-
| Masked | ✅ Yes |
|
|
503
|
-
| Protected | ✅ Yes (only needed on tag pipelines, which are protected) |
|
|
309
|
+
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`).
|
|
504
310
|
|
|
505
311
|
## Configuration Reference
|
|
506
312
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
| Field | Type | Required | Description |
|
|
510
|
-
|-------|------|----------|-------------|
|
|
511
|
-
| `name` | string | ✅ | Unique server identifier |
|
|
512
|
-
| `description` | string | ✅ | Human-readable description |
|
|
513
|
-
| `type` | "local" \| "remote" | Optional | Inferred from `url` (remote) or `command` (local) if omitted |
|
|
514
|
-
| `command` | string[] \| string | For local | Command to execute (array format recommended) |
|
|
515
|
-
| `args` | string[] | Optional | Arguments (only if command is string) |
|
|
516
|
-
| `url` | string | For remote | HTTP/HTTPS URL |
|
|
517
|
-
| `headers` | object | Optional | Static HTTP headers for remote servers |
|
|
518
|
-
| `oauth` | object | Optional | OAuth 2.0 config for remote servers (see below) |
|
|
519
|
-
| `env` | object | Optional | Environment variables (supports `${VAR}` and `{file:...}` expansion) |
|
|
520
|
-
| `enabled` | boolean | Optional | Enable/disable server (default: true) |
|
|
521
|
-
| `examples` | object[] | Optional | Usage examples shown in `list_servers` output |
|
|
522
|
-
| `tags` | string[] | Optional | Capability tags for filtering (e.g. `"api"`, `"browser"`) |
|
|
523
|
-
|
|
524
|
-
### OAuth 2.0 Authentication
|
|
525
|
-
|
|
526
|
-
lazy-mcp has built-in OAuth 2.0 + PKCE support for remote servers that require user authorization. It works without opening a browser automatically, making it suitable for sandboxed agent environments: when authentication is needed, lazy-mcp returns the authorization URL in the error message so the agent can present it to the user.
|
|
527
|
-
|
|
528
|
-
OAuth server endpoints are **discovered automatically** via RFC 8414 (`/.well-known/oauth-authorization-server`). Dynamic client registration (RFC 7591) is used when no `clientId` is provided.
|
|
529
|
-
|
|
530
|
-
Tokens are persisted to `~/.config/lazy-mcp/tokens.json` (mode `0600`) and refreshed automatically via `refresh_token` when available.
|
|
531
|
-
|
|
532
|
-
**Minimal config** (fully automatic — discovery + dynamic registration):
|
|
533
|
-
```json
|
|
534
|
-
{
|
|
535
|
-
"name": "glean",
|
|
536
|
-
"description": "Glean enterprise search",
|
|
537
|
-
"url": "https://your-company.glean.com/mcp/default",
|
|
538
|
-
"oauth": {}
|
|
539
|
-
}
|
|
540
|
-
```
|
|
541
|
-
|
|
542
|
-
**With a pre-registered client ID**:
|
|
543
|
-
```json
|
|
544
|
-
{
|
|
545
|
-
"name": "glean",
|
|
546
|
-
"description": "Glean enterprise search",
|
|
547
|
-
"url": "https://your-company.glean.com/mcp/default",
|
|
548
|
-
"oauth": {
|
|
549
|
-
"clientId": "${GLEAN_CLIENT_ID}",
|
|
550
|
-
"extraHeaders": { "X-Glean-Auth-Type": "OAUTH" }
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
```
|
|
554
|
-
|
|
555
|
-
**`oauth` object fields:**
|
|
556
|
-
|
|
557
|
-
| Field | Type | Default | Description |
|
|
558
|
-
|-------|------|---------|-------------|
|
|
559
|
-
| `clientId` | string | — | OAuth client ID. If omitted, dynamic registration (RFC 7591) is attempted |
|
|
560
|
-
| `clientSecret` | string | — | Client secret (omit for public-client / PKCE-only flows) |
|
|
561
|
-
| `callbackPort` | number | `8947` | Local port for the OAuth redirect callback server |
|
|
562
|
-
| `extraHeaders` | object | — | Additional headers added to every authenticated request (e.g. `X-Glean-Auth-Type`) |
|
|
563
|
-
|
|
564
|
-
**How it works:**
|
|
565
|
-
|
|
566
|
-
1. Agent calls `invoke_command` (or `list_commands`) on an OAuth-protected server
|
|
567
|
-
2. lazy-mcp returns an `isError: true` response with the authorization URL
|
|
568
|
-
3. Agent presents the URL to the user: _"Open this URL to authorize Glean: https://..."_
|
|
569
|
-
4. User opens the URL in a browser and completes authorization
|
|
570
|
-
5. Browser redirects to `http://localhost:8947/callback` — lazy-mcp captures the token
|
|
571
|
-
6. Agent retries the original command — now succeeds transparently
|
|
572
|
-
|
|
573
|
-
### Command Format
|
|
574
|
-
|
|
575
|
-
**Recommended** (OpenCode-compatible):
|
|
576
|
-
```json
|
|
577
|
-
{
|
|
578
|
-
"command": ["npx", "-y", "my-mcp-server", "--port", "3000"]
|
|
579
|
-
}
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
**Legacy** (still supported):
|
|
583
|
-
```json
|
|
584
|
-
{
|
|
585
|
-
"command": "npx",
|
|
586
|
-
"args": ["-y", "my-mcp-server", "--port", "3000"]
|
|
587
|
-
}
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
### Environment Variables
|
|
591
|
-
|
|
592
|
-
Use `${VAR_NAME}` to reference environment variables:
|
|
593
|
-
|
|
594
|
-
```json
|
|
595
|
-
{
|
|
596
|
-
"env": {
|
|
597
|
-
"API_KEY": "${MY_API_KEY}",
|
|
598
|
-
"DEBUG": "true"
|
|
599
|
-
},
|
|
600
|
-
"headers": {
|
|
601
|
-
"Authorization": "Bearer ${AUTH_TOKEN}"
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
```
|
|
605
|
-
|
|
606
|
-
Use `{file:/path/to/secret}` to read a secret directly from a file (trailing newline is stripped automatically). `~/` is expanded to the home directory. On Windows, `%VAR_NAME%` references inside the path are expanded from environment variables (e.g. `%USERPROFILE%`):
|
|
607
|
-
|
|
608
|
-
```json
|
|
609
|
-
{
|
|
610
|
-
"env": {
|
|
611
|
-
"GITLAB_API_TOKEN": "{file:~/.secrets/gl-pat-token}",
|
|
612
|
-
"OTHER_SECRET": "{file:/run/secrets/other-token}"
|
|
613
|
-
},
|
|
614
|
-
"headers": {
|
|
615
|
-
"Authorization": "Bearer {file:~/.secrets/api-token}"
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
```
|
|
619
|
-
|
|
620
|
-
On Windows you can use `%USERPROFILE%` or any other environment variable in the path:
|
|
621
|
-
|
|
622
|
-
```json
|
|
623
|
-
{
|
|
624
|
-
"env": {
|
|
625
|
-
"API_TOKEN": "{file:%USERPROFILE%\\.secrets\\token}"
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
```
|
|
629
|
-
|
|
630
|
-
Only absolute paths and `~/` paths are accepted. Relative paths are left unchanged. Both notations can be combined in the same value and work in `env`, `headers`, and `transport.authToken` fields. `${VAR}` references inside a `{file:...}` path are not expanded; use an absolute path, `~/`, or Windows-style `%VAR_NAME%` path expansion instead. If the referenced file cannot be read (missing or permission error), the app exits at startup with an error message — e.g. `Cannot read secret file '/run/secrets/token' (from {file:/run/secrets/token}): ENOENT: no such file or directory`.
|
|
631
|
-
|
|
632
|
-
**Security best practices for secret files:**
|
|
633
|
-
|
|
634
|
-
- Set permissions to `0600` (owner read/write only): `chmod 0600 ~/.secrets/my-token`
|
|
635
|
-
- Store secret files in a user-specific directory (e.g. `~/.secrets/`)
|
|
636
|
-
- Never commit secret files to version control — add them to `.gitignore`
|
|
637
|
-
|
|
638
|
-
### Embed Server Summaries
|
|
639
|
-
|
|
640
|
-
By default, the AI must call `list_servers` to discover which MCP servers are available.
|
|
641
|
-
You can opt in to embedding server names and descriptions directly in the `list_servers` tool description so the
|
|
642
|
-
AI sees them upfront — useful for routing requests like "check my email" to the correct server without an extra tool call.
|
|
643
|
-
|
|
644
|
-
```json
|
|
645
|
-
{
|
|
646
|
-
"servers": [...],
|
|
647
|
-
"embedServerSummaries": {
|
|
648
|
-
"enabled": true,
|
|
649
|
-
"maxServers": 5
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
```
|
|
653
|
-
|
|
654
|
-
| Field | Type | Default | Description |
|
|
655
|
-
|-------|------|---------|-------------|
|
|
656
|
-
| `embedServerSummaries.enabled` | boolean | — | Enable/disable embedding |
|
|
657
|
-
| `embedServerSummaries.maxServers` | number | `10` | Maximum number of server summaries to embed |
|
|
658
|
-
|
|
659
|
-
> **Note:** Tool descriptions are included in every request context window.
|
|
660
|
-
> If your system prompt already instructs the LLM to call `list_servers`, this adds tokens with no benefit.
|
|
661
|
-
> Per-server descriptions are unbounded, so one verbose description can significantly increase tool description size.
|
|
662
|
-
> Making this opt-in lets you consciously accept that trade-off.
|
|
663
|
-
|
|
664
|
-
### Transport Configuration
|
|
313
|
+
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).
|
|
665
314
|
|
|
666
|
-
|
|
315
|
+
Common top-level blocks:
|
|
667
316
|
|
|
668
|
-
|
|
317
|
+
- **`servers[]`** — list of MCP servers to aggregate (required)
|
|
318
|
+
- **`permissions`** — global and per-server allow/deny rules for `invoke_command` (experimental)
|
|
319
|
+
- **`transport`** — switch from stdio to HTTP, set port, bind host, bearer auth, etc.
|
|
320
|
+
- **`logging`** — structured stderr logging (level, format, body dumps, redaction)
|
|
321
|
+
- **`healthMonitor`** — background health probes (activity-driven by default)
|
|
322
|
+
- **`embedServerSummaries`** — opt-in: embed configured server names/descriptions in `list_servers` description
|
|
323
|
+
- **`requestTimeout`** — per-server request timeout in ms
|
|
669
324
|
|
|
670
|
-
|
|
671
|
-
|-------|------|---------|-------------|
|
|
672
|
-
| `transport.type` | `"stdio"` \| `"http"` | `"stdio"` | Transport mode |
|
|
673
|
-
| `transport.port` | number | `8080` | HTTP server port |
|
|
674
|
-
| `transport.host` | string | `"127.0.0.1"` | HTTP server bind address. For TLS-terminating reverse proxies, keep localhost binding and configure `allowedOrigins` explicitly. |
|
|
675
|
-
| `transport.path` | string | `"/mcp"` | MCP endpoint URL path |
|
|
676
|
-
| `transport.authToken` | string | — | Bearer token for HTTP auth (supports `${VAR}` and `{file:...}` expansion) |
|
|
677
|
-
| `transport.allowedOrigins` | string[] | — | List of allowed Origin header values for request-origin validation / CSRF protection (e.g. `["https://example.com"]`). Full origins including scheme and port. Recommended for TLS-terminating reverse-proxy deployments. Does not send CORS response headers. |
|
|
678
|
-
| `transport.allowedHosts` | string[] | — | List of explicitly allowed host domains for DNS rebinding protection. If a reverse proxy preserves the public `Host` header, add that host here. |
|
|
679
|
-
| `transport.maxPayloadSize` | number | `4194304` | Maximum request body size in bytes. Requests exceeding this limit return HTTP 413 |
|
|
325
|
+
You can also expand secrets in any string value with `${VAR}` (env var) or `{file:/path/to/secret}` (file-based, owner-only `0600` recommended).
|
|
680
326
|
|
|
681
|
-
|
|
327
|
+
Send `SIGHUP` to reload the config without restarting (`kill -HUP <pid>` — the PID is in `list_servers`).
|
|
682
328
|
|
|
683
|
-
|
|
684
|
-
|------|-------------|-------------|
|
|
685
|
-
| `--transport` | `LAZY_MCP_TRANSPORT` | Transport type (`stdio` or `http`) |
|
|
686
|
-
| `--port` | `LAZY_MCP_PORT` | HTTP server port |
|
|
687
|
-
| `--host` | `LAZY_MCP_HOST` | HTTP server bind address |
|
|
688
|
-
| `--path` | `LAZY_MCP_PATH` | MCP endpoint URL path |
|
|
689
|
-
| `--auth-token` | `LAZY_MCP_AUTH_TOKEN` | Bearer token for HTTP auth |
|
|
690
|
-
| `--request-timeout` | `LAZY_MCP_REQUEST_TIMEOUT` | Request timeout in ms for server calls, including MCP handshake requests and remote response parsing (default: 10000) |
|
|
691
|
-
| `--max-payload-size` | `LAZY_MCP_MAX_PAYLOAD_SIZE` | Maximum request body size in bytes (default: 4194304) |
|
|
692
|
-
|
|
693
|
-
When `authToken` is set, all HTTP requests must include `Authorization: Bearer <token>` — unauthenticated requests receive `401 Unauthorized`.
|
|
694
|
-
|
|
695
|
-
### Logging Configuration
|
|
696
|
-
|
|
697
|
-
lazy-mcp now emits structured logs to **stderr** (stdout remains reserved for MCP protocol traffic).
|
|
698
|
-
|
|
699
|
-
**Top-level configuration** (in `servers.json`):
|
|
700
|
-
|
|
701
|
-
| Field | Type | Default | Description |
|
|
702
|
-
|-------|------|---------|-------------|
|
|
703
|
-
| `logging.level` | `"error"` \| `"info"` \| `"debug"` | `"info"` | Minimum log level |
|
|
704
|
-
| `logging.format` | `"json"` \| `"plain"` | `"json"` | Log output format |
|
|
705
|
-
| `logging.dumpBodies` | boolean | `false` | Enable debug request/response body dumps |
|
|
706
|
-
| `logging.maxBodyLogBytes` | number | `8192` | Max body-dump size in bytes before truncation |
|
|
707
|
-
| `logging.redactKeys` | string[] | — | Additional case-insensitive keys to redact (merged with built-in defaults) |
|
|
708
|
-
|
|
709
|
-
Built-in redaction includes common secret keys like `authorization`, `token`, `access_token`, `refresh_token`, `client_secret`, and `headers.authorization`.
|
|
710
|
-
|
|
711
|
-
Example:
|
|
712
|
-
|
|
713
|
-
```json
|
|
714
|
-
{
|
|
715
|
-
"servers": [
|
|
716
|
-
{
|
|
717
|
-
"name": "my-server",
|
|
718
|
-
"description": "Example",
|
|
719
|
-
"command": ["npx", "-y", "my-mcp-server"]
|
|
720
|
-
}
|
|
721
|
-
],
|
|
722
|
-
"logging": {
|
|
723
|
-
"level": "debug",
|
|
724
|
-
"format": "json",
|
|
725
|
-
"dumpBodies": true,
|
|
726
|
-
"maxBodyLogBytes": 4096,
|
|
727
|
-
"redactKeys": ["my_custom_secret"]
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
```
|
|
731
|
-
|
|
732
|
-
Typical access log event fields include:
|
|
733
|
-
- client source (`clientIp`, optional `forwardedFor`)
|
|
734
|
-
- request shape (`httpMethod`, `path`, `mcpMethod`, `lazyTool`)
|
|
735
|
-
- downstream routing (`downstreamServer`, `downstreamCommand`)
|
|
736
|
-
- outcome (`status`, `reason`, `durationMs`)
|
|
737
|
-
|
|
738
|
-
### Health Monitoring
|
|
739
|
-
|
|
740
|
-
lazy-mcp includes a background health monitor that probes all servers periodically. The monitor is **activity-driven**: it sleeps on startup and only begins probing after the first user tool call (`list_servers`, `list_commands`, etc.). After a configurable idle timeout (default: 5 minutes) with no tool calls, the monitor goes back to sleep. This prevents OAuth-protected servers (e.g. GitLab via `mcp-remote`) from opening browser windows when no one is using the tools.
|
|
741
|
-
|
|
742
|
-
Successful probes populate the discovery cache, so subsequent `list_commands` calls return instantly from cache.
|
|
743
|
-
|
|
744
|
-
**Top-level configuration** (in `servers.json`):
|
|
745
|
-
|
|
746
|
-
| Field | Type | Default | Description |
|
|
747
|
-
|-------|------|---------|-------------|
|
|
748
|
-
| `healthMonitor.enabled` | boolean | `true` | Enable/disable background health monitoring |
|
|
749
|
-
| `healthMonitor.interval` | number | `30000` | Interval between health checks (ms) |
|
|
750
|
-
| `healthMonitor.timeout` | number | `10000` | Timeout per server probe (ms) |
|
|
751
|
-
| `healthMonitor.idleTimeout` | number | `300000` | Stop probing after this much inactivity (ms). `0` = never sleep (legacy) |
|
|
752
|
-
| `requestTimeout` | number | `10000` | Timeout for individual server requests. For remote HTTP servers, this includes waiting for headers, reading response bodies, and SSE response parsing. Override via `--request-timeout` or `LAZY_MCP_REQUEST_TIMEOUT` |
|
|
753
|
-
|
|
754
|
-
To disable health monitoring:
|
|
755
|
-
```json
|
|
756
|
-
{
|
|
757
|
-
"servers": [...],
|
|
758
|
-
"healthMonitor": { "enabled": false }
|
|
759
|
-
}
|
|
760
|
-
```
|
|
761
|
-
|
|
762
|
-
To increase the request timeout (e.g. for slow remote servers):
|
|
763
|
-
```json
|
|
764
|
-
{
|
|
765
|
-
"servers": [...],
|
|
766
|
-
"requestTimeout": 30000
|
|
767
|
-
}
|
|
768
|
-
```
|
|
769
|
-
|
|
770
|
-
### Config Reload (SIGHUP)
|
|
771
|
-
|
|
772
|
-
You can reload the configuration without restarting the process by sending a `SIGHUP` signal:
|
|
773
|
-
|
|
774
|
-
```bash
|
|
775
|
-
kill -HUP $(pgrep -f lazy-mcp)
|
|
776
|
-
```
|
|
777
|
-
|
|
778
|
-
This will:
|
|
779
|
-
- Re-read and validate the config file
|
|
780
|
-
- Add newly configured servers (lazy connection on first use)
|
|
781
|
-
- Remove servers no longer in config (closes connections)
|
|
782
|
-
- Reconnect servers whose config changed (updated URL, env, etc.)
|
|
783
|
-
- Preserve unchanged servers (keeps existing connections and caches)
|
|
784
|
-
- Restart the health monitor and probe all servers
|
|
785
|
-
|
|
786
|
-
If the new config is invalid, the reload is rejected and the current config continues running. All reload activity is logged to stderr.
|
|
787
|
-
|
|
788
|
-
> **Note**: SIGHUP is not available on Windows.
|
|
329
|
+
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)**.
|
|
789
330
|
|
|
790
331
|
## Benefits
|
|
791
332
|
|
|
@@ -802,9 +343,12 @@ If the new config is invalid, the reload is rejected and the current config cont
|
|
|
802
343
|
|
|
803
344
|
## Documentation
|
|
804
345
|
|
|
805
|
-
- **[
|
|
806
|
-
- **[
|
|
346
|
+
- **[doc/CONFIGURATION.md](./doc/CONFIGURATION.md)** - Full configuration reference (servers, permissions, OAuth, transport, logging, health monitoring, SIGHUP reload)
|
|
347
|
+
- **[doc/HTTP_TRANSPORT.md](./doc/HTTP_TRANSPORT.md)** - Streamable HTTP transport setup, security, and reverse-proxy guidance
|
|
348
|
+
- **[doc/INTEGRATION.md](./doc/INTEGRATION.md)** - Client integration examples (Claude Desktop, OpenCode, Cursor, VS Code) for stdio and HTTP modes
|
|
349
|
+
- **[doc/RELEASES.md](./doc/RELEASES.md)** - Release pipeline and required CI/CD variables
|
|
807
350
|
- **[doc/ARCHITECTURE.md](./doc/ARCHITECTURE.md)** - Architecture overview and design patterns
|
|
808
351
|
- **[doc/CONTRIBUTING.md](./doc/CONTRIBUTING.md)** - Contributing guide with common development tasks
|
|
809
352
|
- **[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.
|
|
810
|
-
- **[
|
|
353
|
+
- **[CHANGELOG.md](./CHANGELOG.md)** - Version history and release notes
|
|
354
|
+
- **[AGENTS.md](./AGENTS.md)** - Development guide for AI coding agents (build commands, code style, testing patterns)
|