supipowers 2.0.2 → 2.2.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 (84) 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 +8 -133
  5. package/src/commands/optimize-context.ts +153 -16
  6. package/src/commands/runbook.ts +511 -0
  7. package/src/config/defaults.ts +5 -5
  8. package/src/config/loader.ts +1 -0
  9. package/src/config/schema.ts +2 -6
  10. package/src/context/rule-renderer.ts +274 -2
  11. package/src/context/runbook-extension-template.ts +193 -0
  12. package/src/context/startup-check.ts +197 -2
  13. package/src/context/startup-optimizer.ts +133 -10
  14. package/src/context-mode/knowledge/store.ts +381 -43
  15. package/src/context-mode/tools.ts +41 -3
  16. package/src/deps/registry.ts +1 -12
  17. package/src/fix-pr/assessment.ts +1 -0
  18. package/src/fix-pr/prompt-builder.ts +1 -0
  19. package/src/git/commit.ts +76 -18
  20. package/src/harness/command.ts +201 -12
  21. package/src/harness/default-agents/docs.md +39 -0
  22. package/src/harness/docs/config.ts +29 -0
  23. package/src/harness/docs/glob-match.ts +27 -0
  24. package/src/harness/docs/index-renderer.ts +82 -0
  25. package/src/harness/docs/provenance.ts +125 -0
  26. package/src/harness/docs/regen-decision.ts +167 -0
  27. package/src/harness/docs/representative-files.ts +175 -0
  28. package/src/harness/docs/source-hash.ts +106 -0
  29. package/src/harness/docs/validator.ts +233 -0
  30. package/src/harness/git-verification.ts +515 -0
  31. package/src/harness/git-verify-qa.ts +406 -0
  32. package/src/harness/hooks/layer-context-inject.ts +35 -1
  33. package/src/harness/hooks/register.ts +24 -3
  34. package/src/harness/pipeline.ts +37 -13
  35. package/src/harness/pr-comment/baseline.ts +105 -0
  36. package/src/harness/pr-comment/ci-env.ts +120 -0
  37. package/src/harness/pr-comment/gh-poster.ts +227 -0
  38. package/src/harness/pr-comment/handler.ts +198 -0
  39. package/src/harness/pr-comment/render.ts +297 -0
  40. package/src/harness/pr-comment/status.ts +95 -0
  41. package/src/harness/pr-comment/types.ts +73 -0
  42. package/src/harness/pr-comment/workflow-summary.ts +47 -0
  43. package/src/harness/project-paths.ts +95 -0
  44. package/src/harness/stages/design.ts +1 -0
  45. package/src/harness/stages/discover.ts +1 -13
  46. package/src/harness/stages/docs.ts +708 -0
  47. package/src/harness/stages/implement-apply.ts +934 -0
  48. package/src/harness/stages/implement.ts +64 -51
  49. package/src/harness/stages/plan.ts +25 -16
  50. package/src/harness/stages/validate.ts +478 -0
  51. package/src/harness/storage.ts +142 -0
  52. package/src/harness/tools.ts +130 -0
  53. package/src/mempalace/bridge.ts +207 -41
  54. package/src/mempalace/config.ts +10 -4
  55. package/src/mempalace/format.ts +122 -6
  56. package/src/mempalace/hooks.ts +204 -56
  57. package/src/mempalace/installer-helper.ts +18 -4
  58. package/src/mempalace/python/mempalace_bridge.py +128 -3
  59. package/src/mempalace/runtime.ts +53 -16
  60. package/src/mempalace/schema.ts +151 -30
  61. package/src/mempalace/session-summary.ts +5 -0
  62. package/src/mempalace/tool.ts +17 -4
  63. package/src/mempalace/upstream-limits.ts +69 -0
  64. package/src/planning/approval-flow.ts +25 -2
  65. package/src/planning/planning-ask-tool.ts +34 -4
  66. package/src/planning/system-prompt.ts +1 -1
  67. package/src/tool-catalog/active-tool-controller.ts +0 -22
  68. package/src/tool-catalog/active-tool-planner.ts +0 -26
  69. package/src/tool-catalog/tool-groups.ts +1 -9
  70. package/src/types.ts +127 -8
  71. package/src/ui-design/session.ts +114 -8
  72. package/src/utils/executable.ts +10 -1
  73. package/src/workspace/state-paths.ts +1 -1
  74. package/src/commands/mcp.ts +0 -814
  75. package/src/mcp/activation.ts +0 -77
  76. package/src/mcp/config.ts +0 -223
  77. package/src/mcp/docs.ts +0 -154
  78. package/src/mcp/gateway.ts +0 -103
  79. package/src/mcp/lifecycle.ts +0 -79
  80. package/src/mcp/manager-tool.ts +0 -104
  81. package/src/mcp/mcpc.ts +0 -113
  82. package/src/mcp/registry.ts +0 -98
  83. package/src/mcp/triggers.ts +0 -62
  84. package/src/mcp/types.ts +0 -95
