@sammorrowdrums/mcpi-ext 0.2.0 → 0.2.1
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/dist/code-mode/code-mode-integration.test.d.ts +2 -0
- package/dist/code-mode/code-mode-integration.test.d.ts.map +1 -0
- package/dist/code-mode/code-mode-integration.test.js +116 -0
- package/dist/code-mode/code-mode-integration.test.js.map +1 -0
- package/dist/code-mode/eligibility.d.ts +10 -0
- package/dist/code-mode/eligibility.d.ts.map +1 -0
- package/dist/code-mode/eligibility.js +14 -0
- package/dist/code-mode/eligibility.js.map +1 -0
- package/dist/code-mode/eligibility.test.d.ts +2 -0
- package/dist/code-mode/eligibility.test.d.ts.map +1 -0
- package/dist/code-mode/eligibility.test.js +53 -0
- package/dist/code-mode/eligibility.test.js.map +1 -0
- package/dist/code-mode/executor.d.ts +35 -0
- package/dist/code-mode/executor.d.ts.map +1 -0
- package/dist/code-mode/executor.js +120 -0
- package/dist/code-mode/executor.js.map +1 -0
- package/dist/code-mode/executor.test.d.ts +2 -0
- package/dist/code-mode/executor.test.d.ts.map +1 -0
- package/dist/code-mode/executor.test.js +130 -0
- package/dist/code-mode/executor.test.js.map +1 -0
- package/dist/code-mode/index.d.ts +65 -0
- package/dist/code-mode/index.d.ts.map +1 -0
- package/dist/code-mode/index.js +175 -0
- package/dist/code-mode/index.js.map +1 -0
- package/dist/code-mode/tools.d.ts +45 -0
- package/dist/code-mode/tools.d.ts.map +1 -0
- package/dist/code-mode/tools.js +70 -0
- package/dist/code-mode/tools.js.map +1 -0
- package/dist/code-mode/type-hints.d.ts +20 -0
- package/dist/code-mode/type-hints.d.ts.map +1 -0
- package/dist/code-mode/type-hints.js +189 -0
- package/dist/code-mode/type-hints.js.map +1 -0
- package/dist/code-mode/type-hints.test.d.ts +2 -0
- package/dist/code-mode/type-hints.test.d.ts.map +1 -0
- package/dist/code-mode/type-hints.test.js +157 -0
- package/dist/code-mode/type-hints.test.js.map +1 -0
- package/dist/docker-e2e.d.ts +55 -0
- package/dist/docker-e2e.d.ts.map +1 -0
- package/dist/docker-e2e.js +235 -0
- package/dist/docker-e2e.js.map +1 -0
- package/dist/docker-e2e.test.d.ts +2 -0
- package/dist/docker-e2e.test.d.ts.map +1 -0
- package/dist/docker-e2e.test.js +176 -0
- package/dist/docker-e2e.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +148 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +8 -0
- package/dist/index.test.js.map +1 -0
- package/dist/mcp/client-manager.d.ts +46 -0
- package/dist/mcp/client-manager.d.ts.map +1 -0
- package/dist/mcp/client-manager.js +148 -0
- package/dist/mcp/client-manager.js.map +1 -0
- package/dist/mcp/client-manager.test.d.ts +2 -0
- package/dist/mcp/client-manager.test.d.ts.map +1 -0
- package/dist/mcp/client-manager.test.js +204 -0
- package/dist/mcp/client-manager.test.js.map +1 -0
- package/dist/mcp/config-loader.d.ts +8 -0
- package/dist/mcp/config-loader.d.ts.map +1 -0
- package/dist/mcp/config-loader.js +43 -0
- package/dist/mcp/config-loader.js.map +1 -0
- package/dist/mcp/config-loader.test.d.ts +2 -0
- package/dist/mcp/config-loader.test.d.ts.map +1 -0
- package/dist/mcp/config-loader.test.js +93 -0
- package/dist/mcp/config-loader.test.js.map +1 -0
- package/dist/mcp/config.d.ts +46 -0
- package/dist/mcp/config.d.ts.map +1 -0
- package/dist/mcp/config.js +28 -0
- package/dist/mcp/config.js.map +1 -0
- package/{src/mcp/index.ts → dist/mcp/index.d.ts} +1 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +4 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/skills/discover.d.ts +10 -0
- package/dist/skills/discover.d.ts.map +1 -0
- package/dist/skills/discover.js +60 -0
- package/dist/skills/discover.js.map +1 -0
- package/dist/skills/format.d.ts +10 -0
- package/dist/skills/format.d.ts.map +1 -0
- package/dist/skills/format.js +38 -0
- package/dist/skills/format.js.map +1 -0
- package/dist/skills/format.test.d.ts +2 -0
- package/dist/skills/format.test.d.ts.map +1 -0
- package/dist/skills/format.test.js +63 -0
- package/dist/skills/format.test.js.map +1 -0
- package/{src/skills/index.ts → dist/skills/index.d.ts} +2 -5
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +6 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/load-skill-tool.d.ts +40 -0
- package/dist/skills/load-skill-tool.d.ts.map +1 -0
- package/dist/skills/load-skill-tool.js +104 -0
- package/dist/skills/load-skill-tool.js.map +1 -0
- package/dist/skills/mcp-tool-proxy.d.ts +11 -0
- package/dist/skills/mcp-tool-proxy.d.ts.map +1 -0
- package/dist/skills/mcp-tool-proxy.js +116 -0
- package/dist/skills/mcp-tool-proxy.js.map +1 -0
- package/dist/skills/skill-integration.test.d.ts +2 -0
- package/dist/skills/skill-integration.test.d.ts.map +1 -0
- package/dist/skills/skill-integration.test.js +119 -0
- package/dist/skills/skill-integration.test.js.map +1 -0
- package/dist/skills/skill-registry.d.ts +39 -0
- package/dist/skills/skill-registry.d.ts.map +1 -0
- package/dist/skills/skill-registry.js +48 -0
- package/dist/skills/skill-registry.js.map +1 -0
- package/dist/skills/skill-registry.test.d.ts +2 -0
- package/dist/skills/skill-registry.test.d.ts.map +1 -0
- package/dist/skills/skill-registry.test.js +67 -0
- package/dist/skills/skill-registry.test.js.map +1 -0
- package/dist/test-servers/weather-server.d.ts +12 -0
- package/dist/test-servers/weather-server.d.ts.map +1 -0
- package/dist/test-servers/weather-server.js +139 -0
- package/dist/test-servers/weather-server.js.map +1 -0
- package/dist/test-servers/weather-stdio.d.ts +2 -0
- package/dist/test-servers/weather-stdio.d.ts.map +1 -0
- package/{src/test-servers/weather-stdio.ts → dist/test-servers/weather-stdio.js} +1 -1
- package/dist/test-servers/weather-stdio.js.map +1 -0
- package/dist/tool-cli/cli.d.ts +3 -0
- package/dist/tool-cli/cli.d.ts.map +1 -0
- package/dist/tool-cli/cli.js +179 -0
- package/dist/tool-cli/cli.js.map +1 -0
- package/dist/tool-cli/constants.d.ts +7 -0
- package/dist/tool-cli/constants.d.ts.map +1 -0
- package/dist/tool-cli/constants.js +15 -0
- package/dist/tool-cli/constants.js.map +1 -0
- package/dist/tool-cli/format.d.ts +8 -0
- package/dist/tool-cli/format.d.ts.map +1 -0
- package/{src/tool-cli/format.ts → dist/tool-cli/format.js} +5 -4
- package/dist/tool-cli/format.js.map +1 -0
- package/dist/tool-cli/format.test.d.ts +2 -0
- package/dist/tool-cli/format.test.d.ts.map +1 -0
- package/dist/tool-cli/format.test.js +30 -0
- package/dist/tool-cli/format.test.js.map +1 -0
- package/{src/tool-cli/index.ts → dist/tool-cli/index.d.ts} +1 -0
- package/dist/tool-cli/index.d.ts.map +1 -0
- package/dist/tool-cli/index.js +3 -0
- package/dist/tool-cli/index.js.map +1 -0
- package/dist/tool-cli/rpc-client.d.ts +6 -0
- package/dist/tool-cli/rpc-client.d.ts.map +1 -0
- package/dist/tool-cli/rpc-client.js +25 -0
- package/dist/tool-cli/rpc-client.js.map +1 -0
- package/dist/tool-cli/rpc-server.d.ts +48 -0
- package/dist/tool-cli/rpc-server.d.ts.map +1 -0
- package/dist/tool-cli/rpc-server.js +220 -0
- package/dist/tool-cli/rpc-server.js.map +1 -0
- package/dist/tool-cli/rpc-server.test.d.ts +2 -0
- package/dist/tool-cli/rpc-server.test.d.ts.map +1 -0
- package/dist/tool-cli/rpc-server.test.js +154 -0
- package/dist/tool-cli/rpc-server.test.js.map +1 -0
- package/package.json +5 -1
- 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/skills/discover.ts +0 -81
- package/src/skills/format.test.ts +0 -70
- package/src/skills/format.ts +0 -42
- 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/tool-cli/format.test.ts +0 -33
- package/tsconfig.json +0 -21
package/src/skills/discover.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
-
import { parseFrontmatter } from "@sammorrowdrums/mcpi";
|
|
3
|
-
import type { McpSkillMetadata } from "./skill-registry.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Discover skills from a connected MCP server by reading its resources.
|
|
7
|
-
*
|
|
8
|
-
* Looks for resources with `skill://` URIs ending in `/SKILL.md`,
|
|
9
|
-
* reads each one, and parses YAML frontmatter for skill metadata.
|
|
10
|
-
*/
|
|
11
|
-
export async function discoverSkillsFromServer(
|
|
12
|
-
client: Client,
|
|
13
|
-
serverName: string,
|
|
14
|
-
log: (msg: string) => void = console.error,
|
|
15
|
-
): Promise<McpSkillMetadata[]> {
|
|
16
|
-
const skills: McpSkillMetadata[] = [];
|
|
17
|
-
|
|
18
|
-
let resources: { uri: string; name: string }[];
|
|
19
|
-
try {
|
|
20
|
-
const result = await client.listResources();
|
|
21
|
-
resources = result.resources;
|
|
22
|
-
} catch {
|
|
23
|
-
log(
|
|
24
|
-
`[skills] Server "${serverName}" does not support resources/list, skipping skill discovery`,
|
|
25
|
-
);
|
|
26
|
-
return skills;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const skillResources = resources.filter(
|
|
30
|
-
(r) => r.uri.startsWith("skill://") && r.uri.endsWith("/SKILL.md"),
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
if (skillResources.length === 0) return skills;
|
|
34
|
-
|
|
35
|
-
for (const resource of skillResources) {
|
|
36
|
-
try {
|
|
37
|
-
const result = await client.readResource({ uri: resource.uri });
|
|
38
|
-
const textContent = result.contents.find(
|
|
39
|
-
(c): c is { uri: string; text: string } => "text" in c,
|
|
40
|
-
);
|
|
41
|
-
if (!textContent) {
|
|
42
|
-
log(`[skills] Skill resource ${resource.uri} returned no text content, skipping`);
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const parsed = parseFrontmatter(textContent.text);
|
|
47
|
-
const fm = parsed.frontmatter as Record<string, unknown>;
|
|
48
|
-
const name = (fm.name as string | undefined) ?? resource.name;
|
|
49
|
-
const description = (fm.description as string | undefined) ?? "";
|
|
50
|
-
const allowedTools = parseAllowedTools(fm["allowed-tools"]);
|
|
51
|
-
|
|
52
|
-
if (!name) {
|
|
53
|
-
log(`[skills] Skill at ${resource.uri} has no name, skipping`);
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
skills.push({
|
|
58
|
-
name,
|
|
59
|
-
description,
|
|
60
|
-
uri: resource.uri,
|
|
61
|
-
serverName,
|
|
62
|
-
allowedTools,
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
log(
|
|
66
|
-
`[skills] Discovered skill "${name}" from "${serverName}" (${allowedTools.length} gated tools)`,
|
|
67
|
-
);
|
|
68
|
-
} catch (err) {
|
|
69
|
-
log(`[skills] Failed to read skill ${resource.uri}: ${(err as Error).message}`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return skills;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function parseAllowedTools(value: unknown): string[] {
|
|
77
|
-
if (Array.isArray(value)) {
|
|
78
|
-
return value.filter((v): v is string => typeof v === "string");
|
|
79
|
-
}
|
|
80
|
-
return [];
|
|
81
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { formatMcpSkillsForPrompt } from "./format.js";
|
|
3
|
-
import type { McpSkillMetadata } from "./skill-registry.js";
|
|
4
|
-
|
|
5
|
-
describe("formatMcpSkillsForPrompt", () => {
|
|
6
|
-
it("returns empty string for no skills", () => {
|
|
7
|
-
expect(formatMcpSkillsForPrompt([])).toBe("");
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it("formats a single skill", () => {
|
|
11
|
-
const skills: McpSkillMetadata[] = [
|
|
12
|
-
{
|
|
13
|
-
name: "weather",
|
|
14
|
-
description: "Check weather forecasts",
|
|
15
|
-
uri: "skill://weather/SKILL.md",
|
|
16
|
-
serverName: "weather-srv",
|
|
17
|
-
allowedTools: ["check_weather"],
|
|
18
|
-
},
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
const result = formatMcpSkillsForPrompt(skills);
|
|
22
|
-
expect(result).toContain("<available_mcp_skills>");
|
|
23
|
-
expect(result).toContain("</available_mcp_skills>");
|
|
24
|
-
expect(result).toContain("<name>weather</name>");
|
|
25
|
-
expect(result).toContain("<description>Check weather forecasts</description>");
|
|
26
|
-
expect(result).toContain("<server>weather-srv</server>");
|
|
27
|
-
expect(result).toContain("load_skill");
|
|
28
|
-
expect(result).toContain("deferred");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("formats multiple skills", () => {
|
|
32
|
-
const skills: McpSkillMetadata[] = [
|
|
33
|
-
{
|
|
34
|
-
name: "a",
|
|
35
|
-
description: "Skill A",
|
|
36
|
-
uri: "skill://a/SKILL.md",
|
|
37
|
-
serverName: "srv",
|
|
38
|
-
allowedTools: [],
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
name: "b",
|
|
42
|
-
description: "Skill B",
|
|
43
|
-
uri: "skill://b/SKILL.md",
|
|
44
|
-
serverName: "srv",
|
|
45
|
-
allowedTools: ["tool_b"],
|
|
46
|
-
},
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
const result = formatMcpSkillsForPrompt(skills);
|
|
50
|
-
expect(result).toContain("<name>a</name>");
|
|
51
|
-
expect(result).toContain("<name>b</name>");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("escapes XML characters", () => {
|
|
55
|
-
const skills: McpSkillMetadata[] = [
|
|
56
|
-
{
|
|
57
|
-
name: "test",
|
|
58
|
-
description: 'Uses <tags> & "quotes"',
|
|
59
|
-
uri: "skill://test/SKILL.md",
|
|
60
|
-
serverName: "srv",
|
|
61
|
-
allowedTools: [],
|
|
62
|
-
},
|
|
63
|
-
];
|
|
64
|
-
|
|
65
|
-
const result = formatMcpSkillsForPrompt(skills);
|
|
66
|
-
expect(result).toContain("<tags>");
|
|
67
|
-
expect(result).toContain("&");
|
|
68
|
-
expect(result).toContain(""quotes"");
|
|
69
|
-
});
|
|
70
|
-
});
|
package/src/skills/format.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { McpSkillMetadata } from "./skill-registry.js";
|
|
2
|
-
|
|
3
|
-
function escapeXml(str: string): string {
|
|
4
|
-
return str
|
|
5
|
-
.replace(/&/g, "&")
|
|
6
|
-
.replace(/</g, "<")
|
|
7
|
-
.replace(/>/g, ">")
|
|
8
|
-
.replace(/"/g, """)
|
|
9
|
-
.replace(/'/g, "'");
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Format MCP-discovered skills for inclusion in the system prompt.
|
|
14
|
-
*
|
|
15
|
-
* Produces XML matching Pi's native `formatSkillsForPrompt` structure,
|
|
16
|
-
* but references the `load_skill` tool instead of `read` and uses
|
|
17
|
-
* `mcp:<serverName>` as the location.
|
|
18
|
-
*/
|
|
19
|
-
export function formatMcpSkillsForPrompt(skills: McpSkillMetadata[]): string {
|
|
20
|
-
if (skills.length === 0) return "";
|
|
21
|
-
|
|
22
|
-
const lines = [
|
|
23
|
-
"",
|
|
24
|
-
"",
|
|
25
|
-
"The following MCP skills provide specialized instructions and tools for specific tasks.",
|
|
26
|
-
"MCP tools are deferred — they are available but not described in this prompt.",
|
|
27
|
-
"You MUST call load_skill to get usage instructions before using a skill's tools.",
|
|
28
|
-
"",
|
|
29
|
-
"<available_mcp_skills>",
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
for (const skill of skills) {
|
|
33
|
-
lines.push(" <skill>");
|
|
34
|
-
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
35
|
-
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
36
|
-
lines.push(` <server>${escapeXml(skill.serverName)}</server>`);
|
|
37
|
-
lines.push(" </skill>");
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
lines.push("</available_mcp_skills>");
|
|
41
|
-
return lines.join("\n");
|
|
42
|
-
}
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import type { AgentToolResult, ExtensionContext } from "@sammorrowdrums/mcpi";
|
|
2
|
-
import { stripFrontmatter } from "@sammorrowdrums/mcpi";
|
|
3
|
-
import { Type, type Static } from "typebox";
|
|
4
|
-
import type { McpClientManager } from "../mcp/index.js";
|
|
5
|
-
import type { SkillRegistry } from "./skill-registry.js";
|
|
6
|
-
|
|
7
|
-
const LoadSkillParams = Type.Object({
|
|
8
|
-
name: Type.String({ description: "Name of the MCP skill to load" }),
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
type LoadSkillInput = Static<typeof LoadSkillParams>;
|
|
12
|
-
|
|
13
|
-
export interface LoadSkillDeps {
|
|
14
|
-
registry: SkillRegistry;
|
|
15
|
-
mcpManager: McpClientManager;
|
|
16
|
-
enabledTools: Set<string>;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface LoadSkillDetails {
|
|
20
|
-
skillName: string;
|
|
21
|
-
serverName?: string;
|
|
22
|
-
activatedTools?: string[];
|
|
23
|
-
error?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Create the load_skill tool definition.
|
|
28
|
-
*
|
|
29
|
-
* When the model calls this tool, it:
|
|
30
|
-
* 1. Looks up the skill in the registry
|
|
31
|
-
* 2. Reads the full SKILL.md content from the MCP server
|
|
32
|
-
* 3. Returns the SKILL.md body (the skill names its tools, and the model
|
|
33
|
-
* already has their schemas from the deferred tools array)
|
|
34
|
-
*/
|
|
35
|
-
export function createLoadSkillTool(deps: LoadSkillDeps) {
|
|
36
|
-
const { registry, mcpManager, enabledTools } = deps;
|
|
37
|
-
|
|
38
|
-
return {
|
|
39
|
-
name: "load_skill",
|
|
40
|
-
label: "Load Skill",
|
|
41
|
-
description: "Load an MCP skill by name. Activates the skill's instructions and tools.",
|
|
42
|
-
promptSnippet: "Load an MCP skill to get specialized instructions and activate its tools.",
|
|
43
|
-
parameters: LoadSkillParams,
|
|
44
|
-
|
|
45
|
-
async execute(
|
|
46
|
-
_toolCallId: string,
|
|
47
|
-
params: LoadSkillInput,
|
|
48
|
-
_signal: AbortSignal | undefined,
|
|
49
|
-
_onUpdate: undefined,
|
|
50
|
-
_ctx: ExtensionContext,
|
|
51
|
-
): Promise<AgentToolResult<LoadSkillDetails>> {
|
|
52
|
-
const skill = registry.get(params.name);
|
|
53
|
-
if (!skill) {
|
|
54
|
-
const available = registry
|
|
55
|
-
.getAll()
|
|
56
|
-
.map((s) => s.name)
|
|
57
|
-
.join(", ");
|
|
58
|
-
return {
|
|
59
|
-
content: [
|
|
60
|
-
{
|
|
61
|
-
type: "text",
|
|
62
|
-
text: `Skill "${params.name}" not found. Available skills: ${available || "(none)"}`,
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
details: { skillName: params.name, error: "not_found" },
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const client = mcpManager.getClient(skill.serverName);
|
|
70
|
-
if (!client) {
|
|
71
|
-
return {
|
|
72
|
-
content: [
|
|
73
|
-
{
|
|
74
|
-
type: "text",
|
|
75
|
-
text: `MCP server "${skill.serverName}" is not connected. Cannot load skill "${params.name}".`,
|
|
76
|
-
},
|
|
77
|
-
],
|
|
78
|
-
details: { skillName: params.name, serverName: skill.serverName, error: "disconnected" },
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
let body: string;
|
|
83
|
-
try {
|
|
84
|
-
const result = await client.readResource({ uri: skill.uri });
|
|
85
|
-
const textContent = result.contents.find(
|
|
86
|
-
(c): c is { uri: string; text: string } => "text" in c,
|
|
87
|
-
);
|
|
88
|
-
if (!textContent) {
|
|
89
|
-
return {
|
|
90
|
-
content: [
|
|
91
|
-
{
|
|
92
|
-
type: "text",
|
|
93
|
-
text: `Skill "${params.name}" returned no text content.`,
|
|
94
|
-
},
|
|
95
|
-
],
|
|
96
|
-
details: { skillName: params.name, serverName: skill.serverName, error: "no_content" },
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
body = stripFrontmatter(textContent.text);
|
|
100
|
-
} catch (err) {
|
|
101
|
-
return {
|
|
102
|
-
content: [
|
|
103
|
-
{
|
|
104
|
-
type: "text",
|
|
105
|
-
text: `Failed to read skill "${params.name}" from server "${skill.serverName}": ${(err as Error).message}`,
|
|
106
|
-
},
|
|
107
|
-
],
|
|
108
|
-
details: {
|
|
109
|
-
skillName: params.name,
|
|
110
|
-
serverName: skill.serverName,
|
|
111
|
-
error: (err as Error).message,
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Enable the skill's tools so the tool_call gate allows them
|
|
117
|
-
for (const t of skill.allowedTools) {
|
|
118
|
-
enabledTools.add(t);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return {
|
|
122
|
-
content: [
|
|
123
|
-
{
|
|
124
|
-
type: "text",
|
|
125
|
-
text: body,
|
|
126
|
-
},
|
|
127
|
-
],
|
|
128
|
-
details: {
|
|
129
|
-
skillName: params.name,
|
|
130
|
-
serverName: skill.serverName,
|
|
131
|
-
activatedTools: skill.allowedTools,
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
},
|
|
135
|
-
};
|
|
136
|
-
}
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import type { AgentToolResult, ExtensionAPI, ExtensionContext } from "@sammorrowdrums/mcpi";
|
|
2
|
-
import { writeFileSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { Type } from "typebox";
|
|
6
|
-
import type { McpClientManager, McpTool } from "../mcp/index.js";
|
|
7
|
-
|
|
8
|
-
/** Threshold in chars above which tool output is written to a tmp file. */
|
|
9
|
-
const LARGE_OUTPUT_THRESHOLD = 10_000;
|
|
10
|
-
|
|
11
|
-
interface McpToolProxyDetails {
|
|
12
|
-
serverName: string;
|
|
13
|
-
toolName: string;
|
|
14
|
-
error?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Register MCP tools as Pi tool proxies.
|
|
19
|
-
*
|
|
20
|
-
* Each registered tool forwards calls to the MCP server via `client.callTool()`.
|
|
21
|
-
* Uses `Type.Unsafe()` to pass the MCP tool's original JSON Schema through
|
|
22
|
-
* to Pi, preserving property names and types for the model.
|
|
23
|
-
*/
|
|
24
|
-
export function registerMcpToolProxies(
|
|
25
|
-
toolNames: string[],
|
|
26
|
-
mcpManager: McpClientManager,
|
|
27
|
-
pi: ExtensionAPI,
|
|
28
|
-
): string[] {
|
|
29
|
-
const registered: string[] = [];
|
|
30
|
-
const allTools = mcpManager.getTools();
|
|
31
|
-
|
|
32
|
-
// Check which tools are already registered to avoid double-registration
|
|
33
|
-
const existingTools = new Set(pi.getAllTools().map((t: { name: string }) => t.name));
|
|
34
|
-
|
|
35
|
-
for (const name of toolNames) {
|
|
36
|
-
if (existingTools.has(name)) {
|
|
37
|
-
registered.push(name);
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const mcpTool = allTools.find((t) => t.name === name);
|
|
42
|
-
if (!mcpTool) continue;
|
|
43
|
-
|
|
44
|
-
pi.registerTool(createMcpToolProxy(mcpTool, mcpManager));
|
|
45
|
-
registered.push(name);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return registered;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function createMcpToolProxy(mcpTool: McpTool, mcpManager: McpClientManager) {
|
|
52
|
-
// Pass through the MCP tool's JSON Schema directly via Type.Unsafe()
|
|
53
|
-
// This preserves the original property names and types for the model
|
|
54
|
-
const inputSchema = mcpTool.inputSchema;
|
|
55
|
-
const parameters = Type.Unsafe({
|
|
56
|
-
type: "object",
|
|
57
|
-
properties: (inputSchema.properties as Record<string, unknown>) ?? {},
|
|
58
|
-
required: (inputSchema.required as string[]) ?? [],
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
name: mcpTool.name,
|
|
63
|
-
label: mcpTool.name,
|
|
64
|
-
description: mcpTool.description ?? `MCP tool from ${mcpTool.serverName}`,
|
|
65
|
-
deferred: true,
|
|
66
|
-
parameters,
|
|
67
|
-
|
|
68
|
-
async execute(
|
|
69
|
-
_toolCallId: string,
|
|
70
|
-
params: Record<string, unknown>,
|
|
71
|
-
_signal: AbortSignal | undefined,
|
|
72
|
-
_onUpdate: undefined,
|
|
73
|
-
_ctx: ExtensionContext,
|
|
74
|
-
): Promise<AgentToolResult<McpToolProxyDetails>> {
|
|
75
|
-
const client = mcpManager.getClient(mcpTool.serverName);
|
|
76
|
-
if (!client) {
|
|
77
|
-
return {
|
|
78
|
-
content: [
|
|
79
|
-
{
|
|
80
|
-
type: "text",
|
|
81
|
-
text: `MCP server "${mcpTool.serverName}" is not connected.`,
|
|
82
|
-
},
|
|
83
|
-
],
|
|
84
|
-
details: {
|
|
85
|
-
serverName: mcpTool.serverName,
|
|
86
|
-
toolName: mcpTool.name,
|
|
87
|
-
error: "disconnected",
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
try {
|
|
93
|
-
const result = await client.callTool({
|
|
94
|
-
name: mcpTool.name,
|
|
95
|
-
arguments: params,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
// Prefer structuredContent when available
|
|
99
|
-
let text: string;
|
|
100
|
-
if (result.structuredContent) {
|
|
101
|
-
text = JSON.stringify(result.structuredContent, null, 2);
|
|
102
|
-
} else if (Array.isArray(result.content)) {
|
|
103
|
-
text = result.content
|
|
104
|
-
.map((c) => {
|
|
105
|
-
if (typeof c === "object" && c !== null && "text" in c) {
|
|
106
|
-
return String((c as { text: unknown }).text);
|
|
107
|
-
}
|
|
108
|
-
return JSON.stringify(c);
|
|
109
|
-
})
|
|
110
|
-
.join("\n");
|
|
111
|
-
} else {
|
|
112
|
-
text = JSON.stringify(result);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Write large outputs to tmp file to avoid bloating context
|
|
116
|
-
if (text.length > LARGE_OUTPUT_THRESHOLD) {
|
|
117
|
-
const tmpPath = join(tmpdir(), `mcp-${mcpTool.name}-${Date.now()}.json`);
|
|
118
|
-
writeFileSync(tmpPath, text, "utf-8");
|
|
119
|
-
text = `Output too large (${text.length} chars). Written to: ${tmpPath}`;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
content: [{ type: "text" as const, text }],
|
|
124
|
-
details: { serverName: mcpTool.serverName, toolName: mcpTool.name },
|
|
125
|
-
};
|
|
126
|
-
} catch (err) {
|
|
127
|
-
return {
|
|
128
|
-
content: [
|
|
129
|
-
{
|
|
130
|
-
type: "text",
|
|
131
|
-
text: `MCP tool "${mcpTool.name}" failed: ${(err as Error).message}`,
|
|
132
|
-
},
|
|
133
|
-
],
|
|
134
|
-
details: {
|
|
135
|
-
serverName: mcpTool.serverName,
|
|
136
|
-
toolName: mcpTool.name,
|
|
137
|
-
error: (err as Error).message,
|
|
138
|
-
},
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
};
|
|
143
|
-
}
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
-
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
-
import { McpClientManager } from "../mcp/client-manager.js";
|
|
4
|
-
import { SkillRegistry } from "./skill-registry.js";
|
|
5
|
-
import { discoverSkillsFromServer } from "./discover.js";
|
|
6
|
-
import { formatMcpSkillsForPrompt } from "./format.js";
|
|
7
|
-
import { createLoadSkillTool } from "./load-skill-tool.js";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Integration test: connect to the test weather server via stdio,
|
|
11
|
-
* discover skills, load a skill, verify tool schemas in result, call a gated tool.
|
|
12
|
-
*/
|
|
13
|
-
describe("skill integration (weather server)", () => {
|
|
14
|
-
const manager = new McpClientManager();
|
|
15
|
-
const registry = new SkillRegistry();
|
|
16
|
-
const enabledTools = new Set<string>();
|
|
17
|
-
let client: Client;
|
|
18
|
-
|
|
19
|
-
beforeAll(async () => {
|
|
20
|
-
await manager.connectAll({
|
|
21
|
-
mcpServers: {
|
|
22
|
-
"test-weather": {
|
|
23
|
-
type: "stdio",
|
|
24
|
-
command: "node",
|
|
25
|
-
args: ["dist/test-servers/weather-stdio.js"],
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const c = manager.getClient("test-weather");
|
|
31
|
-
if (!c) throw new Error("Expected client for test-weather");
|
|
32
|
-
client = c;
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
afterAll(async () => {
|
|
36
|
-
await manager.disconnectAll();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("connects and discovers tools", () => {
|
|
40
|
-
expect(manager.getConnectedServers()).toEqual(["test-weather"]);
|
|
41
|
-
const tools = manager.getTools();
|
|
42
|
-
expect(tools.map((t) => t.name).sort()).toEqual([
|
|
43
|
-
"check_weather_for_city",
|
|
44
|
-
"check_weekly_forecast_for_city",
|
|
45
|
-
"echo",
|
|
46
|
-
]);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("discovers skill:// resources with frontmatter", async () => {
|
|
50
|
-
const skills = await discoverSkillsFromServer(client, "test-weather");
|
|
51
|
-
|
|
52
|
-
expect(skills).toHaveLength(1);
|
|
53
|
-
expect(skills[0].name).toBe("weather");
|
|
54
|
-
expect(skills[0].description).toBe("Check current weather and weekly forecasts for any city");
|
|
55
|
-
expect(skills[0].allowedTools).toEqual([
|
|
56
|
-
"check_weather_for_city",
|
|
57
|
-
"check_weekly_forecast_for_city",
|
|
58
|
-
]);
|
|
59
|
-
expect(skills[0].uri).toBe("skill://weather/SKILL.md");
|
|
60
|
-
expect(skills[0].serverName).toBe("test-weather");
|
|
61
|
-
|
|
62
|
-
registry.registerAll(skills);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("formats skills for system prompt", () => {
|
|
66
|
-
const prompt = formatMcpSkillsForPrompt(registry.getAll());
|
|
67
|
-
expect(prompt).toContain("<available_mcp_skills>");
|
|
68
|
-
expect(prompt).toContain("<name>weather</name>");
|
|
69
|
-
expect(prompt).toContain("load_skill");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("load_skill returns skill body and reports activated tools", async () => {
|
|
73
|
-
const tool = createLoadSkillTool({
|
|
74
|
-
registry,
|
|
75
|
-
mcpManager: manager,
|
|
76
|
-
enabledTools,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const result = await tool.execute(
|
|
80
|
-
"call-1",
|
|
81
|
-
{ name: "weather" },
|
|
82
|
-
undefined,
|
|
83
|
-
undefined,
|
|
84
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
85
|
-
{} as any,
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
expect(result.details.error).toBeUndefined();
|
|
89
|
-
expect(result.details.activatedTools).toEqual([
|
|
90
|
-
"check_weather_for_city",
|
|
91
|
-
"check_weekly_forecast_for_city",
|
|
92
|
-
]);
|
|
93
|
-
const text = result.content[0];
|
|
94
|
-
expect(text.type).toBe("text");
|
|
95
|
-
// Skill body should mention the tools
|
|
96
|
-
expect("text" in text && text.text).toContain("check_weather_for_city");
|
|
97
|
-
// Tools should now be in enabledTools set
|
|
98
|
-
expect(enabledTools.has("check_weather_for_city")).toBe(true);
|
|
99
|
-
expect(enabledTools.has("check_weekly_forecast_for_city")).toBe(true);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("calls gated tools via MCP client", async () => {
|
|
103
|
-
const weather = await client.callTool({
|
|
104
|
-
name: "check_weather_for_city",
|
|
105
|
-
arguments: { city: "Tokyo" },
|
|
106
|
-
});
|
|
107
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
108
|
-
expect((weather.content as any)[0].text).toContain("26");
|
|
109
|
-
expect(weather.structuredContent).toEqual({
|
|
110
|
-
temperature: 26,
|
|
111
|
-
conditions: "Sunny",
|
|
112
|
-
humidity: 55,
|
|
113
|
-
city: "Tokyo",
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const forecast = await client.callTool({
|
|
117
|
-
name: "check_weekly_forecast_for_city",
|
|
118
|
-
arguments: { city: "London" },
|
|
119
|
-
});
|
|
120
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
121
|
-
expect((forecast.content as any)[0].text).toContain("London");
|
|
122
|
-
expect(forecast.structuredContent).toHaveProperty("city", "London");
|
|
123
|
-
expect(forecast.structuredContent).toHaveProperty("forecast");
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("load_skill returns error for unknown skill", async () => {
|
|
127
|
-
const tool = createLoadSkillTool({
|
|
128
|
-
registry,
|
|
129
|
-
mcpManager: manager,
|
|
130
|
-
enabledTools,
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
134
|
-
const result = await tool.execute("call-2", { name: "nope" }, undefined, undefined, {} as any);
|
|
135
|
-
expect(result.details.error).toBe("not_found");
|
|
136
|
-
const text = result.content[0];
|
|
137
|
-
expect("text" in text && text.text).toContain("not found");
|
|
138
|
-
});
|
|
139
|
-
});
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { SkillRegistry, type McpSkillMetadata } from "./skill-registry.js";
|
|
3
|
-
|
|
4
|
-
function makeSkill(overrides: Partial<McpSkillMetadata> = {}): McpSkillMetadata {
|
|
5
|
-
return {
|
|
6
|
-
name: "test-skill",
|
|
7
|
-
description: "A test skill",
|
|
8
|
-
uri: "skill://test/SKILL.md",
|
|
9
|
-
serverName: "test-server",
|
|
10
|
-
allowedTools: ["tool_a"],
|
|
11
|
-
...overrides,
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
describe("SkillRegistry", () => {
|
|
16
|
-
it("registers and retrieves a skill", () => {
|
|
17
|
-
const reg = new SkillRegistry();
|
|
18
|
-
const skill = makeSkill();
|
|
19
|
-
reg.register(skill);
|
|
20
|
-
|
|
21
|
-
expect(reg.get("test-skill")).toEqual(skill);
|
|
22
|
-
expect(reg.size).toBe(1);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("getAll returns all registered skills", () => {
|
|
26
|
-
const reg = new SkillRegistry();
|
|
27
|
-
reg.register(makeSkill({ name: "a" }));
|
|
28
|
-
reg.register(makeSkill({ name: "b" }));
|
|
29
|
-
|
|
30
|
-
expect(reg.getAll()).toHaveLength(2);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("overwrites on duplicate name", () => {
|
|
34
|
-
const reg = new SkillRegistry();
|
|
35
|
-
reg.register(makeSkill({ description: "first" }));
|
|
36
|
-
reg.register(makeSkill({ description: "second" }));
|
|
37
|
-
|
|
38
|
-
expect(reg.size).toBe(1);
|
|
39
|
-
expect(reg.get("test-skill")?.description).toBe("second");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("unregister removes by name", () => {
|
|
43
|
-
const reg = new SkillRegistry();
|
|
44
|
-
reg.register(makeSkill());
|
|
45
|
-
|
|
46
|
-
expect(reg.unregister("test-skill")).toBe(true);
|
|
47
|
-
expect(reg.get("test-skill")).toBeUndefined();
|
|
48
|
-
expect(reg.unregister("test-skill")).toBe(false);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("unregisterByServer removes all skills from a server", () => {
|
|
52
|
-
const reg = new SkillRegistry();
|
|
53
|
-
reg.register(makeSkill({ name: "a", serverName: "srv1" }));
|
|
54
|
-
reg.register(makeSkill({ name: "b", serverName: "srv1" }));
|
|
55
|
-
reg.register(makeSkill({ name: "c", serverName: "srv2" }));
|
|
56
|
-
|
|
57
|
-
reg.unregisterByServer("srv1");
|
|
58
|
-
|
|
59
|
-
expect(reg.size).toBe(1);
|
|
60
|
-
expect(reg.get("c")).toBeDefined();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("registerAll registers multiple skills", () => {
|
|
64
|
-
const reg = new SkillRegistry();
|
|
65
|
-
reg.registerAll([makeSkill({ name: "x" }), makeSkill({ name: "y" })]);
|
|
66
|
-
|
|
67
|
-
expect(reg.size).toBe(2);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("clear removes everything", () => {
|
|
71
|
-
const reg = new SkillRegistry();
|
|
72
|
-
reg.registerAll([makeSkill({ name: "a" }), makeSkill({ name: "b" })]);
|
|
73
|
-
|
|
74
|
-
reg.clear();
|
|
75
|
-
expect(reg.size).toBe(0);
|
|
76
|
-
expect(reg.getAll()).toEqual([]);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("get returns undefined for unknown name", () => {
|
|
80
|
-
const reg = new SkillRegistry();
|
|
81
|
-
expect(reg.get("nonexistent")).toBeUndefined();
|
|
82
|
-
});
|
|
83
|
-
});
|