skybridge 0.0.0-dev.89e35fa → 0.0.0-dev.89f9a0f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/cli/build-helpers.d.ts +6 -2
- package/dist/cli/build-helpers.js +37 -12
- package/dist/cli/build-helpers.js.map +1 -1
- package/dist/cli/build-helpers.test.js +71 -3
- package/dist/cli/build-helpers.test.js.map +1 -1
- package/dist/cli/build-steps.d.ts +2 -0
- package/dist/cli/build-steps.js +73 -0
- package/dist/cli/build-steps.js.map +1 -0
- package/dist/cli/build-steps.test.d.ts +1 -0
- package/dist/cli/build-steps.test.js +52 -0
- package/dist/cli/build-steps.test.js.map +1 -0
- package/dist/cli/header.d.ts +1 -1
- package/dist/cli/resolve-skybridge-config.d.ts +5 -0
- package/dist/cli/{resolve-views-dir.js → resolve-skybridge-config.js} +4 -3
- package/dist/cli/resolve-skybridge-config.js.map +1 -0
- package/dist/cli/run-plain.d.ts +18 -0
- package/dist/cli/run-plain.js +89 -0
- package/dist/cli/run-plain.js.map +1 -0
- package/dist/cli/use-nodemon.d.ts +14 -0
- package/dist/cli/use-nodemon.js +71 -60
- package/dist/cli/use-nodemon.js.map +1 -1
- package/dist/cli/use-typescript-check.d.ts +8 -2
- package/dist/cli/use-typescript-check.js +70 -67
- package/dist/cli/use-typescript-check.js.map +1 -1
- package/dist/commands/build.d.ts +0 -2
- package/dist/commands/build.js +2 -61
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/dev.d.ts +1 -0
- package/dist/commands/dev.js +35 -5
- package/dist/commands/dev.js.map +1 -1
- package/dist/context-warnings.d.ts +5 -0
- package/dist/context-warnings.js +31 -0
- package/dist/context-warnings.js.map +1 -0
- package/dist/context-warnings.test.d.ts +1 -0
- package/dist/context-warnings.test.js +28 -0
- package/dist/context-warnings.test.js.map +1 -0
- package/dist/server/auth/discovery.d.ts +32 -0
- package/dist/server/auth/discovery.js +56 -0
- package/dist/server/auth/discovery.js.map +1 -0
- package/dist/server/auth/discovery.test.d.ts +1 -0
- package/dist/server/auth/discovery.test.js +93 -0
- package/dist/server/auth/discovery.test.js.map +1 -0
- package/dist/server/auth/index.d.ts +44 -0
- package/dist/server/auth/index.js +2 -0
- package/dist/server/auth/index.js.map +1 -0
- package/dist/server/auth/providers/auth0.d.ts +40 -0
- package/dist/server/auth/providers/auth0.js +31 -0
- package/dist/server/auth/providers/auth0.js.map +1 -0
- package/dist/server/auth/providers/auth0.test.d.ts +1 -0
- package/dist/server/auth/providers/auth0.test.js +50 -0
- package/dist/server/auth/providers/auth0.test.js.map +1 -0
- package/dist/server/auth/providers/authplane.d.ts +49 -0
- package/dist/server/auth/providers/authplane.js +57 -0
- package/dist/server/auth/providers/authplane.js.map +1 -0
- package/dist/server/auth/providers/authplane.test.d.ts +1 -0
- package/dist/server/auth/providers/authplane.test.js +140 -0
- package/dist/server/auth/providers/authplane.test.js.map +1 -0
- package/dist/server/auth/providers/clerk.d.ts +51 -0
- package/dist/server/auth/providers/clerk.js +19 -0
- package/dist/server/auth/providers/clerk.js.map +1 -0
- package/dist/server/auth/providers/clerk.test.d.ts +1 -0
- package/dist/server/auth/providers/clerk.test.js +30 -0
- package/dist/server/auth/providers/clerk.test.js.map +1 -0
- package/dist/server/auth/providers/custom.d.ts +40 -0
- package/dist/server/auth/providers/custom.js +56 -0
- package/dist/server/auth/providers/custom.js.map +1 -0
- package/dist/server/auth/providers/custom.test.d.ts +1 -0
- package/dist/server/auth/providers/custom.test.js +158 -0
- package/dist/server/auth/providers/custom.test.js.map +1 -0
- package/dist/server/auth/providers/descope.d.ts +44 -0
- package/dist/server/auth/providers/descope.js +44 -0
- package/dist/server/auth/providers/descope.js.map +1 -0
- package/dist/server/auth/providers/descope.test.d.ts +1 -0
- package/dist/server/auth/providers/descope.test.js +62 -0
- package/dist/server/auth/providers/descope.test.js.map +1 -0
- package/dist/server/auth/providers/shared.d.ts +2 -0
- package/dist/server/auth/providers/shared.js +6 -0
- package/dist/server/auth/providers/shared.js.map +1 -0
- package/dist/server/auth/providers/shared.test.d.ts +1 -0
- package/dist/server/auth/providers/shared.test.js +10 -0
- package/dist/server/auth/providers/shared.test.js.map +1 -0
- package/dist/server/auth/providers/stytch.d.ts +25 -0
- package/dist/server/auth/providers/stytch.js +16 -0
- package/dist/server/auth/providers/stytch.js.map +1 -0
- package/dist/server/auth/providers/verify-spy.d.ts +12 -0
- package/dist/server/auth/providers/verify-spy.js +21 -0
- package/dist/server/auth/providers/verify-spy.js.map +1 -0
- package/dist/server/auth/providers/workos.d.ts +33 -0
- package/dist/server/auth/providers/workos.js +15 -0
- package/dist/server/auth/providers/workos.js.map +1 -0
- package/dist/server/auth/security-schemes.d.ts +23 -0
- package/dist/server/auth/security-schemes.js +64 -0
- package/dist/server/auth/security-schemes.js.map +1 -0
- package/dist/server/auth/security-schemes.test.d.ts +1 -0
- package/dist/server/auth/security-schemes.test.js +99 -0
- package/dist/server/auth/security-schemes.test.js.map +1 -0
- package/dist/server/auth/setup.d.ts +6 -0
- package/dist/server/auth/setup.js +92 -0
- package/dist/server/auth/setup.js.map +1 -0
- package/dist/server/auth/setup.test.d.ts +1 -0
- package/dist/server/auth/setup.test.js +508 -0
- package/dist/server/auth/setup.test.js.map +1 -0
- package/dist/server/auth/verify.d.ts +46 -0
- package/dist/server/auth/verify.js +62 -0
- package/dist/server/auth/verify.js.map +1 -0
- package/dist/server/auth/verify.test.d.ts +1 -0
- package/dist/server/auth/verify.test.js +149 -0
- package/dist/server/auth/verify.test.js.map +1 -0
- package/dist/server/auth-extra.test-d.d.ts +1 -0
- package/dist/server/auth-extra.test-d.js +82 -0
- package/dist/server/auth-extra.test-d.js.map +1 -0
- package/dist/server/auth.d.ts +39 -1
- package/dist/server/auth.js.map +1 -1
- package/dist/server/express.test.js +31 -0
- package/dist/server/express.test.js.map +1 -1
- package/dist/server/host.d.ts +2 -0
- package/dist/server/host.js +11 -0
- package/dist/server/host.js.map +1 -0
- package/dist/server/host.test.d.ts +1 -0
- package/dist/server/host.test.js +13 -0
- package/dist/server/host.test.js.map +1 -0
- package/dist/server/index.d.ts +14 -3
- package/dist/server/index.js +11 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/middleware.d.ts +31 -6
- package/dist/server/middleware.js +33 -0
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/middleware.test.js +37 -1
- package/dist/server/middleware.test.js.map +1 -1
- package/dist/server/requestOrigin.d.ts +7 -0
- package/dist/server/requestOrigin.js +25 -0
- package/dist/server/requestOrigin.js.map +1 -0
- package/dist/server/server.d.ts +97 -20
- package/dist/server/server.js +237 -53
- package/dist/server/server.js.map +1 -1
- package/dist/server/skills-integration.test.d.ts +1 -0
- package/dist/server/skills-integration.test.js +79 -0
- package/dist/server/skills-integration.test.js.map +1 -0
- package/dist/server/skills.d.ts +29 -0
- package/dist/server/skills.js +195 -0
- package/dist/server/skills.js.map +1 -0
- package/dist/server/skills.test.d.ts +1 -0
- package/dist/server/skills.test.js +247 -0
- package/dist/server/skills.test.js.map +1 -0
- package/dist/server/view-name.test-d.d.ts +1 -0
- package/dist/server/view-name.test-d.js +8 -0
- package/dist/server/view-name.test-d.js.map +1 -0
- package/dist/server/view-resource-resolution.test.d.ts +6 -0
- package/dist/server/view-resource-resolution.test.js +171 -0
- package/dist/server/view-resource-resolution.test.js.map +1 -0
- package/dist/server/viewsDevServer.js +4 -1
- package/dist/server/viewsDevServer.js.map +1 -1
- package/dist/test/utils.d.ts +3 -3
- package/dist/test/view.test.js +37 -11
- package/dist/test/view.test.js.map +1 -1
- package/dist/web/bridges/adaptor.d.ts +6 -5
- package/dist/web/bridges/adaptor.js +50 -37
- package/dist/web/bridges/adaptor.js.map +1 -1
- package/dist/web/bridges/adaptor.test.js +32 -12
- package/dist/web/bridges/adaptor.test.js.map +1 -1
- package/dist/web/bridges/apps-sdk/bridge.d.ts +5 -1
- package/dist/web/bridges/apps-sdk/bridge.js +12 -0
- package/dist/web/bridges/apps-sdk/bridge.js.map +1 -1
- package/dist/web/bridges/get-adaptor.d.ts +0 -2
- package/dist/web/bridges/get-adaptor.js +1 -13
- package/dist/web/bridges/get-adaptor.js.map +1 -1
- package/dist/web/bridges/get-adaptor.test.js +3 -3
- package/dist/web/bridges/get-adaptor.test.js.map +1 -1
- package/dist/web/bridges/mcp-app/bridge.d.ts +14 -2
- package/dist/web/bridges/mcp-app/bridge.js +72 -4
- package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
- package/dist/web/bridges/mcp-app/bridge.test.js +1 -5
- package/dist/web/bridges/mcp-app/bridge.test.js.map +1 -1
- package/dist/web/bridges/mcp-app/types.d.ts +2 -0
- package/dist/web/bridges/mcp-app/types.js.map +1 -1
- package/dist/web/bridges/mcp-app/view-tools.test.d.ts +1 -0
- package/dist/web/bridges/mcp-app/view-tools.test.js +143 -0
- package/dist/web/bridges/mcp-app/view-tools.test.js.map +1 -0
- package/dist/web/bridges/types.d.ts +34 -6
- package/dist/web/bridges/types.js.map +1 -1
- package/dist/web/components/modal-provider.d.ts +1 -1
- package/dist/web/create-store.test.js +4 -3
- package/dist/web/create-store.test.js.map +1 -1
- package/dist/web/data-llm.d.ts +1 -1
- package/dist/web/data-llm.test.js +13 -3
- package/dist/web/data-llm.test.js.map +1 -1
- package/dist/web/generate-helpers.test-d.js +4 -2
- package/dist/web/generate-helpers.test-d.js.map +1 -1
- package/dist/web/hooks/index.d.ts +2 -0
- package/dist/web/hooks/index.js +2 -0
- package/dist/web/hooks/index.js.map +1 -1
- package/dist/web/hooks/test/utils.d.ts +2 -1
- package/dist/web/hooks/test/utils.js +1 -1
- package/dist/web/hooks/test/utils.js.map +1 -1
- package/dist/web/hooks/use-call-tool.test.js +6 -5
- package/dist/web/hooks/use-call-tool.test.js.map +1 -1
- package/dist/web/hooks/use-display-mode.test.js +4 -3
- package/dist/web/hooks/use-display-mode.test.js.map +1 -1
- package/dist/web/hooks/use-download.test.js +5 -5
- package/dist/web/hooks/use-download.test.js.map +1 -1
- package/dist/web/hooks/use-files.test.js +3 -3
- package/dist/web/hooks/use-files.test.js.map +1 -1
- package/dist/web/hooks/use-host-info.d.ts +22 -0
- package/dist/web/hooks/use-host-info.js +30 -0
- package/dist/web/hooks/use-host-info.js.map +1 -0
- package/dist/web/hooks/use-host-info.test.d.ts +1 -0
- package/dist/web/hooks/use-host-info.test.js +59 -0
- package/dist/web/hooks/use-host-info.test.js.map +1 -0
- package/dist/web/hooks/use-layout.test.js +5 -5
- package/dist/web/hooks/use-layout.test.js.map +1 -1
- package/dist/web/hooks/use-open-external.test.js +7 -11
- package/dist/web/hooks/use-open-external.test.js.map +1 -1
- package/dist/web/hooks/use-register-view-tool.d.ts +38 -0
- package/dist/web/hooks/use-register-view-tool.js +50 -0
- package/dist/web/hooks/use-register-view-tool.js.map +1 -0
- package/dist/web/hooks/use-request-close.test.js +5 -4
- package/dist/web/hooks/use-request-close.test.js.map +1 -1
- package/dist/web/hooks/use-request-modal.test.js +3 -3
- package/dist/web/hooks/use-request-modal.test.js.map +1 -1
- package/dist/web/hooks/use-request-size.test.js +3 -3
- package/dist/web/hooks/use-request-size.test.js.map +1 -1
- package/dist/web/hooks/use-set-open-in-app-url.test.js +2 -2
- package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -1
- package/dist/web/hooks/use-tool-info.d.ts +25 -7
- package/dist/web/hooks/use-tool-info.js +5 -8
- package/dist/web/hooks/use-tool-info.js.map +1 -1
- package/dist/web/hooks/use-tool-info.test-d.js +11 -29
- package/dist/web/hooks/use-tool-info.test-d.js.map +1 -1
- package/dist/web/hooks/use-tool-info.test.js +8 -8
- package/dist/web/hooks/use-tool-info.test.js.map +1 -1
- package/dist/web/hooks/use-user.test.js +5 -5
- package/dist/web/hooks/use-user.test.js.map +1 -1
- package/dist/web/hooks/use-view-state.js +19 -9
- package/dist/web/hooks/use-view-state.js.map +1 -1
- package/dist/web/hooks/use-view-state.test.js +34 -3
- package/dist/web/hooks/use-view-state.test.js.map +1 -1
- package/dist/web/plugin/plugin-build.test.d.ts +1 -0
- package/dist/web/plugin/plugin-build.test.js +54 -0
- package/dist/web/plugin/plugin-build.test.js.map +1 -0
- package/dist/web/plugin/plugin.d.ts +6 -0
- package/dist/web/plugin/plugin.js +18 -7
- package/dist/web/plugin/plugin.js.map +1 -1
- package/dist/web/plugin/plugin.test.d.ts +1 -0
- package/dist/web/plugin/plugin.test.js +61 -0
- package/dist/web/plugin/plugin.test.js.map +1 -0
- package/package.json +22 -20
- package/dist/cli/resolve-views-dir.d.ts +0 -1
- package/dist/cli/resolve-views-dir.js.map +0 -1
- package/dist/cli/use-open-tunnel-browser.d.ts +0 -6
- package/dist/cli/use-open-tunnel-browser.js +0 -19
- package/dist/cli/use-open-tunnel-browser.js.map +0 -1
- package/dist/web/bridges/host-context-stores.d.ts +0 -27
- package/dist/web/bridges/host-context-stores.js +0 -73
- package/dist/web/bridges/host-context-stores.js.map +0 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { __setBuildManifest, McpServer } from "./index.js";
|
|
7
|
+
vi.mock("@skybridge/devtools", () => ({
|
|
8
|
+
devtoolsStaticServer: () => ((_req, _res, next) => next()),
|
|
9
|
+
}));
|
|
10
|
+
vi.mock("./viewsDevServer.js", () => ({
|
|
11
|
+
viewsDevServer: (_httpServer) => ((_req, _res, next) => next()),
|
|
12
|
+
}));
|
|
13
|
+
// SKY-435: a view resource must resolve no matter the `?v=` query param value.
|
|
14
|
+
// The version param is a content-derived cache key for external consumers
|
|
15
|
+
// (it busts host/CDN caches when the bundle changes); it is not part of the
|
|
16
|
+
// resource's identity, so a stale, absent, or arbitrary param must still serve
|
|
17
|
+
// the underlying asset.
|
|
18
|
+
async function connect(register) {
|
|
19
|
+
const server = new McpServer({ name: "test", version: "1.0.0" });
|
|
20
|
+
register(server);
|
|
21
|
+
const client = new Client({ name: "test-client", version: "1.0.0" });
|
|
22
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
23
|
+
await server.connect(serverTransport);
|
|
24
|
+
await client.connect(clientTransport);
|
|
25
|
+
return {
|
|
26
|
+
client,
|
|
27
|
+
teardown: async () => {
|
|
28
|
+
await client.close();
|
|
29
|
+
await server.close();
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
let openHttpServer;
|
|
34
|
+
afterEach(() => openHttpServer?.close());
|
|
35
|
+
// Connect over real HTTP so forwarded proxy headers reach the resource handler.
|
|
36
|
+
async function connectHttp(register, headers) {
|
|
37
|
+
const { createApp } = await import("./express.js");
|
|
38
|
+
const server = new McpServer({ name: "test", version: "1.0.0" });
|
|
39
|
+
register(server);
|
|
40
|
+
const httpServer = http.createServer();
|
|
41
|
+
await createApp({ mcpServer: server, httpServer });
|
|
42
|
+
const listening = http.createServer(server.express);
|
|
43
|
+
await new Promise((resolve) => listening.listen(0, resolve));
|
|
44
|
+
openHttpServer = listening;
|
|
45
|
+
const port = listening.address().port;
|
|
46
|
+
const client = new Client({ name: "test-client", version: "1.0.0" });
|
|
47
|
+
const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`), { requestInit: { headers } });
|
|
48
|
+
await client.connect(transport);
|
|
49
|
+
return {
|
|
50
|
+
client,
|
|
51
|
+
teardown: async () => {
|
|
52
|
+
await client.close();
|
|
53
|
+
await server.close();
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function registerWidget(server) {
|
|
58
|
+
server.registerTool({ name: "show", description: "show", view: { component: "widget" } }, () => ({ content: [{ type: "text", text: "ok" }] }));
|
|
59
|
+
}
|
|
60
|
+
/** Narrow a read-resource content entry to its text variant. */
|
|
61
|
+
function textContent(contents) {
|
|
62
|
+
return contents[0];
|
|
63
|
+
}
|
|
64
|
+
describe("view resource resolution (cache key)", () => {
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
delete process.env.NODE_ENV;
|
|
67
|
+
});
|
|
68
|
+
it("resolves regardless of the ?v= param and echoes the requested URI", async () => {
|
|
69
|
+
const { client, teardown } = await connect(registerWidget);
|
|
70
|
+
const { resources } = await client.listResources();
|
|
71
|
+
const listed = resources.find((r) => r.uri.startsWith("ui://views/ext-apps/widget.html"));
|
|
72
|
+
// Dev build advertises no cache key.
|
|
73
|
+
expect(listed?.uri).toBe("ui://views/ext-apps/widget.html");
|
|
74
|
+
// A stale/arbitrary/absent param all resolve the same underlying asset,
|
|
75
|
+
// and the response echoes the URI the consumer asked for (never rewritten).
|
|
76
|
+
for (const uri of [
|
|
77
|
+
"ui://views/ext-apps/widget.html?v=stale123",
|
|
78
|
+
"ui://views/ext-apps/widget.html?v=whatever-else",
|
|
79
|
+
"ui://views/ext-apps/widget.html",
|
|
80
|
+
]) {
|
|
81
|
+
const { contents } = await client.readResource({ uri });
|
|
82
|
+
expect(contents).toHaveLength(1);
|
|
83
|
+
const content = textContent(contents);
|
|
84
|
+
expect(typeof content.text).toBe("string");
|
|
85
|
+
expect(content.text.length).toBeGreaterThan(0);
|
|
86
|
+
expect(content.uri).toBe(uri);
|
|
87
|
+
}
|
|
88
|
+
await teardown();
|
|
89
|
+
});
|
|
90
|
+
it("resolves a stale cache key when the canonical URI carries ?v= (prod)", async () => {
|
|
91
|
+
process.env.NODE_ENV = "production";
|
|
92
|
+
__setBuildManifest({
|
|
93
|
+
"src/views/widget.tsx": {
|
|
94
|
+
isEntry: true,
|
|
95
|
+
name: "widget",
|
|
96
|
+
file: "assets/widget-ABC123.js",
|
|
97
|
+
},
|
|
98
|
+
"style.css": { file: "assets/style-XYZ.css" },
|
|
99
|
+
});
|
|
100
|
+
const { client, teardown } = await connect(registerWidget);
|
|
101
|
+
const { resources } = await client.listResources();
|
|
102
|
+
const listed = resources.find((r) => r.uri.startsWith("ui://views/ext-apps/widget.html"));
|
|
103
|
+
// Production advertises a content-derived cache key.
|
|
104
|
+
expect(listed?.uri).toMatch(/^ui:\/\/views\/ext-apps\/widget\.html\?v=[0-9a-f]{8}$/);
|
|
105
|
+
// A consumer holding a stale key still resolves the current asset.
|
|
106
|
+
const staleUri = "ui://views/ext-apps/widget.html?v=00000000";
|
|
107
|
+
const { contents } = await client.readResource({ uri: staleUri });
|
|
108
|
+
expect(contents).toHaveLength(1);
|
|
109
|
+
const content = textContent(contents);
|
|
110
|
+
expect(typeof content.text).toBe("string");
|
|
111
|
+
expect(content.uri).toBe(staleUri);
|
|
112
|
+
await teardown();
|
|
113
|
+
});
|
|
114
|
+
it("still throws for an unknown view path", async () => {
|
|
115
|
+
const { client, teardown } = await connect(registerWidget);
|
|
116
|
+
await expect(client.readResource({ uri: "ui://views/ext-apps/nope.html?v=1" })).rejects.toThrow();
|
|
117
|
+
await teardown();
|
|
118
|
+
});
|
|
119
|
+
// A proxy routing the request under a path prefix sends x-forwarded-prefix;
|
|
120
|
+
// the view's asset URLs must carry that prefix (and the forwarded origin) so
|
|
121
|
+
// they resolve back through the same routing. Per-request, so one process can
|
|
122
|
+
// serve many hosts/prefixes at once.
|
|
123
|
+
it("prefixes emitted asset URLs with x-forwarded-prefix (prod)", async () => {
|
|
124
|
+
process.env.NODE_ENV = "production";
|
|
125
|
+
__setBuildManifest({
|
|
126
|
+
"src/views/widget.tsx": {
|
|
127
|
+
isEntry: true,
|
|
128
|
+
name: "widget",
|
|
129
|
+
file: "assets/widget-ABC123.js",
|
|
130
|
+
},
|
|
131
|
+
"style.css": { file: "assets/style-XYZ.css" },
|
|
132
|
+
});
|
|
133
|
+
const { client, teardown } = await connectHttp(registerWidget, {
|
|
134
|
+
"x-forwarded-host": "foo.com",
|
|
135
|
+
"x-forwarded-proto": "https",
|
|
136
|
+
"x-forwarded-prefix": "/v1/canary",
|
|
137
|
+
});
|
|
138
|
+
try {
|
|
139
|
+
const { contents } = await client.readResource({
|
|
140
|
+
uri: "ui://views/ext-apps/widget.html",
|
|
141
|
+
});
|
|
142
|
+
const { text } = textContent(contents);
|
|
143
|
+
// Forwarded origin + prefix. (`assets/assets` is the existing layout: the
|
|
144
|
+
// manifest `file` already carries `assets/` and the template prepends
|
|
145
|
+
// `/assets/`; build.tsx's `_redirects` collapses it on the asset host.)
|
|
146
|
+
expect(text).toContain("https://foo.com/v1/canary/assets/assets/widget-ABC123.js");
|
|
147
|
+
expect(text).toContain("https://foo.com/v1/canary/assets/assets/style-XYZ.css");
|
|
148
|
+
// No unprefixed asset URL leaks through.
|
|
149
|
+
expect(text).not.toContain("foo.com/assets/");
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
await teardown();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
// Back-compat: older Skybridge advertised the view at `ui://views/apps-sdk/...`
|
|
156
|
+
// via openai/outputTemplate. We no longer advertise it, but apps published then
|
|
157
|
+
// have it cached, so the read must still resolve to the ext-apps content.
|
|
158
|
+
it("resolves the legacy apps-sdk URL to the ext-apps content", async () => {
|
|
159
|
+
const { client, teardown } = await connect(registerWidget);
|
|
160
|
+
const legacyUri = "ui://views/apps-sdk/widget.html";
|
|
161
|
+
const { contents } = await client.readResource({ uri: legacyUri });
|
|
162
|
+
expect(contents).toHaveLength(1);
|
|
163
|
+
const content = contents[0];
|
|
164
|
+
expect(content.mimeType).toBe("text/html;profile=mcp-app");
|
|
165
|
+
expect(content.text.length).toBeGreaterThan(0);
|
|
166
|
+
// The response echoes the requested (legacy) URI, never the canonical one.
|
|
167
|
+
expect(content.uri).toBe(legacyUri);
|
|
168
|
+
await teardown();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
//# sourceMappingURL=view-resource-resolution.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view-resource-resolution.test.js","sourceRoot":"","sources":["../../src/server/view-resource-resolution.test.ts"],"names":[],"mappings":"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","sourcesContent":["import http from \"node:http\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport { InMemoryTransport } from \"@modelcontextprotocol/sdk/inMemory.js\";\nimport type { RequestHandler } from \"express\";\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { __setBuildManifest, McpServer } from \"./index.js\";\n\nvi.mock(\"@skybridge/devtools\", () => ({\n devtoolsStaticServer: () =>\n ((_req: unknown, _res: unknown, next: () => void) =>\n next()) as RequestHandler,\n}));\nvi.mock(\"./viewsDevServer.js\", () => ({\n viewsDevServer: (_httpServer: unknown) =>\n ((_req: unknown, _res: unknown, next: () => void) =>\n next()) as RequestHandler,\n}));\n\n// Mirror what the Skybridge Vite plugin generates in `.skybridge/views.d.ts`:\n// narrow `ViewName` so `component: \"widget\"` typechecks against the registry.\ndeclare module \"./server.js\" {\n interface ViewNameRegistry {\n widget: true;\n }\n}\n\n// SKY-435: a view resource must resolve no matter the `?v=` query param value.\n// The version param is a content-derived cache key for external consumers\n// (it busts host/CDN caches when the bundle changes); it is not part of the\n// resource's identity, so a stale, absent, or arbitrary param must still serve\n// the underlying asset.\n\nasync function connect(register: (server: McpServer) => void) {\n const server = new McpServer({ name: \"test\", version: \"1.0.0\" });\n register(server);\n const client = new Client({ name: \"test-client\", version: \"1.0.0\" });\n const [clientTransport, serverTransport] =\n InMemoryTransport.createLinkedPair();\n await server.connect(serverTransport);\n await client.connect(clientTransport);\n return {\n client,\n teardown: async () => {\n await client.close();\n await server.close();\n },\n };\n}\n\nlet openHttpServer: http.Server | undefined;\nafterEach(() => openHttpServer?.close());\n\n// Connect over real HTTP so forwarded proxy headers reach the resource handler.\nasync function connectHttp(\n register: (server: McpServer) => void,\n headers: Record<string, string>,\n) {\n const { createApp } = await import(\"./express.js\");\n const server = new McpServer({ name: \"test\", version: \"1.0.0\" });\n register(server);\n const httpServer = http.createServer();\n await createApp({ mcpServer: server, httpServer });\n const listening = http.createServer(server.express);\n await new Promise<void>((resolve) => listening.listen(0, resolve));\n openHttpServer = listening;\n const port = (listening.address() as { port: number }).port;\n\n const client = new Client({ name: \"test-client\", version: \"1.0.0\" });\n const transport = new StreamableHTTPClientTransport(\n new URL(`http://localhost:${port}/mcp`),\n { requestInit: { headers } },\n );\n await client.connect(transport);\n return {\n client,\n teardown: async () => {\n await client.close();\n await server.close();\n },\n };\n}\n\nfunction registerWidget(server: McpServer) {\n server.registerTool(\n { name: \"show\", description: \"show\", view: { component: \"widget\" } },\n () => ({ content: [{ type: \"text\" as const, text: \"ok\" }] }),\n );\n}\n\n/** Narrow a read-resource content entry to its text variant. */\nfunction textContent(contents: Array<{ uri: string }>): {\n uri: string;\n text: string;\n} {\n return contents[0] as { uri: string; text: string };\n}\n\ndescribe(\"view resource resolution (cache key)\", () => {\n afterEach(() => {\n delete process.env.NODE_ENV;\n });\n\n it(\"resolves regardless of the ?v= param and echoes the requested URI\", async () => {\n const { client, teardown } = await connect(registerWidget);\n\n const { resources } = await client.listResources();\n const listed = resources.find((r) =>\n r.uri.startsWith(\"ui://views/ext-apps/widget.html\"),\n );\n // Dev build advertises no cache key.\n expect(listed?.uri).toBe(\"ui://views/ext-apps/widget.html\");\n\n // A stale/arbitrary/absent param all resolve the same underlying asset,\n // and the response echoes the URI the consumer asked for (never rewritten).\n for (const uri of [\n \"ui://views/ext-apps/widget.html?v=stale123\",\n \"ui://views/ext-apps/widget.html?v=whatever-else\",\n \"ui://views/ext-apps/widget.html\",\n ]) {\n const { contents } = await client.readResource({ uri });\n expect(contents).toHaveLength(1);\n const content = textContent(contents);\n expect(typeof content.text).toBe(\"string\");\n expect(content.text.length).toBeGreaterThan(0);\n expect(content.uri).toBe(uri);\n }\n\n await teardown();\n });\n\n it(\"resolves a stale cache key when the canonical URI carries ?v= (prod)\", async () => {\n process.env.NODE_ENV = \"production\";\n __setBuildManifest({\n \"src/views/widget.tsx\": {\n isEntry: true,\n name: \"widget\",\n file: \"assets/widget-ABC123.js\",\n },\n \"style.css\": { file: \"assets/style-XYZ.css\" },\n } as unknown as Record<string, { file: string }>);\n\n const { client, teardown } = await connect(registerWidget);\n\n const { resources } = await client.listResources();\n const listed = resources.find((r) =>\n r.uri.startsWith(\"ui://views/ext-apps/widget.html\"),\n );\n // Production advertises a content-derived cache key.\n expect(listed?.uri).toMatch(\n /^ui:\\/\\/views\\/ext-apps\\/widget\\.html\\?v=[0-9a-f]{8}$/,\n );\n\n // A consumer holding a stale key still resolves the current asset.\n const staleUri = \"ui://views/ext-apps/widget.html?v=00000000\";\n const { contents } = await client.readResource({ uri: staleUri });\n expect(contents).toHaveLength(1);\n const content = textContent(contents);\n expect(typeof content.text).toBe(\"string\");\n expect(content.uri).toBe(staleUri);\n\n await teardown();\n });\n\n it(\"still throws for an unknown view path\", async () => {\n const { client, teardown } = await connect(registerWidget);\n\n await expect(\n client.readResource({ uri: \"ui://views/ext-apps/nope.html?v=1\" }),\n ).rejects.toThrow();\n\n await teardown();\n });\n\n // A proxy routing the request under a path prefix sends x-forwarded-prefix;\n // the view's asset URLs must carry that prefix (and the forwarded origin) so\n // they resolve back through the same routing. Per-request, so one process can\n // serve many hosts/prefixes at once.\n it(\"prefixes emitted asset URLs with x-forwarded-prefix (prod)\", async () => {\n process.env.NODE_ENV = \"production\";\n __setBuildManifest({\n \"src/views/widget.tsx\": {\n isEntry: true,\n name: \"widget\",\n file: \"assets/widget-ABC123.js\",\n },\n \"style.css\": { file: \"assets/style-XYZ.css\" },\n } as unknown as Record<string, { file: string }>);\n\n const { client, teardown } = await connectHttp(registerWidget, {\n \"x-forwarded-host\": \"foo.com\",\n \"x-forwarded-proto\": \"https\",\n \"x-forwarded-prefix\": \"/v1/canary\",\n });\n try {\n const { contents } = await client.readResource({\n uri: \"ui://views/ext-apps/widget.html\",\n });\n const { text } = textContent(contents);\n // Forwarded origin + prefix. (`assets/assets` is the existing layout: the\n // manifest `file` already carries `assets/` and the template prepends\n // `/assets/`; build.tsx's `_redirects` collapses it on the asset host.)\n expect(text).toContain(\n \"https://foo.com/v1/canary/assets/assets/widget-ABC123.js\",\n );\n expect(text).toContain(\n \"https://foo.com/v1/canary/assets/assets/style-XYZ.css\",\n );\n // No unprefixed asset URL leaks through.\n expect(text).not.toContain(\"foo.com/assets/\");\n } finally {\n await teardown();\n }\n });\n\n // Back-compat: older Skybridge advertised the view at `ui://views/apps-sdk/...`\n // via openai/outputTemplate. We no longer advertise it, but apps published then\n // have it cached, so the read must still resolve to the ext-apps content.\n it(\"resolves the legacy apps-sdk URL to the ext-apps content\", async () => {\n const { client, teardown } = await connect(registerWidget);\n\n const legacyUri = \"ui://views/apps-sdk/widget.html\";\n const { contents } = await client.readResource({ uri: legacyUri });\n\n expect(contents).toHaveLength(1);\n const content = contents[0] as {\n uri: string;\n text: string;\n mimeType: string;\n };\n expect(content.mimeType).toBe(\"text/html;profile=mcp-app\");\n expect(content.text.length).toBeGreaterThan(0);\n // The response echoes the requested (legacy) URI, never the canonical one.\n expect(content.uri).toBe(legacyUri);\n\n await teardown();\n });\n});\n"]}
|
|
@@ -19,7 +19,7 @@ export const viewsDevServer = async (httpServer) => {
|
|
|
19
19
|
const root = process.cwd();
|
|
20
20
|
const configFile = path.join(root, "vite.config.ts");
|
|
21
21
|
const configResult = await loadConfigFromFile({ command: "serve", mode: "development" }, configFile, root);
|
|
22
|
-
const { build, preview, plugins: userPlugins = [], ...devConfig } = configResult?.config || {};
|
|
22
|
+
const { build, preview, plugins: userPlugins = [], server: userServer, ...devConfig } = configResult?.config || {};
|
|
23
23
|
const vite = await createServer({
|
|
24
24
|
...devConfig,
|
|
25
25
|
// Pass `false` so Vite skips re-resolving a config file — we already
|
|
@@ -27,6 +27,9 @@ export const viewsDevServer = async (httpServer) => {
|
|
|
27
27
|
configFile: false,
|
|
28
28
|
appType: "custom",
|
|
29
29
|
server: {
|
|
30
|
+
// Keep the user's server options (e.g. forwardConsole) but force the
|
|
31
|
+
// three settings skybridge's Express integration requires.
|
|
32
|
+
...userServer,
|
|
30
33
|
allowedHosts: true,
|
|
31
34
|
middlewareMode: true,
|
|
32
35
|
hmr: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"viewsDevServer.js","sourceRoot":"","sources":["../../src/server/viewsDevServer.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,OAAO,EAAE,EAAe,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,2BAA2B,EAAE,MAAM,sCAAsC,CAAC;AAEnF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EACjC,UAAuB,EACN,EAAE;IACnB,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAEhC,MAAM,EAAE,YAAY,EAAE,kBAAkB,EAAE,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC;IAElE,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAC3B,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IAErD,MAAM,YAAY,GAAG,MAAM,kBAAkB,CAC3C,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,EACzC,UAAU,EACV,IAAI,CACL,CAAC;IAEF,MAAM,EACJ,KAAK,EACL,OAAO,EACP,OAAO,EAAE,WAAW,GAAG,EAAE,EACzB,GAAG,SAAS,EACb,GAAG,YAAY,EAAE,MAAM,IAAI,EAAE,CAAC;IAE/B,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC;QAC9B,GAAG,SAAS;QACZ,qEAAqE;QACrE,6CAA6C;QAC7C,UAAU,EAAE,KAAK;QACjB,OAAO,EAAE,QAAQ;QACjB,MAAM,EAAE;YACN,YAAY,EAAE,IAAI;YAClB,cAAc,EAAE,IAAI;YACpB,GAAG,EAAE;gBACH,MAAM,EAAE,UAAU;aACnB;SACF;QACD,IAAI;QACJ,uEAAuE;QACvE,kDAAkD;QAClD,OAAO,EAAE,CAAC,GAAG,WAAW,EAAE,2BAA2B,EAAE,CAAC;KACzD,CAAC,CAAC;IAEH,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IACnB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IAElC,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC","sourcesContent":["import type http from \"node:http\";\nimport path from \"node:path\";\nimport cors from \"cors\";\nimport express, { type Router } from \"express\";\nimport { assetBaseUrlTransformPlugin } from \"./asset-base-url-transform-plugin.js\";\n\n/**\n * Vite dev-server middleware for view assets.\n *\n * MUST be mounted at the Express app root so Vite can intercept\n * `/@vite/client`, `/@react-refresh`, and `/_skybridge/view/...` imports:\n *\n * const app = express();\n * if (env.NODE_ENV !== \"production\") {\n * app.use(await viewsDevServer(httpServer));\n * }\n */\nexport const viewsDevServer = async (\n httpServer: http.Server,\n): Promise<Router> => {\n const router = express.Router();\n\n const { createServer, loadConfigFromFile } = await import(\"vite\");\n\n const root = process.cwd();\n const configFile = path.join(root, \"vite.config.ts\");\n\n const configResult = await loadConfigFromFile(\n { command: \"serve\", mode: \"development\" },\n configFile,\n root,\n );\n\n const {\n build,\n preview,\n plugins: userPlugins = [],\n ...devConfig\n } = configResult?.config || {};\n\n const vite = await createServer({\n ...devConfig,\n // Pass `false` so Vite skips re-resolving a config file — we already\n // loaded and spread the user's config above.\n configFile: false,\n appType: \"custom\",\n server: {\n allowedHosts: true,\n middlewareMode: true,\n hmr: {\n server: httpServer,\n },\n },\n root,\n // optimizeDeps is set by the skybridge Vite plugin (entries + include)\n // so it can derive the view glob from `viewsDir`.\n plugins: [...userPlugins, assetBaseUrlTransformPlugin()],\n });\n\n router.use(cors());\n router.use(\"/\", vite.middlewares);\n\n return router;\n};\n"]}
|
|
1
|
+
{"version":3,"file":"viewsDevServer.js","sourceRoot":"","sources":["../../src/server/viewsDevServer.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,OAAO,EAAE,EAAe,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,2BAA2B,EAAE,MAAM,sCAAsC,CAAC;AAEnF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EACjC,UAAuB,EACN,EAAE;IACnB,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAEhC,MAAM,EAAE,YAAY,EAAE,kBAAkB,EAAE,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC;IAElE,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAC3B,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC;IAErD,MAAM,YAAY,GAAG,MAAM,kBAAkB,CAC3C,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,EACzC,UAAU,EACV,IAAI,CACL,CAAC;IAEF,MAAM,EACJ,KAAK,EACL,OAAO,EACP,OAAO,EAAE,WAAW,GAAG,EAAE,EACzB,MAAM,EAAE,UAAU,EAClB,GAAG,SAAS,EACb,GAAG,YAAY,EAAE,MAAM,IAAI,EAAE,CAAC;IAE/B,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC;QAC9B,GAAG,SAAS;QACZ,qEAAqE;QACrE,6CAA6C;QAC7C,UAAU,EAAE,KAAK;QACjB,OAAO,EAAE,QAAQ;QACjB,MAAM,EAAE;YACN,qEAAqE;YACrE,2DAA2D;YAC3D,GAAG,UAAU;YACb,YAAY,EAAE,IAAI;YAClB,cAAc,EAAE,IAAI;YACpB,GAAG,EAAE;gBACH,MAAM,EAAE,UAAU;aACnB;SACF;QACD,IAAI;QACJ,uEAAuE;QACvE,kDAAkD;QAClD,OAAO,EAAE,CAAC,GAAG,WAAW,EAAE,2BAA2B,EAAE,CAAC;KACzD,CAAC,CAAC;IAEH,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IACnB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IAElC,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC","sourcesContent":["import type http from \"node:http\";\nimport path from \"node:path\";\nimport cors from \"cors\";\nimport express, { type Router } from \"express\";\nimport { assetBaseUrlTransformPlugin } from \"./asset-base-url-transform-plugin.js\";\n\n/**\n * Vite dev-server middleware for view assets.\n *\n * MUST be mounted at the Express app root so Vite can intercept\n * `/@vite/client`, `/@react-refresh`, and `/_skybridge/view/...` imports:\n *\n * const app = express();\n * if (env.NODE_ENV !== \"production\") {\n * app.use(await viewsDevServer(httpServer));\n * }\n */\nexport const viewsDevServer = async (\n httpServer: http.Server,\n): Promise<Router> => {\n const router = express.Router();\n\n const { createServer, loadConfigFromFile } = await import(\"vite\");\n\n const root = process.cwd();\n const configFile = path.join(root, \"vite.config.ts\");\n\n const configResult = await loadConfigFromFile(\n { command: \"serve\", mode: \"development\" },\n configFile,\n root,\n );\n\n const {\n build,\n preview,\n plugins: userPlugins = [],\n server: userServer,\n ...devConfig\n } = configResult?.config || {};\n\n const vite = await createServer({\n ...devConfig,\n // Pass `false` so Vite skips re-resolving a config file — we already\n // loaded and spread the user's config above.\n configFile: false,\n appType: \"custom\",\n server: {\n // Keep the user's server options (e.g. forwardConsole) but force the\n // three settings skybridge's Express integration requires.\n ...userServer,\n allowedHosts: true,\n middlewareMode: true,\n hmr: {\n server: httpServer,\n },\n },\n root,\n // optimizeDeps is set by the skybridge Vite plugin (entries + include)\n // so it can derive the view glob from `viewsDir`.\n plugins: [...userPlugins, assetBaseUrlTransformPlugin()],\n });\n\n router.use(cors());\n router.use(\"/\", vite.middlewares);\n\n return router;\n};\n"]}
|
package/dist/test/utils.d.ts
CHANGED
|
@@ -91,7 +91,7 @@ export declare function createTestServer(): McpServer<Record<never, import("../s
|
|
|
91
91
|
processedAt: number;
|
|
92
92
|
region: string;
|
|
93
93
|
}>;
|
|
94
|
-
}>;
|
|
94
|
+
}, import("../server/auth.js").ExtraClaims>;
|
|
95
95
|
export declare function createMinimalTestServer(): McpServer<Record<never, import("../server/server.js").ToolDef<unknown, unknown, unknown>> & {
|
|
96
96
|
"search-trip": import("../server/server.js").ToolDef<{
|
|
97
97
|
destination: string;
|
|
@@ -100,7 +100,7 @@ export declare function createMinimalTestServer(): McpServer<Record<never, impor
|
|
|
100
100
|
id: string;
|
|
101
101
|
}[];
|
|
102
102
|
}, unknown>;
|
|
103
|
-
}>;
|
|
103
|
+
}, import("../server/auth.js").ExtraClaims>;
|
|
104
104
|
export declare function createInterfaceTestServer(): McpServer<Record<never, import("../server/server.js").ToolDef<unknown, unknown, unknown>> & {
|
|
105
105
|
"interface-view": import("../server/server.js").ToolDef<{
|
|
106
106
|
id: string;
|
|
@@ -111,7 +111,7 @@ export declare function createInterfaceTestServer(): McpServer<Record<never, imp
|
|
|
111
111
|
processedBy: string;
|
|
112
112
|
version: number;
|
|
113
113
|
}>;
|
|
114
|
-
}>;
|
|
114
|
+
}, import("../server/auth.js").ExtraClaims>;
|
|
115
115
|
export declare function createMockExtra(host: string, options?: {
|
|
116
116
|
headers?: Record<string, string | string[]>;
|
|
117
117
|
url?: URL | string;
|
package/dist/test/view.test.js
CHANGED
|
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
3
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi, } from "vitest";
|
|
5
|
+
import { TOOL_OUTPUT_WARNING_TOKENS } from "../context-warnings.js";
|
|
5
6
|
import { McpServer as McpServerClass } from "../server/server.js";
|
|
6
7
|
import { createMockExtra, createMockMcpServer, resetTestEnv, setTestEnv, } from "./utils.js";
|
|
7
8
|
const mockManifest = {
|
|
@@ -92,6 +93,7 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
92
93
|
domain: serverUrl,
|
|
93
94
|
description: "Test view",
|
|
94
95
|
},
|
|
96
|
+
"openai/widgetDescription": "Test view",
|
|
95
97
|
},
|
|
96
98
|
},
|
|
97
99
|
],
|
|
@@ -134,6 +136,7 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
134
136
|
domain: serverUrl,
|
|
135
137
|
description: "Test view",
|
|
136
138
|
},
|
|
139
|
+
"openai/widgetDescription": "Test view",
|
|
137
140
|
},
|
|
138
141
|
},
|
|
139
142
|
],
|
|
@@ -176,6 +179,7 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
176
179
|
description: "Test view",
|
|
177
180
|
domain: expectedDomain,
|
|
178
181
|
},
|
|
182
|
+
"openai/widgetDescription": "Test view",
|
|
179
183
|
});
|
|
180
184
|
});
|
|
181
185
|
it("should register a single ext-apps resource", async () => {
|
|
@@ -212,13 +216,14 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
212
216
|
description: "Test view",
|
|
213
217
|
prefersBorder: true,
|
|
214
218
|
},
|
|
219
|
+
"openai/widgetDescription": "Test view",
|
|
215
220
|
},
|
|
216
221
|
},
|
|
217
222
|
],
|
|
218
223
|
});
|
|
219
224
|
expect(result.contents[0]?.text).toContain('window.skybridge = { hostType: "mcp-app", serverUrl: "http://localhost:3000" };');
|
|
220
225
|
});
|
|
221
|
-
it("should advertise the single resource via ui.resourceUri
|
|
226
|
+
it("should advertise the single resource via ui.resourceUri only (no openai/outputTemplate)", async () => {
|
|
222
227
|
server.registerTool({
|
|
223
228
|
name: "my-view",
|
|
224
229
|
description: "Test tool",
|
|
@@ -228,7 +233,7 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
228
233
|
const uri = "ui://views/ext-apps/my-view.html";
|
|
229
234
|
expect(toolConfig._meta?.ui?.resourceUri).toBe(uri);
|
|
230
235
|
expect(toolConfig._meta?.["ui/resourceUri"]).toBe(uri);
|
|
231
|
-
expect(toolConfig._meta?.["openai/outputTemplate"]).
|
|
236
|
+
expect(toolConfig._meta?.["openai/outputTemplate"]).toBeUndefined();
|
|
232
237
|
});
|
|
233
238
|
it("treats the deprecated hosts option as a no-op (always the single resource)", async () => {
|
|
234
239
|
server.registerTool({
|
|
@@ -243,7 +248,7 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
243
248
|
expect(mockRegisterResource).toHaveBeenCalledTimes(1);
|
|
244
249
|
const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
|
|
245
250
|
expect(toolConfig._meta?.ui?.resourceUri).toBe("ui://views/ext-apps/my-view.html");
|
|
246
|
-
expect(toolConfig._meta?.["openai/outputTemplate"]).
|
|
251
|
+
expect(toolConfig._meta?.["openai/outputTemplate"]).toBeUndefined();
|
|
247
252
|
});
|
|
248
253
|
it("should not version view URIs in development", () => {
|
|
249
254
|
server.registerTool({
|
|
@@ -252,10 +257,10 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
252
257
|
view: { component: "my-view", description: "Test view" },
|
|
253
258
|
}, vi.fn());
|
|
254
259
|
const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
|
|
255
|
-
expect(toolConfig._meta?.["openai/outputTemplate"]).
|
|
260
|
+
expect(toolConfig._meta?.["openai/outputTemplate"]).toBeUndefined();
|
|
256
261
|
expect(toolConfig._meta?.ui?.resourceUri).toBe("ui://views/ext-apps/my-view.html");
|
|
257
|
-
// The URI registered with the resource handler must match
|
|
258
|
-
//
|
|
262
|
+
// The URI registered with the resource handler must match ui.resourceUri
|
|
263
|
+
// exactly so the SDK can resolve `resources/read` requests.
|
|
259
264
|
expect(mockRegisterResource.mock.calls[0]?.[1]).toBe("ui://views/ext-apps/my-view.html");
|
|
260
265
|
});
|
|
261
266
|
it("should append a stable content hash to view URIs in production", () => {
|
|
@@ -267,7 +272,7 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
267
272
|
}, vi.fn());
|
|
268
273
|
const expected = expectedVersionParam("assets/my-view-abc123.js", "style.css");
|
|
269
274
|
const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
|
|
270
|
-
expect(toolConfig._meta?.["openai/outputTemplate"]).
|
|
275
|
+
expect(toolConfig._meta?.["openai/outputTemplate"]).toBeUndefined();
|
|
271
276
|
expect(toolConfig._meta?.ui?.resourceUri).toBe(`ui://views/ext-apps/my-view.html${expected}`);
|
|
272
277
|
expect(mockRegisterResource.mock.calls[0]?.[1]).toBe(`ui://views/ext-apps/my-view.html${expected}`);
|
|
273
278
|
});
|
|
@@ -283,8 +288,8 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
283
288
|
description: "Second tool",
|
|
284
289
|
view: { component: "folder-view" },
|
|
285
290
|
}, vi.fn());
|
|
286
|
-
const myviewTemplate =
|
|
287
|
-
const folderviewTemplate =
|
|
291
|
+
const myviewTemplate = mockRegisterTool.mock.calls[0][1]._meta?.ui?.resourceUri;
|
|
292
|
+
const folderviewTemplate = mockRegisterTool.mock.calls[1][1]._meta?.ui?.resourceUri;
|
|
288
293
|
expect(myviewTemplate).not.toEqual(folderviewTemplate);
|
|
289
294
|
expect(myviewTemplate).toMatch(/\?v=[0-9a-f]{8}$/);
|
|
290
295
|
expect(folderviewTemplate).toMatch(/\?v=[0-9a-f]{8}$/);
|
|
@@ -297,7 +302,7 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
297
302
|
view: { component: "unknown-view" },
|
|
298
303
|
}, vi.fn());
|
|
299
304
|
const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
|
|
300
|
-
expect(toolConfig._meta?.
|
|
305
|
+
expect(toolConfig._meta?.ui?.resourceUri).toBe("ui://views/ext-apps/unknown-view.html");
|
|
301
306
|
});
|
|
302
307
|
it("should inject viewUUID into _meta of tool callback results", async () => {
|
|
303
308
|
const mockToolCallback = vi.fn().mockResolvedValue({
|
|
@@ -317,6 +322,21 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
317
322
|
expect(typeof result._meta?.viewUUID).toBe("string");
|
|
318
323
|
expect(result._meta?.viewUUID).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
|
|
319
324
|
});
|
|
325
|
+
it("warns when a tool callback returns large model-visible output", async () => {
|
|
326
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
327
|
+
server.registerTool({ name: "large-output", description: "Test tool" }, vi.fn().mockResolvedValue({
|
|
328
|
+
content: [
|
|
329
|
+
{
|
|
330
|
+
type: "text",
|
|
331
|
+
text: "x".repeat(TOOL_OUTPUT_WARNING_TOKENS * 4),
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
}));
|
|
335
|
+
const wrappedCallback = mockRegisterTool.mock.calls[0]?.[2];
|
|
336
|
+
await wrappedCallback({}, {});
|
|
337
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Tool "large-output" returned'));
|
|
338
|
+
warnSpy.mockRestore();
|
|
339
|
+
});
|
|
320
340
|
it("should preserve existing _meta when injecting viewUUID", async () => {
|
|
321
341
|
const mockToolCallback = vi.fn().mockResolvedValue({
|
|
322
342
|
content: [{ type: "text", text: "result" }],
|
|
@@ -517,10 +537,16 @@ describe("resources/list view _meta injection", () => {
|
|
|
517
537
|
const extApps = resources.find((r) => r.uri.includes("ext-apps"));
|
|
518
538
|
expect(appsSdk).toBeUndefined();
|
|
519
539
|
expect(extApps?._meta).toBeDefined();
|
|
520
|
-
const extUi =
|
|
540
|
+
const extUi = extApps._meta?.ui;
|
|
521
541
|
expect(extUi?.csp?.connectDomains?.length).toBeGreaterThan(0);
|
|
522
542
|
expect(extUi?.csp?.resourceDomains).toContain("https://fonts.googleapis.com");
|
|
523
543
|
expect(extUi?.domain).toBe("skybridge.tech");
|
|
544
|
+
const meta = extApps?._meta;
|
|
545
|
+
expect(meta["openai/widgetDescription"]).toBe("Onboarding deck");
|
|
546
|
+
expect(meta["openai/widgetCSP"]?.redirect_domains).toEqual([
|
|
547
|
+
"https://docs.skybridge.tech",
|
|
548
|
+
]);
|
|
549
|
+
expect(meta.ui?.csp).not.toHaveProperty("redirectDomains");
|
|
524
550
|
});
|
|
525
551
|
});
|
|
526
552
|
//# sourceMappingURL=view.test.js.map
|