skybridge 0.0.0-dev.ecfdeac → 0.0.0-dev.ed29eee

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 (233) hide show
  1. package/README.md +2 -9
  2. package/dist/cli/build-helpers.d.ts +6 -2
  3. package/dist/cli/build-helpers.js +37 -12
  4. package/dist/cli/build-helpers.js.map +1 -1
  5. package/dist/cli/build-helpers.test.js +71 -3
  6. package/dist/cli/build-helpers.test.js.map +1 -1
  7. package/dist/cli/build-steps.d.ts +2 -0
  8. package/dist/cli/build-steps.js +73 -0
  9. package/dist/cli/build-steps.js.map +1 -0
  10. package/dist/cli/build-steps.test.d.ts +1 -0
  11. package/dist/cli/build-steps.test.js +52 -0
  12. package/dist/cli/build-steps.test.js.map +1 -0
  13. package/dist/cli/header.d.ts +1 -1
  14. package/dist/cli/resolve-skybridge-config.d.ts +5 -0
  15. package/dist/cli/{resolve-views-dir.js → resolve-skybridge-config.js} +4 -3
  16. package/dist/cli/resolve-skybridge-config.js.map +1 -0
  17. package/dist/cli/run-plain.d.ts +18 -0
  18. package/dist/cli/run-plain.js +89 -0
  19. package/dist/cli/run-plain.js.map +1 -0
  20. package/dist/cli/use-nodemon.d.ts +14 -0
  21. package/dist/cli/use-nodemon.js +71 -60
  22. package/dist/cli/use-nodemon.js.map +1 -1
  23. package/dist/cli/use-typescript-check.d.ts +8 -2
  24. package/dist/cli/use-typescript-check.js +70 -67
  25. package/dist/cli/use-typescript-check.js.map +1 -1
  26. package/dist/commands/build.d.ts +0 -2
  27. package/dist/commands/build.js +2 -61
  28. package/dist/commands/build.js.map +1 -1
  29. package/dist/commands/dev.d.ts +1 -0
  30. package/dist/commands/dev.js +34 -2
  31. package/dist/commands/dev.js.map +1 -1
  32. package/dist/context-warnings.d.ts +5 -0
  33. package/dist/context-warnings.js +31 -0
  34. package/dist/context-warnings.js.map +1 -0
  35. package/dist/context-warnings.test.d.ts +1 -0
  36. package/dist/context-warnings.test.js +28 -0
  37. package/dist/context-warnings.test.js.map +1 -0
  38. package/dist/server/auth/index.d.ts +30 -4
  39. package/dist/server/auth/index.js.map +1 -1
  40. package/dist/server/auth/providers/auth0.d.ts +24 -2
  41. package/dist/server/auth/providers/auth0.js.map +1 -1
  42. package/dist/server/auth/providers/auth0.test.js +4 -2
  43. package/dist/server/auth/providers/auth0.test.js.map +1 -1
  44. package/dist/server/auth/providers/authplane.d.ts +49 -0
  45. package/dist/server/auth/providers/authplane.js +57 -0
  46. package/dist/server/auth/providers/authplane.js.map +1 -0
  47. package/dist/server/auth/providers/authplane.test.d.ts +1 -0
  48. package/dist/server/auth/providers/authplane.test.js +140 -0
  49. package/dist/server/auth/providers/authplane.test.js.map +1 -0
  50. package/dist/server/auth/providers/clerk.d.ts +39 -2
  51. package/dist/server/auth/providers/clerk.js +4 -1
  52. package/dist/server/auth/providers/clerk.js.map +1 -1
  53. package/dist/server/auth/providers/clerk.test.js +5 -3
  54. package/dist/server/auth/providers/clerk.test.js.map +1 -1
  55. package/dist/server/auth/providers/custom.d.ts +18 -2
  56. package/dist/server/auth/providers/custom.js +29 -10
  57. package/dist/server/auth/providers/custom.js.map +1 -1
  58. package/dist/server/auth/providers/custom.test.js +61 -10
  59. package/dist/server/auth/providers/custom.test.js.map +1 -1
  60. package/dist/server/auth/providers/descope.d.ts +36 -5
  61. package/dist/server/auth/providers/descope.js +18 -5
  62. package/dist/server/auth/providers/descope.js.map +1 -1
  63. package/dist/server/auth/providers/descope.test.js +42 -17
  64. package/dist/server/auth/providers/descope.test.js.map +1 -1
  65. package/dist/server/auth/providers/stytch.d.ts +15 -2
  66. package/dist/server/auth/providers/stytch.js +4 -1
  67. package/dist/server/auth/providers/stytch.js.map +1 -1
  68. package/dist/server/auth/providers/verify-spy.d.ts +12 -0
  69. package/dist/server/auth/providers/verify-spy.js +21 -0
  70. package/dist/server/auth/providers/verify-spy.js.map +1 -0
  71. package/dist/server/auth/providers/workos.d.ts +24 -2
  72. package/dist/server/auth/providers/workos.js +4 -1
  73. package/dist/server/auth/providers/workos.js.map +1 -1
  74. package/dist/server/auth/security-schemes.d.ts +23 -0
  75. package/dist/server/auth/security-schemes.js +64 -0
  76. package/dist/server/auth/security-schemes.js.map +1 -0
  77. package/dist/server/auth/security-schemes.test.d.ts +1 -0
  78. package/dist/server/auth/security-schemes.test.js +99 -0
  79. package/dist/server/auth/security-schemes.test.js.map +1 -0
  80. package/dist/server/auth/setup.d.ts +3 -1
  81. package/dist/server/auth/setup.js +65 -24
  82. package/dist/server/auth/setup.js.map +1 -1
  83. package/dist/server/auth/setup.test.js +328 -5
  84. package/dist/server/auth/setup.test.js.map +1 -1
  85. package/dist/server/auth/verify.d.ts +37 -3
  86. package/dist/server/auth/verify.js +26 -2
  87. package/dist/server/auth/verify.js.map +1 -1
  88. package/dist/server/auth/verify.test.js +49 -0
  89. package/dist/server/auth/verify.test.js.map +1 -1
  90. package/dist/server/auth-extra.test-d.d.ts +1 -0
  91. package/dist/server/auth-extra.test-d.js +82 -0
  92. package/dist/server/auth-extra.test-d.js.map +1 -0
  93. package/dist/server/auth.d.ts +39 -1
  94. package/dist/server/auth.js.map +1 -1
  95. package/dist/server/host.d.ts +2 -0
  96. package/dist/server/host.js +11 -0
  97. package/dist/server/host.js.map +1 -0
  98. package/dist/server/host.test.d.ts +1 -0
  99. package/dist/server/host.test.js +13 -0
  100. package/dist/server/host.test.js.map +1 -0
  101. package/dist/server/index.d.ts +11 -7
  102. package/dist/server/index.js +6 -2
  103. package/dist/server/index.js.map +1 -1
  104. package/dist/server/middleware.d.ts +31 -6
  105. package/dist/server/middleware.js +33 -0
  106. package/dist/server/middleware.js.map +1 -1
  107. package/dist/server/middleware.test.js +37 -1
  108. package/dist/server/middleware.test.js.map +1 -1
  109. package/dist/server/server.d.ts +87 -25
  110. package/dist/server/server.js +221 -120
  111. package/dist/server/server.js.map +1 -1
  112. package/dist/server/skills-integration.test.d.ts +1 -0
  113. package/dist/server/skills-integration.test.js +79 -0
  114. package/dist/server/skills-integration.test.js.map +1 -0
  115. package/dist/server/skills.d.ts +29 -0
  116. package/dist/server/skills.js +195 -0
  117. package/dist/server/skills.js.map +1 -0
  118. package/dist/server/skills.test.d.ts +1 -0
  119. package/dist/server/skills.test.js +247 -0
  120. package/dist/server/skills.test.js.map +1 -0
  121. package/dist/server/view-name.test-d.d.ts +1 -0
  122. package/dist/server/view-name.test-d.js +8 -0
  123. package/dist/server/view-name.test-d.js.map +1 -0
  124. package/dist/server/view-resource-resolution.test.js +93 -10
  125. package/dist/server/view-resource-resolution.test.js.map +1 -1
  126. package/dist/server/viewsDevServer.js +4 -1
  127. package/dist/server/viewsDevServer.js.map +1 -1
  128. package/dist/test/utils.d.ts +3 -3
  129. package/dist/test/view.test.js +86 -102
  130. package/dist/test/view.test.js.map +1 -1
  131. package/dist/web/bridges/adaptor.d.ts +51 -0
  132. package/dist/web/bridges/adaptor.js +327 -0
  133. package/dist/web/bridges/adaptor.js.map +1 -0
  134. package/dist/web/bridges/adaptor.test.d.ts +1 -0
  135. package/dist/web/bridges/adaptor.test.js +225 -0
  136. package/dist/web/bridges/adaptor.test.js.map +1 -0
  137. package/dist/web/bridges/apps-sdk/bridge.d.ts +6 -2
  138. package/dist/web/bridges/apps-sdk/bridge.js +15 -4
  139. package/dist/web/bridges/apps-sdk/bridge.js.map +1 -1
  140. package/dist/web/bridges/apps-sdk/index.d.ts +0 -1
  141. package/dist/web/bridges/apps-sdk/index.js +0 -1
  142. package/dist/web/bridges/apps-sdk/index.js.map +1 -1
  143. package/dist/web/bridges/get-adaptor.d.ts +4 -4
  144. package/dist/web/bridges/get-adaptor.js +6 -11
  145. package/dist/web/bridges/get-adaptor.js.map +1 -1
  146. package/dist/web/bridges/get-adaptor.test.d.ts +1 -0
  147. package/dist/web/bridges/get-adaptor.test.js +32 -0
  148. package/dist/web/bridges/get-adaptor.test.js.map +1 -0
  149. package/dist/web/bridges/mcp-app/bridge.d.ts +13 -2
  150. package/dist/web/bridges/mcp-app/bridge.js +51 -7
  151. package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
  152. package/dist/web/bridges/mcp-app/bridge.test.d.ts +1 -0
  153. package/dist/web/bridges/mcp-app/bridge.test.js +17 -0
  154. package/dist/web/bridges/mcp-app/bridge.test.js.map +1 -0
  155. package/dist/web/bridges/mcp-app/index.d.ts +0 -1
  156. package/dist/web/bridges/mcp-app/index.js +0 -1
  157. package/dist/web/bridges/mcp-app/index.js.map +1 -1
  158. package/dist/web/bridges/mcp-app/types.d.ts +2 -0
  159. package/dist/web/bridges/mcp-app/types.js.map +1 -1
  160. package/dist/web/bridges/mcp-app/view-tools.test.js +5 -6
  161. package/dist/web/bridges/mcp-app/view-tools.test.js.map +1 -1
  162. package/dist/web/bridges/types.d.ts +10 -0
  163. package/dist/web/bridges/types.js +14 -1
  164. package/dist/web/bridges/types.js.map +1 -1
  165. package/dist/web/bridges/types.test.d.ts +1 -0
  166. package/dist/web/bridges/types.test.js +19 -0
  167. package/dist/web/bridges/types.test.js.map +1 -0
  168. package/dist/web/components/modal-provider.d.ts +1 -1
  169. package/dist/web/components/modal-provider.js +4 -3
  170. package/dist/web/components/modal-provider.js.map +1 -1
  171. package/dist/web/create-store.test.js +8 -5
  172. package/dist/web/create-store.test.js.map +1 -1
  173. package/dist/web/data-llm.d.ts +1 -1
  174. package/dist/web/data-llm.test.js +18 -5
  175. package/dist/web/data-llm.test.js.map +1 -1
  176. package/dist/web/hooks/index.d.ts +1 -0
  177. package/dist/web/hooks/index.js +1 -0
  178. package/dist/web/hooks/index.js.map +1 -1
  179. package/dist/web/hooks/test/utils.d.ts +2 -1
  180. package/dist/web/hooks/test/utils.js +1 -1
  181. package/dist/web/hooks/test/utils.js.map +1 -1
  182. package/dist/web/hooks/use-call-tool.test.js +37 -23
  183. package/dist/web/hooks/use-call-tool.test.js.map +1 -1
  184. package/dist/web/hooks/use-display-mode.test.js +56 -20
  185. package/dist/web/hooks/use-display-mode.test.js.map +1 -1
  186. package/dist/web/hooks/use-download.test.js +11 -3
  187. package/dist/web/hooks/use-download.test.js.map +1 -1
  188. package/dist/web/hooks/use-files.test.js +10 -2
  189. package/dist/web/hooks/use-files.test.js.map +1 -1
  190. package/dist/web/hooks/use-host-info.d.ts +22 -0
  191. package/dist/web/hooks/use-host-info.js +30 -0
  192. package/dist/web/hooks/use-host-info.js.map +1 -0
  193. package/dist/web/hooks/use-host-info.test.d.ts +1 -0
  194. package/dist/web/hooks/use-host-info.test.js +59 -0
  195. package/dist/web/hooks/use-host-info.test.js.map +1 -0
  196. package/dist/web/hooks/use-layout.test.js +59 -26
  197. package/dist/web/hooks/use-layout.test.js.map +1 -1
  198. package/dist/web/hooks/use-open-external.test.js +13 -3
  199. package/dist/web/hooks/use-open-external.test.js.map +1 -1
  200. package/dist/web/hooks/use-request-close.test.js +35 -40
  201. package/dist/web/hooks/use-request-close.test.js.map +1 -1
  202. package/dist/web/hooks/use-request-modal.test.js +10 -0
  203. package/dist/web/hooks/use-request-modal.test.js.map +1 -1
  204. package/dist/web/hooks/use-request-size.test.js +35 -53
  205. package/dist/web/hooks/use-request-size.test.js.map +1 -1
  206. package/dist/web/hooks/use-set-open-in-app-url.test.js +28 -8
  207. package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -1
  208. package/dist/web/hooks/use-tool-info.test.js +37 -27
  209. package/dist/web/hooks/use-tool-info.test.js.map +1 -1
  210. package/dist/web/hooks/use-user.test.js +81 -35
  211. package/dist/web/hooks/use-user.test.js.map +1 -1
  212. package/dist/web/hooks/use-view-state.js +19 -9
  213. package/dist/web/hooks/use-view-state.js.map +1 -1
  214. package/dist/web/hooks/use-view-state.test.js +37 -2
  215. package/dist/web/hooks/use-view-state.test.js.map +1 -1
  216. package/dist/web/plugin/plugin-build.test.d.ts +1 -0
  217. package/dist/web/plugin/plugin-build.test.js +54 -0
  218. package/dist/web/plugin/plugin-build.test.js.map +1 -0
  219. package/dist/web/plugin/plugin.d.ts +6 -0
  220. package/dist/web/plugin/plugin.js +18 -7
  221. package/dist/web/plugin/plugin.js.map +1 -1
  222. package/dist/web/plugin/plugin.test.d.ts +1 -0
  223. package/dist/web/plugin/plugin.test.js +61 -0
  224. package/dist/web/plugin/plugin.test.js.map +1 -0
  225. package/package.json +21 -20
  226. package/dist/cli/resolve-views-dir.d.ts +0 -1
  227. package/dist/cli/resolve-views-dir.js.map +0 -1
  228. package/dist/web/bridges/apps-sdk/adaptor.d.ts +0 -29
  229. package/dist/web/bridges/apps-sdk/adaptor.js +0 -117
  230. package/dist/web/bridges/apps-sdk/adaptor.js.map +0 -1
  231. package/dist/web/bridges/mcp-app/adaptor.d.ts +0 -53
  232. package/dist/web/bridges/mcp-app/adaptor.js +0 -283
  233. package/dist/web/bridges/mcp-app/adaptor.js.map +0 -1
