@sammorrowdrums/mcpi-ext 0.2.1 → 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 (199) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -47
  3. package/dist/code-mode/eligibility.d.ts +29 -4
  4. package/dist/code-mode/eligibility.js +56 -6
  5. package/dist/code-mode/executor.d.ts +19 -1
  6. package/dist/code-mode/executor.js +73 -8
  7. package/dist/code-mode/index.d.ts +54 -12
  8. package/dist/code-mode/index.js +157 -65
  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 +4 -1
  12. package/dist/code-mode/tools.js +7 -4
  13. package/dist/code-mode/type-hints.d.ts +2 -2
  14. package/dist/code-mode/type-hints.js +24 -8
  15. package/dist/docker-e2e.d.ts +0 -1
  16. package/dist/docker-e2e.js +0 -1
  17. package/dist/index.d.ts +9 -1
  18. package/dist/index.js +146 -67
  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 +61 -19
  24. package/dist/mcp/client-manager.js +223 -64
  25. package/dist/mcp/config-loader.d.ts +0 -1
  26. package/dist/mcp/config-loader.js +0 -1
  27. package/dist/mcp/config.d.ts +11 -1
  28. package/dist/mcp/config.js +15 -1
  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 +4 -1
  36. package/dist/mcp/index.js +4 -1
  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 +5 -3
  50. package/dist/skills/discover.js +37 -12
  51. package/dist/skills/format.d.ts +4 -1
  52. package/dist/skills/format.js +8 -4
  53. package/dist/skills/index.d.ts +2 -2
  54. package/dist/skills/index.js +1 -1
  55. package/dist/skills/load-skill-tool.d.ts +23 -7
  56. package/dist/skills/load-skill-tool.js +112 -33
  57. package/dist/skills/mcp-tool-proxy.d.ts +4 -8
  58. package/dist/skills/mcp-tool-proxy.js +22 -97
  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 +48 -2
  74. package/dist/skills/skill-registry.js +40 -3
  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 +17 -5
  78. package/dist/tool-cli/format.js +36 -20
  79. package/dist/tool-cli/index.d.ts +5 -2
  80. package/dist/tool-cli/index.js +3 -1
  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 -30
  86. package/dist/code-mode/code-mode-integration.test.d.ts +0 -2
  87. package/dist/code-mode/code-mode-integration.test.d.ts.map +0 -1
  88. package/dist/code-mode/code-mode-integration.test.js +0 -116
  89. package/dist/code-mode/code-mode-integration.test.js.map +0 -1
  90. package/dist/code-mode/eligibility.d.ts.map +0 -1
  91. package/dist/code-mode/eligibility.js.map +0 -1
  92. package/dist/code-mode/eligibility.test.d.ts +0 -2
  93. package/dist/code-mode/eligibility.test.d.ts.map +0 -1
  94. package/dist/code-mode/eligibility.test.js +0 -53
  95. package/dist/code-mode/eligibility.test.js.map +0 -1
  96. package/dist/code-mode/executor.d.ts.map +0 -1
  97. package/dist/code-mode/executor.js.map +0 -1
  98. package/dist/code-mode/executor.test.d.ts +0 -2
  99. package/dist/code-mode/executor.test.d.ts.map +0 -1
  100. package/dist/code-mode/executor.test.js +0 -130
  101. package/dist/code-mode/executor.test.js.map +0 -1
  102. package/dist/code-mode/index.d.ts.map +0 -1
  103. package/dist/code-mode/index.js.map +0 -1
  104. package/dist/code-mode/tools.d.ts.map +0 -1
  105. package/dist/code-mode/tools.js.map +0 -1
  106. package/dist/code-mode/type-hints.d.ts.map +0 -1
  107. package/dist/code-mode/type-hints.js.map +0 -1
  108. package/dist/code-mode/type-hints.test.d.ts +0 -2
  109. package/dist/code-mode/type-hints.test.d.ts.map +0 -1
  110. package/dist/code-mode/type-hints.test.js +0 -157
  111. package/dist/code-mode/type-hints.test.js.map +0 -1
  112. package/dist/docker-e2e.d.ts.map +0 -1
  113. package/dist/docker-e2e.js.map +0 -1
  114. package/dist/docker-e2e.test.d.ts +0 -2
  115. package/dist/docker-e2e.test.d.ts.map +0 -1
  116. package/dist/docker-e2e.test.js +0 -176
  117. package/dist/docker-e2e.test.js.map +0 -1
  118. package/dist/index.d.ts.map +0 -1
  119. package/dist/index.js.map +0 -1
  120. package/dist/index.test.d.ts +0 -2
  121. package/dist/index.test.d.ts.map +0 -1
  122. package/dist/index.test.js +0 -8
  123. package/dist/index.test.js.map +0 -1
  124. package/dist/mcp/client-manager.d.ts.map +0 -1
  125. package/dist/mcp/client-manager.js.map +0 -1
  126. package/dist/mcp/client-manager.test.d.ts +0 -2
  127. package/dist/mcp/client-manager.test.d.ts.map +0 -1
  128. package/dist/mcp/client-manager.test.js +0 -204
  129. package/dist/mcp/client-manager.test.js.map +0 -1
  130. package/dist/mcp/config-loader.d.ts.map +0 -1
  131. package/dist/mcp/config-loader.js.map +0 -1
  132. package/dist/mcp/config-loader.test.d.ts +0 -2
  133. package/dist/mcp/config-loader.test.d.ts.map +0 -1
  134. package/dist/mcp/config-loader.test.js +0 -93
  135. package/dist/mcp/config-loader.test.js.map +0 -1
  136. package/dist/mcp/config.d.ts.map +0 -1
  137. package/dist/mcp/config.js.map +0 -1
  138. package/dist/mcp/index.d.ts.map +0 -1
  139. package/dist/mcp/index.js.map +0 -1
  140. package/dist/skills/discover.d.ts.map +0 -1
  141. package/dist/skills/discover.js.map +0 -1
  142. package/dist/skills/format.d.ts.map +0 -1
  143. package/dist/skills/format.js.map +0 -1
  144. package/dist/skills/format.test.d.ts +0 -2
  145. package/dist/skills/format.test.d.ts.map +0 -1
  146. package/dist/skills/format.test.js +0 -63
  147. package/dist/skills/format.test.js.map +0 -1
  148. package/dist/skills/index.d.ts.map +0 -1
  149. package/dist/skills/index.js.map +0 -1
  150. package/dist/skills/load-skill-tool.d.ts.map +0 -1
  151. package/dist/skills/load-skill-tool.js.map +0 -1
  152. package/dist/skills/mcp-tool-proxy.d.ts.map +0 -1
  153. package/dist/skills/mcp-tool-proxy.js.map +0 -1
  154. package/dist/skills/skill-integration.test.d.ts +0 -2
  155. package/dist/skills/skill-integration.test.d.ts.map +0 -1
  156. package/dist/skills/skill-integration.test.js +0 -119
  157. package/dist/skills/skill-integration.test.js.map +0 -1
  158. package/dist/skills/skill-registry.d.ts.map +0 -1
  159. package/dist/skills/skill-registry.js.map +0 -1
  160. package/dist/skills/skill-registry.test.d.ts +0 -2
  161. package/dist/skills/skill-registry.test.d.ts.map +0 -1
  162. package/dist/skills/skill-registry.test.js +0 -67
  163. package/dist/skills/skill-registry.test.js.map +0 -1
  164. package/dist/test-servers/weather-server.d.ts +0 -12
  165. package/dist/test-servers/weather-server.d.ts.map +0 -1
  166. package/dist/test-servers/weather-server.js +0 -139
  167. package/dist/test-servers/weather-server.js.map +0 -1
  168. package/dist/test-servers/weather-stdio.d.ts +0 -2
  169. package/dist/test-servers/weather-stdio.d.ts.map +0 -1
  170. package/dist/test-servers/weather-stdio.js +0 -11
  171. package/dist/test-servers/weather-stdio.js.map +0 -1
  172. package/dist/tool-cli/cli.d.ts +0 -3
  173. package/dist/tool-cli/cli.d.ts.map +0 -1
  174. package/dist/tool-cli/cli.js +0 -179
  175. package/dist/tool-cli/cli.js.map +0 -1
  176. package/dist/tool-cli/constants.d.ts +0 -7
  177. package/dist/tool-cli/constants.d.ts.map +0 -1
  178. package/dist/tool-cli/constants.js +0 -15
  179. package/dist/tool-cli/constants.js.map +0 -1
  180. package/dist/tool-cli/format.d.ts.map +0 -1
  181. package/dist/tool-cli/format.js.map +0 -1
  182. package/dist/tool-cli/format.test.d.ts +0 -2
  183. package/dist/tool-cli/format.test.d.ts.map +0 -1
  184. package/dist/tool-cli/format.test.js +0 -30
  185. package/dist/tool-cli/format.test.js.map +0 -1
  186. package/dist/tool-cli/index.d.ts.map +0 -1
  187. package/dist/tool-cli/index.js.map +0 -1
  188. package/dist/tool-cli/rpc-client.d.ts +0 -6
  189. package/dist/tool-cli/rpc-client.d.ts.map +0 -1
  190. package/dist/tool-cli/rpc-client.js +0 -25
  191. package/dist/tool-cli/rpc-client.js.map +0 -1
  192. package/dist/tool-cli/rpc-server.d.ts +0 -48
  193. package/dist/tool-cli/rpc-server.d.ts.map +0 -1
  194. package/dist/tool-cli/rpc-server.js +0 -220
  195. package/dist/tool-cli/rpc-server.js.map +0 -1
  196. package/dist/tool-cli/rpc-server.test.d.ts +0 -2
  197. package/dist/tool-cli/rpc-server.test.d.ts.map +0 -1
  198. package/dist/tool-cli/rpc-server.test.js +0 -154
  199. package/dist/tool-cli/rpc-server.test.js.map +0 -1
