skybridge 0.0.0-next.59f82a1 → 0.0.0-next.6117fea
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/cli/build-helpers.d.ts +3 -2
- package/dist/cli/build-helpers.js +22 -8
- package/dist/cli/build-helpers.js.map +1 -1
- package/dist/cli/build-helpers.test.js +46 -3
- package/dist/cli/build-helpers.test.js.map +1 -1
- package/dist/cli/build-steps.js +9 -4
- package/dist/cli/build-steps.js.map +1 -1
- package/dist/cli/resolve-skybridge-config.d.ts +5 -0
- package/dist/cli/{resolve-views-dir.js → resolve-skybridge-config.js} +4 -3
- package/dist/cli/resolve-skybridge-config.js.map +1 -0
- package/dist/cli/use-execute-steps.d.ts +1 -1
- package/dist/cli/use-nodemon.js +1 -1
- package/dist/cli/use-nodemon.js.map +1 -1
- package/dist/commands/dev.js +2 -2
- package/dist/commands/dev.js.map +1 -1
- package/dist/server/auth/providers/authplane.d.ts +47 -0
- package/dist/server/auth/providers/authplane.js +57 -0
- package/dist/server/auth/providers/authplane.js.map +1 -0
- package/dist/server/auth/providers/authplane.test.d.ts +1 -0
- package/dist/server/auth/providers/authplane.test.js +138 -0
- package/dist/server/auth/providers/authplane.test.js.map +1 -0
- package/dist/server/auth/providers/custom.d.ts +8 -0
- package/dist/server/auth/providers/custom.js +19 -7
- package/dist/server/auth/providers/custom.js.map +1 -1
- package/dist/server/auth/providers/custom.test.js +53 -4
- package/dist/server/auth/providers/custom.test.js.map +1 -1
- package/dist/server/auth/providers/descope.js +13 -2
- package/dist/server/auth/providers/descope.js.map +1 -1
- package/dist/server/auth/providers/descope.test.js +38 -15
- package/dist/server/auth/providers/descope.test.js.map +1 -1
- package/dist/server/auth/security-schemes.d.ts +23 -0
- package/dist/server/auth/security-schemes.js +64 -0
- package/dist/server/auth/security-schemes.js.map +1 -0
- package/dist/server/auth/security-schemes.test.d.ts +1 -0
- package/dist/server/auth/security-schemes.test.js +99 -0
- package/dist/server/auth/security-schemes.test.js.map +1 -0
- package/dist/server/auth/setup.d.ts +3 -1
- package/dist/server/auth/setup.js +62 -21
- package/dist/server/auth/setup.js.map +1 -1
- package/dist/server/auth/setup.test.js +291 -2
- package/dist/server/auth/setup.test.js.map +1 -1
- package/dist/server/host.d.ts +2 -0
- package/dist/server/host.js +11 -0
- package/dist/server/host.js.map +1 -0
- package/dist/server/host.test.d.ts +1 -0
- package/dist/server/host.test.js +13 -0
- package/dist/server/host.test.js.map +1 -0
- package/dist/server/index.d.ts +3 -1
- package/dist/server/index.js +3 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/server.d.ts +40 -4
- package/dist/server/server.js +114 -15
- package/dist/server/server.js.map +1 -1
- package/dist/server/skills-integration.test.d.ts +1 -0
- package/dist/server/skills-integration.test.js +79 -0
- package/dist/server/skills-integration.test.js.map +1 -0
- package/dist/server/skills.d.ts +29 -0
- package/dist/server/skills.js +195 -0
- package/dist/server/skills.js.map +1 -0
- package/dist/server/skills.test.d.ts +1 -0
- package/dist/server/skills.test.js +247 -0
- package/dist/server/skills.test.js.map +1 -0
- package/dist/server/viewsDevServer.js +4 -1
- package/dist/server/viewsDevServer.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 +34 -3
- package/dist/web/bridges/adaptor.js.map +1 -1
- package/dist/web/bridges/adaptor.test.js +31 -4
- package/dist/web/bridges/adaptor.test.js.map +1 -1
- package/dist/web/bridges/mcp-app/bridge.js +5 -3
- package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
- package/dist/web/bridges/mcp-app/types.d.ts +2 -0
- package/dist/web/bridges/mcp-app/types.js.map +1 -1
- package/dist/web/data-llm.test.js +8 -0
- package/dist/web/data-llm.test.js.map +1 -1
- package/dist/web/hooks/index.d.ts +1 -0
- package/dist/web/hooks/index.js +1 -0
- package/dist/web/hooks/index.js.map +1 -1
- package/dist/web/hooks/test/utils.d.ts +2 -1
- package/dist/web/hooks/test/utils.js +1 -1
- package/dist/web/hooks/test/utils.js.map +1 -1
- package/dist/web/hooks/use-host-info.d.ts +22 -0
- package/dist/web/hooks/use-host-info.js +30 -0
- package/dist/web/hooks/use-host-info.js.map +1 -0
- package/dist/web/hooks/use-host-info.test.d.ts +1 -0
- package/dist/web/hooks/use-host-info.test.js +59 -0
- package/dist/web/hooks/use-host-info.test.js.map +1 -0
- package/dist/web/hooks/use-view-state.js +19 -9
- package/dist/web/hooks/use-view-state.js.map +1 -1
- package/dist/web/hooks/use-view-state.test.js +31 -0
- package/dist/web/hooks/use-view-state.test.js.map +1 -1
- package/dist/web/plugin/plugin-build.test.d.ts +1 -0
- package/dist/web/plugin/plugin-build.test.js +54 -0
- package/dist/web/plugin/plugin-build.test.js.map +1 -0
- package/dist/web/plugin/plugin.d.ts +6 -0
- package/dist/web/plugin/plugin.js +18 -7
- package/dist/web/plugin/plugin.js.map +1 -1
- package/dist/web/plugin/plugin.test.d.ts +1 -0
- package/dist/web/plugin/plugin.test.js +61 -0
- package/dist/web/plugin/plugin.test.js.map +1 -0
- package/package.json +11 -10
- package/dist/cli/resolve-views-dir.d.ts +0 -1
- package/dist/cli/resolve-views-dir.js.map +0 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { extname, join } from "node:path";
|
|
4
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
import { parse as parseYaml } from "yaml";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
export const SKILLS_EXTENSION_KEY = "io.modelcontextprotocol/skills";
|
|
9
|
+
const SKILL_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
10
|
+
const SkillFrontmatterSchema = z
|
|
11
|
+
.object({
|
|
12
|
+
name: z
|
|
13
|
+
.string()
|
|
14
|
+
.min(1)
|
|
15
|
+
.max(64)
|
|
16
|
+
.regex(SKILL_NAME_RE, "must be lowercase alphanumeric words separated by single hyphens"),
|
|
17
|
+
description: z.string().min(1).max(1024),
|
|
18
|
+
})
|
|
19
|
+
.loose();
|
|
20
|
+
const sha256 = (content) => `sha256:${createHash("sha256").update(content, "utf8").digest("hex")}`;
|
|
21
|
+
const FRONTMATTER_RE = /^?\s*---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
|
|
22
|
+
function parseFrontmatter(content, source) {
|
|
23
|
+
const match = FRONTMATTER_RE.exec(content);
|
|
24
|
+
if (!match) {
|
|
25
|
+
throw new Error(`Skill ${source} is missing YAML frontmatter`);
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
return parseYaml(match[1] ?? "");
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
32
|
+
throw new Error(`Cannot parse frontmatter of ${source}: ${detail}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const readSkillDir = (root, rel = "") => {
|
|
36
|
+
const files = {};
|
|
37
|
+
for (const entry of readdirSync(join(root, rel), { withFileTypes: true })) {
|
|
38
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
39
|
+
if (entry.isSymbolicLink()) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (entry.isDirectory()) {
|
|
43
|
+
Object.assign(files, readSkillDir(root, childRel));
|
|
44
|
+
}
|
|
45
|
+
else if (entry.isFile() && extname(entry.name) === ".md") {
|
|
46
|
+
files[childRel] = readFileSync(join(root, childRel), "utf8");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return files;
|
|
50
|
+
};
|
|
51
|
+
export function discoverSkills(dir) {
|
|
52
|
+
if (!existsSync(dir)) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const skills = [];
|
|
56
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
57
|
+
if (!entry.isDirectory()) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const skillRoot = join(dir, entry.name);
|
|
61
|
+
const files = readSkillDir(skillRoot);
|
|
62
|
+
const skillMd = files["SKILL.md"];
|
|
63
|
+
if (skillMd === undefined) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const source = `${entry.name}/SKILL.md`;
|
|
67
|
+
const parsed = SkillFrontmatterSchema.safeParse(parseFrontmatter(skillMd, source));
|
|
68
|
+
if (!parsed.success) {
|
|
69
|
+
throw new Error(`Invalid skill frontmatter in ${source}: ${z.prettifyError(parsed.error)}`);
|
|
70
|
+
}
|
|
71
|
+
if (parsed.data.name !== entry.name) {
|
|
72
|
+
throw new Error(`Skill name "${parsed.data.name}" in ${source} must match its directory name "${entry.name}"`);
|
|
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
|
+
}));
|
|
82
|
+
skills.push({
|
|
83
|
+
uri: `skill://${entry.name}/SKILL.md`,
|
|
84
|
+
frontmatter: parsed.data,
|
|
85
|
+
resources,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return skills;
|
|
89
|
+
}
|
|
90
|
+
export function skillUriToRelPath(uri) {
|
|
91
|
+
const match = /^skill:\/\/([^/]+)(?:\/(.*))?$/.exec(uri.replace(/\/+$/, ""));
|
|
92
|
+
if (!match) {
|
|
93
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid skill uri: ${uri}`);
|
|
94
|
+
}
|
|
95
|
+
const [, name = "", relPath = ""] = match;
|
|
96
|
+
const segments = relPath.split("/").filter(Boolean);
|
|
97
|
+
if (name === "" || segments.includes("..") || segments.includes(".")) {
|
|
98
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid skill uri: ${uri}`);
|
|
99
|
+
}
|
|
100
|
+
return { name, relPath: segments.join("/") };
|
|
101
|
+
}
|
|
102
|
+
function listDir(skill, relPath) {
|
|
103
|
+
const prefix = relPath === "" ? "" : `${relPath}/`;
|
|
104
|
+
const base = skill.uri.slice(0, -"SKILL.md".length);
|
|
105
|
+
const children = new Map();
|
|
106
|
+
let matched = relPath === "";
|
|
107
|
+
for (const filePath of skill.resources.map((resource) => resource.uri.slice(base.length))) {
|
|
108
|
+
if (!filePath.startsWith(prefix)) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
matched = true;
|
|
112
|
+
const rest = filePath.slice(prefix.length);
|
|
113
|
+
const slash = rest.indexOf("/");
|
|
114
|
+
if (slash === -1) {
|
|
115
|
+
children.set(rest, "text/markdown");
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
children.set(rest.slice(0, slash), "inode/directory");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!matched) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return [...children].map(([name, mimeType]) => ({ name, mimeType }));
|
|
125
|
+
}
|
|
126
|
+
const DirectoryReadRequestSchema = z.object({
|
|
127
|
+
method: z.literal("resources/directory/read"),
|
|
128
|
+
params: z.object({ uri: z.string(), cursor: z.string().optional() }),
|
|
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
|
+
});
|
|
143
|
+
export function registerSkills(server, manifest) {
|
|
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;
|
|
149
|
+
if (text === undefined) {
|
|
150
|
+
throw new McpError(ErrorCode.InvalidParams, `Not found: ${href}`);
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
contents: [{ uri: href, text, mimeType: "text/markdown" }],
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
for (const skill of manifest) {
|
|
157
|
+
server.registerResource(skill.frontmatter.name, skill.uri, {
|
|
158
|
+
description: skill.frontmatter.description,
|
|
159
|
+
mimeType: "text/markdown",
|
|
160
|
+
}, (readUri) => serveFile(skill.uri, skill.uri, readUri.href));
|
|
161
|
+
}
|
|
162
|
+
server.registerResource("skill-files", new ResourceTemplate("skill://{skillName}/{+filePath}", {
|
|
163
|
+
list: undefined,
|
|
164
|
+
}), {}, (readUri) => {
|
|
165
|
+
const { name, relPath } = skillUriToRelPath(readUri.href);
|
|
166
|
+
return serveFile(`skill://${name}/SKILL.md`, `skill://${name}/${relPath}`, readUri.href);
|
|
167
|
+
});
|
|
168
|
+
server.server.setRequestHandler(SkillsListRequestSchema, () => ({
|
|
169
|
+
skills: manifest.map(toWireEntry),
|
|
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
|
+
});
|
|
178
|
+
server.server.setRequestHandler(DirectoryReadRequestSchema, ({ params }) => {
|
|
179
|
+
const { name, relPath } = skillUriToRelPath(params.uri);
|
|
180
|
+
const skill = byUri.get(`skill://${name}/SKILL.md`);
|
|
181
|
+
const entries = skill ? listDir(skill, relPath) : null;
|
|
182
|
+
if (!entries) {
|
|
183
|
+
throw new McpError(ErrorCode.InvalidParams, `Not a directory: ${params.uri}`);
|
|
184
|
+
}
|
|
185
|
+
const base = params.uri.replace(/\/+$/, "");
|
|
186
|
+
return {
|
|
187
|
+
resources: entries.map((entry) => ({
|
|
188
|
+
uri: `${base}/${entry.name}`,
|
|
189
|
+
name: entry.name,
|
|
190
|
+
mimeType: entry.mimeType,
|
|
191
|
+
})),
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=skills.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills.js","sourceRoot":"","sources":["../../src/server/skills.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAChE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAO1C,OAAO,EAAE,gBAAgB,EAAE,MAAM,yCAAyC,CAAC;AAC3E,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,oBAAoB,GAAG,gCAAgC,CAAC;AAErE,MAAM,aAAa,GAAG,0BAA0B,CAAC;AAEjD,MAAM,sBAAsB,GAAG,CAAC;KAC7B,MAAM,CAAC;IACN,IAAI,EAAE,CAAC;SACJ,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,GAAG,CAAC,EAAE,CAAC;SACP,KAAK,CACJ,aAAa,EACb,kEAAkE,CACnE;IACH,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC;CACzC,CAAC;KACD,KAAK,EAAE,CAAC;AAgBX,MAAM,MAAM,GAAG,CAAC,OAAe,EAAU,EAAE,CACzC,UAAU,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;AAEzE,MAAM,cAAc,GAAG,yDAAyD,CAAC;AAEjF,SAAS,gBAAgB,CAAC,OAAe,EAAE,MAAc;IACvD,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC3C,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,SAAS,MAAM,8BAA8B,CAAC,CAAC;IACjE,CAAC;IACD,IAAI,CAAC;QACH,OAAO,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACnC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACtE,MAAM,IAAI,KAAK,CAAC,+BAA+B,MAAM,KAAK,MAAM,EAAE,CAAC,CAAC;IACtE,CAAC;AACH,CAAC;AAED,MAAM,YAAY,GAAG,CAAC,IAAY,EAAE,GAAG,GAAG,EAAE,EAA0B,EAAE;IACtE,MAAM,KAAK,GAA2B,EAAE,CAAC;IACzC,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QAC1E,MAAM,QAAQ,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;QAC3D,IAAI,KAAK,CAAC,cAAc,EAAE,EAAE,CAAC;YAC3B,SAAS;QACX,CAAC;QACD,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACxB,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,YAAY,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;QACrD,CAAC;aAAM,IAAI,KAAK,CAAC,MAAM,EAAE,IAAI,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK,EAAE,CAAC;YAC3D,KAAK,CAAC,QAAQ,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAC;QAC/D,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAEF,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACrB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,MAAM,GAAmB,EAAE,CAAC;IAClC,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QAC9D,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACzB,SAAS;QACX,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QACxC,MAAM,KAAK,GAAG,YAAY,CAAC,SAAS,CAAC,CAAC;QACtC,MAAM,OAAO,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC;QAClC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,SAAS;QACX,CAAC;QAED,MAAM,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,WAAW,CAAC;QAExC,MAAM,MAAM,GAAG,sBAAsB,CAAC,SAAS,CAC7C,gBAAgB,CAAC,OAAO,EAAE,MAAM,CAAC,CAClC,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CACb,gCAAgC,MAAM,KAAK,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAC3E,CAAC;QACJ,CAAC;QACD,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,EAAE,CAAC;YACpC,MAAM,IAAI,KAAK,CACb,eAAe,MAAM,CAAC,IAAI,CAAC,IAAI,QAAQ,MAAM,mCAAmC,KAAK,CAAC,IAAI,GAAG,CAC9F,CAAC;QACJ,CAAC;QAED,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;aACpC,IAAI,CACH,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CACX,MAAM,CAAC,CAAC,KAAK,UAAU,CAAC,GAAG,MAAM,CAAC,CAAC,KAAK,UAAU,CAAC;YACnD,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CACrB;aACA,GAAG,CAAC,CAAC,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;YAC5B,GAAG,EAAE,WAAW,KAAK,CAAC,IAAI,IAAI,OAAO,EAAE;YACvC,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC;YACvB,OAAO;SACR,CAAC,CAAC,CAAC;QAEN,MAAM,CAAC,IAAI,CAAC;YACV,GAAG,EAAE,WAAW,KAAK,CAAC,IAAI,WAAW;YACrC,WAAW,EAAE,MAAM,CAAC,IAAI;YACxB,SAAS;SACV,CAAC,CAAC;IACL,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAI3C,MAAM,KAAK,GAAG,gCAAgC,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;IAC7E,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,QAAQ,CAAC,SAAS,CAAC,aAAa,EAAE,sBAAsB,GAAG,EAAE,CAAC,CAAC;IAC3E,CAAC;IACD,MAAM,CAAC,EAAE,IAAI,GAAG,EAAE,EAAE,OAAO,GAAG,EAAE,CAAC,GAAG,KAAK,CAAC;IAC1C,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACpD,IAAI,IAAI,KAAK,EAAE,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACrE,MAAM,IAAI,QAAQ,CAAC,SAAS,CAAC,aAAa,EAAE,sBAAsB,GAAG,EAAE,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;AAC/C,CAAC;AAED,SAAS,OAAO,CACd,KAAY,EACZ,OAAe;IAEf,MAAM,MAAM,GAAG,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,OAAO,GAAG,CAAC;IACnD,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;IACpD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC3C,IAAI,OAAO,GAAG,OAAO,KAAK,EAAE,CAAC;IAC7B,KAAK,MAAM,QAAQ,IAAI,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CACtD,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAChC,EAAE,CAAC;QACF,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YACjC,SAAS;QACX,CAAC;QACD,OAAO,GAAG,IAAI,CAAC;QACf,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC3C,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;YACjB,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,eAAe,CAAC,CAAC;QACtC,CAAC;aAAM,CAAC;YACN,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,iBAAiB,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;IACD,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,CAAC,GAAG,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;AACvE,CAAC;AAkBD,MAAM,0BAA0B,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1C,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,0BAA0B,CAAC;IAC7C,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC;CACrE,CAAC,CAAC;AAEH,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,aAAa,CAAC;IAChC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,QAAQ,EAAE;CAC/D,CAAC,CAAC;AAEH,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IACtC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,YAAY,CAAC;IAC/B,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;CACtC,CAAC,CAAC;AAEH,MAAM,WAAW,GAAG,CAAC,KAAY,EAAE,EAAE,CAAC,CAAC;IACrC,GAAG,EAAE,KAAK,CAAC,GAAG;IACd,WAAW,EAAE,KAAK,CAAC,WAAW;IAC9B,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC;CACvE,CAAC,CAAC;AAEH,MAAM,UAAU,cAAc,CAC5B,MAAsB,EACtB,QAAwB;IAExB,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IACnE,MAAM,SAAS,GAAG,CAAC,QAAgB,EAAE,OAAe,EAAE,IAAY,EAAE,EAAE;QACpE,MAAM,IAAI,GAAG,KAAK;aACf,GAAG,CAAC,QAAQ,CAAC;YACd,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,KAAK,OAAO,CAAC,EAAE,OAAO,CAAC;QACpE,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,MAAM,IAAI,QAAQ,CAAC,SAAS,CAAC,aAAa,EAAE,cAAc,IAAI,EAAE,CAAC,CAAC;QACpE,CAAC;QACD,OAAO;YACL,QAAQ,EAAE,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,eAAe,EAAE,CAAC;SAC3D,CAAC;IACJ,CAAC,CAAC;IAEF,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;QAC7B,MAAM,CAAC,gBAAgB,CACrB,KAAK,CAAC,WAAW,CAAC,IAAI,EACtB,KAAK,CAAC,GAAG,EACT;YACE,WAAW,EAAE,KAAK,CAAC,WAAW,CAAC,WAAW;YAC1C,QAAQ,EAAE,eAAe;SAC1B,EACD,CAAC,OAAO,EAAE,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,CAAC,CAC3D,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,gBAAgB,CACrB,aAAa,EACb,IAAI,gBAAgB,CAAC,iCAAiC,EAAE;QACtD,IAAI,EAAE,SAAS;KAChB,CAAC,EACF,EAAE,EACF,CAAC,OAAO,EAAE,EAAE;QACV,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,iBAAiB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC1D,OAAO,SAAS,CACd,WAAW,IAAI,WAAW,EAC1B,WAAW,IAAI,IAAI,OAAO,EAAE,EAC5B,OAAO,CAAC,IAAI,CACb,CAAC;IACJ,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC,uBAAuB,EAAE,GAAG,EAAE,CAAC,CAAC;QAC9D,MAAM,EAAE,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC;KAClC,CAAC,CAAC,CAAC;IAEJ,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE;QACrE,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACpC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,QAAQ,CAChB,SAAS,CAAC,aAAa,EACvB,kBAAkB,MAAM,CAAC,GAAG,EAAE,CAC/B,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC,0BAA0B,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE;QACzE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,iBAAiB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACxD,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,WAAW,IAAI,WAAW,CAAC,CAAC;QACpD,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACvD,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,QAAQ,CAChB,SAAS,CAAC,aAAa,EACvB,oBAAoB,MAAM,CAAC,GAAG,EAAE,CACjC,CAAC;QACJ,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC5C,OAAO;YACL,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBACjC,GAAG,EAAE,GAAG,IAAI,IAAI,KAAK,CAAC,IAAI,EAAE;gBAC5B,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;aACzB,CAAC,CAAC;SACJ,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC","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"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdirSync, mkdtempSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { describe, expect, it, vi } from "vitest";
|
|
8
|
+
import { discoverSkills, registerSkills, skillUriToRelPath, } from "./skills.js";
|
|
9
|
+
function mkSkillDir(files) {
|
|
10
|
+
const root = mkdtempSync(join(tmpdir(), "skybridge-skills-"));
|
|
11
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
12
|
+
const full = join(root, rel);
|
|
13
|
+
mkdirSync(join(full, ".."), { recursive: true });
|
|
14
|
+
writeFileSync(full, content);
|
|
15
|
+
}
|
|
16
|
+
return root;
|
|
17
|
+
}
|
|
18
|
+
const FM = (name, description) => `---\nname: ${name}\ndescription: ${description}\n---\n`;
|
|
19
|
+
const digestOf = (content) => `sha256:${createHash("sha256").update(content, "utf8").digest("hex")}`;
|
|
20
|
+
describe("discoverSkills", () => {
|
|
21
|
+
it("discovers a skill with frontmatter and per-file resources", () => {
|
|
22
|
+
const skillMd = `${FM("git-workflow", "Team git conventions")}Body`;
|
|
23
|
+
const dir = mkSkillDir({
|
|
24
|
+
"git-workflow/SKILL.md": skillMd,
|
|
25
|
+
"git-workflow/references/GUIDE.md": "# Guide",
|
|
26
|
+
});
|
|
27
|
+
const [skill, ...rest] = discoverSkills(dir);
|
|
28
|
+
expect(rest).toHaveLength(0);
|
|
29
|
+
expect(skill?.uri).toBe("skill://git-workflow/SKILL.md");
|
|
30
|
+
expect(skill?.frontmatter).toMatchObject({
|
|
31
|
+
name: "git-workflow",
|
|
32
|
+
description: "Team git conventions",
|
|
33
|
+
});
|
|
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
|
+
},
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
it("returns [] for a missing directory and skips dirs without SKILL.md", () => {
|
|
48
|
+
expect(discoverSkills(join(tmpdir(), "does-not-exist-xyz"))).toEqual([]);
|
|
49
|
+
const dir = mkSkillDir({ "not-a-skill/README.md": "hi" });
|
|
50
|
+
expect(discoverSkills(dir)).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
it("parses a nested metadata map in frontmatter", () => {
|
|
53
|
+
const dir = mkSkillDir({
|
|
54
|
+
"s/SKILL.md": `---\nname: s\ndescription: d\nmetadata:\n version: "1.2"\n team: core\n---\n`,
|
|
55
|
+
});
|
|
56
|
+
expect(discoverSkills(dir)[0]?.frontmatter.metadata).toEqual({
|
|
57
|
+
version: "1.2",
|
|
58
|
+
team: "core",
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
it("throws on missing frontmatter", () => {
|
|
62
|
+
const dir = mkSkillDir({ "s/SKILL.md": "no frontmatter here" });
|
|
63
|
+
expect(() => discoverSkills(dir)).toThrow(/missing YAML frontmatter/);
|
|
64
|
+
});
|
|
65
|
+
it("throws when required fields are absent", () => {
|
|
66
|
+
const dir = mkSkillDir({ "s/SKILL.md": "---\nname: s\n---\n" });
|
|
67
|
+
expect(() => discoverSkills(dir)).toThrow(/Invalid skill frontmatter/);
|
|
68
|
+
});
|
|
69
|
+
it("throws when the frontmatter name does not match the directory", () => {
|
|
70
|
+
const dir = mkSkillDir({ "s/SKILL.md": FM("other", "d") });
|
|
71
|
+
expect(() => discoverSkills(dir)).toThrow(/must match its directory name/);
|
|
72
|
+
});
|
|
73
|
+
it("preserves arbitrary YAML frontmatter (arrays, nesting) verbatim", () => {
|
|
74
|
+
const dir = mkSkillDir({
|
|
75
|
+
"s/SKILL.md": "---\nname: s\ndescription: d\nallowed-tools:\n - get-order\n - refund\n---\n",
|
|
76
|
+
});
|
|
77
|
+
expect(discoverSkills(dir)[0]?.frontmatter["allowed-tools"]).toEqual([
|
|
78
|
+
"get-order",
|
|
79
|
+
"refund",
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
it("throws on malformed YAML frontmatter", () => {
|
|
83
|
+
const dir = mkSkillDir({
|
|
84
|
+
"s/SKILL.md": "---\nname: 's\ndescription: [unclosed\n---\n",
|
|
85
|
+
});
|
|
86
|
+
expect(() => discoverSkills(dir)).toThrow(/Cannot parse frontmatter/);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
describe("skillUriToRelPath", () => {
|
|
90
|
+
it("splits name and relative path", () => {
|
|
91
|
+
expect(skillUriToRelPath("skill://refunds/SKILL.md")).toEqual({
|
|
92
|
+
name: "refunds",
|
|
93
|
+
relPath: "SKILL.md",
|
|
94
|
+
});
|
|
95
|
+
expect(skillUriToRelPath("skill://refunds")).toEqual({
|
|
96
|
+
name: "refunds",
|
|
97
|
+
relPath: "",
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
it("rejects path traversal and malformed URIs", () => {
|
|
101
|
+
expect(() => skillUriToRelPath("skill://s/../../etc/passwd")).toThrow();
|
|
102
|
+
expect(() => skillUriToRelPath("skill://s/./x")).toThrow();
|
|
103
|
+
expect(() => skillUriToRelPath("https://example.com")).toThrow();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
describe("registerSkills", () => {
|
|
107
|
+
const manifest = [
|
|
108
|
+
{
|
|
109
|
+
uri: "skill://refunds/SKILL.md",
|
|
110
|
+
frontmatter: { name: "refunds", description: "Process refunds" },
|
|
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
|
+
],
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
function fakeRegistrar() {
|
|
126
|
+
const resources = new Map();
|
|
127
|
+
const handlers = new Map();
|
|
128
|
+
const server = {
|
|
129
|
+
registerResource: vi.fn((name, uri, _cfg, cb) => {
|
|
130
|
+
resources.set(name, {
|
|
131
|
+
uri,
|
|
132
|
+
cb: cb,
|
|
133
|
+
});
|
|
134
|
+
}),
|
|
135
|
+
server: {
|
|
136
|
+
setRequestHandler: vi.fn((schema, handler) => {
|
|
137
|
+
const method = schema
|
|
138
|
+
.shape.method.value;
|
|
139
|
+
handlers.set(method, handler);
|
|
140
|
+
}),
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
return { server, resources, handlers };
|
|
144
|
+
}
|
|
145
|
+
it("serves supporting files through the template resource", () => {
|
|
146
|
+
const { server, resources } = fakeRegistrar();
|
|
147
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural test double
|
|
148
|
+
registerSkills(server, manifest);
|
|
149
|
+
const tpl = resources.get("skill-files");
|
|
150
|
+
expect(tpl?.uri).toBeInstanceOf(ResourceTemplate);
|
|
151
|
+
const url = new URL("skill://refunds/templates/email.md");
|
|
152
|
+
const result = tpl?.cb(url, {}, {});
|
|
153
|
+
expect(result.contents[0]?.text).toBe("Hi");
|
|
154
|
+
});
|
|
155
|
+
it("lists a subdirectory through the directory-read handler", () => {
|
|
156
|
+
const on = fakeRegistrar();
|
|
157
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural test double
|
|
158
|
+
registerSkills(on.server, manifest);
|
|
159
|
+
const result = on.handlers.get("resources/directory/read")?.({
|
|
160
|
+
params: { uri: "skill://refunds/templates" },
|
|
161
|
+
});
|
|
162
|
+
expect(result.resources).toEqual([
|
|
163
|
+
{
|
|
164
|
+
uri: "skill://refunds/templates/email.md",
|
|
165
|
+
name: "email.md",
|
|
166
|
+
mimeType: "text/markdown",
|
|
167
|
+
},
|
|
168
|
+
]);
|
|
169
|
+
});
|
|
170
|
+
it("lists a skill root and marks subdirectories as inode/directory", () => {
|
|
171
|
+
const { server, handlers } = fakeRegistrar();
|
|
172
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural test double
|
|
173
|
+
registerSkills(server, manifest);
|
|
174
|
+
const result = handlers.get("resources/directory/read")?.({
|
|
175
|
+
params: { uri: "skill://refunds" },
|
|
176
|
+
});
|
|
177
|
+
expect(result.resources).toContainEqual({
|
|
178
|
+
uri: "skill://refunds/templates",
|
|
179
|
+
name: "templates",
|
|
180
|
+
mimeType: "inode/directory",
|
|
181
|
+
});
|
|
182
|
+
expect(result.resources).toContainEqual({
|
|
183
|
+
uri: "skill://refunds/SKILL.md",
|
|
184
|
+
name: "SKILL.md",
|
|
185
|
+
mimeType: "text/markdown",
|
|
186
|
+
});
|
|
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
|
+
});
|
|
222
|
+
});
|
|
223
|
+
describe("discoverSkills symlink safety", () => {
|
|
224
|
+
it("excludes symlinked files and directories from the manifest", () => {
|
|
225
|
+
const outside = mkdtempSync(join(tmpdir(), "skybridge-outside-"));
|
|
226
|
+
writeFileSync(join(outside, "secret.md"), "TOP SECRET");
|
|
227
|
+
const dir = mkSkillDir({
|
|
228
|
+
"demo/SKILL.md": FM("demo", "d"),
|
|
229
|
+
"demo/real.md": "ok",
|
|
230
|
+
});
|
|
231
|
+
symlinkSync(join(outside, "secret.md"), join(dir, "demo", "leak.md"));
|
|
232
|
+
symlinkSync(outside, join(dir, "demo", "escape"));
|
|
233
|
+
const uris = discoverSkills(dir)[0]?.resources.map((r) => r.uri) ?? [];
|
|
234
|
+
expect(uris).toEqual(["skill://demo/SKILL.md", "skill://demo/real.md"]);
|
|
235
|
+
});
|
|
236
|
+
it("ignores non-markdown supporting files", () => {
|
|
237
|
+
const dir = mkSkillDir({
|
|
238
|
+
"demo/SKILL.md": FM("demo", "d"),
|
|
239
|
+
"demo/notes.md": "kept",
|
|
240
|
+
"demo/data.json": "{}",
|
|
241
|
+
"demo/scripts/run.py": "print(1)",
|
|
242
|
+
});
|
|
243
|
+
const uris = discoverSkills(dir)[0]?.resources.map((r) => r.uri) ?? [];
|
|
244
|
+
expect(uris).toEqual(["skill://demo/SKILL.md", "skill://demo/notes.md"]);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
//# sourceMappingURL=skills.test.js.map
|
|
@@ -0,0 +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 { 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"]}
|
|
@@ -19,7 +19,7 @@ export const viewsDevServer = async (httpServer) => {
|
|
|
19
19
|
const root = process.cwd();
|
|
20
20
|
const configFile = path.join(root, "vite.config.ts");
|
|
21
21
|
const configResult = await loadConfigFromFile({ command: "serve", mode: "development" }, configFile, root);
|
|
22
|
-
const { build, preview, plugins: userPlugins = [], ...devConfig } = configResult?.config || {};
|
|
22
|
+
const { build, preview, plugins: userPlugins = [], server: userServer, ...devConfig } = configResult?.config || {};
|
|
23
23
|
const vite = await createServer({
|
|
24
24
|
...devConfig,
|
|
25
25
|
// Pass `false` so Vite skips re-resolving a config file — we already
|
|
@@ -27,6 +27,9 @@ export const viewsDevServer = async (httpServer) => {
|
|
|
27
27
|
configFile: false,
|
|
28
28
|
appType: "custom",
|
|
29
29
|
server: {
|
|
30
|
+
// Keep the user's server options (e.g. forwardConsole) but force the
|
|
31
|
+
// three settings skybridge's Express integration requires.
|
|
32
|
+
...userServer,
|
|
30
33
|
allowedHosts: true,
|
|
31
34
|
middlewareMode: true,
|
|
32
35
|
hmr: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"viewsDevServer.js","sourceRoot":"","sources":["../../src/server/viewsDevServer.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,OAAO,EAAE,EAAe,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,2BAA2B,EAAE,MAAM,sCAAsC,CAAC;AAEnF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EACjC,UAAuB,EACN,EAAE;IACnB,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAEhC,MAAM,EAAE,YAAY,EAAE,kBAAkB,EAAE,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC;IAElE,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAC3B,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IAErD,MAAM,YAAY,GAAG,MAAM,kBAAkB,CAC3C,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,EACzC,UAAU,EACV,IAAI,CACL,CAAC;IAEF,MAAM,EACJ,KAAK,EACL,OAAO,EACP,OAAO,EAAE,WAAW,GAAG,EAAE,EACzB,GAAG,SAAS,EACb,GAAG,YAAY,EAAE,MAAM,IAAI,EAAE,CAAC;IAE/B,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC;QAC9B,GAAG,SAAS;QACZ,qEAAqE;QACrE,6CAA6C;QAC7C,UAAU,EAAE,KAAK;QACjB,OAAO,EAAE,QAAQ;QACjB,MAAM,EAAE;YACN,YAAY,EAAE,IAAI;YAClB,cAAc,EAAE,IAAI;YACpB,GAAG,EAAE;gBACH,MAAM,EAAE,UAAU;aACnB;SACF;QACD,IAAI;QACJ,uEAAuE;QACvE,kDAAkD;QAClD,OAAO,EAAE,CAAC,GAAG,WAAW,EAAE,2BAA2B,EAAE,CAAC;KACzD,CAAC,CAAC;IAEH,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IACnB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IAElC,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC","sourcesContent":["import type http from \"node:http\";\nimport path from \"node:path\";\nimport cors from \"cors\";\nimport express, { type Router } from \"express\";\nimport { assetBaseUrlTransformPlugin } from \"./asset-base-url-transform-plugin.js\";\n\n/**\n * Vite dev-server middleware for view assets.\n *\n * MUST be mounted at the Express app root so Vite can intercept\n * `/@vite/client`, `/@react-refresh`, and `/_skybridge/view/...` imports:\n *\n * const app = express();\n * if (env.NODE_ENV !== \"production\") {\n * app.use(await viewsDevServer(httpServer));\n * }\n */\nexport const viewsDevServer = async (\n httpServer: http.Server,\n): Promise<Router> => {\n const router = express.Router();\n\n const { createServer, loadConfigFromFile } = await import(\"vite\");\n\n const root = process.cwd();\n const configFile = path.join(root, \"vite.config.ts\");\n\n const configResult = await loadConfigFromFile(\n { command: \"serve\", mode: \"development\" },\n configFile,\n root,\n );\n\n const {\n build,\n preview,\n plugins: userPlugins = [],\n ...devConfig\n } = configResult?.config || {};\n\n const vite = await createServer({\n ...devConfig,\n // Pass `false` so Vite skips re-resolving a config file — we already\n // loaded and spread the user's config above.\n configFile: false,\n appType: \"custom\",\n server: {\n allowedHosts: true,\n middlewareMode: true,\n hmr: {\n server: httpServer,\n },\n },\n root,\n // optimizeDeps is set by the skybridge Vite plugin (entries + include)\n // so it can derive the view glob from `viewsDir`.\n plugins: [...userPlugins, assetBaseUrlTransformPlugin()],\n });\n\n router.use(cors());\n router.use(\"/\", vite.middlewares);\n\n return router;\n};\n"]}
|
|
1
|
+
{"version":3,"file":"viewsDevServer.js","sourceRoot":"","sources":["../../src/server/viewsDevServer.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,OAAO,EAAE,EAAe,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,2BAA2B,EAAE,MAAM,sCAAsC,CAAC;AAEnF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EACjC,UAAuB,EACN,EAAE;IACnB,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAEhC,MAAM,EAAE,YAAY,EAAE,kBAAkB,EAAE,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC;IAElE,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAC3B,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IAErD,MAAM,YAAY,GAAG,MAAM,kBAAkB,CAC3C,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,EACzC,UAAU,EACV,IAAI,CACL,CAAC;IAEF,MAAM,EACJ,KAAK,EACL,OAAO,EACP,OAAO,EAAE,WAAW,GAAG,EAAE,EACzB,MAAM,EAAE,UAAU,EAClB,GAAG,SAAS,EACb,GAAG,YAAY,EAAE,MAAM,IAAI,EAAE,CAAC;IAE/B,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC;QAC9B,GAAG,SAAS;QACZ,qEAAqE;QACrE,6CAA6C;QAC7C,UAAU,EAAE,KAAK;QACjB,OAAO,EAAE,QAAQ;QACjB,MAAM,EAAE;YACN,qEAAqE;YACrE,2DAA2D;YAC3D,GAAG,UAAU;YACb,YAAY,EAAE,IAAI;YAClB,cAAc,EAAE,IAAI;YACpB,GAAG,EAAE;gBACH,MAAM,EAAE,UAAU;aACnB;SACF;QACD,IAAI;QACJ,uEAAuE;QACvE,kDAAkD;QAClD,OAAO,EAAE,CAAC,GAAG,WAAW,EAAE,2BAA2B,EAAE,CAAC;KACzD,CAAC,CAAC;IAEH,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IACnB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IAElC,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC","sourcesContent":["import type http from \"node:http\";\nimport path from \"node:path\";\nimport cors from \"cors\";\nimport express, { type Router } from \"express\";\nimport { assetBaseUrlTransformPlugin } from \"./asset-base-url-transform-plugin.js\";\n\n/**\n * Vite dev-server middleware for view assets.\n *\n * MUST be mounted at the Express app root so Vite can intercept\n * `/@vite/client`, `/@react-refresh`, and `/_skybridge/view/...` imports:\n *\n * const app = express();\n * if (env.NODE_ENV !== \"production\") {\n * app.use(await viewsDevServer(httpServer));\n * }\n */\nexport const viewsDevServer = async (\n httpServer: http.Server,\n): Promise<Router> => {\n const router = express.Router();\n\n const { createServer, loadConfigFromFile } = await import(\"vite\");\n\n const root = process.cwd();\n const configFile = path.join(root, \"vite.config.ts\");\n\n const configResult = await loadConfigFromFile(\n { command: \"serve\", mode: \"development\" },\n configFile,\n root,\n );\n\n const {\n build,\n preview,\n plugins: userPlugins = [],\n server: userServer,\n ...devConfig\n } = configResult?.config || {};\n\n const vite = await createServer({\n ...devConfig,\n // Pass `false` so Vite skips re-resolving a config file — we already\n // loaded and spread the user's config above.\n configFile: false,\n appType: \"custom\",\n server: {\n // Keep the user's server options (e.g. forwardConsole) but force the\n // three settings skybridge's Express integration requires.\n ...userServer,\n allowedHosts: true,\n middlewareMode: true,\n hmr: {\n server: httpServer,\n },\n },\n root,\n // optimizeDeps is set by the skybridge Vite plugin (entries + include)\n // so it can derive the view glob from `viewsDir`.\n plugins: [...userPlugins, assetBaseUrlTransformPlugin()],\n });\n\n router.use(cors());\n router.use(\"/\", vite.middlewares);\n\n return router;\n};\n"]}
|
package/dist/test/view.test.js
CHANGED
|
@@ -287,8 +287,8 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
287
287
|
description: "Second tool",
|
|
288
288
|
view: { component: "folder-view" },
|
|
289
289
|
}, vi.fn());
|
|
290
|
-
const myviewTemplate =
|
|
291
|
-
const folderviewTemplate =
|
|
290
|
+
const myviewTemplate = mockRegisterTool.mock.calls[0][1]._meta?.ui?.resourceUri;
|
|
291
|
+
const folderviewTemplate = mockRegisterTool.mock.calls[1][1]._meta?.ui?.resourceUri;
|
|
292
292
|
expect(myviewTemplate).not.toEqual(folderviewTemplate);
|
|
293
293
|
expect(myviewTemplate).toMatch(/\?v=[0-9a-f]{8}$/);
|
|
294
294
|
expect(folderviewTemplate).toMatch(/\?v=[0-9a-f]{8}$/);
|
|
@@ -521,7 +521,7 @@ describe("resources/list view _meta injection", () => {
|
|
|
521
521
|
const extApps = resources.find((r) => r.uri.includes("ext-apps"));
|
|
522
522
|
expect(appsSdk).toBeUndefined();
|
|
523
523
|
expect(extApps?._meta).toBeDefined();
|
|
524
|
-
const extUi =
|
|
524
|
+
const extUi = extApps._meta?.ui;
|
|
525
525
|
expect(extUi?.csp?.connectDomains?.length).toBeGreaterThan(0);
|
|
526
526
|
expect(extUi?.csp?.resourceDomains).toContain("https://fonts.googleapis.com");
|
|
527
527
|
expect(extUi?.domain).toBe("skybridge.tech");
|