pi-mcp-adapter 1.5.0 → 2.0.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/ARCHITECTURE.md CHANGED
@@ -329,28 +329,30 @@
329
329
  ┌───────────────────────────────────────────────────────────────┐
330
330
  │ mcp tool execute() - executeCall() │
331
331
  │ │
332
- │ 1. Look up tool in toolMetadata
332
+ │ 1. Look up tool in toolMetadata (may be from cache)
333
333
  │ for (const [server, metadata] of toolMetadata) { │
334
334
  │ const found = metadata.find(m => m.name === toolName); │
335
335
  │ if (found) { serverName = server; toolMeta = found; } │
336
336
  │ } │
337
+ │ If not found: try prefix-match → lazy connect candidate │
337
338
  │ │
338
- │ 2. Get connection from ServerManager
339
+ │ 2. Ensure connection (lazy connect if needed)
339
340
  │ const connection = manager.getConnection(serverName); │
340
- │ if (!connection || connection.status !== "connected")
341
- return error;
341
+ │ if (!connection || status !== "connected")
342
+ check failure backoff (60s)
343
+ │ → connect, refresh metadata, re-resolve toolMeta │
342
344
  │ │
343
- │ 3. Call MCP server
345
+ │ 3. Call MCP server (with in-flight tracking)
346
+ │ incrementInFlight() + touch() │
344
347
  │ if (toolMeta.resourceUri) { │
345
- │ // Resource tool - use readResource │
346
348
  │ connection.client.readResource({ uri: resourceUri }); │
347
349
  │ } else { │
348
- │ // Regular tool - use callTool │
349
350
  │ connection.client.callTool({ │
350
351
  │ name: toolMeta.originalName, ◄── Original name! │
351
352
  │ arguments: args ?? {} │
352
353
  │ }); │
353
354
  │ } │
355
+ │ finally { decrementInFlight() + touch() } │
354
356
  └───────────────────────────┬───────────────────────────────────┘
355
357
 
356
358
 
@@ -411,6 +413,12 @@
411
413
 
412
414
  ## Lifecycle & Health Checks
413
415
 
