@pi-unipi/mcp 0.1.0 → 0.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.7",
4
4
  "description": "MCP server management extension for Pi coding agent — browse, add, configure, and use MCP servers",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -94,9 +94,28 @@ export class ServerRegistry {
94
94
  this.entries.set(name, entry);
95
95
 
96
96
  try {
97
+ // Defensive: ensure server definition has correct types before passing
98
+ // to the client. Catches rare serialization bugs where env/args become
99
+ // strings instead of objects/arrays.
100
+ const safeCommand = typeof def.command === "string" ? def.command : String(def.command);
101
+ const safeArgs = Array.isArray(def.args) ? def.args : [];
102
+ let safeEnv: Record<string, string> | undefined;
103
+ if (def.env !== undefined && def.env !== null) {
104
+ if (typeof def.env === "object" && !Array.isArray(def.env)) {
105
+ safeEnv = {};
106
+ for (const [k, v] of Object.entries(def.env)) {
107
+ safeEnv[k] = typeof v === "string" ? v : String(v);
108
+ }
109
+ } else {
110
+ console.error(
111
+ `[MCP] Server '${name}': env is not an object (${typeof def.env}), skipping env vars`,
112
+ );
113
+ }
114
+ }
115
+
97
116
  // Create and connect client
98
117
  const client = new McpClient({ timeoutMs: this.timeoutMs });
99
- await client.connect(def.command, def.args ?? [], def.env);
118
+ await client.connect(safeCommand, safeArgs, safeEnv);
100
119
 
101
120
  entry.client = client;
102
121
 
@@ -16,12 +16,29 @@ interface ToolParameters {
16
16
  required?: string[];
17
17
  }
18
18
 
19
+ /** Content block returned by a pi tool */
20
+ interface PiContentBlock {
21
+ type: "text";
22
+ text: string;
23
+ }
24
+
25
+ /** Pi tool execution result shape */
26
+ interface PiToolResult {
27
+ content: PiContentBlock[];
28
+ details?: Record<string, unknown>;
29
+ }
30
+
19
31
  /** Pi-compatible external tool */
20
32
  export interface PiExternalTool {
21
33
  name: string;
22
34
  description: string;
23
35
  parameters: ToolParameters;
24
- execute: (params: Record<string, unknown>) => Promise<string>;
36
+ execute: (
37
+ toolCallId: string,
38
+ params: Record<string, unknown>,
39
+ signal?: AbortSignal,
40
+ onUpdate?: (update: string) => void,
41
+ ) => Promise<PiToolResult>;
25
42
  }
26
43
 
27
44
  /**
@@ -55,39 +72,66 @@ export function translateMcpTool(
55
72
  ].join(" ");
56
73
 
57
74
  const execute = async (
75
+ _toolCallId: string,
58
76
  params: Record<string, unknown>,
59
- ): Promise<string> => {
77
+ _signal?: AbortSignal,
78
+ _onUpdate?: (update: string) => void,
79
+ ): Promise<PiToolResult> => {
60
80
  try {
61
81
  const result: McpToolResult = await client.callTool(
62
82
  mcpTool.name,
63
83
  params,
64
84
  );
65
85
 
66
- // Join all text content blocks
67
- const textParts: string[] = [];
68
- for (const block of result.content) {
86
+ // Defensive: some MCP servers return malformed results without content
87
+ const contentBlocks = result.content ?? [];
88
+ const blocks: PiContentBlock[] = [];
89
+
90
+ for (const block of contentBlocks) {
69
91
  if (block.type === "text" && block.text) {
70
- textParts.push(block.text);
92
+ blocks.push({ type: "text", text: block.text });
71
93
  } else if (block.type === "image" && block.data) {
72
- textParts.push(`[Image: ${block.mimeType ?? "unknown"}]`);
94
+ blocks.push({
95
+ type: "text",
96
+ text: `[Image: ${block.mimeType ?? "unknown"}]`,
97
+ });
73
98
  } else if (block.type === "resource") {
74
- textParts.push(`[Resource: ${block.text ?? block.mimeType ?? "unknown"}]`);
99
+ blocks.push({
100
+ type: "text",
101
+ text: `[Resource: ${block.text ?? block.mimeType ?? "unknown"}]`,
102
+ });
75
103
  }
76
104
  }
77
105
 
78
106
  if (result.isError) {
79
- const joined = textParts.join("\n") || "Unknown error";
80
- throw new Error(`MCP tool error from ${serverName}: ${joined}`);
107
+ const errorText = blocks.map((b) => b.text).join("\n") || "Unknown error";
108
+ return {
109
+ content: [{ type: "text", text: `MCP tool error from ${serverName}: ${errorText}` }],
110
+ details: { error: true, server: serverName, tool: mcpTool.name },
111
+ };
112
+ }
113
+
114
+ if (blocks.length === 0) {
115
+ blocks.push({ type: "text", text: "(no output)" });
81
116
  }
82
117
 
83
- return textParts.join("\n") || "(no output)";
118
+ return {
119
+ content: blocks,
120
+ details: { server: serverName, tool: mcpTool.name },
121
+ };
84
122
  } catch (err) {
85
- const message =
86
- err instanceof Error ? err.message : String(err);
87
- throw new Error(
88
- `MCP tool "${mcpTool.name}" on server "${serverName}" failed: ${message}\n` +
89
- `Check server status via /unipi:mcp-settings`,
90
- );
123
+ const message = err instanceof Error ? err.message : String(err);
124
+ return {
125
+ content: [
126
+ {
127
+ type: "text",
128
+ text:
129
+ `MCP tool "${mcpTool.name}" on server "${serverName}" failed: ${message}\n` +
130
+ `Check server status via /unipi:mcp-settings`,
131
+ },
132
+ ],
133
+ details: { error: true, server: serverName, tool: mcpTool.name },
134
+ };
91
135
  }
92
136
  };
93
137
 
package/src/index.ts CHANGED
@@ -119,19 +119,38 @@ export default function (pi: ExtensionAPI) {
119
119
  { id: "failed", label: "Failed", show: true },
120
120
  ],
121
121
  },
122
- dataProvider: async () => ({
123
- total: { value: String(reg.getAll().length) },
124
- active: { value: String(reg.getActive().length) },
125
- tools: { value: String(reg.getTotalToolCount()) },
126
- failed: {
127
- value: String(reg.getFailed().length),
128
- detail:
129
- reg.getFailed().length > 0
130
- ? reg.getFailed().map((f) => f.name).join(", ")
131
- : undefined,
132
- },
133
- }),
122
+ dataProvider: async () => {
123
+ try {
124
+ const all = reg.getAll();
125
+ const active = reg.getActive();
126
+ const failed = reg.getFailed();
127
+ const toolCount = reg.getTotalToolCount();
128
+ return {
129
+ total: { value: String(all.length) },
130
+ active: { value: String(active.length) },
131
+ tools: { value: String(toolCount) },
132
+ failed: {
133
+ value: String(failed.length),
134
+ detail:
135
+ failed.length > 0
136
+ ? failed.map((f) => f.name).join(", ")
137
+ : undefined,
138
+ },
139
+ };
140
+ } catch (err) {
141
+ console.error("[MCP] Info dataProvider error:", err);
142
+ return {
143
+ total: { value: "?" },
144
+ active: { value: "?" },
145
+ tools: { value: "?" },
146
+ failed: { value: "?" },
147
+ };
148
+ }
149
+ },
134
150
  });
151
+ // Trigger initial data fetch so the info overlay shows real values
152
+ // instead of the empty GroupData from notifyGroupRegistered.
153
+ infoRegistry.getGroupData("mcp").catch(() => {});
135
154
  }
136
155
 
137
156
  // Emit MODULE_READY
@@ -144,6 +163,7 @@ export default function (pi: ExtensionAPI) {
144
163
  `unipi:${MCP_COMMANDS.SETTINGS}`,
145
164
  `unipi:${MCP_COMMANDS.SYNC}`,
146
165
  `unipi:${MCP_COMMANDS.STATUS}`,
166
+ `unipi:${MCP_COMMANDS.RELOAD}`,
147
167
  ],
148
168
  tools: activeServers.flatMap((s) =>
149
169
  registry?.getEntry(s.name)?.toolNames ?? [],
@@ -294,4 +314,44 @@ export default function (pi: ExtensionAPI) {
294
314
  openSettings();
295
315
  },
296
316
  });
317
+
318
+ // /unipi:mcp-reload — restart all MCP servers
319
+ pi.registerCommand(`unipi:${MCP_COMMANDS.RELOAD}`, {
320
+ description: "Reload all MCP servers (restart with current config)",
321
+ handler: async (_args: string, ctx: any) => {
322
+ const reg = getRegistry();
323
+ if (!reg) {
324
+ ctx.ui.notify("MCP extension not initialized", "warning");
325
+ return;
326
+ }
327
+
328
+ const all = reg.getAll();
329
+ if (all.length === 0) {
330
+ ctx.ui.notify("No MCP servers configured. Use /unipi:mcp-add to add one.", "info");
331
+ return;
332
+ }
333
+
334
+ ctx.ui.notify(`Reloading ${all.length} MCP server(s)...`, "info");
335
+
336
+ let restarted = 0;
337
+ let failed = 0;
338
+ for (const state of all) {
339
+ try {
340
+ await reg.restartServer(state.name);
341
+ restarted++;
342
+ } catch (err) {
343
+ failed++;
344
+ console.error(
345
+ `[MCP] Failed to restart server '${state.name}':`,
346
+ err instanceof Error ? err.message : err,
347
+ );
348
+ }
349
+ }
350
+
351
+ const msg = failed > 0
352
+ ? `Reloaded: ${restarted} ok, ${failed} failed`
353
+ : `Reloaded ${restarted} MCP server(s) successfully`;
354
+ ctx.ui.notify(msg, failed > 0 ? "warning" : "info");
355
+ },
356
+ });
297
357
  }