skybridge 0.0.0-dev.89e35fa → 0.0.0-dev.89f9a0f

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 (255) hide show
  1. package/README.md +2 -0
  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 +35 -5
  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/discovery.d.ts +32 -0
  39. package/dist/server/auth/discovery.js +56 -0
  40. package/dist/server/auth/discovery.js.map +1 -0
  41. package/dist/server/auth/discovery.test.d.ts +1 -0
  42. package/dist/server/auth/discovery.test.js +93 -0
  43. package/dist/server/auth/discovery.test.js.map +1 -0
  44. package/dist/server/auth/index.d.ts +44 -0
  45. package/dist/server/auth/index.js +2 -0
  46. package/dist/server/auth/index.js.map +1 -0
  47. package/dist/server/auth/providers/auth0.d.ts +40 -0
  48. package/dist/server/auth/providers/auth0.js +31 -0
  49. package/dist/server/auth/providers/auth0.js.map +1 -0
  50. package/dist/server/auth/providers/auth0.test.d.ts +1 -0
  51. package/dist/server/auth/providers/auth0.test.js +50 -0
  52. package/dist/server/auth/providers/auth0.test.js.map +1 -0
  53. package/dist/server/auth/providers/authplane.d.ts +49 -0
  54. package/dist/server/auth/providers/authplane.js +57 -0
  55. package/dist/server/auth/providers/authplane.js.map +1 -0
  56. package/dist/server/auth/providers/authplane.test.d.ts +1 -0
  57. package/dist/server/auth/providers/authplane.test.js +140 -0
  58. package/dist/server/auth/providers/authplane.test.js.map +1 -0
  59. package/dist/server/auth/providers/clerk.d.ts +51 -0
  60. package/dist/server/auth/providers/clerk.js +19 -0
  61. package/dist/server/auth/providers/clerk.js.map +1 -0
  62. package/dist/server/auth/providers/clerk.test.d.ts +1 -0
  63. package/dist/server/auth/providers/clerk.test.js +30 -0
  64. package/dist/server/auth/providers/clerk.test.js.map +1 -0
  65. package/dist/server/auth/providers/custom.d.ts +40 -0
  66. package/dist/server/auth/providers/custom.js +56 -0
  67. package/dist/server/auth/providers/custom.js.map +1 -0
  68. package/dist/server/auth/providers/custom.test.d.ts +1 -0
  69. package/dist/server/auth/providers/custom.test.js +158 -0
  70. package/dist/server/auth/providers/custom.test.js.map +1 -0
  71. package/dist/server/auth/providers/descope.d.ts +44 -0
  72. package/dist/server/auth/providers/descope.js +44 -0
  73. package/dist/server/auth/providers/descope.js.map +1 -0
  74. package/dist/server/auth/providers/descope.test.d.ts +1 -0
  75. package/dist/server/auth/providers/descope.test.js +62 -0
  76. package/dist/server/auth/providers/descope.test.js.map +1 -0
  77. package/dist/server/auth/providers/shared.d.ts +2 -0
  78. package/dist/server/auth/providers/shared.js +6 -0
  79. package/dist/server/auth/providers/shared.js.map +1 -0
  80. package/dist/server/auth/providers/shared.test.d.ts +1 -0
  81. package/dist/server/auth/providers/shared.test.js +10 -0
  82. package/dist/server/auth/providers/shared.test.js.map +1 -0
  83. package/dist/server/auth/providers/stytch.d.ts +25 -0
  84. package/dist/server/auth/providers/stytch.js +16 -0
  85. package/dist/server/auth/providers/stytch.js.map +1 -0
  86. package/dist/server/auth/providers/verify-spy.d.ts +12 -0
  87. package/dist/server/auth/providers/verify-spy.js +21 -0
  88. package/dist/server/auth/providers/verify-spy.js.map +1 -0
  89. package/dist/server/auth/providers/workos.d.ts +33 -0
  90. package/dist/server/auth/providers/workos.js +15 -0
  91. package/dist/server/auth/providers/workos.js.map +1 -0
  92. package/dist/server/auth/security-schemes.d.ts +23 -0
  93. package/dist/server/auth/security-schemes.js +64 -0
  94. package/dist/server/auth/security-schemes.js.map +1 -0
  95. package/dist/server/auth/security-schemes.test.d.ts +1 -0
  96. package/dist/server/auth/security-schemes.test.js +99 -0
  97. package/dist/server/auth/security-schemes.test.js.map +1 -0
  98. package/dist/server/auth/setup.d.ts +6 -0
  99. package/dist/server/auth/setup.js +92 -0
  100. package/dist/server/auth/setup.js.map +1 -0
  101. package/dist/server/auth/setup.test.d.ts +1 -0
  102. package/dist/server/auth/setup.test.js +508 -0
  103. package/dist/server/auth/setup.test.js.map +1 -0
  104. package/dist/server/auth/verify.d.ts +46 -0
  105. package/dist/server/auth/verify.js +62 -0
  106. package/dist/server/auth/verify.js.map +1 -0
  107. package/dist/server/auth/verify.test.d.ts +1 -0
  108. package/dist/server/auth/verify.test.js +149 -0
  109. package/dist/server/auth/verify.test.js.map +1 -0
  110. package/dist/server/auth-extra.test-d.d.ts +1 -0
  111. package/dist/server/auth-extra.test-d.js +82 -0
  112. package/dist/server/auth-extra.test-d.js.map +1 -0
  113. package/dist/server/auth.d.ts +39 -1
  114. package/dist/server/auth.js.map +1 -1
  115. package/dist/server/express.test.js +31 -0
  116. package/dist/server/express.test.js.map +1 -1
  117. package/dist/server/host.d.ts +2 -0
  118. package/dist/server/host.js +11 -0
  119. package/dist/server/host.js.map +1 -0
  120. package/dist/server/host.test.d.ts +1 -0
  121. package/dist/server/host.test.js +13 -0
  122. package/dist/server/host.test.js.map +1 -0
  123. package/dist/server/index.d.ts +14 -3
  124. package/dist/server/index.js +11 -1
  125. package/dist/server/index.js.map +1 -1
  126. package/dist/server/middleware.d.ts +31 -6
  127. package/dist/server/middleware.js +33 -0
  128. package/dist/server/middleware.js.map +1 -1
  129. package/dist/server/middleware.test.js +37 -1
  130. package/dist/server/middleware.test.js.map +1 -1
  131. package/dist/server/requestOrigin.d.ts +7 -0
  132. package/dist/server/requestOrigin.js +25 -0
  133. package/dist/server/requestOrigin.js.map +1 -0
  134. package/dist/server/server.d.ts +97 -20
  135. package/dist/server/server.js +237 -53
  136. package/dist/server/server.js.map +1 -1
  137. package/dist/server/skills-integration.test.d.ts +1 -0
  138. package/dist/server/skills-integration.test.js +79 -0
  139. package/dist/server/skills-integration.test.js.map +1 -0
  140. package/dist/server/skills.d.ts +29 -0
  141. package/dist/server/skills.js +195 -0
  142. package/dist/server/skills.js.map +1 -0
  143. package/dist/server/skills.test.d.ts +1 -0
  144. package/dist/server/skills.test.js +247 -0
  145. package/dist/server/skills.test.js.map +1 -0
  146. package/dist/server/view-name.test-d.d.ts +1 -0
  147. package/dist/server/view-name.test-d.js +8 -0
  148. package/dist/server/view-name.test-d.js.map +1 -0
  149. package/dist/server/view-resource-resolution.test.d.ts +6 -0
  150. package/dist/server/view-resource-resolution.test.js +171 -0
  151. package/dist/server/view-resource-resolution.test.js.map +1 -0
  152. package/dist/server/viewsDevServer.js +4 -1
  153. package/dist/server/viewsDevServer.js.map +1 -1
  154. package/dist/test/utils.d.ts +3 -3
  155. package/dist/test/view.test.js +37 -11
  156. package/dist/test/view.test.js.map +1 -1
  157. package/dist/web/bridges/adaptor.d.ts +6 -5
  158. package/dist/web/bridges/adaptor.js +50 -37
  159. package/dist/web/bridges/adaptor.js.map +1 -1
  160. package/dist/web/bridges/adaptor.test.js +32 -12
  161. package/dist/web/bridges/adaptor.test.js.map +1 -1
  162. package/dist/web/bridges/apps-sdk/bridge.d.ts +5 -1
  163. package/dist/web/bridges/apps-sdk/bridge.js +12 -0
  164. package/dist/web/bridges/apps-sdk/bridge.js.map +1 -1
  165. package/dist/web/bridges/get-adaptor.d.ts +0 -2
  166. package/dist/web/bridges/get-adaptor.js +1 -13
  167. package/dist/web/bridges/get-adaptor.js.map +1 -1
  168. package/dist/web/bridges/get-adaptor.test.js +3 -3
  169. package/dist/web/bridges/get-adaptor.test.js.map +1 -1
  170. package/dist/web/bridges/mcp-app/bridge.d.ts +14 -2
  171. package/dist/web/bridges/mcp-app/bridge.js +72 -4
  172. package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
  173. package/dist/web/bridges/mcp-app/bridge.test.js +1 -5
  174. package/dist/web/bridges/mcp-app/bridge.test.js.map +1 -1
  175. package/dist/web/bridges/mcp-app/types.d.ts +2 -0
  176. package/dist/web/bridges/mcp-app/types.js.map +1 -1
  177. package/dist/web/bridges/mcp-app/view-tools.test.d.ts +1 -0
  178. package/dist/web/bridges/mcp-app/view-tools.test.js +143 -0
  179. package/dist/web/bridges/mcp-app/view-tools.test.js.map +1 -0
  180. package/dist/web/bridges/types.d.ts +34 -6
  181. package/dist/web/bridges/types.js.map +1 -1
  182. package/dist/web/components/modal-provider.d.ts +1 -1
  183. package/dist/web/create-store.test.js +4 -3
  184. package/dist/web/create-store.test.js.map +1 -1
  185. package/dist/web/data-llm.d.ts +1 -1
  186. package/dist/web/data-llm.test.js +13 -3
  187. package/dist/web/data-llm.test.js.map +1 -1
  188. package/dist/web/generate-helpers.test-d.js +4 -2
  189. package/dist/web/generate-helpers.test-d.js.map +1 -1
  190. package/dist/web/hooks/index.d.ts +2 -0
  191. package/dist/web/hooks/index.js +2 -0
  192. package/dist/web/hooks/index.js.map +1 -1
  193. package/dist/web/hooks/test/utils.d.ts +2 -1
  194. package/dist/web/hooks/test/utils.js +1 -1
  195. package/dist/web/hooks/test/utils.js.map +1 -1
  196. package/dist/web/hooks/use-call-tool.test.js +6 -5
  197. package/dist/web/hooks/use-call-tool.test.js.map +1 -1
  198. package/dist/web/hooks/use-display-mode.test.js +4 -3
  199. package/dist/web/hooks/use-display-mode.test.js.map +1 -1
  200. package/dist/web/hooks/use-download.test.js +5 -5
  201. package/dist/web/hooks/use-download.test.js.map +1 -1
  202. package/dist/web/hooks/use-files.test.js +3 -3
  203. package/dist/web/hooks/use-files.test.js.map +1 -1
  204. package/dist/web/hooks/use-host-info.d.ts +22 -0
  205. package/dist/web/hooks/use-host-info.js +30 -0
  206. package/dist/web/hooks/use-host-info.js.map +1 -0
  207. package/dist/web/hooks/use-host-info.test.d.ts +1 -0
  208. package/dist/web/hooks/use-host-info.test.js +59 -0
  209. package/dist/web/hooks/use-host-info.test.js.map +1 -0
  210. package/dist/web/hooks/use-layout.test.js +5 -5
  211. package/dist/web/hooks/use-layout.test.js.map +1 -1
  212. package/dist/web/hooks/use-open-external.test.js +7 -11
  213. package/dist/web/hooks/use-open-external.test.js.map +1 -1
  214. package/dist/web/hooks/use-register-view-tool.d.ts +38 -0
  215. package/dist/web/hooks/use-register-view-tool.js +50 -0
  216. package/dist/web/hooks/use-register-view-tool.js.map +1 -0
  217. package/dist/web/hooks/use-request-close.test.js +5 -4
  218. package/dist/web/hooks/use-request-close.test.js.map +1 -1
  219. package/dist/web/hooks/use-request-modal.test.js +3 -3
  220. package/dist/web/hooks/use-request-modal.test.js.map +1 -1
  221. package/dist/web/hooks/use-request-size.test.js +3 -3
  222. package/dist/web/hooks/use-request-size.test.js.map +1 -1
  223. package/dist/web/hooks/use-set-open-in-app-url.test.js +2 -2
  224. package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -1
  225. package/dist/web/hooks/use-tool-info.d.ts +25 -7
  226. package/dist/web/hooks/use-tool-info.js +5 -8
  227. package/dist/web/hooks/use-tool-info.js.map +1 -1
  228. package/dist/web/hooks/use-tool-info.test-d.js +11 -29
  229. package/dist/web/hooks/use-tool-info.test-d.js.map +1 -1
  230. package/dist/web/hooks/use-tool-info.test.js +8 -8
  231. package/dist/web/hooks/use-tool-info.test.js.map +1 -1
  232. package/dist/web/hooks/use-user.test.js +5 -5
  233. package/dist/web/hooks/use-user.test.js.map +1 -1
  234. package/dist/web/hooks/use-view-state.js +19 -9
  235. package/dist/web/hooks/use-view-state.js.map +1 -1
  236. package/dist/web/hooks/use-view-state.test.js +34 -3
  237. package/dist/web/hooks/use-view-state.test.js.map +1 -1
  238. package/dist/web/plugin/plugin-build.test.d.ts +1 -0
  239. package/dist/web/plugin/plugin-build.test.js +54 -0
  240. package/dist/web/plugin/plugin-build.test.js.map +1 -0
  241. package/dist/web/plugin/plugin.d.ts +6 -0
  242. package/dist/web/plugin/plugin.js +18 -7
  243. package/dist/web/plugin/plugin.js.map +1 -1
  244. package/dist/web/plugin/plugin.test.d.ts +1 -0
  245. package/dist/web/plugin/plugin.test.js +61 -0
  246. package/dist/web/plugin/plugin.test.js.map +1 -0
  247. package/package.json +22 -20
  248. package/dist/cli/resolve-views-dir.d.ts +0 -1
  249. package/dist/cli/resolve-views-dir.js.map +0 -1
  250. package/dist/cli/use-open-tunnel-browser.d.ts +0 -6
  251. package/dist/cli/use-open-tunnel-browser.js +0 -19
  252. package/dist/cli/use-open-tunnel-browser.js.map +0 -1
  253. package/dist/web/bridges/host-context-stores.d.ts +0 -27
  254. package/dist/web/bridges/host-context-stores.js +0 -73
  255. package/dist/web/bridges/host-context-stores.js.map +0 -1