416
+ Servers support three lifecycle modes:
417
+
418
+ - **lazy** (default): Don't connect at startup. Connect on first tool call. Subject to idle timeout (default 10 minutes). Cached metadata enables search/list without connections.
419
+ - **eager**: Connect at startup. No idle timeout by default. If the connection drops, reconnects on next use (like lazy).
420
+ - **keep-alive**: Connect at startup. No idle timeout. Auto-reconnects via health checks if the connection drops.
421
+
414
422
  ```
415
423
  ┌─────────────────────────────────────────────────────────────────────────────┐
416
424
  │ Session Start │
@@ -423,11 +431,17 @@
423
431
  │ 1. Load config │
424
432
  │ 2. Create ServerManager │
425
433
  │ 3. Create LifecycleManager │
426
- │ 4. Connect to each server
427
- │ 5. Collect tool metadata
428
- 6. Mark keep-alive servers
429
- 7. Start health checks
430
- 8. Set reconnect callback
434
+ │ 4. Load metadata cache
435
+ │ 5. Register all servers with
436
+ lifecycle manager
437
+ 6. Reconstruct toolMetadata
438
+ from cache (no connection)
439
+ │ 7. Connect only eager + │
440
+ │ keep-alive servers │
441
+ │ (or all on first-run │
442
+ │ bootstrap) │
443
+ │ 8. Start health checks │
444
+ │ 9. Set reconnect/idle callbacks │
431
445
  └─────────────────┬───────────────┘
432
446
 
433
447
 
@@ -439,25 +453,39 @@
439
453
  │ │ │ │
440
454
  │ │ for each keep-alive server: │ │
441
455
  │ │ if (status !== "connected"): │ │
442
- │ │ try reconnect │ │
443
- │ │ if success: call onReconnect callback │ │
444
- │ │ updates toolMetadata │ │
456
+ │ │ try reconnect → onReconnect → updates toolMetadata + cache │ │
457
+ │ │ │ │
458
+ │ │ for each non-keep-alive server: │ │
459
+ │ │ if idle > timeout and inFlight == 0: │ │
460
+ │ │ close connection → onIdleShutdown │ │
461
+ │ │ (toolMetadata preserved for search/list) │ │
462
+ │ │ │ │
463
+ │ └─────────────────────────────────────────────────────────────────────┘ │
464
+ │ │
465
+ │ ┌─────────────────────────────────────────────────────────────────────┐ │
466
+ │ │ Lazy Connection (on tool call) │ │
467
+ │ │ │ │
468
+ │ │ executeCall() finds tool in cached metadata but no connection: │ │
469
+ │ │ 1. Check failure backoff (60s) │ │
470
+ │ │ 2. Connect server │ │
471
+ │ │ 3. Refresh metadata from live connection │ │
472
+ │ │ 4. Re-resolve tool (may have changed since cache) │ │
473
+ │ │ 5. Execute tool call with in-flight tracking │ │
445
474
  │ │ │ │
446
- │ │ ┌──────────────────────────────────────────────────────────┐ │ │
447
- │ │ │ Note: Reconnect callback updates tool metadata so
448
- │ │ │ the mcp proxy tool can find tools after reconnection. │ │ │
449
- │ │ └──────────────────────────────────────────────────────────┘ │ │
475
+ │ │ Prefix-match fallback: if tool name has a server prefix but │ │
476
+ │ │ no metadata, try connecting the matching server │ │
450
477
  │ │ │ │
451
478
  │ └─────────────────────────────────────────────────────────────────────┘ │
452
479
  │ │
453
480
  │ ┌─────────────────────────────────────────────────────────────────────┐ │
454
481
  │ │ /mcp Commands │ │
455
482
  │ │ │ │
456
- │ │ /mcp status - Show all servers and their connection status │ │
457
- │ │ /mcp tools - List all available MCP tools │ │
458
- │ │ /mcp reconnect - Force reconnect all servers, update metadata │ │
483
+ │ │ /mcp status - Show all servers and connection status │ │
484
+ │ │ /mcp tools - List all available MCP tools │ │
485
+ │ │ /mcp reconnect - Force reconnect all servers │ │
486
+ │ │ /mcp reconnect <name> - Connect or reconnect a single server │ │
459
487
  │ │ │ │
460
- │ │ /mcp-auth <server> - Show OAuth setup instructions │ │
488
+ │ │ /mcp-auth <server> - Show OAuth setup instructions │ │
461
489
  │ │ │ │
462
490
  │ └─────────────────────────────────────────────────────────────────────┘ │
463
491
  │ │
@@ -468,9 +496,9 @@
468
496
  ┌─────────────────────────────────────────────────────────────────────────────┐
469
497
  │ Graceful Shutdown │
470
498
  │ │
471
- │ 1. Clear health check interval
472
- │ 2. Close all MCP connections (client + transport)
473
- │ 3. Tool calls via mcp proxy return "not connected" error
499
+ │ 1. Flush metadata cache for all connected servers
500
+ │ 2. Clear health check interval
501
+ │ 3. Close all MCP connections (client + transport)
474
502
  │ │
475
503
  └─────────────────────────────────────────────────────────────────────────────┘
476
504
  ```
@@ -498,17 +526,26 @@
498
526
  │ - HTTP transport (StreamableHTTP + SSE fallback)
499
527
  │ - connection pooling and deduplication
500
528
 
501
- ├── tool-registrar.ts Tool name collection (NOT registration!)
502
- │ - collectToolNames() - builds name list
529
+ ├── tool-registrar.ts MCP content transformation
503
530
  │ - transformMcpContent() - MCP → Pi content
504
531
 
