@pi-unipi/mcp 0.1.0 → 0.1.6
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 +1 -1
- package/src/bridge/registry.ts +20 -1
- package/src/bridge/translator.ts +61 -17
- package/src/index.ts +72 -12
- package/src/tui/add-overlay.ts +537 -190
- package/src/tui/settings-overlay.ts +48 -53
package/package.json
CHANGED
package/src/bridge/registry.ts
CHANGED
|
@@ -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(
|
|
118
|
+
await client.connect(safeCommand, safeArgs, safeEnv);
|
|
100
119
|
|
|
101
120
|
entry.client = client;
|
|
102
121
|
|
package/src/bridge/translator.ts
CHANGED
|
@@ -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: (
|
|
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
|
-
|
|
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
|
-
//
|
|
67
|
-
const
|
|
68
|
-
|
|
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
|
-
|
|
92
|
+
blocks.push({ type: "text", text: block.text });
|
|
71
93
|
} else if (block.type === "image" && block.data) {
|
|
72
|
-
|
|
94
|
+
blocks.push({
|
|
95
|
+
type: "text",
|
|
96
|
+
text: `[Image: ${block.mimeType ?? "unknown"}]`,
|
|
97
|
+
});
|
|
73
98
|
} else if (block.type === "resource") {
|
|
74
|
-
|
|
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
|
|
80
|
-
|
|
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
|
|
118
|
+
return {
|
|
119
|
+
content: blocks,
|
|
120
|
+
details: { server: serverName, tool: mcpTool.name },
|
|
121
|
+
};
|
|
84
122
|
} catch (err) {
|
|
85
|
-
const message =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
}
|