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.
- package/dist/cli/build-helpers.js +3 -1
- package/dist/cli/build-helpers.js.map +1 -1
- package/dist/cli/use-nodemon.js +1 -1
- package/dist/cli/use-nodemon.js.map +1 -1
- package/dist/server/auth/providers/custom.d.ts +0 -6
- package/dist/server/auth/providers/custom.js +4 -7
- package/dist/server/auth/providers/custom.js.map +1 -1
- package/dist/server/auth/providers/custom.test.js +0 -21
- package/dist/server/auth/providers/custom.test.js.map +1 -1
- package/dist/server/auth/providers/descope.js +2 -4
- package/dist/server/auth/providers/descope.js.map +1 -1
- package/dist/server/auth/providers/descope.test.js +10 -12
- package/dist/server/auth/providers/descope.test.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/server.d.ts +3 -2
- package/dist/server/server.js +13 -4
- package/dist/server/server.js.map +1 -1
- package/dist/server/skills-integration.test.js +1 -1
- package/dist/server/skills-integration.test.js.map +1 -1
- package/dist/server/skills.d.ts +26 -2
- package/dist/server/skills.js +203 -76
- package/dist/server/skills.js.map +1 -1
- package/dist/server/skills.test.js +42 -38
- package/dist/server/skills.test.js.map +1 -1
- package/dist/test/view.test.js +3 -3
- package/dist/test/view.test.js.map +1 -1
- package/dist/web/bridges/adaptor.js +0 -20
- package/dist/web/bridges/adaptor.js.map +1 -1
- package/dist/web/bridges/adaptor.test.js +0 -11
- package/dist/web/bridges/adaptor.test.js.map +1 -1
- package/dist/web/data-llm.test.js +0 -8
- package/dist/web/data-llm.test.js.map +1 -1
- package/dist/web/plugin/plugin.js +1 -0
- package/dist/web/plugin/plugin.js.map +1 -1
- package/package.json +9 -10
package/dist/server/skills.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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()
|
|
47
|
-
files[childRel] =
|
|
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
|
|
63
|
-
|
|
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
|
|
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:
|
|
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
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
children
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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) =>
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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("
|
|
63
|
+
it("throws on unsupported (non-scalar) frontmatter values", () => {
|
|
64
64
|
const dir = mkSkillDir({
|
|
65
|
-
"s/SKILL.md": "---\nname: s\ndescription: d\
|
|
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("
|
|
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("
|
|
187
|
-
it("
|
|
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
|
-
"
|
|
192
|
-
"
|
|
194
|
+
"refunds/SKILL.md": FM("refunds", "d"),
|
|
195
|
+
"refunds/templates/email.md": "Hi",
|
|
193
196
|
});
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
expect(
|
|
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("
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
expect(
|
|
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
|