Files
cokaremote/test/tool-metadata.test.ts

125 lines
4.6 KiB
TypeScript

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { describe, expect, it } from "vitest";
import { loadConfig } from "../src/config.js";
import { createMcpServer, createServices } from "../src/mcp-server.js";
async function withClient<T>(
env: NodeJS.ProcessEnv,
operation: (client: Client) => Promise<T> | T,
): Promise<T> {
const config = loadConfig(env, "/tmp");
const server = createMcpServer(config, createServices(config));
const client = new Client({ name: "tool-metadata-test", version: "1.0.0" });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
try {
return await operation(client);
} finally {
await client.close();
await server.close();
}
}
async function listTools(env: NodeJS.ProcessEnv) {
return withClient(env, async (client) => (await client.listTools()).tools);
}
describe("tool authentication metadata", () => {
it("advertises the OAuth scope on every tool when OAuth is enabled", async () => {
const tools = await listTools({
MCP_OAUTH_ENABLED: "true",
MCP_OAUTH_APPROVAL_KEY: "approval-key",
MCP_PUBLIC_URL: "https://mcp.example.com",
MCP_OAUTH_ISSUER: "https://mcp.example.com",
MCP_OAUTH_RESOURCE: "https://mcp.example.com/mcp",
});
expect(tools).toHaveLength(24);
for (const tool of tools) {
expect(tool._meta, tool.name).toEqual({
securitySchemes: [{ type: "oauth2", scopes: ["mcp:tools"] }],
});
}
});
it("does not infer noauth from the internal authentication bypass", async () => {
const tools = await listTools({ MCP_ALLOW_NO_AUTH: "true" });
expect(tools).toHaveLength(24);
for (const tool of tools) {
expect(tool._meta, tool.name).toBeUndefined();
}
});
it("does not mislabel static bearer authentication as noauth or OAuth", async () => {
const tools = await listTools({ MCP_AUTH_TOKEN: "static-secret" });
expect(tools).toHaveLength(24);
for (const tool of tools) {
expect(tool._meta, tool.name).toBeUndefined();
}
});
});
describe("client-facing metadata accuracy", () => {
it("does not mislabel the MCP service origin as an implementation website", async () => {
const serverInfo = await withClient(
{
MCP_AUTH_TOKEN: "static-secret",
MCP_PUBLIC_URL: "https://mcp.example.com",
},
(client) => client.getServerVersion(),
);
expect(serverInfo).toMatchObject({ name: "cokacremote", version: "0.1.0" });
expect(serverInfo?.websiteUrl).not.toBe("https://mcp.example.com");
});
it("describes every tool and every input field", async () => {
const tools = await listTools({ MCP_AUTH_TOKEN: "static-secret" });
expect(tools).toHaveLength(24);
for (const tool of tools) {
expect(tool.title?.trim().length, tool.name).toBeGreaterThan(0);
expect(tool.description?.trim().length, tool.name).toBeGreaterThan(0);
for (const [fieldName, schema] of Object.entries(
tool.inputSchema.properties ?? {},
)) {
const description = (schema as { description?: unknown }).description;
expect(
typeof description === "string" ? description.trim().length : 0,
`${tool.name}.${fieldName}`,
).toBeGreaterThan(0);
}
}
});
it("describes process timing, session, polling, and escalation semantics exactly", async () => {
const tools = await listTools({ MCP_AUTH_TOKEN: "static-secret" });
const byName = new Map(tools.map((tool) => [tool.name, tool]));
const execCommand = byName.get("exec_command")!;
const runScript = byName.get("run_script")!;
const writeStdin = byName.get("write_stdin")!;
const terminateProcess = byName.get("terminate_process")!;
expect(execCommand.description).toContain("always returns a process session ID");
expect(writeStdin.description).toContain("greater than afterSeq");
expect(terminateProcess.description).toContain("SIGINT and SIGTERM");
for (const tool of [execCommand, runScript]) {
const properties = tool.inputSchema.properties as Record<
string,
{ description?: string }
>;
expect(properties.timeoutMs?.description).toContain("sending SIGTERM");
expect(properties.timeoutMs?.description).toContain("sent SIGKILL");
expect(properties.timeoutMs?.description).not.toContain("Maximum runtime");
expect(properties.yieldTimeMs?.description).toContain("wait for");
expect(properties.yieldTimeMs?.description).toContain("to exit");
}
});
});