skybridge 0.0.0-dev.f864975 → 0.0.0-dev.f865597

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/README.md +2 -0
  2. package/dist/cli/build-helpers.d.ts +6 -2
  3. package/dist/cli/build-helpers.js +37 -12
  4. package/dist/cli/build-helpers.js.map +1 -1
  5. package/dist/cli/build-helpers.test.js +69 -3
  6. package/dist/cli/build-helpers.test.js.map +1 -1
  7. package/dist/cli/build-steps.d.ts +2 -0
  8. package/dist/cli/build-steps.js +73 -0
  9. package/dist/cli/build-steps.js.map +1 -0
  10. package/dist/cli/build-steps.test.js +52 -0
  11. package/dist/cli/build-steps.test.js.map +1 -0
  12. package/dist/cli/header.d.ts +1 -1
  13. package/dist/cli/resolve-skybridge-config.d.ts +5 -0
  14. package/dist/cli/{resolve-views-dir.js → resolve-skybridge-config.js} +4 -3
  15. package/dist/cli/resolve-skybridge-config.js.map +1 -0
  16. package/dist/cli/run-plain.d.ts +18 -0
  17. package/dist/cli/run-plain.js +89 -0
  18. package/dist/cli/run-plain.js.map +1 -0
  19. package/dist/cli/use-nodemon.d.ts +14 -0
  20. package/dist/cli/use-nodemon.js +71 -60
  21. package/dist/cli/use-nodemon.js.map +1 -1
  22. package/dist/cli/use-typescript-check.d.ts +8 -2
  23. package/dist/cli/use-typescript-check.js +70 -67
  24. package/dist/cli/use-typescript-check.js.map +1 -1
  25. package/dist/commands/build.d.ts +0 -2
  26. package/dist/commands/build.js +2 -61
  27. package/dist/commands/build.js.map +1 -1
  28. package/dist/commands/dev.d.ts +1 -0
  29. package/dist/commands/dev.js +34 -2
  30. package/dist/commands/dev.js.map +1 -1
  31. package/dist/server/auth/discovery.d.ts +28 -7
  32. package/dist/server/auth/discovery.js +26 -8
  33. package/dist/server/auth/discovery.js.map +1 -1
  34. package/dist/server/auth/discovery.test.js +12 -0
  35. package/dist/server/auth/discovery.test.js.map +1 -1
  36. package/dist/server/auth/providers/auth0.d.ts +18 -0
  37. package/dist/server/auth/providers/auth0.js +31 -0
  38. package/dist/server/auth/providers/auth0.js.map +1 -0
  39. package/dist/server/auth/providers/auth0.test.js +48 -0
  40. package/dist/server/auth/providers/auth0.test.js.map +1 -0
  41. package/dist/server/auth/providers/authplane.d.ts +47 -0
  42. package/dist/server/auth/providers/authplane.js +57 -0
  43. package/dist/server/auth/providers/authplane.js.map +1 -0
  44. package/dist/server/auth/providers/authplane.test.d.ts +1 -0
  45. package/dist/server/auth/providers/authplane.test.js +138 -0
  46. package/dist/server/auth/providers/authplane.test.js.map +1 -0
  47. package/dist/server/auth/providers/clerk.d.ts +14 -0
  48. package/dist/server/auth/providers/clerk.js +16 -0
  49. package/dist/server/auth/providers/clerk.js.map +1 -0
  50. package/dist/server/auth/providers/clerk.test.d.ts +1 -0
  51. package/dist/server/auth/providers/clerk.test.js +28 -0
  52. package/dist/server/auth/providers/clerk.test.js.map +1 -0
  53. package/dist/server/auth/providers/custom.d.ts +20 -2
  54. package/dist/server/auth/providers/custom.js +33 -12
  55. package/dist/server/auth/providers/custom.js.map +1 -1
  56. package/dist/server/auth/providers/custom.test.js +68 -25
  57. package/dist/server/auth/providers/custom.test.js.map +1 -1
  58. package/dist/server/auth/providers/descope.d.ts +9 -7
  59. package/dist/server/auth/providers/descope.js +38 -8
  60. package/dist/server/auth/providers/descope.js.map +1 -1
  61. package/dist/server/auth/providers/descope.test.js +40 -15
  62. package/dist/server/auth/providers/descope.test.js.map +1 -1
  63. package/dist/server/auth/providers/shared.d.ts +2 -0
  64. package/dist/server/auth/providers/shared.js +6 -0
  65. package/dist/server/auth/providers/shared.js.map +1 -0
  66. package/dist/server/auth/providers/shared.test.d.ts +1 -0
  67. package/dist/server/auth/providers/shared.test.js +10 -0
  68. package/dist/server/auth/providers/shared.test.js.map +1 -0
  69. package/dist/server/auth/providers/stytch.d.ts +2 -1
  70. package/dist/server/auth/providers/stytch.js +1 -5
  71. package/dist/server/auth/providers/stytch.js.map +1 -1
  72. package/dist/server/auth/providers/workos.d.ts +2 -1
  73. package/dist/server/auth/providers/workos.js +1 -5
  74. package/dist/server/auth/providers/workos.js.map +1 -1
  75. package/dist/server/auth/security-schemes.d.ts +23 -0
  76. package/dist/server/auth/security-schemes.js +64 -0
  77. package/dist/server/auth/security-schemes.js.map +1 -0
  78. package/dist/server/auth/security-schemes.test.d.ts +1 -0
  79. package/dist/server/auth/security-schemes.test.js +99 -0
  80. package/dist/server/auth/security-schemes.test.js.map +1 -0
  81. package/dist/server/auth/setup.d.ts +3 -1
  82. package/dist/server/auth/setup.js +64 -23
  83. package/dist/server/auth/setup.js.map +1 -1
  84. package/dist/server/auth/setup.test.js +291 -2
  85. package/dist/server/auth/setup.test.js.map +1 -1
  86. package/dist/server/auth/verify.d.ts +3 -2
  87. package/dist/server/auth/verify.js +2 -1
  88. package/dist/server/auth/verify.js.map +1 -1
  89. package/dist/server/auth/verify.test.js +13 -0
  90. package/dist/server/auth/verify.test.js.map +1 -1
  91. package/dist/server/host.d.ts +2 -0
  92. package/dist/server/host.js +11 -0
  93. package/dist/server/host.js.map +1 -0
  94. package/dist/server/host.test.d.ts +1 -0
  95. package/dist/server/host.test.js +13 -0
  96. package/dist/server/host.test.js.map +1 -0
  97. package/dist/server/index.d.ts +5 -1
  98. package/dist/server/index.js +5 -1
  99. package/dist/server/index.js.map +1 -1
  100. package/dist/server/server.d.ts +61 -9
  101. package/dist/server/server.js +208 -118
  102. package/dist/server/server.js.map +1 -1
  103. package/dist/server/skills-integration.test.d.ts +1 -0
  104. package/dist/server/skills-integration.test.js +63 -0
  105. package/dist/server/skills-integration.test.js.map +1 -0
  106. package/dist/server/skills.d.ts +23 -0
  107. package/dist/server/skills.js +178 -0
  108. package/dist/server/skills.js.map +1 -0
  109. package/dist/server/skills.test.d.ts +1 -0
  110. package/dist/server/skills.test.js +210 -0
  111. package/dist/server/skills.test.js.map +1 -0
  112. package/dist/server/view-name.test-d.d.ts +1 -0
  113. package/dist/server/view-name.test-d.js +8 -0
  114. package/dist/server/view-name.test-d.js.map +1 -0
  115. package/dist/server/view-resource-resolution.test.js +93 -10
  116. package/dist/server/view-resource-resolution.test.js.map +1 -1
  117. package/dist/server/viewsDevServer.js +4 -1
  118. package/dist/server/viewsDevServer.js.map +1 -1
  119. package/dist/test/view.test.js +70 -102
  120. package/dist/test/view.test.js.map +1 -1
  121. package/dist/web/bridges/adaptor.d.ts +51 -0
  122. package/dist/web/bridges/adaptor.js +341 -0
  123. package/dist/web/bridges/adaptor.js.map +1 -0
  124. package/dist/web/bridges/adaptor.test.d.ts +1 -0
  125. package/dist/web/bridges/adaptor.test.js +224 -0
  126. package/dist/web/bridges/adaptor.test.js.map +1 -0
  127. package/dist/web/bridges/apps-sdk/bridge.d.ts +6 -2
  128. package/dist/web/bridges/apps-sdk/bridge.js +15 -4
  129. package/dist/web/bridges/apps-sdk/bridge.js.map +1 -1
  130. package/dist/web/bridges/apps-sdk/index.d.ts +0 -1
  131. package/dist/web/bridges/apps-sdk/index.js +0 -1
  132. package/dist/web/bridges/apps-sdk/index.js.map +1 -1
  133. package/dist/web/bridges/get-adaptor.d.ts +4 -4
  134. package/dist/web/bridges/get-adaptor.js +6 -11
  135. package/dist/web/bridges/get-adaptor.js.map +1 -1
  136. package/dist/web/bridges/get-adaptor.test.d.ts +1 -0
  137. package/dist/web/bridges/get-adaptor.test.js +32 -0
  138. package/dist/web/bridges/get-adaptor.test.js.map +1 -0
  139. package/dist/web/bridges/mcp-app/bridge.d.ts +13 -2
  140. package/dist/web/bridges/mcp-app/bridge.js +51 -7
  141. package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
  142. package/dist/web/bridges/mcp-app/bridge.test.d.ts +1 -0
  143. package/dist/web/bridges/mcp-app/bridge.test.js +17 -0
  144. package/dist/web/bridges/mcp-app/bridge.test.js.map +1 -0
  145. package/dist/web/bridges/mcp-app/index.d.ts +0 -1
  146. package/dist/web/bridges/mcp-app/index.js +0 -1
  147. package/dist/web/bridges/mcp-app/index.js.map +1 -1
  148. package/dist/web/bridges/mcp-app/types.d.ts +2 -0
  149. package/dist/web/bridges/mcp-app/types.js.map +1 -1
  150. package/dist/web/bridges/mcp-app/view-tools.test.js +5 -6
  151. package/dist/web/bridges/mcp-app/view-tools.test.js.map +1 -1
  152. package/dist/web/bridges/types.d.ts +10 -0
  153. package/dist/web/bridges/types.js +14 -1
  154. package/dist/web/bridges/types.js.map +1 -1
  155. package/dist/web/bridges/types.test.d.ts +1 -0
  156. package/dist/web/bridges/types.test.js +19 -0
  157. package/dist/web/bridges/types.test.js.map +1 -0
  158. package/dist/web/components/modal-provider.d.ts +1 -1
  159. package/dist/web/components/modal-provider.js +4 -3
  160. package/dist/web/components/modal-provider.js.map +1 -1
  161. package/dist/web/create-store.test.js +8 -5
  162. package/dist/web/create-store.test.js.map +1 -1
  163. package/dist/web/data-llm.d.ts +1 -1
  164. package/dist/web/data-llm.test.js +17 -5
  165. package/dist/web/data-llm.test.js.map +1 -1
  166. package/dist/web/hooks/index.d.ts +1 -0
  167. package/dist/web/hooks/index.js +1 -0
  168. package/dist/web/hooks/index.js.map +1 -1
  169. package/dist/web/hooks/test/utils.d.ts +2 -1
  170. package/dist/web/hooks/test/utils.js +1 -1
  171. package/dist/web/hooks/test/utils.js.map +1 -1
  172. package/dist/web/hooks/use-call-tool.test.js +37 -23
  173. package/dist/web/hooks/use-call-tool.test.js.map +1 -1
  174. package/dist/web/hooks/use-display-mode.test.js +56 -20
  175. package/dist/web/hooks/use-display-mode.test.js.map +1 -1
  176. package/dist/web/hooks/use-download.test.js +11 -3
  177. package/dist/web/hooks/use-download.test.js.map +1 -1
  178. package/dist/web/hooks/use-files.test.js +10 -2
  179. package/dist/web/hooks/use-files.test.js.map +1 -1
  180. package/dist/web/hooks/use-host-info.d.ts +22 -0
  181. package/dist/web/hooks/use-host-info.js +30 -0
  182. package/dist/web/hooks/use-host-info.js.map +1 -0
  183. package/dist/web/hooks/use-host-info.test.d.ts +1 -0
  184. package/dist/web/hooks/use-host-info.test.js +59 -0
  185. package/dist/web/hooks/use-host-info.test.js.map +1 -0
  186. package/dist/web/hooks/use-layout.test.js +59 -26
  187. package/dist/web/hooks/use-layout.test.js.map +1 -1
  188. package/dist/web/hooks/use-open-external.test.js +13 -3
  189. package/dist/web/hooks/use-open-external.test.js.map +1 -1
  190. package/dist/web/hooks/use-request-close.test.js +35 -40
  191. package/dist/web/hooks/use-request-close.test.js.map +1 -1
  192. package/dist/web/hooks/use-request-modal.test.js +10 -0
  193. package/dist/web/hooks/use-request-modal.test.js.map +1 -1
  194. package/dist/web/hooks/use-request-size.test.js +35 -53
  195. package/dist/web/hooks/use-request-size.test.js.map +1 -1
  196. package/dist/web/hooks/use-set-open-in-app-url.test.js +28 -8
  197. package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -1
  198. package/dist/web/hooks/use-tool-info.test.js +37 -27
  199. package/dist/web/hooks/use-tool-info.test.js.map +1 -1
  200. package/dist/web/hooks/use-user.test.js +81 -35
  201. package/dist/web/hooks/use-user.test.js.map +1 -1
  202. package/dist/web/hooks/use-view-state.js +19 -9
  203. package/dist/web/hooks/use-view-state.js.map +1 -1
  204. package/dist/web/hooks/use-view-state.test.js +37 -2
  205. package/dist/web/hooks/use-view-state.test.js.map +1 -1
  206. package/dist/web/plugin/plugin.d.ts +6 -0
  207. package/dist/web/plugin/plugin.js +3 -3
  208. package/dist/web/plugin/plugin.js.map +1 -1
  209. package/package.json +21 -20
  210. package/dist/cli/resolve-views-dir.d.ts +0 -1
  211. package/dist/cli/resolve-views-dir.js.map +0 -1
  212. package/dist/server/auth/providers/stytch.test.js +0 -33
  213. package/dist/server/auth/providers/stytch.test.js.map +0 -1
  214. package/dist/server/auth/providers/workos.test.js +0 -41
  215. package/dist/server/auth/providers/workos.test.js.map +0 -1
  216. package/dist/web/bridges/apps-sdk/adaptor.d.ts +0 -29
  217. package/dist/web/bridges/apps-sdk/adaptor.js +0 -117
  218. package/dist/web/bridges/apps-sdk/adaptor.js.map +0 -1
  219. package/dist/web/bridges/mcp-app/adaptor.d.ts +0 -53
  220. package/dist/web/bridges/mcp-app/adaptor.js +0 -283
  221. package/dist/web/bridges/mcp-app/adaptor.js.map +0 -1
  222. /package/dist/{server/auth/providers/stytch.test.d.ts → cli/build-steps.test.d.ts} +0 -0
  223. /package/dist/server/auth/providers/{workos.test.d.ts → auth0.test.d.ts} +0 -0
