Files
nocobase-mcp/test/tools.test.ts
T

98 lines
3.1 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import type { CrmService } from "../src/crm-service.js";
import type { ProjectService } from "../src/project-service.js";
import { createServer } from "../src/tools.js";
function fakeService(): CrmService {
return new Proxy(
{},
{
get:
(_target, property) =>
async (...args: unknown[]) => ({ operation: String(property), args }),
},
) as CrmService;
}
async function connected(readonly: boolean): Promise<{
client: Client;
close: () => Promise<void>;
}> {
const server = createServer({
crmService: fakeService(),
projectService: fakeService() as unknown as ProjectService,
readonly,
});
const client = new Client({ name: "test", version: "1" });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
return {
client,
close: async () => await Promise.all([client.close(), server.close()]).then(() => undefined),
};
}
test("read-only server exposes the stable 17-tool contract", async () => {
const connection = await connected(true);
try {
const result = await connection.client.listTools();
assert.deepEqual(
new Set(result.tools.map((tool) => tool.name)),
new Set([
"crm_health",
"crm_schema",
"crm_overview",
"crm_filter_options",
"resolve_owner",
"list_teams",
"resolve_team",
"get_team",
"team_crm_overview",
"list_customers",
"get_customer",
"list_contacts",
"list_followups",
"list_projects",
"get_project",
"get_project_timeline",
"list_stale_customers",
]),
);
} finally {
await connection.close();
}
});
test("write mode adds only fixed project create and update tools", async () => {
const connection = await connected(false);
try {
const names = (await connection.client.listTools()).tools.map((tool) => tool.name);
assert.ok(names.includes("create_project"));
assert.ok(names.includes("update_project"));
assert.ok(!names.some((name) => name.includes("destroy") || name.includes("delete")));
} finally {
await connection.close();
}
});
test("raw rows require explicit local policy opt-in", async () => {
const previous = process.env.NOCOBASE_MCP_ENABLE_RAW_DATA;
delete process.env.NOCOBASE_MCP_ENABLE_RAW_DATA;
const connection = await connected(true);
try {
const result = await connection.client.callTool({
name: "list_customers",
arguments: { include_raw: true },
});
const structured = result.structuredContent as { error: Record<string, unknown> };
assert.equal(structured.error.code, "raw_data_disabled");
} finally {
if (previous === undefined) delete process.env.NOCOBASE_MCP_ENABLE_RAW_DATA;
else process.env.NOCOBASE_MCP_ENABLE_RAW_DATA = previous;
await connection.close();
}
});