Files
homaroma/apps/mcp-server/index.contract.test.ts
2026-10-11 06:43:08 +07:00

87 lines
3.9 KiB
TypeScript

import { expect, test } from "bun:test";
import { join } from "node:path";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
async function connectMcp(baseUrl: string) {
const env = { ...process.env, MCP_BACKEND_URL: baseUrl } as Record<string, string>;
delete env.ADMIN_USERNAME;
delete env.ADMIN_PASSWORD;
delete env.MCP_ADMIN_USERNAME;
delete env.MCP_ADMIN_PASSWORD;
env.MCP_ADMIN_USERNAME = "contract-test";
env.MCP_ADMIN_PASSWORD = "contract-test";
const transport = new StdioClientTransport({
command: process.execPath,
args: [join(import.meta.dir, "index.ts")],
cwd: import.meta.dir,
env,
stderr: "pipe",
});
const client = new Client({ name: "mcp-contract-test", version: "1.0.0" });
await client.connect(transport);
return client;
}
test("settings writes send only supported fields and simple product fields are advertised", async () => {
const writes: unknown[] = [];
let getSettingsCount = 0;
const backend = Bun.serve({
port: 0,
fetch: async (request) => {
const path = new URL(request.url).pathname;
if (path === "/api/auth/login") return Response.json({ token: "contract-token" });
if (path === "/api/settings/footer-layout" && request.method === "GET") {
return Response.json({ version: "2026-10-11T00:00:00.000Z", layout: { columns: ["brand", "explore"] } });
}
if (path === "/api/settings" && request.method === "GET") {
getSettingsCount++;
return Response.json({ brandName: "Old", footerLayout: { columns: [] }, themeTokens: { brandAccent: "#123" } });
}
if (path === "/api/settings" && request.method === "PUT") {
const body = await request.json();
writes.push(body);
return Response.json({ ...(body as object), themeTokens: { brandAccent: "#123", ...((body as any).themeTokens ?? {}) } });
}
return new Response("Not found", { status: 404 });
},
});
let client: Client | undefined;
try {
client = await connectMcp(`http://localhost:${backend.port}/api`);
const tools = (await client.listTools()).tools;
expect(tools).toHaveLength(113);
expect(tools.find((item) => item.name === "get_footer_layout")?.annotations?.readOnlyHint).toBe(true);
expect(tools.find((item) => item.name === "delete_category_section")?.annotations?.destructiveHint).toBe(true);
const footerLayout = await client.callTool({ name: "get_footer_layout", arguments: {} });
expect(footerLayout.isError).toBeFalsy();
expect(footerLayout.structuredContent).toEqual({ version: "2026-10-11T00:00:00.000Z", layout: { columns: ["brand", "explore"] } });
for (const name of ["create_product", "bulk_create_products"]) {
const tool = tools.find((item) => item.name === name);
expect(tool).toBeDefined();
const properties = name === "create_product"
? tool!.inputSchema.properties
: (tool!.inputSchema.properties?.products as any).items.properties;
for (const field of ["price", "stock", "sku", "barcode"]) {
expect(properties?.[field]).toBeDefined();
}
}
expect((await client.callTool({ name: "update_theme_tokens", arguments: { brandPrimary: "#456" } })).isError).toBeFalsy();
expect((await client.callTool({ name: "update_brand_name", arguments: { brandName: "New" } })).isError).toBeFalsy();
expect((await client.callTool({ name: "update_footer_content", arguments: { brandDescription: "New footer", socialLinks: { facebookUrl: "https://example.com" } } })).isError).toBeFalsy();
expect(getSettingsCount).toBe(0);
expect(writes).toEqual([
{ themeTokens: { brandPrimary: "#456" } },
{ brandName: "New" },
{ footerDescription: "New footer", facebookUrl: "https://example.com" },
]);
} finally {
await client?.close();
backend.stop();
}
});