import { describe, it, expect, vi, beforeEach } from "vitest"; import { UiResourceHandler } from "../ui-resource-handler.ts"; import { UrlElicitationRequiredError } from "@modelcontextprotocol/sdk/types.js"; import type { McpServerManager } from "../server-manager.ts"; // Mock the manager function createMockManager(overrides: Partial = {}): McpServerManager { return { readResource: vi.fn(), getConnection: vi.fn().mockReturnValue(null), ...overrides, } as unknown as McpServerManager; } describe("UiResourceHandler", () => { describe("readUiResource", () => { it("throws for non-ui:// URIs", async () => { const manager = createMockManager(); const handler = new UiResourceHandler(manager); await expect(handler.readUiResource("server", "https://example.com")).rejects.toThrow( /URI must start with ui:\/\// ); }); it("preserves URL-required errors for the outer tool adapter", async () => { const error = new UrlElicitationRequiredError([{ mode: "url", message: "Connect", elicitationId: "connect-1", url: "https://example.com/connect", }]); const manager = createMockManager({ readResource: vi.fn().mockRejectedValue(error) }); const handler = new UiResourceHandler(manager); await expect(handler.readUiResource("server", "ui://test/widget")).rejects.toBe(error); }); it("reads and returns HTML from text content", async () => { const manager = createMockManager({ readResource: vi.fn().mockResolvedValue({ contents: [ { uri: "ui://test/widget", mimeType: "text/html", text: "

Hello

", }, ], }), }); const handler = new UiResourceHandler(manager); const result = await handler.readUiResource("server", "ui://test/widget"); expect(result.uri).toBe("ui://test/widget"); expect(result.html).toBe("

Hello

"); expect(result.mimeType).toBe("text/html"); }); it("reads and decodes blob content", async () => { const htmlContent = "
Blob content
"; const base64Content = Buffer.from(htmlContent).toString("base64"); const manager = createMockManager({ readResource: vi.fn().mockResolvedValue({ contents: [ { uri: "ui://test/widget", mimeType: "text/html", blob: base64Content, }, ], }), }); const handler = new UiResourceHandler(manager); const result = await handler.readUiResource("server", "ui://test/widget"); expect(result.html).toBe(htmlContent); }); it("throws for empty content", async () => { const manager = createMockManager({ readResource: vi.fn().mockResolvedValue({ contents: [ { uri: "ui://test/widget", mimeType: "text/html", text: " ", }, ], }), }); const handler = new UiResourceHandler(manager); await expect(handler.readUiResource("server", "ui://test/widget")).rejects.toThrow( /content is empty/ ); }); it("throws for unsupported MIME type", async () => { const manager = createMockManager({ readResource: vi.fn().mockResolvedValue({ contents: [ { uri: "ui://test/widget", mimeType: "application/json", text: '{"key": "value"}', }, ], }), }); const handler = new UiResourceHandler(manager); await expect(handler.readUiResource("server", "ui://test/widget")).rejects.toThrow( /unsupported MIME type/ ); }); it("accepts text/html;profile=mcp-app MIME type", async () => { const manager = createMockManager({ readResource: vi.fn().mockResolvedValue({ contents: [ { uri: "ui://test/widget", mimeType: "text/html;profile=mcp-app", text: "content", }, ], }), }); const handler = new UiResourceHandler(manager); const result = await handler.readUiResource("server", "ui://test/widget"); expect(result.html).toBe("content"); }); it("throws when no contents returned", async () => { const manager = createMockManager({ readResource: vi.fn().mockResolvedValue({ contents: [], }), }); const handler = new UiResourceHandler(manager); await expect(handler.readUiResource("server", "ui://test/widget")).rejects.toThrow( "No contents returned for UI resource: ui://test/widget" ); }); it("prefers content with matching URI", async () => { const manager = createMockManager({ readResource: vi.fn().mockResolvedValue({ contents: [ { uri: "ui://other/widget", mimeType: "text/html", text: "

Wrong

", }, { uri: "ui://test/widget", mimeType: "text/html", text: "

Correct

", }, ], }), }); const handler = new UiResourceHandler(manager); const result = await handler.readUiResource("server", "ui://test/widget"); expect(result.html).toBe("

Correct

"); }); it("falls back to first HTML content if no URI match", async () => { const manager = createMockManager({ readResource: vi.fn().mockResolvedValue({ contents: [ { mimeType: "application/json", text: "{}", }, { mimeType: "text/html", text: "

HTML

", }, ], }), }); const handler = new UiResourceHandler(manager); const result = await handler.readUiResource("server", "ui://test/widget"); expect(result.html).toBe("

HTML

"); }); it("extracts CSP meta from content _meta", async () => { const manager = createMockManager({ readResource: vi.fn().mockResolvedValue({ contents: [ { uri: "ui://test/widget", mimeType: "text/html", text: "

Content

", _meta: { ui: { csp: { scriptDomains: ["'self'", "cdn.example.com"], styleDomains: ["'self'"], }, }, }, }, ], }), }); const handler = new UiResourceHandler(manager); const result = await handler.readUiResource("server", "ui://test/widget"); expect(result.meta.csp).toEqual({ scriptDomains: ["'self'", "cdn.example.com"], styleDomains: ["'self'"], }); }); it("extracts permissions meta", async () => { const manager = createMockManager({ readResource: vi.fn().mockResolvedValue({ contents: [ { uri: "ui://test/widget", mimeType: "text/html", text: "

Content

", _meta: { ui: { permissions: { camera: {}, microphone: {}, }, }, }, }, ], }), }); const handler = new UiResourceHandler(manager); const result = await handler.readUiResource("server", "ui://test/widget"); expect(result.meta.permissions).toEqual({ camera: {}, microphone: {}, }); }); it("extracts domain and prefersBorder meta", async () => { const manager = createMockManager({ readResource: vi.fn().mockResolvedValue({ contents: [ { uri: "ui://test/widget", mimeType: "text/html", text: "

Content

", _meta: { ui: { domain: "example.com", prefersBorder: true, }, }, }, ], }), }); const handler = new UiResourceHandler(manager); const result = await handler.readUiResource("server", "ui://test/widget"); expect(result.meta.domain).toBe("example.com"); expect(result.meta.prefersBorder).toBe(true); }); it("throws when content has no text or blob", async () => { const manager = createMockManager({ readResource: vi.fn().mockResolvedValue({ contents: [ { uri: "ui://test/widget", mimeType: "text/html", // No text or blob }, ], }), }); const handler = new UiResourceHandler(manager); await expect(handler.readUiResource("server", "ui://test/widget")).rejects.toThrow( "did not include text or blob content" ); }); }); });