pi-mcp-extension 1.10.2 → 1.11.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
@@ -2,6 +2,9 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/pi-mcp-extension.svg)](https://www.npmjs.com/package/pi-mcp-extension)
4
4
  [![license](https://img.shields.io/npm/l/pi-mcp-extension.svg)](https://github.com/irahardianto/pi-mcp-extension/blob/main/LICENSE)
5
+ [![CI](https://github.com/irahardianto/pi-mcp-extension/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/irahardianto/pi-mcp-extension/actions/workflows/ci.yml)
6
+ [![Code Coverage](https://qlty.sh/gh/irahardianto/projects/pi-mcp-extension/coverage.svg)](https://qlty.sh/gh/irahardianto/projects/pi-mcp-extension)
7
+ [![Maintainability](https://qlty.sh/gh/irahardianto/projects/pi-mcp-extension/maintainability.svg)](https://qlty.sh/gh/irahardianto/projects/pi-mcp-extension)<br>
5
8
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/irahardianto/pi-mcp-extension/badge)](https://scorecard.dev/viewer/?uri=github.com/irahardianto/pi-mcp-extension)
6
9
  [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/15659/badge)](https://www.bestpractices.dev/projects/15659)
7
10
 
@@ -182,13 +185,39 @@ Server-name arguments provide autocomplete suggestions from the configured MCP s
182
185
  | Command | Description |
183
186
  |---|---|
184
187
  | `/mcp` | Show status summary of all configured servers |
185
- | `/mcp <name>` | Show detailed status and stderr log for a specific server |
188
+ | `/mcp <name>` | Show detailed status for one server: config source, transport/lifecycle, tool activation, OAuth credential status, failure hint, and stderr log |
186
189
  | `/mcp:start <name>` | Connect a server and wait for tool discovery |
187
190
  | `/mcp:stop <name>` | Cancel a pending connection, startup prompt, or authorization, or stop a running server and deactivate its tools |
188
191
  | `/mcp:auth <name>` | Connect with stored OAuth credentials and client registration; request browser authorization only if needed |
189
192
  | `/mcp:auth <name> --reset` | Discard stored tokens and client registration (re-running Dynamic Client Registration) and request fresh authorization |
190
193
 
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.
194
+ 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.
195
+
196
+ ### Inspecting one server (`/mcp <name>`)
197
+
198
+ `/mcp <name>` answers "why is this server in this state?" without leaving Pi:
199
+
200
+ ```text
201
+ Server: supabase
202
+ State: stopped
203
+ Retries: 5
204
+ Transport: streamable-http
205
+ Lifecycle: lazy
206
+ Source: global
207
+ Tools: 0/0 active
208
+ OAuth: credentials stored
209
+ Last error: Connection timed out after 30000ms
210
+ 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
211
+
212
+ Recent output:
213
+ (no stderr output from supabase)
214
+ ```
215
+
216
+ - `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.
217
+ - 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.
218
+ - `Tools` compares discovered tools with the ones Pi has active; a server can be `ready` with `0/N active` when another extension restricts tools.
219
+ - `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).
220
+ - 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
221
 
193
222
  ### OAuth startup, headless/SSH, and concurrency behavior
194
223
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mcp-extension",
3
- "version": "1.10.2",
3
+ "version": "1.11.0",
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.1",
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": "^5.7.0",
51
- "typescript-eslint": "^8.71.2"
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
+ }