skybridge 2.0.4 → 2.1.1

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 (139) hide show
  1. package/README.md +9 -9
  2. package/dist/cli/build-helpers.d.ts +1 -1
  3. package/dist/cli/build-helpers.js +9 -1
  4. package/dist/cli/build-helpers.js.map +1 -1
  5. package/dist/cli/build-helpers.test.js +24 -0
  6. package/dist/cli/build-helpers.test.js.map +1 -1
  7. package/dist/cli/build-steps.js +16 -5
  8. package/dist/cli/build-steps.js.map +1 -1
  9. package/dist/cli/build-steps.test.js +20 -0
  10. package/dist/cli/build-steps.test.js.map +1 -1
  11. package/dist/cli/resolve-skybridge-config.d.ts +1 -0
  12. package/dist/cli/resolve-skybridge-config.js +11 -1
  13. package/dist/cli/resolve-skybridge-config.js.map +1 -1
  14. package/dist/cli/run-plain.js +2 -1
  15. package/dist/cli/run-plain.js.map +1 -1
  16. package/dist/cli/use-nodemon.d.ts +11 -1
  17. package/dist/cli/use-nodemon.js +19 -5
  18. package/dist/cli/use-nodemon.js.map +1 -1
  19. package/dist/cli/use-typescript-check.js +4 -2
  20. package/dist/cli/use-typescript-check.js.map +1 -1
  21. package/dist/commands/dev.js +3 -3
  22. package/dist/commands/dev.js.map +1 -1
  23. package/dist/server/app.d.ts +22 -5
  24. package/dist/server/app.js +24 -13
  25. package/dist/server/app.js.map +1 -1
  26. package/dist/server/app.test.js +81 -0
  27. package/dist/server/app.test.js.map +1 -1
  28. package/dist/server/auth/providers/custom.d.ts +4 -2
  29. package/dist/server/auth/providers/custom.js.map +1 -1
  30. package/dist/server/auth/providers/workos.d.ts +3 -2
  31. package/dist/server/auth/providers/workos.js +2 -1
  32. package/dist/server/auth/providers/workos.js.map +1 -1
  33. package/dist/server/auth/verify.d.ts +5 -3
  34. package/dist/server/auth/verify.js.map +1 -1
  35. package/dist/server/auth/verify.test.js +16 -0
  36. package/dist/server/auth/verify.test.js.map +1 -1
  37. package/dist/server/events.d.ts +113 -0
  38. package/dist/server/events.js +318 -0
  39. package/dist/server/events.js.map +1 -0
  40. package/dist/server/events.test.d.ts +1 -0
  41. package/dist/server/events.test.js +172 -0
  42. package/dist/server/events.test.js.map +1 -0
  43. package/dist/server/express.d.ts +3 -2
  44. package/dist/server/express.js +17 -11
  45. package/dist/server/express.js.map +1 -1
  46. package/dist/server/express.test.js +45 -0
  47. package/dist/server/express.test.js.map +1 -1
  48. package/dist/server/form-input.d.ts +120 -0
  49. package/dist/server/form-input.js +89 -0
  50. package/dist/server/form-input.js.map +1 -0
  51. package/dist/server/index.d.ts +4 -2
  52. package/dist/server/index.js +3 -1
  53. package/dist/server/index.js.map +1 -1
  54. package/dist/server/mcp-errors.test.js +188 -0
  55. package/dist/server/mcp-errors.test.js.map +1 -1
  56. package/dist/server/metric.d.ts +5 -3
  57. package/dist/server/metric.js +5 -5
  58. package/dist/server/metric.js.map +1 -1
  59. package/dist/server/register-tool.test.js +203 -0
  60. package/dist/server/register-tool.test.js.map +1 -1
  61. package/dist/server/server.d.ts +255 -10
  62. package/dist/server/server.js +278 -9
  63. package/dist/server/server.js.map +1 -1
  64. package/dist/server/templateHelper.d.ts +1 -0
  65. package/dist/server/templateHelper.js.map +1 -1
  66. package/dist/server/templates.generated.js +26 -12
  67. package/dist/server/templates.generated.js.map +1 -1
  68. package/dist/server/tool-extra.test.js +79 -0
  69. package/dist/server/tool-extra.test.js.map +1 -1
  70. package/dist/server/view-resource-resolution.test.js +35 -0
  71. package/dist/server/view-resource-resolution.test.js.map +1 -1
  72. package/dist/web/bridges/adaptor.d.ts +14 -3
  73. package/dist/web/bridges/adaptor.js +164 -14
  74. package/dist/web/bridges/adaptor.js.map +1 -1
  75. package/dist/web/bridges/adaptor.test.js +62 -6
  76. package/dist/web/bridges/adaptor.test.js.map +1 -1
  77. package/dist/web/bridges/apps-sdk/bridge.js +1 -1
  78. package/dist/web/bridges/apps-sdk/bridge.js.map +1 -1
  79. package/dist/web/bridges/apps-sdk/types.d.ts +2 -3
  80. package/dist/web/bridges/apps-sdk/types.js.map +1 -1
  81. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.d.ts +3 -3
  82. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js +3 -3
  83. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js.map +1 -1
  84. package/dist/web/bridges/mcp-app/bridge.d.ts +1 -1
  85. package/dist/web/bridges/mcp-app/bridge.js +9 -2
  86. package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
  87. package/dist/web/bridges/mcp-app/types.d.ts +18 -3
  88. package/dist/web/bridges/mcp-app/types.js.map +1 -1
  89. package/dist/web/bridges/mcp-app/use-mcp-app-context.d.ts +2 -2
  90. package/dist/web/bridges/mcp-app/use-mcp-app-context.js +1 -1
  91. package/dist/web/bridges/mcp-app/use-mcp-app-context.js.map +1 -1
  92. package/dist/web/bridges/types.d.ts +57 -4
  93. package/dist/web/bridges/types.js +1 -1
  94. package/dist/web/bridges/types.js.map +1 -1
  95. package/dist/web/data-llm.js +3 -2
  96. package/dist/web/data-llm.js.map +1 -1
  97. package/dist/web/data-llm.test.js +6 -0
  98. package/dist/web/data-llm.test.js.map +1 -1
  99. package/dist/web/generate-helpers.d.ts +2 -2
  100. package/dist/web/generate-helpers.js +2 -2
  101. package/dist/web/generate-helpers.js.map +1 -1
  102. package/dist/web/hooks/index.d.ts +5 -1
  103. package/dist/web/hooks/index.js +5 -1
  104. package/dist/web/hooks/index.js.map +1 -1
  105. package/dist/web/hooks/openai-extensions.test.d.ts +1 -0
  106. package/dist/web/hooks/openai-extensions.test.js +107 -0
  107. package/dist/web/hooks/openai-extensions.test.js.map +1 -0
  108. package/dist/web/hooks/use-call-tool.d.ts +2 -2
  109. package/dist/web/hooks/use-call-tool.js +15 -10
  110. package/dist/web/hooks/use-call-tool.js.map +1 -1
  111. package/dist/web/hooks/use-call-tool.test.js +13 -2
  112. package/dist/web/hooks/use-call-tool.test.js.map +1 -1
  113. package/dist/web/hooks/use-deep-link.d.ts +22 -0
  114. package/dist/web/hooks/use-deep-link.js +34 -0
  115. package/dist/web/hooks/use-deep-link.js.map +1 -0
  116. package/dist/web/hooks/use-file-resource.d.ts +37 -0
  117. package/dist/web/hooks/use-file-resource.js +98 -0
  118. package/dist/web/hooks/use-file-resource.js.map +1 -0
  119. package/dist/web/hooks/use-file-resource.test.d.ts +1 -0
  120. package/dist/web/hooks/use-file-resource.test.js +65 -0
  121. package/dist/web/hooks/use-file-resource.test.js.map +1 -0
  122. package/dist/web/hooks/use-host.d.ts +27 -5
  123. package/dist/web/hooks/use-host.js +20 -5
  124. package/dist/web/hooks/use-host.js.map +1 -1
  125. package/dist/web/hooks/use-model-context.d.ts +44 -0
  126. package/dist/web/hooks/use-model-context.js +63 -0
  127. package/dist/web/hooks/use-model-context.js.map +1 -0
  128. package/dist/web/hooks/use-open-file.d.ts +20 -0
  129. package/dist/web/hooks/use-open-file.js +25 -0
  130. package/dist/web/hooks/use-open-file.js.map +1 -0
  131. package/dist/web/hooks/use-register-view-tool.d.ts +1 -1
  132. package/dist/web/hooks/use-register-view-tool.js +1 -1
  133. package/dist/web/hooks/use-register-view-tool.js.map +1 -1
  134. package/dist/web/hooks/use-send-follow-up-message.d.ts +8 -2
  135. package/dist/web/hooks/use-send-follow-up-message.js +6 -0
  136. package/dist/web/hooks/use-send-follow-up-message.js.map +1 -1
  137. package/dist/web/mount-view.js +3 -5
  138. package/dist/web/mount-view.js.map +1 -1
  139. package/package.json +6 -5
