@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/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
+ }