@@ -0,0 +1,171 @@
1
+ import http from "node:http";
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
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
+ }));
13
+ // SKY-435: a view resource must resolve no matter the `?v=` query param value.
14
+ // The version param is a content-derived cache key for external consumers
15
+ // (it busts host/CDN caches when the bundle changes); it is not part of the
16
+ // resource's identity, so a stale, absent, or arbitrary param must still serve
17
+ // the underlying asset.
18
+ async function connect(register) {
19
+ const server = new McpServer({ name: "test", version: "1.0.0" });
20
+ register(server);
21
+ const client = new Client({ name: "test-client", version: "1.0.0" });
22
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
23
+ await server.connect(serverTransport);
24
+ await client.connect(clientTransport);
25
+ return {
26
+ client,
27
+ teardown: async () => {
28
+ await client.close();
29
+ await server.close();
30
+ },
31
+ };
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
+ }
57
+ function registerWidget(server) {
58
+ server.registerTool({ name: "show", description: "show", view: { component: "widget" } }, () => ({ content: [{ type: "text", text: "ok" }] }));
59
+ }
60
+ /** Narrow a read-resource content entry to its text variant. */
61
+ function textContent(contents) {
62
+ return contents[0];
63
+ }
64
+ describe("view resource resolution (cache key)", () => {
65
+ afterEach(() => {
66
+ delete process.env.NODE_ENV;
67
+ });
68
+ it("resolves regardless of the ?v= param and echoes the requested URI", async () => {
69
+ const { client, teardown } = await connect(registerWidget);
70
+ const { resources } = await client.listResources();
71
+ const listed = resources.find((r) => r.uri.startsWith("ui://views/ext-apps/widget.html"));
72
+ // Dev build advertises no cache key.
73
+ expect(listed?.uri).toBe("ui://views/ext-apps/widget.html");
74
+ // A stale/arbitrary/absent param all resolve the same underlying asset,
75
+ // and the response echoes the URI the consumer asked for (never rewritten).
76
+ for (const uri of [
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",
80
+ ]) {
81
+ const { contents } = await client.readResource({ uri });
82
+ expect(contents).toHaveLength(1);
83
+ const content = textContent(contents);
84
+ expect(typeof content.text).toBe("string");
85
+ expect(content.text.length).toBeGreaterThan(0);
86
+ expect(content.uri).toBe(uri);
87
+ }
88
+ await teardown();
89
+ });
90
+ it("resolves a stale cache key when the canonical URI carries ?v= (prod)", async () => {
91
+ process.env.NODE_ENV = "production";
92
+ __setBuildManifest({
93
+ "src/views/widget.tsx": {
94
+ isEntry: true,
95
+ name: "widget",
96
+ file: "assets/widget-ABC123.js",
97
+ },
98
+ "style.css": { file: "assets/style-XYZ.css" },
99
+ });
100
+ const { client, teardown } = await connect(registerWidget);
101
+ const { resources } = await client.listResources();
102
+ const listed = resources.find((r) => r.uri.startsWith("ui://views/ext-apps/widget.html"));
103
+ // Production advertises a content-derived cache key.
104
+ expect(listed?.uri).toMatch(/^ui:\/\/views\/ext-apps\/widget\.html\?v=[0-9a-f]{8}$/);
105
+ // A consumer holding a stale key still resolves the current asset.
106
+ const staleUri = "ui://views/ext-apps/widget.html?v=00000000";
107
+ const { contents } = await client.readResource({ uri: staleUri });
108
+ expect(contents).toHaveLength(1);
109
+ const content = textContent(contents);
110
+ expect(typeof content.text).toBe("string");
111
+ expect(content.uri).toBe(staleUri);
112
+ await teardown();
113
+ });
114
+ it("still throws for an unknown view path", async () => {
115
+ const { client, teardown } = await connect(registerWidget);
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);
168
+ await teardown();
169
+ });
170
+ });
171
+ //# sourceMappingURL=view-resource-resolution.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"view-resource-resolution.test.js","sourceRoot":"","sources":["../../src/server/view-resource-resolution.test.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AACnG,OAAO,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAE1E,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE3D,EAAE,CAAC,IAAI,CAAC,qBAAqB,EAAE,GAAG,EAAE,CAAC,CAAC;IACpC,oBAAoB,EAAE,GAAG,EAAE,CACzB,CAAC,CAAC,IAAa,EAAE,IAAa,EAAE,IAAgB,EAAE,EAAE,CAClD,IAAI,EAAE,CAAmB;CAC9B,CAAC,CAAC,CAAC;AACJ,EAAE,CAAC,IAAI,CAAC,qBAAqB,EAAE,GAAG,EAAE,CAAC,CAAC;IACpC,cAAc,EAAE,CAAC,WAAoB,EAAE,EAAE,CACvC,CAAC,CAAC,IAAa,EAAE,IAAa,EAAE,IAAgB,EAAE,EAAE,CAClD,IAAI,EAAE,CAAmB;CAC9B,CAAC,CAAC,CAAC;AAUJ,+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,IAAI,cAAuC,CAAC;AAC5C,SAAS,CAAC,GAAG,EAAE,CAAC,cAAc,EAAE,KAAK,EAAE,CAAC,CAAC;AAEzC,gFAAgF;AAChF,KAAK,UAAU,WAAW,CACxB,QAAqC,EACrC,OAA+B;IAE/B,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;IACnD,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,UAAU,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;IACvC,MAAM,SAAS,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;IACnD,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACpD,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;IACnE,cAAc,GAAG,SAAS,CAAC;IAC3B,MAAM,IAAI,GAAI,SAAS,CAAC,OAAO,EAAuB,CAAC,IAAI,CAAC;IAE5D,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IACrE,MAAM,SAAS,GAAG,IAAI,6BAA6B,CACjD,IAAI,GAAG,CAAC,oBAAoB,IAAI,MAAM,CAAC,EACvC,EAAE,WAAW,EAAE,EAAE,OAAO,EAAE,EAAE,CAC7B,CAAC;IACF,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,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,4EAA4E;IAC5E,6EAA6E;IAC7E,8EAA8E;IAC9E,qCAAqC;IACrC,EAAE,CAAC,4DAA4D,EAAE,KAAK,IAAI,EAAE;QAC1E,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,WAAW,CAAC,cAAc,EAAE;YAC7D,kBAAkB,EAAE,SAAS;YAC7B,mBAAmB,EAAE,OAAO;YAC5B,oBAAoB,EAAE,YAAY;SACnC,CAAC,CAAC;QACH,IAAI,CAAC;YACH,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC;gBAC7C,GAAG,EAAE,iCAAiC;aACvC,CAAC,CAAC;YACH,MAAM,EAAE,IAAI,EAAE,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;YACvC,0EAA0E;YAC1E,sEAAsE;YACtE,wEAAwE;YACxE,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CACpB,0DAA0D,CAC3D,CAAC;YACF,MAAM,CAAC,IAAI,CAAC,CAAC,SAAS,CACpB,uDAAuD,CACxD,CAAC;YACF,yCAAyC;YACzC,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,iBAAiB,CAAC,CAAC;QAChD,CAAC;gBAAS,CAAC;YACT,MAAM,QAAQ,EAAE,CAAC;QACnB,CAAC;IACH,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 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 = {
@@ -92,6 +93,7 @@ describe("McpServer.registerTool (unified API)", () => {
92
93
  domain: serverUrl,
93
94
  description: "Test view",
94
95
  },
96
+ "openai/widgetDescription": "Test view",
95
97
  },
