skybridge 0.0.0-dev.fec1c58 → 0.0.0-dev.fef24cf

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 (309) hide show
  1. package/README.md +123 -116
  2. package/dist/cli/detect-port.d.ts +18 -0
  3. package/dist/cli/detect-port.js +61 -0
  4. package/dist/cli/detect-port.js.map +1 -0
  5. package/dist/cli/header.js +1 -1
  6. package/dist/cli/header.js.map +1 -1
  7. package/dist/cli/run-command.js.map +1 -1
  8. package/dist/cli/telemetry.js.map +1 -1
  9. package/dist/cli/tunnel-control-server.d.ts +9 -0
  10. package/dist/cli/tunnel-control-server.js +31 -0
  11. package/dist/cli/tunnel-control-server.js.map +1 -0
  12. package/dist/cli/tunnel-control-server.test.js +39 -0
  13. package/dist/cli/tunnel-control-server.test.js.map +1 -0
  14. package/dist/cli/tunnel-handler.d.ts +3 -0
  15. package/dist/cli/tunnel-handler.js +48 -0
  16. package/dist/cli/tunnel-handler.js.map +1 -0
  17. package/dist/cli/tunnel-handler.test.js +105 -0
  18. package/dist/cli/tunnel-handler.test.js.map +1 -0
  19. package/dist/cli/tunnel.d.ts +57 -0
  20. package/dist/cli/tunnel.js +154 -0
  21. package/dist/cli/tunnel.js.map +1 -0
  22. package/dist/cli/tunnel.test.d.ts +1 -0
  23. package/dist/cli/tunnel.test.js +190 -0
  24. package/dist/cli/tunnel.test.js.map +1 -0
  25. package/dist/cli/types.d.ts +5 -0
  26. package/dist/cli/types.js +2 -0
  27. package/dist/cli/types.js.map +1 -0
  28. package/dist/cli/use-execute-steps.js.map +1 -1
  29. package/dist/cli/use-messages.d.ts +3 -0
  30. package/dist/cli/use-messages.js +11 -0
  31. package/dist/cli/use-messages.js.map +1 -0
  32. package/dist/cli/use-nodemon.d.ts +2 -6
  33. package/dist/cli/use-nodemon.js +18 -14
  34. package/dist/cli/use-nodemon.js.map +1 -1
  35. package/dist/cli/use-open-browser.d.ts +1 -0
  36. package/dist/cli/use-open-browser.js +44 -0
  37. package/dist/cli/use-open-browser.js.map +1 -0
  38. package/dist/cli/use-tunnel.d.ts +14 -0
  39. package/dist/cli/use-tunnel.js +131 -0
  40. package/dist/cli/use-tunnel.js.map +1 -0
  41. package/dist/cli/use-typescript-check.d.ts +1 -0
  42. package/dist/cli/use-typescript-check.js +42 -7
  43. package/dist/cli/use-typescript-check.js.map +1 -1
  44. package/dist/commands/build.js +63 -7
  45. package/dist/commands/build.js.map +1 -1
  46. package/dist/commands/create.d.ts +9 -0
  47. package/dist/commands/create.js +30 -0
  48. package/dist/commands/create.js.map +1 -0
  49. package/dist/commands/dev.d.ts +4 -1
  50. package/dist/commands/dev.js +57 -8
  51. package/dist/commands/dev.js.map +1 -1
  52. package/dist/commands/start.d.ts +3 -1
  53. package/dist/commands/start.js +30 -9
  54. package/dist/commands/start.js.map +1 -1
  55. package/dist/commands/telemetry/disable.js.map +1 -1
  56. package/dist/commands/telemetry/enable.js.map +1 -1
  57. package/dist/commands/telemetry/status.js.map +1 -1
  58. package/dist/server/asset-base-url-transform-plugin.d.ts +6 -6
  59. package/dist/server/asset-base-url-transform-plugin.js +25 -11
  60. package/dist/server/asset-base-url-transform-plugin.js.map +1 -1
  61. package/dist/server/asset-base-url-transform-plugin.test.js +92 -14
  62. package/dist/server/asset-base-url-transform-plugin.test.js.map +1 -1
  63. package/dist/server/auth.d.ts +20 -0
  64. package/dist/server/auth.js +28 -0
  65. package/dist/server/auth.js.map +1 -0
  66. package/dist/server/content-helpers.d.ts +67 -0
  67. package/dist/server/content-helpers.js +79 -0
  68. package/dist/server/content-helpers.js.map +1 -0
  69. package/dist/server/content-helpers.test.d.ts +1 -0
  70. package/dist/server/content-helpers.test.js +70 -0
  71. package/dist/server/content-helpers.test.js.map +1 -0
  72. package/dist/server/express.d.ts +7 -5
  73. package/dist/server/express.js +60 -28
  74. package/dist/server/express.js.map +1 -1
  75. package/dist/server/express.test.js +381 -25
  76. package/dist/server/express.test.js.map +1 -1
  77. package/dist/server/file-ref.d.ts +28 -0
  78. package/dist/server/file-ref.js +27 -0
  79. package/dist/server/file-ref.js.map +1 -0
  80. package/dist/server/index.d.ts +7 -3
  81. package/dist/server/index.js +5 -2
  82. package/dist/server/index.js.map +1 -1
  83. package/dist/server/inferUtilityTypes.d.ts +6 -6
  84. package/dist/server/inferUtilityTypes.js.map +1 -1
  85. package/dist/server/metric.d.ts +14 -0
  86. package/dist/server/metric.js +62 -0
  87. package/dist/server/metric.js.map +1 -0
  88. package/dist/server/middleware.d.ts +137 -0
  89. package/dist/server/middleware.js +93 -0
  90. package/dist/server/middleware.js.map +1 -0
  91. package/dist/server/middleware.test-d.d.ts +1 -0
  92. package/dist/server/middleware.test-d.js +75 -0
  93. package/dist/server/middleware.test-d.js.map +1 -0
  94. package/dist/server/middleware.test.d.ts +1 -0
  95. package/dist/server/middleware.test.js +493 -0
  96. package/dist/server/middleware.test.js.map +1 -0
  97. package/dist/server/server.d.ts +358 -69
  98. package/dist/server/server.js +469 -102
  99. package/dist/server/server.js.map +1 -1
  100. package/dist/server/templateHelper.d.ts +5 -8
  101. package/dist/server/templateHelper.js +3 -22
  102. package/dist/server/templateHelper.js.map +1 -1
  103. package/dist/server/templates.generated.d.ts +4 -0
  104. package/dist/server/templates.generated.js +47 -0
  105. package/dist/server/templates.generated.js.map +1 -0
  106. package/dist/server/tunnel-proxy-router.d.ts +7 -0
  107. package/dist/server/tunnel-proxy-router.js +110 -0
  108. package/dist/server/tunnel-proxy-router.js.map +1 -0
  109. package/dist/server/tunnel-proxy-router.test.d.ts +1 -0
  110. package/dist/server/tunnel-proxy-router.test.js +229 -0
  111. package/dist/server/tunnel-proxy-router.test.js.map +1 -0
  112. package/dist/server/viewsDevServer.d.ts +14 -0
  113. package/dist/server/viewsDevServer.js +45 -0
  114. package/dist/server/viewsDevServer.js.map +1 -0
  115. package/dist/test/utils.d.ts +16 -244
  116. package/dist/test/utils.js +42 -37
  117. package/dist/test/utils.js.map +1 -1
  118. package/dist/test/view.test.d.ts +1 -0
  119. package/dist/test/view.test.js +568 -0
  120. package/dist/test/view.test.js.map +1 -0
  121. package/dist/version.d.ts +1 -0
  122. package/dist/version.js +3 -0
  123. package/dist/version.js.map +1 -0
  124. package/dist/web/bridges/apps-sdk/adaptor.d.ts +13 -7
  125. package/dist/web/bridges/apps-sdk/adaptor.js +62 -29
  126. package/dist/web/bridges/apps-sdk/adaptor.js.map +1 -1
  127. package/dist/web/bridges/apps-sdk/bridge.d.ts +2 -1
  128. package/dist/web/bridges/apps-sdk/bridge.js +1 -0
  129. package/dist/web/bridges/apps-sdk/bridge.js.map +1 -1
  130. package/dist/web/bridges/apps-sdk/index.d.ts +1 -1
  131. package/dist/web/bridges/apps-sdk/index.js.map +1 -1
  132. package/dist/web/bridges/apps-sdk/types.d.ts +26 -11
  133. package/dist/web/bridges/apps-sdk/types.js.map +1 -1
  134. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.d.ts +11 -0
  135. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js +11 -0
  136. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js.map +1 -1
  137. package/dist/web/bridges/get-adaptor.d.ts +7 -0
  138. package/dist/web/bridges/get-adaptor.js +7 -0
  139. package/dist/web/bridges/get-adaptor.js.map +1 -1
  140. package/dist/web/bridges/index.js.map +1 -1
  141. package/dist/web/bridges/mcp-app/adaptor.d.ts +25 -9
  142. package/dist/web/bridges/mcp-app/adaptor.js +156 -66
  143. package/dist/web/bridges/mcp-app/adaptor.js.map +1 -1
  144. package/dist/web/bridges/mcp-app/bridge.d.ts +14 -30
  145. package/dist/web/bridges/mcp-app/bridge.js +44 -196
  146. package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
  147. package/dist/web/bridges/mcp-app/index.js.map +1 -1
  148. package/dist/web/bridges/mcp-app/types.js.map +1 -1
  149. package/dist/web/bridges/mcp-app/use-mcp-app-context.d.ts +17 -3
  150. package/dist/web/bridges/mcp-app/use-mcp-app-context.js +14 -2
  151. package/dist/web/bridges/mcp-app/use-mcp-app-context.js.map +1 -1
  152. package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js +1 -41
  153. package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js.map +1 -1
  154. package/dist/web/bridges/types.d.ts +87 -14
  155. package/dist/web/bridges/types.js.map +1 -1
  156. package/dist/web/bridges/use-host-context.d.ts +5 -0
  157. package/dist/web/bridges/use-host-context.js +5 -0
  158. package/dist/web/bridges/use-host-context.js.map +1 -1
  159. package/dist/web/components/modal-provider.js +3 -5
  160. package/dist/web/components/modal-provider.js.map +1 -1
  161. package/dist/web/create-store.d.ts +26 -0
  162. package/dist/web/create-store.js +43 -3
  163. package/dist/web/create-store.js.map +1 -1
  164. package/dist/web/create-store.test.js +21 -18
  165. package/dist/web/create-store.test.js.map +1 -1
  166. package/dist/web/data-llm.d.ts +34 -1
  167. package/dist/web/data-llm.js +31 -3
  168. package/dist/web/data-llm.js.map +1 -1
  169. package/dist/web/data-llm.test.js +24 -23
  170. package/dist/web/data-llm.test.js.map +1 -1
  171. package/dist/web/generate-helpers.d.ts +22 -18
  172. package/dist/web/generate-helpers.js +22 -18
  173. package/dist/web/generate-helpers.js.map +1 -1
  174. package/dist/web/generate-helpers.test-d.js +26 -26
  175. package/dist/web/generate-helpers.test-d.js.map +1 -1
  176. package/dist/web/generate-helpers.test.js.map +1 -1
  177. package/dist/web/helpers/state.d.ts +2 -2
  178. package/dist/web/helpers/state.js +11 -11
  179. package/dist/web/helpers/state.js.map +1 -1
  180. package/dist/web/helpers/state.test.js +9 -9
  181. package/dist/web/helpers/state.test.js.map +1 -1
  182. package/dist/web/hooks/index.d.ts +5 -2
  183. package/dist/web/hooks/index.js +4 -1
  184. package/dist/web/hooks/index.js.map +1 -1
  185. package/dist/web/hooks/test/utils.d.ts +6 -2
  186. package/dist/web/hooks/test/utils.js +17 -2
  187. package/dist/web/hooks/test/utils.js.map +1 -1
  188. package/dist/web/hooks/use-call-tool.d.ts +45 -0
  189. package/dist/web/hooks/use-call-tool.js +28 -0
  190. package/dist/web/hooks/use-call-tool.js.map +1 -1
  191. package/dist/web/hooks/use-call-tool.test-d.js.map +1 -1
  192. package/dist/web/hooks/use-call-tool.test.js +27 -6
  193. package/dist/web/hooks/use-call-tool.test.js.map +1 -1
  194. package/dist/web/hooks/use-display-mode.d.ts +23 -3
  195. package/dist/web/hooks/use-display-mode.js +20 -0
  196. package/dist/web/hooks/use-display-mode.js.map +1 -1
  197. package/dist/web/hooks/use-display-mode.test-d.d.ts +1 -0
  198. package/dist/web/hooks/use-display-mode.test-d.js +8 -0
  199. package/dist/web/hooks/use-display-mode.test-d.js.map +1 -0
  200. package/dist/web/hooks/use-display-mode.test.js.map +1 -1
  201. package/dist/web/hooks/use-download.d.ts +5 -0
  202. package/dist/web/hooks/use-download.js +8 -0
  203. package/dist/web/hooks/use-download.js.map +1 -0
  204. package/dist/web/hooks/use-download.test.d.ts +1 -0
  205. package/dist/web/hooks/use-download.test.js +95 -0
  206. package/dist/web/hooks/use-download.test.js.map +1 -0
  207. package/dist/web/hooks/use-files.d.ts +34 -1
  208. package/dist/web/hooks/use-files.js +33 -0
  209. package/dist/web/hooks/use-files.js.map +1 -1
  210. package/dist/web/hooks/use-files.test.js +22 -2
  211. package/dist/web/hooks/use-files.test.js.map +1 -1
  212. package/dist/web/hooks/use-layout.d.ts +2 -0
  213. package/dist/web/hooks/use-layout.js +2 -0
  214. package/dist/web/hooks/use-layout.js.map +1 -1
  215. package/dist/web/hooks/use-layout.test.js +3 -3
  216. package/dist/web/hooks/use-layout.test.js.map +1 -1
  217. package/dist/web/hooks/use-open-external.d.ts +20 -1
  218. package/dist/web/hooks/use-open-external.js +17 -1
  219. package/dist/web/hooks/use-open-external.js.map +1 -1
  220. package/dist/web/hooks/use-open-external.test.js +26 -11
  221. package/dist/web/hooks/use-open-external.test.js.map +1 -1
  222. package/dist/web/hooks/use-request-close.d.ts +16 -0
  223. package/dist/web/hooks/use-request-close.js +21 -0
  224. package/dist/web/hooks/use-request-close.js.map +1 -0
  225. package/dist/web/hooks/use-request-close.test.d.ts +1 -0
  226. package/dist/web/hooks/use-request-close.test.js +52 -0
  227. package/dist/web/hooks/use-request-close.test.js.map +1 -0
  228. package/dist/web/hooks/use-request-modal.d.ts +16 -1
  229. package/dist/web/hooks/use-request-modal.js +19 -4
  230. package/dist/web/hooks/use-request-modal.js.map +1 -1
  231. package/dist/web/hooks/use-request-modal.test.js +5 -1
  232. package/dist/web/hooks/use-request-modal.test.js.map +1 -1
  233. package/dist/web/hooks/use-request-size.d.ts +20 -0
  234. package/dist/web/hooks/use-request-size.js +24 -0
  235. package/dist/web/hooks/use-request-size.js.map +1 -0
  236. package/dist/web/hooks/use-request-size.test.d.ts +1 -0
  237. package/dist/web/hooks/use-request-size.test.js +65 -0
  238. package/dist/web/hooks/use-request-size.test.js.map +1 -0
  239. package/dist/web/hooks/use-send-follow-up-message.d.ts +19 -1
  240. package/dist/web/hooks/use-send-follow-up-message.js +19 -2
  241. package/dist/web/hooks/use-send-follow-up-message.js.map +1 -1
  242. package/dist/web/hooks/use-set-open-in-app-url.d.ts +17 -0
  243. package/dist/web/hooks/use-set-open-in-app-url.js +17 -0
  244. package/dist/web/hooks/use-set-open-in-app-url.js.map +1 -1
  245. package/dist/web/hooks/use-set-open-in-app-url.test.js +5 -11
  246. package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -1
  247. package/dist/web/hooks/use-tool-info.d.ts +33 -0
  248. package/dist/web/hooks/use-tool-info.js +26 -0
  249. package/dist/web/hooks/use-tool-info.js.map +1 -1
  250. package/dist/web/hooks/use-tool-info.test-d.js.map +1 -1
  251. package/dist/web/hooks/use-tool-info.test.js +1 -1
  252. package/dist/web/hooks/use-tool-info.test.js.map +1 -1
  253. package/dist/web/hooks/use-user.d.ts +2 -0
  254. package/dist/web/hooks/use-user.js +20 -2
  255. package/dist/web/hooks/use-user.js.map +1 -1
  256. package/dist/web/hooks/use-user.test.js +29 -1
  257. package/dist/web/hooks/use-user.test.js.map +1 -1
  258. package/dist/web/hooks/use-view-state.d.ts +25 -0
  259. package/dist/web/hooks/use-view-state.js +32 -0
  260. package/dist/web/hooks/use-view-state.js.map +1 -0
  261. package/dist/web/hooks/use-view-state.test.d.ts +1 -0
  262. package/dist/web/hooks/use-view-state.test.js +177 -0
  263. package/dist/web/hooks/use-view-state.test.js.map +1 -0
  264. package/dist/web/index.d.ts +1 -2
  265. package/dist/web/index.js +1 -2
  266. package/dist/web/index.js.map +1 -1
  267. package/dist/web/mount-view.d.ts +20 -0
  268. package/dist/web/{mount-widget.js → mount-view.js} +21 -2
  269. package/dist/web/mount-view.js.map +1 -0
  270. package/dist/web/plugin/data-llm.test.js.map +1 -1
  271. package/dist/web/plugin/plugin.d.ts +32 -1
  272. package/dist/web/plugin/plugin.js +161 -18
  273. package/dist/web/plugin/plugin.js.map +1 -1
  274. package/dist/web/plugin/scan-views.d.ts +16 -0
  275. package/dist/web/plugin/scan-views.js +88 -0
  276. package/dist/web/plugin/scan-views.js.map +1 -0
  277. package/dist/web/plugin/scan-views.test.d.ts +1 -0
  278. package/dist/web/plugin/scan-views.test.js +99 -0
  279. package/dist/web/plugin/scan-views.test.js.map +1 -0
  280. package/dist/web/plugin/transform-data-llm.js +1 -1
  281. package/dist/web/plugin/transform-data-llm.js.map +1 -1
  282. package/dist/web/plugin/transform-data-llm.test.js.map +1 -1
  283. package/dist/web/plugin/validate-view.d.ts +1 -0
  284. package/dist/web/plugin/validate-view.js +9 -0
  285. package/dist/web/plugin/validate-view.js.map +1 -0
  286. package/dist/web/plugin/validate-view.test.d.ts +1 -0
  287. package/dist/web/plugin/validate-view.test.js +24 -0
  288. package/dist/web/plugin/validate-view.test.js.map +1 -0
  289. package/dist/web/proxy.js.map +1 -1
  290. package/dist/web/types.d.ts +4 -0
  291. package/dist/web/types.js.map +1 -1
  292. package/package.json +42 -25
  293. package/tsconfig.base.json +33 -0
  294. package/dist/server/templates/development.hbs +0 -67
  295. package/dist/server/templates/production.hbs +0 -6
  296. package/dist/server/widgetsDevServer.d.ts +0 -12
  297. package/dist/server/widgetsDevServer.js +0 -57
  298. package/dist/server/widgetsDevServer.js.map +0 -1
  299. package/dist/test/widget.test.js +0 -261
  300. package/dist/test/widget.test.js.map +0 -1
  301. package/dist/web/hooks/use-widget-state.d.ts +0 -4
  302. package/dist/web/hooks/use-widget-state.js +0 -32
  303. package/dist/web/hooks/use-widget-state.js.map +0 -1
  304. package/dist/web/hooks/use-widget-state.test.js +0 -62
  305. package/dist/web/hooks/use-widget-state.test.js.map +0 -1
  306. package/dist/web/mount-widget.d.ts +0 -1
  307. package/dist/web/mount-widget.js.map +0 -1
  308. /package/dist/{test/widget.test.d.ts → cli/tunnel-control-server.test.d.ts} +0 -0
  309. /package/dist/{web/hooks/use-widget-state.test.d.ts → cli/tunnel-handler.test.d.ts} +0 -0
