skybridge 0.0.0-dev.b67d8de → 0.0.0-dev.b6ba410

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.
Files changed (36) hide show
  1. package/dist/cli/build-helpers.js +3 -1
  2. package/dist/cli/build-helpers.js.map +1 -1
  3. package/dist/cli/use-nodemon.js +1 -1
  4. package/dist/cli/use-nodemon.js.map +1 -1
  5. package/dist/server/auth/providers/custom.d.ts +0 -6
  6. package/dist/server/auth/providers/custom.js +4 -7
  7. package/dist/server/auth/providers/custom.js.map +1 -1
  8. package/dist/server/auth/providers/custom.test.js +0 -21
  9. package/dist/server/auth/providers/custom.test.js.map +1 -1
  10. package/dist/server/auth/providers/descope.js +2 -4
  11. package/dist/server/auth/providers/descope.js.map +1 -1
  12. package/dist/server/auth/providers/descope.test.js +10 -12
  13. package/dist/server/auth/providers/descope.test.js.map +1 -1
  14. package/dist/server/index.d.ts +1 -1
  15. package/dist/server/index.js.map +1 -1
  16. package/dist/server/server.d.ts +3 -2
  17. package/dist/server/server.js +13 -4
  18. package/dist/server/server.js.map +1 -1
  19. package/dist/server/skills-integration.test.js +1 -1
  20. package/dist/server/skills-integration.test.js.map +1 -1
  21. package/dist/server/skills.d.ts +26 -2
  22. package/dist/server/skills.js +203 -76
  23. package/dist/server/skills.js.map +1 -1
  24. package/dist/server/skills.test.js +42 -38
  25. package/dist/server/skills.test.js.map +1 -1
  26. package/dist/test/view.test.js +3 -3
  27. package/dist/test/view.test.js.map +1 -1
  28. package/dist/web/bridges/adaptor.js +0 -20
  29. package/dist/web/bridges/adaptor.js.map +1 -1
  30. package/dist/web/bridges/adaptor.test.js +0 -11
  31. package/dist/web/bridges/adaptor.test.js.map +1 -1
  32. package/dist/web/data-llm.test.js +0 -8
  33. package/dist/web/data-llm.test.js.map +1 -1
  34. package/dist/web/plugin/plugin.js +1 -0
  35. package/dist/web/plugin/plugin.js.map +1 -1
  36. package/package.json +9 -10
@@ -1,13 +1,19 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, readdirSync, readFileSync } from "node:fs";
3
- import { extname, join } from "node:path";
2
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync, } from "node:fs";
3
+ import { extname, join, resolve, sep } from "node:path";
4
4
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
6
- import { parse as parseYaml } from "yaml";
7
6
  import { z } from "zod";
7
+ /**
8
+ * Skills over MCP — the Skills Extension ([SEP-2640](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2640), experimental).
9
+ * All spec-facing logic is kept in this one module so churn in the in-review
10
+ * SEP stays a single-file change.
11
+ */
8
12
  export const SKILLS_EXTENSION_KEY = "io.modelcontextprotocol/skills";
9
13
  export const SKILL_INDEX_URI = "skill://index.json";
10
14
  const SKILL_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
15
+ // Only the fields the extension depends on are validated; `.loose()` keeps any
16
+ // other frontmatter, which the index echoes verbatim.
11
17
  const SkillFrontmatterSchema = z
12
18
  .object({
13
19
  name: z
@@ -18,37 +24,90 @@ const SkillFrontmatterSchema = z
18
24
  description: z.string().min(1).max(1024),
19
25
  })
20
26
  .loose();
27
+ const mimeTypeForFile = (name) => {
28
+ switch (extname(name)) {
29
+ case ".md":
30
+ return "text/markdown";
31
+ case ".json":
32
+ return "application/json";
33
+ default:
34
+ return "text/plain";
35
+ }
36
+ };
21
37
  const sha256 = (content) => `sha256:${createHash("sha256").update(content, "utf8").digest("hex")}`;