@@ -1,7 +1,15 @@
1
+ import http from "node:http";
1
2
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
2
4
  import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
3
- import { afterEach, describe, expect, it } from "vitest";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
6
  import { __setBuildManifest, McpServer } from "./index.js";
7
+ vi.mock("@skybridge/devtools", () => ({
8
+ devtoolsStaticServer: () => ((_req, _res, next) => next()),
9
+ }));
10
+ vi.mock("./viewsDevServer.js", () => ({
11
+ viewsDevServer: (_httpServer) => ((_req, _res, next) => next()),
12
+ }));
5
13
  // SKY-435: a view resource must resolve no matter the `?v=` query param value.
6
14
  // The version param is a content-derived cache key for external consumers
7
15
  // (it busts host/CDN caches when the bundle changes); it is not part of the
@@ -22,6 +30,30 @@ async function connect(register) {
22
30
  },
23
31
  };
24
32
  }
33
+ let openHttpServer;
34
+ afterEach(() => openHttpServer?.close());
35
+ // Connect over real HTTP so forwarded proxy headers reach the resource handler.
36
+ async function connectHttp(register, headers) {
37
+ const { createApp } = await import("./express.js");
38
+ const server = new McpServer({ name: "test", version: "1.0.0" });
39
+ register(server);
40
+ const httpServer = http.createServer();
41
+ await createApp({ mcpServer: server, httpServer });
42
+ const listening = http.createServer(server.express);
43
+ await new Promise((resolve) => listening.listen(0, resolve));
44
+ openHttpServer = listening;
45
+ const port = listening.address().port;
46
+ const client = new Client({ name: "test-client", version: "1.0.0" });
47
+ const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`), { requestInit: { headers } });
48
+ await client.connect(transport);
49
+ return {
50
+ client,
51
+ teardown: async () => {
52
+ await client.close();
53
+ await server.close();
54
+ },
55
+ };
56
+ }
25
57
  function registerWidget(server) {
26
58
  server.registerTool({ name: "show", description: "show", view: { component: "widget" } }, () => ({ content: [{ type: "text", text: "ok" }] }));
27
59
  }
@@ -36,15 +68,15 @@ describe("view resource resolution (cache key)", () => {
36
68
  it("resolves regardless of the ?v= param and echoes the requested URI", async () => {
37
69
  const { client, teardown } = await connect(registerWidget);
38
70
  const { resources } = await client.listResources();
39
- const listed = resources.find((r) => r.uri.startsWith("ui://views/apps-sdk/widget.html"));
71
+ const listed = resources.find((r) => r.uri.startsWith("ui://views/ext-apps/widget.html"));
40
72
  // Dev build advertises no cache key.
41
- expect(listed?.uri).toBe("ui://views/apps-sdk/widget.html");
73
+ expect(listed?.uri).toBe("ui://views/ext-apps/widget.html");
42
74
  // A stale/arbitrary/absent param all resolve the same underlying asset,
43
75
  // and the response echoes the URI the consumer asked for (never rewritten).
44
76
  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",
77
+ "ui://views/ext-apps/widget.html?v=stale123",
78
+ "ui://views/ext-apps/widget.html?v=whatever-else",
79
+ "ui://views/ext-apps/widget.html",
48
80
  ]) {
49
81
  const { contents } = await client.readResource({ uri });
50
82
  expect(contents).toHaveLength(1);
@@ -67,11 +99,11 @@ describe("view resource resolution (cache key)", () => {
67
99
  });
68
100
  const { client, teardown } = await connect(registerWidget);
69
101
  const { resources } = await client.listResources();
70
- const listed = resources.find((r) => r.uri.startsWith("ui://views/apps-sdk/widget.html"));
102
+ const listed = resources.find((r) => r.uri.startsWith("ui://views/ext-apps/widget.html"));
71
103
  // Production advertises a content-derived cache key.
72
- expect(listed?.uri).toMatch(/^ui:\/\/views\/apps-sdk\/widget\.html\?v=[0-9a-f]{8}$/);
104
+ expect(listed?.uri).toMatch(/^ui:\/\/views\/ext-apps\/widget\.html\?v=[0-9a-f]{8}$/);
73
105
  // A consumer holding a stale key still resolves the current asset.
74
- const staleUri = "ui://views/apps-sdk/widget.html?v=00000000";
106
+ const staleUri = "ui://views/ext-apps/widget.html?v=00000000";
75
107
  const { contents } = await client.readResource({ uri: staleUri });
76
108
  expect(contents).toHaveLength(1);
77
109
  const content = textContent(contents);
@@ -81,7 +113,58 @@ describe("view resource resolution (cache key)", () => {
81
113
  });
82
114
  it("still throws for an unknown view path", async () => {
83
115
  const { client, teardown } = await connect(registerWidget);
84
- await expect(client.readResource({ uri: "ui://views/apps-sdk/nope.html?v=1" })).rejects.toThrow();
116
+ await expect(client.readResource({ uri: "ui://views/ext-apps/nope.html?v=1" })).rejects.toThrow();
117
+ await teardown();
118
+ });
119
+ // A proxy routing the request under a path prefix sends x-forwarded-prefix;
120
+ // the view's asset URLs must carry that prefix (and the forwarded origin) so
121
+ // they resolve back through the same routing. Per-request, so one process can
122
+ // serve many hosts/prefixes at once.
123
+ it("prefixes emitted asset URLs with x-forwarded-prefix (prod)", async () => {
124
+ process.env.NODE_ENV = "production";
125
+ __setBuildManifest({
126
+ "src/views/widget.tsx": {
127
+ isEntry: true,
128
+ name: "widget",
129
+ file: "assets/widget-ABC123.js",
130
+ },
131
+ "style.css": { file: "assets/style-XYZ.css" },
132
+ });
133
+ const { client, teardown } = await connectHttp(registerWidget, {
134
+ "x-forwarded-host": "foo.com",
135
+ "x-forwarded-proto": "https",
136
+ "x-forwarded-prefix": "/v1/canary",
137
+ });
138
+ try {
139
+ const { contents } = await client.readResource({
140
+ uri: "ui://views/ext-apps/widget.html",
141
+ });
142
+ const { text } = textContent(contents);
143
+ // Forwarded origin + prefix. (`assets/assets` is the existing layout: the
144
+ // manifest `file` already carries `assets/` and the template prepends
145
+ // `/assets/`; build.tsx's `_redirects` collapses it on the asset host.)
146
+ expect(text).toContain("https://foo.com/v1/canary/assets/assets/widget-ABC123.js");
147
+ expect(text).toContain("https://foo.com/v1/canary/assets/assets/style-XYZ.css");
148
+ // No unprefixed asset URL leaks through.
149
+ expect(text).not.toContain("foo.com/assets/");
150
+ }
151
+ finally {
152
+ await teardown();
153
+ }
154
+ });
155
+ // Back-compat: older Skybridge advertised the view at `ui://views/apps-sdk/...`
156
+ // via openai/outputTemplate. We no longer advertise it, but apps published then
157
+ // have it cached, so the read must still resolve to the ext-apps content.
158
+ it("resolves the legacy apps-sdk URL to the ext-apps content", async () => {
159
+ const { client, teardown } = await connect(registerWidget);
160
+ const legacyUri = "ui://views/apps-sdk/widget.html";
161
+ const { contents } = await client.readResource({ uri: legacyUri });
162
+ expect(contents).toHaveLength(1);
163
+ const content = contents[0];
164
+ expect(content.mimeType).toBe("text/html;profile=mcp-app");
165
+ expect(content.text.length).toBeGreaterThan(0);
166
+ // The response echoes the requested (legacy) URI, never the canonical one.
167
+ expect(content.uri).toBe(legacyUri);
85
168
  await teardown();
86
169
  });
87
170
  });
