@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
@@ -10,16 +10,48 @@ export interface McpSkillMetadata {
10
10
  serverName: string;
11
11
  /** Tool names this skill gates via allowed-tools frontmatter. */
12
12
  allowedTools: string[];
13
+ /**
14
+ * Which contract this skill was discovered over.
15
+ *
16
+ * `"sep2640"` skills were negotiated through the draft skills extension and
17
+ * carry verifiable digests; `"legacy"` skills came from plain `skill://`
18
+ * resource listing and have no integrity metadata at all. Loading code needs
19
+ * to tell them apart because only the former can be verified.
20
+ *
21
+ * Absent means legacy, so existing callers keep working unchanged.
22
+ */
23
+ origin?: "sep2640" | "legacy";
24
+ /**
25
+ * Digest of the skill's declared resource set, when known.
26
+ *
27
+ * SEP-2640 makes tool approval content-bound: if a later listing advertises a
28
+ * different resource set, prior approval is revoked. This value is what the
29
+ * policy hashes into the grant key to make that happen.
30
+ */
31
+ contentFingerprint?: string;
13
32
  }
14
33
  /**
15
34
  * Registry for MCP-discovered skills.
16
35
  *
17
36
  * Holds skill metadata discovered from MCP servers. Skills are registered
18
37
  * during MCP connection and looked up when the model invokes load_skill.
38
+ *
39
+ * Names live in a per-origin namespace. Re-registering the same skill (same
40
+ * server and URI) replaces it, but a *different* origin claiming an
41
+ * already-taken name never silently wins: the newcomer is kept under a
42
+ * server-qualified name so both stay reachable. SEP-2640 requires collisions to
43
+ * be disambiguated rather than dropped or preferred.
19
44
  */
