lazy-mcp 2.2.6 → 2.3.0

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,60 @@ 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
+ **Via environment variables** (lowest precedence):
193
+
194
+ ```bash
195
+ LAZY_MCP_TRANSPORT=http LAZY_MCP_PORT=3000 LAZY_MCP_AUTH_TOKEN=my-secret lazy-mcp
196
+ ```
197
+
198
+ **Precedence**: CLI flags > config file > environment variables > defaults.
199
+
200
+ Once running, clients connect as a remote MCP server:
201
+
202
+ ```bash
203
+ # Quick test with curl
204
+ curl -X POST http://localhost:3000/mcp \
205
+ -H "Content-Type: application/json" \
206
+ -H "Accept: application/json, text/event-stream" \
207
+ -H "Authorization: Bearer ${MY_API_KEY}" \
208
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
209
+ ```
210
+
211
+ > **Note**: Server-initiated notifications via GET/SSE are not supported in stateless mode. Use POST for all requests.
212
+
136
213
  ## Integration (Claude Desktop, OpenCode, Cursor, VS Code, and more)
137
214
 
138
215
  Replace multiple MCP server entries with one aggregated proxy:
@@ -163,6 +240,24 @@ Replace multiple MCP server entries with one aggregated proxy:
163
240
  }
164
241
  ```
165
242
 
243
+ **HTTP mode** — for clients that support remote MCP servers:
244
+
245
+ Start lazy-mcp in HTTP mode (e.g. `lazy-mcp --config servers.json --transport http --port 3000`), then configure your client:
246
+
247
+ ```json
248
+ {
249
+ "mcp": {
250
+ "lazy-mcp": {
251
+ "type": "remote",
252
+ "url": "http://localhost:3000/mcp",
253
+ "headers": {
254
+ "Authorization": "Bearer ${LAZY_MCP_AUTH_TOKEN}"
255
+ }
256
+ }
257
+ }
258
+ }
259
+ ```
260
+
166
261
  Where `~/.config/lazy-mcp/servers.json` contains all 5 servers:
167
262
  ```json
