111 lines
3.1 KiB
TypeScript
Vendored
111 lines
3.1 KiB
TypeScript
Vendored
import assert from "node:assert/strict";
|
|
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
import { mkdtemp, rm, symlink } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { test } from "vitest";
|
|
|
|
const packageDir = fileURLToPath(new URL("..", import.meta.url));
|
|
const mcpBin = fileURLToPath(new URL("../dist/index.js", import.meta.url));
|
|
|
|
type InitializeResponse = {
|
|
id: number;
|
|
result: {
|
|
serverInfo: {
|
|
name: string;
|
|
};
|
|
};
|
|
};
|
|
|
|
test("responds to initialize when invoked through an npm-style symlink", async () => {
|
|
const bin = await symlinkedMcpServer();
|
|
let child: ChildProcessWithoutNullStreams | undefined;
|
|
try {
|
|
child = spawn(process.execPath, [bin.path], {
|
|
cwd: packageDir,
|
|
env: { ...process.env },
|
|
});
|
|
|
|
const responsePromise = readJsonRpcResponse(child, 5000);
|
|
child.stdin.write(
|
|
encodeMessage({
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "initialize",
|
|
params: {
|
|
protocolVersion: "2024-11-05",
|
|
capabilities: {},
|
|
clientInfo: { name: "dbx-test", version: "0.0.0" },
|
|
},
|
|
}),
|
|
);
|
|
|
|
const response = await responsePromise;
|
|
|
|
assert.equal(response.id, 1);
|
|
assert.equal(response.result.serverInfo.name, "dbx");
|
|
} finally {
|
|
child?.kill();
|
|
await rm(bin.dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
async function symlinkedMcpServer() {
|
|
const dir = await mkdtemp(join(tmpdir(), "dbx-mcp-bin-"));
|
|
const path = join(dir, "dbx-mcp-server");
|
|
await symlink(mcpBin, path);
|
|
return { dir, path };
|
|
}
|
|
|
|
function encodeMessage(payload: unknown): string {
|
|
return `${JSON.stringify(payload)}\n`;
|
|
}
|
|
|
|
function readJsonRpcResponse(child: ChildProcessWithoutNullStreams, timeoutMs: number): Promise<InitializeResponse> {
|
|
return new Promise((resolve, reject) => {
|
|
let stdout = "";
|
|
let stderr = "";
|
|
|
|
const timeout = setTimeout(() => {
|
|
cleanup();
|
|
reject(new Error(`Timed out waiting for initialize response. stderr: ${stderr}`));
|
|
}, timeoutMs);
|
|
|
|
const cleanup = () => {
|
|
clearTimeout(timeout);
|
|
child.stdout.off("data", onStdout);
|
|
child.stderr.off("data", onStderr);
|
|
child.off("exit", onExit);
|
|
child.off("error", onError);
|
|
};
|
|
|
|
const onStdout = (chunk: Buffer) => {
|
|
stdout += chunk.toString("utf-8");
|
|
const lineEnd = stdout.indexOf("\n");
|
|
if (lineEnd === -1) return;
|
|
cleanup();
|
|
resolve(JSON.parse(stdout.slice(0, lineEnd)));
|
|
};
|
|
|
|
const onStderr = (chunk: Buffer) => {
|
|
stderr += chunk.toString("utf-8");
|
|
};
|
|
|
|
const onExit = (code: number | null, signal: NodeJS.Signals | null) => {
|
|
cleanup();
|
|
reject(new Error(`MCP server exited before initialize response. code=${code} signal=${signal} stderr: ${stderr}`));
|
|
};
|
|
|
|
const onError = (error: Error) => {
|
|
cleanup();
|
|
reject(error);
|
|
};
|
|
|
|
child.stdout.on("data", onStdout);
|
|
child.stderr.on("data", onStderr);
|
|
child.on("exit", onExit);
|
|
child.on("error", onError);
|
|
});
|
|
}
|