@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.
- package/LICENSE +21 -0
- package/README.md +137 -47
- package/dist/code-mode/eligibility.d.ts +29 -4
- package/dist/code-mode/eligibility.js +56 -6
- package/dist/code-mode/executor.d.ts +19 -1
- package/dist/code-mode/executor.js +73 -8
- package/dist/code-mode/index.d.ts +54 -12
- package/dist/code-mode/index.js +157 -65
- package/dist/code-mode/isolated-vm.d.ts +65 -0
- package/dist/code-mode/isolated-vm.js +116 -0
- package/dist/code-mode/tools.d.ts +4 -1
- package/dist/code-mode/tools.js +7 -4
- package/dist/code-mode/type-hints.d.ts +2 -2
- package/dist/code-mode/type-hints.js +24 -8
- package/dist/docker-e2e.d.ts +0 -1
- package/dist/docker-e2e.js +0 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +146 -67
- package/dist/mcp/call-tool-result.d.ts +10 -0
- package/dist/mcp/call-tool-result.js +70 -0
- package/dist/mcp/client-factory.d.ts +59 -0
- package/dist/mcp/client-factory.js +76 -0
- package/dist/mcp/client-manager.d.ts +61 -19
- package/dist/mcp/client-manager.js +223 -64
- package/dist/mcp/config-loader.d.ts +0 -1
- package/dist/mcp/config-loader.js +0 -1
- package/dist/mcp/config.d.ts +11 -1
- package/dist/mcp/config.js +15 -1
- package/dist/mcp/gateway-defaults.d.ts +14 -0
- package/dist/mcp/gateway-defaults.js +21 -0
- package/dist/mcp/host-approval.d.ts +16 -0
- package/dist/mcp/host-approval.js +19 -0
- package/dist/mcp/host-elicitation.d.ts +19 -0
- package/dist/mcp/host-elicitation.js +94 -0
- package/dist/mcp/index.d.ts +4 -1
- package/dist/mcp/index.js +4 -1
- package/dist/mcp/policy.d.ts +314 -0
- package/dist/mcp/policy.js +848 -0
- package/dist/routing/facilities.d.ts +109 -0
- package/dist/routing/facilities.js +175 -0
- package/dist/routing/format.d.ts +24 -0
- package/dist/routing/format.js +64 -0
- package/dist/routing/index.d.ts +4 -0
- package/dist/routing/index.js +4 -0
- package/dist/routing/seam.d.ts +35 -0
- package/dist/routing/seam.js +28 -0
- package/dist/routing/tripwire.d.ts +42 -0
- package/dist/routing/tripwire.js +80 -0
- package/dist/skills/discover.d.ts +5 -3
- package/dist/skills/discover.js +37 -12
- package/dist/skills/format.d.ts +4 -1
- package/dist/skills/format.js +8 -4
- package/dist/skills/index.d.ts +2 -2
- package/dist/skills/index.js +1 -1
- package/dist/skills/load-skill-tool.d.ts +23 -7
- package/dist/skills/load-skill-tool.js +112 -33
- package/dist/skills/mcp-tool-proxy.d.ts +4 -8
- package/dist/skills/mcp-tool-proxy.js +22 -97
- package/dist/skills/sep2640/client.d.ts +91 -0
- package/dist/skills/sep2640/client.js +160 -0
- package/dist/skills/sep2640/discover.d.ts +33 -0
- package/dist/skills/sep2640/discover.js +82 -0
- package/dist/skills/sep2640/index.d.ts +13 -0
- package/dist/skills/sep2640/index.js +13 -0
- package/dist/skills/sep2640/integrity.d.ts +58 -0
- package/dist/skills/sep2640/integrity.js +185 -0
- package/dist/skills/sep2640/load.d.ts +61 -0
- package/dist/skills/sep2640/load.js +90 -0
- package/dist/skills/sep2640/protocol.d.ts +146 -0
- package/dist/skills/sep2640/protocol.js +214 -0
- package/dist/skills/sep2640/spec.d.ts +76 -0
- package/dist/skills/sep2640/spec.js +88 -0
- package/dist/skills/skill-registry.d.ts +48 -2
- package/dist/skills/skill-registry.js +40 -3
- package/dist/tool-cli/bridge.d.ts +14 -0
- package/dist/tool-cli/bridge.js +196 -0
- package/dist/tool-cli/format.d.ts +17 -5
- package/dist/tool-cli/format.js +36 -20
- package/dist/tool-cli/index.d.ts +5 -2
- package/dist/tool-cli/index.js +3 -1
- package/dist/tool-cli/provider.d.ts +21 -0
- package/dist/tool-cli/provider.js +114 -0
- package/dist/tool-cli/startup.d.ts +22 -0
- package/dist/tool-cli/startup.js +47 -0
- package/package.json +78 -30
- package/dist/code-mode/code-mode-integration.test.d.ts +0 -2
- package/dist/code-mode/code-mode-integration.test.d.ts.map +0 -1
- package/dist/code-mode/code-mode-integration.test.js +0 -116
- package/dist/code-mode/code-mode-integration.test.js.map +0 -1
- package/dist/code-mode/eligibility.d.ts.map +0 -1
- package/dist/code-mode/eligibility.js.map +0 -1
- package/dist/code-mode/eligibility.test.d.ts +0 -2
- package/dist/code-mode/eligibility.test.d.ts.map +0 -1
- package/dist/code-mode/eligibility.test.js +0 -53
- package/dist/code-mode/eligibility.test.js.map +0 -1
- package/dist/code-mode/executor.d.ts.map +0 -1
- package/dist/code-mode/executor.js.map +0 -1
- package/dist/code-mode/executor.test.d.ts +0 -2
- package/dist/code-mode/executor.test.d.ts.map +0 -1
- package/dist/code-mode/executor.test.js +0 -130
- package/dist/code-mode/executor.test.js.map +0 -1
- package/dist/code-mode/index.d.ts.map +0 -1
- package/dist/code-mode/index.js.map +0 -1
- package/dist/code-mode/tools.d.ts.map +0 -1
- package/dist/code-mode/tools.js.map +0 -1
- package/dist/code-mode/type-hints.d.ts.map +0 -1
- package/dist/code-mode/type-hints.js.map +0 -1
- package/dist/code-mode/type-hints.test.d.ts +0 -2
- package/dist/code-mode/type-hints.test.d.ts.map +0 -1
- package/dist/code-mode/type-hints.test.js +0 -157
- package/dist/code-mode/type-hints.test.js.map +0 -1
- package/dist/docker-e2e.d.ts.map +0 -1
- package/dist/docker-e2e.js.map +0 -1
- package/dist/docker-e2e.test.d.ts +0 -2
- package/dist/docker-e2e.test.d.ts.map +0 -1
- package/dist/docker-e2e.test.js +0 -176
- package/dist/docker-e2e.test.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -8
- package/dist/index.test.js.map +0 -1
- package/dist/mcp/client-manager.d.ts.map +0 -1
- package/dist/mcp/client-manager.js.map +0 -1
- package/dist/mcp/client-manager.test.d.ts +0 -2
- package/dist/mcp/client-manager.test.d.ts.map +0 -1
- package/dist/mcp/client-manager.test.js +0 -204
- package/dist/mcp/client-manager.test.js.map +0 -1
- package/dist/mcp/config-loader.d.ts.map +0 -1
- package/dist/mcp/config-loader.js.map +0 -1
- package/dist/mcp/config-loader.test.d.ts +0 -2
- package/dist/mcp/config-loader.test.d.ts.map +0 -1
- package/dist/mcp/config-loader.test.js +0 -93
- package/dist/mcp/config-loader.test.js.map +0 -1
- package/dist/mcp/config.d.ts.map +0 -1
- package/dist/mcp/config.js.map +0 -1
- package/dist/mcp/index.d.ts.map +0 -1
- package/dist/mcp/index.js.map +0 -1
- package/dist/skills/discover.d.ts.map +0 -1
- package/dist/skills/discover.js.map +0 -1
- package/dist/skills/format.d.ts.map +0 -1
- package/dist/skills/format.js.map +0 -1
- package/dist/skills/format.test.d.ts +0 -2
- package/dist/skills/format.test.d.ts.map +0 -1
- package/dist/skills/format.test.js +0 -63
- package/dist/skills/format.test.js.map +0 -1
- package/dist/skills/index.d.ts.map +0 -1
- package/dist/skills/index.js.map +0 -1
- package/dist/skills/load-skill-tool.d.ts.map +0 -1
- package/dist/skills/load-skill-tool.js.map +0 -1
- package/dist/skills/mcp-tool-proxy.d.ts.map +0 -1
- package/dist/skills/mcp-tool-proxy.js.map +0 -1
- package/dist/skills/skill-integration.test.d.ts +0 -2
- package/dist/skills/skill-integration.test.d.ts.map +0 -1
- package/dist/skills/skill-integration.test.js +0 -119
- package/dist/skills/skill-integration.test.js.map +0 -1
- package/dist/skills/skill-registry.d.ts.map +0 -1
- package/dist/skills/skill-registry.js.map +0 -1
- package/dist/skills/skill-registry.test.d.ts +0 -2
- package/dist/skills/skill-registry.test.d.ts.map +0 -1
- package/dist/skills/skill-registry.test.js +0 -67
- package/dist/skills/skill-registry.test.js.map +0 -1
- package/dist/test-servers/weather-server.d.ts +0 -12
- package/dist/test-servers/weather-server.d.ts.map +0 -1
- package/dist/test-servers/weather-server.js +0 -139
- package/dist/test-servers/weather-server.js.map +0 -1
- package/dist/test-servers/weather-stdio.d.ts +0 -2
- package/dist/test-servers/weather-stdio.d.ts.map +0 -1
- package/dist/test-servers/weather-stdio.js +0 -11
- package/dist/test-servers/weather-stdio.js.map +0 -1
- package/dist/tool-cli/cli.d.ts +0 -3
- package/dist/tool-cli/cli.d.ts.map +0 -1
- package/dist/tool-cli/cli.js +0 -179
- package/dist/tool-cli/cli.js.map +0 -1
- package/dist/tool-cli/constants.d.ts +0 -7
- package/dist/tool-cli/constants.d.ts.map +0 -1
- package/dist/tool-cli/constants.js +0 -15
- package/dist/tool-cli/constants.js.map +0 -1
- package/dist/tool-cli/format.d.ts.map +0 -1
- package/dist/tool-cli/format.js.map +0 -1
- package/dist/tool-cli/format.test.d.ts +0 -2
- package/dist/tool-cli/format.test.d.ts.map +0 -1
- package/dist/tool-cli/format.test.js +0 -30
- package/dist/tool-cli/format.test.js.map +0 -1
- package/dist/tool-cli/index.d.ts.map +0 -1
- package/dist/tool-cli/index.js.map +0 -1
- package/dist/tool-cli/rpc-client.d.ts +0 -6
- package/dist/tool-cli/rpc-client.d.ts.map +0 -1
- package/dist/tool-cli/rpc-client.js +0 -25
- package/dist/tool-cli/rpc-client.js.map +0 -1
- package/dist/tool-cli/rpc-server.d.ts +0 -48
- package/dist/tool-cli/rpc-server.d.ts.map +0 -1
- package/dist/tool-cli/rpc-server.js +0 -220
- package/dist/tool-cli/rpc-server.js.map +0 -1
- package/dist/tool-cli/rpc-server.test.d.ts +0 -2
- package/dist/tool-cli/rpc-server.test.d.ts.map +0 -1
- package/dist/tool-cli/rpc-server.test.js +0 -154
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
3
|
+
* Usage documentation for tool-cli.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
8
|
-
|
|
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;
|
package/dist/tool-cli/format.js
CHANGED
|
@@ -1,23 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
<
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
${
|
|
55
|
-
|
|
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
|
package/dist/tool-cli/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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";
|
package/dist/tool-cli/index.js
CHANGED
|
@@ -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
|
-
|
|
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>;
|