Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"
import vitestPlugin from "@vitest/eslint-plugin";
import enforceZodV4 from "./eslint-rules/enforce-zod-v4.js";

const testFiles = ["tests/**/*.test.ts", "tests/**/*.ts"];
const testFiles = ["tests/**/*.test.ts", "tests/**/*.test.tsx", "tests/**/*.ts", "tests/**/*.tsx"];

const files = [...testFiles, "src/**/*.ts", "src/**/*.tsx", "scripts/**/*.ts"];

Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,13 @@
"@modelcontextprotocol/inspector": "^0.17.1",
"@mongodb-js/oidc-mock-provider": "^0.12.0",
"@redocly/cli": "^2.0.8",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@types/express": "^5.0.3",
"@types/node": "^24.5.2",
"@types/proper-lockfile": "^4.1.4",
"@types/react": "^18.3.0",
"@types/react-dom": "^19.2.3",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"@types/semver": "^7.7.0",
"@types/yargs-parser": "^21.0.3",
"@typescript-eslint/parser": "^8.44.0",
Expand All @@ -115,6 +115,7 @@
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"globals": "^16.3.0",
"happy-dom": "^20.0.11",
"husky": "^9.1.7",
"knip": "^5.63.1",
"mongodb": "^6.21.0",
Expand All @@ -123,6 +124,8 @@
"openapi-typescript": "^7.9.1",
"prettier": "^3.6.2",
"proper-lockfile": "^4.1.2",
"react": "^18.3.0",
"react-dom": "^18.3.0",
"semver": "^7.7.2",
"simple-git": "^3.28.0",
"testcontainers": "^11.7.1",
Expand Down
482 changes: 474 additions & 8 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

266 changes: 266 additions & 0 deletions tests/integration/ui/mcpUIFeature.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
import { describe, expect, it, afterAll } from "vitest";
import { describeWithMongoDB } from "../tools/mongodb/mongodbHelpers.js";
import { defaultTestConfig, expectDefined, getResponseElements } from "../helpers.js";
import { CompositeLogger } from "../../../src/common/logger.js";
import { ExportsManager } from "../../../src/common/exportsManager.js";
import { Session } from "../../../src/common/session.js";
import { Telemetry } from "../../../src/telemetry/telemetry.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Server } from "../../../src/server.js";
import { MCPConnectionManager } from "../../../src/common/connectionManager.js";
import { DeviceId } from "../../../src/helpers/deviceId.js";
import { connectionErrorHandler } from "../../../src/common/connectionErrorHandler.js";
import { Keychain } from "../../../src/common/keychain.js";
import { Elicitation } from "../../../src/elicitation.js";
import { VectorSearchEmbeddingsManager } from "../../../src/common/search/vectorSearchEmbeddingsManager.js";
import { defaultCreateAtlasLocalClient } from "../../../src/common/atlasLocal.js";
import { InMemoryTransport } from "../../../src/transports/inMemoryTransport.js";
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";

describeWithMongoDB(
"mcpUI feature with feature disabled (default)",
(integration) => {
describe("list-databases tool", () => {
it("should NOT return UIResource content when mcpUI feature is disabled", async () => {
await integration.connectMcpClient();
const response = await integration.mcpClient().callTool({
name: "list-databases",
arguments: {},
});

expect(response.content).toBeDefined();
expect(Array.isArray(response.content)).toBe(true);

const elements = response.content as Array<{ type: string }>;
const resourceElements = elements.filter((e) => e.type === "resource");
expect(resourceElements).toHaveLength(0);

const textElements = getResponseElements(response.content);
expect(textElements.length).toBeGreaterThan(0);
});
});
},
{
getUserConfig: () => ({
...defaultTestConfig,
previewFeatures: [], // mcpUI is NOT enabled
}),
}
);

