Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
124 lines
4.0 KiB
TypeScript
124 lines
4.0 KiB
TypeScript
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<string, string>;
|
|
};
|
|
|
|
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\(/);
|
|
});
|
|
});
|