22
- const FRONTMATTER_RE = /^?\s*---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
38
+ const unquote = (raw) => {
39
+ const v = raw.trim();
40
+ if (v.length >= 2 &&
41
+ ((v.startsWith('"') && v.endsWith('"')) ||
42
+ (v.startsWith("'") && v.endsWith("'")))) {
43
+ return v.slice(1, -1);
44
+ }
45
+ return v;
46
+ };
47
+ // Supports only the frontmatter subset the Agent Skills spec uses — top-level
48
+ // scalars plus one level of nested `key: value` maps. Anything else (sequences,
49
+ // block scalars, deeper nesting) throws rather than silently mis-parsing, which
50
+ // avoids pulling in a full YAML dependency.
23
51
  function parseFrontmatter(content, source) {
24
- const match = FRONTMATTER_RE.exec(content);
52
+ const match = /^?\s*---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(content);
25
53
  if (!match) {
26
54
  throw new Error(`Skill ${source} is missing YAML frontmatter`);
27
55
  }
28
- try {
29
- return parseYaml(match[1] ?? "");
30
- }
31
- catch (error) {
32
- const detail = error instanceof Error ? error.message : String(error);
33
- throw new Error(`Cannot parse frontmatter of ${source}: ${detail}`);
56
+ const result = {};
57
+ let currentMap = null;
58
+ for (const line of (match[1] ?? "").split(/\r?\n/)) {
59
+ if (line.trim() === "" || line.trim().startsWith("#")) {
60
+ continue;
61
+ }
62
+ if (/^\s/.test(line)) {
63
+ const nested = /^\s+([^:#]+):[ \t]?(.*)$/.exec(line);
64
+ const nestedKey = nested?.[1];
65
+ if (!currentMap || nestedKey === undefined) {
66
+ throw new Error(`Cannot parse frontmatter of ${source} at: "${line}"`);
67
+ }
68
+ currentMap[nestedKey.trim()] = unquote(nested?.[2] ?? "");
69
+ continue;
70
+ }
71
+ const top = /^([^:#\s][^:]*):[ \t]?(.*)$/.exec(line);
72
+ const topKey = top?.[1];
73
+ if (topKey === undefined) {
74
+ throw new Error(`Cannot parse frontmatter of ${source} at: "${line}"`);
75
+ }
76
+ const key = topKey.trim();
77
+ const value = top?.[2] ?? "";
78
+ if (value.trim() === "") {
79
+ currentMap = {};
80
+ result[key] = currentMap;
81
+ }
82
+ else {
83
+ if (/^[-|>[{]/.test(value.trim())) {
84
+ throw new Error(`Unsupported frontmatter value for "${key}" in ${source}: only scalars and simple maps are supported`);
85
+ }
86
+ result[key] = unquote(value);
87
+ currentMap = null;
88
+ }
34
89
  }
90
+ return result;
35
91
  }
36
92
  const readSkillDir = (root, rel = "") => {
37
93
  const files = {};
38
94
  for (const entry of readdirSync(join(root, rel), { withFileTypes: true })) {
39
95
  const childRel = rel ? `${rel}/${entry.name}` : entry.name;
40
- if (entry.isSymbolicLink()) {
41
- continue;
42
- }
43
96
  if (entry.isDirectory()) {
44
97
  Object.assign(files, readSkillDir(root, childRel));
45
98
  }
46
- else if (entry.isFile() && extname(entry.name) === ".md") {
47
- files[childRel] = readFileSync(join(root, childRel), "utf8");
99
+ else if (entry.isFile()) {
100
+ files[childRel] = {
101
+ text: readFileSync(join(root, childRel), "utf8"),
102
+ mimeType: mimeTypeForFile(entry.name),
103
+ };
48
104
  }
49
105
  }
50
106
  return files;
51
107
  };
108
+ // Each immediate subdirectory with a `SKILL.md` is one skill; its directory name
109
+ // must equal the frontmatter `name`. Throws on any invalid skill so the
110
+ // build/dev startup fails loudly instead of silently skipping.
52
111
  export function discoverSkills(dir) {
53
112
  if (!existsSync(dir)) {
54
113
  return [];
@@ -59,13 +118,14 @@ export function discoverSkills(dir) {
59
118
  continue;
60
119
  }
61
120
  const skillRoot = join(dir, entry.name);
62
- const files = readSkillDir(skillRoot);
63
- const skillMd = files["SKILL.md"];
64
- if (skillMd === undefined) {
121
+ const skillMdPath = join(skillRoot, "SKILL.md");
122
+ if (!existsSync(skillMdPath)) {
65
123
  continue;
66
124
  }
125
+ const skillMd = readFileSync(skillMdPath, "utf8");
67
126
  const source = `${entry.name}/SKILL.md`;
68
- const parsed = SkillFrontmatterSchema.safeParse(parseFrontmatter(skillMd, source));
127
+ const raw = parseFrontmatter(skillMd, source);
128
+ const parsed = SkillFrontmatterSchema.safeParse(raw);
69
129
  if (!parsed.success) {
70
130
  throw new Error(`Invalid skill frontmatter in ${source}: ${z.prettifyError(parsed.error)}`);
71
131
  }
@@ -74,13 +134,14 @@ export function discoverSkills(dir) {
74
134
  }
75
135
  skills.push({
76
136
  name: entry.name,
77
- frontmatter: parsed.data,
137
+ frontmatter: raw,
78
138
  digest: sha256(skillMd),
79
- files,
139
+ files: readSkillDir(skillRoot),
80
140
  });
81
141
  }
82
142
  return skills;
83
143
  }
144
+ // Parses a `skill://<name>/<subpath>` URI and rejects `..`/`.` traversal.
84
145
  export function skillUriToRelPath(uri) {
85
146
  const match = /^skill:\/\/([^/]+)(?:\/(.*))?$/.exec(uri.replace(/\/+$/, ""));
86
147
  if (!match) {
@@ -93,55 +154,120 @@ export function skillUriToRelPath(uri) {
93
154
  }
94
155
  return { name, relPath: segments.join("/") };
95
156
  }
96
- function listDir(skill, relPath) {
97
- const prefix = relPath === "" ? "" : `${relPath}/`;
98
- const children = new Map();
99
- let matched = relPath === "";
100
- for (const filePath of Object.keys(skill.files)) {
101
- if (!filePath.startsWith(prefix)) {
102
- continue;
103
- }
104
- matched = true;
105
- const rest = filePath.slice(prefix.length);
106
- const slash = rest.indexOf("/");
107
- if (slash === -1) {
108
- children.set(rest, "text/markdown");
109
- }
110
- else {
111
- children.set(rest.slice(0, slash), "inode/directory");
157
+ export function manifestSource(manifest) {
158
+ const byName = new Map(manifest.map((s) => [s.name, s]));
159
+ const fileKey = (relPath) => relPath.replace(/\/+$/, "");
160
+ return {
161
+ list: () => manifest,
162
+ readFile: (name, relPath) => byName.get(name)?.files[relPath] ?? null,
163
+ readDir: (name, relPath) => {
164
+ const skill = byName.get(name);
165
+ if (!skill) {
166
+ return null;
167
+ }
168
+ const prefix = relPath === "" ? "" : `${fileKey(relPath)}/`;
169
+ const children = new Map();
170
+ let matched = relPath === "";
171
+ for (const filePath of Object.keys(skill.files)) {
172
+ if (!filePath.startsWith(prefix)) {
173
+ continue;
174
+ }
175
+ matched = true;
176
+ const rest = filePath.slice(prefix.length);
177
+ const slash = rest.indexOf("/");
178
+ if (slash === -1) {
179
+ children.set(rest, mimeTypeForFile(rest));
180
+ }
181
+ else {
182
+ children.set(rest.slice(0, slash), "inode/directory");
183
+ }
184
+ }
185
+ if (!matched) {
186
+ return null;
187
+ }
188
+ return [...children].map(([name, mimeType]) => ({ name, mimeType }));
189
+ },
190
+ };
191
+ }
192
+ export function diskSource(dir) {
193
+ const rootReal = existsSync(dir) ? realpathSync(dir) : resolve(dir);
194
+ // Confirms the real (symlink-followed) path stays within the skills root, else
195
+ // null — closes the traversal hole where an in-tree symlink points outside it.
196
+ const contain = (name, relPath) => {
197
+ const path = join(dir, name, relPath);
198
+ if (!existsSync(path)) {
199
+ return null;
112
200
  }
113
- }
114
- if (!matched) {
115
- return null;
116
- }
117
- return [...children].map(([name, mimeType]) => ({ name, mimeType }));
201
+ const real = realpathSync(path);
202
+ return real === rootReal || real.startsWith(`${rootReal}${sep}`)
203
+ ? path
204
+ : null;
205
+ };
206
+ return {
207
+ list: () => discoverSkills(dir),
208
+ readFile: (name, relPath) => {
209
+ const path = contain(name, relPath || "SKILL.md");
210
+ if (!path || !statSync(path).isFile()) {
211
+ return null;
212
+ }
213
+ return {
214
+ text: readFileSync(path, "utf8"),
215
+ mimeType: mimeTypeForFile(path),
216
+ };
217
+ },
218
+ readDir: (name, relPath) => {
219
+ const path = contain(name, relPath);
220
+ if (!path || !statSync(path).isDirectory()) {
221
+ return null;
222
+ }
223
+ return readdirSync(path, { withFileTypes: true }).map((entry) => ({
224
+ name: entry.name,
225
+ mimeType: entry.isDirectory()
226
+ ? "inode/directory"
227
+ : mimeTypeForFile(entry.name),
228
+ }));
229
+ },
230
+ };
118
231
  }
119
232
  const DirectoryReadRequestSchema = z.object({
120
233
  method: z.literal("resources/directory/read"),
121
234
  params: z.object({ uri: z.string(), cursor: z.string().optional() }),
122
235
  });
123
- 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];
127
- if (text === undefined) {
128
- throw new McpError(ErrorCode.InvalidParams, `Not found: ${href}`);
129
- }
130
- return {
131
- contents: [{ uri: href, text, mimeType: "text/markdown" }],
132
- };
133
- };
134
- for (const skill of manifest) {
135
- server.registerResource(skill.name, `skill://${skill.name}/SKILL.md`, {
136
- description: String(skill.frontmatter.description),
236
+ // Registers per SEP-2640: one resource per `SKILL.md`, a template for supporting
237
+ // files, the `skill://index.json` index, and optionally `resources/directory/read`.
238
+ export function registerSkills(server, source, { directoryRead }) {
239
+ for (const skill of source.list()) {
240
+ const uri = `skill://${skill.name}/SKILL.md`;
241
+ server.registerResource(skill.name, uri, {
242
+ description: typeof skill.frontmatter.description === "string"
243
+ ? skill.frontmatter.description
244
+ : undefined,
137
245
  mimeType: "text/markdown",
138
- }, (readUri) => serveFile(skill.name, "SKILL.md", readUri.href));
246
+ }, (readUri) => {
247
+ const file = source.readFile(skill.name, "SKILL.md");
248
+ if (!file) {
249
+ throw new McpError(ErrorCode.InvalidParams, `Not found: ${readUri.href}`);
250
+ }
251
+ return {
252
+ contents: [
253
+ { uri: readUri.href, text: file.text, mimeType: file.mimeType },
254
+ ],
255
+ };
256
+ });
139
257
  }
140
258
  server.registerResource("skill-files", new ResourceTemplate("skill://{skillName}/{+filePath}", {
141
259
  list: undefined,
142
260
  }), {}, (readUri) => {
143
261
  const { name, relPath } = skillUriToRelPath(readUri.href);
144
- return serveFile(name, relPath, readUri.href);
262
+ const file = source.readFile(name, relPath);
263
+ if (!file) {
264
+ throw new McpError(ErrorCode.InvalidParams, `Not found: ${readUri.href}`);
265
+ }
266
+ return {
267
+ contents: [
268
+ { uri: readUri.href, text: file.text, mimeType: file.mimeType },
269
+ ],
270
+ };
145
271
  });
146
272
  server.registerResource("skill-index", SKILL_INDEX_URI, { mimeType: "application/json" }, () => ({
147
273
  contents: [
@@ -149,7 +275,7 @@ export function registerSkills(server, manifest) {
149
275
  uri: SKILL_INDEX_URI,
150
276
  mimeType: "application/json",
151
277
  text: JSON.stringify({
152
- skills: manifest.map((skill) => ({
278
+ skills: source.list().map((skill) => ({
153
279
  url: `skill://${skill.name}/SKILL.md`,
154
280
  digest: skill.digest,
155
281
  frontmatter: skill.frontmatter,
@@ -158,21 +284,22 @@ export function registerSkills(server, manifest) {
158
284
  },
159
285
  ],
160
286
  }));
161
- server.server.setRequestHandler(DirectoryReadRequestSchema, ({ params }) => {
162
- const { name, relPath } = skillUriToRelPath(params.uri);
163
- const skill = byName.get(name);
164
- const entries = skill ? listDir(skill, relPath) : null;
165
- if (!entries) {
166
- throw new McpError(ErrorCode.InvalidParams, `Not a directory: ${params.uri}`);
167
- }
168
- const base = params.uri.replace(/\/+$/, "");
169
- return {
170
- resources: entries.map((entry) => ({
171
- uri: `${base}/${entry.name}`,
172
- name: entry.name,
173
- mimeType: entry.mimeType,
174
- })),
175
- };
176
- });
287
+ if (directoryRead) {
288
+ server.server.setRequestHandler(DirectoryReadRequestSchema, ({ params }) => {
289
+ const { name, relPath } = skillUriToRelPath(params.uri);
290
+ const entries = source.readDir(name, relPath);
291
+ if (!entries) {
292
+ throw new McpError(ErrorCode.InvalidParams, `Not a directory: ${params.uri}`);
293
+ }
294
+ const base = params.uri.replace(/\/+$/, "");
295
+ return {
296
+ resources: entries.map((entry) => ({
297
+ uri: `${base}/${entry.name}`,
298
+ name: entry.name,
299
+ mimeType: entry.mimeType,
300
+ })),
301
+ };
302
+ });
303
+ }
177
304
  }
178
305
  //# sourceMappingURL=skills.js.map
@@ -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 {\n existsSync,\n readdirSync,\n readFileSync,\n realpathSync,\n statSync,\n} from \"node:fs\";\nimport { extname, join, resolve, sep } 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 { z } from \"zod\";\n\n/**\n * Skills over MCP — the Skills Extension ([SEP-2640](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2640), experimental).\n * All spec-facing logic is kept in this one module so churn in the in-review\n * SEP stays a single-file change.\n */\n\nexport const SKILLS_EXTENSION_KEY = \"io.modelcontextprotocol/skills\";\nexport const SKILL_INDEX_URI = \"skill://index.json\";\n\nconst SKILL_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;\n\n// Only the fields the extension depends on are validated; `.loose()` keeps any\n// other frontmatter, which the index echoes verbatim.\nconst SkillFrontmatterSchema = z\n .object({\n name: z\n .string()\n .min(1)\n .max(64)\n .regex(\n SKILL_NAME_RE,\n \"must be lowercase alphanumeric words separated by single hyphens\",\n ),\n description: z.string().min(1).max(1024),\n })\n .loose();\n\nexport interface SkillFile {\n text: string;\n mimeType: string;\n}\n\nexport interface Skill {\n name: string;\n frontmatter: Record<string, unknown>;\n /** `sha256:<hex>` over the raw `SKILL.md` bytes. */\n digest: string;\n /** Keyed by POSIX path relative to the skill directory, including `SKILL.md`. */\n files: Record<string, SkillFile>;\n}\n\nexport type SkillsManifest = Skill[];\n\nconst mimeTypeForFile = (name: string): string => {\n switch (extname(name)) {\n case \".md\":\n return \"text/markdown\";\n case \".json\":\n return \"application/json\";\n default:\n return \"text/plain\";\n }\n};\n\nconst sha256 = (content: string): string =>\n `sha256:${createHash(\"sha256\").update(content, \"utf8\").digest(\"hex\")}`;\n\nconst unquote = (raw: string): string => {\n const v = raw.trim();\n if (\n v.length >= 2 &&\n ((v.startsWith('\"') && v.endsWith('\"')) ||\n (v.startsWith(\"'\") && v.endsWith(\"'\")))\n ) {\n return v.slice(1, -1);\n }\n return v;\n};\n\n// Supports only the frontmatter subset the Agent Skills spec uses — top-level\n// scalars plus one level of nested `key: value` maps. Anything else (sequences,\n// block scalars, deeper nesting) throws rather than silently mis-parsing, which\n// avoids pulling in a full YAML dependency.\nfunction parseFrontmatter(\n content: string,\n source: string,\n): Record<string, unknown> {\n const match = /^?\\s*---[ \\t]*\\r?\\n([\\s\\S]*?)\\r?\\n---[ \\t]*(?:\\r?\\n|$)/.exec(\n content,\n );\n if (!match) {\n throw new Error(`Skill ${source} is missing YAML frontmatter`);\n }\n\n const result: Record<string, unknown> = {};\n let currentMap: Record<string, string> | null = null;\n\n for (const line of (match[1] ?? \"\").split(/\\r?\\n/)) {\n if (line.trim() === \"\" || line.trim().startsWith(\"#\")) {\n continue;\n }\n\n if (/^\\s/.test(line)) {\n const nested = /^\\s+([^:#]+):[ \\t]?(.*)$/.exec(line);\n const nestedKey = nested?.[1];\n if (!currentMap || nestedKey === undefined) {\n throw new Error(`Cannot parse frontmatter of ${source} at: \"${line}\"`);\n }\n currentMap[nestedKey.trim()] = unquote(nested?.[2] ?? \"\");\n continue;\n }\n\n const top = /^([^:#\\s][^:]*):[ \\t]?(.*)$/.exec(line);\n const topKey = top?.[1];\n if (topKey === undefined) {\n throw new Error(`Cannot parse frontmatter of ${source} at: \"${line}\"`);\n }\n const key = topKey.trim();\n const value = top?.[2] ?? \"\";\n if (value.trim() === \"\") {\n currentMap = {};\n result[key] = currentMap;\n } else {\n if (/^[-|>[{]/.test(value.trim())) {\n throw new Error(\n `Unsupported frontmatter value for \"${key}\" in ${source}: only scalars and simple maps are supported`,\n );\n }\n result[key] = unquote(value);\n currentMap = null;\n }\n }\n return result;\n}\n\nconst readSkillDir = (root: string, rel = \"\"): Record<string, SkillFile> => {\n const files: Record<string, SkillFile> = {};\n for (const entry of readdirSync(join(root, rel), { withFileTypes: true })) {\n const childRel = rel ? `${rel}/${entry.name}` : entry.name;\n if (entry.isDirectory()) {\n Object.assign(files, readSkillDir(root, childRel));\n } else if (entry.isFile()) {\n files[childRel] = {\n text: readFileSync(join(root, childRel), \"utf8\"),\n mimeType: mimeTypeForFile(entry.name),\n };\n }\n }\n return files;\n};\n\n// Each immediate subdirectory with a `SKILL.md` is one skill; its directory name\n// must equal the frontmatter `name`. Throws on any invalid skill so the\n// build/dev startup fails loudly instead of silently skipping.\nexport function discoverSkills(dir: string): SkillsManifest {\n if (!existsSync(dir)) {\n return [];\n }\n\n const skills: SkillsManifest = [];\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (!entry.isDirectory()) {\n continue;\n }\n\n const skillRoot = join(dir, entry.name);\n const skillMdPath = join(skillRoot, \"SKILL.md\");\n if (!existsSync(skillMdPath)) {\n continue;\n }\n\n const skillMd = readFileSync(skillMdPath, \"utf8\");\n const source = `${entry.name}/SKILL.md`;\n const raw = parseFrontmatter(skillMd, source);\n\n const parsed = SkillFrontmatterSchema.safeParse(raw);\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: raw,\n digest: sha256(skillMd),\n files: readSkillDir(skillRoot),\n });\n }\n return skills;\n}\n\n// Parses a `skill://<name>/<subpath>` URI and rejects `..`/`.` traversal.\nexport function skillUriToRelPath(uri: string): {\n name: string;\n relPath: string;\n} {\n const match = /^skill:\\/\\/([^/]+)(?:\\/(.*))?$/.exec(uri.replace(/\\/+$/, \"\"));\n if (!match) {\n throw new McpError(ErrorCode.InvalidParams, `Invalid skill uri: ${uri}`);\n }\n const [, name = \"\", relPath = \"\"] = match;\n const segments = relPath.split(\"/\").filter(Boolean);\n if (name === \"\" || segments.includes(\"..\") || segments.includes(\".\")) {\n throw new McpError(ErrorCode.InvalidParams, `Invalid skill uri: ${uri}`);\n }\n return { name, relPath: segments.join(\"/\") };\n}\n\n// The indirection both runtime modes share: an in-memory manifest (production)\n// and a live disk reader (dev). `readFile`/`readDir` return null when absent.\nexport interface SkillsSource {\n list(): Skill[];\n readFile(name: string, relPath: string): SkillFile | null;\n readDir(\n name: string,\n relPath: string,\n ): { name: string; mimeType: string }[] | null;\n}\n\nexport function manifestSource(manifest: SkillsManifest): SkillsSource {\n const byName = new Map(manifest.map((s) => [s.name, s]));\n const fileKey = (relPath: string) => relPath.replace(/\\/+$/, \"\");\n\n return {\n list: () => manifest,\n readFile: (name, relPath) => byName.get(name)?.files[relPath] ?? null,\n readDir: (name, relPath) => {\n const skill = byName.get(name);\n if (!skill) {\n return null;\n }\n const prefix = relPath === \"\" ? \"\" : `${fileKey(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, mimeTypeForFile(rest));\n } else {\n children.set(rest.slice(0, slash), \"inode/directory\");\n }\n }\n if (!matched) {\n return null;\n }\n return [...children].map(([name, mimeType]) => ({ name, mimeType }));\n },\n };\n}\n\nexport function diskSource(dir: string): SkillsSource {\n const rootReal = existsSync(dir) ? realpathSync(dir) : resolve(dir);\n\n // Confirms the real (symlink-followed) path stays within the skills root, else\n // null — closes the traversal hole where an in-tree symlink points outside it.\n const contain = (name: string, relPath: string): string | null => {\n const path = join(dir, name, relPath);\n if (!existsSync(path)) {\n return null;\n }\n const real = realpathSync(path);\n return real === rootReal || real.startsWith(`${rootReal}${sep}`)\n ? path\n : null;\n };\n\n return {\n list: () => discoverSkills(dir),\n readFile: (name, relPath) => {\n const path = contain(name, relPath || \"SKILL.md\");\n if (!path || !statSync(path).isFile()) {\n return null;\n }\n return {\n text: readFileSync(path, \"utf8\"),\n mimeType: mimeTypeForFile(path),\n };\n },\n readDir: (name, relPath) => {\n const path = contain(name, relPath);\n if (!path || !statSync(path).isDirectory()) {\n return null;\n }\n return readdirSync(path, { withFileTypes: true }).map((entry) => ({\n name: entry.name,\n mimeType: entry.isDirectory()\n ? \"inode/directory\"\n : mimeTypeForFile(entry.name),\n }));\n },\n };\n}\n\n/** The subset of `McpServer` that skill registration needs. */\ninterface SkillRegistrar {\n registerResource(\n name: string,\n uri: string,\n config: ResourceMetadata,\n readCallback: ReadResourceCallback,\n ): unknown;\n registerResource(\n name: string,\n template: ResourceTemplate,\n config: ResourceMetadata,\n readCallback: ReadResourceTemplateCallback,\n ): unknown;\n readonly server: SdkMcpServer[\"server\"];\n}\n\nconst DirectoryReadRequestSchema = z.object({\n method: z.literal(\"resources/directory/read\"),\n params: z.object({ uri: z.string(), cursor: z.string().optional() }),\n});\n\n// Registers per SEP-2640: one resource per `SKILL.md`, a template for supporting\n// files, the `skill://index.json` index, and optionally `resources/directory/read`.\nexport function registerSkills(\n server: SkillRegistrar,\n source: SkillsSource,\n { directoryRead }: { directoryRead: boolean },\n): void {\n for (const skill of source.list()) {\n const uri = `skill://${skill.name}/SKILL.md`;\n server.registerResource(\n skill.name,\n uri,\n {\n description:\n typeof skill.frontmatter.description === \"string\"\n ? skill.frontmatter.description\n : undefined,\n mimeType: \"text/markdown\",\n },\n (readUri) => {\n const file = source.readFile(skill.name, \"SKILL.md\");\n if (!file) {\n throw new McpError(\n ErrorCode.InvalidParams,\n `Not found: ${readUri.href}`,\n );\n }\n return {\n contents: [\n { uri: readUri.href, text: file.text, mimeType: file.mimeType },\n ],\n };\n },\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 const file = source.readFile(name, relPath);\n if (!file) {\n throw new McpError(\n ErrorCode.InvalidParams,\n `Not found: ${readUri.href}`,\n );\n }\n return {\n contents: [\n { uri: readUri.href, text: file.text, mimeType: file.mimeType },\n ],\n };\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: source.list().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 if (directoryRead) {\n server.server.setRequestHandler(\n DirectoryReadRequestSchema,\n ({ params }) => {\n const { name, relPath } = skillUriToRelPath(params.uri);\n const entries = source.readDir(name, relPath);\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 }\n}\n"]}
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
6
6
  import { describe, expect, it, vi } from "vitest";
7
- import { discoverSkills, registerSkills, SKILL_INDEX_URI, skillUriToRelPath, } from "./skills.js";
7
+ import { discoverSkills, diskSource, manifestSource, registerSkills, SKILL_INDEX_URI, skillUriToRelPath, } from "./skills.js";
8
8
  function mkSkillDir(files) {
9
9
  const root = mkdtempSync(join(tmpdir(), "skybridge-skills-"));
10
10
  for (const [rel, content] of Object.entries(files)) {
@@ -60,18 +60,9 @@ describe("discoverSkills", () => {
60
60
  const dir = mkSkillDir({ "s/SKILL.md": FM("other", "d") });
61
61
  expect(() => discoverSkills(dir)).toThrow(/must match its directory name/);
62
62
  });
63
- it("preserves arbitrary YAML frontmatter (arrays, nesting) verbatim", () => {
63
+ it("throws on unsupported (non-scalar) frontmatter values", () => {
64
64
  const dir = mkSkillDir({
65
- "s/SKILL.md": "---\nname: s\ndescription: d\nallowed-tools:\n - get-order\n - refund\n---\n",
66
- });
67
- expect(discoverSkills(dir)[0]?.frontmatter["allowed-tools"]).toEqual([
68
- "get-order",
69
- "refund",
70
- ]);
71
- });
72
- it("throws on malformed YAML frontmatter", () => {
73
- const dir = mkSkillDir({
74
- "s/SKILL.md": "---\nname: 's\ndescription: [unclosed\n---\n",
65
+ "s/SKILL.md": "---\nname: s\ndescription: d\ntags:\n - a\n - b\n---\n",
75
66
  });
76
67
  expect(() => discoverSkills(dir)).toThrow(/Cannot parse frontmatter/);
77
68
  });
@@ -100,8 +91,8 @@ describe("registerSkills", () => {
100
91
  frontmatter: { name: "refunds", description: "Process refunds" },
101
92
  digest: "sha256:abc",
102
93
  files: {
103
- "SKILL.md": "# Refunds",
104
- "templates/email.md": "Hi",
94
+ "SKILL.md": { text: "# Refunds", mimeType: "text/markdown" },
95
+ "templates/email.md": { text: "Hi", mimeType: "text/markdown" },
105
96
  },
106
97
  },
107
98
  ];
@@ -126,7 +117,9 @@ describe("registerSkills", () => {
126
117
  it("builds an index.json with url, digest, and verbatim frontmatter", () => {
127
118
  const { server, resources } = fakeRegistrar();
128
119
  // biome-ignore lint/suspicious/noExplicitAny: structural test double
129
- registerSkills(server, manifest);
120
+ registerSkills(server, manifestSource(manifest), {
121
+ directoryRead: false,
122
+ });
130
123
  const index = resources.get("skill-index");
131
124
  const result = index?.cb(new URL(SKILL_INDEX_URI));
132
125
  expect(JSON.parse(result.contents[0]?.text ?? "")).toEqual({
@@ -142,17 +135,27 @@ describe("registerSkills", () => {
142
135
  it("serves supporting files through the template resource", () => {
143
136
  const { server, resources } = fakeRegistrar();
144
137
  // biome-ignore lint/suspicious/noExplicitAny: structural test double
145
- registerSkills(server, manifest);
138
+ registerSkills(server, manifestSource(manifest), {
139
+ directoryRead: false,
140
+ });
146
141
  const tpl = resources.get("skill-files");
147
142
  expect(tpl?.uri).toBeInstanceOf(ResourceTemplate);
148
143
  const url = new URL("skill://refunds/templates/email.md");
149
144
  const result = tpl?.cb(url, {}, {});
150
145
  expect(result.contents[0]?.text).toBe("Hi");
151
146
  });
152
- it("lists a subdirectory through the directory-read handler", () => {
147
+ it("only registers the directory-read handler when enabled", () => {
148
+ const off = fakeRegistrar();
149
+ // biome-ignore lint/suspicious/noExplicitAny: structural test double
150
+ registerSkills(off.server, manifestSource(manifest), {
151
+ directoryRead: false,
152
+ });
153
+ expect(off.getDirHandler()).toBeUndefined();
153
154
  const on = fakeRegistrar();
154
155
  // biome-ignore lint/suspicious/noExplicitAny: structural test double
155
- registerSkills(on.server, manifest);
156
+ registerSkills(on.server, manifestSource(manifest), {
157
+ directoryRead: true,
158
+ });
156
159
  const result = on.getDirHandler()?.({
157
160
  params: { uri: "skill://refunds/templates" },
158
161
  });
@@ -167,7 +170,9 @@ describe("registerSkills", () => {
167
170
  it("lists a skill root and marks subdirectories as inode/directory", () => {
168
171
  const { server, getDirHandler } = fakeRegistrar();
169
172
  // biome-ignore lint/suspicious/noExplicitAny: structural test double
170
- registerSkills(server, manifest);
173
+ registerSkills(server, manifestSource(manifest), {
174
+ directoryRead: true,
175
+ });
171
176
  const result = getDirHandler()?.({
172
177
  params: { uri: "skill://refunds" },
173
178
  });
@@ -183,28 +188,27 @@ describe("registerSkills", () => {
183
188
  });
184
189
  });
185
190
  });
186
- describe("discoverSkills symlink safety", () => {
187
- it("excludes symlinked files and directories from the manifest", () => {
188
- const outside = mkdtempSync(join(tmpdir(), "skybridge-outside-"));
189
- writeFileSync(join(outside, "secret.md"), "TOP SECRET");
191
+ describe("diskSource", () => {
192
+ it("reads files live and lists directories", () => {
190
193
  const dir = mkSkillDir({
191
- "demo/SKILL.md": FM("demo", "d"),
192
- "demo/real.md": "ok",
194
+ "refunds/SKILL.md": FM("refunds", "d"),
195
+ "refunds/templates/email.md": "Hi",
193
196
  });
194
- symlinkSync(join(outside, "secret.md"), join(dir, "demo", "leak.md"));
195
- symlinkSync(outside, join(dir, "demo", "escape"));
196
- const files = discoverSkills(dir)[0]?.files ?? {};
197
- expect(Object.keys(files).sort()).toEqual(["SKILL.md", "real.md"]);
197
+ const source = diskSource(dir);
198
+ expect(source.list()).toHaveLength(1);
199
+ expect(source.readFile("refunds", "templates/email.md")?.text).toBe("Hi");
200
+ expect(source.readFile("refunds", "missing.md")).toBeNull();
201
+ expect(source.readDir("refunds", "nope")).toBeNull();
198
202
  });
199
- it("ignores non-markdown supporting files", () => {
200
- const dir = mkSkillDir({
201
- "demo/SKILL.md": FM("demo", "d"),
202
- "demo/notes.md": "kept",
203
- "demo/data.json": "{}",
204
- "demo/scripts/run.py": "print(1)",
205
- });
206
- const files = discoverSkills(dir)[0]?.files ?? {};
207
- expect(Object.keys(files).sort()).toEqual(["SKILL.md", "notes.md"]);
203
+ it("refuses to read or list through a symlink that escapes the skills root", () => {
204
+ const outside = mkdtempSync(join(tmpdir(), "skybridge-outside-"));
205
+ writeFileSync(join(outside, "secret.txt"), "TOP SECRET");
206
+ const dir = mkSkillDir({ "demo/SKILL.md": FM("demo", "d") });
207
+ symlinkSync(join(outside, "secret.txt"), join(dir, "demo", "leak.txt"));
208
+ symlinkSync(outside, join(dir, "demo", "escape"));
209
+ const source = diskSource(dir);
210
+ expect(source.readFile("demo", "leak.txt")).toBeNull();
211
+ expect(source.readDir("demo", "escape")).toBeNull();
208
212
  });
209
213
  });
210
214
  //# sourceMappingURL=skills.test.js.map