lazy-mcp 2.2.7 → 2.3.1

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
@@ -12,9 +12,11 @@ A client-agnostic proxy that converts normal MCP servers to use a lazy-loading p
12
12
  - [How It Works](#how-it-works)
13
13
  - [Installation](#installation)
14
14
  - [Usage](#usage)
15
+ - [Streamable HTTP Transport](#streamable-http-transport)
15
16
  - [Integration (Claude Desktop, OpenCode, Cursor, VS Code, and more)](#integration-claude-desktop-opencode-cursor-vs-code-and-more)
16
17
  - [Example](#example)
17
18
  - [Development](#development)
19
+ - [Running from Local Source](#running-from-local-source)
18
20
  - [Releases](#releases)
19
21
  - [How It Works](#how-it-works)
20
22
  - [Required CI/CD Variables](#required-cicd-variables)
@@ -27,6 +29,8 @@ A client-agnostic proxy that converts normal MCP servers to use a lazy-loading p
27
29
  - [OAuth 2.0 Authentication](#oauth-20-authentication)
28
30
  - [Command Format](#command-format)
29
31
  - [Environment Variables](#environment-variables)
32
+ - [Transport Configuration](#transport-configuration)
33
+ - [Logging Configuration](#logging-configuration)
30
34
  - [Health Monitoring](#health-monitoring)
31
35
  - [Config Reload (SIGHUP)](#config-reload-sighup)
32
36
  - [Benefits](#benefits)
@@ -43,6 +47,7 @@ A client-agnostic proxy that converts normal MCP servers to use a lazy-loading p
43
47
  - **Built-in OAuth 2.0 + PKCE**: Authenticate with OAuth-protected remote servers without a browser — works in sandboxed agent environments
44
48
  - **Background Health Monitoring**: Probes all servers on startup and periodically; `list_servers` shows accurate health from the first call
45
49
  - **Hot Config Reload**: Send `SIGHUP` to reload config without restarting — add, remove, or update servers on the fly
50
+ - **Streamable HTTP Transport**: Run as an HTTP server — expose lazy-mcp over the network so remote clients can connect via `POST /mcp`
46
51
 
47
52
  ## How It Works
48
53
 
@@ -86,6 +91,24 @@ Or install globally (locks to specific version):
86
91
  npm install -g lazy-mcp
87
92
  ```
88
93
 
94
+ **Docker / Podman**:
95
+
96
+ ```bash
97
+ docker build -t lazy-mcp .
98
+ docker compose up
99
+ ```
100
+
101
+ The image compiles the TypeScript CLI during `docker build`, so this works from a clean checkout without a prebuilt `dist/` directory.
102
+
103
+ Or with Podman:
104
+
105
+ ```bash
106
+ podman build -t lazy-mcp .
107
+ podman compose up
108
+ ```
109
+
110
+ 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.
111
+
89
112
  ## Usage
90
113
 
91
114
  Create a configuration file at `~/.config/lazy-mcp/servers.json`:
@@ -133,6 +156,86 @@ LAZY_MCP_CONFIG=~/.config/lazy-mcp/servers.json npx lazy-mcp@latest
133
156
  lazy-mcp --config ~/.config/lazy-mcp/servers.json
134
157
  ```
135
158
 
159
+ ### Streamable HTTP Transport
160
+
161
+ 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:
162
+
163
+ **Via config file** — add a `transport` block to `servers.json`:
164
+
165
+ ```json
166
+ {
167
+ "servers": [
168
+ { "name": "my-server", "description": "My MCP server", "command": ["python", "server.py"] }
169
+ ],
170
+ "transport": {
171
+ "type": "http",
172
+ "port": 3000,
173
+ "host": "localhost",
174
+ "path": "/mcp",
175
+ "authToken": "${MY_API_KEY}"
176
+ }
177
+ }
178
+ ```
179
+
180
+ **Via CLI flags** (override config values):
181
+
182
+ ```bash
183
+ # Start HTTP server on port 3000
184
+ lazy-mcp --config servers.json --transport http --port 3000
185
+
186
+ # With custom host, path, and auth
187
+ lazy-mcp --config servers.json --transport http --port 3000 --host 127.0.0.1 --path /mcp --auth-token "my-secret"
188
+ ```
189
+
190
+ **Security note:** `--host 0.0.0.0` exposes the HTTP server on all network interfaces. Use it only inside Docker or trusted networks.
191
+
192
+ > **Reverse proxy note:** The default Origin check uses the scheme visible to
193
+ > lazy-mcp's own socket plus the incoming `Host` header. If you run lazy-mcp
194
+ > behind a TLS-terminating reverse proxy, the backend hop is usually plain
195
+ > HTTP, so the implicit same-origin shortcut may reject public HTTPS origins.
196
+ > In that setup, configure `transport.allowedOrigins` explicitly. If your
197
+ > proxy preserves the public `Host` header, configure `transport.allowedHosts`
198
+ > too.
199
+ >
200
+ > Example:
201
+ > ```json
202
+ > {
203
+ > "transport": {
204
+ > "type": "http",
205
+ > "host": "127.0.0.1",
206
+ > "port": 8080,
207
+ > "path": "/mcp",
208
+ > "allowedHosts": ["mcp.example.com"],
209
+ > "allowedOrigins": ["https://mcp.example.com"]
210
+ > }
211
+ > }
212
+ > ```
213
+ >
214
+ > lazy-mcp intentionally does not trust `Forwarded` / `X-Forwarded-*` headers
215
+ > by default. If proxy-aware origin reconstruction is ever added, it should be
216
+ > behind an explicit trusted-proxy setting.
217
+
218
+ **Via environment variables** (lowest precedence):
219
+
220
+ ```bash
221
+ LAZY_MCP_TRANSPORT=http LAZY_MCP_PORT=3000 LAZY_MCP_AUTH_TOKEN=my-secret lazy-mcp
222
+ ```
223
+
224
+ **Precedence**: CLI flags > config file > environment variables > defaults.
225
+
226
+ Once running, clients connect as a remote MCP server:
227
+
228
+ ```bash
229
+ # Quick test with curl
230
+ curl -X POST http://localhost:3000/mcp \
231
+ -H "Content-Type: application/json" \
232
+ -H "Accept: application/json, text/event-stream" \
233
+ -H "Authorization: Bearer ${MY_API_KEY}" \
234
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
235
+ ```
236
+
237
+ > **Note**: Server-initiated notifications via GET/SSE are not supported in stateless mode. Use POST for all requests.
238
+
136
239
  ## Integration (Claude Desktop, OpenCode, Cursor, VS Code, and more)
137
240
 
138
241
  Replace multiple MCP server entries with one aggregated proxy:
@@ -163,6 +266,24 @@ Replace multiple MCP server entries with one aggregated proxy:
163
266
  }
164
267
  ```
165
268
 
269
+ **HTTP mode** — for clients that support remote MCP servers:
270
+
271
+ Start lazy-mcp in HTTP mode (e.g. `lazy-mcp --config servers.json --transport http --port 3000`), then configure your client:
272
+
273
+ ```json
274
+ {
275
+ "mcp": {
276
+ "lazy-mcp": {
277
+ "type": "remote",
278
+ "url": "http://localhost:3000/mcp",
279
+ "headers": {
280
+ "Authorization": "Bearer ${LAZY_MCP_AUTH_TOKEN}"
281
+ }
282
+ }
283
+ }
284
+ }
285
+ ```
286
+
166
287
  Where `~/.config/lazy-mcp/servers.json` contains all 5 servers:
167
288
  ```json
168
289
  {
@@ -196,6 +317,51 @@ npm run build
196
317
  npm test
197
318
  ```
198
319
 
320
+ ### Running from Local Source
321
+
322
+ Instead of `npx lazy-mcp@latest` (which downloads the published package), you can run directly from the cloned repo:
323
+
324
+ **Without building** — using `ts-node` (picks up source changes immediately):
325
+
326
+ ```bash
327
+ npm run dev -- --config ~/.config/lazy-mcp/servers.json
328
+ ```
329
+
330
+ **After building** — run the compiled output:
331
+
332
+ ```bash
333
+ npm run build
334
+ node dist/cli.js --config ~/.config/lazy-mcp/servers.json
335
+ # or equivalently:
336
+ npm start -- --config ~/.config/lazy-mcp/servers.json
337
+ ```
338
+
339
+ **In an MCP client config** — point directly at the local build:
340
+
341
+ ```json
342
+ {
343
+ "mcp": {
344
+ "lazy-mcp": {
345
+ "command": "node",
346
+ "args": ["/path/to/lazy-mcp/dist/cli.js", "--config", "~/.config/lazy-mcp/servers.json"]
347
+ }
348
+ }
349
+ }
350
+ ```
351
+
352
+ Or with `ts-node` (no build needed, always reflects latest source):
353
+
354
+ ```json
355
+ {
356
+ "mcp": {
357
+ "lazy-mcp": {
358
+ "command": "npx",
359
+ "args": ["ts-node", "/path/to/lazy-mcp/src/cli.ts", "--config", "~/.config/lazy-mcp/servers.json"]
360
+ }
361
+ }
362
+ }
363
+ ```
364
+
199
365
  ## Releases
200
366
 
201
367
  Releases are fully automated via [semantic-release](https://semantic-release.gitbook.io/) on every push to `main`.
@@ -393,6 +559,80 @@ Use `${VAR_NAME}` to reference environment variables:
393
559
  }
394
560
  ```
395
561
 
562
+ ### Transport Configuration
563
+
564
+ 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.
565
+
566
+ **Top-level configuration** (in `servers.json`):
567
+
568
+ | Field | Type | Default | Description |
569
+ |-------|------|---------|-------------|
570
+ | `transport.type` | `"stdio"` \| `"http"` | `"stdio"` | Transport mode |
571
+ | `transport.port` | number | `8080` | HTTP server port |
572
+ | `transport.host` | string | `"127.0.0.1"` | HTTP server bind address. For TLS-terminating reverse proxies, keep localhost binding and configure `allowedOrigins` explicitly. |
573
+ | `transport.path` | string | `"/mcp"` | MCP endpoint URL path |
574
+ | `transport.authToken` | string | — | Bearer token for HTTP auth (supports `${VAR}` expansion) |
575
+ | `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. |
576
+ | `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. |
577
+ | `transport.maxPayloadSize` | number | `4194304` | Maximum request body size in bytes. Requests exceeding this limit return HTTP 413 |
578
+
579
+ **CLI flags** (override config values):
580
+
581
+ | Flag | Env Variable | Description |
582
+ |------|-------------|-------------|
583
+ | `--transport` | `LAZY_MCP_TRANSPORT` | Transport type (`stdio` or `http`) |
584
+ | `--port` | `LAZY_MCP_PORT` | HTTP server port |
585
+ | `--host` | `LAZY_MCP_HOST` | HTTP server bind address |
586
+ | `--path` | `LAZY_MCP_PATH` | MCP endpoint URL path |
587
+ | `--auth-token` | `LAZY_MCP_AUTH_TOKEN` | Bearer token for HTTP auth |
588
+ | `--request-timeout` | `LAZY_MCP_REQUEST_TIMEOUT` | Request timeout in ms for server calls, including MCP handshake requests and remote response parsing (default: 10000) |
589
+ | `--max-payload-size` | `LAZY_MCP_MAX_PAYLOAD_SIZE` | Maximum request body size in bytes (default: 4194304) |
590
+
591
+ When `authToken` is set, all HTTP requests must include `Authorization: Bearer <token>` — unauthenticated requests receive `401 Unauthorized`.
592
+
593
+ ### Logging Configuration
594
+
595
+ lazy-mcp now emits structured logs to **stderr** (stdout remains reserved for MCP protocol traffic).
596
+
597
+ **Top-level configuration** (in `servers.json`):
598
+
599
+ | Field | Type | Default | Description |
600
+ |-------|------|---------|-------------|
601
+ | `logging.level` | `"error"` \| `"info"` \| `"debug"` | `"info"` | Minimum log level |
602
+ | `logging.format` | `"json"` \| `"plain"` | `"json"` | Log output format |
603
+ | `logging.dumpBodies` | boolean | `false` | Enable debug request/response body dumps |
604
+ | `logging.maxBodyLogBytes` | number | `8192` | Max body-dump size in bytes before truncation |
605
+ | `logging.redactKeys` | string[] | — | Additional case-insensitive keys to redact (merged with built-in defaults) |
606
+
607
+ Built-in redaction includes common secret keys like `authorization`, `token`, `access_token`, `refresh_token`, `client_secret`, and `headers.authorization`.
608
+
609
+ Example:
610
+
611
+ ```json
612
+ {
613
+ "servers": [
614
+ {
615
+ "name": "my-server",
616
+ "description": "Example",
617
+ "command": ["npx", "-y", "my-mcp-server"]
618
+ }
619
+ ],
620
+ "logging": {
621
+ "level": "debug",
622
+ "format": "json",
623
+ "dumpBodies": true,
624
+ "maxBodyLogBytes": 4096,
625
+ "redactKeys": ["my_custom_secret"]
626
+ }
627
+ }
628
+ ```
629
+
630
+ Typical access log event fields include:
631
+ - client source (`clientIp`, optional `forwardedFor`)
632
+ - request shape (`httpMethod`, `path`, `mcpMethod`, `lazyTool`)
633
+ - downstream routing (`downstreamServer`, `downstreamCommand`)
634
+ - outcome (`status`, `reason`, `durationMs`)
635
+
396
636
  ### Health Monitoring
397
637
 
398
638
  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.
@@ -407,8 +647,9 @@ Successful probes populate the discovery cache, so subsequent `list_commands` ca
407
647
  | `healthMonitor.interval` | number | `30000` | Interval between health checks (ms) |
408
648
  | `healthMonitor.timeout` | number | `10000` | Timeout per server probe (ms) |
409
649
  | `healthMonitor.idleTimeout` | number | `300000` | Stop probing after this much inactivity (ms). `0` = never sleep (legacy) |
650
+ | `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` |
410
651
 
411
- To disable:
652
+ To disable health monitoring:
412
653
  ```json
413
654
  {
414
655
  "servers": [...],
@@ -416,6 +657,14 @@ To disable:
416
657
  }
417
658
  ```
418
659
 
660
+ To increase the request timeout (e.g. for slow remote servers):
661
+ ```json
662
+ {
663
+ "servers": [...],
664
+ "requestTimeout": 30000
665
+ }
666
+ ```
667
+
419
668
  ### Config Reload (SIGHUP)
420
669
 
421
670
  You can reload the configuration without restarting the process by sending a `SIGHUP` signal:
@@ -446,6 +695,7 @@ If the new config is invalid, the reload is rejected and the current config cont
446
695
  - **Flexible configuration** - Enable/disable servers on demand
447
696
  - **Environment variable support** - Secure credential management
448
697
  - **Both local and remote** - Support for subprocess and HTTP servers
698
+ - **Streamable HTTP transport** - Run as an HTTP server for remote client access
449
699
  - **Health monitoring** - Background probes detect broken servers before you hit them
450
700
 
451
701
  ## Documentation
@@ -454,4 +704,5 @@ If the new config is invalid, the reload is rejected and the current config cont
454
704
  - **[AGENTS.md](./AGENTS.md)** - Development guide for AI coding agents (build commands, code style, testing patterns)
455
705
  - **[doc/ARCHITECTURE.md](./doc/ARCHITECTURE.md)** - Architecture overview and design patterns
456
706
  - **[doc/CONTRIBUTING.md](./doc/CONTRIBUTING.md)** - Contributing guide with common development tasks
707
+ - **[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.
457
708
  - **[Configuration Reference](#configuration-reference)** - Server configuration options (above)