@docyrus/docyrus 0.0.20 → 0.0.21

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.
Files changed (65) hide show
  1. package/agent-loader.js +32 -1
  2. package/agent-loader.js.map +2 -2
  3. package/main.js +321 -70
  4. package/main.js.map +4 -4
  5. package/package.json +12 -2
  6. package/resources/chrome-tools/browser-content.js +103 -0
  7. package/resources/chrome-tools/browser-cookies.js +35 -0
  8. package/resources/chrome-tools/browser-eval.js +53 -0
  9. package/resources/chrome-tools/browser-hn-scraper.js +108 -0
  10. package/resources/chrome-tools/browser-nav.js +44 -0
  11. package/resources/chrome-tools/browser-pick.js +162 -0
  12. package/resources/chrome-tools/browser-screenshot.js +34 -0
  13. package/resources/chrome-tools/browser-start.js +86 -0
  14. package/resources/pi-agent/extensions/answer.ts +532 -0
  15. package/resources/pi-agent/extensions/context.ts +578 -0
  16. package/resources/pi-agent/extensions/control.ts +1779 -0
  17. package/resources/pi-agent/extensions/diff.ts +218 -0
  18. package/resources/pi-agent/extensions/files.ts +199 -0
  19. package/resources/pi-agent/extensions/loop.ts +446 -0
  20. package/resources/pi-agent/extensions/multi-edit.ts +835 -0
  21. package/resources/pi-agent/extensions/notify.ts +88 -0
  22. package/resources/pi-agent/extensions/pi-mcp-adapter/CHANGELOG.md +192 -0
  23. package/resources/pi-agent/extensions/pi-mcp-adapter/LICENSE +21 -0
  24. package/resources/pi-agent/extensions/pi-mcp-adapter/README.md +296 -0
  25. package/resources/pi-agent/extensions/pi-mcp-adapter/app-bridge.bundle.js +67 -0
  26. package/resources/pi-agent/extensions/pi-mcp-adapter/cli.js +108 -0
  27. package/resources/pi-agent/extensions/pi-mcp-adapter/commands.ts +211 -0
  28. package/resources/pi-agent/extensions/pi-mcp-adapter/config.ts +227 -0
  29. package/resources/pi-agent/extensions/pi-mcp-adapter/consent-manager.ts +64 -0
  30. package/resources/pi-agent/extensions/pi-mcp-adapter/direct-tools.ts +301 -0
  31. package/resources/pi-agent/extensions/pi-mcp-adapter/errors.ts +219 -0
  32. package/resources/pi-agent/extensions/pi-mcp-adapter/glimpse-ui.ts +80 -0
  33. package/resources/pi-agent/extensions/pi-mcp-adapter/host-html-template.ts +427 -0
  34. package/resources/pi-agent/extensions/pi-mcp-adapter/index.ts +232 -0
  35. package/resources/pi-agent/extensions/pi-mcp-adapter/init.ts +319 -0
  36. package/resources/pi-agent/extensions/pi-mcp-adapter/lifecycle.ts +93 -0
  37. package/resources/pi-agent/extensions/pi-mcp-adapter/logger.ts +169 -0
  38. package/resources/pi-agent/extensions/pi-mcp-adapter/mcp-panel.ts +713 -0
  39. package/resources/pi-agent/extensions/pi-mcp-adapter/metadata-cache.ts +191 -0
  40. package/resources/pi-agent/extensions/pi-mcp-adapter/npx-resolver.ts +419 -0
  41. package/resources/pi-agent/extensions/pi-mcp-adapter/oauth-handler.ts +56 -0
  42. package/resources/pi-agent/extensions/pi-mcp-adapter/package.json +85 -0
  43. package/resources/pi-agent/extensions/pi-mcp-adapter/paths.ts +29 -0
  44. package/resources/pi-agent/extensions/pi-mcp-adapter/proxy-modes.ts +635 -0
  45. package/resources/pi-agent/extensions/pi-mcp-adapter/resource-tools.ts +17 -0
  46. package/resources/pi-agent/extensions/pi-mcp-adapter/server-manager.ts +330 -0
  47. package/resources/pi-agent/extensions/pi-mcp-adapter/state.ts +41 -0
  48. package/resources/pi-agent/extensions/pi-mcp-adapter/tool-metadata.ts +144 -0
  49. package/resources/pi-agent/extensions/pi-mcp-adapter/tool-registrar.ts +46 -0
  50. package/resources/pi-agent/extensions/pi-mcp-adapter/types.ts +367 -0
  51. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-resource-handler.ts +145 -0
  52. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-server.ts +623 -0
  53. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-session.ts +384 -0
  54. package/resources/pi-agent/extensions/pi-mcp-adapter/ui-stream-types.ts +89 -0
  55. package/resources/pi-agent/extensions/pi-mcp-adapter/utils.ts +75 -0
  56. package/resources/pi-agent/extensions/prompt-editor.ts +1315 -0
  57. package/resources/pi-agent/extensions/prompt-url-widget.ts +158 -0
  58. package/resources/pi-agent/extensions/redraws.ts +24 -0
  59. package/resources/pi-agent/extensions/review.ts +2160 -0
  60. package/resources/pi-agent/extensions/todos.ts +2076 -0
  61. package/resources/pi-agent/extensions/tps.ts +47 -0
  62. package/resources/pi-agent/extensions/whimsical.ts +474 -0
  63. package/resources/pi-agent/skills/changelog-generator/SKILL.md +425 -0
  64. package/resources/pi-agent/skills/docyrus-chrome-devtools-cli/SKILL.md +80 -0
  65. package/resources/pi-agent/skills/docyrus-platform/references/docyrus-cli-usage.md +51 -0