@@ -1,28 +1,48 @@
1
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
1
+ import { Client, type ReadResourceResult, type Resource, type ResourceTemplateType, type Tool, type Transport } from "@modelcontextprotocol/client";
2
+ import { type DirectoryReadResult, type SkillsGetResult, type SkillsListResult } from "../skills/sep2640/protocol.js";
3
+ import { type TerminalCallToolResult } from "./call-tool-result.js";
4
+ import { type CreateMcpClientOptions, type McpClientDiagnostics, type McpElicitationHandler } from "./client-factory.js";
2
5
  import type { McpConfig, ServerConfig } from "./config.js";
3
- /** An MCP tool as discovered from a server. */
4
- export interface McpTool {
5
- /** Tool name as reported by the server. */
6
- name: string;
7
- description?: string;
8
- /** JSON Schema for the tool's parameters. */
9
- inputSchema: Record<string, unknown>;
10
- /** JSON Schema for structured output, if declared. */
11
- outputSchema?: Record<string, unknown>;
12
- /** Tool annotations (readOnlyHint, destructiveHint, etc.). */
13
- annotations?: Record<string, unknown>;
6
+ /** A lossless MCP tool as discovered from a server, tagged with its origin. */
7
+ export type McpTool = Tool & {
14
8
  /** Which configured server this tool came from. */
15
9
  serverName: string;
10
+ };
11
+ export interface McpClientManagerOptions {
12
+ elicitation?: McpElicitationHandler;
13
+ clientFactory?: (options: CreateMcpClientOptions) => Client;
14
+ transportFactory?: (config: ServerConfig) => Transport;
15
+ /**
16
+ * Advertise the draft skills extension (SEP-2640) at initialize.
17
+ *
18
+ * Off by default; the host turns it on from config. When off, no connection
19
+ * this manager opens mentions the extension at all.
20
+ */
21
+ skillsExtension?: boolean;
16
22
  }
17
23
  /**
18
24
  * Manages connections to multiple MCP servers and aggregates their tools.
19
25
  *
20
- * Tools are discovered and stored internally. They are NOT registered with pi
21
- * automatically — that responsibility belongs to the access tiers (Skills,
22
- * Football, Code Mode).
26
+ * Tools are discovered and stored internally. They are NOT registered with mcpi
27
+ * automatically — that responsibility belongs to the access tiers.
23
28
  */
24
29
  export declare class McpClientManager {
25
- private connections;
30
+ private readonly connections;
31
+ private readonly elicitation;
32
+ private readonly clientFactory;
33
+ private readonly transportFactory;
34
+ private skillsExtensionRequested;
35
+ constructor(options?: McpClientManagerOptions);
36
+ /** Whether this manager advertises the draft skills extension. */
37
+ requestsSkillsExtension(): boolean;
38
+ /**
39
+ * Turn the draft skills extension on or off for future connections.
40
+ *
41
+ * The host reads the gate from config, which loads after this manager is
42
+ * constructed. Already-open connections keep whatever they negotiated at
43
+ * initialize, because the capability set is fixed for a session.
44
+ */
45
+ enableSkillsExtension(enabled: boolean): void;
26
46
  /**
27
47
  * Connect to all servers defined in the config.
28
48
  * Connections that fail are logged and skipped — partial success is fine.
@@ -30,6 +50,26 @@ export declare class McpClientManager {
30
50
  connectAll(config: McpConfig, log?: (msg: string) => void): Promise<void>;
31
51
  /** Connect to a single server by name. */
32
52
  connectOne(name: string, serverConfig: ServerConfig, log?: (msg: string) => void): Promise<void>;
53
+ /** Call a tool and return only its terminal protocol result. */
54
+ callTool(serverName: string, toolName: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<TerminalCallToolResult>;
55
+ /** List a server's resources. Discovery only; authorization lives in McpPolicy. */
56
+ listResources(serverName: string, signal?: AbortSignal): Promise<Resource[]>;
57
+ /** List a server's resource templates. Authorization lives in McpPolicy. */
58
+ listResourceTemplates(serverName: string, signal?: AbortSignal): Promise<ResourceTemplateType[]>;
59
+ /** Read a single resource. Authorization is the caller-side policy's responsibility. */
60
+ readResource(serverName: string, uri: string, signal?: AbortSignal): Promise<ReadResourceResult>;
61
+ /**
62
+ * The settings a server declared for one extension during `initialize`, or
63
+ * `undefined` if it declared no such extension.
64
+ */
65
+ getExtensionCapability(serverName: string, extensionName: string): Record<string, unknown> | undefined;
66
+ /** Draft SEP-2640 `skills/list`. */
67
+ requestSkillsList(serverName: string, cursor?: string, signal?: AbortSignal): Promise<SkillsListResult>;
68
+ /** Draft SEP-2640 `skills/get`. */
69
+ requestSkillsGet(serverName: string, uri: string, signal?: AbortSignal): Promise<SkillsGetResult>;
70
+ /** Draft SEP-2640 `resources/directory/read`. */
71
+ requestDirectoryRead(serverName: string, uri: string, cursor?: string, signal?: AbortSignal): Promise<DirectoryReadResult>;
72
+ private requestExtension;
33
73
  /** Disconnect a single server. */
34
74
  disconnectOne(name: string): Promise<void>;
35
75
  /** Disconnect all servers. */
@@ -38,9 +78,11 @@ export declare class McpClientManager {
38
78
  getTools(): McpTool[];
39
79
  /** Get tools from a specific server. */
40
80
  getToolsForServer(name: string): McpTool[];
41
- /** Get the MCP Client for a specific server (needed to call tools). */
81
+ /** Get the MCP Client for discovery APIs that are not tool calls. */
42
82
  getClient(name: string): Client | undefined;
43
- /** List connected server names. */
83
+ getDiagnostics(name: string): McpClientDiagnostics | undefined;
84
+ /** List connected server names in deterministic order. */
44
85
  getConnectedServers(): string[];
45
86
  }
46
- //# sourceMappingURL=client-manager.d.ts.map
87
+ /** Build the SDK's safe child environment without leaking this or a parent bridge endpoint. */
88
+ export declare function buildStdioEnvironment(configured: Readonly<Record<string, string>> | undefined): Record<string, string>;
@@ -1,148 +1,307 @@
1
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
1
+ import { StreamableHTTPClientTransport, } from "@modelcontextprotocol/client";
2
+ import { getDefaultEnvironment, StdioClientTransport } from "@modelcontextprotocol/client/stdio";
3
+ import { DirectoryReadResultSchema, SkillsGetResultSchema, SkillsListResultSchema, } from "../skills/sep2640/protocol.js";
4
+ import { describeNegotiation, SKILLS_METHODS } from "../skills/sep2640/spec.js";
5
+ import { adaptTerminalCallToolResult } from "./call-tool-result.js";
6
+ import { createMcpClient, getMcpClientDiagnostics, MCP_CLIENT_POLICY, } from "./client-factory.js";
7
+ import { McpHostElicitationError } from "./host-elicitation.js";
8
+ const unavailableElicitation = {
9
+ elicit: () => Promise.reject(new McpHostElicitationError("The MCP server requested user input, but no host elicitation integration is configured. The request was not approved.")),
10
+ };
4
11
  /**
5
12
  * Manages connections to multiple MCP servers and aggregates their tools.
6
13
  *
7
- * Tools are discovered and stored internally. They are NOT registered with pi
8
- * automatically — that responsibility belongs to the access tiers (Skills,
9
- * Football, Code Mode).
14
+ * Tools are discovered and stored internally. They are NOT registered with mcpi
15
+ * automatically — that responsibility belongs to the access tiers.
10
16
  */
11
17
  export class McpClientManager {
12
18
  connections = new Map();
19
+ elicitation;
20
+ clientFactory;
21
+ transportFactory;
22
+ skillsExtensionRequested;
23
+ constructor(options = {}) {
24
+ this.elicitation = options.elicitation ?? unavailableElicitation;
25
+ this.clientFactory = options.clientFactory ?? createMcpClient;
26
+ this.transportFactory = options.transportFactory ?? createTransport;
27
+ this.skillsExtensionRequested = options.skillsExtension === true;
28
+ }
29
+ /** Whether this manager advertises the draft skills extension. */
30
+ requestsSkillsExtension() {
31
+ return this.skillsExtensionRequested;
32
+ }
33
+ /**
34
+ * Turn the draft skills extension on or off for future connections.
35
+ *
36
+ * The host reads the gate from config, which loads after this manager is
37
+ * constructed. Already-open connections keep whatever they negotiated at
38
+ * initialize, because the capability set is fixed for a session.
39
+ */
40
+ enableSkillsExtension(enabled) {
41
+ this.skillsExtensionRequested = enabled;
42
+ }
13
43
  /**
14
44
  * Connect to all servers defined in the config.
15
45
  * Connections that fail are logged and skipped — partial success is fine.
16
46
  */
17
47
  async connectAll(config, log = console.error) {
18
- const entries = Object.entries(config.mcpServers);
48
+ const entries = Object.entries(config.mcpServers).sort(([left], [right]) => compareStrings(left, right));
19
49
  const results = await Promise.allSettled(entries.map(([name, serverConfig]) => this.connectOne(name, serverConfig, log)));
20
- for (let i = 0; i < results.length; i++) {
21
- const result = results[i];
50
+ for (let index = 0; index < results.length; index++) {
51
+ const result = results[index];
22
52
  if (result.status === "rejected") {
23
- log(`[mcp] Failed to connect to "${entries[i][0]}": ${result.reason}`);
53
+ log(`[mcp] Failed to connect to "${entries[index][0]}": ${formatError(result.reason)}`);
24
54
  }
25
55
  }
26
56
  }
27
57
  /** Connect to a single server by name. */
28
58
  async connectOne(name, serverConfig, log = console.error) {
29
- // Disconnect existing connection with this name if any
30
59
  if (this.connections.has(name)) {
31
60
  await this.disconnectOne(name);
32
61
  }
33
- const transport = createTransport(serverConfig);
34
- const client = new Client({ name: "mcpi-ext", version: "0.1.0" }, {
35
- capabilities: {},
62
+ const transport = this.transportFactory(serverConfig);
63
+ const client = this.clientFactory({
64
+ elicitation: this.elicitation,
65
+ skillsExtension: this.skillsExtensionRequested,
36
66
  listChanged: {
37
67
  tools: {
38
- onChanged: (_error, tools) => {
39
- if (tools) {
40
- const conn = this.connections.get(name);
41
- if (conn && conn.client === client) {
42
- conn.tools = toMcpTools(name, tools);
43
- log(`[mcp] Tools updated for "${name}" (${conn.tools.length} tools)`);
44
- }
68
+ onChanged: (error, tools) => {
69
+ if (error) {
70
+ log(`[mcp] Failed to refresh tools for "${name}": ${formatError(error)}`);
71
+ return;
72
+ }
73
+ if (tools === null)
74
+ return;
75
+ const connection = this.connections.get(name);
76
+ if (connection?.client === client) {
77
+ connection.tools = toMcpTools(name, tools);
78
+ log(`[mcp] Tools updated for "${name}" (${connection.tools.length} tools)`);
45
79
  }
46
80
  },
47
81
  },
48
82
  },
49
83
  });
50
- // Insert placeholder so listChanged notifications during connect aren't dropped
51
84
  this.connections.set(name, { client, transport, tools: [] });
52
85
  try {
53
- await client.connect(transport);
54
- const toolsResult = await client.listTools();
86
+ await client.connect(transport, {
87
+ timeout: MCP_CLIENT_POLICY.requestTimeoutMs,
88
+ maxTotalTimeout: MCP_CLIENT_POLICY.maxTotalTimeoutMs,
89
+ });
90
+ const toolsResult = await client.listTools(undefined, {
91
+ timeout: MCP_CLIENT_POLICY.requestTimeoutMs,
92
+ maxTotalTimeout: MCP_CLIENT_POLICY.maxTotalTimeoutMs,
93
+ });
55
94
  const tools = toMcpTools(name, toolsResult.tools);
56
- const conn = this.connections.get(name);
57
- if (conn && conn.client === client) {
58
- conn.tools = tools;
95
+ const diagnostics = getMcpClientDiagnostics(client, {
96
+ skillsExtensionRequested: this.skillsExtensionRequested,
97
+ });
98
+ const connection = this.connections.get(name);
99
+ if (connection?.client === client) {
100
+ connection.tools = tools;
101
+ connection.diagnostics = diagnostics;
102
+ }
103
+ log(`[mcp] Connected to "${name}" (${tools.length} tools, protocol era: ${diagnostics.protocolEra ?? "unknown"})`);
104
+ if (this.skillsExtensionRequested) {
105
+ log(`[mcp] "${name}": ${describeNegotiation(name, diagnostics.skillsExtension.serverCapability)}`);
59
106
  }
60
- log(`[mcp] Connected to "${name}" (${tools.length} tools)`);
61
107
  }
62
108
  catch (error) {
63
- // Clean up on failure so we don't leak a partially connected client
64
- const conn = this.connections.get(name);
65
- if (conn && conn.client === client) {
109
+ const connection = this.connections.get(name);
110
+ if (connection?.client === client) {
66
111
  this.connections.delete(name);
67
112
  }
68
113
  try {
69
114
  await client.close();
70
115
  }
71
116
  catch {
72
- // best-effort cleanup
117
+ // Preserve the original connection failure.
73
118
  }
74
119
  throw error;
75
120
  }
76
121
  }
122
+ /** Call a tool and return only its terminal protocol result. */
123
+ async callTool(serverName, toolName, args, signal) {
124
+ const connection = this.connections.get(serverName);
125
+ if (!connection) {
126
+ throw new Error(`MCP server "${serverName}" is not connected`);
127
+ }
128
+ const result = await connection.client.callTool({ name: toolName, arguments: args }, {
129
+ timeout: MCP_CLIENT_POLICY.requestTimeoutMs,
130
+ maxTotalTimeout: MCP_CLIENT_POLICY.maxTotalTimeoutMs,
131
+ ...(signal !== undefined ? { signal } : {}),
132
+ });
133
+ return adaptTerminalCallToolResult(result);
134
+ }
135
+ /** List a server's resources. Discovery only; authorization lives in McpPolicy. */
136
+ async listResources(serverName, signal) {
137
+ const connection = this.connections.get(serverName);
138
+ if (!connection) {
139
+ throw new Error(`MCP server "${serverName}" is not connected`);
140
+ }
141
+ const result = await connection.client.listResources(undefined, {
142
+ timeout: MCP_CLIENT_POLICY.requestTimeoutMs,
143
+ maxTotalTimeout: MCP_CLIENT_POLICY.maxTotalTimeoutMs,
144
+ ...(signal !== undefined ? { signal } : {}),
145
+ });
146
+ return [...result.resources].sort(compareResources);
147
+ }
148
+ /** List a server's resource templates. Authorization lives in McpPolicy. */
149
+ async listResourceTemplates(serverName, signal) {
150
+ const connection = this.connections.get(serverName);
151
+ if (!connection) {
152
+ throw new Error(`MCP server "${serverName}" is not connected`);
153
+ }
154
+ const result = await connection.client.listResourceTemplates(undefined, {
155
+ timeout: MCP_CLIENT_POLICY.requestTimeoutMs,
156
+ maxTotalTimeout: MCP_CLIENT_POLICY.maxTotalTimeoutMs,
157
+ ...(signal !== undefined ? { signal } : {}),
158
+ });
159
+ return [...result.resourceTemplates].sort(compareResourceTemplates);
160
+ }
161
+ /** Read a single resource. Authorization is the caller-side policy's responsibility. */
162
+ async readResource(serverName, uri, signal) {
163
+ const connection = this.connections.get(serverName);
164
+ if (!connection) {
165
+ throw new Error(`MCP server "${serverName}" is not connected`);
166
+ }
167
+ return connection.client.readResource({ uri }, {
168
+ timeout: MCP_CLIENT_POLICY.requestTimeoutMs,
169
+ maxTotalTimeout: MCP_CLIENT_POLICY.maxTotalTimeoutMs,
170
+ ...(signal !== undefined ? { signal } : {}),
171
+ });
172
+ }
173
+ // ---------------------------------------------------------------------------
174
+ // Draft SEP-2640 skills extension transport
175
+ //
176
+ // These are transport methods, not authorization. `McpPolicy` decides whether
177
+ // a call may happen; this class only knows how to put it on the wire and how
178
+ // to refuse to hand back a payload that does not match the draft schema.
179
+ // ---------------------------------------------------------------------------
180
+ /**
181
+ * The settings a server declared for one extension during `initialize`, or
182
+ * `undefined` if it declared no such extension.
183
+ */
184
+ getExtensionCapability(serverName, extensionName) {
185
+ const capability = this.connections.get(serverName)?.client.getServerCapabilities()
186
+ ?.extensions?.[extensionName];
187
+ if (capability === undefined) {
188
+ return undefined;
189
+ }
190
+ if (capability === null || typeof capability !== "object" || Array.isArray(capability)) {
191
+ // An extension declared with a non-object body is still "declared"; treat
192
+ // it as declared-with-no-settings rather than inventing settings for it.
193
+ return {};
194
+ }
195
+ return capability;
196
+ }
197
+ /** Draft SEP-2640 `skills/list`. */
198
+ async requestSkillsList(serverName, cursor, signal) {
199
+ return this.requestExtension(serverName, SKILLS_METHODS.list, cursor !== undefined ? { cursor } : {}, SkillsListResultSchema, signal);
200
+ }
201
+ /** Draft SEP-2640 `skills/get`. */
202
+ async requestSkillsGet(serverName, uri, signal) {
203
+ return this.requestExtension(serverName, SKILLS_METHODS.get, { uri }, SkillsGetResultSchema, signal);
204
+ }
205
+ /** Draft SEP-2640 `resources/directory/read`. */
206
+ async requestDirectoryRead(serverName, uri, cursor, signal) {
207
+ return this.requestExtension(serverName, SKILLS_METHODS.directoryRead, cursor !== undefined ? { uri, cursor } : { uri }, DirectoryReadResultSchema, signal);
208
+ }
209
+ async requestExtension(serverName, method, params, schema, signal) {
210
+ const connection = this.connections.get(serverName);
211
+ if (!connection) {
212
+ throw new Error(`MCP server "${serverName}" is not connected`);
213
+ }
214
+ return connection.client.request({ method, params }, schema, {
215
+ timeout: MCP_CLIENT_POLICY.requestTimeoutMs,
216
+ maxTotalTimeout: MCP_CLIENT_POLICY.maxTotalTimeoutMs,
217
+ ...(signal !== undefined ? { signal } : {}),
218
+ });
219
+ }
77
220
  /** Disconnect a single server. */
78
221
  async disconnectOne(name) {
79
- const conn = this.connections.get(name);
80
- if (!conn)
222
+ const connection = this.connections.get(name);
223
+ if (!connection)
81
224
  return;
82
225
  this.connections.delete(name);
83
226
  try {
84
- await conn.client.close();
227
+ await connection.client.close();
85
228
  }
86
229
  catch {
87
- // best-effort
230
+ // The connection is already removed; shutdown is best-effort.
88
231
  }
89
232
  }
90
233
  /** Disconnect all servers. */
91
234
  async disconnectAll() {
92
- const names = [...this.connections.keys()];
93
- await Promise.allSettled(names.map((n) => this.disconnectOne(n)));
235
+ const names = this.getConnectedServers();
236
+ await Promise.allSettled(names.map((name) => this.disconnectOne(name)));
94
237
  }
95
238
  /** Get all discovered tools across all connected servers. */
96
239
  getTools() {
97
- const tools = [];
98
- for (const conn of this.connections.values()) {
99
- tools.push(...conn.tools);
100
- }
101
- return tools;
240
+ return [...this.connections.values()]
241
+ .flatMap((connection) => connection.tools)
242
+ .sort(compareTools);
102
243
  }
103
244
  /** Get tools from a specific server. */
104
245
  getToolsForServer(name) {
105
- return this.connections.get(name)?.tools ?? [];
246
+ return [...(this.connections.get(name)?.tools ?? [])].sort(compareTools);
106
247
  }
107
- /** Get the MCP Client for a specific server (needed to call tools). */
248
+ /** Get the MCP Client for discovery APIs that are not tool calls. */
108
249
  getClient(name) {
109
250
  return this.connections.get(name)?.client;
110
251
  }
111
- /** List connected server names. */
252
+ getDiagnostics(name) {
253
+ return this.connections.get(name)?.diagnostics;
254
+ }
255
+ /** List connected server names in deterministic order. */
112
256
  getConnectedServers() {
113
- return [...this.connections.keys()];
257
+ return [...this.connections.keys()].sort(compareStrings);
114
258
  }
115
259
  }
116
260
  function createTransport(config) {
117
261
  if (config.type === "stdio") {
118
- const env = config.env
119
- ? Object.fromEntries(Object.entries({ ...process.env, ...config.env }).filter((entry) => entry[1] !== undefined))
120
- : undefined;
121
262
  return new StdioClientTransport({
122
263
  command: config.command,
123
264
  args: config.args,
124
- env,
265
+ env: buildStdioEnvironment(config.env),
125
266
  cwd: config.cwd,
126
267
  });
127
268
  }
128
- const headers = {
129
- ...(config.headers ?? {}),
130
- };
131
269
  return new StreamableHTTPClientTransport(new URL(config.url), {
132
270
  requestInit: {
133
- headers,
271
+ headers: config.headers ?? {},
134
272
  },
135
273
  });
136
274
  }
137
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
275
+ /** Build the SDK's safe child environment without leaking this or a parent bridge endpoint. */
276
+ export function buildStdioEnvironment(configured) {
277
+ const environment = { ...getDefaultEnvironment(), ...configured };
278
+ for (const name of Object.keys(environment)) {
279
+ if (name.startsWith("TOOL_CLI_")) {
280
+ Reflect.deleteProperty(environment, name);
281
+ }
282
+ }
283
+ return environment;
284
+ }
138
285
  function toMcpTools(serverName, tools) {
139
- return tools.map((t) => ({
140
- name: t.name,
141
- description: t.description,
142
- inputSchema: t.inputSchema,
143
- outputSchema: t.outputSchema,
144
- annotations: t.annotations,
286
+ return tools
287
+ .map((tool) => ({
288
+ ...tool,
145
289
  serverName,
146
- }));
290
+ }))
291
+ .sort(compareTools);
292
+ }
293
+ function compareTools(left, right) {
294
+ return compareStrings(left.serverName, right.serverName) || compareStrings(left.name, right.name);
295
+ }
296
+ function compareResources(left, right) {
297
+ return compareStrings(left.uri, right.uri) || compareStrings(left.name, right.name);
298
+ }
299
+ function compareResourceTemplates(left, right) {
300
+ return (compareStrings(left.uriTemplate, right.uriTemplate) || compareStrings(left.name, right.name));
301
+ }
302
+ function compareStrings(left, right) {
303
+ return left < right ? -1 : left > right ? 1 : 0;
304
+ }
305
+ function formatError(error) {
306
+ return error instanceof Error ? error.message : String(error);
147
307
  }
148
- //# sourceMappingURL=client-manager.js.map
@@ -5,4 +5,3 @@ import { McpConfig } from "./config.js";
5
5
  * Throws on invalid JSON or schema violations.
6
6
  */
7
7
  export declare function loadMcpConfig(configPath?: string): Promise<McpConfig>;
8
- //# sourceMappingURL=config-loader.d.ts.map
@@ -40,4 +40,3 @@ export async function loadMcpConfig(configPath) {
40
40
  }
41
41
  return parsed;
42
42
  }
43
- //# sourceMappingURL=config-loader.js.map
@@ -38,9 +38,19 @@ export declare const McpConfig: Type.TObject<{
38
38
  url: Type.TString;
39
39
  headers: Type.TOptional<Type.TRecord<"^.*$", Type.TString>>;
40
40
  }>]>>;
41
+ experimental: Type.TOptional<Type.TObject<{
42
+ skillsExtension: Type.TOptional<Type.TBoolean>;
43
+ }>>;
41
44
  }>;
42
45
  export type StdioServerConfig = Static<typeof StdioServerConfig>;
43
46
  export type RemoteServerConfig = Static<typeof RemoteServerConfig>;
44
47
  export type ServerConfig = Static<typeof ServerConfig>;
45
48
  export type McpConfig = Static<typeof McpConfig>;
46
- //# sourceMappingURL=config.d.ts.map
49
+ /**
50
+ * Whether the draft skills extension is enabled.
51
+ *
52
+ * Defaults to **off**. SEP-2640 is a draft: nothing here should start speaking
53
+ * it because a config file happened to omit a key, and a host that quietly
54
+ * enabled a proposal would make it indistinguishable from ratified support.
55
+ */
56
+ export declare function isSkillsExtensionEnabled(config: Pick<McpConfig, "experimental">): boolean;
@@ -24,5 +24,19 @@ export const McpConfig = Type.Object({
24
24
  mcpServers: Type.Record(Type.String(), ServerConfig, {
25
25
  description: "Named MCP server configurations",
26
26
  }),
27
+ experimental: Type.Optional(Type.Object({
28
+ skillsExtension: Type.Optional(Type.Boolean({
29
+ description: "Opt in to the DRAFT MCP skills extension (SEP-2640). Unratified and subject to change; off by default.",
30
+ })),
31
+ }, { description: "Opt-in support for unratified MCP proposals" })),
27
32
  });
28
- //# sourceMappingURL=config.js.map
33
+ /**
34
+ * Whether the draft skills extension is enabled.
35
+ *
36
+ * Defaults to **off**. SEP-2640 is a draft: nothing here should start speaking
37
+ * it because a config file happened to omit a key, and a host that quietly
38
+ * enabled a proposal would make it indistinguishable from ratified support.
39
+ */
40
+ export function isSkillsExtensionEnabled(config) {
41
+ return config.experimental?.skillsExtension === true;
42
+ }
@@ -0,0 +1,14 @@
1
+ import type { McpPolicyGateway } from "./policy.js";
2
+ /**
3
+ * Gateway behaviour for a server that does not speak the draft SEP-2640 skills
4
+ * extension.
5
+ *
6
+ * `getExtensionCapability` returns `undefined` — the honest answer for a server
7
+ * that declared nothing — and the three request methods reject. They reject
8
+ * rather than return empty results because reaching them at all would mean the
9
+ * policy dispatched an extension request to a server that never declared the
10
+ * extension, and a silent empty result would hide that bug.
11
+ *
12
+ * Spread this into any gateway that only implements the classic MCP surface.
13
+ */
14
+ export declare const noSkillsExtensionGateway: Pick<McpPolicyGateway, "getExtensionCapability" | "requestSkillsList" | "requestSkillsGet" | "requestDirectoryRead">;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Gateway behaviour for a server that does not speak the draft SEP-2640 skills
3
+ * extension.
4
+ *
5
+ * `getExtensionCapability` returns `undefined` — the honest answer for a server
6
+ * that declared nothing — and the three request methods reject. They reject
7
+ * rather than return empty results because reaching them at all would mean the
8
+ * policy dispatched an extension request to a server that never declared the
9
+ * extension, and a silent empty result would hide that bug.
10
+ *
11
+ * Spread this into any gateway that only implements the classic MCP surface.
12
+ */
13
+ export const noSkillsExtensionGateway = {
14
+ getExtensionCapability: () => undefined,
15
+ requestSkillsList: () => Promise.reject(new Error(unsupported("skills/list"))),
16
+ requestSkillsGet: () => Promise.reject(new Error(unsupported("skills/get"))),
17
+ requestDirectoryRead: () => Promise.reject(new Error(unsupported("resources/directory/read"))),
18
+ };
19
+ function unsupported(method) {
20
+ return `This MCP server does not declare the skills extension, so ${method} is unavailable.`;
21
+ }
@@ -0,0 +1,16 @@
1
+ import type { ExtensionContext } from "@sammorrowdrums/mcpi";
2
+ import type { McpApprovalPrompt, McpApprovalRequest } from "./policy.js";
3
+ type ApprovalContext = Pick<ExtensionContext, "hasUI" | "ui">;
4
+ /**
5
+ * Bridges policy approval requests to mcpi's explicit user-facing confirmation.
6
+ *
7
+ * Mirrors {@link McpiHostElicitation}: without an interactive UI the request is
8
+ * reported as undecided rather than approved, so nothing is ever silently
9
+ * enabled.
10
+ */
11
+ export declare class McpiHostApproval implements McpApprovalPrompt {
12
+ private context;
13
+ setContext(context: ApprovalContext | undefined): void;
14
+ confirm(request: McpApprovalRequest): Promise<boolean | undefined>;
15
+ }
16
+ export {};
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Bridges policy approval requests to mcpi's explicit user-facing confirmation.
3
+ *
4
+ * Mirrors {@link McpiHostElicitation}: without an interactive UI the request is
5
+ * reported as undecided rather than approved, so nothing is ever silently
6
+ * enabled.
7
+ */
8
+ export class McpiHostApproval {
9
+ context;
10
+ setContext(context) {
11
+ this.context = context;
12
+ }
13
+ async confirm(request) {
14
+ const context = this.context;
15
+ if (!context?.hasUI)
16
+ return undefined;
17
+ return context.ui.confirm(request.title, request.message, request.signal ? { signal: request.signal } : undefined);
18
+ }
19
+ }
@@ -0,0 +1,19 @@
1
+ import type { ElicitRequestParams, ElicitResult } from "@modelcontextprotocol/client";
2
+ import type { ExtensionContext } from "@sammorrowdrums/mcpi";
3
+ import type { McpElicitationHandler } from "./client-factory.js";
4
+ type ElicitationContext = Pick<ExtensionContext, "hasUI" | "ui">;
5
+ export declare class McpHostElicitationError extends Error {
6
+ constructor(message: string);
7
+ }
8
+ /**
9
+ * Bridges MCP form elicitation to mcpi's explicit user-facing UI.
10
+ *
11
+ * URL elicitation is intentionally unsupported and is not advertised by the
12
+ * client. Form requests never receive an implicit approval.
13
+ */
14
+ export declare class McpiHostElicitation implements McpElicitationHandler {
15
+ private context;
16
+ setContext(context: ElicitationContext | undefined): void;
17
+ elicit(params: ElicitRequestParams): Promise<ElicitResult>;
18
+ }
19
+ export {};