@@ -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":"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","sourcesContent":["import http from \"node:http\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport { InMemoryTransport } from \"@modelcontextprotocol/sdk/inMemory.js\";\nimport type { RequestHandler } from \"express\";\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { __setBuildManifest, McpServer } 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\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\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 server = new McpServer({ name: \"test\", version: \"1.0.0\" });\n register(server);\n const httpServer = http.createServer();\n await createApp({ mcpServer: server, httpServer });\n const listening = http.createServer(server.express);\n await new Promise<void>((resolve) => listening.listen(0, resolve));\n 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 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 // 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"]}
@@ -19,7 +19,7 @@ export const viewsDevServer = async (httpServer) => {
19
19
  const root = process.cwd();
20
20
  const configFile = path.join(root, "vite.config.ts");
21
21
  const configResult = await loadConfigFromFile({ command: "serve", mode: "development" }, configFile, root);
22
- const { build, preview, plugins: userPlugins = [], ...devConfig } = configResult?.config || {};
22
+ const { build, preview, plugins: userPlugins = [], server: userServer, ...devConfig } = configResult?.config || {};
23
23
  const vite = await createServer({
24
24
  ...devConfig,
25
25
  // Pass `false` so Vite skips re-resolving a config file — we already
@@ -27,6 +27,9 @@ export const viewsDevServer = async (httpServer) => {
27
27
  configFile: false,
28
28
  appType: "custom",
29
29
  server: {
30
+ // Keep the user's server options (e.g. forwardConsole) but force the
31
+ // three settings skybridge's Express integration requires.
32
+ ...userServer,
30
33
  allowedHosts: true,
31
34
  middlewareMode: true,
32
35
  hmr: {
@@ -1 +1 @@
1
- {"version":3,"file":"viewsDevServer.js","sourceRoot":"","sources":["../../src/server/viewsDevServer.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,OAAO,EAAE,EAAe,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,2BAA2B,EAAE,MAAM,sCAAsC,CAAC;AAEnF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EACjC,UAAuB,EACN,EAAE;IACnB,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAEhC,MAAM,EAAE,YAAY,EAAE,kBAAkB,EAAE,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC;IAElE,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAC3B,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IAErD,MAAM,YAAY,GAAG,MAAM,kBAAkB,CAC3C,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,EACzC,UAAU,EACV,IAAI,CACL,CAAC;IAEF,MAAM,EACJ,KAAK,EACL,OAAO,EACP,OAAO,EAAE,WAAW,GAAG,EAAE,EACzB,GAAG,SAAS,EACb,GAAG,YAAY,EAAE,MAAM,IAAI,EAAE,CAAC;IAE/B,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC;QAC9B,GAAG,SAAS;QACZ,qEAAqE;QACrE,6CAA6C;QAC7C,UAAU,EAAE,KAAK;QACjB,OAAO,EAAE,QAAQ;QACjB,MAAM,EAAE;YACN,YAAY,EAAE,IAAI;YAClB,cAAc,EAAE,IAAI;YACpB,GAAG,EAAE;gBACH,MAAM,EAAE,UAAU;aACnB;SACF;QACD,IAAI;QACJ,uEAAuE;QACvE,kDAAkD;QAClD,OAAO,EAAE,CAAC,GAAG,WAAW,EAAE,2BAA2B,EAAE,CAAC;KACzD,CAAC,CAAC;IAEH,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IACnB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IAElC,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC","sourcesContent":["import type http from \"node:http\";\nimport path from \"node:path\";\nimport cors from \"cors\";\nimport express, { type Router } from \"express\";\nimport { assetBaseUrlTransformPlugin } from \"./asset-base-url-transform-plugin.js\";\n\n/**\n * Vite dev-server middleware for view assets.\n *\n * MUST be mounted at the Express app root so Vite can intercept\n * `/@vite/client`, `/@react-refresh`, and `/_skybridge/view/...` imports:\n *\n * const app = express();\n * if (env.NODE_ENV !== \"production\") {\n * app.use(await viewsDevServer(httpServer));\n * }\n */\nexport const viewsDevServer = async (\n httpServer: http.Server,\n): Promise<Router> => {\n const router = express.Router();\n\n const { createServer, loadConfigFromFile } = await import(\"vite\");\n\n const root = process.cwd();\n const configFile = path.join(root, \"vite.config.ts\");\n\n const configResult = await loadConfigFromFile(\n { command: \"serve\", mode: \"development\" },\n configFile,\n root,\n );\n\n const {\n build,\n preview,\n plugins: userPlugins = [],\n ...devConfig\n } = configResult?.config || {};\n\n const vite = await createServer({\n ...devConfig,\n // Pass `false` so Vite skips re-resolving a config file — we already\n // loaded and spread the user's config above.\n configFile: false,\n appType: \"custom\",\n server: {\n allowedHosts: true,\n middlewareMode: true,\n hmr: {\n server: httpServer,\n },\n },\n root,\n // optimizeDeps is set by the skybridge Vite plugin (entries + include)\n // so it can derive the view glob from `viewsDir`.\n plugins: [...userPlugins, assetBaseUrlTransformPlugin()],\n });\n\n router.use(cors());\n router.use(\"/\", vite.middlewares);\n\n return router;\n};\n"]}
1
+ {"version":3,"file":"viewsDevServer.js","sourceRoot":"","sources":["../../src/server/viewsDevServer.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,OAAO,EAAE,EAAe,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,2BAA2B,EAAE,MAAM,sCAAsC,CAAC;AAEnF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EACjC,UAAuB,EACN,EAAE;IACnB,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAEhC,MAAM,EAAE,YAAY,EAAE,kBAAkB,EAAE,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC;IAElE,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAC3B,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IAErD,MAAM,YAAY,GAAG,MAAM,kBAAkB,CAC3C,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,EACzC,UAAU,EACV,IAAI,CACL,CAAC;IAEF,MAAM,EACJ,KAAK,EACL,OAAO,EACP,OAAO,EAAE,WAAW,GAAG,EAAE,EACzB,MAAM,EAAE,UAAU,EAClB,GAAG,SAAS,EACb,GAAG,YAAY,EAAE,MAAM,IAAI,EAAE,CAAC;IAE/B,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC;QAC9B,GAAG,SAAS;QACZ,qEAAqE;QACrE,6CAA6C;QAC7C,UAAU,EAAE,KAAK;QACjB,OAAO,EAAE,QAAQ;QACjB,MAAM,EAAE;YACN,qEAAqE;YACrE,2DAA2D;YAC3D,GAAG,UAAU;YACb,YAAY,EAAE,IAAI;YAClB,cAAc,EAAE,IAAI;YACpB,GAAG,EAAE;gBACH,MAAM,EAAE,UAAU;aACnB;SACF;QACD,IAAI;QACJ,uEAAuE;QACvE,kDAAkD;QAClD,OAAO,EAAE,CAAC,GAAG,WAAW,EAAE,2BAA2B,EAAE,CAAC;KACzD,CAAC,CAAC;IAEH,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IACnB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IAElC,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC","sourcesContent":["import type http from \"node:http\";\nimport path from \"node:path\";\nimport cors from \"cors\";\nimport express, { type Router } from \"express\";\nimport { assetBaseUrlTransformPlugin } from \"./asset-base-url-transform-plugin.js\";\n\n/**\n * Vite dev-server middleware for view assets.\n *\n * MUST be mounted at the Express app root so Vite can intercept\n * `/@vite/client`, `/@react-refresh`, and `/_skybridge/view/...` imports:\n *\n * const app = express();\n * if (env.NODE_ENV !== \"production\") {\n * app.use(await viewsDevServer(httpServer));\n * }\n */\nexport const viewsDevServer = async (\n httpServer: http.Server,\n): Promise<Router> => {\n const router = express.Router();\n\n const { createServer, loadConfigFromFile } = await import(\"vite\");\n\n const root = process.cwd();\n const configFile = path.join(root, \"vite.config.ts\");\n\n const configResult = await loadConfigFromFile(\n { command: \"serve\", mode: \"development\" },\n configFile,\n root,\n );\n\n const {\n build,\n preview,\n plugins: userPlugins = [],\n server: userServer,\n ...devConfig\n } = configResult?.config || {};\n\n const vite = await createServer({\n ...devConfig,\n // Pass `false` so Vite skips re-resolving a config file — we already\n // loaded and spread the user's config above.\n configFile: false,\n appType: \"custom\",\n server: {\n // Keep the user's server options (e.g. forwardConsole) but force the\n // three settings skybridge's Express integration requires.\n ...userServer,\n allowedHosts: true,\n middlewareMode: true,\n hmr: {\n server: httpServer,\n },\n },\n root,\n // optimizeDeps is set by the skybridge Vite plugin (entries + include)\n // so it can derive the view glob from `viewsDir`.\n plugins: [...userPlugins, assetBaseUrlTransformPlugin()],\n });\n\n router.use(cors());\n router.use(\"/\", vite.middlewares);\n\n return router;\n};\n"]}
@@ -91,7 +91,7 @@ export declare function createTestServer(): McpServer<Record<never, import("../s
91
91
  processedAt: number;
92
92
  region: string;
93
93
  }>;
94
- }>;
94
+ }, import("../server/auth.js").ExtraClaims>;
95
95
  export declare function createMinimalTestServer(): McpServer<Record<never, import("../server/server.js").ToolDef<unknown, unknown, unknown>> & {
96
96
  "search-trip": import("../server/server.js").ToolDef<{
97
97
  destination: string;
@@ -100,7 +100,7 @@ export declare function createMinimalTestServer(): McpServer<Record<never, impor
100
100
  id: string;
101
101
  }[];
102
102
  }, unknown>;
103
- }>;
103
+ }, import("../server/auth.js").ExtraClaims>;
104
104
  export declare function createInterfaceTestServer(): McpServer<Record<never, import("../server/server.js").ToolDef<unknown, unknown, unknown>> & {
105
105
  "interface-view": import("../server/server.js").ToolDef<{
106
106
  id: string;
@@ -111,7 +111,7 @@ export declare function createInterfaceTestServer(): McpServer<Record<never, imp
111
111
  processedBy: string;
112
112
  version: number;
113
113
  }>;
114
- }>;
114
+ }, import("../server/auth.js").ExtraClaims>;
115
115
  export declare function createMockExtra(host: string, options?: {
116
116
  headers?: Record<string, string | string[]>;
117
117
  url?: URL | string;
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
2
2
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
3
  import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
4
4
  import { afterEach, beforeEach, describe, expect, it, vi, } from "vitest";
5
+ import { TOOL_OUTPUT_WARNING_TOKENS } from "../context-warnings.js";
5
6
  import { McpServer as McpServerClass } from "../server/server.js";
6
7
  import { createMockExtra, createMockMcpServer, resetTestEnv, setTestEnv, } from "./utils.js";
7
8
  const mockManifest = {
@@ -74,20 +75,24 @@ describe("McpServer.registerTool (unified API)", () => {
74
75
  const serverUrl = `http://${host}`;
75
76
  const hmrUrl = `ws://${host}`;
76
77
  const mockExtra = createMockExtra(host);
77
- const result = await appsSdkResourceCallback(new URL("ui://views/apps-sdk/my-view.html"), mockExtra);
78
+ const result = await appsSdkResourceCallback(new URL("ui://views/ext-apps/my-view.html"), mockExtra);
78
79
  expect(mockRegisterTool).toHaveBeenCalled();
79
80
  expect(result).toEqual({
80
81
  contents: [
81
82
  {
82
- uri: "ui://views/apps-sdk/my-view.html",
83
- mimeType: "text/html+skybridge",
83
+ uri: "ui://views/ext-apps/my-view.html",
84
+ mimeType: "text/html;profile=mcp-app",
84
85
  text: expect.stringContaining('<div id="root"></div>'),
85
86
  _meta: {
86
- "openai/widgetCSP": {
87
- resource_domains: [serverUrl],
88
- connect_domains: [serverUrl, hmrUrl],
87
+ ui: {
88
+ csp: {
89
+ resourceDomains: [serverUrl],
90
+ connectDomains: [serverUrl, hmrUrl],
91
+ baseUriDomains: [serverUrl],
92
+ },
93
+ domain: serverUrl,
94
+ description: "Test view",
89
95
  },
90
- "openai/widgetDomain": serverUrl,
91
96
  "openai/widgetDescription": "Test view",
92
97
  },
93
98
  },
@@ -113,20 +118,24 @@ describe("McpServer.registerTool (unified API)", () => {
113
118
  const host = "myapp.com";
114
119
  const serverUrl = `https://${host}`;
115
120
  const mockExtra = createMockExtra(host);
116
- const versionedUri = `ui://views/apps-sdk/my-view.html${expectedVersionParam("assets/my-view-abc123.js", "style.css")}`;
121
+ const versionedUri = `ui://views/ext-apps/my-view.html${expectedVersionParam("assets/my-view-abc123.js", "style.css")}`;
117
122
  const result = await appsSdkResourceCallback(new URL(versionedUri), mockExtra);
118
123
  expect(result).toEqual({
119
124
  contents: [
120
125
  {
121
126
  uri: versionedUri,
122
- mimeType: "text/html+skybridge",
127
+ mimeType: "text/html;profile=mcp-app",
123
128
  text: expect.stringContaining('<div id="root"></div>'),
124
129
  _meta: {
125
- "openai/widgetCSP": {
126
- resource_domains: [serverUrl],
127
- connect_domains: [serverUrl],
130
+ ui: {
131
+ csp: {
132
+ resourceDomains: [serverUrl],
133
+ connectDomains: [serverUrl],
134
+ baseUriDomains: [serverUrl],
135
+ },
136
+ domain: serverUrl,
137
+ description: "Test view",
128
138
  },
129
- "openai/widgetDomain": serverUrl,
130
139
  "openai/widgetDescription": "Test view",
131
140
  },
132
141
  },
@@ -145,7 +154,7 @@ describe("McpServer.registerTool (unified API)", () => {
145
154
  view: { component: "my-view", description: "Test view" },
146
155
  }, vi.fn());
147
156
  const extAppsResourceCallback = mockRegisterResource.mock
148
- .calls[1]?.[3];
157
+ .calls[0]?.[3];
149
158
  expect(extAppsResourceCallback).toBeDefined();
150
159
  const forwardedUrl = "https://everything-3a2c1264.staging.alpic.live/mcp?foo=bar";
151
160
  const expectedDomain = `${crypto
@@ -170,9 +179,10 @@ describe("McpServer.registerTool (unified API)", () => {
170
179
  description: "Test view",
171
180
  domain: expectedDomain,
172
181
  },
182
+ "openai/widgetDescription": "Test view",
173
183
  });
174
184
  });
175
- it("should register resources with correct hostType for both apps-sdk and ext-apps", async () => {
185
+ it("should register a single ext-apps resource", async () => {
176
186
  server.registerTool({
177
187
  name: "my-view",
178
188
  description: "Test tool",
@@ -182,37 +192,14 @@ describe("McpServer.registerTool (unified API)", () => {
182
192
  prefersBorder: true,
183
193
  },
184
194
  }, vi.fn());
185
- expect(mockRegisterResource).toHaveBeenCalledTimes(2);
186
- const appsSdkCallback = mockRegisterResource.mock
195
+ expect(mockRegisterResource).toHaveBeenCalledTimes(1);
196
+ const resourceCallback = mockRegisterResource.mock
187
197
  .calls[0]?.[3];
188
198
  const host = "localhost:3000";
189
199
  const serverUrl = `http://${host}`;
190
200
  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({
201
+ const result = await resourceCallback(new URL("ui://views/ext-apps/my-view.html"), createMockExtra(host));
202
+ expect(result).toEqual({
216
203
  contents: [
217
204
  {
218
205
  uri: "ui://views/ext-apps/my-view.html",
@@ -229,27 +216,26 @@ describe("McpServer.registerTool (unified API)", () => {
229
216
  description: "Test view",
230
217
  prefersBorder: true,
231
218
  },
219
+ "openai/widgetDescription": "Test view",
232
220
  },
233
221
  },
234
222
  ],
235
223
  });
236
- expect(extAppsResult.contents[0]?.text).toContain('window.skybridge = { hostType: "mcp-app", serverUrl: "http://localhost:3000" };');
224
+ expect(result.contents[0]?.text).toContain('window.skybridge = { hostType: "mcp-app", serverUrl: "http://localhost:3000" };');
237
225
  });
238
- it("should register tool with ui.resourceUri metadata", async () => {
226
+ it("should advertise the single resource via ui.resourceUri only (no openai/outputTemplate)", async () => {
239
227
  server.registerTool({
240
228
  name: "my-view",
241
229
  description: "Test tool",
242
230
  view: { component: "my-view", description: "Test view" },
243
231
  }, 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
- });
232
+ const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
233
+ const uri = "ui://views/ext-apps/my-view.html";
234
+ expect(toolConfig._meta?.ui?.resourceUri).toBe(uri);
235
+ expect(toolConfig._meta?.["ui/resourceUri"]).toBe(uri);
236
+ expect(toolConfig._meta?.["openai/outputTemplate"]).toBeUndefined();
251
237
  });
252
- it("should register tool with openai/outputTemplate when apps-sdk only", async () => {
238
+ it("treats the deprecated hosts option as a no-op (always the single resource)", async () => {
253
239
  server.registerTool({
254
240
  name: "my-view",
255
241
  description: "Test tool",
@@ -259,11 +245,10 @@ describe("McpServer.registerTool (unified API)", () => {
259
245
  hosts: ["apps-sdk"],
260
246
  },
261
247
  }, 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");
248
+ expect(mockRegisterResource).toHaveBeenCalledTimes(1);
249
+ const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
250
+ expect(toolConfig._meta?.ui?.resourceUri).toBe("ui://views/ext-apps/my-view.html");
251
+ expect(toolConfig._meta?.["openai/outputTemplate"]).toBeUndefined();
267
252
  });
268
253
  it("should not version view URIs in development", () => {
269
254
  server.registerTool({
@@ -272,12 +257,11 @@ describe("McpServer.registerTool (unified API)", () => {
272
257
  view: { component: "my-view", description: "Test view" },
273
258
  }, vi.fn());
274
259
  const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
275
- expect(toolConfig._meta?.["openai/outputTemplate"]).toBe("ui://views/apps-sdk/my-view.html");
260
+ expect(toolConfig._meta?.["openai/outputTemplate"]).toBeUndefined();
276
261
  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");
262
+ // The URI registered with the resource handler must match ui.resourceUri
263
+ // exactly so the SDK can resolve `resources/read` requests.
264
+ expect(mockRegisterResource.mock.calls[0]?.[1]).toBe("ui://views/ext-apps/my-view.html");
281
265
  });
282
266
  it("should append a stable content hash to view URIs in production", () => {
283
267
  setTestEnv({ NODE_ENV: "production" });
@@ -288,10 +272,9 @@ describe("McpServer.registerTool (unified API)", () => {
288
272
  }, vi.fn());
289
273
  const expected = expectedVersionParam("assets/my-view-abc123.js", "style.css");
290
274
  const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
291
- expect(toolConfig._meta?.["openai/outputTemplate"]).toBe(`ui://views/apps-sdk/my-view.html${expected}`);
275
+ expect(toolConfig._meta?.["openai/outputTemplate"]).toBeUndefined();
292
276
  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}`);
277
+ expect(mockRegisterResource.mock.calls[0]?.[1]).toBe(`ui://views/ext-apps/my-view.html${expected}`);
295
278
  });
296
279
  it("should produce different version params for views with different bundles", () => {
297
280
  setTestEnv({ NODE_ENV: "production" });
@@ -305,8 +288,8 @@ describe("McpServer.registerTool (unified API)", () => {
305
288
  description: "Second tool",
306
289
  view: { component: "folder-view" },
307
290
  }, vi.fn());
308
- const myviewTemplate = (mockRegisterTool.mock.calls[0]?.[1])._meta?.["openai/outputTemplate"];
309
- const folderviewTemplate = (mockRegisterTool.mock.calls[1]?.[1])._meta?.["openai/outputTemplate"];
291
+ const myviewTemplate = mockRegisterTool.mock.calls[0][1]._meta?.ui?.resourceUri;
292
+ const folderviewTemplate = mockRegisterTool.mock.calls[1][1]._meta?.ui?.resourceUri;
310
293
  expect(myviewTemplate).not.toEqual(folderviewTemplate);
311
294
  expect(myviewTemplate).toMatch(/\?v=[0-9a-f]{8}$/);
312
295
  expect(folderviewTemplate).toMatch(/\?v=[0-9a-f]{8}$/);
@@ -319,26 +302,7 @@ describe("McpServer.registerTool (unified API)", () => {
319
302
  view: { component: "unknown-view" },
320
303
  }, vi.fn());
321
304
  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();
305
+ expect(toolConfig._meta?.ui?.resourceUri).toBe("ui://views/ext-apps/unknown-view.html");
342
306
  });
343
307
  it("should inject viewUUID into _meta of tool callback results", async () => {
344
308
  const mockToolCallback = vi.fn().mockResolvedValue({
@@ -358,6 +322,21 @@ describe("McpServer.registerTool (unified API)", () => {
358
322
  expect(typeof result._meta?.viewUUID).toBe("string");
359
323
  expect(result._meta?.viewUUID).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
360
324
  });
325
+ it("warns when a tool callback returns large model-visible output", async () => {
326
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => { });
327
+ server.registerTool({ name: "large-output", description: "Test tool" }, vi.fn().mockResolvedValue({
328
+ content: [
329
+ {
330
+ type: "text",
331
+ text: "x".repeat(TOOL_OUTPUT_WARNING_TOKENS * 4),
332
+ },
333
+ ],
334
+ }));
335
+ const wrappedCallback = mockRegisterTool.mock.calls[0]?.[2];
336
+ await wrappedCallback({}, {});
337
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Tool "large-output" returned'));
338
+ warnSpy.mockRestore();
339
+ });
361
340
  it("should preserve existing _meta when injecting viewUUID", async () => {
362
341
  const mockToolCallback = vi.fn().mockResolvedValue({
363
342
  content: [{ type: "text", text: "result" }],
@@ -476,11 +455,12 @@ describe("McpServer.registerTool (unified API)", () => {
476
455
  const host = "localhost:3000";
477
456
  const serverUrl = `http://${host}`;
478
457
  const hmrUrl = `ws://${host}`;
479
- const result = await appsSdkCallback(new URL("ui://views/apps-sdk/csp-tool.html"), createMockExtra(host));
458
+ const result = await appsSdkCallback(new URL("ui://views/ext-apps/csp-tool.html"), createMockExtra(host));
480
459
  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"],
460
+ expect(meta.ui?.csp).toEqual({
461
+ resourceDomains: [serverUrl, "https://cdn.example.com"],
462
+ connectDomains: [serverUrl, hmrUrl, "https://api.example.com"],
463
+ baseUriDomains: [serverUrl],
484
464
  });
485
465
  });
486
466
  it("should let view._meta override framework-computed keys", async () => {
@@ -492,18 +472,20 @@ describe("McpServer.registerTool (unified API)", () => {
492
472
  description: "Test view",
493
473
  csp: { connectDomains: ["https://api.x.com"] },
494
474
  _meta: {
495
- "openai/widgetCSP": {
496
- connect_domains: ["https://api.y.com"],
475
+ ui: {
476
+ csp: {
477
+ connectDomains: ["https://api.y.com"],
478
+ },
497
479
  },
498
480
  },
499
481
  },
500
482
  }, vi.fn());
501
483
  const appsSdkCallback = mockRegisterResource.mock
502
484
  .calls[0]?.[3];
503
- const result = await appsSdkCallback(new URL("ui://views/apps-sdk/override-tool.html"), createMockExtra("localhost:3000"));
485
+ const result = await appsSdkCallback(new URL("ui://views/ext-apps/override-tool.html"), createMockExtra("localhost:3000"));
504
486
  const meta = result.contents[0]?._meta;
505
- expect(meta["openai/widgetCSP"]).toEqual({
506
- connect_domains: ["https://api.y.com"],
487
+ expect(meta.ui?.csp).toEqual({
488
+ connectDomains: ["https://api.y.com"],
507
489
  });
508
490
  });
509
491
  it("should pass user _meta keys through to tool config", () => {
@@ -553,16 +535,18 @@ describe("resources/list view _meta injection", () => {
553
535
  await server.close();
554
536
  const appsSdk = resources.find((r) => r.uri.includes("apps-sdk"));
555
537
  const extApps = resources.find((r) => r.uri.includes("ext-apps"));
556
- expect(appsSdk?._meta).toBeDefined();
538
+ expect(appsSdk).toBeUndefined();
557
539
  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
- const extUi = (extApps?._meta).ui;
540
+ const extUi = extApps._meta?.ui;
563
541
  expect(extUi?.csp?.connectDomains?.length).toBeGreaterThan(0);
564
542
  expect(extUi?.csp?.resourceDomains).toContain("https://fonts.googleapis.com");
565
543
  expect(extUi?.domain).toBe("skybridge.tech");
544
+ const meta = extApps?._meta;
545
+ expect(meta["openai/widgetDescription"]).toBe("Onboarding deck");
546
+ expect(meta["openai/widgetCSP"]?.redirect_domains).toEqual([
547
+ "https://docs.skybridge.tech",
548
+ ]);
549
+ expect(meta.ui?.csp).not.toHaveProperty("redirectDomains");
566
550
  });
567
551
  });
568
552
  //# sourceMappingURL=view.test.js.map