skybridge 1.3.2 → 1.3.4

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.
@@ -41,10 +41,12 @@ describe("emitSkillsModule", () => {
41
41
  const skills = JSON.parse(literal);
42
42
  expect(skills).toHaveLength(1);
43
43
  expect(skills[0]).toMatchObject({
44
- name: "refunds",
44
+ uri: "skill://refunds/SKILL.md",
45
45
  frontmatter: { name: "refunds", description: "Process refunds" },
46
46
  });
47
- expect(skills[0].digest).toMatch(/^sha256:[a-f0-9]{64}$/);
47
+ expect(skills[0].resources).toHaveLength(1);
48
+ expect(skills[0].resources[0].uri).toBe("skill://refunds/SKILL.md");
49
+ expect(skills[0].resources[0].digest).toMatch(/^sha256:[a-f0-9]{64}$/);
48
50
  });
49
51
  it("emits an empty array when the skills directory is absent", () => {
50
52
  const dir = mkTmp();
@@ -1 +1 @@
1
- {"version":3,"file":"build-helpers.test.js","sourceRoot":"","sources":["../../src/cli/build-helpers.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\n// esbuild's invariant check on TextEncoder/Uint8Array trips jsdom's polyfill.\nimport {\n existsSync,\n mkdirSync,\n mkdtempSync,\n readFileSync,\n writeFileSync,\n} from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport path from \"node:path\";\nimport { describe, expect, it } from \"vitest\";\nimport {\n ENTRY_WRAPPER_CONTENT,\n emitEmptyManifestModule,\n emitEntryWrapper,\n emitManifestModule,\n emitSkillsModule,\n emitVercelBuildOutput,\n ensureAssetsDir,\n VERCEL_CONFIG,\n VERCEL_VC_CONFIG,\n} from \"./build-helpers.js\";\n\nfunction mkTmp(prefix = \"skybridge-build-helpers-\") {\n return mkdtempSync(path.join(tmpdir(), prefix));\n}\n\ndescribe(\"emitEntryWrapper\", () => {\n it(\"writes dist/__entry.js that primes the manifest before importing user code\", () => {\n const dir = mkTmp();\n emitEntryWrapper(dir);\n const out = readFileSync(path.join(dir, \"__entry.js\"), \"utf-8\");\n expect(out).toBe(ENTRY_WRAPPER_CONTENT);\n expect(out).toContain(\n 'import { __setBuildManifest, __setSkillsManifest } from \"skybridge/server\"',\n );\n expect(out).toContain('import manifest from \"./vite-manifest.js\"');\n expect(out).toContain('import skills from \"./skills.js\"');\n expect(out).toContain(\"__setBuildManifest(manifest)\");\n expect(out).toContain(\"__setSkillsManifest(skills)\");\n // Dynamic import is load-bearing: `server.js` must evaluate after the\n // setters run, so a static re-export wouldn't work.\n expect(out).toContain('await import(\"./server.js\")');\n expect(out).toContain(\"export default userMod.default\");\n });\n});\n\ndescribe(\"emitSkillsModule\", () => {\n it(\"inlines discovered skills as an ESM default export\", () => {\n const dir = mkTmp();\n const skillsDir = path.join(dir, \"src\", \"skills\");\n mkdirSync(path.join(skillsDir, \"refunds\"), { recursive: true });\n writeFileSync(\n path.join(skillsDir, \"refunds\", \"SKILL.md\"),\n \"---\\nname: refunds\\ndescription: Process refunds\\n---\\nBody\",\n );\n const outPath = path.join(dir, \"skills.js\");\n emitSkillsModule(skillsDir, outPath);\n const out = readFileSync(outPath, \"utf-8\");\n const literal = out\n .slice(\"export default \".length)\n .trim()\n .replace(/;$/, \"\");\n const skills = JSON.parse(literal);\n expect(skills).toHaveLength(1);\n expect(skills[0]).toMatchObject({\n name: \"refunds\",\n frontmatter: { name: \"refunds\", description: \"Process refunds\" },\n });\n expect(skills[0].digest).toMatch(/^sha256:[a-f0-9]{64}$/);\n });\n\n it(\"emits an empty array when the skills directory is absent\", () => {\n const dir = mkTmp();\n const outPath = path.join(dir, \"skills.js\");\n emitSkillsModule(path.join(dir, \"nope\"), outPath);\n expect(readFileSync(outPath, \"utf-8\")).toBe(\"export default [];\\n\");\n });\n});\n\ndescribe(\"emitManifestModule\", () => {\n it(\"inlines the JSON manifest as an ESM default export\", () => {\n const dir = mkTmp();\n const inPath = path.join(dir, \"manifest.json\");\n const outPath = path.join(dir, \"vite-manifest.js\");\n const manifest = { \"src/views/foo.tsx\": { file: \"assets/foo-abc.js\" } };\n writeFileSync(inPath, JSON.stringify(manifest));\n emitManifestModule(inPath, outPath);\n const out = readFileSync(outPath, \"utf-8\");\n expect(out.startsWith(\"export default \")).toBe(true);\n const literal = out\n .slice(\"export default \".length)\n .trim()\n .replace(/;$/, \"\");\n expect(JSON.parse(literal)).toEqual(manifest);\n });\n});\n\ndescribe(\"emitEmptyManifestModule\", () => {\n it(\"writes an empty object default export\", () => {\n const dir = mkTmp();\n const outPath = path.join(dir, \"vite-manifest.js\");\n emitEmptyManifestModule(outPath);\n expect(readFileSync(outPath, \"utf-8\")).toBe(\"export default {};\\n\");\n });\n});\n\ndescribe(\"ensureAssetsDir\", () => {\n it(\"creates the assets directory when missing\", () => {\n const dir = mkTmp();\n const assetsDir = path.join(dir, \"dist\", \"assets\");\n ensureAssetsDir(assetsDir);\n expect(existsSync(assetsDir)).toBe(true);\n });\n});\n\ndescribe(\"emitVercelBuildOutput\", () => {\n it(\"emits a Build Output API tree with bundled function and static assets\", async () => {\n const root = mkTmp();\n mkdirSync(path.join(root, \"dist\", \"assets\"), { recursive: true });\n writeFileSync(\n path.join(root, \"dist\", \"server.js\"),\n \"export default function handler(_req, res) { res.end('ok'); }\\n\",\n );\n // Minimal `dist/__entry.js` (the real wrapper imports `skybridge/server`,\n // which isn't resolvable in this test's tmp dir — the function-bundling\n // contract we care about here is just \"bundle whatever `__entry.js`\n // imports into a single function file\").\n writeFileSync(\n path.join(root, \"dist\", \"__entry.js\"),\n \"const userMod = await import('./server.js');\\nexport default userMod.default;\\n\",\n );\n writeFileSync(\n path.join(root, \"dist\", \"assets\", \"view-abc.js\"),\n \"/* bundled view */\\n\",\n );\n\n await emitVercelBuildOutput(root);\n\n const outputDir = path.join(root, \".vercel\", \"output\");\n const funcDir = path.join(outputDir, \"functions\", \"mcp.func\");\n\n expect(existsSync(path.join(funcDir, \"index.js\"))).toBe(true);\n expect(\n JSON.parse(readFileSync(path.join(funcDir, \".vc-config.json\"), \"utf-8\")),\n ).toEqual(VERCEL_VC_CONFIG);\n expect(\n JSON.parse(readFileSync(path.join(funcDir, \"package.json\"), \"utf-8\")),\n ).toEqual({ type: \"module\" });\n expect(\n JSON.parse(readFileSync(path.join(outputDir, \"config.json\"), \"utf-8\")),\n ).toEqual(VERCEL_CONFIG);\n expect(\n readFileSync(\n path.join(outputDir, \"static\", \"assets\", \"view-abc.js\"),\n \"utf-8\",\n ),\n ).toContain(\"bundled view\");\n });\n\n it(\"leaves serverExternal packages unbundled\", async () => {\n const root = mkTmp();\n mkdirSync(path.join(root, \"dist\"), { recursive: true });\n // Unresolvable from the tmp root, so esbuild fails the build unless the\n // package is marked external (bunyan → optional `dtrace-provider`).\n writeFileSync(\n path.join(root, \"dist\", \"__entry.js\"),\n 'import \"dtrace-provider\";\\nexport default null;\\n',\n );\n\n await expect(emitVercelBuildOutput(root)).rejects.toThrow(\n /Could not resolve \"dtrace-provider\"/,\n );\n await emitVercelBuildOutput(root, [\"dtrace-provider\"]);\n\n const bundle = readFileSync(\n path.join(root, \".vercel\", \"output\", \"functions\", \"mcp.func\", \"index.js\"),\n \"utf-8\",\n );\n expect(bundle).toContain('import \"dtrace-provider\"');\n });\n\n it(\"emits static assets directory when dist/assets is missing\", async () => {\n const root = mkTmp();\n mkdirSync(path.join(root, \"dist\"), { recursive: true });\n writeFileSync(\n path.join(root, \"dist\", \"__entry.js\"),\n \"const userMod = await import('./server.js');\\nexport default userMod.default;\\n\",\n );\n writeFileSync(\n path.join(root, \"dist\", \"server.js\"),\n \"export default function handler(_req, res) { res.end('ok'); }\\n\",\n );\n\n await emitVercelBuildOutput(root);\n\n expect(\n existsSync(path.join(root, \".vercel\", \"output\", \"static\", \"assets\")),\n ).toBe(true);\n expect(\n existsSync(\n path.join(\n root,\n \".vercel\",\n \"output\",\n \"functions\",\n \"mcp.func\",\n \"index.js\",\n ),\n ),\n ).toBe(true);\n });\n});\n"]}
1
+ {"version":3,"file":"build-helpers.test.js","sourceRoot":"","sources":["../../src/cli/build-helpers.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\n// esbuild's invariant check on TextEncoder/Uint8Array trips jsdom's polyfill.\nimport {\n existsSync,\n mkdirSync,\n mkdtempSync,\n readFileSync,\n writeFileSync,\n} from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport path from \"node:path\";\nimport { describe, expect, it } from \"vitest\";\nimport {\n ENTRY_WRAPPER_CONTENT,\n emitEmptyManifestModule,\n emitEntryWrapper,\n emitManifestModule,\n emitSkillsModule,\n emitVercelBuildOutput,\n ensureAssetsDir,\n VERCEL_CONFIG,\n VERCEL_VC_CONFIG,\n} from \"./build-helpers.js\";\n\nfunction mkTmp(prefix = \"skybridge-build-helpers-\") {\n return mkdtempSync(path.join(tmpdir(), prefix));\n}\n\ndescribe(\"emitEntryWrapper\", () => {\n it(\"writes dist/__entry.js that primes the manifest before importing user code\", () => {\n const dir = mkTmp();\n emitEntryWrapper(dir);\n const out = readFileSync(path.join(dir, \"__entry.js\"), \"utf-8\");\n expect(out).toBe(ENTRY_WRAPPER_CONTENT);\n expect(out).toContain(\n 'import { __setBuildManifest, __setSkillsManifest } from \"skybridge/server\"',\n );\n expect(out).toContain('import manifest from \"./vite-manifest.js\"');\n expect(out).toContain('import skills from \"./skills.js\"');\n expect(out).toContain(\"__setBuildManifest(manifest)\");\n expect(out).toContain(\"__setSkillsManifest(skills)\");\n // Dynamic import is load-bearing: `server.js` must evaluate after the\n // setters run, so a static re-export wouldn't work.\n expect(out).toContain('await import(\"./server.js\")');\n expect(out).toContain(\"export default userMod.default\");\n });\n});\n\ndescribe(\"emitSkillsModule\", () => {\n it(\"inlines discovered skills as an ESM default export\", () => {\n const dir = mkTmp();\n const skillsDir = path.join(dir, \"src\", \"skills\");\n mkdirSync(path.join(skillsDir, \"refunds\"), { recursive: true });\n writeFileSync(\n path.join(skillsDir, \"refunds\", \"SKILL.md\"),\n \"---\\nname: refunds\\ndescription: Process refunds\\n---\\nBody\",\n );\n const outPath = path.join(dir, \"skills.js\");\n emitSkillsModule(skillsDir, outPath);\n const out = readFileSync(outPath, \"utf-8\");\n const literal = out\n .slice(\"export default \".length)\n .trim()\n .replace(/;$/, \"\");\n const skills = JSON.parse(literal);\n expect(skills).toHaveLength(1);\n expect(skills[0]).toMatchObject({\n uri: \"skill://refunds/SKILL.md\",\n frontmatter: { name: \"refunds\", description: \"Process refunds\" },\n });\n expect(skills[0].resources).toHaveLength(1);\n expect(skills[0].resources[0].uri).toBe(\"skill://refunds/SKILL.md\");\n expect(skills[0].resources[0].digest).toMatch(/^sha256:[a-f0-9]{64}$/);\n });\n\n it(\"emits an empty array when the skills directory is absent\", () => {\n const dir = mkTmp();\n const outPath = path.join(dir, \"skills.js\");\n emitSkillsModule(path.join(dir, \"nope\"), outPath);\n expect(readFileSync(outPath, \"utf-8\")).toBe(\"export default [];\\n\");\n });\n});\n\ndescribe(\"emitManifestModule\", () => {\n it(\"inlines the JSON manifest as an ESM default export\", () => {\n const dir = mkTmp();\n const inPath = path.join(dir, \"manifest.json\");\n const outPath = path.join(dir, \"vite-manifest.js\");\n const manifest = { \"src/views/foo.tsx\": { file: \"assets/foo-abc.js\" } };\n writeFileSync(inPath, JSON.stringify(manifest));\n emitManifestModule(inPath, outPath);\n const out = readFileSync(outPath, \"utf-8\");\n expect(out.startsWith(\"export default \")).toBe(true);\n const literal = out\n .slice(\"export default \".length)\n .trim()\n .replace(/;$/, \"\");\n expect(JSON.parse(literal)).toEqual(manifest);\n });\n});\n\ndescribe(\"emitEmptyManifestModule\", () => {\n it(\"writes an empty object default export\", () => {\n const dir = mkTmp();\n const outPath = path.join(dir, \"vite-manifest.js\");\n emitEmptyManifestModule(outPath);\n expect(readFileSync(outPath, \"utf-8\")).toBe(\"export default {};\\n\");\n });\n});\n\ndescribe(\"ensureAssetsDir\", () => {\n it(\"creates the assets directory when missing\", () => {\n const dir = mkTmp();\n const assetsDir = path.join(dir, \"dist\", \"assets\");\n ensureAssetsDir(assetsDir);\n expect(existsSync(assetsDir)).toBe(true);\n });\n});\n\ndescribe(\"emitVercelBuildOutput\", () => {\n it(\"emits a Build Output API tree with bundled function and static assets\", async () => {\n const root = mkTmp();\n mkdirSync(path.join(root, \"dist\", \"assets\"), { recursive: true });\n writeFileSync(\n path.join(root, \"dist\", \"server.js\"),\n \"export default function handler(_req, res) { res.end('ok'); }\\n\",\n );\n // Minimal `dist/__entry.js` (the real wrapper imports `skybridge/server`,\n // which isn't resolvable in this test's tmp dir — the function-bundling\n // contract we care about here is just \"bundle whatever `__entry.js`\n // imports into a single function file\").\n writeFileSync(\n path.join(root, \"dist\", \"__entry.js\"),\n \"const userMod = await import('./server.js');\\nexport default userMod.default;\\n\",\n );\n writeFileSync(\n path.join(root, \"dist\", \"assets\", \"view-abc.js\"),\n \"/* bundled view */\\n\",\n );\n\n await emitVercelBuildOutput(root);\n\n const outputDir = path.join(root, \".vercel\", \"output\");\n const funcDir = path.join(outputDir, \"functions\", \"mcp.func\");\n\n expect(existsSync(path.join(funcDir, \"index.js\"))).toBe(true);\n expect(\n JSON.parse(readFileSync(path.join(funcDir, \".vc-config.json\"), \"utf-8\")),\n ).toEqual(VERCEL_VC_CONFIG);\n expect(\n JSON.parse(readFileSync(path.join(funcDir, \"package.json\"), \"utf-8\")),\n ).toEqual({ type: \"module\" });\n expect(\n JSON.parse(readFileSync(path.join(outputDir, \"config.json\"), \"utf-8\")),\n ).toEqual(VERCEL_CONFIG);\n expect(\n readFileSync(\n path.join(outputDir, \"static\", \"assets\", \"view-abc.js\"),\n \"utf-8\",\n ),\n ).toContain(\"bundled view\");\n });\n\n it(\"leaves serverExternal packages unbundled\", async () => {\n const root = mkTmp();\n mkdirSync(path.join(root, \"dist\"), { recursive: true });\n // Unresolvable from the tmp root, so esbuild fails the build unless the\n // package is marked external (bunyan → optional `dtrace-provider`).\n writeFileSync(\n path.join(root, \"dist\", \"__entry.js\"),\n 'import \"dtrace-provider\";\\nexport default null;\\n',\n );\n\n await expect(emitVercelBuildOutput(root)).rejects.toThrow(\n /Could not resolve \"dtrace-provider\"/,\n );\n await emitVercelBuildOutput(root, [\"dtrace-provider\"]);\n\n const bundle = readFileSync(\n path.join(root, \".vercel\", \"output\", \"functions\", \"mcp.func\", \"index.js\"),\n \"utf-8\",\n );\n expect(bundle).toContain('import \"dtrace-provider\"');\n });\n\n it(\"emits static assets directory when dist/assets is missing\", async () => {\n const root = mkTmp();\n mkdirSync(path.join(root, \"dist\"), { recursive: true });\n writeFileSync(\n path.join(root, \"dist\", \"__entry.js\"),\n \"const userMod = await import('./server.js');\\nexport default userMod.default;\\n\",\n );\n writeFileSync(\n path.join(root, \"dist\", \"server.js\"),\n \"export default function handler(_req, res) { res.end('ok'); }\\n\",\n );\n\n await emitVercelBuildOutput(root);\n\n expect(\n existsSync(path.join(root, \".vercel\", \"output\", \"static\", \"assets\")),\n ).toBe(true);\n expect(\n existsSync(\n path.join(\n root,\n \".vercel\",\n \"output\",\n \"functions\",\n \"mcp.func\",\n \"index.js\",\n ),\n ),\n ).toBe(true);\n });\n});\n"]}
@@ -1,13 +1,20 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
3
  import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
3
4
  import { describe, expect, it, vi } from "vitest";
5
+ import { z } from "zod";
4
6
  import { __setSkillsManifest, McpServer, } from "./index.js";
5
7
  const MANIFEST = [
6
8
  {
7
- name: "demo",
9
+ uri: "skill://demo/SKILL.md",
8
10
  frontmatter: { name: "demo", description: "A demo skill" },
9
- digest: "sha256:deadbeef",
10
- files: { "SKILL.md": "# Demo" },
11
+ resources: [
12
+ {
13
+ uri: "skill://demo/SKILL.md",
14
+ digest: `sha256:${createHash("sha256").update("# Demo", "utf8").digest("hex")}`,
15
+ content: "# Demo",
16
+ },
17
+ ],
11
18
  },
12
19
  ];
13
20
  function extensionsOf(server) {
@@ -24,15 +31,15 @@ describe("skills server option", () => {
24
31
  expect(extensionsOf(server)?.["io.modelcontextprotocol/skills"]).toEqual({
25
32
  directoryRead: true,
26
33
  });
27
- // Resources are keyed by URI: the skill's SKILL.md plus the discovery
28
- // index. Locks that the primed manifest was consumed and wired up.
29
- expect(registeredResourceNames(server)).toEqual(expect.arrayContaining(["skill://demo/SKILL.md", "skill://index.json"]));
34
+ // Resources are keyed by URI. Locks that the primed manifest was
35
+ // consumed and wired up.
36
+ expect(registeredResourceNames(server)).toEqual(expect.arrayContaining(["skill://demo/SKILL.md"]));
30
37
  });
31
38
  it("does nothing when the skills option is absent", () => {
32
39
  __setSkillsManifest(MANIFEST);
33
40
  const server = new McpServer({ name: "t", version: "0.0.1" }, {});
34
41
  expect(extensionsOf(server)?.["io.modelcontextprotocol/skills"]).toBeUndefined();
35
- expect(registeredResourceNames(server)).not.toContain("skill://index.json");
42
+ expect(registeredResourceNames(server)).not.toContain("skill://demo/SKILL.md");
36
43
  });
37
44
  it("serves skills through the stateless transport (capability + reads)", async () => {
38
45
  __setSkillsManifest(MANIFEST);
@@ -45,10 +52,19 @@ describe("skills server option", () => {
45
52
  await server.connectStatelessTransport(serverTransport);
46
53
  await client.connect(clientTransport);
47
54
  expect(client.getServerCapabilities()?.extensions?.["io.modelcontextprotocol/skills"]).toEqual({ directoryRead: true });
48
- const index = await client.readResource({ uri: "skill://index.json" });
49
- expect(index.contents[0].text).toContain("skill://demo/SKILL.md");
50
55
  const skill = await client.readResource({ uri: "skill://demo/SKILL.md" });
51
56
  expect(skill.contents[0].text).toBe("# Demo");
57
+ const SkillEntrySchema = z.object({
58
+ uri: z.string(),
59
+ frontmatter: z.record(z.string(), z.unknown()),
60
+ resources: z.array(z.object({ uri: z.string(), digest: z.string() })),
61
+ });
62
+ const list = await client.request({ method: "skills/list", params: {} }, z.object({ skills: z.array(SkillEntrySchema) }));
63
+ expect(list.skills).toHaveLength(1);
64
+ expect(list.skills[0]?.uri).toBe("skill://demo/SKILL.md");
65
+ expect(list.skills[0]?.resources[0]?.digest).toMatch(/^sha256:[a-f0-9]{64}$/);
66
+ const got = await client.request({ method: "skills/get", params: { uri: "skill://demo/SKILL.md" } }, z.object({ skill: SkillEntrySchema }));
67
+ expect(got.skill).toEqual(list.skills[0]);
52
68
  await client.close();
53
69
  await server.close();
54
70
  });
@@ -1 +1 @@
1
- {"version":3,"file":"skills-integration.test.js","sourceRoot":"","sources":["../../src/server/skills-integration.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { InMemoryTransport } from \"@modelcontextprotocol/sdk/inMemory.js\";\nimport { describe, expect, it, vi } from \"vitest\";\nimport {\n __setSkillsManifest,\n McpServer,\n type SkillsManifest,\n} from \"./index.js\";\n\nconst MANIFEST: SkillsManifest = [\n {\n name: \"demo\",\n frontmatter: { name: \"demo\", description: \"A demo skill\" },\n digest: \"sha256:deadbeef\",\n files: { \"SKILL.md\": \"# Demo\" },\n },\n];\n\nfunction extensionsOf(server: McpServer): Record<string, unknown> | undefined {\n return (\n server.server as unknown as {\n _capabilities?: { extensions?: Record<string, unknown> };\n }\n )._capabilities?.extensions;\n}\n\nfunction registeredResourceNames(server: McpServer): string[] {\n const registered = (\n server as unknown as {\n _registeredResources?: Record<string, unknown>;\n _registeredResourceTemplates?: Record<string, unknown>;\n }\n )._registeredResources;\n return registered ? Object.keys(registered) : [];\n}\n\ndescribe(\"skills server option\", () => {\n it(\"declares the extension capability and registers skill resources from the primed manifest\", () => {\n __setSkillsManifest(MANIFEST);\n const server = new McpServer(\n { name: \"t\", version: \"0.0.1\" },\n { capabilities: {} },\n { skills: true },\n );\n\n expect(extensionsOf(server)?.[\"io.modelcontextprotocol/skills\"]).toEqual({\n directoryRead: true,\n });\n // Resources are keyed by URI: the skill's SKILL.md plus the discovery\n // index. Locks that the primed manifest was consumed and wired up.\n expect(registeredResourceNames(server)).toEqual(\n expect.arrayContaining([\"skill://demo/SKILL.md\", \"skill://index.json\"]),\n );\n });\n\n it(\"does nothing when the skills option is absent\", () => {\n __setSkillsManifest(MANIFEST);\n const server = new McpServer({ name: \"t\", version: \"0.0.1\" }, {});\n\n expect(\n extensionsOf(server)?.[\"io.modelcontextprotocol/skills\"],\n ).toBeUndefined();\n expect(registeredResourceNames(server)).not.toContain(\"skill://index.json\");\n });\n\n it(\"serves skills through the stateless transport (capability + reads)\", async () => {\n __setSkillsManifest(MANIFEST);\n const server = new McpServer(\n { name: \"t\", version: \"0.0.1\" },\n {},\n { skills: true },\n );\n const client = new Client({ name: \"c\", version: \"0.0.1\" });\n const [clientTransport, serverTransport] =\n InMemoryTransport.createLinkedPair();\n // The production HTTP path builds a fresh per-request server and copies\n // handler maps by reference; exercise it directly to lock that skills\n // (capability + resource reads) survive that hop.\n await server.connectStatelessTransport(serverTransport);\n await client.connect(clientTransport);\n\n expect(\n client.getServerCapabilities()?.extensions?.[\n \"io.modelcontextprotocol/skills\"\n ],\n ).toEqual({ directoryRead: true });\n\n const index = await client.readResource({ uri: \"skill://index.json\" });\n expect((index.contents[0] as { text?: string }).text).toContain(\n \"skill://demo/SKILL.md\",\n );\n\n const skill = await client.readResource({ uri: \"skill://demo/SKILL.md\" });\n expect((skill.contents[0] as { text?: string }).text).toBe(\"# Demo\");\n\n await client.close();\n await server.close();\n });\n\n it(\"warns when skills are enabled but none are found\", () => {\n __setSkillsManifest([]);\n const warn = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n new McpServer({ name: \"t\", version: \"0.0.1\" }, {}, { skills: true });\n expect(warn).toHaveBeenCalledWith(\n expect.stringContaining(\"no skills were found\"),\n );\n warn.mockRestore();\n });\n});\n"]}
1
+ {"version":3,"file":"skills-integration.test.js","sourceRoot":"","sources":["../../src/server/skills-integration.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from \"node:crypto\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { InMemoryTransport } from \"@modelcontextprotocol/sdk/inMemory.js\";\nimport { describe, expect, it, vi } from \"vitest\";\nimport { z } from \"zod\";\nimport {\n __setSkillsManifest,\n McpServer,\n type SkillsManifest,\n} from \"./index.js\";\n\nconst MANIFEST: SkillsManifest = [\n {\n uri: \"skill://demo/SKILL.md\",\n frontmatter: { name: \"demo\", description: \"A demo skill\" },\n resources: [\n {\n uri: \"skill://demo/SKILL.md\",\n digest: `sha256:${createHash(\"sha256\").update(\"# Demo\", \"utf8\").digest(\"hex\")}`,\n content: \"# Demo\",\n },\n ],\n },\n];\n\nfunction extensionsOf(server: McpServer): Record<string, unknown> | undefined {\n return (\n server.server as unknown as {\n _capabilities?: { extensions?: Record<string, unknown> };\n }\n )._capabilities?.extensions;\n}\n\nfunction registeredResourceNames(server: McpServer): string[] {\n const registered = (\n server as unknown as {\n _registeredResources?: Record<string, unknown>;\n _registeredResourceTemplates?: Record<string, unknown>;\n }\n )._registeredResources;\n return registered ? Object.keys(registered) : [];\n}\n\ndescribe(\"skills server option\", () => {\n it(\"declares the extension capability and registers skill resources from the primed manifest\", () => {\n __setSkillsManifest(MANIFEST);\n const server = new McpServer(\n { name: \"t\", version: \"0.0.1\" },\n { capabilities: {} },\n { skills: true },\n );\n\n expect(extensionsOf(server)?.[\"io.modelcontextprotocol/skills\"]).toEqual({\n directoryRead: true,\n });\n // Resources are keyed by URI. Locks that the primed manifest was\n // consumed and wired up.\n expect(registeredResourceNames(server)).toEqual(\n expect.arrayContaining([\"skill://demo/SKILL.md\"]),\n );\n });\n\n it(\"does nothing when the skills option is absent\", () => {\n __setSkillsManifest(MANIFEST);\n const server = new McpServer({ name: \"t\", version: \"0.0.1\" }, {});\n\n expect(\n extensionsOf(server)?.[\"io.modelcontextprotocol/skills\"],\n ).toBeUndefined();\n expect(registeredResourceNames(server)).not.toContain(\n \"skill://demo/SKILL.md\",\n );\n });\n\n it(\"serves skills through the stateless transport (capability + reads)\", async () => {\n __setSkillsManifest(MANIFEST);\n const server = new McpServer(\n { name: \"t\", version: \"0.0.1\" },\n {},\n { skills: true },\n );\n const client = new Client({ name: \"c\", version: \"0.0.1\" });\n const [clientTransport, serverTransport] =\n InMemoryTransport.createLinkedPair();\n // The production HTTP path builds a fresh per-request server and copies\n // handler maps by reference; exercise it directly to lock that skills\n // (capability + resource reads) survive that hop.\n await server.connectStatelessTransport(serverTransport);\n await client.connect(clientTransport);\n\n expect(\n client.getServerCapabilities()?.extensions?.[\n \"io.modelcontextprotocol/skills\"\n ],\n ).toEqual({ directoryRead: true });\n\n const skill = await client.readResource({ uri: \"skill://demo/SKILL.md\" });\n expect((skill.contents[0] as { text?: string }).text).toBe(\"# Demo\");\n\n const SkillEntrySchema = z.object({\n uri: z.string(),\n frontmatter: z.record(z.string(), z.unknown()),\n resources: z.array(z.object({ uri: z.string(), digest: z.string() })),\n });\n\n const list = await client.request(\n { method: \"skills/list\", params: {} },\n z.object({ skills: z.array(SkillEntrySchema) }),\n );\n expect(list.skills).toHaveLength(1);\n expect(list.skills[0]?.uri).toBe(\"skill://demo/SKILL.md\");\n expect(list.skills[0]?.resources[0]?.digest).toMatch(\n /^sha256:[a-f0-9]{64}$/,\n );\n\n const got = await client.request(\n { method: \"skills/get\", params: { uri: \"skill://demo/SKILL.md\" } },\n z.object({ skill: SkillEntrySchema }),\n );\n expect(got.skill).toEqual(list.skills[0]);\n\n await client.close();\n await server.close();\n });\n\n it(\"warns when skills are enabled but none are found\", () => {\n __setSkillsManifest([]);\n const warn = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n new McpServer({ name: \"t\", version: \"0.0.1\" }, {}, { skills: true });\n expect(warn).toHaveBeenCalledWith(\n expect.stringContaining(\"no skills were found\"),\n );\n warn.mockRestore();\n });\n});\n"]}
@@ -1,12 +1,18 @@
1
1
  import type { ReadResourceCallback, ReadResourceTemplateCallback, ResourceMetadata, McpServer as SdkMcpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  export declare const SKILLS_EXTENSION_KEY = "io.modelcontextprotocol/skills";
4
- export declare const SKILL_INDEX_URI = "skill://index.json";
5
- export interface Skill {
6
- name: string;
7
- frontmatter: Record<string, unknown>;
4
+ export interface SkillResource {
5
+ uri: string;
8
6
  digest: string;
9
- files: Record<string, string>;
7
+ content: string;
8
+ }
9
+ export interface Skill {
10
+ uri: string;
11
+ frontmatter: {
12
+ name: string;
13
+ description: string;
14
+ } & Record<string, unknown>;
15
+ resources: SkillResource[];
10
16
  }
11
17
  export type SkillsManifest = Skill[];
12
18
  export declare function discoverSkills(dir: string): SkillsManifest;
@@ -6,7 +6,6 @@ import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
6
6
  import { parse as parseYaml } from "yaml";
7
7
  import { z } from "zod";
8
8
  export const SKILLS_EXTENSION_KEY = "io.modelcontextprotocol/skills";
9
- export const SKILL_INDEX_URI = "skill://index.json";
10
9
  const SKILL_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
11
10
  const SkillFrontmatterSchema = z
12
11
  .object({
@@ -72,11 +71,18 @@ export function discoverSkills(dir) {
72
71
  if (parsed.data.name !== entry.name) {
73
72
  throw new Error(`Skill name "${parsed.data.name}" in ${source} must match its directory name "${entry.name}"`);
74
73
  }
74
+ const resources = Object.entries(files)
75
+ .sort(([a], [b]) => Number(b === "SKILL.md") - Number(a === "SKILL.md") ||
76
+ a.localeCompare(b))
77
+ .map(([relPath, content]) => ({
78
+ uri: `skill://${entry.name}/${relPath}`,
79
+ digest: sha256(content),
80
+ content,
81
+ }));
75
82
  skills.push({
76
- name: entry.name,
83
+ uri: `skill://${entry.name}/SKILL.md`,
77
84
  frontmatter: parsed.data,
78
- digest: sha256(skillMd),
79
- files,
85
+ resources,
80
86
  });
81
87
  }
82
88
  return skills;
@@ -95,9 +101,10 @@ export function skillUriToRelPath(uri) {
95
101
  }
96
102
  function listDir(skill, relPath) {
97
103
  const prefix = relPath === "" ? "" : `${relPath}/`;
104
+ const base = skill.uri.slice(0, -"SKILL.md".length);
98
105
  const children = new Map();
99
106
  let matched = relPath === "";
100
- for (const filePath of Object.keys(skill.files)) {
107
+ for (const filePath of skill.resources.map((resource) => resource.uri.slice(base.length))) {
101
108
  if (!filePath.startsWith(prefix)) {
102
109
  continue;
103
110
  }
@@ -120,10 +127,25 @@ const DirectoryReadRequestSchema = z.object({
120
127
  method: z.literal("resources/directory/read"),
121
128
  params: z.object({ uri: z.string(), cursor: z.string().optional() }),
122
129
  });
130
+ const SkillsListRequestSchema = z.object({
131
+ method: z.literal("skills/list"),
132
+ params: z.object({ cursor: z.string().optional() }).optional(),
133
+ });
134
+ const SkillsGetRequestSchema = z.object({
135
+ method: z.literal("skills/get"),
136
+ params: z.object({ uri: z.string() }),
137
+ });
138
+ const toWireEntry = (skill) => ({
139
+ uri: skill.uri,
140
+ frontmatter: skill.frontmatter,
141
+ resources: skill.resources.map(({ uri, digest }) => ({ uri, digest })),
142
+ });
123
143
  export function registerSkills(server, manifest) {
124
- const byName = new Map(manifest.map((s) => [s.name, s]));
125
- const serveFile = (name, relPath, href) => {
126
- const text = byName.get(name)?.files[relPath];
144
+ const byUri = new Map(manifest.map((skill) => [skill.uri, skill]));
145
+ const serveFile = (skillUri, fileUri, href) => {
146
+ const text = byUri
147
+ .get(skillUri)
148
+ ?.resources.find((resource) => resource.uri === fileUri)?.content;
127
149
  if (text === undefined) {
128
150
  throw new McpError(ErrorCode.InvalidParams, `Not found: ${href}`);
129
151
  }
@@ -132,35 +154,30 @@ export function registerSkills(server, manifest) {
132
154
  };
133
155
  };
134
156
  for (const skill of manifest) {
135
- server.registerResource(skill.name, `skill://${skill.name}/SKILL.md`, {
136
- description: String(skill.frontmatter.description),
157
+ server.registerResource(skill.frontmatter.name, skill.uri, {
158
+ description: skill.frontmatter.description,
137
159
  mimeType: "text/markdown",
138
- }, (readUri) => serveFile(skill.name, "SKILL.md", readUri.href));
160
+ }, (readUri) => serveFile(skill.uri, skill.uri, readUri.href));
139
161
  }
140
162
  server.registerResource("skill-files", new ResourceTemplate("skill://{skillName}/{+filePath}", {
141
163
  list: undefined,
142
164
  }), {}, (readUri) => {
143
165
  const { name, relPath } = skillUriToRelPath(readUri.href);
144
- return serveFile(name, relPath, readUri.href);
166
+ return serveFile(`skill://${name}/SKILL.md`, `skill://${name}/${relPath}`, readUri.href);
145
167
  });
146
- server.registerResource("skill-index", SKILL_INDEX_URI, { mimeType: "application/json" }, () => ({
147
- contents: [
148
- {
149
- uri: SKILL_INDEX_URI,
150
- mimeType: "application/json",
151
- text: JSON.stringify({
152
- skills: manifest.map((skill) => ({
153
- url: `skill://${skill.name}/SKILL.md`,
154
- digest: skill.digest,
155
- frontmatter: skill.frontmatter,
156
- })),
157
- }),
158
- },
159
- ],
168
+ server.server.setRequestHandler(SkillsListRequestSchema, () => ({
169
+ skills: manifest.map(toWireEntry),
160
170
  }));
171
+ server.server.setRequestHandler(SkillsGetRequestSchema, ({ params }) => {
172
+ const skill = byUri.get(params.uri);
173
+ if (!skill) {
174
+ throw new McpError(ErrorCode.InvalidParams, `Unknown skill: ${params.uri}`);
175
+ }
176
+ return { skill: toWireEntry(skill) };
177
+ });
161
178
  server.server.setRequestHandler(DirectoryReadRequestSchema, ({ params }) => {
162
179
  const { name, relPath } = skillUriToRelPath(params.uri);
163
- const skill = byName.get(name);
180
+ const skill = byUri.get(`skill://${name}/SKILL.md`);
164
181
  const entries = skill ? listDir(skill, relPath) : null;
165
182
  if (!entries) {
166
183
  throw new McpError(ErrorCode.InvalidParams, `Not a directory: ${params.uri}`);
@@ -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\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\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 digest: string;\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 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 skills.push({\n name: entry.name,\n frontmatter: parsed.data,\n digest: sha256(skillMd),\n files,\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 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\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\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\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 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\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 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\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\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\nconst SkillsListRequestSchema = z.object({\n method: z.literal(\"skills/list\"),\n params: z.object({ cursor: z.string().optional() }).optional(),\n});\n\nconst SkillsGetRequestSchema = z.object({\n method: z.literal(\"skills/get\"),\n params: z.object({ uri: z.string() }),\n});\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 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.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(SkillsListRequestSchema, () => ({\n skills: manifest.map(toWireEntry),\n }));\n\n server.server.setRequestHandler(SkillsGetRequestSchema, ({ params }) => {\n const skill = byUri.get(params.uri);\n if (!skill) {\n throw new McpError(\n ErrorCode.InvalidParams,\n `Unknown skill: ${params.uri}`,\n );\n }\n return { skill: toWireEntry(skill) };\n });\n\n server.server.setRequestHandler(DirectoryReadRequestSchema, ({ 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 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,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
6
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
6
7
  import { describe, expect, it, vi } from "vitest";
7
- import { discoverSkills, registerSkills, SKILL_INDEX_URI, skillUriToRelPath, } from "./skills.js";
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, digest, and supporting files", () => {
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": `${FM("git-workflow", "Team git conventions")}Body`,
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?.name).toBe("git-workflow");
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?.digest).toMatch(/^sha256:[a-f0-9]{64}$/);
32
- expect(Object.keys(skill?.files ?? {}).sort()).toEqual([
33
- "SKILL.md",
34
- "references/GUIDE.md",
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
- name: "refunds",
109
+ uri: "skill://refunds/SKILL.md",
100
110
  frontmatter: { name: "refunds", description: "Process refunds" },
101
- digest: "sha256:abc",
102
- files: {
103
- "SKILL.md": "# Refunds",
104
- "templates/email.md": "Hi",
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
- let dirHandler;
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,15 @@ describe("registerSkills", () => {
116
133
  });
117
134
  }),
118
135
  server: {
119
- setRequestHandler: vi.fn((_schema, handler) => {
120
- dirHandler = handler;
136
+ setRequestHandler: vi.fn((schema, handler) => {
137
+ const method = schema
138
+ .shape.method.value;
139
+ handlers.set(method, handler);
121
140
  }),
122
141
  },
123
142
  };
124
- return { server, resources, getDirHandler: () => dirHandler };
143
+ return { server, resources, handlers };
125
144
  }
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
145
  it("serves supporting files through the template resource", () => {
143
146
  const { server, resources } = fakeRegistrar();
144
147
  // biome-ignore lint/suspicious/noExplicitAny: structural test double
@@ -153,7 +156,7 @@ describe("registerSkills", () => {
153
156
  const on = fakeRegistrar();
154
157
  // biome-ignore lint/suspicious/noExplicitAny: structural test double
155
158
  registerSkills(on.server, manifest);
156
- const result = on.getDirHandler()?.({
159
+ const result = on.handlers.get("resources/directory/read")?.({
157
160
  params: { uri: "skill://refunds/templates" },
158
161
  });
159
162
  expect(result.resources).toEqual([
@@ -165,10 +168,10 @@ describe("registerSkills", () => {
165
168
  ]);
166
169
  });
167
170
  it("lists a skill root and marks subdirectories as inode/directory", () => {
168
- const { server, getDirHandler } = fakeRegistrar();
171
+ const { server, handlers } = fakeRegistrar();
169
172
  // biome-ignore lint/suspicious/noExplicitAny: structural test double
170
173
  registerSkills(server, manifest);
171
- const result = getDirHandler()?.({
174
+ const result = handlers.get("resources/directory/read")?.({
172
175
  params: { uri: "skill://refunds" },
173
176
  });
174
177
  expect(result.resources).toContainEqual({
@@ -182,6 +185,40 @@ describe("registerSkills", () => {
182
185
  mimeType: "text/markdown",
183
186
  });
184
187
  });
188
+ it("lists entries with complete per-file resources via skills/list", () => {
189
+ const { server, handlers } = fakeRegistrar();
190
+ // biome-ignore lint/suspicious/noExplicitAny: structural test double
191
+ registerSkills(server, manifest);
192
+ const result = handlers.get("skills/list")?.({ params: {} });
193
+ expect(result).toEqual({
194
+ skills: [
195
+ {
196
+ uri: "skill://refunds/SKILL.md",
197
+ frontmatter: { name: "refunds", description: "Process refunds" },
198
+ resources: [
199
+ { uri: "skill://refunds/SKILL.md", digest: digestOf("# Refunds") },
200
+ {
201
+ uri: "skill://refunds/templates/email.md",
202
+ digest: digestOf("Hi"),
203
+ },
204
+ ],
205
+ },
206
+ ],
207
+ });
208
+ });
209
+ it("returns a single entry via skills/get and rejects non-skill URIs", () => {
210
+ const { server, handlers } = fakeRegistrar();
211
+ // biome-ignore lint/suspicious/noExplicitAny: structural test double
212
+ registerSkills(server, manifest);
213
+ const get = handlers.get("skills/get");
214
+ const result = get?.({
215
+ params: { uri: "skill://refunds/SKILL.md" },
216
+ });
217
+ expect(result.skill.uri).toBe("skill://refunds/SKILL.md");
218
+ expect(result.skill.resources).toHaveLength(2);
219
+ expect(() => get?.({ params: { uri: "skill://unknown/SKILL.md" } })).toThrow(/Unknown skill/);
220
+ expect(() => get?.({ params: { uri: "skill://refunds/templates/email.md" } })).toThrow(/Unknown skill/);
221
+ });
185
222
  });
186
223
  describe("discoverSkills symlink safety", () => {
187
224
  it("excludes symlinked files and directories from the manifest", () => {
@@ -193,8 +230,8 @@ describe("discoverSkills symlink safety", () => {
193
230
  });
194
231
  symlinkSync(join(outside, "secret.md"), join(dir, "demo", "leak.md"));
195
232
  symlinkSync(outside, join(dir, "demo", "escape"));
196
- const files = discoverSkills(dir)[0]?.files ?? {};
197
- expect(Object.keys(files).sort()).toEqual(["SKILL.md", "real.md"]);
233
+ const uris = discoverSkills(dir)[0]?.resources.map((r) => r.uri) ?? [];
234
+ expect(uris).toEqual(["skill://demo/SKILL.md", "skill://demo/real.md"]);
198
235
  });
199
236
  it("ignores non-markdown supporting files", () => {
200
237
  const dir = mkSkillDir({
@@ -203,8 +240,8 @@ describe("discoverSkills symlink safety", () => {
203
240
  "demo/data.json": "{}",
204
241
  "demo/scripts/run.py": "print(1)",
205
242
  });
206
- const files = discoverSkills(dir)[0]?.files ?? {};
207
- expect(Object.keys(files).sort()).toEqual(["SKILL.md", "notes.md"]);
243
+ const uris = discoverSkills(dir)[0]?.resources.map((r) => r.uri) ?? [];
244
+ expect(uris).toEqual(["skill://demo/SKILL.md", "skill://demo/notes.md"]);
208
245
  });
209
246
  });
210
247
  //# 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/sdk/server/mcp.js\";\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((schema: unknown, handler: unknown) => {\n const method = (schema as { shape: { method: { value: string } } })\n .shape.method.value;\n handlers.set(method, handler as (req: unknown) => unknown);\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 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, 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 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 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\")?.({ params: {} });\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?.({\n params: { uri: \"skill://refunds/SKILL.md\" },\n }) as { skill: { uri: string; resources: unknown[] } };\n expect(result.skill.uri).toBe(\"skill://refunds/SKILL.md\");\n expect(result.skill.resources).toHaveLength(2);\n expect(() =>\n get?.({ params: { uri: \"skill://unknown/SKILL.md\" } }),\n ).toThrow(/Unknown skill/);\n expect(() =>\n get?.({ params: { uri: \"skill://refunds/templates/email.md\" } }),\n ).toThrow(/Unknown skill/);\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"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,54 @@
1
+ // @vitest-environment node
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { build } from "vite";
6
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
7
+ import { skybridge } from "./plugin.js";
8
+ /**
9
+ * Full `vite build` over a fixture whose stylesheet contains a `url()`. The
10
+ * unit test pins the `renderBuiltUrl` contract; this one proves Vite actually
11
+ * accepts what the plugin returns.
12
+ */
13
+ describe("skybridge plugin build", () => {
14
+ let root;
15
+ beforeEach(() => {
16
+ root = mkdtempSync(join(tmpdir(), "skybridge-build-"));
17
+ mkdirSync(join(root, "views"), { recursive: true });
18
+ mkdirSync(join(root, "src"), { recursive: true });
19
+ writeFileSync(join(root, "src/demo.woff2"), Buffer.alloc(64, 1));
20
+ writeFileSync(join(root, "src/styles.css"), [
21
+ "@font-face {",
22
+ ' font-family: "Demo";',
23
+ ' src: url("./demo.woff2") format("woff2");',
24
+ "}",
25
+ ].join("\n"));
26
+ writeFileSync(join(root, "src/entry.js"), `import "./styles.css";\n`);
27
+ });
28
+ afterEach(() => {
29
+ rmSync(root, { recursive: true, force: true });
30
+ });
31
+ it("builds a view stylesheet that references a font", async () => {
32
+ const result = await build({
33
+ root,
34
+ logLevel: "silent",
35
+ plugins: [skybridge({ viewsDir: join(root, "views") })],
36
+ build: {
37
+ // Force the font to be emitted as a file rather than inlined, so the
38
+ // stylesheet actually carries a `url()`.
39
+ assetsInlineLimit: 0,
40
+ rollupOptions: { input: { entry: join(root, "src/entry.js") } },
41
+ },
42
+ });
43
+ const outputs = (Array.isArray(result) ? result : [result]).flatMap((bundle) => ("output" in bundle ? bundle.output : []));
44
+ const css = outputs.find((chunk) => chunk.type === "asset" && chunk.fileName.endsWith(".css"));
45
+ if (css?.type !== "asset") {
46
+ throw new Error("no CSS asset was emitted");
47
+ }
48
+ // The font resolves relative to the stylesheet, which the server hands out
49
+ // from `${serverUrl}/assets/` — no runtime expression, which CSS can't run.
50
+ expect(String(css.source)).toMatch(/url\(\.\/demo-[\w-]+\.woff2\)/);
51
+ expect(String(css.source)).not.toContain("window.skybridge");
52
+ });
53
+ });
54
+ //# sourceMappingURL=plugin-build.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-build.test.js","sourceRoot":"","sources":["../../../src/web/plugin/plugin-build.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\nimport { mkdirSync, mkdtempSync, rmSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { build } from \"vite\";\nimport { afterEach, beforeEach, describe, expect, it } from \"vitest\";\nimport { skybridge } from \"./plugin.js\";\n\n/**\n * Full `vite build` over a fixture whose stylesheet contains a `url()`. The\n * unit test pins the `renderBuiltUrl` contract; this one proves Vite actually\n * accepts what the plugin returns.\n */\ndescribe(\"skybridge plugin build\", () => {\n let root: string;\n\n beforeEach(() => {\n root = mkdtempSync(join(tmpdir(), \"skybridge-build-\"));\n mkdirSync(join(root, \"views\"), { recursive: true });\n mkdirSync(join(root, \"src\"), { recursive: true });\n\n writeFileSync(join(root, \"src/demo.woff2\"), Buffer.alloc(64, 1));\n writeFileSync(\n join(root, \"src/styles.css\"),\n [\n \"@font-face {\",\n ' font-family: \"Demo\";',\n ' src: url(\"./demo.woff2\") format(\"woff2\");',\n \"}\",\n ].join(\"\\n\"),\n );\n writeFileSync(join(root, \"src/entry.js\"), `import \"./styles.css\";\\n`);\n });\n\n afterEach(() => {\n rmSync(root, { recursive: true, force: true });\n });\n\n it(\"builds a view stylesheet that references a font\", async () => {\n const result = await build({\n root,\n logLevel: \"silent\",\n plugins: [skybridge({ viewsDir: join(root, \"views\") })],\n build: {\n // Force the font to be emitted as a file rather than inlined, so the\n // stylesheet actually carries a `url()`.\n assetsInlineLimit: 0,\n rollupOptions: { input: { entry: join(root, \"src/entry.js\") } },\n },\n });\n\n const outputs = (Array.isArray(result) ? result : [result]).flatMap(\n (bundle) => (\"output\" in bundle ? bundle.output : []),\n );\n const css = outputs.find(\n (chunk) => chunk.type === \"asset\" && chunk.fileName.endsWith(\".css\"),\n );\n if (css?.type !== \"asset\") {\n throw new Error(\"no CSS asset was emitted\");\n }\n\n // The font resolves relative to the stylesheet, which the server hands out\n // from `${serverUrl}/assets/` — no runtime expression, which CSS can't run.\n expect(String(css.source)).toMatch(/url\\(\\.\\/demo-[\\w-]+\\.woff2\\)/);\n expect(String(css.source)).not.toContain(\"window.skybridge\");\n });\n});\n"]}
@@ -101,10 +101,21 @@ export function skybridge(options) {
101
101
  include: ["react", "react-dom/client", "react/jsx-runtime"],
102
102
  },
103
103
  experimental: {
104
- renderBuiltUrl: (filename) => {
105
- return {
106
- runtime: `window.skybridge.serverUrl + "/assets/${filename}"`,
107
- };
104
+ renderBuiltUrl: (filename, { hostType }) => {
105
+ // Views render inside a host sandbox iframe, so `import.meta.url`
106
+ // points at the sandbox domain rather than the Skybridge server.
107
+ // JS asset references have to be resolved at runtime against
108
+ // `window.skybridge.serverUrl` so they follow tunnels too.
109
+ if (hostType === "js") {
110
+ return {
111
+ runtime: `window.skybridge.serverUrl + "/assets/${filename}"`,
112
+ };
113
+ }
114
+ // CSS has nowhere to evaluate a runtime expression — `vite:css-post`
115
+ // throws on one. It doesn't need it either: the stylesheet is served
116
+ // from `${serverUrl}/assets/…` and a relative `url()` resolves
117
+ // against the stylesheet's own URL, tunnel origin included.
118
+ return { relative: true };
108
119
  },
109
120
  },
110
121
  };
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../../src/web/plugin/plugin.ts"],"names":[],"mappings":"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","sourcesContent":["import { isAbsolute, relative, resolve } from \"node:path\";\nimport type { Plugin, ViteDevServer } from \"vite\";\nimport {\n assertUniqueViewNames,\n type DiscoveredView,\n discoverViewsSync,\n scanViewsSync,\n writeViewsDts,\n} from \"./scan-views.js\";\nimport { transform as dataLlmTransform } from \"./transform-data-llm.js\";\nimport { hasDefaultExport } from \"./validate-view.js\";\n\nconst VIRTUAL_PREFIX = \"/_skybridge/view/\";\nconst VIRTUAL_MODULE_PREFIX = \"\\0skybridge:view:\";\n\n/** Options for the {@link skybridge} Vite plugin. */\nexport interface SkybridgePluginOptions {\n /** Directory scanned for view modules. Defaults to `\"src/views\"`. */\n viewsDir?: string;\n /**\n * Package names to leave unbundled when `skybridge build` bundles the server\n * for deployment. Use it for dependencies esbuild can't resolve or load —\n * typically optional native modules pulled in by a transitive dependency.\n */\n serverExternal?: string[];\n}\n\nfunction buildVirtualEntry(viewFilePath: string): string {\n const normalized = viewFilePath.replace(/\\\\/g, \"/\");\n return [\n `import { mountView } from \"skybridge/web\";`,\n `import Component from \"${normalized}\";`,\n `import { createElement } from \"react\";`,\n `mountView(createElement(Component));`,\n ].join(\"\\n\");\n}\n\nfunction getViewEntryPattern(viewsDir: string): RegExp {\n const escaped = viewsDir.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return new RegExp(\n `${escaped}\\\\/(?:[^/]+\\\\.(?:jsx|tsx)|[^/]+\\\\/index\\\\.(?:tsx|jsx))(?:\\\\?.*)?$`,\n );\n}\n\n/**\n * Vite plugin that wires a Skybridge project's view files into Vite.\n *\n * For each `.tsx` / `.jsx` file in `viewsDir` with a default export, the\n * plugin:\n * - exposes a virtual entry that calls {@link mountView} with the view's\n * default export,\n * - generates `.skybridge/views.d.ts` to augment {@link ViewNameRegistry} so\n * {@link ViewName} narrows to the actual view names,\n * - rewrites `<DataLLM>` JSX so the host can extract its content,\n * - warns in dev if a view file is missing a default export.\n *\n * Add it to your `vite.config.ts` alongside `@vitejs/plugin-react`.\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { defineConfig } from \"vite\";\n * import react from \"@vitejs/plugin-react\";\n * import { skybridge } from \"skybridge/vite\";\n *\n * export default defineConfig({\n * plugins: [react(), skybridge({ viewsDir: \"src/views\" })],\n * });\n * ```\n */\nexport function skybridge(options?: SkybridgePluginOptions): Plugin {\n const rawViewsDir = options?.viewsDir ?? \"src/views\";\n let resolvedViewsDir: string;\n let projectRoot: string;\n let viewMap = new Map<string, DiscoveredView>();\n let viewEntryPattern: RegExp;\n\n return {\n name: \"skybridge\",\n enforce: \"pre\",\n // Read by `skybridge build` to resolve viewsDir before `tsc -b` runs, and\n // to feed esbuild's external list when bundling the server.\n api: { viewsDir: rawViewsDir, serverExternal: options?.serverExternal },\n\n config(config) {\n projectRoot = config.root || process.cwd();\n resolvedViewsDir = isAbsolute(rawViewsDir)\n ? rawViewsDir\n : resolve(projectRoot, rawViewsDir);\n viewEntryPattern = getViewEntryPattern(resolvedViewsDir);\n\n const views = discoverViewsSync(resolvedViewsDir);\n viewMap = new Map(views.map((v) => [v.name, v]));\n writeViewsDts(projectRoot, views);\n\n const input: Record<string, string> = {};\n for (const view of views) {\n input[view.name] = `${VIRTUAL_PREFIX}${view.name}`;\n }\n\n return {\n base: \"/assets\",\n // Fixes \"Invalid hook call\" on createStore by forcing a single\n // copy of React. Under pnpm's isolated node_modules, zustand\n // inside `skybridge` resolves React from skybridge's own\n // dependencies while the host app loads its own copy\n resolve: {\n dedupe: [\"react\", \"react-dom\"],\n },\n build: {\n outDir: \"dist/assets\",\n emptyOutDir: true,\n manifest: true,\n minify: true,\n cssCodeSplit: false,\n rollupOptions: {\n input,\n },\n },\n // Pre-bundle view deps at startup so the first tool invocation\n // doesn't hit Vite's on-demand re-optimization path (which sends\n // `full-reload` over HMR — in our iframe flow the parent host\n // can't honour a reload, and the view silently never mounts).\n optimizeDeps: {\n // Scan view files so transitive user deps (zod, tailwind, etc.)\n // get pre-bundled at startup.\n entries: [\n `${resolvedViewsDir}/*.{tsx,jsx}`,\n `${resolvedViewsDir}/*/index.{tsx,jsx}`,\n ],\n include: [\"react\", \"react-dom/client\", \"react/jsx-runtime\"],\n },\n experimental: {\n renderBuiltUrl: (filename) => {\n return {\n runtime: `window.skybridge.serverUrl + \"/assets/${filename}\"`,\n };\n },\n },\n };\n },\n\n resolveId(id) {\n if (id.startsWith(VIRTUAL_PREFIX)) {\n const name = id.slice(VIRTUAL_PREFIX.length);\n if (viewMap.has(name)) {\n return `${VIRTUAL_MODULE_PREFIX}${name}`;\n }\n }\n return null;\n },\n\n load(id) {\n if (id.startsWith(VIRTUAL_MODULE_PREFIX)) {\n const name = id.slice(VIRTUAL_MODULE_PREFIX.length);\n const view = viewMap.get(name);\n if (view) {\n return buildVirtualEntry(view.filePath);\n }\n }\n return null;\n },\n\n configureServer(server: ViteDevServer) {\n if (!resolvedViewsDir) {\n const root = server.config.root || process.cwd();\n resolvedViewsDir = isAbsolute(rawViewsDir)\n ? rawViewsDir\n : resolve(root, rawViewsDir);\n projectRoot = root;\n viewEntryPattern = getViewEntryPattern(resolvedViewsDir);\n }\n\n server.watcher.add(resolvedViewsDir);\n // Track which view files we've already warned about so a rescan\n // triggered by an unrelated edit doesn't re-emit the same warning.\n let knownInvalid = new Set<string>();\n const rescan = () => {\n try {\n // Surface broken view files. Without this, files lacking a\n // default export are silently dropped from the input and the\n // user has no idea why their widget never mounts.\n const { valid, invalid } = scanViewsSync(resolvedViewsDir);\n const nextInvalid = new Set(invalid.map((v) => v.filePath));\n\n for (const filePath of nextInvalid) {\n if (!knownInvalid.has(filePath)) {\n server.config.logger.warn(\n `[skybridge] view file \"${relative(projectRoot, filePath)}\" is missing a default export — it won't be served until fixed.`,\n );\n }\n }\n for (const filePath of knownInvalid) {\n if (!nextInvalid.has(filePath)) {\n server.config.logger.info(\n `[skybridge] view file \"${relative(projectRoot, filePath)}\" resolved.`,\n );\n }\n }\n knownInvalid = nextInvalid;\n\n assertUniqueViewNames(valid);\n viewMap = new Map(valid.map((v) => [v.name, v]));\n writeViewsDts(projectRoot, valid);\n } catch (err) {\n // assertUniqueViewNames throws on duplicate view names. Catch so\n // chokidar's listener chain doesn't surface it as unhandled and\n // crash the dev server — previous viewMap stays active until\n // the user fixes the conflict.\n const message = err instanceof Error ? err.message : String(err);\n server.config.logger.error(\n `[skybridge] view rescan failed: ${message}`,\n );\n }\n };\n\n // Initial scan emits warnings for broken files that exist at startup.\n rescan();\n server.watcher.on(\"add\", rescan);\n server.watcher.on(\"change\", rescan);\n server.watcher.on(\"unlink\", rescan);\n },\n\n async transform(code, id) {\n if (viewEntryPattern?.test(id) && !hasDefaultExport(code, id)) {\n this.warn(\n `View file \"${id.split(\"/\").pop()}\" is missing a default export.`,\n );\n }\n\n return await dataLlmTransform(code, id);\n },\n };\n}\n"]}
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../../src/web/plugin/plugin.ts"],"names":[],"mappings":"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","sourcesContent":["import { isAbsolute, relative, resolve } from \"node:path\";\nimport type { Plugin, ViteDevServer } from \"vite\";\nimport {\n assertUniqueViewNames,\n type DiscoveredView,\n discoverViewsSync,\n scanViewsSync,\n writeViewsDts,\n} from \"./scan-views.js\";\nimport { transform as dataLlmTransform } from \"./transform-data-llm.js\";\nimport { hasDefaultExport } from \"./validate-view.js\";\n\nconst VIRTUAL_PREFIX = \"/_skybridge/view/\";\nconst VIRTUAL_MODULE_PREFIX = \"\\0skybridge:view:\";\n\n/** Options for the {@link skybridge} Vite plugin. */\nexport interface SkybridgePluginOptions {\n /** Directory scanned for view modules. Defaults to `\"src/views\"`. */\n viewsDir?: string;\n /**\n * Package names to leave unbundled when `skybridge build` bundles the server\n * for deployment. Use it for dependencies esbuild can't resolve or load —\n * typically optional native modules pulled in by a transitive dependency.\n */\n serverExternal?: string[];\n}\n\nfunction buildVirtualEntry(viewFilePath: string): string {\n const normalized = viewFilePath.replace(/\\\\/g, \"/\");\n return [\n `import { mountView } from \"skybridge/web\";`,\n `import Component from \"${normalized}\";`,\n `import { createElement } from \"react\";`,\n `mountView(createElement(Component));`,\n ].join(\"\\n\");\n}\n\nfunction getViewEntryPattern(viewsDir: string): RegExp {\n const escaped = viewsDir.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return new RegExp(\n `${escaped}\\\\/(?:[^/]+\\\\.(?:jsx|tsx)|[^/]+\\\\/index\\\\.(?:tsx|jsx))(?:\\\\?.*)?$`,\n );\n}\n\n/**\n * Vite plugin that wires a Skybridge project's view files into Vite.\n *\n * For each `.tsx` / `.jsx` file in `viewsDir` with a default export, the\n * plugin:\n * - exposes a virtual entry that calls {@link mountView} with the view's\n * default export,\n * - generates `.skybridge/views.d.ts` to augment {@link ViewNameRegistry} so\n * {@link ViewName} narrows to the actual view names,\n * - rewrites `<DataLLM>` JSX so the host can extract its content,\n * - warns in dev if a view file is missing a default export.\n *\n * Add it to your `vite.config.ts` alongside `@vitejs/plugin-react`.\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { defineConfig } from \"vite\";\n * import react from \"@vitejs/plugin-react\";\n * import { skybridge } from \"skybridge/vite\";\n *\n * export default defineConfig({\n * plugins: [react(), skybridge({ viewsDir: \"src/views\" })],\n * });\n * ```\n */\nexport function skybridge(options?: SkybridgePluginOptions): Plugin {\n const rawViewsDir = options?.viewsDir ?? \"src/views\";\n let resolvedViewsDir: string;\n let projectRoot: string;\n let viewMap = new Map<string, DiscoveredView>();\n let viewEntryPattern: RegExp;\n\n return {\n name: \"skybridge\",\n enforce: \"pre\",\n // Read by `skybridge build` to resolve viewsDir before `tsc -b` runs, and\n // to feed esbuild's external list when bundling the server.\n api: { viewsDir: rawViewsDir, serverExternal: options?.serverExternal },\n\n config(config) {\n projectRoot = config.root || process.cwd();\n resolvedViewsDir = isAbsolute(rawViewsDir)\n ? rawViewsDir\n : resolve(projectRoot, rawViewsDir);\n viewEntryPattern = getViewEntryPattern(resolvedViewsDir);\n\n const views = discoverViewsSync(resolvedViewsDir);\n viewMap = new Map(views.map((v) => [v.name, v]));\n writeViewsDts(projectRoot, views);\n\n const input: Record<string, string> = {};\n for (const view of views) {\n input[view.name] = `${VIRTUAL_PREFIX}${view.name}`;\n }\n\n return {\n base: \"/assets\",\n // Fixes \"Invalid hook call\" on createStore by forcing a single\n // copy of React. Under pnpm's isolated node_modules, zustand\n // inside `skybridge` resolves React from skybridge's own\n // dependencies while the host app loads its own copy\n resolve: {\n dedupe: [\"react\", \"react-dom\"],\n },\n build: {\n outDir: \"dist/assets\",\n emptyOutDir: true,\n manifest: true,\n minify: true,\n cssCodeSplit: false,\n rollupOptions: {\n input,\n },\n },\n // Pre-bundle view deps at startup so the first tool invocation\n // doesn't hit Vite's on-demand re-optimization path (which sends\n // `full-reload` over HMR — in our iframe flow the parent host\n // can't honour a reload, and the view silently never mounts).\n optimizeDeps: {\n // Scan view files so transitive user deps (zod, tailwind, etc.)\n // get pre-bundled at startup.\n entries: [\n `${resolvedViewsDir}/*.{tsx,jsx}`,\n `${resolvedViewsDir}/*/index.{tsx,jsx}`,\n ],\n include: [\"react\", \"react-dom/client\", \"react/jsx-runtime\"],\n },\n experimental: {\n renderBuiltUrl: (filename, { hostType }) => {\n // Views render inside a host sandbox iframe, so `import.meta.url`\n // points at the sandbox domain rather than the Skybridge server.\n // JS asset references have to be resolved at runtime against\n // `window.skybridge.serverUrl` so they follow tunnels too.\n if (hostType === \"js\") {\n return {\n runtime: `window.skybridge.serverUrl + \"/assets/${filename}\"`,\n };\n }\n // CSS has nowhere to evaluate a runtime expression — `vite:css-post`\n // throws on one. It doesn't need it either: the stylesheet is served\n // from `${serverUrl}/assets/…` and a relative `url()` resolves\n // against the stylesheet's own URL, tunnel origin included.\n return { relative: true };\n },\n },\n };\n },\n\n resolveId(id) {\n if (id.startsWith(VIRTUAL_PREFIX)) {\n const name = id.slice(VIRTUAL_PREFIX.length);\n if (viewMap.has(name)) {\n return `${VIRTUAL_MODULE_PREFIX}${name}`;\n }\n }\n return null;\n },\n\n load(id) {\n if (id.startsWith(VIRTUAL_MODULE_PREFIX)) {\n const name = id.slice(VIRTUAL_MODULE_PREFIX.length);\n const view = viewMap.get(name);\n if (view) {\n return buildVirtualEntry(view.filePath);\n }\n }\n return null;\n },\n\n configureServer(server: ViteDevServer) {\n if (!resolvedViewsDir) {\n const root = server.config.root || process.cwd();\n resolvedViewsDir = isAbsolute(rawViewsDir)\n ? rawViewsDir\n : resolve(root, rawViewsDir);\n projectRoot = root;\n viewEntryPattern = getViewEntryPattern(resolvedViewsDir);\n }\n\n server.watcher.add(resolvedViewsDir);\n // Track which view files we've already warned about so a rescan\n // triggered by an unrelated edit doesn't re-emit the same warning.\n let knownInvalid = new Set<string>();\n const rescan = () => {\n try {\n // Surface broken view files. Without this, files lacking a\n // default export are silently dropped from the input and the\n // user has no idea why their widget never mounts.\n const { valid, invalid } = scanViewsSync(resolvedViewsDir);\n const nextInvalid = new Set(invalid.map((v) => v.filePath));\n\n for (const filePath of nextInvalid) {\n if (!knownInvalid.has(filePath)) {\n server.config.logger.warn(\n `[skybridge] view file \"${relative(projectRoot, filePath)}\" is missing a default export — it won't be served until fixed.`,\n );\n }\n }\n for (const filePath of knownInvalid) {\n if (!nextInvalid.has(filePath)) {\n server.config.logger.info(\n `[skybridge] view file \"${relative(projectRoot, filePath)}\" resolved.`,\n );\n }\n }\n knownInvalid = nextInvalid;\n\n assertUniqueViewNames(valid);\n viewMap = new Map(valid.map((v) => [v.name, v]));\n writeViewsDts(projectRoot, valid);\n } catch (err) {\n // assertUniqueViewNames throws on duplicate view names. Catch so\n // chokidar's listener chain doesn't surface it as unhandled and\n // crash the dev server — previous viewMap stays active until\n // the user fixes the conflict.\n const message = err instanceof Error ? err.message : String(err);\n server.config.logger.error(\n `[skybridge] view rescan failed: ${message}`,\n );\n }\n };\n\n // Initial scan emits warnings for broken files that exist at startup.\n rescan();\n server.watcher.on(\"add\", rescan);\n server.watcher.on(\"change\", rescan);\n server.watcher.on(\"unlink\", rescan);\n },\n\n async transform(code, id) {\n if (viewEntryPattern?.test(id) && !hasDefaultExport(code, id)) {\n this.warn(\n `View file \"${id.split(\"/\").pop()}\" is missing a default export.`,\n );\n }\n\n return await dataLlmTransform(code, id);\n },\n };\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,61 @@
1
+ import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import { skybridge } from "./plugin.js";
6
+ function getRenderBuiltUrl(root) {
7
+ const plugin = skybridge({ viewsDir: join(root, "views") });
8
+ const hook = plugin.config;
9
+ if (!hook) {
10
+ throw new Error("plugin.config is not defined");
11
+ }
12
+ const handler = typeof hook === "function" ? hook : hook.handler;
13
+ const config = handler.call(
14
+ // biome-ignore lint/suspicious/noExplicitAny: vitest harness for plugin hook
15
+ {}, { root }, { command: "build", mode: "production" });
16
+ const renderBuiltUrl = config.experimental?.renderBuiltUrl;
17
+ if (!renderBuiltUrl) {
18
+ throw new Error("experimental.renderBuiltUrl is not defined");
19
+ }
20
+ return renderBuiltUrl;
21
+ }
22
+ describe("skybridge plugin renderBuiltUrl", () => {
23
+ let root;
24
+ beforeEach(() => {
25
+ root = mkdtempSync(join(tmpdir(), "skybridge-plugin-"));
26
+ mkdirSync(join(root, "views"), { recursive: true });
27
+ });
28
+ afterEach(() => {
29
+ rmSync(root, { recursive: true, force: true });
30
+ });
31
+ // Assets referenced from JS can't use `import.meta.url`: views render in a
32
+ // host sandbox iframe (web-sandbox.oaiusercontent.com), so the module origin
33
+ // doesn't point back at the Skybridge server. They have to be resolved at
34
+ // runtime against `window.skybridge.serverUrl` instead.
35
+ it("resolves JS asset references against window.skybridge.serverUrl", () => {
36
+ const renderBuiltUrl = getRenderBuiltUrl(root);
37
+ expect(renderBuiltUrl("assets/logo-BXtQ8kd2.png", {
38
+ hostId: "assets/view-CwF9pQ1a.js",
39
+ hostType: "js",
40
+ type: "asset",
41
+ ssr: false,
42
+ })).toEqual({
43
+ runtime: `window.skybridge.serverUrl + "/assets/assets/logo-BXtQ8kd2.png"`,
44
+ });
45
+ });
46
+ // Reproducer for the CSS build failure: Vite has nowhere to evaluate a
47
+ // runtime expression inside a stylesheet, so `{ runtime }` makes
48
+ // `vite:css-post` throw for any `url()` in CSS. It isn't needed either — the
49
+ // stylesheet is served from `${serverUrl}/assets/…`, and a relative `url()`
50
+ // resolves against the stylesheet's own URL, tunnel origin included.
51
+ it("resolves CSS asset references relatively", () => {
52
+ const renderBuiltUrl = getRenderBuiltUrl(root);
53
+ expect(renderBuiltUrl("assets/demo-0IQFxQqs.woff2", {
54
+ hostId: "assets/style-DmQ2xY7b.css",
55
+ hostType: "css",
56
+ type: "asset",
57
+ ssr: false,
58
+ })).toEqual({ relative: true });
59
+ });
60
+ });
61
+ //# sourceMappingURL=plugin.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.test.js","sourceRoot":"","sources":["../../../src/web/plugin/plugin.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdirSync, mkdtempSync, rmSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { UserConfig } from \"vite\";\nimport { afterEach, beforeEach, describe, expect, it } from \"vitest\";\nimport { skybridge } from \"./plugin.js\";\n\ntype RenderBuiltUrl = NonNullable<\n NonNullable<UserConfig[\"experimental\"]>[\"renderBuiltUrl\"]\n>;\n\nfunction getRenderBuiltUrl(root: string): RenderBuiltUrl {\n const plugin = skybridge({ viewsDir: join(root, \"views\") });\n const hook = plugin.config;\n if (!hook) {\n throw new Error(\"plugin.config is not defined\");\n }\n const handler = typeof hook === \"function\" ? hook : hook.handler;\n const config = handler.call(\n // biome-ignore lint/suspicious/noExplicitAny: vitest harness for plugin hook\n {} as any,\n { root },\n { command: \"build\", mode: \"production\" },\n ) as UserConfig;\n\n const renderBuiltUrl = config.experimental?.renderBuiltUrl;\n if (!renderBuiltUrl) {\n throw new Error(\"experimental.renderBuiltUrl is not defined\");\n }\n return renderBuiltUrl;\n}\n\ndescribe(\"skybridge plugin renderBuiltUrl\", () => {\n let root: string;\n\n beforeEach(() => {\n root = mkdtempSync(join(tmpdir(), \"skybridge-plugin-\"));\n mkdirSync(join(root, \"views\"), { recursive: true });\n });\n\n afterEach(() => {\n rmSync(root, { recursive: true, force: true });\n });\n\n // Assets referenced from JS can't use `import.meta.url`: views render in a\n // host sandbox iframe (web-sandbox.oaiusercontent.com), so the module origin\n // doesn't point back at the Skybridge server. They have to be resolved at\n // runtime against `window.skybridge.serverUrl` instead.\n it(\"resolves JS asset references against window.skybridge.serverUrl\", () => {\n const renderBuiltUrl = getRenderBuiltUrl(root);\n\n expect(\n renderBuiltUrl(\"assets/logo-BXtQ8kd2.png\", {\n hostId: \"assets/view-CwF9pQ1a.js\",\n hostType: \"js\",\n type: \"asset\",\n ssr: false,\n }),\n ).toEqual({\n runtime: `window.skybridge.serverUrl + \"/assets/assets/logo-BXtQ8kd2.png\"`,\n });\n });\n\n // Reproducer for the CSS build failure: Vite has nowhere to evaluate a\n // runtime expression inside a stylesheet, so `{ runtime }` makes\n // `vite:css-post` throw for any `url()` in CSS. It isn't needed either — the\n // stylesheet is served from `${serverUrl}/assets/…`, and a relative `url()`\n // resolves against the stylesheet's own URL, tunnel origin included.\n it(\"resolves CSS asset references relatively\", () => {\n const renderBuiltUrl = getRenderBuiltUrl(root);\n\n expect(\n renderBuiltUrl(\"assets/demo-0IQFxQqs.woff2\", {\n hostId: \"assets/style-DmQ2xY7b.css\",\n hostType: \"css\",\n type: \"asset\",\n ssr: false,\n }),\n ).toEqual({ relative: true });\n });\n});\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skybridge",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "Skybridge is a framework for building ChatGPT and MCP Apps",
5
5
  "repository": {
6
6
  "type": "git",