505
- ├── resource-tools.ts Resource tool name collection
506
- │ - collectResourceToolNames()
532
+ ├── resource-tools.ts Resource name utilities
507
533
  │ - resourceNameToToolName()
508
534
 
509
- ├── lifecycle.ts Health checks, reconnection
510
- │ - keep-alive server tracking
511
- │ - reconnect callback for metadata updates
535
+ ├── metadata-cache.ts Persistent tool/resource metadata cache
536
+ │ - Per-server cache at ~/.pi/agent/mcp-cache.json
537
+ │ - Config hashing, staleness checks, reconstruction
538
+ │ - Read-merge-write for multi-session safety
539
+
540
+ ├── npx-resolver.ts npx binary resolution (skip npm parent process)
541
+ │ - Probes ~/.npm/_npx/ cache directly
542
+ │ - Persistent cache at ~/.pi/agent/mcp-npx-cache.json
543
+ │ - JS detection (extension + shebang)
544
+
545
+ ├── lifecycle.ts Health checks, reconnection, idle timeout
546
+ │ - keep-alive server tracking + auto-reconnect
547
+ │ - Idle timeout for lazy/eager servers
548
+ │ - Per-server and global timeout settings
512
549
 
513
550
  ├── oauth-handler.ts OAuth token file reading
514
551
  │ - getStoredTokens() from ~/.pi/agent/mcp-oauth/
@@ -568,5 +605,26 @@
568
605
  │ Lifecycle manager notifies extension after auto-reconnect. │
569
606
  │ Extension updates tool metadata so proxy can find tools. │
570
607
  │ │
608
+ │ 8. LAZY BY DEFAULT │
609
+ │ ─────────────── │
610
+ │ All servers default to lifecycle: "lazy". They only connect │
611
+ │ when a tool call needs them. Cached metadata enables │
612
+ │ search/list/describe without live connections. Idle servers │
613
+ │ are disconnected after 10 minutes (configurable). │
614
+ │ │
615
+ │ 9. METADATA CACHE │
616
+ │ ────────────── │
617
+ │ ~/.pi/agent/mcp-cache.json stores per-server tool/resource │
618
+ │ metadata with config hash validation and 7-day staleness. │
619
+ │ Cache stores original MCP names (not prefixed) — toolPrefix │
620
+ │ changes never invalidate the cache. Read-merge-write with │
621
+ │ per-process tmp files for multi-session safety. │
622
+ │ │
623
+ │ 10. NPX RESOLUTION │
624
+ │ ─────────────── │
625
+ │ npx-based servers resolve to direct binary paths, eliminating │
626
+ │ the ~143 MB npm parent process per server. Probes ~/.npm/_npx/ │
627
+ │ cache directly. JS files run via node, others executed directly. │
628
+ │ │
571
629
  └─────────────────────────────────────────────────────────────────────────────┘
