skybridge 1.1.2 → 1.2.0

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 (151) hide show
  1. package/dist/commands/dev.js +1 -3
  2. package/dist/commands/dev.js.map +1 -1
  3. package/dist/server/auth/discovery.d.ts +32 -0
  4. package/dist/server/auth/discovery.js +56 -0
  5. package/dist/server/auth/discovery.js.map +1 -0
  6. package/dist/server/auth/discovery.test.d.ts +1 -0
  7. package/dist/server/auth/discovery.test.js +93 -0
  8. package/dist/server/auth/discovery.test.js.map +1 -0
  9. package/dist/server/auth/index.d.ts +18 -0
  10. package/dist/server/auth/index.js +2 -0
  11. package/dist/server/auth/index.js.map +1 -0
  12. package/dist/server/auth/providers/auth0.d.ts +18 -0
  13. package/dist/server/auth/providers/auth0.js +31 -0
  14. package/dist/server/auth/providers/auth0.js.map +1 -0
  15. package/dist/server/auth/providers/auth0.test.d.ts +1 -0
  16. package/dist/server/auth/providers/auth0.test.js +48 -0
  17. package/dist/server/auth/providers/auth0.test.js.map +1 -0
  18. package/dist/server/auth/providers/clerk.d.ts +14 -0
  19. package/dist/server/auth/providers/clerk.js +16 -0
  20. package/dist/server/auth/providers/clerk.js.map +1 -0
  21. package/dist/server/auth/providers/clerk.test.d.ts +1 -0
  22. package/dist/server/auth/providers/clerk.test.js +28 -0
  23. package/dist/server/auth/providers/clerk.test.js.map +1 -0
  24. package/dist/server/auth/providers/custom.d.ts +24 -0
  25. package/dist/server/auth/providers/custom.js +37 -0
  26. package/dist/server/auth/providers/custom.js.map +1 -0
  27. package/dist/server/auth/providers/custom.test.d.ts +1 -0
  28. package/dist/server/auth/providers/custom.test.js +107 -0
  29. package/dist/server/auth/providers/custom.test.js.map +1 -0
  30. package/dist/server/auth/providers/descope.d.ts +15 -0
  31. package/dist/server/auth/providers/descope.js +33 -0
  32. package/dist/server/auth/providers/descope.js.map +1 -0
  33. package/dist/server/auth/providers/descope.test.d.ts +1 -0
  34. package/dist/server/auth/providers/descope.test.js +37 -0
  35. package/dist/server/auth/providers/descope.test.js.map +1 -0
  36. package/dist/server/auth/providers/shared.d.ts +2 -0
  37. package/dist/server/auth/providers/shared.js +6 -0
  38. package/dist/server/auth/providers/shared.js.map +1 -0
  39. package/dist/server/auth/providers/shared.test.d.ts +1 -0
  40. package/dist/server/auth/providers/shared.test.js +10 -0
  41. package/dist/server/auth/providers/shared.test.js.map +1 -0
  42. package/dist/server/auth/providers/stytch.d.ts +12 -0
  43. package/dist/server/auth/providers/stytch.js +13 -0
  44. package/dist/server/auth/providers/stytch.js.map +1 -0
  45. package/dist/server/auth/providers/workos.d.ts +11 -0
  46. package/dist/server/auth/providers/workos.js +12 -0
  47. package/dist/server/auth/providers/workos.js.map +1 -0
  48. package/dist/server/auth/setup.d.ts +4 -0
  49. package/dist/server/auth/setup.js +51 -0
  50. package/dist/server/auth/setup.js.map +1 -0
  51. package/dist/server/auth/setup.test.d.ts +1 -0
  52. package/dist/server/auth/setup.test.js +185 -0
  53. package/dist/server/auth/setup.test.js.map +1 -0
  54. package/dist/server/auth/verify.d.ts +12 -0
  55. package/dist/server/auth/verify.js +38 -0
  56. package/dist/server/auth/verify.js.map +1 -0
  57. package/dist/server/auth/verify.test.d.ts +1 -0
  58. package/dist/server/auth/verify.test.js +100 -0
  59. package/dist/server/auth/verify.test.js.map +1 -0
  60. package/dist/server/express.test.js +31 -0
  61. package/dist/server/express.test.js.map +1 -1
  62. package/dist/server/index.d.ts +8 -1
  63. package/dist/server/index.js +6 -0
  64. package/dist/server/index.js.map +1 -1
  65. package/dist/server/requestOrigin.d.ts +7 -0
  66. package/dist/server/requestOrigin.js +25 -0
  67. package/dist/server/requestOrigin.js.map +1 -0
  68. package/dist/server/server.d.ts +30 -7
  69. package/dist/server/server.js +77 -123
  70. package/dist/server/server.js.map +1 -1
  71. package/dist/server/view-resource-resolution.test.js +24 -9
  72. package/dist/server/view-resource-resolution.test.js.map +1 -1
  73. package/dist/test/view.test.js +69 -101
  74. package/dist/test/view.test.js.map +1 -1
  75. package/dist/web/bridges/adaptor.d.ts +51 -0
  76. package/dist/web/bridges/adaptor.js +310 -0
  77. package/dist/web/bridges/adaptor.js.map +1 -0
  78. package/dist/web/bridges/adaptor.test.d.ts +1 -0
  79. package/dist/web/bridges/adaptor.test.js +197 -0
  80. package/dist/web/bridges/adaptor.test.js.map +1 -0
  81. package/dist/web/bridges/apps-sdk/bridge.d.ts +6 -2
  82. package/dist/web/bridges/apps-sdk/bridge.js +15 -4
  83. package/dist/web/bridges/apps-sdk/bridge.js.map +1 -1
  84. package/dist/web/bridges/apps-sdk/index.d.ts +0 -1
  85. package/dist/web/bridges/apps-sdk/index.js +0 -1
  86. package/dist/web/bridges/apps-sdk/index.js.map +1 -1
  87. package/dist/web/bridges/get-adaptor.d.ts +4 -4
  88. package/dist/web/bridges/get-adaptor.js +6 -11
  89. package/dist/web/bridges/get-adaptor.js.map +1 -1
  90. package/dist/web/bridges/get-adaptor.test.d.ts +1 -0
  91. package/dist/web/bridges/get-adaptor.test.js +32 -0
  92. package/dist/web/bridges/get-adaptor.test.js.map +1 -0
  93. package/dist/web/bridges/mcp-app/bridge.d.ts +13 -2
  94. package/dist/web/bridges/mcp-app/bridge.js +46 -4
  95. package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
  96. package/dist/web/bridges/mcp-app/bridge.test.d.ts +1 -0
  97. package/dist/web/bridges/mcp-app/bridge.test.js +17 -0
  98. package/dist/web/bridges/mcp-app/bridge.test.js.map +1 -0
  99. package/dist/web/bridges/mcp-app/index.d.ts +0 -1
  100. package/dist/web/bridges/mcp-app/index.js +0 -1
  101. package/dist/web/bridges/mcp-app/index.js.map +1 -1
  102. package/dist/web/bridges/mcp-app/view-tools.test.js +5 -6
  103. package/dist/web/bridges/mcp-app/view-tools.test.js.map +1 -1
  104. package/dist/web/bridges/types.d.ts +10 -0
  105. package/dist/web/bridges/types.js +14 -1
  106. package/dist/web/bridges/types.js.map +1 -1
  107. package/dist/web/bridges/types.test.d.ts +1 -0
  108. package/dist/web/bridges/types.test.js +19 -0
  109. package/dist/web/bridges/types.test.js.map +1 -0
  110. package/dist/web/components/modal-provider.js +4 -3
  111. package/dist/web/components/modal-provider.js.map +1 -1
  112. package/dist/web/create-store.test.js +8 -5
  113. package/dist/web/create-store.test.js.map +1 -1
  114. package/dist/web/data-llm.test.js +9 -5
  115. package/dist/web/data-llm.test.js.map +1 -1
  116. package/dist/web/hooks/use-call-tool.test.js +37 -23
  117. package/dist/web/hooks/use-call-tool.test.js.map +1 -1
  118. package/dist/web/hooks/use-display-mode.test.js +56 -20
  119. package/dist/web/hooks/use-display-mode.test.js.map +1 -1
  120. package/dist/web/hooks/use-download.test.js +11 -3
  121. package/dist/web/hooks/use-download.test.js.map +1 -1
  122. package/dist/web/hooks/use-files.test.js +10 -2
  123. package/dist/web/hooks/use-files.test.js.map +1 -1
  124. package/dist/web/hooks/use-layout.test.js +59 -26
  125. package/dist/web/hooks/use-layout.test.js.map +1 -1
  126. package/dist/web/hooks/use-open-external.test.js +13 -3
  127. package/dist/web/hooks/use-open-external.test.js.map +1 -1
  128. package/dist/web/hooks/use-request-close.test.js +35 -40
  129. package/dist/web/hooks/use-request-close.test.js.map +1 -1
  130. package/dist/web/hooks/use-request-modal.test.js +10 -0
  131. package/dist/web/hooks/use-request-modal.test.js.map +1 -1
  132. package/dist/web/hooks/use-request-size.test.js +35 -53
  133. package/dist/web/hooks/use-request-size.test.js.map +1 -1
  134. package/dist/web/hooks/use-set-open-in-app-url.test.js +28 -8
  135. package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -1
  136. package/dist/web/hooks/use-tool-info.test.js +37 -27
  137. package/dist/web/hooks/use-tool-info.test.js.map +1 -1
  138. package/dist/web/hooks/use-user.test.js +81 -35
  139. package/dist/web/hooks/use-user.test.js.map +1 -1
  140. package/dist/web/hooks/use-view-state.test.js +6 -2
  141. package/dist/web/hooks/use-view-state.test.js.map +1 -1
  142. package/package.json +3 -2
  143. package/dist/cli/use-open-tunnel-browser.d.ts +0 -6
  144. package/dist/cli/use-open-tunnel-browser.js +0 -19
  145. package/dist/cli/use-open-tunnel-browser.js.map +0 -1
  146. package/dist/web/bridges/apps-sdk/adaptor.d.ts +0 -29
  147. package/dist/web/bridges/apps-sdk/adaptor.js +0 -117
  148. package/dist/web/bridges/apps-sdk/adaptor.js.map +0 -1
  149. package/dist/web/bridges/mcp-app/adaptor.d.ts +0 -53
  150. package/dist/web/bridges/mcp-app/adaptor.js +0 -283
  151. package/dist/web/bridges/mcp-app/adaptor.js.map +0 -1
