skybridge 0.0.0-dev.f40327b → 0.0.0-dev.f40afe0

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 (214) hide show
  1. package/README.md +1 -0
  2. package/dist/cli/build-helpers.d.ts +10 -0
  3. package/dist/cli/build-helpers.js +93 -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 +89 -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 +68 -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-views-dir.d.ts +1 -0
  19. package/dist/cli/resolve-views-dir.js +17 -0
  20. package/dist/cli/resolve-views-dir.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/start-tunnel-view-build.d.ts +1 -0
  25. package/dist/cli/start-tunnel-view-build.js +16 -0
  26. package/dist/cli/start-tunnel-view-build.js.map +1 -0
  27. package/dist/cli/use-execute-steps.d.ts +1 -1
  28. package/dist/cli/use-nodemon.d.ts +14 -0
  29. package/dist/cli/use-nodemon.js +71 -60
  30. package/dist/cli/use-nodemon.js.map +1 -1
  31. package/dist/cli/use-typescript-check.d.ts +8 -2
  32. package/dist/cli/use-typescript-check.js +70 -67
  33. package/dist/cli/use-typescript-check.js.map +1 -1
  34. package/dist/commands/build.d.ts +0 -3
  35. package/dist/commands/build.js +2 -73
  36. package/dist/commands/build.js.map +1 -1
  37. package/dist/commands/dev.d.ts +1 -0
  38. package/dist/commands/dev.js +57 -0
  39. package/dist/commands/dev.js.map +1 -1
  40. package/dist/commands/start.js +7 -1
  41. package/dist/commands/start.js.map +1 -1
  42. package/dist/server/auth/discovery.d.ts +32 -0
  43. package/dist/server/auth/discovery.js +56 -0
  44. package/dist/server/auth/discovery.js.map +1 -0
  45. package/dist/server/auth/discovery.test.d.ts +1 -0
  46. package/dist/server/auth/discovery.test.js +93 -0
  47. package/dist/server/auth/discovery.test.js.map +1 -0
  48. package/dist/server/auth/index.d.ts +18 -0
  49. package/dist/server/auth/index.js +2 -0
  50. package/dist/server/auth/index.js.map +1 -0
  51. package/dist/server/auth/providers/auth0.d.ts +18 -0
  52. package/dist/server/auth/providers/auth0.js +31 -0
  53. package/dist/server/auth/providers/auth0.js.map +1 -0
  54. package/dist/server/auth/providers/auth0.test.d.ts +1 -0
  55. package/dist/server/auth/providers/auth0.test.js +48 -0
  56. package/dist/server/auth/providers/auth0.test.js.map +1 -0
  57. package/dist/server/auth/providers/clerk.d.ts +14 -0
  58. package/dist/server/auth/providers/clerk.js +16 -0
  59. package/dist/server/auth/providers/clerk.js.map +1 -0
  60. package/dist/server/auth/providers/clerk.test.d.ts +1 -0
  61. package/dist/server/auth/providers/clerk.test.js +28 -0
  62. package/dist/server/auth/providers/clerk.test.js.map +1 -0
  63. package/dist/server/auth/providers/custom.d.ts +24 -0
  64. package/dist/server/auth/providers/custom.js +37 -0
  65. package/dist/server/auth/providers/custom.js.map +1 -0
  66. package/dist/server/auth/providers/custom.test.d.ts +1 -0
  67. package/dist/server/auth/providers/custom.test.js +107 -0
  68. package/dist/server/auth/providers/custom.test.js.map +1 -0
  69. package/dist/server/auth/providers/descope.d.ts +15 -0
  70. package/dist/server/auth/providers/descope.js +33 -0
  71. package/dist/server/auth/providers/descope.js.map +1 -0
  72. package/dist/server/auth/providers/descope.test.d.ts +1 -0
  73. package/dist/server/auth/providers/descope.test.js +37 -0
  74. package/dist/server/auth/providers/descope.test.js.map +1 -0
  75. package/dist/server/auth/providers/shared.d.ts +2 -0
  76. package/dist/server/auth/providers/shared.js +6 -0
  77. package/dist/server/auth/providers/shared.js.map +1 -0
  78. package/dist/server/auth/providers/shared.test.d.ts +1 -0
  79. package/dist/server/auth/providers/shared.test.js +10 -0
  80. package/dist/server/auth/providers/shared.test.js.map +1 -0
  81. package/dist/server/auth/providers/stytch.d.ts +12 -0
  82. package/dist/server/auth/providers/stytch.js +13 -0
  83. package/dist/server/auth/providers/stytch.js.map +1 -0
  84. package/dist/server/auth/providers/workos.d.ts +11 -0
  85. package/dist/server/auth/providers/workos.js +12 -0
  86. package/dist/server/auth/providers/workos.js.map +1 -0
  87. package/dist/server/auth/setup.d.ts +4 -0
  88. package/dist/server/auth/setup.js +51 -0
  89. package/dist/server/auth/setup.js.map +1 -0
  90. package/dist/server/auth/setup.test.d.ts +1 -0
  91. package/dist/server/auth/setup.test.js +185 -0
  92. package/dist/server/auth/setup.test.js.map +1 -0
  93. package/dist/server/auth/verify.d.ts +12 -0
  94. package/dist/server/auth/verify.js +38 -0
  95. package/dist/server/auth/verify.js.map +1 -0
  96. package/dist/server/auth/verify.test.d.ts +1 -0
  97. package/dist/server/auth/verify.test.js +100 -0
  98. package/dist/server/auth/verify.test.js.map +1 -0
  99. package/dist/server/build-manifest.test.d.ts +1 -0
  100. package/dist/server/build-manifest.test.js +27 -0
  101. package/dist/server/build-manifest.test.js.map +1 -0
  102. package/dist/server/express.js +3 -0
  103. package/dist/server/express.js.map +1 -1
  104. package/dist/server/express.test.js +61 -0
  105. package/dist/server/express.test.js.map +1 -1
  106. package/dist/server/index.d.ts +9 -2
  107. package/dist/server/index.js +7 -1
  108. package/dist/server/index.js.map +1 -1
  109. package/dist/server/requestOrigin.d.ts +7 -0
  110. package/dist/server/requestOrigin.js +25 -0
  111. package/dist/server/requestOrigin.js.map +1 -0
  112. package/dist/server/server.d.ts +56 -35
  113. package/dist/server/server.js +232 -136
  114. package/dist/server/server.js.map +1 -1
  115. package/dist/server/view-name.test-d.d.ts +1 -0
  116. package/dist/server/view-name.test-d.js +8 -0
  117. package/dist/server/view-name.test-d.js.map +1 -0
  118. package/dist/server/view-resource-resolution.test.d.ts +6 -0
  119. package/dist/server/view-resource-resolution.test.js +207 -0
  120. package/dist/server/view-resource-resolution.test.js.map +1 -0
  121. package/dist/test/view.test.js +69 -101
  122. package/dist/test/view.test.js.map +1 -1
  123. package/dist/web/bridges/adaptor.d.ts +51 -0
  124. package/dist/web/bridges/adaptor.js +330 -0
  125. package/dist/web/bridges/adaptor.js.map +1 -0
  126. package/dist/web/bridges/adaptor.test.d.ts +1 -0
  127. package/dist/web/bridges/adaptor.test.js +208 -0
  128. package/dist/web/bridges/adaptor.test.js.map +1 -0
  129. package/dist/web/bridges/apps-sdk/bridge.d.ts +6 -2
  130. package/dist/web/bridges/apps-sdk/bridge.js +15 -4
  131. package/dist/web/bridges/apps-sdk/bridge.js.map +1 -1
  132. package/dist/web/bridges/apps-sdk/index.d.ts +0 -1
  133. package/dist/web/bridges/apps-sdk/index.js +0 -1
  134. package/dist/web/bridges/apps-sdk/index.js.map +1 -1
  135. package/dist/web/bridges/get-adaptor.d.ts +4 -4
  136. package/dist/web/bridges/get-adaptor.js +6 -11
  137. package/dist/web/bridges/get-adaptor.js.map +1 -1
  138. package/dist/web/bridges/get-adaptor.test.d.ts +1 -0
  139. package/dist/web/bridges/get-adaptor.test.js +32 -0
  140. package/dist/web/bridges/get-adaptor.test.js.map +1 -0
  141. package/dist/web/bridges/mcp-app/bridge.d.ts +15 -3
  142. package/dist/web/bridges/mcp-app/bridge.js +68 -5
  143. package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
  144. package/dist/web/bridges/mcp-app/bridge.test.d.ts +1 -0
  145. package/dist/web/bridges/mcp-app/bridge.test.js +17 -0
  146. package/dist/web/bridges/mcp-app/bridge.test.js.map +1 -0
  147. package/dist/web/bridges/mcp-app/index.d.ts +0 -1
  148. package/dist/web/bridges/mcp-app/index.js +0 -1
  149. package/dist/web/bridges/mcp-app/index.js.map +1 -1
  150. package/dist/web/bridges/mcp-app/view-tools.test.d.ts +1 -0
  151. package/dist/web/bridges/mcp-app/view-tools.test.js +143 -0
  152. package/dist/web/bridges/mcp-app/view-tools.test.js.map +1 -0
  153. package/dist/web/bridges/types.d.ts +44 -1
  154. package/dist/web/bridges/types.js +14 -1
  155. package/dist/web/bridges/types.js.map +1 -1
  156. package/dist/web/bridges/types.test.d.ts +1 -0
  157. package/dist/web/bridges/types.test.js +19 -0
  158. package/dist/web/bridges/types.test.js.map +1 -0
  159. package/dist/web/components/modal-provider.d.ts +1 -1
  160. package/dist/web/components/modal-provider.js +4 -3
  161. package/dist/web/components/modal-provider.js.map +1 -1
  162. package/dist/web/create-store.test.js +8 -5
  163. package/dist/web/create-store.test.js.map +1 -1
  164. package/dist/web/data-llm.d.ts +1 -1
  165. package/dist/web/data-llm.test.js +17 -5
  166. package/dist/web/data-llm.test.js.map +1 -1
  167. package/dist/web/generate-helpers.test-d.js +4 -2
  168. package/dist/web/generate-helpers.test-d.js.map +1 -1
  169. package/dist/web/hooks/index.d.ts +1 -0
  170. package/dist/web/hooks/index.js +1 -0
  171. package/dist/web/hooks/index.js.map +1 -1
  172. package/dist/web/hooks/use-call-tool.test.js +37 -23
  173. package/dist/web/hooks/use-call-tool.test.js.map +1 -1
  174. package/dist/web/hooks/use-display-mode.test.js +56 -20
  175. package/dist/web/hooks/use-display-mode.test.js.map +1 -1
  176. package/dist/web/hooks/use-download.test.js +11 -3
  177. package/dist/web/hooks/use-download.test.js.map +1 -1
  178. package/dist/web/hooks/use-files.test.js +10 -2
  179. package/dist/web/hooks/use-files.test.js.map +1 -1
  180. package/dist/web/hooks/use-layout.test.js +59 -26
  181. package/dist/web/hooks/use-layout.test.js.map +1 -1
  182. package/dist/web/hooks/use-open-external.test.js +13 -3
  183. package/dist/web/hooks/use-open-external.test.js.map +1 -1
  184. package/dist/web/hooks/use-register-view-tool.d.ts +38 -0
  185. package/dist/web/hooks/use-register-view-tool.js +50 -0
  186. package/dist/web/hooks/use-register-view-tool.js.map +1 -0
  187. package/dist/web/hooks/use-request-close.test.js +35 -40
  188. package/dist/web/hooks/use-request-close.test.js.map +1 -1
  189. package/dist/web/hooks/use-request-modal.test.js +10 -0
  190. package/dist/web/hooks/use-request-modal.test.js.map +1 -1
  191. package/dist/web/hooks/use-request-size.test.js +35 -53
  192. package/dist/web/hooks/use-request-size.test.js.map +1 -1
  193. package/dist/web/hooks/use-set-open-in-app-url.test.js +28 -8
  194. package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -1
  195. package/dist/web/hooks/use-tool-info.d.ts +25 -7
  196. package/dist/web/hooks/use-tool-info.js +5 -8
  197. package/dist/web/hooks/use-tool-info.js.map +1 -1
  198. package/dist/web/hooks/use-tool-info.test-d.js +11 -29
  199. package/dist/web/hooks/use-tool-info.test-d.js.map +1 -1
  200. package/dist/web/hooks/use-tool-info.test.js +42 -32
  201. package/dist/web/hooks/use-tool-info.test.js.map +1 -1
  202. package/dist/web/hooks/use-user.test.js +81 -35
  203. package/dist/web/hooks/use-user.test.js.map +1 -1
  204. package/dist/web/hooks/use-view-state.test.js +6 -2
  205. package/dist/web/hooks/use-view-state.test.js.map +1 -1
  206. package/dist/web/plugin/plugin.js +0 -1
  207. package/dist/web/plugin/plugin.js.map +1 -1
  208. package/package.json +22 -19
  209. package/dist/web/bridges/apps-sdk/adaptor.d.ts +0 -28
  210. package/dist/web/bridges/apps-sdk/adaptor.js +0 -113
  211. package/dist/web/bridges/apps-sdk/adaptor.js.map +0 -1
  212. package/dist/web/bridges/mcp-app/adaptor.d.ts +0 -52
  213. package/dist/web/bridges/mcp-app/adaptor.js +0 -280
  214. package/dist/web/bridges/mcp-app/adaptor.js.map +0 -1
