116 lines
4.3 KiB
TypeScript
116 lines
4.3 KiB
TypeScript
import { describe, it, expect, mock } from "bun:test";
|
|||
|
|
|
||
|
|
// Tests for the MCP client wrapper. Uses a fake Client that implements the
|
||
|
|
// subset of methods McpediaClient uses, so no network/DB is needed.
|
||
|
|
|
||
|
|
// Mock the SDK client to avoid real network connections.
|
||
|
|
const mockTools = [
|
||
|
|
{ name: "search_documents", description: "keyword search" },
|
||
|
|
{ name: "get_document", description: "get doc body" },
|
||
|
|
{ name: "create_document", description: "create doc" },
|
||
|
|
{ name: "delete_document", description: "delete doc" },
|
||
|
|
];
|
||
|
|
|
||
|
|
mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({
|
||
|
|
Client: class {
|
||
|
|
connect() {}
|
||
|
|
close() {}
|
||
|
|
listTools() {
|
||
|
|
return Promise.resolve({ tools: mockTools });
|
||
|
|
}
|
||
|
|
callTool(opts: any) {
|
||
|
|
const { name, arguments: args } = opts;
|
||
|
|
if (name === "get_document") {
|
||
|
|
return Promise.resolve({ content: [{ type: "text", text: `# ${args.slug}` }] });
|
||
|
|
}
|
||
|
|
if (name === "create_document") {
|
||
|
|
return Promise.resolve({ content: [{ type: "text", text: JSON.stringify({ ok: true, slug: args.slug }) }] });
|
||
|
|
}
|
||
|
|
if (name === "delete_document") {
|
||
|
|
return Promise.resolve({ content: [{ type: "text", text: JSON.stringify({ ok: true, deleted: true }) }] });
|
||
|
|
}
|
||
|
|
return Promise.resolve({ content: [{ type: "text", text: "[]" }] });
|
||
|
|
}
|
||
|
|
listResources() {
|
||
|
|
return Promise.resolve({ resources: [{ uri: "mcpedia://docs", mimeType: "application/json" }] });
|
||
|
|
}
|
||
|
|
readResource({ uri }: any) {
|
||
|
|
return Promise.resolve({ contents: [{ uri, mimeType: "text/markdown", text: "# Test" }] });
|
||
|
|
}
|
||
|
|
},
|
||
|
|
}));
|
||
|
|
|
||
|
|
mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
|
||
|
|
StreamableHTTPClientTransport: class {
|
||
|
|
constructor(url: URL, opts?: any) {}
|
||
|
|
},
|
||
|
|
}));
|
||
|
|
|
||
|
|
const { McpediaClient } = await import("../src/client");
|
||
|
|
|
||
|
|
describe("McpediaClient", () => {
|
||
|
|
it("listTools returns all registered tools", async () => {
|
||
|
|
const client = await McpediaClient.connect("http://localhost:0/mcp");
|
||
|
|
const tools = await client.listTools();
|
||
|
|
expect(tools.length).toBe(4);
|
||
|
|
expect(tools.map((t: any) => t.name)).toContain("search_documents");
|
||
|
|
expect(tools.map((t: any) => t.name)).toContain("get_document");
|
||
|
|
await client.disconnect();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("getDocument returns markdown body", async () => {
|
||
|
|
const client = await McpediaClient.connect("http://localhost:0/mcp");
|
||
|
|
const body = await client.getDocument("docs/websocket/contract");
|
||
|
|
expect(body).toBe("# docs/websocket/contract");
|
||
|
|
await client.disconnect();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("createDocument posts args and parses JSON response", async () => {
|
||
|
|
const client = await McpediaClient.connect("http://localhost:0/mcp");
|
||
|
|
const res = await client.createDocument({
|
||
|
|
slug: "docs/test",
|
||
|
|
title: "Test",
|
||
|
|
section: "docs",
|
||
|
|
body: "# Hello",
|
||
|
|
});
|
||
|
|
expect(res.ok).toBe(true);
|
||
|
|
expect(res.slug).toBe("docs/test");
|
||
|
|
await client.disconnect();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("deleteDocument returns deleted flag", async () => {
|
||
|
|
const client = await McpediaClient.connect("http://localhost:0/mcp");
|
||
|
|
const res = await client.deleteDocument("docs/test");
|
||
|
|
expect(res.ok).toBe(true);
|
||
|
|
expect(res.deleted).toBe(true);
|
||
|
|
await client.disconnect();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("listResources returns resource list", async () => {
|
||
|
|
const client = await McpediaClient.connect("http://localhost:0/mcp");
|
||
|
|
const resources = await client.listResources();
|
||
|
|
expect(resources.length).toBe(1);
|
||
|
|
expect(resources[0].uri).toBe("mcpedia://docs");
|
||
|
|
await client.disconnect();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("readResource returns text content", async () => {
|
||
|
|
const client = await McpediaClient.connect("http://localhost:0/mcp");
|
||
|
|
const res = await client.readResource("mcpedia://docs/docs/websocket/contract");
|
||
|
|
expect(res.text).toBe("# Test");
|
||
|
|
expect(res.mimeType).toBe("text/markdown");
|
||
|
|
await client.disconnect();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("send headers via requestInit for auth", async () => {
|
||
|
|
// Verify that options.headers are passed through to the transport constructor
|
||
|
|
const client = await McpediaClient.connect("http://localhost:0/mcp", {
|
||
|
|
headers: { "x-webhook-secret": "secret123" },
|
||
|
|
});
|
||
|
|
// We can't directly assert the transport constructor received headers,
|
||
|
|
// but we verify the client connects and works with headers set.
|
||
|
|
const tools = await client.listTools();
|
||
|
|
expect(tools.length).toBeGreaterThan(0);
|
||
|
|
await client.disconnect();
|
||
|
|
});
|
||
|
|
});
|