skybridge 0.0.0-dev.d60c856 → 0.0.0-dev.d62300f

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 (347) hide show
  1. package/README.md +5 -1
  2. package/dist/cli/build-helpers.d.ts +11 -0
  3. package/dist/cli/build-helpers.js +107 -0
  4. package/dist/cli/build-helpers.js.map +1 -0
  5. package/dist/cli/build-helpers.test.d.ts +1 -0
  6. package/dist/cli/build-helpers.test.js +132 -0
  7. package/dist/cli/build-helpers.test.js.map +1 -0
  8. package/dist/cli/build-steps.d.ts +2 -0
  9. package/dist/cli/build-steps.js +73 -0
  10. package/dist/cli/build-steps.js.map +1 -0
  11. package/dist/cli/build-steps.test.d.ts +1 -0
  12. package/dist/cli/build-steps.test.js +52 -0
  13. package/dist/cli/build-steps.test.js.map +1 -0
  14. package/dist/cli/detect-port.d.ts +2 -2
  15. package/dist/cli/detect-port.js +9 -20
  16. package/dist/cli/detect-port.js.map +1 -1
  17. package/dist/cli/header.d.ts +1 -1
  18. package/dist/cli/resolve-skybridge-config.d.ts +5 -0
  19. package/dist/cli/resolve-skybridge-config.js +18 -0
  20. package/dist/cli/resolve-skybridge-config.js.map +1 -0
  21. package/dist/cli/run-plain.d.ts +18 -0
  22. package/dist/cli/run-plain.js +89 -0
  23. package/dist/cli/run-plain.js.map +1 -0
  24. package/dist/cli/use-nodemon.d.ts +14 -0
  25. package/dist/cli/use-nodemon.js +71 -60
  26. package/dist/cli/use-nodemon.js.map +1 -1
  27. package/dist/cli/use-typescript-check.d.ts +8 -2
  28. package/dist/cli/use-typescript-check.js +71 -68
  29. package/dist/cli/use-typescript-check.js.map +1 -1
  30. package/dist/commands/build.d.ts +0 -3
  31. package/dist/commands/build.js +2 -73
  32. package/dist/commands/build.js.map +1 -1
  33. package/dist/commands/create.d.ts +9 -0
  34. package/dist/commands/create.js +30 -0
  35. package/dist/commands/create.js.map +1 -0
  36. package/dist/commands/dev.d.ts +1 -0
  37. package/dist/commands/dev.js +48 -0
  38. package/dist/commands/dev.js.map +1 -1
  39. package/dist/commands/start.js +7 -1
  40. package/dist/commands/start.js.map +1 -1
  41. package/dist/context-warnings.d.ts +5 -0
  42. package/dist/context-warnings.js +31 -0
  43. package/dist/context-warnings.js.map +1 -0
  44. package/dist/context-warnings.test.d.ts +1 -0
  45. package/dist/context-warnings.test.js +28 -0
  46. package/dist/context-warnings.test.js.map +1 -0
  47. package/dist/server/auth/discovery.d.ts +32 -0
  48. package/dist/server/auth/discovery.js +56 -0
  49. package/dist/server/auth/discovery.js.map +1 -0
  50. package/dist/server/auth/discovery.test.d.ts +1 -0
  51. package/dist/server/auth/discovery.test.js +93 -0
  52. package/dist/server/auth/discovery.test.js.map +1 -0
  53. package/dist/server/auth/index.d.ts +44 -0
  54. package/dist/server/auth/index.js +2 -0
  55. package/dist/server/auth/index.js.map +1 -0
  56. package/dist/server/auth/providers/auth0.d.ts +40 -0
  57. package/dist/server/auth/providers/auth0.js +31 -0
  58. package/dist/server/auth/providers/auth0.js.map +1 -0
  59. package/dist/server/auth/providers/auth0.test.d.ts +1 -0
  60. package/dist/server/auth/providers/auth0.test.js +50 -0
  61. package/dist/server/auth/providers/auth0.test.js.map +1 -0
  62. package/dist/server/auth/providers/authplane.d.ts +49 -0
  63. package/dist/server/auth/providers/authplane.js +57 -0
  64. package/dist/server/auth/providers/authplane.js.map +1 -0
  65. package/dist/server/auth/providers/authplane.test.d.ts +1 -0
  66. package/dist/server/auth/providers/authplane.test.js +140 -0
  67. package/dist/server/auth/providers/authplane.test.js.map +1 -0
  68. package/dist/server/auth/providers/clerk.d.ts +51 -0
  69. package/dist/server/auth/providers/clerk.js +19 -0
  70. package/dist/server/auth/providers/clerk.js.map +1 -0
  71. package/dist/server/auth/providers/clerk.test.d.ts +1 -0
  72. package/dist/server/auth/providers/clerk.test.js +30 -0
  73. package/dist/server/auth/providers/clerk.test.js.map +1 -0
  74. package/dist/server/auth/providers/custom.d.ts +40 -0
  75. package/dist/server/auth/providers/custom.js +56 -0
  76. package/dist/server/auth/providers/custom.js.map +1 -0
  77. package/dist/server/auth/providers/custom.test.d.ts +1 -0
  78. package/dist/server/auth/providers/custom.test.js +158 -0
  79. package/dist/server/auth/providers/custom.test.js.map +1 -0
  80. package/dist/server/auth/providers/descope.d.ts +44 -0
  81. package/dist/server/auth/providers/descope.js +44 -0
  82. package/dist/server/auth/providers/descope.js.map +1 -0
  83. package/dist/server/auth/providers/descope.test.d.ts +1 -0
  84. package/dist/server/auth/providers/descope.test.js +62 -0
  85. package/dist/server/auth/providers/descope.test.js.map +1 -0
  86. package/dist/server/auth/providers/shared.d.ts +2 -0
  87. package/dist/server/auth/providers/shared.js +6 -0
  88. package/dist/server/auth/providers/shared.js.map +1 -0
  89. package/dist/server/auth/providers/shared.test.d.ts +1 -0
  90. package/dist/server/auth/providers/shared.test.js +10 -0
  91. package/dist/server/auth/providers/shared.test.js.map +1 -0
  92. package/dist/server/auth/providers/stytch.d.ts +25 -0
  93. package/dist/server/auth/providers/stytch.js +16 -0
  94. package/dist/server/auth/providers/stytch.js.map +1 -0
  95. package/dist/server/auth/providers/verify-spy.d.ts +12 -0
  96. package/dist/server/auth/providers/verify-spy.js +21 -0
  97. package/dist/server/auth/providers/verify-spy.js.map +1 -0
  98. package/dist/server/auth/providers/workos.d.ts +33 -0
  99. package/dist/server/auth/providers/workos.js +15 -0
  100. package/dist/server/auth/providers/workos.js.map +1 -0
  101. package/dist/server/auth/security-schemes.d.ts +23 -0
  102. package/dist/server/auth/security-schemes.js +64 -0
  103. package/dist/server/auth/security-schemes.js.map +1 -0
  104. package/dist/server/auth/security-schemes.test.d.ts +1 -0
  105. package/dist/server/auth/security-schemes.test.js +99 -0
  106. package/dist/server/auth/security-schemes.test.js.map +1 -0
  107. package/dist/server/auth/setup.d.ts +6 -0
  108. package/dist/server/auth/setup.js +92 -0
  109. package/dist/server/auth/setup.js.map +1 -0
  110. package/dist/server/auth/setup.test.d.ts +1 -0
  111. package/dist/server/auth/setup.test.js +508 -0
  112. package/dist/server/auth/setup.test.js.map +1 -0
  113. package/dist/server/auth/verify.d.ts +46 -0
  114. package/dist/server/auth/verify.js +62 -0
  115. package/dist/server/auth/verify.js.map +1 -0
  116. package/dist/server/auth/verify.test.d.ts +1 -0
  117. package/dist/server/auth/verify.test.js +149 -0
  118. package/dist/server/auth/verify.test.js.map +1 -0
  119. package/dist/server/auth-extra.test-d.d.ts +1 -0
  120. package/dist/server/auth-extra.test-d.js +82 -0
  121. package/dist/server/auth-extra.test-d.js.map +1 -0
  122. package/dist/server/auth.d.ts +58 -0
  123. package/dist/server/auth.js +28 -0
  124. package/dist/server/auth.js.map +1 -0
  125. package/dist/server/build-manifest.test.d.ts +1 -0
  126. package/dist/server/build-manifest.test.js +27 -0
  127. package/dist/server/build-manifest.test.js.map +1 -0
  128. package/dist/server/content-helpers.d.ts +40 -0
  129. package/dist/server/content-helpers.js +33 -0
  130. package/dist/server/content-helpers.js.map +1 -1
  131. package/dist/server/express.test.js +61 -0
  132. package/dist/server/express.test.js.map +1 -1
  133. package/dist/server/file-ref.d.ts +20 -0
  134. package/dist/server/file-ref.js +19 -0
  135. package/dist/server/file-ref.js.map +1 -1
  136. package/dist/server/host.d.ts +2 -0
  137. package/dist/server/host.js +11 -0
  138. package/dist/server/host.js.map +1 -0
  139. package/dist/server/host.test.d.ts +1 -0
  140. package/dist/server/host.test.js +13 -0
  141. package/dist/server/host.test.js.map +1 -0
  142. package/dist/server/index.d.ts +14 -2
  143. package/dist/server/index.js +12 -1
  144. package/dist/server/index.js.map +1 -1
  145. package/dist/server/middleware.d.ts +47 -9
  146. package/dist/server/middleware.js +33 -0
  147. package/dist/server/middleware.js.map +1 -1
  148. package/dist/server/middleware.test.js +37 -1
  149. package/dist/server/middleware.test.js.map +1 -1
  150. package/dist/server/requestOrigin.d.ts +7 -0
  151. package/dist/server/requestOrigin.js +25 -0
  152. package/dist/server/requestOrigin.js.map +1 -0
  153. package/dist/server/server.d.ts +253 -19
  154. package/dist/server/server.js +449 -175
  155. package/dist/server/server.js.map +1 -1
  156. package/dist/server/skills-integration.test.d.ts +1 -0
  157. package/dist/server/skills-integration.test.js +79 -0
  158. package/dist/server/skills-integration.test.js.map +1 -0
  159. package/dist/server/skills.d.ts +29 -0
  160. package/dist/server/skills.js +195 -0
  161. package/dist/server/skills.js.map +1 -0
  162. package/dist/server/skills.test.d.ts +1 -0
  163. package/dist/server/skills.test.js +247 -0
  164. package/dist/server/skills.test.js.map +1 -0
  165. package/dist/server/view-name.test-d.d.ts +1 -0
  166. package/dist/server/view-name.test-d.js +8 -0
  167. package/dist/server/view-name.test-d.js.map +1 -0
  168. package/dist/server/view-resource-resolution.test.d.ts +6 -0
  169. package/dist/server/view-resource-resolution.test.js +171 -0
  170. package/dist/server/view-resource-resolution.test.js.map +1 -0
  171. package/dist/server/viewsDevServer.js +4 -1
  172. package/dist/server/viewsDevServer.js.map +1 -1
  173. package/dist/test/utils.d.ts +3 -3
  174. package/dist/test/view.test.js +125 -96
  175. package/dist/test/view.test.js.map +1 -1
  176. package/dist/web/bridges/adaptor.d.ts +51 -0
  177. package/dist/web/bridges/adaptor.js +327 -0
  178. package/dist/web/bridges/adaptor.js.map +1 -0
  179. package/dist/web/bridges/adaptor.test.d.ts +1 -0
  180. package/dist/web/bridges/adaptor.test.js +225 -0
  181. package/dist/web/bridges/adaptor.test.js.map +1 -0
  182. package/dist/web/bridges/apps-sdk/bridge.d.ts +6 -1
  183. package/dist/web/bridges/apps-sdk/bridge.js +15 -3
  184. package/dist/web/bridges/apps-sdk/bridge.js.map +1 -1
  185. package/dist/web/bridges/apps-sdk/index.d.ts +0 -1
  186. package/dist/web/bridges/apps-sdk/index.js +0 -1
  187. package/dist/web/bridges/apps-sdk/index.js.map +1 -1
  188. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.d.ts +11 -0
  189. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js +11 -0
  190. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js.map +1 -1
  191. package/dist/web/bridges/get-adaptor.d.ts +7 -0
  192. package/dist/web/bridges/get-adaptor.js +9 -7
  193. package/dist/web/bridges/get-adaptor.js.map +1 -1
  194. package/dist/web/bridges/get-adaptor.test.d.ts +1 -0
  195. package/dist/web/bridges/get-adaptor.test.js +32 -0
  196. package/dist/web/bridges/get-adaptor.test.js.map +1 -0
  197. package/dist/web/bridges/mcp-app/bridge.d.ts +15 -2
  198. package/dist/web/bridges/mcp-app/bridge.js +73 -7
  199. package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
  200. package/dist/web/bridges/mcp-app/bridge.test.d.ts +1 -0
  201. package/dist/web/bridges/mcp-app/bridge.test.js +17 -0
  202. package/dist/web/bridges/mcp-app/bridge.test.js.map +1 -0
  203. package/dist/web/bridges/mcp-app/index.d.ts +0 -1
  204. package/dist/web/bridges/mcp-app/index.js +0 -1
  205. package/dist/web/bridges/mcp-app/index.js.map +1 -1
  206. package/dist/web/bridges/mcp-app/types.d.ts +2 -0
  207. package/dist/web/bridges/mcp-app/types.js.map +1 -1
  208. package/dist/web/bridges/mcp-app/use-mcp-app-context.d.ts +12 -0
  209. package/dist/web/bridges/mcp-app/use-mcp-app-context.js +12 -0
  210. package/dist/web/bridges/mcp-app/use-mcp-app-context.js.map +1 -1
  211. package/dist/web/bridges/mcp-app/view-tools.test.d.ts +1 -0
  212. package/dist/web/bridges/mcp-app/view-tools.test.js +143 -0
  213. package/dist/web/bridges/mcp-app/view-tools.test.js.map +1 -0
  214. package/dist/web/bridges/types.d.ts +98 -1
  215. package/dist/web/bridges/types.js +14 -1
  216. package/dist/web/bridges/types.js.map +1 -1
  217. package/dist/web/bridges/types.test.d.ts +1 -0
  218. package/dist/web/bridges/types.test.js +19 -0
  219. package/dist/web/bridges/types.test.js.map +1 -0
  220. package/dist/web/bridges/use-host-context.d.ts +5 -0
  221. package/dist/web/bridges/use-host-context.js +5 -0
  222. package/dist/web/bridges/use-host-context.js.map +1 -1
  223. package/dist/web/components/modal-provider.d.ts +1 -1
  224. package/dist/web/components/modal-provider.js +4 -3
  225. package/dist/web/components/modal-provider.js.map +1 -1
  226. package/dist/web/create-store.d.ts +26 -0
  227. package/dist/web/create-store.js +26 -0
  228. package/dist/web/create-store.js.map +1 -1
  229. package/dist/web/create-store.test.js +8 -5
  230. package/dist/web/create-store.test.js.map +1 -1
  231. package/dist/web/data-llm.d.ts +34 -1
  232. package/dist/web/data-llm.js +28 -0
  233. package/dist/web/data-llm.js.map +1 -1
  234. package/dist/web/data-llm.test.js +18 -5
  235. package/dist/web/data-llm.test.js.map +1 -1
  236. package/dist/web/generate-helpers.d.ts +2 -0
  237. package/dist/web/generate-helpers.js +2 -0
  238. package/dist/web/generate-helpers.js.map +1 -1
  239. package/dist/web/generate-helpers.test-d.js +4 -2
  240. package/dist/web/generate-helpers.test-d.js.map +1 -1
  241. package/dist/web/hooks/index.d.ts +3 -0
  242. package/dist/web/hooks/index.js +3 -0
  243. package/dist/web/hooks/index.js.map +1 -1
  244. package/dist/web/hooks/test/utils.d.ts +7 -2
  245. package/dist/web/hooks/test/utils.js +14 -3
  246. package/dist/web/hooks/test/utils.js.map +1 -1
  247. package/dist/web/hooks/use-call-tool.d.ts +45 -0
  248. package/dist/web/hooks/use-call-tool.js +28 -0
  249. package/dist/web/hooks/use-call-tool.js.map +1 -1
  250. package/dist/web/hooks/use-call-tool.test.js +37 -23
  251. package/dist/web/hooks/use-call-tool.test.js.map +1 -1
  252. package/dist/web/hooks/use-display-mode.d.ts +20 -0
  253. package/dist/web/hooks/use-display-mode.js +20 -0
  254. package/dist/web/hooks/use-display-mode.js.map +1 -1
  255. package/dist/web/hooks/use-display-mode.test.js +56 -20
  256. package/dist/web/hooks/use-display-mode.test.js.map +1 -1
  257. package/dist/web/hooks/use-download.d.ts +5 -0
  258. package/dist/web/hooks/use-download.js +8 -0
  259. package/dist/web/hooks/use-download.js.map +1 -0
  260. package/dist/web/hooks/use-download.test.d.ts +1 -0
  261. package/dist/web/hooks/use-download.test.js +103 -0
  262. package/dist/web/hooks/use-download.test.js.map +1 -0
  263. package/dist/web/hooks/use-files.d.ts +32 -0
  264. package/dist/web/hooks/use-files.js +32 -0
  265. package/dist/web/hooks/use-files.js.map +1 -1
  266. package/dist/web/hooks/use-files.test.js +10 -2
  267. package/dist/web/hooks/use-files.test.js.map +1 -1
  268. package/dist/web/hooks/use-host-info.d.ts +22 -0
  269. package/dist/web/hooks/use-host-info.js +30 -0
  270. package/dist/web/hooks/use-host-info.js.map +1 -0
  271. package/dist/web/hooks/use-host-info.test.d.ts +1 -0
  272. package/dist/web/hooks/use-host-info.test.js +59 -0
  273. package/dist/web/hooks/use-host-info.test.js.map +1 -0
  274. package/dist/web/hooks/use-layout.d.ts +2 -0
  275. package/dist/web/hooks/use-layout.js +2 -0
  276. package/dist/web/hooks/use-layout.js.map +1 -1
  277. package/dist/web/hooks/use-layout.test.js +59 -26
  278. package/dist/web/hooks/use-layout.test.js.map +1 -1
  279. package/dist/web/hooks/use-open-external.d.ts +17 -0
  280. package/dist/web/hooks/use-open-external.js +16 -0
  281. package/dist/web/hooks/use-open-external.js.map +1 -1
  282. package/dist/web/hooks/use-open-external.test.js +13 -3
  283. package/dist/web/hooks/use-open-external.test.js.map +1 -1
  284. package/dist/web/hooks/use-register-view-tool.d.ts +38 -0
  285. package/dist/web/hooks/use-register-view-tool.js +50 -0
  286. package/dist/web/hooks/use-register-view-tool.js.map +1 -0
  287. package/dist/web/hooks/use-request-close.d.ts +14 -0
  288. package/dist/web/hooks/use-request-close.js +13 -0
  289. package/dist/web/hooks/use-request-close.js.map +1 -1
  290. package/dist/web/hooks/use-request-close.test.js +35 -40
  291. package/dist/web/hooks/use-request-close.test.js.map +1 -1
  292. package/dist/web/hooks/use-request-modal.d.ts +16 -1
  293. package/dist/web/hooks/use-request-modal.js +16 -1
  294. package/dist/web/hooks/use-request-modal.js.map +1 -1
  295. package/dist/web/hooks/use-request-modal.test.js +10 -0
  296. package/dist/web/hooks/use-request-modal.test.js.map +1 -1
  297. package/dist/web/hooks/use-request-size.d.ts +17 -0
  298. package/dist/web/hooks/use-request-size.js +16 -0
  299. package/dist/web/hooks/use-request-size.js.map +1 -1
  300. package/dist/web/hooks/use-request-size.test.js +35 -53
  301. package/dist/web/hooks/use-request-size.test.js.map +1 -1
  302. package/dist/web/hooks/use-send-follow-up-message.d.ts +17 -0
  303. package/dist/web/hooks/use-send-follow-up-message.js +17 -0
  304. package/dist/web/hooks/use-send-follow-up-message.js.map +1 -1
  305. package/dist/web/hooks/use-set-open-in-app-url.d.ts +17 -0
  306. package/dist/web/hooks/use-set-open-in-app-url.js +17 -0
  307. package/dist/web/hooks/use-set-open-in-app-url.js.map +1 -1
  308. package/dist/web/hooks/use-set-open-in-app-url.test.js +28 -8
  309. package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -1
  310. package/dist/web/hooks/use-tool-info.d.ts +53 -2
  311. package/dist/web/hooks/use-tool-info.js +30 -7
  312. package/dist/web/hooks/use-tool-info.js.map +1 -1
  313. package/dist/web/hooks/use-tool-info.test-d.js +11 -29
  314. package/dist/web/hooks/use-tool-info.test-d.js.map +1 -1
  315. package/dist/web/hooks/use-tool-info.test.js +42 -32
  316. package/dist/web/hooks/use-tool-info.test.js.map +1 -1
  317. package/dist/web/hooks/use-user.d.ts +2 -0
  318. package/dist/web/hooks/use-user.js +2 -0
  319. package/dist/web/hooks/use-user.js.map +1 -1
  320. package/dist/web/hooks/use-user.test.js +81 -35
  321. package/dist/web/hooks/use-user.test.js.map +1 -1
  322. package/dist/web/hooks/use-view-state.d.ts +21 -0
  323. package/dist/web/hooks/use-view-state.js +19 -9
  324. package/dist/web/hooks/use-view-state.js.map +1 -1
  325. package/dist/web/hooks/use-view-state.test.js +37 -2
  326. package/dist/web/hooks/use-view-state.test.js.map +1 -1
  327. package/dist/web/mount-view.d.ts +19 -0
  328. package/dist/web/mount-view.js +19 -0
  329. package/dist/web/mount-view.js.map +1 -1
  330. package/dist/web/plugin/plugin-build.test.d.ts +1 -0
  331. package/dist/web/plugin/plugin-build.test.js +54 -0
  332. package/dist/web/plugin/plugin-build.test.js.map +1 -0
  333. package/dist/web/plugin/plugin.d.ts +34 -0
  334. package/dist/web/plugin/plugin.js +44 -7
  335. package/dist/web/plugin/plugin.js.map +1 -1
  336. package/dist/web/plugin/plugin.test.d.ts +1 -0
  337. package/dist/web/plugin/plugin.test.js +61 -0
  338. package/dist/web/plugin/plugin.test.js.map +1 -0
  339. package/dist/web/types.d.ts +4 -0
  340. package/dist/web/types.js.map +1 -1
  341. package/package.json +23 -15
  342. package/dist/web/bridges/apps-sdk/adaptor.d.ts +0 -26
  343. package/dist/web/bridges/apps-sdk/adaptor.js +0 -108
  344. package/dist/web/bridges/apps-sdk/adaptor.js.map +0 -1
  345. package/dist/web/bridges/mcp-app/adaptor.d.ts +0 -50
  346. package/dist/web/bridges/mcp-app/adaptor.js +0 -271
  347. package/dist/web/bridges/mcp-app/adaptor.js.map +0 -1