@@ -0,0 +1,51 @@
1
+ import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter, } from "@modelcontextprotocol/sdk/server/auth/router.js";
2
+ import cors from "cors";
3
+ import { requireBearerAuth } from "../auth.js";
4
+ import { resolveServerOrigin } from "../requestOrigin.js";
5
+ import { createJwksVerifier } from "./verify.js";
6
+ /** Mounts the well-known OAuth metadata and bearer auth on `/mcp`. */
7
+ export function setupOAuth(app, config) {
8
+ if (!config.verify?.issuer) {
9
+ throw new Error("oauth.verify requires an `issuer`");
10
+ }
11
+ const verifier = createJwksVerifier(config.verify);
12
+ // baseUrl known at boot: bake the resource URLs once, no Host-header trust.
13
+ if (config.baseUrl !== undefined) {
14
+ let baseUrl;
15
+ try {
16
+ baseUrl = new URL(config.baseUrl);
17
+ }
18
+ catch {
19
+ throw new Error(`oauth.baseUrl must be a valid absolute URL, got: ${JSON.stringify(config.baseUrl)}`);
20
+ }
21
+ app.use(mcpAuthMetadataRouter({
22
+ oauthMetadata: config.oauthMetadata,
23
+ resourceServerUrl: baseUrl,
24
+ scopesSupported: config.scopesSupported,
25
+ }));
26
+ app.use("/mcp", requireBearerAuth({
27
+ verifier,
28
+ requiredScopes: config.requiredScopes,
29
+ resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(baseUrl),
30
+ }));
31
+ return;
32
+ }
33
+ // No baseUrl: resolve the resource origin per request from forwarded headers
34
+ // (same precedence the framework uses for view serverUrl). Origin headers are
35
+ // pathless, so the PRM path stays at root, matching the SDK's static layout.
36
+ const resolveOrigin = (req) => new URL(resolveServerOrigin((key) => req.get(key)));
37
+ app.use("/.well-known/oauth-authorization-server", cors(), (_req, res) => void res.json(config.oauthMetadata));
38
+ app.use("/.well-known/oauth-protected-resource", cors(), (req, res) => {
39
+ res.json({
40
+ resource: resolveOrigin(req).href,
41
+ authorization_servers: [config.oauthMetadata.issuer],
42
+ scopes_supported: config.scopesSupported,
43
+ });
44
+ });
45
+ app.use("/mcp", (req, res, next) => requireBearerAuth({
46
+ verifier,
47
+ requiredScopes: config.requiredScopes,
48
+ resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(resolveOrigin(req)),
49
+ })(req, res, next));
50
+ }
51
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../src/server/auth/setup.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oCAAoC,EACpC,qBAAqB,GACtB,MAAM,iDAAiD,CAAC;AACzD,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD,sEAAsE;AACtE,MAAM,UAAU,UAAU,CAAC,GAAY,EAAE,MAAmB;IAC1D,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;IACvD,CAAC;IAED,MAAM,QAAQ,GAAG,kBAAkB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAEnD,4EAA4E;IAC5E,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACjC,IAAI,OAAY,CAAC;QACjB,IAAI,CAAC;YACH,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACpC,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,KAAK,CACb,oDAAoD,IAAI,CAAC,SAAS,CAChE,MAAM,CAAC,OAAO,CACf,EAAE,CACJ,CAAC;QACJ,CAAC;QACD,GAAG,CAAC,GAAG,CACL,qBAAqB,CAAC;YACpB,aAAa,EAAE,MAAM,CAAC,aAAa;YACnC,iBAAiB,EAAE,OAAO;YAC1B,eAAe,EAAE,MAAM,CAAC,eAAe;SACxC,CAAC,CACH,CAAC;QACF,GAAG,CAAC,GAAG,CACL,MAAM,EACN,iBAAiB,CAAC;YAChB,QAAQ;YACR,cAAc,EAAE,MAAM,CAAC,cAAc;YACrC,mBAAmB,EAAE,oCAAoC,CAAC,OAAO,CAAC;SACnE,CAAC,CACH,CAAC;QACF,OAAO;IACT,CAAC;IAED,6EAA6E;IAC7E,8EAA8E;IAC9E,6EAA6E;IAC7E,MAAM,aAAa,GAAG,CAAC,GAAY,EAAE,EAAE,CACrC,IAAI,GAAG,CAAC,mBAAmB,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAEtD,GAAG,CAAC,GAAG,CACL,yCAAyC,EACzC,IAAI,EAAE,EACN,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CACnD,CAAC;IACF,GAAG,CAAC,GAAG,CAAC,uCAAuC,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QACpE,GAAG,CAAC,IAAI,CAAC;YACP,QAAQ,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC,IAAI;YACjC,qBAAqB,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC;YACpD,gBAAgB,EAAE,MAAM,CAAC,eAAe;SACzC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IACH,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,CACjC,iBAAiB,CAAC;QAChB,QAAQ;QACR,cAAc,EAAE,MAAM,CAAC,cAAc;QACrC,mBAAmB,EAAE,oCAAoC,CACvD,aAAa,CAAC,GAAG,CAAC,CACnB;KACF,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CACnB,CAAC;AACJ,CAAC","sourcesContent":["import {\n getOAuthProtectedResourceMetadataUrl,\n mcpAuthMetadataRouter,\n} from \"@modelcontextprotocol/sdk/server/auth/router.js\";\nimport cors from \"cors\";\nimport type { Express, Request } from \"express\";\nimport { requireBearerAuth } from \"../auth.js\";\nimport { resolveServerOrigin } from \"../requestOrigin.js\";\nimport type { OAuthConfig } from \"./index.js\";\nimport { createJwksVerifier } from \"./verify.js\";\n\n/** Mounts the well-known OAuth metadata and bearer auth on `/mcp`. */\nexport function setupOAuth(app: Express, config: OAuthConfig): void {\n if (!config.verify?.issuer) {\n throw new Error(\"oauth.verify requires an `issuer`\");\n }\n\n const verifier = createJwksVerifier(config.verify);\n\n // baseUrl known at boot: bake the resource URLs once, no Host-header trust.\n if (config.baseUrl !== undefined) {\n let baseUrl: URL;\n try {\n baseUrl = new URL(config.baseUrl);\n } catch {\n throw new Error(\n `oauth.baseUrl must be a valid absolute URL, got: ${JSON.stringify(\n config.baseUrl,\n )}`,\n );\n }\n app.use(\n mcpAuthMetadataRouter({\n oauthMetadata: config.oauthMetadata,\n resourceServerUrl: baseUrl,\n scopesSupported: config.scopesSupported,\n }),\n );\n app.use(\n \"/mcp\",\n requireBearerAuth({\n verifier,\n requiredScopes: config.requiredScopes,\n resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(baseUrl),\n }),\n );\n return;\n }\n\n // No baseUrl: resolve the resource origin per request from forwarded headers\n // (same precedence the framework uses for view serverUrl). Origin headers are\n // pathless, so the PRM path stays at root, matching the SDK's static layout.\n const resolveOrigin = (req: Request) =>\n new URL(resolveServerOrigin((key) => req.get(key)));\n\n app.use(\n \"/.well-known/oauth-authorization-server\",\n cors(),\n (_req, res) => void res.json(config.oauthMetadata),\n );\n app.use(\"/.well-known/oauth-protected-resource\", cors(), (req, res) => {\n res.json({\n resource: resolveOrigin(req).href,\n authorization_servers: [config.oauthMetadata.issuer],\n scopes_supported: config.scopesSupported,\n });\n });\n app.use(\"/mcp\", (req, res, next) =>\n requireBearerAuth({\n verifier,\n requiredScopes: config.requiredScopes,\n resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(\n resolveOrigin(req),\n ),\n })(req, res, next),\n );\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,185 @@
1
+ // @vitest-environment node
2
+ import http from "node:http";
3
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5
+ import * as jose from "jose";
6
+ import { afterEach, describe, expect, it, vi } from "vitest";
7
+ import { McpServer } from "../server.js";
8
+ vi.mock("@skybridge/devtools", () => ({
9
+ devtoolsStaticServer: () => ((_req, _res, next) => next()),
10
+ }));
11
+ vi.mock("../viewsDevServer.js", () => ({
12
+ viewsDevServer: (_httpServer) => ((_req, _res, next) => next()),
13
+ }));
14
+ const ISSUER = "https://issuer.test";
15
+ const AUDIENCE = "api://default";
16
+ let jwksServer;
17
+ let appServer;
18
+ afterEach(() => {
19
+ jwksServer?.close();
20
+ appServer?.close();
21
+ });
22
+ async function startJwks() {
23
+ const { publicKey, privateKey } = await jose.generateKeyPair("RS256");
24
+ const jwk = {
25
+ ...(await jose.exportJWK(publicKey)),
26
+ kid: "test-key",
27
+ alg: "RS256",
28
+ use: "sig",
29
+ };
30
+ const server = http.createServer((_req, res) => {
31
+ res.setHeader("content-type", "application/json");
32
+ res.end(JSON.stringify({ keys: [jwk] }));
33
+ });
34
+ await new Promise((resolve) => server.listen(0, resolve));
35
+ jwksServer = server;
36
+ const port = server.address().port;
37
+ return { privateKey, jwksUri: `http://localhost:${port}/jwks` };
38
+ }
39
+ function signToken(key) {
40
+ return new jose.SignJWT({
41
+ client_id: "client-1",
42
+ scope: "openid email",
43
+ sub: "user-1",
44
+ })
45
+ .setProtectedHeader({ alg: "RS256", kid: "test-key" })
46
+ .setIssuer(ISSUER)
47
+ .setAudience(AUDIENCE)
48
+ .setExpirationTime("1h")
49
+ .sign(key);
50
+ }
51
+ async function bootServer(jwksUri, { baseUrl = "https://app.example.test" } = {}) {
52
+ const { createApp } = await import("../express.js");
53
+ const server = new McpServer({ name: "auth-test", version: "0.0.0" }, { capabilities: {} }, {
54
+ oauth: {
55
+ ...(baseUrl === null ? {} : { baseUrl }),
56
+ oauthMetadata: {
57
+ issuer: ISSUER,
58
+ authorization_endpoint: `${ISSUER}/authorize`,
59
+ token_endpoint: `${ISSUER}/token`,
60
+ response_types_supported: ["code"],
61
+ },
62
+ verify: { issuer: ISSUER, audience: AUDIENCE, jwksUri },
63
+ scopesSupported: ["openid", "email"],
64
+ requiredScopes: ["openid"],
65
+ },
66
+ }).registerTool({
67
+ name: "whoami",
68
+ description: "Returns the caller identity.",
69
+ inputSchema: {},
70
+ }, (_args, extra) => ({
71
+ structuredContent: { clientId: extra.authInfo?.clientId ?? null },
72
+ content: [{ type: "text", text: extra.authInfo?.clientId ?? "anon" }],
73
+ }));
74
+ const httpServer = http.createServer();
75
+ await createApp({ mcpServer: server, httpServer });
76
+ const listening = http.createServer(server.express);
77
+ await new Promise((resolve) => listening.listen(0, resolve));
78
+ appServer = listening;
79
+ const port = listening.address().port;
80
+ return `http://localhost:${port}`;
81
+ }
82
+ describe("setupOAuth wiring", () => {
83
+ it("serves protected-resource metadata", async () => {
84
+ const { jwksUri } = await startJwks();
85
+ const base = await bootServer(jwksUri);
86
+ const res = await fetch(`${base}/.well-known/oauth-protected-resource`);
87
+ expect(res.status).toBe(200);
88
+ const body = (await res.json());
89
+ expect(body.resource).toBe("https://app.example.test/");
90
+ expect(body.authorization_servers).toContain(ISSUER);
91
+ expect(body.scopes_supported).toEqual(["openid", "email"]);
92
+ });
93
+ it("rejects /mcp without a token (401 + WWW-Authenticate)", async () => {
94
+ const { jwksUri } = await startJwks();
95
+ const base = await bootServer(jwksUri);
96
+ const res = await fetch(`${base}/mcp`, {
97
+ method: "POST",
98
+ headers: { "Content-Type": "application/json" },
99
+ body: JSON.stringify({ jsonrpc: "2.0", method: "initialize", id: 1 }),
100
+ });
101
+ expect(res.status).toBe(401);
102
+ expect(res.headers.get("www-authenticate")).toMatch(/resource_metadata=/);
103
+ });
104
+ it("threads authInfo into the tool handler for a valid token", async () => {
105
+ const { privateKey, jwksUri } = await startJwks();
106
+ const base = await bootServer(jwksUri);
107
+ const token = await signToken(privateKey);
108
+ const client = new Client({ name: "test-client", version: "0.0.0" });
109
+ const transport = new StreamableHTTPClientTransport(new URL(`${base}/mcp`), { requestInit: { headers: { Authorization: `Bearer ${token}` } } });
110
+ await client.connect(transport);
111
+ const result = (await client.callTool({
112
+ name: "whoami",
113
+ arguments: {},
114
+ }));
115
+ expect(result.content[0]?.text).toBe("client-1");
116
+ await client.close();
117
+ });
118
+ });
119
+ describe("baseUrl inferred from headers", () => {
120
+ it("derives the resource origin from x-forwarded-host", async () => {
121
+ const { jwksUri } = await startJwks();
122
+ const base = await bootServer(jwksUri, { baseUrl: null });
123
+ const res = await fetch(`${base}/.well-known/oauth-protected-resource`, {
124
+ headers: { "x-forwarded-host": "infer.example.test" },
125
+ });
126
+ expect(res.status).toBe(200);
127
+ const body = (await res.json());
128
+ expect(body.resource).toBe("https://infer.example.test/");
129
+ });
130
+ it("uses the first hop of a forwarded-host chain", async () => {
131
+ const { jwksUri } = await startJwks();
132
+ const base = await bootServer(jwksUri, { baseUrl: null });
133
+ const res = await fetch(`${base}/.well-known/oauth-protected-resource`, {
134
+ headers: {
135
+ "x-forwarded-host": "public.example, internal.local",
136
+ "x-forwarded-proto": "https, http",
137
+ },
138
+ });
139
+ expect(res.status).toBe(200);
140
+ const body = (await res.json());
141
+ expect(body.resource).toBe("https://public.example/");
142
+ });
143
+ it("ignores the client Origin header, using Host instead", async () => {
144
+ const { jwksUri } = await startJwks();
145
+ const base = await bootServer(jwksUri, { baseUrl: null });
146
+ const res = await fetch(`${base}/.well-known/oauth-protected-resource`, {
147
+ headers: { origin: "https://chatgpt.com" },
148
+ });
149
+ expect(res.status).toBe(200);
150
+ const body = (await res.json());
151
+ expect(body.resource).toMatch(/^http:\/\/(localhost|127\.0\.0\.1):/);
152
+ });
153
+ it("points the 401 WWW-Authenticate at the inferred host", async () => {
154
+ const { jwksUri } = await startJwks();
155
+ const base = await bootServer(jwksUri, { baseUrl: null });
156
+ const res = await fetch(`${base}/mcp`, {
157
+ method: "POST",
158
+ headers: {
159
+ "Content-Type": "application/json",
160
+ "x-forwarded-host": "infer.example.test",
161
+ },
162
+ body: JSON.stringify({ jsonrpc: "2.0", method: "initialize", id: 1 }),
163
+ });
164
+ expect(res.status).toBe(401);
165
+ expect(res.headers.get("www-authenticate")).toMatch(/resource_metadata="https:\/\/infer\.example\.test\//);
166
+ });
167
+ });
168
+ describe("oauth config validation", () => {
169
+ const validMetadata = {
170
+ issuer: ISSUER,
171
+ authorization_endpoint: `${ISSUER}/authorize`,
172
+ token_endpoint: `${ISSUER}/token`,
173
+ response_types_supported: ["code"],
174
+ };
175
+ it("throws on a non-absolute baseUrl", () => {
176
+ expect(() => new McpServer({ name: "t", version: "0" }, undefined, {
177
+ oauth: {
178
+ baseUrl: "not-a-url",
179
+ oauthMetadata: validMetadata,
180
+ verify: { issuer: ISSUER, audience: AUDIENCE },
181
+ },
182
+ })).toThrow(/baseUrl must be a valid absolute URL/);
183
+ });
184
+ });
185
+ //# sourceMappingURL=setup.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.test.js","sourceRoot":"","sources":["../../../src/server/auth/setup.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\nimport http from \"node:http\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport type { RequestHandler } from \"express\";\nimport * as jose from \"jose\";\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { McpServer } from \"../server.js\";\n\nvi.mock(\"@skybridge/devtools\", () => ({\n devtoolsStaticServer: () =>\n ((_req: unknown, _res: unknown, next: () => void) =>\n next()) as RequestHandler,\n}));\nvi.mock(\"../viewsDevServer.js\", () => ({\n viewsDevServer: (_httpServer: unknown) =>\n ((_req: unknown, _res: unknown, next: () => void) =>\n next()) as RequestHandler,\n}));\n\nconst ISSUER = \"https://issuer.test\";\nconst AUDIENCE = \"api://default\";\n\nlet jwksServer: http.Server | undefined;\nlet appServer: http.Server | undefined;\nafterEach(() => {\n jwksServer?.close();\n appServer?.close();\n});\n\nasync function startJwks() {\n const { publicKey, privateKey } = await jose.generateKeyPair(\"RS256\");\n const jwk = {\n ...(await jose.exportJWK(publicKey)),\n kid: \"test-key\",\n alg: \"RS256\",\n use: \"sig\",\n };\n const server = http.createServer((_req, res) => {\n res.setHeader(\"content-type\", \"application/json\");\n res.end(JSON.stringify({ keys: [jwk] }));\n });\n await new Promise<void>((resolve) => server.listen(0, resolve));\n jwksServer = server;\n const port = (server.address() as { port: number }).port;\n return { privateKey, jwksUri: `http://localhost:${port}/jwks` };\n}\n\nfunction signToken(key: CryptoKey) {\n return new jose.SignJWT({\n client_id: \"client-1\",\n scope: \"openid email\",\n sub: \"user-1\",\n })\n .setProtectedHeader({ alg: \"RS256\", kid: \"test-key\" })\n .setIssuer(ISSUER)\n .setAudience(AUDIENCE)\n .setExpirationTime(\"1h\")\n .sign(key);\n}\n\nasync function bootServer(\n jwksUri: string,\n { baseUrl = \"https://app.example.test\" }: { baseUrl?: string | null } = {},\n) {\n const { createApp } = await import(\"../express.js\");\n const server = new McpServer(\n { name: \"auth-test\", version: \"0.0.0\" },\n { capabilities: {} },\n {\n oauth: {\n ...(baseUrl === null ? {} : { baseUrl }),\n oauthMetadata: {\n issuer: ISSUER,\n authorization_endpoint: `${ISSUER}/authorize`,\n token_endpoint: `${ISSUER}/token`,\n response_types_supported: [\"code\"],\n },\n verify: { issuer: ISSUER, audience: AUDIENCE, jwksUri },\n scopesSupported: [\"openid\", \"email\"],\n requiredScopes: [\"openid\"],\n },\n },\n ).registerTool(\n {\n name: \"whoami\",\n description: \"Returns the caller identity.\",\n inputSchema: {},\n },\n (_args, extra) => ({\n structuredContent: { clientId: extra.authInfo?.clientId ?? null },\n content: [{ type: \"text\", text: extra.authInfo?.clientId ?? \"anon\" }],\n }),\n );\n\n const httpServer = http.createServer();\n await createApp({ mcpServer: server, httpServer });\n const listening = http.createServer(server.express);\n await new Promise<void>((resolve) => listening.listen(0, resolve));\n appServer = listening;\n const port = (listening.address() as { port: number }).port;\n return `http://localhost:${port}`;\n}\n\ndescribe(\"setupOAuth wiring\", () => {\n it(\"serves protected-resource metadata\", async () => {\n const { jwksUri } = await startJwks();\n const base = await bootServer(jwksUri);\n\n const res = await fetch(`${base}/.well-known/oauth-protected-resource`);\n expect(res.status).toBe(200);\n const body = (await res.json()) as {\n resource: string;\n authorization_servers: string[];\n scopes_supported: string[];\n };\n expect(body.resource).toBe(\"https://app.example.test/\");\n expect(body.authorization_servers).toContain(ISSUER);\n expect(body.scopes_supported).toEqual([\"openid\", \"email\"]);\n });\n\n it(\"rejects /mcp without a token (401 + WWW-Authenticate)\", async () => {\n const { jwksUri } = await startJwks();\n const base = await bootServer(jwksUri);\n\n const res = await fetch(`${base}/mcp`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ jsonrpc: \"2.0\", method: \"initialize\", id: 1 }),\n });\n expect(res.status).toBe(401);\n expect(res.headers.get(\"www-authenticate\")).toMatch(/resource_metadata=/);\n });\n\n it(\"threads authInfo into the tool handler for a valid token\", async () => {\n const { privateKey, jwksUri } = await startJwks();\n const base = await bootServer(jwksUri);\n const token = await signToken(privateKey);\n\n const client = new Client({ name: \"test-client\", version: \"0.0.0\" });\n const transport = new StreamableHTTPClientTransport(\n new URL(`${base}/mcp`),\n { requestInit: { headers: { Authorization: `Bearer ${token}` } } },\n );\n await client.connect(transport);\n\n const result = (await client.callTool({\n name: \"whoami\",\n arguments: {},\n })) as unknown as {\n content: { type: string; text: string }[];\n };\n expect(result.content[0]?.text).toBe(\"client-1\");\n\n await client.close();\n });\n});\n\ndescribe(\"baseUrl inferred from headers\", () => {\n it(\"derives the resource origin from x-forwarded-host\", async () => {\n const { jwksUri } = await startJwks();\n const base = await bootServer(jwksUri, { baseUrl: null });\n\n const res = await fetch(`${base}/.well-known/oauth-protected-resource`, {\n headers: { \"x-forwarded-host\": \"infer.example.test\" },\n });\n expect(res.status).toBe(200);\n const body = (await res.json()) as { resource: string };\n expect(body.resource).toBe(\"https://infer.example.test/\");\n });\n\n it(\"uses the first hop of a forwarded-host chain\", async () => {\n const { jwksUri } = await startJwks();\n const base = await bootServer(jwksUri, { baseUrl: null });\n\n const res = await fetch(`${base}/.well-known/oauth-protected-resource`, {\n headers: {\n \"x-forwarded-host\": \"public.example, internal.local\",\n \"x-forwarded-proto\": \"https, http\",\n },\n });\n expect(res.status).toBe(200);\n const body = (await res.json()) as { resource: string };\n expect(body.resource).toBe(\"https://public.example/\");\n });\n\n it(\"ignores the client Origin header, using Host instead\", async () => {\n const { jwksUri } = await startJwks();\n const base = await bootServer(jwksUri, { baseUrl: null });\n\n const res = await fetch(`${base}/.well-known/oauth-protected-resource`, {\n headers: { origin: \"https://chatgpt.com\" },\n });\n expect(res.status).toBe(200);\n const body = (await res.json()) as { resource: string };\n expect(body.resource).toMatch(/^http:\\/\\/(localhost|127\\.0\\.0\\.1):/);\n });\n\n it(\"points the 401 WWW-Authenticate at the inferred host\", async () => {\n const { jwksUri } = await startJwks();\n const base = await bootServer(jwksUri, { baseUrl: null });\n\n const res = await fetch(`${base}/mcp`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-forwarded-host\": \"infer.example.test\",\n },\n body: JSON.stringify({ jsonrpc: \"2.0\", method: \"initialize\", id: 1 }),\n });\n expect(res.status).toBe(401);\n expect(res.headers.get(\"www-authenticate\")).toMatch(\n /resource_metadata=\"https:\\/\\/infer\\.example\\.test\\//,\n );\n });\n});\n\ndescribe(\"oauth config validation\", () => {\n const validMetadata = {\n issuer: ISSUER,\n authorization_endpoint: `${ISSUER}/authorize`,\n token_endpoint: `${ISSUER}/token`,\n response_types_supported: [\"code\"],\n };\n\n it(\"throws on a non-absolute baseUrl\", () => {\n expect(\n () =>\n new McpServer({ name: \"t\", version: \"0\" }, undefined, {\n oauth: {\n baseUrl: \"not-a-url\",\n oauthMetadata: validMetadata,\n verify: { issuer: ISSUER, audience: AUDIENCE },\n },\n }),\n ).toThrow(/baseUrl must be a valid absolute URL/);\n });\n});\n"]}
@@ -0,0 +1,12 @@
1
+ import type { OAuthTokenVerifier } from "@modelcontextprotocol/sdk/server/auth/provider.js";
2
+ export type JwksVerifyConfig = {
3
+ /** Expected `iss` claim. */
4
+ issuer: string;
5
+ /** Expected `aud` claim. Omit to skip audience verification — for IdPs that
6
+ * don't bind an audience to their access tokens (e.g. Clerk). */
7
+ audience?: string;
8
+ /** Defaults to `${issuer}/.well-known/jwks.json`. */
9
+ jwksUri?: string;
10
+ };
11
+ /** Builds an `OAuthTokenVerifier` validating JWTs against a remote JWKS. Internal, not exported. */
12
+ export declare function createJwksVerifier(config: JwksVerifyConfig): OAuthTokenVerifier;
@@ -0,0 +1,38 @@
1
+ import { InvalidTokenError } from "@modelcontextprotocol/sdk/server/auth/errors.js";
2
+ import * as jose from "jose";
3
+ /** Builds an `OAuthTokenVerifier` validating JWTs against a remote JWKS. Internal, not exported. */
4
+ export function createJwksVerifier(config) {
5
+ const jwksUri = config.jwksUri ??
6
+ `${config.issuer.replace(/\/$/, "")}/.well-known/jwks.json`;
7
+ const jwks = jose.createRemoteJWKSet(new URL(jwksUri));
8
+ return {
9
+ async verifyAccessToken(token) {
10
+ let payload;
11
+ try {
12
+ ({ payload } = await jose.jwtVerify(token, jwks, {
13
+ issuer: config.issuer,
14
+ // jose skips the aud check when `audience` is undefined.
15
+ ...(config.audience !== undefined && { audience: config.audience }),
16
+ }));
17
+ }
18
+ catch (err) {
19
+ const message = err instanceof Error ? err.message : String(err);
20
+ throw new InvalidTokenError(`Token verification failed: ${message}`);
21
+ }
22
+ const { client_id, scope, exp, sub, ...rest } = payload;
23
+ const scopes = Array.isArray(scope)
24
+ ? scope.map(String)
25
+ : typeof scope === "string"
26
+ ? scope.split(/\s+/).filter(Boolean)
27
+ : [];
28
+ return {
29
+ token,
30
+ clientId: client_id ?? "",
31
+ scopes,
32
+ expiresAt: exp,
33
+ extra: { subject: sub, ...rest },
34
+ };
35
+ },
36
+ };
37
+ }
38
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../../../src/server/auth/verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,iDAAiD,CAAC;AAGpF,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAY7B,oGAAoG;AACpG,MAAM,UAAU,kBAAkB,CAChC,MAAwB;IAExB,MAAM,OAAO,GACX,MAAM,CAAC,OAAO;QACd,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,wBAAwB,CAAC;IAC9D,MAAM,IAAI,GAAG,IAAI,CAAC,kBAAkB,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;IAEvD,OAAO;QACL,KAAK,CAAC,iBAAiB,CAAC,KAAa;YACnC,IAAI,OAAwB,CAAC;YAC7B,IAAI,CAAC;gBACH,CAAC,EAAE,OAAO,EAAE,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE;oBAC/C,MAAM,EAAE,MAAM,CAAC,MAAM;oBACrB,yDAAyD;oBACzD,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC;iBACpE,CAAC,CAAC,CAAC;YACN,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBACjE,MAAM,IAAI,iBAAiB,CAAC,8BAA8B,OAAO,EAAE,CAAC,CAAC;YACvE,CAAC;YAED,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,GAAG,OAQ/C,CAAC;YAEF,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;gBACjC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC;gBACnB,CAAC,CAAC,OAAO,KAAK,KAAK,QAAQ;oBACzB,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;oBACpC,CAAC,CAAC,EAAE,CAAC;YAET,OAAO;gBACL,KAAK;gBACL,QAAQ,EAAE,SAAS,IAAI,EAAE;gBACzB,MAAM;gBACN,SAAS,EAAE,GAAG;gBACd,KAAK,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE;aACd,CAAC;QACvB,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import { InvalidTokenError } from \"@modelcontextprotocol/sdk/server/auth/errors.js\";\nimport type { OAuthTokenVerifier } from \"@modelcontextprotocol/sdk/server/auth/provider.js\";\nimport type { AuthInfo } from \"@modelcontextprotocol/sdk/server/auth/types.js\";\nimport * as jose from \"jose\";\n\nexport type JwksVerifyConfig = {\n /** Expected `iss` claim. */\n issuer: string;\n /** Expected `aud` claim. Omit to skip audience verification — for IdPs that\n * don't bind an audience to their access tokens (e.g. Clerk). */\n audience?: string;\n /** Defaults to `${issuer}/.well-known/jwks.json`. */\n jwksUri?: string;\n};\n\n/** Builds an `OAuthTokenVerifier` validating JWTs against a remote JWKS. Internal, not exported. */\nexport function createJwksVerifier(\n config: JwksVerifyConfig,\n): OAuthTokenVerifier {\n const jwksUri =\n config.jwksUri ??\n `${config.issuer.replace(/\\/$/, \"\")}/.well-known/jwks.json`;\n const jwks = jose.createRemoteJWKSet(new URL(jwksUri));\n\n return {\n async verifyAccessToken(token: string): Promise<AuthInfo> {\n let payload: jose.JWTPayload;\n try {\n ({ payload } = await jose.jwtVerify(token, jwks, {\n issuer: config.issuer,\n // jose skips the aud check when `audience` is undefined.\n ...(config.audience !== undefined && { audience: config.audience }),\n }));\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new InvalidTokenError(`Token verification failed: ${message}`);\n }\n\n const { client_id, scope, exp, sub, ...rest } = payload as Record<\n string,\n unknown\n > & {\n client_id?: string;\n scope?: string | string[];\n exp?: number;\n sub?: string;\n };\n\n const scopes = Array.isArray(scope)\n ? scope.map(String)\n : typeof scope === \"string\"\n ? scope.split(/\\s+/).filter(Boolean)\n : [];\n\n return {\n token,\n clientId: client_id ?? \"\",\n scopes,\n expiresAt: exp,\n extra: { subject: sub, ...rest },\n } satisfies AuthInfo;\n },\n };\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,100 @@
1
+ // @vitest-environment node
2
+ import http from "node:http";
3
+ import * as jose from "jose";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import { createJwksVerifier } from "./verify.js";
6
+ const ISSUER = "https://issuer.test";
7
+ const AUDIENCE = "api://default";
8
+ let jwksServer;
9
+ afterEach(() => jwksServer?.close());
10
+ async function startJwks() {
11
+ const { publicKey, privateKey } = await jose.generateKeyPair("RS256");
12
+ const jwk = {
13
+ ...(await jose.exportJWK(publicKey)),
14
+ kid: "test-key",
15
+ alg: "RS256",
16
+ use: "sig",
17
+ };
18
+ const server = http.createServer((_req, res) => {
19
+ res.setHeader("content-type", "application/json");
20
+ res.end(JSON.stringify({ keys: [jwk] }));
21
+ });
22
+ await new Promise((resolve) => server.listen(0, resolve));
23
+ jwksServer = server;
24
+ const port = server.address().port;
25
+ return { privateKey, jwksUri: `http://localhost:${port}/jwks` };
26
+ }
27
+ function sign(key, claims, opts = {}) {
28
+ return new jose.SignJWT(claims)
29
+ .setProtectedHeader({ alg: "RS256", kid: "test-key" })
30
+ .setIssuer(opts.issuer ?? ISSUER)
31
+ .setAudience(opts.audience ?? AUDIENCE)
32
+ .setExpirationTime(opts.expiresAt ?? "1h")
33
+ .sign(key);
34
+ }
35
+ describe("createJwksVerifier", () => {
36
+ it("verifies a valid token and maps claims to AuthInfo", async () => {
37
+ const { privateKey, jwksUri } = await startJwks();
38
+ const verifier = createJwksVerifier({
39
+ issuer: ISSUER,
40
+ audience: AUDIENCE,
41
+ jwksUri,
42
+ });
43
+ const token = await sign(privateKey, {
44
+ client_id: "client-1",
45
+ scope: "openid email",
46
+ sub: "user-1",
47
+ email: "a@b.test",
48
+ });
49
+ const auth = await verifier.verifyAccessToken(token);
50
+ expect(auth.token).toBe(token);
51
+ expect(auth.clientId).toBe("client-1");
52
+ expect(auth.scopes).toEqual(["openid", "email"]);
53
+ expect(auth.extra?.subject).toBe("user-1");
54
+ expect(auth.extra?.email).toBe("a@b.test");
55
+ });
56
+ it("rejects a token with the wrong audience", async () => {
57
+ const { privateKey, jwksUri } = await startJwks();
58
+ const verifier = createJwksVerifier({
59
+ issuer: ISSUER,
60
+ audience: AUDIENCE,
61
+ jwksUri,
62
+ });
63
+ const token = await sign(privateKey, { client_id: "c" }, { audience: "api://other" });
64
+ await expect(verifier.verifyAccessToken(token)).rejects.toThrow(/Token verification failed/);
65
+ });
66
+ it("skips the aud check when no audience is configured (e.g. Clerk)", async () => {
67
+ const { privateKey, jwksUri } = await startJwks();
68
+ const verifier = createJwksVerifier({ issuer: ISSUER, jwksUri });
69
+ // Clerk-shaped access token: no `aud` claim at all.
70
+ const token = await new jose.SignJWT({ client_id: "c", sub: "u" })
71
+ .setProtectedHeader({ alg: "RS256", kid: "test-key" })
72
+ .setIssuer(ISSUER)
73
+ .setExpirationTime("1h")
74
+ .sign(privateKey);
75
+ const auth = await verifier.verifyAccessToken(token);
76
+ expect(auth.clientId).toBe("c");
77
+ expect(auth.extra?.subject).toBe("u");
78
+ });
79
+ it("parses array scope claims and trims extra whitespace", async () => {
80
+ const { privateKey, jwksUri } = await startJwks();
81
+ const verifier = createJwksVerifier({
82
+ issuer: ISSUER,
83
+ audience: AUDIENCE,
84
+ jwksUri,
85
+ });
86
+ const arrayToken = await sign(privateKey, {
87
+ scope: ["openid", "email"],
88
+ });
89
+ expect((await verifier.verifyAccessToken(arrayToken)).scopes).toEqual([
90
+ "openid",
91
+ "email",
92
+ ]);
93
+ const messyToken = await sign(privateKey, { scope: " openid email " });
94
+ expect((await verifier.verifyAccessToken(messyToken)).scopes).toEqual([
95
+ "openid",
96
+ "email",
97
+ ]);
98
+ });
99
+ });
100
+ //# sourceMappingURL=verify.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.test.js","sourceRoot":"","sources":["../../../src/server/auth/verify.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\nimport http from \"node:http\";\nimport * as jose from \"jose\";\nimport { afterEach, describe, expect, it } from \"vitest\";\nimport { createJwksVerifier } from \"./verify.js\";\n\nconst ISSUER = \"https://issuer.test\";\nconst AUDIENCE = \"api://default\";\n\nlet jwksServer: http.Server | undefined;\nafterEach(() => jwksServer?.close());\n\nasync function startJwks() {\n const { publicKey, privateKey } = await jose.generateKeyPair(\"RS256\");\n const jwk = {\n ...(await jose.exportJWK(publicKey)),\n kid: \"test-key\",\n alg: \"RS256\",\n use: \"sig\",\n };\n const server = http.createServer((_req, res) => {\n res.setHeader(\"content-type\", \"application/json\");\n res.end(JSON.stringify({ keys: [jwk] }));\n });\n await new Promise<void>((resolve) => server.listen(0, resolve));\n jwksServer = server;\n const port = (server.address() as { port: number }).port;\n return { privateKey, jwksUri: `http://localhost:${port}/jwks` };\n}\n\nfunction sign(\n key: CryptoKey,\n claims: Record<string, unknown>,\n opts: { issuer?: string; audience?: string; expiresAt?: string } = {},\n) {\n return new jose.SignJWT(claims)\n .setProtectedHeader({ alg: \"RS256\", kid: \"test-key\" })\n .setIssuer(opts.issuer ?? ISSUER)\n .setAudience(opts.audience ?? AUDIENCE)\n .setExpirationTime(opts.expiresAt ?? \"1h\")\n .sign(key);\n}\n\ndescribe(\"createJwksVerifier\", () => {\n it(\"verifies a valid token and maps claims to AuthInfo\", async () => {\n const { privateKey, jwksUri } = await startJwks();\n const verifier = createJwksVerifier({\n issuer: ISSUER,\n audience: AUDIENCE,\n jwksUri,\n });\n const token = await sign(privateKey, {\n client_id: \"client-1\",\n scope: \"openid email\",\n sub: \"user-1\",\n email: \"a@b.test\",\n });\n\n const auth = await verifier.verifyAccessToken(token);\n\n expect(auth.token).toBe(token);\n expect(auth.clientId).toBe(\"client-1\");\n expect(auth.scopes).toEqual([\"openid\", \"email\"]);\n expect(auth.extra?.subject).toBe(\"user-1\");\n expect(auth.extra?.email).toBe(\"a@b.test\");\n });\n\n it(\"rejects a token with the wrong audience\", async () => {\n const { privateKey, jwksUri } = await startJwks();\n const verifier = createJwksVerifier({\n issuer: ISSUER,\n audience: AUDIENCE,\n jwksUri,\n });\n const token = await sign(\n privateKey,\n { client_id: \"c\" },\n { audience: \"api://other\" },\n );\n\n await expect(verifier.verifyAccessToken(token)).rejects.toThrow(\n /Token verification failed/,\n );\n });\n\n it(\"skips the aud check when no audience is configured (e.g. Clerk)\", async () => {\n const { privateKey, jwksUri } = await startJwks();\n const verifier = createJwksVerifier({ issuer: ISSUER, jwksUri });\n // Clerk-shaped access token: no `aud` claim at all.\n const token = await new jose.SignJWT({ client_id: \"c\", sub: \"u\" })\n .setProtectedHeader({ alg: \"RS256\", kid: \"test-key\" })\n .setIssuer(ISSUER)\n .setExpirationTime(\"1h\")\n .sign(privateKey);\n\n const auth = await verifier.verifyAccessToken(token);\n expect(auth.clientId).toBe(\"c\");\n expect(auth.extra?.subject).toBe(\"u\");\n });\n\n it(\"parses array scope claims and trims extra whitespace\", async () => {\n const { privateKey, jwksUri } = await startJwks();\n const verifier = createJwksVerifier({\n issuer: ISSUER,\n audience: AUDIENCE,\n jwksUri,\n });\n\n const arrayToken = await sign(privateKey, {\n scope: [\"openid\", \"email\"],\n });\n expect((await verifier.verifyAccessToken(arrayToken)).scopes).toEqual([\n \"openid\",\n \"email\",\n ]);\n\n const messyToken = await sign(privateKey, { scope: \" openid email \" });\n expect((await verifier.verifyAccessToken(messyToken)).scopes).toEqual([\n \"openid\",\n \"email\",\n ]);\n });\n});\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { __setBuildManifest, McpServer } from "./index.js";
3
+ function manifestOf(server) {
4
+ return server.viteManifest;
5
+ }
6
+ function makeServer() {
7
+ return new McpServer({ name: "test", version: "0.0.1" }, { capabilities: {} });
8
+ }
9
+ describe("__setBuildManifest", () => {
10
+ it("primes the Vite manifest for the next McpServer constructed", () => {
11
+ const manifest = {
12
+ "src/views/index.tsx": { file: "assets/index-DEADBEEF.js" },
13
+ };
14
+ __setBuildManifest(manifest);
15
+ // viteManifest is private; reach into it to lock the contract that the
16
+ // generated `dist/__entry.js` relies on.
17
+ expect(manifestOf(makeServer())).toEqual(manifest);
18
+ });
19
+ it("is consume-once: a second McpServer built without re-priming gets no manifest", () => {
20
+ __setBuildManifest({
21
+ "src/views/index.tsx": { file: "assets/index-CAFEBABE.js" },
22
+ });
23
+ makeServer(); // consumes the primed manifest
24
+ expect(manifestOf(makeServer())).toBeNull();
25
+ });
26
+ });
27
+ //# sourceMappingURL=build-manifest.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-manifest.test.js","sourceRoot":"","sources":["../../src/server/build-manifest.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE3D,SAAS,UAAU,CACjB,MAAiB;IAEjB,OACE,MAGD,CAAC,YAAY,CAAC;AACjB,CAAC;AAED,SAAS,UAAU;IACjB,OAAO,IAAI,SAAS,CAClB,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,EAClC,EAAE,YAAY,EAAE,EAAE,EAAE,CACrB,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;IAClC,EAAE,CAAC,6DAA6D,EAAE,GAAG,EAAE;QACrE,MAAM,QAAQ,GAAG;YACf,qBAAqB,EAAE,EAAE,IAAI,EAAE,0BAA0B,EAAE;SAC5D,CAAC;QACF,kBAAkB,CAAC,QAAQ,CAAC,CAAC;QAE7B,uEAAuE;QACvE,yCAAyC;QACzC,MAAM,CAAC,UAAU,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+EAA+E,EAAE,GAAG,EAAE;QACvF,kBAAkB,CAAC;YACjB,qBAAqB,EAAE,EAAE,IAAI,EAAE,0BAA0B,EAAE;SAC5D,CAAC,CAAC;QAEH,UAAU,EAAE,CAAC,CAAC,+BAA+B;QAC7C,MAAM,CAAC,UAAU,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC9C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { describe, expect, it } from \"vitest\";\nimport { __setBuildManifest, McpServer } from \"./index.js\";\n\nfunction manifestOf(\n server: McpServer,\n): Record<string, { file: string }> | null {\n return (\n server as unknown as {\n viteManifest: Record<string, { file: string }> | null;\n }\n ).viteManifest;\n}\n\nfunction makeServer(): McpServer {\n return new McpServer(\n { name: \"test\", version: \"0.0.1\" },\n { capabilities: {} },\n );\n}\n\ndescribe(\"__setBuildManifest\", () => {\n it(\"primes the Vite manifest for the next McpServer constructed\", () => {\n const manifest = {\n \"src/views/index.tsx\": { file: \"assets/index-DEADBEEF.js\" },\n };\n __setBuildManifest(manifest);\n\n // viteManifest is private; reach into it to lock the contract that the\n // generated `dist/__entry.js` relies on.\n expect(manifestOf(makeServer())).toEqual(manifest);\n });\n\n it(\"is consume-once: a second McpServer built without re-priming gets no manifest\", () => {\n __setBuildManifest({\n \"src/views/index.tsx\": { file: \"assets/index-CAFEBABE.js\" },\n });\n\n makeServer(); // consumes the primed manifest\n expect(manifestOf(makeServer())).toBeNull();\n });\n});\n"]}
@@ -39,6 +39,9 @@ export async function createApp({ mcpServer, httpServer, errorMiddleware = [], }
39
39
  if (process.env.NODE_ENV !== "production") {
40
40
  const { devtoolsStaticServer } = await import("@skybridge/devtools");
41
41
  app.use(await devtoolsStaticServer());
42
+ const assetsPath = path.join(process.cwd(), "dist", "assets");
43
+ app.use("/assets", cors());
44
+ app.use("/assets", express.static(assetsPath));
42
45
  const { viewsDevServer } = await import("./viewsDevServer.js");
43
46
  app.use(await viewsDevServer(httpServer));
44
47
  const controlPort = parseControlPort(process.env.__TUNNEL_CONTROL_PORT);
@@ -1 +1 @@
1
- {"version":3,"file":"express.js","sourceRoot":"","sources":["../../src/server/express.ts"],"names":[],"mappings":"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","sourcesContent":["import type http from \"node:http\";\nimport path from \"node:path\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport cors from \"cors\";\nimport express from \"express\";\nimport type { McpServer } from \"./server.js\";\n\nfunction parseControlPort(raw: string | undefined): number | null {\n if (raw === undefined) {\n return null;\n }\n const n = Number.parseInt(raw, 10);\n if (!Number.isFinite(n) || n <= 0 || n >= 65536) {\n return null;\n }\n return n;\n}\n\nfunction applyMiddlewares(\n app: express.Express,\n middlewares: Array<{\n path?: string;\n handlers: express.ErrorRequestHandler[];\n }>,\n): void {\n for (const middleware of middlewares) {\n if (middleware.path) {\n app.use(middleware.path, ...middleware.handlers);\n } else {\n app.use(...middleware.handlers);\n }\n }\n}\n\nfunction defaultErrorHandler(\n err: unknown,\n _req: express.Request,\n res: express.Response,\n _next: express.NextFunction,\n) {\n console.error(\"Error handling MCP request:\", err);\n if (!res.headersSent) {\n res.status(500).json({\n jsonrpc: \"2.0\",\n error: { code: -32603, message: \"Internal server error\" },\n id: null,\n });\n }\n}\n\nexport async function createApp({\n mcpServer,\n httpServer,\n errorMiddleware = [],\n}: {\n mcpServer: McpServer;\n httpServer: http.Server;\n errorMiddleware?: {\n path?: string;\n handlers: express.ErrorRequestHandler[];\n }[];\n}): Promise<express.Express> {\n const app = mcpServer.express;\n\n // Read `process.env.NODE_ENV` inline: wrangler/esbuild only substitute the literal expression,\n // so a local const would defeat dead-code elimination of the dev-only imports below.\n if (process.env.NODE_ENV !== \"production\") {\n const { devtoolsStaticServer } = await import(\"@skybridge/devtools\");\n app.use(await devtoolsStaticServer());\n const { viewsDevServer } = await import(\"./viewsDevServer.js\");\n app.use(await viewsDevServer(httpServer));\n\n const controlPort = parseControlPort(process.env.__TUNNEL_CONTROL_PORT);\n if (controlPort !== null) {\n const { createTunnelProxyRouter } = await import(\n \"./tunnel-proxy-router.js\"\n );\n app.use(createTunnelProxyRouter(controlPort));\n } else if (process.env.__TUNNEL_CONTROL_PORT !== undefined) {\n console.warn(\n `Ignoring invalid __TUNNEL_CONTROL_PORT=${process.env.__TUNNEL_CONTROL_PORT}`,\n );\n }\n } else {\n const assetsPath = path.join(process.cwd(), \"dist\", \"assets\");\n\n app.use(\"/assets\", cors());\n app.use(\"/assets\", express.static(assetsPath));\n }\n\n app.use(\"/mcp\", mcpMiddleware(mcpServer));\n\n applyMiddlewares(app, errorMiddleware);\n\n app.use(\"/mcp\", defaultErrorHandler);\n\n return app;\n}\n\nconst mcpMiddleware = (server: McpServer): express.RequestHandler => {\n return async (\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n ) => {\n if (req.method !== \"POST\") {\n res.writeHead(405).end(\n JSON.stringify({\n jsonrpc: \"2.0\",\n error: {\n code: -32000,\n message: \"Method not allowed.\",\n },\n id: null,\n }),\n );\n return;\n }\n\n try {\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n // Respond with a single JSON body instead of SSE. Skybridge's stateless\n // transport never streams server-initiated messages, so SSE adds no\n // capability — and on workerd specifically, `cloudflare:node`'s http\n // bridge silently drops chunked writes that happen after the request\n // handler awaits, which manifests as a 200 with empty body for any\n // async tools/call.\n enableJsonResponse: true,\n });\n\n res.on(\"close\", () => {\n transport.close();\n });\n\n await server.connectStatelessTransport(transport);\n // Express strips the mount path from req.url (e.g. \"/mcp\" becomes \"/\").\n // Restore it so the SDK builds the correct requestInfo.url.\n req.url = req.originalUrl;\n await transport.handleRequest(req, res, req.body);\n } catch (error) {\n next(error);\n }\n };\n};\n"]}
1
+ {"version":3,"file":"express.js","sourceRoot":"","sources":["../../src/server/express.ts"],"names":[],"mappings":"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","sourcesContent":["import type http from \"node:http\";\nimport path from \"node:path\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport cors from \"cors\";\nimport express from \"express\";\nimport type { McpServer } from \"./server.js\";\n\nfunction parseControlPort(raw: string | undefined): number | null {\n if (raw === undefined) {\n return null;\n }\n const n = Number.parseInt(raw, 10);\n if (!Number.isFinite(n) || n <= 0 || n >= 65536) {\n return null;\n }\n return n;\n}\n\nfunction applyMiddlewares(\n app: express.Express,\n middlewares: Array<{\n path?: string;\n handlers: express.ErrorRequestHandler[];\n }>,\n): void {\n for (const middleware of middlewares) {\n if (middleware.path) {\n app.use(middleware.path, ...middleware.handlers);\n } else {\n app.use(...middleware.handlers);\n }\n }\n}\n\nfunction defaultErrorHandler(\n err: unknown,\n _req: express.Request,\n res: express.Response,\n _next: express.NextFunction,\n) {\n console.error(\"Error handling MCP request:\", err);\n if (!res.headersSent) {\n res.status(500).json({\n jsonrpc: \"2.0\",\n error: { code: -32603, message: \"Internal server error\" },\n id: null,\n });\n }\n}\n\nexport async function createApp({\n mcpServer,\n httpServer,\n errorMiddleware = [],\n}: {\n mcpServer: McpServer;\n httpServer: http.Server;\n errorMiddleware?: {\n path?: string;\n handlers: express.ErrorRequestHandler[];\n }[];\n}): Promise<express.Express> {\n const app = mcpServer.express;\n\n // Read `process.env.NODE_ENV` inline: wrangler/esbuild only substitute the literal expression,\n // so a local const would defeat dead-code elimination of the dev-only imports below.\n if (process.env.NODE_ENV !== \"production\") {\n const { devtoolsStaticServer } = await import(\"@skybridge/devtools\");\n app.use(await devtoolsStaticServer());\n\n const assetsPath = path.join(process.cwd(), \"dist\", \"assets\");\n app.use(\"/assets\", cors());\n app.use(\"/assets\", express.static(assetsPath));\n\n const { viewsDevServer } = await import(\"./viewsDevServer.js\");\n app.use(await viewsDevServer(httpServer));\n\n const controlPort = parseControlPort(process.env.__TUNNEL_CONTROL_PORT);\n if (controlPort !== null) {\n const { createTunnelProxyRouter } = await import(\n \"./tunnel-proxy-router.js\"\n );\n app.use(createTunnelProxyRouter(controlPort));\n } else if (process.env.__TUNNEL_CONTROL_PORT !== undefined) {\n console.warn(\n `Ignoring invalid __TUNNEL_CONTROL_PORT=${process.env.__TUNNEL_CONTROL_PORT}`,\n );\n }\n } else {\n const assetsPath = path.join(process.cwd(), \"dist\", \"assets\");\n\n app.use(\"/assets\", cors());\n app.use(\"/assets\", express.static(assetsPath));\n }\n\n app.use(\"/mcp\", mcpMiddleware(mcpServer));\n\n applyMiddlewares(app, errorMiddleware);\n\n app.use(\"/mcp\", defaultErrorHandler);\n\n return app;\n}\n\nconst mcpMiddleware = (server: McpServer): express.RequestHandler => {\n return async (\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n ) => {\n if (req.method !== \"POST\") {\n res.writeHead(405).end(\n JSON.stringify({\n jsonrpc: \"2.0\",\n error: {\n code: -32000,\n message: \"Method not allowed.\",\n },\n id: null,\n }),\n );\n return;\n }\n\n try {\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n // Respond with a single JSON body instead of SSE. Skybridge's stateless\n // transport never streams server-initiated messages, so SSE adds no\n // capability — and on workerd specifically, `cloudflare:node`'s http\n // bridge silently drops chunked writes that happen after the request\n // handler awaits, which manifests as a 200 with empty body for any\n // async tools/call.\n enableJsonResponse: true,\n });\n\n res.on(\"close\", () => {\n transport.close();\n });\n\n await server.connectStatelessTransport(transport);\n // Express strips the mount path from req.url (e.g. \"/mcp\" becomes \"/\").\n // Restore it so the SDK builds the correct requestInfo.url.\n req.url = req.originalUrl;\n await transport.handleRequest(req, res, req.body);\n } catch (error) {\n next(error);\n }\n };\n};\n"]}