572
630
  ```
package/CHANGELOG.md CHANGED
@@ -5,6 +5,44 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [2.0.0] - 2026-01-29
11
+
12
+ ### Changed
13
+ - **BREAKING: Lazy startup by default** - All servers now default to `lifecycle: "lazy"` and only connect when a tool call needs them. Previously all servers connected eagerly on session start. Set `lifecycle: "keep-alive"` or `lifecycle: "eager"` to restore the old behavior per-server.
14
+ - **Idle timeout** - Connected servers are automatically disconnected after 10 minutes of inactivity (configurable via `settings.idleTimeout` or per-server `idleTimeout`). Cached metadata keeps search/list working after disconnect. Set `idleTimeout: 0` to disable.
15
+ - `/mcp reconnect` accepts an optional server name to connect or reconnect a single server
16
+
17
+ ### Added
18
+ - **Metadata cache** - Tool and resource metadata persisted to `~/.pi/agent/mcp-cache.json`. Enables search/list/describe without live connections. Per-server config hashing with 7-day staleness. Multi-session safe via read-merge-write with per-process tmp files.
19
+ - **npx binary resolution** - Resolves npx package binaries to direct paths, eliminating the ~143 MB npm parent process per server. Persistent cache at `~/.pi/agent/mcp-npx-cache.json` with 24h TTL.
20
+ - **`mcp({ connect: "server-name" })` mode** - Explicitly trigger connection and metadata refresh for a named server
21
+ - **Failure backoff** - Servers that fail to connect are skipped for 60 seconds to avoid repeated connection storms
22
+ - **In-flight tracking** - Active tool calls prevent idle timeout from shutting down a server mid-request
23
+ - **Prefix-match fallback** - Tool calls with unrecognized names try to match a server prefix and lazy-connect the matching server
24
+ - Lifecycle options: `lazy` (default), `eager` (connect at startup, no auto-reconnect), `keep-alive` (unchanged)
25
+ - Per-server `idleTimeout` override and global `settings.idleTimeout`
26
+ - First-run bootstrap: connects all servers on first session to populate the cache
27
+
28
+ ### Fixed
29
+ - Connection close race condition: concurrent close + connect no longer orphans server processes
30
+ - **Fuzzy tool name matching** - Hyphens and underscores are treated as equivalent during tool lookup. MCP tools like `resolve-library-id` are now found when called as `resolve_library_id`, which LLMs naturally guess since the prefix separator is `_`.
31
+ - **Better "tool not found" errors** - When a server is identified (via prefix match or override) but the tool isn't found, the error now lists that server's available tools so the LLM can self-correct immediately instead of needing a separate list call
32
+
33
+ ## [1.6.0] - 2026-01-29
34
+
35
+ ### Added
36
+ - **Unified pi tool search** - `mcp({ search: "..." })` now searches both MCP tools and Pi tools (from installed extensions)
37
+ - Pi tools appear first in results with `[pi tool]` prefix
38
+ - Details object includes `server: "pi"` for pi tools
39
+ - Banner image for README
40
+
41
+ ## [1.5.1] - 2026-01-26
42
+
43
+ ### Changed
44
+ - Added `pi-package` keyword for npm discoverability (pi v0.50.0 package system)
45
+
8
46
  ## [1.5.0] - 2026-01-22
9
47
 
10
48
  ### Changed
package/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ <p>
2
+ <img src="banner.png" alt="pi-mcp-adapter" width="1100">
3
+ </p>
4
+
1
5
  # Pi MCP Adapter
2
6
 
3
7
  Use MCP servers with [Pi](https://github.com/badlogic/pi-mono/) without burning your context window.
@@ -10,15 +14,15 @@ Mario wrote about [why you might not need MCP](https://mariozechner.at/posts/202
10
14
 
11
15
  His take: skip MCP entirely, write simple CLI tools instead.
12
16
 
13
- But the MCP ecosystem has useful stuff - databases, browsers, APIs. This adapter gives you access without the bloat. One proxy tool (~200 tokens) instead of hundreds. The agent discovers what it needs on-demand.
17
+ But the MCP ecosystem has useful stuff - databases, browsers, APIs. This adapter gives you access without the bloat. One proxy tool (~200 tokens) instead of hundreds. The agent discovers what it needs on-demand. Servers only start when you actually use them.
14
18
 
15
19
  ## Install
16
20
 
17
21
  ```bash
18
- npx pi-mcp-adapter
22
+ pi install npm:pi-mcp-adapter
19
23
  ```
20
24
 
21
- This downloads the extension to `~/.pi/agent/extensions/pi-mcp-adapter/`, installs dependencies, and configures Pi to load it. Restart Pi after installation.
25
+ Restart Pi after installation.
22
26
 
23
27
  ## Quick Start
24
28
 
@@ -29,14 +33,13 @@ Create `~/.pi/agent/mcp.json`:
29
33
  "mcpServers": {
30
34
  "chrome-devtools": {
31
35
  "command": "npx",
32
- "args": ["-y", "chrome-devtools-mcp@latest"],
33
- "lifecycle": "keep-alive"
36
+ "args": ["-y", "chrome-devtools-mcp@latest"]
34
37
  }
35
38
  }
36
39
  }
37
40
  ```