describeWithMongoDB(
"mcpUI feature with feature enabled",
(integration) => {
describe("list-databases tool with mcpUI enabled", () => {
it("should return UIResource content when mcpUI feature is enabled", async () => {
await integration.connectMcpClient();
const response = await integration.mcpClient().callTool({
name: "list-databases",
arguments: {},
});

expect(response.content).toBeDefined();
expect(Array.isArray(response.content)).toBe(true);

const elements = response.content as Array<{ type: string; resource?: unknown }>;

const textElements = elements.filter((e) => e.type === "text");
expect(textElements.length).toBeGreaterThan(0);

const resourceElements = elements.filter((e) => e.type === "resource");
expect(resourceElements).toHaveLength(1);

Check failure on line 71 in tests/integration/ui/mcpUIFeature.test.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (ubuntu-latest)

tests/integration/ui/mcpUIFeature.test.ts > mcpUI feature with feature enabled > list-databases tool with mcpUI enabled > should return UIResource content when mcpUI feature is enabled

AssertionError: expected [] to have a length of 1 but got +0 - Expected + Received - 1 + 0 ❯ tests/integration/ui/mcpUIFeature.test.ts:71:42

Check failure on line 71 in tests/integration/ui/mcpUIFeature.test.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (windows-latest)

tests/integration/ui/mcpUIFeature.test.ts > mcpUI feature with feature enabled > list-databases tool with mcpUI enabled > should return UIResource content when mcpUI feature is enabled

AssertionError: expected [] to have a length of 1 but got +0 - Expected + Received - 1 + 0 ❯ tests/integration/ui/mcpUIFeature.test.ts:71:42

Check failure on line 71 in tests/integration/ui/mcpUIFeature.test.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (macos-latest)

tests/integration/ui/mcpUIFeature.test.ts > mcpUI feature with feature enabled > list-databases tool with mcpUI enabled > should return UIResource content when mcpUI feature is enabled

AssertionError: expected [] to have a length of 1 but got +0 - Expected + Received - 1 + 0 ❯ tests/integration/ui/mcpUIFeature.test.ts:71:42

const uiResource = resourceElements[0] as {
type: string;
resource: {
uri: string;
mimeType: string;
text: string;
_meta?: Record<string, unknown>;
};
};

expect(uiResource.type).toBe("resource");
expectDefined(uiResource.resource);
expect(uiResource.resource.uri).toMatch(/^ui:\/\/list-databases\/\d+$/);
expect(uiResource.resource.mimeType).toBe("text/html");
expect(typeof uiResource.resource.text).toBe("string");
expect(uiResource.resource.text.length).toBeGreaterThan(0);

expectDefined(uiResource.resource._meta);
expect(uiResource.resource._meta["mcpui.dev/ui-initial-render-data"]).toBeDefined();

const renderData = uiResource.resource._meta["mcpui.dev/ui-initial-render-data"] as {
databases: Array<{ name: string; size: number }>;
totalCount: number;
};
expect(renderData.databases).toBeInstanceOf(Array);
expect(typeof renderData.totalCount).toBe("number");
expect(renderData.totalCount).toBe(renderData.databases.length);

for (const db of renderData.databases) {
expect(typeof db.name).toBe("string");
expect(typeof db.size).toBe("number");
}
});

it("should include system databases in the response", async () => {
await integration.connectMcpClient();
const response = await integration.mcpClient().callTool({
name: "list-databases",
arguments: {},
});

const elements = response.content as Array<{
type: string;
resource?: { _meta?: Record<string, unknown> };
}>;
const resourceElement = elements.find((e) => e.type === "resource");
expectDefined(resourceElement);

const renderData = resourceElement.resource?._meta?.["mcpui.dev/ui-initial-render-data"] as {
databases: Array<{ name: string; size: number }>;
};

const dbNames = renderData.databases.map((db) => db.name);

expect(dbNames).toContain("admin");
expect(dbNames).toContain("local");
});
});
},
{
getUserConfig: () => ({
...defaultTestConfig,
previewFeatures: ["mcpUI"], // mcpUI IS enabled
}),
}
);

