pi-mcp-extension 1.10.2 → 1.11.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 +49 -3
- package/package.json +8 -5
- package/src/auth-lock-policy.ts +63 -0
- package/src/auth-lock.ts +341 -265
- package/src/authorize-dialog.ts +244 -0
- package/src/authorize-flow.ts +498 -366
- package/src/callback-server.ts +81 -43
- package/src/callback-url.ts +55 -43
- package/src/commands.ts +9 -5
- package/src/config.ts +258 -41
- package/src/connect-policy.ts +244 -0
- package/src/connection-coordinator.ts +348 -242
- package/src/debug-log-format.ts +194 -0
- package/src/debug-log.ts +25 -177
- package/src/index.ts +120 -163
- package/src/jsonc.ts +92 -58
- package/src/oauth-provider.ts +333 -302
- package/src/oauth-state-store.ts +136 -99
- package/src/oauth-status.ts +80 -0
- package/src/register-commands.ts +177 -0
- package/src/schema-convert.ts +473 -410
- package/src/schema-refs.ts +98 -0
- package/src/server-manager.ts +334 -372
- package/src/server-provenance.ts +78 -0
- package/src/server-status.ts +184 -0
- package/src/startup-auth.ts +80 -0
- package/src/stdio-process.ts +24 -28
- package/src/tool-bridge.ts +290 -258
- package/src/tool-discovery.ts +147 -0
- package/src/transport-factory.ts +156 -100
package/README.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/pi-mcp-extension)
|
|
4
4
|
[](https://github.com/irahardianto/pi-mcp-extension/blob/main/LICENSE)
|
|
5
|
+
[](https://github.com/irahardianto/pi-mcp-extension/actions/workflows/ci.yml)
|
|
6
|
+
[](https://qlty.sh/gh/irahardianto/projects/pi-mcp-extension)
|
|
7
|
+
[](https://qlty.sh/gh/irahardianto/projects/pi-mcp-extension)<br>
|
|
5
8
|
[](https://scorecard.dev/viewer/?uri=github.com/irahardianto/pi-mcp-extension)
|
|
6
9
|
[](https://www.bestpractices.dev/projects/15659)
|
|
7
10
|
|
|
@@ -148,21 +151,37 @@ In project-level `.pi/mcp.json`, `settings` keys carry no defaults — only the
|
|
|
148
151
|
|
|
149
152
|
### Per-Server Config
|
|
150
153
|
|
|
151
|
-
For Pi 1.x compatibility, server entries also accept `type` as an alias for `transport`, `"http"` as an alias for `"streamable-http"`, `oauth` as an alias for `auth`,
|
|
154
|
+
For Pi 1.x compatibility, server entries also accept `type` as an alias for `transport`, `"http"` as an alias for `"streamable-http"`, `oauth` as an alias for `auth`, `timeout` (in seconds) as an alias for `requestTimeoutMs`, and `enabled: false` as a request to keep the server out of the eager set (`lifecycle: "lazy"`). Pi's `auth.provider` (`/login` provider token) is **rejected** at load time instead of being silently downgraded to OAuth 2.1 — use `auth` (OAuth) or static `headers`.
|
|
152
155
|
|
|
153
156
|
| Field | Type | Default | Description |
|
|
154
157
|
|---|---|---|---|
|
|
155
158
|
| `transport` (or `type`) | `"stdio" \| "streamable-http" \| "http" \| "sse"` | `"stdio"` (`"streamable-http"` when `url` is set without `command`) | Transport protocol (`"http"` is normalized to `"streamable-http"`) |
|
|
156
159
|
| `command` | `string` | — | Executable to spawn (**required** for `stdio`) |
|
|
157
160
|
| `args` | `string[]` | `[]` | Arguments for the command |
|
|
161
|
+
| `cwd` | `string` | session working directory | Working directory for a `stdio` server (rejected on `streamable-http` / `sse`). A relative path resolves against the session working directory and a leading `~` expands to your home directory |
|
|
158
162
|
| `env` | `Record<string, string>` | — | Extra environment variables for the child process (literal values, no `${VAR}` interpolation) |
|
|
159
163
|
| `url` | `string` | — | Server URL (**required** for `streamable-http` and `sse`) |
|
|
160
164
|
| `headers` | `Record<string, string>` | — | Static headers sent with `streamable-http` and `sse` requests |
|
|
161
165
|
| `auth` (or `oauth`) | `object` | — | OAuth 2.1 configuration described below and in [OAUTH.md](./OAUTH.md) (`streamable-http` / `sse` only) |
|
|
162
166
|
| `lifecycle` | `"eager" \| "lazy"` | `"lazy"` (`"eager"` for `pi.registerMcpServer()` unless `enabled: false`) | `eager` = auto-start on `session_start`, `lazy` = connect on request via `mcp_connect`, `/mcp:start`, or `/mcp:auth` |
|
|
167
|
+
| `enabled` | `boolean` | `true` | Pi 1.3.0 switch: `false` keeps the entry configured but out of the eager set (normalized to `lifecycle: "lazy"`, even when `lifecycle: "eager"` is also set) |
|
|
163
168
|
| `requestTimeoutMs` (or `timeout`) | `number` | global setting | Per-server timeout override in ms (`timeout` is specified in seconds and multiplied by `1000`) |
|
|
164
169
|
| `healthCheckIntervalMs` | `number` | disabled | Opt-in ping interval in ms for proactive connection health monitoring |
|
|
165
170
|
|
|
171
|
+
### Project Override Entries
|
|
172
|
+
|
|
173
|
+
A trusted project's `.pi/mcp.json` may carry a Pi 1.3.0 **partial override** for a global server: an entry with no `command`, `url`, `type`, or `transport` changes only per-project settings and keeps the rest of the global entry, including credentials the project cannot set itself.
|
|
174
|
+
|
|
175
|
+
```jsonc
|
|
176
|
+
// ~/.pi/agent/mcp.json
|
|
177
|
+
{ "mcpServers": { "internal-tools": { "command": "internal-mcp", "lifecycle": "eager" } } }
|
|
178
|
+
|
|
179
|
+
// <project>/.pi/mcp.json — turn it off for this project only
|
|
180
|
+
{ "mcpServers": { "internal-tools": { "enabled": false } } }
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
An override may set `enabled`, `lifecycle`, and Pi's `exposure` / `toolExposure` (the two Pi-native exposure keys are accepted for Pi compatibility and ignored, because this extension decides tool activation itself). Any other key, or an override for a name no global entry defines, is reported as a config error naming the file and the server.
|
|
184
|
+
|
|
166
185
|
### OAuth Config
|
|
167
186
|
|
|
168
187
|
Configured under `auth` (or `oauth`). Token and client state are persisted per server in `<agent dir>/mcp-auth/<hash>.json` (`0700` directory, `0600` files on POSIX) with atomic writes and automatic backup of corrupt state files. See [OAUTH.md](./OAUTH.md) for architecture and troubleshooting details.
|
|
@@ -173,6 +192,7 @@ Configured under `auth` (or `oauth`). Token and client state are persisted per s
|
|
|
173
192
|
| `scope` | `string` | discovered | Requested scope when the server does not advertise one in `WWW-Authenticate` |
|
|
174
193
|
| `clientId` | `string` | dynamic registration | Pre-registered OAuth client ID (skips Dynamic Client Registration) |
|
|
175
194
|
| `clientSecret` | `string` | — | Secret for a pre-registered confidential client |
|
|
195
|
+
| `clientName` (or `oauth.clientName`) | `string` | `pi-mcp/<server name>` | `client_name` sent with Dynamic Client Registration, for servers that only accept known clients |
|
|
176
196
|
| `redirectUrl` (or `oauth.callbackUrl` / `oauth.callbackPort`) | `string` | `http://127.0.0.1:19876/callback` | Local HTTP loopback callback (`localhost`, `127.0.0.1`, or `::1` with path `/callback` and an explicit non-zero port; `callbackPort` maps to `http://127.0.0.1:<port>/callback`). Scans forward from `19876` when omitted; binds the exact host and port when configured. |
|
|
177
197
|
|
|
178
198
|
## Commands
|
|
@@ -182,13 +202,39 @@ Server-name arguments provide autocomplete suggestions from the configured MCP s
|
|
|
182
202
|
| Command | Description |
|
|
183
203
|
|---|---|
|
|
184
204
|
| `/mcp` | Show status summary of all configured servers |
|
|
185
|
-
| `/mcp <name>` | Show detailed status
|
|
205
|
+
| `/mcp <name>` | Show detailed status for one server: config source, transport/lifecycle, tool activation, OAuth credential status, failure hint, and stderr log |
|
|
186
206
|
| `/mcp:start <name>` | Connect a server and wait for tool discovery |
|
|
187
207
|
| `/mcp:stop <name>` | Cancel a pending connection, startup prompt, or authorization, or stop a running server and deactivate its tools |
|
|
188
208
|
| `/mcp:auth <name>` | Connect with stored OAuth credentials and client registration; request browser authorization only if needed |
|
|
189
209
|
| `/mcp:auth <name> --reset` | Discard stored tokens and client registration (re-running Dynamic Client Registration) and request fresh authorization |
|
|
190
210
|
|
|
191
|
-
Agent tools `mcp_status` and `mcp_connect` let Pi inspect configured servers and connect one named server without routine slash commands. `mcp_status` reports lifecycle, connection state, discovered and active tool counts, credential presence, and safe action hints without returning tokens, headers, or authorization URLs. `mcp_connect` activates discovered tools before returning success. Connection failures return fixed actionable categories instead of raw server response text.
|
|
211
|
+
Agent tools `mcp_status` and `mcp_connect` let Pi inspect configured servers and connect one named server without routine slash commands. `mcp_status` reports lifecycle, connection state, discovered and active tool counts, credential presence, config source, and safe action hints without returning tokens, headers, or authorization URLs. `mcp_connect` activates discovered tools before returning success. Connection failures return fixed actionable categories instead of raw server response text.
|
|
212
|
+
|
|
213
|
+
### Inspecting one server (`/mcp <name>`)
|
|
214
|
+
|
|
215
|
+
`/mcp <name>` answers "why is this server in this state?" without leaving Pi:
|
|
216
|
+
|
|
217
|
+
```text
|
|
218
|
+
Server: supabase
|
|
219
|
+
State: stopped
|
|
220
|
+
Retries: 5
|
|
221
|
+
Transport: streamable-http
|
|
222
|
+
Lifecycle: lazy
|
|
223
|
+
Source: global
|
|
224
|
+
Tools: 0/0 active
|
|
225
|
+
OAuth: credentials stored
|
|
226
|
+
Last error: Connection timed out after 30000ms
|
|
227
|
+
Hint: run with PI_MCP_DEBUG=1 pi to log redacted diagnostics, or report server issues at https://github.com/irahardianto/pi-mcp-extension/issues
|
|
228
|
+
|
|
229
|
+
Recent output:
|
|
230
|
+
(no stderr output from supabase)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
- `Source` is the entry that won: `global` (`<agent dir>/mcp.json`), `project` (`<cwd>/.pi/mcp.json`), or `extension` (`pi.registerMcpServer()`). `mcp_status` reports the same value as `source` on each row.
|
|
234
|
+
- When the project is untrusted, `Source` never says `project` and a `Project config: not loaded (<path>)` line explains why `mcp.json` in the workspace is not in effect.
|
|
235
|
+
- `Tools` compares discovered tools with the ones Pi has active; a server can be `ready` with `0/N active` when another extension restricts tools.
|
|
236
|
+
- `OAuth` appears only for servers with an `auth` config: `credentials stored`, `authorization may be required`, or `corrupt state file backed up (re-auth required)` (the corrupt file is backed up and the view still renders).
|
|
237
|
+
- A failed server adds one `Hint:` line: `/mcp:auth <name>` (plus `--reset` when stored credentials expired) for a missing/expired authorization, waiting for another Pi process holding the auth lock, or `PI_MCP_DEBUG=1 pi` plus the issue tracker for a connection failure.
|
|
192
238
|
|
|
193
239
|
### OAuth startup, headless/SSH, and concurrency behavior
|
|
194
240
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mcp-extension",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.1",
|
|
4
4
|
"description": "MCP (Model Context Protocol) client extension for the Pi coding agent — connect Pi to any MCP server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
]
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@modelcontextprotocol/sdk": "^1.32.
|
|
36
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
37
37
|
"zod": "^4.6.7"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
@@ -41,20 +41,23 @@
|
|
|
41
41
|
"@earendil-works/pi-coding-agent": "*",
|
|
42
42
|
"typebox": "*"
|
|
43
43
|
},
|
|
44
|
+
"overrides": {
|
|
45
|
+
"undici": "^8.12.3"
|
|
46
|
+
},
|
|
44
47
|
"devDependencies": {
|
|
45
48
|
"@eslint/js": "^10.0.1",
|
|
46
49
|
"@types/node": "^26.6.5",
|
|
47
50
|
"eslint": "^10.13.0",
|
|
48
51
|
"fast-check": "^4.10.2",
|
|
49
52
|
"tsx": "^4.19.4",
|
|
50
|
-
"typescript": "^
|
|
51
|
-
"typescript-eslint": "^8.
|
|
53
|
+
"typescript": "^6.0.3",
|
|
54
|
+
"typescript-eslint": "^8.72.0"
|
|
52
55
|
},
|
|
53
56
|
"scripts": {
|
|
54
57
|
"lint": "eslint .",
|
|
55
58
|
"typecheck": "tsc --noEmit",
|
|
56
59
|
"test": "node --import tsx/esm --test ./tests/*.test.ts",
|
|
57
|
-
"test:coverage": "node --import tsx/esm --test --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-lines=95 --test-coverage-branches=85 --test-coverage-functions=83 ./tests/*.test.ts",
|
|
60
|
+
"test:coverage": "node --import tsx/esm --test --experimental-test-coverage --test-reporter=spec --test-reporter-destination=stdout --test-reporter=lcov --test-reporter-destination=lcov.info --test-coverage-include='src/**' --test-coverage-lines=95 --test-coverage-branches=85 --test-coverage-functions=83 ./tests/*.test.ts",
|
|
58
61
|
"test:integration": "node --import tsx/esm --test ./tests/server-manager.test.ts"
|
|
59
62
|
}
|
|
60
63
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure lock-record and staleness policy for pi-mcp's cross-process OAuth
|
|
3
|
+
* authorization lock. No filesystem, clock, or process access.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
// ─── Constants & Schema ───────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export const HEARTBEAT_MS = 30_000;
|
|
11
|
+
export const STALE_MS = 90_000; // 3 × 30s heartbeat
|
|
12
|
+
|
|
13
|
+
export const LockRecordSchema = z.object({
|
|
14
|
+
v: z.literal(1),
|
|
15
|
+
pid: z.number().int().positive(),
|
|
16
|
+
hostname: z.string().min(1),
|
|
17
|
+
nonce: z.string().min(1),
|
|
18
|
+
createdAt: z.number(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export type LockRecord = z.output<typeof LockRecordSchema>;
|
|
22
|
+
|
|
23
|
+
// ─── Pure Staleness Policy ────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export function parseLockRecord(raw: string): LockRecord | null {
|
|
26
|
+
if (!raw) return null;
|
|
27
|
+
try {
|
|
28
|
+
const parsed: unknown = JSON.parse(raw);
|
|
29
|
+
const result = LockRecordSchema.safeParse(parsed);
|
|
30
|
+
return result.success ? result.data : null;
|
|
31
|
+
} catch {
|
|
32
|
+
// WHY: JSON.parse on a string throws SyntaxError if the lock file is corrupt or
|
|
33
|
+
// partially written. LockRecordSchema.safeParse does not throw. Unparseable lock
|
|
34
|
+
// files map to null so isLockStale() can fall back to mtime-based staleness.
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Pure policy determining whether an existing lock file is stale and safe to reclaim.
|
|
41
|
+
*/
|
|
42
|
+
export function isLockStale(
|
|
43
|
+
record: LockRecord | null,
|
|
44
|
+
mtimeMs: number,
|
|
45
|
+
now: number,
|
|
46
|
+
self: { hostname: string; isPidAlive: (pid: number) => boolean },
|
|
47
|
+
): boolean {
|
|
48
|
+
if (now - mtimeMs > STALE_MS) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (record === null) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (record.hostname === self.hostname && !self.isPidAlive(record.pid)) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function formatHolderDetails(record: LockRecord | null): string | undefined {
|
|
61
|
+
if (!record) return undefined;
|
|
62
|
+
return `pid ${record.pid} on ${record.hostname}`;
|
|
63
|
+
}
|