@@ -0,0 +1,195 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
3
+ import { extname, join } from "node:path";
4
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
6
+ import { parse as parseYaml } from "yaml";
7
+ import { z } from "zod";
8
+ export const SKILLS_EXTENSION_KEY = "io.modelcontextprotocol/skills";
9
+ const SKILL_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
10
+ const SkillFrontmatterSchema = z
11
+ .object({
12
+ name: z
13
+ .string()
14
+ .min(1)
15
+ .max(64)
16
+ .regex(SKILL_NAME_RE, "must be lowercase alphanumeric words separated by single hyphens"),
17
+ description: z.string().min(1).max(1024),
18
+ })
19
+ .loose();
20
+ const sha256 = (content) => `sha256:${createHash("sha256").update(content, "utf8").digest("hex")}`;
21
+ const FRONTMATTER_RE = /^?\s*---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
22
+ function parseFrontmatter(content, source) {
23
+ const match = FRONTMATTER_RE.exec(content);
24
+ if (!match) {
25
+ throw new Error(`Skill ${source} is missing YAML frontmatter`);
26
+ }
27
+ try {
28
+ return parseYaml(match[1] ?? "");
29
+ }
30
+ catch (error) {
31
+ const detail = error instanceof Error ? error.message : String(error);
32
+ throw new Error(`Cannot parse frontmatter of ${source}: ${detail}`);
33
+ }
34
+ }
35
+ const readSkillDir = (root, rel = "") => {
36
+ const files = {};
37
+ for (const entry of readdirSync(join(root, rel), { withFileTypes: true })) {
38
+ const childRel = rel ? `${rel}/${entry.name}` : entry.name;
39
+ if (entry.isSymbolicLink()) {
40
+ continue;
41
+ }
42
+ if (entry.isDirectory()) {
43
+ Object.assign(files, readSkillDir(root, childRel));
44
+ }
45
+ else if (entry.isFile() && extname(entry.name) === ".md") {
46
+ files[childRel] = readFileSync(join(root, childRel), "utf8");
47
+ }
48
+ }
49
+ return files;
50
+ };
51
+ export function discoverSkills(dir) {
52
+ if (!existsSync(dir)) {
53
+ return [];
54
+ }
55
+ const skills = [];
56
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
57
+ if (!entry.isDirectory()) {
58
+ continue;
59
+ }
60
+ const skillRoot = join(dir, entry.name);
61
+ const files = readSkillDir(skillRoot);
62
+ const skillMd = files["SKILL.md"];
63
+ if (skillMd === undefined) {
64
+ continue;
65
+ }
66
+ const source = `${entry.name}/SKILL.md`;
67
+ const parsed = SkillFrontmatterSchema.safeParse(parseFrontmatter(skillMd, source));
68
+ if (!parsed.success) {
69
+ throw new Error(`Invalid skill frontmatter in ${source}: ${z.prettifyError(parsed.error)}`);
70
+ }
71
+ if (parsed.data.name !== entry.name) {
72
+ throw new Error(`Skill name "${parsed.data.name}" in ${source} must match its directory name "${entry.name}"`);
73
+ }
74
+ const resources = Object.entries(files)
75
+ .sort(([a], [b]) => Number(b === "SKILL.md") - Number(a === "SKILL.md") ||
76
+ a.localeCompare(b))
77
+ .map(([relPath, content]) => ({
78
+ uri: `skill://${entry.name}/${relPath}`,
79
+ digest: sha256(content),
80
+ content,
81
+ }));
82
+ skills.push({
83
+ uri: `skill://${entry.name}/SKILL.md`,
84
+ frontmatter: parsed.data,
85
+ resources,
86
+ });
87
+ }
88
+ return skills;
89
+ }
90
+ export function skillUriToRelPath(uri) {
91
+ const match = /^skill:\/\/([^/]+)(?:\/(.*))?$/.exec(uri.replace(/\/+$/, ""));
92
+ if (!match) {
93
+ throw new McpError(ErrorCode.InvalidParams, `Invalid skill uri: ${uri}`);
94
+ }
95
+ const [, name = "", relPath = ""] = match;
96
+ const segments = relPath.split("/").filter(Boolean);
97
+ if (name === "" || segments.includes("..") || segments.includes(".")) {
98
+ throw new McpError(ErrorCode.InvalidParams, `Invalid skill uri: ${uri}`);
99
+ }
100
+ return { name, relPath: segments.join("/") };
101
+ }
102
+ function listDir(skill, relPath) {
103
+ const prefix = relPath === "" ? "" : `${relPath}/`;
104
+ const base = skill.uri.slice(0, -"SKILL.md".length);
105
+ const children = new Map();
106
+ let matched = relPath === "";
107
+ for (const filePath of skill.resources.map((resource) => resource.uri.slice(base.length))) {
108
+ if (!filePath.startsWith(prefix)) {
109
+ continue;
110
+ }
111
+ matched = true;
112
+ const rest = filePath.slice(prefix.length);
113
+ const slash = rest.indexOf("/");
114
+ if (slash === -1) {
115
+ children.set(rest, "text/markdown");
116
+ }
117
+ else {
118
+ children.set(rest.slice(0, slash), "inode/directory");
119
+ }
120
+ }
121
+ if (!matched) {
122
+ return null;
123
+ }
124
+ return [...children].map(([name, mimeType]) => ({ name, mimeType }));
125
+ }
126
+ const DirectoryReadRequestSchema = z.object({
127
+ method: z.literal("resources/directory/read"),
128
+ params: z.object({ uri: z.string(), cursor: z.string().optional() }),
129
+ });
130
+ const SkillsListRequestSchema = z.object({
131
+ method: z.literal("skills/list"),
132
+ params: z.object({ cursor: z.string().optional() }).optional(),
133
+ });
134
+ const SkillsGetRequestSchema = z.object({
135
+ method: z.literal("skills/get"),
136
+ params: z.object({ uri: z.string() }),
137
+ });
138
+ const toWireEntry = (skill) => ({
139
+ uri: skill.uri,
140
+ frontmatter: skill.frontmatter,
141
+ resources: skill.resources.map(({ uri, digest }) => ({ uri, digest })),
142
+ });
143
+ export function registerSkills(server, manifest) {
144
+ const byUri = new Map(manifest.map((skill) => [skill.uri, skill]));
145
+ const serveFile = (skillUri, fileUri, href) => {
146
+ const text = byUri
147
+ .get(skillUri)
148
+ ?.resources.find((resource) => resource.uri === fileUri)?.content;
149
+ if (text === undefined) {
150
+ throw new McpError(ErrorCode.InvalidParams, `Not found: ${href}`);
151
+ }
152
+ return {
153
+ contents: [{ uri: href, text, mimeType: "text/markdown" }],
154
+ };
155
+ };
156
+ for (const skill of manifest) {
157
+ server.registerResource(skill.frontmatter.name, skill.uri, {
158
+ description: skill.frontmatter.description,
159
+ mimeType: "text/markdown",
160
+ }, (readUri) => serveFile(skill.uri, skill.uri, readUri.href));
161
+ }
162
+ server.registerResource("skill-files", new ResourceTemplate("skill://{skillName}/{+filePath}", {
163
+ list: undefined,
164
+ }), {}, (readUri) => {
165
+ const { name, relPath } = skillUriToRelPath(readUri.href);
166
+ return serveFile(`skill://${name}/SKILL.md`, `skill://${name}/${relPath}`, readUri.href);
167
+ });
168
+ server.server.setRequestHandler(SkillsListRequestSchema, () => ({
169
+ skills: manifest.map(toWireEntry),
170
+ }));
171
+ server.server.setRequestHandler(SkillsGetRequestSchema, ({ params }) => {
172
+ const skill = byUri.get(params.uri);
173
+ if (!skill) {
174
+ throw new McpError(ErrorCode.InvalidParams, `Unknown skill: ${params.uri}`);
175
+ }
176
+ return { skill: toWireEntry(skill) };
177
+ });
178
+ server.server.setRequestHandler(DirectoryReadRequestSchema, ({ params }) => {
179
+ const { name, relPath } = skillUriToRelPath(params.uri);
180
+ const skill = byUri.get(`skill://${name}/SKILL.md`);
181
+ const entries = skill ? listDir(skill, relPath) : null;
182
+ if (!entries) {
183
+ throw new McpError(ErrorCode.InvalidParams, `Not a directory: ${params.uri}`);
184
+ }
185
+ const base = params.uri.replace(/\/+$/, "");
186
+ return {
187
+ resources: entries.map((entry) => ({
188
+ uri: `${base}/${entry.name}`,
189
+ name: entry.name,
190
+ mimeType: entry.mimeType,
191
+ })),
192
+ };
193
+ });
194
+ }
195
+ //# sourceMappingURL=skills.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skills.js","sourceRoot":"","sources":["../../src/server/skills.ts"],"names":[],"mappings":"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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\";\n\nconst SKILL_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;\n\nconst SkillFrontmatterSchema = z\n .object({\n name: z\n .string()\n .min(1)\n .max(64)\n .regex(\n SKILL_NAME_RE,\n \"must be lowercase alphanumeric words separated by single hyphens\",\n ),\n description: z.string().min(1).max(1024),\n })\n .loose();\n\nexport interface SkillResource {\n uri: string;\n digest: string;\n content: string;\n}\n\nexport interface Skill {\n uri: string;\n frontmatter: { name: string; description: string } & Record<string, unknown>;\n resources: SkillResource[];\n}\n\nexport type SkillsManifest = Skill[];\n\nconst sha256 = (content: string): string =>\n `sha256:${createHash(\"sha256\").update(content, \"utf8\").digest(\"hex\")}`;\n\nconst FRONTMATTER_RE = /^?\\s*---[ \\t]*\\r?\\n([\\s\\S]*?)\\r?\\n---[ \\t]*(?:\\r?\\n|$)/;\n\nfunction parseFrontmatter(content: string, source: string): unknown {\n const match = FRONTMATTER_RE.exec(content);\n if (!match) {\n throw new Error(`Skill ${source} is missing YAML frontmatter`);\n }\n try {\n return parseYaml(match[1] ?? \"\");\n } catch (error) {\n const detail = error instanceof Error ? error.message : String(error);\n throw new Error(`Cannot parse frontmatter of ${source}: ${detail}`);\n }\n}\n\nconst readSkillDir = (root: string, rel = \"\"): Record<string, string> => {\n const files: Record<string, string> = {};\n for (const entry of readdirSync(join(root, rel), { withFileTypes: true })) {\n const childRel = rel ? `${rel}/${entry.name}` : entry.name;\n if (entry.isSymbolicLink()) {\n continue;\n }\n if (entry.isDirectory()) {\n Object.assign(files, readSkillDir(root, childRel));\n } else if (entry.isFile() && extname(entry.name) === \".md\") {\n files[childRel] = readFileSync(join(root, childRel), \"utf8\");\n }\n }\n return files;\n};\n\nexport function discoverSkills(dir: string): SkillsManifest {\n if (!existsSync(dir)) {\n return [];\n }\n\n const skills: SkillsManifest = [];\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (!entry.isDirectory()) {\n continue;\n }\n\n const skillRoot = join(dir, entry.name);\n const files = readSkillDir(skillRoot);\n const skillMd = files[\"SKILL.md\"];\n if (skillMd === undefined) {\n continue;\n }\n\n const source = `${entry.name}/SKILL.md`;\n\n const parsed = SkillFrontmatterSchema.safeParse(\n parseFrontmatter(skillMd, source),\n );\n if (!parsed.success) {\n throw new Error(\n `Invalid skill frontmatter in ${source}: ${z.prettifyError(parsed.error)}`,\n );\n }\n if (parsed.data.name !== entry.name) {\n throw new Error(\n `Skill name \"${parsed.data.name}\" in ${source} must match its directory name \"${entry.name}\"`,\n );\n }\n\n const resources = Object.entries(files)\n .sort(\n ([a], [b]) =>\n Number(b === \"SKILL.md\") - Number(a === \"SKILL.md\") ||\n a.localeCompare(b),\n )\n .map(([relPath, content]) => ({\n uri: `skill://${entry.name}/${relPath}`,\n digest: sha256(content),\n content,\n }));\n\n skills.push({\n uri: `skill://${entry.name}/SKILL.md`,\n frontmatter: parsed.data,\n resources,\n });\n }\n return skills;\n}\n\nexport function skillUriToRelPath(uri: string): {\n name: string;\n relPath: string;\n} {\n const match = /^skill:\\/\\/([^/]+)(?:\\/(.*))?$/.exec(uri.replace(/\\/+$/, \"\"));\n if (!match) {\n throw new McpError(ErrorCode.InvalidParams, `Invalid skill uri: ${uri}`);\n }\n const [, name = \"\", relPath = \"\"] = match;\n const segments = relPath.split(\"/\").filter(Boolean);\n if (name === \"\" || segments.includes(\"..\") || segments.includes(\".\")) {\n throw new McpError(ErrorCode.InvalidParams, `Invalid skill uri: ${uri}`);\n }\n return { name, relPath: segments.join(\"/\") };\n}\n\nfunction listDir(\n skill: Skill,\n relPath: string,\n): { name: string; mimeType: string }[] | null {\n const prefix = relPath === \"\" ? \"\" : `${relPath}/`;\n const base = skill.uri.slice(0, -\"SKILL.md\".length);\n const children = new Map<string, string>();\n let matched = relPath === \"\";\n for (const filePath of skill.resources.map((resource) =>\n resource.uri.slice(base.length),\n )) {\n if (!filePath.startsWith(prefix)) {\n continue;\n }\n matched = true;\n const rest = filePath.slice(prefix.length);\n const slash = rest.indexOf(\"/\");\n if (slash === -1) {\n children.set(rest, \"text/markdown\");\n } else {\n children.set(rest.slice(0, slash), \"inode/directory\");\n }\n }\n if (!matched) {\n return null;\n }\n return [...children].map(([name, mimeType]) => ({ name, mimeType }));\n}\n\ninterface SkillRegistrar {\n registerResource(\n name: string,\n uri: string,\n config: ResourceMetadata,\n readCallback: ReadResourceCallback,\n ): unknown;\n registerResource(\n name: string,\n template: ResourceTemplate,\n config: ResourceMetadata,\n readCallback: ReadResourceTemplateCallback,\n ): unknown;\n readonly server: SdkMcpServer[\"server\"];\n}\n\nconst DirectoryReadRequestSchema = z.object({\n method: z.literal(\"resources/directory/read\"),\n params: z.object({ uri: z.string(), cursor: z.string().optional() }),\n});\n\nconst SkillsListRequestSchema = z.object({\n method: z.literal(\"skills/list\"),\n params: z.object({ cursor: z.string().optional() }).optional(),\n});\n\nconst SkillsGetRequestSchema = z.object({\n method: z.literal(\"skills/get\"),\n params: z.object({ uri: z.string() }),\n});\n\nconst toWireEntry = (skill: Skill) => ({\n uri: skill.uri,\n frontmatter: skill.frontmatter,\n resources: skill.resources.map(({ uri, digest }) => ({ uri, digest })),\n});\n\nexport function registerSkills(\n server: SkillRegistrar,\n manifest: SkillsManifest,\n): void {\n const byUri = new Map(manifest.map((skill) => [skill.uri, skill]));\n const serveFile = (skillUri: string, fileUri: string, href: string) => {\n const text = byUri\n .get(skillUri)\n ?.resources.find((resource) => resource.uri === fileUri)?.content;\n if (text === undefined) {\n throw new McpError(ErrorCode.InvalidParams, `Not found: ${href}`);\n }\n return {\n contents: [{ uri: href, text, mimeType: \"text/markdown\" }],\n };\n };\n\n for (const skill of manifest) {\n server.registerResource(\n skill.frontmatter.name,\n skill.uri,\n {\n description: skill.frontmatter.description,\n mimeType: \"text/markdown\",\n },\n (readUri) => serveFile(skill.uri, skill.uri, readUri.href),\n );\n }\n\n server.registerResource(\n \"skill-files\",\n new ResourceTemplate(\"skill://{skillName}/{+filePath}\", {\n list: undefined,\n }),\n {},\n (readUri) => {\n const { name, relPath } = skillUriToRelPath(readUri.href);\n return serveFile(\n `skill://${name}/SKILL.md`,\n `skill://${name}/${relPath}`,\n readUri.href,\n );\n },\n );\n\n server.server.setRequestHandler(SkillsListRequestSchema, () => ({\n skills: manifest.map(toWireEntry),\n }));\n\n server.server.setRequestHandler(SkillsGetRequestSchema, ({ params }) => {\n const skill = byUri.get(params.uri);\n if (!skill) {\n throw new McpError(\n ErrorCode.InvalidParams,\n `Unknown skill: ${params.uri}`,\n );\n }\n return { skill: toWireEntry(skill) };\n });\n\n server.server.setRequestHandler(DirectoryReadRequestSchema, ({ params }) => {\n const { name, relPath } = skillUriToRelPath(params.uri);\n const skill = byUri.get(`skill://${name}/SKILL.md`);\n const entries = skill ? listDir(skill, relPath) : null;\n if (!entries) {\n throw new McpError(\n ErrorCode.InvalidParams,\n `Not a directory: ${params.uri}`,\n );\n }\n const base = params.uri.replace(/\\/+$/, \"\");\n return {\n resources: entries.map((entry) => ({\n uri: `${base}/${entry.name}`,\n name: entry.name,\n mimeType: entry.mimeType,\n })),\n };\n });\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,247 @@
1
+ // @vitest-environment node
2
+ import { createHash } from "node:crypto";
3
+ import { mkdirSync, mkdtempSync, symlinkSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { describe, expect, it, vi } from "vitest";
8
+ import { discoverSkills, registerSkills, skillUriToRelPath, } from "./skills.js";
9
+ function mkSkillDir(files) {
10
+ const root = mkdtempSync(join(tmpdir(), "skybridge-skills-"));
11
+ for (const [rel, content] of Object.entries(files)) {
12
+ const full = join(root, rel);
13
+ mkdirSync(join(full, ".."), { recursive: true });
14
+ writeFileSync(full, content);
15
+ }
16
+ return root;
17
+ }
18
+ const FM = (name, description) => `---\nname: ${name}\ndescription: ${description}\n---\n`;
19
+ const digestOf = (content) => `sha256:${createHash("sha256").update(content, "utf8").digest("hex")}`;
20
+ describe("discoverSkills", () => {
21
+ it("discovers a skill with frontmatter and per-file resources", () => {
22
+ const skillMd = `${FM("git-workflow", "Team git conventions")}Body`;
23
+ const dir = mkSkillDir({
24
+ "git-workflow/SKILL.md": skillMd,
25
+ "git-workflow/references/GUIDE.md": "# Guide",
26
+ });
27
+ const [skill, ...rest] = discoverSkills(dir);
28
+ expect(rest).toHaveLength(0);
29
+ expect(skill?.uri).toBe("skill://git-workflow/SKILL.md");
30
+ expect(skill?.frontmatter).toMatchObject({
31
+ name: "git-workflow",
32
+ description: "Team git conventions",
33
+ });
34
+ expect(skill?.resources).toEqual([
35
+ {
36
+ uri: "skill://git-workflow/SKILL.md",
37
+ digest: digestOf(skillMd),
38
+ content: skillMd,
39
+ },
40
+ {
41
+ uri: "skill://git-workflow/references/GUIDE.md",
42
+ digest: digestOf("# Guide"),
43
+ content: "# Guide",
44
+ },
45
+ ]);
46
+ });
47
+ it("returns [] for a missing directory and skips dirs without SKILL.md", () => {
48
+ expect(discoverSkills(join(tmpdir(), "does-not-exist-xyz"))).toEqual([]);
49
+ const dir = mkSkillDir({ "not-a-skill/README.md": "hi" });
50
+ expect(discoverSkills(dir)).toEqual([]);
51
+ });
52
+ it("parses a nested metadata map in frontmatter", () => {
53
+ const dir = mkSkillDir({
54
+ "s/SKILL.md": `---\nname: s\ndescription: d\nmetadata:\n version: "1.2"\n team: core\n---\n`,
55
+ });
56
+ expect(discoverSkills(dir)[0]?.frontmatter.metadata).toEqual({
57
+ version: "1.2",
58
+ team: "core",
59
+ });
60
+ });
61
+ it("throws on missing frontmatter", () => {
62
+ const dir = mkSkillDir({ "s/SKILL.md": "no frontmatter here" });
63
+ expect(() => discoverSkills(dir)).toThrow(/missing YAML frontmatter/);
64
+ });
65
+ it("throws when required fields are absent", () => {
66
+ const dir = mkSkillDir({ "s/SKILL.md": "---\nname: s\n---\n" });
67
+ expect(() => discoverSkills(dir)).toThrow(/Invalid skill frontmatter/);
68
+ });
69
+ it("throws when the frontmatter name does not match the directory", () => {
70
+ const dir = mkSkillDir({ "s/SKILL.md": FM("other", "d") });
71
+ expect(() => discoverSkills(dir)).toThrow(/must match its directory name/);
72
+ });
73
+ it("preserves arbitrary YAML frontmatter (arrays, nesting) verbatim", () => {
74
+ const dir = mkSkillDir({
75
+ "s/SKILL.md": "---\nname: s\ndescription: d\nallowed-tools:\n - get-order\n - refund\n---\n",
76
+ });
77
+ expect(discoverSkills(dir)[0]?.frontmatter["allowed-tools"]).toEqual([
78
+ "get-order",
79
+ "refund",
80
+ ]);
81
+ });
82
+ it("throws on malformed YAML frontmatter", () => {
83
+ const dir = mkSkillDir({
84
+ "s/SKILL.md": "---\nname: 's\ndescription: [unclosed\n---\n",
85
+ });
86
+ expect(() => discoverSkills(dir)).toThrow(/Cannot parse frontmatter/);
87
+ });
88
+ });
89
+ describe("skillUriToRelPath", () => {
90
+ it("splits name and relative path", () => {
91
+ expect(skillUriToRelPath("skill://refunds/SKILL.md")).toEqual({
92
+ name: "refunds",
93
+ relPath: "SKILL.md",
94
+ });
95
+ expect(skillUriToRelPath("skill://refunds")).toEqual({
96
+ name: "refunds",
97
+ relPath: "",
98
+ });
99
+ });
100
+ it("rejects path traversal and malformed URIs", () => {
101
+ expect(() => skillUriToRelPath("skill://s/../../etc/passwd")).toThrow();
102
+ expect(() => skillUriToRelPath("skill://s/./x")).toThrow();
103
+ expect(() => skillUriToRelPath("https://example.com")).toThrow();
104
+ });
105
+ });
106
+ describe("registerSkills", () => {
107
+ const manifest = [
108
+ {
109
+ uri: "skill://refunds/SKILL.md",
110
+ frontmatter: { name: "refunds", description: "Process refunds" },
111
+ resources: [
112
+ {
113
+ uri: "skill://refunds/SKILL.md",
114
+ digest: digestOf("# Refunds"),
115
+ content: "# Refunds",
116
+ },
117
+ {
118
+ uri: "skill://refunds/templates/email.md",
119
+ digest: digestOf("Hi"),
120
+ content: "Hi",
121
+ },
122
+ ],
123
+ },
124
+ ];
125
+ function fakeRegistrar() {
126
+ const resources = new Map();
127
+ const handlers = new Map();
128
+ const server = {
129
+ registerResource: vi.fn((name, uri, _cfg, cb) => {
130
+ resources.set(name, {
131
+ uri,
132
+ cb: cb,
133
+ });
134
+ }),
135
+ server: {
136
+ setRequestHandler: vi.fn((schema, handler) => {
137
+ const method = schema
138
+ .shape.method.value;
139
+ handlers.set(method, handler);
140
+ }),
141
+ },
142
+ };
143
+ return { server, resources, handlers };
144
+ }
145
+ it("serves supporting files through the template resource", () => {
146
+ const { server, resources } = fakeRegistrar();
147
+ // biome-ignore lint/suspicious/noExplicitAny: structural test double
148
+ registerSkills(server, manifest);
149
+ const tpl = resources.get("skill-files");
150
+ expect(tpl?.uri).toBeInstanceOf(ResourceTemplate);
151
+ const url = new URL("skill://refunds/templates/email.md");
152
+ const result = tpl?.cb(url, {}, {});
153
+ expect(result.contents[0]?.text).toBe("Hi");
154
+ });
155
+ it("lists a subdirectory through the directory-read handler", () => {
156
+ const on = fakeRegistrar();
157
+ // biome-ignore lint/suspicious/noExplicitAny: structural test double
158
+ registerSkills(on.server, manifest);
159
+ const result = on.handlers.get("resources/directory/read")?.({
160
+ params: { uri: "skill://refunds/templates" },
161
+ });
162
+ expect(result.resources).toEqual([
163
+ {
164
+ uri: "skill://refunds/templates/email.md",
165
+ name: "email.md",
166
+ mimeType: "text/markdown",
167
+ },
168
+ ]);
169
+ });
170
+ it("lists a skill root and marks subdirectories as inode/directory", () => {
171
+ const { server, handlers } = fakeRegistrar();
172
+ // biome-ignore lint/suspicious/noExplicitAny: structural test double
173
+ registerSkills(server, manifest);
174
+ const result = handlers.get("resources/directory/read")?.({
175
+ params: { uri: "skill://refunds" },
176
+ });
177
+ expect(result.resources).toContainEqual({
178
+ uri: "skill://refunds/templates",
179
+ name: "templates",
180
+ mimeType: "inode/directory",
181
+ });
182
+ expect(result.resources).toContainEqual({
183
+ uri: "skill://refunds/SKILL.md",
184
+ name: "SKILL.md",
185
+ mimeType: "text/markdown",
186
+ });
187
+ });
188
+ it("lists entries with complete per-file resources via skills/list", () => {
189
+ const { server, handlers } = fakeRegistrar();
190
+ // biome-ignore lint/suspicious/noExplicitAny: structural test double
191
+ registerSkills(server, manifest);
192
+ const result = handlers.get("skills/list")?.({ params: {} });
193
+ expect(result).toEqual({
194
+ skills: [
195
+ {
196
+ uri: "skill://refunds/SKILL.md",
197
+ frontmatter: { name: "refunds", description: "Process refunds" },
198
+ resources: [
199
+ { uri: "skill://refunds/SKILL.md", digest: digestOf("# Refunds") },
200
+ {
201
+ uri: "skill://refunds/templates/email.md",
202
+ digest: digestOf("Hi"),
203
+ },
204
+ ],
205
+ },
206
+ ],
207
+ });
208
+ });
209
+ it("returns a single entry via skills/get and rejects non-skill URIs", () => {
210
+ const { server, handlers } = fakeRegistrar();
211
+ // biome-ignore lint/suspicious/noExplicitAny: structural test double
212
+ registerSkills(server, manifest);
213
+ const get = handlers.get("skills/get");
214
+ const result = get?.({
215
+ params: { uri: "skill://refunds/SKILL.md" },
216
+ });
217
+ expect(result.skill.uri).toBe("skill://refunds/SKILL.md");
218
+ expect(result.skill.resources).toHaveLength(2);
219
+ expect(() => get?.({ params: { uri: "skill://unknown/SKILL.md" } })).toThrow(/Unknown skill/);
220
+ expect(() => get?.({ params: { uri: "skill://refunds/templates/email.md" } })).toThrow(/Unknown skill/);
221
+ });
222
+ });
223
+ describe("discoverSkills symlink safety", () => {
224
+ it("excludes symlinked files and directories from the manifest", () => {
225
+ const outside = mkdtempSync(join(tmpdir(), "skybridge-outside-"));
226
+ writeFileSync(join(outside, "secret.md"), "TOP SECRET");
227
+ const dir = mkSkillDir({
228
+ "demo/SKILL.md": FM("demo", "d"),
229
+ "demo/real.md": "ok",
230
+ });
231
+ symlinkSync(join(outside, "secret.md"), join(dir, "demo", "leak.md"));
232
+ symlinkSync(outside, join(dir, "demo", "escape"));
233
+ const uris = discoverSkills(dir)[0]?.resources.map((r) => r.uri) ?? [];
234
+ expect(uris).toEqual(["skill://demo/SKILL.md", "skill://demo/real.md"]);
235
+ });
236
+ it("ignores non-markdown supporting files", () => {
237
+ const dir = mkSkillDir({
238
+ "demo/SKILL.md": FM("demo", "d"),
239
+ "demo/notes.md": "kept",
240
+ "demo/data.json": "{}",
241
+ "demo/scripts/run.py": "print(1)",
242
+ });
243
+ const uris = discoverSkills(dir)[0]?.resources.map((r) => r.uri) ?? [];
244
+ expect(uris).toEqual(["skill://demo/SKILL.md", "skill://demo/notes.md"]);
245
+ });
246
+ });
247
+ //# sourceMappingURL=skills.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skills.test.js","sourceRoot":"","sources":["../../src/server/skills.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\nimport { createHash } from \"node:crypto\";\nimport { mkdirSync, mkdtempSync, symlinkSync, writeFileSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { ResourceTemplate } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { describe, expect, it, vi } from \"vitest\";\nimport {\n discoverSkills,\n registerSkills,\n type Skill,\n skillUriToRelPath,\n} from \"./skills.js\";\n\nfunction mkSkillDir(files: Record<string, string>): string {\n const root = mkdtempSync(join(tmpdir(), \"skybridge-skills-\"));\n for (const [rel, content] of Object.entries(files)) {\n const full = join(root, rel);\n mkdirSync(join(full, \"..\"), { recursive: true });\n writeFileSync(full, content);\n }\n return root;\n}\n\nconst FM = (name: string, description: string) =>\n `---\\nname: ${name}\\ndescription: ${description}\\n---\\n`;\n\nconst digestOf = (content: string) =>\n `sha256:${createHash(\"sha256\").update(content, \"utf8\").digest(\"hex\")}`;\n\ndescribe(\"discoverSkills\", () => {\n it(\"discovers a skill with frontmatter and per-file resources\", () => {\n const skillMd = `${FM(\"git-workflow\", \"Team git conventions\")}Body`;\n const dir = mkSkillDir({\n \"git-workflow/SKILL.md\": skillMd,\n \"git-workflow/references/GUIDE.md\": \"# Guide\",\n });\n const [skill, ...rest] = discoverSkills(dir);\n expect(rest).toHaveLength(0);\n expect(skill?.uri).toBe(\"skill://git-workflow/SKILL.md\");\n expect(skill?.frontmatter).toMatchObject({\n name: \"git-workflow\",\n description: \"Team git conventions\",\n });\n expect(skill?.resources).toEqual([\n {\n uri: \"skill://git-workflow/SKILL.md\",\n digest: digestOf(skillMd),\n content: skillMd,\n },\n {\n uri: \"skill://git-workflow/references/GUIDE.md\",\n digest: digestOf(\"# Guide\"),\n content: \"# Guide\",\n },\n ]);\n });\n\n it(\"returns [] for a missing directory and skips dirs without SKILL.md\", () => {\n expect(discoverSkills(join(tmpdir(), \"does-not-exist-xyz\"))).toEqual([]);\n const dir = mkSkillDir({ \"not-a-skill/README.md\": \"hi\" });\n expect(discoverSkills(dir)).toEqual([]);\n });\n\n it(\"parses a nested metadata map in frontmatter\", () => {\n const dir = mkSkillDir({\n \"s/SKILL.md\": `---\\nname: s\\ndescription: d\\nmetadata:\\n version: \"1.2\"\\n team: core\\n---\\n`,\n });\n expect(discoverSkills(dir)[0]?.frontmatter.metadata).toEqual({\n version: \"1.2\",\n team: \"core\",\n });\n });\n\n it(\"throws on missing frontmatter\", () => {\n const dir = mkSkillDir({ \"s/SKILL.md\": \"no frontmatter here\" });\n expect(() => discoverSkills(dir)).toThrow(/missing YAML frontmatter/);\n });\n\n it(\"throws when required fields are absent\", () => {\n const dir = mkSkillDir({ \"s/SKILL.md\": \"---\\nname: s\\n---\\n\" });\n expect(() => discoverSkills(dir)).toThrow(/Invalid skill frontmatter/);\n });\n\n it(\"throws when the frontmatter name does not match the directory\", () => {\n const dir = mkSkillDir({ \"s/SKILL.md\": FM(\"other\", \"d\") });\n expect(() => discoverSkills(dir)).toThrow(/must match its directory name/);\n });\n\n it(\"preserves arbitrary YAML frontmatter (arrays, nesting) verbatim\", () => {\n const dir = mkSkillDir({\n \"s/SKILL.md\":\n \"---\\nname: s\\ndescription: d\\nallowed-tools:\\n - get-order\\n - refund\\n---\\n\",\n });\n expect(discoverSkills(dir)[0]?.frontmatter[\"allowed-tools\"]).toEqual([\n \"get-order\",\n \"refund\",\n ]);\n });\n\n it(\"throws on malformed YAML frontmatter\", () => {\n const dir = mkSkillDir({\n \"s/SKILL.md\": \"---\\nname: 's\\ndescription: [unclosed\\n---\\n\",\n });\n expect(() => discoverSkills(dir)).toThrow(/Cannot parse frontmatter/);\n });\n});\n\ndescribe(\"skillUriToRelPath\", () => {\n it(\"splits name and relative path\", () => {\n expect(skillUriToRelPath(\"skill://refunds/SKILL.md\")).toEqual({\n name: \"refunds\",\n relPath: \"SKILL.md\",\n });\n expect(skillUriToRelPath(\"skill://refunds\")).toEqual({\n name: \"refunds\",\n relPath: \"\",\n });\n });\n\n it(\"rejects path traversal and malformed URIs\", () => {\n expect(() => skillUriToRelPath(\"skill://s/../../etc/passwd\")).toThrow();\n expect(() => skillUriToRelPath(\"skill://s/./x\")).toThrow();\n expect(() => skillUriToRelPath(\"https://example.com\")).toThrow();\n });\n});\n\ndescribe(\"registerSkills\", () => {\n const manifest: Skill[] = [\n {\n uri: \"skill://refunds/SKILL.md\",\n frontmatter: { name: \"refunds\", description: \"Process refunds\" },\n resources: [\n {\n uri: \"skill://refunds/SKILL.md\",\n digest: digestOf(\"# Refunds\"),\n content: \"# Refunds\",\n },\n {\n uri: \"skill://refunds/templates/email.md\",\n digest: digestOf(\"Hi\"),\n content: \"Hi\",\n },\n ],\n },\n ];\n\n function fakeRegistrar() {\n const resources = new Map<\n string,\n { uri: unknown; cb: (...args: unknown[]) => unknown }\n >();\n const handlers = new Map<string, (req: unknown) => unknown>();\n const server = {\n registerResource: vi.fn(\n (name: string, uri: unknown, _cfg: unknown, cb: unknown) => {\n resources.set(name, {\n uri,\n cb: cb as (...args: unknown[]) => unknown,\n });\n },\n ),\n server: {\n setRequestHandler: vi.fn((schema: unknown, handler: unknown) => {\n const method = (schema as { shape: { method: { value: string } } })\n .shape.method.value;\n handlers.set(method, handler as (req: unknown) => unknown);\n }),\n },\n };\n return { server, resources, handlers };\n }\n\n it(\"serves supporting files through the template resource\", () => {\n const { server, resources } = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(server as any, manifest);\n const tpl = resources.get(\"skill-files\");\n expect(tpl?.uri).toBeInstanceOf(ResourceTemplate);\n const url = new URL(\"skill://refunds/templates/email.md\");\n const result = tpl?.cb(url, {}, {}) as { contents: { text: string }[] };\n expect(result.contents[0]?.text).toBe(\"Hi\");\n });\n\n it(\"lists a subdirectory through the directory-read handler\", () => {\n const on = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(on.server as any, manifest);\n const result = on.handlers.get(\"resources/directory/read\")?.({\n params: { uri: \"skill://refunds/templates\" },\n }) as { resources: { uri: string; name: string; mimeType: string }[] };\n expect(result.resources).toEqual([\n {\n uri: \"skill://refunds/templates/email.md\",\n name: \"email.md\",\n mimeType: \"text/markdown\",\n },\n ]);\n });\n\n it(\"lists a skill root and marks subdirectories as inode/directory\", () => {\n const { server, handlers } = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(server as any, manifest);\n const result = handlers.get(\"resources/directory/read\")?.({\n params: { uri: \"skill://refunds\" },\n }) as { resources: { name: string; mimeType: string }[] };\n expect(result.resources).toContainEqual({\n uri: \"skill://refunds/templates\",\n name: \"templates\",\n mimeType: \"inode/directory\",\n });\n expect(result.resources).toContainEqual({\n uri: \"skill://refunds/SKILL.md\",\n name: \"SKILL.md\",\n mimeType: \"text/markdown\",\n });\n });\n\n it(\"lists entries with complete per-file resources via skills/list\", () => {\n const { server, handlers } = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(server as any, manifest);\n const result = handlers.get(\"skills/list\")?.({ params: {} });\n expect(result).toEqual({\n skills: [\n {\n uri: \"skill://refunds/SKILL.md\",\n frontmatter: { name: \"refunds\", description: \"Process refunds\" },\n resources: [\n { uri: \"skill://refunds/SKILL.md\", digest: digestOf(\"# Refunds\") },\n {\n uri: \"skill://refunds/templates/email.md\",\n digest: digestOf(\"Hi\"),\n },\n ],\n },\n ],\n });\n });\n\n it(\"returns a single entry via skills/get and rejects non-skill URIs\", () => {\n const { server, handlers } = fakeRegistrar();\n // biome-ignore lint/suspicious/noExplicitAny: structural test double\n registerSkills(server as any, manifest);\n const get = handlers.get(\"skills/get\");\n const result = get?.({\n params: { uri: \"skill://refunds/SKILL.md\" },\n }) as { skill: { uri: string; resources: unknown[] } };\n expect(result.skill.uri).toBe(\"skill://refunds/SKILL.md\");\n expect(result.skill.resources).toHaveLength(2);\n expect(() =>\n get?.({ params: { uri: \"skill://unknown/SKILL.md\" } }),\n ).toThrow(/Unknown skill/);\n expect(() =>\n get?.({ params: { uri: \"skill://refunds/templates/email.md\" } }),\n ).toThrow(/Unknown skill/);\n });\n});\n\ndescribe(\"discoverSkills symlink safety\", () => {\n it(\"excludes symlinked files and directories from the manifest\", () => {\n const outside = mkdtempSync(join(tmpdir(), \"skybridge-outside-\"));\n writeFileSync(join(outside, \"secret.md\"), \"TOP SECRET\");\n const dir = mkSkillDir({\n \"demo/SKILL.md\": FM(\"demo\", \"d\"),\n \"demo/real.md\": \"ok\",\n });\n symlinkSync(join(outside, \"secret.md\"), join(dir, \"demo\", \"leak.md\"));\n symlinkSync(outside, join(dir, \"demo\", \"escape\"));\n\n const uris = discoverSkills(dir)[0]?.resources.map((r) => r.uri) ?? [];\n expect(uris).toEqual([\"skill://demo/SKILL.md\", \"skill://demo/real.md\"]);\n });\n\n it(\"ignores non-markdown supporting files\", () => {\n const dir = mkSkillDir({\n \"demo/SKILL.md\": FM(\"demo\", \"d\"),\n \"demo/notes.md\": \"kept\",\n \"demo/data.json\": \"{}\",\n \"demo/scripts/run.py\": \"print(1)\",\n });\n const uris = discoverSkills(dir)[0]?.resources.map((r) => r.uri) ?? [];\n expect(uris).toEqual([\"skill://demo/SKILL.md\", \"skill://demo/notes.md\"]);\n });\n});\n"]}
@@ -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"]}
@@ -0,0 +1,6 @@
1
+ declare module "./server.js" {
2
+ interface ViewNameRegistry {
3
+ widget: true;
4
+ }
5
+ }
6
+ export {};