describeWithMongoDB(
"mcpUI feature - UIRegistry initialization",
(integration) => {
describe("server UIRegistry", () => {
it("should have UIRegistry initialized with bundled UIs", () => {
const server = integration.mcpServer();
expectDefined(server.uiRegistry);

expect(server.uiRegistry.has("list-databases")).toBe(true);

Check failure on line 148 in tests/integration/ui/mcpUIFeature.test.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (ubuntu-latest)

tests/integration/ui/mcpUIFeature.test.ts > mcpUI feature - UIRegistry initialization > server UIRegistry > should have UIRegistry initialized with bundled UIs

TypeError: server.uiRegistry.has is not a function ❯ tests/integration/ui/mcpUIFeature.test.ts:148:42

Check failure on line 148 in tests/integration/ui/mcpUIFeature.test.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (windows-latest)

tests/integration/ui/mcpUIFeature.test.ts > mcpUI feature - UIRegistry initialization > server UIRegistry > should have UIRegistry initialized with bundled UIs

TypeError: server.uiRegistry.has is not a function ❯ tests/integration/ui/mcpUIFeature.test.ts:148:42

Check failure on line 148 in tests/integration/ui/mcpUIFeature.test.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (macos-latest)

tests/integration/ui/mcpUIFeature.test.ts > mcpUI feature - UIRegistry initialization > server UIRegistry > should have UIRegistry initialized with bundled UIs

TypeError: server.uiRegistry.has is not a function ❯ tests/integration/ui/mcpUIFeature.test.ts:148:42

const uiHtml = server.uiRegistry.get("list-databases");
expectDefined(uiHtml);
expect(uiHtml.length).toBeGreaterThan(0);
});

it("should return list of available tools with UIs", () => {
const server = integration.mcpServer();
const availableTools = server.uiRegistry.getAvailableTools();

Check failure on line 157 in tests/integration/ui/mcpUIFeature.test.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (ubuntu-latest)

tests/integration/ui/mcpUIFeature.test.ts > mcpUI feature - UIRegistry initialization > server UIRegistry > should return list of available tools with UIs

TypeError: server.uiRegistry.getAvailableTools is not a function ❯ tests/integration/ui/mcpUIFeature.test.ts:157:58

Check failure on line 157 in tests/integration/ui/mcpUIFeature.test.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (windows-latest)

tests/integration/ui/mcpUIFeature.test.ts > mcpUI feature - UIRegistry initialization > server UIRegistry > should return list of available tools with UIs

TypeError: server.uiRegistry.getAvailableTools is not a function ❯ tests/integration/ui/mcpUIFeature.test.ts:157:58

Check failure on line 157 in tests/integration/ui/mcpUIFeature.test.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (macos-latest)

tests/integration/ui/mcpUIFeature.test.ts > mcpUI feature - UIRegistry initialization > server UIRegistry > should return list of available tools with UIs

TypeError: server.uiRegistry.getAvailableTools is not a function ❯ tests/integration/ui/mcpUIFeature.test.ts:157:58

expect(Array.isArray(availableTools)).toBe(true);
expect(availableTools).toContain("list-databases");
});
});
},
{
getUserConfig: () => ({
...defaultTestConfig,
previewFeatures: ["mcpUI"],
}),
}
);

