@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,85 @@
1
+ /** Metadata for a skill discovered from an MCP server. */
2
+ export interface McpSkillMetadata {
3
+ /** Skill name from SKILL.md frontmatter. */
4
+ name: string;
5
+ /** Short description from SKILL.md frontmatter. */
6
+ description: string;
7
+ /** The skill:// URI for the SKILL.md resource. */
8
+ uri: string;
9
+ /** Which MCP server this skill came from. */
10
+ serverName: string;
11
+ /** Tool names this skill gates via allowed-tools frontmatter. */
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;
32
+ }
33
+ /**
34
+ * Registry for MCP-discovered skills.
35
+ *
36
+ * Holds skill metadata discovered from MCP servers. Skills are registered
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.
44
+ */
45
+ export declare class SkillRegistry {
46
+ private skills;
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
+ */
55
+ register(skill: McpSkillMetadata): void;
56
+ /** Register multiple skills at once. */
57
+ registerAll(skills: McpSkillMetadata[]): void;
58
+ /** Get a skill by name. */
59
+ get(name: string): McpSkillMetadata | undefined;
60
+ /** Get all registered skills. */
61
+ getAll(): McpSkillMetadata[];
62
+ /** Name collisions resolved by qualification, oldest first. */
63
+ getCollisions(): SkillNameCollision[];
64
+ /** Remove a skill by name. */
65
+ unregister(name: string): boolean;
66
+ /** Remove all skills from a specific MCP server. */
67
+ unregisterByServer(serverName: string): void;
68
+ /** Remove all skills. */
69
+ clear(): void;
70
+ /** Number of registered skills. */
71
+ get size(): number;
72
+ }
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
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Registry for MCP-discovered skills.
3
+ *
4
+ * Holds skill metadata discovered from MCP servers. Skills are registered
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.
12
+ */
13
+ export class SkillRegistry {
14
+ skills = new Map();
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
+ */
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
+ }
36
+ this.skills.set(skill.name, skill);
37
+ }
38
+ /** Register multiple skills at once. */
39
+ registerAll(skills) {
40
+ for (const skill of skills) {
41
+ this.register(skill);
42
+ }
43
+ }
44
+ /** Get a skill by name. */
45
+ get(name) {
46
+ return this.skills.get(name);
47
+ }
48
+ /** Get all registered skills. */
49
+ getAll() {
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];
55
+ }
56
+ /** Remove a skill by name. */
57
+ unregister(name) {
58
+ return this.skills.delete(name);
59
+ }
60
+ /** Remove all skills from a specific MCP server. */
61
+ unregisterByServer(serverName) {
62
+ for (const [name, skill] of this.skills) {
63
+ if (skill.serverName === serverName) {
64
+ this.skills.delete(name);
65
+ }
66
+ }
67
+ this.collisions = this.collisions.filter((collision) => collision.incumbent.serverName !== serverName &&
68
+ collision.challenger.serverName !== serverName);
69
+ }
70
+ /** Remove all skills. */
71
+ clear() {
72
+ this.skills.clear();
73
+ this.collisions = [];
74
+ }
75
+ /** Number of registered skills. */
76
+ get size() {
77
+ return this.skills.size;
78
+ }
79
+ }
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
+ }
@@ -0,0 +1,20 @@
1
+ import type { BashState, ToolCliState } from "../routing/facilities.js";
2
+ /**
3
+ * Usage documentation for tool-cli.
4
+ *
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.
13
+ */
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;
@@ -0,0 +1,73 @@
1
+ export function formatToolCliForPrompt(state) {
2
+ if (state.toolCli.kind !== "verified" || state.bash.kind !== "registered")
3
+ return "";
4
+ const { bridgeInfo } = state.toolCli;
5
+ const upstreamServerCount = readUpstreamServerCount(bridgeInfo.upstreamMcp);
6
+ return `
7
+
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.
11
+
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.
16
+
17
+ Discovery (progressive — only fetch what you need):
18
+ tool-cli --help # List MCP servers with tool counts
19
+ tool-cli <server> # List all tools on a server
20
+ tool-cli <server> <tool> # Show full schema for a tool
21
+
22
+ Calling tools:
23
+ tool-cli <server> <tool> '{"key":"value"}' # Call a tool with JSON arguments
24
+ tool-cli <server> <tool> '{}' --out /tmp/result.json # Save large output to file
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
+
32
+ tool-cli outputs plain text or JSON. When a tool provides structured output (typed JSON),
33
+ tool-cli returns it directly as JSON — use \`jq\` to query fields.
34
+ Chain calls, filter, and transform results using pipes and bash idioms:
35
+
36
+ # Search across tool results
37
+ tool-cli myserver search_docs '{"query":"auth"}' | grep -i "token"
38
+
39
+ # Query structured JSON output with jq
40
+ tool-cli myserver list_issues '{"repo":"owner/repo"}' | jq '.[].title'
41
+
42
+ # Chain tool calls — feed one result into another
43
+ tool-cli myserver list_items '{}' | jq -r '.[0].id' | xargs -I{} tool-cli myserver get_item '{"id":"{}"}'
44
+
45
+ # Process multiple items
46
+ for city in London Tokyo Paris; do
47
+ echo "=== $city ==="; tool-cli weather check_weather '{"city":"'$city'"}';
48
+ done
49
+
50
+ # Combine with standard tools
51
+ tool-cli myserver export_csv '{"table":"users"}' | sort -t, -k2 | head -20
52
+
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.
56
+ Errors go to stderr with exit code 1 — use \`&&\` or \`set -e\` for safe chaining.
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";
73
+ }
@@ -0,0 +1,7 @@
1
+ export { ToolCliServer } 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";
4
+ export { formatToolCliForPrompt } from "./format.js";
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";
@@ -0,0 +1,5 @@
1
+ export { ToolCliServer } from "@sammorrowdrums/tool-cli/server";
2
+ export { formatToolCliBridgeError, isToolCliCompatibilityError, verifyToolCliBridge, } from "./bridge.js";
3
+ export { formatToolCliForPrompt } from "./format.js";
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>;