skybridge 0.0.0-dev.ff4b4a2 → 0.0.0-dev.ffe77e9

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 (263) hide show
  1. package/README.md +24 -15
  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 +30 -18
  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 +41 -6
  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/dev.d.ts +4 -1
  47. package/dist/commands/dev.js +57 -8
  48. package/dist/commands/dev.js.map +1 -1
  49. package/dist/commands/start.d.ts +3 -1
  50. package/dist/commands/start.js +31 -15
  51. package/dist/commands/start.js.map +1 -1
  52. package/dist/commands/telemetry/disable.js.map +1 -1
  53. package/dist/commands/telemetry/enable.js.map +1 -1
  54. package/dist/commands/telemetry/status.js.map +1 -1
  55. package/dist/server/asset-base-url-transform-plugin.d.ts +5 -6
  56. package/dist/server/asset-base-url-transform-plugin.js +9 -10
  57. package/dist/server/asset-base-url-transform-plugin.js.map +1 -1
  58. package/dist/server/asset-base-url-transform-plugin.test.js +41 -13
  59. package/dist/server/asset-base-url-transform-plugin.test.js.map +1 -1
  60. package/dist/server/content-helpers.d.ts +27 -0
  61. package/dist/server/content-helpers.js +46 -0
  62. package/dist/server/content-helpers.js.map +1 -0
  63. package/dist/server/content-helpers.test.d.ts +1 -0
  64. package/dist/server/content-helpers.test.js +70 -0
  65. package/dist/server/content-helpers.test.js.map +1 -0
  66. package/dist/server/express.d.ts +11 -0
  67. package/dist/server/express.js +101 -0
  68. package/dist/server/express.js.map +1 -0
  69. package/dist/server/express.test.d.ts +1 -0
  70. package/dist/server/express.test.js +430 -0
  71. package/dist/server/express.test.js.map +1 -0
  72. package/dist/server/index.d.ts +5 -3
  73. package/dist/server/index.js +3 -2
  74. package/dist/server/index.js.map +1 -1
  75. package/dist/server/inferUtilityTypes.d.ts +6 -6
  76. package/dist/server/inferUtilityTypes.js.map +1 -1
  77. package/dist/server/metric.d.ts +14 -0
  78. package/dist/server/metric.js +62 -0
  79. package/dist/server/metric.js.map +1 -0
  80. package/dist/server/middleware.d.ts +124 -0
  81. package/dist/server/middleware.js +93 -0
  82. package/dist/server/middleware.js.map +1 -0
  83. package/dist/server/middleware.test-d.d.ts +1 -0
  84. package/dist/server/middleware.test-d.js +75 -0
  85. package/dist/server/middleware.test-d.js.map +1 -0
  86. package/dist/server/middleware.test.d.ts +1 -0
  87. package/dist/server/middleware.test.js +493 -0
  88. package/dist/server/middleware.test.js.map +1 -0
  89. package/dist/server/server.d.ts +160 -63
  90. package/dist/server/server.js +391 -76
  91. package/dist/server/server.js.map +1 -1
  92. package/dist/server/templateHelper.d.ts +5 -8
  93. package/dist/server/templateHelper.js +3 -22
  94. package/dist/server/templateHelper.js.map +1 -1
  95. package/dist/server/templates.generated.d.ts +4 -0
  96. package/dist/server/templates.generated.js +47 -0
  97. package/dist/server/templates.generated.js.map +1 -0
  98. package/dist/server/tunnel-proxy-router.d.ts +7 -0
  99. package/dist/server/tunnel-proxy-router.js +110 -0
  100. package/dist/server/tunnel-proxy-router.js.map +1 -0
  101. package/dist/server/tunnel-proxy-router.test.d.ts +1 -0
  102. package/dist/server/tunnel-proxy-router.test.js +229 -0
  103. package/dist/server/tunnel-proxy-router.test.js.map +1 -0
  104. package/dist/server/viewsDevServer.d.ts +14 -0
  105. package/dist/server/viewsDevServer.js +45 -0
  106. package/dist/server/viewsDevServer.js.map +1 -0
  107. package/dist/test/utils.d.ts +13 -21
  108. package/dist/test/utils.js +42 -37
  109. package/dist/test/utils.js.map +1 -1
  110. package/dist/test/view.test.d.ts +1 -0
  111. package/dist/test/view.test.js +523 -0
  112. package/dist/test/view.test.js.map +1 -0
  113. package/dist/version.d.ts +1 -0
  114. package/dist/version.js +3 -0
  115. package/dist/version.js.map +1 -0
  116. package/dist/web/bridges/apps-sdk/adaptor.d.ts +9 -7
  117. package/dist/web/bridges/apps-sdk/adaptor.js +51 -19
  118. package/dist/web/bridges/apps-sdk/adaptor.js.map +1 -1
  119. package/dist/web/bridges/apps-sdk/bridge.d.ts +1 -1
  120. package/dist/web/bridges/apps-sdk/bridge.js.map +1 -1
  121. package/dist/web/bridges/apps-sdk/index.d.ts +1 -1
  122. package/dist/web/bridges/apps-sdk/index.js.map +1 -1
  123. package/dist/web/bridges/apps-sdk/types.d.ts +30 -14
  124. package/dist/web/bridges/apps-sdk/types.js.map +1 -1
  125. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js.map +1 -1
  126. package/dist/web/bridges/get-adaptor.js.map +1 -1
  127. package/dist/web/bridges/index.js.map +1 -1
  128. package/dist/web/bridges/mcp-app/adaptor.d.ts +21 -9
  129. package/dist/web/bridges/mcp-app/adaptor.js +142 -64
  130. package/dist/web/bridges/mcp-app/adaptor.js.map +1 -1
  131. package/dist/web/bridges/mcp-app/bridge.d.ts +13 -30
  132. package/dist/web/bridges/mcp-app/bridge.js +43 -196
  133. package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
  134. package/dist/web/bridges/mcp-app/index.js.map +1 -1
  135. package/dist/web/bridges/mcp-app/types.js.map +1 -1
  136. package/dist/web/bridges/mcp-app/use-mcp-app-context.d.ts +5 -3
  137. package/dist/web/bridges/mcp-app/use-mcp-app-context.js +2 -2
  138. package/dist/web/bridges/mcp-app/use-mcp-app-context.js.map +1 -1
  139. package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js +1 -41
  140. package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js.map +1 -1
  141. package/dist/web/bridges/types.d.ts +26 -13
  142. package/dist/web/bridges/types.js.map +1 -1
  143. package/dist/web/bridges/use-host-context.js.map +1 -1
  144. package/dist/web/components/modal-provider.js +3 -5
  145. package/dist/web/components/modal-provider.js.map +1 -1
  146. package/dist/web/create-store.js +17 -3
  147. package/dist/web/create-store.js.map +1 -1
  148. package/dist/web/create-store.test.js +23 -20
  149. package/dist/web/create-store.test.js.map +1 -1
  150. package/dist/web/data-llm.d.ts +1 -1
  151. package/dist/web/data-llm.js +3 -3
  152. package/dist/web/data-llm.js.map +1 -1
  153. package/dist/web/data-llm.test.js +33 -30
  154. package/dist/web/data-llm.test.js.map +1 -1
  155. package/dist/web/generate-helpers.d.ts +20 -18
  156. package/dist/web/generate-helpers.js +20 -18
  157. package/dist/web/generate-helpers.js.map +1 -1
  158. package/dist/web/generate-helpers.test-d.js +26 -26
  159. package/dist/web/generate-helpers.test-d.js.map +1 -1
  160. package/dist/web/generate-helpers.test.js.map +1 -1
  161. package/dist/web/helpers/state.d.ts +2 -2
  162. package/dist/web/helpers/state.js +11 -11
  163. package/dist/web/helpers/state.js.map +1 -1
  164. package/dist/web/helpers/state.test.js +9 -9
  165. package/dist/web/helpers/state.test.js.map +1 -1
  166. package/dist/web/hooks/index.d.ts +2 -2
  167. package/dist/web/hooks/index.js +1 -1
  168. package/dist/web/hooks/index.js.map +1 -1
  169. package/dist/web/hooks/test/utils.js +4 -0
  170. package/dist/web/hooks/test/utils.js.map +1 -1
  171. package/dist/web/hooks/use-call-tool.js.map +1 -1
  172. package/dist/web/hooks/use-call-tool.test-d.js.map +1 -1
  173. package/dist/web/hooks/use-call-tool.test.js +0 -4
  174. package/dist/web/hooks/use-call-tool.test.js.map +1 -1
  175. package/dist/web/hooks/use-display-mode.d.ts +3 -3
  176. package/dist/web/hooks/use-display-mode.js.map +1 -1
  177. package/dist/web/hooks/use-display-mode.test-d.d.ts +1 -0
  178. package/dist/web/hooks/use-display-mode.test-d.js +8 -0
  179. package/dist/web/hooks/use-display-mode.test-d.js.map +1 -0
  180. package/dist/web/hooks/use-display-mode.test.js.map +1 -1
  181. package/dist/web/hooks/use-files.d.ts +2 -1
  182. package/dist/web/hooks/use-files.js +1 -0
  183. package/dist/web/hooks/use-files.js.map +1 -1
  184. package/dist/web/hooks/use-files.test.js +27 -3
  185. package/dist/web/hooks/use-files.test.js.map +1 -1
  186. package/dist/web/hooks/use-layout.js.map +1 -1
  187. package/dist/web/hooks/use-layout.test.js +3 -3
  188. package/dist/web/hooks/use-layout.test.js.map +1 -1
  189. package/dist/web/hooks/use-open-external.d.ts +3 -1
  190. package/dist/web/hooks/use-open-external.js +1 -1
  191. package/dist/web/hooks/use-open-external.js.map +1 -1
  192. package/dist/web/hooks/use-open-external.test.js +26 -11
  193. package/dist/web/hooks/use-open-external.test.js.map +1 -1
  194. package/dist/web/hooks/use-request-modal.d.ts +1 -1
  195. package/dist/web/hooks/use-request-modal.js +4 -4
  196. package/dist/web/hooks/use-request-modal.js.map +1 -1
  197. package/dist/web/hooks/use-request-modal.test.js +5 -1
  198. package/dist/web/hooks/use-request-modal.test.js.map +1 -1
  199. package/dist/web/hooks/use-send-follow-up-message.d.ts +2 -1
  200. package/dist/web/hooks/use-send-follow-up-message.js +2 -2
  201. package/dist/web/hooks/use-send-follow-up-message.js.map +1 -1
  202. package/dist/web/hooks/use-set-open-in-app-url.js.map +1 -1
  203. package/dist/web/hooks/use-set-open-in-app-url.test.js +5 -11
  204. package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -1
  205. package/dist/web/hooks/use-tool-info.js.map +1 -1
  206. package/dist/web/hooks/use-tool-info.test-d.js.map +1 -1
  207. package/dist/web/hooks/use-tool-info.test.js +1 -1
  208. package/dist/web/hooks/use-tool-info.test.js.map +1 -1
  209. package/dist/web/hooks/use-user.js +18 -2
  210. package/dist/web/hooks/use-user.js.map +1 -1
  211. package/dist/web/hooks/use-user.test.js +29 -1
  212. package/dist/web/hooks/use-user.test.js.map +1 -1
  213. package/dist/web/hooks/use-view-state.d.ts +4 -0
  214. package/dist/web/hooks/use-view-state.js +32 -0
  215. package/dist/web/hooks/use-view-state.js.map +1 -0
  216. package/dist/web/hooks/use-view-state.test.d.ts +1 -0
  217. package/dist/web/hooks/use-view-state.test.js +177 -0
  218. package/dist/web/hooks/use-view-state.test.js.map +1 -0
  219. package/dist/web/index.d.ts +1 -2
  220. package/dist/web/index.js +1 -2
  221. package/dist/web/index.js.map +1 -1
  222. package/dist/web/mount-view.d.ts +1 -0
  223. package/dist/web/{mount-widget.js → mount-view.js} +2 -2
  224. package/dist/web/mount-view.js.map +1 -0
  225. package/dist/web/plugin/data-llm.test.js.map +1 -1
  226. package/dist/web/plugin/plugin.d.ts +4 -1
  227. package/dist/web/plugin/plugin.js +128 -18
  228. package/dist/web/plugin/plugin.js.map +1 -1
  229. package/dist/web/plugin/scan-views.d.ts +16 -0
  230. package/dist/web/plugin/scan-views.js +88 -0
  231. package/dist/web/plugin/scan-views.js.map +1 -0
  232. package/dist/web/plugin/scan-views.test.d.ts +1 -0
  233. package/dist/web/plugin/scan-views.test.js +99 -0
  234. package/dist/web/plugin/scan-views.test.js.map +1 -0
  235. package/dist/web/plugin/transform-data-llm.js +1 -1
  236. package/dist/web/plugin/transform-data-llm.js.map +1 -1
  237. package/dist/web/plugin/transform-data-llm.test.js.map +1 -1
  238. package/dist/web/plugin/validate-view.d.ts +1 -0
  239. package/dist/web/plugin/validate-view.js +9 -0
  240. package/dist/web/plugin/validate-view.js.map +1 -0
  241. package/dist/web/plugin/validate-view.test.d.ts +1 -0
  242. package/dist/web/plugin/validate-view.test.js +24 -0
  243. package/dist/web/plugin/validate-view.test.js.map +1 -0
  244. package/dist/web/proxy.js.map +1 -1
  245. package/dist/web/types.js.map +1 -1
  246. package/package.json +44 -30
  247. package/tsconfig.base.json +33 -0
  248. package/dist/server/templates/development.hbs +0 -67
  249. package/dist/server/templates/production.hbs +0 -6
  250. package/dist/server/widgetsDevServer.d.ts +0 -12
  251. package/dist/server/widgetsDevServer.js +0 -57
  252. package/dist/server/widgetsDevServer.js.map +0 -1
  253. package/dist/test/widget.test.js +0 -255
  254. package/dist/test/widget.test.js.map +0 -1
  255. package/dist/web/hooks/use-widget-state.d.ts +0 -4
  256. package/dist/web/hooks/use-widget-state.js +0 -32
  257. package/dist/web/hooks/use-widget-state.js.map +0 -1
  258. package/dist/web/hooks/use-widget-state.test.js +0 -61
  259. package/dist/web/hooks/use-widget-state.test.js.map +0 -1
  260. package/dist/web/mount-widget.d.ts +0 -1
  261. package/dist/web/mount-widget.js.map +0 -1
  262. /package/dist/{test/widget.test.d.ts → cli/tunnel-control-server.test.d.ts} +0 -0
  263. /package/dist/{web/hooks/use-widget-state.test.d.ts → cli/tunnel-handler.test.d.ts} +0 -0
