@sammorrowdrums/mcpi-ext 0.2.0 → 1.0.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 (145) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -47
  3. package/dist/code-mode/eligibility.d.ts +35 -0
  4. package/dist/code-mode/eligibility.js +64 -0
  5. package/dist/code-mode/executor.d.ts +53 -0
  6. package/dist/code-mode/executor.js +185 -0
  7. package/dist/code-mode/index.d.ts +107 -0
  8. package/dist/code-mode/index.js +267 -0
  9. package/dist/code-mode/isolated-vm.d.ts +65 -0
  10. package/dist/code-mode/isolated-vm.js +116 -0
  11. package/dist/code-mode/tools.d.ts +48 -0
  12. package/dist/code-mode/tools.js +73 -0
  13. package/dist/code-mode/type-hints.d.ts +20 -0
  14. package/dist/code-mode/type-hints.js +205 -0
  15. package/dist/docker-e2e.d.ts +54 -0
  16. package/dist/docker-e2e.js +234 -0
  17. package/dist/index.d.ts +11 -0
  18. package/dist/index.js +227 -0
  19. package/dist/mcp/call-tool-result.d.ts +10 -0
  20. package/dist/mcp/call-tool-result.js +70 -0
  21. package/dist/mcp/client-factory.d.ts +59 -0
  22. package/dist/mcp/client-factory.js +76 -0
  23. package/dist/mcp/client-manager.d.ts +88 -0
  24. package/dist/mcp/client-manager.js +307 -0
  25. package/dist/mcp/config-loader.d.ts +7 -0
  26. package/dist/mcp/config-loader.js +42 -0
  27. package/dist/mcp/config.d.ts +56 -0
  28. package/dist/mcp/config.js +42 -0
  29. package/dist/mcp/gateway-defaults.d.ts +14 -0
  30. package/dist/mcp/gateway-defaults.js +21 -0
  31. package/dist/mcp/host-approval.d.ts +16 -0
  32. package/dist/mcp/host-approval.js +19 -0
  33. package/dist/mcp/host-elicitation.d.ts +19 -0
  34. package/dist/mcp/host-elicitation.js +94 -0
  35. package/dist/mcp/index.d.ts +8 -0
  36. package/dist/mcp/index.js +7 -0
  37. package/dist/mcp/policy.d.ts +314 -0
  38. package/dist/mcp/policy.js +848 -0
  39. package/dist/routing/facilities.d.ts +109 -0
  40. package/dist/routing/facilities.js +175 -0
  41. package/dist/routing/format.d.ts +24 -0
  42. package/dist/routing/format.js +64 -0
  43. package/dist/routing/index.d.ts +4 -0
  44. package/dist/routing/index.js +4 -0
  45. package/dist/routing/seam.d.ts +35 -0
  46. package/dist/routing/seam.js +28 -0
  47. package/dist/routing/tripwire.d.ts +42 -0
  48. package/dist/routing/tripwire.js +80 -0
  49. package/dist/skills/discover.d.ts +12 -0
  50. package/dist/skills/discover.js +85 -0
  51. package/dist/skills/format.d.ts +13 -0
  52. package/dist/skills/format.js +42 -0
  53. package/dist/skills/index.d.ts +6 -0
  54. package/dist/skills/index.js +6 -0
  55. package/dist/skills/load-skill-tool.d.ts +56 -0
  56. package/dist/skills/load-skill-tool.js +183 -0
  57. package/dist/skills/mcp-tool-proxy.d.ts +7 -0
  58. package/dist/skills/mcp-tool-proxy.js +41 -0
  59. package/dist/skills/sep2640/client.d.ts +91 -0
  60. package/dist/skills/sep2640/client.js +160 -0
  61. package/dist/skills/sep2640/discover.d.ts +33 -0
  62. package/dist/skills/sep2640/discover.js +82 -0
  63. package/dist/skills/sep2640/index.d.ts +13 -0
  64. package/dist/skills/sep2640/index.js +13 -0
  65. package/dist/skills/sep2640/integrity.d.ts +58 -0
  66. package/dist/skills/sep2640/integrity.js +185 -0
  67. package/dist/skills/sep2640/load.d.ts +61 -0
  68. package/dist/skills/sep2640/load.js +90 -0
  69. package/dist/skills/sep2640/protocol.d.ts +146 -0
  70. package/dist/skills/sep2640/protocol.js +214 -0
  71. package/dist/skills/sep2640/spec.d.ts +76 -0
  72. package/dist/skills/sep2640/spec.js +88 -0
  73. package/dist/skills/skill-registry.d.ts +85 -0
  74. package/dist/skills/skill-registry.js +85 -0
  75. package/dist/tool-cli/bridge.d.ts +14 -0
  76. package/dist/tool-cli/bridge.js +196 -0
  77. package/dist/tool-cli/format.d.ts +20 -0
  78. package/dist/tool-cli/format.js +73 -0
  79. package/dist/tool-cli/index.d.ts +7 -0
  80. package/dist/tool-cli/index.js +5 -0
  81. package/dist/tool-cli/provider.d.ts +21 -0
  82. package/dist/tool-cli/provider.js +114 -0
  83. package/dist/tool-cli/startup.d.ts +22 -0
  84. package/dist/tool-cli/startup.js +47 -0
  85. package/package.json +78 -26
  86. package/.dockerignore +0 -6
  87. package/.github/dependabot.yml +0 -25
  88. package/.github/workflows/ci.yml +0 -50
  89. package/.hadolint.yaml +0 -6
  90. package/.prettierignore +0 -4
  91. package/.prettierrc +0 -7
  92. package/AGENTS.md +0 -118
  93. package/DECISIONS.md +0 -99
  94. package/Dockerfile +0 -39
  95. package/aube-lock.yaml +0 -4146
  96. package/docs/code-mode.md +0 -38
  97. package/docs/skills.md +0 -57
  98. package/docs/tool-cli.md +0 -54
  99. package/eslint.config.js +0 -27
  100. package/images/banner.webp +0 -0
  101. package/images/code-c-maude.webp +0 -0
  102. package/images/code-mode-histogram.png +0 -0
  103. package/images/nuclear-mcp-football.webp +0 -0
  104. package/images/skills-enabling-mcp-tools.png +0 -0
  105. package/images/the-skill-dealer.webp +0 -0
  106. package/images/tool-cli-grep.png +0 -0
  107. package/mise.toml +0 -53
  108. package/scripts/docker-entrypoint.sh +0 -29
  109. package/scripts/test-echo-server.mjs +0 -28
  110. package/scripts/test-mcp-integration.mjs +0 -59
  111. package/scripts/test-tool-cli-smoke.mjs +0 -93
  112. package/src/code-mode/code-mode-integration.test.ts +0 -135
  113. package/src/code-mode/eligibility.test.ts +0 -60
  114. package/src/code-mode/eligibility.ts +0 -16
  115. package/src/code-mode/executor.test.ts +0 -168
  116. package/src/code-mode/executor.ts +0 -163
  117. package/src/code-mode/index.ts +0 -208
  118. package/src/code-mode/tools.ts +0 -110
  119. package/src/code-mode/type-hints.test.ts +0 -187
  120. package/src/code-mode/type-hints.ts +0 -249
  121. package/src/docker-e2e.test.ts +0 -195
  122. package/src/docker-e2e.ts +0 -277
  123. package/src/index.test.ts +0 -8
  124. package/src/index.ts +0 -179
  125. package/src/mcp/client-manager.test.ts +0 -250
  126. package/src/mcp/client-manager.ts +0 -199
  127. package/src/mcp/config-loader.test.ts +0 -115
  128. package/src/mcp/config-loader.ts +0 -46
  129. package/src/mcp/config.ts +0 -40
  130. package/src/mcp/index.ts +0 -4
  131. package/src/skills/discover.ts +0 -81
  132. package/src/skills/format.test.ts +0 -70
  133. package/src/skills/format.ts +0 -42
  134. package/src/skills/index.ts +0 -9
  135. package/src/skills/load-skill-tool.ts +0 -136
  136. package/src/skills/mcp-tool-proxy.ts +0 -143
  137. package/src/skills/skill-integration.test.ts +0 -139
  138. package/src/skills/skill-registry.test.ts +0 -83
  139. package/src/skills/skill-registry.ts +0 -69
  140. package/src/test-servers/weather-server.ts +0 -171
  141. package/src/test-servers/weather-stdio.ts +0 -11
  142. package/src/tool-cli/format.test.ts +0 -33
  143. package/src/tool-cli/format.ts +0 -56
  144. package/src/tool-cli/index.ts +0 -3
  145. package/tsconfig.json +0 -21