describe("mcpUI feature with custom UIs", () => {
const initServerWithCustomUIs = async (
customUIs: Record<string, string>
): Promise<{ server: Server; transport: Transport }> => {
const userConfig = {
...defaultTestConfig,
previewFeatures: ["mcpUI" as const],
};
const logger = new CompositeLogger();
const deviceId = DeviceId.create(logger);
const connectionManager = new MCPConnectionManager(userConfig, logger, deviceId);
const exportsManager = ExportsManager.init(userConfig, logger);

const session = new Session({
userConfig,
logger,
exportsManager,
connectionManager,
keychain: Keychain.root,
vectorSearchEmbeddingsManager: new VectorSearchEmbeddingsManager(userConfig, connectionManager),
atlasLocalClient: await defaultCreateAtlasLocalClient(),
});

const telemetry = Telemetry.create(session, userConfig, deviceId);
const mcpServerInstance = new McpServer({ name: "test", version: "1.0" });
const elicitation = new Elicitation({ server: mcpServerInstance.server });

const server = new Server({
session,
userConfig,
telemetry,
mcpServer: mcpServerInstance,
elicitation,
connectionErrorHandler,
customUIs,
});

const transport = new InMemoryTransport();

return { transport, server };
};

let server: Server | undefined;
let transport: Transport | undefined;

afterAll(async () => {
await transport?.close();
await server?.close();
});

it("should use custom UI when provided via server options", async () => {
const customUIs = {
"list-databases": "<html>Custom Test UI</html>",
};

({ server, transport } = await initServerWithCustomUIs(customUIs));
await server.connect(transport);

expectDefined(server.uiRegistry);
expect(server.uiRegistry.has("list-databases")).toBe(true);

Check failure on line 231 in tests/integration/ui/mcpUIFeature.test.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (ubuntu-latest)

tests/integration/ui/mcpUIFeature.test.ts > mcpUI feature with custom UIs > should use custom UI when provided via server options

TypeError: server.uiRegistry.has is not a function ❯ tests/integration/ui/mcpUIFeature.test.ts:231:34

Check failure on line 231 in tests/integration/ui/mcpUIFeature.test.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (windows-latest)

tests/integration/ui/mcpUIFeature.test.ts > mcpUI feature with custom UIs > should use custom UI when provided via server options

TypeError: server.uiRegistry.has is not a function ❯ tests/integration/ui/mcpUIFeature.test.ts:231:34

Check failure on line 231 in tests/integration/ui/mcpUIFeature.test.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (macos-latest)

tests/integration/ui/mcpUIFeature.test.ts > mcpUI feature with custom UIs > should use custom UI when provided via server options

TypeError: server.uiRegistry.has is not a function ❯ tests/integration/ui/mcpUIFeature.test.ts:231:34
expect(server.uiRegistry.get("list-databases")).toBe("<html>Custom Test UI</html>");
});

it("should add new custom UIs for tools without bundled UIs", async () => {
const customUIs = {
"custom-tool": "<html>Custom Tool UI</html>",
};

({ server, transport } = await initServerWithCustomUIs(customUIs));
await server.connect(transport);

expectDefined(server.uiRegistry);
expect(server.uiRegistry.has("custom-tool")).toBe(true);

Check failure on line 244 in tests/integration/ui/mcpUIFeature.test.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (ubuntu-latest)

tests/integration/ui/mcpUIFeature.test.ts > mcpUI feature with custom UIs > should add new custom UIs for tools without bundled UIs

TypeError: server.uiRegistry.has is not a function ❯ tests/integration/ui/mcpUIFeature.test.ts:244:34

Check failure on line 244 in tests/integration/ui/mcpUIFeature.test.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (windows-latest)

tests/integration/ui/mcpUIFeature.test.ts > mcpUI feature with custom UIs > should add new custom UIs for tools without bundled UIs

TypeError: server.uiRegistry.has is not a function ❯ tests/integration/ui/mcpUIFeature.test.ts:244:34

Check failure on line 244 in tests/integration/ui/mcpUIFeature.test.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (macos-latest)

tests/integration/ui/mcpUIFeature.test.ts > mcpUI feature with custom UIs > should add new custom UIs for tools without bundled UIs

TypeError: server.uiRegistry.has is not a function ❯ tests/integration/ui/mcpUIFeature.test.ts:244:34
expect(server.uiRegistry.get("custom-tool")).toBe("<html>Custom Tool UI</html>");
});

it("should merge custom UIs with bundled UIs", async () => {
const customUIs = {
"new-tool": "<html>New Tool UI</html>",
};

({ server, transport } = await initServerWithCustomUIs(customUIs));
await server.connect(transport);

expectDefined(server.uiRegistry);

expect(server.uiRegistry.has("new-tool")).toBe(true);

Check failure on line 258 in tests/integration/ui/mcpUIFeature.test.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (ubuntu-latest)

tests/integration/ui/mcpUIFeature.test.ts > mcpUI feature with custom UIs > should merge custom UIs with bundled UIs

TypeError: server.uiRegistry.has is not a function ❯ tests/integration/ui/mcpUIFeature.test.ts:258:34

Check failure on line 258 in tests/integration/ui/mcpUIFeature.test.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (windows-latest)

tests/integration/ui/mcpUIFeature.test.ts > mcpUI feature with custom UIs > should merge custom UIs with bundled UIs

TypeError: server.uiRegistry.has is not a function ❯ tests/integration/ui/mcpUIFeature.test.ts:258:34

Check failure on line 258 in tests/integration/ui/mcpUIFeature.test.ts

View workflow job for this annotation

GitHub Actions / Run MongoDB tests (macos-latest)

tests/integration/ui/mcpUIFeature.test.ts > mcpUI feature with custom UIs > should merge custom UIs with bundled UIs

TypeError: server.uiRegistry.has is not a function ❯ tests/integration/ui/mcpUIFeature.test.ts:258:34
expect(server.uiRegistry.get("new-tool")).toBe("<html>New Tool UI</html>");

expect(server.uiRegistry.has("list-databases")).toBe(true);
const bundledUI = server.uiRegistry.get("list-databases");
expectDefined(bundledUI);
expect(bundledUI.length).toBeGreaterThan(0);
});
});
1 change: 1 addition & 0 deletions tests/setupReact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "@testing-library/jest-dom/vitest";
Loading
Loading