38
41
 
39
- The LLM searches for tools, sees their schemas, and calls them:
42
+ Servers are **lazy by default** they won't connect until you actually call one of their tools. The adapter caches tool metadata so search and describe work without live connections.
40
43
 
41
44
  ```
42
45
  mcp({ search: "screenshot" })
@@ -67,7 +70,8 @@ Two calls instead of 26 tools cluttering the context.
67
70
  "my-server": {
68
71
  "command": "npx",
69
72
  "args": ["-y", "some-mcp-server"],
70
- "lifecycle": "keep-alive"
73
+ "lifecycle": "lazy",
74
+ "idleTimeout": 10
71
75
  }
72
76
  }
73
77
  }
@@ -78,13 +82,40 @@ Two calls instead of 26 tools cluttering the context.
78
82
  | `command` | Executable for stdio transport |
79
83
  | `args` | Command arguments |
80
84
  | `env` | Environment variables (`${VAR}` interpolation) |
85
+ | `cwd` | Working directory |
81
86
  | `url` | HTTP endpoint (StreamableHTTP with SSE fallback) |
82
87
  | `auth` | `"bearer"` or `"oauth"` |
83
- | `bearerToken` / `bearerTokenEnv` | Token or env var |
84
- | `lifecycle` | `"keep-alive"` for auto-reconnect |
88
+ | `bearerToken` / `bearerTokenEnv` | Token or env var name |
89
+ | `lifecycle` | `"lazy"` (default), `"eager"`, or `"keep-alive"` |
90
+ | `idleTimeout` | Minutes before idle disconnect (overrides global) |
85
91
  | `exposeResources` | Expose MCP resources as tools (default: true) |
86
92
  | `debug` | Show server stderr (default: false) |
87
93
 
94
+ ### Lifecycle Modes
95
+
96
+ - **`lazy`** (default) — Don't connect at startup. Connect on first tool call. Disconnect after idle timeout. Cached metadata keeps search/list working without connections.
97
+ - **`eager`** — Connect at startup but don't auto-reconnect if the connection drops. No idle timeout by default (set `idleTimeout` explicitly to enable).
98
+ - **`keep-alive`** — Connect at startup. Auto-reconnect via health checks. No idle timeout. Use for servers you always need available.
99
+
100
+ ### Settings
101
+
102
+ ```json
103
+ {
104
+ "settings": {
105
+ "toolPrefix": "server",
106
+ "idleTimeout": 10
107
+ },
108
+ "mcpServers": { }
109
+ }
110
+ ```
111
+
112
+ | Setting | Description |
113
+ |---------|-------------|
114
+ | `toolPrefix` | `"server"` (default), `"short"` (strips `-mcp` suffix), or `"none"` |
115
+ | `idleTimeout` | Global idle timeout in minutes (default: 10, 0 to disable) |
116
+
117
+ Per-server `idleTimeout` overrides the global setting.
118
+
88
119
  ### Import Existing Configs
89
120
 
90
121
  Already have MCP set up elsewhere? Import it:
@@ -98,16 +129,24 @@ Already have MCP set up elsewhere? Import it:
98
129
 
99
130
  Supported: `cursor`, `claude-code`, `claude-desktop`, `vscode`, `windsurf`, `codex`
100
131
 
132
+ ### Project Config
133
+
134
+ Add `.pi/mcp.json` in a project root for project-specific servers. Project config overrides global and imported servers.
135
+
101
136
  ## Usage
102
137
 
103
138
  | Mode | Example |
104
139
  |------|---------|
140
+ | Status | `mcp({ })` |
141
+ | List server | `mcp({ server: "name" })` |
105
142
  | Search | `mcp({ search: "screenshot navigate" })` |
106
143
  | Describe | `mcp({ describe: "tool_name" })` |
107
144
  | Call | `mcp({ tool: "...", args: '{"key": "value"}' })` |
108
- | Status | `mcp({ })` or `mcp({ server: "name" })` |
145
+ | Connect | `mcp({ connect: "server-name" })` |
146
+
147
+ Search includes both MCP tools and Pi tools (from extensions). Pi tools appear first with `[pi tool]` prefix. Space-separated words are OR'd.
109
148
 
110
- Search includes parameter schemas by default. Space-separated words are OR'd.
149
+ Tool names are fuzzy-matched on hyphens and underscores `context7_resolve_library_id` finds `context7_resolve-library-id`.
111
150
 
112
151
  ## Commands
113
152
 
@@ -115,19 +154,24 @@ Search includes parameter schemas by default. Space-separated words are OR'd.
115
154
  |---------|--------------|
116
155
  | `/mcp` | Server status |
117
156
  | `/mcp tools` | List all tools |
118
- | `/mcp reconnect` | Reconnect servers |
157
+ | `/mcp reconnect` | Reconnect all servers |
158
+ | `/mcp reconnect <server>` | Connect or reconnect a single server |
119
159
  | `/mcp-auth <server>` | OAuth setup |
120
160
 
121
161
  ## How It Works
122
162
 
123
- See [ARCHITECTURE.md](./ARCHITECTURE.md) for details. Short version:
163
+ See [ARCHITECTURE.md](./ARCHITECTURE.md) for the full picture. Short version:
124
164
 
125
- - One `mcp` tool in context (~200 tokens)
126
- - Tool metadata stored in a map, looked up at call time
127
- - MCP server validates arguments
165
+ - One `mcp` tool in context (~200 tokens) instead of hundreds
166
+ - Servers are lazy by default they connect on first tool call, not at startup
167
+ - Tool metadata is cached to disk so search/list/describe work without live connections
168
+ - Idle servers disconnect after 10 minutes (configurable), reconnect automatically on next use
169
+ - npx-based servers resolve to direct binary paths, skipping the ~143 MB npm parent process
170
+ - MCP server validates arguments, not the adapter
128
171
  - Keep-alive servers get health checks and auto-reconnect
129
172
 
130
173
  ## Limitations
131
174
 
132
175
  - OAuth tokens obtained externally (no browser flow)
133
176
  - No automatic token refresh
177
+ - Cross-session server sharing not yet implemented (each Pi session runs its own server processes)
package/cli.js CHANGED
@@ -19,6 +19,8 @@ const FILES = [
19
19
  "tool-registrar.ts",
20
20
  "resource-tools.ts",
21
21
  "lifecycle.ts",
22
+ "metadata-cache.ts",
23
+ "npx-resolver.ts",
22
24
  "oauth-handler.ts",
23
25
  "package.json",
24
26
  "tsconfig.json",
package/config.ts CHANGED
@@ -109,6 +109,7 @@ function extractServers(config: unknown, kind: ImportKind): Record<string, Serve
109
109
  switch (kind) {
110
110
  case "claude-desktop":
111
111
  case "claude-code":
112
+ case "codex":
112
113
  servers = obj.mcpServers;
113
114
  break;
114
115
  case "cursor":
@@ -116,14 +117,10 @@ function extractServers(config: unknown, kind: ImportKind): Record<string, Serve
116
117
  case "vscode":
117
118
  servers = obj.mcpServers ?? obj["mcp-servers"];
118
119
  break;
119
- case "codex":
120
- servers = obj.mcpServers;
121
- break;
122
120
  default:
123
121
  return {};
124
122
  }
125
123
 
126
- // Validate servers is a plain object
127
124
  if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
128
125
  return {};
129
126
  }