skybridge 0.0.0-dev.815d058 → 0.0.0-dev.8164d23

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 (325) hide show
  1. package/README.md +3 -0
  2. package/dist/cli/build-helpers.d.ts +6 -2
  3. package/dist/cli/build-helpers.js +39 -14
  4. package/dist/cli/build-helpers.js.map +1 -1
  5. package/dist/cli/build-helpers.test.js +73 -5
  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-execute-steps.d.ts +1 -1
  20. package/dist/cli/use-nodemon.d.ts +14 -0
  21. package/dist/cli/use-nodemon.js +71 -60
  22. package/dist/cli/use-nodemon.js.map +1 -1
  23. package/dist/cli/use-typescript-check.d.ts +8 -2
  24. package/dist/cli/use-typescript-check.js +70 -67
  25. package/dist/cli/use-typescript-check.js.map +1 -1
  26. package/dist/commands/build.d.ts +0 -2
  27. package/dist/commands/build.js +2 -61
  28. package/dist/commands/build.js.map +1 -1
  29. package/dist/commands/dev.d.ts +1 -0
  30. package/dist/commands/dev.js +34 -2
  31. package/dist/commands/dev.js.map +1 -1
  32. package/dist/commands/start.js +2 -2
  33. package/dist/commands/start.js.map +1 -1
  34. package/dist/context-warnings.d.ts +5 -0
  35. package/dist/context-warnings.js +31 -0
  36. package/dist/context-warnings.js.map +1 -0
  37. package/dist/context-warnings.test.js +28 -0
  38. package/dist/context-warnings.test.js.map +1 -0
  39. package/dist/server/app.d.ts +177 -0
  40. package/dist/server/app.js +261 -0
  41. package/dist/server/app.js.map +1 -0
  42. package/dist/server/auth/discovery.d.ts +29 -7
  43. package/dist/server/auth/discovery.js +16 -11
  44. package/dist/server/auth/discovery.js.map +1 -1
  45. package/dist/server/auth/index.d.ts +17 -5
  46. package/dist/server/auth/index.js.map +1 -1
  47. package/dist/server/auth/providers/auth0.d.ts +33 -6
  48. package/dist/server/auth/providers/auth0.js +25 -12
  49. package/dist/server/auth/providers/auth0.js.map +1 -1
  50. package/dist/server/auth/providers/auth0.test.js +30 -13
  51. package/dist/server/auth/providers/auth0.test.js.map +1 -1
  52. package/dist/server/auth/providers/authplane.d.ts +49 -0
  53. package/dist/server/auth/providers/authplane.js +57 -0
  54. package/dist/server/auth/providers/authplane.js.map +1 -0
  55. package/dist/server/auth/providers/authplane.test.d.ts +1 -0
  56. package/dist/server/auth/providers/authplane.test.js +140 -0
  57. package/dist/server/auth/providers/authplane.test.js.map +1 -0
  58. package/dist/server/auth/providers/clerk.d.ts +39 -2
  59. package/dist/server/auth/providers/clerk.js +5 -6
  60. package/dist/server/auth/providers/clerk.js.map +1 -1
  61. package/dist/server/auth/providers/clerk.test.js +7 -25
  62. package/dist/server/auth/providers/clerk.test.js.map +1 -1
  63. package/dist/server/auth/providers/custom.d.ts +26 -3
  64. package/dist/server/auth/providers/custom.js +42 -14
  65. package/dist/server/auth/providers/custom.js.map +1 -1
  66. package/dist/server/auth/providers/custom.test.js +79 -12
  67. package/dist/server/auth/providers/custom.test.js.map +1 -1
  68. package/dist/server/auth/providers/descope.d.ts +40 -11
  69. package/dist/server/auth/providers/descope.js +38 -9
  70. package/dist/server/auth/providers/descope.js.map +1 -1
  71. package/dist/server/auth/providers/descope.test.js +42 -15
  72. package/dist/server/auth/providers/descope.test.js.map +1 -1
  73. package/dist/server/auth/providers/shared.d.ts +2 -0
  74. package/dist/server/auth/providers/shared.js +6 -0
  75. package/dist/server/auth/providers/shared.js.map +1 -0
  76. package/dist/server/auth/providers/shared.test.d.ts +1 -0
  77. package/dist/server/auth/providers/shared.test.js +10 -0
  78. package/dist/server/auth/providers/shared.test.js.map +1 -0
  79. package/dist/server/auth/providers/stytch.d.ts +15 -2
  80. package/dist/server/auth/providers/stytch.js +5 -6
  81. package/dist/server/auth/providers/stytch.js.map +1 -1
  82. package/dist/server/auth/providers/verify-spy.d.ts +12 -0
  83. package/dist/server/auth/providers/verify-spy.js +21 -0
  84. package/dist/server/auth/providers/verify-spy.js.map +1 -0
  85. package/dist/server/auth/providers/workos.d.ts +24 -2
  86. package/dist/server/auth/providers/workos.js +5 -6
  87. package/dist/server/auth/providers/workos.js.map +1 -1
  88. package/dist/server/auth/security-schemes.d.ts +23 -0
  89. package/dist/server/auth/security-schemes.js +64 -0
  90. package/dist/server/auth/security-schemes.js.map +1 -0
  91. package/dist/server/auth/security-schemes.test.d.ts +1 -0
  92. package/dist/server/auth/security-schemes.test.js +99 -0
  93. package/dist/server/auth/security-schemes.test.js.map +1 -0
  94. package/dist/server/auth/setup.d.ts +3 -1
  95. package/dist/server/auth/setup.js +66 -26
  96. package/dist/server/auth/setup.js.map +1 -1
  97. package/dist/server/auth/setup.test.js +342 -15
  98. package/dist/server/auth/setup.test.js.map +1 -1
  99. package/dist/server/auth/verify.d.ts +37 -3
  100. package/dist/server/auth/verify.js +27 -3
  101. package/dist/server/auth/verify.js.map +1 -1
  102. package/dist/server/auth/verify.test.js +49 -0
  103. package/dist/server/auth/verify.test.js.map +1 -1
  104. package/dist/server/auth-extra.test-d.js +70 -0
  105. package/dist/server/auth-extra.test-d.js.map +1 -0
  106. package/dist/server/auth.d.ts +42 -5
  107. package/dist/server/auth.js +3 -4
  108. package/dist/server/auth.js.map +1 -1
  109. package/dist/server/build-manifest.test.js +6 -5
  110. package/dist/server/build-manifest.test.js.map +1 -1
  111. package/dist/server/content-helpers.d.ts +1 -1
  112. package/dist/server/content-helpers.js.map +1 -1
  113. package/dist/server/express.d.ts +5 -3
  114. package/dist/server/express.js +8 -32
  115. package/dist/server/express.js.map +1 -1
  116. package/dist/server/express.test.js +130 -102
  117. package/dist/server/express.test.js.map +1 -1
  118. package/dist/server/host.d.ts +2 -0
  119. package/dist/server/host.js +11 -0
  120. package/dist/server/host.js.map +1 -0
  121. package/dist/server/host.test.d.ts +1 -0
  122. package/dist/server/host.test.js +13 -0
  123. package/dist/server/host.test.js.map +1 -0
  124. package/dist/server/index.d.ts +15 -8
  125. package/dist/server/index.js +8 -3
  126. package/dist/server/index.js.map +1 -1
  127. package/dist/server/mcp-errors.test.d.ts +1 -0
  128. package/dist/server/mcp-errors.test.js +37 -0
  129. package/dist/server/mcp-errors.test.js.map +1 -0
  130. package/dist/server/middleware.d.ts +35 -10
  131. package/dist/server/middleware.js +33 -0
  132. package/dist/server/middleware.js.map +1 -1
  133. package/dist/server/middleware.test-d.js +9 -9
  134. package/dist/server/middleware.test-d.js.map +1 -1
  135. package/dist/server/middleware.test.js +164 -98
  136. package/dist/server/middleware.test.js.map +1 -1
  137. package/dist/server/register-tool.test.d.ts +1 -0
  138. package/dist/server/register-tool.test.js +23 -0
  139. package/dist/server/register-tool.test.js.map +1 -0
  140. package/dist/server/server.d.ts +155 -137
  141. package/dist/server/server.js +281 -351
  142. package/dist/server/server.js.map +1 -1
  143. package/dist/server/skills-integration.test.d.ts +1 -0
  144. package/dist/server/skills-integration.test.js +79 -0
  145. package/dist/server/skills-integration.test.js.map +1 -0
  146. package/dist/server/skills.d.ts +27 -0
  147. package/dist/server/skills.js +188 -0
  148. package/dist/server/skills.js.map +1 -0
  149. package/dist/server/skills.test.d.ts +1 -0
  150. package/dist/server/skills.test.js +243 -0
  151. package/dist/server/skills.test.js.map +1 -0
  152. package/dist/server/tool-extra.test.d.ts +1 -0
  153. package/dist/server/tool-extra.test.js +72 -0
  154. package/dist/server/tool-extra.test.js.map +1 -0
  155. package/dist/server/view-name.test-d.d.ts +1 -0
  156. package/dist/server/view-name.test-d.js +8 -0
  157. package/dist/server/view-name.test-d.js.map +1 -0
  158. package/dist/server/view-resource-resolution.test.js +101 -17
  159. package/dist/server/view-resource-resolution.test.js.map +1 -1
  160. package/dist/server/viewsDevServer.js +4 -1
  161. package/dist/server/viewsDevServer.js.map +1 -1
  162. package/dist/standard-schema.d.ts +3 -0
  163. package/dist/standard-schema.js +2 -0
  164. package/dist/standard-schema.js.map +1 -0
  165. package/dist/test/utils.d.ts +9 -11
  166. package/dist/test/utils.js +18 -13
  167. package/dist/test/utils.js.map +1 -1
  168. package/dist/test/view.test.js +88 -120
  169. package/dist/test/view.test.js.map +1 -1
  170. package/dist/web/bridges/adaptor.d.ts +51 -0
  171. package/dist/web/bridges/adaptor.js +327 -0
  172. package/dist/web/bridges/adaptor.js.map +1 -0
  173. package/dist/web/bridges/adaptor.test.d.ts +1 -0
  174. package/dist/web/bridges/adaptor.test.js +225 -0
  175. package/dist/web/bridges/adaptor.test.js.map +1 -0
  176. package/dist/web/bridges/apps-sdk/bridge.d.ts +6 -2
  177. package/dist/web/bridges/apps-sdk/bridge.js +15 -4
  178. package/dist/web/bridges/apps-sdk/bridge.js.map +1 -1
  179. package/dist/web/bridges/apps-sdk/index.d.ts +0 -1
  180. package/dist/web/bridges/apps-sdk/index.js +0 -1
  181. package/dist/web/bridges/apps-sdk/index.js.map +1 -1
  182. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.d.ts +1 -1
  183. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js +1 -1
  184. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js.map +1 -1
  185. package/dist/web/bridges/get-adaptor.d.ts +4 -4
  186. package/dist/web/bridges/get-adaptor.js +6 -11
  187. package/dist/web/bridges/get-adaptor.js.map +1 -1
  188. package/dist/web/bridges/get-adaptor.test.d.ts +1 -0
  189. package/dist/web/bridges/get-adaptor.test.js +32 -0
  190. package/dist/web/bridges/get-adaptor.test.js.map +1 -0
  191. package/dist/web/bridges/mcp-app/bridge.d.ts +13 -2
  192. package/dist/web/bridges/mcp-app/bridge.js +51 -7
  193. package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
  194. package/dist/web/bridges/mcp-app/bridge.test.d.ts +1 -0
  195. package/dist/web/bridges/mcp-app/bridge.test.js +17 -0
  196. package/dist/web/bridges/mcp-app/bridge.test.js.map +1 -0
  197. package/dist/web/bridges/mcp-app/index.d.ts +0 -1
  198. package/dist/web/bridges/mcp-app/index.js +0 -1
  199. package/dist/web/bridges/mcp-app/index.js.map +1 -1
  200. package/dist/web/bridges/mcp-app/types.d.ts +2 -0
  201. package/dist/web/bridges/mcp-app/types.js.map +1 -1
  202. package/dist/web/bridges/mcp-app/use-mcp-app-context.d.ts +1 -1
  203. package/dist/web/bridges/mcp-app/use-mcp-app-context.js +1 -1
  204. package/dist/web/bridges/mcp-app/use-mcp-app-context.js.map +1 -1
  205. package/dist/web/bridges/mcp-app/view-tools.test.js +5 -6
  206. package/dist/web/bridges/mcp-app/view-tools.test.js.map +1 -1
  207. package/dist/web/bridges/types.d.ts +27 -9
  208. package/dist/web/bridges/types.js +14 -1
  209. package/dist/web/bridges/types.js.map +1 -1
  210. package/dist/web/bridges/types.test.d.ts +1 -0
  211. package/dist/web/bridges/types.test.js +19 -0
  212. package/dist/web/bridges/types.test.js.map +1 -0
  213. package/dist/web/components/modal-provider.d.ts +1 -1
  214. package/dist/web/components/modal-provider.js +4 -3
  215. package/dist/web/components/modal-provider.js.map +1 -1
  216. package/dist/web/create-store.test.js +9 -6
  217. package/dist/web/create-store.test.js.map +1 -1
  218. package/dist/web/data-llm.d.ts +1 -1
  219. package/dist/web/data-llm.test.js +19 -6
  220. package/dist/web/data-llm.test.js.map +1 -1
  221. package/dist/web/generate-helpers.d.ts +13 -12
  222. package/dist/web/generate-helpers.js +10 -9
  223. package/dist/web/generate-helpers.js.map +1 -1
  224. package/dist/web/generate-helpers.test-d.js +3 -3
  225. package/dist/web/generate-helpers.test-d.js.map +1 -1
  226. package/dist/web/generate-helpers.test.js +2 -2
  227. package/dist/web/generate-helpers.test.js.map +1 -1
  228. package/dist/web/hooks/index.d.ts +2 -1
  229. package/dist/web/hooks/index.js +2 -1
  230. package/dist/web/hooks/index.js.map +1 -1
  231. package/dist/web/hooks/test/utils.d.ts +2 -1
  232. package/dist/web/hooks/test/utils.js +1 -1
  233. package/dist/web/hooks/test/utils.js.map +1 -1
  234. package/dist/web/hooks/use-call-tool.test.js +39 -25
  235. package/dist/web/hooks/use-call-tool.test.js.map +1 -1
  236. package/dist/web/hooks/use-display-mode.d.ts +1 -1
  237. package/dist/web/hooks/use-display-mode.js +1 -1
  238. package/dist/web/hooks/use-display-mode.js.map +1 -1
  239. package/dist/web/hooks/use-display-mode.test.js +57 -21
  240. package/dist/web/hooks/use-display-mode.test.js.map +1 -1
  241. package/dist/web/hooks/use-download.d.ts +1 -3
  242. package/dist/web/hooks/use-download.js +1 -2
  243. package/dist/web/hooks/use-download.js.map +1 -1
  244. package/dist/web/hooks/use-download.test.js +16 -8
  245. package/dist/web/hooks/use-download.test.js.map +1 -1
  246. package/dist/web/hooks/use-files.test.js +11 -3
  247. package/dist/web/hooks/use-files.test.js.map +1 -1
  248. package/dist/web/hooks/use-host.d.ts +22 -0
  249. package/dist/web/hooks/use-host.js +30 -0
  250. package/dist/web/hooks/use-host.js.map +1 -0
  251. package/dist/web/hooks/use-host.test.d.ts +1 -0
  252. package/dist/web/hooks/use-host.test.js +59 -0
  253. package/dist/web/hooks/use-host.test.js.map +1 -0
  254. package/dist/web/hooks/use-open-external.test.js +14 -4
  255. package/dist/web/hooks/use-open-external.test.js.map +1 -1
  256. package/dist/web/hooks/use-register-view-tool.d.ts +2 -3
  257. package/dist/web/hooks/use-register-view-tool.js.map +1 -1
  258. package/dist/web/hooks/use-request-close.test.js +35 -40
  259. package/dist/web/hooks/use-request-close.test.js.map +1 -1
  260. package/dist/web/hooks/use-request-modal.test.js +11 -1
  261. package/dist/web/hooks/use-request-modal.test.js.map +1 -1
  262. package/dist/web/hooks/use-request-size.d.ts +1 -1
  263. package/dist/web/hooks/use-request-size.js +1 -1
  264. package/dist/web/hooks/use-request-size.js.map +1 -1
  265. package/dist/web/hooks/use-request-size.test.js +35 -53
  266. package/dist/web/hooks/use-request-size.test.js.map +1 -1
  267. package/dist/web/hooks/use-set-open-in-app-url.test.js +29 -9
  268. package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -1
  269. package/dist/web/hooks/use-tool-info.d.ts +1 -20
  270. package/dist/web/hooks/use-tool-info.js +0 -1
  271. package/dist/web/hooks/use-tool-info.js.map +1 -1
  272. package/dist/web/hooks/use-tool-info.test-d.js +0 -5
  273. package/dist/web/hooks/use-tool-info.test-d.js.map +1 -1
  274. package/dist/web/hooks/use-tool-info.test.js +37 -32
  275. package/dist/web/hooks/use-tool-info.test.js.map +1 -1
  276. package/dist/web/hooks/use-user.d.ts +3 -2
  277. package/dist/web/hooks/use-user.js +4 -3
  278. package/dist/web/hooks/use-user.js.map +1 -1
  279. package/dist/web/hooks/use-user.test.js +84 -36
  280. package/dist/web/hooks/use-user.test.js.map +1 -1
  281. package/dist/web/hooks/use-view-state.js +19 -9
  282. package/dist/web/hooks/use-view-state.js.map +1 -1
  283. package/dist/web/hooks/use-view-state.test.js +38 -3
  284. package/dist/web/hooks/use-view-state.test.js.map +1 -1
  285. package/dist/web/hooks/use-viewport.d.ts +20 -0
  286. package/dist/web/hooks/use-viewport.js +21 -0
  287. package/dist/web/hooks/use-viewport.js.map +1 -0
  288. package/dist/web/hooks/use-viewport.test.d.ts +1 -0
  289. package/dist/web/hooks/use-viewport.test.js +109 -0
  290. package/dist/web/hooks/use-viewport.test.js.map +1 -0
  291. package/dist/web/index.d.ts +1 -0
  292. package/dist/web/index.js.map +1 -1
  293. package/dist/web/mount-view.d.ts +3 -2
  294. package/dist/web/mount-view.js +3 -2
  295. package/dist/web/mount-view.js.map +1 -1
  296. package/dist/web/plugin/plugin-build.test.d.ts +1 -0
  297. package/dist/web/plugin/plugin-build.test.js +54 -0
  298. package/dist/web/plugin/plugin-build.test.js.map +1 -0
  299. package/dist/web/plugin/plugin.d.ts +6 -0
  300. package/dist/web/plugin/plugin.js +18 -7
  301. package/dist/web/plugin/plugin.js.map +1 -1
  302. package/dist/web/plugin/plugin.test.d.ts +1 -0
  303. package/dist/web/plugin/plugin.test.js +61 -0
  304. package/dist/web/plugin/plugin.test.js.map +1 -0
  305. package/package.json +28 -23
  306. package/dist/cli/resolve-views-dir.d.ts +0 -1
  307. package/dist/cli/resolve-views-dir.js.map +0 -1
  308. package/dist/server/auth/providers/stytch.test.js +0 -33
  309. package/dist/server/auth/providers/stytch.test.js.map +0 -1
  310. package/dist/server/auth/providers/workos.test.js +0 -41
  311. package/dist/server/auth/providers/workos.test.js.map +0 -1
  312. package/dist/web/bridges/apps-sdk/adaptor.d.ts +0 -29
  313. package/dist/web/bridges/apps-sdk/adaptor.js +0 -117
  314. package/dist/web/bridges/apps-sdk/adaptor.js.map +0 -1
  315. package/dist/web/bridges/mcp-app/adaptor.d.ts +0 -53
  316. package/dist/web/bridges/mcp-app/adaptor.js +0 -283
  317. package/dist/web/bridges/mcp-app/adaptor.js.map +0 -1
  318. package/dist/web/hooks/use-layout.d.ts +0 -24
  319. package/dist/web/hooks/use-layout.js +0 -25
  320. package/dist/web/hooks/use-layout.js.map +0 -1
  321. package/dist/web/hooks/use-layout.test.js +0 -96
  322. package/dist/web/hooks/use-layout.test.js.map +0 -1
  323. /package/dist/{server/auth/providers/stytch.test.d.ts → cli/build-steps.test.d.ts} +0 -0
  324. /package/dist/{server/auth/providers/workos.test.d.ts → context-warnings.test.d.ts} +0 -0
  325. /package/dist/{web/hooks/use-layout.test.d.ts → server/auth-extra.test-d.d.ts} +0 -0
