@pi-unipi/mcp 0.1.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 +109 -0
- package/data/seed-servers.json +727 -0
- package/package.json +47 -0
- package/skills/mcp/SKILL.md +104 -0
- package/src/bridge/client.ts +365 -0
- package/src/bridge/registry.ts +281 -0
- package/src/bridge/translator.ts +100 -0
- package/src/config/manager.ts +267 -0
- package/src/config/schema.ts +114 -0
- package/src/config/sync.ts +416 -0
- package/src/index.ts +297 -0
- package/src/tui/add-overlay.ts +436 -0
- package/src/tui/settings-overlay.ts +369 -0
- package/src/types.ts +162 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/mcp — Extension entry point
|
|
3
|
+
*
|
|
4
|
+
* Registers commands, handles session lifecycle, wires up MCP server management.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
8
|
+
import {
|
|
9
|
+
UNIPI_EVENTS,
|
|
10
|
+
MODULES,
|
|
11
|
+
MCP_COMMANDS,
|
|
12
|
+
emitEvent,
|
|
13
|
+
getPackageVersion,
|
|
14
|
+
} from "@pi-unipi/core";
|
|
15
|
+
import type { ResolvedServer } from "./types.js";
|
|
16
|
+
import { loadAndResolve, getGlobalConfigDir } from "./config/manager.js";
|
|
17
|
+
import { syncCatalog, loadCatalog } from "./config/sync.js";
|
|
18
|
+
import { ServerRegistry } from "./bridge/registry.js";
|
|
19
|
+
import { renderMcpAddOverlay } from "./tui/add-overlay.js";
|
|
20
|
+
import { renderMcpSettingsOverlay } from "./tui/settings-overlay.js";
|
|
21
|
+
|
|
22
|
+
/** Package version */
|
|
23
|
+
const VERSION = getPackageVersion(new URL(".", import.meta.url).pathname);
|
|
24
|
+
|
|
25
|
+
/** Module-local registry instance */
|
|
26
|
+
let registry: ServerRegistry | null = null;
|
|
27
|
+
|
|
28
|
+
/** Get info registry from global */
|
|
29
|
+
function getInfoRegistry() {
|
|
30
|
+
const g = globalThis as any;
|
|
31
|
+
return g.__unipi_info_registry;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Get the server registry (for commands) */
|
|
35
|
+
function getRegistry(): ServerRegistry | null {
|
|
36
|
+
return registry;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default function (pi: ExtensionAPI) {
|
|
40
|
+
// Register skills directory
|
|
41
|
+
const skillsDir = new URL("../skills", import.meta.url).pathname;
|
|
42
|
+
pi.on("resources_discover", async (_event, _ctx) => {
|
|
43
|
+
return {
|
|
44
|
+
skillPaths: [skillsDir],
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Session start — load configs, start servers
|
|
49
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
50
|
+
// Create registry with pi integration callbacks
|
|
51
|
+
registry = new ServerRegistry({
|
|
52
|
+
emitEvent: (event, payload) => emitEvent(pi, event, payload),
|
|
53
|
+
registerTool: (tool) => {
|
|
54
|
+
try {
|
|
55
|
+
(pi as any).registerTool?.(tool) ??
|
|
56
|
+
(pi as any).registerExternalTool?.(tool);
|
|
57
|
+
} catch {
|
|
58
|
+
// Tool registration may not be available in all contexts
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
unregisterTool: (toolName) => {
|
|
62
|
+
try {
|
|
63
|
+
(pi as any).unregisterTool?.(toolName) ??
|
|
64
|
+
(pi as any).unregisterExternalTool?.(toolName);
|
|
65
|
+
} catch {
|
|
66
|
+
// Ignore
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Load and resolve server configs
|
|
72
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
73
|
+
let servers: ResolvedServer[] = [];
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const result = loadAndResolve(cwd);
|
|
77
|
+
servers = result.servers;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.error(
|
|
80
|
+
"[MCP] Failed to load config:",
|
|
81
|
+
err instanceof Error ? err.message : err,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Start enabled servers (parallel, non-blocking errors)
|
|
86
|
+
const startPromises = servers
|
|
87
|
+
.filter((s) => s.enabled)
|
|
88
|
+
.map(async (server) => {
|
|
89
|
+
try {
|
|
90
|
+
await registry!.startServer(server);
|
|
91
|
+
console.log(
|
|
92
|
+
`[MCP] Started server '${server.name}' (${registry!.getServerState(server.name)?.toolCount ?? 0} tools)`,
|
|
93
|
+
);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error(
|
|
96
|
+
`[MCP] Failed to start server '${server.name}':`,
|
|
97
|
+
err instanceof Error ? err.message : err,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await Promise.allSettled(startPromises);
|
|
103
|
+
|
|
104
|
+
// Register info-screen group
|
|
105
|
+
const infoRegistry = getInfoRegistry();
|
|
106
|
+
if (infoRegistry && registry) {
|
|
107
|
+
const reg = registry;
|
|
108
|
+
infoRegistry.registerGroup({
|
|
109
|
+
id: "mcp",
|
|
110
|
+
name: "MCP Servers",
|
|
111
|
+
icon: "🔌",
|
|
112
|
+
priority: 15,
|
|
113
|
+
config: {
|
|
114
|
+
showByDefault: true,
|
|
115
|
+
stats: [
|
|
116
|
+
{ id: "total", label: "Total servers", show: true },
|
|
117
|
+
{ id: "active", label: "Active", show: true },
|
|
118
|
+
{ id: "tools", label: "Total tools", show: true },
|
|
119
|
+
{ id: "failed", label: "Failed", show: true },
|
|
120
|
+
],
|
|
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
|
+
}),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Emit MODULE_READY
|
|
138
|
+
const activeServers = registry?.getActive() ?? [];
|
|
139
|
+
emitEvent(pi, UNIPI_EVENTS.MODULE_READY, {
|
|
140
|
+
name: MODULES.MCP,
|
|
141
|
+
version: VERSION,
|
|
142
|
+
commands: [
|
|
143
|
+
`unipi:${MCP_COMMANDS.ADD}`,
|
|
144
|
+
`unipi:${MCP_COMMANDS.SETTINGS}`,
|
|
145
|
+
`unipi:${MCP_COMMANDS.SYNC}`,
|
|
146
|
+
`unipi:${MCP_COMMANDS.STATUS}`,
|
|
147
|
+
],
|
|
148
|
+
tools: activeServers.flatMap((s) =>
|
|
149
|
+
registry?.getEntry(s.name)?.toolNames ?? [],
|
|
150
|
+
),
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Session shutdown — stop all servers
|
|
155
|
+
pi.on("session_shutdown", async (_event, _ctx) => {
|
|
156
|
+
if (registry) {
|
|
157
|
+
await registry.stopAll();
|
|
158
|
+
registry = null;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ── Register commands ─────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
// /unipi:mcp-status — text summary of all servers
|
|
165
|
+
pi.registerCommand(`unipi:${MCP_COMMANDS.STATUS}`, {
|
|
166
|
+
description: "Show status of all configured MCP servers",
|
|
167
|
+
handler: async (_args: string, ctx: any) => {
|
|
168
|
+
const reg = getRegistry();
|
|
169
|
+
if (!reg) {
|
|
170
|
+
ctx.ui.notify("MCP extension not initialized", "warning");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const all = reg.getAll();
|
|
175
|
+
if (all.length === 0) {
|
|
176
|
+
ctx.ui.notify("No MCP servers configured. Use /unipi:mcp-add to add one.", "info");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const lines: string[] = ["MCP Server Status:\n"];
|
|
181
|
+
|
|
182
|
+
for (const state of all) {
|
|
183
|
+
const icon =
|
|
184
|
+
state.status === "running"
|
|
185
|
+
? "●"
|
|
186
|
+
: state.status === "error"
|
|
187
|
+
? "✗"
|
|
188
|
+
: state.status === "starting"
|
|
189
|
+
? "◐"
|
|
190
|
+
: "○";
|
|
191
|
+
|
|
192
|
+
const toolInfo =
|
|
193
|
+
state.status === "running" && state.toolCount > 0
|
|
194
|
+
? ` (${state.toolCount} tools)`
|
|
195
|
+
: state.status === "error" && state.error
|
|
196
|
+
? ` — ${state.error}`
|
|
197
|
+
: "";
|
|
198
|
+
|
|
199
|
+
lines.push(`${icon} ${state.name} — ${state.status}${toolInfo}`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const totalTools = reg.getTotalToolCount();
|
|
203
|
+
const active = reg.getActive().length;
|
|
204
|
+
lines.push(
|
|
205
|
+
`\n---\n${active} active, ${reg.getFailed().length} failed, ${totalTools} total tools`,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// /unipi:mcp-sync — force catalog sync
|
|
213
|
+
pi.registerCommand(`unipi:${MCP_COMMANDS.SYNC}`, {
|
|
214
|
+
description: "Sync MCP server catalog from GitHub",
|
|
215
|
+
handler: async (_args: string, ctx: any) => {
|
|
216
|
+
try {
|
|
217
|
+
ctx.ui.notify("Syncing MCP catalog from GitHub...", "info");
|
|
218
|
+
const catalog = await syncCatalog();
|
|
219
|
+
emitEvent(pi, UNIPI_EVENTS.MCP_CATALOG_SYNCED, {
|
|
220
|
+
totalServers: catalog.totalServers,
|
|
221
|
+
source: catalog.source,
|
|
222
|
+
});
|
|
223
|
+
ctx.ui.notify(
|
|
224
|
+
`MCP Catalog Synced\nSource: ${catalog.source}\nServers: ${catalog.totalServers}\nUpdated: ${catalog.lastUpdated}`,
|
|
225
|
+
"info",
|
|
226
|
+
);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
ctx.ui.notify(
|
|
229
|
+
`MCP sync failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
230
|
+
"error",
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// /unipi:mcp-add — add server overlay
|
|
237
|
+
pi.registerCommand(`unipi:${MCP_COMMANDS.ADD}`, {
|
|
238
|
+
description: "Add an MCP server (browse catalog or custom config)",
|
|
239
|
+
handler: async (_args: string, ctx: any) => {
|
|
240
|
+
if (!ctx.hasUI) {
|
|
241
|
+
ctx.ui.notify("MCP Add requires an interactive UI.", "warning");
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
ctx.ui.custom(
|
|
246
|
+
renderMcpAddOverlay({
|
|
247
|
+
onComplete: () => {
|
|
248
|
+
ctx.ui.notify("MCP server saved. Restart pi to activate.", "info");
|
|
249
|
+
},
|
|
250
|
+
}),
|
|
251
|
+
{
|
|
252
|
+
overlay: true,
|
|
253
|
+
overlayOptions: {
|
|
254
|
+
width: "90%",
|
|
255
|
+
minWidth: 80,
|
|
256
|
+
anchor: "center",
|
|
257
|
+
margin: 2,
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
);
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// /unipi:mcp-settings — settings overlay
|
|
265
|
+
pi.registerCommand(`unipi:${MCP_COMMANDS.SETTINGS}`, {
|
|
266
|
+
description: "Manage MCP server settings",
|
|
267
|
+
handler: async (_args: string, ctx: any) => {
|
|
268
|
+
if (!ctx.hasUI) {
|
|
269
|
+
ctx.ui.notify("MCP Settings requires an interactive UI.", "warning");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
274
|
+
|
|
275
|
+
function openSettings() {
|
|
276
|
+
ctx.ui.custom(
|
|
277
|
+
renderMcpSettingsOverlay({
|
|
278
|
+
registry: registry ?? undefined,
|
|
279
|
+
cwd,
|
|
280
|
+
onComplete: () => {},
|
|
281
|
+
}),
|
|
282
|
+
{
|
|
283
|
+
overlay: true,
|
|
284
|
+
overlayOptions: {
|
|
285
|
+
width: "80%",
|
|
286
|
+
minWidth: 70,
|
|
287
|
+
anchor: "center",
|
|
288
|
+
margin: 2,
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
openSettings();
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
}
|