@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.
- package/LICENSE +21 -0
- package/README.md +137 -47
- package/dist/code-mode/eligibility.d.ts +35 -0
- package/dist/code-mode/eligibility.js +64 -0
- package/dist/code-mode/executor.d.ts +53 -0
- package/dist/code-mode/executor.js +185 -0
- package/dist/code-mode/index.d.ts +107 -0
- package/dist/code-mode/index.js +267 -0
- 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 +48 -0
- package/dist/code-mode/tools.js +73 -0
- package/dist/code-mode/type-hints.d.ts +20 -0
- package/dist/code-mode/type-hints.js +205 -0
- package/dist/docker-e2e.d.ts +54 -0
- package/dist/docker-e2e.js +234 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +227 -0
- 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 +88 -0
- package/dist/mcp/client-manager.js +307 -0
- package/dist/mcp/config-loader.d.ts +7 -0
- package/dist/mcp/config-loader.js +42 -0
- package/dist/mcp/config.d.ts +56 -0
- package/dist/mcp/config.js +42 -0
- 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 +8 -0
- package/dist/mcp/index.js +7 -0
- 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 +12 -0
- package/dist/skills/discover.js +85 -0
- package/dist/skills/format.d.ts +13 -0
- package/dist/skills/format.js +42 -0
- package/dist/skills/index.d.ts +6 -0
- package/dist/skills/index.js +6 -0
- package/dist/skills/load-skill-tool.d.ts +56 -0
- package/dist/skills/load-skill-tool.js +183 -0
- package/dist/skills/mcp-tool-proxy.d.ts +7 -0
- package/dist/skills/mcp-tool-proxy.js +41 -0
- 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 +85 -0
- package/dist/skills/skill-registry.js +85 -0
- package/dist/tool-cli/bridge.d.ts +14 -0
- package/dist/tool-cli/bridge.js +196 -0
- package/dist/tool-cli/format.d.ts +20 -0
- package/dist/tool-cli/format.js +73 -0
- package/dist/tool-cli/index.d.ts +7 -0
- package/dist/tool-cli/index.js +5 -0
- 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 -26
- package/.dockerignore +0 -6
- package/.github/dependabot.yml +0 -25
- package/.github/workflows/ci.yml +0 -50
- package/.hadolint.yaml +0 -6
- package/.prettierignore +0 -4
- package/.prettierrc +0 -7
- package/AGENTS.md +0 -118
- package/DECISIONS.md +0 -99
- package/Dockerfile +0 -39
- package/aube-lock.yaml +0 -4146
- package/docs/code-mode.md +0 -38
- package/docs/skills.md +0 -57
- package/docs/tool-cli.md +0 -54
- package/eslint.config.js +0 -27
- package/images/banner.webp +0 -0
- package/images/code-c-maude.webp +0 -0
- package/images/code-mode-histogram.png +0 -0
- package/images/nuclear-mcp-football.webp +0 -0
- package/images/skills-enabling-mcp-tools.png +0 -0
- package/images/the-skill-dealer.webp +0 -0
- package/images/tool-cli-grep.png +0 -0
- package/mise.toml +0 -53
- package/scripts/docker-entrypoint.sh +0 -29
- package/scripts/test-echo-server.mjs +0 -28
- package/scripts/test-mcp-integration.mjs +0 -59
- package/scripts/test-tool-cli-smoke.mjs +0 -93
- package/src/code-mode/code-mode-integration.test.ts +0 -135
- package/src/code-mode/eligibility.test.ts +0 -60
- package/src/code-mode/eligibility.ts +0 -16
- package/src/code-mode/executor.test.ts +0 -168
- package/src/code-mode/executor.ts +0 -163
- package/src/code-mode/index.ts +0 -208
- package/src/code-mode/tools.ts +0 -110
- package/src/code-mode/type-hints.test.ts +0 -187
- package/src/code-mode/type-hints.ts +0 -249
- package/src/docker-e2e.test.ts +0 -195
- package/src/docker-e2e.ts +0 -277
- package/src/index.test.ts +0 -8
- package/src/index.ts +0 -179
- package/src/mcp/client-manager.test.ts +0 -250
- package/src/mcp/client-manager.ts +0 -199
- package/src/mcp/config-loader.test.ts +0 -115
- package/src/mcp/config-loader.ts +0 -46
- package/src/mcp/config.ts +0 -40
- package/src/mcp/index.ts +0 -4
- package/src/skills/discover.ts +0 -81
- package/src/skills/format.test.ts +0 -70
- package/src/skills/format.ts +0 -42
- package/src/skills/index.ts +0 -9
- package/src/skills/load-skill-tool.ts +0 -136
- package/src/skills/mcp-tool-proxy.ts +0 -143
- package/src/skills/skill-integration.test.ts +0 -139
- package/src/skills/skill-registry.test.ts +0 -83
- package/src/skills/skill-registry.ts +0 -69
- package/src/test-servers/weather-server.ts +0 -171
- package/src/test-servers/weather-stdio.ts +0 -11
- package/src/tool-cli/format.test.ts +0 -33
- package/src/tool-cli/format.ts +0 -56
- package/src/tool-cli/index.ts +0 -3
- package/tsconfig.json +0 -21
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { BridgeInfo } from "@sammorrowdrums/tool-cli/client";
|
|
2
|
+
/**
|
|
3
|
+
* The model-facing execution facilities mcpi-ext puts in front of an agent.
|
|
4
|
+
*
|
|
5
|
+
* An agent has to decide, per task, which surface actually does the work. This
|
|
6
|
+
* module is the single source of truth for how those surfaces are described:
|
|
7
|
+
* `buildExecutionFacilities` turns observed session state into a stable ordered
|
|
8
|
+
* list of descriptors, and every consumer — the prompt fallback and the future
|
|
9
|
+
* host registration seam — renders from that same list.
|
|
10
|
+
*
|
|
11
|
+
* Two rules govern everything here:
|
|
12
|
+
*
|
|
13
|
+
* 1. **Task shape, not precedence.** Each facility is described by the kind of
|
|
14
|
+
* work it suits. No facility outranks another and none is a default. The
|
|
15
|
+
* list is ordered alphabetically by id purely so the emitted bytes are
|
|
16
|
+
* stable across turns; the order carries no ranking.
|
|
17
|
+
* 2. **Availability is always stated.** A facility is never silently dropped.
|
|
18
|
+
* An unavailable facility is still listed together with the reason, so the
|
|
19
|
+
* agent neither attempts it nor invents output it never produced.
|
|
20
|
+
*/
|
|
21
|
+
/** Stable machine identifiers for the four facilities. */
|
|
22
|
+
export type FacilityId = "bash" | "code_mode" | "skills" | "tool_cli";
|
|
23
|
+
/**
|
|
24
|
+
* Facility order. Alphabetical by id so the rendered section is byte-stable.
|
|
25
|
+
* This is deliberately not a preference order.
|
|
26
|
+
*/
|
|
27
|
+
export declare const FACILITY_ORDER: readonly FacilityId[];
|
|
28
|
+
/**
|
|
29
|
+
* Three states, because "we could not tell" is a real answer and is not the
|
|
30
|
+
* same as "it is not there". Claiming either would be untruthful.
|
|
31
|
+
*/
|
|
32
|
+
export type AvailabilityState = "available" | "unavailable" | "unknown";
|
|
33
|
+
export interface FacilityAvailability {
|
|
34
|
+
state: AvailabilityState;
|
|
35
|
+
/** Always populated. The reason for the state, never an empty string. */
|
|
36
|
+
detail: string;
|
|
37
|
+
}
|
|
38
|
+
export interface ExecutionFacility {
|
|
39
|
+
id: FacilityId;
|
|
40
|
+
/** Heading shown to the model. */
|
|
41
|
+
title: string;
|
|
42
|
+
/** Intent-first sentence. Always begins with "Use when". */
|
|
43
|
+
useWhen: string;
|
|
44
|
+
/** What the facility actually does. */
|
|
45
|
+
provides: string[];
|
|
46
|
+
/** What it deliberately cannot do, so the agent does not mis-route to it. */
|
|
47
|
+
doesNotProvide: string[];
|
|
48
|
+
availability: FacilityAvailability;
|
|
49
|
+
}
|
|
50
|
+
/** Skills discovered this session, plus the status of the draft extension. */
|
|
51
|
+
export interface SkillsState {
|
|
52
|
+
count: number;
|
|
53
|
+
/** Whether the DRAFT, unratified SEP-2640 skills extension is enabled. */
|
|
54
|
+
draftExtensionEnabled: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* tool-cli is only advertised after its local bridge completes an authenticated
|
|
58
|
+
* compatible handshake. Failures remain explicit so the prompt has a next step.
|
|
59
|
+
*/
|
|
60
|
+
export type ToolCliState = {
|
|
61
|
+
kind: "verified";
|
|
62
|
+
port: number;
|
|
63
|
+
bridgeInfo: BridgeInfo;
|
|
64
|
+
} | {
|
|
65
|
+
kind: "not_started";
|
|
66
|
+
reason: string;
|
|
67
|
+
} | {
|
|
68
|
+
kind: "failed";
|
|
69
|
+
reason: string;
|
|
70
|
+
} | {
|
|
71
|
+
kind: "incompatible";
|
|
72
|
+
reason: string;
|
|
73
|
+
} | {
|
|
74
|
+
kind: "no_bash";
|
|
75
|
+
reason: string;
|
|
76
|
+
};
|
|
77
|
+
/** Whether the host currently exposes a shell tool, when that is discoverable at all. */
|
|
78
|
+
export type BashState = {
|
|
79
|
+
kind: "registered";
|
|
80
|
+
toolName: string;
|
|
81
|
+
} | {
|
|
82
|
+
kind: "absent";
|
|
83
|
+
} | {
|
|
84
|
+
kind: "undiscoverable";
|
|
85
|
+
reason: string;
|
|
86
|
+
};
|
|
87
|
+
export interface ExecutionRoutingState {
|
|
88
|
+
skills: SkillsState;
|
|
89
|
+
codeMode: CodeModeState;
|
|
90
|
+
toolCli: ToolCliState;
|
|
91
|
+
bash: BashState;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Code mode needs the optional `isolated-vm` native addon. When that addon is
|
|
95
|
+
* absent the facility is genuinely unavailable, and `reason` carries the
|
|
96
|
+
* specific cause so the prompt can say *why* rather than just that it failed.
|
|
97
|
+
*/
|
|
98
|
+
export interface CodeModeState {
|
|
99
|
+
active: boolean;
|
|
100
|
+
/** Why code mode is inactive. Ignored when `active` is true. */
|
|
101
|
+
reason?: string;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Build the ordered facility descriptors for the current session.
|
|
105
|
+
*
|
|
106
|
+
* Pure and deterministic: equal input always yields an equal list in
|
|
107
|
+
* `FACILITY_ORDER`, so the rendered prompt section is byte-stable.
|
|
108
|
+
*/
|
|
109
|
+
export declare function buildExecutionFacilities(state: ExecutionRoutingState): ExecutionFacility[];
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Facility order. Alphabetical by id so the rendered section is byte-stable.
|
|
3
|
+
* This is deliberately not a preference order.
|
|
4
|
+
*/
|
|
5
|
+
export const FACILITY_ORDER = ["bash", "code_mode", "skills", "tool_cli"];
|
|
6
|
+
function skillsAvailability(skills) {
|
|
7
|
+
const draft = skills.draftExtensionEnabled
|
|
8
|
+
? "Draft SEP-2640 skills extension: enabled (unratified draft)."
|
|
9
|
+
: "Draft SEP-2640 skills extension: disabled.";
|
|
10
|
+
if (skills.count === 0) {
|
|
11
|
+
return {
|
|
12
|
+
state: "unavailable",
|
|
13
|
+
detail: `No MCP skills were discovered this session. ${draft} There is nothing for load_skill to load, so route the task to another facility.`,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
state: "available",
|
|
18
|
+
detail: `${skills.count} MCP skill(s) discovered and loadable by name. ${draft}`,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function codeModeAvailability(codeMode) {
|
|
22
|
+
if (!codeMode.active) {
|
|
23
|
+
const cause = codeMode.reason?.trim();
|
|
24
|
+
return {
|
|
25
|
+
state: "unavailable",
|
|
26
|
+
detail: cause
|
|
27
|
+
? `Code mode is unavailable because ${cause}. code_execute and code_search cannot run; sandboxed execution is never downgraded to an in-process fallback. Route exact computation to bash or tool-cli instead.`
|
|
28
|
+
: "Code mode did not initialise this session, so code_execute and code_search cannot run.",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
state: "available",
|
|
33
|
+
detail: "Available with zero MCP servers connected — pure computation needs no server, and code_execute is registered whenever mcpi-ext loads.",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function toolCliAvailability(toolCli, bash) {
|
|
37
|
+
if (toolCli.kind === "incompatible") {
|
|
38
|
+
return {
|
|
39
|
+
state: "unavailable",
|
|
40
|
+
detail: `The authenticated tool-cli bridge handshake found an incompatible bridge/client contract: ${toolCli.reason}. TOOL_CLI_PORT and TOOL_CLI_TOKEN were not exposed. Install matching tool-cli and mcpi-ext major versions before retrying.`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (toolCli.kind === "failed") {
|
|
44
|
+
return {
|
|
45
|
+
state: "unavailable",
|
|
46
|
+
detail: `The local tool-cli bridge failed startup or its authenticated handshake: ${toolCli.reason}. MCP tools cannot be reached from the shell this session — use another available facility and report this failure rather than retrying tool-cli blindly.`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
if (toolCli.kind === "no_bash") {
|
|
50
|
+
return {
|
|
51
|
+
state: "unavailable",
|
|
52
|
+
detail: `The tool-cli bridge was not started because bash availability is required and was not confirmed: ${toolCli.reason}. No bridge credentials were exposed.`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (toolCli.kind === "not_started") {
|
|
56
|
+
return {
|
|
57
|
+
state: "unavailable",
|
|
58
|
+
detail: `The local tool-cli RPC server was not started: ${toolCli.reason}.`,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const verified = `The local tool-cli bridge started on port ${toolCli.port}, completed an authenticated ` +
|
|
62
|
+
`${toolCli.bridgeInfo.bridgeProtocol.name} v${toolCli.bridgeInfo.bridgeProtocol.version} handshake, ` +
|
|
63
|
+
`and reported ${toolCli.bridgeInfo.serverImplementation.name}@${toolCli.bridgeInfo.serverImplementation.version}.`;
|
|
64
|
+
// tool-cli is reached through the shell, so a missing shell tool makes a
|
|
65
|
+
// healthy RPC server unusable. Saying "available" here would be a lie.
|
|
66
|
+
if (bash.kind === "absent") {
|
|
67
|
+
return {
|
|
68
|
+
state: "unavailable",
|
|
69
|
+
detail: `${verified} However, no host bash tool is active now, so its credentials are not usable through the required invocation path.`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (bash.kind === "undiscoverable") {
|
|
73
|
+
return {
|
|
74
|
+
state: "unavailable",
|
|
75
|
+
detail: `${verified} However, current bash availability is unconfirmed (${bash.reason}), so tool-cli is not advertised as invocable.`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
state: "available",
|
|
80
|
+
detail: `${verified} TOOL_CLI_PORT and TOOL_CLI_TOKEN are set for commands run with the "${bash.toolName}" tool.`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function bashAvailability(bash) {
|
|
84
|
+
switch (bash.kind) {
|
|
85
|
+
case "registered":
|
|
86
|
+
return {
|
|
87
|
+
state: "available",
|
|
88
|
+
detail: `The host "${bash.toolName}" tool is active this session.`,
|
|
89
|
+
};
|
|
90
|
+
case "absent":
|
|
91
|
+
return {
|
|
92
|
+
state: "unavailable",
|
|
93
|
+
detail: "No host shell tool is active this session, so shell commands, external programs, and tool-cli cannot run.",
|
|
94
|
+
};
|
|
95
|
+
case "undiscoverable":
|
|
96
|
+
return {
|
|
97
|
+
state: "unknown",
|
|
98
|
+
detail: `The host tool registry could not be read (${bash.reason}), so shell availability is unconfirmed. Try the command you need and treat a failure as absence rather than assuming either way.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Build the ordered facility descriptors for the current session.
|
|
104
|
+
*
|
|
105
|
+
* Pure and deterministic: equal input always yields an equal list in
|
|
106
|
+
* `FACILITY_ORDER`, so the rendered prompt section is byte-stable.
|
|
107
|
+
*/
|
|
108
|
+
export function buildExecutionFacilities(state) {
|
|
109
|
+
return [
|
|
110
|
+
{
|
|
111
|
+
id: "bash",
|
|
112
|
+
title: "bash and external programs",
|
|
113
|
+
useWhen: "Use when the task touches the real machine: reading or writing files, running git, package managers, compilers, formatters or test runners, moving data between programs, or producing an artifact that has to exist on disk afterwards.",
|
|
114
|
+
provides: [
|
|
115
|
+
"The host bash tool, which runs real shell commands with filesystem, network, and process access.",
|
|
116
|
+
"Every external program installed on the host, composed with pipes, redirection, loops, globs, and exit codes.",
|
|
117
|
+
"The substrate the other facilities lack: this is the only facility that can create, modify, or inspect files and artifacts.",
|
|
118
|
+
],
|
|
119
|
+
doesNotProvide: [
|
|
120
|
+
"MCP tool access on its own — reaching an MCP tool from the shell is the tool-cli facility, itself run as a bash command.",
|
|
121
|
+
"A sandbox. Commands run with the host's real permissions and their effects persist.",
|
|
122
|
+
],
|
|
123
|
+
availability: bashAvailability(state.bash),
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "code_mode",
|
|
127
|
+
title: "Code mode (code_execute, code_search)",
|
|
128
|
+
useWhen: "Use when the task needs exact computation or control flow: arithmetic, date maths, parsing, filtering, aggregation, pagination loops, or joining results — anywhere an approximated answer would simply be wrong.",
|
|
129
|
+
provides: [
|
|
130
|
+
"code_execute, which runs vanilla JavaScript in a sandboxed V8 isolate and returns the value you return.",
|
|
131
|
+
"code_search, which queries the MCP tool catalogue so you can find dispatchable tools before writing code.",
|
|
132
|
+
"Read-only MCP tools dispatched from inside the sandbox through the codemode namespace, so one execution can loop over many calls.",
|
|
133
|
+
],
|
|
134
|
+
doesNotProvide: [
|
|
135
|
+
"Filesystem access. There is no fs, no file read or write, and no path the isolate can reach.",
|
|
136
|
+
"Network access. There is no fetch, no sockets, and no outbound request of any kind.",
|
|
137
|
+
"Process access. There is no process, no require, no import, and no child process.",
|
|
138
|
+
"Non-read-only MCP tools, which are refused inside the sandbox rather than prompted for.",
|
|
139
|
+
],
|
|
140
|
+
availability: codeModeAvailability(state.codeMode),
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: "skills",
|
|
144
|
+
title: "Skills (load_skill)",
|
|
145
|
+
useWhen: "Use when the task is a domain workflow an MCP server has already documented — a named procedure with its own sequencing, conventions, and curated tool set, such as a triage runbook or a release checklist.",
|
|
146
|
+
provides: [
|
|
147
|
+
"Workflow guidance authored by the server: the skill body, loaded on demand by name with load_skill.",
|
|
148
|
+
"The specific tools that skill declares, enabled only after you approve the grant.",
|
|
149
|
+
],
|
|
150
|
+
doesNotProvide: [
|
|
151
|
+
"Computation, filesystem access, or shell access.",
|
|
152
|
+
"Authority over tools the skill did not declare; loading a skill never widens access beyond its approved list.",
|
|
153
|
+
"Anything at all before approval — a declined or unavailable approval leaves every gated tool locked.",
|
|
154
|
+
],
|
|
155
|
+
availability: skillsAvailability(state.skills),
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "tool_cli",
|
|
159
|
+
title: "tool-cli (MCP-to-shell on-ramp)",
|
|
160
|
+
useWhen: "Use when you need to reach a specific MCP tool directly and no documented skill covers the task, or when you want to discover which servers and tools exist before committing to an approach.",
|
|
161
|
+
provides: [
|
|
162
|
+
"An authenticated command-line on-ramp to the same MCP tools the host already authorises, invoked through the host bash tool as `tool-cli ...`.",
|
|
163
|
+
"Progressive discovery: servers, then a server's tools, then one tool's schema, so you read only what you need.",
|
|
164
|
+
"Policy-authorized MCP resource listing, templates, and reads, including binary output written with `--out`.",
|
|
165
|
+
"Plain text and JSON on stdout, so results compose with jq, grep, pipes, and loops inside the same bash command.",
|
|
166
|
+
],
|
|
167
|
+
doesNotProvide: [
|
|
168
|
+
"A tool of its own. tool-cli is a program you run with the bash tool, never something you call directly.",
|
|
169
|
+
"Any authority the host has not already granted — every call is re-authorised before it reaches a server.",
|
|
170
|
+
"Access to `skill://` resources, which remain isolated behind skill discovery and load_skill.",
|
|
171
|
+
],
|
|
172
|
+
availability: toolCliAvailability(state.toolCli, state.bash),
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type ExecutionFacility, type ExecutionRoutingState } from "./facilities.js";
|
|
2
|
+
/**
|
|
3
|
+
* Tag for the routing section. Named `execution_routing` rather than anything
|
|
4
|
+
* that reads like an invocable surface, so the model never mistakes the section
|
|
5
|
+
* itself for something it can call.
|
|
6
|
+
*/
|
|
7
|
+
export declare const EXECUTION_ROUTING_TAG = "execution_routing";
|
|
8
|
+
/**
|
|
9
|
+
* Render an already-built facility list.
|
|
10
|
+
*
|
|
11
|
+
* Kept separate from {@link formatExecutionRouting} so the future host seam and
|
|
12
|
+
* the prompt fallback can render byte-identical text from one descriptor list.
|
|
13
|
+
*/
|
|
14
|
+
export declare function formatExecutionFacilities(facilities: readonly ExecutionFacility[]): string;
|
|
15
|
+
/**
|
|
16
|
+
* Render the execution routing section for the current session.
|
|
17
|
+
*
|
|
18
|
+
* Emitted whenever mcpi-ext loads, including with zero MCP servers connected —
|
|
19
|
+
* an agent still needs to know that exact computation and the shell are on the
|
|
20
|
+
* table, and why the MCP-backed facilities are not.
|
|
21
|
+
*
|
|
22
|
+
* Deterministic: equal state always produces byte-identical output.
|
|
23
|
+
*/
|
|
24
|
+
export declare function formatExecutionRouting(state: ExecutionRoutingState): string;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { buildExecutionFacilities, } from "./facilities.js";
|
|
2
|
+
/**
|
|
3
|
+
* Tag for the routing section. Named `execution_routing` rather than anything
|
|
4
|
+
* that reads like an invocable surface, so the model never mistakes the section
|
|
5
|
+
* itself for something it can call.
|
|
6
|
+
*/
|
|
7
|
+
export const EXECUTION_ROUTING_TAG = "execution_routing";
|
|
8
|
+
const PREAMBLE = [
|
|
9
|
+
"This session exposes four execution facilities. They differ by the *shape* of the work, not by",
|
|
10
|
+
"rank: none of them is a default, none outranks another, and there is no sequence to try them in.",
|
|
11
|
+
"Match the facility to what the task actually requires.",
|
|
12
|
+
"",
|
|
13
|
+
"Every facility below states its own availability. Treat an unavailable facility as absent for",
|
|
14
|
+
"this session: do not invoke it, and never describe or summarise output it did not produce.",
|
|
15
|
+
];
|
|
16
|
+
const COMPOSITION = [
|
|
17
|
+
"### Composing facilities",
|
|
18
|
+
"",
|
|
19
|
+
"One task may need more than one facility. tool-cli and bash compose especially closely: tool-cli",
|
|
20
|
+
"*is* a program you run with the bash tool, so fetching MCP data and then filtering, joining, or",
|
|
21
|
+
"writing it to disk with ordinary shell programs is a single bash command rather than two rival",
|
|
22
|
+
"approaches. Code mode composes by handing you an exact value you then act on elsewhere, and a",
|
|
23
|
+
"skill composes by telling you which tools its workflow expects you to use.",
|
|
24
|
+
];
|
|
25
|
+
function formatFacility(facility) {
|
|
26
|
+
const lines = [`### ${facility.title}`, "", facility.useWhen, "", "Provides:"];
|
|
27
|
+
for (const item of facility.provides) {
|
|
28
|
+
lines.push(`- ${item}`);
|
|
29
|
+
}
|
|
30
|
+
lines.push("", "Does not provide:");
|
|
31
|
+
for (const item of facility.doesNotProvide) {
|
|
32
|
+
lines.push(`- ${item}`);
|
|
33
|
+
}
|
|
34
|
+
lines.push("", `Availability: ${facility.availability.state} — ${facility.availability.detail}`);
|
|
35
|
+
return lines;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Render an already-built facility list.
|
|
39
|
+
*
|
|
40
|
+
* Kept separate from {@link formatExecutionRouting} so the future host seam and
|
|
41
|
+
* the prompt fallback can render byte-identical text from one descriptor list.
|
|
42
|
+
*/
|
|
43
|
+
export function formatExecutionFacilities(facilities) {
|
|
44
|
+
const lines = ["", "", `<${EXECUTION_ROUTING_TAG}>`, "## Execution routing", ""];
|
|
45
|
+
lines.push(...PREAMBLE, "");
|
|
46
|
+
for (const facility of facilities) {
|
|
47
|
+
lines.push(...formatFacility(facility), "");
|
|
48
|
+
}
|
|
49
|
+
lines.push(...COMPOSITION);
|
|
50
|
+
lines.push(`</${EXECUTION_ROUTING_TAG}>`);
|
|
51
|
+
return lines.join("\n");
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Render the execution routing section for the current session.
|
|
55
|
+
*
|
|
56
|
+
* Emitted whenever mcpi-ext loads, including with zero MCP servers connected —
|
|
57
|
+
* an agent still needs to know that exact computation and the shell are on the
|
|
58
|
+
* table, and why the MCP-backed facilities are not.
|
|
59
|
+
*
|
|
60
|
+
* Deterministic: equal state always produces byte-identical output.
|
|
61
|
+
*/
|
|
62
|
+
export function formatExecutionRouting(state) {
|
|
63
|
+
return formatExecutionFacilities(buildExecutionFacilities(state));
|
|
64
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { buildExecutionFacilities, FACILITY_ORDER, type AvailabilityState, type BashState, type CodeModeState, type ExecutionFacility, type ExecutionRoutingState, type FacilityAvailability, type FacilityId, type SkillsState, type ToolCliState, } from "./facilities.js";
|
|
2
|
+
export { EXECUTION_ROUTING_TAG, formatExecutionFacilities, formatExecutionRouting, } from "./format.js";
|
|
3
|
+
export { publishExecutionFacilities, supportsExecutionFacilityRegistration, type ExecutionFacilityRegistrar, } from "./seam.js";
|
|
4
|
+
export { detectToolCliTripwires, type AssistantTurn, type ObservedToolCall, type TripwireCode, type TripwireFinding, type TripwireOptions, } from "./tripwire.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { buildExecutionFacilities, FACILITY_ORDER, } from "./facilities.js";
|
|
2
|
+
export { EXECUTION_ROUTING_TAG, formatExecutionFacilities, formatExecutionRouting, } from "./format.js";
|
|
3
|
+
export { publishExecutionFacilities, supportsExecutionFacilityRegistration, } from "./seam.js";
|
|
4
|
+
export { detectToolCliTripwires, } from "./tripwire.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ExecutionFacility } from "./facilities.js";
|
|
2
|
+
/**
|
|
3
|
+
* Narrow feature-detection seam for a future mcpi core capability.
|
|
4
|
+
*
|
|
5
|
+
* If mcpi core ever grows `registerExecutionFacility`, the host owns rendering
|
|
6
|
+
* and ordering of execution-facility guidance and mcpi-ext should hand over
|
|
7
|
+
* structured descriptors instead of appending its own prompt section. Until
|
|
8
|
+
* then — and mcpi core does not expose this today — the fallback in
|
|
9
|
+
* `before_agent_start` renders the same descriptors itself.
|
|
10
|
+
*
|
|
11
|
+
* The two paths are mutually exclusive by construction, so a host that gains
|
|
12
|
+
* the API cannot end up with the section twice.
|
|
13
|
+
*
|
|
14
|
+
* This file deliberately contains no type assertions: the guard narrows with
|
|
15
|
+
* `in` plus a `typeof` check, so a host that merely happens to carry a
|
|
16
|
+
* non-function property of the same name is correctly rejected.
|
|
17
|
+
*/
|
|
18
|
+
export interface ExecutionFacilityRegistrar {
|
|
19
|
+
registerExecutionFacility(facility: ExecutionFacility): void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* True when the host implements the (not-yet-existing) registration API.
|
|
23
|
+
*
|
|
24
|
+
* Narrowing without a cast: `in` refines the object type, and the `typeof`
|
|
25
|
+
* check proves the member is callable before we treat it as such.
|
|
26
|
+
*/
|
|
27
|
+
export declare function supportsExecutionFacilityRegistration<T extends object>(host: T): host is T & ExecutionFacilityRegistrar;
|
|
28
|
+
/**
|
|
29
|
+
* Hand the facility descriptors to the host if it can take them.
|
|
30
|
+
*
|
|
31
|
+
* Returns `true` when the host consumed them — in which case the caller must
|
|
32
|
+
* not also emit the fallback prompt section. Returns `false` when the API is
|
|
33
|
+
* absent, which is the current state of every shipped mcpi core.
|
|
34
|
+
*/
|
|
35
|
+
export declare function publishExecutionFacilities(host: object, facilities: readonly ExecutionFacility[]): boolean;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True when the host implements the (not-yet-existing) registration API.
|
|
3
|
+
*
|
|
4
|
+
* Narrowing without a cast: `in` refines the object type, and the `typeof`
|
|
5
|
+
* check proves the member is callable before we treat it as such.
|
|
6
|
+
*/
|
|
7
|
+
export function supportsExecutionFacilityRegistration(host) {
|
|
8
|
+
if (!("registerExecutionFacility" in host)) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
return typeof host.registerExecutionFacility === "function";
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Hand the facility descriptors to the host if it can take them.
|
|
15
|
+
*
|
|
16
|
+
* Returns `true` when the host consumed them — in which case the caller must
|
|
17
|
+
* not also emit the fallback prompt section. Returns `false` when the API is
|
|
18
|
+
* absent, which is the current state of every shipped mcpi core.
|
|
19
|
+
*/
|
|
20
|
+
export function publishExecutionFacilities(host, facilities) {
|
|
21
|
+
if (!supportsExecutionFacilityRegistration(host)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
for (const facility of facilities) {
|
|
25
|
+
host.registerExecutionFacility(facility);
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anti-hallucination tripwire for tool-cli.
|
|
3
|
+
*
|
|
4
|
+
* tool-cli is a program, not a callable tool. The failure mode this guards
|
|
5
|
+
* against is an agent that "invokes" it by writing markup or a shell transcript
|
|
6
|
+
* into its own message and then narrates output that no process ever produced.
|
|
7
|
+
* Both halves are wrong, and both are cheap to detect: a real invocation always
|
|
8
|
+
* leaves a bash tool call whose command mentions `tool-cli`.
|
|
9
|
+
*
|
|
10
|
+
* The detector is deliberately strict. Mentioning the `<tool_cli_usage_docs>`
|
|
11
|
+
* tag in prose without a real call trips it, because that is exactly the
|
|
12
|
+
* confusion — treating the documentation tag as an invocation syntax — the
|
|
13
|
+
* routing prompt is written to prevent.
|
|
14
|
+
*/
|
|
15
|
+
/** A tool call as observed on an assistant turn. */
|
|
16
|
+
export interface ObservedToolCall {
|
|
17
|
+
name: string;
|
|
18
|
+
arguments?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
/** The assistant output under inspection. */
|
|
21
|
+
export interface AssistantTurn {
|
|
22
|
+
text: string;
|
|
23
|
+
toolCalls?: readonly ObservedToolCall[];
|
|
24
|
+
}
|
|
25
|
+
export type TripwireCode = "tool_cli_pseudo_call" | "tool_cli_narrated_without_bash";
|
|
26
|
+
export interface TripwireFinding {
|
|
27
|
+
code: TripwireCode;
|
|
28
|
+
/** The offending excerpt, so a failure message can point at it. */
|
|
29
|
+
evidence: string;
|
|
30
|
+
message: string;
|
|
31
|
+
}
|
|
32
|
+
export interface TripwireOptions {
|
|
33
|
+
/** Host tool names that actually execute shell commands. */
|
|
34
|
+
bashToolNames?: readonly string[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Inspect one assistant turn for fabricated tool-cli usage.
|
|
38
|
+
*
|
|
39
|
+
* Returns every finding rather than the first, so a regression test can assert
|
|
40
|
+
* on the specific failure mode instead of just "something was wrong".
|
|
41
|
+
*/
|
|
42
|
+
export declare function detectToolCliTripwires(turn: AssistantTurn, options?: TripwireOptions): TripwireFinding[];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anti-hallucination tripwire for tool-cli.
|
|
3
|
+
*
|
|
4
|
+
* tool-cli is a program, not a callable tool. The failure mode this guards
|
|
5
|
+
* against is an agent that "invokes" it by writing markup or a shell transcript
|
|
6
|
+
* into its own message and then narrates output that no process ever produced.
|
|
7
|
+
* Both halves are wrong, and both are cheap to detect: a real invocation always
|
|
8
|
+
* leaves a bash tool call whose command mentions `tool-cli`.
|
|
9
|
+
*
|
|
10
|
+
* The detector is deliberately strict. Mentioning the `<tool_cli_usage_docs>`
|
|
11
|
+
* tag in prose without a real call trips it, because that is exactly the
|
|
12
|
+
* confusion — treating the documentation tag as an invocation syntax — the
|
|
13
|
+
* routing prompt is written to prevent.
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_BASH_TOOL_NAMES = ["bash"];
|
|
16
|
+
/** `<tool_cli`, `</tool-cli`, `< tool_cli_usage_docs`, and friends. */
|
|
17
|
+
const PSEUDO_CALL_PATTERN = /<\s*\/?\s*tool[_-]cli[\w-]*/i;
|
|
18
|
+
/**
|
|
19
|
+
* Lines that read like a shell transcript rather than prose.
|
|
20
|
+
*
|
|
21
|
+
* Two shapes qualify, and both are deliberately narrow so ordinary sentences
|
|
22
|
+
* such as "tool-cli is available this session." are not mistaken for commands:
|
|
23
|
+
* a line carrying an explicit shell prompt, or a line that opens with the
|
|
24
|
+
* program and goes on to use recognisably CLI syntax — a flag, a quoted
|
|
25
|
+
* argument, or a JSON brace.
|
|
26
|
+
*/
|
|
27
|
+
const NARRATED_INVOCATION_PATTERNS = [
|
|
28
|
+
/^[ \t]*[$>][ \t]*tool-cli[ \t]+\S/m,
|
|
29
|
+
/^[ \t]*tool-cli[ \t]+\S.*(?:--?[A-Za-z]|['"{}])/m,
|
|
30
|
+
];
|
|
31
|
+
function commandOf(call) {
|
|
32
|
+
const command = call.arguments?.command;
|
|
33
|
+
return typeof command === "string" ? command : "";
|
|
34
|
+
}
|
|
35
|
+
function hasRealToolCliCall(turn, bashToolNames) {
|
|
36
|
+
return (turn.toolCalls ?? []).some((call) => bashToolNames.includes(call.name) && commandOf(call).includes("tool-cli"));
|
|
37
|
+
}
|
|
38
|
+
function excerpt(text, pattern) {
|
|
39
|
+
const match = pattern.exec(text);
|
|
40
|
+
if (!match) {
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
const start = Math.max(0, match.index - 20);
|
|
44
|
+
return text.slice(start, match.index + match[0].length + 60).trim();
|
|
45
|
+
}
|
|
46
|
+
/** The first narration pattern that matches, or undefined when none do. */
|
|
47
|
+
function narratedInvocation(text) {
|
|
48
|
+
return NARRATED_INVOCATION_PATTERNS.find((pattern) => pattern.test(text));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Inspect one assistant turn for fabricated tool-cli usage.
|
|
52
|
+
*
|
|
53
|
+
* Returns every finding rather than the first, so a regression test can assert
|
|
54
|
+
* on the specific failure mode instead of just "something was wrong".
|
|
55
|
+
*/
|
|
56
|
+
export function detectToolCliTripwires(turn, options = {}) {
|
|
57
|
+
const bashToolNames = options.bashToolNames ?? DEFAULT_BASH_TOOL_NAMES;
|
|
58
|
+
// A genuine invocation clears both tripwires: the agent really did run it, so
|
|
59
|
+
// whatever it wrote about the command is grounded in a real result.
|
|
60
|
+
if (hasRealToolCliCall(turn, bashToolNames)) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
const findings = [];
|
|
64
|
+
if (PSEUDO_CALL_PATTERN.test(turn.text)) {
|
|
65
|
+
findings.push({
|
|
66
|
+
code: "tool_cli_pseudo_call",
|
|
67
|
+
evidence: excerpt(turn.text, PSEUDO_CALL_PATTERN),
|
|
68
|
+
message: "Assistant text contains tool-cli markup but no bash tool call ran tool-cli. tool-cli is invoked by calling the bash tool with a `tool-cli ...` command, never by emitting XML or text that imitates a call.",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const narrated = narratedInvocation(turn.text);
|
|
72
|
+
if (narrated) {
|
|
73
|
+
findings.push({
|
|
74
|
+
code: "tool_cli_narrated_without_bash",
|
|
75
|
+
evidence: excerpt(turn.text, narrated),
|
|
76
|
+
message: "Assistant text narrates a tool-cli invocation but no bash tool call ran tool-cli, so any output shown was fabricated rather than observed.",
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return findings;
|
|
80
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { McpPolicy } from "../mcp/policy.js";
|
|
2
|
+
import type { McpSkillMetadata } from "./skill-registry.js";
|
|
3
|
+
/**
|
|
4
|
+
* Discover skills from a connected MCP server by reading its resources.
|
|
5
|
+
*
|
|
6
|
+
* Looks for resources with `skill://` URIs ending in `/SKILL.md`,
|
|
7
|
+
* reads each one, and parses YAML frontmatter for skill metadata.
|
|
8
|
+
*
|
|
9
|
+
* All resource I/O goes through the shared policy boundary, so a server can
|
|
10
|
+
* only ever surface its own skill resources.
|
|
11
|
+
*/
|
|
12
|
+
export declare function discoverSkillsFromServer(policy: McpPolicy, serverName: string, log?: (msg: string) => void, signal?: AbortSignal): Promise<McpSkillMetadata[]>;
|