@@ -1,77 +0,0 @@
1
- import type { ServerConfig } from "./types.js";
2
-
3
- /** Extract $tags from user input, matching against registered server names */
4
- export function parseTags(text: string, registeredNames: Set<string>): string[] {
5
- const tags: string[] = [];
6
- // Sort names by length (longest first) to match "figma-plugin" before "figma"
7
- const sorted = [...registeredNames].sort((a, b) => b.length - a.length);
8
-
9
- for (const name of sorted) {
10
- // Build regex: word-boundary-aware, case-insensitive
11
- const pattern = new RegExp(`(?:^|[\\s(])\\$${escapeRegex(name)}(?=$|[\\s).,;:!?])`, "gi");
12
- if (pattern.test(text)) {
13
- tags.push(name);
14
- }
15
- }
16
-
17
- return tags;
18
- }
19
-
20
- function escapeRegex(s: string): string {
21
- return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
22
- }
23
-
24
- /** Check if a trigger/antiTrigger matches the message */
25
- function matchesAny(words: string[], message: string): boolean {
26
- const lower = message.toLowerCase();
27
- return words.some((w) => {
28
- const pattern = new RegExp(`\\b${escapeRegex(w.toLowerCase())}\\b`);
29
- return pattern.test(lower);
30
- });
31
- }
32
-
33
- /** Determine if a server should be active for this turn */
34
- export function shouldActivate(
35
- config: ServerConfig,
36
- message: string,
37
- isTagged: boolean,
38
- ): boolean {
39
- if (!config.enabled) return false;
40
-
41
- // Tag overrides everything (if taggable)
42
- if (isTagged && config.taggable) return true;
43
-
44
- switch (config.activation) {
45
- case "always":
46
- return true;
47
- case "contextual": {
48
- const triggerMatch = config.triggers.length > 0 && matchesAny(config.triggers, message);
49
- if (!triggerMatch) return false;
50
- // AntiTrigger wins
51
- const antiMatch = config.antiTriggers.length > 0 && matchesAny(config.antiTriggers, message);
52
- return !antiMatch;
53
- }
54
- case "disabled":
55
- return false;
56
- default:
57
- return false;
58
- }
59
- }
60
-
61
- /** Compute which servers should be active for a given message */
62
- export function computeActiveServers(
63
- servers: Record<string, ServerConfig>,
64
- message: string,
65
- taggedNames: string[],
66
- ): string[] {
67
- const tagSet = new Set(taggedNames);
68
- const active: string[] = [];
69
-
70
- for (const [name, config] of Object.entries(servers)) {
71
- if (shouldActivate(config, message, tagSet.has(name))) {
72
- active.push(name);
73
- }
74
- }
75
-
76
- return active;
77
- }
package/src/mcp/config.ts DELETED
@@ -1,223 +0,0 @@
1
- // src/mcp/config.ts
2
- import * as fs from "node:fs";
3
- import { homedir } from "node:os";
4
- import * as path from "node:path";
5
- import type { PlatformPaths } from "../platform/types.js";
6
- import type { McpRegistry, ServerConfig, HostMcpServer } from "./types.js";
7
- import { createEmptyRegistry, isValidServerName } from "./types.js";
8
-
9
- function getMcpConfigPath(paths: PlatformPaths, cwd: string): string {
10
- return paths.project(cwd, ".mcp.json");
11
- }
12
-
13
- function getGlobalMcpConfigPath(paths: PlatformPaths): string {
14
- return paths.global(".mcp.json");
15
- }
16
-
17
- function readJsonSafe(filePath: string): Record<string, unknown> | null {
18
- try {
19
- if (!fs.existsSync(filePath)) return null;
20
- return JSON.parse(fs.readFileSync(filePath, "utf-8"));
21
- } catch {
22
- return null;
23
- }
24
- }
25
-
26
- export function loadMcpRegistry(paths: PlatformPaths, cwd: string): McpRegistry {
27
- const globalData = readJsonSafe(getGlobalMcpConfigPath(paths)) as McpRegistry | null;
28
- const projectData = readJsonSafe(getMcpConfigPath(paths, cwd)) as McpRegistry | null;
29
-
30
- const merged: McpRegistry = createEmptyRegistry();
31
-
32
- // Global servers first
33
- if (globalData?.servers) {
34
- for (const [name, config] of Object.entries(globalData.servers)) {
35
- merged.servers[name] = applyDefaults(config);
36
- }
37
- }
38
-
39
- // Project servers override global
40
- if (projectData?.servers) {
41
- for (const [name, config] of Object.entries(projectData.servers)) {
42
- merged.servers[name] = applyDefaults(config);
43
- }
44
- }
45
-
46
- return merged;
47
- }
48
-
49
- function applyDefaults(partial: Partial<ServerConfig>): ServerConfig {
50
- return {
51
- transport: "http",
52
- activation: "contextual",
53
- taggable: true,
54
- triggers: [],
55
- antiTriggers: [],
56
- enabled: true,
57
- authPending: false,
58
- addedAt: new Date().toISOString(),
59
- ...partial,
60
- } as ServerConfig;
61
- }
62
-
63
- export function saveMcpRegistry(paths: PlatformPaths, cwd: string, registry: McpRegistry): void {
64
- const configPath = getMcpConfigPath(paths, cwd);
65
- fs.mkdirSync(path.dirname(configPath), { recursive: true });
66
- fs.writeFileSync(configPath, JSON.stringify(registry, null, 2) + "\n");
67
- }
68
-
69
- /** Load project-only registry (not merged with global) for save operations */
70
- function loadProjectRegistry(paths: PlatformPaths, cwd: string): McpRegistry {
71
- const data = readJsonSafe(getMcpConfigPath(paths, cwd)) as McpRegistry | null;
72
- return data ?? createEmptyRegistry();
73
- }
74
-
75
- export function addServer(
76
- paths: PlatformPaths,
77
- cwd: string,
78
- name: string,
79
- config: Partial<ServerConfig>,
80
- ): { ok: boolean; reason?: string } {
81
- const validation = isValidServerName(name);
82
- if (!validation.valid) return { ok: false, reason: validation.reason };
83
-
84
- const registry = loadProjectRegistry(paths, cwd);
85
- registry.servers[name] = applyDefaults({ ...config, addedAt: new Date().toISOString() });
86
- saveMcpRegistry(paths, cwd, registry);
87
- return { ok: true };
88
- }
89
-
90
- export function removeServer(paths: PlatformPaths, cwd: string, name: string): void {
91
- const registry = loadProjectRegistry(paths, cwd);
92
- delete registry.servers[name];
93
- saveMcpRegistry(paths, cwd, registry);
94
- }
95
-
96
- export function updateServer(
97
- paths: PlatformPaths,
98
- cwd: string,
99
- name: string,
100
- updates: Partial<ServerConfig>,
101
- ): { ok: boolean; reason?: string } {
102
- const registry = loadProjectRegistry(paths, cwd);
103
- if (!registry.servers[name]) return { ok: false, reason: `Server "${name}" not found` };
104
- registry.servers[name] = { ...registry.servers[name], ...updates };
105
- saveMcpRegistry(paths, cwd, registry);
106
- return { ok: true };
107
- }
108
-
109
- export function getServerConfig(
110
- paths: PlatformPaths,
111
- cwd: string,
112
- name: string,
113
- ): ServerConfig | undefined {
114
- const registry = loadMcpRegistry(paths, cwd);
115
- return registry.servers[name];
116
- }
117
-
118
- // ── Lockfile ──────────────────────────────────────────────────
119
-
120
- const LOCK_TIMEOUT_MS = 5000;
121
-
122
- export function acquireLock(paths: PlatformPaths, cwd: string): { acquired: boolean; release: () => void } {
123
- const lockPath = paths.project(cwd, ".mcp.lock");
124
- fs.mkdirSync(path.dirname(lockPath), { recursive: true });
125
-
126
- // Check for stale lock
127
- if (fs.existsSync(lockPath)) {
128
- try {
129
- const data = JSON.parse(fs.readFileSync(lockPath, "utf-8"));
130
- const age = Date.now() - data.timestamp;
131
- if (age < LOCK_TIMEOUT_MS) {
132
- return { acquired: false, release: () => {} };
133
- }
134
- } catch { /* corrupt lock, overwrite */ }
135
- }
136
-
137
- fs.writeFileSync(lockPath, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
138
-
139
- return {
140
- acquired: true,
141
- release: () => {
142
- try { fs.unlinkSync(lockPath); } catch { /* already cleaned */ }
143
- },
144
- };
145
- }
146
-
147
-
148
- // ── Host Config Discovery ─────────────────────────────────────
149
-
150
- interface HostMcpRaw {
151
- mcpServers?: Record<string, {
152
- type?: "stdio" | "http" | "sse";
153
- command?: string;
154
- args?: string[];
155
- url?: string;
156
- headers?: Record<string, string>;
157
- env?: Record<string, string>;
158
- enabled?: boolean;
159
- auth?: unknown;
160
- oauth?: unknown;
161
- }>;
162
- }
163
-
164
- /**
165
- * Discover MCP servers from host config files:
166
- * - User-level OMP: paths.agent("mcp.json") → ~/.omp/agent/mcp.json
167
- * - Project-level OMP: <cwd>/<dotDir>/mcp.json → <cwd>/.omp/mcp.json
168
- * - Claude Code: ~/.claude.json
169
- *
170
- * All use the same { mcpServers: Record<string, ...> } shape.
171
- */
172
- export function discoverHostMcpServers(
173
- paths: PlatformPaths,
174
- cwd: string,
175
- homeDir: string = homedir(),
176
- ): HostMcpServer[] {
177
- const dot = paths.dotDir;
178
-
179
- // OMP agent-level (user) config: ~/.omp/agent/mcp.json
180
- const userPath = paths.agent("mcp.json");
181
- // OMP project-level config: <cwd>/.omp/mcp.json
182
- const projectPath = path.join(cwd, dot, "mcp.json");
183
- // Claude Code user config: ~/.claude.json
184
- const claudeCodePath = path.join(homeDir, ".claude.json");
185
-
186
- const sources: Array<{ scope: HostMcpServer["scope"]; filePath: string }> = [
187
- { scope: "user", filePath: userPath },
188
- { scope: "project", filePath: projectPath },
189
- { scope: "claude-code", filePath: claudeCodePath },
190
- ];
191
-
192
- const discovered: HostMcpServer[] = [];
193
-
194
- for (const { scope, filePath } of sources) {
195
- const data = readJsonSafe(filePath) as HostMcpRaw | null;
196
- if (!data?.mcpServers) continue;
197
-
198
- for (const [name, raw] of Object.entries(data.mcpServers)) {
199
- // Skip explicitly disabled servers
200
- if (raw.enabled === false) continue;
201
-
202
- const effectiveType = raw.type ?? "stdio";
203
- let transport: HostMcpServer["transport"] = "stdio";
204
- if (effectiveType === "http") transport = "http";
205
- else if (effectiveType === "sse") transport = "sse";
206
-
207
- discovered.push({
208
- name,
209
- scope,
210
- transport,
211
- url: raw.url,
212
- command: raw.command,
213
- args: raw.args,
214
- env: raw.env,
215
- headers: raw.headers,
216
- enabled: raw.enabled,
217
- hasAuth: raw.auth !== undefined || raw.oauth !== undefined,
218
- });
219
- }
220
- }
221
-
222
- return discovered;
223
- }
package/src/mcp/docs.ts DELETED
@@ -1,154 +0,0 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import type { McpTool, ServerConfig } from "./types.js";
4
-
5
- // ── README Generation ────────────────────────────────────────
6
-
7
- export function generateReadme(name: string, config: ServerConfig, tools: McpTool[]): string {
8
- const lines: string[] = [];
9
-
10
- lines.push(`# ${name} (MCP Server)`);
11
- lines.push("");
12
- if (config.url) lines.push(`**URL:** ${config.url}`);
13
- if (config.command) lines.push(`**Command:** ${config.command} ${(config.args ?? []).join(" ")}`);
14
- lines.push(`**Transport:** ${config.transport.toUpperCase()} | **Session:** @supi-${name}`);
15
- if (config.auth) lines.push(`**Auth:** ${config.auth.type}${config.auth.profile ? ` (profile: ${config.auth.profile})` : ""}`);
16
- lines.push("");
17
-
18
- if (config.triggers?.length > 0) {
19
- lines.push("## When to Use");
20
- for (const t of config.triggers) lines.push(`- ${t}`);
21
- lines.push("");
22
- }
23
-
24
- lines.push("## Tools");
25
- lines.push("");
26
- for (const tool of tools) {
27
- lines.push(`### ${tool.name}`);
28
- if (tool.description) lines.push(tool.description);
29
- if (tool.inputSchema) {
30
- const props = (tool.inputSchema as any).properties;
31
- if (props) {
32
- lines.push("**Parameters:**");
33
- for (const [pName, pDef] of Object.entries(props)) {
34
- const def = pDef as any;
35
- lines.push(`- \`${pName}\` (${def.type ?? "any"}) — ${def.description ?? ""}`);
36
- }
37
- }
38
- }
39
- lines.push("");
40
- lines.push("**Example:**");
41
- lines.push("```bash");
42
- lines.push(`mcpc --json @supi-${name} tools-call ${tool.name}`);
43
- lines.push("```");
44
- lines.push("");
45
- }
46
-
47
- lines.push("---");
48
- lines.push(`*Generated by supipowers. Refresh with /supi:mcp refresh ${name}*`);
49
-
50
- return lines.join("\n");
51
- }
52
-
53
- export function writeReadme(basePath: string, name: string, content: string): void {
54
- const dir = path.join(basePath, "mcpc", name);
55
- fs.mkdirSync(dir, { recursive: true });
56
- fs.writeFileSync(path.join(dir, "README.md"), content);
57
- }
58
-
59
- export function writeToolsCache(basePath: string, name: string, tools: McpTool[]): void {
60
- const dir = path.join(basePath, "mcpc", name);
61
- fs.mkdirSync(dir, { recursive: true });
62
- fs.writeFileSync(path.join(dir, "tools.json"), JSON.stringify(tools, null, 2));
63
- }
64
-
65
- // ── Skill Generation ─────────────────────────────────────────
66
-
67
- export function generateSkill(servers: Record<string, { tools: McpTool[] }>): string {
68
- const names = Object.keys(servers);
69
- const triggerList = names.map((n) => `$${n}`).join(", ");
70
-
71
- const lines: string[] = [];
72
- lines.push("---");
73
- lines.push("name: supi:mcpc");
74
- lines.push(`description: Use mcpc CLI to interact with MCP servers managed by supipowers. Triggers on: ${triggerList} or any mcpc tool invocation.`);
75
- lines.push("allowed-tools: Bash(mcpc:*)");
76
- lines.push("---");
77
- lines.push("");
78
- lines.push("# mcpc: MCP tools via supipowers");
79
- lines.push("");
80
- lines.push("Use the `mcpc_<name>` gateway tools to interact with MCP servers.");
81
- lines.push("Each server has a dedicated tool registered in your Available Tools.");
82
- lines.push("");
83
- lines.push("## Quick Reference");
84
- lines.push("");
85
- lines.push("The gateway tools accept `tool` (MCP tool name) and `args` (key-value object).");
86
- lines.push("Under the hood, they run: `mcpc --json @supi-<name> tools-call <tool> key:=value`");
87
- lines.push("");
88
- lines.push("## Available Servers");
89
- lines.push("");
90
-
91
- for (const [name, { tools }] of Object.entries(servers)) {
92
- lines.push(`### ${name} (@supi-${name})`);
93
- lines.push(`Tools: ${tools.map((t) => t.name).join(", ")}`);
94
- lines.push("");
95
- }
96
-
97
- return lines.join("\n");
98
- }
99
-
100
- export function writeSkill(basePath: string, content: string): void {
101
- const dir = path.join(basePath, "skills", "mcpc");
102
- fs.mkdirSync(dir, { recursive: true });
103
- fs.writeFileSync(path.join(dir, "SKILL.md"), content);
104
- }
105
-
106
- // ── AGENTS.md ────────────────────────────────────────────────
107
-
108
- const MARKER_START = "<!-- supipowers:mcpc:start -->";
109
- const MARKER_END = "<!-- supipowers:mcpc:end -->";
110
-
111
- function buildManagedSection(servers: Record<string, { description: string }>): string {
112
- const entries = Object.entries(servers);
113
- if (entries.length === 0) return "";
114
-
115
- const lines: string[] = [];
116
- lines.push(MARKER_START);
117
- lines.push("## MCP Servers (managed by supipowers)");
118
- lines.push("");
119
- lines.push("The following MCP servers are available via `$tag` or the `mcpc_<name>` tool:");
120
- lines.push("");
121
- for (const [name, { description }] of entries) {
122
- lines.push(`- **$${name}** — ${description}. Details: .omp/supipowers/mcpc/${name}/README.md`);
123
- }
124
- lines.push("");
125
- lines.push("Use `/supi:mcp` to manage servers. Invoke the `supi:mcpc` skill for usage patterns.");
126
- lines.push(MARKER_END);
127
- return lines.join("\n");
128
- }
129
-
130
- export function updateAgentsMd(cwd: string, servers: Record<string, { description: string }>): void {
131
- const agentsPath = path.join(cwd, "AGENTS.md");
132
- const section = buildManagedSection(servers);
133
-
134
- if (!fs.existsSync(agentsPath)) {
135
- if (section) fs.writeFileSync(agentsPath, section + "\n");
136
- return;
137
- }
138
-
139
- let content = fs.readFileSync(agentsPath, "utf-8");
140
- const startIdx = content.indexOf(MARKER_START);
141
- const endIdx = content.indexOf(MARKER_END);
142
-
143
- if (startIdx !== -1 && endIdx !== -1) {
144
- // Replace existing section
145
- const before = content.slice(0, startIdx);
146
- const after = content.slice(endIdx + MARKER_END.length);
147
- content = section ? before + section + after : before.trimEnd() + after;
148
- } else if (section) {
149
- // Append
150
- content = content.trimEnd() + "\n\n" + section + "\n";
151
- }
152
-
153
- fs.writeFileSync(agentsPath, content);
154
- }
@@ -1,103 +0,0 @@
1
- // src/mcp/gateway.ts
2
- import type { ServerConfig, McpTool } from "./types.js";
3
- import type { McpcClient } from "./mcpc.js";
4
-
5
- const SNIPPET_TOOL_LIMIT = 10;
6
-
7
- export function buildPromptSnippet(name: string, tools: McpTool[]): string {
8
- const toolNames = tools.slice(0, SNIPPET_TOOL_LIMIT).map((t) => t.name);
9
- const suffix = tools.length > SNIPPET_TOOL_LIMIT
10
- ? ` (${tools.length} tools, see README for full list)`
11
- : ` (${tools.length} tools)`;
12
- return `mcpc_${name} — ${toolNames.join(", ")}${suffix}`;
13
- }
14
-
15
- export function buildPromptGuidelines(config: ServerConfig): string[] {
16
- const guidelines: string[] = [];
17
- if (config.triggers.length > 0) {
18
- guidelines.push(`Use when the context involves: ${config.triggers.join(", ")}`);
19
- }
20
- if (config.antiTriggers.length > 0) {
21
- guidelines.push(`Do NOT use for: ${config.antiTriggers.join(", ")}`);
22
- }
23
- return guidelines;
24
- }
25
-
26
- export interface GatewayToolDef {
27
- name: string;
28
- label: string;
29
- description: string;
30
- promptSnippet: string;
31
- promptGuidelines: string[];
32
- parameters: any; // TypeBox schema — built at registration time
33
- executeFn: (toolName: string, args?: Record<string, unknown>) => Promise<any>;
34
- }
35
-
36
- export function buildGatewayToolDef(
37
- name: string,
38
- config: ServerConfig,
39
- tools: McpTool[],
40
- ): GatewayToolDef {
41
- const descriptions = tools.slice(0, 5).map((t) => t.description).filter(Boolean);
42
- const summary = descriptions.join(". ").slice(0, 200);
43
-
44
- return {
45
- name: `mcpc_${name}`,
46
- label: `${name} (via mcpc)`,
47
- description: summary || `MCP server: ${name}`,
48
- promptSnippet: buildPromptSnippet(name, tools),
49
- promptGuidelines: buildPromptGuidelines(config),
50
- parameters: {}, // Placeholder — actual TypeBox schema built at registration time
51
- executeFn: async () => {}, // Placeholder — wired at registration time
52
- };
53
- }
54
-
55
- /**
56
- * Build the mcpc args serialization and execute a tool call via mcpc.
57
- * Returns content blocks for the agent.
58
- */
59
- export async function executeGatewayCall(
60
- client: McpcClient,
61
- sessionName: string,
62
- toolName: string,
63
- args?: Record<string, unknown>,
64
- ): Promise<{ content: Array<{ type: string; text: string }>; details: Record<string, any> }> {
65
- let result = await client.toolsCall(sessionName, toolName, args);
66
-
67
- // Session crash (exit code 3) — restart and retry once
68
- if (result.code === 3) {
69
- const restartResult = await client.restart(sessionName);
70
- if (restartResult.code !== 0) {
71
- throw new Error(`Session @supi-${sessionName} crashed and couldn't recover. Run /supi:mcp login ${sessionName} or check network.`);
72
- }
73
- result = await client.toolsCall(sessionName, toolName, args);
74
- if (result.code !== 0) {
75
- throw new Error(result.error || `mcpc call failed after restart (code ${result.code})`);
76
- }
77
- }
78
-
79
- // Stale tool cache (exit code 2) — hint to refresh
80
- if (result.code === 2) {
81
- const refreshed = await client.toolsList(sessionName);
82
- const toolNames = refreshed.tools.map((t) => t.name).join(", ");
83
- throw new Error(`Tool "${toolName}" not found. Server tools have been refreshed. Available: [${toolNames}]. Please retry.`);
84
- }
85
-
86
- if (result.code !== 0) {
87
- throw new Error(result.error || `mcpc exited with code ${result.code}`);
88
- }
89
-
90
- // mcpc --json returns { content: [...] } for tool calls
91
- if (result.data?.content) {
92
- return {
93
- content: result.data.content,
94
- details: { serverSession: `@supi-${sessionName}`, toolName },
95
- };
96
- }
97
-
98
- // Fallback: wrap raw output
99
- return {
100
- content: [{ type: "text", text: typeof result.data === "string" ? result.data : JSON.stringify(result.data) }],
101
- details: { serverSession: `@supi-${sessionName}`, toolName },
102
- };
103
- }
@@ -1,79 +0,0 @@
1
- // src/mcp/lifecycle.ts
2
- import type { McpRegistry, ServerState } from "./types.js";
3
- import type { McpcClient } from "./mcpc.js";
4
-
5
- const RETRY_DELAY_MS = 2000;
6
-
7
- export async function initializeMcpServers(
8
- registry: McpRegistry,
9
- client: McpcClient,
10
- ): Promise<Record<string, ServerState>> {
11
- const states: Record<string, ServerState> = {};
12
-
13
- for (const [name, config] of Object.entries(registry.servers)) {
14
- if (!config.enabled) {
15
- states[name] = { config, status: "disconnected" };
16
- continue;
17
- }
18
-
19
- const target = config.url ?? config.command ?? "";
20
-
21
- // Build auth header for bearer tokens
22
- let authHeader: string | undefined;
23
- if (config.auth?.type === "bearer" && config.auth.envVar) {
24
- const token = process.env[config.auth.envVar];
25
- if (token) authHeader = `Authorization: Bearer ${token}`;
26
- }
27
-
28
- let connectResult = await client.connect(target, name, authHeader);
29
-
30
- // Auth error
31
- if (connectResult.code === 4) {
32
- states[name] = { config: { ...config, authPending: true }, status: "auth-pending" };
33
- continue;
34
- }
35
-
36
- // Network error — retry once
37
- if (connectResult.code === 3) {
38
- await delay(RETRY_DELAY_MS);
39
- connectResult = await client.connect(target, name, authHeader);
40
- if (connectResult.code !== 0) {
41
- states[name] = { config, status: "offline" };
42
- continue;
43
- }
44
- }
45
-
46
- if (connectResult.code !== 0) {
47
- states[name] = { config, status: "offline" };
48
- continue;
49
- }
50
-
51
- // Fetch tool catalog
52
- const toolsResult = await client.toolsList(name);
53
- const catalog = {
54
- serverName: name,
55
- tools: toolsResult.tools,
56
- fetchedAt: new Date().toISOString(),
57
- };
58
-
59
- states[name] = { config, status: "connected", catalog };
60
- }
61
-
62
- return states;
63
- }
64
-
65
- export async function shutdownMcpServers(
66
- sessionNames: string[],
67
- client: McpcClient,
68
- closeOnExit: boolean,
69
- ): Promise<void> {
70
- if (!closeOnExit) return;
71
-
72
- await Promise.allSettled(
73
- sessionNames.map((name) => client.close(name)),
74
- );
75
- }
76
-
77
- function delay(ms: number): Promise<void> {
78
- return new Promise((resolve) => setTimeout(resolve, ms));
79
- }