168
263
  {
@@ -196,6 +291,51 @@ npm run build
196
291
  npm test
197
292
  ```
198
293
 
294
+ ### Running from Local Source
295
+
296
+ Instead of `npx lazy-mcp@latest` (which downloads the published package), you can run directly from the cloned repo:
297
+
298
+ **Without building** — using `ts-node` (picks up source changes immediately):
299
+
300
+ ```bash
301
+ npm run dev -- --config ~/.config/lazy-mcp/servers.json
302
+ ```
303
+
304
+ **After building** — run the compiled output:
305
+
306
+ ```bash
307
+ npm run build
308
+ node dist/cli.js --config ~/.config/lazy-mcp/servers.json
309
+ # or equivalently:
310
+ npm start -- --config ~/.config/lazy-mcp/servers.json
311
+ ```
312
+
313
+ **In an MCP client config** — point directly at the local build:
314
+
315
+ ```json
316
+ {
317
+ "mcp": {
318
+ "lazy-mcp": {
319
+ "command": "node",
320
+ "args": ["/path/to/lazy-mcp/dist/cli.js", "--config", "~/.config/lazy-mcp/servers.json"]
321
+ }
322
+ }
323
+ }
324
+ ```
325
+
326
+ Or with `ts-node` (no build needed, always reflects latest source):
327
+
328
+ ```json
329
+ {
330
+ "mcp": {
331
+ "lazy-mcp": {
332
+ "command": "npx",
333
+ "args": ["ts-node", "/path/to/lazy-mcp/src/cli.ts", "--config", "~/.config/lazy-mcp/servers.json"]
334
+ }
335
+ }
336
+ }
337
+ ```
338
+
199
339
  ## Releases
200
340
 
201
341
  Releases are fully automated via [semantic-release](https://semantic-release.gitbook.io/) on every push to `main`.
@@ -393,6 +533,80 @@ Use `${VAR_NAME}` to reference environment variables:
393
533
  }
394
534
  ```
395
535
 
536
+ ### Transport Configuration
537
+
538
+ 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.
539
+
540
+ **Top-level configuration** (in `servers.json`):
541
+
542
+ | Field | Type | Default | Description |
543
+ |-------|------|---------|-------------|
544
+ | `transport.type` | `"stdio"` \| `"http"` | `"stdio"` | Transport mode |
545
+ | `transport.port` | number | `8080` | HTTP server port |
546
+ | `transport.host` | string | `"127.0.0.1"` | HTTP server bind address |
547
+ | `transport.path` | string | `"/mcp"` | MCP endpoint URL path |
548
+ | `transport.authToken` | string | — | Bearer token for HTTP auth (supports `${VAR}` expansion) |
549
+ | `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. Does not send CORS response headers. |
550
+ | `transport.allowedHosts` | string[] | — | List of explicitly allowed host domains for DNS rebinding protection |
551
+ | `transport.maxPayloadSize` | number | `4194304` | Maximum request body size in bytes. Requests exceeding this limit return HTTP 413 |
552
+
553
+ **CLI flags** (override config values):
554
+
555
+ | Flag | Env Variable | Description |
556
+ |------|-------------|-------------|
557
+ | `--transport` | `LAZY_MCP_TRANSPORT` | Transport type (`stdio` or `http`) |
558
+ | `--port` | `LAZY_MCP_PORT` | HTTP server port |
559
+ | `--host` | `LAZY_MCP_HOST` | HTTP server bind address |
560
+ | `--path` | `LAZY_MCP_PATH` | MCP endpoint URL path |
561
+ | `--auth-token` | `LAZY_MCP_AUTH_TOKEN` | Bearer token for HTTP auth |
562
+ | `--request-timeout` | `LAZY_MCP_REQUEST_TIMEOUT` | Request timeout in ms for server calls, including MCP handshake requests and remote response parsing (default: 10000) |
563
+ | `--max-payload-size` | `LAZY_MCP_MAX_PAYLOAD_SIZE` | Maximum request body size in bytes (default: 4194304) |
564
+
565
+ When `authToken` is set, all HTTP requests must include `Authorization: Bearer <token>` — unauthenticated requests receive `401 Unauthorized`.
566
+
567
+ ### Logging Configuration
568
+
569
+ lazy-mcp now emits structured logs to **stderr** (stdout remains reserved for MCP protocol traffic).
570
+
571
+ **Top-level configuration** (in `servers.json`):
572
+
573
+ | Field | Type | Default | Description |
574
+ |-------|------|---------|-------------|
575
+ | `logging.level` | `"error"` \| `"info"` \| `"debug"` | `"info"` | Minimum log level |
576
+ | `logging.format` | `"json"` \| `"plain"` | `"json"` | Log output format |
577
+ | `logging.dumpBodies` | boolean | `false` | Enable debug request/response body dumps |
578
+ | `logging.maxBodyLogBytes` | number | `8192` | Max body-dump size in bytes before truncation |
579
+ | `logging.redactKeys` | string[] | — | Additional case-insensitive keys to redact (merged with built-in defaults) |
580
+
581
+ Built-in redaction includes common secret keys like `authorization`, `token`, `access_token`, `refresh_token`, `client_secret`, and `headers.authorization`.
582
+
583
+ Example:
584
+
585
+ ```json
586
+ {
587
+ "servers": [
588
+ {
589
+ "name": "my-server",
590
+ "description": "Example",
591
+ "command": ["npx", "-y", "my-mcp-server"]
592
+ }
593
+ ],
594
+ "logging": {
595
+ "level": "debug",
596
+ "format": "json",
597
+ "dumpBodies": true,
598
+ "maxBodyLogBytes": 4096,
599
+ "redactKeys": ["my_custom_secret"]
600
+ }
601
+ }
602
+ ```
603
+
604
+ Typical access log event fields include:
605
+ - client source (`clientIp`, optional `forwardedFor`)
606
+ - request shape (`httpMethod`, `path`, `mcpMethod`, `lazyTool`)
607
+ - downstream routing (`downstreamServer`, `downstreamCommand`)
608
+ - outcome (`status`, `reason`, `durationMs`)
609
+
396
610
  ### Health Monitoring
397
611
 
398
612
  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 +621,9 @@ Successful probes populate the discovery cache, so subsequent `list_commands` ca
407
621
  | `healthMonitor.interval` | number | `30000` | Interval between health checks (ms) |
408
622
  | `healthMonitor.timeout` | number | `10000` | Timeout per server probe (ms) |
409
623
  | `healthMonitor.idleTimeout` | number | `300000` | Stop probing after this much inactivity (ms). `0` = never sleep (legacy) |
624
+ | `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
625
 
411
- To disable:
626
+ To disable health monitoring:
412
627
  ```json
413
628
  {
414
629
  "servers": [...],
@@ -416,6 +631,14 @@ To disable:
416
631
  }
417
632
  ```
418
633
 
634
+ To increase the request timeout (e.g. for slow remote servers):
635
+ ```json
636
+ {
637
+ "servers": [...],
638
+ "requestTimeout": 30000
639
+ }
640
+ ```
641
+
419
642
  ### Config Reload (SIGHUP)
420
643
 
421
644
  You can reload the configuration without restarting the process by sending a `SIGHUP` signal:
@@ -446,6 +669,7 @@ If the new config is invalid, the reload is rejected and the current config cont
446
669
  - **Flexible configuration** - Enable/disable servers on demand
447
670
  - **Environment variable support** - Secure credential management
448
671
  - **Both local and remote** - Support for subprocess and HTTP servers
672
+ - **Streamable HTTP transport** - Run as an HTTP server for remote client access
449
673
  - **Health monitoring** - Background probes detect broken servers before you hit them
450
674
 
451
675
  ## Documentation
@@ -454,4 +678,5 @@ If the new config is invalid, the reload is rejected and the current config cont
454
678
  - **[AGENTS.md](./AGENTS.md)** - Development guide for AI coding agents (build commands, code style, testing patterns)
455
679
  - **[doc/ARCHITECTURE.md](./doc/ARCHITECTURE.md)** - Architecture overview and design patterns
456
680
  - **[doc/CONTRIBUTING.md](./doc/CONTRIBUTING.md)** - Contributing guide with common development tasks
681
+ - **[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
682
  - **[Configuration Reference](#configuration-reference)** - Server configuration options (above)