96
98
  },
97
99
  ],
@@ -134,6 +136,7 @@ describe("McpServer.registerTool (unified API)", () => {
134
136
  domain: serverUrl,
135
137
  description: "Test view",
136
138
  },
139
+ "openai/widgetDescription": "Test view",
137
140
  },
138
141
  },
139
142
  ],
@@ -176,6 +179,7 @@ describe("McpServer.registerTool (unified API)", () => {
176
179
  description: "Test view",
177
180
  domain: expectedDomain,
178
181
  },
182
+ "openai/widgetDescription": "Test view",
179
183
  });
180
184
  });
181
185
  it("should register a single ext-apps resource", async () => {
@@ -212,13 +216,14 @@ describe("McpServer.registerTool (unified API)", () => {
212
216
  description: "Test view",
213
217
  prefersBorder: true,
214
218
  },
219
+ "openai/widgetDescription": "Test view",
215
220
  },
216
221
  },
217
222
  ],
218
223
  });
219
224
  expect(result.contents[0]?.text).toContain('window.skybridge = { hostType: "mcp-app", serverUrl: "http://localhost:3000" };');
220
225
  });
221
- it("should advertise the single resource via ui.resourceUri and openai/outputTemplate alias", async () => {
226
+ it("should advertise the single resource via ui.resourceUri only (no openai/outputTemplate)", async () => {
222
227
  server.registerTool({
223
228
  name: "my-view",
224
229
  description: "Test tool",
@@ -228,7 +233,7 @@ describe("McpServer.registerTool (unified API)", () => {
228
233
  const uri = "ui://views/ext-apps/my-view.html";
229
234
  expect(toolConfig._meta?.ui?.resourceUri).toBe(uri);
230
235
  expect(toolConfig._meta?.["ui/resourceUri"]).toBe(uri);
231
- expect(toolConfig._meta?.["openai/outputTemplate"]).toBe(uri);
236
+ expect(toolConfig._meta?.["openai/outputTemplate"]).toBeUndefined();
232
237
  });
233
238
  it("treats the deprecated hosts option as a no-op (always the single resource)", async () => {
234
239
  server.registerTool({
@@ -243,7 +248,7 @@ describe("McpServer.registerTool (unified API)", () => {
243
248
  expect(mockRegisterResource).toHaveBeenCalledTimes(1);
244
249
  const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
245
250
  expect(toolConfig._meta?.ui?.resourceUri).toBe("ui://views/ext-apps/my-view.html");
246
- expect(toolConfig._meta?.["openai/outputTemplate"]).toBe("ui://views/ext-apps/my-view.html");
251
+ expect(toolConfig._meta?.["openai/outputTemplate"]).toBeUndefined();
247
252
  });
248
253
  it("should not version view URIs in development", () => {
249
254
  server.registerTool({
@@ -252,10 +257,10 @@ describe("McpServer.registerTool (unified API)", () => {
252
257
  view: { component: "my-view", description: "Test view" },
253
258
  }, vi.fn());
254
259
  const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
255
- expect(toolConfig._meta?.["openai/outputTemplate"]).toBe("ui://views/ext-apps/my-view.html");
260
+ expect(toolConfig._meta?.["openai/outputTemplate"]).toBeUndefined();
256
261
  expect(toolConfig._meta?.ui?.resourceUri).toBe("ui://views/ext-apps/my-view.html");
257
- // The URI registered with the resource handler must match the URI in
258
- // outputTemplate exactly so the SDK can resolve `resources/read` requests.
262
+ // The URI registered with the resource handler must match ui.resourceUri
263
+ // exactly so the SDK can resolve `resources/read` requests.
259
264
  expect(mockRegisterResource.mock.calls[0]?.[1]).toBe("ui://views/ext-apps/my-view.html");
260
265
  });
261
266
  it("should append a stable content hash to view URIs in production", () => {
@@ -267,7 +272,7 @@ describe("McpServer.registerTool (unified API)", () => {
267
272
  }, vi.fn());
268
273
  const expected = expectedVersionParam("assets/my-view-abc123.js", "style.css");
269
274
  const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
270
- expect(toolConfig._meta?.["openai/outputTemplate"]).toBe(`ui://views/ext-apps/my-view.html${expected}`);
275
+ expect(toolConfig._meta?.["openai/outputTemplate"]).toBeUndefined();
271
276
  expect(toolConfig._meta?.ui?.resourceUri).toBe(`ui://views/ext-apps/my-view.html${expected}`);
272
277
  expect(mockRegisterResource.mock.calls[0]?.[1]).toBe(`ui://views/ext-apps/my-view.html${expected}`);
273
278
  });
@@ -283,8 +288,8 @@ describe("McpServer.registerTool (unified API)", () => {
283
288
  description: "Second tool",
284
289
  view: { component: "folder-view" },
285
290
  }, vi.fn());
286
- const myviewTemplate = (mockRegisterTool.mock.calls[0]?.[1])._meta?.["openai/outputTemplate"];
287
- 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;
288
293
  expect(myviewTemplate).not.toEqual(folderviewTemplate);
289
294
  expect(myviewTemplate).toMatch(/\?v=[0-9a-f]{8}$/);
290
295
  expect(folderviewTemplate).toMatch(/\?v=[0-9a-f]{8}$/);
@@ -297,7 +302,7 @@ describe("McpServer.registerTool (unified API)", () => {
297
302
  view: { component: "unknown-view" },
298
303
  }, vi.fn());
299
304
  const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
300
- expect(toolConfig._meta?.["openai/outputTemplate"]).toBe("ui://views/ext-apps/unknown-view.html");
305
+ expect(toolConfig._meta?.ui?.resourceUri).toBe("ui://views/ext-apps/unknown-view.html");
301
306
  });
302
307
  it("should inject viewUUID into _meta of tool callback results", async () => {
303
308
  const mockToolCallback = vi.fn().mockResolvedValue({
@@ -317,6 +322,21 @@ describe("McpServer.registerTool (unified API)", () => {
317
322
  expect(typeof result._meta?.viewUUID).toBe("string");
318
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}$/);
319
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
+ });
320
340
  it("should preserve existing _meta when injecting viewUUID", async () => {
321
341
  const mockToolCallback = vi.fn().mockResolvedValue({
322
342
  content: [{ type: "text", text: "result" }],
@@ -517,10 +537,16 @@ describe("resources/list view _meta injection", () => {
517
537
  const extApps = resources.find((r) => r.uri.includes("ext-apps"));
518
538
  expect(appsSdk).toBeUndefined();
519
539
  expect(extApps?._meta).toBeDefined();
520
- const extUi = (extApps?._meta).ui;
540
+ const extUi = extApps._meta?.ui;
521
541
  expect(extUi?.csp?.connectDomains?.length).toBeGreaterThan(0);
522
542
  expect(extUi?.csp?.resourceDomains).toContain("https://fonts.googleapis.com");
523
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");
524
550
  });
525
551
  });
526
552
  //# sourceMappingURL=view.test.js.map