skybridge 0.0.0-dev.ee2c1ff → 0.0.0-dev.ee6593a
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 +1 -0
- package/dist/cli/build-helpers.d.ts +5 -1
- package/dist/cli/build-helpers.js +30 -10
- package/dist/cli/build-helpers.js.map +1 -1
- package/dist/cli/build-helpers.test.js +58 -3
- package/dist/cli/build-helpers.test.js.map +1 -1
- package/dist/cli/build-steps.d.ts +2 -0
- package/dist/cli/build-steps.js +73 -0
- package/dist/cli/build-steps.js.map +1 -0
- package/dist/cli/build-steps.test.d.ts +1 -0
- package/dist/cli/build-steps.test.js +52 -0
- package/dist/cli/build-steps.test.js.map +1 -0
- package/dist/cli/header.d.ts +1 -1
- package/dist/cli/run-plain.d.ts +18 -0
- package/dist/cli/run-plain.js +89 -0
- package/dist/cli/run-plain.js.map +1 -0
- package/dist/cli/use-nodemon.d.ts +14 -0
- package/dist/cli/use-nodemon.js +71 -60
- package/dist/cli/use-nodemon.js.map +1 -1
- package/dist/cli/use-typescript-check.d.ts +8 -2
- package/dist/cli/use-typescript-check.js +70 -67
- package/dist/cli/use-typescript-check.js.map +1 -1
- package/dist/commands/build.d.ts +0 -2
- package/dist/commands/build.js +2 -61
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/dev.d.ts +1 -0
- package/dist/commands/dev.js +32 -0
- package/dist/commands/dev.js.map +1 -1
- package/dist/server/auth/providers/custom.d.ts +6 -0
- package/dist/server/auth/providers/custom.js +7 -4
- package/dist/server/auth/providers/custom.js.map +1 -1
- package/dist/server/auth/providers/custom.test.js +21 -0
- package/dist/server/auth/providers/custom.test.js.map +1 -1
- package/dist/server/auth/providers/descope.js +4 -2
- package/dist/server/auth/providers/descope.js.map +1 -1
- package/dist/server/auth/providers/descope.test.js +12 -10
- package/dist/server/auth/providers/descope.test.js.map +1 -1
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +2 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/server.d.ts +8 -0
- package/dist/server/server.js +65 -6
- 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 +63 -0
- package/dist/server/skills-integration.test.js.map +1 -0
- package/dist/server/skills.d.ts +23 -0
- package/dist/server/skills.js +178 -0
- package/dist/server/skills.js.map +1 -0
- package/dist/server/skills.test.d.ts +1 -0
- package/dist/server/skills.test.js +210 -0
- package/dist/server/skills.test.js.map +1 -0
- package/dist/server/view-resource-resolution.test.js +69 -1
- package/dist/server/view-resource-resolution.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 +20 -0
- package/dist/web/bridges/adaptor.js.map +1 -1
- package/dist/web/bridges/adaptor.test.js +11 -0
- package/dist/web/bridges/adaptor.test.js.map +1 -1
- package/dist/web/components/modal-provider.d.ts +1 -1
- package/dist/web/data-llm.d.ts +1 -1
- package/dist/web/data-llm.test.js +8 -0
- package/dist/web/data-llm.test.js.map +1 -1
- package/dist/web/plugin/plugin.js +0 -1
- package/dist/web/plugin/plugin.js.map +1 -1
- package/package.json +21 -20
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
import { mkdirSync, mkdtempSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { discoverSkills, registerSkills, SKILL_INDEX_URI, skillUriToRelPath, } from "./skills.js";
|
|
8
|
+
function mkSkillDir(files) {
|
|
9
|
+
const root = mkdtempSync(join(tmpdir(), "skybridge-skills-"));
|
|
10
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
11
|
+
const full = join(root, rel);
|
|
12
|
+
mkdirSync(join(full, ".."), { recursive: true });
|
|
13
|
+
writeFileSync(full, content);
|
|
14
|
+
}
|
|
15
|
+
return root;
|
|
16
|
+
}
|
|
17
|
+
const FM = (name, description) => `---\nname: ${name}\ndescription: ${description}\n---\n`;
|
|
18
|
+
describe("discoverSkills", () => {
|
|
19
|
+
it("discovers a skill with frontmatter, digest, and supporting files", () => {
|
|
20
|
+
const dir = mkSkillDir({
|
|
21
|
+
"git-workflow/SKILL.md": `${FM("git-workflow", "Team git conventions")}Body`,
|
|
22
|
+
"git-workflow/references/GUIDE.md": "# Guide",
|
|
23
|
+
});
|
|
24
|
+
const [skill, ...rest] = discoverSkills(dir);
|
|
25
|
+
expect(rest).toHaveLength(0);
|
|
26
|
+
expect(skill?.name).toBe("git-workflow");
|
|
27
|
+
expect(skill?.frontmatter).toMatchObject({
|
|
28
|
+
name: "git-workflow",
|
|
29
|
+
description: "Team git conventions",
|
|
30
|
+
});
|
|
31
|
+
expect(skill?.digest).toMatch(/^sha256:[a-f0-9]{64}$/);
|
|
32
|
+
expect(Object.keys(skill?.files ?? {}).sort()).toEqual([
|
|
33
|
+
"SKILL.md",
|
|
34
|
+
"references/GUIDE.md",
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
it("returns [] for a missing directory and skips dirs without SKILL.md", () => {
|
|
38
|
+
expect(discoverSkills(join(tmpdir(), "does-not-exist-xyz"))).toEqual([]);
|
|
39
|
+
const dir = mkSkillDir({ "not-a-skill/README.md": "hi" });
|
|
40
|
+
expect(discoverSkills(dir)).toEqual([]);
|
|
41
|
+
});
|
|
42
|
+
it("parses a nested metadata map in frontmatter", () => {
|
|
43
|
+
const dir = mkSkillDir({
|
|
44
|
+
"s/SKILL.md": `---\nname: s\ndescription: d\nmetadata:\n version: "1.2"\n team: core\n---\n`,
|
|
45
|
+
});
|
|
46
|
+
expect(discoverSkills(dir)[0]?.frontmatter.metadata).toEqual({
|
|
47
|
+
version: "1.2",
|
|
48
|
+
team: "core",
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
it("throws on missing frontmatter", () => {
|
|
52
|
+
const dir = mkSkillDir({ "s/SKILL.md": "no frontmatter here" });
|
|
53
|
+
expect(() => discoverSkills(dir)).toThrow(/missing YAML frontmatter/);
|
|
54
|
+
});
|
|
55
|
+
it("throws when required fields are absent", () => {
|
|
56
|
+
const dir = mkSkillDir({ "s/SKILL.md": "---\nname: s\n---\n" });
|
|
57
|
+
expect(() => discoverSkills(dir)).toThrow(/Invalid skill frontmatter/);
|
|
58
|
+
});
|
|
59
|
+
it("throws when the frontmatter name does not match the directory", () => {
|
|
60
|
+
const dir = mkSkillDir({ "s/SKILL.md": FM("other", "d") });
|
|
61
|
+
expect(() => discoverSkills(dir)).toThrow(/must match its directory name/);
|
|
62
|
+
});
|
|
63
|
+
it("preserves arbitrary YAML frontmatter (arrays, nesting) verbatim", () => {
|
|
64
|
+
const dir = mkSkillDir({
|
|
65
|
+
"s/SKILL.md": "---\nname: s\ndescription: d\nallowed-tools:\n - get-order\n - refund\n---\n",
|
|
66
|
+
});
|
|
67
|
+
expect(discoverSkills(dir)[0]?.frontmatter["allowed-tools"]).toEqual([
|
|
68
|
+
"get-order",
|
|
69
|
+
"refund",
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
it("throws on malformed YAML frontmatter", () => {
|
|
73
|
+
const dir = mkSkillDir({
|
|
74
|
+
"s/SKILL.md": "---\nname: 's\ndescription: [unclosed\n---\n",
|
|
75
|
+
});
|
|
76
|
+
expect(() => discoverSkills(dir)).toThrow(/Cannot parse frontmatter/);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe("skillUriToRelPath", () => {
|
|
80
|
+
it("splits name and relative path", () => {
|
|
81
|
+
expect(skillUriToRelPath("skill://refunds/SKILL.md")).toEqual({
|
|
82
|
+
name: "refunds",
|
|
83
|
+
relPath: "SKILL.md",
|
|
84
|
+
});
|
|
85
|
+
expect(skillUriToRelPath("skill://refunds")).toEqual({
|
|
86
|
+
name: "refunds",
|
|
87
|
+
relPath: "",
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
it("rejects path traversal and malformed URIs", () => {
|
|
91
|
+
expect(() => skillUriToRelPath("skill://s/../../etc/passwd")).toThrow();
|
|
92
|
+
expect(() => skillUriToRelPath("skill://s/./x")).toThrow();
|
|
93
|
+
expect(() => skillUriToRelPath("https://example.com")).toThrow();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe("registerSkills", () => {
|
|
97
|
+
const manifest = [
|
|
98
|
+
{
|
|
99
|
+
name: "refunds",
|
|
100
|
+
frontmatter: { name: "refunds", description: "Process refunds" },
|
|
101
|
+
digest: "sha256:abc",
|
|
102
|
+
files: {
|
|
103
|
+
"SKILL.md": "# Refunds",
|
|
104
|
+
"templates/email.md": "Hi",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
function fakeRegistrar() {
|
|
109
|
+
const resources = new Map();
|
|
110
|
+
let dirHandler;
|
|
111
|
+
const server = {
|
|
112
|
+
registerResource: vi.fn((name, uri, _cfg, cb) => {
|
|
113
|
+
resources.set(name, {
|
|
114
|
+
uri,
|
|
115
|
+
cb: cb,
|
|
116
|
+
});
|
|
117
|
+
}),
|
|
118
|
+
server: {
|
|
119
|
+
setRequestHandler: vi.fn((_schema, handler) => {
|
|
120
|
+
dirHandler = handler;
|
|
121
|
+
}),
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
return { server, resources, getDirHandler: () => dirHandler };
|
|
125
|
+
}
|
|
126
|
+
it("builds an index.json with url, digest, and verbatim frontmatter", () => {
|
|
127
|
+
const { server, resources } = fakeRegistrar();
|
|
128
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural test double
|
|
129
|
+
registerSkills(server, manifest);
|
|
130
|
+
const index = resources.get("skill-index");
|
|
131
|
+
const result = index?.cb(new URL(SKILL_INDEX_URI));
|
|
132
|
+
expect(JSON.parse(result.contents[0]?.text ?? "")).toEqual({
|
|
133
|
+
skills: [
|
|
134
|
+
{
|
|
135
|
+
url: "skill://refunds/SKILL.md",
|
|
136
|
+
digest: "sha256:abc",
|
|
137
|
+
frontmatter: { name: "refunds", description: "Process refunds" },
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
it("serves supporting files through the template resource", () => {
|
|
143
|
+
const { server, resources } = fakeRegistrar();
|
|
144
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural test double
|
|
145
|
+
registerSkills(server, manifest);
|
|
146
|
+
const tpl = resources.get("skill-files");
|
|
147
|
+
expect(tpl?.uri).toBeInstanceOf(ResourceTemplate);
|
|
148
|
+
const url = new URL("skill://refunds/templates/email.md");
|
|
149
|
+
const result = tpl?.cb(url, {}, {});
|
|
150
|
+
expect(result.contents[0]?.text).toBe("Hi");
|
|
151
|
+
});
|
|
152
|
+
it("lists a subdirectory through the directory-read handler", () => {
|
|
153
|
+
const on = fakeRegistrar();
|
|
154
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural test double
|
|
155
|
+
registerSkills(on.server, manifest);
|
|
156
|
+
const result = on.getDirHandler()?.({
|
|
157
|
+
params: { uri: "skill://refunds/templates" },
|
|
158
|
+
});
|
|
159
|
+
expect(result.resources).toEqual([
|
|
160
|
+
{
|
|
161
|
+
uri: "skill://refunds/templates/email.md",
|
|
162
|
+
name: "email.md",
|
|
163
|
+
mimeType: "text/markdown",
|
|
164
|
+
},
|
|
165
|
+
]);
|
|
166
|
+
});
|
|
167
|
+
it("lists a skill root and marks subdirectories as inode/directory", () => {
|
|
168
|
+
const { server, getDirHandler } = fakeRegistrar();
|
|
169
|
+
// biome-ignore lint/suspicious/noExplicitAny: structural test double
|
|
170
|
+
registerSkills(server, manifest);
|
|
171
|
+
const result = getDirHandler()?.({
|
|
172
|
+
params: { uri: "skill://refunds" },
|
|
173
|
+
});
|
|
174
|
+
expect(result.resources).toContainEqual({
|
|
175
|
+
uri: "skill://refunds/templates",
|
|
176
|
+
name: "templates",
|
|
177
|
+
mimeType: "inode/directory",
|
|
178
|
+
});
|
|
179
|
+
expect(result.resources).toContainEqual({
|
|
180
|
+
uri: "skill://refunds/SKILL.md",
|
|
181
|
+
name: "SKILL.md",
|
|
182
|
+
mimeType: "text/markdown",
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
describe("discoverSkills symlink safety", () => {
|
|
187
|
+
it("excludes symlinked files and directories from the manifest", () => {
|
|
188
|
+
const outside = mkdtempSync(join(tmpdir(), "skybridge-outside-"));
|
|
189
|
+
writeFileSync(join(outside, "secret.md"), "TOP SECRET");
|
|
190
|
+
const dir = mkSkillDir({
|
|
191
|
+
"demo/SKILL.md": FM("demo", "d"),
|
|
192
|
+
"demo/real.md": "ok",
|
|
193
|
+
});
|
|
194
|
+
symlinkSync(join(outside, "secret.md"), join(dir, "demo", "leak.md"));
|
|
195
|
+
symlinkSync(outside, join(dir, "demo", "escape"));
|
|
196
|
+
const files = discoverSkills(dir)[0]?.files ?? {};
|
|
197
|
+
expect(Object.keys(files).sort()).toEqual(["SKILL.md", "real.md"]);
|
|
198
|
+
});
|
|
199
|
+
it("ignores non-markdown supporting files", () => {
|
|
200
|
+
const dir = mkSkillDir({
|
|
201
|
+
"demo/SKILL.md": FM("demo", "d"),
|
|
202
|
+
"demo/notes.md": "kept",
|
|
203
|
+
"demo/data.json": "{}",
|
|
204
|
+
"demo/scripts/run.py": "print(1)",
|
|
205
|
+
});
|
|
206
|
+
const files = discoverSkills(dir)[0]?.files ?? {};
|
|
207
|
+
expect(Object.keys(files).sort()).toEqual(["SKILL.md", "notes.md"]);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
//# 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 { mkdirSync, mkdtempSync, symlinkSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { ResourceTemplate } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { describe, expect, it, vi } from \"vitest\";\nimport {\n discoverSkills,\n registerSkills,\n SKILL_INDEX_URI,\n type Skill,\n skillUriToRelPath,\n} from \"./skills.js\";\n\nfunction mkSkillDir(files: Record<string, string>): string {\n const root = mkdtempSync(join(tmpdir(), \"skybridge-skills-\"));\n for (const [rel, content] of Object.entries(files)) {\n const full = join(root, rel);\n mkdirSync(join(full, \"..\"), { recursive: true });\n writeFileSync(full, content);\n }\n return root;\n}\n\nconst FM = (name: string, description: string) =>\n `---\\nname: ${name}\\ndescription: ${description}\\n---\\n`;\n\ndescribe(\"discoverSkills\", () => {\n it(\"discovers a skill with frontmatter, digest, and supporting files\", () => {\n const dir = mkSkillDir({\n \"git-workflow/SKILL.md\": `${FM(\"git-workflow\", \"Team git conventions\")}Body`,\n \"git-workflow/references/GUIDE.md\": \"# Guide\",\n });\n const [skill, ...rest] = discoverSkills(dir);\n expect(rest).toHaveLength(0);\n expect(skill?.name).toBe(\"git-workflow\");\n expect(skill?.frontmatter).toMatchObject({\n name: \"git-workflow\",\n description: \"Team git conventions\",\n });\n expect(skill?.digest).toMatch(/^sha256:[a-f0-9]{64}$/);\n expect(Object.keys(skill?.files ?? {}).sort()).toEqual([\n \"SKILL.md\",\n \"references/GUIDE.md\",\n ]);\n });\n\n it(\"returns [] for a missing directory and skips dirs without SKILL.md\", () => {\n expect(discoverSkills(join(tmpdir(), \"does-not-exist-xyz\"))).toEqual([]);\n const dir = mkSkillDir({ \"not-a-skill/README.md\": \"hi\" });\n expect(discoverSkills(dir)).toEqual([]);\n });\n\n it(\"parses a nested metadata map in frontmatter\", () => {\n const dir = mkSkillDir({\n \"s/SKILL.md\": `---\\nname: s\\ndescription: d\\nmetadata:\\n version: \"1.2\"\\n team: core\\n---\\n`,\n });\n expect(discoverSkills(dir)[0]?.frontmatter.metadata).toEqual({\n version: \"1.2\",\n team: \"core\",\n });\n });\n\n it(\"throws on missing frontmatter\", () => {\n const dir = mkSkillDir({ \"s/SKILL.md\": \"no frontmatter here\" });\n expect(() => discoverSkills(dir)).toThrow(/missing YAML frontmatter/);\n });\n\n it(\"throws when required fields are absent\", () => {\n const dir = mkSkillDir({ \"s/SKILL.md\": \"---\\nname: s\\n---\\n\" });\n expect(() => discoverSkills(dir)).toThrow(/Invalid skill frontmatter/);\n });\n\n it(\"throws when the frontmatter name does not match the directory\", () => {\n const dir = mkSkillDir({ \"s/SKILL.md\": FM(\"other\", \"d\") });\n expect(() => discoverSkills(dir)).toThrow(/must match its directory name/);\n });\n\n it(\"preserves arbitrary YAML frontmatter (arrays, nesting) verbatim\", () => {\n const dir = mkSkillDir({\n \"s/SKILL.md\":\n \"---\\nname: s\\ndescription: d\\nallowed-tools:\\n - get-order\\n - refund\\n---\\n\",\n });\n expect(discoverSkills(dir)[0]?.frontmatter[\"allowed-tools\"]).toEqual([\n \"get-order\",\n \"refund\",\n ]);\n });\n\n it(\"throws on malformed YAML frontmatter\", () => {\n const dir = mkSkillDir({\n \"s/SKILL.md\": \"---\\nname: 's\\ndescription: [unclosed\\n---\\n\",\n });\n expect(() => discoverSkills(dir)).toThrow(/Cannot parse frontmatter/);\n });\n});\n\ndescribe(\"skillUriToRelPath\", () => {\n it(\"splits name and relative path\", () => {\n expect(skillUriToRelPath(\"skill://refunds/SKILL.md\")).toEqual({\n name: \"refunds\",\n relPath: \"SKILL.md\",\n });\n expect(skillUriToRelPath(\"skill://refunds\")).toEqual({\n name: \"refunds\",\n relPath: \"\",\n });\n });\n\n it(\"rejects path traversal and malformed URIs\", () => {\n expect(() => skillUriToRelPath(\"skill://s/../../etc/passwd\")).toThrow();\n expect(() => skillUriToRelPath(\"skill://s/./x\")).toThrow();\n expect(() => skillUriToRelPath(\"https://example.com\")).toThrow();\n });\n});\n\ndescribe(\"registerSkills\", () => {\n const manifest: Skill[] = [\n {\n name: \"refunds\",\n frontmatter: { name: \"refunds\", description: \"Process refunds\" },\n digest: \"sha256:abc\",\n files: {\n \"SKILL.md\": \"# Refunds\",\n \"templates/email.md\": \"Hi\",\n },\n },\n ];\n\n function fakeRegistrar() {\n const resources = new Map<\n string,\n { uri: unknown; cb: (...args: unknown[]) => unknown }\n >();\n let dirHandler: ((req: unknown) => unknown) | undefined;\n const server = {\n registerResource: vi.fn(\n (name: string, uri: unknown, _cfg: unknown, cb: unknown) => {\n resources.set(name, {\n uri,\n cb: cb as (...args: unknown[]) => unknown,\n });\n },\n ),\n server: {\n setRequestHandler: vi.fn((_schema: unknown, handler: unknown) => {\n dirHandler = handler as (req: unknown) => unknown;\n }),\n },\n };\n return { server, resources, getDirHandler: () => dirHandler };\n }\n\n it(\"builds an index.json with url, digest, and verbatim frontmatter\", () => {\n const { server, resources } = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(server as any, manifest);\n const index = resources.get(\"skill-index\");\n const result = index?.cb(new URL(SKILL_INDEX_URI)) as {\n contents: { text: string }[];\n };\n expect(JSON.parse(result.contents[0]?.text ?? \"\")).toEqual({\n skills: [\n {\n url: \"skill://refunds/SKILL.md\",\n digest: \"sha256:abc\",\n frontmatter: { name: \"refunds\", description: \"Process refunds\" },\n },\n ],\n });\n });\n\n it(\"serves supporting files through the template resource\", () => {\n const { server, resources } = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(server as any, manifest);\n const tpl = resources.get(\"skill-files\");\n expect(tpl?.uri).toBeInstanceOf(ResourceTemplate);\n const url = new URL(\"skill://refunds/templates/email.md\");\n const result = tpl?.cb(url, {}, {}) as { contents: { text: string }[] };\n expect(result.contents[0]?.text).toBe(\"Hi\");\n });\n\n it(\"lists a subdirectory through the directory-read handler\", () => {\n const on = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(on.server as any, manifest);\n const result = on.getDirHandler()?.({\n params: { uri: \"skill://refunds/templates\" },\n }) as { resources: { uri: string; name: string; mimeType: string }[] };\n expect(result.resources).toEqual([\n {\n uri: \"skill://refunds/templates/email.md\",\n name: \"email.md\",\n mimeType: \"text/markdown\",\n },\n ]);\n });\n\n it(\"lists a skill root and marks subdirectories as inode/directory\", () => {\n const { server, getDirHandler } = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(server as any, manifest);\n const result = getDirHandler()?.({\n params: { uri: \"skill://refunds\" },\n }) as { resources: { name: string; mimeType: string }[] };\n expect(result.resources).toContainEqual({\n uri: \"skill://refunds/templates\",\n name: \"templates\",\n mimeType: \"inode/directory\",\n });\n expect(result.resources).toContainEqual({\n uri: \"skill://refunds/SKILL.md\",\n name: \"SKILL.md\",\n mimeType: \"text/markdown\",\n });\n });\n});\n\ndescribe(\"discoverSkills symlink safety\", () => {\n it(\"excludes symlinked files and directories from the manifest\", () => {\n const outside = mkdtempSync(join(tmpdir(), \"skybridge-outside-\"));\n writeFileSync(join(outside, \"secret.md\"), \"TOP SECRET\");\n const dir = mkSkillDir({\n \"demo/SKILL.md\": FM(\"demo\", \"d\"),\n \"demo/real.md\": \"ok\",\n });\n symlinkSync(join(outside, \"secret.md\"), join(dir, \"demo\", \"leak.md\"));\n symlinkSync(outside, join(dir, \"demo\", \"escape\"));\n\n const files = discoverSkills(dir)[0]?.files ?? {};\n expect(Object.keys(files).sort()).toEqual([\"SKILL.md\", \"real.md\"]);\n });\n\n it(\"ignores non-markdown supporting files\", () => {\n const dir = mkSkillDir({\n \"demo/SKILL.md\": FM(\"demo\", \"d\"),\n \"demo/notes.md\": \"kept\",\n \"demo/data.json\": \"{}\",\n \"demo/scripts/run.py\": \"print(1)\",\n });\n const files = discoverSkills(dir)[0]?.files ?? {};\n expect(Object.keys(files).sort()).toEqual([\"SKILL.md\", \"notes.md\"]);\n });\n});\n"]}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
import http from "node:http";
|
|
1
2
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
2
4
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
3
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
6
|
import { __setBuildManifest, McpServer } from "./index.js";
|
|
7
|
+
vi.mock("@skybridge/devtools", () => ({
|
|
8
|
+
devtoolsStaticServer: () => ((_req, _res, next) => next()),
|
|
9
|
+
}));
|
|
10
|
+
vi.mock("./viewsDevServer.js", () => ({
|
|
11
|
+
viewsDevServer: (_httpServer) => ((_req, _res, next) => next()),
|
|
12
|
+
}));
|
|
5
13
|
// SKY-435: a view resource must resolve no matter the `?v=` query param value.
|
|
6
14
|
// The version param is a content-derived cache key for external consumers
|
|
7
15
|
// (it busts host/CDN caches when the bundle changes); it is not part of the
|
|
@@ -22,6 +30,30 @@ async function connect(register) {
|
|
|
22
30
|
},
|
|
23
31
|
};
|
|
24
32
|
}
|
|
33
|
+
let openHttpServer;
|
|
34
|
+
afterEach(() => openHttpServer?.close());
|
|
35
|
+
// Connect over real HTTP so forwarded proxy headers reach the resource handler.
|
|
36
|
+
async function connectHttp(register, headers) {
|
|
37
|
+
const { createApp } = await import("./express.js");
|
|
38
|
+
const server = new McpServer({ name: "test", version: "1.0.0" });
|
|
39
|
+
register(server);
|
|
40
|
+
const httpServer = http.createServer();
|
|
41
|
+
await createApp({ mcpServer: server, httpServer });
|
|
42
|
+
const listening = http.createServer(server.express);
|
|
43
|
+
await new Promise((resolve) => listening.listen(0, resolve));
|
|
44
|
+
openHttpServer = listening;
|
|
45
|
+
const port = listening.address().port;
|
|
46
|
+
const client = new Client({ name: "test-client", version: "1.0.0" });
|
|
47
|
+
const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`), { requestInit: { headers } });
|
|
48
|
+
await client.connect(transport);
|
|
49
|
+
return {
|
|
50
|
+
client,
|
|
51
|
+
teardown: async () => {
|
|
52
|
+
await client.close();
|
|
53
|
+
await server.close();
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
25
57
|
function registerWidget(server) {
|
|
26
58
|
server.registerTool({ name: "show", description: "show", view: { component: "widget" } }, () => ({ content: [{ type: "text", text: "ok" }] }));
|
|
27
59
|
}
|
|
@@ -84,6 +116,42 @@ describe("view resource resolution (cache key)", () => {
|
|
|
84
116
|
await expect(client.readResource({ uri: "ui://views/ext-apps/nope.html?v=1" })).rejects.toThrow();
|
|
85
117
|
await teardown();
|
|
86
118
|
});
|
|
119
|
+
// A proxy routing the request under a path prefix sends x-forwarded-prefix;
|
|
120
|
+
// the view's asset URLs must carry that prefix (and the forwarded origin) so
|
|
121
|
+
// they resolve back through the same routing. Per-request, so one process can
|
|
122
|
+
// serve many hosts/prefixes at once.
|
|
123
|
+
it("prefixes emitted asset URLs with x-forwarded-prefix (prod)", async () => {
|
|
124
|
+
process.env.NODE_ENV = "production";
|
|
125
|
+
__setBuildManifest({
|
|
126
|
+
"src/views/widget.tsx": {
|
|
127
|
+
isEntry: true,
|
|
128
|
+
name: "widget",
|
|
129
|
+
file: "assets/widget-ABC123.js",
|
|
130
|
+
},
|
|
131
|
+
"style.css": { file: "assets/style-XYZ.css" },
|
|
132
|
+
});
|
|
133
|
+
const { client, teardown } = await connectHttp(registerWidget, {
|
|
134
|
+
"x-forwarded-host": "foo.com",
|
|
135
|
+
"x-forwarded-proto": "https",
|
|
136
|
+
"x-forwarded-prefix": "/v1/canary",
|
|
137
|
+
});
|
|
138
|
+
try {
|
|
139
|
+
const { contents } = await client.readResource({
|
|
140
|
+
uri: "ui://views/ext-apps/widget.html",
|
|
141
|
+
});
|
|
142
|
+
const { text } = textContent(contents);
|
|
143
|
+
// Forwarded origin + prefix. (`assets/assets` is the existing layout: the
|
|
144
|
+
// manifest `file` already carries `assets/` and the template prepends
|
|
145
|
+
// `/assets/`; build.tsx's `_redirects` collapses it on the asset host.)
|
|
146
|
+
expect(text).toContain("https://foo.com/v1/canary/assets/assets/widget-ABC123.js");
|
|
147
|
+
expect(text).toContain("https://foo.com/v1/canary/assets/assets/style-XYZ.css");
|
|
148
|
+
// No unprefixed asset URL leaks through.
|
|
149
|
+
expect(text).not.toContain("foo.com/assets/");
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
await teardown();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
87
155
|
// Back-compat: older Skybridge advertised the view at `ui://views/apps-sdk/...`
|
|
88
156
|
// via openai/outputTemplate. We no longer advertise it, but apps published then
|
|
89
157
|
// have it cached, so the read must still resolve to the ext-apps content.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"view-resource-resolution.test.js","sourceRoot":"","sources":["../../src/server/view-resource-resolution.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { InMemoryTransport } from \"@modelcontextprotocol/sdk/inMemory.js\";\nimport { afterEach, describe, expect, it } from \"vitest\";\nimport { __setBuildManifest, McpServer } from \"./index.js\";\n\n// Mirror what the Skybridge Vite plugin generates in `.skybridge/views.d.ts`:\n// narrow `ViewName` so `component: \"widget\"` typechecks against the registry.\ndeclare module \"./server.js\" {\n interface ViewNameRegistry {\n widget: true;\n }\n}\n\n// SKY-435: a view resource must resolve no matter the `?v=` query param value.\n// The version param is a content-derived cache key for external consumers\n// (it busts host/CDN caches when the bundle changes); it is not part of the\n// resource's identity, so a stale, absent, or arbitrary param must still serve\n// the underlying asset.\n\nasync function connect(register: (server: McpServer) => void) {\n const server = new McpServer({ name: \"test\", version: \"1.0.0\" });\n register(server);\n const client = new Client({ name: \"test-client\", version: \"1.0.0\" });\n const [clientTransport, serverTransport] =\n InMemoryTransport.createLinkedPair();\n await server.connect(serverTransport);\n await client.connect(clientTransport);\n return {\n client,\n teardown: async () => {\n await client.close();\n await server.close();\n },\n };\n}\n\nfunction registerWidget(server: McpServer) {\n server.registerTool(\n { name: \"show\", description: \"show\", view: { component: \"widget\" } },\n () => ({ content: [{ type: \"text\" as const, text: \"ok\" }] }),\n );\n}\n\n/** Narrow a read-resource content entry to its text variant. */\nfunction textContent(contents: Array<{ uri: string }>): {\n uri: string;\n text: string;\n} {\n return contents[0] as { uri: string; text: string };\n}\n\ndescribe(\"view resource resolution (cache key)\", () => {\n afterEach(() => {\n delete process.env.NODE_ENV;\n });\n\n it(\"resolves regardless of the ?v= param and echoes the requested URI\", async () => {\n const { client, teardown } = await connect(registerWidget);\n\n const { resources } = await client.listResources();\n const listed = resources.find((r) =>\n r.uri.startsWith(\"ui://views/ext-apps/widget.html\"),\n );\n // Dev build advertises no cache key.\n expect(listed?.uri).toBe(\"ui://views/ext-apps/widget.html\");\n\n // A stale/arbitrary/absent param all resolve the same underlying asset,\n // and the response echoes the URI the consumer asked for (never rewritten).\n for (const uri of [\n \"ui://views/ext-apps/widget.html?v=stale123\",\n \"ui://views/ext-apps/widget.html?v=whatever-else\",\n \"ui://views/ext-apps/widget.html\",\n ]) {\n const { contents } = await client.readResource({ uri });\n expect(contents).toHaveLength(1);\n const content = textContent(contents);\n expect(typeof content.text).toBe(\"string\");\n expect(content.text.length).toBeGreaterThan(0);\n expect(content.uri).toBe(uri);\n }\n\n await teardown();\n });\n\n it(\"resolves a stale cache key when the canonical URI carries ?v= (prod)\", async () => {\n process.env.NODE_ENV = \"production\";\n __setBuildManifest({\n \"src/views/widget.tsx\": {\n isEntry: true,\n name: \"widget\",\n file: \"assets/widget-ABC123.js\",\n },\n \"style.css\": { file: \"assets/style-XYZ.css\" },\n } as unknown as Record<string, { file: string }>);\n\n const { client, teardown } = await connect(registerWidget);\n\n const { resources } = await client.listResources();\n const listed = resources.find((r) =>\n r.uri.startsWith(\"ui://views/ext-apps/widget.html\"),\n );\n // Production advertises a content-derived cache key.\n expect(listed?.uri).toMatch(\n /^ui:\\/\\/views\\/ext-apps\\/widget\\.html\\?v=[0-9a-f]{8}$/,\n );\n\n // A consumer holding a stale key still resolves the current asset.\n const staleUri = \"ui://views/ext-apps/widget.html?v=00000000\";\n const { contents } = await client.readResource({ uri: staleUri });\n expect(contents).toHaveLength(1);\n const content = textContent(contents);\n expect(typeof content.text).toBe(\"string\");\n expect(content.uri).toBe(staleUri);\n\n await teardown();\n });\n\n it(\"still throws for an unknown view path\", async () => {\n const { client, teardown } = await connect(registerWidget);\n\n await expect(\n client.readResource({ uri: \"ui://views/ext-apps/nope.html?v=1\" }),\n ).rejects.toThrow();\n\n await teardown();\n });\n\n // Back-compat: older Skybridge advertised the view at `ui://views/apps-sdk/...`\n // via openai/outputTemplate. We no longer advertise it, but apps published then\n // have it cached, so the read must still resolve to the ext-apps content.\n it(\"resolves the legacy apps-sdk URL to the ext-apps content\", async () => {\n const { client, teardown } = await connect(registerWidget);\n\n const legacyUri = \"ui://views/apps-sdk/widget.html\";\n const { contents } = await client.readResource({ uri: legacyUri });\n\n expect(contents).toHaveLength(1);\n const content = contents[0] as {\n uri: string;\n text: string;\n mimeType: string;\n };\n expect(content.mimeType).toBe(\"text/html;profile=mcp-app\");\n expect(content.text.length).toBeGreaterThan(0);\n // The response echoes the requested (legacy) URI, never the canonical one.\n expect(content.uri).toBe(legacyUri);\n\n await teardown();\n });\n});\n"]}
|
|
1
|
+
{"version":3,"file":"view-resource-resolution.test.js","sourceRoot":"","sources":["../../src/server/view-resource-resolution.test.ts"],"names":[],"mappings":"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","sourcesContent":["import http from \"node:http\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport { InMemoryTransport } from \"@modelcontextprotocol/sdk/inMemory.js\";\nimport type { RequestHandler } from \"express\";\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { __setBuildManifest, McpServer } from \"./index.js\";\n\nvi.mock(\"@skybridge/devtools\", () => ({\n devtoolsStaticServer: () =>\n ((_req: unknown, _res: unknown, next: () => void) =>\n next()) as RequestHandler,\n}));\nvi.mock(\"./viewsDevServer.js\", () => ({\n viewsDevServer: (_httpServer: unknown) =>\n ((_req: unknown, _res: unknown, next: () => void) =>\n next()) as RequestHandler,\n}));\n\n// Mirror what the Skybridge Vite plugin generates in `.skybridge/views.d.ts`:\n// narrow `ViewName` so `component: \"widget\"` typechecks against the registry.\ndeclare module \"./server.js\" {\n interface ViewNameRegistry {\n widget: true;\n }\n}\n\n// SKY-435: a view resource must resolve no matter the `?v=` query param value.\n// The version param is a content-derived cache key for external consumers\n// (it busts host/CDN caches when the bundle changes); it is not part of the\n// resource's identity, so a stale, absent, or arbitrary param must still serve\n// the underlying asset.\n\nasync function connect(register: (server: McpServer) => void) {\n const server = new McpServer({ name: \"test\", version: \"1.0.0\" });\n register(server);\n const client = new Client({ name: \"test-client\", version: \"1.0.0\" });\n const [clientTransport, serverTransport] =\n InMemoryTransport.createLinkedPair();\n await server.connect(serverTransport);\n await client.connect(clientTransport);\n return {\n client,\n teardown: async () => {\n await client.close();\n await server.close();\n },\n };\n}\n\nlet openHttpServer: http.Server | undefined;\nafterEach(() => openHttpServer?.close());\n\n// Connect over real HTTP so forwarded proxy headers reach the resource handler.\nasync function connectHttp(\n register: (server: McpServer) => void,\n headers: Record<string, string>,\n) {\n const { createApp } = await import(\"./express.js\");\n const server = new McpServer({ name: \"test\", version: \"1.0.0\" });\n register(server);\n const httpServer = http.createServer();\n await createApp({ mcpServer: server, httpServer });\n const listening = http.createServer(server.express);\n await new Promise<void>((resolve) => listening.listen(0, resolve));\n openHttpServer = listening;\n const port = (listening.address() as { port: number }).port;\n\n const client = new Client({ name: \"test-client\", version: \"1.0.0\" });\n const transport = new StreamableHTTPClientTransport(\n new URL(`http://localhost:${port}/mcp`),\n { requestInit: { headers } },\n );\n await client.connect(transport);\n return {\n client,\n teardown: async () => {\n await client.close();\n await server.close();\n },\n };\n}\n\nfunction registerWidget(server: McpServer) {\n server.registerTool(\n { name: \"show\", description: \"show\", view: { component: \"widget\" } },\n () => ({ content: [{ type: \"text\" as const, text: \"ok\" }] }),\n );\n}\n\n/** Narrow a read-resource content entry to its text variant. */\nfunction textContent(contents: Array<{ uri: string }>): {\n uri: string;\n text: string;\n} {\n return contents[0] as { uri: string; text: string };\n}\n\ndescribe(\"view resource resolution (cache key)\", () => {\n afterEach(() => {\n delete process.env.NODE_ENV;\n });\n\n it(\"resolves regardless of the ?v= param and echoes the requested URI\", async () => {\n const { client, teardown } = await connect(registerWidget);\n\n const { resources } = await client.listResources();\n const listed = resources.find((r) =>\n r.uri.startsWith(\"ui://views/ext-apps/widget.html\"),\n );\n // Dev build advertises no cache key.\n expect(listed?.uri).toBe(\"ui://views/ext-apps/widget.html\");\n\n // A stale/arbitrary/absent param all resolve the same underlying asset,\n // and the response echoes the URI the consumer asked for (never rewritten).\n for (const uri of [\n \"ui://views/ext-apps/widget.html?v=stale123\",\n \"ui://views/ext-apps/widget.html?v=whatever-else\",\n \"ui://views/ext-apps/widget.html\",\n ]) {\n const { contents } = await client.readResource({ uri });\n expect(contents).toHaveLength(1);\n const content = textContent(contents);\n expect(typeof content.text).toBe(\"string\");\n expect(content.text.length).toBeGreaterThan(0);\n expect(content.uri).toBe(uri);\n }\n\n await teardown();\n });\n\n it(\"resolves a stale cache key when the canonical URI carries ?v= (prod)\", async () => {\n process.env.NODE_ENV = \"production\";\n __setBuildManifest({\n \"src/views/widget.tsx\": {\n isEntry: true,\n name: \"widget\",\n file: \"assets/widget-ABC123.js\",\n },\n \"style.css\": { file: \"assets/style-XYZ.css\" },\n } as unknown as Record<string, { file: string }>);\n\n const { client, teardown } = await connect(registerWidget);\n\n const { resources } = await client.listResources();\n const listed = resources.find((r) =>\n r.uri.startsWith(\"ui://views/ext-apps/widget.html\"),\n );\n // Production advertises a content-derived cache key.\n expect(listed?.uri).toMatch(\n /^ui:\\/\\/views\\/ext-apps\\/widget\\.html\\?v=[0-9a-f]{8}$/,\n );\n\n // A consumer holding a stale key still resolves the current asset.\n const staleUri = \"ui://views/ext-apps/widget.html?v=00000000\";\n const { contents } = await client.readResource({ uri: staleUri });\n expect(contents).toHaveLength(1);\n const content = textContent(contents);\n expect(typeof content.text).toBe(\"string\");\n expect(content.uri).toBe(staleUri);\n\n await teardown();\n });\n\n it(\"still throws for an unknown view path\", async () => {\n const { client, teardown } = await connect(registerWidget);\n\n await expect(\n client.readResource({ uri: \"ui://views/ext-apps/nope.html?v=1\" }),\n ).rejects.toThrow();\n\n await teardown();\n });\n\n // A proxy routing the request under a path prefix sends x-forwarded-prefix;\n // the view's asset URLs must carry that prefix (and the forwarded origin) so\n // they resolve back through the same routing. Per-request, so one process can\n // serve many hosts/prefixes at once.\n it(\"prefixes emitted asset URLs with x-forwarded-prefix (prod)\", async () => {\n process.env.NODE_ENV = \"production\";\n __setBuildManifest({\n \"src/views/widget.tsx\": {\n isEntry: true,\n name: \"widget\",\n file: \"assets/widget-ABC123.js\",\n },\n \"style.css\": { file: \"assets/style-XYZ.css\" },\n } as unknown as Record<string, { file: string }>);\n\n const { client, teardown } = await connectHttp(registerWidget, {\n \"x-forwarded-host\": \"foo.com\",\n \"x-forwarded-proto\": \"https\",\n \"x-forwarded-prefix\": \"/v1/canary\",\n });\n try {\n const { contents } = await client.readResource({\n uri: \"ui://views/ext-apps/widget.html\",\n });\n const { text } = textContent(contents);\n // Forwarded origin + prefix. (`assets/assets` is the existing layout: the\n // manifest `file` already carries `assets/` and the template prepends\n // `/assets/`; build.tsx's `_redirects` collapses it on the asset host.)\n expect(text).toContain(\n \"https://foo.com/v1/canary/assets/assets/widget-ABC123.js\",\n );\n expect(text).toContain(\n \"https://foo.com/v1/canary/assets/assets/style-XYZ.css\",\n );\n // No unprefixed asset URL leaks through.\n expect(text).not.toContain(\"foo.com/assets/\");\n } finally {\n await teardown();\n }\n });\n\n // Back-compat: older Skybridge advertised the view at `ui://views/apps-sdk/...`\n // via openai/outputTemplate. We no longer advertise it, but apps published then\n // have it cached, so the read must still resolve to the ext-apps content.\n it(\"resolves the legacy apps-sdk URL to the ext-apps content\", async () => {\n const { client, teardown } = await connect(registerWidget);\n\n const legacyUri = \"ui://views/apps-sdk/widget.html\";\n const { contents } = await client.readResource({ uri: legacyUri });\n\n expect(contents).toHaveLength(1);\n const content = contents[0] as {\n uri: string;\n text: string;\n mimeType: string;\n };\n expect(content.mimeType).toBe(\"text/html;profile=mcp-app\");\n expect(content.text.length).toBeGreaterThan(0);\n // The response echoes the requested (legacy) URI, never the canonical one.\n expect(content.uri).toBe(legacyUri);\n\n await teardown();\n });\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");
|