skybridge 1.1.1 → 1.2.0
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 +9 -0
- package/dist/commands/dev.js +1 -3
- package/dist/commands/dev.js.map +1 -1
- 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 +18 -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 +18 -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 +48 -0
- package/dist/server/auth/providers/auth0.test.js.map +1 -0
- package/dist/server/auth/providers/clerk.d.ts +14 -0
- package/dist/server/auth/providers/clerk.js +16 -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 +28 -0
- package/dist/server/auth/providers/clerk.test.js.map +1 -0
- package/dist/server/auth/providers/custom.d.ts +24 -0
- package/dist/server/auth/providers/custom.js +37 -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 +107 -0
- package/dist/server/auth/providers/custom.test.js.map +1 -0
- package/dist/server/auth/providers/descope.d.ts +15 -0
- package/dist/server/auth/providers/descope.js +33 -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 +37 -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 +12 -0
- package/dist/server/auth/providers/stytch.js +13 -0
- package/dist/server/auth/providers/stytch.js.map +1 -0
- package/dist/server/auth/providers/workos.d.ts +11 -0
- package/dist/server/auth/providers/workos.js +12 -0
- package/dist/server/auth/providers/workos.js.map +1 -0
- package/dist/server/auth/setup.d.ts +4 -0
- package/dist/server/auth/setup.js +51 -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 +185 -0
- package/dist/server/auth/setup.test.js.map +1 -0
- package/dist/server/auth/verify.d.ts +12 -0
- package/dist/server/auth/verify.js +38 -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 +100 -0
- package/dist/server/auth/verify.test.js.map +1 -0
- package/dist/server/express.test.js +31 -0
- package/dist/server/express.test.js.map +1 -1
- package/dist/server/index.d.ts +8 -1
- package/dist/server/index.js +6 -0
- package/dist/server/index.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 +30 -7
- package/dist/server/server.js +77 -123
- package/dist/server/server.js.map +1 -1
- package/dist/server/view-resource-resolution.test.js +24 -9
- package/dist/server/view-resource-resolution.test.js.map +1 -1
- package/dist/test/view.test.js +69 -101
- package/dist/test/view.test.js.map +1 -1
- package/dist/web/bridges/adaptor.d.ts +51 -0
- package/dist/web/bridges/adaptor.js +310 -0
- package/dist/web/bridges/adaptor.js.map +1 -0
- package/dist/web/bridges/adaptor.test.d.ts +1 -0
- package/dist/web/bridges/adaptor.test.js +197 -0
- package/dist/web/bridges/adaptor.test.js.map +1 -0
- package/dist/web/bridges/apps-sdk/bridge.d.ts +6 -2
- package/dist/web/bridges/apps-sdk/bridge.js +15 -4
- package/dist/web/bridges/apps-sdk/bridge.js.map +1 -1
- package/dist/web/bridges/apps-sdk/index.d.ts +0 -1
- package/dist/web/bridges/apps-sdk/index.js +0 -1
- package/dist/web/bridges/apps-sdk/index.js.map +1 -1
- package/dist/web/bridges/get-adaptor.d.ts +4 -4
- package/dist/web/bridges/get-adaptor.js +6 -11
- package/dist/web/bridges/get-adaptor.js.map +1 -1
- package/dist/web/bridges/get-adaptor.test.d.ts +1 -0
- package/dist/web/bridges/get-adaptor.test.js +32 -0
- package/dist/web/bridges/get-adaptor.test.js.map +1 -0
- package/dist/web/bridges/mcp-app/bridge.d.ts +13 -2
- package/dist/web/bridges/mcp-app/bridge.js +46 -4
- package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
- package/dist/web/bridges/mcp-app/bridge.test.d.ts +1 -0
- package/dist/web/bridges/mcp-app/bridge.test.js +17 -0
- package/dist/web/bridges/mcp-app/bridge.test.js.map +1 -0
- package/dist/web/bridges/mcp-app/index.d.ts +0 -1
- package/dist/web/bridges/mcp-app/index.js +0 -1
- package/dist/web/bridges/mcp-app/index.js.map +1 -1
- package/dist/web/bridges/mcp-app/view-tools.test.js +5 -6
- package/dist/web/bridges/mcp-app/view-tools.test.js.map +1 -1
- package/dist/web/bridges/types.d.ts +10 -0
- package/dist/web/bridges/types.js +14 -1
- package/dist/web/bridges/types.js.map +1 -1
- package/dist/web/bridges/types.test.d.ts +1 -0
- package/dist/web/bridges/types.test.js +19 -0
- package/dist/web/bridges/types.test.js.map +1 -0
- package/dist/web/components/modal-provider.js +4 -3
- package/dist/web/components/modal-provider.js.map +1 -1
- package/dist/web/create-store.test.js +8 -5
- package/dist/web/create-store.test.js.map +1 -1
- package/dist/web/data-llm.test.js +9 -5
- package/dist/web/data-llm.test.js.map +1 -1
- package/dist/web/hooks/use-call-tool.test.js +37 -23
- package/dist/web/hooks/use-call-tool.test.js.map +1 -1
- package/dist/web/hooks/use-display-mode.test.js +56 -20
- package/dist/web/hooks/use-display-mode.test.js.map +1 -1
- package/dist/web/hooks/use-download.test.js +11 -3
- package/dist/web/hooks/use-download.test.js.map +1 -1
- package/dist/web/hooks/use-files.test.js +10 -2
- package/dist/web/hooks/use-files.test.js.map +1 -1
- package/dist/web/hooks/use-layout.test.js +59 -26
- package/dist/web/hooks/use-layout.test.js.map +1 -1
- package/dist/web/hooks/use-open-external.test.js +13 -3
- package/dist/web/hooks/use-open-external.test.js.map +1 -1
- package/dist/web/hooks/use-request-close.test.js +35 -40
- package/dist/web/hooks/use-request-close.test.js.map +1 -1
- package/dist/web/hooks/use-request-modal.test.js +10 -0
- package/dist/web/hooks/use-request-modal.test.js.map +1 -1
- package/dist/web/hooks/use-request-size.test.js +35 -53
- package/dist/web/hooks/use-request-size.test.js.map +1 -1
- package/dist/web/hooks/use-set-open-in-app-url.test.js +28 -8
- package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -1
- package/dist/web/hooks/use-tool-info.test.js +37 -27
- package/dist/web/hooks/use-tool-info.test.js.map +1 -1
- package/dist/web/hooks/use-user.test.js +81 -35
- package/dist/web/hooks/use-user.test.js.map +1 -1
- package/dist/web/hooks/use-view-state.test.js +6 -2
- package/dist/web/hooks/use-view-state.test.js.map +1 -1
- package/package.json +3 -2
- 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/apps-sdk/adaptor.d.ts +0 -29
- package/dist/web/bridges/apps-sdk/adaptor.js +0 -117
- package/dist/web/bridges/apps-sdk/adaptor.js.map +0 -1
- package/dist/web/bridges/mcp-app/adaptor.d.ts +0 -53
- package/dist/web/bridges/mcp-app/adaptor.js +0 -283
- package/dist/web/bridges/mcp-app/adaptor.js.map +0 -1
|
@@ -36,15 +36,15 @@ describe("view resource resolution (cache key)", () => {
|
|
|
36
36
|
it("resolves regardless of the ?v= param and echoes the requested URI", async () => {
|
|
37
37
|
const { client, teardown } = await connect(registerWidget);
|
|
38
38
|
const { resources } = await client.listResources();
|
|
39
|
-
const listed = resources.find((r) => r.uri.startsWith("ui://views/apps
|
|
39
|
+
const listed = resources.find((r) => r.uri.startsWith("ui://views/ext-apps/widget.html"));
|
|
40
40
|
// Dev build advertises no cache key.
|
|
41
|
-
expect(listed?.uri).toBe("ui://views/apps
|
|
41
|
+
expect(listed?.uri).toBe("ui://views/ext-apps/widget.html");
|
|
42
42
|
// A stale/arbitrary/absent param all resolve the same underlying asset,
|
|
43
43
|
// and the response echoes the URI the consumer asked for (never rewritten).
|
|
44
44
|
for (const uri of [
|
|
45
|
-
"ui://views/apps
|
|
46
|
-
"ui://views/apps
|
|
47
|
-
"ui://views/apps
|
|
45
|
+
"ui://views/ext-apps/widget.html?v=stale123",
|
|
46
|
+
"ui://views/ext-apps/widget.html?v=whatever-else",
|
|
47
|
+
"ui://views/ext-apps/widget.html",
|
|
48
48
|
]) {
|
|
49
49
|
const { contents } = await client.readResource({ uri });
|
|
50
50
|
expect(contents).toHaveLength(1);
|
|
@@ -67,11 +67,11 @@ describe("view resource resolution (cache key)", () => {
|
|
|
67
67
|
});
|
|
68
68
|
const { client, teardown } = await connect(registerWidget);
|
|
69
69
|
const { resources } = await client.listResources();
|
|
70
|
-
const listed = resources.find((r) => r.uri.startsWith("ui://views/apps
|
|
70
|
+
const listed = resources.find((r) => r.uri.startsWith("ui://views/ext-apps/widget.html"));
|
|
71
71
|
// Production advertises a content-derived cache key.
|
|
72
|
-
expect(listed?.uri).toMatch(/^ui:\/\/views\/apps
|
|
72
|
+
expect(listed?.uri).toMatch(/^ui:\/\/views\/ext-apps\/widget\.html\?v=[0-9a-f]{8}$/);
|
|
73
73
|
// A consumer holding a stale key still resolves the current asset.
|
|
74
|
-
const staleUri = "ui://views/apps
|
|
74
|
+
const staleUri = "ui://views/ext-apps/widget.html?v=00000000";
|
|
75
75
|
const { contents } = await client.readResource({ uri: staleUri });
|
|
76
76
|
expect(contents).toHaveLength(1);
|
|
77
77
|
const content = textContent(contents);
|
|
@@ -81,7 +81,22 @@ describe("view resource resolution (cache key)", () => {
|
|
|
81
81
|
});
|
|
82
82
|
it("still throws for an unknown view path", async () => {
|
|
83
83
|
const { client, teardown } = await connect(registerWidget);
|
|
84
|
-
await expect(client.readResource({ uri: "ui://views/apps
|
|
84
|
+
await expect(client.readResource({ uri: "ui://views/ext-apps/nope.html?v=1" })).rejects.toThrow();
|
|
85
|
+
await teardown();
|
|
86
|
+
});
|
|
87
|
+
// Back-compat: older Skybridge advertised the view at `ui://views/apps-sdk/...`
|
|
88
|
+
// via openai/outputTemplate. We no longer advertise it, but apps published then
|
|
89
|
+
// have it cached, so the read must still resolve to the ext-apps content.
|
|
90
|
+
it("resolves the legacy apps-sdk URL to the ext-apps content", async () => {
|
|
91
|
+
const { client, teardown } = await connect(registerWidget);
|
|
92
|
+
const legacyUri = "ui://views/apps-sdk/widget.html";
|
|
93
|
+
const { contents } = await client.readResource({ uri: legacyUri });
|
|
94
|
+
expect(contents).toHaveLength(1);
|
|
95
|
+
const content = contents[0];
|
|
96
|
+
expect(content.mimeType).toBe("text/html;profile=mcp-app");
|
|
97
|
+
expect(content.text.length).toBeGreaterThan(0);
|
|
98
|
+
// The response echoes the requested (legacy) URI, never the canonical one.
|
|
99
|
+
expect(content.uri).toBe(legacyUri);
|
|
85
100
|
await teardown();
|
|
86
101
|
});
|
|
87
102
|
});
|
|
@@ -1 +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 { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { InMemoryTransport } from \"@modelcontextprotocol/sdk/inMemory.js\";\nimport { afterEach, describe, expect, it } from \"vitest\";\nimport { __setBuildManifest, McpServer } from \"./index.js\";\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\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/apps-sdk/widget.html\"),\n );\n // Dev build advertises no cache key.\n expect(listed?.uri).toBe(\"ui://views/apps-sdk/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/apps-sdk/widget.html?v=stale123\",\n \"ui://views/apps-sdk/widget.html?v=whatever-else\",\n \"ui://views/apps-sdk/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/apps-sdk/widget.html\"),\n );\n // Production advertises a content-derived cache key.\n expect(listed?.uri).toMatch(\n /^ui:\\/\\/views\\/apps-sdk\\/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/apps-sdk/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/apps-sdk/nope.html?v=1\" }),\n ).rejects.toThrow();\n\n await teardown();\n });\n});\n"]}
|
|
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 { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { InMemoryTransport } from \"@modelcontextprotocol/sdk/inMemory.js\";\nimport { afterEach, describe, expect, it } from \"vitest\";\nimport { __setBuildManifest, McpServer } from \"./index.js\";\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\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 // 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"]}
|
package/dist/test/view.test.js
CHANGED
|
@@ -74,20 +74,24 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
74
74
|
const serverUrl = `http://${host}`;
|
|
75
75
|
const hmrUrl = `ws://${host}`;
|
|
76
76
|
const mockExtra = createMockExtra(host);
|
|
77
|
-
const result = await appsSdkResourceCallback(new URL("ui://views/apps
|
|
77
|
+
const result = await appsSdkResourceCallback(new URL("ui://views/ext-apps/my-view.html"), mockExtra);
|
|
78
78
|
expect(mockRegisterTool).toHaveBeenCalled();
|
|
79
79
|
expect(result).toEqual({
|
|
80
80
|
contents: [
|
|
81
81
|
{
|
|
82
|
-
uri: "ui://views/apps
|
|
83
|
-
mimeType: "text/html
|
|
82
|
+
uri: "ui://views/ext-apps/my-view.html",
|
|
83
|
+
mimeType: "text/html;profile=mcp-app",
|
|
84
84
|
text: expect.stringContaining('<div id="root"></div>'),
|
|
85
85
|
_meta: {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
ui: {
|
|
87
|
+
csp: {
|
|
88
|
+
resourceDomains: [serverUrl],
|
|
89
|
+
connectDomains: [serverUrl, hmrUrl],
|
|
90
|
+
baseUriDomains: [serverUrl],
|
|
91
|
+
},
|
|
92
|
+
domain: serverUrl,
|
|
93
|
+
description: "Test view",
|
|
89
94
|
},
|
|
90
|
-
"openai/widgetDomain": serverUrl,
|
|
91
95
|
"openai/widgetDescription": "Test view",
|
|
92
96
|
},
|
|
93
97
|
},
|
|
@@ -113,20 +117,24 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
113
117
|
const host = "myapp.com";
|
|
114
118
|
const serverUrl = `https://${host}`;
|
|
115
119
|
const mockExtra = createMockExtra(host);
|
|
116
|
-
const versionedUri = `ui://views/apps
|
|
120
|
+
const versionedUri = `ui://views/ext-apps/my-view.html${expectedVersionParam("assets/my-view-abc123.js", "style.css")}`;
|
|
117
121
|
const result = await appsSdkResourceCallback(new URL(versionedUri), mockExtra);
|
|
118
122
|
expect(result).toEqual({
|
|
119
123
|
contents: [
|
|
120
124
|
{
|
|
121
125
|
uri: versionedUri,
|
|
122
|
-
mimeType: "text/html
|
|
126
|
+
mimeType: "text/html;profile=mcp-app",
|
|
123
127
|
text: expect.stringContaining('<div id="root"></div>'),
|
|
124
128
|
_meta: {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
ui: {
|
|
130
|
+
csp: {
|
|
131
|
+
resourceDomains: [serverUrl],
|
|
132
|
+
connectDomains: [serverUrl],
|
|
133
|
+
baseUriDomains: [serverUrl],
|
|
134
|
+
},
|
|
135
|
+
domain: serverUrl,
|
|
136
|
+
description: "Test view",
|
|
128
137
|
},
|
|
129
|
-
"openai/widgetDomain": serverUrl,
|
|
130
138
|
"openai/widgetDescription": "Test view",
|
|
131
139
|
},
|
|
132
140
|
},
|
|
@@ -145,7 +153,7 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
145
153
|
view: { component: "my-view", description: "Test view" },
|
|
146
154
|
}, vi.fn());
|
|
147
155
|
const extAppsResourceCallback = mockRegisterResource.mock
|
|
148
|
-
.calls[
|
|
156
|
+
.calls[0]?.[3];
|
|
149
157
|
expect(extAppsResourceCallback).toBeDefined();
|
|
150
158
|
const forwardedUrl = "https://everything-3a2c1264.staging.alpic.live/mcp?foo=bar";
|
|
151
159
|
const expectedDomain = `${crypto
|
|
@@ -170,9 +178,10 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
170
178
|
description: "Test view",
|
|
171
179
|
domain: expectedDomain,
|
|
172
180
|
},
|
|
181
|
+
"openai/widgetDescription": "Test view",
|
|
173
182
|
});
|
|
174
183
|
});
|
|
175
|
-
it("should register
|
|
184
|
+
it("should register a single ext-apps resource", async () => {
|
|
176
185
|
server.registerTool({
|
|
177
186
|
name: "my-view",
|
|
178
187
|
description: "Test tool",
|
|
@@ -182,37 +191,14 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
182
191
|
prefersBorder: true,
|
|
183
192
|
},
|
|
184
193
|
}, vi.fn());
|
|
185
|
-
expect(mockRegisterResource).toHaveBeenCalledTimes(
|
|
186
|
-
const
|
|
194
|
+
expect(mockRegisterResource).toHaveBeenCalledTimes(1);
|
|
195
|
+
const resourceCallback = mockRegisterResource.mock
|
|
187
196
|
.calls[0]?.[3];
|
|
188
197
|
const host = "localhost:3000";
|
|
189
198
|
const serverUrl = `http://${host}`;
|
|
190
199
|
const hmrUrl = `ws://${host}`;
|
|
191
|
-
const
|
|
192
|
-
expect(
|
|
193
|
-
contents: [
|
|
194
|
-
{
|
|
195
|
-
uri: "ui://views/apps-sdk/my-view.html",
|
|
196
|
-
mimeType: "text/html+skybridge",
|
|
197
|
-
text: expect.stringContaining('<div id="root"></div>'),
|
|
198
|
-
_meta: {
|
|
199
|
-
"openai/widgetCSP": {
|
|
200
|
-
resource_domains: [serverUrl],
|
|
201
|
-
connect_domains: [serverUrl, hmrUrl],
|
|
202
|
-
},
|
|
203
|
-
"openai/widgetDomain": serverUrl,
|
|
204
|
-
"openai/widgetDescription": "Test view",
|
|
205
|
-
"openai/widgetPrefersBorder": true,
|
|
206
|
-
},
|
|
207
|
-
},
|
|
208
|
-
],
|
|
209
|
-
});
|
|
210
|
-
expect(appsSdkResult.contents[0]?.text).toContain('window.skybridge = { hostType: "apps-sdk", serverUrl: "http://localhost:3000" };');
|
|
211
|
-
const extAppsResourceCallback = mockRegisterResource.mock
|
|
212
|
-
.calls[1]?.[3];
|
|
213
|
-
expect(extAppsResourceCallback).toBeDefined();
|
|
214
|
-
const extAppsResult = await extAppsResourceCallback(new URL("ui://views/ext-apps/my-view.html"), createMockExtra(host));
|
|
215
|
-
expect(extAppsResult).toEqual({
|
|
200
|
+
const result = await resourceCallback(new URL("ui://views/ext-apps/my-view.html"), createMockExtra(host));
|
|
201
|
+
expect(result).toEqual({
|
|
216
202
|
contents: [
|
|
217
203
|
{
|
|
218
204
|
uri: "ui://views/ext-apps/my-view.html",
|
|
@@ -229,27 +215,26 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
229
215
|
description: "Test view",
|
|
230
216
|
prefersBorder: true,
|
|
231
217
|
},
|
|
218
|
+
"openai/widgetDescription": "Test view",
|
|
232
219
|
},
|
|
233
220
|
},
|
|
234
221
|
],
|
|
235
222
|
});
|
|
236
|
-
expect(
|
|
223
|
+
expect(result.contents[0]?.text).toContain('window.skybridge = { hostType: "mcp-app", serverUrl: "http://localhost:3000" };');
|
|
237
224
|
});
|
|
238
|
-
it("should
|
|
225
|
+
it("should advertise the single resource via ui.resourceUri only (no openai/outputTemplate)", async () => {
|
|
239
226
|
server.registerTool({
|
|
240
227
|
name: "my-view",
|
|
241
228
|
description: "Test tool",
|
|
242
229
|
view: { component: "my-view", description: "Test view" },
|
|
243
230
|
}, vi.fn());
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
expect(toolConfig._meta
|
|
248
|
-
expect(toolConfig._meta?.
|
|
249
|
-
resourceUri: "ui://views/ext-apps/my-view.html",
|
|
250
|
-
});
|
|
231
|
+
const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
|
|
232
|
+
const uri = "ui://views/ext-apps/my-view.html";
|
|
233
|
+
expect(toolConfig._meta?.ui?.resourceUri).toBe(uri);
|
|
234
|
+
expect(toolConfig._meta?.["ui/resourceUri"]).toBe(uri);
|
|
235
|
+
expect(toolConfig._meta?.["openai/outputTemplate"]).toBeUndefined();
|
|
251
236
|
});
|
|
252
|
-
it("
|
|
237
|
+
it("treats the deprecated hosts option as a no-op (always the single resource)", async () => {
|
|
253
238
|
server.registerTool({
|
|
254
239
|
name: "my-view",
|
|
255
240
|
description: "Test tool",
|
|
@@ -259,11 +244,10 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
259
244
|
hosts: ["apps-sdk"],
|
|
260
245
|
},
|
|
261
246
|
}, vi.fn());
|
|
262
|
-
expect(
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
expect(toolConfig._meta).
|
|
266
|
-
expect(toolConfig._meta?.["openai/outputTemplate"]).toBe("ui://views/apps-sdk/my-view.html");
|
|
247
|
+
expect(mockRegisterResource).toHaveBeenCalledTimes(1);
|
|
248
|
+
const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
|
|
249
|
+
expect(toolConfig._meta?.ui?.resourceUri).toBe("ui://views/ext-apps/my-view.html");
|
|
250
|
+
expect(toolConfig._meta?.["openai/outputTemplate"]).toBeUndefined();
|
|
267
251
|
});
|
|
268
252
|
it("should not version view URIs in development", () => {
|
|
269
253
|
server.registerTool({
|
|
@@ -272,12 +256,11 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
272
256
|
view: { component: "my-view", description: "Test view" },
|
|
273
257
|
}, vi.fn());
|
|
274
258
|
const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
|
|
275
|
-
expect(toolConfig._meta?.["openai/outputTemplate"]).
|
|
259
|
+
expect(toolConfig._meta?.["openai/outputTemplate"]).toBeUndefined();
|
|
276
260
|
expect(toolConfig._meta?.ui?.resourceUri).toBe("ui://views/ext-apps/my-view.html");
|
|
277
|
-
// The URI registered with the resource handler must match
|
|
278
|
-
//
|
|
279
|
-
expect(mockRegisterResource.mock.calls[0]?.[1]).toBe("ui://views/apps
|
|
280
|
-
expect(mockRegisterResource.mock.calls[1]?.[1]).toBe("ui://views/ext-apps/my-view.html");
|
|
261
|
+
// The URI registered with the resource handler must match ui.resourceUri
|
|
262
|
+
// exactly so the SDK can resolve `resources/read` requests.
|
|
263
|
+
expect(mockRegisterResource.mock.calls[0]?.[1]).toBe("ui://views/ext-apps/my-view.html");
|
|
281
264
|
});
|
|
282
265
|
it("should append a stable content hash to view URIs in production", () => {
|
|
283
266
|
setTestEnv({ NODE_ENV: "production" });
|
|
@@ -288,10 +271,9 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
288
271
|
}, vi.fn());
|
|
289
272
|
const expected = expectedVersionParam("assets/my-view-abc123.js", "style.css");
|
|
290
273
|
const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
|
|
291
|
-
expect(toolConfig._meta?.["openai/outputTemplate"]).
|
|
274
|
+
expect(toolConfig._meta?.["openai/outputTemplate"]).toBeUndefined();
|
|
292
275
|
expect(toolConfig._meta?.ui?.resourceUri).toBe(`ui://views/ext-apps/my-view.html${expected}`);
|
|
293
|
-
expect(mockRegisterResource.mock.calls[0]?.[1]).toBe(`ui://views/apps
|
|
294
|
-
expect(mockRegisterResource.mock.calls[1]?.[1]).toBe(`ui://views/ext-apps/my-view.html${expected}`);
|
|
276
|
+
expect(mockRegisterResource.mock.calls[0]?.[1]).toBe(`ui://views/ext-apps/my-view.html${expected}`);
|
|
295
277
|
});
|
|
296
278
|
it("should produce different version params for views with different bundles", () => {
|
|
297
279
|
setTestEnv({ NODE_ENV: "production" });
|
|
@@ -305,8 +287,8 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
305
287
|
description: "Second tool",
|
|
306
288
|
view: { component: "folder-view" },
|
|
307
289
|
}, vi.fn());
|
|
308
|
-
const myviewTemplate = (mockRegisterTool.mock.calls[0]?.[1])._meta?.
|
|
309
|
-
const folderviewTemplate = (mockRegisterTool.mock.calls[1]?.[1])._meta?.
|
|
290
|
+
const myviewTemplate = (mockRegisterTool.mock.calls[0]?.[1])._meta?.ui?.resourceUri;
|
|
291
|
+
const folderviewTemplate = (mockRegisterTool.mock.calls[1]?.[1])._meta?.ui?.resourceUri;
|
|
310
292
|
expect(myviewTemplate).not.toEqual(folderviewTemplate);
|
|
311
293
|
expect(myviewTemplate).toMatch(/\?v=[0-9a-f]{8}$/);
|
|
312
294
|
expect(folderviewTemplate).toMatch(/\?v=[0-9a-f]{8}$/);
|
|
@@ -319,26 +301,7 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
319
301
|
view: { component: "unknown-view" },
|
|
320
302
|
}, vi.fn());
|
|
321
303
|
const toolConfig = mockRegisterTool.mock.calls[0]?.[1];
|
|
322
|
-
expect(toolConfig._meta?.
|
|
323
|
-
});
|
|
324
|
-
it("should register tool with ui.resourceUri only when mcp-app only", async () => {
|
|
325
|
-
server.registerTool({
|
|
326
|
-
name: "my-view",
|
|
327
|
-
description: "Test tool",
|
|
328
|
-
view: {
|
|
329
|
-
component: "my-view",
|
|
330
|
-
description: "Test view",
|
|
331
|
-
hosts: ["mcp-app"],
|
|
332
|
-
},
|
|
333
|
-
}, vi.fn());
|
|
334
|
-
expect(mockRegisterTool).toHaveBeenCalledTimes(1);
|
|
335
|
-
const toolCallArgs = mockRegisterTool.mock.calls[0];
|
|
336
|
-
const toolConfig = toolCallArgs?.[1];
|
|
337
|
-
expect(toolConfig._meta).toHaveProperty("ui");
|
|
338
|
-
expect(toolConfig._meta?.ui).toEqual({
|
|
339
|
-
resourceUri: "ui://views/ext-apps/my-view.html",
|
|
340
|
-
});
|
|
341
|
-
expect(toolConfig._meta?.["openai/outputTemplate"]).toBeUndefined();
|
|
304
|
+
expect(toolConfig._meta?.ui?.resourceUri).toBe("ui://views/ext-apps/unknown-view.html");
|
|
342
305
|
});
|
|
343
306
|
it("should inject viewUUID into _meta of tool callback results", async () => {
|
|
344
307
|
const mockToolCallback = vi.fn().mockResolvedValue({
|
|
@@ -476,11 +439,12 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
476
439
|
const host = "localhost:3000";
|
|
477
440
|
const serverUrl = `http://${host}`;
|
|
478
441
|
const hmrUrl = `ws://${host}`;
|
|
479
|
-
const result = await appsSdkCallback(new URL("ui://views/apps
|
|
442
|
+
const result = await appsSdkCallback(new URL("ui://views/ext-apps/csp-tool.html"), createMockExtra(host));
|
|
480
443
|
const meta = result.contents[0]?._meta;
|
|
481
|
-
expect(meta
|
|
482
|
-
|
|
483
|
-
|
|
444
|
+
expect(meta.ui?.csp).toEqual({
|
|
445
|
+
resourceDomains: [serverUrl, "https://cdn.example.com"],
|
|
446
|
+
connectDomains: [serverUrl, hmrUrl, "https://api.example.com"],
|
|
447
|
+
baseUriDomains: [serverUrl],
|
|
484
448
|
});
|
|
485
449
|
});
|
|
486
450
|
it("should let view._meta override framework-computed keys", async () => {
|
|
@@ -492,18 +456,20 @@ describe("McpServer.registerTool (unified API)", () => {
|
|
|
492
456
|
description: "Test view",
|
|
493
457
|
csp: { connectDomains: ["https://api.x.com"] },
|
|
494
458
|
_meta: {
|
|
495
|
-
|
|
496
|
-
|
|
459
|
+
ui: {
|
|
460
|
+
csp: {
|
|
461
|
+
connectDomains: ["https://api.y.com"],
|
|
462
|
+
},
|
|
497
463
|
},
|
|
498
464
|
},
|
|
499
465
|
},
|
|
500
466
|
}, vi.fn());
|
|
501
467
|
const appsSdkCallback = mockRegisterResource.mock
|
|
502
468
|
.calls[0]?.[3];
|
|
503
|
-
const result = await appsSdkCallback(new URL("ui://views/apps
|
|
469
|
+
const result = await appsSdkCallback(new URL("ui://views/ext-apps/override-tool.html"), createMockExtra("localhost:3000"));
|
|
504
470
|
const meta = result.contents[0]?._meta;
|
|
505
|
-
expect(meta
|
|
506
|
-
|
|
471
|
+
expect(meta.ui?.csp).toEqual({
|
|
472
|
+
connectDomains: ["https://api.y.com"],
|
|
507
473
|
});
|
|
508
474
|
});
|
|
509
475
|
it("should pass user _meta keys through to tool config", () => {
|
|
@@ -553,16 +519,18 @@ describe("resources/list view _meta injection", () => {
|
|
|
553
519
|
await server.close();
|
|
554
520
|
const appsSdk = resources.find((r) => r.uri.includes("apps-sdk"));
|
|
555
521
|
const extApps = resources.find((r) => r.uri.includes("ext-apps"));
|
|
556
|
-
expect(appsSdk
|
|
522
|
+
expect(appsSdk).toBeUndefined();
|
|
557
523
|
expect(extApps?._meta).toBeDefined();
|
|
558
|
-
const appsSdkCsp = appsSdk?._meta?.["openai/widgetCSP"];
|
|
559
|
-
expect(appsSdkCsp.connect_domains?.length).toBeGreaterThan(0);
|
|
560
|
-
expect(appsSdkCsp.resource_domains).toContain("https://fonts.googleapis.com");
|
|
561
|
-
expect(appsSdk?._meta?.["openai/widgetDomain"]).toBe("skybridge.tech");
|
|
562
524
|
const extUi = (extApps?._meta).ui;
|
|
563
525
|
expect(extUi?.csp?.connectDomains?.length).toBeGreaterThan(0);
|
|
564
526
|
expect(extUi?.csp?.resourceDomains).toContain("https://fonts.googleapis.com");
|
|
565
527
|
expect(extUi?.domain).toBe("skybridge.tech");
|
|
528
|
+
const meta = extApps?._meta;
|
|
529
|
+
expect(meta["openai/widgetDescription"]).toBe("Onboarding deck");
|
|
530
|
+
expect(meta["openai/widgetCSP"]?.redirect_domains).toEqual([
|
|
531
|
+
"https://docs.skybridge.tech",
|
|
532
|
+
]);
|
|
533
|
+
expect(meta.ui?.csp).not.toHaveProperty("redirectDomains");
|
|
566
534
|
});
|
|
567
535
|
});
|
|
568
536
|
//# sourceMappingURL=view.test.js.map
|