@@ -1 +1 @@
1
- {"version":3,"file":"tool-extra.test.js","sourceRoot":"","sources":["../../src/server/tool-extra.test.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EACL,MAAM,EACN,6BAA6B,GAC9B,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC7D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAErC,EAAE,CAAC,IAAI,CAAC,qBAAqB,EAAE,GAAG,EAAE,CAAC,CAAC;IACpC,oBAAoB,EAAE,GAAG,EAAE,CACzB,CAAC,CAAC,EAAW,EAAE,EAAW,EAAE,CAAa,EAAE,EAAE,CAAC,CAAC,EAAE,CAAmB;CACvE,CAAC,CAAC,CAAC;AACJ,EAAE,CAAC,IAAI,CAAC,qBAAqB,EAAE,GAAG,EAAE,CAAC,CAAC;IACpC,cAAc,EAAE,GAAG,EAAE,CACnB,CAAC,CAAC,EAAW,EAAE,EAAW,EAAE,CAAa,EAAE,EAAE,CAAC,CAAC,EAAE,CAAmB;CACvE,CAAC,CAAC,CAAC;AAEJ,IAAI,UAAmC,CAAC;AACxC,SAAS,CAAC,GAAG,EAAE,CAAC,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;AAErC,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;IAClC,EAAE,CAAC,uDAAuD,EAAE,KAAK,IAAI,EAAE;QACrE,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;QACnD,IAAI,UAAmB,CAAC;QACxB,MAAM,GAAG,GAAG,IAAI,SAAS,CAAC;YACxB,IAAI,EAAE,GAAG;YACT,OAAO,EAAE,OAAO;YAChB,OAAO,EAAE,CAAC,MAAM,EAAE,EAAE,CAClB,MAAM,CAAC,YAAY,CACjB;gBACE,IAAI,EAAE,OAAO;gBACb,WAAW,EAAE,qBAAqB;gBAClC,WAAW,EAAE,EAAE;aAChB,EACD,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;gBACf,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,eAAe,CAAC,CAAC;gBACnD,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;YACrD,CAAC,CACF;SACJ,CAAC,CAAC;QAEH,MAAM,UAAU,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QACvC,MAAM,UAAU,GAAG,MAAM,SAAS,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,CAAC;QACxD,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;QAChD,MAAM,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACvD,UAAU,GAAG,SAAS,CAAC;QACvB,MAAM,IAAI,GAAI,SAAS,CAAC,OAAO,EAAuB,CAAC,IAAI,CAAC;QAE5D,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;QAC3D,MAAM,MAAM,CAAC,OAAO,CAClB,IAAI,6BAA6B,CAC/B,IAAI,GAAG,CAAC,oBAAoB,IAAI,MAAM,CAAC,CACxC,CACF,CAAC;QACF,MAAM,MAAM,CAAC,QAAQ,CAAC;YACpB,IAAI,EAAE,OAAO;YACb,SAAS,EAAE,EAAE;YACb,KAAK,EAAE,EAAE,eAAe,EAAE,OAAO,EAAE;SACpC,CAAC,CAAC;QACH,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QAErB,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,4BAA4B,EAAE,GAAG,EAAE;IAC1C,EAAE,CAAC,oEAAoE,EAAE,KAAK,IAAI,EAAE;QAClF,MAAM,GAAG,GAAG,IAAI,SAAS,CAAC;YACxB,IAAI,EAAE,GAAG;YACT,OAAO,EAAE,OAAO;YAChB,OAAO,EAAE,CAAC,MAAM,EAAE,EAAE,CAClB,MAAM,CAAC,YAAY,CACjB,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,EAAE,WAAW,EAAE,EAAE,EAAE,EACrD,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CACpD;SACJ,CAAC,CAAC;QAEH,MAAM,KAAK,GAAG,MAAM,GAAG,CAAC,oBAAoB,EAAE,CAAC;QAE/C,MAAM,CAAC,KAAK,CAAC,eAAe,EAAE,CAAC,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,KAAK,IAAI,EAAE;QACrE,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;QACnD,MAAM,GAAG,GAAG,IAAI,SAAS,CAAC;YACxB,IAAI,EAAE,GAAG;YACT,OAAO,EAAE,OAAO;YAChB,OAAO,EAAE,CAAC,MAAM,EAAE,EAAE,CAClB,MAAM,CAAC,YAAY,CACjB;gBACE,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,qCAAqC;gBAClD,WAAW,EAAE,EAAE;gBACf,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE;aACzB,EACD,GAAG,EAAE,CAAC,CAAC;gBACL,iBAAiB,EAAE,EAAE;gBACrB,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;aACxC,CAAC,CACH;SACJ,CAAC,CAAC;QAEH,MAAM,UAAU,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QACvC,MAAM,UAAU,GAAG,MAAM,SAAS,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,CAAC;QACxD,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;QAChD,MAAM,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACvD,UAAU,GAAG,SAAS,CAAC;QACvB,MAAM,IAAI,GAAI,SAAS,CAAC,OAAO,EAAuB,CAAC,IAAI,CAAC;QAE5D,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,EAC/B,EAAE,kBAAkB,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,CACzC,CAAC;QACF,MAAM,MAAM,CAAC,OAAO,CAClB,IAAI,6BAA6B,CAC/B,IAAI,GAAG,CAAC,oBAAoB,IAAI,MAAM,CAAC,CACxC,CACF,CAAC;QACF,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,CAAC;QACxE,MAAM,UAAU,GAAG,MAAM,CAAC,4BAA4B,EAAE,CAAC;QACzD,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QAErB,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACtC,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["// @vitest-environment node\nimport http from \"node:http\";\nimport {\n Client,\n StreamableHTTPClientTransport,\n} from \"@modelcontextprotocol/client\";\nimport type { RequestHandler } from \"express\";\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { z } from \"zod\";\nimport { Skybridge } from \"./app.js\";\n\nvi.mock(\"@skybridge/devtools\", () => ({\n devtoolsStaticServer: () =>\n ((_r: unknown, _s: unknown, n: () => void) => n()) as RequestHandler,\n}));\nvi.mock(\"./viewsDevServer.js\", () => ({\n viewsDevServer: () =>\n ((_r: unknown, _s: unknown, n: () => void) => n()) as RequestHandler,\n}));\n\nlet openServer: http.Server | undefined;\nafterEach(() => openServer?.close());\n\ndescribe(\"tool handler extra\", () => {\n it(\"exposes the client hints the host sent on the request\", async () => {\n const { createApp } = await import(\"./express.js\");\n let seenLocale: unknown;\n const app = new Skybridge({\n name: \"t\",\n version: \"0.0.0\",\n handler: (server) =>\n server.registerTool(\n {\n name: \"hints\",\n description: \"Reads client hints.\",\n inputSchema: {},\n },\n (_args, extra) => {\n seenLocale = extra.mcpReq._meta?.[\"openai/locale\"];\n return { content: [{ type: \"text\", text: \"ok\" }] };\n },\n ),\n });\n\n const httpServer = http.createServer();\n const expressApp = await createApp({ app, httpServer });\n const listening = http.createServer(expressApp);\n await new Promise<void>((r) => listening.listen(0, r));\n openServer = listening;\n const port = (listening.address() as { port: number }).port;\n\n const client = new Client({ name: \"c\", version: \"0.0.0\" });\n await client.connect(\n new StreamableHTTPClientTransport(\n new URL(`http://localhost:${port}/mcp`),\n ),\n );\n await client.callTool({\n name: \"hints\",\n arguments: {},\n _meta: { \"openai/locale\": \"fr-FR\" },\n });\n await client.close();\n\n expect(seenLocale).toBe(\"fr-FR\");\n });\n});\n\ndescribe(\"stateless server instances\", () => {\n it(\"carries the registered capabilities onto each per-request instance\", async () => {\n const app = new Skybridge({\n name: \"t\",\n version: \"0.0.0\",\n handler: (server) =>\n server.registerTool(\n { name: \"a-tool\", description: \"d\", inputSchema: {} },\n () => ({ content: [{ type: \"text\", text: \"ok\" }] }),\n ),\n });\n\n const fresh = await app.createServerInstance();\n\n expect(fresh.getCapabilities().tools).toBeDefined();\n });\n\n it(\"serves a 2026-era caller with that era's result shape\", async () => {\n const { createApp } = await import(\"./express.js\");\n const app = new Skybridge({\n name: \"t\",\n version: \"0.0.0\",\n handler: (server) =>\n server.registerTool(\n {\n name: \"scalar\",\n description: \"Returns a scalar structuredContent.\",\n inputSchema: {},\n outputSchema: z.number(),\n },\n () => ({\n structuredContent: 42,\n content: [{ type: \"text\", text: \"42\" }],\n }),\n ),\n });\n\n const httpServer = http.createServer();\n const expressApp = await createApp({ app, httpServer });\n const listening = http.createServer(expressApp);\n await new Promise<void>((r) => listening.listen(0, r));\n openServer = listening;\n const port = (listening.address() as { port: number }).port;\n\n const client = new Client(\n { name: \"c\", version: \"0.0.0\" },\n { versionNegotiation: { mode: \"auto\" } },\n );\n await client.connect(\n new StreamableHTTPClientTransport(\n new URL(`http://localhost:${port}/mcp`),\n ),\n );\n const result = await client.callTool({ name: \"scalar\", arguments: {} });\n const negotiated = client.getNegotiatedProtocolVersion();\n await client.close();\n\n expect(negotiated).toBe(\"2026-07-28\");\n expect(result.structuredContent).toBe(42);\n });\n});\n"]}
1
+ {"version":3,"file":"tool-extra.test.js","sourceRoot":"","sources":["../../src/server/tool-extra.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\nimport http from \"node:http\";\nimport {\n Client,\n StreamableHTTPClientTransport,\n} from \"@modelcontextprotocol/client\";\nimport type { RequestHandler } from \"express\";\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { z } from \"zod\";\nimport { Skybridge } from \"./app.js\";\nimport { requestFormInput } from \"./form-input.js\";\n\nvi.mock(\"@skybridge/devtools\", () => ({\n devtoolsStaticServer: () =>\n ((_r: unknown, _s: unknown, n: () => void) => n()) as RequestHandler,\n}));\nvi.mock(\"./viewsDevServer.js\", () => ({\n viewsDevServer: () =>\n ((_r: unknown, _s: unknown, n: () => void) => n()) as RequestHandler,\n}));\n\nlet openServer: http.Server | undefined;\nafterEach(() => openServer?.close());\n\ndescribe(\"tool handler extra\", () => {\n it(\"exposes the client hints the host sent on the request\", async () => {\n const { createApp } = await import(\"./express.js\");\n let seenLocale: unknown;\n const app = new Skybridge({\n name: \"t\",\n version: \"0.0.0\",\n handler: (server) =>\n server.registerTool(\n {\n name: \"hints\",\n description: \"Reads client hints.\",\n inputSchema: {},\n },\n (_args, extra) => {\n seenLocale = extra.mcpReq._meta?.[\"openai/locale\"];\n return { content: [{ type: \"text\", text: \"ok\" }] };\n },\n ),\n });\n\n const httpServer = http.createServer();\n const expressApp = await createApp({ app, httpServer });\n const listening = http.createServer(expressApp);\n await new Promise<void>((r) => listening.listen(0, r));\n openServer = listening;\n const port = (listening.address() as { port: number }).port;\n\n const client = new Client({ name: \"c\", version: \"0.0.0\" });\n await client.connect(\n new StreamableHTTPClientTransport(\n new URL(`http://localhost:${port}/mcp`),\n ),\n );\n await client.callTool({\n name: \"hints\",\n arguments: {},\n _meta: { \"openai/locale\": \"fr-FR\" },\n });\n await client.close();\n\n expect(seenLocale).toBe(\"fr-FR\");\n });\n});\n\ndescribe(\"stateless server instances\", () => {\n it(\"carries the registered capabilities onto each per-request instance\", async () => {\n const app = new Skybridge({\n name: \"t\",\n version: \"0.0.0\",\n handler: (server) =>\n server.registerTool(\n { name: \"a-tool\", description: \"d\", inputSchema: {} },\n () => ({ content: [{ type: \"text\", text: \"ok\" }] }),\n ),\n });\n\n const fresh = await app.createServerInstance();\n\n expect(fresh.getCapabilities().tools).toBeDefined();\n });\n\n it(\"serves a 2026-era caller with that era's result shape\", async () => {\n const { createApp } = await import(\"./express.js\");\n const app = new Skybridge({\n name: \"t\",\n version: \"0.0.0\",\n handler: (server) =>\n server.registerTool(\n {\n name: \"scalar\",\n description: \"Returns a scalar structuredContent.\",\n inputSchema: {},\n outputSchema: z.number(),\n },\n () => ({\n structuredContent: 42,\n content: [{ type: \"text\", text: \"42\" }],\n }),\n ),\n });\n\n const httpServer = http.createServer();\n const expressApp = await createApp({ app, httpServer });\n const listening = http.createServer(expressApp);\n await new Promise<void>((r) => listening.listen(0, r));\n openServer = listening;\n const port = (listening.address() as { port: number }).port;\n\n const client = new Client(\n { name: \"c\", version: \"0.0.0\" },\n { versionNegotiation: { mode: \"auto\" } },\n );\n await client.connect(\n new StreamableHTTPClientTransport(\n new URL(`http://localhost:${port}/mcp`),\n ),\n );\n const result = await client.callTool({ name: \"scalar\", arguments: {} });\n const negotiated = client.getNegotiatedProtocolVersion();\n await client.close();\n\n expect(negotiated).toBe(\"2026-07-28\");\n expect(result.structuredContent).toBe(42);\n });\n\n it(\"asks for an OpenAI form over multi-round-trip requests\", async () => {\n const { createApp } = await import(\"./express.js\");\n const requestedSchema = {\n type: \"object\" as const,\n properties: {\n part: {\n type: \"string\" as const,\n oneOf: [{ const: \"bolt\", title: \"M6 bolt\", description: \"Fastener\" }],\n },\n drawing: {\n type: \"string\" as const,\n format: \"uri\" as const,\n \"x-openai-input\": {\n type: \"resource\" as const,\n options: [{ uri: \"cad://drawings/bolt\", name: \"bolt\" }],\n },\n },\n },\n required: [\"part\"],\n };\n const app = new Skybridge({\n name: \"t\",\n version: \"0.0.0\",\n handler: (server) =>\n server.registerTool({ name: \"pick-part\" }, (_args, extra) => {\n const result = requestFormInput(extra, {\n key: \"part\",\n message: \"Choose a part\",\n requestedSchema,\n });\n if (\"resultType\" in result) {\n return result;\n }\n return {\n content: `${result.action}:${result.action === \"accept\" && result.content.part}`,\n };\n }),\n });\n const httpServer = http.createServer();\n const expressApp = await createApp({ app, httpServer });\n const listening = http.createServer(expressApp);\n await new Promise<void>((r) => listening.listen(0, r));\n openServer = listening;\n const port = (listening.address() as { port: number }).port;\n\n const client = new Client(\n { name: \"c\", version: \"0.0.0\" },\n {\n capabilities: {\n elicitation: { form: {} },\n extensions: { \"openai/elicitation\": { form: {} } },\n },\n versionNegotiation: { mode: \"auto\" },\n },\n );\n const asked: unknown[] = [];\n let answer = { part: \"bolt\", drawing: \"cad://drawings/bolt\" };\n client.setRequestHandler(\"elicitation/create\", (request) => {\n asked.push(request.params);\n return { action: \"accept\", content: answer };\n });\n await client.connect(\n new StreamableHTTPClientTransport(\n new URL(`http://localhost:${port}/mcp`),\n ),\n );\n\n const result = await client.callTool({ name: \"pick-part\" });\n expect(result.content).toEqual([{ type: \"text\", text: \"accept:bolt\" }]);\n expect(asked).toEqual([\n {\n mode: \"form\",\n message: \"Choose a part\",\n requestedSchema: { type: \"object\", properties: {} },\n _meta: { \"openai/elicitation\": { requestedSchema } },\n },\n ]);\n\n for (answer of [\n { part: \"washer\", drawing: \"cad://drawings/bolt\" },\n { part: \"bolt\", drawing: \"cad://drawings/other\" },\n ]) {\n await expect(\n client.callTool({ name: \"pick-part\" }),\n ).resolves.toMatchObject({\n isError: true,\n content: [{ text: expect.stringContaining(\"does not match\") }],\n });\n }\n await client.close();\n });\n});\n"]}
@@ -157,6 +157,41 @@ describe("view resource resolution (cache key)", () => {
157
157
  await teardown();
158
158
  }
159
159
  });
160
+ it("preloads the view entry and its static imports (prod)", async () => {
161
+ process.env.NODE_ENV = "production";
162
+ __setBuildManifest({
163
+ "src/views/widget.tsx": {
164
+ isEntry: true,
165
+ name: "widget",
166
+ file: "assets/widget-ABC123.js",
167
+ imports: ["_shared.js", "_ui.js"],
168
+ dynamicImports: ["src/lazy.tsx"],
169
+ },
170
+ "_shared.js": { file: "assets/shared-DEF456.js" },
171
+ "_ui.js": { file: "assets/ui-GHI789.js", imports: ["_shared.js"] },
172
+ "src/lazy.tsx": { file: "assets/lazy-JKL012.js", isDynamicEntry: true },
173
+ "style.css": { file: "assets/style-XYZ.css" },
174
+ });
175
+ const { client, teardown } = await connect(registerWidget);
176
+ try {
177
+ const { contents } = await client.readResource({
178
+ uri: "ui://views/ext-apps/widget.html",
179
+ });
180
+ const { text } = textContent(contents);
181
+ const preloaded = [
182
+ ...text.matchAll(/<link rel="modulepreload" crossorigin href="[^"]*\/assets\/(assets\/[^"]+)" \/>/g),
183
+ ].map((match) => match[1]);
184
+ expect(preloaded).toEqual([
185
+ "assets/widget-ABC123.js",
186
+ "assets/shared-DEF456.js",
187
+ "assets/ui-GHI789.js",
188
+ ]);
189
+ expect(text.indexOf("modulepreload")).toBeLessThan(text.indexOf("import("));
190
+ }
191
+ finally {
192
+ await teardown();
193
+ }
194
+ });
160
195
  // Back-compat: older Skybridge advertised the view at `ui://views/apps-sdk/...`
161
196
  // via openai/outputTemplate. We no longer advertise it, but apps published then
162
197
  // have it cached, so the read must still resolve to the ext-apps content.
@@ -1 +1 @@
1
- {"version":3,"file":"view-resource-resolution.test.js","sourceRoot":"","sources":["../../src/server/view-resource-resolution.test.ts"],"names":[],"mappings":"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","sourcesContent":["import http from \"node:http\";\nimport {\n Client,\n InMemoryTransport,\n StreamableHTTPClientTransport,\n} from \"@modelcontextprotocol/client\";\nimport type { RequestHandler } from \"express\";\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { __setBuildManifest, type McpServer, Skybridge } from \"./index.js\";\n\nvi.mock(\"@skybridge/devtools\", () => ({\n devtoolsStaticServer: () =>\n ((_req: unknown, _res: unknown, next: () => void) =>\n next()) as RequestHandler,\n}));\nvi.mock(\"./viewsDevServer.js\", () => ({\n viewsDevServer: (_httpServer: unknown) =>\n ((_req: unknown, _res: unknown, next: () => void) =>\n next()) as RequestHandler,\n}));\n\n// Mirror what the Skybridge Vite plugin generates in `.skybridge/views.d.ts`:\n// narrow `ViewName` so `component: \"widget\"` typechecks against the registry.\ndeclare module \"./server.js\" {\n interface ViewNameRegistry {\n widget: true;\n }\n}\n\n// SKY-435: a view resource must resolve no matter the `?v=` query param value.\n// The version param is a content-derived cache key for external consumers\n// (it busts host/CDN caches when the bundle changes); it is not part of the\n// resource's identity, so a stale, absent, or arbitrary param must still serve\n// the underlying asset.\n\nfunction buildApp(register: (server: McpServer) => void) {\n return new Skybridge({\n name: \"test\",\n version: \"1.0.0\",\n handler: (server) => {\n register(server);\n return server;\n },\n });\n}\n\nasync function connect(register: (server: McpServer) => void) {\n const instance = await buildApp(register).createServerInstance();\n const client = new Client({ name: \"test-client\", version: \"1.0.0\" });\n const [clientTransport, serverTransport] =\n InMemoryTransport.createLinkedPair();\n await instance.connect(serverTransport);\n await client.connect(clientTransport);\n return {\n client,\n teardown: async () => {\n await client.close();\n await instance.close();\n },\n };\n}\n\nlet openHttpServer: http.Server | undefined;\nafterEach(() => openHttpServer?.close());\n\n// Connect over real HTTP so forwarded proxy headers reach the resource handler.\nasync function connectHttp(\n register: (server: McpServer) => void,\n headers: Record<string, string>,\n) {\n const { createApp } = await import(\"./express.js\");\n const app = buildApp(register);\n const httpServer = http.createServer();\n await createApp({ app, httpServer });\n const listening = http.createServer(app.express);\n await new Promise<void>((resolve) => listening.listen(0, resolve));\n openHttpServer = listening;\n const port = (listening.address() as { port: number }).port;\n\n const client = new Client({ name: \"test-client\", version: \"1.0.0\" });\n const transport = new StreamableHTTPClientTransport(\n new URL(`http://localhost:${port}/mcp`),\n { requestInit: { headers } },\n );\n await client.connect(transport);\n return {\n client,\n teardown: async () => {\n await client.close();\n },\n };\n}\n\nfunction registerWidget(server: McpServer) {\n server.registerTool(\n { name: \"show\", description: \"show\", view: { component: \"widget\" } },\n () => ({ content: [{ type: \"text\" as const, text: \"ok\" }] }),\n );\n}\n\n/** Narrow a read-resource content entry to its text variant. */\nfunction textContent(contents: Array<{ uri: string }>): {\n uri: string;\n text: string;\n} {\n return contents[0] as { uri: string; text: string };\n}\n\ndescribe(\"view resource resolution (cache key)\", () => {\n afterEach(() => {\n delete process.env.NODE_ENV;\n });\n\n it(\"resolves regardless of the ?v= param and echoes the requested URI\", async () => {\n const { client, teardown } = await connect(registerWidget);\n\n const { resources } = await client.listResources();\n const listed = resources.find((r) =>\n r.uri.startsWith(\"ui://views/ext-apps/widget.html\"),\n );\n // Dev build advertises no cache key.\n expect(listed?.uri).toBe(\"ui://views/ext-apps/widget.html\");\n\n // A stale/arbitrary/absent param all resolve the same underlying asset,\n // and the response echoes the URI the consumer asked for (never rewritten).\n for (const uri of [\n \"ui://views/ext-apps/widget.html?v=stale123\",\n \"ui://views/ext-apps/widget.html?v=whatever-else\",\n \"ui://views/ext-apps/widget.html\",\n ]) {\n const { contents } = await client.readResource({ uri });\n expect(contents).toHaveLength(1);\n const content = textContent(contents);\n expect(typeof content.text).toBe(\"string\");\n expect(content.text.length).toBeGreaterThan(0);\n expect(content.uri).toBe(uri);\n }\n\n await teardown();\n });\n\n it(\"resolves a stale cache key when the canonical URI carries ?v= (prod)\", async () => {\n process.env.NODE_ENV = \"production\";\n __setBuildManifest({\n \"src/views/widget.tsx\": {\n isEntry: true,\n name: \"widget\",\n file: \"assets/widget-ABC123.js\",\n },\n \"style.css\": { file: \"assets/style-XYZ.css\" },\n } as unknown as Record<string, { file: string }>);\n\n const { client, teardown } = await connect(registerWidget);\n\n const { resources } = await client.listResources();\n const listed = resources.find((r) =>\n r.uri.startsWith(\"ui://views/ext-apps/widget.html\"),\n );\n // Production advertises a content-derived cache key.\n expect(listed?.uri).toMatch(\n /^ui:\\/\\/views\\/ext-apps\\/widget\\.html\\?v=[0-9a-f]{8}$/,\n );\n\n // A consumer holding a stale key still resolves the current asset.\n const staleUri = \"ui://views/ext-apps/widget.html?v=00000000\";\n const { contents } = await client.readResource({ uri: staleUri });\n expect(contents).toHaveLength(1);\n const content = textContent(contents);\n expect(typeof content.text).toBe(\"string\");\n expect(content.uri).toBe(staleUri);\n\n await teardown();\n });\n\n it(\"still throws for an unknown view path\", async () => {\n const { client, teardown } = await connect(registerWidget);\n\n await expect(\n client.readResource({ uri: \"ui://views/ext-apps/nope.html?v=1\" }),\n ).rejects.toThrow();\n\n await teardown();\n });\n\n // A proxy routing the request under a path prefix sends x-forwarded-prefix;\n // the view's asset URLs must carry that prefix (and the forwarded origin) so\n // they resolve back through the same routing. Per-request, so one process can\n // serve many hosts/prefixes at once.\n it(\"prefixes emitted asset URLs with x-forwarded-prefix (prod)\", async () => {\n process.env.NODE_ENV = \"production\";\n __setBuildManifest({\n \"src/views/widget.tsx\": {\n isEntry: true,\n name: \"widget\",\n file: \"assets/widget-ABC123.js\",\n },\n \"style.css\": { file: \"assets/style-XYZ.css\" },\n } as unknown as Record<string, { file: string }>);\n\n const { client, teardown } = await connectHttp(registerWidget, {\n \"x-forwarded-host\": \"foo.com\",\n \"x-forwarded-proto\": \"https\",\n \"x-forwarded-prefix\": \"/v1/canary\",\n });\n try {\n const { contents } = await client.readResource({\n uri: \"ui://views/ext-apps/widget.html\",\n });\n const { text } = textContent(contents);\n // Forwarded origin + prefix. (`assets/assets` is the existing layout: the\n // manifest `file` already carries `assets/` and the template prepends\n // `/assets/`; build.tsx's `_redirects` collapses it on the asset host.)\n expect(text).toContain(\n \"https://foo.com/v1/canary/assets/assets/widget-ABC123.js\",\n );\n expect(text).toContain(\n \"https://foo.com/v1/canary/assets/assets/style-XYZ.css\",\n );\n // No unprefixed asset URL leaks through.\n expect(text).not.toContain(\"foo.com/assets/\");\n } finally {\n await teardown();\n }\n });\n\n // Back-compat: older Skybridge advertised the view at `ui://views/apps-sdk/...`\n // via openai/outputTemplate. We no longer advertise it, but apps published then\n // have it cached, so the read must still resolve to the ext-apps content.\n it(\"resolves the legacy apps-sdk URL to the ext-apps content\", async () => {\n const { client, teardown } = await connect(registerWidget);\n\n const legacyUri = \"ui://views/apps-sdk/widget.html\";\n const { contents } = await client.readResource({ uri: legacyUri });\n\n expect(contents).toHaveLength(1);\n const content = contents[0] as {\n uri: string;\n text: string;\n mimeType: string;\n };\n expect(content.mimeType).toBe(\"text/html;profile=mcp-app\");\n expect(content.text.length).toBeGreaterThan(0);\n // The response echoes the requested (legacy) URI, never the canonical one.\n expect(content.uri).toBe(legacyUri);\n\n await teardown();\n });\n});\n"]}
1
+ {"version":3,"file":"view-resource-resolution.test.js","sourceRoot":"","sources":["../../src/server/view-resource-resolution.test.ts"],"names":[],"mappings":"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","sourcesContent":["import http from \"node:http\";\nimport {\n Client,\n InMemoryTransport,\n StreamableHTTPClientTransport,\n} from \"@modelcontextprotocol/client\";\nimport type { RequestHandler } from \"express\";\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { __setBuildManifest, type McpServer, Skybridge } from \"./index.js\";\n\nvi.mock(\"@skybridge/devtools\", () => ({\n devtoolsStaticServer: () =>\n ((_req: unknown, _res: unknown, next: () => void) =>\n next()) as RequestHandler,\n}));\nvi.mock(\"./viewsDevServer.js\", () => ({\n viewsDevServer: (_httpServer: unknown) =>\n ((_req: unknown, _res: unknown, next: () => void) =>\n next()) as RequestHandler,\n}));\n\n// Mirror what the Skybridge Vite plugin generates in `.skybridge/views.d.ts`:\n// narrow `ViewName` so `component: \"widget\"` typechecks against the registry.\ndeclare module \"./server.js\" {\n interface ViewNameRegistry {\n widget: true;\n }\n}\n\n// SKY-435: a view resource must resolve no matter the `?v=` query param value.\n// The version param is a content-derived cache key for external consumers\n// (it busts host/CDN caches when the bundle changes); it is not part of the\n// resource's identity, so a stale, absent, or arbitrary param must still serve\n// the underlying asset.\n\nfunction buildApp(register: (server: McpServer) => void) {\n return new Skybridge({\n name: \"test\",\n version: \"1.0.0\",\n handler: (server) => {\n register(server);\n return server;\n },\n });\n}\n\nasync function connect(register: (server: McpServer) => void) {\n const instance = await buildApp(register).createServerInstance();\n const client = new Client({ name: \"test-client\", version: \"1.0.0\" });\n const [clientTransport, serverTransport] =\n InMemoryTransport.createLinkedPair();\n await instance.connect(serverTransport);\n await client.connect(clientTransport);\n return {\n client,\n teardown: async () => {\n await client.close();\n await instance.close();\n },\n };\n}\n\nlet openHttpServer: http.Server | undefined;\nafterEach(() => openHttpServer?.close());\n\n// Connect over real HTTP so forwarded proxy headers reach the resource handler.\nasync function connectHttp(\n register: (server: McpServer) => void,\n headers: Record<string, string>,\n) {\n const { createApp } = await import(\"./express.js\");\n const app = buildApp(register);\n const httpServer = http.createServer();\n await createApp({ app, httpServer });\n const listening = http.createServer(app.express);\n await new Promise<void>((resolve) => listening.listen(0, resolve));\n openHttpServer = listening;\n const port = (listening.address() as { port: number }).port;\n\n const client = new Client({ name: \"test-client\", version: \"1.0.0\" });\n const transport = new StreamableHTTPClientTransport(\n new URL(`http://localhost:${port}/mcp`),\n { requestInit: { headers } },\n );\n await client.connect(transport);\n return {\n client,\n teardown: async () => {\n await client.close();\n },\n };\n}\n\nfunction registerWidget(server: McpServer) {\n server.registerTool(\n { name: \"show\", description: \"show\", view: { component: \"widget\" } },\n () => ({ content: [{ type: \"text\" as const, text: \"ok\" }] }),\n );\n}\n\n/** Narrow a read-resource content entry to its text variant. */\nfunction textContent(contents: Array<{ uri: string }>): {\n uri: string;\n text: string;\n} {\n return contents[0] as { uri: string; text: string };\n}\n\ndescribe(\"view resource resolution (cache key)\", () => {\n afterEach(() => {\n delete process.env.NODE_ENV;\n });\n\n it(\"resolves regardless of the ?v= param and echoes the requested URI\", async () => {\n const { client, teardown } = await connect(registerWidget);\n\n const { resources } = await client.listResources();\n const listed = resources.find((r) =>\n r.uri.startsWith(\"ui://views/ext-apps/widget.html\"),\n );\n // Dev build advertises no cache key.\n expect(listed?.uri).toBe(\"ui://views/ext-apps/widget.html\");\n\n // A stale/arbitrary/absent param all resolve the same underlying asset,\n // and the response echoes the URI the consumer asked for (never rewritten).\n for (const uri of [\n \"ui://views/ext-apps/widget.html?v=stale123\",\n \"ui://views/ext-apps/widget.html?v=whatever-else\",\n \"ui://views/ext-apps/widget.html\",\n ]) {\n const { contents } = await client.readResource({ uri });\n expect(contents).toHaveLength(1);\n const content = textContent(contents);\n expect(typeof content.text).toBe(\"string\");\n expect(content.text.length).toBeGreaterThan(0);\n expect(content.uri).toBe(uri);\n }\n\n await teardown();\n });\n\n it(\"resolves a stale cache key when the canonical URI carries ?v= (prod)\", async () => {\n process.env.NODE_ENV = \"production\";\n __setBuildManifest({\n \"src/views/widget.tsx\": {\n isEntry: true,\n name: \"widget\",\n file: \"assets/widget-ABC123.js\",\n },\n \"style.css\": { file: \"assets/style-XYZ.css\" },\n } as unknown as Record<string, { file: string }>);\n\n const { client, teardown } = await connect(registerWidget);\n\n const { resources } = await client.listResources();\n const listed = resources.find((r) =>\n r.uri.startsWith(\"ui://views/ext-apps/widget.html\"),\n );\n // Production advertises a content-derived cache key.\n expect(listed?.uri).toMatch(\n /^ui:\\/\\/views\\/ext-apps\\/widget\\.html\\?v=[0-9a-f]{8}$/,\n );\n\n // A consumer holding a stale key still resolves the current asset.\n const staleUri = \"ui://views/ext-apps/widget.html?v=00000000\";\n const { contents } = await client.readResource({ uri: staleUri });\n expect(contents).toHaveLength(1);\n const content = textContent(contents);\n expect(typeof content.text).toBe(\"string\");\n expect(content.uri).toBe(staleUri);\n\n await teardown();\n });\n\n it(\"still throws for an unknown view path\", async () => {\n const { client, teardown } = await connect(registerWidget);\n\n await expect(\n client.readResource({ uri: \"ui://views/ext-apps/nope.html?v=1\" }),\n ).rejects.toThrow();\n\n await teardown();\n });\n\n // A proxy routing the request under a path prefix sends x-forwarded-prefix;\n // the view's asset URLs must carry that prefix (and the forwarded origin) so\n // they resolve back through the same routing. Per-request, so one process can\n // serve many hosts/prefixes at once.\n it(\"prefixes emitted asset URLs with x-forwarded-prefix (prod)\", async () => {\n process.env.NODE_ENV = \"production\";\n __setBuildManifest({\n \"src/views/widget.tsx\": {\n isEntry: true,\n name: \"widget\",\n file: \"assets/widget-ABC123.js\",\n },\n \"style.css\": { file: \"assets/style-XYZ.css\" },\n } as unknown as Record<string, { file: string }>);\n\n const { client, teardown } = await connectHttp(registerWidget, {\n \"x-forwarded-host\": \"foo.com\",\n \"x-forwarded-proto\": \"https\",\n \"x-forwarded-prefix\": \"/v1/canary\",\n });\n try {\n const { contents } = await client.readResource({\n uri: \"ui://views/ext-apps/widget.html\",\n });\n const { text } = textContent(contents);\n // Forwarded origin + prefix. (`assets/assets` is the existing layout: the\n // manifest `file` already carries `assets/` and the template prepends\n // `/assets/`; build.tsx's `_redirects` collapses it on the asset host.)\n expect(text).toContain(\n \"https://foo.com/v1/canary/assets/assets/widget-ABC123.js\",\n );\n expect(text).toContain(\n \"https://foo.com/v1/canary/assets/assets/style-XYZ.css\",\n );\n // No unprefixed asset URL leaks through.\n expect(text).not.toContain(\"foo.com/assets/\");\n } finally {\n await teardown();\n }\n });\n\n it(\"preloads the view entry and its static imports (prod)\", async () => {\n process.env.NODE_ENV = \"production\";\n __setBuildManifest({\n \"src/views/widget.tsx\": {\n isEntry: true,\n name: \"widget\",\n file: \"assets/widget-ABC123.js\",\n imports: [\"_shared.js\", \"_ui.js\"],\n dynamicImports: [\"src/lazy.tsx\"],\n },\n \"_shared.js\": { file: \"assets/shared-DEF456.js\" },\n \"_ui.js\": { file: \"assets/ui-GHI789.js\", imports: [\"_shared.js\"] },\n \"src/lazy.tsx\": { file: \"assets/lazy-JKL012.js\", isDynamicEntry: true },\n \"style.css\": { file: \"assets/style-XYZ.css\" },\n } as unknown as Record<string, { file: string }>);\n\n const { client, teardown } = await connect(registerWidget);\n try {\n const { contents } = await client.readResource({\n uri: \"ui://views/ext-apps/widget.html\",\n });\n const { text } = textContent(contents);\n const preloaded = [\n ...text.matchAll(\n /<link rel=\"modulepreload\" crossorigin href=\"[^\"]*\\/assets\\/(assets\\/[^\"]+)\" \\/>/g,\n ),\n ].map((match) => match[1]);\n expect(preloaded).toEqual([\n \"assets/widget-ABC123.js\",\n \"assets/shared-DEF456.js\",\n \"assets/ui-GHI789.js\",\n ]);\n expect(text.indexOf(\"modulepreload\")).toBeLessThan(\n text.indexOf(\"import(\"),\n );\n } finally {\n await teardown();\n }\n });\n\n // Back-compat: older Skybridge advertised the view at `ui://views/apps-sdk/...`\n // via openai/outputTemplate. We no longer advertise it, but apps published then\n // have it cached, so the read must still resolve to the ext-apps content.\n it(\"resolves the legacy apps-sdk URL to the ext-apps content\", async () => {\n const { client, teardown } = await connect(registerWidget);\n\n const legacyUri = \"ui://views/apps-sdk/widget.html\";\n const { contents } = await client.readResource({ uri: legacyUri });\n\n expect(contents).toHaveLength(1);\n const content = contents[0] as {\n uri: string;\n text: string;\n mimeType: string;\n };\n expect(content.mimeType).toBe(\"text/html;profile=mcp-app\");\n expect(content.text.length).toBeGreaterThan(0);\n // The response echoes the requested (legacy) URI, never the canonical one.\n expect(content.uri).toBe(legacyUri);\n\n await teardown();\n });\n});\n"]}
@@ -1,4 +1,4 @@
1
- import type { Adaptor, AnyViewToolHandler, CallToolResponse, DownloadParams, DownloadResult, FileMetadata, HostContext, HostContextStore, OpenExternalOptions, RequestDisplayMode, RequestModalOptions, RequestSizeOptions, SendFollowUpMessageOptions, SetViewStateAction, UploadFileOptions, ViewToolConfig } from "./types.js";
1
+ import type { Adaptor, AnyViewToolHandler, CallToolOptions, CallToolResponse, DownloadParams, DownloadResult, FileMetadata, FileResource, FileResourceContent, FileResourceWriteResult, FollowUpMessage, HostContext, HostContextStore, ModelContextParams, OpenExternalOptions, RequestDisplayMode, RequestModalOptions, RequestSizeOptions, ResourceRepresentation, SendFollowUpMessageOptions, SetViewStateAction, UploadFileOptions, ViewToolConfig } from "./types.js";
2
2
  /**
3
3
  * @internal
4
4
  * Single composite implementation of {@link Adaptor}. Composes the MCP App
@@ -15,16 +15,20 @@ export declare class HostAdaptor implements Adaptor {
15
15
  private _viewState;
16
16
  private readonly viewStateListeners;
17
17
  private _viewUUID;
18
+ private _modelContext;
19
+ private readonly resourceListeners;
20
+ private resourceApp?;
18
21
  private _polyfillDisplay;
19
22
  private readonly polyfillDisplayListeners;
20
23
  private readonly polyfillDisplayStore;
21
24
  private readonly polyfillViewStateStore;
22
25
  private unsubscribeViewUUID;
26
+ private unsubscribeModelContext;
23
27
  constructor();
24
28
  /** @internal Release any subscriptions held on the underlying bridges. */
25
29
  cleanup(): void;
26
30
  getHostContextStore: <K extends keyof HostContext>(key: K) => HostContextStore<K>;
27
- callTool: <ToolArgs extends Record<string, unknown> | null = null, ToolResponse extends CallToolResponse = CallToolResponse>(name: string, args: ToolArgs) => Promise<ToolResponse>;
31
+ callTool: <ToolArgs extends Record<string, unknown> | null = null, ToolResponse extends CallToolResponse = CallToolResponse>(name: string, args: ToolArgs, options?: CallToolOptions) => Promise<ToolResponse>;
28
32
  registerViewTool: (config: ViewToolConfig, handler: AnyViewToolHandler) => (() => void);
29
33
  requestDisplayMode: (mode: RequestDisplayMode) => Promise<{
30
34
  [x: string]: unknown;
@@ -32,10 +36,17 @@ export declare class HostAdaptor implements Adaptor {
32
36
  }>;
33
37
  requestClose: () => Promise<void>;
34
38
  requestSize: (size: RequestSizeOptions) => Promise<void>;
35
- sendFollowUpMessage: (prompt: string, options?: SendFollowUpMessageOptions) => Promise<void>;
39
+ sendFollowUpMessage: (prompt: FollowUpMessage, options?: SendFollowUpMessageOptions) => Promise<void>;
36
40
  openExternal: (href: string, options?: OpenExternalOptions) => void;
37
41
  download: (params: DownloadParams) => Promise<DownloadResult>;
38
42
  setViewState: (stateOrUpdater: SetViewStateAction) => Promise<void>;
43
+ updateModelContext: (params: ModelContextParams) => Promise<string | undefined>;
44
+ private sendModelContext;
45
+ openFile: (path: string) => Promise<void>;
46
+ readResource: (uri: string, representation?: ResourceRepresentation) => Promise<FileResource>;
47
+ watchResource: (uri: string, onUpdate: () => void) => (() => void);
48
+ writeResource: (uri: string, content: FileResourceContent, ifMatch?: string) => Promise<FileResourceWriteResult>;
49
+ private getResourceApp;
39
50
  uploadFile: (file: File, options?: UploadFileOptions) => Promise<FileMetadata>;
40
51
  getFileDownloadUrl: (file: FileMetadata) => Promise<{
41
52
  downloadUrl: string;
@@ -1,7 +1,17 @@
1
+ import { ResultSchema } from "@modelcontextprotocol/core";
2
+ import * as z from "zod/v4";
1
3
  import { warnOnLargeViewState } from "../../context-warnings.js";
2
4
  import { AppsSdkBridge } from "./apps-sdk/bridge.js";
3
5
  import { McpAppBridge } from "./mcp-app/bridge.js";
4
6
  import { NotSupportedError } from "./types.js";
7
+ const ResourceMetaSchema = z.object({
8
+ writable: z.boolean().optional(),
9
+ etag: z.string().optional(),
10
+ });
11
+ const WriteResultSchema = z.discriminatedUnion("outcome", [
12
+ z.object({ outcome: z.enum(["saved", "conflict"]), etag: z.string() }),
13
+ z.object({ outcome: z.literal("too-large"), maxBytes: z.number() }),
14
+ ]);
5
15
  const STORAGE_PREFIX = "sb:";
6
16
  const MAX_STORAGE_ENTRIES = 200;
7
17
  function isImage(mimeType) {
@@ -41,11 +51,15 @@ export class HostAdaptor {
41
51
  _viewState = null;
42
52
  viewStateListeners = new Set();
43
53
  _viewUUID = null;
54
+ _modelContext = null;
55
+ resourceListeners = new Map();
56
+ resourceApp;
44
57
  _polyfillDisplay = { mode: "inline" };
45
58
  polyfillDisplayListeners = new Set();
46
59
  polyfillDisplayStore;
47
60
  polyfillViewStateStore;
48
61
  unsubscribeViewUUID = null;
62
+ unsubscribeModelContext = null;
49
63
  constructor() {
50
64
  this.mcp = McpAppBridge.getInstance();
51
65
  let overlayStores = null;
@@ -79,20 +93,29 @@ export class HostAdaptor {
79
93
  viewState: overlayStores?.viewState ?? this.polyfillViewStateStore,
80
94
  };
81
95
  this.subscribeToViewUUID();
96
+ this.unsubscribeModelContext = this.mcp.subscribe("openai/modelContext")(() => {
97
+ if (this.mcp.getSnapshot("openai/modelContext") === null) {
98
+ this._modelContext = null;
99
+ }
100
+ });
82
101
  }
83
102
  /** @internal Release any subscriptions held on the underlying bridges. */
84
103
  cleanup() {
85
104
  this.unsubscribeViewUUID?.();
86
105
  this.unsubscribeViewUUID = null;
106
+ this.unsubscribeModelContext?.();
107
+ this.unsubscribeModelContext = null;
87
108
  }
88
109
  // ---- Adaptor interface ----
89
110
  getHostContextStore = (key) => this.stores[key];
90
- callTool = async (name, args) => {
111
+ callTool = async (name, args, options) => {
112
+ const timeout = options?.timeout;
113
+ if (timeout !== undefined && (!Number.isFinite(timeout) || timeout <= 0)) {
114
+ throw new RangeError("Tool call timeout must be a positive, finite number of milliseconds.");
115
+ }
91
116
  const app = await this.mcp.getApp();
92
- const response = await app.callServerTool({
93
- name,
94
- arguments: args ?? undefined,
95
- });
117
+ const params = { name, arguments: args ?? undefined };
118
+ const response = await app.callServerTool(params, { timeout });
96
119
  return {
97
120
  content: response.content,
98
121
  structuredContent: response.structuredContent ?? {},
@@ -119,7 +142,8 @@ export class HostAdaptor {
119
142
  await app.sendSizeChanged(size);
120
143
  };
121
144
  sendFollowUpMessage = async (prompt, options) => {
122
- if (this.openai) {
145
+ const target = options?.target;
146
+ if (this.openai && typeof prompt === "string" && target !== "new") {
123
147
  await this.openai.sendFollowUpMessage({
124
148
  prompt,
125
149
  scrollToBottom: options?.scrollToBottom,
@@ -127,10 +151,18 @@ export class HostAdaptor {
127
151
  return;
128
152
  }
129
153
  const app = await this.mcp.getApp();
130
- await app.sendMessage({
154
+ if (target === "new" &&
155
+ !app.getHostCapabilities()?.experimental?.["openai/message"]) {
156
+ throw new NotSupportedError("sendFollowUpMessage", "the host does not advertise openai/message, so it can't open a new conversation");
157
+ }
158
+ const result = await app.sendMessage({
131
159
  role: "user",
132
- content: [{ type: "text", text: prompt }],
160
+ content: typeof prompt === "string" ? [{ type: "text", text: prompt }] : prompt,
161
+ ...(target === "new" && { _meta: { "openai/message": { target } } }),
133
162
  });
163
+ if (result?.isError) {
164
+ throw new Error("The host rejected the follow-up message.");
165
+ }
134
166
  };
135
167
  openExternal = (href, options) => {
136
168
  if (this.openai) {
@@ -176,20 +208,135 @@ export class HostAdaptor {
176
208
  });
177
209
  this.persistToLocalStorage(newState);
178
210
  try {
179
- const app = await this.mcp.getApp();
180
- await app.updateModelContext({
181
- structuredContent: newState,
182
- content: [{ type: "text", text: JSON.stringify(newState) }],
183
- });
211
+ await this.sendModelContext();
184
212
  }
185
213
  catch (error) {
186
214
  console.error("Failed to update view state in MCP App.", error);
187
215
  }
188
216
  };
217
+ updateModelContext = async (params) => {
218
+ const app = await this.mcp.getApp();
219
+ const capabilities = app.getHostCapabilities();
220
+ if (!capabilities?.updateModelContext &&
221
+ !capabilities?.experimental?.["openai/modelContext"]) {
222
+ throw new NotSupportedError("updateModelContext", "the host does not advertise updateModelContext");
223
+ }
224
+ const previous = this._modelContext;
225
+ this._modelContext = params;
226
+ const result = await this.sendModelContext().catch((error) => {
227
+ if (this._modelContext === params) {
228
+ this._modelContext = previous;
229
+ }
230
+ throw error;
231
+ });
232
+ const parsed = z
233
+ .object({ updateId: z.string() })
234
+ .safeParse(result._meta?.["openai/modelContext"]);
235
+ return parsed.success ? parsed.data.updateId : undefined;
236
+ };
237
+ async sendModelContext() {
238
+ const viewState = this.openai ? null : this._viewState;
239
+ const extra = this._modelContext;
240
+ const collisions = Object.keys(extra?.structuredContent ?? {}).filter((key) => viewState !== null && key in viewState);
241
+ if (collisions.length > 0) {
242
+ console.warn(`skybridge: view state overrides model context keys ${collisions.join(", ")}.`);
243
+ }
244
+ const structuredContent = { ...extra?.structuredContent, ...viewState };
245
+ const app = await this.mcp.getApp();
246
+ return app.updateModelContext({
247
+ ...(Object.keys(structuredContent).length > 0 && { structuredContent }),
248
+ content: [
249
+ ...(viewState === null
250
+ ? []
251
+ : [
252
+ {
253
+ type: "text",
254
+ text: JSON.stringify(viewState),
255
+ annotations: { audience: ["assistant"] },
256
+ },
257
+ ]),
258
+ ...(extra?.content ?? []),
259
+ ],
260
+ });
261
+ }
262
+ openFile = async (path) => {
263
+ const app = await this.mcp.getApp();
264
+ if (!app.getHostCapabilities()?.experimental?.["openai/files"]) {
265
+ throw new NotSupportedError("openFile", "the host does not advertise openai/files");
266
+ }
267
+ await app.request({ method: "openai/files/open", params: { path } }, ResultSchema);
268
+ };
269
+ readResource = async (uri, representation) => {
270
+ const app = await this.mcp.getApp();
271
+ const { contents } = await app.readServerResource({
272
+ uri,
273
+ ...(representation && {
274
+ _meta: { "openai/resource": { representation } },
275
+ }),
276
+ });
277
+ const item = contents.find((content) => content.uri === uri) ?? contents[0];
278
+ if (!item) {
279
+ throw new Error(`The host returned no contents for ${uri}.`);
280
+ }
281
+ const { data: meta } = ResourceMetaSchema.safeParse(item._meta?.["openai/resource"]);
282
+ return {
283
+ ...("text" in item ? { text: item.text } : { blob: item.blob }),
284
+ mimeType: item.mimeType,
285
+ writable: meta?.writable ?? false,
286
+ etag: meta?.etag,
287
+ };
288
+ };
289
+ watchResource = (uri, onUpdate) => {
290
+ const listeners = this.resourceListeners.get(uri) ?? new Set();
291
+ if (listeners.size === 0) {
292
+ this.resourceListeners.set(uri, listeners);
293
+ this.getResourceApp()
294
+ .then((app) => app?.request({ method: "resources/subscribe", params: { uri } }, ResultSchema))
295
+ .catch((error) => {
296
+ console.warn(`Failed to subscribe to ${uri}.`, error);
297
+ });
298
+ }
299
+ listeners.add(onUpdate);
300
+ return () => {
301
+ if (listeners.delete(onUpdate) && listeners.size === 0) {
302
+ this.resourceListeners.delete(uri);
303
+ this.getResourceApp()
304
+ .then((app) => app?.request({ method: "resources/unsubscribe", params: { uri } }, ResultSchema))
305
+ .catch(() => { });
306
+ }
307
+ };
308
+ };
309
+ writeResource = async (uri, content, ifMatch) => {
310
+ const app = await this.getResourceApp();
311
+ if (!app) {
312
+ throw new NotSupportedError("writeResource", "the host does not advertise openai/resource");
313
+ }
314
+ return app.request({
315
+ method: "openai/resources/write",
316
+ params: { uri, ...content, ...(ifMatch && { ifMatch }) },
317
+ }, WriteResultSchema);
318
+ };
319
+ getResourceApp() {
320
+ this.resourceApp ??= this.mcp.getApp().then((app) => {
321
+ if (!app.getHostCapabilities()?.experimental?.["openai/resource"]) {
322
+ return undefined;
323
+ }
324
+ app.setNotificationHandler("notifications/resources/updated", ({ params }) => {
325
+ this.resourceListeners.get(params.uri)?.forEach((l) => {
326
+ l();
327
+ });
328
+ });
329
+ return app;
330
+ });
331
+ return this.resourceApp;
332
+ }
189
333
  uploadFile = async (file, options) => {
190
334
  if (!this.openai) {
191
335
  throw new NotSupportedError("uploadFile");
192
336
  }
337
+ if (!this.openai.uploadFile) {
338
+ throw new NotSupportedError("uploadFile", "not available on the current host version");
339
+ }
193
340
  const metadata = await this.openai.uploadFile(file, options);
194
341
  if (isImage(metadata.mimeType ?? file.type)) {
195
342
  await this.trackFileIds(metadata.fileId);
@@ -200,6 +347,9 @@ export class HostAdaptor {
200
347
  if (!this.openai) {
201
348
  return Promise.reject(new NotSupportedError("getFileDownloadUrl"));
202
349
  }
350
+ if (!this.openai.getFileDownloadUrl) {
351
+ return Promise.reject(new NotSupportedError("getFileDownloadUrl", "not available on the current host version"));
352
+ }
203
353
  return this.openai.getFileDownloadUrl(file);
204
354
  };
205
355
  selectFiles = async () => {
@@ -207,7 +357,7 @@ export class HostAdaptor {
207
357
  throw new NotSupportedError("selectFiles");
208
358
  }
209
359
  if (!this.openai.selectFiles) {
210
- throw new Error("selectFiles is not supported by the current host version.");
360
+ throw new NotSupportedError("selectFiles", "not available on the current host version");
211
361
  }
212
362
  const files = await this.openai.selectFiles();
213
363
  const imageIds = [];