@@ -0,0 +1,178 @@
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
+ export const SKILL_INDEX_URI = "skill://index.json";
10
+ const SKILL_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
11
+ const SkillFrontmatterSchema = z
12
+ .object({
13
+ name: z
14
+ .string()
15
+ .min(1)
16
+ .max(64)
17
+ .regex(SKILL_NAME_RE, "must be lowercase alphanumeric words separated by single hyphens"),
18
+ description: z.string().min(1).max(1024),
19
+ })
20
+ .loose();
21
+ const sha256 = (content) => `sha256:${createHash("sha256").update(content, "utf8").digest("hex")}`;
22
+ const FRONTMATTER_RE = /^?\s*---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
23
+ function parseFrontmatter(content, source) {
24
+ const match = FRONTMATTER_RE.exec(content);
25
+ if (!match) {
26
+ throw new Error(`Skill ${source} is missing YAML frontmatter`);
27
+ }
28
+ try {
29
+ return parseYaml(match[1] ?? "");
30
+ }
31
+ catch (error) {
32
+ const detail = error instanceof Error ? error.message : String(error);
33
+ throw new Error(`Cannot parse frontmatter of ${source}: ${detail}`);
34
+ }
35
+ }
36
+ const readSkillDir = (root, rel = "") => {
37
+ const files = {};
38
+ for (const entry of readdirSync(join(root, rel), { withFileTypes: true })) {
39
+ const childRel = rel ? `${rel}/${entry.name}` : entry.name;
40
+ if (entry.isSymbolicLink()) {
41
+ continue;
42
+ }
43
+ if (entry.isDirectory()) {
44
+ Object.assign(files, readSkillDir(root, childRel));
45
+ }
46
+ else if (entry.isFile() && extname(entry.name) === ".md") {
47
+ files[childRel] = readFileSync(join(root, childRel), "utf8");
48
+ }
49
+ }
50
+ return files;
51
+ };
52
+ export function discoverSkills(dir) {
53
+ if (!existsSync(dir)) {
54
+ return [];
55
+ }
56
+ const skills = [];
57
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
58
+ if (!entry.isDirectory()) {
59
+ continue;
60
+ }
61
+ const skillRoot = join(dir, entry.name);
62
+ const files = readSkillDir(skillRoot);
63
+ const skillMd = files["SKILL.md"];
64
+ if (skillMd === undefined) {
65
+ continue;
66
+ }
67
+ const source = `${entry.name}/SKILL.md`;
68
+ const parsed = SkillFrontmatterSchema.safeParse(parseFrontmatter(skillMd, source));
69
+ if (!parsed.success) {
70
+ throw new Error(`Invalid skill frontmatter in ${source}: ${z.prettifyError(parsed.error)}`);
71
+ }
72
+ if (parsed.data.name !== entry.name) {
73
+ throw new Error(`Skill name "${parsed.data.name}" in ${source} must match its directory name "${entry.name}"`);
74
+ }
75
+ skills.push({
76
+ name: entry.name,
77
+ frontmatter: parsed.data,
78
+ digest: sha256(skillMd),
79
+ files,
80
+ });
81
+ }
82
+ return skills;
83
+ }
84
+ export function skillUriToRelPath(uri) {
85
+ const match = /^skill:\/\/([^/]+)(?:\/(.*))?$/.exec(uri.replace(/\/+$/, ""));
86
+ if (!match) {
87
+ throw new McpError(ErrorCode.InvalidParams, `Invalid skill uri: ${uri}`);
88
+ }
89
+ const [, name = "", relPath = ""] = match;
90
+ const segments = relPath.split("/").filter(Boolean);
91
+ if (name === "" || segments.includes("..") || segments.includes(".")) {
92
+ throw new McpError(ErrorCode.InvalidParams, `Invalid skill uri: ${uri}`);
93
+ }
94
+ return { name, relPath: segments.join("/") };
95
+ }
96
+ function listDir(skill, relPath) {
97
+ const prefix = relPath === "" ? "" : `${relPath}/`;
98
+ const children = new Map();
99
+ let matched = relPath === "";
100
+ for (const filePath of Object.keys(skill.files)) {
101
+ if (!filePath.startsWith(prefix)) {
102
+ continue;
103
+ }
104
+ matched = true;
105
+ const rest = filePath.slice(prefix.length);
106
+ const slash = rest.indexOf("/");
107
+ if (slash === -1) {
108
+ children.set(rest, "text/markdown");
109
+ }
110
+ else {
111
+ children.set(rest.slice(0, slash), "inode/directory");
112
+ }
113
+ }
114
+ if (!matched) {
115
+ return null;
116
+ }
117
+ return [...children].map(([name, mimeType]) => ({ name, mimeType }));
118
+ }
119
+ const DirectoryReadRequestSchema = z.object({
120
+ method: z.literal("resources/directory/read"),
121
+ params: z.object({ uri: z.string(), cursor: z.string().optional() }),
122
+ });
123
+ export function registerSkills(server, manifest) {
124
+ const byName = new Map(manifest.map((s) => [s.name, s]));
125
+ const serveFile = (name, relPath, href) => {
126
+ const text = byName.get(name)?.files[relPath];
127
+ if (text === undefined) {
128
+ throw new McpError(ErrorCode.InvalidParams, `Not found: ${href}`);
129
+ }
130
+ return {
131
+ contents: [{ uri: href, text, mimeType: "text/markdown" }],
132
+ };
133
+ };
134
+ for (const skill of manifest) {
135
+ server.registerResource(skill.name, `skill://${skill.name}/SKILL.md`, {
136
+ description: String(skill.frontmatter.description),
137
+ mimeType: "text/markdown",
138
+ }, (readUri) => serveFile(skill.name, "SKILL.md", readUri.href));
139
+ }
140
+ server.registerResource("skill-files", new ResourceTemplate("skill://{skillName}/{+filePath}", {
141
+ list: undefined,
142
+ }), {}, (readUri) => {
143
+ const { name, relPath } = skillUriToRelPath(readUri.href);
144
+ return serveFile(name, relPath, readUri.href);
145
+ });
146
+ server.registerResource("skill-index", SKILL_INDEX_URI, { mimeType: "application/json" }, () => ({
147
+ contents: [
148
+ {
149
+ uri: SKILL_INDEX_URI,
150
+ mimeType: "application/json",
151
+ text: JSON.stringify({
152
+ skills: manifest.map((skill) => ({
153
+ url: `skill://${skill.name}/SKILL.md`,
154
+ digest: skill.digest,
155
+ frontmatter: skill.frontmatter,
156
+ })),
157
+ }),
158
+ },
159
+ ],
160
+ }));
161
+ server.server.setRequestHandler(DirectoryReadRequestSchema, ({ params }) => {
162
+ const { name, relPath } = skillUriToRelPath(params.uri);
163
+ const skill = byName.get(name);
164
+ const entries = skill ? listDir(skill, relPath) : null;
165
+ if (!entries) {
166
+ throw new McpError(ErrorCode.InvalidParams, `Not a directory: ${params.uri}`);
167
+ }
168
+ const base = params.uri.replace(/\/+$/, "");
169
+ return {
170
+ resources: entries.map((entry) => ({
171
+ uri: `${base}/${entry.name}`,
172
+ name: entry.name,
173
+ mimeType: entry.mimeType,
174
+ })),
175
+ };
176
+ });
177
+ }
178
+ //# sourceMappingURL=skills.js.map
@@ -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"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import { expectTypeOf, test } from "vitest";
2
+ test("ViewNameFor falls back to string when the registry is empty", () => {
3
+ expectTypeOf().toEqualTypeOf();
4
+ });
5
+ test("ViewNameFor narrows to the registered view names when populated", () => {
6
+ expectTypeOf().toEqualTypeOf();
7
+ });
8
+ //# sourceMappingURL=view-name.test-d.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view-name.test-d.js","sourceRoot":"","sources":["../../src/server/view-name.test-d.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAG5C,IAAI,CAAC,6DAA6D,EAAE,GAAG,EAAE;IACvE,YAAY,EAAqC,CAAC,aAAa,EAAU,CAAC;AAC5E,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,iEAAiE,EAAE,GAAG,EAAE;IAC3E,YAAY,EAA4C,CAAC,aAAa,EAEnE,CAAC;AACN,CAAC,CAAC,CAAC","sourcesContent":["import { expectTypeOf, test } from \"vitest\";\nimport type { ViewNameFor } from \"./server.js\";\n\ntest(\"ViewNameFor falls back to string when the registry is empty\", () => {\n expectTypeOf<ViewNameFor<Record<never, never>>>().toEqualTypeOf<string>();\n});\n\ntest(\"ViewNameFor narrows to the registered view names when populated\", () => {\n expectTypeOf<ViewNameFor<{ home: true; about: true }>>().toEqualTypeOf<\n \"home\" | \"about\"\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
  }