@@ -0,0 +1,568 @@
1
+ import crypto from "node:crypto";
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
4
+ import { afterEach, beforeEach, describe, expect, it, vi, } from "vitest";
5
+ import { McpServer as McpServerClass } from "../server/server.js";
6
+ import { createMockExtra, createMockMcpServer, resetTestEnv, setTestEnv, } from "./utils.js";
7
+ const mockManifest = {
8
+ "skybridge:view:my-view": {
9
+ file: "assets/my-view-abc123.js",
10
+ name: "my-view",
11
+ isEntry: true,
12
+ },
13
+ "skybridge:view:folder-view": {
14
+ file: "assets/folder-view-def456.js",
15
+ name: "folder-view",
16
+ isEntry: true,
17
+ },
18
+ "style.css": { file: "style.css" },
19
+ };
20
+ // Mirrors `McpServer.computeViewVersionParam`. Tests recompute the expected
21
+ // hash from the mocked manifest so they don't hardcode digest output.
22
+ function expectedVersionParam(viewFile, styleFile) {
23
+ const hash = crypto
24
+ .createHash("sha256")
25
+ .update(viewFile)
26
+ .update("\0")
27
+ .update(styleFile)
28
+ .digest("hex")
29
+ .slice(0, 8);
30
+ return `?v=${hash}`;
31
+ }
32
+ const actual = vi.hoisted(() => require("node:fs"));
33
+ vi.mock("node:fs", () => {
34
+ const readFileSyncImpl = (path, ...args) => {
35
+ if (typeof path === "string" && path.includes("manifest.json")) {
36
+ return JSON.stringify(mockManifest);
37
+ }
38
+ return actual.readFileSync(path, ...args);
39
+ };
40
+ const readFileSync = vi.fn(readFileSyncImpl);
41
+ return {
42
+ readFileSync,
43
+ default: {
44
+ readFileSync,
45
+ },
46
+ };
47
+ });
48
+ describe("McpServer.registerTool (unified API)", () => {
49
+ let server;
50
+ let mockRegisterResource;
51
+ let mockRegisterTool;
52
+ beforeEach(() => {
53
+ ({ server, mockRegisterResource, mockRegisterTool } =
54
+ createMockMcpServer());
55
+ });
56
+ afterEach(() => {
57
+ vi.clearAllMocks();
58
+ resetTestEnv();
59
+ });
60
+ it("should generate correct HTML for development mode", async () => {
61
+ setTestEnv({ NODE_ENV: "development" });
62
+ server.registerTool({
63
+ name: "my-view",
64
+ description: "Test tool",
65
+ view: {
66
+ component: "my-view",
67
+ description: "Test view",
68
+ },
69
+ }, vi.fn());
70
+ const appsSdkResourceCallback = mockRegisterResource.mock
71
+ .calls[0]?.[3];
72
+ expect(appsSdkResourceCallback).toBeDefined();
73
+ const host = "localhost:3000";
74
+ const serverUrl = `http://${host}`;
75
+ const hmrUrl = `ws://${host}`;
76
+ const mockExtra = createMockExtra(host);
77
+ const result = await appsSdkResourceCallback(new URL("ui://views/apps-sdk/my-view.html"), mockExtra);
78
+ expect(mockRegisterTool).toHaveBeenCalled();
79
+ expect(result).toEqual({
80
+ contents: [
81
+ {
82
+ uri: "ui://views/apps-sdk/my-view.html",
83
+ mimeType: "text/html+skybridge",
84
+ text: expect.stringContaining('<div id="root"></div>'),
85
+ _meta: {
86
+ "openai/widgetCSP": {
87
+ resource_domains: [serverUrl],
88
+ connect_domains: [serverUrl, hmrUrl],
89
+ },
90
+ "openai/widgetDomain": serverUrl,
91
+ "openai/widgetDescription": "Test view",
92
+ },
93
+ },
94
+ ],
95
+ });
96
+ expect(result.contents[0]?.text).toContain(`${serverUrl}/assets/@react-refresh`);
97
+ expect(result.contents[0]?.text).toContain(`${serverUrl}/@vite/client`);
98
+ expect(result.contents[0]?.text).toContain(`${serverUrl}/_skybridge/view/my-view`);
99
+ });
100
+ it("should generate correct HTML for production mode", async () => {
101
+ setTestEnv({ NODE_ENV: "production" });
102
+ server.registerTool({
103
+ name: "my-view",
104
+ description: "Test tool",
105
+ view: {
106
+ component: "my-view",
107
+ description: "Test view",
108
+ },
109
+ }, vi.fn());
110
+ const appsSdkResourceCallback = mockRegisterResource.mock
111
+ .calls[0]?.[3];
112
+ expect(appsSdkResourceCallback).toBeDefined();
113
+ const host = "myapp.com";
114
+ const serverUrl = `https://${host}`;
115
+ const mockExtra = createMockExtra(host);
116
+ const versionedUri = `ui://views/apps-sdk/my-view.html${expectedVersionParam("assets/my-view-abc123.js", "style.css")}`;
117
+ const result = await appsSdkResourceCallback(new URL(versionedUri), mockExtra);
118
+ expect(result).toEqual({
119
+ contents: [
120
+ {
121
+ uri: versionedUri,
122
+ mimeType: "text/html+skybridge",
123
+ text: expect.stringContaining('<div id="root"></div>'),
124
+ _meta: {
125
+ "openai/widgetCSP": {
126
+ resource_domains: [serverUrl],
127
+ connect_domains: [serverUrl],
128
+ },
129
+ "openai/widgetDomain": serverUrl,
130
+ "openai/widgetDescription": "Test view",
131
+ },
132
+ },
133
+ ],
134
+ });
135
+ expect(result.contents[0]?.text).not.toContain(`${serverUrl}/assets/@react-refresh`);
136
+ expect(result.contents[0]?.text).not.toContain(`${serverUrl}@vite/client`);
137
+ expect(result.contents[0]?.text).toContain(`${serverUrl}/assets/assets/my-view-abc123.js`);
138
+ expect(result.contents[0]?.text).toContain(`${serverUrl}/assets/style.css`);
139
+ });
140
+ it("should prefer x-alpic-forwarded-url when hashing Claude view domains", async () => {
141
+ setTestEnv({ NODE_ENV: "production" });
142
+ server.registerTool({
143
+ name: "my-view",
144
+ description: "Test tool",
145
+ view: { component: "my-view", description: "Test view" },
146
+ }, vi.fn());
147
+ const extAppsResourceCallback = mockRegisterResource.mock
148
+ .calls[1]?.[3];
149
+ expect(extAppsResourceCallback).toBeDefined();
150
+ const forwardedUrl = "https://everything-3a2c1264.staging.alpic.live/mcp?foo=bar";
151
+ const expectedDomain = `${crypto
152
+ .createHash("sha256")
153
+ .update(forwardedUrl)
154
+ .digest("hex")
155
+ .slice(0, 32)}.claudemcpcontent.com`;
156
+ const result = await extAppsResourceCallback(new URL(`ui://views/ext-apps/my-view.html${expectedVersionParam("assets/my-view-abc123.js", "style.css")}`), createMockExtra("localhost:3000", {
157
+ headers: {
158
+ "user-agent": "Claude-User",
159
+ "x-alpic-forwarded-url": forwardedUrl,
160
+ },
161
+ url: "http://localhost:3000/mcp",
162
+ }));
163
+ expect(result.contents[0]?._meta).toEqual({
164
+ ui: {
165
+ csp: {
166
+ resourceDomains: ["http://localhost:3000"],
167
+ connectDomains: ["http://localhost:3000"],
168
+ baseUriDomains: ["http://localhost:3000"],
169
+ },
170
+ description: "Test view",
171
+ domain: expectedDomain,
172
+ },
173
+ });
174
+ });
175
+ it("should register resources with correct hostType for both apps-sdk and ext-apps", async () => {
176
+ server.registerTool({
177
+ name: "my-view",
178
+ description: "Test tool",
179
+ view: {
180
+ component: "my-view",
181
+ description: "Test view",
182
+ prefersBorder: true,
183
+ },
184
+ }, vi.fn());
185
+ expect(mockRegisterResource).toHaveBeenCalledTimes(2);
186
+ const appsSdkCallback = mockRegisterResource.mock
187
+ .calls[0]?.[3];
188
+ const host = "localhost:3000";
189
+ const serverUrl = `http://${host}`;
190
+ 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({
216
+ contents: [
217
+ {
218
+ uri: "ui://views/ext-apps/my-view.html",
219
+ mimeType: "text/html;profile=mcp-app",
220
+ text: expect.stringContaining('<div id="root"></div>'),
221
+ _meta: {
222
+ ui: {
223
+ csp: {
224
+ resourceDomains: [serverUrl],
225
+ connectDomains: [serverUrl, hmrUrl],
226
+ baseUriDomains: [serverUrl],
227
+ },
228
+ domain: serverUrl,
229
+ description: "Test view",
230
+ prefersBorder: true,
231
+ },
232
+ },
233
+ },
234
+ ],
235
+ });
236
+ expect(extAppsResult.contents[0]?.text).toContain('window.skybridge = { hostType: "mcp-app", serverUrl: "http://localhost:3000" };');
237
+ });
238
+ it("should register tool with ui.resourceUri metadata", async () => {
239
+ server.registerTool({
240
+ name: "my-view",
241
+ description: "Test tool",
242
+ view: { component: "my-view", description: "Test view" },
243
+ }, 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
+ });
251
+ });
252
+ it("should register tool with openai/outputTemplate when apps-sdk only", async () => {
253
+ server.registerTool({
254
+ name: "my-view",
255
+ description: "Test tool",
256
+ view: {
257
+ component: "my-view",
258
+ description: "Test view",
259
+ hosts: ["apps-sdk"],
260
+ },
261
+ }, 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");
267
+ });
268
+ it("should not version view URIs in development", () => {
269
+ server.registerTool({
270
+ name: "my-view",
271
+ description: "Test tool",
272
+ view: { component: "my-view", description: "Test view" },
273
+ }, vi.fn());
274
+ const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
275
+ expect(toolConfig._meta?.["openai/outputTemplate"]).toBe("ui://views/apps-sdk/my-view.html");
276
+ 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");
281
+ });
282
+ it("should append a stable content hash to view URIs in production", () => {
283
+ setTestEnv({ NODE_ENV: "production" });
284
+ server.registerTool({
285
+ name: "my-view",
286
+ description: "Test tool",
287
+ view: { component: "my-view", description: "Test view" },
288
+ }, vi.fn());
289
+ const expected = expectedVersionParam("assets/my-view-abc123.js", "style.css");
290
+ const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
291
+ expect(toolConfig._meta?.["openai/outputTemplate"]).toBe(`ui://views/apps-sdk/my-view.html${expected}`);
292
+ 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}`);
295
+ });
296
+ it("should produce different version params for views with different bundles", () => {
297
+ setTestEnv({ NODE_ENV: "production" });
298
+ server.registerTool({
299
+ name: "my-view",
300
+ description: "First tool",
301
+ view: { component: "my-view" },
302
+ }, vi.fn());
303
+ server.registerTool({
304
+ name: "folder-view",
305
+ description: "Second tool",
306
+ view: { component: "folder-view" },
307
+ }, vi.fn());
308
+ const myviewTemplate = (mockRegisterTool.mock.calls[0]?.[1])._meta?.["openai/outputTemplate"];
309
+ const folderviewTemplate = (mockRegisterTool.mock.calls[1]?.[1])._meta?.["openai/outputTemplate"];
310
+ expect(myviewTemplate).not.toEqual(folderviewTemplate);
311
+ expect(myviewTemplate).toMatch(/\?v=[0-9a-f]{8}$/);
312
+ expect(folderviewTemplate).toMatch(/\?v=[0-9a-f]{8}$/);
313
+ });
314
+ it("should fall back to bare URI in production when manifest is missing", () => {
315
+ setTestEnv({ NODE_ENV: "production" });
316
+ server.registerTool({
317
+ name: "unknown-view",
318
+ description: "Test tool",
319
+ view: { component: "unknown-view" },
320
+ }, vi.fn());
321
+ 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();
342
+ });
343
+ it("should inject viewUUID into _meta of tool callback results", async () => {
344
+ const mockToolCallback = vi.fn().mockResolvedValue({
345
+ content: [{ type: "text", text: "result" }],
346
+ structuredContent: { data: "test" },
347
+ });
348
+ server.registerTool({
349
+ name: "my-view",
350
+ description: "Test tool",
351
+ view: { component: "my-view", description: "Test view" },
352
+ }, mockToolCallback);
353
+ const wrappedCallback = mockRegisterTool.mock.calls[0]?.[2];
354
+ expect(wrappedCallback).toBeDefined();
355
+ const result = await wrappedCallback({}, {});
356
+ expect(result._meta).toBeDefined();
357
+ expect(result._meta?.viewUUID).toBeDefined();
358
+ expect(typeof result._meta?.viewUUID).toBe("string");
359
+ 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
+ });
361
+ it("should preserve existing _meta when injecting viewUUID", async () => {
362
+ const mockToolCallback = vi.fn().mockResolvedValue({
363
+ content: [{ type: "text", text: "result" }],
364
+ structuredContent: { data: "test" },
365
+ _meta: { requestId: "req-123", cached: true },
366
+ });
367
+ server.registerTool({
368
+ name: "my-view",
369
+ description: "Test tool",
370
+ view: { component: "my-view", description: "Test view" },
371
+ }, mockToolCallback);
372
+ const wrappedCallback = mockRegisterTool.mock.calls[0]?.[2];
373
+ const result = await wrappedCallback({}, {});
374
+ expect(result._meta?.requestId).toBe("req-123");
375
+ expect(result._meta?.cached).toBe(true);
376
+ expect(result._meta?.viewUUID).toBeDefined();
377
+ });
378
+ it("should generate unique viewUUIDs across calls", async () => {
379
+ const mockToolCallback = vi.fn().mockResolvedValue({
380
+ content: [{ type: "text", text: "result" }],
381
+ structuredContent: {},
382
+ });
383
+ server.registerTool({
384
+ name: "my-view",
385
+ description: "Test tool",
386
+ view: { component: "my-view", description: "Test view" },
387
+ }, mockToolCallback);
388
+ const wrappedCallback = mockRegisterTool.mock.calls[0]?.[2];
389
+ const result1 = await wrappedCallback({}, {});
390
+ const result2 = await wrappedCallback({}, {});
391
+ expect(result1._meta?.viewUUID).not.toBe(result2._meta?.viewUUID);
392
+ });
393
+ it("should enforce one-tool-per-view constraint", () => {
394
+ server.registerTool({
395
+ name: "shake",
396
+ description: "First tool",
397
+ view: { component: "magic-8-ball" },
398
+ }, vi.fn());
399
+ expect(() => {
400
+ server.registerTool({
401
+ name: "shake-v2",
402
+ description: "Second tool",
403
+ view: { component: "magic-8-ball" },
404
+ }, vi.fn());
405
+ }).toThrow('skybridge: view "magic-8-ball" is already used by tool "shake"');
406
+ });
407
+ it("should normalize string content to ContentBlock array", async () => {
408
+ const mockToolCallback = vi.fn().mockResolvedValue({
409
+ content: "Hello world",
410
+ structuredContent: {},
411
+ });
412
+ server.registerTool({
413
+ name: "string-content",
414
+ description: "Test tool",
415
+ view: { component: "string-content" },
416
+ }, mockToolCallback);
417
+ const wrappedCallback = mockRegisterTool.mock.calls[0]?.[2];
418
+ const result = await wrappedCallback({}, {});
419
+ expect(result.content).toEqual([{ type: "text", text: "Hello world" }]);
420
+ });
421
+ it("should normalize single ContentBlock to array", async () => {
422
+ const mockToolCallback = vi.fn().mockResolvedValue({
423
+ content: { type: "text", text: "Single block" },
424
+ structuredContent: {},
425
+ });
426
+ server.registerTool({
427
+ name: "single-block",
428
+ description: "Test tool",
429
+ view: { component: "single-block" },
430
+ }, mockToolCallback);
431
+ const wrappedCallback = mockRegisterTool.mock.calls[0]?.[2];
432
+ const result = await wrappedCallback({}, {});
433
+ expect(result.content).toEqual([{ type: "text", text: "Single block" }]);
434
+ });
435
+ it("should pass through ContentBlock array unchanged", async () => {
436
+ const blocks = [
437
+ { type: "text", text: "A" },
438
+ { type: "text", text: "B" },
439
+ ];
440
+ const mockToolCallback = vi.fn().mockResolvedValue({
441
+ content: blocks,
442
+ structuredContent: {},
443
+ });
444
+ server.registerTool({
445
+ name: "array-content",
446
+ description: "Test tool",
447
+ view: { component: "array-content" },
448
+ }, mockToolCallback);
449
+ const wrappedCallback = mockRegisterTool.mock.calls[0]?.[2];
450
+ const result = await wrappedCallback({}, {});
451
+ expect(result.content).toEqual(blocks);
452
+ });
453
+ it("should register tool without view (no resource registration)", () => {
454
+ server.registerTool({
455
+ name: "plain-tool",
456
+ description: "No view",
457
+ }, vi.fn());
458
+ expect(mockRegisterResource).not.toHaveBeenCalled();
459
+ expect(mockRegisterTool).toHaveBeenCalledTimes(1);
460
+ });
461
+ it("should apply view.csp fields to resource _meta", async () => {
462
+ server.registerTool({
463
+ name: "csp-tool",
464
+ description: "Test tool",
465
+ view: {
466
+ component: "csp-tool",
467
+ description: "Test view",
468
+ csp: {
469
+ connectDomains: ["https://api.example.com"],
470
+ resourceDomains: ["https://cdn.example.com"],
471
+ },
472
+ },
473
+ }, vi.fn());
474
+ const appsSdkCallback = mockRegisterResource.mock
475
+ .calls[0]?.[3];
476
+ const host = "localhost:3000";
477
+ const serverUrl = `http://${host}`;
478
+ const hmrUrl = `ws://${host}`;
479
+ const result = await appsSdkCallback(new URL("ui://views/apps-sdk/csp-tool.html"), createMockExtra(host));
480
+ 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"],
484
+ });
485
+ });
486
+ it("should let view._meta override framework-computed keys", async () => {
487
+ server.registerTool({
488
+ name: "override-tool",
489
+ description: "Test tool",
490
+ view: {
491
+ component: "override-tool",
492
+ description: "Test view",
493
+ csp: { connectDomains: ["https://api.x.com"] },
494
+ _meta: {
495
+ "openai/widgetCSP": {
496
+ connect_domains: ["https://api.y.com"],
497
+ },
498
+ },
499
+ },
500
+ }, vi.fn());
501
+ const appsSdkCallback = mockRegisterResource.mock
502
+ .calls[0]?.[3];
503
+ const result = await appsSdkCallback(new URL("ui://views/apps-sdk/override-tool.html"), createMockExtra("localhost:3000"));
504
+ const meta = result.contents[0]?._meta;
505
+ expect(meta["openai/widgetCSP"]).toEqual({
506
+ connect_domains: ["https://api.y.com"],
507
+ });
508
+ });
509
+ it("should pass user _meta keys through to tool config", () => {
510
+ server.registerTool({
511
+ name: "meta-tool",
512
+ description: "Test tool",
513
+ _meta: {
514
+ "openai/widgetAccessible": true,
515
+ "openai/toolInvocation/invoking": "Loading...",
516
+ "acme.com/category": "utility",
517
+ },
518
+ }, vi.fn());
519
+ const toolCallArgs = mockRegisterTool.mock.calls[0];
520
+ const toolConfig = toolCallArgs?.[1];
521
+ expect(toolConfig._meta?.["openai/widgetAccessible"]).toBe(true);
522
+ expect(toolConfig._meta?.["openai/toolInvocation/invoking"]).toBe("Loading...");
523
+ expect(toolConfig._meta?.["acme.com/category"]).toBe("utility");
524
+ });
525
+ });
526
+ describe("resources/list view _meta injection", () => {
527
+ afterEach(() => resetTestEnv());
528
+ it("attaches CSP, domain, and connectDomains _meta to view resources at list time", async () => {
529
+ setTestEnv({ NODE_ENV: "production" });
530
+ const server = new McpServerClass({ name: "test", version: "1.0.0" }, { capabilities: {} });
531
+ server.registerTool({
532
+ name: "start",
533
+ description: "Start",
534
+ view: {
535
+ component: "my-view",
536
+ description: "Onboarding deck",
537
+ domain: "skybridge.tech",
538
+ csp: {
539
+ resourceDomains: ["https://fonts.googleapis.com"],
540
+ redirectDomains: ["https://docs.skybridge.tech"],
541
+ },
542
+ },
543
+ }, vi.fn().mockResolvedValue({
544
+ content: [{ type: "text", text: "ok" }],
545
+ structuredContent: {},
546
+ }));
547
+ const client = new Client({ name: "test-client", version: "1.0.0" });
548
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
549
+ await server.connect(serverTransport);
550
+ await client.connect(clientTransport);
551
+ const { resources } = await client.listResources();
552
+ await client.close();
553
+ await server.close();
554
+ const appsSdk = resources.find((r) => r.uri.includes("apps-sdk"));
555
+ const extApps = resources.find((r) => r.uri.includes("ext-apps"));
556
+ expect(appsSdk?._meta).toBeDefined();
557
+ 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;
563
+ expect(extUi?.csp?.connectDomains?.length).toBeGreaterThan(0);
564
+ expect(extUi?.csp?.resourceDomains).toContain("https://fonts.googleapis.com");
565
+ expect(extUi?.domain).toBe("skybridge.tech");
566
+ });
567
+ });
568
+ //# sourceMappingURL=view.test.js.map