test(packages): add mcp-stdio-server.test.ts
This commit is contained in:
@@ -0,0 +1,63 @@
|
|||||||
|
import { describe, expect, it } from "bun:test";
|
||||||
|
import { PassThrough } from "node:stream";
|
||||||
|
import { runMcpStdioServer } from "./mcp";
|
||||||
|
|
||||||
|
describe("ast-grep MCP stdio server", () => {
|
||||||
|
it("#given Codex sends a content-length framed initialize #when stdio server handles it #then responds with a framed initialize result", async () => {
|
||||||
|
const input = new PassThrough();
|
||||||
|
const output = new PassThrough();
|
||||||
|
const received = nextOutput(output);
|
||||||
|
const server = runMcpStdioServer(input, output);
|
||||||
|
|
||||||
|
writeContentLengthFrame(input, {
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: 5,
|
||||||
|
method: "initialize",
|
||||||
|
params: {
|
||||||
|
protocolVersion: "2024-11-05",
|
||||||
|
capabilities: {},
|
||||||
|
clientInfo: { name: "codex", version: "0.0.0" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = parseContentLengthFrame(await received);
|
||||||
|
input.end();
|
||||||
|
await server;
|
||||||
|
|
||||||
|
expect(response).toEqual({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
id: 5,
|
||||||
|
result: {
|
||||||
|
capabilities: { tools: { listChanged: false } },
|
||||||
|
serverInfo: { name: "ast_grep", version: "0.1.0" },
|
||||||
|
protocolVersion: "2024-11-05",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function writeContentLengthFrame(input: PassThrough, message: unknown): void {
|
||||||
|
const body = JSON.stringify(message);
|
||||||
|
input.write(`Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextOutput(output: PassThrough): Promise<string> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
output.once("data", (chunk: Buffer | string) => {
|
||||||
|
resolve(String(chunk));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseContentLengthFrame(raw: string): unknown {
|
||||||
|
const separator = raw.indexOf("\r\n\r\n");
|
||||||
|
expect(separator).toBeGreaterThan(0);
|
||||||
|
const headers = raw.slice(0, separator);
|
||||||
|
const match = /^Content-Length: (\d+)$/im.exec(headers);
|
||||||
|
expect(match).not.toBeNull();
|
||||||
|
if (match === null) throw new TypeError(`Missing Content-Length header: ${raw}`);
|
||||||
|
const lengthValue = match[1];
|
||||||
|
if (lengthValue === undefined) throw new TypeError(`Invalid Content-Length header: ${raw}`);
|
||||||
|
const bodyStart = separator + "\r\n\r\n".length;
|
||||||
|
return JSON.parse(raw.slice(bodyStart, bodyStart + Number(lengthValue)));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user