lazy-mcp 2.6.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 CHANGED
@@ -14,37 +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
- - [Permissions](#permissions)
32
- - [Pattern syntax](#pattern-syntax)
33
- - [Resolution algorithm](#resolution-algorithm)
34
- - [Per-server permissions](#per-server-permissions)
35
- - [Global (cross-server) permissions](#global-cross-server-permissions)
36
- - [Read-only preset](#read-only-preset)
37
- - [Hot reload](#hot-reload)
38
- - [Soft-fail validation](#soft-fail-validation)
39
- - [Error contract](#error-contract)
40
- - [OAuth 2.0 Authentication](#oauth-20-authentication)
41
- - [Command Format](#command-format)
42
- - [Environment Variables](#environment-variables)
43
- - [Embed Server Summaries](#embed-server-summaries)
44
- - [Transport Configuration](#transport-configuration)
45
- - [Logging Configuration](#logging-configuration)
46
- - [Health Monitoring](#health-monitoring)
47
- - [Config Reload (SIGHUP)](#config-reload-sighup)
48
25
  - [Benefits](#benefits)
49
26
  - [Documentation](#documentation)
50
27
  <!-- TOC_END -->
@@ -209,104 +186,43 @@ LAZY_MCP_CONFIG=~/.config/lazy-mcp/servers.json npx lazy-mcp@latest
209
186
  lazy-mcp --config ~/.config/lazy-mcp/servers.json
210
187
  ```
211
188
 
212
- ### Streamable HTTP Transport
213
-
214
- By default, lazy-mcp communicates over **stdio** (standard MCP transport). You can also run it as an **HTTP server** using the Streamable HTTP transport, allowing remote clients to connect over the network:
189
+ ### Config directory location (XDG Base Directory)
215
190
 
216
- **Via config file** add a `transport` block to `servers.json`:
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:
217
195
 
218
- ```json
219
- {
220
- "servers": [
221
- { "name": "my-server", "description": "My MCP server", "command": ["python", "server.py"] }
222
- ],
223
- "transport": {
224
- "type": "http",
225
- "port": 3000,
226
- "host": "localhost",
227
- "path": "/mcp",
228
- "authToken": "${MY_API_KEY}"
229
- }
230
- }
231
- ```
196
+ - the default `servers.json` location,
197
+ - the OAuth token store (`tokens.json`, `client-info.json`, …),
198
+ - the OAuth callback PID lock files.
232
199
 
233
- **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`:
234
202
 
235
203
  ```bash
236
- # Start HTTP server on port 3000
237
- lazy-mcp --config servers.json --transport http --port 3000
238
-
239
- # With custom host, path, and auth
240
- 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"
241
206
  ```
242
207
 
243
- **Security note:** `--host 0.0.0.0` exposes the HTTP server on all network interfaces. Use it only inside Docker or trusted networks.
244
-
245
- > **Reverse proxy note:** The default Origin check uses the scheme visible to
246
- > lazy-mcp's own socket plus the incoming `Host` header. If you run lazy-mcp
247
- > behind a TLS-terminating reverse proxy, the backend hop is usually plain
248
- > HTTP, so the implicit same-origin shortcut may reject public HTTPS origins.
249
- > In that setup, configure `transport.allowedOrigins` explicitly. If your
250
- > proxy preserves the public `Host` header, configure `transport.allowedHosts`
251
- > too.
252
- >
253
- > Example:
254
- > ```json
255
- > {
256
- > "transport": {
257
- > "type": "http",
258
- > "host": "127.0.0.1",
259
- > "port": 8080,
260
- > "path": "/mcp",
261
- > "allowedHosts": ["mcp.example.com"],
262
- > "allowedOrigins": ["https://mcp.example.com"]
263
- > }
264
- > }
265
- > ```
266
- >
267
- > lazy-mcp intentionally does not trust `Forwarded` / `X-Forwarded-*` headers
268
- > by default. If proxy-aware origin reconstruction is ever added, it should be
269
- > behind an explicit trusted-proxy setting.
270
-
271
- **Via environment variables** (lowest precedence):
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.
272
211
 
273
- ```bash
274
- LAZY_MCP_TRANSPORT=http LAZY_MCP_PORT=3000 LAZY_MCP_AUTH_TOKEN=my-secret lazy-mcp
275
- ```
276
-
277
- **Precedence**: CLI flags > config file > environment variables > defaults.
212
+ ### Streamable HTTP Transport
278
213
 
279
- Once running, clients connect as a remote MCP server:
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:
280
215
 
281
216
  ```bash
282
- # Quick test with curl
283
- curl -X POST http://localhost:3000/mcp \
284
- -H "Content-Type: application/json" \
285
- -H "Accept: application/json, text/event-stream" \
286
- -H "Authorization: Bearer ${MY_API_KEY}" \
287
- -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
217
+ lazy-mcp --config servers.json --transport http --port 3000 --auth-token "my-secret"
288
218
  ```
289
219
 
290
- > **Note**: Server-initiated notifications via GET/SSE are not supported in stateless mode. Use POST for all requests.
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.
291
221
 
292
222
  ## Integration (Claude Desktop, OpenCode, Cursor, VS Code, and more)
293
223
 
294
- Replace multiple MCP server entries with one aggregated proxy:
295
-
296
- **Before** (5 separate MCP servers):
297
- ```json
298
- {
299
- "mcp": {
300
- "chrome-devtools": { "command": ["npx", "lazy-mcp@latest", "npx", "-y", "chrome-devtools-mcp@latest"] },
301
- "gitlab": { "command": ["npx", "lazy-mcp@latest", "npx", "mcp-remote@latest", "https://..."] },
302
- "grepai": { "command": ["npx", "lazy-mcp@latest", "grepai", "mcp-serve"] },
303
- "context7": { "command": ["npx", "-y", "@upstash/context7-mcp"] },
304
- "perplexity": { "command": ["npx", "-y", "@perplexity-ai/mcp-server"] }
305
- }
306
- }
307
- ```
224
+ Replace multiple MCP server entries in your client with one aggregated lazy-mcp proxy:
308
225
 
309
- **After** (Consolidated into 1 multi-server proxy):
310
226
  ```json
311
227
  {
312
228
  "mcp": {
@@ -319,38 +235,9 @@ Replace multiple MCP server entries with one aggregated proxy:
319
235
  }
320
236
  ```
321
237
 
322
- **HTTP mode** for clients that support remote MCP servers:
238
+ All downstream MCP servers live in `~/.config/lazy-mcp/servers.json`. **Result**: ~90% context reduction (from ~16K to ~1.5K tokens initially).
323
239
 
324
- Start lazy-mcp in HTTP mode (e.g. `lazy-mcp --config servers.json --transport http --port 3000`), then configure your client:
325
-
326
- ```json
327
- {
328
- "mcp": {
329
- "lazy-mcp": {
330
- "type": "remote",
331
- "url": "http://localhost:3000/mcp",
332
- "headers": {
333
- "Authorization": "Bearer ${LAZY_MCP_AUTH_TOKEN}"
334
- }
335
- }
336
- }
337
- }
338
- ```
339
-
340
- Where `~/.config/lazy-mcp/servers.json` contains all 5 servers:
341
- ```json
342
- {
343
- "servers": [
344
- { "name": "chrome-devtools", "description": "Chrome DevTools automation", "command": ["npx", "-y", "chrome-devtools-mcp@latest"] },
345
- { "name": "gitlab", "description": "GitLab API integration", "command": ["npx", "mcp-remote@latest", "https://..."] },
346
- { "name": "grepai", "description": "Search codebase", "command": ["grepai", "mcp-serve"] },
347
- { "name": "context7", "description": "Library documentation", "command": ["npx", "-y", "@upstash/context7-mcp"] },
348
- { "name": "perplexity", "description": "Web search", "command": ["npx", "-y", "@perplexity-ai/mcp-server"] }
349
- ]
350
- }
351
- ```
352
-
353
- **Result**: ~90% context reduction (from ~16K to ~1.5K tokens initially)
240
+ See [doc/INTEGRATION.md](./doc/INTEGRATION.md) for before/after examples and HTTP-mode client configuration.
354
241
 
355
242
  ## Example
356
243
 
@@ -417,494 +304,29 @@ Or with `ts-node` (no build needed, always reflects latest source):
417
304
 
418
305
  ## Releases
419
306
 
420
- Releases are fully automated via [semantic-release](https://semantic-release.gitbook.io/) on every push to `main`.
421
-
422
- ### How It Works
423
-
424
- 1. CI analyzes all commits since the last tag using [Conventional Commits](https://www.conventionalcommits.org/)
425
- 2. Determines the next version (`feat` → minor bump, `fix`/`perf`/`chore`/`refactor`/`test` → patch bump)
426
- 3. Updates `package.json`, `CHANGELOG.md`, `VERSION`, and `RELEASE_NOTES.md`
427
- 4. Commits those files as `chore(release): X.Y.Z` and pushes a `vX.Y.Z` tag
428
- 5. Creates a GitLab Release with the generated release notes
429
- 6. The `vX.Y.Z` tag triggers a separate pipeline that publishes the package to npm automatically
430
-
431
- No manual version bumping or tagging needed — just merge to `main` with conventional commit messages.
432
-
433
- ### Required CI/CD Variables
434
-
435
- Four CI/CD variables must be configured (GitLab → Project → Settings → CI/CD → Variables):
436
-
437
- | Variable | Description |
438
- |----------|-------------|
439
- | `GITLAB_RELEASE_TOKEN` | Project access token — pushes the release commit + tag to `main` and creates the GitLab Release |
440
- | `NPM_TOKEN` | npm automation token — publishes the package to the npm registry on tag pipelines |
441
- | `PYPI_TOKEN` | PyPI API token — publishes the package to PyPI on tag pipelines |
442
- | `CARGO_TOKEN` | crates.io API token — publishes the crate to crates.io on tag pipelines |
443
-
444
- #### `GITLAB_RELEASE_TOKEN`
445
-
446
- **Creating the token** (GitLab → Project → Settings → Access Tokens):
447
-
448
- | Setting | Value |
449
- |---------|-------|
450
- | Token name | `semantic-release-bot` (or any name) |
451
- | Role | **Developer** (push to a protected branch should be configured separately) |
452
- | Scopes | `api`, `write_repository` |
453
-
454
- **Adding the variable:**
455
-
456
- | Setting | Value |
457
- |---------|-------|
458
- | Key | `GITLAB_RELEASE_TOKEN` |
459
- | Masked | ✅ Yes |
460
- | Protected | ❌ No (must be available on the unprotected `main` pipeline) |
461
-
462
- > **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.
463
-
464
- #### `NPM_TOKEN`
465
-
466
- **Creating the token** (npmjs.com → Account → Access Tokens → Generate New Token):
467
-
468
- | Setting | Value |
469
- |---------|-------|
470
- | Token type | **Automation** (bypasses 2FA, suitable for CI) |
471
-
472
- **Adding the variable:**
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.
473
308
 
474
- | Setting | Value |
475
- |---------|-------|
476
- | Key | `NPM_TOKEN` |
477
- | Masked | ✅ Yes |
478
- | Protected | ✅ Yes (only needed on tag pipelines, which are protected) |
479
-
480
- #### `PYPI_TOKEN`
481
-
482
- **Creating the token** (pypi.org → Account Settings → API tokens → Add API token):
483
-
484
- | Setting | Value |
485
- |---------|-------|
486
- | Token name | `lazy-mcp-ci` (or any name) |
487
- | Scope | **Project: lazy-mcp** (restrict to this project after first publish; use "Entire account" for the very first publish) |
488
-
489
- **Adding the variable:**
490
-
491
- | Setting | Value |
492
- |---------|-------|
493
- | Key | `PYPI_TOKEN` |
494
- | Masked | ✅ Yes |
495
- | Protected | ✅ Yes (only needed on tag pipelines, which are protected) |
496
-
497
- #### `CARGO_TOKEN`
498
-
499
- **Creating the token** (crates.io → Account Settings → API Tokens → New Token):
500
-
501
- | Setting | Value |
502
- |---------|-------|
503
- | Token name | `lazy-mcp-ci` (or any name) |
504
- | Scopes | `publish-new`, `publish-update` |
505
-
506
- **Adding the variable:**
507
-
508
- | Setting | Value |
509
- |---------|-------|
510
- | Key | `CARGO_TOKEN` |
511
- | Masked | ✅ Yes |
512
- | 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`).
513
310
 
514
311
  ## Configuration Reference
515
312
 
516
- ### Server Configuration Fields
517
-
518
- | Field | Type | Required | Description |
519
- |-------|------|----------|-------------|
520
- | `name` | string | ✅ | Unique server identifier |
521
- | `description` | string | ✅ | Human-readable description |
522
- | `type` | "local" \| "remote" | Optional | Inferred from `url` (remote) or `command` (local) if omitted |
523
- | `command` | string[] \| string | For local | Command to execute (array format recommended) |
524
- | `args` | string[] | Optional | Arguments (only if command is string) |
525
- | `url` | string | For remote | HTTP/HTTPS URL |
526
- | `headers` | object | Optional | Static HTTP headers for remote servers |
527
- | `oauth` | object | Optional | OAuth 2.0 config for remote servers (see below) |
528
- | `env` | object | Optional | Environment variables (supports `${VAR}` and `{file:...}` expansion) |
529
- | `enabled` | boolean | Optional | Enable/disable server (default: true) |
530
- | `examples` | object[] | Optional | Usage examples shown in `list_servers` output |
531
- | `tags` | string[] | Optional | Capability tags for filtering (e.g. `"api"`, `"browser"`) |
532
- | `permissions` | object | Optional | Per-server `invoke_command` allow/deny rules (see [Permissions](#permissions)) |
533
-
534
- ### Permissions
535
-
536
- > **Experimental.** The permissions feature is experimental and may change in future releases. Only the `allow` and `deny` actions are supported today. An `ask` action (for interactive confirmation) is planned but not yet implemented — it requires host-side companion hooks that are still in design. Configs that use `"action": "ask"` will fail validation for now.
537
-
538
- The `permissions` block gives you per-server and global allow/deny rules over `invoke_command`. Rules are glob patterns matched (case-insensitively) against the command name. Calls that match a `deny` rule are blocked before they reach the downstream server and the agent receives a structured error (with `_meta.lazyMcpPolicy: "blocked"` so host plugins can hook into it).
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).
539
314
 
540
- #### Pattern syntax
315
+ Common top-level blocks:
541
316
 
542
- A tiny glob no regex, no alternation, no negation:
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
543
324
 
544
- | Token | Meaning |
545
- |-------|---------|
546
- | `*` | Zero or more characters |
547
- | `?` | Exactly one character |
548
- | Any other char | Literal (case-insensitive) |
549
-
550
- #### Resolution algorithm
551
-
552
- For each `invoke_command` call, lazy-mcp evaluates rules in this order. The first level that produces a match wins; within a level, the last matching rule wins.
553
-
554
- 1. Per-server rules (`servers[i].permissions.rules`)
555
- 2. Global rules (top-level `permissions.rules`), gated by the optional `server` glob (defaults to `*`)
556
- 3. Per-server `default` (`servers[i].permissions.default`)
557
- 4. Global `default` (top-level `permissions.default`)
558
- 5. Implicit `"allow"`
559
-
560
- #### Per-server permissions
561
-
562
- ```jsonc
563
- {
564
- "servers": [
565
- {
566
- "name": "gitlab",
567
- "url": "https://gitlab.com/api/v4/mcp",
568
- "permissions": {
569
- "default": "allow",
570
- "rules": [
571
- { "pattern": "list_*", "action": "allow" },
572
- { "pattern": "get_*", "action": "allow" },
573
- { "pattern": "search_*", "action": "allow" },
574
- { "pattern": "delete_*", "action": "deny",
575
- "message": "Destructive GitLab calls are blocked by policy." }
576
- ]
577
- }
578
- }
579
- ]
580
- }
581
- ```
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).
582
326
 
583
- #### Global (cross-server) permissions
327
+ Send `SIGHUP` to reload the config without restarting (`kill -HUP <pid>` — the PID is in `list_servers`).
584
328
 
585
- A top-level `permissions` block applies to every server unless a per-server rule matches first. Global rules can scope themselves to specific servers via the `server` glob:
586
-
587
- ```jsonc
588
- {
589
- "permissions": {
590
- "default": "allow",
591
- "rules": [
592
- { "server": "*", "pattern": "*delete*", "action": "deny" },
593
- { "server": "*", "pattern": "*destroy*", "action": "deny" }
594
- ]
595
- },
596
- "servers": [/* … */]
597
- }
598
- ```
599
-
600
- #### Read-only preset
601
-
602
- A handy "exploration" preset that denies everything by default but allows discovery-style commands:
603
-
604
- ```jsonc
605
- {
606
- "permissions": {
607
- "default": "deny",
608
- "rules": [
609
- { "server": "*", "pattern": "list_*", "action": "allow" },
610
- { "server": "*", "pattern": "get_*", "action": "allow" },
611
- { "server": "*", "pattern": "search_*", "action": "allow" }
612
- ]
613
- },
614
- "servers": [/* … */]
615
- }
616
- ```
617
-
618
- #### Hot reload
619
-
620
- Permission changes pick up via the existing `SIGHUP` config-reload path — no restart required. Edit `~/.config/lazy-mcp/servers.json`, then `kill -HUP <pid>` (the PID is included in the `list_servers` response).
621
-
622
- #### Soft-fail validation
623
-
624
- Invalid `permissions` blocks are soft-failed: per-server errors surface as `_configError` on the offending server (it appears unhealthy in `list_servers`); an invalid top-level block is stripped at load time with a warning on stderr, leaving the rest of the config working.
625
-
626
- A practical asymmetry to be aware of: a per-server `permissions` typo makes **that server** unreachable until fixed (it shows up unhealthy with a clear `_configError` in `list_servers`), while a global `permissions` typo is stripped and the rest of the config still loads — only the global rules are dropped, with a stderr warning. The reasoning: a per-server typo is most likely localised to the server you were editing, so failing fast there gives you the loudest signal; a global typo shouldn't take the entire proxy offline.
627
-
628
- #### Error contract
629
-
630
- Blocked calls return an `isError: true` tool result with a `_meta.lazyMcpPolicy: "blocked"` field that host plugins can intercept without parsing the message:
631
-
632
- ```jsonc
633
- {
634
- "content": [{
635
- "type": "text",
636
- "text": "🚫 Blocked by lazy-mcp policy: Tool 'gitlab:delete_project' is blocked by lazy-mcp policy\n\nDetails: Destructive GitLab calls are blocked by policy.\n\nTo allow this, remove the matching deny rule from your lazy-mcp config (~/.config/lazy-mcp/servers.json) and reload with: kill -HUP <pid>"
637
- }],
638
- "isError": true,
639
- "_meta": { "lazyMcpPolicy": "blocked" }
640
- }
641
- ```
642
-
643
- ### OAuth 2.0 Authentication
644
-
645
- 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.
646
-
647
- 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.
648
-
649
- Tokens are persisted to `~/.config/lazy-mcp/tokens.json` (mode `0600`) and refreshed automatically via `refresh_token` when available.
650
-
651
- **Minimal config** (fully automatic — discovery + dynamic registration):
652
- ```json
653
- {
654
- "name": "glean",
655
- "description": "Glean enterprise search",
656
- "url": "https://your-company.glean.com/mcp/default",
657
- "oauth": {}
658
- }
659
- ```
660
-
661
- **With a pre-registered client ID**:
662
- ```json
663
- {
664
- "name": "glean",
665
- "description": "Glean enterprise search",
666
- "url": "https://your-company.glean.com/mcp/default",
667
- "oauth": {
668
- "clientId": "${GLEAN_CLIENT_ID}",
669
- "extraHeaders": { "X-Glean-Auth-Type": "OAUTH" }
670
- }
671
- }
672
- ```
673
-
674
- **`oauth` object fields:**
675
-
676
- | Field | Type | Default | Description |
677
- |-------|------|---------|-------------|
678
- | `clientId` | string | — | OAuth client ID. If omitted, dynamic registration (RFC 7591) is attempted |
679
- | `clientSecret` | string | — | Client secret (omit for public-client / PKCE-only flows) |
680
- | `callbackPort` | number | `8947` | Local port for the OAuth redirect callback server |
681
- | `extraHeaders` | object | — | Additional headers added to every authenticated request (e.g. `X-Glean-Auth-Type`) |
682
-
683
- **How it works:**
684
-
685
- 1. Agent calls `invoke_command` (or `list_commands`) on an OAuth-protected server
686
- 2. lazy-mcp returns an `isError: true` response with the authorization URL
687
- 3. Agent presents the URL to the user: _"Open this URL to authorize Glean: https://..."_
688
- 4. User opens the URL in a browser and completes authorization
689
- 5. Browser redirects to `http://localhost:8947/callback` — lazy-mcp captures the token
690
- 6. Agent retries the original command — now succeeds transparently
691
-
692
- ### Command Format
693
-
694
- **Recommended** (OpenCode-compatible):
695
- ```json
696
- {
697
- "command": ["npx", "-y", "my-mcp-server", "--port", "3000"]
698
- }
699
- ```
700
-
701
- **Legacy** (still supported):
702
- ```json
703
- {
704
- "command": "npx",
705
- "args": ["-y", "my-mcp-server", "--port", "3000"]
706
- }
707
- ```
708
-
709
- ### Environment Variables
710
-
711
- Use `${VAR_NAME}` to reference environment variables:
712
-
713
- ```json
714
- {
715
- "env": {
716
- "API_KEY": "${MY_API_KEY}",
717
- "DEBUG": "true"
718
- },
719
- "headers": {
720
- "Authorization": "Bearer ${AUTH_TOKEN}"
721
- }
722
- }
723
- ```
724
-
725
- 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%`):
726
-
727
- ```json
728
- {
729
- "env": {
730
- "GITLAB_API_TOKEN": "{file:~/.secrets/gl-pat-token}",
731
- "OTHER_SECRET": "{file:/run/secrets/other-token}"
732
- },
733
- "headers": {
734
- "Authorization": "Bearer {file:~/.secrets/api-token}"
735
- }
736
- }
737
- ```
738
-
739
- On Windows you can use `%USERPROFILE%` or any other environment variable in the path:
740
-
741
- ```json
742
- {
743
- "env": {
744
- "API_TOKEN": "{file:%USERPROFILE%\\.secrets\\token}"
745
- }
746
- }
747
- ```
748
-
749
- 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`.
750
-
751
- **Security best practices for secret files:**
752
-
753
- - Set permissions to `0600` (owner read/write only): `chmod 0600 ~/.secrets/my-token`
754
- - Store secret files in a user-specific directory (e.g. `~/.secrets/`)
755
- - Never commit secret files to version control — add them to `.gitignore`
756
-
757
- ### Embed Server Summaries
758
-
759
- By default, the AI must call `list_servers` to discover which MCP servers are available.
760
- You can opt in to embedding server names and descriptions directly in the `list_servers` tool description so the
761
- AI sees them upfront — useful for routing requests like "check my email" to the correct server without an extra tool call.
762
-
763
- ```json
764
- {
765
- "servers": [...],
766
- "embedServerSummaries": {
767
- "enabled": true,
768
- "maxServers": 5
769
- }
770
- }
771
- ```
772
-
773
- | Field | Type | Default | Description |
774
- |-------|------|---------|-------------|
775
- | `embedServerSummaries.enabled` | boolean | — | Enable/disable embedding |
776
- | `embedServerSummaries.maxServers` | number | `10` | Maximum number of server summaries to embed |
777
-
778
- > **Note:** Tool descriptions are included in every request context window.
779
- > If your system prompt already instructs the LLM to call `list_servers`, this adds tokens with no benefit.
780
- > Per-server descriptions are unbounded, so one verbose description can significantly increase tool description size.
781
- > Making this opt-in lets you consciously accept that trade-off.
782
-
783
- ### Transport Configuration
784
-
785
- Configure how lazy-mcp exposes its MCP endpoint. By default, it uses stdio (for subprocess-based clients). Set `type: "http"` to run as an HTTP server.
786
-
787
- **Top-level configuration** (in `servers.json`):
788
-
789
- | Field | Type | Default | Description |
790
- |-------|------|---------|-------------|
791
- | `transport.type` | `"stdio"` \| `"http"` | `"stdio"` | Transport mode |
792
- | `transport.port` | number | `8080` | HTTP server port |
793
- | `transport.host` | string | `"127.0.0.1"` | HTTP server bind address. For TLS-terminating reverse proxies, keep localhost binding and configure `allowedOrigins` explicitly. |
794
- | `transport.path` | string | `"/mcp"` | MCP endpoint URL path |
795
- | `transport.authToken` | string | — | Bearer token for HTTP auth (supports `${VAR}` and `{file:...}` expansion) |
796
- | `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. |
797
- | `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. |
798
- | `transport.maxPayloadSize` | number | `4194304` | Maximum request body size in bytes. Requests exceeding this limit return HTTP 413 |
799
-
800
- **CLI flags** (override config values):
801
-
802
- | Flag | Env Variable | Description |
803
- |------|-------------|-------------|
804
- | `--transport` | `LAZY_MCP_TRANSPORT` | Transport type (`stdio` or `http`) |
805
- | `--port` | `LAZY_MCP_PORT` | HTTP server port |
806
- | `--host` | `LAZY_MCP_HOST` | HTTP server bind address |
807
- | `--path` | `LAZY_MCP_PATH` | MCP endpoint URL path |
808
- | `--auth-token` | `LAZY_MCP_AUTH_TOKEN` | Bearer token for HTTP auth |
809
- | `--request-timeout` | `LAZY_MCP_REQUEST_TIMEOUT` | Request timeout in ms for server calls, including MCP handshake requests and remote response parsing (default: 10000) |
810
- | `--max-payload-size` | `LAZY_MCP_MAX_PAYLOAD_SIZE` | Maximum request body size in bytes (default: 4194304) |
811
-
812
- When `authToken` is set, all HTTP requests must include `Authorization: Bearer <token>` — unauthenticated requests receive `401 Unauthorized`.
813
-
814
- ### Logging Configuration
815
-
816
- lazy-mcp now emits structured logs to **stderr** (stdout remains reserved for MCP protocol traffic).
817
-
818
- **Top-level configuration** (in `servers.json`):
819
-
820
- | Field | Type | Default | Description |
821
- |-------|------|---------|-------------|
822
- | `logging.level` | `"error"` \| `"info"` \| `"debug"` | `"info"` | Minimum log level |
823
- | `logging.format` | `"json"` \| `"plain"` | `"json"` | Log output format |
824
- | `logging.dumpBodies` | boolean | `false` | Enable debug request/response body dumps |
825
- | `logging.maxBodyLogBytes` | number | `8192` | Max body-dump size in bytes before truncation |
826
- | `logging.redactKeys` | string[] | — | Additional case-insensitive keys to redact (merged with built-in defaults) |
827
-
828
- Built-in redaction includes common secret keys like `authorization`, `token`, `access_token`, `refresh_token`, `client_secret`, and `headers.authorization`.
829
-
830
- Example:
831
-
832
- ```json
833
- {
834
- "servers": [
835
- {
836
- "name": "my-server",
837
- "description": "Example",
838
- "command": ["npx", "-y", "my-mcp-server"]
839
- }
840
- ],
841
- "logging": {
842
- "level": "debug",
843
- "format": "json",
844
- "dumpBodies": true,
845
- "maxBodyLogBytes": 4096,
846
- "redactKeys": ["my_custom_secret"]
847
- }
848
- }
849
- ```
850
-
851
- Typical access log event fields include:
852
- - client source (`clientIp`, optional `forwardedFor`)
853
- - request shape (`httpMethod`, `path`, `mcpMethod`, `lazyTool`)
854
- - downstream routing (`downstreamServer`, `downstreamCommand`)
855
- - outcome (`status`, `reason`, `durationMs`)
856
-
857
- ### Health Monitoring
858
-
859
- 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.
860
-
861
- Successful probes populate the discovery cache, so subsequent `list_commands` calls return instantly from cache.
862
-
863
- **Top-level configuration** (in `servers.json`):
864
-
865
- | Field | Type | Default | Description |
866
- |-------|------|---------|-------------|
867
- | `healthMonitor.enabled` | boolean | `true` | Enable/disable background health monitoring |
868
- | `healthMonitor.interval` | number | `30000` | Interval between health checks (ms) |
869
- | `healthMonitor.timeout` | number | `10000` | Timeout per server probe (ms) |
870
- | `healthMonitor.idleTimeout` | number | `300000` | Stop probing after this much inactivity (ms). `0` = never sleep (legacy) |
871
- | `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` |
872
-
873
- To disable health monitoring:
874
- ```json
875
- {
876
- "servers": [...],
877
- "healthMonitor": { "enabled": false }
878
- }
879
- ```
880
-
881
- To increase the request timeout (e.g. for slow remote servers):
882
- ```json
883
- {
884
- "servers": [...],
885
- "requestTimeout": 30000
886
- }
887
- ```
888
-
889
- ### Config Reload (SIGHUP)
890
-
891
- You can reload the configuration without restarting the process by sending a `SIGHUP` signal:
892
-
893
- ```bash
894
- kill -HUP $(pgrep -f lazy-mcp)
895
- ```
896
-
897
- This will:
898
- - Re-read and validate the config file
899
- - Add newly configured servers (lazy connection on first use)
900
- - Remove servers no longer in config (closes connections)
901
- - Reconnect servers whose config changed (updated URL, env, etc.)
902
- - Preserve unchanged servers (keeps existing connections and caches)
903
- - Restart the health monitor and probe all servers
904
-
905
- If the new config is invalid, the reload is rejected and the current config continues running. All reload activity is logged to stderr.
906
-
907
- > **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)**.
908
330
 
909
331
  ## Benefits
910
332
 
@@ -921,9 +343,12 @@ If the new config is invalid, the reload is rejected and the current config cont
921
343
 
922
344
  ## Documentation
923
345
 
924
- - **[CHANGELOG.md](./CHANGELOG.md)** - Version history and release notes
925
- - **[AGENTS.md](./AGENTS.md)** - Development guide for AI coding agents (build commands, code style, testing patterns)
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
926
350
  - **[doc/ARCHITECTURE.md](./doc/ARCHITECTURE.md)** - Architecture overview and design patterns
927
351
  - **[doc/CONTRIBUTING.md](./doc/CONTRIBUTING.md)** - Contributing guide with common development tasks
928
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.
929
- - **[Configuration Reference](#configuration-reference)** - Server configuration options (above)
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)