@@ -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 {\n INVALID_PARAMS,\n ProtocolError,\n type ReadResourceCallback,\n type ReadResourceTemplateCallback,\n type ResourceMetadata,\n ResourceTemplate,\n type McpServer as SdkMcpServer,\n} from \"@modelcontextprotocol/server\";\nimport { parse as parseYaml } from \"yaml\";\nimport { z } from \"zod\";\n\nexport const SKILLS_EXTENSION_KEY = \"io.modelcontextprotocol/skills\";\n\nconst SKILL_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;\n\nconst SkillFrontmatterSchema = z\n .object({\n name: z\n .string()\n .min(1)\n .max(64)\n .regex(\n SKILL_NAME_RE,\n \"must be lowercase alphanumeric words separated by single hyphens\",\n ),\n description: z.string().min(1).max(1024),\n })\n .loose();\n\nexport interface SkillResource {\n uri: string;\n digest: string;\n content: string;\n}\n\nexport interface Skill {\n uri: string;\n frontmatter: { name: string; description: string } & Record<string, unknown>;\n resources: SkillResource[];\n}\n\nexport type SkillsManifest = Skill[];\n\nconst sha256 = (content: string): string =>\n `sha256:${createHash(\"sha256\").update(content, \"utf8\").digest(\"hex\")}`;\n\nconst FRONTMATTER_RE = /^?\\s*---[ \\t]*\\r?\\n([\\s\\S]*?)\\r?\\n---[ \\t]*(?:\\r?\\n|$)/;\n\nfunction parseFrontmatter(content: string, source: string): unknown {\n const match = FRONTMATTER_RE.exec(content);\n if (!match) {\n throw new Error(`Skill ${source} is missing YAML frontmatter`);\n }\n try {\n return parseYaml(match[1] ?? \"\");\n } catch (error) {\n const detail = error instanceof Error ? error.message : String(error);\n throw new Error(`Cannot parse frontmatter of ${source}: ${detail}`);\n }\n}\n\nconst readSkillDir = (root: string, rel = \"\"): Record<string, string> => {\n const files: Record<string, string> = {};\n for (const entry of readdirSync(join(root, rel), { withFileTypes: true })) {\n const childRel = rel ? `${rel}/${entry.name}` : entry.name;\n if (entry.isSymbolicLink()) {\n continue;\n }\n if (entry.isDirectory()) {\n Object.assign(files, readSkillDir(root, childRel));\n } else if (entry.isFile() && extname(entry.name) === \".md\") {\n files[childRel] = readFileSync(join(root, childRel), \"utf8\");\n }\n }\n return files;\n};\n\nexport function discoverSkills(dir: string): SkillsManifest {\n if (!existsSync(dir)) {\n return [];\n }\n\n const skills: SkillsManifest = [];\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (!entry.isDirectory()) {\n continue;\n }\n\n const skillRoot = join(dir, entry.name);\n const files = readSkillDir(skillRoot);\n const skillMd = files[\"SKILL.md\"];\n if (skillMd === undefined) {\n continue;\n }\n\n const source = `${entry.name}/SKILL.md`;\n\n const parsed = SkillFrontmatterSchema.safeParse(\n parseFrontmatter(skillMd, source),\n );\n if (!parsed.success) {\n throw new Error(\n `Invalid skill frontmatter in ${source}: ${z.prettifyError(parsed.error)}`,\n );\n }\n if (parsed.data.name !== entry.name) {\n throw new Error(\n `Skill name \"${parsed.data.name}\" in ${source} must match its directory name \"${entry.name}\"`,\n );\n }\n\n const resources = Object.entries(files)\n .sort(\n ([a], [b]) =>\n Number(b === \"SKILL.md\") - Number(a === \"SKILL.md\") ||\n a.localeCompare(b),\n )\n .map(([relPath, content]) => ({\n uri: `skill://${entry.name}/${relPath}`,\n digest: sha256(content),\n content,\n }));\n\n skills.push({\n uri: `skill://${entry.name}/SKILL.md`,\n frontmatter: parsed.data,\n resources,\n });\n }\n return skills;\n}\n\nexport function skillUriToRelPath(uri: string): {\n name: string;\n relPath: string;\n} {\n const match = /^skill:\\/\\/([^/]+)(?:\\/(.*))?$/.exec(uri.replace(/\\/+$/, \"\"));\n if (!match) {\n throw new ProtocolError(INVALID_PARAMS, `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 ProtocolError(INVALID_PARAMS, `Invalid skill uri: ${uri}`);\n }\n return { name, relPath: segments.join(\"/\") };\n}\n\nfunction listDir(\n skill: Skill,\n relPath: string,\n): { name: string; mimeType: string }[] | null {\n const prefix = relPath === \"\" ? \"\" : `${relPath}/`;\n const base = skill.uri.slice(0, -\"SKILL.md\".length);\n const children = new Map<string, string>();\n let matched = relPath === \"\";\n for (const filePath of skill.resources.map((resource) =>\n resource.uri.slice(base.length),\n )) {\n if (!filePath.startsWith(prefix)) {\n continue;\n }\n matched = true;\n const rest = filePath.slice(prefix.length);\n const slash = rest.indexOf(\"/\");\n if (slash === -1) {\n children.set(rest, \"text/markdown\");\n } else {\n children.set(rest.slice(0, slash), \"inode/directory\");\n }\n }\n if (!matched) {\n return null;\n }\n return [...children].map(([name, mimeType]) => ({ name, mimeType }));\n}\n\nexport interface 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: Pick<SdkMcpServer[\"server\"], \"setRequestHandler\">;\n}\n\nconst DirectoryReadParamsSchema = z.object({\n uri: z.string(),\n cursor: z.string().optional(),\n});\n\nconst SkillsListParamsSchema = z\n .object({ cursor: z.string().optional() })\n .optional();\n\nconst SkillsGetParamsSchema = z.object({ uri: z.string() });\n\nconst toWireEntry = (skill: Skill) => ({\n uri: skill.uri,\n frontmatter: skill.frontmatter,\n resources: skill.resources.map(({ uri, digest }) => ({ uri, digest })),\n});\n\nexport function registerSkills(\n server: SkillRegistrar,\n manifest: SkillsManifest,\n): void {\n const byUri = new Map(manifest.map((skill) => [skill.uri, skill]));\n const serveFile = (skillUri: string, fileUri: string, href: string) => {\n const text = byUri\n .get(skillUri)\n ?.resources.find((resource) => resource.uri === fileUri)?.content;\n if (text === undefined) {\n throw new ProtocolError(INVALID_PARAMS, `Not found: ${href}`);\n }\n return {\n contents: [{ uri: href, text, mimeType: \"text/markdown\" }],\n };\n };\n\n for (const skill of manifest) {\n server.registerResource(\n skill.frontmatter.name,\n skill.uri,\n {\n description: skill.frontmatter.description,\n mimeType: \"text/markdown\",\n },\n (readUri) => serveFile(skill.uri, skill.uri, readUri.href),\n );\n }\n\n server.registerResource(\n \"skill-files\",\n new ResourceTemplate(\"skill://{skillName}/{+filePath}\", {\n list: undefined,\n }),\n {},\n (readUri) => {\n const { name, relPath } = skillUriToRelPath(readUri.href);\n return serveFile(\n `skill://${name}/SKILL.md`,\n `skill://${name}/${relPath}`,\n readUri.href,\n );\n },\n );\n\n server.server.setRequestHandler(\n \"skills/list\",\n { params: SkillsListParamsSchema },\n () => ({ skills: manifest.map(toWireEntry) }),\n );\n\n server.server.setRequestHandler(\n \"skills/get\",\n { params: SkillsGetParamsSchema },\n (params) => {\n const skill = byUri.get(params.uri);\n if (!skill) {\n throw new ProtocolError(INVALID_PARAMS, `Unknown skill: ${params.uri}`);\n }\n return { skill: toWireEntry(skill) };\n },\n );\n\n server.server.setRequestHandler(\n \"resources/directory/read\",\n { params: DirectoryReadParamsSchema },\n (params) => {\n const { name, relPath } = skillUriToRelPath(params.uri);\n const skill = byUri.get(`skill://${name}/SKILL.md`);\n const entries = skill ? listDir(skill, relPath) : null;\n if (!entries) {\n throw new ProtocolError(\n INVALID_PARAMS,\n `Not a directory: ${params.uri}`,\n );\n }\n const base = params.uri.replace(/\\/+$/, \"\");\n return {\n resources: entries.map((entry) => ({\n uri: `${base}/${entry.name}`,\n name: entry.name,\n mimeType: entry.mimeType,\n })),\n };\n },\n );\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,243 @@
1
+ // @vitest-environment node
2
+ import { createHash } from "node:crypto";
3
+ import { mkdirSync, mkdtempSync, symlinkSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { ResourceTemplate } from "@modelcontextprotocol/server";
7
+ import { describe, expect, it, vi } from "vitest";
8
+ import { discoverSkills, registerSkills, skillUriToRelPath, } from "./skills.js";
9
+ function mkSkillDir(files) {
10
+ const root = mkdtempSync(join(tmpdir(), "skybridge-skills-"));
11
+ for (const [rel, content] of Object.entries(files)) {
12
+ const full = join(root, rel);
13
+ mkdirSync(join(full, ".."), { recursive: true });
14
+ writeFileSync(full, content);
15
+ }
16
+ return root;
17
+ }
18
+ const FM = (name, description) => `---\nname: ${name}\ndescription: ${description}\n---\n`;
19
+ const digestOf = (content) => `sha256:${createHash("sha256").update(content, "utf8").digest("hex")}`;
20
+ describe("discoverSkills", () => {
21
+ it("discovers a skill with frontmatter and per-file resources", () => {
22
+ const skillMd = `${FM("git-workflow", "Team git conventions")}Body`;
23
+ const dir = mkSkillDir({
24
+ "git-workflow/SKILL.md": skillMd,
25
+ "git-workflow/references/GUIDE.md": "# Guide",
26
+ });
27
+ const [skill, ...rest] = discoverSkills(dir);
28
+ expect(rest).toHaveLength(0);
29
+ expect(skill?.uri).toBe("skill://git-workflow/SKILL.md");
30
+ expect(skill?.frontmatter).toMatchObject({
31
+ name: "git-workflow",
32
+ description: "Team git conventions",
33
+ });
34
+ expect(skill?.resources).toEqual([
35
+ {
36
+ uri: "skill://git-workflow/SKILL.md",
37
+ digest: digestOf(skillMd),
38
+ content: skillMd,
39
+ },
40
+ {
41
+ uri: "skill://git-workflow/references/GUIDE.md",
42
+ digest: digestOf("# Guide"),
43
+ content: "# Guide",
44
+ },
45
+ ]);
46
+ });
47
+ it("returns [] for a missing directory and skips dirs without SKILL.md", () => {
48
+ expect(discoverSkills(join(tmpdir(), "does-not-exist-xyz"))).toEqual([]);
49
+ const dir = mkSkillDir({ "not-a-skill/README.md": "hi" });
50
+ expect(discoverSkills(dir)).toEqual([]);
51
+ });
52
+ it("parses a nested metadata map in frontmatter", () => {
53
+ const dir = mkSkillDir({
54
+ "s/SKILL.md": `---\nname: s\ndescription: d\nmetadata:\n version: "1.2"\n team: core\n---\n`,
55
+ });
56
+ expect(discoverSkills(dir)[0]?.frontmatter.metadata).toEqual({
57
+ version: "1.2",
58
+ team: "core",
59
+ });
60
+ });
61
+ it("throws on missing frontmatter", () => {
62
+ const dir = mkSkillDir({ "s/SKILL.md": "no frontmatter here" });
63
+ expect(() => discoverSkills(dir)).toThrow(/missing YAML frontmatter/);
64
+ });
65
+ it("throws when required fields are absent", () => {
66
+ const dir = mkSkillDir({ "s/SKILL.md": "---\nname: s\n---\n" });
67
+ expect(() => discoverSkills(dir)).toThrow(/Invalid skill frontmatter/);
68
+ });
69
+ it("throws when the frontmatter name does not match the directory", () => {
70
+ const dir = mkSkillDir({ "s/SKILL.md": FM("other", "d") });
71
+ expect(() => discoverSkills(dir)).toThrow(/must match its directory name/);
72
+ });
73
+ it("preserves arbitrary YAML frontmatter (arrays, nesting) verbatim", () => {
74
+ const dir = mkSkillDir({
75
+ "s/SKILL.md": "---\nname: s\ndescription: d\nallowed-tools:\n - get-order\n - refund\n---\n",
76
+ });
77
+ expect(discoverSkills(dir)[0]?.frontmatter["allowed-tools"]).toEqual([
78
+ "get-order",
79
+ "refund",
80
+ ]);
81
+ });
82
+ it("throws on malformed YAML frontmatter", () => {
83
+ const dir = mkSkillDir({
84
+ "s/SKILL.md": "---\nname: 's\ndescription: [unclosed\n---\n",
85
+ });
86
+ expect(() => discoverSkills(dir)).toThrow(/Cannot parse frontmatter/);
87
+ });
88
+ });
89
+ describe("skillUriToRelPath", () => {
90
+ it("splits name and relative path", () => {
91
+ expect(skillUriToRelPath("skill://refunds/SKILL.md")).toEqual({
92
+ name: "refunds",
93
+ relPath: "SKILL.md",
94
+ });
95
+ expect(skillUriToRelPath("skill://refunds")).toEqual({
96
+ name: "refunds",
97
+ relPath: "",
98
+ });
99
+ });
100
+ it("rejects path traversal and malformed URIs", () => {
101
+ expect(() => skillUriToRelPath("skill://s/../../etc/passwd")).toThrow();
102
+ expect(() => skillUriToRelPath("skill://s/./x")).toThrow();
103
+ expect(() => skillUriToRelPath("https://example.com")).toThrow();
104
+ });
105
+ });
106
+ describe("registerSkills", () => {
107
+ const manifest = [
108
+ {
109
+ uri: "skill://refunds/SKILL.md",
110
+ frontmatter: { name: "refunds", description: "Process refunds" },
111
+ resources: [
112
+ {
113
+ uri: "skill://refunds/SKILL.md",
114
+ digest: digestOf("# Refunds"),
115
+ content: "# Refunds",
116
+ },
117
+ {
118
+ uri: "skill://refunds/templates/email.md",
119
+ digest: digestOf("Hi"),
120
+ content: "Hi",
121
+ },
122
+ ],
123
+ },
124
+ ];
125
+ function fakeRegistrar() {
126
+ const resources = new Map();
127
+ const handlers = new Map();
128
+ const server = {
129
+ registerResource: vi.fn((name, uri, _cfg, cb) => {
130
+ resources.set(name, {
131
+ uri,
132
+ cb: cb,
133
+ });
134
+ }),
135
+ server: {
136
+ setRequestHandler: vi.fn((method, _schemas, handler) => {
137
+ handlers.set(method, handler);
138
+ }),
139
+ },
140
+ };
141
+ return { server, resources, handlers };
142
+ }
143
+ it("serves supporting files through the template resource", () => {
144
+ const { server, resources } = fakeRegistrar();
145
+ // biome-ignore lint/suspicious/noExplicitAny: structural test double
146
+ registerSkills(server, manifest);
147
+ const tpl = resources.get("skill-files");
148
+ expect(tpl?.uri).toBeInstanceOf(ResourceTemplate);
149
+ const url = new URL("skill://refunds/templates/email.md");
150
+ const result = tpl?.cb(url, {}, {});
151
+ expect(result.contents[0]?.text).toBe("Hi");
152
+ });
153
+ it("lists a subdirectory through the directory-read handler", () => {
154
+ const on = fakeRegistrar();
155
+ // biome-ignore lint/suspicious/noExplicitAny: structural test double
156
+ registerSkills(on.server, manifest);
157
+ const result = on.handlers.get("resources/directory/read")?.({
158
+ uri: "skill://refunds/templates",
159
+ });
160
+ expect(result.resources).toEqual([
161
+ {
162
+ uri: "skill://refunds/templates/email.md",
163
+ name: "email.md",
164
+ mimeType: "text/markdown",
165
+ },
166
+ ]);
167
+ });
168
+ it("lists a skill root and marks subdirectories as inode/directory", () => {
169
+ const { server, handlers } = fakeRegistrar();
170
+ // biome-ignore lint/suspicious/noExplicitAny: structural test double
171
+ registerSkills(server, manifest);
172
+ const result = handlers.get("resources/directory/read")?.({
173
+ uri: "skill://refunds",
174
+ });
175
+ expect(result.resources).toContainEqual({
176
+ uri: "skill://refunds/templates",
177
+ name: "templates",
178
+ mimeType: "inode/directory",
179
+ });
180
+ expect(result.resources).toContainEqual({
181
+ uri: "skill://refunds/SKILL.md",
182
+ name: "SKILL.md",
183
+ mimeType: "text/markdown",
184
+ });
185
+ });
186
+ it("lists entries with complete per-file resources via skills/list", () => {
187
+ const { server, handlers } = fakeRegistrar();
188
+ // biome-ignore lint/suspicious/noExplicitAny: structural test double
189
+ registerSkills(server, manifest);
190
+ const result = handlers.get("skills/list")?.({});
191
+ expect(result).toEqual({
192
+ skills: [
193
+ {
194
+ uri: "skill://refunds/SKILL.md",
195
+ frontmatter: { name: "refunds", description: "Process refunds" },
196
+ resources: [
197
+ { uri: "skill://refunds/SKILL.md", digest: digestOf("# Refunds") },
198
+ {
199
+ uri: "skill://refunds/templates/email.md",
200
+ digest: digestOf("Hi"),
201
+ },
202
+ ],
203
+ },
204
+ ],
205
+ });
206
+ });
207
+ it("returns a single entry via skills/get and rejects non-skill URIs", () => {
208
+ const { server, handlers } = fakeRegistrar();
209
+ // biome-ignore lint/suspicious/noExplicitAny: structural test double
210
+ registerSkills(server, manifest);
211
+ const get = handlers.get("skills/get");
212
+ const result = get?.({ uri: "skill://refunds/SKILL.md" });
213
+ expect(result.skill.uri).toBe("skill://refunds/SKILL.md");
214
+ expect(result.skill.resources).toHaveLength(2);
215
+ expect(() => get?.({ uri: "skill://unknown/SKILL.md" })).toThrow(/Unknown skill/);
216
+ expect(() => get?.({ uri: "skill://refunds/templates/email.md" })).toThrow(/Unknown skill/);
217
+ });
218
+ });
219
+ describe("discoverSkills symlink safety", () => {
220
+ it("excludes symlinked files and directories from the manifest", () => {
221
+ const outside = mkdtempSync(join(tmpdir(), "skybridge-outside-"));
222
+ writeFileSync(join(outside, "secret.md"), "TOP SECRET");
223
+ const dir = mkSkillDir({
224
+ "demo/SKILL.md": FM("demo", "d"),
225
+ "demo/real.md": "ok",
226
+ });
227
+ symlinkSync(join(outside, "secret.md"), join(dir, "demo", "leak.md"));
228
+ symlinkSync(outside, join(dir, "demo", "escape"));
229
+ const uris = discoverSkills(dir)[0]?.resources.map((r) => r.uri) ?? [];
230
+ expect(uris).toEqual(["skill://demo/SKILL.md", "skill://demo/real.md"]);
231
+ });
232
+ it("ignores non-markdown supporting files", () => {
233
+ const dir = mkSkillDir({
234
+ "demo/SKILL.md": FM("demo", "d"),
235
+ "demo/notes.md": "kept",
236
+ "demo/data.json": "{}",
237
+ "demo/scripts/run.py": "print(1)",
238
+ });
239
+ const uris = discoverSkills(dir)[0]?.resources.map((r) => r.uri) ?? [];
240
+ expect(uris).toEqual(["skill://demo/SKILL.md", "skill://demo/notes.md"]);
241
+ });
242
+ });
243
+ //# sourceMappingURL=skills.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skills.test.js","sourceRoot":"","sources":["../../src/server/skills.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\nimport { createHash } from \"node:crypto\";\nimport { mkdirSync, mkdtempSync, symlinkSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { ResourceTemplate } from \"@modelcontextprotocol/server\";\nimport { describe, expect, it, vi } from \"vitest\";\nimport {\n discoverSkills,\n registerSkills,\n type Skill,\n skillUriToRelPath,\n} from \"./skills.js\";\n\nfunction mkSkillDir(files: Record<string, string>): string {\n const root = mkdtempSync(join(tmpdir(), \"skybridge-skills-\"));\n for (const [rel, content] of Object.entries(files)) {\n const full = join(root, rel);\n mkdirSync(join(full, \"..\"), { recursive: true });\n writeFileSync(full, content);\n }\n return root;\n}\n\nconst FM = (name: string, description: string) =>\n `---\\nname: ${name}\\ndescription: ${description}\\n---\\n`;\n\nconst digestOf = (content: string) =>\n `sha256:${createHash(\"sha256\").update(content, \"utf8\").digest(\"hex\")}`;\n\ndescribe(\"discoverSkills\", () => {\n it(\"discovers a skill with frontmatter and per-file resources\", () => {\n const skillMd = `${FM(\"git-workflow\", \"Team git conventions\")}Body`;\n const dir = mkSkillDir({\n \"git-workflow/SKILL.md\": skillMd,\n \"git-workflow/references/GUIDE.md\": \"# Guide\",\n });\n const [skill, ...rest] = discoverSkills(dir);\n expect(rest).toHaveLength(0);\n expect(skill?.uri).toBe(\"skill://git-workflow/SKILL.md\");\n expect(skill?.frontmatter).toMatchObject({\n name: \"git-workflow\",\n description: \"Team git conventions\",\n });\n expect(skill?.resources).toEqual([\n {\n uri: \"skill://git-workflow/SKILL.md\",\n digest: digestOf(skillMd),\n content: skillMd,\n },\n {\n uri: \"skill://git-workflow/references/GUIDE.md\",\n digest: digestOf(\"# Guide\"),\n content: \"# Guide\",\n },\n ]);\n });\n\n it(\"returns [] for a missing directory and skips dirs without SKILL.md\", () => {\n expect(discoverSkills(join(tmpdir(), \"does-not-exist-xyz\"))).toEqual([]);\n const dir = mkSkillDir({ \"not-a-skill/README.md\": \"hi\" });\n expect(discoverSkills(dir)).toEqual([]);\n });\n\n it(\"parses a nested metadata map in frontmatter\", () => {\n const dir = mkSkillDir({\n \"s/SKILL.md\": `---\\nname: s\\ndescription: d\\nmetadata:\\n version: \"1.2\"\\n team: core\\n---\\n`,\n });\n expect(discoverSkills(dir)[0]?.frontmatter.metadata).toEqual({\n version: \"1.2\",\n team: \"core\",\n });\n });\n\n it(\"throws on missing frontmatter\", () => {\n const dir = mkSkillDir({ \"s/SKILL.md\": \"no frontmatter here\" });\n expect(() => discoverSkills(dir)).toThrow(/missing YAML frontmatter/);\n });\n\n it(\"throws when required fields are absent\", () => {\n const dir = mkSkillDir({ \"s/SKILL.md\": \"---\\nname: s\\n---\\n\" });\n expect(() => discoverSkills(dir)).toThrow(/Invalid skill frontmatter/);\n });\n\n it(\"throws when the frontmatter name does not match the directory\", () => {\n const dir = mkSkillDir({ \"s/SKILL.md\": FM(\"other\", \"d\") });\n expect(() => discoverSkills(dir)).toThrow(/must match its directory name/);\n });\n\n it(\"preserves arbitrary YAML frontmatter (arrays, nesting) verbatim\", () => {\n const dir = mkSkillDir({\n \"s/SKILL.md\":\n \"---\\nname: s\\ndescription: d\\nallowed-tools:\\n - get-order\\n - refund\\n---\\n\",\n });\n expect(discoverSkills(dir)[0]?.frontmatter[\"allowed-tools\"]).toEqual([\n \"get-order\",\n \"refund\",\n ]);\n });\n\n it(\"throws on malformed YAML frontmatter\", () => {\n const dir = mkSkillDir({\n \"s/SKILL.md\": \"---\\nname: 's\\ndescription: [unclosed\\n---\\n\",\n });\n expect(() => discoverSkills(dir)).toThrow(/Cannot parse frontmatter/);\n });\n});\n\ndescribe(\"skillUriToRelPath\", () => {\n it(\"splits name and relative path\", () => {\n expect(skillUriToRelPath(\"skill://refunds/SKILL.md\")).toEqual({\n name: \"refunds\",\n relPath: \"SKILL.md\",\n });\n expect(skillUriToRelPath(\"skill://refunds\")).toEqual({\n name: \"refunds\",\n relPath: \"\",\n });\n });\n\n it(\"rejects path traversal and malformed URIs\", () => {\n expect(() => skillUriToRelPath(\"skill://s/../../etc/passwd\")).toThrow();\n expect(() => skillUriToRelPath(\"skill://s/./x\")).toThrow();\n expect(() => skillUriToRelPath(\"https://example.com\")).toThrow();\n });\n});\n\ndescribe(\"registerSkills\", () => {\n const manifest: Skill[] = [\n {\n uri: \"skill://refunds/SKILL.md\",\n frontmatter: { name: \"refunds\", description: \"Process refunds\" },\n resources: [\n {\n uri: \"skill://refunds/SKILL.md\",\n digest: digestOf(\"# Refunds\"),\n content: \"# Refunds\",\n },\n {\n uri: \"skill://refunds/templates/email.md\",\n digest: digestOf(\"Hi\"),\n content: \"Hi\",\n },\n ],\n },\n ];\n\n function fakeRegistrar() {\n const resources = new Map<\n string,\n { uri: unknown; cb: (...args: unknown[]) => unknown }\n >();\n const handlers = new Map<string, (req: unknown) => unknown>();\n const server = {\n registerResource: vi.fn(\n (name: string, uri: unknown, _cfg: unknown, cb: unknown) => {\n resources.set(name, {\n uri,\n cb: cb as (...args: unknown[]) => unknown,\n });\n },\n ),\n server: {\n setRequestHandler: vi.fn(\n (method: unknown, _schemas: unknown, handler: unknown) => {\n handlers.set(\n method as string,\n handler as (req: unknown) => unknown,\n );\n },\n ),\n },\n };\n return { server, resources, handlers };\n }\n\n it(\"serves supporting files through the template resource\", () => {\n const { server, resources } = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(server as any, manifest);\n const tpl = resources.get(\"skill-files\");\n expect(tpl?.uri).toBeInstanceOf(ResourceTemplate);\n const url = new URL(\"skill://refunds/templates/email.md\");\n const result = tpl?.cb(url, {}, {}) as { contents: { text: string }[] };\n expect(result.contents[0]?.text).toBe(\"Hi\");\n });\n\n it(\"lists a subdirectory through the directory-read handler\", () => {\n const on = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(on.server as any, manifest);\n const result = on.handlers.get(\"resources/directory/read\")?.({\n uri: \"skill://refunds/templates\",\n }) as { resources: { uri: string; name: string; mimeType: string }[] };\n expect(result.resources).toEqual([\n {\n uri: \"skill://refunds/templates/email.md\",\n name: \"email.md\",\n mimeType: \"text/markdown\",\n },\n ]);\n });\n\n it(\"lists a skill root and marks subdirectories as inode/directory\", () => {\n const { server, handlers } = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(server as any, manifest);\n const result = handlers.get(\"resources/directory/read\")?.({\n uri: \"skill://refunds\",\n }) as { resources: { name: string; mimeType: string }[] };\n expect(result.resources).toContainEqual({\n uri: \"skill://refunds/templates\",\n name: \"templates\",\n mimeType: \"inode/directory\",\n });\n expect(result.resources).toContainEqual({\n uri: \"skill://refunds/SKILL.md\",\n name: \"SKILL.md\",\n mimeType: \"text/markdown\",\n });\n });\n\n it(\"lists entries with complete per-file resources via skills/list\", () => {\n const { server, handlers } = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(server as any, manifest);\n const result = handlers.get(\"skills/list\")?.({});\n expect(result).toEqual({\n skills: [\n {\n uri: \"skill://refunds/SKILL.md\",\n frontmatter: { name: \"refunds\", description: \"Process refunds\" },\n resources: [\n { uri: \"skill://refunds/SKILL.md\", digest: digestOf(\"# Refunds\") },\n {\n uri: \"skill://refunds/templates/email.md\",\n digest: digestOf(\"Hi\"),\n },\n ],\n },\n ],\n });\n });\n\n it(\"returns a single entry via skills/get and rejects non-skill URIs\", () => {\n const { server, handlers } = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(server as any, manifest);\n const get = handlers.get(\"skills/get\");\n const result = get?.({ uri: \"skill://refunds/SKILL.md\" }) as {\n skill: { uri: string; resources: unknown[] };\n };\n expect(result.skill.uri).toBe(\"skill://refunds/SKILL.md\");\n expect(result.skill.resources).toHaveLength(2);\n expect(() => get?.({ uri: \"skill://unknown/SKILL.md\" })).toThrow(\n /Unknown skill/,\n );\n expect(() => get?.({ uri: \"skill://refunds/templates/email.md\" })).toThrow(\n /Unknown skill/,\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 uris = discoverSkills(dir)[0]?.resources.map((r) => r.uri) ?? [];\n expect(uris).toEqual([\"skill://demo/SKILL.md\", \"skill://demo/real.md\"]);\n });\n\n it(\"ignores non-markdown supporting files\", () => {\n const dir = mkSkillDir({\n \"demo/SKILL.md\": FM(\"demo\", \"d\"),\n \"demo/notes.md\": \"kept\",\n \"demo/data.json\": \"{}\",\n \"demo/scripts/run.py\": \"print(1)\",\n });\n const uris = discoverSkills(dir)[0]?.resources.map((r) => r.uri) ?? [];\n expect(uris).toEqual([\"skill://demo/SKILL.md\", \"skill://demo/notes.md\"]);\n });\n});\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,72 @@
1
+ // @vitest-environment node
2
+ import http from "node:http";
3
+ import { Client, StreamableHTTPClientTransport, } from "@modelcontextprotocol/client";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+ import { z } from "zod";
6
+ import { Skybridge } from "./app.js";
7
+ vi.mock("@skybridge/devtools", () => ({
8
+ devtoolsStaticServer: () => ((_r, _s, n) => n()),
9
+ }));
10
+ vi.mock("./viewsDevServer.js", () => ({
11
+ viewsDevServer: () => ((_r, _s, n) => n()),
12
+ }));
13
+ let openServer;
14
+ afterEach(() => openServer?.close());
15
+ describe("tool handler extra", () => {
16
+ it("exposes the client hints the host sent on the request", async () => {
17
+ const { createApp } = await import("./express.js");
18
+ let seenLocale;
19
+ const app = new Skybridge({ name: "t", version: "0.0.0" }, (server) => server.registerTool({ name: "hints", description: "Reads client hints.", inputSchema: {} }, (_args, extra) => {
20
+ seenLocale = extra.mcpReq._meta?.["openai/locale"];
21
+ return { content: [{ type: "text", text: "ok" }] };
22
+ }));
23
+ const httpServer = http.createServer();
24
+ const expressApp = await createApp({ app, httpServer });
25
+ const listening = http.createServer(expressApp);
26
+ await new Promise((r) => listening.listen(0, r));
27
+ openServer = listening;
28
+ const port = listening.address().port;
29
+ const client = new Client({ name: "c", version: "0.0.0" });
30
+ await client.connect(new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)));
31
+ await client.callTool({
32
+ name: "hints",
33
+ arguments: {},
34
+ _meta: { "openai/locale": "fr-FR" },
35
+ });
36
+ await client.close();
37
+ expect(seenLocale).toBe("fr-FR");
38
+ });
39
+ });
40
+ describe("stateless server instances", () => {
41
+ it("carries the registered capabilities onto each per-request instance", () => {
42
+ const app = new Skybridge({ name: "t", version: "0.0.0" }, (server) => server.registerTool({ name: "a-tool", description: "d", inputSchema: {} }, () => ({ content: [{ type: "text", text: "ok" }] })));
43
+ const fresh = app.createServerInstance();
44
+ expect(fresh.getCapabilities().tools).toBeDefined();
45
+ });
46
+ it("serves a 2026-era caller with that era's result shape", async () => {
47
+ const { createApp } = await import("./express.js");
48
+ const app = new Skybridge({ name: "t", version: "0.0.0" }, (server) => server.registerTool({
49
+ name: "scalar",
50
+ description: "Returns a scalar structuredContent.",
51
+ inputSchema: {},
52
+ outputSchema: z.number(),
53
+ }, () => ({
54
+ structuredContent: 42,
55
+ content: [{ type: "text", text: "42" }],
56
+ })));
57
+ const httpServer = http.createServer();
58
+ const expressApp = await createApp({ app, httpServer });
59
+ const listening = http.createServer(expressApp);
60
+ await new Promise((r) => listening.listen(0, r));
61
+ openServer = listening;
62
+ const port = listening.address().port;
63
+ const client = new Client({ name: "c", version: "0.0.0" }, { versionNegotiation: { mode: "auto" } });
64
+ await client.connect(new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`)));
65
+ const result = await client.callTool({ name: "scalar", arguments: {} });
66
+ const negotiated = client.getNegotiatedProtocolVersion();
67
+ await client.close();
68
+ expect(negotiated).toBe("2026-07-28");
69
+ expect(result.structuredContent).toBe(42);
70
+ });
71
+ });
72
+ //# sourceMappingURL=tool-extra.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-extra.test.js","sourceRoot":"","sources":["../../src/server/tool-extra.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\nimport http from \"node:http\";\nimport {\n Client,\n StreamableHTTPClientTransport,\n} from \"@modelcontextprotocol/client\";\nimport type { RequestHandler } from \"express\";\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { z } from \"zod\";\nimport { Skybridge } from \"./app.js\";\n\nvi.mock(\"@skybridge/devtools\", () => ({\n devtoolsStaticServer: () =>\n ((_r: unknown, _s: unknown, n: () => void) => n()) as RequestHandler,\n}));\nvi.mock(\"./viewsDevServer.js\", () => ({\n viewsDevServer: () =>\n ((_r: unknown, _s: unknown, n: () => void) => n()) as RequestHandler,\n}));\n\nlet openServer: http.Server | undefined;\nafterEach(() => openServer?.close());\n\ndescribe(\"tool handler extra\", () => {\n it(\"exposes the client hints the host sent on the request\", async () => {\n const { createApp } = await import(\"./express.js\");\n let seenLocale: unknown;\n const app = new Skybridge({ name: \"t\", version: \"0.0.0\" }, (server) =>\n server.registerTool(\n { name: \"hints\", description: \"Reads client hints.\", inputSchema: {} },\n (_args, extra) => {\n seenLocale = extra.mcpReq._meta?.[\"openai/locale\"];\n return { content: [{ type: \"text\", text: \"ok\" }] };\n },\n ),\n );\n\n const httpServer = http.createServer();\n const expressApp = await createApp({ app, httpServer });\n const listening = http.createServer(expressApp);\n await new Promise<void>((r) => listening.listen(0, r));\n openServer = listening;\n const port = (listening.address() as { port: number }).port;\n\n const client = new Client({ name: \"c\", version: \"0.0.0\" });\n await client.connect(\n new StreamableHTTPClientTransport(\n new URL(`http://localhost:${port}/mcp`),\n ),\n );\n await client.callTool({\n name: \"hints\",\n arguments: {},\n _meta: { \"openai/locale\": \"fr-FR\" },\n });\n await client.close();\n\n expect(seenLocale).toBe(\"fr-FR\");\n });\n});\n\ndescribe(\"stateless server instances\", () => {\n it(\"carries the registered capabilities onto each per-request instance\", () => {\n const app = new Skybridge({ name: \"t\", version: \"0.0.0\" }, (server) =>\n server.registerTool(\n { name: \"a-tool\", description: \"d\", inputSchema: {} },\n () => ({ content: [{ type: \"text\", text: \"ok\" }] }),\n ),\n );\n\n const fresh = app.createServerInstance();\n\n expect(fresh.getCapabilities().tools).toBeDefined();\n });\n\n it(\"serves a 2026-era caller with that era's result shape\", async () => {\n const { createApp } = await import(\"./express.js\");\n const app = new Skybridge({ name: \"t\", version: \"0.0.0\" }, (server) =>\n server.registerTool(\n {\n name: \"scalar\",\n description: \"Returns a scalar structuredContent.\",\n inputSchema: {},\n outputSchema: z.number(),\n },\n () => ({\n structuredContent: 42,\n content: [{ type: \"text\", text: \"42\" }],\n }),\n ),\n );\n\n const httpServer = http.createServer();\n const expressApp = await createApp({ app, httpServer });\n const listening = http.createServer(expressApp);\n await new Promise<void>((r) => listening.listen(0, r));\n openServer = listening;\n const port = (listening.address() as { port: number }).port;\n\n const client = new Client(\n { name: \"c\", version: \"0.0.0\" },\n { versionNegotiation: { mode: \"auto\" } },\n );\n await client.connect(\n new StreamableHTTPClientTransport(\n new URL(`http://localhost:${port}/mcp`),\n ),\n );\n const result = await client.callTool({ name: \"scalar\", arguments: {} });\n const negotiated = client.getNegotiatedProtocolVersion();\n await client.close();\n\n expect(negotiated).toBe(\"2026-07-28\");\n expect(result.structuredContent).toBe(42);\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,24 +1,57 @@
1
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
- import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
3
- import { afterEach, describe, expect, it } from "vitest";
4
- import { __setBuildManifest, McpServer } from "./index.js";
1
+ import http from "node:http";
2
+ import { Client, InMemoryTransport, StreamableHTTPClientTransport, } from "@modelcontextprotocol/client";
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+ import { __setBuildManifest, Skybridge } from "./index.js";
5
+ vi.mock("@skybridge/devtools", () => ({
6
+ devtoolsStaticServer: () => ((_req, _res, next) => next()),
7
+ }));
8
+ vi.mock("./viewsDevServer.js", () => ({
9
+ viewsDevServer: (_httpServer) => ((_req, _res, next) => next()),
10
+ }));
5
11
  // SKY-435: a view resource must resolve no matter the `?v=` query param value.
6
12
  // The version param is a content-derived cache key for external consumers
7
13
  // (it busts host/CDN caches when the bundle changes); it is not part of the
8
14
  // resource's identity, so a stale, absent, or arbitrary param must still serve
9
15
  // the underlying asset.
16
+ function buildApp(register) {
17
+ return new Skybridge({ name: "test", version: "1.0.0" }, (server) => {
18
+ register(server);
19
+ return server;
20
+ });
21
+ }
10
22
  async function connect(register) {
11
- const server = new McpServer({ name: "test", version: "1.0.0" });
12
- register(server);
23
+ const instance = buildApp(register).createServerInstance();
13
24
  const client = new Client({ name: "test-client", version: "1.0.0" });
14
25
  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
15
- await server.connect(serverTransport);
26
+ await instance.connect(serverTransport);
16
27
  await client.connect(clientTransport);
17
28
  return {
18
29
  client,
19
30
  teardown: async () => {
20
31
  await client.close();
21
- await server.close();
32
+ await instance.close();
33
+ },
34
+ };
35
+ }
36
+ let openHttpServer;
37
+ afterEach(() => openHttpServer?.close());
38
+ // Connect over real HTTP so forwarded proxy headers reach the resource handler.
39
+ async function connectHttp(register, headers) {
40
+ const { createApp } = await import("./express.js");
41
+ const app = buildApp(register);
42
+ const httpServer = http.createServer();
43
+ await createApp({ app, httpServer });
44
+ const listening = http.createServer(app.express);
45
+ await new Promise((resolve) => listening.listen(0, resolve));
46
+ openHttpServer = listening;
47
+ const port = listening.address().port;
48
+ const client = new Client({ name: "test-client", version: "1.0.0" });
49
+ const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`), { requestInit: { headers } });
50
+ await client.connect(transport);
51
+ return {
52
+ client,
53
+ teardown: async () => {
54
+ await client.close();
22
55
  },
23
56
  };
24
57
  }
@@ -36,15 +69,15 @@ describe("view resource resolution (cache key)", () => {
36
69
  it("resolves regardless of the ?v= param and echoes the requested URI", async () => {
37
70
  const { client, teardown } = await connect(registerWidget);
38
71
  const { resources } = await client.listResources();
39
- const listed = resources.find((r) => r.uri.startsWith("ui://views/apps-sdk/widget.html"));
72
+ const listed = resources.find((r) => r.uri.startsWith("ui://views/ext-apps/widget.html"));
40
73
  // Dev build advertises no cache key.
41
- expect(listed?.uri).toBe("ui://views/apps-sdk/widget.html");
74
+ expect(listed?.uri).toBe("ui://views/ext-apps/widget.html");
42
75
  // A stale/arbitrary/absent param all resolve the same underlying asset,
43
76
  // and the response echoes the URI the consumer asked for (never rewritten).
44
77
  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",
78
+ "ui://views/ext-apps/widget.html?v=stale123",
79
+ "ui://views/ext-apps/widget.html?v=whatever-else",
80
+ "ui://views/ext-apps/widget.html",
48
81
  ]) {
49
82
  const { contents } = await client.readResource({ uri });
50
83
  expect(contents).toHaveLength(1);
@@ -67,11 +100,11 @@ describe("view resource resolution (cache key)", () => {
67
100
  });
68
101
  const { client, teardown } = await connect(registerWidget);
69
102
  const { resources } = await client.listResources();
70
- const listed = resources.find((r) => r.uri.startsWith("ui://views/apps-sdk/widget.html"));
103
+ const listed = resources.find((r) => r.uri.startsWith("ui://views/ext-apps/widget.html"));
71
104
  // Production advertises a content-derived cache key.
72
- expect(listed?.uri).toMatch(/^ui:\/\/views\/apps-sdk\/widget\.html\?v=[0-9a-f]{8}$/);
105
+ expect(listed?.uri).toMatch(/^ui:\/\/views\/ext-apps\/widget\.html\?v=[0-9a-f]{8}$/);
73
106
  // A consumer holding a stale key still resolves the current asset.
74
- const staleUri = "ui://views/apps-sdk/widget.html?v=00000000";
107
+ const staleUri = "ui://views/ext-apps/widget.html?v=00000000";
75
108
  const { contents } = await client.readResource({ uri: staleUri });
76
109
  expect(contents).toHaveLength(1);
77
110
  const content = textContent(contents);
@@ -81,7 +114,58 @@ describe("view resource resolution (cache key)", () => {
81
114
  });
82
115
  it("still throws for an unknown view path", async () => {
83
116
  const { client, teardown } = await connect(registerWidget);
84
- await expect(client.readResource({ uri: "ui://views/apps-sdk/nope.html?v=1" })).rejects.toThrow();
117
+ await expect(client.readResource({ uri: "ui://views/ext-apps/nope.html?v=1" })).rejects.toThrow();
118
+ await teardown();
119
+ });
120
+ // A proxy routing the request under a path prefix sends x-forwarded-prefix;
121
+ // the view's asset URLs must carry that prefix (and the forwarded origin) so
122
+ // they resolve back through the same routing. Per-request, so one process can
123
+ // serve many hosts/prefixes at once.
124
+ it("prefixes emitted asset URLs with x-forwarded-prefix (prod)", async () => {
125
+ process.env.NODE_ENV = "production";
126
+ __setBuildManifest({
127
+ "src/views/widget.tsx": {
128
+ isEntry: true,
129
+ name: "widget",
130
+ file: "assets/widget-ABC123.js",
131
+ },
132
+ "style.css": { file: "assets/style-XYZ.css" },
133
+ });
134
+ const { client, teardown } = await connectHttp(registerWidget, {
135
+ "x-forwarded-host": "foo.com",
136
+ "x-forwarded-proto": "https",
137
+ "x-forwarded-prefix": "/v1/canary",
138
+ });
139
+ try {
140
+ const { contents } = await client.readResource({
141
+ uri: "ui://views/ext-apps/widget.html",
142
+ });
143
+ const { text } = textContent(contents);
144
+ // Forwarded origin + prefix. (`assets/assets` is the existing layout: the
145
+ // manifest `file` already carries `assets/` and the template prepends
146
+ // `/assets/`; build.tsx's `_redirects` collapses it on the asset host.)
147
+ expect(text).toContain("https://foo.com/v1/canary/assets/assets/widget-ABC123.js");
148
+ expect(text).toContain("https://foo.com/v1/canary/assets/assets/style-XYZ.css");
149
+ // No unprefixed asset URL leaks through.
150
+ expect(text).not.toContain("foo.com/assets/");
151
+ }
152
+ finally {
153
+ await teardown();
154
+ }
155
+ });
156
+ // Back-compat: older Skybridge advertised the view at `ui://views/apps-sdk/...`
157
+ // via openai/outputTemplate. We no longer advertise it, but apps published then
158
+ // have it cached, so the read must still resolve to the ext-apps content.
159
+ it("resolves the legacy apps-sdk URL to the ext-apps content", async () => {
160
+ const { client, teardown } = await connect(registerWidget);
161
+ const legacyUri = "ui://views/apps-sdk/widget.html";
162
+ const { contents } = await client.readResource({ uri: legacyUri });
163
+ expect(contents).toHaveLength(1);
164
+ const content = contents[0];
165
+ expect(content.mimeType).toBe("text/html;profile=mcp-app");
166
+ expect(content.text.length).toBeGreaterThan(0);
167
+ // The response echoes the requested (legacy) URI, never the canonical one.
168
+ expect(content.uri).toBe(legacyUri);
85
169
  await teardown();
86
170
  });
87
171
  });