@@ -0,0 +1,330 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
5
+ import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
6
+ import type {
7
+ McpTool,
8
+ McpResource,
9
+ ServerDefinition,
10
+ ServerStreamResultPatchNotification,
11
+ Transport,
12
+ } from "./types.js";
13
+ import { serverStreamResultPatchNotificationSchema } from "./types.js";
14
+ import { getStoredTokens } from "./oauth-handler.js";
15
+ import { resolveNpxBinary } from "./npx-resolver.js";
16
+ import { logger } from "./logger.js";
17
+
18
+ interface ServerConnection {
19
+ client: Client;
20
+ transport: Transport;
21
+ definition: ServerDefinition;
22
+ tools: McpTool[];
23
+ resources: McpResource[];
24
+ lastUsedAt: number;
25
+ inFlight: number;
26
+ status: "connected" | "closed";
27
+ }
28
+
29
+ type UiStreamListener = (serverName: string, notification: ServerStreamResultPatchNotification["params"]) => void;
30
+
31
+ export class McpServerManager {
32
+ private connections = new Map<string, ServerConnection>();
33
+ private connectPromises = new Map<string, Promise<ServerConnection>>();
34
+ private uiStreamListeners = new Map<string, UiStreamListener>();
35
+
36
+ async connect(name: string, definition: ServerDefinition): Promise<ServerConnection> {
37
+ // Dedupe concurrent connection attempts
38
+ if (this.connectPromises.has(name)) {
39
+ return this.connectPromises.get(name)!;
40
+ }
41
+
42
+ // Reuse existing connection if healthy
43
+ const existing = this.connections.get(name);
44
+ if (existing?.status === "connected") {
45
+ existing.lastUsedAt = Date.now();
46
+ return existing;
47
+ }
48
+
49
+ const promise = this.createConnection(name, definition);
50
+ this.connectPromises.set(name, promise);
51
+
52
+ try {
53
+ const connection = await promise;
54
+ this.connections.set(name, connection);
55
+ return connection;
56
+ } finally {
57
+ this.connectPromises.delete(name);
58
+ }
59
+ }
60
+
61
+ private async createConnection(
62
+ name: string,
63
+ definition: ServerDefinition
64
+ ): Promise<ServerConnection> {
65
+ const client = new Client({ name: `pi-mcp-${name}`, version: "1.0.0" });
66
+
67
+ let transport: Transport;
68
+
69
+ if (definition.command) {
70
+ let command = definition.command;
71
+ let args = definition.args ?? [];
72
+
73
+ if (command === "npx" || command === "npm") {
74
+ const resolved = await resolveNpxBinary(command, args);
75
+ if (resolved) {
76
+ command = resolved.isJs ? "node" : resolved.binPath;
77
+ args = resolved.isJs ? [resolved.binPath, ...resolved.extraArgs] : resolved.extraArgs;
78
+ logger.debug(`${name} resolved to ${resolved.binPath} (skipping npm parent)`);
79
+ }
80
+ }
81
+
82
+ transport = new StdioClientTransport({
83
+ command,
84
+ args,
85
+ env: resolveEnv(definition.env),
86
+ cwd: definition.cwd,
87
+ stderr: definition.debug ? "inherit" : "ignore",
88
+ });
89
+ } else if (definition.url) {
90
+ // HTTP transport with fallback
91
+ transport = await this.createHttpTransport(definition, name);
92
+ } else {
93
+ throw new Error(`Server ${name} has no command or url`);
94
+ }
95
+
96
+ try {
97
+ await client.connect(transport);
98
+ this.attachAdapterNotificationHandlers(name, client);
99
+
100
+ // Discover tools and resources
101
+ const [tools, resources] = await Promise.all([
102
+ this.fetchAllTools(client),
103
+ this.fetchAllResources(client),
104
+ ]);
105
+
106
+ return {
107
+ client,
108
+ transport,
109
+ definition,
110
+ tools,
111
+ resources,
112
+ lastUsedAt: Date.now(),
113
+ inFlight: 0,
114
+ status: "connected",
115
+ };
116
+ } catch (error) {
117
+ // Clean up both client and transport on any error
118
+ await client.close().catch(() => {});
119
+ await transport.close().catch(() => {});
120
+ throw error;
121
+ }
122
+ }
123
+
124
+ private async createHttpTransport(definition: ServerDefinition, serverName?: string): Promise<Transport> {
125
+ const url = new URL(definition.url!);
126
+ const headers = resolveHeaders(definition.headers) ?? {};
127
+
128
+ // Add bearer token if configured
129
+ if (definition.auth === "bearer") {
130
+ const token = definition.bearerToken
131
+ ?? (definition.bearerTokenEnv ? process.env[definition.bearerTokenEnv] : undefined);
132
+ if (token) {
133
+ headers["Authorization"] = `Bearer ${token}`;
134
+ }
135
+ }
136
+
137
+ // Handle OAuth auth - use stored tokens
138
+ if (definition.auth === "oauth") {
139
+ if (!serverName) {
140
+ throw new Error("Server name required for OAuth authentication");
141
+ }
142
+ const tokens = getStoredTokens(serverName);
143
+ if (!tokens) {
144
+ throw new Error(
145
+ `No OAuth tokens found for "${serverName}". Run /mcp-auth ${serverName} to authenticate.`
146
+ );
147
+ }
148
+ headers["Authorization"] = `Bearer ${tokens.access_token}`;
149
+ }
150
+
151
+ const requestInit = Object.keys(headers).length > 0 ? { headers } : undefined;
152
+
153
+ // Try StreamableHTTP first (modern MCP servers)
154
+ const streamableTransport = new StreamableHTTPClientTransport(url, { requestInit });
155
+
156
+ try {
157
+ // Create a test client to verify the transport works
158
+ const testClient = new Client({ name: "pi-mcp-probe", version: "1.0.0" });
159
+ await testClient.connect(streamableTransport);
160
+ await testClient.close().catch(() => {});
161
+ // Close probe transport before creating fresh one
162
+ await streamableTransport.close().catch(() => {});
163
+
164
+ // StreamableHTTP works - create fresh transport for actual use
165
+ return new StreamableHTTPClientTransport(url, { requestInit });
166
+ } catch {
167
+ // StreamableHTTP failed, close and try SSE fallback
168
+ await streamableTransport.close().catch(() => {});
169
+
170
+ // SSE is the legacy transport
171
+ return new SSEClientTransport(url, { requestInit });
172
+ }
173
+ }
174
+
175
+ private async fetchAllTools(client: Client): Promise<McpTool[]> {
176
+ const allTools: McpTool[] = [];
177
+ let cursor: string | undefined;
178
+
179
+ do {
180
+ const result = await client.listTools(cursor ? { cursor } : undefined);
181
+ allTools.push(...(result.tools ?? []));
182
+ cursor = result.nextCursor;
183
+ } while (cursor);
184
+
185
+ return allTools;
186
+ }
187
+
188
+ private async fetchAllResources(client: Client): Promise<McpResource[]> {
189
+ try {
190
+ const allResources: McpResource[] = [];
191
+ let cursor: string | undefined;
192
+
193
+ do {
194
+ const result = await client.listResources(cursor ? { cursor } : undefined);
195
+ allResources.push(...(result.resources ?? []));
196
+ cursor = result.nextCursor;
197
+ } while (cursor);
198
+
199
+ return allResources;
200
+ } catch {
201
+ // Server may not support resources
202
+ return [];
203
+ }
204
+ }
205
+
206
+ private attachAdapterNotificationHandlers(serverName: string, client: Client): void {
207
+ client.setNotificationHandler(serverStreamResultPatchNotificationSchema, (notification) => {
208
+ const listener = this.uiStreamListeners.get(notification.params.streamToken);
209
+ if (!listener) return;
210
+ listener(serverName, notification.params);
211
+ });
212
+ }
213
+
214
+ registerUiStreamListener(streamToken: string, listener: UiStreamListener): void {
215
+ this.uiStreamListeners.set(streamToken, listener);
216
+ }
217
+
218
+ removeUiStreamListener(streamToken: string): void {
219
+ this.uiStreamListeners.delete(streamToken);
220
+ }
221
+
222
+ async readResource(name: string, uri: string): Promise<ReadResourceResult> {
223
+ const connection = this.connections.get(name);
224
+ if (!connection || connection.status !== "connected") {
225
+ throw new Error(`Server "${name}" is not connected`);
226
+ }
227
+
228
+ try {
229
+ this.touch(name);
230
+ this.incrementInFlight(name);
231
+ return await connection.client.readResource({ uri });
232
+ } finally {
233
+ this.decrementInFlight(name);
234
+ this.touch(name);
235
+ }
236
+ }
237
+
238
+ async close(name: string): Promise<void> {
239
+ const connection = this.connections.get(name);
240
+ if (!connection) return;
241
+
242
+ // Delete from map BEFORE async cleanup to prevent a race where a
243
+ // concurrent connect() creates a new connection that our deferred
244
+ // delete() would then remove, orphaning the new server process.
245
+ connection.status = "closed";
246
+ this.connections.delete(name);
247
+ await connection.client.close().catch(() => {});
248
+ await connection.transport.close().catch(() => {});
249
+ }
250
+
251
+ async closeAll(): Promise<void> {
252
+ const names = [...this.connections.keys()];
253
+ await Promise.all(names.map(name => this.close(name)));
254
+ }
255
+
256
+ getConnection(name: string): ServerConnection | undefined {
257
+ return this.connections.get(name);
258
+ }
259
+
260
+ getAllConnections(): Map<string, ServerConnection> {
261
+ return new Map(this.connections);
262
+ }
263
+
264
+ touch(name: string): void {
265
+ const connection = this.connections.get(name);
266
+ if (connection) {
267
+ connection.lastUsedAt = Date.now();
268
+ }
269
+ }
270
+
271
+ incrementInFlight(name: string): void {
272
+ const connection = this.connections.get(name);
273
+ if (connection) {
274
+ connection.inFlight = (connection.inFlight ?? 0) + 1;
275
+ }
276
+ }
277
+
278
+ decrementInFlight(name: string): void {
279
+ const connection = this.connections.get(name);
280
+ if (connection && connection.inFlight) {
281
+ connection.inFlight--;
282
+ }
283
+ }
284
+
285
+ isIdle(name: string, timeoutMs: number): boolean {
286
+ const connection = this.connections.get(name);
287
+ if (!connection || connection.status !== "connected") return false;
288
+ if (connection.inFlight > 0) return false;
289
+ return (Date.now() - connection.lastUsedAt) > timeoutMs;
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Resolve environment variables with interpolation.
295
+ */
296
+ function resolveEnv(env?: Record<string, string>): Record<string, string> {
297
+ // Copy process.env, filtering out undefined values
298
+ const resolved: Record<string, string> = {};
299
+ for (const [key, value] of Object.entries(process.env)) {
300
+ if (value !== undefined) {
301
+ resolved[key] = value;
302
+ }
303
+ }
304
+
305
+ if (!env) return resolved;
306
+
307
+ for (const [key, value] of Object.entries(env)) {
308
+ // Support ${VAR} and $env:VAR interpolation
309
+ resolved[key] = value
310
+ .replace(/\$\{(\w+)\}/g, (_, name) => process.env[name] ?? "")
311
+ .replace(/\$env:(\w+)/g, (_, name) => process.env[name] ?? "");
312
+ }
313
+
314
+ return resolved;
315
+ }
316
+
317
+ /**
318
+ * Resolve headers with environment variable interpolation.
319
+ */
320
+ function resolveHeaders(headers?: Record<string, string>): Record<string, string> | undefined {
321
+ if (!headers) return undefined;
322
+
323
+ const resolved: Record<string, string> = {};
324
+ for (const [key, value] of Object.entries(headers)) {
325
+ resolved[key] = value
326
+ .replace(/\$\{(\w+)\}/g, (_, name) => process.env[name] ?? "")
327
+ .replace(/\$env:(\w+)/g, (_, name) => process.env[name] ?? "");
328
+ }
329
+ return resolved;
330
+ }
@@ -0,0 +1,41 @@
1
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import type { ConsentManager } from "./consent-manager.js";
3
+ import type { McpLifecycleManager } from "./lifecycle.js";
4
+ import type { McpServerManager } from "./server-manager.js";
5
+ import type { ToolMetadata, McpConfig, UiSessionMessages, UiStreamSummary } from "./types.js";
6
+ import type { UiResourceHandler } from "./ui-resource-handler.js";
7
+ import type { UiServerHandle } from "./ui-server.js";
8
+
9
+ export interface CompletedUiSession {
10
+ serverName: string;
11
+ toolName: string;
12
+ completedAt: Date;
13
+ reason: string;
14
+ messages: UiSessionMessages;
15
+ stream?: UiStreamSummary;
16
+ }
17
+
18
+ export type SendMessageFn = (
19
+ message: {
20
+ customType: string;
21
+ content: Array<{ type: string; text: string }>;
22
+ display?: string;
23
+ details?: unknown;
24
+ },
25
+ options?: { triggerTurn?: boolean }
26
+ ) => void;
27
+
28
+ export interface McpExtensionState {
29
+ manager: McpServerManager;
30
+ lifecycle: McpLifecycleManager;
31
+ toolMetadata: Map<string, ToolMetadata[]>;
32
+ config: McpConfig;
33
+ failureTracker: Map<string, number>;
34
+ uiResourceHandler: UiResourceHandler;
35
+ consentManager: ConsentManager;
36
+ uiServer: UiServerHandle | null;
37
+ completedUiSessions: CompletedUiSession[];
38
+ openBrowser: (url: string) => Promise<void>;
39
+ ui?: ExtensionContext["ui"];
40
+ sendMessage?: SendMessageFn;
41
+ }
@@ -0,0 +1,144 @@
1
+ import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
2
+ import type { McpExtensionState } from "./state.js";
3
+ import type { ToolMetadata, McpTool, McpResource, ServerEntry } from "./types.js";
4
+ import { formatToolName } from "./types.js";
5
+ import { resourceNameToToolName } from "./resource-tools.js";
6
+ import { extractToolUiStreamMode } from "./utils.js";
7
+
8
+ export function buildToolMetadata(
9
+ tools: McpTool[],
10
+ resources: McpResource[],
11
+ definition: ServerEntry,
12
+ serverName: string,
13
+ prefix: "server" | "none" | "short"
14
+ ): { metadata: ToolMetadata[]; failedTools: string[] } {
15
+ const metadata: ToolMetadata[] = [];
16
+ const failedTools: string[] = [];
17
+
18
+ for (const tool of tools) {
19
+ if (!tool?.name) {
20
+ failedTools.push("(unnamed)");
21
+ continue;
22
+ }
23
+ let uiResourceUri: string | undefined;
24
+ try {
25
+ uiResourceUri = getToolUiResourceUri({ _meta: tool._meta });
26
+ } catch {
27
+ failedTools.push(tool.name);
28
+ }
29
+ metadata.push({
30
+ name: formatToolName(tool.name, serverName, prefix),
31
+ originalName: tool.name,
32
+ description: tool.description ?? "",
33
+ inputSchema: tool.inputSchema,
34
+ uiResourceUri,
35
+ uiStreamMode: extractToolUiStreamMode(tool._meta),
36
+ });
37
+ }
38
+
39
+ if (definition.exposeResources !== false) {
40
+ for (const resource of resources) {
41
+ const baseName = `get_${resourceNameToToolName(resource.name)}`;
42
+ metadata.push({
43
+ name: formatToolName(baseName, serverName, prefix),
44
+ originalName: baseName,
45
+ description: resource.description ?? `Read resource: ${resource.uri}`,
46
+ resourceUri: resource.uri,
47
+ });
48
+ }
49
+ }
50
+
51
+ return { metadata, failedTools };
52
+ }
53
+
54
+ export function getToolNames(state: McpExtensionState, serverName: string): string[] {
55
+ return state.toolMetadata.get(serverName)?.map(m => m.name) ?? [];
56
+ }
57
+
58
+ export function totalToolCount(state: McpExtensionState): number {
59
+ let count = 0;
60
+ for (const metadata of state.toolMetadata.values()) {
61
+ count += metadata.length;
62
+ }
63
+ return count;
64
+ }
65
+
66
+ export function findToolByName(metadata: ToolMetadata[] | undefined, toolName: string): ToolMetadata | undefined {
67
+ if (!metadata) return undefined;
68
+ const exact = metadata.find(m => m.name === toolName);
69
+ if (exact) return exact;
70
+ const normalized = toolName.replace(/-/g, "_");
71
+ return metadata.find(m => m.name.replace(/-/g, "_") === normalized);
72
+ }
73
+
74
+ export function formatSchema(schema: unknown, indent = " "): string {
75
+ if (!schema || typeof schema !== "object") {
76
+ return `${indent}(no schema)`;
77
+ }
78
+
79
+ const s = schema as Record<string, unknown>;
80
+
81
+ if (s.type === "object" && s.properties && typeof s.properties === "object") {
82
+ const props = s.properties as Record<string, unknown>;
83
+ const required = Array.isArray(s.required) ? s.required as string[] : [];
84
+
85
+ if (Object.keys(props).length === 0) {
86
+ return `${indent}(no parameters)`;
87
+ }
88
+
89
+ const lines: string[] = [];
90
+ for (const [name, propSchema] of Object.entries(props)) {
91
+ const isRequired = required.includes(name);
92
+ const propLine = formatProperty(name, propSchema, isRequired, indent);
93
+ lines.push(propLine);
94
+ }
95
+ return lines.join("\n");
96
+ }
97
+
98
+ if (s.type) {
99
+ return `${indent}(${s.type})`;
100
+ }
101
+
102
+ return `${indent}(complex schema)`;
103
+ }
104
+
105
+ function formatProperty(name: string, schema: unknown, required: boolean, indent: string): string {
106
+ if (!schema || typeof schema !== "object") {
107
+ return `${indent}${name}${required ? " *required*" : ""}`;
108
+ }
109
+
110
+ const s = schema as Record<string, unknown>;
111
+ const parts: string[] = [];
112
+
113
+ let typeStr = "";
114
+ if (s.type) {
115
+ if (Array.isArray(s.type)) {
116
+ typeStr = s.type.join(" | ");
117
+ } else {
118
+ typeStr = String(s.type);
119
+ }
120
+ } else if (s.enum) {
121
+ typeStr = "enum";
122
+ } else if (s.anyOf || s.oneOf) {
123
+ typeStr = "union";
124
+ }
125
+
126
+ if (Array.isArray(s.enum)) {
127
+ const enumVals = s.enum.map(v => JSON.stringify(v)).join(", ");
128
+ typeStr = `enum: ${enumVals}`;
129
+ }
130
+
131
+ parts.push(`${indent}${name}`);
132
+ if (typeStr) parts.push(`(${typeStr})`);
133
+ if (required) parts.push("*required*");
134
+
135
+ if (s.description && typeof s.description === "string") {
136
+ parts.push(`- ${s.description}`);
137
+ }
138
+
139
+ if (s.default !== undefined) {
140
+ parts.push(`[default: ${JSON.stringify(s.default)}]`);
141
+ }
142
+
143
+ return parts.join(" ");
144
+ }
@@ -0,0 +1,46 @@
1
+ // tool-registrar.ts - MCP content transformation
2
+ // NOTE: Tools are NOT registered with Pi - only the unified `mcp` proxy tool is registered.
3
+ // This keeps the LLM context small (1 tool instead of 100s).
4
+
5
+ import type { McpContent, ContentBlock } from "./types.js";
6
+
7
+ /**
8
+ * Transform MCP content types to Pi content blocks.
9
+ */
10
+ export function transformMcpContent(content: McpContent[]): ContentBlock[] {
11
+ return content.map(c => {
12
+ if (c.type === "text") {
13
+ return { type: "text" as const, text: c.text ?? "" };
14
+ }
15
+ if (c.type === "image") {
16
+ return {
17
+ type: "image" as const,
18
+ data: c.data ?? "",
19
+ mimeType: c.mimeType ?? "image/png",
20
+ };
21
+ }
22
+ if (c.type === "resource") {
23
+ const resourceUri = c.resource?.uri ?? "(no URI)";
24
+ const resourceContent = c.resource?.text ?? (c.resource ? JSON.stringify(c.resource) : "(no content)");
25
+ return {
26
+ type: "text" as const,
27
+ text: `[Resource: ${resourceUri}]\n${resourceContent}`,
28
+ };
29
+ }
30
+ if (c.type === "resource_link") {
31
+ const linkName = c.name ?? c.uri ?? "unknown";
32
+ const linkUri = c.uri ?? "(no URI)";
33
+ return {
34
+ type: "text" as const,
35
+ text: `[Resource Link: ${linkName}]\nURI: ${linkUri}`,
36
+ };
37
+ }
38
+ if (c.type === "audio") {
39
+ return {
40
+ type: "text" as const,
41
+ text: `[Audio content: ${c.mimeType ?? "audio/*"}]`,
42
+ };
43
+ }
44
+ return { type: "text" as const, text: JSON.stringify(c) };
45
+ });
46
+ }