@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
package/dist/skills/discover.js
CHANGED
|
@@ -4,24 +4,30 @@ import { parseFrontmatter } from "@sammorrowdrums/mcpi";
|
|
|
4
4
|
*
|
|
5
5
|
* Looks for resources with `skill://` URIs ending in `/SKILL.md`,
|
|
6
6
|
* reads each one, and parses YAML frontmatter for skill metadata.
|
|
7
|
+
*
|
|
8
|
+
* All resource I/O goes through the shared policy boundary, so a server can
|
|
9
|
+
* only ever surface its own skill resources.
|
|
7
10
|
*/
|
|
8
|
-
export async function discoverSkillsFromServer(
|
|
11
|
+
export async function discoverSkillsFromServer(policy, serverName, log = console.error, signal) {
|
|
9
12
|
const skills = [];
|
|
10
|
-
let
|
|
13
|
+
let skillResources;
|
|
11
14
|
try {
|
|
12
|
-
|
|
13
|
-
resources = result.resources;
|
|
15
|
+
skillResources = await policy.listSkillResources(serverName, signal);
|
|
14
16
|
}
|
|
15
17
|
catch {
|
|
16
18
|
log(`[skills] Server "${serverName}" does not support resources/list, skipping skill discovery`);
|
|
17
19
|
return skills;
|
|
18
20
|
}
|
|
19
|
-
const skillResources = resources.filter((r) => r.uri.startsWith("skill://") && r.uri.endsWith("/SKILL.md"));
|
|
20
21
|
if (skillResources.length === 0)
|
|
21
22
|
return skills;
|
|
22
23
|
for (const resource of skillResources) {
|
|
23
24
|
try {
|
|
24
|
-
const result = await
|
|
25
|
+
const result = await policy.readResource({
|
|
26
|
+
source: "skill-discovery",
|
|
27
|
+
serverName,
|
|
28
|
+
uri: resource.uri,
|
|
29
|
+
...(signal ? { signal } : {}),
|
|
30
|
+
});
|
|
25
31
|
const textContent = result.contents.find((c) => "text" in c);
|
|
26
32
|
if (!textContent) {
|
|
27
33
|
log(`[skills] Skill resource ${resource.uri} returned no text content, skipping`);
|
|
@@ -31,7 +37,7 @@ export async function discoverSkillsFromServer(client, serverName, log = console
|
|
|
31
37
|
const fm = parsed.frontmatter;
|
|
32
38
|
const name = fm.name ?? resource.name;
|
|
33
39
|
const description = fm.description ?? "";
|
|
34
|
-
const allowedTools = parseAllowedTools(fm
|
|
40
|
+
const allowedTools = parseAllowedTools(fm);
|
|
35
41
|
if (!name) {
|
|
36
42
|
log(`[skills] Skill at ${resource.uri} has no name, skipping`);
|
|
37
43
|
continue;
|
|
@@ -49,12 +55,31 @@ export async function discoverSkillsFromServer(client, serverName, log = console
|
|
|
49
55
|
log(`[skills] Failed to read skill ${resource.uri}: ${err.message}`);
|
|
50
56
|
}
|
|
51
57
|
}
|
|
52
|
-
return skills;
|
|
58
|
+
return skills.sort((left, right) => compareStrings(left.name, right.name));
|
|
59
|
+
}
|
|
60
|
+
function compareStrings(left, right) {
|
|
61
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
53
62
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
/**
|
|
64
|
+
* Parse tool names from frontmatter, supporting both formats:
|
|
65
|
+
* - Current: `allowed-tools: [tool_a, tool_b]` (YAML array)
|
|
66
|
+
* - Proposed spec: `metadata.io.modelcontextprotocol/tools: "tool_a tool_b"` (space-separated)
|
|
67
|
+
*
|
|
68
|
+
* Prefers the proposed spec format when both are present.
|
|
69
|
+
*/
|
|
70
|
+
function parseAllowedTools(fm) {
|
|
71
|
+
// Proposed spec format: metadata.io.modelcontextprotocol/tools (space-separated string)
|
|
72
|
+
const metadata = fm.metadata;
|
|
73
|
+
if (metadata) {
|
|
74
|
+
const specTools = metadata["io.modelcontextprotocol/tools"];
|
|
75
|
+
if (typeof specTools === "string" && specTools.trim().length > 0) {
|
|
76
|
+
return specTools.trim().split(/\s+/);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Current format: allowed-tools (YAML array)
|
|
80
|
+
const legacy = fm["allowed-tools"];
|
|
81
|
+
if (Array.isArray(legacy)) {
|
|
82
|
+
return legacy.filter((v) => typeof v === "string");
|
|
57
83
|
}
|
|
58
84
|
return [];
|
|
59
85
|
}
|
|
60
|
-
//# sourceMappingURL=discover.js.map
|
package/dist/skills/format.d.ts
CHANGED
|
@@ -5,6 +5,9 @@ import type { McpSkillMetadata } from "./skill-registry.js";
|
|
|
5
5
|
* Produces XML matching Pi's native `formatSkillsForPrompt` structure,
|
|
6
6
|
* but references the `load_skill` tool instead of `read` and uses
|
|
7
7
|
* `mcp:<serverName>` as the location.
|
|
8
|
+
*
|
|
9
|
+
* This is the catalogue of what exists. Which kind of task suits a skill at all
|
|
10
|
+
* is decided by the `<execution_routing>` section, which reports skill
|
|
11
|
+
* availability whether or not any were discovered.
|
|
8
12
|
*/
|
|
9
13
|
export declare function formatMcpSkillsForPrompt(skills: McpSkillMetadata[]): string;
|
|
10
|
-
//# sourceMappingURL=format.d.ts.map
|
package/dist/skills/format.js
CHANGED
|
@@ -12,6 +12,10 @@ function escapeXml(str) {
|
|
|
12
12
|
* Produces XML matching Pi's native `formatSkillsForPrompt` structure,
|
|
13
13
|
* but references the `load_skill` tool instead of `read` and uses
|
|
14
14
|
* `mcp:<serverName>` as the location.
|
|
15
|
+
*
|
|
16
|
+
* This is the catalogue of what exists. Which kind of task suits a skill at all
|
|
17
|
+
* is decided by the `<execution_routing>` section, which reports skill
|
|
18
|
+
* availability whether or not any were discovered.
|
|
15
19
|
*/
|
|
16
20
|
export function formatMcpSkillsForPrompt(skills) {
|
|
17
21
|
if (skills.length === 0)
|
|
@@ -19,9 +23,10 @@ export function formatMcpSkillsForPrompt(skills) {
|
|
|
19
23
|
const lines = [
|
|
20
24
|
"",
|
|
21
25
|
"",
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
26
|
+
"Use when a task matches one of the domain workflows these MCP skills document.",
|
|
27
|
+
"Call load_skill with the skill's name to read its instructions before working through it.",
|
|
28
|
+
"MCP tools are deferred — registered for dispatch but not described in this prompt. A skill's",
|
|
29
|
+
"declared tools are enabled only after you approve its grant; declining leaves them locked.",
|
|
25
30
|
"",
|
|
26
31
|
"<available_mcp_skills>",
|
|
27
32
|
];
|
|
@@ -35,4 +40,3 @@ export function formatMcpSkillsForPrompt(skills) {
|
|
|
35
40
|
lines.push("</available_mcp_skills>");
|
|
36
41
|
return lines.join("\n");
|
|
37
42
|
}
|
|
38
|
-
//# sourceMappingURL=format.js.map
|
package/dist/skills/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export { SkillRegistry, type McpSkillMetadata } from "./skill-registry.js";
|
|
1
|
+
export { SkillRegistry, type McpSkillMetadata, type SkillNameCollision } from "./skill-registry.js";
|
|
2
2
|
export { formatMcpSkillsForPrompt } from "./format.js";
|
|
3
3
|
export { createLoadSkillTool, type LoadSkillDeps, type LoadSkillDetails, } from "./load-skill-tool.js";
|
|
4
4
|
export { discoverSkillsFromServer } from "./discover.js";
|
|
5
5
|
export { registerMcpToolProxies } from "./mcp-tool-proxy.js";
|
|
6
|
-
|
|
6
|
+
export { SKILLS_EXTENSION_NAME, SKILLS_EXTENSION_REVISION, SKILLS_EXTENSION_STATUS, SkillsExtensionClient, describeNegotiation, discoverSkillsViaExtension, loadSkillDocument, readSkillResource, skillsExtensionDiagnostic, type Sep2640DiscoveryResult, type SkillEntry, type SkillsListing, } from "./sep2640/index.js";
|
package/dist/skills/index.js
CHANGED
|
@@ -3,4 +3,4 @@ export { formatMcpSkillsForPrompt } from "./format.js";
|
|
|
3
3
|
export { createLoadSkillTool, } from "./load-skill-tool.js";
|
|
4
4
|
export { discoverSkillsFromServer } from "./discover.js";
|
|
5
5
|
export { registerMcpToolProxies } from "./mcp-tool-proxy.js";
|
|
6
|
-
|
|
6
|
+
export { SKILLS_EXTENSION_NAME, SKILLS_EXTENSION_REVISION, SKILLS_EXTENSION_STATUS, SkillsExtensionClient, describeNegotiation, discoverSkillsViaExtension, loadSkillDocument, readSkillResource, skillsExtensionDiagnostic, } from "./sep2640/index.js";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentToolResult, ExtensionContext } from "@sammorrowdrums/mcpi";
|
|
2
2
|
import { Type, type Static } from "typebox";
|
|
3
|
-
import type {
|
|
3
|
+
import type { McpPolicy } from "../mcp/policy.js";
|
|
4
|
+
import type { SkillsExtensionClient } from "./sep2640/client.js";
|
|
4
5
|
import type { SkillRegistry } from "./skill-registry.js";
|
|
5
6
|
declare const LoadSkillParams: Type.TObject<{
|
|
6
7
|
name: Type.TString;
|
|
@@ -8,23 +9,39 @@ declare const LoadSkillParams: Type.TObject<{
|
|
|
8
9
|
type LoadSkillInput = Static<typeof LoadSkillParams>;
|
|
9
10
|
export interface LoadSkillDeps {
|
|
10
11
|
registry: SkillRegistry;
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
policy: McpPolicy;
|
|
13
|
+
/**
|
|
14
|
+
* Client for the draft skills extension.
|
|
15
|
+
*
|
|
16
|
+
* Required to load a skill discovered over SEP-2640: that contract only holds
|
|
17
|
+
* if the digests are re-fetched at load time, so a skill with no client to
|
|
18
|
+
* ask is refused rather than loaded unverified.
|
|
19
|
+
*/
|
|
20
|
+
skillsClient?: SkillsExtensionClient;
|
|
13
21
|
}
|
|
14
22
|
export interface LoadSkillDetails {
|
|
15
23
|
skillName: string;
|
|
16
24
|
serverName?: string;
|
|
17
25
|
activatedTools?: string[];
|
|
18
26
|
error?: string;
|
|
27
|
+
/** True when the content was verified against SEP-2640 digests. */
|
|
28
|
+
verified?: boolean;
|
|
29
|
+
/** True when the server's resource set changed since discovery. */
|
|
30
|
+
resourceSetRotated?: boolean;
|
|
19
31
|
}
|
|
20
32
|
/**
|
|
21
33
|
* Create the load_skill tool definition.
|
|
22
34
|
*
|
|
23
35
|
* When the model calls this tool, it:
|
|
24
36
|
* 1. Looks up the skill in the registry
|
|
25
|
-
* 2. Reads the full SKILL.md content
|
|
26
|
-
* 3.
|
|
37
|
+
* 2. Reads the full SKILL.md content through the shared policy boundary
|
|
38
|
+
* 3. Asks the user to approve the skill's `allowed-tools` grant
|
|
39
|
+
* 4. Returns the SKILL.md body (the skill names its tools, and the model
|
|
27
40
|
* already has their schemas from the deferred tools array)
|
|
41
|
+
*
|
|
42
|
+
* The grant is requested before the body is returned, so a server cannot use
|
|
43
|
+
* skill instructions to influence a pending authorization decision. A declined
|
|
44
|
+
* or unavailable approval leaves every gated tool locked.
|
|
28
45
|
*/
|
|
29
46
|
export declare function createLoadSkillTool(deps: LoadSkillDeps): {
|
|
30
47
|
name: string;
|
|
@@ -34,7 +51,6 @@ export declare function createLoadSkillTool(deps: LoadSkillDeps): {
|
|
|
34
51
|
parameters: Type.TObject<{
|
|
35
52
|
name: Type.TString;
|
|
36
53
|
}>;
|
|
37
|
-
execute(_toolCallId: string, params: LoadSkillInput,
|
|
54
|
+
execute(_toolCallId: string, params: LoadSkillInput, signal: AbortSignal | undefined, _onUpdate: undefined, _ctx: ExtensionContext): Promise<AgentToolResult<LoadSkillDetails>>;
|
|
38
55
|
};
|
|
39
56
|
export {};
|
|
40
|
-
//# sourceMappingURL=load-skill-tool.d.ts.map
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { stripFrontmatter } from "@sammorrowdrums/mcpi";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
+
import { loadSkillDocument, SkillFetchBudget } from "./sep2640/load.js";
|
|
4
|
+
import { resourceSetFingerprint } from "./sep2640/protocol.js";
|
|
3
5
|
const LoadSkillParams = Type.Object({
|
|
4
6
|
name: Type.String({ description: "Name of the MCP skill to load" }),
|
|
5
7
|
});
|
|
@@ -8,19 +10,24 @@ const LoadSkillParams = Type.Object({
|
|
|
8
10
|
*
|
|
9
11
|
* When the model calls this tool, it:
|
|
10
12
|
* 1. Looks up the skill in the registry
|
|
11
|
-
* 2. Reads the full SKILL.md content
|
|
12
|
-
* 3.
|
|
13
|
+
* 2. Reads the full SKILL.md content through the shared policy boundary
|
|
14
|
+
* 3. Asks the user to approve the skill's `allowed-tools` grant
|
|
15
|
+
* 4. Returns the SKILL.md body (the skill names its tools, and the model
|
|
13
16
|
* already has their schemas from the deferred tools array)
|
|
17
|
+
*
|
|
18
|
+
* The grant is requested before the body is returned, so a server cannot use
|
|
19
|
+
* skill instructions to influence a pending authorization decision. A declined
|
|
20
|
+
* or unavailable approval leaves every gated tool locked.
|
|
14
21
|
*/
|
|
15
22
|
export function createLoadSkillTool(deps) {
|
|
16
|
-
const { registry,
|
|
23
|
+
const { registry, policy, skillsClient } = deps;
|
|
17
24
|
return {
|
|
18
25
|
name: "load_skill",
|
|
19
26
|
label: "Load Skill",
|
|
20
|
-
description: "
|
|
21
|
-
promptSnippet: "
|
|
27
|
+
description: "Use when a task matches an MCP skill's documented workflow and you need its instructions. Returns the skill body and requests approval to enable the tools it declares; the tools stay locked unless that grant is approved.",
|
|
28
|
+
promptSnippet: "Use when a task matches an MCP skill's workflow: returns its instructions and, once you approve the grant, enables the tools it declares.",
|
|
22
29
|
parameters: LoadSkillParams,
|
|
23
|
-
async execute(_toolCallId, params,
|
|
30
|
+
async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
|
|
24
31
|
const skill = registry.get(params.name);
|
|
25
32
|
if (!skill) {
|
|
26
33
|
const available = registry
|
|
@@ -37,34 +44,69 @@ export function createLoadSkillTool(deps) {
|
|
|
37
44
|
details: { skillName: params.name, error: "not_found" },
|
|
38
45
|
};
|
|
39
46
|
}
|
|
40
|
-
const
|
|
41
|
-
if (!
|
|
47
|
+
const verifiable = skill.origin === "sep2640";
|
|
48
|
+
if (verifiable && !skillsClient) {
|
|
42
49
|
return {
|
|
43
50
|
content: [
|
|
44
51
|
{
|
|
45
52
|
type: "text",
|
|
46
|
-
text: `
|
|
53
|
+
text: `Skill "${params.name}" was discovered over the draft skills extension, but no extension client is available to verify it. Refusing to load unverified content.`,
|
|
47
54
|
},
|
|
48
55
|
],
|
|
49
|
-
details: {
|
|
56
|
+
details: {
|
|
57
|
+
skillName: params.name,
|
|
58
|
+
serverName: skill.serverName,
|
|
59
|
+
error: "verification_unavailable",
|
|
60
|
+
verified: false,
|
|
61
|
+
},
|
|
50
62
|
};
|
|
51
63
|
}
|
|
52
64
|
let body;
|
|
65
|
+
let entry;
|
|
66
|
+
let rotated = false;
|
|
53
67
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
if (verifiable && skillsClient) {
|
|
69
|
+
// Re-fetch the entry so verification uses the digests the server is
|
|
70
|
+
// publishing now, not the ones it published at discovery.
|
|
71
|
+
entry = await skillsClient.getSkill(skill.serverName, skill.uri, signal);
|
|
72
|
+
const fingerprint = resourceSetFingerprint(entry);
|
|
73
|
+
rotated =
|
|
74
|
+
skill.contentFingerprint !== undefined && skill.contentFingerprint !== fingerprint;
|
|
75
|
+
policy.registerSkillResources(skill.serverName, entry.uri, entry.resources === "dynamic" ? [] : entry.resources.map((ref) => ref.uri));
|
|
76
|
+
const document = await loadSkillDocument({
|
|
77
|
+
policy,
|
|
78
|
+
entry,
|
|
79
|
+
serverName: skill.serverName,
|
|
80
|
+
budget: new SkillFetchBudget(),
|
|
81
|
+
...(signal ? { signal } : {}),
|
|
82
|
+
});
|
|
83
|
+
body = stripFrontmatter(document.text);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
const result = await policy.readResource({
|
|
87
|
+
source: "skill-load",
|
|
88
|
+
serverName: skill.serverName,
|
|
89
|
+
uri: skill.uri,
|
|
90
|
+
...(signal ? { signal } : {}),
|
|
91
|
+
});
|
|
92
|
+
const textContent = result.contents.find((c) => "text" in c);
|
|
93
|
+
if (!textContent) {
|
|
94
|
+
return {
|
|
95
|
+
content: [
|
|
96
|
+
{
|
|
97
|
+
type: "text",
|
|
98
|
+
text: `Skill "${params.name}" returned no text content.`,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
details: {
|
|
102
|
+
skillName: params.name,
|
|
103
|
+
serverName: skill.serverName,
|
|
104
|
+
error: "no_content",
|
|
62
105
|
},
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
body = stripFrontmatter(textContent.text);
|
|
66
109
|
}
|
|
67
|
-
body = stripFrontmatter(textContent.text);
|
|
68
110
|
}
|
|
69
111
|
catch (err) {
|
|
70
112
|
return {
|
|
@@ -78,27 +120,64 @@ export function createLoadSkillTool(deps) {
|
|
|
78
120
|
skillName: params.name,
|
|
79
121
|
serverName: skill.serverName,
|
|
80
122
|
error: err.message,
|
|
123
|
+
verified: false,
|
|
81
124
|
},
|
|
82
125
|
};
|
|
83
126
|
}
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
127
|
+
// Approval is bound to the resource set the server just published. A
|
|
128
|
+
// rotated set produces a different grant key, so a previously approved
|
|
129
|
+
// skill is re-prompted instead of inheriting the old answer.
|
|
130
|
+
const grantSubject = entry ? withFreshContent(skill, entry) : skill;
|
|
131
|
+
const grant = await policy.activateSkillGrant(grantSubject, signal);
|
|
132
|
+
if (grant.status === "granted" || grant.status === "reused") {
|
|
133
|
+
return {
|
|
134
|
+
content: [
|
|
135
|
+
{
|
|
136
|
+
type: "text",
|
|
137
|
+
text: body,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
details: {
|
|
141
|
+
skillName: params.name,
|
|
142
|
+
serverName: skill.serverName,
|
|
143
|
+
activatedTools: [...grant.activatedTools],
|
|
144
|
+
verified: verifiable,
|
|
145
|
+
resourceSetRotated: rotated,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
87
148
|
}
|
|
88
149
|
return {
|
|
89
|
-
content: [
|
|
90
|
-
{
|
|
91
|
-
type: "text",
|
|
92
|
-
text: body,
|
|
93
|
-
},
|
|
94
|
-
],
|
|
150
|
+
content: [{ type: "text", text: grant.message }],
|
|
95
151
|
details: {
|
|
96
152
|
skillName: params.name,
|
|
97
153
|
serverName: skill.serverName,
|
|
98
|
-
activatedTools:
|
|
154
|
+
activatedTools: [],
|
|
155
|
+
error: grant.status === "declined" ? "approval_declined" : "approval_unavailable",
|
|
156
|
+
verified: verifiable,
|
|
157
|
+
resourceSetRotated: rotated,
|
|
99
158
|
},
|
|
100
159
|
};
|
|
101
160
|
},
|
|
102
161
|
};
|
|
103
162
|
}
|
|
104
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Rebuild skill metadata from the entry the server just served.
|
|
165
|
+
*
|
|
166
|
+
* Both the gated tool names and the content fingerprint come from the verified
|
|
167
|
+
* entry rather than the discovery-time copy, so an `allowed-tools` list that
|
|
168
|
+
* grew since discovery cannot ride in on an approval the user gave for a
|
|
169
|
+
* smaller one.
|
|
170
|
+
*/
|
|
171
|
+
function withFreshContent(skill, entry) {
|
|
172
|
+
const declared = entry.frontmatter["allowed-tools"];
|
|
173
|
+
const allowedTools = Array.isArray(declared)
|
|
174
|
+
? declared.filter((value) => typeof value === "string")
|
|
175
|
+
: typeof declared === "string" && declared.trim().length > 0
|
|
176
|
+
? declared.trim().split(/\s+/)
|
|
177
|
+
: [];
|
|
178
|
+
return {
|
|
179
|
+
...skill,
|
|
180
|
+
allowedTools,
|
|
181
|
+
contentFingerprint: resourceSetFingerprint(entry),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@sammorrowdrums/mcpi";
|
|
2
|
-
import type { McpClientManager } from "../mcp/
|
|
2
|
+
import type { McpClientManager } from "../mcp/client-manager.js";
|
|
3
|
+
import type { McpPolicy } from "../mcp/policy.js";
|
|
3
4
|
/**
|
|
4
|
-
* Register MCP tools as
|
|
5
|
-
*
|
|
6
|
-
* Each registered tool forwards calls to the MCP server via `client.callTool()`.
|
|
7
|
-
* Uses `Type.Unsafe()` to pass the MCP tool's original JSON Schema through
|
|
8
|
-
* to Pi, preserving property names and types for the model.
|
|
5
|
+
* Register MCP tools as deferred mcpi proxies backed by the shared policy boundary.
|
|
9
6
|
*/
|
|
10
|
-
export declare function registerMcpToolProxies(toolNames: string[],
|
|
11
|
-
//# sourceMappingURL=mcp-tool-proxy.d.ts.map
|
|
7
|
+
export declare function registerMcpToolProxies(toolNames: string[], manager: McpClientManager, policy: McpPolicy, pi: ExtensionAPI): string[];
|
|
@@ -1,116 +1,41 @@
|
|
|
1
|
-
import { writeFileSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
1
|
import { Type } from "typebox";
|
|
5
|
-
|
|
6
|
-
const LARGE_OUTPUT_THRESHOLD = 10_000;
|
|
2
|
+
import { renderTerminalCallToolResult } from "../mcp/call-tool-result.js";
|
|
7
3
|
/**
|
|
8
|
-
* Register MCP tools as
|
|
9
|
-
*
|
|
10
|
-
* Each registered tool forwards calls to the MCP server via `client.callTool()`.
|
|
11
|
-
* Uses `Type.Unsafe()` to pass the MCP tool's original JSON Schema through
|
|
12
|
-
* to Pi, preserving property names and types for the model.
|
|
4
|
+
* Register MCP tools as deferred mcpi proxies backed by the shared policy boundary.
|
|
13
5
|
*/
|
|
14
|
-
export function registerMcpToolProxies(toolNames,
|
|
6
|
+
export function registerMcpToolProxies(toolNames, manager, policy, pi) {
|
|
15
7
|
const registered = [];
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
const existingTools = new Set(pi.getAllTools().map((t) => t.name));
|
|
8
|
+
const existingTools = new Set(pi.getAllTools().map((tool) => tool.name));
|
|
9
|
+
const toolsByName = new Map(manager.getTools().map((tool) => [tool.name, tool]));
|
|
19
10
|
for (const name of toolNames) {
|
|
20
11
|
if (existingTools.has(name)) {
|
|
21
12
|
registered.push(name);
|
|
22
13
|
continue;
|
|
23
14
|
}
|
|
24
|
-
const
|
|
25
|
-
if (!
|
|
15
|
+
const tool = toolsByName.get(name);
|
|
16
|
+
if (!tool)
|
|
26
17
|
continue;
|
|
27
|
-
pi.registerTool(createMcpToolProxy(
|
|
18
|
+
pi.registerTool(createMcpToolProxy(policy, tool));
|
|
28
19
|
registered.push(name);
|
|
29
20
|
}
|
|
30
21
|
return registered;
|
|
31
22
|
}
|
|
32
|
-
function createMcpToolProxy(
|
|
33
|
-
// Pass through the MCP tool's JSON Schema directly via Type.Unsafe()
|
|
34
|
-
// This preserves the original property names and types for the model
|
|
35
|
-
const inputSchema = mcpTool.inputSchema;
|
|
36
|
-
const parameters = Type.Unsafe({
|
|
37
|
-
type: "object",
|
|
38
|
-
properties: inputSchema.properties ?? {},
|
|
39
|
-
required: inputSchema.required ?? [],
|
|
40
|
-
});
|
|
23
|
+
function createMcpToolProxy(policy, tool) {
|
|
41
24
|
return {
|
|
42
|
-
name:
|
|
43
|
-
label:
|
|
44
|
-
description:
|
|
25
|
+
name: tool.name,
|
|
26
|
+
label: tool.name,
|
|
27
|
+
description: tool.description ?? `MCP tool from ${tool.serverName}`,
|
|
45
28
|
deferred: true,
|
|
46
|
-
parameters,
|
|
47
|
-
async execute(_toolCallId, params,
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
],
|
|
57
|
-
details: {
|
|
58
|
-
serverName: mcpTool.serverName,
|
|
59
|
-
toolName: mcpTool.name,
|
|
60
|
-
error: "disconnected",
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
try {
|
|
65
|
-
const result = await client.callTool({
|
|
66
|
-
name: mcpTool.name,
|
|
67
|
-
arguments: params,
|
|
68
|
-
});
|
|
69
|
-
// Prefer structuredContent when available
|
|
70
|
-
let text;
|
|
71
|
-
if (result.structuredContent) {
|
|
72
|
-
text = JSON.stringify(result.structuredContent, null, 2);
|
|
73
|
-
}
|
|
74
|
-
else if (Array.isArray(result.content)) {
|
|
75
|
-
text = result.content
|
|
76
|
-
.map((c) => {
|
|
77
|
-
if (typeof c === "object" && c !== null && "text" in c) {
|
|
78
|
-
return String(c.text);
|
|
79
|
-
}
|
|
80
|
-
return JSON.stringify(c);
|
|
81
|
-
})
|
|
82
|
-
.join("\n");
|
|
83
|
-
}
|
|
84
|
-
else {
|
|
85
|
-
text = JSON.stringify(result);
|
|
86
|
-
}
|
|
87
|
-
// Write large outputs to tmp file to avoid bloating context
|
|
88
|
-
if (text.length > LARGE_OUTPUT_THRESHOLD) {
|
|
89
|
-
const tmpPath = join(tmpdir(), `mcp-${mcpTool.name}-${Date.now()}.json`);
|
|
90
|
-
writeFileSync(tmpPath, text, "utf-8");
|
|
91
|
-
text = `Output too large (${text.length} chars). Written to: ${tmpPath}`;
|
|
92
|
-
}
|
|
93
|
-
return {
|
|
94
|
-
content: [{ type: "text", text }],
|
|
95
|
-
details: { serverName: mcpTool.serverName, toolName: mcpTool.name },
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
catch (err) {
|
|
99
|
-
return {
|
|
100
|
-
content: [
|
|
101
|
-
{
|
|
102
|
-
type: "text",
|
|
103
|
-
text: `MCP tool "${mcpTool.name}" failed: ${err.message}`,
|
|
104
|
-
},
|
|
105
|
-
],
|
|
106
|
-
details: {
|
|
107
|
-
serverName: mcpTool.serverName,
|
|
108
|
-
toolName: mcpTool.name,
|
|
109
|
-
error: err.message,
|
|
110
|
-
},
|
|
111
|
-
};
|
|
112
|
-
}
|
|
29
|
+
parameters: Type.Unsafe(tool.inputSchema),
|
|
30
|
+
async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
|
|
31
|
+
const terminal = await policy.callTool({
|
|
32
|
+
source: "proxy",
|
|
33
|
+
serverName: tool.serverName,
|
|
34
|
+
toolName: tool.name,
|
|
35
|
+
args: params,
|
|
36
|
+
...(signal ? { signal } : {}),
|
|
37
|
+
});
|
|
38
|
+
return renderTerminalCallToolResult(terminal);
|
|
113
39
|
},
|
|
114
40
|
};
|
|
115
41
|
}
|
|
116
|
-
//# sourceMappingURL=mcp-tool-proxy.js.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { McpPolicy } from "../../mcp/policy.js";
|
|
2
|
+
import { type DirectoryResource, type SkillEntry } from "./protocol.js";
|
|
3
|
+
/** What a listing pass observed, including why it may be incomplete. */
|
|
4
|
+
export interface SkillsListing {
|
|
5
|
+
/** Entries that passed pre-fetch validation. */
|
|
6
|
+
readonly skills: SkillEntry[];
|
|
7
|
+
/**
|
|
8
|
+
* Entries the server returned that this host refused to consider, with the
|
|
9
|
+
* reason. Surfaced rather than silently dropped so a malformed skill is
|
|
10
|
+
* visible instead of just missing.
|
|
11
|
+
*/
|
|
12
|
+
readonly rejected: {
|
|
13
|
+
readonly uri: string;
|
|
14
|
+
readonly reason: string;
|
|
15
|
+
}[];
|
|
16
|
+
/**
|
|
17
|
+
* True when the server had more pages than this host was willing to walk.
|
|
18
|
+
*
|
|
19
|
+
* SEP-2640 forbids treating a listing as proof of what a server has; this
|
|
20
|
+
* flag is how that uncertainty travels with the data instead of being
|
|
21
|
+
* flattened into "these are the skills".
|
|
22
|
+
*/
|
|
23
|
+
readonly truncated: boolean;
|
|
24
|
+
/** Whether this listing came from the in-memory freshness cache. */
|
|
25
|
+
readonly fromCache: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface SkillsExtensionClientOptions {
|
|
28
|
+
readonly policy: McpPolicy;
|
|
29
|
+
/** Injectable clock; defaults to `Date.now`. */
|
|
30
|
+
readonly now?: () => number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A client for the draft SEP-2640 skills extension.
|
|
34
|
+
*
|
|
35
|
+
* Every request goes through {@link McpPolicy}, which re-checks the negotiated
|
|
36
|
+
* capability immediately before dispatch. This class therefore never caches
|
|
37
|
+
* "server X supports skills" — it caches only listing *content*, and only when
|
|
38
|
+
* the server asked it to with terms this host is willing to honour.
|
|
39
|
+
*/
|
|
40
|
+
export declare class SkillsExtensionClient {
|
|
41
|
+
private readonly policy;
|
|
42
|
+
private readonly now;
|
|
43
|
+
private readonly listCache;
|
|
44
|
+
constructor(options: SkillsExtensionClientOptions);
|
|
45
|
+
/** True when the server declared the extension on the live connection. */
|
|
46
|
+
supports(serverName: string): boolean;
|
|
47
|
+
/** True when the server additionally declared `directoryRead: true`. */
|
|
48
|
+
supportsDirectoryRead(serverName: string): boolean;
|
|
49
|
+
/** The declared extension settings, for diagnostics. */
|
|
50
|
+
capability(serverName: string): Record<string, unknown> | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Walk `skills/list` to completion, validating each entry before it is
|
|
53
|
+
* admitted.
|
|
54
|
+
*
|
|
55
|
+
* Pagination stops at {@link MAX_SKILL_LIST_PAGES}; the result is then marked
|
|
56
|
+
* `truncated` rather than presented as the whole set. A server that returns
|
|
57
|
+
* the same cursor twice is also treated as truncated, which stops a cursor
|
|
58
|
+
* loop from becoming an unbounded request stream.
|
|
59
|
+
*/
|
|
60
|
+
listSkills(serverName: string, signal?: AbortSignal): Promise<SkillsListing>;
|
|
61
|
+
/**
|
|
62
|
+
* Fetch one skill's authoritative entry with `skills/get`.
|
|
63
|
+
*
|
|
64
|
+
* Always goes to the server: `skills/get` is what re-establishes current
|
|
65
|
+
* digests after a verification failure, so serving it from cache would defeat
|
|
66
|
+
* the recovery path the spec defines.
|
|
67
|
+
*/
|
|
68
|
+
getSkill(serverName: string, uri: string, signal?: AbortSignal): Promise<SkillEntry>;
|
|
69
|
+
/**
|
|
70
|
+
* Enumerate a skill directory, when the server declared `directoryRead`.
|
|
71
|
+
*
|
|
72
|
+
* Directory listings are navigational only. Nothing they return becomes
|
|
73
|
+
* readable: a file is readable because the skill entry lists it with a
|
|
74
|
+
* digest, and a directory cannot add entries to that list.
|
|
75
|
+
*/
|
|
76
|
+
readDirectory(serverName: string, uri: string, signal?: AbortSignal): Promise<DirectoryResource[]>;
|
|
77
|
+
/** Drop cached listings; call on disconnect or reset. */
|
|
78
|
+
clearCache(serverName?: string): void;
|
|
79
|
+
private readCache;
|
|
80
|
+
/**
|
|
81
|
+
* Cache a listing only when the server asked for it in terms this host
|
|
82
|
+
* understands.
|
|
83
|
+
*
|
|
84
|
+
* `cacheScope`'s value set is defined by SEP-2549, not SEP-2640, so an
|
|
85
|
+
* unrecognised scope is treated as "do not cache" rather than guessed at. A
|
|
86
|
+
* truncated listing is never cached, because caching an incomplete answer is
|
|
87
|
+
* exactly the "empty listing means no skills" mistake the spec warns against,
|
|
88
|
+
* just with a longer lifetime.
|
|
89
|
+
*/
|
|
90
|
+
private writeCache;
|
|
91
|
+
}
|