@@ -0,0 +1,229 @@
1
+ import { EventEmitter } from "node:events";
2
+ import http from "node:http";
3
+ import { Readable } from "node:stream";
4
+ import express from "express";
5
+ import { afterEach, describe, expect, it, vi } from "vitest";
6
+ import { startTunnelControlServer } from "../cli/tunnel-control-server.js";
7
+ import { createTunnelProxyRouter } from "./tunnel-proxy-router.js";
8
+ function makeFakeChild() {
9
+ const child = new EventEmitter();
10
+ child.stdout = new Readable({ read() { } });
11
+ child.stderr = new Readable({ read() { } });
12
+ child.kill = vi.fn(() => true);
13
+ return child;
14
+ }
15
+ async function listen(handler) {
16
+ const server = http.createServer(handler);
17
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
18
+ const port = server.address().port;
19
+ return { port, server };
20
+ }
21
+ const cleanups = [];
22
+ afterEach(async () => {
23
+ while (cleanups.length > 0) {
24
+ const cleanup = cleanups.pop();
25
+ if (cleanup) {
26
+ await cleanup();
27
+ }
28
+ }
29
+ });
30
+ async function startProxy(controlPort) {
31
+ const app = express();
32
+ app.use(createTunnelProxyRouter(controlPort));
33
+ const { port, server } = await listen(app);
34
+ cleanups.push(() => new Promise((resolve) => {
35
+ server.closeAllConnections?.();
36
+ server.close(() => resolve());
37
+ }));
38
+ return { port, server };
39
+ }
40
+ async function startControl() {
41
+ const child = makeFakeChild();
42
+ const control = await startTunnelControlServer(() => 3000, {
43
+ spawn: () => child,
44
+ });
45
+ cleanups.push(() => control.close());
46
+ return { control, child };
47
+ }
48
+ describe("createTunnelProxyRouter", () => {
49
+ describe("POST /__skybridge/tunnel", () => {
50
+ it("forwards to upstream and returns the upstream JSON", async () => {
51
+ const { control } = await startControl();
52
+ const { port } = await startProxy(control.port);
53
+ const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {
54
+ method: "POST",
55
+ });
56
+ expect(res.status).toBe(200);
57
+ expect(res.headers.get("content-type")).toMatch(/application\/json/);
58
+ expect(await res.json()).toEqual({
59
+ status: "starting",
60
+ message: "Starting tunnel…",
61
+ });
62
+ expect(control.manager.getState().status).toBe("starting");
63
+ });
64
+ it("returns 502 when upstream is unavailable", async () => {
65
+ // Pick a port nothing is listening on by starting+stopping a server.
66
+ const probe = http.createServer();
67
+ await new Promise((resolve) => probe.listen(0, "127.0.0.1", resolve));
68
+ const deadPort = probe.address().port;
69
+ await new Promise((resolve) => probe.close(() => resolve()));
70
+ const { port } = await startProxy(deadPort);
71
+ const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {
72
+ method: "POST",
73
+ });
74
+ expect(res.status).toBe(502);
75
+ const body = (await res.json());
76
+ expect(body.status).toBe("error");
77
+ expect(typeof body.message).toBe("string");
78
+ });
79
+ });
80
+ describe("DELETE /__skybridge/tunnel", () => {
81
+ it("forwards to upstream and returns the upstream JSON", async () => {
82
+ const { control, child } = await startControl();
83
+ const { port } = await startProxy(control.port);
84
+ // First start the tunnel so DELETE has something to stop.
85
+ await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {
86
+ method: "POST",
87
+ });
88
+ const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {
89
+ method: "DELETE",
90
+ });
91
+ expect(res.status).toBe(200);
92
+ expect(await res.json()).toEqual({ status: "idle" });
93
+ expect(child.kill).toHaveBeenCalled();
94
+ });
95
+ it("returns 502 when upstream is unavailable", async () => {
96
+ const probe = http.createServer();
97
+ await new Promise((resolve) => probe.listen(0, "127.0.0.1", resolve));
98
+ const deadPort = probe.address().port;
99
+ await new Promise((resolve) => probe.close(() => resolve()));
100
+ const { port } = await startProxy(deadPort);
101
+ const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {
102
+ method: "DELETE",
103
+ });
104
+ expect(res.status).toBe(502);
105
+ });
106
+ });
107
+ describe("GET /__skybridge/tunnel/events", () => {
108
+ it("pipes the upstream SSE stream through to the client", async () => {
109
+ const { control, child } = await startControl();
110
+ const { port } = await startProxy(control.port);
111
+ // Get the manager into a known state so the initial SSE frame is
112
+ // deterministic.
113
+ await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {
114
+ method: "POST",
115
+ });
116
+ child.stdout.emit("data", Buffer.from("Forwarding: https://abc.tunnel.example -> http://localhost:3000\n"));
117
+ const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel/events`);
118
+ expect(res.status).toBe(200);
119
+ expect(res.headers.get("content-type")).toMatch(/text\/event-stream/);
120
+ expect(res.headers.get("cache-control")).toMatch(/no-cache/);
121
+ const body = res.body;
122
+ if (!body) {
123
+ throw new Error("expected response body");
124
+ }
125
+ const reader = body.getReader();
126
+ const { value } = await reader.read();
127
+ const chunk = new TextDecoder().decode(value);
128
+ expect(chunk).toContain("event: state");
129
+ expect(chunk).toContain('"status":"connected"');
130
+ expect(chunk).toContain('"url":"https://abc.tunnel.example"');
131
+ await reader.cancel();
132
+ });
133
+ it("forwards subsequent state changes through the SSE stream", async () => {
134
+ const { control, child } = await startControl();
135
+ const { port } = await startProxy(control.port);
136
+ await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {
137
+ method: "POST",
138
+ });
139
+ const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel/events`);
140
+ const body = res.body;
141
+ if (!body) {
142
+ throw new Error("expected response body");
143
+ }
144
+ const reader = body.getReader();
145
+ const decoder = new TextDecoder();
146
+ // Drain the initial "starting" frame.
147
+ const first = await reader.read();
148
+ expect(decoder.decode(first.value)).toContain('"status":"starting"');
149
+ // Now drive a state change on the manager and read the next frame.
150
+ child.stdout.emit("data", Buffer.from("Forwarding: https://abc.tunnel.example -> http://localhost:3000\n"));
151
+ let combined = "";
152
+ // Reads may chunk arbitrarily, so accumulate until we see the connected
153
+ // event or hit a sane cap.
154
+ for (let i = 0; i < 5; i++) {
155
+ const { value, done } = await reader.read();
156
+ if (done) {
157
+ break;
158
+ }
159
+ combined += decoder.decode(value);
160
+ if (combined.includes('"status":"connected"')) {
161
+ break;
162
+ }
163
+ }
164
+ expect(combined).toContain('"status":"connected"');
165
+ expect(combined).toContain('"url":"https://abc.tunnel.example"');
166
+ await reader.cancel();
167
+ });
168
+ it("returns 502 when upstream is unavailable", async () => {
169
+ const probe = http.createServer();
170
+ await new Promise((resolve) => probe.listen(0, "127.0.0.1", resolve));
171
+ const deadPort = probe.address().port;
172
+ await new Promise((resolve) => probe.close(() => resolve()));
173
+ const { port } = await startProxy(deadPort);
174
+ const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel/events`);
175
+ expect(res.status).toBe(502);
176
+ const body = (await res.json());
177
+ expect(body.status).toBe("error");
178
+ });
179
+ it("aborts the upstream connection when the proxy server is closed mid-stream", async () => {
180
+ const { control } = await startControl();
181
+ const { port, server } = await startProxy(control.port);
182
+ await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {
183
+ method: "POST",
184
+ });
185
+ // Snapshot the manager's listener counts before the SSE subscription so
186
+ // we can verify the proxy disconnected from upstream after shutdown.
187
+ const baseStateListeners = control.manager.listenerCount("state");
188
+ const baseActivityListeners = control.manager.listenerCount("activity");
189
+ const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel/events`);
190
+ const body = res.body;
191
+ if (!body) {
192
+ throw new Error("expected response body");
193
+ }
194
+ const reader = body.getReader();
195
+ // Drain the first frame to confirm the stream is live and the upstream
196
+ // SSE handler has subscribed to the manager.
197
+ await reader.read();
198
+ expect(control.manager.listenerCount("state")).toBe(baseStateListeners + 1);
199
+ // Close the proxy server, destroying in-flight responses. The proxy's
200
+ // req.on("close", ...) should fire and abort the upstream fetch.
201
+ server.closeAllConnections?.();
202
+ await new Promise((resolve) => server.close(() => resolve()));
203
+ // The client-side stream is dead — drain or fail, either is acceptable.
204
+ try {
205
+ while (true) {
206
+ const { done } = await reader.read();
207
+ if (done) {
208
+ break;
209
+ }
210
+ }
211
+ }
212
+ catch {
213
+ // expected: socket terminated when proxy server was destroyed
214
+ }
215
+ // Wait briefly for the upstream's req.on("close") to fire, then assert
216
+ // the manager listeners were detached. This is the load-bearing
217
+ // verification: it proves the proxy's AbortController propagated and
218
+ // upstream cleaned up its SSE subscription.
219
+ const start = Date.now();
220
+ while (control.manager.listenerCount("state") > baseStateListeners &&
221
+ Date.now() - start < 1000) {
222
+ await new Promise((r) => setTimeout(r, 20));
223
+ }
224
+ expect(control.manager.listenerCount("state")).toBe(baseStateListeners);
225
+ expect(control.manager.listenerCount("activity")).toBe(baseActivityListeners);
226
+ });
227
+ });
228
+ });
229
+ //# sourceMappingURL=tunnel-proxy-router.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tunnel-proxy-router.test.js","sourceRoot":"","sources":["../../src/server/tunnel-proxy-router.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventEmitter } from \"node:events\";\nimport http from \"node:http\";\nimport { Readable } from \"node:stream\";\nimport express from \"express\";\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { startTunnelControlServer } from \"../cli/tunnel-control-server.js\";\nimport { createTunnelProxyRouter } from \"./tunnel-proxy-router.js\";\n\ntype FakeChild = EventEmitter & {\n stdout: Readable;\n stderr: Readable;\n kill: ReturnType<typeof vi.fn<() => boolean>>;\n};\n\nfunction makeFakeChild(): FakeChild {\n const child = new EventEmitter() as FakeChild;\n child.stdout = new Readable({ read() {} });\n child.stderr = new Readable({ read() {} });\n child.kill = vi.fn<() => boolean>(() => true);\n return child;\n}\n\nasync function listen(handler: http.RequestListener) {\n const server = http.createServer(handler);\n await new Promise<void>((resolve) => server.listen(0, \"127.0.0.1\", resolve));\n const port = (server.address() as { port: number }).port;\n return { port, server };\n}\n\ntype Cleanup = () => Promise<void> | void;\nconst cleanups: Cleanup[] = [];\n\nafterEach(async () => {\n while (cleanups.length > 0) {\n const cleanup = cleanups.pop();\n if (cleanup) {\n await cleanup();\n }\n }\n});\n\nasync function startProxy(controlPort: number) {\n const app = express();\n app.use(createTunnelProxyRouter(controlPort));\n const { port, server } = await listen(app);\n cleanups.push(\n () =>\n new Promise<void>((resolve) => {\n server.closeAllConnections?.();\n server.close(() => resolve());\n }),\n );\n return { port, server };\n}\n\nasync function startControl() {\n const child = makeFakeChild();\n const control = await startTunnelControlServer(() => 3000, {\n spawn: () => child,\n });\n cleanups.push(() => control.close());\n return { control, child };\n}\n\ndescribe(\"createTunnelProxyRouter\", () => {\n describe(\"POST /__skybridge/tunnel\", () => {\n it(\"forwards to upstream and returns the upstream JSON\", async () => {\n const { control } = await startControl();\n const { port } = await startProxy(control.port);\n\n const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {\n method: \"POST\",\n });\n\n expect(res.status).toBe(200);\n expect(res.headers.get(\"content-type\")).toMatch(/application\\/json/);\n expect(await res.json()).toEqual({\n status: \"starting\",\n message: \"Starting tunnel…\",\n });\n expect(control.manager.getState().status).toBe(\"starting\");\n });\n\n it(\"returns 502 when upstream is unavailable\", async () => {\n // Pick a port nothing is listening on by starting+stopping a server.\n const probe = http.createServer();\n await new Promise<void>((resolve) =>\n probe.listen(0, \"127.0.0.1\", resolve),\n );\n const deadPort = (probe.address() as { port: number }).port;\n await new Promise<void>((resolve) => probe.close(() => resolve()));\n\n const { port } = await startProxy(deadPort);\n\n const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {\n method: \"POST\",\n });\n expect(res.status).toBe(502);\n const body = (await res.json()) as { status: string; message: string };\n expect(body.status).toBe(\"error\");\n expect(typeof body.message).toBe(\"string\");\n });\n });\n\n describe(\"DELETE /__skybridge/tunnel\", () => {\n it(\"forwards to upstream and returns the upstream JSON\", async () => {\n const { control, child } = await startControl();\n const { port } = await startProxy(control.port);\n\n // First start the tunnel so DELETE has something to stop.\n await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {\n method: \"POST\",\n });\n\n const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {\n method: \"DELETE\",\n });\n\n expect(res.status).toBe(200);\n expect(await res.json()).toEqual({ status: \"idle\" });\n expect(child.kill).toHaveBeenCalled();\n });\n\n it(\"returns 502 when upstream is unavailable\", async () => {\n const probe = http.createServer();\n await new Promise<void>((resolve) =>\n probe.listen(0, \"127.0.0.1\", resolve),\n );\n const deadPort = (probe.address() as { port: number }).port;\n await new Promise<void>((resolve) => probe.close(() => resolve()));\n\n const { port } = await startProxy(deadPort);\n\n const res = await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {\n method: \"DELETE\",\n });\n expect(res.status).toBe(502);\n });\n });\n\n describe(\"GET /__skybridge/tunnel/events\", () => {\n it(\"pipes the upstream SSE stream through to the client\", async () => {\n const { control, child } = await startControl();\n const { port } = await startProxy(control.port);\n\n // Get the manager into a known state so the initial SSE frame is\n // deterministic.\n await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {\n method: \"POST\",\n });\n child.stdout.emit(\n \"data\",\n Buffer.from(\n \"Forwarding: https://abc.tunnel.example -> http://localhost:3000\\n\",\n ),\n );\n\n const res = await fetch(\n `http://127.0.0.1:${port}/__skybridge/tunnel/events`,\n );\n\n expect(res.status).toBe(200);\n expect(res.headers.get(\"content-type\")).toMatch(/text\\/event-stream/);\n expect(res.headers.get(\"cache-control\")).toMatch(/no-cache/);\n\n const body = res.body;\n if (!body) {\n throw new Error(\"expected response body\");\n }\n const reader = body.getReader();\n const { value } = await reader.read();\n const chunk = new TextDecoder().decode(value);\n\n expect(chunk).toContain(\"event: state\");\n expect(chunk).toContain('\"status\":\"connected\"');\n expect(chunk).toContain('\"url\":\"https://abc.tunnel.example\"');\n\n await reader.cancel();\n });\n\n it(\"forwards subsequent state changes through the SSE stream\", async () => {\n const { control, child } = await startControl();\n const { port } = await startProxy(control.port);\n\n await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {\n method: \"POST\",\n });\n\n const res = await fetch(\n `http://127.0.0.1:${port}/__skybridge/tunnel/events`,\n );\n const body = res.body;\n if (!body) {\n throw new Error(\"expected response body\");\n }\n const reader = body.getReader();\n const decoder = new TextDecoder();\n\n // Drain the initial \"starting\" frame.\n const first = await reader.read();\n expect(decoder.decode(first.value)).toContain('\"status\":\"starting\"');\n\n // Now drive a state change on the manager and read the next frame.\n child.stdout.emit(\n \"data\",\n Buffer.from(\n \"Forwarding: https://abc.tunnel.example -> http://localhost:3000\\n\",\n ),\n );\n\n let combined = \"\";\n // Reads may chunk arbitrarily, so accumulate until we see the connected\n // event or hit a sane cap.\n for (let i = 0; i < 5; i++) {\n const { value, done } = await reader.read();\n if (done) {\n break;\n }\n combined += decoder.decode(value);\n if (combined.includes('\"status\":\"connected\"')) {\n break;\n }\n }\n expect(combined).toContain('\"status\":\"connected\"');\n expect(combined).toContain('\"url\":\"https://abc.tunnel.example\"');\n\n await reader.cancel();\n });\n\n it(\"returns 502 when upstream is unavailable\", async () => {\n const probe = http.createServer();\n await new Promise<void>((resolve) =>\n probe.listen(0, \"127.0.0.1\", resolve),\n );\n const deadPort = (probe.address() as { port: number }).port;\n await new Promise<void>((resolve) => probe.close(() => resolve()));\n\n const { port } = await startProxy(deadPort);\n\n const res = await fetch(\n `http://127.0.0.1:${port}/__skybridge/tunnel/events`,\n );\n expect(res.status).toBe(502);\n const body = (await res.json()) as { status: string };\n expect(body.status).toBe(\"error\");\n });\n\n it(\"aborts the upstream connection when the proxy server is closed mid-stream\", async () => {\n const { control } = await startControl();\n const { port, server } = await startProxy(control.port);\n\n await fetch(`http://127.0.0.1:${port}/__skybridge/tunnel`, {\n method: \"POST\",\n });\n\n // Snapshot the manager's listener counts before the SSE subscription so\n // we can verify the proxy disconnected from upstream after shutdown.\n const baseStateListeners = control.manager.listenerCount(\"state\");\n const baseActivityListeners = control.manager.listenerCount(\"activity\");\n\n const res = await fetch(\n `http://127.0.0.1:${port}/__skybridge/tunnel/events`,\n );\n const body = res.body;\n if (!body) {\n throw new Error(\"expected response body\");\n }\n const reader = body.getReader();\n // Drain the first frame to confirm the stream is live and the upstream\n // SSE handler has subscribed to the manager.\n await reader.read();\n expect(control.manager.listenerCount(\"state\")).toBe(\n baseStateListeners + 1,\n );\n\n // Close the proxy server, destroying in-flight responses. The proxy's\n // req.on(\"close\", ...) should fire and abort the upstream fetch.\n server.closeAllConnections?.();\n await new Promise<void>((resolve) => server.close(() => resolve()));\n\n // The client-side stream is dead — drain or fail, either is acceptable.\n try {\n while (true) {\n const { done } = await reader.read();\n if (done) {\n break;\n }\n }\n } catch {\n // expected: socket terminated when proxy server was destroyed\n }\n\n // Wait briefly for the upstream's req.on(\"close\") to fire, then assert\n // the manager listeners were detached. This is the load-bearing\n // verification: it proves the proxy's AbortController propagated and\n // upstream cleaned up its SSE subscription.\n const start = Date.now();\n while (\n control.manager.listenerCount(\"state\") > baseStateListeners &&\n Date.now() - start < 1000\n ) {\n await new Promise((r) => setTimeout(r, 20));\n }\n expect(control.manager.listenerCount(\"state\")).toBe(baseStateListeners);\n expect(control.manager.listenerCount(\"activity\")).toBe(\n baseActivityListeners,\n );\n });\n });\n});\n"]}
@@ -0,0 +1,14 @@
1
+ import type http from "node:http";
2
+ import { type Router } from "express";
3
+ /**
4
+ * Vite dev-server middleware for view assets.
5
+ *
6
+ * MUST be mounted at the Express app root so Vite can intercept
7
+ * `/@vite/client`, `/@react-refresh`, and `/_skybridge/view/...` imports:
8
+ *
9
+ * const app = express();
10
+ * if (env.NODE_ENV !== "production") {
11
+ * app.use(await viewsDevServer(httpServer));
12
+ * }
13
+ */
14
+ export declare const viewsDevServer: (httpServer: http.Server) => Promise<Router>;
@@ -0,0 +1,45 @@
1
+ import path from "node:path";
2
+ import cors from "cors";
3
+ import express, {} from "express";
4
+ import { assetBaseUrlTransformPlugin } from "./asset-base-url-transform-plugin.js";
5
+ /**
6
+ * Vite dev-server middleware for view assets.
7
+ *
8
+ * MUST be mounted at the Express app root so Vite can intercept
9
+ * `/@vite/client`, `/@react-refresh`, and `/_skybridge/view/...` imports:
10
+ *
11
+ * const app = express();
12
+ * if (env.NODE_ENV !== "production") {
13
+ * app.use(await viewsDevServer(httpServer));
14
+ * }
15
+ */
16
+ export const viewsDevServer = async (httpServer) => {
17
+ const router = express.Router();
18
+ const { createServer, loadConfigFromFile } = await import("vite");
19
+ const root = process.cwd();
20
+ const configFile = path.join(root, "vite.config.ts");
21
+ const configResult = await loadConfigFromFile({ command: "serve", mode: "development" }, configFile, root);
22
+ const { build, preview, plugins: userPlugins = [], ...devConfig } = configResult?.config || {};
23
+ const vite = await createServer({
24
+ ...devConfig,
25
+ // Pass `false` so Vite skips re-resolving a config file — we already
26
+ // loaded and spread the user's config above.
27
+ configFile: false,
28
+ appType: "custom",
29
+ server: {
30
+ allowedHosts: true,
31
+ middlewareMode: true,
32
+ hmr: {
33
+ server: httpServer,
34
+ },
35
+ },
36
+ root,
37
+ // optimizeDeps is set by the skybridge Vite plugin (entries + include)
38
+ // so it can derive the view glob from `viewsDir`.
39
+ plugins: [...userPlugins, assetBaseUrlTransformPlugin()],
40
+ });
41
+ router.use(cors());
42
+ router.use("/", vite.middlewares);
43
+ return router;
44
+ };
45
+ //# sourceMappingURL=viewsDevServer.js.map
@@ -0,0 +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,15 +1,12 @@
1
1
  import { type MockInstance } from "vitest";
