skybridge 0.0.0-dev.91d35d2 → 0.0.0-dev.91f1004
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/README.md +9 -7
- package/dist/cli/build-helpers.d.ts +2 -2
- package/dist/cli/build-helpers.js +11 -8
- package/dist/cli/build-helpers.js.map +1 -1
- package/dist/cli/build-helpers.test.js +17 -4
- package/dist/cli/build-helpers.test.js.map +1 -1
- package/dist/cli/build-steps.js +4 -4
- package/dist/cli/build-steps.js.map +1 -1
- package/dist/cli/build-steps.test.js +3 -3
- package/dist/cli/build-steps.test.js.map +1 -1
- package/dist/cli/detect-port.js +0 -1
- package/dist/cli/detect-port.js.map +1 -1
- package/dist/cli/resolve-skybridge-config.d.ts +5 -0
- package/dist/cli/{resolve-views-dir.js → resolve-skybridge-config.js} +4 -3
- package/dist/cli/resolve-skybridge-config.js.map +1 -0
- package/dist/cli/run-plain.js +2 -1
- package/dist/cli/run-plain.js.map +1 -1
- package/dist/cli/use-execute-steps.d.ts +1 -1
- package/dist/cli/use-nodemon.d.ts +11 -1
- package/dist/cli/use-nodemon.js +30 -8
- package/dist/cli/use-nodemon.js.map +1 -1
- package/dist/cli/use-typescript-check.js +1 -1
- package/dist/cli/use-typescript-check.js.map +1 -1
- package/dist/commands/dev.js +6 -6
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/start.js +2 -2
- package/dist/commands/start.js.map +1 -1
- package/dist/context-warnings.d.ts +5 -0
- package/dist/context-warnings.js +31 -0
- package/dist/context-warnings.js.map +1 -0
- package/dist/context-warnings.test.js +28 -0
- package/dist/context-warnings.test.js.map +1 -0
- package/dist/server/app.d.ts +194 -0
- package/dist/server/app.js +266 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/app.test.js +97 -0
- package/dist/server/app.test.js.map +1 -0
- package/dist/server/auth/discovery.d.ts +2 -1
- package/dist/server/auth/discovery.js +2 -2
- package/dist/server/auth/discovery.js.map +1 -1
- package/dist/server/auth/index.d.ts +35 -5
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/auth/providers/auth0.d.ts +25 -3
- package/dist/server/auth/providers/auth0.js +13 -8
- package/dist/server/auth/providers/auth0.js.map +1 -1
- package/dist/server/auth/providers/auth0.test.js +5 -3
- package/dist/server/auth/providers/auth0.test.js.map +1 -1
- package/dist/server/auth/providers/authplane.d.ts +49 -0
- package/dist/server/auth/providers/authplane.js +57 -0
- package/dist/server/auth/providers/authplane.js.map +1 -0
- package/dist/server/auth/providers/authplane.test.js +143 -0
- package/dist/server/auth/providers/authplane.test.js.map +1 -0
- package/dist/server/auth/providers/clerk.d.ts +40 -3
- package/dist/server/auth/providers/clerk.js +4 -1
- package/dist/server/auth/providers/clerk.js.map +1 -1
- package/dist/server/auth/providers/clerk.test.js +5 -3
- package/dist/server/auth/providers/clerk.test.js.map +1 -1
- package/dist/server/auth/providers/custom.d.ts +17 -7
- package/dist/server/auth/providers/custom.js +29 -10
- package/dist/server/auth/providers/custom.js.map +1 -1
- package/dist/server/auth/providers/custom.test.js +57 -24
- package/dist/server/auth/providers/custom.test.js.map +1 -1
- package/dist/server/auth/providers/descope.d.ts +32 -3
- package/dist/server/auth/providers/descope.js +12 -3
- package/dist/server/auth/providers/descope.js.map +1 -1
- package/dist/server/auth/providers/descope.test.js +42 -19
- package/dist/server/auth/providers/descope.test.js.map +1 -1
- package/dist/server/auth/providers/stytch.d.ts +16 -3
- package/dist/server/auth/providers/stytch.js +4 -1
- package/dist/server/auth/providers/stytch.js.map +1 -1
- package/dist/server/auth/providers/verify-spy.d.ts +12 -0
- package/dist/server/auth/providers/verify-spy.js +21 -0
- package/dist/server/auth/providers/verify-spy.js.map +1 -0
- package/dist/server/auth/providers/workos.d.ts +25 -3
- package/dist/server/auth/providers/workos.js +4 -1
- package/dist/server/auth/providers/workos.js.map +1 -1
- package/dist/server/auth/security-schemes.d.ts +23 -0
- package/dist/server/auth/security-schemes.js +64 -0
- package/dist/server/auth/security-schemes.js.map +1 -0
- package/dist/server/auth/security-schemes.test.js +99 -0
- package/dist/server/auth/security-schemes.test.js.map +1 -0
- package/dist/server/auth/setup.d.ts +3 -1
- package/dist/server/auth/setup.js +66 -26
- package/dist/server/auth/setup.js.map +1 -1
- package/dist/server/auth/setup.test.js +349 -20
- package/dist/server/auth/setup.test.js.map +1 -1
- package/dist/server/auth/verify.d.ts +37 -3
- package/dist/server/auth/verify.js +27 -3
- package/dist/server/auth/verify.js.map +1 -1
- package/dist/server/auth/verify.test.js +49 -0
- package/dist/server/auth/verify.test.js.map +1 -1
- package/dist/server/auth-extra.test-d.js +130 -0
- package/dist/server/auth-extra.test-d.js.map +1 -0
- package/dist/server/auth.d.ts +43 -6
- package/dist/server/auth.js +4 -5
- package/dist/server/auth.js.map +1 -1
- package/dist/server/build-manifest.test.js +7 -6
- package/dist/server/build-manifest.test.js.map +1 -1
- package/dist/server/content-helpers.d.ts +1 -1
- package/dist/server/content-helpers.js.map +1 -1
- package/dist/server/express.d.ts +23 -3
- package/dist/server/express.js +38 -32
- package/dist/server/express.js.map +1 -1
- package/dist/server/express.test.js +220 -103
- package/dist/server/express.test.js.map +1 -1
- package/dist/server/file-ref.d.ts +1 -1
- package/dist/server/file-ref.js +1 -1
- package/dist/server/file-ref.js.map +1 -1
- package/dist/server/host.d.ts +2 -0
- package/dist/server/host.js +11 -0
- package/dist/server/host.js.map +1 -0
- package/dist/server/host.test.d.ts +1 -0
- package/dist/server/host.test.js +13 -0
- package/dist/server/host.test.js.map +1 -0
- package/dist/server/index.d.ts +14 -8
- package/dist/server/index.js +7 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp-errors.test.d.ts +1 -0
- package/dist/server/mcp-errors.test.js +41 -0
- package/dist/server/mcp-errors.test.js.map +1 -0
- package/dist/server/middleware.d.ts +35 -10
- package/dist/server/middleware.js +33 -0
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/middleware.test-d.js +1 -1
- package/dist/server/middleware.test-d.js.map +1 -1
- package/dist/server/middleware.test.js +242 -134
- package/dist/server/middleware.test.js.map +1 -1
- package/dist/server/register-tool.test.d.ts +1 -0
- package/dist/server/register-tool.test.js +27 -0
- package/dist/server/register-tool.test.js.map +1 -0
- package/dist/server/server.d.ts +116 -124
- package/dist/server/server.js +155 -282
- package/dist/server/server.js.map +1 -1
- package/dist/server/skills-integration.test.js +44 -21
- package/dist/server/skills-integration.test.js.map +1 -1
- package/dist/server/skills.d.ts +14 -18
- package/dist/server/skills.js +52 -59
- package/dist/server/skills.js.map +1 -1
- package/dist/server/skills.test.js +77 -44
- package/dist/server/skills.test.js.map +1 -1
- package/dist/server/templateHelper.d.ts +0 -3
- package/dist/server/templateHelper.js.map +1 -1
- package/dist/server/templates.generated.js +4 -8
- package/dist/server/templates.generated.js.map +1 -1
- package/dist/server/tool-extra.test.d.ts +1 -0
- package/dist/server/tool-extra.test.js +88 -0
- package/dist/server/tool-extra.test.js.map +1 -0
- package/dist/server/view-resource-resolution.test.js +18 -13
- package/dist/server/view-resource-resolution.test.js.map +1 -1
- package/dist/server/viewsDevServer.js +4 -1
- package/dist/server/viewsDevServer.js.map +1 -1
- package/dist/test/utils.d.ts +9 -11
- package/dist/test/utils.js +220 -201
- package/dist/test/utils.js.map +1 -1
- package/dist/test/view.test.js +44 -40
- package/dist/test/view.test.js.map +1 -1
- package/dist/views/index.d.ts +10 -0
- package/dist/views/index.js +3 -0
- package/dist/views/index.js.map +1 -0
- package/dist/{web/plugin → views}/scan-views.js +1 -1
- package/dist/views/scan-views.js.map +1 -0
- package/dist/views/scan-views.test.d.ts +1 -0
- package/dist/views/scan-views.test.js.map +1 -0
- package/dist/views/validate-view.d.ts +1 -0
- package/dist/{web/plugin → views}/validate-view.js +2 -2
- package/dist/views/validate-view.js.map +1 -0
- package/dist/views/validate-view.test.d.ts +1 -0
- package/dist/views/validate-view.test.js.map +1 -0
- package/dist/web/bridges/adaptor.js +14 -17
- package/dist/web/bridges/adaptor.js.map +1 -1
- package/dist/web/bridges/adaptor.test.js +24 -7
- package/dist/web/bridges/adaptor.test.js.map +1 -1
- package/dist/web/bridges/apps-sdk/use-apps-sdk-context.d.ts +1 -1
- package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js +1 -1
- package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js.map +1 -1
- package/dist/web/bridges/get-adaptor.test.js +2 -2
- package/dist/web/bridges/get-adaptor.test.js.map +1 -1
- package/dist/web/bridges/mcp-app/bridge.js +6 -4
- package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
- package/dist/web/bridges/mcp-app/bridge.test.js +2 -2
- package/dist/web/bridges/mcp-app/bridge.test.js.map +1 -1
- package/dist/web/bridges/mcp-app/types.d.ts +2 -0
- package/dist/web/bridges/mcp-app/types.js.map +1 -1
- package/dist/web/bridges/mcp-app/use-mcp-app-context.d.ts +1 -1
- package/dist/web/bridges/mcp-app/use-mcp-app-context.js +1 -1
- package/dist/web/bridges/mcp-app/use-mcp-app-context.js.map +1 -1
- package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js +1 -1
- package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js.map +1 -1
- package/dist/web/bridges/mcp-app/view-tools.test.js +1 -1
- package/dist/web/bridges/mcp-app/view-tools.test.js.map +1 -1
- package/dist/web/bridges/types.d.ts +17 -12
- package/dist/web/bridges/types.js.map +1 -1
- package/dist/web/create-store.test.js +2 -2
- package/dist/web/create-store.test.js.map +1 -1
- package/dist/web/data-llm.test.js +5 -4
- package/dist/web/data-llm.test.js.map +1 -1
- package/dist/web/generate-helpers.d.ts +22 -18
- package/dist/web/generate-helpers.js +19 -15
- package/dist/web/generate-helpers.js.map +1 -1
- package/dist/web/generate-helpers.test-d.js +3 -3
- package/dist/web/generate-helpers.test-d.js.map +1 -1
- package/dist/web/generate-helpers.test.js +2 -2
- package/dist/web/generate-helpers.test.js.map +1 -1
- package/dist/web/hooks/index.d.ts +2 -1
- package/dist/web/hooks/index.js +2 -1
- package/dist/web/hooks/index.js.map +1 -1
- package/dist/web/hooks/test/utils.d.ts +2 -1
- package/dist/web/hooks/test/utils.js +1 -1
- package/dist/web/hooks/test/utils.js.map +1 -1
- package/dist/web/hooks/use-call-tool.test.js +2 -2
- package/dist/web/hooks/use-call-tool.test.js.map +1 -1
- package/dist/web/hooks/use-display-mode.d.ts +1 -1
- package/dist/web/hooks/use-display-mode.js +1 -1
- package/dist/web/hooks/use-display-mode.js.map +1 -1
- package/dist/web/hooks/use-display-mode.test.js +1 -1
- package/dist/web/hooks/use-display-mode.test.js.map +1 -1
- package/dist/web/hooks/use-download.d.ts +1 -3
- package/dist/web/hooks/use-download.js +1 -2
- package/dist/web/hooks/use-download.js.map +1 -1
- package/dist/web/hooks/use-download.test.js +7 -7
- package/dist/web/hooks/use-download.test.js.map +1 -1
- package/dist/web/hooks/use-files.test.js +1 -1
- package/dist/web/hooks/use-files.test.js.map +1 -1
- package/dist/web/hooks/use-host.d.ts +22 -0
- package/dist/web/hooks/use-host.js +30 -0
- package/dist/web/hooks/use-host.js.map +1 -0
- package/dist/web/hooks/use-host.test.d.ts +1 -0
- package/dist/web/hooks/use-host.test.js +59 -0
- package/dist/web/hooks/use-host.test.js.map +1 -0
- package/dist/web/hooks/use-open-external.test.js +2 -2
- package/dist/web/hooks/use-open-external.test.js.map +1 -1
- package/dist/web/hooks/use-register-view-tool.d.ts +2 -3
- package/dist/web/hooks/use-register-view-tool.js.map +1 -1
- package/dist/web/hooks/use-request-close.test.js +1 -1
- package/dist/web/hooks/use-request-close.test.js.map +1 -1
- package/dist/web/hooks/use-request-modal.test.js +1 -1
- package/dist/web/hooks/use-request-modal.test.js.map +1 -1
- package/dist/web/hooks/use-request-size.d.ts +1 -1
- package/dist/web/hooks/use-request-size.js +1 -1
- package/dist/web/hooks/use-request-size.js.map +1 -1
- package/dist/web/hooks/use-request-size.test.js +1 -1
- package/dist/web/hooks/use-request-size.test.js.map +1 -1
- package/dist/web/hooks/use-set-open-in-app-url.test.js +2 -8
- package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -1
- package/dist/web/hooks/use-tool-info.d.ts +1 -20
- package/dist/web/hooks/use-tool-info.js +0 -1
- package/dist/web/hooks/use-tool-info.js.map +1 -1
- package/dist/web/hooks/use-tool-info.test-d.js +0 -5
- package/dist/web/hooks/use-tool-info.test-d.js.map +1 -1
- package/dist/web/hooks/use-tool-info.test.js +2 -7
- package/dist/web/hooks/use-tool-info.test.js.map +1 -1
- package/dist/web/hooks/use-user.d.ts +3 -2
- package/dist/web/hooks/use-user.js +4 -3
- package/dist/web/hooks/use-user.js.map +1 -1
- package/dist/web/hooks/use-user.test.js +5 -3
- package/dist/web/hooks/use-user.test.js.map +1 -1
- package/dist/web/hooks/use-view-state.js +19 -9
- package/dist/web/hooks/use-view-state.js.map +1 -1
- package/dist/web/hooks/use-view-state.test.js +33 -2
- package/dist/web/hooks/use-view-state.test.js.map +1 -1
- package/dist/web/hooks/use-viewport.d.ts +20 -0
- package/dist/web/hooks/use-viewport.js +21 -0
- package/dist/web/hooks/use-viewport.js.map +1 -0
- package/dist/web/hooks/use-viewport.test.d.ts +1 -0
- package/dist/web/hooks/{use-layout.test.js → use-viewport.test.js} +12 -32
- package/dist/web/hooks/use-viewport.test.js.map +1 -0
- package/dist/web/mount-view.d.ts +3 -2
- package/dist/web/mount-view.js +5 -8
- package/dist/web/mount-view.js.map +1 -1
- package/package.json +17 -15
- package/dist/cli/resolve-views-dir.d.ts +0 -1
- package/dist/cli/resolve-views-dir.js.map +0 -1
- package/dist/web/hooks/use-layout.d.ts +0 -24
- package/dist/web/hooks/use-layout.js +0 -25
- package/dist/web/hooks/use-layout.js.map +0 -1
- package/dist/web/hooks/use-layout.test.js.map +0 -1
- package/dist/web/plugin/data-llm.test.js +0 -81
- package/dist/web/plugin/data-llm.test.js.map +0 -1
- package/dist/web/plugin/plugin.d.ts +0 -33
- package/dist/web/plugin/plugin.js +0 -188
- package/dist/web/plugin/plugin.js.map +0 -1
- package/dist/web/plugin/scan-views.js.map +0 -1
- package/dist/web/plugin/scan-views.test.js.map +0 -1
- package/dist/web/plugin/transform-data-llm.d.ts +0 -12
- package/dist/web/plugin/transform-data-llm.js +0 -96
- package/dist/web/plugin/transform-data-llm.js.map +0 -1
- package/dist/web/plugin/transform-data-llm.test.js +0 -81
- package/dist/web/plugin/transform-data-llm.test.js.map +0 -1
- package/dist/web/plugin/validate-view.d.ts +0 -1
- package/dist/web/plugin/validate-view.js.map +0 -1
- package/dist/web/plugin/validate-view.test.js.map +0 -1
- /package/dist/{web/hooks/use-layout.test.d.ts → context-warnings.test.d.ts} +0 -0
- /package/dist/{web/plugin/data-llm.test.d.ts → server/app.test.d.ts} +0 -0
- /package/dist/{web/plugin/transform-data-llm.test.d.ts → server/auth/providers/authplane.test.d.ts} +0 -0
- /package/dist/{web/plugin/validate-view.test.d.ts → server/auth/security-schemes.test.d.ts} +0 -0
- /package/dist/{web/plugin/scan-views.test.d.ts → server/auth-extra.test-d.d.ts} +0 -0
- /package/dist/{web/plugin → views}/scan-views.d.ts +0 -0
- /package/dist/{web/plugin → views}/scan-views.test.js +0 -0
- /package/dist/{web/plugin → views}/validate-view.test.js +0 -0
package/dist/server/skills.js
CHANGED
|
@@ -1,20 +1,11 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { extname, join } from "node:path";
|
|
4
|
-
import { ResourceTemplate } from "@modelcontextprotocol/
|
|
5
|
-
import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { INVALID_PARAMS, ProtocolError, ResourceTemplate, } from "@modelcontextprotocol/server";
|
|
6
5
|
import { parse as parseYaml } from "yaml";
|
|
7
|
-
import { z } from "zod";
|
|
8
|
-
/**
|
|
9
|
-
* Skills over MCP — the Skills Extension ([SEP-2640](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2640), experimental).
|
|
10
|
-
* All spec-facing logic is kept in this one module so churn in the in-review
|
|
11
|
-
* SEP stays a single-file change.
|
|
12
|
-
*/
|
|
6
|
+
import { z } from "zod/v4";
|
|
13
7
|
export const SKILLS_EXTENSION_KEY = "io.modelcontextprotocol/skills";
|
|
14
|
-
export const SKILL_INDEX_URI = "skill://index.json";
|
|
15
8
|
const SKILL_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
16
|
-
// Only the fields the extension depends on are validated; `.loose()` keeps any
|
|
17
|
-
// other frontmatter, which the index echoes verbatim.
|
|
18
9
|
const SkillFrontmatterSchema = z
|
|
19
10
|
.object({
|
|
20
11
|
name: z
|
|
@@ -44,8 +35,6 @@ const readSkillDir = (root, rel = "") => {
|
|
|
44
35
|
const files = {};
|
|
45
36
|
for (const entry of readdirSync(join(root, rel), { withFileTypes: true })) {
|
|
46
37
|
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
47
|
-
// Skip symlinks so a link can't pull content from outside the skills tree
|
|
48
|
-
// into the served manifest.
|
|
49
38
|
if (entry.isSymbolicLink()) {
|
|
50
39
|
continue;
|
|
51
40
|
}
|
|
@@ -58,9 +47,6 @@ const readSkillDir = (root, rel = "") => {
|
|
|
58
47
|
}
|
|
59
48
|
return files;
|
|
60
49
|
};
|
|
61
|
-
// Each immediate subdirectory with a `SKILL.md` is one skill; its directory name
|
|
62
|
-
// must equal the frontmatter `name`. Throws on any invalid skill so the
|
|
63
|
-
// build/dev startup fails loudly instead of silently skipping.
|
|
64
50
|
export function discoverSkills(dir) {
|
|
65
51
|
if (!existsSync(dir)) {
|
|
66
52
|
return [];
|
|
@@ -71,11 +57,11 @@ export function discoverSkills(dir) {
|
|
|
71
57
|
continue;
|
|
72
58
|
}
|
|
73
59
|
const skillRoot = join(dir, entry.name);
|
|
74
|
-
const
|
|
75
|
-
|
|
60
|
+
const files = readSkillDir(skillRoot);
|
|
61
|
+
const skillMd = files["SKILL.md"];
|
|
62
|
+
if (skillMd === undefined) {
|
|
76
63
|
continue;
|
|
77
64
|
}
|
|
78
|
-
const skillMd = readFileSync(skillMdPath, "utf8");
|
|
79
65
|
const source = `${entry.name}/SKILL.md`;
|
|
80
66
|
const parsed = SkillFrontmatterSchema.safeParse(parseFrontmatter(skillMd, source));
|
|
81
67
|
if (!parsed.success) {
|
|
@@ -84,35 +70,40 @@ export function discoverSkills(dir) {
|
|
|
84
70
|
if (parsed.data.name !== entry.name) {
|
|
85
71
|
throw new Error(`Skill name "${parsed.data.name}" in ${source} must match its directory name "${entry.name}"`);
|
|
86
72
|
}
|
|
73
|
+
const resources = Object.entries(files)
|
|
74
|
+
.sort(([a], [b]) => Number(b === "SKILL.md") - Number(a === "SKILL.md") ||
|
|
75
|
+
a.localeCompare(b))
|
|
76
|
+
.map(([relPath, content]) => ({
|
|
77
|
+
uri: `skill://${entry.name}/${relPath}`,
|
|
78
|
+
digest: sha256(content),
|
|
79
|
+
content,
|
|
80
|
+
}));
|
|
87
81
|
skills.push({
|
|
88
|
-
|
|
82
|
+
uri: `skill://${entry.name}/SKILL.md`,
|
|
89
83
|
frontmatter: parsed.data,
|
|
90
|
-
|
|
91
|
-
files: readSkillDir(skillRoot),
|
|
84
|
+
resources,
|
|
92
85
|
});
|
|
93
86
|
}
|
|
94
87
|
return skills;
|
|
95
88
|
}
|
|
96
|
-
// Parses a `skill://<name>/<subpath>` URI and rejects `..`/`.` traversal.
|
|
97
89
|
export function skillUriToRelPath(uri) {
|
|
98
90
|
const match = /^skill:\/\/([^/]+)(?:\/(.*))?$/.exec(uri.replace(/\/+$/, ""));
|
|
99
91
|
if (!match) {
|
|
100
|
-
throw new
|
|
92
|
+
throw new ProtocolError(INVALID_PARAMS, `Invalid skill uri: ${uri}`);
|
|
101
93
|
}
|
|
102
94
|
const [, name = "", relPath = ""] = match;
|
|
103
95
|
const segments = relPath.split("/").filter(Boolean);
|
|
104
96
|
if (name === "" || segments.includes("..") || segments.includes(".")) {
|
|
105
|
-
throw new
|
|
97
|
+
throw new ProtocolError(INVALID_PARAMS, `Invalid skill uri: ${uri}`);
|
|
106
98
|
}
|
|
107
99
|
return { name, relPath: segments.join("/") };
|
|
108
100
|
}
|
|
109
|
-
// Lists the immediate children of `relPath` within a skill. `null` means the
|
|
110
|
-
// path is absent (empty `relPath` always matches the skill root).
|
|
111
101
|
function listDir(skill, relPath) {
|
|
112
102
|
const prefix = relPath === "" ? "" : `${relPath}/`;
|
|
103
|
+
const base = skill.uri.slice(0, -"SKILL.md".length);
|
|
113
104
|
const children = new Map();
|
|
114
105
|
let matched = relPath === "";
|
|
115
|
-
for (const filePath of
|
|
106
|
+
for (const filePath of skill.resources.map((resource) => resource.uri.slice(base.length))) {
|
|
116
107
|
if (!filePath.startsWith(prefix)) {
|
|
117
108
|
continue;
|
|
118
109
|
}
|
|
@@ -131,56 +122,58 @@ function listDir(skill, relPath) {
|
|
|
131
122
|
}
|
|
132
123
|
return [...children].map(([name, mimeType]) => ({ name, mimeType }));
|
|
133
124
|
}
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
125
|
+
const DirectoryReadParamsSchema = z.object({
|
|
126
|
+
uri: z.string(),
|
|
127
|
+
cursor: z.string().optional(),
|
|
128
|
+
});
|
|
129
|
+
const SkillsListParamsSchema = z
|
|
130
|
+
.object({ cursor: z.string().optional() })
|
|
131
|
+
.optional();
|
|
132
|
+
const SkillsGetParamsSchema = z.object({ uri: z.string() });
|
|
133
|
+
const toWireEntry = (skill) => ({
|
|
134
|
+
uri: skill.uri,
|
|
135
|
+
frontmatter: skill.frontmatter,
|
|
136
|
+
resources: skill.resources.map(({ uri, digest }) => ({ uri, digest })),
|
|
137
137
|
});
|
|
138
|
-
// Registers per SEP-2640: one resource per `SKILL.md`, a template for supporting
|
|
139
|
-
// files, the `skill://index.json` index, and the `resources/directory/read` handler.
|
|
140
138
|
export function registerSkills(server, manifest) {
|
|
141
|
-
const
|
|
142
|
-
const serveFile = (
|
|
143
|
-
const text =
|
|
139
|
+
const byUri = new Map(manifest.map((skill) => [skill.uri, skill]));
|
|
140
|
+
const serveFile = (skillUri, fileUri, href) => {
|
|
141
|
+
const text = byUri
|
|
142
|
+
.get(skillUri)
|
|
143
|
+
?.resources.find((resource) => resource.uri === fileUri)?.content;
|
|
144
144
|
if (text === undefined) {
|
|
145
|
-
throw new
|
|
145
|
+
throw new ProtocolError(INVALID_PARAMS, `Not found: ${href}`);
|
|
146
146
|
}
|
|
147
147
|
return {
|
|
148
148
|
contents: [{ uri: href, text, mimeType: "text/markdown" }],
|
|
149
149
|
};
|
|
150
150
|
};
|
|
151
151
|
for (const skill of manifest) {
|
|
152
|
-
server.registerResource(skill.name,
|
|
153
|
-
description:
|
|
152
|
+
server.registerResource(skill.frontmatter.name, skill.uri, {
|
|
153
|
+
description: skill.frontmatter.description,
|
|
154
154
|
mimeType: "text/markdown",
|
|
155
|
-
}, (readUri) => serveFile(skill.
|
|
155
|
+
}, (readUri) => serveFile(skill.uri, skill.uri, readUri.href));
|
|
156
156
|
}
|
|
157
157
|
server.registerResource("skill-files", new ResourceTemplate("skill://{skillName}/{+filePath}", {
|
|
158
158
|
list: undefined,
|
|
159
159
|
}), {}, (readUri) => {
|
|
160
160
|
const { name, relPath } = skillUriToRelPath(readUri.href);
|
|
161
|
-
return serveFile(name
|
|
161
|
+
return serveFile(`skill://${name}/SKILL.md`, `skill://${name}/${relPath}`, readUri.href);
|
|
162
|
+
});
|
|
163
|
+
server.server.setRequestHandler("skills/list", { params: SkillsListParamsSchema }, () => ({ skills: manifest.map(toWireEntry) }));
|
|
164
|
+
server.server.setRequestHandler("skills/get", { params: SkillsGetParamsSchema }, (params) => {
|
|
165
|
+
const skill = byUri.get(params.uri);
|
|
166
|
+
if (!skill) {
|
|
167
|
+
throw new ProtocolError(INVALID_PARAMS, `Unknown skill: ${params.uri}`);
|
|
168
|
+
}
|
|
169
|
+
return { skill: toWireEntry(skill) };
|
|
162
170
|
});
|
|
163
|
-
server.
|
|
164
|
-
contents: [
|
|
165
|
-
{
|
|
166
|
-
uri: SKILL_INDEX_URI,
|
|
167
|
-
mimeType: "application/json",
|
|
168
|
-
text: JSON.stringify({
|
|
169
|
-
skills: manifest.map((skill) => ({
|
|
170
|
-
url: `skill://${skill.name}/SKILL.md`,
|
|
171
|
-
digest: skill.digest,
|
|
172
|
-
frontmatter: skill.frontmatter,
|
|
173
|
-
})),
|
|
174
|
-
}),
|
|
175
|
-
},
|
|
176
|
-
],
|
|
177
|
-
}));
|
|
178
|
-
server.server.setRequestHandler(DirectoryReadRequestSchema, ({ params }) => {
|
|
171
|
+
server.server.setRequestHandler("resources/directory/read", { params: DirectoryReadParamsSchema }, (params) => {
|
|
179
172
|
const { name, relPath } = skillUriToRelPath(params.uri);
|
|
180
|
-
const skill =
|
|
173
|
+
const skill = byUri.get(`skill://${name}/SKILL.md`);
|
|
181
174
|
const entries = skill ? listDir(skill, relPath) : null;
|
|
182
175
|
if (!entries) {
|
|
183
|
-
throw new
|
|
176
|
+
throw new ProtocolError(INVALID_PARAMS, `Not a directory: ${params.uri}`);
|
|
184
177
|
}
|
|
185
178
|
const base = params.uri.replace(/\/+$/, "");
|
|
186
179
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skills.js","sourceRoot":"","sources":["../../src/server/skills.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { existsSync, readdirSync, readFileSync } from \"node:fs\";\nimport { extname, join } from \"node:path\";\nimport type {\n ReadResourceCallback,\n ReadResourceTemplateCallback,\n ResourceMetadata,\n McpServer as SdkMcpServer,\n} from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { ResourceTemplate } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { ErrorCode, McpError } from \"@modelcontextprotocol/sdk/types.js\";\nimport { parse as parseYaml } from \"yaml\";\nimport { z } from \"zod\";\n\n/**\n * Skills over MCP — the Skills Extension ([SEP-2640](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2640), experimental).\n * All spec-facing logic is kept in this one module so churn in the in-review\n * SEP stays a single-file change.\n */\n\nexport const SKILLS_EXTENSION_KEY = \"io.modelcontextprotocol/skills\";\nexport const SKILL_INDEX_URI = \"skill://index.json\";\n\nconst SKILL_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;\n\n// Only the fields the extension depends on are validated; `.loose()` keeps any\n// other frontmatter, which the index echoes verbatim.\nconst SkillFrontmatterSchema = z\n .object({\n name: z\n .string()\n .min(1)\n .max(64)\n .regex(\n SKILL_NAME_RE,\n \"must be lowercase alphanumeric words separated by single hyphens\",\n ),\n description: z.string().min(1).max(1024),\n })\n .loose();\n\nexport interface Skill {\n name: string;\n frontmatter: Record<string, unknown>;\n /** `sha256:<hex>` over the raw `SKILL.md` bytes. */\n digest: string;\n /** Markdown text keyed by POSIX path relative to the skill directory, including `SKILL.md`. */\n files: Record<string, string>;\n}\n\nexport type SkillsManifest = Skill[];\n\nconst sha256 = (content: string): string =>\n `sha256:${createHash(\"sha256\").update(content, \"utf8\").digest(\"hex\")}`;\n\nconst FRONTMATTER_RE = /^?\\s*---[ \\t]*\\r?\\n([\\s\\S]*?)\\r?\\n---[ \\t]*(?:\\r?\\n|$)/;\n\nfunction parseFrontmatter(content: string, source: string): unknown {\n const match = FRONTMATTER_RE.exec(content);\n if (!match) {\n throw new Error(`Skill ${source} is missing YAML frontmatter`);\n }\n try {\n return parseYaml(match[1] ?? \"\");\n } catch (error) {\n const detail = error instanceof Error ? error.message : String(error);\n throw new Error(`Cannot parse frontmatter of ${source}: ${detail}`);\n }\n}\n\nconst readSkillDir = (root: string, rel = \"\"): Record<string, string> => {\n const files: Record<string, string> = {};\n for (const entry of readdirSync(join(root, rel), { withFileTypes: true })) {\n const childRel = rel ? `${rel}/${entry.name}` : entry.name;\n // Skip symlinks so a link can't pull content from outside the skills tree\n // into the served manifest.\n if (entry.isSymbolicLink()) {\n continue;\n }\n if (entry.isDirectory()) {\n Object.assign(files, readSkillDir(root, childRel));\n } else if (entry.isFile() && extname(entry.name) === \".md\") {\n files[childRel] = readFileSync(join(root, childRel), \"utf8\");\n }\n }\n return files;\n};\n\n// Each immediate subdirectory with a `SKILL.md` is one skill; its directory name\n// must equal the frontmatter `name`. Throws on any invalid skill so the\n// build/dev startup fails loudly instead of silently skipping.\nexport function discoverSkills(dir: string): SkillsManifest {\n if (!existsSync(dir)) {\n return [];\n }\n\n const skills: SkillsManifest = [];\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (!entry.isDirectory()) {\n continue;\n }\n\n const skillRoot = join(dir, entry.name);\n const skillMdPath = join(skillRoot, \"SKILL.md\");\n if (!existsSync(skillMdPath)) {\n continue;\n }\n\n const skillMd = readFileSync(skillMdPath, \"utf8\");\n const source = `${entry.name}/SKILL.md`;\n\n const parsed = SkillFrontmatterSchema.safeParse(\n parseFrontmatter(skillMd, source),\n );\n if (!parsed.success) {\n throw new Error(\n `Invalid skill frontmatter in ${source}: ${z.prettifyError(parsed.error)}`,\n );\n }\n if (parsed.data.name !== entry.name) {\n throw new Error(\n `Skill name \"${parsed.data.name}\" in ${source} must match its directory name \"${entry.name}\"`,\n );\n }\n\n skills.push({\n name: entry.name,\n frontmatter: parsed.data,\n digest: sha256(skillMd),\n files: readSkillDir(skillRoot),\n });\n }\n return skills;\n}\n\n// Parses a `skill://<name>/<subpath>` URI and rejects `..`/`.` traversal.\nexport function skillUriToRelPath(uri: string): {\n name: string;\n relPath: string;\n} {\n const match = /^skill:\\/\\/([^/]+)(?:\\/(.*))?$/.exec(uri.replace(/\\/+$/, \"\"));\n if (!match) {\n throw new McpError(ErrorCode.InvalidParams, `Invalid skill uri: ${uri}`);\n }\n const [, name = \"\", relPath = \"\"] = match;\n const segments = relPath.split(\"/\").filter(Boolean);\n if (name === \"\" || segments.includes(\"..\") || segments.includes(\".\")) {\n throw new McpError(ErrorCode.InvalidParams, `Invalid skill uri: ${uri}`);\n }\n return { name, relPath: segments.join(\"/\") };\n}\n\n// Lists the immediate children of `relPath` within a skill. `null` means the\n// path is absent (empty `relPath` always matches the skill root).\nfunction listDir(\n skill: Skill,\n relPath: string,\n): { name: string; mimeType: string }[] | null {\n const prefix = relPath === \"\" ? \"\" : `${relPath}/`;\n const children = new Map<string, string>();\n let matched = relPath === \"\";\n for (const filePath of Object.keys(skill.files)) {\n if (!filePath.startsWith(prefix)) {\n continue;\n }\n matched = true;\n const rest = filePath.slice(prefix.length);\n const slash = rest.indexOf(\"/\");\n if (slash === -1) {\n children.set(rest, \"text/markdown\");\n } else {\n children.set(rest.slice(0, slash), \"inode/directory\");\n }\n }\n if (!matched) {\n return null;\n }\n return [...children].map(([name, mimeType]) => ({ name, mimeType }));\n}\n\n/** The subset of `McpServer` that skill registration needs. */\ninterface SkillRegistrar {\n registerResource(\n name: string,\n uri: string,\n config: ResourceMetadata,\n readCallback: ReadResourceCallback,\n ): unknown;\n registerResource(\n name: string,\n template: ResourceTemplate,\n config: ResourceMetadata,\n readCallback: ReadResourceTemplateCallback,\n ): unknown;\n readonly server: SdkMcpServer[\"server\"];\n}\n\nconst DirectoryReadRequestSchema = z.object({\n method: z.literal(\"resources/directory/read\"),\n params: z.object({ uri: z.string(), cursor: z.string().optional() }),\n});\n\n// Registers per SEP-2640: one resource per `SKILL.md`, a template for supporting\n// files, the `skill://index.json` index, and the `resources/directory/read` handler.\nexport function registerSkills(\n server: SkillRegistrar,\n manifest: SkillsManifest,\n): void {\n const byName = new Map(manifest.map((s) => [s.name, s]));\n const serveFile = (name: string, relPath: string, href: string) => {\n const text = byName.get(name)?.files[relPath];\n if (text === undefined) {\n throw new McpError(ErrorCode.InvalidParams, `Not found: ${href}`);\n }\n return {\n contents: [{ uri: href, text, mimeType: \"text/markdown\" }],\n };\n };\n\n for (const skill of manifest) {\n server.registerResource(\n skill.name,\n `skill://${skill.name}/SKILL.md`,\n {\n description: String(skill.frontmatter.description),\n mimeType: \"text/markdown\",\n },\n (readUri) => serveFile(skill.name, \"SKILL.md\", readUri.href),\n );\n }\n\n server.registerResource(\n \"skill-files\",\n new ResourceTemplate(\"skill://{skillName}/{+filePath}\", {\n list: undefined,\n }),\n {},\n (readUri) => {\n const { name, relPath } = skillUriToRelPath(readUri.href);\n return serveFile(name, relPath, readUri.href);\n },\n );\n\n server.registerResource(\n \"skill-index\",\n SKILL_INDEX_URI,\n { mimeType: \"application/json\" },\n () => ({\n contents: [\n {\n uri: SKILL_INDEX_URI,\n mimeType: \"application/json\",\n text: JSON.stringify({\n skills: manifest.map((skill) => ({\n url: `skill://${skill.name}/SKILL.md`,\n digest: skill.digest,\n frontmatter: skill.frontmatter,\n })),\n }),\n },\n ],\n }),\n );\n\n server.server.setRequestHandler(DirectoryReadRequestSchema, ({ params }) => {\n const { name, relPath } = skillUriToRelPath(params.uri);\n const skill = byName.get(name);\n const entries = skill ? listDir(skill, relPath) : null;\n if (!entries) {\n throw new McpError(\n ErrorCode.InvalidParams,\n `Not a directory: ${params.uri}`,\n );\n }\n const base = params.uri.replace(/\\/+$/, \"\");\n return {\n resources: entries.map((entry) => ({\n uri: `${base}/${entry.name}`,\n name: entry.name,\n mimeType: entry.mimeType,\n })),\n };\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"skills.js","sourceRoot":"","sources":["../../src/server/skills.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { existsSync, readdirSync, readFileSync } from \"node:fs\";\nimport { extname, join } from \"node:path\";\nimport {\n INVALID_PARAMS,\n ProtocolError,\n type ReadResourceCallback,\n type ReadResourceTemplateCallback,\n type ResourceMetadata,\n ResourceTemplate,\n type McpServer as SdkMcpServer,\n} from \"@modelcontextprotocol/server\";\nimport { parse as parseYaml } from \"yaml\";\nimport { z } from \"zod/v4\";\n\nexport const SKILLS_EXTENSION_KEY = \"io.modelcontextprotocol/skills\";\n\nconst SKILL_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;\n\nconst SkillFrontmatterSchema = z\n .object({\n name: z\n .string()\n .min(1)\n .max(64)\n .regex(\n SKILL_NAME_RE,\n \"must be lowercase alphanumeric words separated by single hyphens\",\n ),\n description: z.string().min(1).max(1024),\n })\n .loose();\n\nexport interface SkillResource {\n uri: string;\n digest: string;\n content: string;\n}\n\nexport interface Skill {\n uri: string;\n frontmatter: { name: string; description: string } & Record<string, unknown>;\n resources: SkillResource[];\n}\n\nexport type SkillsManifest = Skill[];\n\nconst sha256 = (content: string): string =>\n `sha256:${createHash(\"sha256\").update(content, \"utf8\").digest(\"hex\")}`;\n\nconst FRONTMATTER_RE = /^?\\s*---[ \\t]*\\r?\\n([\\s\\S]*?)\\r?\\n---[ \\t]*(?:\\r?\\n|$)/;\n\nfunction parseFrontmatter(content: string, source: string): unknown {\n const match = FRONTMATTER_RE.exec(content);\n if (!match) {\n throw new Error(`Skill ${source} is missing YAML frontmatter`);\n }\n try {\n return parseYaml(match[1] ?? \"\");\n } catch (error) {\n const detail = error instanceof Error ? error.message : String(error);\n throw new Error(`Cannot parse frontmatter of ${source}: ${detail}`);\n }\n}\n\nconst readSkillDir = (root: string, rel = \"\"): Record<string, string> => {\n const files: Record<string, string> = {};\n for (const entry of readdirSync(join(root, rel), { withFileTypes: true })) {\n const childRel = rel ? `${rel}/${entry.name}` : entry.name;\n if (entry.isSymbolicLink()) {\n continue;\n }\n if (entry.isDirectory()) {\n Object.assign(files, readSkillDir(root, childRel));\n } else if (entry.isFile() && extname(entry.name) === \".md\") {\n files[childRel] = readFileSync(join(root, childRel), \"utf8\");\n }\n }\n return files;\n};\n\nexport function discoverSkills(dir: string): SkillsManifest {\n if (!existsSync(dir)) {\n return [];\n }\n\n const skills: SkillsManifest = [];\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (!entry.isDirectory()) {\n continue;\n }\n\n const skillRoot = join(dir, entry.name);\n const files = readSkillDir(skillRoot);\n const skillMd = files[\"SKILL.md\"];\n if (skillMd === undefined) {\n continue;\n }\n\n const source = `${entry.name}/SKILL.md`;\n\n const parsed = SkillFrontmatterSchema.safeParse(\n parseFrontmatter(skillMd, source),\n );\n if (!parsed.success) {\n throw new Error(\n `Invalid skill frontmatter in ${source}: ${z.prettifyError(parsed.error)}`,\n );\n }\n if (parsed.data.name !== entry.name) {\n throw new Error(\n `Skill name \"${parsed.data.name}\" in ${source} must match its directory name \"${entry.name}\"`,\n );\n }\n\n const resources = Object.entries(files)\n .sort(\n ([a], [b]) =>\n Number(b === \"SKILL.md\") - Number(a === \"SKILL.md\") ||\n a.localeCompare(b),\n )\n .map(([relPath, content]) => ({\n uri: `skill://${entry.name}/${relPath}`,\n digest: sha256(content),\n content,\n }));\n\n skills.push({\n uri: `skill://${entry.name}/SKILL.md`,\n frontmatter: parsed.data,\n resources,\n });\n }\n return skills;\n}\n\nexport function skillUriToRelPath(uri: string): {\n name: string;\n relPath: string;\n} {\n const match = /^skill:\\/\\/([^/]+)(?:\\/(.*))?$/.exec(uri.replace(/\\/+$/, \"\"));\n if (!match) {\n throw new ProtocolError(INVALID_PARAMS, `Invalid skill uri: ${uri}`);\n }\n const [, name = \"\", relPath = \"\"] = match;\n const segments = relPath.split(\"/\").filter(Boolean);\n if (name === \"\" || segments.includes(\"..\") || segments.includes(\".\")) {\n throw new ProtocolError(INVALID_PARAMS, `Invalid skill uri: ${uri}`);\n }\n return { name, relPath: segments.join(\"/\") };\n}\n\nfunction listDir(\n skill: Skill,\n relPath: string,\n): { name: string; mimeType: string }[] | null {\n const prefix = relPath === \"\" ? \"\" : `${relPath}/`;\n const base = skill.uri.slice(0, -\"SKILL.md\".length);\n const children = new Map<string, string>();\n let matched = relPath === \"\";\n for (const filePath of skill.resources.map((resource) =>\n resource.uri.slice(base.length),\n )) {\n if (!filePath.startsWith(prefix)) {\n continue;\n }\n matched = true;\n const rest = filePath.slice(prefix.length);\n const slash = rest.indexOf(\"/\");\n if (slash === -1) {\n children.set(rest, \"text/markdown\");\n } else {\n children.set(rest.slice(0, slash), \"inode/directory\");\n }\n }\n if (!matched) {\n return null;\n }\n return [...children].map(([name, mimeType]) => ({ name, mimeType }));\n}\n\nexport interface SkillRegistrar {\n registerResource(\n name: string,\n uri: string,\n config: ResourceMetadata,\n readCallback: ReadResourceCallback,\n ): unknown;\n registerResource(\n name: string,\n template: ResourceTemplate,\n config: ResourceMetadata,\n readCallback: ReadResourceTemplateCallback,\n ): unknown;\n readonly server: Pick<SdkMcpServer[\"server\"], \"setRequestHandler\">;\n}\n\nconst DirectoryReadParamsSchema = z.object({\n uri: z.string(),\n cursor: z.string().optional(),\n});\n\nconst SkillsListParamsSchema = z\n .object({ cursor: z.string().optional() })\n .optional();\n\nconst SkillsGetParamsSchema = z.object({ uri: z.string() });\n\nconst toWireEntry = (skill: Skill) => ({\n uri: skill.uri,\n frontmatter: skill.frontmatter,\n resources: skill.resources.map(({ uri, digest }) => ({ uri, digest })),\n});\n\nexport function registerSkills(\n server: SkillRegistrar,\n manifest: SkillsManifest,\n): void {\n const byUri = new Map(manifest.map((skill) => [skill.uri, skill]));\n const serveFile = (skillUri: string, fileUri: string, href: string) => {\n const text = byUri\n .get(skillUri)\n ?.resources.find((resource) => resource.uri === fileUri)?.content;\n if (text === undefined) {\n throw new ProtocolError(INVALID_PARAMS, `Not found: ${href}`);\n }\n return {\n contents: [{ uri: href, text, mimeType: \"text/markdown\" }],\n };\n };\n\n for (const skill of manifest) {\n server.registerResource(\n skill.frontmatter.name,\n skill.uri,\n {\n description: skill.frontmatter.description,\n mimeType: \"text/markdown\",\n },\n (readUri) => serveFile(skill.uri, skill.uri, readUri.href),\n );\n }\n\n server.registerResource(\n \"skill-files\",\n new ResourceTemplate(\"skill://{skillName}/{+filePath}\", {\n list: undefined,\n }),\n {},\n (readUri) => {\n const { name, relPath } = skillUriToRelPath(readUri.href);\n return serveFile(\n `skill://${name}/SKILL.md`,\n `skill://${name}/${relPath}`,\n readUri.href,\n );\n },\n );\n\n server.server.setRequestHandler(\n \"skills/list\",\n { params: SkillsListParamsSchema },\n () => ({ skills: manifest.map(toWireEntry) }),\n );\n\n server.server.setRequestHandler(\n \"skills/get\",\n { params: SkillsGetParamsSchema },\n (params) => {\n const skill = byUri.get(params.uri);\n if (!skill) {\n throw new ProtocolError(INVALID_PARAMS, `Unknown skill: ${params.uri}`);\n }\n return { skill: toWireEntry(skill) };\n },\n );\n\n server.server.setRequestHandler(\n \"resources/directory/read\",\n { params: DirectoryReadParamsSchema },\n (params) => {\n const { name, relPath } = skillUriToRelPath(params.uri);\n const skill = byUri.get(`skill://${name}/SKILL.md`);\n const entries = skill ? listDir(skill, relPath) : null;\n if (!entries) {\n throw new ProtocolError(\n INVALID_PARAMS,\n `Not a directory: ${params.uri}`,\n );\n }\n const base = params.uri.replace(/\\/+$/, \"\");\n return {\n resources: entries.map((entry) => ({\n uri: `${base}/${entry.name}`,\n name: entry.name,\n mimeType: entry.mimeType,\n })),\n };\n },\n );\n}\n"]}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// @vitest-environment node
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { mkdirSync, mkdtempSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
5
|
-
import { ResourceTemplate } from "@modelcontextprotocol/
|
|
6
|
+
import { ResourceTemplate } from "@modelcontextprotocol/server";
|
|
6
7
|
import { describe, expect, it, vi } from "vitest";
|
|
7
|
-
import { discoverSkills, registerSkills,
|
|
8
|
+
import { discoverSkills, registerSkills, skillUriToRelPath, } from "./skills.js";
|
|
8
9
|
function mkSkillDir(files) {
|
|
9
10
|
const root = mkdtempSync(join(tmpdir(), "skybridge-skills-"));
|
|
10
11
|
for (const [rel, content] of Object.entries(files)) {
|
|
@@ -15,23 +16,32 @@ function mkSkillDir(files) {
|
|
|
15
16
|
return root;
|
|
16
17
|
}
|
|
17
18
|
const FM = (name, description) => `---\nname: ${name}\ndescription: ${description}\n---\n`;
|
|
19
|
+
const digestOf = (content) => `sha256:${createHash("sha256").update(content, "utf8").digest("hex")}`;
|
|
18
20
|
describe("discoverSkills", () => {
|
|
19
|
-
it("discovers a skill with frontmatter
|
|
21
|
+
it("discovers a skill with frontmatter and per-file resources", () => {
|
|
22
|
+
const skillMd = `${FM("git-workflow", "Team git conventions")}Body`;
|
|
20
23
|
const dir = mkSkillDir({
|
|
21
|
-
"git-workflow/SKILL.md":
|
|
24
|
+
"git-workflow/SKILL.md": skillMd,
|
|
22
25
|
"git-workflow/references/GUIDE.md": "# Guide",
|
|
23
26
|
});
|
|
24
27
|
const [skill, ...rest] = discoverSkills(dir);
|
|
25
28
|
expect(rest).toHaveLength(0);
|
|
26
|
-
expect(skill?.
|
|
29
|
+
expect(skill?.uri).toBe("skill://git-workflow/SKILL.md");
|
|
27
30
|
expect(skill?.frontmatter).toMatchObject({
|
|
28
31
|
name: "git-workflow",
|
|
29
32
|
description: "Team git conventions",
|
|
30
33
|
});
|
|
31
|
-
expect(skill?.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
expect(skill?.resources).toEqual([
|
|
35
|
+
{
|
|
36
|
+
uri: "skill://git-workflow/SKILL.md",
|
|
37
|
+
digest: digestOf(skillMd),
|
|
38
|
+
content: skillMd,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
uri: "skill://git-workflow/references/GUIDE.md",
|
|
42
|
+
digest: digestOf("# Guide"),
|
|
43
|
+
content: "# Guide",
|
|
44
|
+
},
|
|
35
45
|
]);
|
|
36
46
|
});
|
|
37
47
|
it("returns [] for a missing directory and skips dirs without SKILL.md", () => {
|
|
@@ -96,18 +106,25 @@ describe("skillUriToRelPath", () => {
|
|
|
96
106
|
describe("registerSkills", () => {
|
|
97
107
|
const manifest = [
|
|
98
108
|
{
|
|
99
|
-
|
|
109
|
+
uri: "skill://refunds/SKILL.md",
|
|
100
110
|
frontmatter: { name: "refunds", description: "Process refunds" },
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
111
|
+
resources: [
|
|
112
|
+
{
|
|
113
|
+
uri: "skill://refunds/SKILL.md",
|
|
114
|
+
digest: digestOf("# Refunds"),
|
|
115
|
+
content: "# Refunds",
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
uri: "skill://refunds/templates/email.md",
|
|
119
|
+
digest: digestOf("Hi"),
|
|
120
|
+
content: "Hi",
|
|
121
|
+
},
|
|
122
|
+
],
|
|
106
123
|
},
|
|
107
124
|
];
|
|
108
125
|
function fakeRegistrar() {
|
|
109
126
|
const resources = new Map();
|
|
110
|
-
|
|
127
|
+
const handlers = new Map();
|
|
111
128
|
const server = {
|
|
112
129
|
registerResource: vi.fn((name, uri, _cfg, cb) => {
|
|
113
130
|
resources.set(name, {
|
|
@@ -116,29 +133,13 @@ describe("registerSkills", () => {
|
|
|
116
133
|
});
|
|
117
134
|
}),
|
|
118
135
|
server: {
|
|
119
|
-
setRequestHandler: vi.fn((
|
|
120
|
-
|
|
136
|
+
setRequestHandler: vi.fn((method, _schemas, handler) => {
|
|
137
|
+
handlers.set(method, handler);
|
|
121
138
|
}),
|
|
122
139
|
},
|
|
123
140
|
};
|
|
124
|
-
return { server, resources,
|
|
141
|
+
return { server, resources, handlers };
|
|
125
142
|
}
|
|
126
|
-
it("builds an index.json with url, digest, and verbatim frontmatter", () => {
|
|
127
|
-
const { server, resources } = fakeRegistrar();
|
|
128
|
-
// biome-ignore lint/suspicious/noExplicitAny: structural test double
|
|
129
|
-
registerSkills(server, manifest);
|
|
130
|
-
const index = resources.get("skill-index");
|
|
131
|
-
const result = index?.cb(new URL(SKILL_INDEX_URI));
|
|
132
|
-
expect(JSON.parse(result.contents[0]?.text ?? "")).toEqual({
|
|
133
|
-
skills: [
|
|
134
|
-
{
|
|
135
|
-
url: "skill://refunds/SKILL.md",
|
|
136
|
-
digest: "sha256:abc",
|
|
137
|
-
frontmatter: { name: "refunds", description: "Process refunds" },
|
|
138
|
-
},
|
|
139
|
-
],
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
143
|
it("serves supporting files through the template resource", () => {
|
|
143
144
|
const { server, resources } = fakeRegistrar();
|
|
144
145
|
// biome-ignore lint/suspicious/noExplicitAny: structural test double
|
|
@@ -153,8 +154,8 @@ describe("registerSkills", () => {
|
|
|
153
154
|
const on = fakeRegistrar();
|
|
154
155
|
// biome-ignore lint/suspicious/noExplicitAny: structural test double
|
|
155
156
|
registerSkills(on.server, manifest);
|
|
156
|
-
const result = on.
|
|
157
|
-
|
|
157
|
+
const result = on.handlers.get("resources/directory/read")?.({
|
|
158
|
+
uri: "skill://refunds/templates",
|
|
158
159
|
});
|
|
159
160
|
expect(result.resources).toEqual([
|
|
160
161
|
{
|
|
@@ -165,11 +166,11 @@ describe("registerSkills", () => {
|
|
|
165
166
|
]);
|
|
166
167
|
});
|
|
167
168
|
it("lists a skill root and marks subdirectories as inode/directory", () => {
|
|
168
|
-
const { server,
|
|
169
|
+
const { server, handlers } = fakeRegistrar();
|
|
169
170
|
// biome-ignore lint/suspicious/noExplicitAny: structural test double
|
|
170
171
|
registerSkills(server, manifest);
|
|
171
|
-
const result =
|
|
172
|
-
|
|
172
|
+
const result = handlers.get("resources/directory/read")?.({
|
|
173
|
+
uri: "skill://refunds",
|
|
173
174
|
});
|
|
174
175
|
expect(result.resources).toContainEqual({
|
|
175
176
|
uri: "skill://refunds/templates",
|
|
@@ -182,6 +183,38 @@ describe("registerSkills", () => {
|
|
|
182
183
|
mimeType: "text/markdown",
|
|
183
184
|
});
|
|
184
185
|
});
|
|
186
|
+
it("lists entries with complete per-file resources via skills/list", () => {
|
|
187
|
+
const { server, handlers } = fakeRegistrar();
|
|
188
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural test double
|
|
189
|
+
registerSkills(server, manifest);
|
|
190
|
+
const result = handlers.get("skills/list")?.({});
|
|
191
|
+
expect(result).toEqual({
|
|
192
|
+
skills: [
|
|
193
|
+
{
|
|
194
|
+
uri: "skill://refunds/SKILL.md",
|
|
195
|
+
frontmatter: { name: "refunds", description: "Process refunds" },
|
|
196
|
+
resources: [
|
|
197
|
+
{ uri: "skill://refunds/SKILL.md", digest: digestOf("# Refunds") },
|
|
198
|
+
{
|
|
199
|
+
uri: "skill://refunds/templates/email.md",
|
|
200
|
+
digest: digestOf("Hi"),
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
it("returns a single entry via skills/get and rejects non-skill URIs", () => {
|
|
208
|
+
const { server, handlers } = fakeRegistrar();
|
|
209
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural test double
|
|
210
|
+
registerSkills(server, manifest);
|
|
211
|
+
const get = handlers.get("skills/get");
|
|
212
|
+
const result = get?.({ uri: "skill://refunds/SKILL.md" });
|
|
213
|
+
expect(result.skill.uri).toBe("skill://refunds/SKILL.md");
|
|
214
|
+
expect(result.skill.resources).toHaveLength(2);
|
|
215
|
+
expect(() => get?.({ uri: "skill://unknown/SKILL.md" })).toThrow(/Unknown skill/);
|
|
216
|
+
expect(() => get?.({ uri: "skill://refunds/templates/email.md" })).toThrow(/Unknown skill/);
|
|
217
|
+
});
|
|
185
218
|
});
|
|
186
219
|
describe("discoverSkills symlink safety", () => {
|
|
187
220
|
it("excludes symlinked files and directories from the manifest", () => {
|
|
@@ -193,8 +226,8 @@ describe("discoverSkills symlink safety", () => {
|
|
|
193
226
|
});
|
|
194
227
|
symlinkSync(join(outside, "secret.md"), join(dir, "demo", "leak.md"));
|
|
195
228
|
symlinkSync(outside, join(dir, "demo", "escape"));
|
|
196
|
-
const
|
|
197
|
-
expect(
|
|
229
|
+
const uris = discoverSkills(dir)[0]?.resources.map((r) => r.uri) ?? [];
|
|
230
|
+
expect(uris).toEqual(["skill://demo/SKILL.md", "skill://demo/real.md"]);
|
|
198
231
|
});
|
|
199
232
|
it("ignores non-markdown supporting files", () => {
|
|
200
233
|
const dir = mkSkillDir({
|
|
@@ -203,8 +236,8 @@ describe("discoverSkills symlink safety", () => {
|
|
|
203
236
|
"demo/data.json": "{}",
|
|
204
237
|
"demo/scripts/run.py": "print(1)",
|
|
205
238
|
});
|
|
206
|
-
const
|
|
207
|
-
expect(
|
|
239
|
+
const uris = discoverSkills(dir)[0]?.resources.map((r) => r.uri) ?? [];
|
|
240
|
+
expect(uris).toEqual(["skill://demo/SKILL.md", "skill://demo/notes.md"]);
|
|
208
241
|
});
|
|
209
242
|
});
|
|
210
243
|
//# sourceMappingURL=skills.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skills.test.js","sourceRoot":"","sources":["../../src/server/skills.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\nimport { mkdirSync, mkdtempSync, symlinkSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { ResourceTemplate } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { describe, expect, it, vi } from \"vitest\";\nimport {\n discoverSkills,\n registerSkills,\n SKILL_INDEX_URI,\n type Skill,\n skillUriToRelPath,\n} from \"./skills.js\";\n\nfunction mkSkillDir(files: Record<string, string>): string {\n const root = mkdtempSync(join(tmpdir(), \"skybridge-skills-\"));\n for (const [rel, content] of Object.entries(files)) {\n const full = join(root, rel);\n mkdirSync(join(full, \"..\"), { recursive: true });\n writeFileSync(full, content);\n }\n return root;\n}\n\nconst FM = (name: string, description: string) =>\n `---\\nname: ${name}\\ndescription: ${description}\\n---\\n`;\n\ndescribe(\"discoverSkills\", () => {\n it(\"discovers a skill with frontmatter, digest, and supporting files\", () => {\n const dir = mkSkillDir({\n \"git-workflow/SKILL.md\": `${FM(\"git-workflow\", \"Team git conventions\")}Body`,\n \"git-workflow/references/GUIDE.md\": \"# Guide\",\n });\n const [skill, ...rest] = discoverSkills(dir);\n expect(rest).toHaveLength(0);\n expect(skill?.name).toBe(\"git-workflow\");\n expect(skill?.frontmatter).toMatchObject({\n name: \"git-workflow\",\n description: \"Team git conventions\",\n });\n expect(skill?.digest).toMatch(/^sha256:[a-f0-9]{64}$/);\n expect(Object.keys(skill?.files ?? {}).sort()).toEqual([\n \"SKILL.md\",\n \"references/GUIDE.md\",\n ]);\n });\n\n it(\"returns [] for a missing directory and skips dirs without SKILL.md\", () => {\n expect(discoverSkills(join(tmpdir(), \"does-not-exist-xyz\"))).toEqual([]);\n const dir = mkSkillDir({ \"not-a-skill/README.md\": \"hi\" });\n expect(discoverSkills(dir)).toEqual([]);\n });\n\n it(\"parses a nested metadata map in frontmatter\", () => {\n const dir = mkSkillDir({\n \"s/SKILL.md\": `---\\nname: s\\ndescription: d\\nmetadata:\\n version: \"1.2\"\\n team: core\\n---\\n`,\n });\n expect(discoverSkills(dir)[0]?.frontmatter.metadata).toEqual({\n version: \"1.2\",\n team: \"core\",\n });\n });\n\n it(\"throws on missing frontmatter\", () => {\n const dir = mkSkillDir({ \"s/SKILL.md\": \"no frontmatter here\" });\n expect(() => discoverSkills(dir)).toThrow(/missing YAML frontmatter/);\n });\n\n it(\"throws when required fields are absent\", () => {\n const dir = mkSkillDir({ \"s/SKILL.md\": \"---\\nname: s\\n---\\n\" });\n expect(() => discoverSkills(dir)).toThrow(/Invalid skill frontmatter/);\n });\n\n it(\"throws when the frontmatter name does not match the directory\", () => {\n const dir = mkSkillDir({ \"s/SKILL.md\": FM(\"other\", \"d\") });\n expect(() => discoverSkills(dir)).toThrow(/must match its directory name/);\n });\n\n it(\"preserves arbitrary YAML frontmatter (arrays, nesting) verbatim\", () => {\n const dir = mkSkillDir({\n \"s/SKILL.md\":\n \"---\\nname: s\\ndescription: d\\nallowed-tools:\\n - get-order\\n - refund\\n---\\n\",\n });\n expect(discoverSkills(dir)[0]?.frontmatter[\"allowed-tools\"]).toEqual([\n \"get-order\",\n \"refund\",\n ]);\n });\n\n it(\"throws on malformed YAML frontmatter\", () => {\n const dir = mkSkillDir({\n \"s/SKILL.md\": \"---\\nname: 's\\ndescription: [unclosed\\n---\\n\",\n });\n expect(() => discoverSkills(dir)).toThrow(/Cannot parse frontmatter/);\n });\n});\n\ndescribe(\"skillUriToRelPath\", () => {\n it(\"splits name and relative path\", () => {\n expect(skillUriToRelPath(\"skill://refunds/SKILL.md\")).toEqual({\n name: \"refunds\",\n relPath: \"SKILL.md\",\n });\n expect(skillUriToRelPath(\"skill://refunds\")).toEqual({\n name: \"refunds\",\n relPath: \"\",\n });\n });\n\n it(\"rejects path traversal and malformed URIs\", () => {\n expect(() => skillUriToRelPath(\"skill://s/../../etc/passwd\")).toThrow();\n expect(() => skillUriToRelPath(\"skill://s/./x\")).toThrow();\n expect(() => skillUriToRelPath(\"https://example.com\")).toThrow();\n });\n});\n\ndescribe(\"registerSkills\", () => {\n const manifest: Skill[] = [\n {\n name: \"refunds\",\n frontmatter: { name: \"refunds\", description: \"Process refunds\" },\n digest: \"sha256:abc\",\n files: {\n \"SKILL.md\": \"# Refunds\",\n \"templates/email.md\": \"Hi\",\n },\n },\n ];\n\n function fakeRegistrar() {\n const resources = new Map<\n string,\n { uri: unknown; cb: (...args: unknown[]) => unknown }\n >();\n let dirHandler: ((req: unknown) => unknown) | undefined;\n const server = {\n registerResource: vi.fn(\n (name: string, uri: unknown, _cfg: unknown, cb: unknown) => {\n resources.set(name, {\n uri,\n cb: cb as (...args: unknown[]) => unknown,\n });\n },\n ),\n server: {\n setRequestHandler: vi.fn((_schema: unknown, handler: unknown) => {\n dirHandler = handler as (req: unknown) => unknown;\n }),\n },\n };\n return { server, resources, getDirHandler: () => dirHandler };\n }\n\n it(\"builds an index.json with url, digest, and verbatim frontmatter\", () => {\n const { server, resources } = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(server as any, manifest);\n const index = resources.get(\"skill-index\");\n const result = index?.cb(new URL(SKILL_INDEX_URI)) as {\n contents: { text: string }[];\n };\n expect(JSON.parse(result.contents[0]?.text ?? \"\")).toEqual({\n skills: [\n {\n url: \"skill://refunds/SKILL.md\",\n digest: \"sha256:abc\",\n frontmatter: { name: \"refunds\", description: \"Process refunds\" },\n },\n ],\n });\n });\n\n it(\"serves supporting files through the template resource\", () => {\n const { server, resources } = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(server as any, manifest);\n const tpl = resources.get(\"skill-files\");\n expect(tpl?.uri).toBeInstanceOf(ResourceTemplate);\n const url = new URL(\"skill://refunds/templates/email.md\");\n const result = tpl?.cb(url, {}, {}) as { contents: { text: string }[] };\n expect(result.contents[0]?.text).toBe(\"Hi\");\n });\n\n it(\"lists a subdirectory through the directory-read handler\", () => {\n const on = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(on.server as any, manifest);\n const result = on.getDirHandler()?.({\n params: { uri: \"skill://refunds/templates\" },\n }) as { resources: { uri: string; name: string; mimeType: string }[] };\n expect(result.resources).toEqual([\n {\n uri: \"skill://refunds/templates/email.md\",\n name: \"email.md\",\n mimeType: \"text/markdown\",\n },\n ]);\n });\n\n it(\"lists a skill root and marks subdirectories as inode/directory\", () => {\n const { server, getDirHandler } = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(server as any, manifest);\n const result = getDirHandler()?.({\n params: { uri: \"skill://refunds\" },\n }) as { resources: { name: string; mimeType: string }[] };\n expect(result.resources).toContainEqual({\n uri: \"skill://refunds/templates\",\n name: \"templates\",\n mimeType: \"inode/directory\",\n });\n expect(result.resources).toContainEqual({\n uri: \"skill://refunds/SKILL.md\",\n name: \"SKILL.md\",\n mimeType: \"text/markdown\",\n });\n });\n});\n\ndescribe(\"discoverSkills symlink safety\", () => {\n it(\"excludes symlinked files and directories from the manifest\", () => {\n const outside = mkdtempSync(join(tmpdir(), \"skybridge-outside-\"));\n writeFileSync(join(outside, \"secret.md\"), \"TOP SECRET\");\n const dir = mkSkillDir({\n \"demo/SKILL.md\": FM(\"demo\", \"d\"),\n \"demo/real.md\": \"ok\",\n });\n symlinkSync(join(outside, \"secret.md\"), join(dir, \"demo\", \"leak.md\"));\n symlinkSync(outside, join(dir, \"demo\", \"escape\"));\n\n const files = discoverSkills(dir)[0]?.files ?? {};\n expect(Object.keys(files).sort()).toEqual([\"SKILL.md\", \"real.md\"]);\n });\n\n it(\"ignores non-markdown supporting files\", () => {\n const dir = mkSkillDir({\n \"demo/SKILL.md\": FM(\"demo\", \"d\"),\n \"demo/notes.md\": \"kept\",\n \"demo/data.json\": \"{}\",\n \"demo/scripts/run.py\": \"print(1)\",\n });\n const files = discoverSkills(dir)[0]?.files ?? {};\n expect(Object.keys(files).sort()).toEqual([\"SKILL.md\", \"notes.md\"]);\n });\n});\n"]}
|
|
1
|
+
{"version":3,"file":"skills.test.js","sourceRoot":"","sources":["../../src/server/skills.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\nimport { createHash } from \"node:crypto\";\nimport { mkdirSync, mkdtempSync, symlinkSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { ResourceTemplate } from \"@modelcontextprotocol/server\";\nimport { describe, expect, it, vi } from \"vitest\";\nimport {\n discoverSkills,\n registerSkills,\n type Skill,\n skillUriToRelPath,\n} from \"./skills.js\";\n\nfunction mkSkillDir(files: Record<string, string>): string {\n const root = mkdtempSync(join(tmpdir(), \"skybridge-skills-\"));\n for (const [rel, content] of Object.entries(files)) {\n const full = join(root, rel);\n mkdirSync(join(full, \"..\"), { recursive: true });\n writeFileSync(full, content);\n }\n return root;\n}\n\nconst FM = (name: string, description: string) =>\n `---\\nname: ${name}\\ndescription: ${description}\\n---\\n`;\n\nconst digestOf = (content: string) =>\n `sha256:${createHash(\"sha256\").update(content, \"utf8\").digest(\"hex\")}`;\n\ndescribe(\"discoverSkills\", () => {\n it(\"discovers a skill with frontmatter and per-file resources\", () => {\n const skillMd = `${FM(\"git-workflow\", \"Team git conventions\")}Body`;\n const dir = mkSkillDir({\n \"git-workflow/SKILL.md\": skillMd,\n \"git-workflow/references/GUIDE.md\": \"# Guide\",\n });\n const [skill, ...rest] = discoverSkills(dir);\n expect(rest).toHaveLength(0);\n expect(skill?.uri).toBe(\"skill://git-workflow/SKILL.md\");\n expect(skill?.frontmatter).toMatchObject({\n name: \"git-workflow\",\n description: \"Team git conventions\",\n });\n expect(skill?.resources).toEqual([\n {\n uri: \"skill://git-workflow/SKILL.md\",\n digest: digestOf(skillMd),\n content: skillMd,\n },\n {\n uri: \"skill://git-workflow/references/GUIDE.md\",\n digest: digestOf(\"# Guide\"),\n content: \"# Guide\",\n },\n ]);\n });\n\n it(\"returns [] for a missing directory and skips dirs without SKILL.md\", () => {\n expect(discoverSkills(join(tmpdir(), \"does-not-exist-xyz\"))).toEqual([]);\n const dir = mkSkillDir({ \"not-a-skill/README.md\": \"hi\" });\n expect(discoverSkills(dir)).toEqual([]);\n });\n\n it(\"parses a nested metadata map in frontmatter\", () => {\n const dir = mkSkillDir({\n \"s/SKILL.md\": `---\\nname: s\\ndescription: d\\nmetadata:\\n version: \"1.2\"\\n team: core\\n---\\n`,\n });\n expect(discoverSkills(dir)[0]?.frontmatter.metadata).toEqual({\n version: \"1.2\",\n team: \"core\",\n });\n });\n\n it(\"throws on missing frontmatter\", () => {\n const dir = mkSkillDir({ \"s/SKILL.md\": \"no frontmatter here\" });\n expect(() => discoverSkills(dir)).toThrow(/missing YAML frontmatter/);\n });\n\n it(\"throws when required fields are absent\", () => {\n const dir = mkSkillDir({ \"s/SKILL.md\": \"---\\nname: s\\n---\\n\" });\n expect(() => discoverSkills(dir)).toThrow(/Invalid skill frontmatter/);\n });\n\n it(\"throws when the frontmatter name does not match the directory\", () => {\n const dir = mkSkillDir({ \"s/SKILL.md\": FM(\"other\", \"d\") });\n expect(() => discoverSkills(dir)).toThrow(/must match its directory name/);\n });\n\n it(\"preserves arbitrary YAML frontmatter (arrays, nesting) verbatim\", () => {\n const dir = mkSkillDir({\n \"s/SKILL.md\":\n \"---\\nname: s\\ndescription: d\\nallowed-tools:\\n - get-order\\n - refund\\n---\\n\",\n });\n expect(discoverSkills(dir)[0]?.frontmatter[\"allowed-tools\"]).toEqual([\n \"get-order\",\n \"refund\",\n ]);\n });\n\n it(\"throws on malformed YAML frontmatter\", () => {\n const dir = mkSkillDir({\n \"s/SKILL.md\": \"---\\nname: 's\\ndescription: [unclosed\\n---\\n\",\n });\n expect(() => discoverSkills(dir)).toThrow(/Cannot parse frontmatter/);\n });\n});\n\ndescribe(\"skillUriToRelPath\", () => {\n it(\"splits name and relative path\", () => {\n expect(skillUriToRelPath(\"skill://refunds/SKILL.md\")).toEqual({\n name: \"refunds\",\n relPath: \"SKILL.md\",\n });\n expect(skillUriToRelPath(\"skill://refunds\")).toEqual({\n name: \"refunds\",\n relPath: \"\",\n });\n });\n\n it(\"rejects path traversal and malformed URIs\", () => {\n expect(() => skillUriToRelPath(\"skill://s/../../etc/passwd\")).toThrow();\n expect(() => skillUriToRelPath(\"skill://s/./x\")).toThrow();\n expect(() => skillUriToRelPath(\"https://example.com\")).toThrow();\n });\n});\n\ndescribe(\"registerSkills\", () => {\n const manifest: Skill[] = [\n {\n uri: \"skill://refunds/SKILL.md\",\n frontmatter: { name: \"refunds\", description: \"Process refunds\" },\n resources: [\n {\n uri: \"skill://refunds/SKILL.md\",\n digest: digestOf(\"# Refunds\"),\n content: \"# Refunds\",\n },\n {\n uri: \"skill://refunds/templates/email.md\",\n digest: digestOf(\"Hi\"),\n content: \"Hi\",\n },\n ],\n },\n ];\n\n function fakeRegistrar() {\n const resources = new Map<\n string,\n { uri: unknown; cb: (...args: unknown[]) => unknown }\n >();\n const handlers = new Map<string, (req: unknown) => unknown>();\n const server = {\n registerResource: vi.fn(\n (name: string, uri: unknown, _cfg: unknown, cb: unknown) => {\n resources.set(name, {\n uri,\n cb: cb as (...args: unknown[]) => unknown,\n });\n },\n ),\n server: {\n setRequestHandler: vi.fn(\n (method: unknown, _schemas: unknown, handler: unknown) => {\n handlers.set(\n method as string,\n handler as (req: unknown) => unknown,\n );\n },\n ),\n },\n };\n return { server, resources, handlers };\n }\n\n it(\"serves supporting files through the template resource\", () => {\n const { server, resources } = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(server as any, manifest);\n const tpl = resources.get(\"skill-files\");\n expect(tpl?.uri).toBeInstanceOf(ResourceTemplate);\n const url = new URL(\"skill://refunds/templates/email.md\");\n const result = tpl?.cb(url, {}, {}) as { contents: { text: string }[] };\n expect(result.contents[0]?.text).toBe(\"Hi\");\n });\n\n it(\"lists a subdirectory through the directory-read handler\", () => {\n const on = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(on.server as any, manifest);\n const result = on.handlers.get(\"resources/directory/read\")?.({\n uri: \"skill://refunds/templates\",\n }) as { resources: { uri: string; name: string; mimeType: string }[] };\n expect(result.resources).toEqual([\n {\n uri: \"skill://refunds/templates/email.md\",\n name: \"email.md\",\n mimeType: \"text/markdown\",\n },\n ]);\n });\n\n it(\"lists a skill root and marks subdirectories as inode/directory\", () => {\n const { server, handlers } = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(server as any, manifest);\n const result = handlers.get(\"resources/directory/read\")?.({\n uri: \"skill://refunds\",\n }) as { resources: { name: string; mimeType: string }[] };\n expect(result.resources).toContainEqual({\n uri: \"skill://refunds/templates\",\n name: \"templates\",\n mimeType: \"inode/directory\",\n });\n expect(result.resources).toContainEqual({\n uri: \"skill://refunds/SKILL.md\",\n name: \"SKILL.md\",\n mimeType: \"text/markdown\",\n });\n });\n\n it(\"lists entries with complete per-file resources via skills/list\", () => {\n const { server, handlers } = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(server as any, manifest);\n const result = handlers.get(\"skills/list\")?.({});\n expect(result).toEqual({\n skills: [\n {\n uri: \"skill://refunds/SKILL.md\",\n frontmatter: { name: \"refunds\", description: \"Process refunds\" },\n resources: [\n { uri: \"skill://refunds/SKILL.md\", digest: digestOf(\"# Refunds\") },\n {\n uri: \"skill://refunds/templates/email.md\",\n digest: digestOf(\"Hi\"),\n },\n ],\n },\n ],\n });\n });\n\n it(\"returns a single entry via skills/get and rejects non-skill URIs\", () => {\n const { server, handlers } = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(server as any, manifest);\n const get = handlers.get(\"skills/get\");\n const result = get?.({ uri: \"skill://refunds/SKILL.md\" }) as {\n skill: { uri: string; resources: unknown[] };\n };\n expect(result.skill.uri).toBe(\"skill://refunds/SKILL.md\");\n expect(result.skill.resources).toHaveLength(2);\n expect(() => get?.({ uri: \"skill://unknown/SKILL.md\" })).toThrow(\n /Unknown skill/,\n );\n expect(() => get?.({ uri: \"skill://refunds/templates/email.md\" })).toThrow(\n /Unknown skill/,\n );\n });\n});\n\ndescribe(\"discoverSkills symlink safety\", () => {\n it(\"excludes symlinked files and directories from the manifest\", () => {\n const outside = mkdtempSync(join(tmpdir(), \"skybridge-outside-\"));\n writeFileSync(join(outside, \"secret.md\"), \"TOP SECRET\");\n const dir = mkSkillDir({\n \"demo/SKILL.md\": FM(\"demo\", \"d\"),\n \"demo/real.md\": \"ok\",\n });\n symlinkSync(join(outside, \"secret.md\"), join(dir, \"demo\", \"leak.md\"));\n symlinkSync(outside, join(dir, \"demo\", \"escape\"));\n\n const uris = discoverSkills(dir)[0]?.resources.map((r) => r.uri) ?? [];\n expect(uris).toEqual([\"skill://demo/SKILL.md\", \"skill://demo/real.md\"]);\n });\n\n it(\"ignores non-markdown supporting files\", () => {\n const dir = mkSkillDir({\n \"demo/SKILL.md\": FM(\"demo\", \"d\"),\n \"demo/notes.md\": \"kept\",\n \"demo/data.json\": \"{}\",\n \"demo/scripts/run.py\": \"print(1)\",\n });\n const uris = discoverSkills(dir)[0]?.resources.map((r) => r.uri) ?? [];\n expect(uris).toEqual([\"skill://demo/SKILL.md\", \"skill://demo/notes.md\"]);\n });\n});\n"]}
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import type { ViewHostType } from "./server.js";
|
|
2
1
|
declare class TemplateHelper {
|
|
3
2
|
renderProduction(data: {
|
|
4
|
-
hostType: ViewHostType;
|
|
5
3
|
serverUrl: string;
|
|
6
4
|
viewFile: string;
|
|
7
5
|
styleFile: string;
|
|
8
6
|
}): string;
|
|
9
7
|
renderDevelopment(data: {
|
|
10
|
-
hostType: ViewHostType;
|
|
11
8
|
serverUrl: string;
|
|
12
9
|
viewName: string;
|
|
13
10
|
}): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templateHelper.js","sourceRoot":"","sources":["../../src/server/templateHelper.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"templateHelper.js","sourceRoot":"","sources":["../../src/server/templateHelper.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC;AAElC,MAAM,cAAc;IAClB,gBAAgB,CAAC,IAIhB;QACC,OAAO,kBAAkB,CAAC,IAAI,CAAC,CAAC;IAClC,CAAC;IAED,iBAAiB,CAAC,IAA6C;QAC7D,OAAO,mBAAmB,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC;CACF;AAED,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,cAAc,EAAE,CAAC","sourcesContent":["import {\n developmentTemplate,\n productionTemplate,\n} from \"./templates.generated.js\";\n\nclass TemplateHelper {\n renderProduction(data: {\n serverUrl: string;\n viewFile: string;\n styleFile: string;\n }): string {\n return productionTemplate(data);\n }\n\n renderDevelopment(data: { serverUrl: string; viewName: string }): string {\n return developmentTemplate(data);\n }\n}\n\nexport const templateHelper = new TemplateHelper();\n"]}
|