@phi-code-admin/phi-code 0.85.0 → 0.87.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/CHANGELOG.md +83 -0
- package/README.md +92 -123
- package/config/routing.example.json +129 -0
- package/config/routing.schema.json +58 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +6 -6
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/config-selector.js +1 -1
- package/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +2 -2
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +9 -8
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/utils/pi-user-agent.d.ts.map +1 -1
- package/dist/utils/pi-user-agent.js +4 -1
- package/dist/utils/pi-user-agent.js.map +1 -1
- package/docs/compaction.md +11 -11
- package/docs/custom-provider.md +4 -4
- package/docs/development.md +2 -2
- package/docs/extensions.md +47 -47
- package/docs/fork-policy.md +81 -0
- package/docs/index.md +7 -7
- package/docs/json.md +3 -3
- package/docs/keybindings.md +5 -5
- package/docs/models.md +6 -6
- package/docs/packages.md +37 -37
- package/docs/prompt-templates.md +4 -4
- package/docs/providers.md +9 -9
- package/docs/quickstart.md +25 -25
- package/docs/rpc.md +3 -3
- package/docs/sdk.md +34 -34
- package/docs/session-format.md +4 -4
- package/docs/sessions.md +11 -11
- package/docs/settings.md +9 -9
- package/docs/shell-aliases.md +2 -2
- package/docs/skills.md +9 -9
- package/docs/terminal-setup.md +7 -7
- package/docs/termux.md +6 -6
- package/docs/themes.md +9 -9
- package/docs/tmux.md +3 -3
- package/docs/tui.md +8 -8
- package/docs/usage.md +39 -39
- package/docs/windows.md +2 -2
- package/extensions/phi/agents.ts +12 -117
- package/extensions/phi/benchmark.ts +129 -49
- package/extensions/phi/browser.ts +10 -37
- package/extensions/phi/btw/btw.ts +1 -7
- package/extensions/phi/chrome/index.ts +1283 -741
- package/extensions/phi/commit.ts +9 -14
- package/extensions/phi/goal/index.ts +10 -33
- package/extensions/phi/init.ts +37 -47
- package/extensions/phi/keys.ts +2 -7
- package/extensions/phi/mcp/callback-server.ts +162 -165
- package/extensions/phi/mcp/config.ts +122 -136
- package/extensions/phi/mcp/errors.ts +18 -23
- package/extensions/phi/mcp/index.ts +322 -355
- package/extensions/phi/mcp/oauth-provider.ts +289 -289
- package/extensions/phi/mcp/server-manager.ts +390 -413
- package/extensions/phi/mcp/tool-bridge.ts +381 -415
- package/extensions/phi/memory.ts +2 -2
- package/extensions/phi/models.ts +27 -26
- package/extensions/phi/orchestrator.ts +343 -266
- package/extensions/phi/productivity.ts +4 -2
- package/extensions/phi/providers/agent-def.ts +128 -0
- package/extensions/phi/providers/alibaba.ts +56 -7
- package/extensions/phi/providers/context-window.ts +4 -1
- package/extensions/phi/providers/live-models.ts +5 -20
- package/extensions/phi/providers/orchestrator-helpers.ts +19 -5
- package/extensions/phi/providers/phase-machine.ts +220 -0
- package/extensions/phi/setup.ts +196 -169
- package/extensions/phi/skill-loader.ts +18 -21
- package/extensions/phi/smart-router.ts +6 -6
- package/extensions/phi/web-search.ts +90 -50
- package/package.json +2 -1
|
@@ -12,20 +12,19 @@
|
|
|
12
12
|
* - Image/audio/resource content → text description passthrough
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
15
|
+
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
16
16
|
import { CallToolResultSchema, ListToolsResultSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
17
|
-
import * as Type from "typebox";
|
|
18
|
-
import type { TSchema } from "typebox";
|
|
19
17
|
import type { ExtensionAPI } from "phi-code";
|
|
20
|
-
import {
|
|
18
|
+
import type { TSchema } from "typebox";
|
|
19
|
+
import * as Type from "typebox";
|
|
21
20
|
import type { Settings } from "./config.js";
|
|
21
|
+
import { McpError } from "./errors.js";
|
|
22
22
|
|
|
23
23
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
24
24
|
|
|
25
25
|
/** Subset of Pi's ExtensionAPI used by the bridge. */
|
|
26
26
|
export type PiExtensionAPI = Pick<ExtensionAPI, "registerTool" | "getActiveTools" | "setActiveTools">;
|
|
27
27
|
|
|
28
|
-
|
|
29
28
|
// ─── Schema Conversion ────────────────────────────────────────────────────────
|
|
30
29
|
|
|
31
30
|
/**
|
|
@@ -40,162 +39,148 @@ export type PiExtensionAPI = Pick<ExtensionAPI, "registerTool" | "getActiveTools
|
|
|
40
39
|
* - allOf → TypeBox Intersect
|
|
41
40
|
* Falls back to Type.Any() for unresolvable $ref or missing type.
|
|
42
41
|
*/
|
|
43
|
-
export function convertJsonSchemaToTypebox(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
objOpts["additionalProperties"] = false;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
base = Type.Object(props, objOpts as any);
|
|
189
|
-
break;
|
|
190
|
-
}
|
|
191
|
-
default: {
|
|
192
|
-
// Truly unsupported or missing type field
|
|
193
|
-
base = Type.Any(opts);
|
|
194
|
-
break;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return isNullable ? Type.Union([base, Type.Null()]) : base;
|
|
42
|
+
export function convertJsonSchemaToTypebox(schema: unknown, depth = 0, defs?: Record<string, unknown>): TSchema {
|
|
43
|
+
// Guard against infinite recursion and malformed schemas
|
|
44
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema) || depth > 10) {
|
|
45
|
+
return Type.Any();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const s = schema as Record<string, unknown>;
|
|
49
|
+
const description = typeof s.description === "string" ? s.description : undefined;
|
|
50
|
+
const opts = description ? { description } : {};
|
|
51
|
+
|
|
52
|
+
// Extract $defs / definitions for $ref resolution (carried through recursive calls)
|
|
53
|
+
const resolvedDefs: Record<string, unknown> = {
|
|
54
|
+
...((s.$defs ?? s.definitions) as Record<string, unknown> | undefined),
|
|
55
|
+
...defs,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// ── Handle $ref ──────────────────────────────────────────────────────────
|
|
59
|
+
if (typeof s.$ref === "string") {
|
|
60
|
+
const ref = s.$ref as string;
|
|
61
|
+
let resolved: unknown;
|
|
62
|
+
|
|
63
|
+
// Local references: #/$defs/Foo, #/definitions/Foo
|
|
64
|
+
if (ref.startsWith("#/")) {
|
|
65
|
+
const parts = ref.slice(2).split("/");
|
|
66
|
+
if (parts[0] === "$defs" || parts[0] === "definitions") {
|
|
67
|
+
const key = parts.slice(1).join("/");
|
|
68
|
+
resolved = resolvedDefs[key];
|
|
69
|
+
} else {
|
|
70
|
+
// Fallback: try walking the defs map by the last part
|
|
71
|
+
const key = parts[parts.length - 1]!;
|
|
72
|
+
resolved = resolvedDefs[key];
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
// External $ref — cannot resolve, fall back
|
|
76
|
+
console.warn(`[pi-mcp] Cannot resolve external $ref "${ref}", using Type.Any()`);
|
|
77
|
+
return Type.Any(opts);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!resolved) {
|
|
81
|
+
console.warn(`[pi-mcp] Could not resolve $ref "${ref}", using Type.Any()`);
|
|
82
|
+
return Type.Any(opts);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Merge description from referencing schema into resolved schema
|
|
86
|
+
const merged = { ...(resolved as Record<string, unknown>) };
|
|
87
|
+
if (description && !merged.description) {
|
|
88
|
+
merged.description = description;
|
|
89
|
+
}
|
|
90
|
+
return convertJsonSchemaToTypebox(merged, depth + 1, resolvedDefs);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Handle oneOf / anyOf → TypeBox Union ─────────────────────────────────
|
|
94
|
+
if (Array.isArray(s.oneOf)) {
|
|
95
|
+
const members = (s.oneOf as unknown[]).map((sub) => convertJsonSchemaToTypebox(sub, depth + 1, resolvedDefs));
|
|
96
|
+
return members.length === 1 ? members[0]! : Type.Union(members, opts);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (Array.isArray(s.anyOf)) {
|
|
100
|
+
const members = (s.anyOf as unknown[]).map((sub) => convertJsonSchemaToTypebox(sub, depth + 1, resolvedDefs));
|
|
101
|
+
return members.length === 1 ? members[0]! : Type.Union(members, opts);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Handle allOf → TypeBox Intersect ─────────────────────────────────────
|
|
105
|
+
if (Array.isArray(s.allOf)) {
|
|
106
|
+
const members = (s.allOf as unknown[]).map((sub) => convertJsonSchemaToTypebox(sub, depth + 1, resolvedDefs));
|
|
107
|
+
return members.length === 1 ? members[0]! : Type.Intersect(members, opts);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Handle nullable types: { "type": ["string", "null"] }
|
|
111
|
+
const rawType = s.type;
|
|
112
|
+
const type = Array.isArray(rawType)
|
|
113
|
+
? (rawType.find((t) => t !== "null") as string | undefined)
|
|
114
|
+
: typeof rawType === "string"
|
|
115
|
+
? rawType
|
|
116
|
+
: undefined;
|
|
117
|
+
|
|
118
|
+
const isNullable = Array.isArray(rawType) && rawType.includes("null");
|
|
119
|
+
|
|
120
|
+
let base: TSchema;
|
|
121
|
+
|
|
122
|
+
switch (type) {
|
|
123
|
+
case "string": {
|
|
124
|
+
const enumVals = s.enum;
|
|
125
|
+
if (Array.isArray(enumVals) && enumVals.every((v) => typeof v === "string")) {
|
|
126
|
+
// TypeBox doesn't have a built-in StringEnum — use Union of Literals
|
|
127
|
+
base = Type.Union(
|
|
128
|
+
(enumVals as string[]).map((v) => Type.Literal(v)),
|
|
129
|
+
opts,
|
|
130
|
+
);
|
|
131
|
+
} else {
|
|
132
|
+
base = Type.String(opts);
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case "number":
|
|
137
|
+
case "integer":
|
|
138
|
+
base = Type.Number(opts);
|
|
139
|
+
break;
|
|
140
|
+
case "boolean":
|
|
141
|
+
base = Type.Boolean(opts);
|
|
142
|
+
break;
|
|
143
|
+
case "null":
|
|
144
|
+
base = Type.Null(opts);
|
|
145
|
+
break;
|
|
146
|
+
case "array": {
|
|
147
|
+
const items = s.items;
|
|
148
|
+
base = Type.Array(items ? convertJsonSchemaToTypebox(items, depth + 1, resolvedDefs) : Type.Unknown(), opts);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case "object": {
|
|
152
|
+
const properties = s.properties as Record<string, unknown> | undefined;
|
|
153
|
+
const required = new Set<string>(Array.isArray(s.required) ? (s.required as string[]) : []);
|
|
154
|
+
const additionalProperties = s.additionalProperties;
|
|
155
|
+
|
|
156
|
+
if (!properties) {
|
|
157
|
+
// Open object — passthrough as Any to avoid over-constraining
|
|
158
|
+
base = Type.Record(Type.String(), Type.Unknown(), opts);
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const props: Record<string, TSchema> = {};
|
|
163
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
164
|
+
const converted = convertJsonSchemaToTypebox(value, depth + 1, resolvedDefs);
|
|
165
|
+
props[key] = required.has(key) ? converted : Type.Optional(converted);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const objOpts: Record<string, unknown> = { ...opts };
|
|
169
|
+
if (additionalProperties === false) {
|
|
170
|
+
objOpts.additionalProperties = false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
base = Type.Object(props, objOpts as any);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
default: {
|
|
177
|
+
// Truly unsupported or missing type field
|
|
178
|
+
base = Type.Any(opts);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return isNullable ? Type.Union([base, Type.Null()]) : base;
|
|
199
184
|
}
|
|
200
185
|
|
|
201
186
|
// ─── Tool Name Sanitization ───────────────────────────────────────────────────
|
|
@@ -209,16 +194,14 @@ const MAX_TOOL_NAME_LEN = 64;
|
|
|
209
194
|
* If truncation is needed, the last 8 chars are replaced with a hash to avoid collisions.
|
|
210
195
|
*/
|
|
211
196
|
export function buildToolName(prefix: string, serverName: string, toolName: string): string {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
.slice(0, 8);
|
|
221
|
-
return safe.slice(0, MAX_TOOL_NAME_LEN - 9) + "_" + hash;
|
|
197
|
+
const raw = `${prefix}_${serverName}_${toolName}`;
|
|
198
|
+
const safe = raw.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
199
|
+
if (safe.length <= MAX_TOOL_NAME_LEN) return safe;
|
|
200
|
+
// Truncate with hash suffix to prevent collisions on long names
|
|
201
|
+
const hash = Math.abs(safe.split("").reduce((acc, c) => ((acc << 5) - acc + c.charCodeAt(0)) | 0, 0))
|
|
202
|
+
.toString(36)
|
|
203
|
+
.slice(0, 8);
|
|
204
|
+
return `${safe.slice(0, MAX_TOOL_NAME_LEN - 9)}_${hash}`;
|
|
222
205
|
}
|
|
223
206
|
|
|
224
207
|
// ─── Content Conversion ───────────────────────────────────────────────────────
|
|
@@ -226,48 +209,48 @@ export function buildToolName(prefix: string, serverName: string, toolName: stri
|
|
|
226
209
|
type PiTextContent = { type: "text"; text: string };
|
|
227
210
|
|
|
228
211
|
function convertMcpContent(items: unknown[]): PiTextContent[] {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
212
|
+
return items.map((item: any) => {
|
|
213
|
+
if (!item || typeof item !== "object") {
|
|
214
|
+
return { type: "text", text: String(item) };
|
|
215
|
+
}
|
|
216
|
+
switch (item.type) {
|
|
217
|
+
case "text":
|
|
218
|
+
return { type: "text", text: String(item.text ?? "") };
|
|
219
|
+
case "image":
|
|
220
|
+
return {
|
|
221
|
+
type: "text",
|
|
222
|
+
text: `[Image: ${item.mimeType ?? "unknown"}, base64 encoded]`,
|
|
223
|
+
};
|
|
224
|
+
case "audio":
|
|
225
|
+
return {
|
|
226
|
+
type: "text",
|
|
227
|
+
text: `[Audio: ${item.mimeType ?? "unknown"}, base64 encoded]`,
|
|
228
|
+
};
|
|
229
|
+
case "resource": {
|
|
230
|
+
const r = item.resource;
|
|
231
|
+
if (r?.text) return { type: "text", text: r.text };
|
|
232
|
+
if (r?.blob) return { type: "text", text: `[Resource blob: ${r.uri}]` };
|
|
233
|
+
return { type: "text", text: `[Resource: ${r?.uri ?? "unknown"}]` };
|
|
234
|
+
}
|
|
235
|
+
default:
|
|
236
|
+
return { type: "text", text: JSON.stringify(item) };
|
|
237
|
+
}
|
|
238
|
+
});
|
|
256
239
|
}
|
|
257
240
|
|
|
258
241
|
// ─── Tool Listing ─────────────────────────────────────────────────────────────
|
|
259
242
|
|
|
260
243
|
export interface McpToolDefinition {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
244
|
+
name: string;
|
|
245
|
+
description?: string;
|
|
246
|
+
inputSchema: Record<string, unknown>;
|
|
247
|
+
annotations?: {
|
|
248
|
+
readOnlyHint?: boolean;
|
|
249
|
+
destructiveHint?: boolean;
|
|
250
|
+
idempotentHint?: boolean;
|
|
251
|
+
openWorldHint?: boolean;
|
|
252
|
+
title?: string;
|
|
253
|
+
};
|
|
271
254
|
}
|
|
272
255
|
|
|
273
256
|
/**
|
|
@@ -275,33 +258,30 @@ export interface McpToolDefinition {
|
|
|
275
258
|
* The MCP spec mandates clients follow nextCursor until exhausted.
|
|
276
259
|
* Includes a max-page guard to prevent infinite loops from broken servers.
|
|
277
260
|
*/
|
|
278
|
-
export async function listAllTools(
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
} while (cursor);
|
|
303
|
-
|
|
304
|
-
return tools;
|
|
261
|
+
export async function listAllTools(client: Client, requestTimeoutMs: number): Promise<McpToolDefinition[]> {
|
|
262
|
+
const tools: McpToolDefinition[] = [];
|
|
263
|
+
let cursor: string | undefined;
|
|
264
|
+
const MAX_PAGES = 100;
|
|
265
|
+
let pageCount = 0;
|
|
266
|
+
|
|
267
|
+
do {
|
|
268
|
+
if (pageCount >= MAX_PAGES) {
|
|
269
|
+
console.warn(
|
|
270
|
+
`[pi-mcp] tools/list pagination exceeded ${MAX_PAGES} pages, stopping. The server may be malfunctioning.`,
|
|
271
|
+
);
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
const result = await client.request(
|
|
275
|
+
{ method: "tools/list", params: cursor ? { cursor } : {} },
|
|
276
|
+
ListToolsResultSchema,
|
|
277
|
+
{ timeout: requestTimeoutMs },
|
|
278
|
+
);
|
|
279
|
+
tools.push(...(result.tools as McpToolDefinition[]));
|
|
280
|
+
cursor = result.nextCursor;
|
|
281
|
+
pageCount++;
|
|
282
|
+
} while (cursor);
|
|
283
|
+
|
|
284
|
+
return tools;
|
|
305
285
|
}
|
|
306
286
|
|
|
307
287
|
// ─── Tool Bridge ──────────────────────────────────────────────────────────────
|
|
@@ -311,184 +291,170 @@ export async function listAllTools(
|
|
|
311
291
|
* Tools are registered once and activated/deactivated as servers connect/disconnect.
|
|
312
292
|
*/
|
|
313
293
|
export class ToolBridge {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const serverTools = this.serverToolNames.get(serverName);
|
|
481
|
-
if (!serverTools || serverTools.size === 0) return;
|
|
482
|
-
|
|
483
|
-
const currentActive = this.pi.getActiveTools();
|
|
484
|
-
const remaining = currentActive.filter((n) => !serverTools.has(n));
|
|
485
|
-
this.pi.setActiveTools(remaining);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/** Deactivate a single tool by Pi name (used when a tool is removed on list_changed). */
|
|
489
|
-
private _deactivateServerTool(piName: string): void {
|
|
490
|
-
const currentActive = this.pi.getActiveTools();
|
|
491
|
-
const remaining = currentActive.filter((n) => n !== piName);
|
|
492
|
-
this.pi.setActiveTools(remaining);
|
|
493
|
-
}
|
|
294
|
+
private readonly settings: Settings;
|
|
295
|
+
private readonly pi: PiExtensionAPI;
|
|
296
|
+
/** Tracks which Pi tool names belong to which MCP server. */
|
|
297
|
+
private readonly serverToolNames = new Map<string, Set<string>>();
|
|
298
|
+
|
|
299
|
+
constructor(settings: Settings, pi: PiExtensionAPI) {
|
|
300
|
+
this.settings = settings;
|
|
301
|
+
this.pi = pi;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Refresh tools for a server — called on initial connect and on list_changed.
|
|
306
|
+
* Always re-registers tools with the current client reference so that
|
|
307
|
+
* tool execute closures capture the latest client after reconnection.
|
|
308
|
+
* Deactivates tools that are no longer in the server's list.
|
|
309
|
+
* Note: Pi's registerTool() overwrites by name (Map.set), so re-registration is safe.
|
|
310
|
+
*/
|
|
311
|
+
async refreshTools(serverName: string, client: Client): Promise<void> {
|
|
312
|
+
const timeoutMs = this.settings.requestTimeoutMs;
|
|
313
|
+
|
|
314
|
+
let tools: McpToolDefinition[];
|
|
315
|
+
try {
|
|
316
|
+
tools = await listAllTools(client, timeoutMs);
|
|
317
|
+
} catch (err) {
|
|
318
|
+
throw new McpError(
|
|
319
|
+
`Failed to list tools from ${serverName}: ${err instanceof Error ? err.message : String(err)}`,
|
|
320
|
+
serverName,
|
|
321
|
+
"protocol",
|
|
322
|
+
err,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const registeredForServer = this.serverToolNames.get(serverName) ?? new Set<string>();
|
|
327
|
+
|
|
328
|
+
// Build the set of currently valid Pi tool names for this server
|
|
329
|
+
const currentToolNames = new Set<string>();
|
|
330
|
+
|
|
331
|
+
for (const tool of tools) {
|
|
332
|
+
const piName = buildToolName(this.settings.toolPrefix, serverName, tool.name);
|
|
333
|
+
// Detect collision: two different MCP tools mapping to the same Pi name
|
|
334
|
+
// (e.g. "my-tool" and "my_tool" both sanitize to "my_tool")
|
|
335
|
+
if (currentToolNames.has(piName)) {
|
|
336
|
+
console.warn(
|
|
337
|
+
`[pi-mcp] Tool name collision: "${tool.name}" maps to "${piName}" which is already taken. ` +
|
|
338
|
+
`The later tool definition will overwrite the earlier one.`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
currentToolNames.add(piName);
|
|
342
|
+
// Always re-register — on reconnect the client reference changes and
|
|
343
|
+
// Pi's registerTool overwrites by name, so this is idempotent.
|
|
344
|
+
this._registerTool(piName, serverName, tool, client);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Deactivate tools that were removed from the server (no longer in tools/list)
|
|
348
|
+
for (const existingName of registeredForServer) {
|
|
349
|
+
if (!currentToolNames.has(existingName)) {
|
|
350
|
+
this._deactivateServerTool(existingName);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
this.serverToolNames.set(serverName, currentToolNames);
|
|
355
|
+
|
|
356
|
+
// Activate all current tools for this server
|
|
357
|
+
this._activateServerTools(serverName);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Deactivate all Pi tools belonging to a server (called on disconnect). */
|
|
361
|
+
deactivateServer(serverName: string): void {
|
|
362
|
+
this._deactivateServerTools(serverName);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Remove all tracking data for a server (called when config changes remove a server). */
|
|
366
|
+
removeServer(serverName: string): void {
|
|
367
|
+
this._deactivateServerTools(serverName);
|
|
368
|
+
this.serverToolNames.delete(serverName);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** Re-activate all Pi tools belonging to a server (called on reconnect). */
|
|
372
|
+
activateServer(serverName: string): void {
|
|
373
|
+
this._activateServerTools(serverName);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ─── Internal ───────────────────────────────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
private _registerTool(piName: string, serverName: string, tool: McpToolDefinition, client: Client): void {
|
|
379
|
+
// Build description with annotation hints for LLM guidance
|
|
380
|
+
let description = tool.description ?? `MCP tool: ${tool.name}`;
|
|
381
|
+
const ann = tool.annotations;
|
|
382
|
+
if (ann) {
|
|
383
|
+
const hints: string[] = [];
|
|
384
|
+
if (ann.readOnlyHint) hints.push("read-only");
|
|
385
|
+
if (ann.destructiveHint) hints.push("⚠️ destructive");
|
|
386
|
+
if (ann.idempotentHint) hints.push("idempotent");
|
|
387
|
+
if (ann.openWorldHint) hints.push("may have side effects");
|
|
388
|
+
if (hints.length > 0) description += ` [${hints.join(", ")}]`;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const schema = convertJsonSchemaToTypebox(tool.inputSchema);
|
|
392
|
+
const timeoutMs = this.settings.requestTimeoutMs;
|
|
393
|
+
|
|
394
|
+
this.pi.registerTool({
|
|
395
|
+
name: piName,
|
|
396
|
+
label: ann?.title ?? tool.name,
|
|
397
|
+
description,
|
|
398
|
+
promptSnippet: description.slice(0, 120),
|
|
399
|
+
parameters: schema,
|
|
400
|
+
|
|
401
|
+
async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
|
|
402
|
+
if (signal?.aborted) {
|
|
403
|
+
return { content: [{ type: "text", text: "Cancelled" }], details: {} };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
const result = await client.request(
|
|
408
|
+
{
|
|
409
|
+
method: "tools/call",
|
|
410
|
+
params: { name: tool.name, arguments: params },
|
|
411
|
+
},
|
|
412
|
+
CallToolResultSchema,
|
|
413
|
+
// Pass AbortSignal to SDK — it will automatically send
|
|
414
|
+
// notifications/cancelled when the signal fires
|
|
415
|
+
{ timeout: timeoutMs, ...(signal ? { signal } : {}) },
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
const content = convertMcpContent(result.content as unknown[]);
|
|
419
|
+
|
|
420
|
+
// Tool execution errors (isError: true) — distinct from protocol errors
|
|
421
|
+
if (result.isError) {
|
|
422
|
+
const errorText = content.map((c) => c.text).join("\n");
|
|
423
|
+
throw new McpError(errorText || "Tool reported an error", serverName, "tool");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return { content, details: {} };
|
|
427
|
+
} catch (err) {
|
|
428
|
+
if (err instanceof McpError) throw err;
|
|
429
|
+
// Protocol-level errors (JSON-RPC error response, timeout, etc.)
|
|
430
|
+
throw new McpError(err instanceof Error ? err.message : String(err), serverName, "protocol", err);
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private _activateServerTools(serverName: string): void {
|
|
437
|
+
const serverTools = this.serverToolNames.get(serverName);
|
|
438
|
+
if (!serverTools || serverTools.size === 0) return;
|
|
439
|
+
|
|
440
|
+
const currentActive = new Set(this.pi.getActiveTools());
|
|
441
|
+
for (const name of serverTools) currentActive.add(name);
|
|
442
|
+
this.pi.setActiveTools(Array.from(currentActive));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private _deactivateServerTools(serverName: string): void {
|
|
446
|
+
const serverTools = this.serverToolNames.get(serverName);
|
|
447
|
+
if (!serverTools || serverTools.size === 0) return;
|
|
448
|
+
|
|
449
|
+
const currentActive = this.pi.getActiveTools();
|
|
450
|
+
const remaining = currentActive.filter((n) => !serverTools.has(n));
|
|
451
|
+
this.pi.setActiveTools(remaining);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** Deactivate a single tool by Pi name (used when a tool is removed on list_changed). */
|
|
455
|
+
private _deactivateServerTool(piName: string): void {
|
|
456
|
+
const currentActive = this.pi.getActiveTools();
|
|
457
|
+
const remaining = currentActive.filter((n) => n !== piName);
|
|
458
|
+
this.pi.setActiveTools(remaining);
|
|
459
|
+
}
|
|
494
460
|
}
|