mcp.test.ts•16.7 kB
import { describe, it, expect, vi, beforeEach } from "vitest";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { createMcpServer, type McpServerOptions } from "../src/mcp.js";
import type {
Tab,
TabContent,
BrowserInterface,
} from "../src/browser/browser.js";
// Mock the getInterface function from browser.js
const mockBrowserInterface: BrowserInterface = {
getTabList: vi.fn(),
getPageContent: vi.fn(),
openURL: vi.fn(),
};
vi.mock("../src/browser/browser.js", async (importOriginal) => {
const actual = await importOriginal();
return {
...(actual as object),
getInterface: vi.fn(() => mockBrowserInterface),
};
});
const defaultTestOptions: McpServerOptions = {
applicationName: "Google Chrome",
browser: "chrome" as const,
excludeHosts: [],
checkInterval: 0,
maxContentChars: 20000,
extractionTimeout: 20000,
};
const mockTabs: Tab[] = [
{
windowId: "1001",
tabId: "2001",
title: "Example Page",
url: "https://example.com/page1",
},
{
windowId: "1001",
tabId: "2002",
title: "GitHub",
url: "https://github.com/user/repo",
},
{
windowId: "1002",
tabId: "2003",
title: "Test Site",
url: "https://test.com/page",
},
];
const mockPageContent: TabContent = {
title: "Example Page",
url: "https://example.com/page1",
content: `<!DOCTYPE html>
<html>
<head>
<title>Example Page</title>
</head>
<body>
<h1>Example</h1>
<p>This is test content.</p>
</body>
</html>`,
};
describe("MCP Server", () => {
let client: Client;
let server: any;
beforeEach(async () => {
vi.clearAllMocks();
// Create test client
client = new Client({
name: "test client",
version: "0.1.0",
});
// Create server
const options = defaultTestOptions;
server = await createMcpServer(options);
// Connect client and server via in-memory transport
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await Promise.all([
client.connect(clientTransport),
server.connect(serverTransport),
]);
});
describe("list_tabs tool", () => {
it("should return all tabs when no domains are excluded", async () => {
vi.mocked(mockBrowserInterface.getTabList).mockResolvedValue(mockTabs);
const result = await client.callTool({
name: "list_tabs",
arguments: {},
});
expect(result.content).toHaveLength(1);
const text = (result.content as any)[0].text;
expect(text).toContain("### Current Tabs (3 tabs exists)");
expect(text).toContain("ID:1001:2001 Example Page (example.com)");
expect(text).toContain("ID:1001:2002 GitHub (github.com)");
expect(text).toContain("ID:1002:2003 Test Site (test.com)");
});
it("should include full URLs when includeUrl is true", async () => {
vi.mocked(mockBrowserInterface.getTabList).mockResolvedValue(mockTabs);
const result = await client.callTool({
name: "list_tabs",
arguments: { includeUrl: true },
});
expect(result.content).toHaveLength(1);
const text = (result.content as any)[0].text;
expect(text).toContain("### Current Tabs (3 tabs exists)");
expect(text).toContain(
"ID:1001:2001 [Example Page](https://example.com/page1)"
);
expect(text).toContain(
"ID:1001:2002 [GitHub](https://github.com/user/repo)"
);
expect(text).toContain("ID:1002:2003 [Test Site](https://test.com/page)");
});
it("should filter out excluded domains", async () => {
// Create server with excluded domains
const options = { ...defaultTestOptions, excludeHosts: ["github.com"] };
const filteredServer = await createMcpServer(options);
// Create new client for filtered server
const filteredClient = new Client({
name: "test client",
version: "0.1.0",
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await Promise.all([
filteredClient.connect(clientTransport),
filteredServer.connect(serverTransport),
]);
vi.mocked(mockBrowserInterface.getTabList).mockResolvedValue(mockTabs);
const result = await filteredClient.callTool({
name: "list_tabs",
arguments: {},
});
const text = (result.content as any)[0].text;
expect(text).toContain("### Current Tabs (2 tabs exists)"); // github.com tab should be filtered out
expect(text).not.toContain("github.com");
expect(text).toContain("example.com");
expect(text).toContain("test.com");
});
});
describe("read_tab_content tool", () => {
it("should get page content for valid tab", async () => {
vi.mocked(mockBrowserInterface.getTabList).mockResolvedValue(mockTabs);
vi.mocked(mockBrowserInterface.getPageContent).mockResolvedValue(
mockPageContent
);
const result = await client.callTool({
name: "read_tab_content",
arguments: {
id: "ID:1001:2001",
},
});
expect(result.content).toHaveLength(1);
const text = (result.content as any)[0].text;
expect(text).toContain("---");
expect(text).toContain(mockPageContent.title);
expect(text).toContain("## Example");
expect(text).toContain("This is test content.");
});
it("should get content from active tab when no id provided", async () => {
vi.mocked(mockBrowserInterface.getPageContent).mockResolvedValue(
mockPageContent
);
const result = await client.callTool({
name: "read_tab_content",
arguments: {},
});
expect(result.content).toHaveLength(1);
const text = (result.content as any)[0].text;
expect(text).toContain("---");
expect(text).toContain(mockPageContent.title);
expect(text).toContain("## Example");
expect(text).toContain("This is test content.");
});
it("should reject content from excluded domains", async () => {
// Create server with excluded domains
const options = { ...defaultTestOptions, excludeHosts: ["example.com"] };
const filteredServer = await createMcpServer(options);
// Create new client for filtered server
const filteredClient = new Client({
name: "test client",
version: "0.1.0",
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await Promise.all([
filteredClient.connect(clientTransport),
filteredServer.connect(serverTransport),
]);
vi.mocked(mockBrowserInterface.getTabList).mockResolvedValue(mockTabs);
vi.mocked(mockBrowserInterface.getPageContent).mockResolvedValue(
mockPageContent
);
const result = await filteredClient.callTool({
name: "read_tab_content",
arguments: {
id: "ID:1001:2001",
},
});
expect(result.isError).toBe(true);
expect((result.content as any)[0].text).toContain(
"Content not available for excluded host"
);
});
});
describe("open_in_new_tab tool", () => {
it("should open URL with correct application name", async () => {
vi.mocked(mockBrowserInterface.openURL).mockResolvedValue({
windowId: "123",
tabId: "456",
});
const result = await client.callTool({
name: "open_in_new_tab",
arguments: {
url: "https://example.com",
},
});
expect(vi.mocked(mockBrowserInterface.openURL)).toHaveBeenCalledWith(
"Google Chrome",
"https://example.com"
);
expect(result.content).toHaveLength(1);
expect((result.content as any)[0]).toEqual({
type: "text",
text: "Successfully opened URL in new tab. Tab: `ID:123:456`",
});
});
});
describe("Resources", () => {
describe("current_tab resource", () => {
it("should return content of active tab", async () => {
vi.mocked(mockBrowserInterface.getPageContent).mockResolvedValue(
mockPageContent
);
const result = await client.readResource({
uri: "tab://current",
});
expect(result.contents).toHaveLength(1);
const content = result.contents[0];
expect(content.uri).toBe("tab://current");
expect(content.mimeType).toBe("text/markdown");
expect(content.text).toContain("---");
expect(content.text).toContain("title: " + mockPageContent.title);
expect(content.text).toContain("## Example");
expect(content.text).toContain("This is test content.");
});
it("should reject content from excluded domains", async () => {
// Create server with excluded domains
const options = {
...defaultTestOptions,
excludeHosts: ["example.com"],
};
const filteredServer = await createMcpServer(options);
const filteredClient = new Client({
name: "test client",
version: "0.1.0",
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await Promise.all([
filteredClient.connect(clientTransport),
filteredServer.connect(serverTransport),
]);
vi.mocked(mockBrowserInterface.getPageContent).mockResolvedValue(
mockPageContent
);
await expect(
filteredClient.readResource({
uri: "tab://current",
})
).rejects.toThrow("Content not available for excluded host");
});
});
describe("tabs resource template", () => {
it("should return content of specific tab when checkInterval > 0", async () => {
// Create server with checkInterval > 0
const options = {
...defaultTestOptions,
checkInterval: 3000,
};
const subscriptionServer = await createMcpServer(options);
const subscriptionClient = new Client({
name: "test client",
version: "0.1.0",
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await Promise.all([
subscriptionClient.connect(clientTransport),
subscriptionServer.connect(serverTransport),
]);
vi.mocked(mockBrowserInterface.getPageContent).mockResolvedValue(
mockPageContent
);
const result = await subscriptionClient.readResource({
uri: "tab://1001/2001",
});
expect(result.contents).toHaveLength(1);
const content = result.contents[0];
expect(content.uri).toBe("tab://1001/2001");
expect(content.mimeType).toBe("text/markdown");
expect(content.text).toContain("---");
expect(content.text).toContain("title: " + mockPageContent.title);
expect(content.text).toContain("## Example");
expect(content.text).toContain("This is test content.");
});
it("should reject content from excluded domains when checkInterval > 0", async () => {
// Create server with excluded domains and checkInterval > 0
const options = {
...defaultTestOptions,
excludeHosts: ["example.com"],
checkInterval: 3000,
};
const filteredServer = await createMcpServer(options);
const filteredClient = new Client({
name: "test client",
version: "0.1.0",
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await Promise.all([
filteredClient.connect(clientTransport),
filteredServer.connect(serverTransport),
]);
vi.mocked(mockBrowserInterface.getPageContent).mockResolvedValue(
mockPageContent
);
await expect(
filteredClient.readResource({
uri: "tab://1001/2001",
})
).rejects.toThrow("Content not available for excluded host");
});
});
describe("resource listing", () => {
it("should only list current_tab resource when checkInterval is 0", async () => {
vi.mocked(mockBrowserInterface.getTabList).mockResolvedValue(mockTabs);
const result = await client.listResources();
// Should only have current_tab resource
expect(result.resources.length).toBe(1);
// Check current_tab resource
const currentTabResource = result.resources.find(
(r) => r.uri === "tab://current"
);
expect(currentTabResource).toBeDefined();
expect(currentTabResource?.name).toBe("current_tab");
expect(currentTabResource?.mimeType).toBe("text/markdown");
// Should not have any tab://{windowId}/{tabId} resources
const tabResources = result.resources.filter(
(r) => r.uri?.startsWith("tab://") && r.uri !== "tab://current"
);
expect(tabResources).toHaveLength(0);
});
it("should list all tab resources when checkInterval > 0", async () => {
// Create server with checkInterval > 0
const options = {
...defaultTestOptions,
checkInterval: 3000,
};
const subscriptionServer = await createMcpServer(options);
const subscriptionClient = new Client({
name: "test client",
version: "0.1.0",
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await Promise.all([
subscriptionClient.connect(clientTransport),
subscriptionServer.connect(serverTransport),
]);
vi.mocked(mockBrowserInterface.getTabList).mockResolvedValue(mockTabs);
const result = await subscriptionClient.listResources();
// Should have current_tab resource plus individual tab resources from template
expect(result.resources.length).toBeGreaterThanOrEqual(1);
// Check current_tab resource
const currentTabResource = result.resources.find(
(r) => r.uri === "tab://current"
);
expect(currentTabResource).toBeDefined();
expect(currentTabResource?.name).toBe("current_tab");
expect(currentTabResource?.mimeType).toBe("text/markdown");
// Check that tab resources are generated from template
const tabResources = result.resources.filter(
(r) => r.uri?.startsWith("tab://") && r.uri !== "tab://current"
);
expect(tabResources).toHaveLength(mockTabs.length);
// Verify first tab resource
const firstTabResource = tabResources[0];
expect(firstTabResource.name).toBe(
`${mockTabs[0].title} (example.com)`
);
expect(firstTabResource.mimeType).toBe("text/markdown");
});
it("should filter resources by excluded domains when checkInterval > 0", async () => {
// Create server with excluded domains and checkInterval > 0
const options = {
...defaultTestOptions,
excludeHosts: ["github.com"],
checkInterval: 3000,
};
const filteredServer = await createMcpServer(options);
const filteredClient = new Client({
name: "test client",
version: "0.1.0",
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await Promise.all([
filteredClient.connect(clientTransport),
filteredServer.connect(serverTransport),
]);
vi.mocked(mockBrowserInterface.getTabList).mockResolvedValue(mockTabs);
const result = await filteredClient.listResources();
// Should have current_tab resource plus filtered tab resources
expect(result.resources.length).toBeGreaterThanOrEqual(1);
// Verify current_tab resource exists
const currentTabResource = result.resources.find(
(r) => r.uri === "tab://current"
);
expect(currentTabResource).toBeDefined();
// Check filtered tab resources - github.com should be excluded
const tabResources = result.resources.filter(
(r) => r.uri?.startsWith("tab://") && r.uri !== "tab://current"
);
// Should have 2 tabs (example.com and test.com), github.com is excluded
expect(tabResources).toHaveLength(2);
// Verify github.com tab is not in the list
const githubTab = tabResources.find((r) => r.name === "GitHub");
expect(githubTab).toBeUndefined();
});
});
});
});