125 lines
4.6 KiB
TypeScript
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");
|
|
}
|
|
});
|
|
});
|