@@ -36,15 +36,15 @@ describe("view resource resolution (cache key)", () => {
36
36
  it("resolves regardless of the ?v= param and echoes the requested URI", async () => {
37
37
  const { client, teardown } = await connect(registerWidget);
38
38
  const { resources } = await client.listResources();
39
- const listed = resources.find((r) => r.uri.startsWith("ui://views/apps-sdk/widget.html"));
39
+ const listed = resources.find((r) => r.uri.startsWith("ui://views/ext-apps/widget.html"));
40
40
  // Dev build advertises no cache key.
41
- expect(listed?.uri).toBe("ui://views/apps-sdk/widget.html");
41
+ expect(listed?.uri).toBe("ui://views/ext-apps/widget.html");
42
42
  // A stale/arbitrary/absent param all resolve the same underlying asset,
43
43
  // and the response echoes the URI the consumer asked for (never rewritten).
44
44
  for (const uri of [
45
- "ui://views/apps-sdk/widget.html?v=stale123",
46
- "ui://views/apps-sdk/widget.html?v=whatever-else",
47
- "ui://views/apps-sdk/widget.html",
45
+ "ui://views/ext-apps/widget.html?v=stale123",
46
+ "ui://views/ext-apps/widget.html?v=whatever-else",
47
+ "ui://views/ext-apps/widget.html",
48
48
  ]) {
49
49
  const { contents } = await client.readResource({ uri });
50
50
  expect(contents).toHaveLength(1);
@@ -67,11 +67,11 @@ describe("view resource resolution (cache key)", () => {
67
67
  });
68
68
  const { client, teardown } = await connect(registerWidget);
69
69
  const { resources } = await client.listResources();
70
- const listed = resources.find((r) => r.uri.startsWith("ui://views/apps-sdk/widget.html"));
70
+ const listed = resources.find((r) => r.uri.startsWith("ui://views/ext-apps/widget.html"));
71
71
  // Production advertises a content-derived cache key.
72
- expect(listed?.uri).toMatch(/^ui:\/\/views\/apps-sdk\/widget\.html\?v=[0-9a-f]{8}$/);
72
+ expect(listed?.uri).toMatch(/^ui:\/\/views\/ext-apps\/widget\.html\?v=[0-9a-f]{8}$/);
73
73
  // A consumer holding a stale key still resolves the current asset.
74
- const staleUri = "ui://views/apps-sdk/widget.html?v=00000000";
74
+ const staleUri = "ui://views/ext-apps/widget.html?v=00000000";
75
75
  const { contents } = await client.readResource({ uri: staleUri });
76
76
  expect(contents).toHaveLength(1);
77
77
  const content = textContent(contents);
@@ -81,7 +81,22 @@ describe("view resource resolution (cache key)", () => {
81
81
  });
82
82
  it("still throws for an unknown view path", async () => {
83
83
  const { client, teardown } = await connect(registerWidget);
84
- await expect(client.readResource({ uri: "ui://views/apps-sdk/nope.html?v=1" })).rejects.toThrow();
84
+ await expect(client.readResource({ uri: "ui://views/ext-apps/nope.html?v=1" })).rejects.toThrow();
85
+ await teardown();
86
+ });
87
+ // Back-compat: older Skybridge advertised the view at `ui://views/apps-sdk/...`
88
+ // via openai/outputTemplate. We no longer advertise it, but apps published then
89
+ // have it cached, so the read must still resolve to the ext-apps content.
90
+ it("resolves the legacy apps-sdk URL to the ext-apps content", async () => {
91
+ const { client, teardown } = await connect(registerWidget);
92
+ const legacyUri = "ui://views/apps-sdk/widget.html";
93
+ const { contents } = await client.readResource({ uri: legacyUri });
94
+ expect(contents).toHaveLength(1);
95
+ const content = contents[0];
96
+ expect(content.mimeType).toBe("text/html;profile=mcp-app");
97
+ expect(content.text.length).toBeGreaterThan(0);
98
+ // The response echoes the requested (legacy) URI, never the canonical one.
99
+ expect(content.uri).toBe(legacyUri);
85
100
  await teardown();
86
101
  });
87
102
  });
@@ -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 { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { InMemoryTransport } from \"@modelcontextprotocol/sdk/inMemory.js\";\nimport { afterEach, describe, expect, it } from \"vitest\";\nimport { __setBuildManifest, McpServer } from \"./index.js\";\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\nasync function connect(register: (server: McpServer) => void) {\n const server = new McpServer({ name: \"test\", version: \"1.0.0\" });\n register(server);\n const client = new Client({ name: \"test-client\", version: \"1.0.0\" });\n const [clientTransport, serverTransport] =\n InMemoryTransport.createLinkedPair();\n await server.connect(serverTransport);\n await client.connect(clientTransport);\n return {\n client,\n teardown: async () => {\n await client.close();\n await server.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/apps-sdk/widget.html\"),\n );\n // Dev build advertises no cache key.\n expect(listed?.uri).toBe(\"ui://views/apps-sdk/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/apps-sdk/widget.html?v=stale123\",\n \"ui://views/apps-sdk/widget.html?v=whatever-else\",\n \"ui://views/apps-sdk/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/apps-sdk/widget.html\"),\n );\n // Production advertises a content-derived cache key.\n expect(listed?.uri).toMatch(\n /^ui:\\/\\/views\\/apps-sdk\\/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/apps-sdk/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/apps-sdk/nope.html?v=1\" }),\n ).rejects.toThrow();\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":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AACzD,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAU3D,+EAA+E;AAC/E,0EAA0E;AAC1E,4EAA4E;AAC5E,+EAA+E;AAC/E,wBAAwB;AAExB,KAAK,UAAU,OAAO,CAAC,QAAqC;IAC1D,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IACjE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACjB,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IACrE,MAAM,CAAC,eAAe,EAAE,eAAe,CAAC,GACtC,iBAAiB,CAAC,gBAAgB,EAAE,CAAC;IACvC,MAAM,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IACtC,MAAM,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;IACtC,OAAO;QACL,MAAM;QACN,QAAQ,EAAE,KAAK,IAAI,EAAE;YACnB,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YACrB,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;QACvB,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,MAAiB;IACvC,MAAM,CAAC,YAAY,CACjB,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,EACpE,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAC7D,CAAC;AACJ,CAAC;AAED,gEAAgE;AAChE,SAAS,WAAW,CAAC,QAAgC;IAInD,OAAO,QAAQ,CAAC,CAAC,CAAkC,CAAC;AACtD,CAAC;AAED,QAAQ,CAAC,sCAAsC,EAAE,GAAG,EAAE;IACpD,SAAS,CAAC,GAAG,EAAE;QACb,OAAO,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;IAC9B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mEAAmE,EAAE,KAAK,IAAI,EAAE;QACjF,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,OAAO,CAAC,cAAc,CAAC,CAAC;QAE3D,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,EAAE,CAAC;QACnD,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAClC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,iCAAiC,CAAC,CACpD,CAAC;QACF,qCAAqC;QACrC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,iCAAiC,CAAC,CAAC;QAE5D,wEAAwE;QACxE,4EAA4E;QAC5E,KAAK,MAAM,GAAG,IAAI;YAChB,4CAA4C;YAC5C,iDAAiD;YACjD,iCAAiC;SAClC,EAAE,CAAC;YACF,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC;YACxD,MAAM,CAAC,QAAQ,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;YACjC,MAAM,OAAO,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;YACtC,MAAM,CAAC,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAC3C,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;YAC/C,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChC,CAAC;QAED,MAAM,QAAQ,EAAE,CAAC;IACnB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sEAAsE,EAAE,KAAK,IAAI,EAAE;QACpF,OAAO,CAAC,GAAG,CAAC,QAAQ,GAAG,YAAY,CAAC;QACpC,kBAAkB,CAAC;YACjB,sBAAsB,EAAE;gBACtB,OAAO,EAAE,IAAI;gBACb,IAAI,EAAE,QAAQ;gBACd,IAAI,EAAE,yBAAyB;aAChC;YACD,WAAW,EAAE,EAAE,IAAI,EAAE,sBAAsB,EAAE;SACC,CAAC,CAAC;QAElD,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,OAAO,CAAC,cAAc,CAAC,CAAC;QAE3D,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,EAAE,CAAC;QACnD,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAClC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,iCAAiC,CAAC,CACpD,CAAC;QACF,qDAAqD;QACrD,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,OAAO,CACzB,uDAAuD,CACxD,CAAC;QAEF,mEAAmE;QACnE,MAAM,QAAQ,GAAG,4CAA4C,CAAC;QAC9D,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;QAClE,MAAM,CAAC,QAAQ,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACjC,MAAM,OAAO,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;QACtC,MAAM,CAAC,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC3C,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAEnC,MAAM,QAAQ,EAAE,CAAC;IACnB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,KAAK,IAAI,EAAE;QACrD,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,OAAO,CAAC,cAAc,CAAC,CAAC;QAE3D,MAAM,MAAM,CACV,MAAM,CAAC,YAAY,CAAC,EAAE,GAAG,EAAE,mCAAmC,EAAE,CAAC,CAClE,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QAEpB,MAAM,QAAQ,EAAE,CAAC;IACnB,CAAC,CAAC,CAAC;IAEH,gFAAgF;IAChF,gFAAgF;IAChF,0EAA0E;IAC1E,EAAE,CAAC,0DAA0D,EAAE,KAAK,IAAI,EAAE;QACxE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,OAAO,CAAC,cAAc,CAAC,CAAC;QAE3D,MAAM,SAAS,GAAG,iCAAiC,CAAC;QACpD,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,CAAC;QAEnE,MAAM,CAAC,QAAQ,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACjC,MAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,CAIzB,CAAC;QACF,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;QAC3D,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QAC/C,2EAA2E;QAC3E,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAEpC,MAAM,QAAQ,EAAE,CAAC;IACnB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { InMemoryTransport } from \"@modelcontextprotocol/sdk/inMemory.js\";\nimport { afterEach, describe, expect, it } from \"vitest\";\nimport { __setBuildManifest, McpServer } from \"./index.js\";\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\nasync function connect(register: (server: McpServer) => void) {\n const server = new McpServer({ name: \"test\", version: \"1.0.0\" });\n register(server);\n const client = new Client({ name: \"test-client\", version: \"1.0.0\" });\n const [clientTransport, serverTransport] =\n InMemoryTransport.createLinkedPair();\n await server.connect(serverTransport);\n await client.connect(clientTransport);\n return {\n client,\n teardown: async () => {\n await client.close();\n await server.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 // 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"]}
@@ -74,20 +74,24 @@ describe("McpServer.registerTool (unified API)", () => {
74
74
  const serverUrl = `http://${host}`;
75
75
  const hmrUrl = `ws://${host}`;
76
76
  const mockExtra = createMockExtra(host);
77
- const result = await appsSdkResourceCallback(new URL("ui://views/apps-sdk/my-view.html"), mockExtra);
77
+ const result = await appsSdkResourceCallback(new URL("ui://views/ext-apps/my-view.html"), mockExtra);
78
78
  expect(mockRegisterTool).toHaveBeenCalled();
79
79
  expect(result).toEqual({
80
80
  contents: [
81
81
  {
82
- uri: "ui://views/apps-sdk/my-view.html",
83
- mimeType: "text/html+skybridge",
82
+ uri: "ui://views/ext-apps/my-view.html",
83
+ mimeType: "text/html;profile=mcp-app",
84
84
  text: expect.stringContaining('<div id="root"></div>'),
85
85
  _meta: {
86
- "openai/widgetCSP": {
87
- resource_domains: [serverUrl],
88
- connect_domains: [serverUrl, hmrUrl],
86
+ ui: {
87
+ csp: {
88
+ resourceDomains: [serverUrl],
89
+ connectDomains: [serverUrl, hmrUrl],
90
+ baseUriDomains: [serverUrl],
91
+ },
92
+ domain: serverUrl,
93
+ description: "Test view",
89
94
  },
90
- "openai/widgetDomain": serverUrl,
91
95
  "openai/widgetDescription": "Test view",
92
96
  },
93
97
  },
@@ -113,20 +117,24 @@ describe("McpServer.registerTool (unified API)", () => {
113
117
  const host = "myapp.com";
114
118
  const serverUrl = `https://${host}`;
115
119
  const mockExtra = createMockExtra(host);
116
- const versionedUri = `ui://views/apps-sdk/my-view.html${expectedVersionParam("assets/my-view-abc123.js", "style.css")}`;
120
+ const versionedUri = `ui://views/ext-apps/my-view.html${expectedVersionParam("assets/my-view-abc123.js", "style.css")}`;
117
121
  const result = await appsSdkResourceCallback(new URL(versionedUri), mockExtra);
118
122
  expect(result).toEqual({
119
123
  contents: [
120
124
  {
121
125
  uri: versionedUri,
122
- mimeType: "text/html+skybridge",
126
+ mimeType: "text/html;profile=mcp-app",
123
127
  text: expect.stringContaining('<div id="root"></div>'),
124
128
  _meta: {
125
- "openai/widgetCSP": {
126
- resource_domains: [serverUrl],
127
- connect_domains: [serverUrl],
129
+ ui: {
130
+ csp: {
131
+ resourceDomains: [serverUrl],
132
+ connectDomains: [serverUrl],
133
+ baseUriDomains: [serverUrl],
134
+ },
135
+ domain: serverUrl,
136
+ description: "Test view",
128
137
  },
129
- "openai/widgetDomain": serverUrl,
130
138
  "openai/widgetDescription": "Test view",
131
139
  },
132
140
  },
@@ -145,7 +153,7 @@ describe("McpServer.registerTool (unified API)", () => {
145
153
  view: { component: "my-view", description: "Test view" },
146
154
  }, vi.fn());
147
155
  const extAppsResourceCallback = mockRegisterResource.mock
148
- .calls[1]?.[3];
156
+ .calls[0]?.[3];
149
157
  expect(extAppsResourceCallback).toBeDefined();
150
158
  const forwardedUrl = "https://everything-3a2c1264.staging.alpic.live/mcp?foo=bar";
151
159
  const expectedDomain = `${crypto
@@ -170,9 +178,10 @@ describe("McpServer.registerTool (unified API)", () => {
170
178
  description: "Test view",
171
179
  domain: expectedDomain,
172
180
  },
181
+ "openai/widgetDescription": "Test view",
173
182
  });
174
183
  });
175
- it("should register resources with correct hostType for both apps-sdk and ext-apps", async () => {
184
+ it("should register a single ext-apps resource", async () => {
176
185
  server.registerTool({
177
186
  name: "my-view",
178
187
  description: "Test tool",
@@ -182,37 +191,14 @@ describe("McpServer.registerTool (unified API)", () => {
182
191
  prefersBorder: true,
183
192
  },
184
193
  }, vi.fn());
185
- expect(mockRegisterResource).toHaveBeenCalledTimes(2);
186
- const appsSdkCallback = mockRegisterResource.mock
194
+ expect(mockRegisterResource).toHaveBeenCalledTimes(1);
195
+ const resourceCallback = mockRegisterResource.mock
187
196
  .calls[0]?.[3];
188
197
  const host = "localhost:3000";
189
198
  const serverUrl = `http://${host}`;
190
199
  const hmrUrl = `ws://${host}`;
191
- const appsSdkResult = await appsSdkCallback(new URL("ui://views/apps-sdk/my-view.html"), createMockExtra(host));
192
- expect(appsSdkResult).toEqual({
193
- contents: [
194
- {
195
- uri: "ui://views/apps-sdk/my-view.html",
196
- mimeType: "text/html+skybridge",
197
- text: expect.stringContaining('<div id="root"></div>'),
198
- _meta: {
199
- "openai/widgetCSP": {
200
- resource_domains: [serverUrl],
201
- connect_domains: [serverUrl, hmrUrl],
202
- },
203
- "openai/widgetDomain": serverUrl,
204
- "openai/widgetDescription": "Test view",
205
- "openai/widgetPrefersBorder": true,
206
- },
207
- },
208
- ],
209
- });
210
- expect(appsSdkResult.contents[0]?.text).toContain('window.skybridge = { hostType: "apps-sdk", serverUrl: "http://localhost:3000" };');
211
- const extAppsResourceCallback = mockRegisterResource.mock
212
- .calls[1]?.[3];
213
- expect(extAppsResourceCallback).toBeDefined();
214
- const extAppsResult = await extAppsResourceCallback(new URL("ui://views/ext-apps/my-view.html"), createMockExtra(host));
215
- expect(extAppsResult).toEqual({
200
+ const result = await resourceCallback(new URL("ui://views/ext-apps/my-view.html"), createMockExtra(host));
201
+ expect(result).toEqual({
216
202
  contents: [
217
203
  {
218
204
  uri: "ui://views/ext-apps/my-view.html",
@@ -229,27 +215,26 @@ describe("McpServer.registerTool (unified API)", () => {
229
215
  description: "Test view",
230
216
  prefersBorder: true,
231
217
  },
218
+ "openai/widgetDescription": "Test view",
232
219
  },
233
220
  },
234
221
  ],
235
222
  });
236
- expect(extAppsResult.contents[0]?.text).toContain('window.skybridge = { hostType: "mcp-app", serverUrl: "http://localhost:3000" };');
223
+ expect(result.contents[0]?.text).toContain('window.skybridge = { hostType: "mcp-app", serverUrl: "http://localhost:3000" };');
237
224
  });
238
- it("should register tool with ui.resourceUri metadata", async () => {
225
+ it("should advertise the single resource via ui.resourceUri only (no openai/outputTemplate)", async () => {
239
226
  server.registerTool({
240
227
  name: "my-view",
241
228
  description: "Test tool",
242
229
  view: { component: "my-view", description: "Test view" },
243
230
  }, vi.fn());
244
- expect(mockRegisterTool).toHaveBeenCalledTimes(1);
245
- const toolCallArgs = mockRegisterTool.mock.calls[0];
246
- const toolConfig = toolCallArgs?.[1];
247
- expect(toolConfig._meta).toHaveProperty("ui");
248
- expect(toolConfig._meta?.ui).toEqual({
249
- resourceUri: "ui://views/ext-apps/my-view.html",
250
- });
231
+ const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
232
+ const uri = "ui://views/ext-apps/my-view.html";
233
+ expect(toolConfig._meta?.ui?.resourceUri).toBe(uri);
234
+ expect(toolConfig._meta?.["ui/resourceUri"]).toBe(uri);
235
+ expect(toolConfig._meta?.["openai/outputTemplate"]).toBeUndefined();
251
236
  });
252
- it("should register tool with openai/outputTemplate when apps-sdk only", async () => {
237
+ it("treats the deprecated hosts option as a no-op (always the single resource)", async () => {
253
238
  server.registerTool({
254
239
  name: "my-view",
255
240
  description: "Test tool",
@@ -259,11 +244,10 @@ describe("McpServer.registerTool (unified API)", () => {
259
244
  hosts: ["apps-sdk"],
260
245
  },
261
246
  }, vi.fn());
262
- expect(mockRegisterTool).toHaveBeenCalledTimes(1);
263
- const toolCallArgs = mockRegisterTool.mock.calls[0];
264
- const toolConfig = toolCallArgs?.[1];
265
- expect(toolConfig._meta).not.toHaveProperty("ui");
266
- expect(toolConfig._meta?.["openai/outputTemplate"]).toBe("ui://views/apps-sdk/my-view.html");
247
+ expect(mockRegisterResource).toHaveBeenCalledTimes(1);
248
+ const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
249
+ expect(toolConfig._meta?.ui?.resourceUri).toBe("ui://views/ext-apps/my-view.html");
250
+ expect(toolConfig._meta?.["openai/outputTemplate"]).toBeUndefined();
267
251
  });
268
252
  it("should not version view URIs in development", () => {
269
253
  server.registerTool({
@@ -272,12 +256,11 @@ describe("McpServer.registerTool (unified API)", () => {
272
256
  view: { component: "my-view", description: "Test view" },
273
257
  }, vi.fn());
274
258
  const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
275
- expect(toolConfig._meta?.["openai/outputTemplate"]).toBe("ui://views/apps-sdk/my-view.html");
259
+ expect(toolConfig._meta?.["openai/outputTemplate"]).toBeUndefined();
276
260
  expect(toolConfig._meta?.ui?.resourceUri).toBe("ui://views/ext-apps/my-view.html");
277
- // The URI registered with the resource handler must match the URI in
278
- // outputTemplate exactly so the SDK can resolve `resources/read` requests.
279
- expect(mockRegisterResource.mock.calls[0]?.[1]).toBe("ui://views/apps-sdk/my-view.html");
280
- expect(mockRegisterResource.mock.calls[1]?.[1]).toBe("ui://views/ext-apps/my-view.html");
261
+ // The URI registered with the resource handler must match ui.resourceUri
262
+ // exactly so the SDK can resolve `resources/read` requests.
263
+ expect(mockRegisterResource.mock.calls[0]?.[1]).toBe("ui://views/ext-apps/my-view.html");
281
264
  });
282
265
  it("should append a stable content hash to view URIs in production", () => {
283
266
  setTestEnv({ NODE_ENV: "production" });
@@ -288,10 +271,9 @@ describe("McpServer.registerTool (unified API)", () => {
288
271
  }, vi.fn());
289
272
  const expected = expectedVersionParam("assets/my-view-abc123.js", "style.css");
290
273
  const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
291
- expect(toolConfig._meta?.["openai/outputTemplate"]).toBe(`ui://views/apps-sdk/my-view.html${expected}`);
274
+ expect(toolConfig._meta?.["openai/outputTemplate"]).toBeUndefined();
292
275
  expect(toolConfig._meta?.ui?.resourceUri).toBe(`ui://views/ext-apps/my-view.html${expected}`);
293
- expect(mockRegisterResource.mock.calls[0]?.[1]).toBe(`ui://views/apps-sdk/my-view.html${expected}`);
294
- expect(mockRegisterResource.mock.calls[1]?.[1]).toBe(`ui://views/ext-apps/my-view.html${expected}`);
276
+ expect(mockRegisterResource.mock.calls[0]?.[1]).toBe(`ui://views/ext-apps/my-view.html${expected}`);
295
277
  });
296
278
  it("should produce different version params for views with different bundles", () => {
297
279
  setTestEnv({ NODE_ENV: "production" });
@@ -305,8 +287,8 @@ describe("McpServer.registerTool (unified API)", () => {
305
287
  description: "Second tool",
306
288
  view: { component: "folder-view" },
307
289
  }, vi.fn());
308
- const myviewTemplate = (mockRegisterTool.mock.calls[0]?.[1])._meta?.["openai/outputTemplate"];
309
- const folderviewTemplate = (mockRegisterTool.mock.calls[1]?.[1])._meta?.["openai/outputTemplate"];
290
+ const myviewTemplate = (mockRegisterTool.mock.calls[0]?.[1])._meta?.ui?.resourceUri;
291
+ const folderviewTemplate = (mockRegisterTool.mock.calls[1]?.[1])._meta?.ui?.resourceUri;
310
292
  expect(myviewTemplate).not.toEqual(folderviewTemplate);
311
293
  expect(myviewTemplate).toMatch(/\?v=[0-9a-f]{8}$/);
312
294
  expect(folderviewTemplate).toMatch(/\?v=[0-9a-f]{8}$/);
@@ -319,26 +301,7 @@ describe("McpServer.registerTool (unified API)", () => {
319
301
  view: { component: "unknown-view" },
320
302
  }, vi.fn());
321
303
  const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
322
- expect(toolConfig._meta?.["openai/outputTemplate"]).toBe("ui://views/apps-sdk/unknown-view.html");
323
- });
324
- it("should register tool with ui.resourceUri only when mcp-app only", async () => {
325
- server.registerTool({
326
- name: "my-view",
327
- description: "Test tool",
328
- view: {
329
- component: "my-view",
330
- description: "Test view",
331
- hosts: ["mcp-app"],
332
- },
333
- }, vi.fn());
334
- expect(mockRegisterTool).toHaveBeenCalledTimes(1);
335
- const toolCallArgs = mockRegisterTool.mock.calls[0];
336
- const toolConfig = toolCallArgs?.[1];
337
- expect(toolConfig._meta).toHaveProperty("ui");
338
- expect(toolConfig._meta?.ui).toEqual({
339
- resourceUri: "ui://views/ext-apps/my-view.html",
340
- });
341
- expect(toolConfig._meta?.["openai/outputTemplate"]).toBeUndefined();
304
+ expect(toolConfig._meta?.ui?.resourceUri).toBe("ui://views/ext-apps/unknown-view.html");
342
305
  });
343
306
  it("should inject viewUUID into _meta of tool callback results", async () => {
344
307
  const mockToolCallback = vi.fn().mockResolvedValue({
@@ -476,11 +439,12 @@ describe("McpServer.registerTool (unified API)", () => {
476
439
  const host = "localhost:3000";
477
440
  const serverUrl = `http://${host}`;
478
441
  const hmrUrl = `ws://${host}`;
479
- const result = await appsSdkCallback(new URL("ui://views/apps-sdk/csp-tool.html"), createMockExtra(host));
442
+ const result = await appsSdkCallback(new URL("ui://views/ext-apps/csp-tool.html"), createMockExtra(host));
480
443
  const meta = result.contents[0]?._meta;
481
- expect(meta["openai/widgetCSP"]).toEqual({
482
- resource_domains: [serverUrl, "https://cdn.example.com"],
483
- connect_domains: [serverUrl, hmrUrl, "https://api.example.com"],
444
+ expect(meta.ui?.csp).toEqual({
445
+ resourceDomains: [serverUrl, "https://cdn.example.com"],
446
+ connectDomains: [serverUrl, hmrUrl, "https://api.example.com"],
447
+ baseUriDomains: [serverUrl],
484
448
  });
485
449
  });
486
450
  it("should let view._meta override framework-computed keys", async () => {
@@ -492,18 +456,20 @@ describe("McpServer.registerTool (unified API)", () => {
492
456
  description: "Test view",
493
457
  csp: { connectDomains: ["https://api.x.com"] },
494
458
  _meta: {
495
- "openai/widgetCSP": {
496
- connect_domains: ["https://api.y.com"],
459
+ ui: {
460
+ csp: {
461
+ connectDomains: ["https://api.y.com"],
462
+ },
497
463
  },
498
464
  },
499
465
  },
500
466
  }, vi.fn());
501
467
  const appsSdkCallback = mockRegisterResource.mock
502
468
  .calls[0]?.[3];
503
- const result = await appsSdkCallback(new URL("ui://views/apps-sdk/override-tool.html"), createMockExtra("localhost:3000"));
469
+ const result = await appsSdkCallback(new URL("ui://views/ext-apps/override-tool.html"), createMockExtra("localhost:3000"));
504
470
  const meta = result.contents[0]?._meta;
505
- expect(meta["openai/widgetCSP"]).toEqual({
506
- connect_domains: ["https://api.y.com"],
471
+ expect(meta.ui?.csp).toEqual({
472
+ connectDomains: ["https://api.y.com"],
507
473
  });
508
474
  });
509
475
  it("should pass user _meta keys through to tool config", () => {
@@ -553,16 +519,18 @@ describe("resources/list view _meta injection", () => {
553
519
  await server.close();
554
520
  const appsSdk = resources.find((r) => r.uri.includes("apps-sdk"));
555
521
  const extApps = resources.find((r) => r.uri.includes("ext-apps"));
556
- expect(appsSdk?._meta).toBeDefined();
522
+ expect(appsSdk).toBeUndefined();
557
523
  expect(extApps?._meta).toBeDefined();
558
- const appsSdkCsp = appsSdk?._meta?.["openai/widgetCSP"];
559
- expect(appsSdkCsp.connect_domains?.length).toBeGreaterThan(0);
560
- expect(appsSdkCsp.resource_domains).toContain("https://fonts.googleapis.com");
561
- expect(appsSdk?._meta?.["openai/widgetDomain"]).toBe("skybridge.tech");
562
524
  const extUi = (extApps?._meta).ui;
563
525
  expect(extUi?.csp?.connectDomains?.length).toBeGreaterThan(0);
564
526
  expect(extUi?.csp?.resourceDomains).toContain("https://fonts.googleapis.com");
565
527
  expect(extUi?.domain).toBe("skybridge.tech");
528
+ const meta = extApps?._meta;
529
+ expect(meta["openai/widgetDescription"]).toBe("Onboarding deck");
530
+ expect(meta["openai/widgetCSP"]?.redirect_domains).toEqual([
531
+ "https://docs.skybridge.tech",
532
+ ]);
533
+ expect(meta.ui?.csp).not.toHaveProperty("redirectDomains");
566
534
  });
567
535
  });
568
536
  //# sourceMappingURL=view.test.js.map