20
45
  export declare class SkillRegistry {
21
46
  private skills;
22
- /** Register a skill. Overwrites any existing skill with the same name. */
47
+ private collisions;
48
+ /**
49
+ * Register a skill.
50
+ *
51
+ * Replaces an entry from the same origin. On a cross-origin name collision
52
+ * the incumbent keeps the bare name and this skill is stored under
53
+ * `<serverName>/<name>`; the collision is recorded for reporting.
54
+ */
23
55
  register(skill: McpSkillMetadata): void;
24
56
  /** Register multiple skills at once. */
25
57
  registerAll(skills: McpSkillMetadata[]): void;
@@ -27,6 +59,8 @@ export declare class SkillRegistry {
27
59
  get(name: string): McpSkillMetadata | undefined;
28
60
  /** Get all registered skills. */
29
61
  getAll(): McpSkillMetadata[];
62
+ /** Name collisions resolved by qualification, oldest first. */
63
+ getCollisions(): SkillNameCollision[];
30
64
  /** Remove a skill by name. */
31
65
  unregister(name: string): boolean;
32
66
  /** Remove all skills from a specific MCP server. */
@@ -36,4 +70,16 @@ export declare class SkillRegistry {
36
70
  /** Number of registered skills. */
37
71
  get size(): number;
38
72
  }
39
- //# sourceMappingURL=skill-registry.d.ts.map
73
+ /** A name claimed by two different origins, and how it was resolved. */
74
+ export interface SkillNameCollision {
75
+ readonly name: string;
76
+ readonly incumbent: {
77
+ readonly serverName: string;
78
+ readonly uri: string;
79
+ };
80
+ readonly challenger: {
81
+ readonly serverName: string;
82
+ readonly uri: string;
83
+ };
84
+ readonly registeredAs: string;
85
+ }
@@ -3,11 +3,36 @@
3
3
  *
4
4
  * Holds skill metadata discovered from MCP servers. Skills are registered
5
5
  * during MCP connection and looked up when the model invokes load_skill.
6
+ *
7
+ * Names live in a per-origin namespace. Re-registering the same skill (same
8
+ * server and URI) replaces it, but a *different* origin claiming an
9
+ * already-taken name never silently wins: the newcomer is kept under a
10
+ * server-qualified name so both stay reachable. SEP-2640 requires collisions to
11
+ * be disambiguated rather than dropped or preferred.
6
12
  */
7
13
  export class SkillRegistry {
8
14
  skills = new Map();
9
- /** Register a skill. Overwrites any existing skill with the same name. */
15
+ collisions = [];
16
+ /**
17
+ * Register a skill.
18
+ *
19
+ * Replaces an entry from the same origin. On a cross-origin name collision
20
+ * the incumbent keeps the bare name and this skill is stored under
21
+ * `<serverName>/<name>`; the collision is recorded for reporting.
22
+ */
10
23
  register(skill) {
24
+ const existing = this.skills.get(skill.name);
25
+ if (existing && !isSameOrigin(existing, skill)) {
26
+ const qualified = qualifiedName(skill);
27
+ this.collisions.push({
28
+ name: skill.name,
29
+ incumbent: { serverName: existing.serverName, uri: existing.uri },
30
+ challenger: { serverName: skill.serverName, uri: skill.uri },
31
+ registeredAs: qualified,
32
+ });
33
+ this.skills.set(qualified, { ...skill, name: qualified });
34
+ return;
35
+ }
11
36
  this.skills.set(skill.name, skill);
12
37
  }
13
38
  /** Register multiple skills at once. */
@@ -22,7 +47,11 @@ export class SkillRegistry {
22
47
  }
23
48
  /** Get all registered skills. */
24
49
  getAll() {
25
- return [...this.skills.values()];
50
+ return [...this.skills.values()].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
51
+ }
52
+ /** Name collisions resolved by qualification, oldest first. */
53
+ getCollisions() {
54
+ return [...this.collisions];
26
55
  }
27
56
  /** Remove a skill by name. */
28
57
  unregister(name) {
@@ -35,14 +64,22 @@ export class SkillRegistry {
35
64
  this.skills.delete(name);
36
65
  }
37
66
  }
67
+ this.collisions = this.collisions.filter((collision) => collision.incumbent.serverName !== serverName &&
68
+ collision.challenger.serverName !== serverName);
38
69
  }
39
70
  /** Remove all skills. */
40
71
  clear() {
41
72
  this.skills.clear();
73
+ this.collisions = [];
42
74
  }
43
75
  /** Number of registered skills. */
44
76
  get size() {
45
77
  return this.skills.size;
46
78
  }
47
79
  }
48
- //# sourceMappingURL=skill-registry.js.map
80
+ function isSameOrigin(left, right) {
81
+ return left.serverName === right.serverName && left.uri === right.uri;
82
+ }
83
+ function qualifiedName(skill) {
84
+ return `${skill.serverName}/${skill.name}`;
85
+ }
@@ -0,0 +1,14 @@
1
+ import { type BridgeInfo } from "@sammorrowdrums/tool-cli/client";
2
+ export declare const TOOL_CLI_HOST_ENV_VAR = "TOOL_CLI_HOST";
3
+ export declare const TOOL_CLI_LOCAL_HOST = "127.0.0.1";
4
+ export interface ToolCliBridgeEndpoint {
5
+ port: number;
6
+ token: string;
7
+ }
8
+ /**
9
+ * Authenticate to the newly started bridge and verify the complete v1 surface
10
+ * before exposing its endpoint to agent-spawned shell commands.
11
+ */
12
+ export declare function verifyToolCliBridge(endpoint: ToolCliBridgeEndpoint, timeoutMs?: number): Promise<BridgeInfo>;
13
+ export declare function formatToolCliBridgeError(error: unknown): string;
14
+ export declare function isToolCliCompatibilityError(error: unknown): boolean;
@@ -0,0 +1,196 @@
1
+ import { BRIDGE_PROTOCOL_MAJOR, BRIDGE_PROTOCOL_NAME, BRIDGE_RPC_OPERATIONS, BridgeCompatibilityError, PORT_ENV_VAR, RpcAbortError, RpcHttpError, RpcInvalidResponseError, RpcNonJsonResponseError, RpcProtocolError, RpcTimeoutError, RpcTransportError, TOKEN_ENV_VAR, getBridgeInfo, } from "@sammorrowdrums/tool-cli/client";
2
+ const HANDSHAKE_TIMEOUT_MS = 5_000;
3
+ export const TOOL_CLI_HOST_ENV_VAR = "TOOL_CLI_HOST";
4
+ export const TOOL_CLI_LOCAL_HOST = "127.0.0.1";
5
+ const REQUIRED_JSON_SCHEMA_DIALECTS = [
6
+ "https://json-schema.org/draft/2020-12/schema",
7
+ "https://json-schema.org/draft/2019-09/schema",
8
+ "http://json-schema.org/draft-07/schema#",
9
+ ];
10
+ let handshakeEnvironmentTail = Promise.resolve();
11
+ /**
12
+ * Authenticate to the newly started bridge and verify the complete v1 surface
13
+ * before exposing its endpoint to agent-spawned shell commands.
14
+ */
15
+ export async function verifyToolCliBridge(endpoint, timeoutMs = HANDSHAKE_TIMEOUT_MS) {
16
+ return withExclusiveClientEnvironment(endpoint, async () => {
17
+ const info = await getBridgeInfo({ timeoutMs });
18
+ assertRequiredV1Surface(info);
19
+ return info;
20
+ });
21
+ }
22
+ async function withExclusiveClientEnvironment(endpoint, operation) {
23
+ const previous = handshakeEnvironmentTail;
24
+ let release;
25
+ handshakeEnvironmentTail = new Promise((resolve) => {
26
+ release = resolve;
27
+ });
28
+ await previous;
29
+ let restore;
30
+ try {
31
+ restore = installTemporaryClientEnvironment(endpoint);
32
+ return await operation();
33
+ }
34
+ finally {
35
+ restore?.();
36
+ release();
37
+ }
38
+ }
39
+ export function formatToolCliBridgeError(error) {
40
+ if (error instanceof BridgeCompatibilityError) {
41
+ return `Tool-cli bridge compatibility error: ${error.message}. Install matching tool-cli and mcpi-ext major versions.`;
42
+ }
43
+ if (error instanceof RpcTimeoutError) {
44
+ return `Tool-cli bridge handshake timed out after ${error.timeoutMs}ms. Check bridge responsiveness and version compatibility.`;
45
+ }
46
+ if (error instanceof RpcHttpError) {
47
+ const auth = error.status === 401
48
+ ? " The bridge rejected the bearer token; restart the bridge to obtain fresh credentials."
49
+ : "";
50
+ return `Tool-cli bridge ${error.message}.${auth}`;
51
+ }
52
+ if (error instanceof RpcProtocolError) {
53
+ return `Tool-cli bridge RPC ${error.code}: ${error.message}${formatErrorData(error.data)}`;
54
+ }
55
+ if (error instanceof RpcNonJsonResponseError) {
56
+ return `Tool-cli bridge returned non-JSON HTTP ${error.status}: ${error.message}`;
57
+ }
58
+ if (error instanceof RpcInvalidResponseError) {
59
+ return `Tool-cli bridge returned an invalid JSON-RPC response: ${error.message}`;
60
+ }
61
+ if (error instanceof RpcAbortError) {
62
+ return `Tool-cli bridge handshake was cancelled${formatErrorData(error.reason)}`;
63
+ }
64
+ if (error instanceof RpcTransportError) {
65
+ return `Tool-cli bridge transport error: ${error.message}. Confirm the local bridge is listening and reachable.`;
66
+ }
67
+ return error instanceof Error ? error.message : String(error);
68
+ }
69
+ export function isToolCliCompatibilityError(error) {
70
+ return error instanceof BridgeCompatibilityError;
71
+ }
72
+ function assertRequiredV1Surface(info) {
73
+ const value = info;
74
+ const root = requireRecord(value, "bridge metadata", value);
75
+ const protocol = requireRecord(root.bridgeProtocol, "bridgeProtocol metadata", value);
76
+ if (protocol.name !== BRIDGE_PROTOCOL_NAME ||
77
+ protocol.major !== BRIDGE_PROTOCOL_MAJOR ||
78
+ typeof protocol.version !== "string" ||
79
+ protocol.version.length === 0 ||
80
+ Number(protocol.version.split(".", 1)[0]) !== BRIDGE_PROTOCOL_MAJOR) {
81
+ throw new BridgeCompatibilityError(`tool-cli bridge did not report a valid ${BRIDGE_PROTOCOL_NAME} v${BRIDGE_PROTOCOL_MAJOR} protocol version`, value);
82
+ }
83
+ const implementation = requireRecord(root.serverImplementation, "serverImplementation metadata", value);
84
+ if (typeof implementation.name !== "string" ||
85
+ implementation.name.length === 0 ||
86
+ typeof implementation.version !== "string" ||
87
+ implementation.version.length === 0) {
88
+ throw new BridgeCompatibilityError("tool-cli bridge did not report a valid implementation name and version", value);
89
+ }
90
+ if (!isStringArray(root.operations) || new Set(root.operations).size !== root.operations.length) {
91
+ throw new BridgeCompatibilityError("tool-cli bridge did not report a valid deterministic operation list", value);
92
+ }
93
+ const operations = root.operations;
94
+ const missingOperations = BRIDGE_RPC_OPERATIONS.filter((operation) => !operations.includes(operation));
95
+ if (missingOperations.length > 0) {
96
+ throw new BridgeCompatibilityError(`tool-cli bridge v1 is missing required operation(s): ${missingOperations.join(", ")}`, value);
97
+ }
98
+ const operationOrder = BRIDGE_RPC_OPERATIONS.map((operation) => operations.indexOf(operation));
99
+ if (operationOrder.some((index, position) => position > 0 && index < operationOrder[position - 1])) {
100
+ throw new BridgeCompatibilityError("tool-cli bridge v1 reported required operations in a non-deterministic order", value);
101
+ }
102
+ const capabilities = requireRecord(root.capabilities, "capabilities metadata", value);
103
+ const authentication = requireRecord(capabilities.authentication, "authentication capabilities", value);
104
+ const tools = requireRecord(capabilities.tools, "tool capabilities", value);
105
+ const resources = requireRecord(capabilities.resources, "resource capabilities", value);
106
+ const cancellation = requireRecord(capabilities.cancellation, "cancellation capabilities", value);
107
+ const supportedDialects = tools.supportedJsonSchemaDialects;
108
+ const missingCapabilities = [
109
+ authentication.required !== true || authentication.scheme !== "bearer"
110
+ ? "bearer authentication"
111
+ : undefined,
112
+ tools.discovery !== true ? "tool discovery" : undefined,
113
+ tools.calls !== true ? "tool calls" : undefined,
114
+ tools.inputSchemaValidation !== true ? "input schema validation" : undefined,
115
+ tools.jsonSchemaDialect !== REQUIRED_JSON_SCHEMA_DIALECTS[0]
116
+ ? "JSON Schema 2020-12 default dialect"
117
+ : undefined,
118
+ !isStringArray(supportedDialects) ||
119
+ REQUIRED_JSON_SCHEMA_DIALECTS.some((dialect) => !supportedDialects.includes(dialect))
120
+ ? "supported JSON Schema dialects"
121
+ : undefined,
122
+ resources.list !== true ? "resource listing" : undefined,
123
+ resources.templates !== true ? "resource template listing" : undefined,
124
+ resources.read !== true ? "resource reads" : undefined,
125
+ cancellation.providerAbortSignal !== true ? "provider cancellation" : undefined,
126
+ ].filter((capability) => capability !== undefined);
127
+ if (missingCapabilities.length > 0) {
128
+ throw new BridgeCompatibilityError(`tool-cli bridge v1 is missing required capability(s): ${missingCapabilities.join(", ")}`, value);
129
+ }
130
+ if (!isRecord(root.upstreamMcp)) {
131
+ throw new BridgeCompatibilityError("tool-cli bridge did not report the required upstream MCP diagnostics summary", value);
132
+ }
133
+ const upstream = root.upstreamMcp;
134
+ if (!isNonNegativeInteger(upstream.serverCount) ||
135
+ !Array.isArray(upstream.servers) ||
136
+ upstream.servers.length !== upstream.serverCount) {
137
+ throw new BridgeCompatibilityError("tool-cli bridge reported an invalid upstream MCP server summary", value);
138
+ }
139
+ let previousServerName;
140
+ for (const server of upstream.servers) {
141
+ const entry = requireRecord(server, "upstream MCP server diagnostics", value);
142
+ if (typeof entry.name !== "string" ||
143
+ entry.name.length === 0 ||
144
+ !isNonNegativeInteger(entry.toolCount) ||
145
+ (previousServerName !== undefined && entry.name <= previousServerName)) {
146
+ throw new BridgeCompatibilityError("tool-cli bridge reported invalid or non-deterministic upstream MCP server diagnostics", value);
147
+ }
148
+ previousServerName = entry.name;
149
+ }
150
+ }
151
+ function requireRecord(value, description, bridgeInfo) {
152
+ if (!isRecord(value)) {
153
+ throw new BridgeCompatibilityError(`tool-cli bridge did not report valid ${description}`, bridgeInfo);
154
+ }
155
+ return value;
156
+ }
157
+ function isRecord(value) {
158
+ return typeof value === "object" && value !== null && !Array.isArray(value);
159
+ }
160
+ function isStringArray(value) {
161
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
162
+ }
163
+ function isNonNegativeInteger(value) {
164
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
165
+ }
166
+ function installTemporaryClientEnvironment(endpoint) {
167
+ const previousPort = process.env[PORT_ENV_VAR];
168
+ const previousToken = process.env[TOKEN_ENV_VAR];
169
+ const previousHost = process.env[TOOL_CLI_HOST_ENV_VAR];
170
+ process.env[PORT_ENV_VAR] = String(endpoint.port);
171
+ process.env[TOKEN_ENV_VAR] = endpoint.token;
172
+ process.env[TOOL_CLI_HOST_ENV_VAR] = TOOL_CLI_LOCAL_HOST;
173
+ return () => {
174
+ restoreEnvironment(PORT_ENV_VAR, previousPort);
175
+ restoreEnvironment(TOKEN_ENV_VAR, previousToken);
176
+ restoreEnvironment(TOOL_CLI_HOST_ENV_VAR, previousHost);
177
+ };
178
+ }
179
+ function restoreEnvironment(name, previous) {
180
+ if (previous !== undefined) {
181
+ process.env[name] = previous;
182
+ }
183
+ else {
184
+ Reflect.deleteProperty(process.env, name);
185
+ }
186
+ }
187
+ function formatErrorData(data) {
188
+ if (data === undefined)
189
+ return "";
190
+ try {
191
+ return ` (${JSON.stringify(data)})`;
192
+ }
193
+ catch {
194
+ return ` (${String(data)})`;
195
+ }
196
+ }
@@ -1,8 +1,20 @@
1
+ import type { BashState, ToolCliState } from "../routing/facilities.js";
1
2
  /**
2
- * Format system prompt section advising the agent when and how to use tool-cli.
3
+ * Usage documentation for tool-cli.
3
4
  *
4
- * Only included when MCP servers are connected. Tells the agent about
5
- * progressive discovery via tool-cli as an alternative to skill-based access.
5
+ * This is the "how", not the "when" the `<execution_routing>` section decides
6
+ * which facility suits a task, and this section explains how to drive tool-cli
7
+ * once it has been chosen.
8
+ *
9
+ * Only emitted once the local RPC server has actually started. Advertising the
10
+ * commands before that would teach an agent an invocation it cannot perform.
11
+ * The routing section still reports tool-cli's availability either way, so
12
+ * nothing is silently omitted.
6
13
  */
7
- export declare function formatToolCliForPrompt(serverCount: number): string;
8
- //# sourceMappingURL=format.d.ts.map
14
+ export interface ToolCliPromptState {
15
+ /** Authenticated bridge state, including the verified v1 handshake metadata. */
16
+ toolCli: ToolCliState;
17
+ /** tool-cli is a shell program, so a registered bash tool is mandatory. */
18
+ bash: BashState;
19
+ }
20
+ export declare function formatToolCliForPrompt(state: ToolCliPromptState): string;
@@ -1,23 +1,18 @@
1
- /**
2
- * Format system prompt section advising the agent when and how to use tool-cli.
3
- *
4
- * Only included when MCP servers are connected. Tells the agent about
5
- * progressive discovery via tool-cli as an alternative to skill-based access.
6
- */
7
- export function formatToolCliForPrompt(serverCount) {
8
- if (serverCount === 0)
1
+ export function formatToolCliForPrompt(state) {
2
+ if (state.toolCli.kind !== "verified" || state.bash.kind !== "registered")
9
3
  return "";
4
+ const { bridgeInfo } = state.toolCli;
5
+ const upstreamServerCount = readUpstreamServerCount(bridgeInfo.upstreamMcp);
10
6
  return `
11
7
 
12
- <tool_cli>
13
- You have access to \`tool-cli\`, a CLI for discovering and calling MCP server tools progressively.
8
+ <tool_cli_usage_docs>
9
+ Use when you need to reach a specific MCP tool from the shell, or to discover which servers and
10
+ tools exist before committing to an approach.
14
11
 
15
- Use tool-cli when:
16
- - No skill covers the task you need to do
17
- - You want to explore what tools are available on a server
18
- - You need ad-hoc access to an MCP tool without loading a full skill
19
-
20
- If a skill exists for the task, prefer the skill — it provides workflow instructions and curated tool access.
12
+ \`tool-cli\` is a program, not a tool you can call. Invoke the bash tool with a command of the form
13
+ \`tool-cli ...\`. Never emit \`<tool_cli...>\` markup, a pseudo-call, or any other text that imitates a
14
+ tool invocation, and never write out what you expect a command would have printed — run it with the
15
+ bash tool and use the real output.
21
16
 
22
17
  Discovery (progressive — only fetch what you need):
23
18
  tool-cli --help # List MCP servers with tool counts
@@ -28,6 +23,12 @@ Calling tools:
28
23
  tool-cli <server> <tool> '{"key":"value"}' # Call a tool with JSON arguments
29
24
  tool-cli <server> <tool> '{}' --out /tmp/result.json # Save large output to file
30
25
 
26
+ Resources:
27
+ tool-cli resource list --server <server>
28
+ tool-cli resource templates --server <server>
29
+ tool-cli resource read --server <server> <uri>
30
+ tool-cli resource read --server <server> <uri> --out /tmp/resource.bin
31
+
31
32
  tool-cli outputs plain text or JSON. When a tool provides structured output (typed JSON),
32
33
  tool-cli returns it directly as JSON — use \`jq\` to query fields.
33
34
  Chain calls, filter, and transform results using pipes and bash idioms:
@@ -49,9 +50,24 @@ Chain calls, filter, and transform results using pipes and bash idioms:
49
50
  # Combine with standard tools
50
51
  tool-cli myserver export_csv '{"table":"users"}' | sort -t, -k2 | head -20
51
52
 
52
- Prefer piping and chaining over multiple separate tool calls when processing collections or filtering results.
53
+ Because tool-cli runs inside a bash command, filtering, joining, or writing results to disk with
54
+ ordinary programs is part of the same invocation — prefer one piped command over many separate
55
+ calls when processing collections.
53
56
  Errors go to stderr with exit code 1 — use \`&&\` or \`set -e\` for safe chaining.
54
- ${serverCount} MCP server(s) currently connected.
55
- </tool_cli>`;
57
+ Verified bridge: ${bridgeInfo.serverImplementation.name}@${bridgeInfo.serverImplementation.version};
58
+ protocol ${bridgeInfo.bridgeProtocol.name} v${bridgeInfo.bridgeProtocol.version}; authenticated bearer RPC;
59
+ operations ${bridgeInfo.operations.join(", ")}.
60
+ Verified capabilities: tool discovery=${bridgeInfo.capabilities.tools.discovery}, calls=${bridgeInfo.capabilities.tools.calls},
61
+ schema validation=${bridgeInfo.capabilities.tools.inputSchemaValidation}; resource list=${bridgeInfo.capabilities.resources.list},
62
+ templates=${bridgeInfo.capabilities.resources.templates}, read=${bridgeInfo.capabilities.resources.read};
63
+ provider cancellation=${bridgeInfo.capabilities.cancellation.providerAbortSignal}.
64
+ Upstream MCP summary: ${upstreamServerCount} server(s) reported by the verified bridge handshake.
65
+ </tool_cli_usage_docs>`;
66
+ }
67
+ function readUpstreamServerCount(upstream) {
68
+ if (upstream === null || typeof upstream !== "object" || Array.isArray(upstream)) {
69
+ return "unknown";
70
+ }
71
+ const count = Reflect.get(upstream, "serverCount");
72
+ return typeof count === "number" && Number.isInteger(count) && count >= 0 ? count : "unknown";
56
73
  }
57
- //# sourceMappingURL=format.js.map
@@ -1,4 +1,7 @@
1
1
  export { ToolCliServer } from "@sammorrowdrums/tool-cli/server";
2
- export type { ToolProvider } from "@sammorrowdrums/tool-cli/server";
2
+ export type { BridgeInfo, ToolProvider } from "@sammorrowdrums/tool-cli/server";
3
+ export { formatToolCliBridgeError, isToolCliCompatibilityError, verifyToolCliBridge, type ToolCliBridgeEndpoint, } from "./bridge.js";
3
4
  export { formatToolCliForPrompt } from "./format.js";
4
- //# sourceMappingURL=index.d.ts.map
5
+ export { buildUpstreamMcpSummary, createPolicyToolProvider, type PolicyToolProviderOptions, } from "./provider.js";
6
+ export { startToolCliBridge, withholdToolCliCredentials } from "./startup.js";
7
+ export type { StartToolCliBridgeOptions, ToolCliBridgeServer, ToolCliEnvironment, } from "./startup.js";
@@ -1,3 +1,5 @@
1
1
  export { ToolCliServer } from "@sammorrowdrums/tool-cli/server";
2
+ export { formatToolCliBridgeError, isToolCliCompatibilityError, verifyToolCliBridge, } from "./bridge.js";
2
3
  export { formatToolCliForPrompt } from "./format.js";
3
- //# sourceMappingURL=index.js.map
4
+ export { buildUpstreamMcpSummary, createPolicyToolProvider, } from "./provider.js";
5
+ export { startToolCliBridge, withholdToolCliCredentials } from "./startup.js";
@@ -0,0 +1,21 @@
1
+ import type { ToolProvider, UpstreamMcpSummary } from "@sammorrowdrums/tool-cli/server";
2
+ import type { McpClientManager } from "../mcp/client-manager.js";
3
+ import type { McpPolicy } from "../mcp/policy.js";
4
+ export interface PolicyToolProviderOptions {
5
+ upstream?: Pick<McpClientManager, "getConnectedServers" | "getToolsForServer" | "getDiagnostics">;
6
+ }
7
+ /**
8
+ * Bridge the shared MCP policy boundary to tool-cli's `ToolProvider` interface.
9
+ *
10
+ * tool-cli's RPC server derives `listTools` and `describeTool` from
11
+ * `getTools`, so restricting discovery here restricts what the CLI can learn.
12
+ * More importantly, `callTool` does not check membership against the
13
+ * discovered set before forwarding, which means a caller can name a tool the
14
+ * CLI never advertised. Routing every call back through {@link McpPolicy}
15
+ * closes that gap: the same dispatcher that gates the proxy and Code Mode
16
+ * paths re-authorizes each RPC call, so naming a hidden tool is refused before
17
+ * the upstream server is contacted.
18
+ */
19
+ export declare function createPolicyToolProvider(policy: McpPolicy, options?: PolicyToolProviderOptions): ToolProvider;
20
+ /** Build a deterministic bridge summary from the manager's live per-server diagnostics. */
21
+ export declare function buildUpstreamMcpSummary(upstream: NonNullable<PolicyToolProviderOptions["upstream"]>): UpstreamMcpSummary;
@@ -0,0 +1,114 @@
1
+ import { toToolCliCallToolResult } from "../mcp/call-tool-result.js";
2
+ import { MCP_CLIENT_IDENTITY } from "../mcp/client-factory.js";
3
+ /**
4
+ * Bridge the shared MCP policy boundary to tool-cli's `ToolProvider` interface.
5
+ *
6
+ * tool-cli's RPC server derives `listTools` and `describeTool` from
7
+ * `getTools`, so restricting discovery here restricts what the CLI can learn.
8
+ * More importantly, `callTool` does not check membership against the
9
+ * discovered set before forwarding, which means a caller can name a tool the
10
+ * CLI never advertised. Routing every call back through {@link McpPolicy}
11
+ * closes that gap: the same dispatcher that gates the proxy and Code Mode
12
+ * paths re-authorizes each RPC call, so naming a hidden tool is refused before
13
+ * the upstream server is contacted.
14
+ */
15
+ export function createPolicyToolProvider(policy, options = {}) {
16
+ const provider = {
17
+ getServerNames: () => policy.getVisibleServers(),
18
+ getTools: (server) => policy.getVisibleTools(server).map(toToolInfo),
19
+ async callTool(server, tool, args, context) {
20
+ const terminal = await policy.callTool({
21
+ source: "tool-cli",
22
+ serverName: server,
23
+ toolName: tool,
24
+ args,
25
+ ...(context?.signal !== undefined ? { signal: context.signal } : {}),
26
+ });
27
+ return toToolCliCallToolResult(terminal);
28
+ },
29
+ async listResources(server, context) {
30
+ const resources = await policy.listResources({
31
+ source: "tool-cli",
32
+ serverName: server,
33
+ ...(context?.signal !== undefined ? { signal: context.signal } : {}),
34
+ });
35
+ return resources.map((resource) => ({ ...resource }));
36
+ },
37
+ async listResourceTemplates(server, context) {
38
+ const templates = await policy.listResourceTemplates({
39
+ source: "tool-cli",
40
+ serverName: server,
41
+ ...(context?.signal !== undefined ? { signal: context.signal } : {}),
42
+ });
43
+ return templates.map((template) => ({ ...template }));
44
+ },
45
+ async readResource(server, uri, context) {
46
+ const result = await policy.readResource({
47
+ source: "tool-cli",
48
+ serverName: server,
49
+ uri,
50
+ ...(context?.signal !== undefined ? { signal: context.signal } : {}),
51
+ });
52
+ return {
53
+ ...result,
54
+ contents: result.contents.map((content) => ({ ...content })),
55
+ };
56
+ },
57
+ };
58
+ const upstream = options.upstream;
59
+ if (upstream !== undefined) {
60
+ provider.getUpstreamMcpSummary = () => buildUpstreamMcpSummary(upstream);
61
+ }
62
+ return provider;
63
+ }
64
+ /** Build a deterministic bridge summary from the manager's live per-server diagnostics. */
65
+ export function buildUpstreamMcpSummary(upstream) {
66
+ const servers = upstream
67
+ .getConnectedServers()
68
+ .sort((left, right) => (left < right ? -1 : left > right ? 1 : 0))
69
+ .map((name) => {
70
+ const diagnostics = upstream.getDiagnostics(name);
71
+ return {
72
+ name,
73
+ toolCount: upstream.getToolsForServer(name).length,
74
+ ...(diagnostics?.protocolVersion !== undefined
75
+ ? { protocolVersion: diagnostics.protocolVersion }
76
+ : {}),
77
+ ...(diagnostics?.protocolEra !== undefined ? { protocolEra: diagnostics.protocolEra } : {}),
78
+ ...(diagnostics?.serverImplementation !== undefined
79
+ ? { implementation: diagnostics.serverImplementation }
80
+ : {}),
81
+ ...(diagnostics?.serverCapabilities !== undefined
82
+ ? { capabilities: diagnostics.serverCapabilities }
83
+ : {}),
84
+ ...(diagnostics?.discoverResult !== undefined
85
+ ? { discoverResult: diagnostics.discoverResult }
86
+ : {}),
87
+ ...(diagnostics !== undefined ? { skillsExtension: diagnostics.skillsExtension } : {}),
88
+ };
89
+ });
90
+ const protocolVersions = [
91
+ ...new Set(servers.flatMap((server) => server.protocolVersion !== undefined ? [server.protocolVersion] : [])),
92
+ ];
93
+ return {
94
+ ...(protocolVersions.length === 1 ? { protocolVersion: protocolVersions[0] } : {}),
95
+ implementation: { ...MCP_CLIENT_IDENTITY },
96
+ capabilities: {
97
+ multiplexedServers: true,
98
+ tools: {
99
+ serverCount: servers.filter((server) => server.toolCount > 0).length,
100
+ },
101
+ resources: {
102
+ serverCount: servers.filter((server) => server.capabilities?.resources !== undefined)
103
+ .length,
104
+ },
105
+ },
106
+ serverCount: servers.length,
107
+ servers,
108
+ };
109
+ }
110
+ function toToolInfo(tool) {
111
+ const info = { ...tool };
112
+ Reflect.deleteProperty(info, "serverName");
113
+ return info;
114
+ }
@@ -0,0 +1,22 @@
1
+ import type { BridgeInfo } from "@sammorrowdrums/tool-cli/client";
2
+ import type { BashState, ToolCliState } from "../routing/facilities.js";
3
+ import { type ToolCliBridgeEndpoint } from "./bridge.js";
4
+ export interface ToolCliBridgeServer {
5
+ start(log?: (message: string) => void): Promise<ToolCliBridgeEndpoint>;
6
+ stop(): Promise<void>;
7
+ }
8
+ export interface ToolCliEnvironment {
9
+ setEnv(name: string, value: string): void;
10
+ unsetEnv(name: string): void;
11
+ }
12
+ export interface StartToolCliBridgeOptions {
13
+ bash: BashState;
14
+ server: ToolCliBridgeServer;
15
+ environment: ToolCliEnvironment;
16
+ log: (message: string) => void;
17
+ verify?: (endpoint: ToolCliBridgeEndpoint) => Promise<BridgeInfo>;
18
+ }
19
+ /** Mask inherited credentials until this session has verified its own bridge endpoint. */
20
+ export declare function withholdToolCliCredentials(environment: ToolCliEnvironment): void;
21
+ /** Start, authenticate, and expose tool-cli only when both bridge and bash are usable. */
22
+ export declare function startToolCliBridge(options: StartToolCliBridgeOptions): Promise<ToolCliState>;