@@ -36,15 +68,15 @@ describe("view resource resolution (cache key)", () => {
36
68
  it("resolves regardless of the ?v= param and echoes the requested URI", async () => {
37
69
  const { client, teardown } = await connect(registerWidget);
38
70
  const { resources } = await client.listResources();
39
- const listed = resources.find((r) => r.uri.startsWith("ui://views/apps-sdk/widget.html"));
71
+ const listed = resources.find((r) => r.uri.startsWith("ui://views/ext-apps/widget.html"));
40
72
  // Dev build advertises no cache key.
41
- expect(listed?.uri).toBe("ui://views/apps-sdk/widget.html");
73
+ expect(listed?.uri).toBe("ui://views/ext-apps/widget.html");
42
74
  // A stale/arbitrary/absent param all resolve the same underlying asset,
43
75
  // and the response echoes the URI the consumer asked for (never rewritten).
44
76
  for (const uri of [
45
- "ui://views/apps-sdk/widget.html?v=stale123",
46
- "ui://views/apps-sdk/widget.html?v=whatever-else",
47
- "ui://views/apps-sdk/widget.html",
77
+ "ui://views/ext-apps/widget.html?v=stale123",
78
+ "ui://views/ext-apps/widget.html?v=whatever-else",
79
+ "ui://views/ext-apps/widget.html",
48
80
  ]) {
49
81
  const { contents } = await client.readResource({ uri });
50
82
  expect(contents).toHaveLength(1);
@@ -67,11 +99,11 @@ describe("view resource resolution (cache key)", () => {
67
99
  });
68
100
  const { client, teardown } = await connect(registerWidget);
69
101
  const { resources } = await client.listResources();
70
- const listed = resources.find((r) => r.uri.startsWith("ui://views/apps-sdk/widget.html"));
102
+ const listed = resources.find((r) => r.uri.startsWith("ui://views/ext-apps/widget.html"));
71
103
  // Production advertises a content-derived cache key.
72
- expect(listed?.uri).toMatch(/^ui:\/\/views\/apps-sdk\/widget\.html\?v=[0-9a-f]{8}$/);
104
+ expect(listed?.uri).toMatch(/^ui:\/\/views\/ext-apps\/widget\.html\?v=[0-9a-f]{8}$/);
73
105
  // A consumer holding a stale key still resolves the current asset.
74
- const staleUri = "ui://views/apps-sdk/widget.html?v=00000000";
106
+ const staleUri = "ui://views/ext-apps/widget.html?v=00000000";
75
107
  const { contents } = await client.readResource({ uri: staleUri });
76
108
  expect(contents).toHaveLength(1);
77
109
  const content = textContent(contents);
@@ -81,7 +113,58 @@ describe("view resource resolution (cache key)", () => {
81
113
  });
82
114
  it("still throws for an unknown view path", async () => {
83
115
  const { client, teardown } = await connect(registerWidget);
84
- await expect(client.readResource({ uri: "ui://views/apps-sdk/nope.html?v=1" })).rejects.toThrow();
116
+ await expect(client.readResource({ uri: "ui://views/ext-apps/nope.html?v=1" })).rejects.toThrow();
117
+ await teardown();
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
+ });
155
+ // Back-compat: older Skybridge advertised the view at `ui://views/apps-sdk/...`
156
+ // via openai/outputTemplate. We no longer advertise it, but apps published then
157
+ // have it cached, so the read must still resolve to the ext-apps content.
158
+ it("resolves the legacy apps-sdk URL to the ext-apps content", async () => {
159
+ const { client, teardown } = await connect(registerWidget);
160
+ const legacyUri = "ui://views/apps-sdk/widget.html";
161
+ const { contents } = await client.readResource({ uri: legacyUri });
162
+ expect(contents).toHaveLength(1);
163
+ const content = contents[0];
164
+ expect(content.mimeType).toBe("text/html;profile=mcp-app");
165
+ expect(content.text.length).toBeGreaterThan(0);
166
+ // The response echoes the requested (legacy) URI, never the canonical one.
167
+ expect(content.uri).toBe(legacyUri);
85
168
  await teardown();
86
169
  });
87
170
  });