@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
@@ -0,0 +1,234 @@
1
+ import { spawn } from "node:child_process";
2
+ import { Type } from "typebox";
3
+ const DOCKER_IMAGE = "mcpi-ext-e2e";
4
+ const DockerE2EParams = Type.Object({
5
+ task: Type.String({ description: "The task/prompt to run inside the sandbox" }),
6
+ token: Type.String({
7
+ description: "GitHub token for Copilot model access",
8
+ }),
9
+ model: Type.Optional(Type.String({
10
+ description: "Model to use (e.g. claude-sonnet-4-5). Defaults to the provider default.",
11
+ })),
12
+ timeout: Type.Optional(Type.Number({
13
+ description: "Timeout in seconds. Defaults to 300 (5 minutes).",
14
+ })),
15
+ });
16
+ export function parseJsonlLine(line) {
17
+ const trimmed = line.trim();
18
+ if (!trimmed)
19
+ return null;
20
+ try {
21
+ const parsed = JSON.parse(trimmed);
22
+ return { type: parsed.type ?? "unknown", timestamp: Date.now(), raw: trimmed };
23
+ }
24
+ catch {
25
+ return { type: "raw", timestamp: Date.now(), raw: trimmed };
26
+ }
27
+ }
28
+ export function extractAssistantText(log) {
29
+ const parts = [];
30
+ for (const entry of log) {
31
+ if (entry.type !== "message_end")
32
+ continue;
33
+ try {
34
+ const event = JSON.parse(entry.raw);
35
+ const msg = event.message;
36
+ if (msg?.role !== "assistant")
37
+ continue;
38
+ for (const part of msg.content ?? []) {
39
+ if (part.type === "text")
40
+ parts.push(part.text);
41
+ }
42
+ }
43
+ catch {
44
+ // skip malformed
45
+ }
46
+ }
47
+ return parts.join("\n");
48
+ }
49
+ export function extractUsage(log) {
50
+ const usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
51
+ for (const entry of log) {
52
+ if (entry.type !== "message_end")
53
+ continue;
54
+ try {
55
+ const event = JSON.parse(entry.raw);
56
+ const msg = event.message;
57
+ if (msg?.role !== "assistant")
58
+ continue;
59
+ usage.turns++;
60
+ const u = msg.usage;
61
+ if (u) {
62
+ usage.input += u.input ?? 0;
63
+ usage.output += u.output ?? 0;
64
+ usage.cacheRead += u.cacheRead ?? 0;
65
+ usage.cacheWrite += u.cacheWrite ?? 0;
66
+ usage.cost += u.cost?.total ?? 0;
67
+ }
68
+ }
69
+ catch {
70
+ // skip
71
+ }
72
+ }
73
+ return usage;
74
+ }
75
+ export function extractModel(log) {
76
+ for (const entry of log) {
77
+ if (entry.type !== "message_end")
78
+ continue;
79
+ try {
80
+ const event = JSON.parse(entry.raw);
81
+ if (event.message?.model)
82
+ return event.message.model;
83
+ }
84
+ catch {
85
+ // skip
86
+ }
87
+ }
88
+ return undefined;
89
+ }
90
+ export function buildDockerArgs(image, params) {
91
+ const args = ["run", "--rm", "-e", `GITHUB_TOKEN=${params.token}`];
92
+ if (params.model) {
93
+ args.push("-e", `PI_MODEL=${params.model}`);
94
+ }
95
+ args.push(image, params.task);
96
+ return args;
97
+ }
98
+ export const dockerE2ETool = {
99
+ name: "docker_e2e",
100
+ label: "Docker E2E",
101
+ description: "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.",
102
+ promptSnippet: "Run an isolated e2e task in Docker. Requires a GitHub token and task prompt.",
103
+ parameters: DockerE2EParams,
104
+ async execute(_toolCallId, params, signal, onUpdate, _ctx) {
105
+ const startTime = Date.now();
106
+ const timeoutMs = (params.timeout ?? 300) * 1000;
107
+ const log = [];
108
+ let lastAssistantText = "";
109
+ const dockerArgs = buildDockerArgs(DOCKER_IMAGE, params);
110
+ const exitCode = await new Promise((resolve) => {
111
+ const proc = spawn("docker", dockerArgs, {
112
+ stdio: ["ignore", "pipe", "pipe"],
113
+ });
114
+ let buffer = "";
115
+ let timedOut = false;
116
+ let timeoutHandle;
117
+ if (timeoutMs > 0) {
118
+ timeoutHandle = setTimeout(() => {
119
+ timedOut = true;
120
+ proc.kill("SIGTERM");
121
+ setTimeout(() => {
122
+ if (!proc.killed)
123
+ proc.kill("SIGKILL");
124
+ }, 5000);
125
+ }, timeoutMs);
126
+ }
127
+ const processLine = (line) => {
128
+ const entry = parseJsonlLine(line);
129
+ if (!entry)
130
+ return;
131
+ log.push(entry);
132
+ if (entry.type === "message_end") {
133
+ const text = extractAssistantText(log);
134
+ if (text)
135
+ lastAssistantText = text;
136
+ }
137
+ onUpdate?.({
138
+ content: [
139
+ {
140
+ type: "text",
141
+ text: lastAssistantText || `(running… ${log.length} events)`,
142
+ },
143
+ ],
144
+ details: {
145
+ exitCode: null,
146
+ log,
147
+ duration: Date.now() - startTime,
148
+ model: extractModel(log),
149
+ usage: extractUsage(log),
150
+ },
151
+ });
152
+ };
153
+ proc.stdout.on("data", (data) => {
154
+ buffer += data.toString();
155
+ const lines = buffer.split("\n");
156
+ buffer = lines.pop() ?? "";
157
+ for (const line of lines)
158
+ processLine(line);
159
+ });
160
+ proc.stderr.on("data", (data) => {
161
+ const text = data.toString().trim();
162
+ if (text) {
163
+ log.push({ type: "stderr", timestamp: Date.now(), raw: text });
164
+ }
165
+ });
166
+ proc.on("close", (code) => {
167
+ if (timeoutHandle)
168
+ clearTimeout(timeoutHandle);
169
+ if (buffer.trim())
170
+ processLine(buffer);
171
+ if (timedOut) {
172
+ log.push({
173
+ type: "timeout",
174
+ timestamp: Date.now(),
175
+ raw: `Timed out after ${params.timeout ?? 300}s`,
176
+ });
177
+ }
178
+ resolve(timedOut ? null : (code ?? 1));
179
+ });
180
+ proc.on("error", (err) => {
181
+ if (timeoutHandle)
182
+ clearTimeout(timeoutHandle);
183
+ log.push({
184
+ type: "error",
185
+ timestamp: Date.now(),
186
+ raw: err.message,
187
+ });
188
+ resolve(null);
189
+ });
190
+ if (signal) {
191
+ const onAbort = () => {
192
+ proc.kill("SIGTERM");
193
+ setTimeout(() => {
194
+ if (!proc.killed)
195
+ proc.kill("SIGKILL");
196
+ }, 5000);
197
+ };
198
+ if (signal.aborted)
199
+ onAbort();
200
+ else
201
+ signal.addEventListener("abort", onAbort, { once: true });
202
+ }
203
+ });
204
+ const duration = Date.now() - startTime;
205
+ const assistantText = extractAssistantText(log);
206
+ const usage = extractUsage(log);
207
+ const model = extractModel(log);
208
+ const fullLog = log.map((e) => e.raw).join("\n");
209
+ const summary = [
210
+ `## Docker E2E Run Complete`,
211
+ ``,
212
+ `**Exit code:** ${exitCode ?? "killed"}`,
213
+ `**Duration:** ${(duration / 1000).toFixed(1)}s`,
214
+ model ? `**Model:** ${model}` : null,
215
+ usage.turns > 0
216
+ ? `**Usage:** ${usage.turns} turns, ↑${usage.input} ↓${usage.output} tokens, $${usage.cost.toFixed(4)}`
217
+ : null,
218
+ ``,
219
+ `### Output`,
220
+ assistantText || "(no assistant output)",
221
+ ``,
222
+ `### Full Log (${log.length} events)`,
223
+ "```",
224
+ fullLog,
225
+ "```",
226
+ ]
227
+ .filter((l) => l !== null)
228
+ .join("\n");
229
+ return {
230
+ content: [{ type: "text", text: summary }],
231
+ details: { exitCode, log, duration, model, usage },
232
+ };
233
+ },
234
+ };
@@ -0,0 +1,11 @@
1
+ import type { ExtensionAPI } from "@sammorrowdrums/mcpi";
2
+ import type { BashState } from "./routing/index.js";
3
+ /**
4
+ * Report whether the host currently exposes a shell tool to the agent.
5
+ *
6
+ * `getActiveTools` may be missing on an older host or throw if the runner context
7
+ * is not bound, and neither is evidence that bash is absent — so both collapse
8
+ * to `undiscoverable` rather than a false negative.
9
+ */
10
+ export declare function detectBashState(pi: ExtensionAPI): BashState;
11
+ export default function (pi: ExtensionAPI): void;
package/dist/index.js ADDED
@@ -0,0 +1,227 @@
1
+ import { CodeModeManager } from "./code-mode/index.js";
2
+ import { dockerE2ETool } from "./docker-e2e.js";
3
+ import { McpiHostApproval } from "./mcp/host-approval.js";
4
+ import { McpiHostElicitation } from "./mcp/host-elicitation.js";
5
+ import { McpClientManager, McpPolicy, isSkillsExtensionEnabled, loadMcpConfig, } from "./mcp/index.js";
6
+ import { SkillRegistry, SkillsExtensionClient, createLoadSkillTool, discoverSkillsFromServer, discoverSkillsViaExtension, formatMcpSkillsForPrompt, registerMcpToolProxies, skillsExtensionDiagnostic, } from "./skills/index.js";
7
+ import { ToolCliServer, createPolicyToolProvider, formatToolCliForPrompt, startToolCliBridge, withholdToolCliCredentials, } from "./tool-cli/index.js";
8
+ import { buildExecutionFacilities, formatExecutionFacilities, publishExecutionFacilities, } from "./routing/index.js";
9
+ function errorMessage(err) {
10
+ return err instanceof Error ? err.message : String(err);
11
+ }
12
+ /**
13
+ * Report whether the host currently exposes a shell tool to the agent.
14
+ *
15
+ * `getActiveTools` may be missing on an older host or throw if the runner context
16
+ * is not bound, and neither is evidence that bash is absent — so both collapse
17
+ * to `undiscoverable` rather than a false negative.
18
+ */
19
+ export function detectBashState(pi) {
20
+ try {
21
+ return pi.getActiveTools().includes("bash")
22
+ ? { kind: "registered", toolName: "bash" }
23
+ : { kind: "absent" };
24
+ }
25
+ catch (err) {
26
+ return { kind: "undiscoverable", reason: errorMessage(err) };
27
+ }
28
+ }
29
+ export default function (pi) {
30
+ pi.registerTool(dockerE2ETool);
31
+ pi.registerFlag("mcp-config", {
32
+ description: "Path to MCP server configuration JSON file",
33
+ type: "string",
34
+ });
35
+ pi.registerFlag("mcp-skills-extension", {
36
+ description: "Enable the DRAFT MCP skills extension (SEP-2640). Unratified; off by default.",
37
+ type: "boolean",
38
+ });
39
+ const hostElicitation = new McpiHostElicitation();
40
+ const hostApproval = new McpiHostApproval();
41
+ const mcpManager = new McpClientManager({ elicitation: hostElicitation });
42
+ const policy = new McpPolicy({ gateway: mcpManager, approvals: hostApproval });
43
+ const skillRegistry = new SkillRegistry();
44
+ const skillsClient = new SkillsExtensionClient({ policy });
45
+ const codeModeManager = new CodeModeManager();
46
+ const { codeSearch, codeExecute } = codeModeManager.createTools();
47
+ // Bridge the shared policy boundary to the ToolProvider interface. tool-cli
48
+ // sees exactly the policy-visible discovered schema set, and every call it
49
+ // makes is re-authorized by the same dispatcher, so it cannot reach a hidden
50
+ // tool by naming it directly.
51
+ const toolProvider = createPolicyToolProvider(policy, { upstream: mcpManager });
52
+ const rpcServer = new ToolCliServer(toolProvider);
53
+ // Routing state the prompt reports on. tool-cli is only ever advertised after
54
+ // a compatible authenticated v1 handshake and confirmed bash availability.
55
+ let toolCliState = {
56
+ kind: "not_started",
57
+ reason: "the session has not finished starting",
58
+ };
59
+ let skillsExtensionEnabled = false;
60
+ let facilitiesPublished = false;
61
+ // Register the load_skill tool so the model can activate MCP skills
62
+ pi.registerTool(createLoadSkillTool({ registry: skillRegistry, policy, skillsClient }));
63
+ pi.registerTool(codeSearch);
64
+ pi.registerTool(codeExecute);
65
+ pi.on("session_start", async (_event, ctx) => {
66
+ hostElicitation.setContext(ctx);
67
+ hostApproval.setContext(ctx);
68
+ withholdToolCliCredentials(pi);
69
+ if (ctx.hasUI) {
70
+ ctx.ui.notify("mcpi-ext loaded", "info");
71
+ }
72
+ const configPath = pi.getFlag("mcp-config");
73
+ const log = (msg) => {
74
+ if (ctx.hasUI) {
75
+ ctx.ui.notify(msg, "info");
76
+ }
77
+ else {
78
+ console.error(msg);
79
+ }
80
+ };
81
+ try {
82
+ const config = await loadMcpConfig(configPath);
83
+ const serverCount = Object.keys(config.mcpServers).length;
84
+ // Draft extension support is opt-in from either the config file or the
85
+ // CLI flag, and must be decided before any connection is opened because
86
+ // capabilities are fixed at initialize.
87
+ const skillsExtensionEnabledNow = pi.getFlag("mcp-skills-extension") === true || isSkillsExtensionEnabled(config);
88
+ skillsExtensionEnabled = skillsExtensionEnabledNow;
89
+ mcpManager.enableSkillsExtension(skillsExtensionEnabledNow);
90
+ if (skillsExtensionEnabledNow) {
91
+ log(skillsExtensionDiagnostic());
92
+ }
93
+ if (serverCount > 0) {
94
+ await mcpManager.connectAll(config, log);
95
+ const tools = mcpManager.getTools();
96
+ log(`MCP: ${mcpManager.getConnectedServers().length} server(s), ${tools.length} tool(s) discovered`);
97
+ // Pre-register all MCP tools as deferred Pi tool proxies
98
+ // (in tools array for dispatch but excluded from system prompt)
99
+ const allToolNames = tools.map((t) => t.name);
100
+ registerMcpToolProxies(allToolNames, mcpManager, policy, pi);
101
+ // Discover skills from all connected servers. A server that declares
102
+ // the draft extension is served entirely by it: the legacy skill://
103
+ // resource scan is a compatibility fallback for servers that do not,
104
+ // never a second opinion on one that does. An empty extension listing
105
+ // therefore means "no skills right now", not "try the old way".
106
+ for (const serverName of mcpManager.getConnectedServers()) {
107
+ const viaExtension = skillsExtensionEnabled && skillsClient.supports(serverName);
108
+ try {
109
+ if (viaExtension) {
110
+ const result = await discoverSkillsViaExtension(policy, skillsClient, serverName, log);
111
+ skillRegistry.registerAll(result.skills);
112
+ }
113
+ else {
114
+ const skills = await discoverSkillsFromServer(policy, serverName, log);
115
+ skillRegistry.registerAll(skills);
116
+ }
117
+ }
118
+ catch (err) {
119
+ log(`[skills] Failed to discover skills from "${serverName}" via ${viaExtension ? "the draft skills extension" : "skill:// resources"}: ${err.message}`);
120
+ }
121
+ }
122
+ for (const collision of skillRegistry.getCollisions()) {
123
+ log(`[skills] Name collision on "${collision.name}": "${collision.challenger.serverName}" exposed as "${collision.registeredAs}" (name held by "${collision.incumbent.serverName}")`);
124
+ }
125
+ // Hand the discovered skills to the policy so their tools are gated
126
+ // everywhere, not just on the mcpi dispatch path.
127
+ policy.registerSkills(skillRegistry.getAll());
128
+ if (skillRegistry.size > 0) {
129
+ log(`MCP: ${skillRegistry.size} skill(s) discovered, ${policy.getGatedToolNames().length} tool(s) deferred`);
130
+ }
131
+ toolCliState = await startToolCliBridge({
132
+ bash: detectBashState(pi),
133
+ server: rpcServer,
134
+ environment: pi,
135
+ log,
136
+ });
137
+ }
138
+ else {
139
+ toolCliState = {
140
+ kind: "not_started",
141
+ reason: "no MCP servers are configured, so there is nothing for it to expose",
142
+ };
143
+ }
144
+ // Code execution remains available even when no MCP servers or callable tools exist.
145
+ codeModeManager.initialize(mcpManager, policy, log);
146
+ // Probe the optional native sandbox backend once, so routing can state
147
+ // plainly whether code mode can run rather than assuming it can.
148
+ await codeModeManager.probeSandbox();
149
+ }
150
+ catch (err) {
151
+ withholdToolCliCredentials(pi);
152
+ await rpcServer.stop();
153
+ const msg = `MCP config error: ${errorMessage(err)}`;
154
+ toolCliState = { kind: "not_started", reason: msg };
155
+ if (ctx.hasUI) {
156
+ ctx.ui.notify(msg, "warning");
157
+ }
158
+ else {
159
+ console.error(msg);
160
+ }
161
+ }
162
+ });
163
+ // Inject execution routing, MCP skills, tool-cli usage docs, and code mode
164
+ // type hints into the system prompt.
165
+ pi.on("before_agent_start", async (event) => {
166
+ let extra = "";
167
+ const skills = skillRegistry.getAll();
168
+ const bashState = detectBashState(pi);
169
+ // Idempotent after the first resolution; covers hosts that reach
170
+ // before_agent_start without a preceding successful session_start.
171
+ const sandbox = await codeModeManager.probeSandbox();
172
+ // The routing section is emitted on every load, including with zero MCP
173
+ // servers — an agent still needs to know that exact computation and the
174
+ // shell are on the table, and why the MCP-backed facilities are not.
175
+ const facilities = buildExecutionFacilities({
176
+ skills: {
177
+ count: skills.length,
178
+ draftExtensionEnabled: skillsExtensionEnabled,
179
+ },
180
+ codeMode: { active: codeModeManager.isActive, reason: sandbox.reason },
181
+ toolCli: toolCliState,
182
+ bash: bashState,
183
+ });
184
+ // Prefer the host's own facility registry when it grows one. The two paths
185
+ // are mutually exclusive, so the section can never be emitted twice.
186
+ if (!facilitiesPublished) {
187
+ facilitiesPublished = publishExecutionFacilities(pi, facilities);
188
+ }
189
+ if (!facilitiesPublished) {
190
+ extra += formatExecutionFacilities(facilities);
191
+ }
192
+ if (skills.length > 0) {
193
+ extra += formatMcpSkillsForPrompt(skills);
194
+ }
195
+ extra += formatToolCliForPrompt({ toolCli: toolCliState, bash: bashState });
196
+ if (codeModeManager.isActive) {
197
+ codeModeManager.refresh();
198
+ extra += codeModeManager.formatSystemPromptSection();
199
+ }
200
+ if (extra.length === 0)
201
+ return;
202
+ return { systemPrompt: event.systemPrompt + extra };
203
+ });
204
+ // Block deferred MCP tools until their skill is loaded. The policy owns the
205
+ // decision so mcpi dispatch and the RPC path cannot disagree.
206
+ pi.on("tool_call", async (event) => {
207
+ const name = "toolName" in event ? event.toolName : undefined;
208
+ if (!name || !policy.isGated(name))
209
+ return;
210
+ const relevantSkills = policy.getGatingSkills(name);
211
+ return {
212
+ block: true,
213
+ reason: `Tool "${name}" requires loading a skill first. Call load_skill with one of: ${relevantSkills.join(", ")}`,
214
+ };
215
+ });
216
+ pi.on("session_shutdown", async () => {
217
+ hostElicitation.setContext(undefined);
218
+ hostApproval.setContext(undefined);
219
+ withholdToolCliCredentials(pi);
220
+ toolCliState = { kind: "not_started", reason: "the session has shut down" };
221
+ facilitiesPublished = false;
222
+ await rpcServer.stop();
223
+ await mcpManager.disconnectAll();
224
+ skillRegistry.clear();
225
+ policy.reset();
226
+ });
227
+ }
@@ -0,0 +1,10 @@
1
+ import type { AgentToolResult } from "@sammorrowdrums/mcpi";
2
+ import type { CallToolResult } from "@modelcontextprotocol/client";
3
+ import type { CallToolResult as ToolCliCallToolResult } from "@sammorrowdrums/tool-cli";
4
+ export interface TerminalCallToolResult {
5
+ readonly kind: "terminal";
6
+ readonly result: CallToolResult;
7
+ }
8
+ export declare function adaptTerminalCallToolResult(result: CallToolResult): TerminalCallToolResult;
9
+ export declare function renderTerminalCallToolResult(terminal: TerminalCallToolResult): AgentToolResult<CallToolResult>;
10
+ export declare function toToolCliCallToolResult(terminal: TerminalCallToolResult): ToolCliCallToolResult;
@@ -0,0 +1,70 @@
1
+ export function adaptTerminalCallToolResult(result) {
2
+ return { kind: "terminal", result };
3
+ }
4
+ export function renderTerminalCallToolResult(terminal) {
5
+ const content = [];
6
+ if (terminal.result.isError) {
7
+ content.push({ type: "text", text: "MCP tool reported an error." });
8
+ }
9
+ for (const block of terminal.result.content) {
10
+ switch (block.type) {
11
+ case "text":
12
+ content.push({ type: "text", text: block.text });
13
+ break;
14
+ case "image":
15
+ content.push({ type: "image", data: block.data, mimeType: block.mimeType });
16
+ break;
17
+ case "audio":
18
+ content.push({
19
+ type: "text",
20
+ text: `[MCP audio: ${block.mimeType}; base64 payload preserved in result details]`,
21
+ });
22
+ break;
23
+ case "resource_link":
24
+ content.push({
25
+ type: "text",
26
+ text: formatResourceLink(block),
27
+ });
28
+ break;
29
+ case "resource":
30
+ content.push({
31
+ type: "text",
32
+ text: formatEmbeddedResource(block),
33
+ });
34
+ break;
35
+ default:
36
+ content.push({ type: "text", text: JSON.stringify(block, null, 2) });
37
+ }
38
+ }
39
+ if (terminal.result.structuredContent !== undefined) {
40
+ content.push({
41
+ type: "text",
42
+ text: `Structured content:\n${JSON.stringify(terminal.result.structuredContent, null, 2)}`,
43
+ });
44
+ }
45
+ if (content.length === 0) {
46
+ content.push({ type: "text", text: terminal.result.isError ? "MCP tool failed." : "" });
47
+ }
48
+ return {
49
+ content,
50
+ details: terminal.result,
51
+ };
52
+ }
53
+ export function toToolCliCallToolResult(terminal) {
54
+ return {
55
+ ...terminal.result,
56
+ content: [...terminal.result.content],
57
+ };
58
+ }
59
+ function formatResourceLink(block) {
60
+ const label = block.title ?? block.name ?? block.uri;
61
+ const details = [block.mimeType, block.description].filter(Boolean).join("; ");
62
+ return `[MCP resource link: ${label}] ${block.uri}${details ? ` (${details})` : ""}`;
63
+ }
64
+ function formatEmbeddedResource(block) {
65
+ const resource = block.resource;
66
+ if ("text" in resource) {
67
+ return `[MCP embedded resource: ${resource.uri}]\n${resource.text}`;
68
+ }
69
+ return `[MCP embedded resource: ${resource.uri}; ${resource.mimeType ?? "binary"} blob preserved in result details]`;
70
+ }
@@ -0,0 +1,59 @@
1
+ import { Client, type DiscoverResult, type ElicitRequestParams, type ElicitResult, type Implementation, type ListChangedHandlers, type ProtocolEra, type ServerCapabilities } from "@modelcontextprotocol/client";
2
+ export declare const MCP_CLIENT_IDENTITY: {
3
+ readonly name: "@sammorrowdrums/mcpi-ext";
4
+ readonly version: "1.0.0";
5
+ };
6
+ export declare const MCP_CLIENT_POLICY: {
7
+ readonly listMaxPages: 64;
8
+ readonly maxInputRequiredRounds: 4;
9
+ readonly probeTimeoutMs: 5000;
10
+ readonly requestTimeoutMs: 60000;
11
+ readonly maxTotalTimeoutMs: number;
12
+ };
13
+ export declare const MCP_SUPPORTED_PROTOCOL_VERSIONS: readonly ["2026-07-28", ...string[]];
14
+ export interface McpElicitationHandler {
15
+ elicit(params: ElicitRequestParams): Promise<ElicitResult>;
16
+ }
17
+ export interface McpClientDiagnostics {
18
+ protocolVersion: string | undefined;
19
+ protocolEra: ProtocolEra | undefined;
20
+ discoverResult: DiscoverResult | undefined;
21
+ serverImplementation: Implementation | undefined;
22
+ serverCapabilities: ServerCapabilities | undefined;
23
+ /**
24
+ * What this client requested and what the server declared for the draft
25
+ * skills extension, so a draft feature is never silently in play.
26
+ */
27
+ skillsExtension: {
28
+ /** Whether this client advertised the extension at initialize. */
29
+ requested: boolean;
30
+ /** Pinned draft revision this implementation targets. */
31
+ revision: string;
32
+ /** Always `"draft"` — this is an unratified proposal. */
33
+ status: string;
34
+ /** Whether the server declared the extension back. */
35
+ serverDeclared: boolean;
36
+ /** Settings the server declared, or `undefined` if it declared none. */
37
+ serverCapability: Record<string, unknown> | undefined;
38
+ };
39
+ }
40
+ export interface CreateMcpClientOptions {
41
+ elicitation: McpElicitationHandler;
42
+ listChanged?: ListChangedHandlers;
43
+ /**
44
+ * Advertise the draft skills extension at initialize.
45
+ *
46
+ * Off by default. When false this client never mentions SEP-2640 on the
47
+ * wire, which is what makes the fallback path a genuine absence of the
48
+ * extension rather than a suppressed one.
49
+ */
50
+ skillsExtension?: boolean;
51
+ }
52
+ /**
53
+ * Creates every MCP client with the same identity, negotiated protocol policy,
54
+ * capabilities, pagination limit, and SDK-managed input_required handling.
55
+ */
56
+ export declare function createMcpClient(options: CreateMcpClientOptions): Client;
57
+ export declare function getMcpClientDiagnostics(client: Client, options?: {
58
+ skillsExtensionRequested?: boolean;
59
+ }): McpClientDiagnostics;