skybridge 0.0.0-dev.f391982 → 0.0.0-dev.f3dd6f0
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.
- package/README.md +8 -8
- package/dist/cli/tunnel-control-server.d.ts +9 -0
- package/dist/cli/tunnel-control-server.js +31 -0
- package/dist/cli/tunnel-control-server.js.map +1 -0
- package/dist/cli/tunnel-control-server.test.js +39 -0
- package/dist/cli/tunnel-control-server.test.js.map +1 -0
- package/dist/cli/tunnel-handler.d.ts +3 -0
- package/dist/cli/tunnel-handler.js +48 -0
- package/dist/cli/tunnel-handler.js.map +1 -0
- package/dist/cli/tunnel-handler.test.d.ts +1 -0
- package/dist/cli/tunnel-handler.test.js +105 -0
- package/dist/cli/tunnel-handler.test.js.map +1 -0
- package/dist/cli/tunnel.d.ts +57 -0
- package/dist/cli/tunnel.js +154 -0
- package/dist/cli/tunnel.js.map +1 -0
- package/dist/cli/tunnel.test.d.ts +1 -0
- package/dist/cli/tunnel.test.js +190 -0
- package/dist/cli/tunnel.test.js.map +1 -0
- package/dist/cli/use-open-browser.d.ts +1 -0
- package/dist/cli/use-open-browser.js +44 -0
- package/dist/cli/use-open-browser.js.map +1 -0
- package/dist/cli/use-tunnel.d.ts +1 -1
- package/dist/cli/use-tunnel.js +102 -68
- package/dist/cli/use-tunnel.js.map +1 -1
- package/dist/commands/build.js +36 -1
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/dev.d.ts +1 -0
- package/dist/commands/dev.js +33 -2
- package/dist/commands/dev.js.map +1 -1
- package/dist/server/asset-base-url-transform-plugin.js +1 -1
- package/dist/server/asset-base-url-transform-plugin.js.map +1 -1
- package/dist/server/asset-base-url-transform-plugin.test.js +29 -0
- package/dist/server/asset-base-url-transform-plugin.test.js.map +1 -1
- package/dist/server/express.d.ts +1 -5
- package/dist/server/express.js +31 -7
- package/dist/server/express.js.map +1 -1
- package/dist/server/express.test.js +210 -69
- package/dist/server/express.test.js.map +1 -1
- package/dist/server/inferUtilityTypes.d.ts +6 -6
- package/dist/server/server.d.ts +27 -4
- package/dist/server/server.js +64 -20
- package/dist/server/server.js.map +1 -1
- package/dist/server/templateHelper.d.ts +0 -2
- package/dist/server/templateHelper.js +3 -22
- package/dist/server/templateHelper.js.map +1 -1
- package/dist/server/templates.generated.d.ts +4 -0
- package/dist/server/templates.generated.js +47 -0
- package/dist/server/templates.generated.js.map +1 -0
- package/dist/server/tunnel-proxy-router.d.ts +7 -0
- package/dist/server/tunnel-proxy-router.js +110 -0
- package/dist/server/tunnel-proxy-router.js.map +1 -0
- package/dist/server/tunnel-proxy-router.test.d.ts +1 -0
- package/dist/server/tunnel-proxy-router.test.js +229 -0
- package/dist/server/tunnel-proxy-router.test.js.map +1 -0
- package/dist/test/view.test.js +66 -66
- package/dist/test/view.test.js.map +1 -1
- package/dist/version.js +1 -3
- package/dist/version.js.map +1 -1
- package/dist/web/bridges/apps-sdk/adaptor.d.ts +2 -2
- package/dist/web/bridges/apps-sdk/adaptor.js +8 -2
- package/dist/web/bridges/apps-sdk/adaptor.js.map +1 -1
- package/dist/web/bridges/mcp-app/adaptor.d.ts +6 -6
- package/dist/web/bridges/mcp-app/adaptor.js +24 -29
- package/dist/web/bridges/mcp-app/adaptor.js.map +1 -1
- package/dist/web/bridges/types.d.ts +5 -6
- package/dist/web/components/modal-provider.js +1 -1
- package/dist/web/components/modal-provider.js.map +1 -1
- package/dist/web/create-store.js +9 -9
- package/dist/web/create-store.js.map +1 -1
- package/dist/web/create-store.test.js +14 -16
- package/dist/web/create-store.test.js.map +1 -1
- package/dist/web/data-llm.d.ts +1 -1
- package/dist/web/data-llm.js +3 -3
- package/dist/web/data-llm.js.map +1 -1
- package/dist/web/data-llm.test.js +22 -22
- package/dist/web/data-llm.test.js.map +1 -1
- package/dist/web/generate-helpers.test-d.js +7 -7
- package/dist/web/generate-helpers.test-d.js.map +1 -1
- package/dist/web/helpers/state.d.ts +2 -2
- package/dist/web/helpers/state.js +11 -11
- package/dist/web/helpers/state.js.map +1 -1
- package/dist/web/helpers/state.test.js +9 -9
- package/dist/web/helpers/state.test.js.map +1 -1
- package/dist/web/hooks/index.d.ts +1 -1
- package/dist/web/hooks/index.js +1 -1
- package/dist/web/hooks/index.js.map +1 -1
- package/dist/web/hooks/use-call-tool.test.js +0 -4
- package/dist/web/hooks/use-call-tool.test.js.map +1 -1
- package/dist/web/hooks/use-request-modal.d.ts +1 -1
- package/dist/web/hooks/use-request-modal.js +4 -4
- package/dist/web/hooks/use-request-modal.js.map +1 -1
- package/dist/web/hooks/use-request-modal.test.js +1 -1
- package/dist/web/hooks/use-request-modal.test.js.map +1 -1
- package/dist/web/hooks/use-view-state.d.ts +4 -0
- package/dist/web/hooks/use-view-state.js +32 -0
- package/dist/web/hooks/use-view-state.js.map +1 -0
- package/dist/web/hooks/use-view-state.test.d.ts +1 -0
- package/dist/web/hooks/{use-widget-state.test.js → use-view-state.test.js} +17 -17
- package/dist/web/hooks/use-view-state.test.js.map +1 -0
- package/dist/web/index.d.ts +1 -3
- package/dist/web/index.js +1 -2
- package/dist/web/index.js.map +1 -1
- package/dist/web/mount-view.d.ts +1 -0
- package/dist/web/{mount-widget.js → mount-view.js} +2 -2
- package/dist/web/mount-view.js.map +1 -0
- package/dist/web/plugin/plugin.js +32 -17
- package/dist/web/plugin/plugin.js.map +1 -1
- package/dist/web/plugin/scan-views.d.ts +8 -0
- package/dist/web/plugin/scan-views.js +26 -8
- package/dist/web/plugin/scan-views.js.map +1 -1
- package/dist/web/plugin/scan-views.test.js +33 -1
- package/dist/web/plugin/scan-views.test.js.map +1 -1
- package/package.json +12 -6
- package/dist/server/templates/development.hbs +0 -12
- package/dist/server/templates/production.hbs +0 -6
- package/dist/web/hooks/use-widget-state.d.ts +0 -4
- package/dist/web/hooks/use-widget-state.js +0 -32
- package/dist/web/hooks/use-widget-state.js.map +0 -1
- package/dist/web/hooks/use-widget-state.test.js.map +0 -1
- package/dist/web/mount-widget.d.ts +0 -1
- package/dist/web/mount-widget.js.map +0 -1
- /package/dist/{web/hooks/use-widget-state.test.d.ts → cli/tunnel-control-server.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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"}
|
package/dist/test/view.test.js
CHANGED
|
@@ -2,14 +2,14 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi, } from "vitest";
|
|
3
3
|
import { createMockExtra, createMockMcpServer, resetTestEnv, setTestEnv, } from "./utils.js";
|
|
4
4
|
const mockManifest = {
|
|
5
|
-
"skybridge:view:my-
|
|
6
|
-
file: "assets/my-
|
|
7
|
-
name: "my-
|
|
5
|
+
"skybridge:view:my-view": {
|
|
6
|
+
file: "assets/my-view-abc123.js",
|
|
7
|
+
name: "my-view",
|
|
8
8
|
isEntry: true,
|
|
9
9
|
},
|
|
10
|
-
"skybridge:view:folder-
|
|
11
|
-
file: "assets/folder-
|
|
12
|
-
name: "folder-
|
|
10
|
+
"skybridge:view:folder-view": {
|
|
11
|
+
file: "assets/folder-view-def456.js",
|
|
12
|
+
name: "folder-view",
|
|
13
13
|
isEntry: true,
|
|
14
14
|
},
|
|
15
15
|
"style.css": { file: "style.css" },
|
|
@@ -57,10 +57,10 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
57
57
|
it("should generate correct HTML for development mode", async () => {
|
|
58
58
|
setTestEnv({ NODE_ENV: "development" });
|
|
59
59
|
server.registerTool({
|
|
60
|
-
name: "my-
|
|
60
|
+
name: "my-view",
|
|
61
61
|
description: "Test tool",
|
|
62
62
|
view: {
|
|
63
|
-
component: "my-
|
|
63
|
+
component: "my-view",
|
|
64
64
|
description: "Test view",
|
|
65
65
|
},
|
|
66
66
|
}, vi.fn());
|
|
@@ -71,12 +71,12 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
71
71
|
const serverUrl = `http://${host}`;
|
|
72
72
|
const hmrUrl = `ws://${host}`;
|
|
73
73
|
const mockExtra = createMockExtra(host);
|
|
74
|
-
const result = await appsSdkResourceCallback(new URL("ui://
|
|
74
|
+
const result = await appsSdkResourceCallback(new URL("ui://views/apps-sdk/my-view.html"), mockExtra);
|
|
75
75
|
expect(mockRegisterTool).toHaveBeenCalled();
|
|
76
76
|
expect(result).toEqual({
|
|
77
77
|
contents: [
|
|
78
78
|
{
|
|
79
|
-
uri: "ui://
|
|
79
|
+
uri: "ui://views/apps-sdk/my-view.html",
|
|
80
80
|
mimeType: "text/html+skybridge",
|
|
81
81
|
text: expect.stringContaining('<div id="root"></div>'),
|
|
82
82
|
_meta: {
|
|
@@ -92,15 +92,15 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
92
92
|
});
|
|
93
93
|
expect(result.contents[0]?.text).toContain(`${serverUrl}/assets/@react-refresh`);
|
|
94
94
|
expect(result.contents[0]?.text).toContain(`${serverUrl}/@vite/client`);
|
|
95
|
-
expect(result.contents[0]?.text).toContain(`${serverUrl}/_skybridge/view/my-
|
|
95
|
+
expect(result.contents[0]?.text).toContain(`${serverUrl}/_skybridge/view/my-view`);
|
|
96
96
|
});
|
|
97
97
|
it("should generate correct HTML for production mode", async () => {
|
|
98
98
|
setTestEnv({ NODE_ENV: "production" });
|
|
99
99
|
server.registerTool({
|
|
100
|
-
name: "my-
|
|
100
|
+
name: "my-view",
|
|
101
101
|
description: "Test tool",
|
|
102
102
|
view: {
|
|
103
|
-
component: "my-
|
|
103
|
+
component: "my-view",
|
|
104
104
|
description: "Test view",
|
|
105
105
|
},
|
|
106
106
|
}, vi.fn());
|
|
@@ -110,7 +110,7 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
110
110
|
const host = "myapp.com";
|
|
111
111
|
const serverUrl = `https://${host}`;
|
|
112
112
|
const mockExtra = createMockExtra(host);
|
|
113
|
-
const versionedUri = `ui://
|
|
113
|
+
const versionedUri = `ui://views/apps-sdk/my-view.html${expectedVersionParam("assets/my-view-abc123.js", "style.css")}`;
|
|
114
114
|
const result = await appsSdkResourceCallback(new URL(versionedUri), mockExtra);
|
|
115
115
|
expect(result).toEqual({
|
|
116
116
|
contents: [
|
|
@@ -131,15 +131,15 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
131
131
|
});
|
|
132
132
|
expect(result.contents[0]?.text).not.toContain(`${serverUrl}/assets/@react-refresh`);
|
|
133
133
|
expect(result.contents[0]?.text).not.toContain(`${serverUrl}@vite/client`);
|
|
134
|
-
expect(result.contents[0]?.text).toContain(`${serverUrl}/assets/assets/my-
|
|
134
|
+
expect(result.contents[0]?.text).toContain(`${serverUrl}/assets/assets/my-view-abc123.js`);
|
|
135
135
|
expect(result.contents[0]?.text).toContain(`${serverUrl}/assets/style.css`);
|
|
136
136
|
});
|
|
137
137
|
it("should prefer x-alpic-forwarded-url when hashing Claude view domains", async () => {
|
|
138
138
|
setTestEnv({ NODE_ENV: "production" });
|
|
139
139
|
server.registerTool({
|
|
140
|
-
name: "my-
|
|
140
|
+
name: "my-view",
|
|
141
141
|
description: "Test tool",
|
|
142
|
-
view: { component: "my-
|
|
142
|
+
view: { component: "my-view", description: "Test view" },
|
|
143
143
|
}, vi.fn());
|
|
144
144
|
const extAppsResourceCallback = mockRegisterResource.mock
|
|
145
145
|
.calls[1]?.[3];
|
|
@@ -150,7 +150,7 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
150
150
|
.update(forwardedUrl)
|
|
151
151
|
.digest("hex")
|
|
152
152
|
.slice(0, 32)}.claudemcpcontent.com`;
|
|
153
|
-
const result = await extAppsResourceCallback(new URL(`ui://
|
|
153
|
+
const result = await extAppsResourceCallback(new URL(`ui://views/ext-apps/my-view.html${expectedVersionParam("assets/my-view-abc123.js", "style.css")}`), createMockExtra("localhost:3000", {
|
|
154
154
|
headers: {
|
|
155
155
|
"user-agent": "Claude-User",
|
|
156
156
|
"x-alpic-forwarded-url": forwardedUrl,
|
|
@@ -171,10 +171,10 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
171
171
|
});
|
|
172
172
|
it("should register resources with correct hostType for both apps-sdk and ext-apps", async () => {
|
|
173
173
|
server.registerTool({
|
|
174
|
-
name: "my-
|
|
174
|
+
name: "my-view",
|
|
175
175
|
description: "Test tool",
|
|
176
176
|
view: {
|
|
177
|
-
component: "my-
|
|
177
|
+
component: "my-view",
|
|
178
178
|
description: "Test view",
|
|
179
179
|
prefersBorder: true,
|
|
180
180
|
},
|
|
@@ -185,11 +185,11 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
185
185
|
const host = "localhost:3000";
|
|
186
186
|
const serverUrl = `http://${host}`;
|
|
187
187
|
const hmrUrl = `ws://${host}`;
|
|
188
|
-
const appsSdkResult = await appsSdkCallback(new URL("ui://
|
|
188
|
+
const appsSdkResult = await appsSdkCallback(new URL("ui://views/apps-sdk/my-view.html"), createMockExtra(host));
|
|
189
189
|
expect(appsSdkResult).toEqual({
|
|
190
190
|
contents: [
|
|
191
191
|
{
|
|
192
|
-
uri: "ui://
|
|
192
|
+
uri: "ui://views/apps-sdk/my-view.html",
|
|
193
193
|
mimeType: "text/html+skybridge",
|
|
194
194
|
text: expect.stringContaining('<div id="root"></div>'),
|
|
195
195
|
_meta: {
|
|
@@ -208,11 +208,11 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
208
208
|
const extAppsResourceCallback = mockRegisterResource.mock
|
|
209
209
|
.calls[1]?.[3];
|
|
210
210
|
expect(extAppsResourceCallback).toBeDefined();
|
|
211
|
-
const extAppsResult = await extAppsResourceCallback(new URL("ui://
|
|
211
|
+
const extAppsResult = await extAppsResourceCallback(new URL("ui://views/ext-apps/my-view.html"), createMockExtra(host));
|
|
212
212
|
expect(extAppsResult).toEqual({
|
|
213
213
|
contents: [
|
|
214
214
|
{
|
|
215
|
-
uri: "ui://
|
|
215
|
+
uri: "ui://views/ext-apps/my-view.html",
|
|
216
216
|
mimeType: "text/html;profile=mcp-app",
|
|
217
217
|
text: expect.stringContaining('<div id="root"></div>'),
|
|
218
218
|
_meta: {
|
|
@@ -234,24 +234,24 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
234
234
|
});
|
|
235
235
|
it("should register tool with ui.resourceUri metadata", async () => {
|
|
236
236
|
server.registerTool({
|
|
237
|
-
name: "my-
|
|
237
|
+
name: "my-view",
|
|
238
238
|
description: "Test tool",
|
|
239
|
-
view: { component: "my-
|
|
239
|
+
view: { component: "my-view", description: "Test view" },
|
|
240
240
|
}, vi.fn());
|
|
241
241
|
expect(mockRegisterTool).toHaveBeenCalledTimes(1);
|
|
242
242
|
const toolCallArgs = mockRegisterTool.mock.calls[0];
|
|
243
243
|
const toolConfig = toolCallArgs?.[1];
|
|
244
244
|
expect(toolConfig._meta).toHaveProperty("ui");
|
|
245
245
|
expect(toolConfig._meta?.ui).toEqual({
|
|
246
|
-
resourceUri: "ui://
|
|
246
|
+
resourceUri: "ui://views/ext-apps/my-view.html",
|
|
247
247
|
});
|
|
248
248
|
});
|
|
249
249
|
it("should register tool with openai/outputTemplate when apps-sdk only", async () => {
|
|
250
250
|
server.registerTool({
|
|
251
|
-
name: "my-
|
|
251
|
+
name: "my-view",
|
|
252
252
|
description: "Test tool",
|
|
253
253
|
view: {
|
|
254
|
-
component: "my-
|
|
254
|
+
component: "my-view",
|
|
255
255
|
description: "Test view",
|
|
256
256
|
hosts: ["apps-sdk"],
|
|
257
257
|
},
|
|
@@ -260,70 +260,70 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
260
260
|
const toolCallArgs = mockRegisterTool.mock.calls[0];
|
|
261
261
|
const toolConfig = toolCallArgs?.[1];
|
|
262
262
|
expect(toolConfig._meta).not.toHaveProperty("ui");
|
|
263
|
-
expect(toolConfig._meta?.["openai/outputTemplate"]).toBe("ui://
|
|
263
|
+
expect(toolConfig._meta?.["openai/outputTemplate"]).toBe("ui://views/apps-sdk/my-view.html");
|
|
264
264
|
});
|
|
265
265
|
it("should not version view URIs in development", () => {
|
|
266
266
|
server.registerTool({
|
|
267
|
-
name: "my-
|
|
267
|
+
name: "my-view",
|
|
268
268
|
description: "Test tool",
|
|
269
|
-
view: { component: "my-
|
|
269
|
+
view: { component: "my-view", description: "Test view" },
|
|
270
270
|
}, vi.fn());
|
|
271
271
|
const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
|
|
272
|
-
expect(toolConfig._meta?.["openai/outputTemplate"]).toBe("ui://
|
|
273
|
-
expect(toolConfig._meta?.ui?.resourceUri).toBe("ui://
|
|
272
|
+
expect(toolConfig._meta?.["openai/outputTemplate"]).toBe("ui://views/apps-sdk/my-view.html");
|
|
273
|
+
expect(toolConfig._meta?.ui?.resourceUri).toBe("ui://views/ext-apps/my-view.html");
|
|
274
274
|
// The URI registered with the resource handler must match the URI in
|
|
275
275
|
// outputTemplate exactly so the SDK can resolve `resources/read` requests.
|
|
276
|
-
expect(mockRegisterResource.mock.calls[0]?.[1]).toBe("ui://
|
|
277
|
-
expect(mockRegisterResource.mock.calls[1]?.[1]).toBe("ui://
|
|
276
|
+
expect(mockRegisterResource.mock.calls[0]?.[1]).toBe("ui://views/apps-sdk/my-view.html");
|
|
277
|
+
expect(mockRegisterResource.mock.calls[1]?.[1]).toBe("ui://views/ext-apps/my-view.html");
|
|
278
278
|
});
|
|
279
279
|
it("should append a stable content hash to view URIs in production", () => {
|
|
280
280
|
setTestEnv({ NODE_ENV: "production" });
|
|
281
281
|
server.registerTool({
|
|
282
|
-
name: "my-
|
|
282
|
+
name: "my-view",
|
|
283
283
|
description: "Test tool",
|
|
284
|
-
view: { component: "my-
|
|
284
|
+
view: { component: "my-view", description: "Test view" },
|
|
285
285
|
}, vi.fn());
|
|
286
|
-
const expected = expectedVersionParam("assets/my-
|
|
286
|
+
const expected = expectedVersionParam("assets/my-view-abc123.js", "style.css");
|
|
287
287
|
const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
|
|
288
|
-
expect(toolConfig._meta?.["openai/outputTemplate"]).toBe(`ui://
|
|
289
|
-
expect(toolConfig._meta?.ui?.resourceUri).toBe(`ui://
|
|
290
|
-
expect(mockRegisterResource.mock.calls[0]?.[1]).toBe(`ui://
|
|
291
|
-
expect(mockRegisterResource.mock.calls[1]?.[1]).toBe(`ui://
|
|
288
|
+
expect(toolConfig._meta?.["openai/outputTemplate"]).toBe(`ui://views/apps-sdk/my-view.html${expected}`);
|
|
289
|
+
expect(toolConfig._meta?.ui?.resourceUri).toBe(`ui://views/ext-apps/my-view.html${expected}`);
|
|
290
|
+
expect(mockRegisterResource.mock.calls[0]?.[1]).toBe(`ui://views/apps-sdk/my-view.html${expected}`);
|
|
291
|
+
expect(mockRegisterResource.mock.calls[1]?.[1]).toBe(`ui://views/ext-apps/my-view.html${expected}`);
|
|
292
292
|
});
|
|
293
293
|
it("should produce different version params for views with different bundles", () => {
|
|
294
294
|
setTestEnv({ NODE_ENV: "production" });
|
|
295
295
|
server.registerTool({
|
|
296
|
-
name: "my-
|
|
296
|
+
name: "my-view",
|
|
297
297
|
description: "First tool",
|
|
298
|
-
view: { component: "my-
|
|
298
|
+
view: { component: "my-view" },
|
|
299
299
|
}, vi.fn());
|
|
300
300
|
server.registerTool({
|
|
301
|
-
name: "folder-
|
|
301
|
+
name: "folder-view",
|
|
302
302
|
description: "Second tool",
|
|
303
|
-
view: { component: "folder-
|
|
303
|
+
view: { component: "folder-view" },
|
|
304
304
|
}, vi.fn());
|
|
305
|
-
const
|
|
306
|
-
const
|
|
307
|
-
expect(
|
|
308
|
-
expect(
|
|
309
|
-
expect(
|
|
305
|
+
const myviewTemplate = (mockRegisterTool.mock.calls[0]?.[1])._meta?.["openai/outputTemplate"];
|
|
306
|
+
const folderviewTemplate = (mockRegisterTool.mock.calls[1]?.[1])._meta?.["openai/outputTemplate"];
|
|
307
|
+
expect(myviewTemplate).not.toEqual(folderviewTemplate);
|
|
308
|
+
expect(myviewTemplate).toMatch(/\?v=[0-9a-f]{8}$/);
|
|
309
|
+
expect(folderviewTemplate).toMatch(/\?v=[0-9a-f]{8}$/);
|
|
310
310
|
});
|
|
311
311
|
it("should fall back to bare URI in production when manifest is missing", () => {
|
|
312
312
|
setTestEnv({ NODE_ENV: "production" });
|
|
313
313
|
server.registerTool({
|
|
314
|
-
name: "unknown-
|
|
314
|
+
name: "unknown-view",
|
|
315
315
|
description: "Test tool",
|
|
316
|
-
view: { component: "unknown-
|
|
316
|
+
view: { component: "unknown-view" },
|
|
317
317
|
}, vi.fn());
|
|
318
318
|
const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
|
|
319
|
-
expect(toolConfig._meta?.["openai/outputTemplate"]).toBe("ui://
|
|
319
|
+
expect(toolConfig._meta?.["openai/outputTemplate"]).toBe("ui://views/apps-sdk/unknown-view.html");
|
|
320
320
|
});
|
|
321
321
|
it("should register tool with ui.resourceUri only when mcp-app only", async () => {
|
|
322
322
|
server.registerTool({
|
|
323
|
-
name: "my-
|
|
323
|
+
name: "my-view",
|
|
324
324
|
description: "Test tool",
|
|
325
325
|
view: {
|
|
326
|
-
component: "my-
|
|
326
|
+
component: "my-view",
|
|
327
327
|
description: "Test view",
|
|
328
328
|
hosts: ["mcp-app"],
|
|
329
329
|
},
|
|
@@ -333,7 +333,7 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
333
333
|
const toolConfig = toolCallArgs?.[1];
|
|
334
334
|
expect(toolConfig._meta).toHaveProperty("ui");
|
|
335
335
|
expect(toolConfig._meta?.ui).toEqual({
|
|
336
|
-
resourceUri: "ui://
|
|
336
|
+
resourceUri: "ui://views/ext-apps/my-view.html",
|
|
337
337
|
});
|
|
338
338
|
expect(toolConfig._meta?.["openai/outputTemplate"]).toBeUndefined();
|
|
339
339
|
});
|
|
@@ -343,9 +343,9 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
343
343
|
structuredContent: { data: "test" },
|
|
344
344
|
});
|
|
345
345
|
server.registerTool({
|
|
346
|
-
name: "my-
|
|
346
|
+
name: "my-view",
|
|
347
347
|
description: "Test tool",
|
|
348
|
-
view: { component: "my-
|
|
348
|
+
view: { component: "my-view", description: "Test view" },
|
|
349
349
|
}, mockToolCallback);
|
|
350
350
|
const wrappedCallback = mockRegisterTool.mock.calls[0]?.[2];
|
|
351
351
|
expect(wrappedCallback).toBeDefined();
|
|
@@ -362,9 +362,9 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
362
362
|
_meta: { requestId: "req-123", cached: true },
|
|
363
363
|
});
|
|
364
364
|
server.registerTool({
|
|
365
|
-
name: "my-
|
|
365
|
+
name: "my-view",
|
|
366
366
|
description: "Test tool",
|
|
367
|
-
view: { component: "my-
|
|
367
|
+
view: { component: "my-view", description: "Test view" },
|
|
368
368
|
}, mockToolCallback);
|
|
369
369
|
const wrappedCallback = mockRegisterTool.mock.calls[0]?.[2];
|
|
370
370
|
const result = await wrappedCallback({}, {});
|
|
@@ -378,9 +378,9 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
378
378
|
structuredContent: {},
|
|
379
379
|
});
|
|
380
380
|
server.registerTool({
|
|
381
|
-
name: "my-
|
|
381
|
+
name: "my-view",
|
|
382
382
|
description: "Test tool",
|
|
383
|
-
view: { component: "my-
|
|
383
|
+
view: { component: "my-view", description: "Test view" },
|
|
384
384
|
}, mockToolCallback);
|
|
385
385
|
const wrappedCallback = mockRegisterTool.mock.calls[0]?.[2];
|
|
386
386
|
const result1 = await wrappedCallback({}, {});
|
|
@@ -473,7 +473,7 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
473
473
|
const host = "localhost:3000";
|
|
474
474
|
const serverUrl = `http://${host}`;
|
|
475
475
|
const hmrUrl = `ws://${host}`;
|
|
476
|
-
const result = await appsSdkCallback(new URL("ui://
|
|
476
|
+
const result = await appsSdkCallback(new URL("ui://views/apps-sdk/csp-tool.html"), createMockExtra(host));
|
|
477
477
|
const meta = result.contents[0]?._meta;
|
|
478
478
|
expect(meta["openai/widgetCSP"]).toEqual({
|
|
479
479
|
resource_domains: [serverUrl, "https://cdn.example.com"],
|
|
@@ -497,7 +497,7 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
497
497
|
}, vi.fn());
|
|
498
498
|
const appsSdkCallback = mockRegisterResource.mock
|
|
499
499
|
.calls[0]?.[3];
|
|
500
|
-
const result = await appsSdkCallback(new URL("ui://
|
|
500
|
+
const result = await appsSdkCallback(new URL("ui://views/apps-sdk/override-tool.html"), createMockExtra("localhost:3000"));
|
|
501
501
|
const meta = result.contents[0]?._meta;
|
|
502
502
|
expect(meta["openai/widgetCSP"]).toEqual({
|
|
503
503
|
connect_domains: ["https://api.y.com"],
|