supipowers 2.0.2 → 2.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.
Files changed (76) hide show
  1. package/README.md +5 -6
  2. package/package.json +4 -2
  3. package/skills/harness/SKILL.md +1 -0
  4. package/src/bootstrap.ts +5 -133
  5. package/src/config/defaults.ts +5 -5
  6. package/src/config/loader.ts +1 -0
  7. package/src/config/schema.ts +2 -6
  8. package/src/context-mode/knowledge/store.ts +381 -43
  9. package/src/context-mode/tools.ts +41 -3
  10. package/src/deps/registry.ts +1 -12
  11. package/src/fix-pr/assessment.ts +1 -0
  12. package/src/fix-pr/prompt-builder.ts +1 -0
  13. package/src/git/commit.ts +76 -18
  14. package/src/harness/command.ts +103 -6
  15. package/src/harness/default-agents/docs.md +39 -0
  16. package/src/harness/docs/config.ts +29 -0
  17. package/src/harness/docs/glob-match.ts +27 -0
  18. package/src/harness/docs/index-renderer.ts +82 -0
  19. package/src/harness/docs/provenance.ts +125 -0
  20. package/src/harness/docs/regen-decision.ts +167 -0
  21. package/src/harness/docs/representative-files.ts +175 -0
  22. package/src/harness/docs/source-hash.ts +106 -0
  23. package/src/harness/docs/validator.ts +233 -0
  24. package/src/harness/hooks/layer-context-inject.ts +35 -1
  25. package/src/harness/hooks/register.ts +24 -3
  26. package/src/harness/pipeline.ts +20 -5
  27. package/src/harness/pr-comment/baseline.ts +105 -0
  28. package/src/harness/pr-comment/ci-env.ts +120 -0
  29. package/src/harness/pr-comment/gh-poster.ts +227 -0
  30. package/src/harness/pr-comment/handler.ts +198 -0
  31. package/src/harness/pr-comment/render.ts +297 -0
  32. package/src/harness/pr-comment/status.ts +95 -0
  33. package/src/harness/pr-comment/types.ts +73 -0
  34. package/src/harness/pr-comment/workflow-summary.ts +47 -0
  35. package/src/harness/project-paths.ts +95 -0
  36. package/src/harness/stages/design.ts +1 -0
  37. package/src/harness/stages/discover.ts +1 -13
  38. package/src/harness/stages/docs.ts +708 -0
  39. package/src/harness/stages/implement-apply.ts +877 -0
  40. package/src/harness/stages/implement.ts +64 -51
  41. package/src/harness/stages/plan.ts +25 -16
  42. package/src/harness/stages/validate.ts +370 -0
  43. package/src/harness/storage.ts +142 -0
  44. package/src/harness/tools.ts +130 -0
  45. package/src/mempalace/bridge.ts +207 -41
  46. package/src/mempalace/config.ts +10 -4
  47. package/src/mempalace/format.ts +122 -6
  48. package/src/mempalace/hooks.ts +204 -56
  49. package/src/mempalace/installer-helper.ts +18 -4
  50. package/src/mempalace/python/mempalace_bridge.py +128 -3
  51. package/src/mempalace/runtime.ts +53 -16
  52. package/src/mempalace/schema.ts +151 -30
  53. package/src/mempalace/session-summary.ts +5 -0
  54. package/src/mempalace/tool.ts +17 -4
  55. package/src/mempalace/upstream-limits.ts +69 -0
  56. package/src/planning/approval-flow.ts +25 -2
  57. package/src/planning/planning-ask-tool.ts +34 -4
  58. package/src/planning/system-prompt.ts +1 -1
  59. package/src/tool-catalog/active-tool-controller.ts +0 -22
  60. package/src/tool-catalog/active-tool-planner.ts +0 -26
  61. package/src/tool-catalog/tool-groups.ts +1 -9
  62. package/src/types.ts +87 -8
  63. package/src/ui-design/session.ts +114 -8
  64. package/src/utils/executable.ts +10 -1
  65. package/src/workspace/state-paths.ts +1 -1
  66. package/src/commands/mcp.ts +0 -814
  67. package/src/mcp/activation.ts +0 -77
  68. package/src/mcp/config.ts +0 -223
  69. package/src/mcp/docs.ts +0 -154
  70. package/src/mcp/gateway.ts +0 -103
  71. package/src/mcp/lifecycle.ts +0 -79
  72. package/src/mcp/manager-tool.ts +0 -104
  73. package/src/mcp/mcpc.ts +0 -113
  74. package/src/mcp/registry.ts +0 -98
  75. package/src/mcp/triggers.ts +0 -62
  76. package/src/mcp/types.ts +0 -95