2
2
  import { McpServer } from "../server/server.js";
3
- /**
4
- * Creates a real McpServer instance for testing
5
- */
6
3
  export declare function createMockMcpServer(): {
7
4
  server: McpServer;
8
5
  mockRegisterResource: MockInstance<McpServer["registerResource"]>;
9
- mockRegisterTool: MockInstance<McpServer["registerTool"]>;
6
+ mockRegisterTool: MockInstance;
10
7
  };
11
8
  export declare function createTestServer(): McpServer<Record<never, import("../server/server.js").ToolDef<unknown, unknown, unknown>> & {
12
- "search-voyage": import("../server/server.js").ToolDef<{
9
+ "search-trip": import("../server/server.js").ToolDef<{
13
10
  destination: string;
14
11
  departureDate?: string | undefined;
15
12
  maxPrice?: number | undefined;
@@ -30,9 +27,9 @@ export declare function createTestServer(): McpServer<Record<never, import("../s
30
27
  images: string[];
31
28
  }, unknown>;
32
29
  } & {
33
- "no-input-widget": import("../server/server.js").ToolDef<{}, {}, unknown>;
30
+ "no-input-view": import("../server/server.js").ToolDef<{}, {}, unknown>;
34
31
  } & {
35
- "inferred-output-widget": import("../server/server.js").ToolDef<{
32
+ "inferred-output-view": import("../server/server.js").ToolDef<{
36
33
  query: string;
37
34
  }, {
38
35
  inferredResults: {
@@ -60,7 +57,7 @@ export declare function createTestServer(): McpServer<Record<never, import("../s
60
57
  fetchedAt: string;
61
58
  }, unknown>;
62
59
  } & {
63
- "widget-with-metadata": import("../server/server.js").ToolDef<{
60
+ "view-with-metadata": import("../server/server.js").ToolDef<{
64
61
  resourceId: string;
65
62
  }, {
66
63
  data: {
@@ -82,7 +79,7 @@ export declare function createTestServer(): McpServer<Record<never, import("../s
82
79
  source: string;
83
80
  }>;
84
81
  } & {
85
- "widget-with-mixed-returns": import("../server/server.js").ToolDef<{
82
+ "view-with-mixed-returns": import("../server/server.js").ToolDef<{
86
83
  shouldSucceed: boolean;
87
84
  }, {
88
85
  error: string;
@@ -96,7 +93,7 @@ export declare function createTestServer(): McpServer<Record<never, import("../s
96
93
  }>;
97
94
  }>;
98
95
  export declare function createMinimalTestServer(): McpServer<Record<never, import("../server/server.js").ToolDef<unknown, unknown, unknown>> & {
99
- "search-voyage": import("../server/server.js").ToolDef<{
96
+ "search-trip": import("../server/server.js").ToolDef<{
100
97
  destination: string;
101
98
  }, {
102
99
  results: {
@@ -105,7 +102,7 @@ export declare function createMinimalTestServer(): McpServer<Record<never, impor
105
102
  }, unknown>;
106
103
  }>;
107
104
  export declare function createInterfaceTestServer(): McpServer<Record<never, import("../server/server.js").ToolDef<unknown, unknown, unknown>> & {
108
- "interface-widget": import("../server/server.js").ToolDef<{
105
+ "interface-view": import("../server/server.js").ToolDef<{
109
106
  id: string;
110
107
  }, {
111
108
  itemName: string;
@@ -115,21 +112,16 @@ export declare function createInterfaceTestServer(): McpServer<Record<never, imp
115
112
  version: number;
116
113
  }>;
117
114
  }>;
118
- /**
119
- * Mock extra parameter for resource callback
120
- */
121
- export declare function createMockExtra(host: string): {
115
+ export declare function createMockExtra(host: string, options?: {
116
+ headers?: Record<string, string | string[]>;
117
+ url?: URL | string;
118
+ }): {
122
119
  requestInfo: {
120
+ url?: string | URL | undefined;
123
121
  headers: {
124
122
  host: string;
125
123
  };
126
124
  };
127
125
  };
128
- /**
129
- * Sets up environment variables for testing
130
- */
131
126
  export declare function setTestEnv(env: Record<string, string>): void;
132
- /**
133
- * Resets environment variables
134
- */
135
127
  export declare function resetTestEnv(): void;
@@ -1,18 +1,14 @@
1
+ import { McpServer as McpServerBase } from "@modelcontextprotocol/sdk/server/mcp.js";
1
2
  import { vi } from "vitest";
2
3
  import * as z from "zod";
3
4
  import { McpServer } from "../server/server.js";
4
- /**
5
- * Creates a real McpServer instance for testing
6
- */
7
5
  export function createMockMcpServer() {
8
- // Create a real McpServer instance
9
6
  const server = new McpServer({
10
7
  name: "alpic-openai-app",
11
8
  version: "0.0.1",
12
9
  }, { capabilities: {} });
13
- // Mock the underlying methods to track calls
14
10
  const mockRegisterResource = vi.spyOn(server, "registerResource");
15
- const mockRegisterTool = vi.spyOn(server, "registerTool");
11
+ const mockRegisterTool = vi.spyOn(McpServerBase.prototype, "registerTool");
16
12
  return {
17
13
  server,
18
14
  mockRegisterResource,
@@ -21,8 +17,9 @@ export function createMockMcpServer() {
21
17
  }
22
18
  export function createTestServer() {
23
19
  return new McpServer({ name: "test-app", version: "1.0.0" }, {})
24
- .registerWidget("search-voyage", {}, {
25
- description: "Search for voyages",
20
+ .registerTool({
21
+ name: "search-trip",
22
+ description: "Search for trips",
26
23
  inputSchema: {
27
24
  destination: z.string(),
28
25
  departureDate: z.string().optional(),
@@ -36,6 +33,7 @@ export function createTestServer() {
36
33
  })),
37
34
  totalCount: z.number(),
38
35
  },
36
+ view: { component: "search-trip" },
39
37
  }, async ({ destination }) => {
40
38
  return {
41
39
  content: [{ type: "text", text: `Found trips to ${destination}` }],
@@ -45,7 +43,8 @@ export function createTestServer() {
45
43
  },
46
44
  };
47
45
  })
48
- .registerWidget("get-trip-details", {}, {
46
+ .registerTool({
47
+ name: "get-trip-details",
49
48
  description: "Get trip details",
50
49
  inputSchema: {
51
50
  tripId: z.string(),
@@ -55,6 +54,7 @@ export function createTestServer() {
55
54
  description: z.string(),
56
55
  images: z.array(z.string()),
57
56
  },
57
+ view: { component: "get-trip-details" },
58
58
  }, async ({ tripId }) => {
59
59
  return {
60
60
  content: [{ type: "text", text: `Details for ${tripId}` }],
@@ -65,21 +65,25 @@ export function createTestServer() {
65
65
  },
66
66
  };
67
67
  })
68
- .registerWidget("no-input-widget", {}, {
69
- description: "Widget with no input",
68
+ .registerTool({
69
+ name: "no-input-view",
70
+ description: "View with no input",
70
71
  inputSchema: {},
71
72
  outputSchema: {},
73
+ view: { component: "no-input-view" },
72
74
  }, async () => {
73
75
  return {
74
76
  content: [{ type: "text", text: "No input needed" }],
75
77
  structuredContent: {},
76
78
  };
77
79
  })
78
- .registerWidget("inferred-output-widget", {}, {
79
- description: "Widget with output inferred from callback",
80
+ .registerTool({
81
+ name: "inferred-output-view",
82
+ description: "View with output inferred from callback",
80
83
  inputSchema: {
81
84
  query: z.string(),
82
85
  },
86
+ view: { component: "inferred-output-view" },
83
87
  }, async ({ query }) => {
84
88
  return {
85
89
  content: [{ type: "text", text: `Query: ${query}` }],
@@ -89,7 +93,8 @@ export function createTestServer() {
89
93
  },
90
94
  };
91
95
  })
92
- .registerTool("calculate-price", {
96
+ .registerTool({
97
+ name: "calculate-price",
93
98
  description: "Calculate trip price",
94
99
  inputSchema: {
95
100
  tripId: z.string(),
@@ -108,7 +113,8 @@ export function createTestServer() {
108
113
  },
109
114
  };
110
115
  })
111
- .registerTool("inferred-tool", {
116
+ .registerTool({
117
+ name: "inferred-tool",
112
118
  description: "Tool with output inferred from callback",
113
119
  inputSchema: {
114
120
  itemId: z.string(),
@@ -122,11 +128,13 @@ export function createTestServer() {
122
128
  },
123
129
  };
124
130
  })
125
- .registerWidget("widget-with-metadata", {}, {
126
- description: "Widget that returns response metadata",
131
+ .registerTool({
132
+ name: "view-with-metadata",
133
+ description: "View that returns response metadata",
127
134
  inputSchema: {
128
135
  resourceId: z.string(),
129
136
  },
137
+ view: { component: "view-with-metadata" },
130
138
  }, async ({ resourceId }) => {
131
139
  return {
132
140
  content: [{ type: "text", text: `Resource: ${resourceId}` }],
@@ -140,7 +148,8 @@ export function createTestServer() {
140
148
  },
141
149
  };
142
150
  })
143
- .registerTool("tool-with-metadata", {
151
+ .registerTool({
152
+ name: "tool-with-metadata",
144
153
  description: "Tool that returns response metadata",
145
154
  inputSchema: {
146
155
  query: z.string(),
@@ -157,20 +166,20 @@ export function createTestServer() {
157
166
  },
158
167
  };
159
168
  })
160
- .registerWidget("widget-with-mixed-returns", {}, {
161
- description: "Widget with mixed return paths (some with _meta, some without)",
169
+ .registerTool({
170
+ name: "view-with-mixed-returns",
171
+ description: "View with mixed return paths (some with _meta, some without)",
162
172
  inputSchema: {
163
173
  shouldSucceed: z.boolean(),
164
174
  },
175
+ view: { component: "view-with-mixed-returns" },
165
176
  }, async ({ shouldSucceed }) => {
166
177
  if (!shouldSucceed) {
167
- // Error path - no _meta
168
178
  return {
169
179
  content: [{ type: "text", text: "Error occurred" }],
170
180
  structuredContent: { error: "Something went wrong" },
171
181
  };
172
182
  }
173
- // Success path - has _meta
174
183
  return {
175
184
  content: [{ type: "text", text: "Success" }],
176
185
  structuredContent: { data: "result" },
@@ -182,14 +191,16 @@ export function createTestServer() {
182
191
  });
183
192
  }
184
193
  export function createMinimalTestServer() {
185
- return new McpServer({ name: "test-app", version: "1.0.0" }, {}).registerWidget("search-voyage", {}, {
186
- description: "Search for voyages",
194
+ return new McpServer({ name: "test-app", version: "1.0.0" }, {}).registerTool({
195
+ name: "search-trip",
196
+ description: "Search for trips",
187
197
  inputSchema: {
188
198
  destination: z.string(),
189
199
  },
190
200
  outputSchema: {
191
201
  results: z.array(z.object({ id: z.string() })),
192
202
  },
203
+ view: { component: "search-trip" },
193
204
  }, async ({ destination }) => {
194
205
  return {
195
206
  content: [{ type: "text", text: `Found trips to ${destination}` }],
@@ -198,11 +209,13 @@ export function createMinimalTestServer() {
198
209
  });
199
210
  }
200
211
  export function createInterfaceTestServer() {
201
- return new McpServer({ name: "interface-test-app", version: "1.0.0" }, {}).registerWidget("interface-widget", {}, {
202
- description: "Widget with interface-typed output",
212
+ return new McpServer({ name: "interface-test-app", version: "1.0.0" }, {}).registerTool({
213
+ name: "interface-view",
214
+ description: "View with interface-typed output",
203
215
  inputSchema: {
204
216
  id: z.string(),
205
217
  },
218
+ view: { component: "interface-view" },
206
219
  }, async ({ id }) => {
207
220
  return {
208
221
  content: [{ type: "text", text: `Item ${id}` }],
@@ -217,25 +230,17 @@ export function createInterfaceTestServer() {
217
230
  };
218
231
  });
219
232
  }
220
- /**
221
- * Mock extra parameter for resource callback
222
- */
223
- export function createMockExtra(host) {
233
+ export function createMockExtra(host, options) {
224
234
  return {
225
235
  requestInfo: {
226
- headers: { host },
236
+ headers: { host, ...(options?.headers ?? {}) },
237
+ ...(options?.url ? { url: options.url } : {}),
227
238
  },
228
239
  };
229
240
  }
230
- /**
231
- * Sets up environment variables for testing
232
- */
233
241
  export function setTestEnv(env) {
234
242
  Object.assign(process.env, env);
235
243
  }
236
- /**
237
- * Resets environment variables
238
- */
239
244
  export function resetTestEnv() {
240
245
  delete process.env.NODE_ENV;
241
246
  }