import { readFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { describe, expect, it } from "vitest"; import { EXAMPLE_ECHO_TOOL_NAME } from "../src/capabilities/index.js"; const testsDir = dirname(fileURLToPath(import.meta.url)); const projectRoot = resolve(testsDir, ".."); const stdioEntryPath = resolve(projectRoot, "src/stdio.ts"); const packageJsonPath = resolve(projectRoot, "package.json"); type StdioCommandConfig = { mcp?: { stdio?: { command?: string; args?: string[]; }; }; scripts?: Record; }; async function readStdioCommandConfig(): Promise<{ command: string; args: string[] }> { const packageJsonRaw = await readFile(packageJsonPath, "utf8"); const packageJson = JSON.parse(packageJsonRaw) as StdioCommandConfig; return { command: packageJson.mcp?.stdio?.command ?? "node", args: packageJson.mcp?.stdio?.args ?? [], }; } async function createConnectedStdioClient(): Promise<{ client: Client; transport: StdioClientTransport; }> { const stdioCommand = await readStdioCommandConfig(); const transport = new StdioClientTransport({ command: stdioCommand.command, args: stdioCommand.args, cwd: projectRoot, stderr: "pipe", }); const client = new Client( { name: "stdio-test-client", version: "0.0.0" }, { capabilities: {} }, ); await client.connect(transport); return { client, transport }; } describe("stdio entrypoint", () => { it("starts over stdio and keeps startup diagnostics on stderr", async () => { const stdioCommand = await readStdioCommandConfig(); const transport = new StdioClientTransport({ command: stdioCommand.command, args: stdioCommand.args, cwd: projectRoot, stderr: "pipe", }); const stderrMessages: string[] = []; const stderr = transport.stderr; stderr?.on("data", (chunk) => { stderrMessages.push(chunk.toString()); }); const client = new Client( { name: "stdio-test-client", version: "0.0.0" }, { capabilities: {} }, ); await client.connect(transport); const tools = await client.listTools(); expect(tools.tools.map((tool) => tool.name)).toContain(EXAMPLE_ECHO_TOOL_NAME); expect(stderrMessages.join("")).toContain("MCP stdio server started"); await client.close(); await transport.close(); }); it("rejects invalid tool calls over stdio transport", async () => { const { client, transport } = await createConnectedStdioClient(); const missingToolResult = await client.callTool({ name: "template.missing-tool", arguments: {}, }); const invalidInputResult = await client.callTool({ name: EXAMPLE_ECHO_TOOL_NAME, arguments: { uppercase: true }, }); expect(missingToolResult.isError).toBe(true); expect(JSON.stringify(missingToolResult.content)).toMatch(/not found|unknown/i); expect(invalidInputResult.isError).toBe(true); expect(JSON.stringify(invalidInputResult.content)).toMatch(/message|required|invalid/i); await client.close(); await transport.close(); }); it("publishes a non-npm stdio launch command for MCP clients", async () => { const packageJsonRaw = await readFile(packageJsonPath, "utf8"); const packageJson = JSON.parse(packageJsonRaw) as StdioCommandConfig; expect(packageJson.mcp?.stdio?.command).not.toMatch(/^npm|^npx/); expect(packageJson.scripts?.["start:stdio"]).toBeUndefined(); expect(packageJson.mcp?.stdio?.command).toBe("node"); expect(packageJson.mcp?.stdio?.args).toEqual([ "./node_modules/tsx/dist/cli.mjs", "src/stdio.ts", ]); }); it("does not write non-protocol logs to stdout from source entry", async () => { const source = await readFile(stdioEntryPath, "utf8"); expect(source).not.toMatch(/console\.log\(/); expect(source).not.toMatch(/process\.stdout\.write\(/); }); });