@@ -1,814 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import type { Platform, PlatformContext } from "../platform/types.js";
4
- import { loadMcpRegistry, addServer, removeServer, updateServer, getServerConfig, acquireLock, discoverHostMcpServers } from "../mcp/config.js";
5
- import { McpcClient } from "../mcp/mcpc.js";
6
- import { generateTriggers } from "../mcp/triggers.js";
7
- import { generateReadme, writeReadme, writeToolsCache, generateSkill, writeSkill, updateAgentsMd } from "../mcp/docs.js";
8
- import { MCPC_EXIT } from "../mcp/types.js";
9
- import type { McpTool, ServerConfig, HostMcpServer } from "../mcp/types.js";
10
- import { lookupMcpServer, pickBestMatch } from "../mcp/registry.js";
11
- import { modelRegistry } from "../config/model-registry-instance.js";
12
- import { resolveModelForAction, createModelBridge, applyModelOverride } from "../config/model-resolver.js";
13
- import { loadModelConfig } from "../config/model-config.js";
14
-
15
- modelRegistry.register({
16
- id: "mcp",
17
- category: "command",
18
- label: "MCP",
19
- harnessRoleHint: "slow",
20
- });
21
-
22
- export interface ParsedMcpArgs {
23
- subcommand?: string;
24
- name?: string;
25
- url?: string;
26
- transport?: string;
27
- command?: string;
28
- commandArgs?: string[];
29
- activation?: string;
30
- taggable?: boolean;
31
- json?: boolean;
32
- docsUrl?: string;
33
- }
34
-
35
- export function parseCliArgs(input: string): ParsedMcpArgs {
36
- const tokens = input.trim().split(/\s+/).filter(Boolean);
37
- if (tokens.length === 0) return {};
38
-
39
- const result: ParsedMcpArgs = { subcommand: tokens[0] };
40
- let i = 1;
41
-
42
- // Parse flags
43
- while (i < tokens.length && tokens[i].startsWith("--")) {
44
- const flag = tokens[i].slice(2);
45
- if (flag === "json") { result.json = true; i++; continue; }
46
- if (flag === "transport" && i + 1 < tokens.length) { result.transport = tokens[++i]; i++; continue; }
47
- if (flag === "docs" && i + 1 < tokens.length) { result.docsUrl = tokens[++i]; i++; continue; }
48
- i++;
49
- }
50
-
51
- // Parse positional args based on subcommand
52
- switch (result.subcommand) {
53
- case "add":
54
- if (i < tokens.length) result.name = tokens[i++];
55
- if (result.transport === "stdio") {
56
- if (i < tokens.length) result.command = tokens[i++];
57
- result.commandArgs = tokens.slice(i);
58
- } else {
59
- if (i < tokens.length) result.url = tokens[i++];
60
- }
61
- break;
62
- case "activation":
63
- if (i < tokens.length) result.name = tokens[i++];
64
- if (i < tokens.length) result.activation = tokens[i++];
65
- break;
66
- case "tag":
67
- if (i < tokens.length) result.name = tokens[i++];
68
- if (i < tokens.length) result.taggable = tokens[i] === "on";
69
- break;
70
- default:
71
- if (i < tokens.length) result.name = tokens[i++];
72
- break;
73
- }
74
-
75
- return result;
76
- }
77
-
78
- // ── Helpers ───────────────────────────────────────────────────
79
-
80
- function createMcpc(platform: Platform): McpcClient {
81
- return new McpcClient((cmd, args, opts) => platform.exec(cmd, args, opts));
82
- }
83
-
84
- /** Check mcpc is installed. Returns null with error message if missing. */
85
- async function ensureMcpc(platform: Platform, ctx: PlatformContext): Promise<McpcClient | null> {
86
- const mcpc = createMcpc(platform);
87
- const installed = await mcpc.checkInstalled();
88
- if (installed.installed) return mcpc;
89
-
90
- ctx.ui.notify(
91
- "mcpc is not installed. Run /supi:upgrade to install required tools, or: npm install -g @apify/mcpc",
92
- "error",
93
- );
94
- return null;
95
- }
96
-
97
- function buildServerDescription(tools: McpTool[]): string {
98
- const descs = tools.slice(0, 3).map((t) => t.description).filter(Boolean);
99
- return descs.length > 0 ? descs.join("; ") : "MCP server";
100
- }
101
-
102
- function collectServersForSkill(
103
- platform: Platform,
104
- cwd: string,
105
- ): Record<string, { tools: McpTool[] }> {
106
- const registry = loadMcpRegistry(platform.paths, cwd);
107
- const result: Record<string, { tools: McpTool[] }> = {};
108
- for (const name of Object.keys(registry.servers)) {
109
- const toolsPath = path.join(
110
- platform.paths.project(cwd, "mcpc", name, "tools.json"),
111
- );
112
- try {
113
- const tools = JSON.parse(fs.readFileSync(toolsPath, "utf-8")) as McpTool[];
114
- result[name] = { tools };
115
- } catch {
116
- result[name] = { tools: [] };
117
- }
118
- }
119
- return result;
120
- }
121
-
122
- function collectServersForAgentsMd(
123
- platform: Platform,
124
- cwd: string,
125
- ): Record<string, { description: string }> {
126
- const registry = loadMcpRegistry(platform.paths, cwd);
127
- const result: Record<string, { description: string }> = {};
128
- for (const [name, config] of Object.entries(registry.servers)) {
129
- if (!config.enabled) continue;
130
- const toolsPath = platform.paths.project(cwd, "mcpc", name, "tools.json");
131
- let tools: McpTool[] = [];
132
- try {
133
- tools = JSON.parse(fs.readFileSync(toolsPath, "utf-8")) as McpTool[];
134
- } catch { /* no cache yet */ }
135
- result[name] = { description: buildServerDescription(tools) };
136
- }
137
- return result;
138
- }
139
-
140
- function regenerateArtifacts(platform: Platform, cwd: string): void {
141
- const basePath = platform.paths.project(cwd, "");
142
- const skillServers = collectServersForSkill(platform, cwd);
143
- writeSkill(basePath, generateSkill(skillServers));
144
- updateAgentsMd(cwd, collectServersForAgentsMd(platform, cwd));
145
- }
146
-
147
- // ── CLI Dispatch ──────────────────────────────────────────────
148
-
149
- export async function handleMcpCli(
150
- platform: Platform,
151
- ctx: PlatformContext,
152
- parsed: ParsedMcpArgs,
153
- ): Promise<void> {
154
- const { paths } = platform;
155
- const { cwd } = ctx;
156
-
157
- switch (parsed.subcommand) {
158
- // ── ADD ────────────────────────────────────────────────
159
- case "add": {
160
- if (!parsed.name) {
161
- ctx.ui.notify("Usage: /supi:mcp add <name> <url>", "warning");
162
- return;
163
- }
164
-
165
- // No URL/command — look up in registry, then prompt, then agentic fallback
166
- if (!parsed.url && !parsed.command) {
167
- ctx.ui.notify(`Looking up "${parsed.name}" in MCP registry...`, "info");
168
-
169
- // Tier 1: Official MCP Registry
170
- const results = await lookupMcpServer(
171
- (cmd, args) => platform.exec(cmd, args),
172
- parsed.name,
173
- );
174
- const match = pickBestMatch(results, parsed.name);
175
-
176
- if (match) {
177
- // Found in registry — confirm with user
178
- const summary = `${match.title} (${match.url})${match.authRequired ? " [auth required]" : ""}`;
179
- ctx.ui.notify(`Found: ${summary}`, "info");
180
-
181
- if (ctx.ui.confirm) {
182
- const confirmed = await ctx.ui.confirm("Add MCP Server", `Add ${match.title}?\n${match.url}`);
183
- if (!confirmed) {
184
- ctx.ui.notify("Cancelled", "info");
185
- return;
186
- }
187
- }
188
-
189
- // Re-enter add flow with the resolved URL
190
- parsed.url = match.url;
191
- parsed.transport = match.transport;
192
- parsed.docsUrl = match.docsUrl;
193
- // Fall through to the normal add logic below
194
- } else if (ctx.hasUI) {
195
- // Tier 2: Not in registry — ask user for URL
196
- ctx.ui.notify(`"${parsed.name}" not found in registry`, "warning");
197
- const manualUrl = await ctx.ui.input("Server URL (or leave empty to search with agent):", {});
198
-
199
- if (manualUrl && manualUrl.trim()) {
200
- parsed.url = manualUrl.trim();
201
- // Fall through to normal add logic
202
- } else {
203
- // Tier 3: Agentic search — last resort
204
- platform.sendMessage({
205
- customType: "supi-mcp-search",
206
- content: `The user wants to add an MCP server called "${parsed.name}" but it wasn't found in the official MCP registry. Search for the official endpoint — check GitHub, project docs, or other sources. Once found, use the mcpc_manager tool to add it with action "add". Include a docsUrl if available.`,
207
- display: true,
208
- }, { deliverAs: "steer", triggerTurn: true });
209
- ctx.ui.notify(`Agent searching for "${parsed.name}"...`, "info");
210
- return;
211
- }
212
- } else {
213
- ctx.ui.notify(`"${parsed.name}" not found. Provide URL: /supi:mcp add ${parsed.name} <url>`, "warning");
214
- return;
215
- }
216
- }
217
-
218
- const lock = acquireLock(paths, cwd);
219
- if (!lock.acquired) {
220
- ctx.ui.notify("Another MCP operation is in progress", "warning");
221
- return;
222
- }
223
-
224
- try {
225
- const serverPartial: Partial<ServerConfig> = {
226
- transport: (parsed.transport ?? "http") as "http" | "stdio",
227
- };
228
- if (parsed.url) serverPartial.url = parsed.url;
229
- if (parsed.command) {
230
- serverPartial.command = parsed.command;
231
- serverPartial.args = parsed.commandArgs ?? [];
232
- }
233
- if (parsed.docsUrl) serverPartial.docsUrl = parsed.docsUrl;
234
-
235
- const addResult = addServer(paths, cwd, parsed.name, serverPartial);
236
- if (!addResult.ok) {
237
- ctx.ui.notify(`Failed to add server: ${addResult.reason}`, "error");
238
- return;
239
- }
240
-
241
- // Ensure mcpc is installed
242
- const mcpc = await ensureMcpc(platform, ctx);
243
- if (!mcpc) return;
244
-
245
- // Connect and fetch tools
246
- const target = parsed.url ?? parsed.command ?? parsed.name;
247
- const connectResult = await mcpc.connect(target, parsed.name);
248
- if (connectResult.code !== MCPC_EXIT.SUCCESS) {
249
- const detail = connectResult.output.trim() || `exit code ${connectResult.code}`;
250
- ctx.ui.notify(`Server added but connection failed: ${detail}`, "warning");
251
- return;
252
- }
253
-
254
- const toolsResult = await mcpc.toolsList(parsed.name);
255
- const tools = toolsResult.tools;
256
-
257
- // Generate triggers and update config
258
- const triggers = generateTriggers(parsed.name, tools);
259
- updateServer(paths, cwd, parsed.name, { triggers });
260
-
261
- // Write artifacts
262
- const basePath = platform.paths.project(cwd, "");
263
- writeToolsCache(basePath, parsed.name, tools);
264
-
265
- const config = getServerConfig(paths, cwd, parsed.name)!;
266
- const readme = generateReadme(parsed.name, config, tools);
267
- writeReadme(basePath, parsed.name, readme);
268
-
269
- // Regenerate skill and AGENTS.md
270
- regenerateArtifacts(platform, cwd);
271
-
272
- ctx.ui.notify(`Added server "${parsed.name}" with ${tools.length} tools`, "info");
273
- } finally {
274
- lock.release();
275
- }
276
- break;
277
- }
278
-
279
- // ── REMOVE ─────────────────────────────────────────────
280
- case "remove": {
281
- if (!parsed.name) {
282
- ctx.ui.notify("Usage: /supi:mcp remove <name>", "warning");
283
- return;
284
- }
285
-
286
- removeServer(paths, cwd, parsed.name);
287
-
288
- // Clean up mcpc/<name>/ directory
289
- const serverDir = platform.paths.project(cwd, "mcpc", parsed.name);
290
- if (fs.existsSync(serverDir)) {
291
- fs.rmSync(serverDir, { recursive: true });
292
- }
293
-
294
- regenerateArtifacts(platform, cwd);
295
- ctx.ui.notify(`Removed server "${parsed.name}"`, "info");
296
- break;
297
- }
298
-
299
- // ── ENABLE ─────────────────────────────────────────────
300
- case "enable": {
301
- if (!parsed.name) {
302
- ctx.ui.notify("Usage: /supi:mcp enable <name>", "warning");
303
- return;
304
- }
305
- const result = updateServer(paths, cwd, parsed.name, { enabled: true });
306
- if (!result.ok) {
307
- ctx.ui.notify(result.reason ?? "Failed to enable server", "error");
308
- return;
309
- }
310
- regenerateArtifacts(platform, cwd);
311
- ctx.ui.notify(`Enabled server "${parsed.name}"`, "info");
312
- break;
313
- }
314
-
315
- // ── DISABLE ────────────────────────────────────────────
316
- case "disable": {
317
- if (!parsed.name) {
318
- ctx.ui.notify("Usage: /supi:mcp disable <name>", "warning");
319
- return;
320
- }
321
- const result = updateServer(paths, cwd, parsed.name, { enabled: false });
322
- if (!result.ok) {
323
- ctx.ui.notify(result.reason ?? "Failed to disable server", "error");
324
- return;
325
- }
326
- regenerateArtifacts(platform, cwd);
327
- ctx.ui.notify(`Disabled server "${parsed.name}"`, "info");
328
- break;
329
- }
330
-
331
- // ── REFRESH ────────────────────────────────────────────
332
- case "refresh": {
333
- const mcpc = await ensureMcpc(platform, ctx);
334
- if (!mcpc) return;
335
- const registry = loadMcpRegistry(paths, cwd);
336
-
337
- const names = parsed.name
338
- ? [parsed.name]
339
- : Object.keys(registry.servers);
340
-
341
- for (const name of names) {
342
- const config = registry.servers[name];
343
- if (!config) {
344
- ctx.ui.notify(`Server "${name}" not found`, "warning");
345
- continue;
346
- }
347
-
348
- const target = config.url ?? config.command ?? name;
349
- const connectResult = await mcpc.connect(target, name);
350
- if (connectResult.code !== MCPC_EXIT.SUCCESS) {
351
- ctx.ui.notify(`Refresh failed for "${name}": ${connectResult.output}`, "warning");
352
- continue;
353
- }
354
-
355
- const toolsResult = await mcpc.toolsList(name);
356
- const tools = toolsResult.tools;
357
-
358
- const triggers = generateTriggers(name, tools);
359
- updateServer(paths, cwd, name, { triggers });
360
-
361
- const basePath = platform.paths.project(cwd, "");
362
- writeToolsCache(basePath, name, tools);
363
-
364
- const updatedConfig = getServerConfig(paths, cwd, name)!;
365
- const readme = generateReadme(name, updatedConfig, tools);
366
- writeReadme(basePath, name, readme);
367
- }
368
-
369
- regenerateArtifacts(platform, cwd);
370
- ctx.ui.notify(
371
- parsed.name
372
- ? `Refreshed server "${parsed.name}"`
373
- : `Refreshed ${names.length} server(s)`,
374
- "info",
375
- );
376
- break;
377
- }
378
-
379
- // ── LOGIN ──────────────────────────────────────────────
380
- case "login": {
381
- if (!parsed.name) {
382
- ctx.ui.notify("Usage: /supi:mcp login <name>", "warning");
383
- return;
384
- }
385
- const config = getServerConfig(paths, cwd, parsed.name);
386
- if (!config) {
387
- ctx.ui.notify(`Server "${parsed.name}" not found`, "error");
388
- return;
389
- }
390
- const target = config.url ?? config.command ?? parsed.name;
391
- const mcpc = await ensureMcpc(platform, ctx);
392
- if (!mcpc) return;
393
- ctx.ui.notify(`Starting OAuth login for "${parsed.name}"...`, "info");
394
- const result = await mcpc.login(target);
395
- if (result.code !== MCPC_EXIT.SUCCESS) {
396
- const detail = result.output.trim() || `exit code ${result.code}`;
397
- ctx.ui.notify(`Login failed: ${detail}`, "error");
398
- return;
399
- }
400
- ctx.ui.notify(`Logged in to "${parsed.name}"`, "info");
401
- break;
402
- }
403
-
404
- // ── LOGOUT ─────────────────────────────────────────────
405
- case "logout": {
406
- if (!parsed.name) {
407
- ctx.ui.notify("Usage: /supi:mcp logout <name>", "warning");
408
- return;
409
- }
410
- const config = getServerConfig(paths, cwd, parsed.name);
411
- if (!config) {
412
- ctx.ui.notify(`Server "${parsed.name}" not found`, "error");
413
- return;
414
- }
415
- const target = config.url ?? config.command ?? parsed.name;
416
- const mcpc = await ensureMcpc(platform, ctx);
417
- if (!mcpc) return;
418
- const result = await mcpc.logout(target);
419
- if (result.code !== MCPC_EXIT.SUCCESS) {
420
- const detail = result.output.trim() || `exit code ${result.code}`;
421
- ctx.ui.notify(`Logout failed: ${detail}`, "error");
422
- return;
423
- }
424
- ctx.ui.notify(`Logged out of "${parsed.name}"`, "info");
425
- break;
426
- }
427
-
428
- // ── ACTIVATION ─────────────────────────────────────────
429
- case "activation": {
430
- if (!parsed.name || !parsed.activation) {
431
- ctx.ui.notify("Usage: /supi:mcp activation <name> <always|contextual|disabled>", "warning");
432
- return;
433
- }
434
- const result = updateServer(paths, cwd, parsed.name, {
435
- activation: parsed.activation as ServerConfig["activation"],
436
- });
437
- if (!result.ok) {
438
- ctx.ui.notify(result.reason ?? "Failed to update activation", "error");
439
- return;
440
- }
441
- ctx.ui.notify(`Set activation for "${parsed.name}" to ${parsed.activation}`, "info");
442
- break;
443
- }
444
-
445
- // ── TAG ────────────────────────────────────────────────
446
- case "tag": {
447
- if (!parsed.name || parsed.taggable === undefined) {
448
- ctx.ui.notify("Usage: /supi:mcp tag <name> <on|off>", "warning");
449
- return;
450
- }
451
- const result = updateServer(paths, cwd, parsed.name, {
452
- taggable: parsed.taggable,
453
- });
454
- if (!result.ok) {
455
- ctx.ui.notify(result.reason ?? "Failed to update taggable", "error");
456
- return;
457
- }
458
- ctx.ui.notify(`Set taggable for "${parsed.name}" to ${parsed.taggable ? "on" : "off"}`, "info");
459
- break;
460
- }
461
-
462
- // ── LIST ───────────────────────────────────────────────
463
- case "list": {
464
- const registry = loadMcpRegistry(paths, cwd);
465
- const entries = Object.entries(registry.servers);
466
-
467
- if (entries.length === 0) {
468
- ctx.ui.notify("No MCP servers configured", "info");
469
- return;
470
- }
471
-
472
- if (parsed.json) {
473
- ctx.ui.notify(JSON.stringify(registry.servers, null, 2), "info");
474
- } else {
475
- const lines = entries.map(([name, config]) => {
476
- const status = config.enabled ? "enabled" : "disabled";
477
- const transport = config.transport.toUpperCase();
478
- const activation = config.activation;
479
- return ` ${name} [${transport}] ${status} (${activation})`;
480
- });
481
- ctx.ui.notify("MCP Servers:\n" + lines.join("\n"), "info");
482
- }
483
- break;
484
- }
485
-
486
- // ── INFO ───────────────────────────────────────────────
487
- case "info": {
488
- if (!parsed.name) {
489
- ctx.ui.notify("Usage: /supi:mcp info <name>", "warning");
490
- return;
491
- }
492
- const config = getServerConfig(paths, cwd, parsed.name);
493
- if (!config) {
494
- ctx.ui.notify(`Server "${parsed.name}" not found`, "error");
495
- return;
496
- }
497
-
498
- const toolsPath = platform.paths.project(cwd, "mcpc", parsed.name, "tools.json");
499
- let tools: McpTool[] = [];
500
- try {
501
- tools = JSON.parse(fs.readFileSync(toolsPath, "utf-8")) as McpTool[];
502
- } catch { /* no cache */ }
503
-
504
- const lines: string[] = [];
505
- lines.push(`Server: ${parsed.name}`);
506
- if (config.url) lines.push(`URL: ${config.url}`);
507
- if (config.command) lines.push(`Command: ${config.command} ${(config.args ?? []).join(" ")}`);
508
- lines.push(`Transport: ${config.transport.toUpperCase()}`);
509
- lines.push(`Activation: ${config.activation}`);
510
- lines.push(`Enabled: ${config.enabled}`);
511
- lines.push(`Taggable: ${config.taggable}`);
512
- lines.push(`Added: ${config.addedAt}`);
513
- if (config.triggers?.length > 0) lines.push(`Triggers: ${config.triggers.join(", ")}`);
514
- lines.push(`Tools (${tools.length}): ${tools.map((t) => t.name).join(", ") || "none cached"}`);
515
-
516
- ctx.ui.notify(lines.join("\n"), "info");
517
- break;
518
- }
519
-
520
- // ── MIGRATE ──────────────────────────────────────────
521
- case "migrate": {
522
- await handleMcpMigrate(platform, ctx);
523
- break;
524
- }
525
-
526
- default:
527
- ctx.ui.notify(
528
- `Unknown subcommand "${parsed.subcommand}". Available: add, remove, enable, disable, refresh, login, logout, activation, tag, list, info, migrate`,
529
- "warning",
530
- );
531
- }
532
- }
533
-
534
- // ── Migrate Handler ───────────────────────────────────────────
535
-
536
- /**
537
- * Discover MCP servers from the host config that are not yet in supi's
538
- * managed registry, present a checkbox multi-select UI, and migrate selected
539
- * servers via the existing "add" flow.
540
- */
541
- export async function handleMcpMigrate(
542
- platform: Platform,
543
- ctx: PlatformContext,
544
- ): Promise<void> {
545
- // 1. Discover host servers
546
- const hostServers = discoverHostMcpServers(platform.paths, ctx.cwd);
547
-
548
- // 2. Load supi registry to find already-managed names
549
- const registry = loadMcpRegistry(platform.paths, ctx.cwd);
550
- const managedNames = new Set(Object.keys(registry.servers));
551
-
552
- // 3. Filter to servers not yet managed
553
- const candidates: HostMcpServer[] = hostServers.filter((s) => !managedNames.has(s.name));
554
-
555
- if (candidates.length === 0) {
556
- ctx.ui.notify("All host MCP servers are already in the supipowers registry.", "info");
557
- return;
558
- }
559
-
560
- // 4. Checkbox-toggle multi-select UI
561
- const selected = new Set<number>();
562
-
563
- while (true) {
564
- const checkboxOptions = candidates.map((s, i) => {
565
- const tick = selected.has(i) ? "✓" : "✗";
566
- // Treat sse as http for display
567
- const t = s.transport === "sse" ? "http" : s.transport;
568
- const auth = s.hasAuth ? " 🔒" : "";
569
- return `${tick} ${s.name} — ${t} (${s.scope})${auth}`;
570
- });
571
-
572
- const selectedCount = selected.size;
573
- const doneLabel = `[Done — migrate ${selectedCount} selected]`;
574
-
575
- checkboxOptions.push("[Select All]");
576
- checkboxOptions.push(doneLabel);
577
- checkboxOptions.push("[Cancel]");
578
-
579
- const pick = await ctx.ui.select(
580
- "Migrate MCP servers from host config",
581
- checkboxOptions,
582
- { helpText: "Select servers to migrate · Esc to cancel" },
583
- );
584
-
585
- if (pick === undefined || pick === null || pick === "[Cancel]") return;
586
-
587
- if (pick === "[Select All]") {
588
- if (selected.size === candidates.length) {
589
- // All selected — deselect all
590
- selected.clear();
591
- } else {
592
- candidates.forEach((_, i) => selected.add(i));
593
- }
594
- continue;
595
- }
596
-
597
- if (pick === doneLabel) break;
598
-
599
- // Toggle the clicked candidate
600
- const idx = checkboxOptions.indexOf(pick);
601
- if (idx >= 0 && idx < candidates.length) {
602
- if (selected.has(idx)) {
603
- selected.delete(idx);
604
- } else {
605
- selected.add(idx);
606
- }
607
- }
608
- }
609
-
610
- if (selected.size === 0) {
611
- ctx.ui.notify("No servers selected. Migration cancelled.", "info");
612
- return;
613
- }
614
-
615
- // 5. Migrate selected servers via the existing add flow
616
- const toMigrate = [...selected].map((i) => candidates[i]);
617
- let migratedCount = 0;
618
-
619
- for (const server of toMigrate) {
620
- // Map host transport to supi's transport (sse → http, treat as HTTP URL-based)
621
- const transport: "http" | "stdio" = server.transport === "stdio" ? "stdio" : "http";
622
-
623
- await handleMcpCli(platform, ctx, {
624
- subcommand: "add",
625
- name: server.name,
626
- url: transport === "http" ? server.url : undefined,
627
- command: transport === "stdio" ? server.command : undefined,
628
- commandArgs: server.args,
629
- transport,
630
- });
631
-
632
- migratedCount++;
633
- }
634
-
635
- ctx.ui.notify(`Migration complete. Processed ${migratedCount} server(s).`, "info");
636
- }
637
-
638
-
639
- export function handleMcp(platform: Platform, ctx: PlatformContext): void {
640
- if (!ctx.hasUI) {
641
- ctx.ui.notify("MCP UI requires interactive mode", "warning");
642
- return;
643
- }
644
-
645
- void (async () => {
646
- while (true) {
647
- const registry = loadMcpRegistry(platform.paths, ctx.cwd);
648
- const entries = Object.entries(registry.servers);
649
-
650
- // Build server list options
651
- const options: string[] = entries.map(([name, config]) => {
652
- const icon = config.enabled ? "\u25cf" : "\u25cb";
653
- const status = config.enabled ? "connected" : "disconnected";
654
- const flags: string[] = [config.activation];
655
- if (config.taggable) flags.push("$taggable");
656
- return `${icon} ${name} — ${status} (${flags.join(", ")})`;
657
- });
658
- options.push("[Add server]");
659
- options.push("[Refresh all]");
660
- options.push("[Migrate from host]");
661
- options.push("[Done]");
662
-
663
- const choice = await ctx.ui.select(
664
- "MCP Servers",
665
- options,
666
- { helpText: "Select a server to manage · Esc to close" },
667
- );
668
-
669
- if (choice === undefined || choice === null || choice === "[Done]") break;
670
-
671
- // ── Add server flow ───────────────────────────────────
672
- if (choice === "[Add server]") {
673
- const name = await ctx.ui.input("Server name", { placeholder: "e.g. figma" });
674
- if (!name) continue;
675
-
676
- const url = await ctx.ui.input("Server URL", { placeholder: "e.g. https://mcp.figma.com" });
677
- if (!url) continue;
678
-
679
- const transport = await ctx.ui.select(
680
- "Transport",
681
- ["http", "stdio"],
682
- { helpText: "How to connect to the server" },
683
- );
684
- if (!transport) continue;
685
-
686
- await handleMcpCli(platform, ctx, {
687
- subcommand: "add",
688
- name,
689
- url: transport === "http" ? url : undefined,
690
- command: transport === "stdio" ? url : undefined,
691
- transport,
692
- });
693
- continue;
694
- }
695
-
696
- // ── Refresh all ───────────────────────────────────────
697
- // ── Migrate from host flow ───────────────────────────
698
- if (choice === "[Migrate from host]") {
699
- await handleMcpMigrate(platform, ctx);
700
- continue;
701
- }
702
-
703
- if (choice === "[Refresh all]") {
704
- await handleMcpCli(platform, ctx, { subcommand: "refresh" });
705
- continue;
706
- }
707
-
708
- // ── Server action menu ────────────────────────────────
709
- const serverIndex = options.indexOf(choice);
710
- if (serverIndex < 0 || serverIndex >= entries.length) continue;
711
-
712
- const [serverName, serverConfig] = entries[serverIndex];
713
-
714
- while (true) {
715
- const serverLabel = serverConfig.url ?? serverConfig.command ?? serverName;
716
- const toggleLabel = serverConfig.enabled ? "[Disable]" : "[Enable]";
717
- const actionOptions = [
718
- toggleLabel,
719
- "[Refresh tools]",
720
- "[Login]",
721
- "[Logout]",
722
- "[Edit triggers]",
723
- "[View README]",
724
- "[Remove]",
725
- "[Back]",
726
- ];
727
-
728
- const action = await ctx.ui.select(
729
- `${serverName} — ${serverLabel}`,
730
- actionOptions,
731
- { helpText: "Select an action" },
732
- );
733
-
734
- if (action === undefined || action === null || action === "[Back]") break;
735
-
736
- switch (action) {
737
- case "[Enable]":
738
- await handleMcpCli(platform, ctx, { subcommand: "enable", name: serverName });
739
- break;
740
- case "[Disable]":
741
- await handleMcpCli(platform, ctx, { subcommand: "disable", name: serverName });
742
- break;
743
- case "[Refresh tools]":
744
- await handleMcpCli(platform, ctx, { subcommand: "refresh", name: serverName });
745
- break;
746
- case "[Login]":
747
- await handleMcpCli(platform, ctx, { subcommand: "login", name: serverName });
748
- break;
749
- case "[Logout]":
750
- await handleMcpCli(platform, ctx, { subcommand: "logout", name: serverName });
751
- break;
752
- case "[Edit triggers]": {
753
- const currentTriggers = serverConfig.triggers?.join(", ") ?? "";
754
- const newTriggers = await ctx.ui.input("Triggers (comma-separated)", {
755
- placeholder: currentTriggers,
756
- });
757
- if (newTriggers !== null && newTriggers !== undefined) {
758
- const triggerList = newTriggers
759
- .split(",")
760
- .map((t) => t.trim())
761
- .filter(Boolean);
762
- updateServer(platform.paths, ctx.cwd, serverName, { triggers: triggerList });
763
- ctx.ui.notify(`Updated triggers for "${serverName}"`, "info");
764
- }
765
- break;
766
- }
767
- case "[View README]": {
768
- const readmePath = platform.paths.project(ctx.cwd, "mcpc", serverName, "README.md");
769
- try {
770
- const content = fs.readFileSync(readmePath, "utf-8");
771
- ctx.ui.notify(content, "info");
772
- } catch {
773
- ctx.ui.notify(`No README found for "${serverName}"`, "warning");
774
- }
775
- break;
776
- }
777
- case "[Remove]": {
778
- const confirmed = ctx.ui.confirm
779
- ? await ctx.ui.confirm("Remove server", `Remove "${serverName}"? This cannot be undone.`)
780
- : true;
781
- if (confirmed) {
782
- await handleMcpCli(platform, ctx, { subcommand: "remove", name: serverName });
783
- }
784
- break;
785
- }
786
- }
787
-
788
- // Re-read config after action (it may have changed)
789
- const updatedConfig = getServerConfig(platform.paths, ctx.cwd, serverName);
790
- if (!updatedConfig) break; // Server was removed
791
- // Update local reference for toggle label on next iteration
792
- Object.assign(serverConfig, updatedConfig);
793
- }
794
- }
795
- })();
796
- }
797
-
798
- export function registerMcpCommand(platform: Platform): void {
799
- platform.registerCommand("supi:mcp", {
800
- description: "Manage MCP servers — add, remove, enable, disable, refresh",
801
- async handler(args: string | undefined, ctx: any) {
802
- const modelCfg = loadModelConfig(platform.paths, ctx.cwd);
803
- const bridge = createModelBridge(platform);
804
- const resolved = resolveModelForAction("mcp", modelRegistry, modelCfg, bridge);
805
- await applyModelOverride(platform, ctx, "mcp", resolved);
806
- if (args) {
807
- // CLI mode — parse and dispatch
808
- await handleMcpCli(platform, ctx, parseCliArgs(args));
809
- } else {
810
- handleMcp(platform, ctx);
811
- }
812
- },
813
- });
814
- }