package/src/docker-e2e.ts DELETED
@@ -1,277 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import type {
3
- AgentToolResult,
4
- AgentToolUpdateCallback,
5
- ExtensionContext,
6
- } from "@sammorrowdrums/mcpi";
7
- import { Type, type Static } from "typebox";
8
-
9
- const DOCKER_IMAGE = "mcpi-ext-e2e";
10
-
11
- const DockerE2EParams = Type.Object({
12
- task: Type.String({ description: "The task/prompt to run inside the sandbox" }),
13
- token: Type.String({
14
- description: "GitHub token for Copilot model access",
15
- }),
16
- model: Type.Optional(
17
- Type.String({
18
- description: "Model to use (e.g. claude-sonnet-4-5). Defaults to the provider default.",
19
- }),
20
- ),
21
- timeout: Type.Optional(
22
- Type.Number({
23
- description: "Timeout in seconds. Defaults to 300 (5 minutes).",
24
- }),
25
- ),
26
- });
27
-
28
- type DockerE2EInput = Static<typeof DockerE2EParams>;
29
-
30
- interface RunLogEntry {
31
- type: string;
32
- timestamp: number;
33
- raw: string;
34
- }
35
-
36
- export interface DockerE2EDetails {
37
- exitCode: number | null;
38
- log: RunLogEntry[];
39
- duration: number;
40
- model?: string;
41
- usage?: {
42
- input: number;
43
- output: number;
44
- cacheRead: number;
45
- cacheWrite: number;
46
- cost: number;
47
- turns: number;
48
- };
49
- }
50
-
51
- export function parseJsonlLine(line: string): RunLogEntry | null {
52
- const trimmed = line.trim();
53
- if (!trimmed) return null;
54
- try {
55
- const parsed = JSON.parse(trimmed);
56
- return { type: parsed.type ?? "unknown", timestamp: Date.now(), raw: trimmed };
57
- } catch {
58
- return { type: "raw", timestamp: Date.now(), raw: trimmed };
59
- }
60
- }
61
-
62
- export function extractAssistantText(log: RunLogEntry[]): string {
63
- const parts: string[] = [];
64
- for (const entry of log) {
65
- if (entry.type !== "message_end") continue;
66
- try {
67
- const event = JSON.parse(entry.raw);
68
- const msg = event.message;
69
- if (msg?.role !== "assistant") continue;
70
- for (const part of msg.content ?? []) {
71
- if (part.type === "text") parts.push(part.text);
72
- }
73
- } catch {
74
- // skip malformed
75
- }
76
- }
77
- return parts.join("\n");
78
- }
79
-
80
- export function extractUsage(log: RunLogEntry[]) {
81
- const usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
82
- for (const entry of log) {
83
- if (entry.type !== "message_end") continue;
84
- try {
85
- const event = JSON.parse(entry.raw);
86
- const msg = event.message;
87
- if (msg?.role !== "assistant") continue;
88
- usage.turns++;
89
- const u = msg.usage;
90
- if (u) {
91
- usage.input += u.input ?? 0;
92
- usage.output += u.output ?? 0;
93
- usage.cacheRead += u.cacheRead ?? 0;
94
- usage.cacheWrite += u.cacheWrite ?? 0;
95
- usage.cost += u.cost?.total ?? 0;
96
- }
97
- } catch {
98
- // skip
99
- }
100
- }
101
- return usage;
102
- }
103
-
104
- export function extractModel(log: RunLogEntry[]): string | undefined {
105
- for (const entry of log) {
106
- if (entry.type !== "message_end") continue;
107
- try {
108
- const event = JSON.parse(entry.raw);
109
- if (event.message?.model) return event.message.model as string;
110
- } catch {
111
- // skip
112
- }
113
- }
114
- return undefined;
115
- }
116
-
117
- export function buildDockerArgs(image: string, params: DockerE2EInput): string[] {
118
- const args = ["run", "--rm", "-e", `GITHUB_TOKEN=${params.token}`];
119
-
120
- if (params.model) {
121
- args.push("-e", `PI_MODEL=${params.model}`);
122
- }
123
-
124
- args.push(image, params.task);
125
- return args;
126
- }
127
-
128
- export const dockerE2ETool = {
129
- name: "docker_e2e",
130
- label: "Docker E2E",
131
- description:
132
- "Run a task end-to-end inside an isolated Docker container with pi and this project's extensions. Returns the full run log for evaluation.",
133
- promptSnippet: "Run an isolated e2e task in Docker. Requires a GitHub token and task prompt.",
134
- parameters: DockerE2EParams,
135
-
136
- async execute(
137
- _toolCallId: string,
138
- params: DockerE2EInput,
139
- signal: AbortSignal | undefined,
140
- onUpdate: AgentToolUpdateCallback<DockerE2EDetails> | undefined,
141
- _ctx: ExtensionContext,
142
- ): Promise<AgentToolResult<DockerE2EDetails>> {
143
- const startTime = Date.now();
144
- const timeoutMs = (params.timeout ?? 300) * 1000;
145
- const log: RunLogEntry[] = [];
146
- let lastAssistantText = "";
147
-
148
- const dockerArgs = buildDockerArgs(DOCKER_IMAGE, params);
149
-
150
- const exitCode = await new Promise<number | null>((resolve) => {
151
- const proc = spawn("docker", dockerArgs, {
152
- stdio: ["ignore", "pipe", "pipe"],
153
- });
154
-
155
- let buffer = "";
156
- let timedOut = false;
157
- let timeoutHandle: NodeJS.Timeout | undefined;
158
-
159
- if (timeoutMs > 0) {
160
- timeoutHandle = setTimeout(() => {
161
- timedOut = true;
162
- proc.kill("SIGTERM");
163
- setTimeout(() => {
164
- if (!proc.killed) proc.kill("SIGKILL");
165
- }, 5000);
166
- }, timeoutMs);
167
- }
168
-
169
- const processLine = (line: string) => {
170
- const entry = parseJsonlLine(line);
171
- if (!entry) return;
172
- log.push(entry);
173
-
174
- if (entry.type === "message_end") {
175
- const text = extractAssistantText(log);
176
- if (text) lastAssistantText = text;
177
- }
178
-
179
- onUpdate?.({
180
- content: [
181
- {
182
- type: "text",
183
- text: lastAssistantText || `(running… ${log.length} events)`,
184
- },
185
- ],
186
- details: {
187
- exitCode: null,
188
- log,
189
- duration: Date.now() - startTime,
190
- model: extractModel(log),
191
- usage: extractUsage(log),
192
- },
193
- });
194
- };
195
-
196
- proc.stdout.on("data", (data: Buffer) => {
197
- buffer += data.toString();
198
- const lines = buffer.split("\n");
199
- buffer = lines.pop() ?? "";
200
- for (const line of lines) processLine(line);
201
- });
202
-
203
- proc.stderr.on("data", (data: Buffer) => {
204
- const text = data.toString().trim();
205
- if (text) {
206
- log.push({ type: "stderr", timestamp: Date.now(), raw: text });
207
- }
208
- });
209
-
210
- proc.on("close", (code) => {
211
- if (timeoutHandle) clearTimeout(timeoutHandle);
212
- if (buffer.trim()) processLine(buffer);
213
- if (timedOut) {
214
- log.push({
215
- type: "timeout",
216
- timestamp: Date.now(),
217
- raw: `Timed out after ${params.timeout ?? 300}s`,
218
- });
219
- }
220
- resolve(timedOut ? null : (code ?? 1));
221
- });
222
-
223
- proc.on("error", (err) => {
224
- if (timeoutHandle) clearTimeout(timeoutHandle);
225
- log.push({
226
- type: "error",
227
- timestamp: Date.now(),
228
- raw: err.message,
229
- });
230
- resolve(null);
231
- });
232
-
233
- if (signal) {
234
- const onAbort = () => {
235
- proc.kill("SIGTERM");
236
- setTimeout(() => {
237
- if (!proc.killed) proc.kill("SIGKILL");
238
- }, 5000);
239
- };
240
- if (signal.aborted) onAbort();
241
- else signal.addEventListener("abort", onAbort, { once: true });
242
- }
243
- });
244
-
245
- const duration = Date.now() - startTime;
246
- const assistantText = extractAssistantText(log);
247
- const usage = extractUsage(log);
248
- const model = extractModel(log);
249
- const fullLog = log.map((e) => e.raw).join("\n");
250
-
251
- const summary = [
252
- `## Docker E2E Run Complete`,
253
- ``,
254
- `**Exit code:** ${exitCode ?? "killed"}`,
255
- `**Duration:** ${(duration / 1000).toFixed(1)}s`,
256
- model ? `**Model:** ${model}` : null,
257
- usage.turns > 0
258
- ? `**Usage:** ${usage.turns} turns, ↑${usage.input} ↓${usage.output} tokens, $${usage.cost.toFixed(4)}`
259
- : null,
260
- ``,
261
- `### Output`,
262
- assistantText || "(no assistant output)",
263
- ``,
264
- `### Full Log (${log.length} events)`,
265
- "```",
266
- fullLog,
267
- "```",
268
- ]
269
- .filter((l) => l !== null)
270
- .join("\n");
271
-
272
- return {
273
- content: [{ type: "text", text: summary }],
274
- details: { exitCode, log, duration, model, usage },
275
- };
276
- },
277
- };
package/src/index.test.ts DELETED
@@ -1,8 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
-
3
- describe("mcpi-ext", () => {
4
- it("exports a default function", async () => {
5
- const mod = await import("./index.js");
6
- expect(typeof mod.default).toBe("function");
7
- });
8
- });
package/src/index.ts DELETED
@@ -1,179 +0,0 @@
1
- import type {
2
- BeforeAgentStartEvent,
3
- ExtensionAPI,
4
- ExtensionContext,
5
- SessionStartEvent,
6
- ToolCallEvent,
7
- } from "@sammorrowdrums/mcpi";
8
- import { CodeModeManager } from "./code-mode/index.js";
9
- import { dockerE2ETool } from "./docker-e2e.js";
10
- import { McpClientManager, loadMcpConfig } from "./mcp/index.js";
11
- import {
12
- SkillRegistry,
13
- createLoadSkillTool,
14
- discoverSkillsFromServer,
15
- formatMcpSkillsForPrompt,
16
- registerMcpToolProxies,
17
- } from "./skills/index.js";
18
- import { ToolCliServer, formatToolCliForPrompt } from "./tool-cli/index.js";
19
- import type { ToolProvider } from "./tool-cli/index.js";
20
-
21
- export default function (pi: ExtensionAPI) {
22
- pi.registerTool(dockerE2ETool);
23
-
24
- pi.registerFlag("mcp-config", {
25
- description: "Path to MCP server configuration JSON file",
26
- type: "string",
27
- });
28
-
29
- const mcpManager = new McpClientManager();
30
- const skillRegistry = new SkillRegistry();
31
- const codeModeManager = new CodeModeManager();
32
- const enabledTools = new Set<string>();
33
- const gatedToolNames = new Set<string>();
34
-
35
- // Bridge McpClientManager to the ToolProvider interface
36
- const toolProvider: ToolProvider = {
37
- getServerNames: () => mcpManager.getConnectedServers(),
38
- getTools: (server) => mcpManager.getToolsForServer(server),
39
- async callTool(server, tool, args) {
40
- const client = mcpManager.getClient(server);
41
- if (!client) throw new Error(`No client for server "${server}"`);
42
- const result = await client.callTool({ name: tool, arguments: args });
43
- return {
44
- content: result.content as unknown[],
45
- isError: result.isError === true ? true : undefined,
46
- structuredContent: result.structuredContent as Record<string, unknown> | undefined,
47
- };
48
- },
49
- };
50
- const rpcServer = new ToolCliServer(toolProvider);
51
-
52
- // Register the load_skill tool so the model can activate MCP skills
53
- pi.registerTool(createLoadSkillTool({ registry: skillRegistry, mcpManager, enabledTools }));
54
-
55
- pi.on("session_start", async (_event: SessionStartEvent, ctx: ExtensionContext) => {
56
- if (ctx.hasUI) {
57
- ctx.ui.notify("mcpi-ext loaded", "info");
58
- }
59
-
60
- const configPath = pi.getFlag("mcp-config") as string | undefined;
61
- const log = (msg: string) => {
62
- if (ctx.hasUI) {
63
- ctx.ui.notify(msg, "info");
64
- } else {
65
- console.error(msg);
66
- }
67
- };
68
- try {
69
- const config = await loadMcpConfig(configPath);
70
- const serverCount = Object.keys(config.mcpServers).length;
71
- if (serverCount > 0) {
72
- await mcpManager.connectAll(config, log);
73
- const tools = mcpManager.getTools();
74
- log(
75
- `MCP: ${mcpManager.getConnectedServers().length} server(s), ${tools.length} tool(s) discovered`,
76
- );
77
-
78
- // Pre-register all MCP tools as deferred Pi tool proxies
79
- // (in tools array for dispatch but excluded from system prompt)
80
- const allToolNames = tools.map((t) => t.name);
81
- registerMcpToolProxies(allToolNames, mcpManager, pi);
82
-
83
- // Discover skills from all connected servers
84
- for (const serverName of mcpManager.getConnectedServers()) {
85
- const client = mcpManager.getClient(serverName);
86
- if (!client) continue;
87
- try {
88
- const skills = await discoverSkillsFromServer(client, serverName, log);
89
- skillRegistry.registerAll(skills);
90
- } catch (err) {
91
- log(
92
- `[skills] Failed to discover skills from "${serverName}": ${(err as Error).message}`,
93
- );
94
- }
95
- }
96
-
97
- if (skillRegistry.size > 0) {
98
- for (const skill of skillRegistry.getAll()) {
99
- for (const t of skill.allowedTools) {
100
- gatedToolNames.add(t);
101
- }
102
- }
103
- log(
104
- `MCP: ${skillRegistry.size} skill(s) discovered, ${gatedToolNames.size} tool(s) deferred`,
105
- );
106
- }
107
-
108
- // Start the tool-cli RPC server for progressive tool discovery
109
- try {
110
- const { port, token } = await rpcServer.start(log);
111
- pi.setEnv("TOOL_CLI_PORT", String(port));
112
- pi.setEnv("TOOL_CLI_TOKEN", token);
113
- } catch (err) {
114
- log(`[tool-cli] Failed to start RPC server: ${(err as Error).message}`);
115
- }
116
-
117
- // Initialize code mode (Tier 3) for read-only tools with structured output
118
- codeModeManager.initialize(mcpManager);
119
- if (codeModeManager.isActive) {
120
- const { codeSearch, codeExecute } = codeModeManager.createTools();
121
- pi.registerTool(codeSearch);
122
- pi.registerTool(codeExecute);
123
- log(
124
- `MCP: Code mode active (${codeModeManager.getEligibleTools().length} eligible tool(s))`,
125
- );
126
- }
127
- }
128
- } catch (err) {
129
- const msg = `MCP config error: ${(err as Error).message}`;
130
- if (ctx.hasUI) {
131
- ctx.ui.notify(msg, "warning");
132
- } else {
133
- console.error(msg);
134
- }
135
- }
136
- });
137
-
138
- // Inject MCP skills, tool-cli advice, and code mode type hints into the system prompt
139
- pi.on("before_agent_start", async (event: BeforeAgentStartEvent) => {
140
- let extra = "";
141
-
142
- const skills = skillRegistry.getAll();
143
- if (skills.length > 0) {
144
- extra += formatMcpSkillsForPrompt(skills);
145
- }
146
-
147
- const serverCount = mcpManager.getConnectedServers().length;
148
- extra += formatToolCliForPrompt(serverCount);
149
-
150
- if (codeModeManager.isActive) {
151
- extra += codeModeManager.formatSystemPromptSection();
152
- }
153
-
154
- if (extra.length === 0) return;
155
- return { systemPrompt: event.systemPrompt + extra };
156
- });
157
-
158
- // Block deferred MCP tools until their skill is loaded
159
- pi.on("tool_call", async (event: ToolCallEvent) => {
160
- const name = "toolName" in event ? event.toolName : undefined;
161
- if (!name || !gatedToolNames.has(name) || enabledTools.has(name)) return;
162
- const relevantSkills = skillRegistry
163
- .getAll()
164
- .filter((s) => s.allowedTools.includes(name))
165
- .map((s) => s.name);
166
- return {
167
- block: true,
168
- reason: `Tool "${name}" requires loading a skill first. Call load_skill with one of: ${relevantSkills.join(", ")}`,
169
- };
170
- });
171
-
172
- pi.on("session_shutdown", async () => {
173
- pi.unsetEnv("TOOL_CLI_PORT");
174
- pi.unsetEnv("TOOL_CLI_TOKEN");
175
- await rpcServer.stop();
176
- await mcpManager.disconnectAll();
177
- skillRegistry.clear();
178
- });
179
- }