Ctrl K
ComunidadGuías…

tests/integration/mcp-stdio.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { afterAll, beforeAll, test } from "bun:test";
import assert from "node:assert/strict";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createSqliteDb, openDatabase } from "../../src/runtime/bun/sqlite";
import { runMcpStdio } from "../../src/runtime/bun/mcp-stdio";
import { serve } from "../../src/runtime/bun/server";
import { provisionWith } from "../contract/harness";

const directory = mkdtempSync(join(tmpdir(), "titen-mcp-stdio-"));
const cli = join(import.meta.dir, "../../src/runtime/bun/cli.ts");
let running: Awaited<ReturnType<typeof serve>>;
let key: string;

beforeAll(async () => {
  const dbPath = join(directory, "titen.db");
  running = await serve({ dbPath, port: 0, hostname: "127.0.0.1", quiet: true });
  const database = openDatabase(dbPath);
  key = (await provisionWith(createSqliteDb(database), { scopes: ["*"] })).key;
  database.close();
});

afterAll(async () => {
  await running.stop();
  rmSync(directory, { recursive: true, force: true });
});

const initialize = {
  jsonrpc: "2.0",
  id: 1,
  method: "initialize",
  params: { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "stdio-test", version: "1" } },
};
const initialized = { jsonrpc: "2.0", method: "notifications/initialized" };
const tools = { jsonrpc: "2.0", id: 2, method: "tools/list" };

async function http(message: unknown): Promise<string> {
  const response = await fetch(`${running.url}/mcp`, {
    method: "POST",
    headers: {
      accept: "application/json, text/event-stream",
      authorization: `Bearer ${key}`,
      "content-type": "application/json",
    },
    body: JSON.stringify(message),
  });
  return response.text();
}

test("the installed CLI bridges requests, drops notification replies, and exits on EOF", async () => {
  const process = Bun.spawn(["bun", cli, "mcp"], {
    env: { ...globalThis.process.env, TITEN_MCP_URL: `${running.url}/mcp`, TITEN_API_KEY: key },
    stdin: "pipe",
    stdout: "pipe",
    stderr: "pipe",
  });
  process.stdin.write(`${JSON.stringify(initialize)}\n${JSON.stringify(initialized)}\n${JSON.stringify(tools)}`);
  process.stdin.end();

  const [exitCode, stdout, stderr] = await Promise.all([
    Promise.race([process.exited, Bun.sleep(1_000).then(() => -1)]),
    new Response(process.stdout).text(),
    new Response(process.stderr).text(),
  ]);
  assert.equal(exitCode, 0, stderr);
  assert.equal(stderr, "");
  assert.deepEqual(stdout.trim().split("\n"), [await http(initialize), await http(tools)]);
  assert.doesNotMatch(`${stdout}${stderr}`, new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
});

test("parse and network failures are sanitized without ending the bridge", async () => {
  async function* input() {
    yield new TextEncoder().encode([
      JSON.stringify({ jsonrpc: "2.0", id: 7, method: "ping" }),
      JSON.stringify(initialized),
      "{",
      JSON.stringify({ jsonrpc: "2.0", id: 8, method: "tools/list" }),
    ].join("\n"));
  }
  const output: string[] = [];
  const logged: string[] = [];
  const secret = "never-print-this-key";
  const realError = console.error;
  console.error = (line: string) => void logged.push(line);
  try {
    await runMcpStdio({
      endpoint: "http://127.0.0.1:9/mcp",
      apiKey: secret,
      input: input(),
      write: (line) => output.push(line),
    });
  } finally {
    console.error = realError;
  }
  assert.deepEqual(output.map((line) => JSON.parse(line)), [
    { jsonrpc: "2.0", id: 7, error: { code: -32000, message: "Titen MCP request failed." } },
    { jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error." } },
    { jsonrpc: "2.0", id: 8, error: { code: -32000, message: "Titen MCP request failed." } },
  ]);
  // -32000 says only that something failed. A revoked key and a dead port were
  // the same sentence until the reason went somewhere readable. Three lines for
  // two replies: the notification fails too, and used to vanish entirely.
  assert.equal(logged.length, 3, logged.join("\n"));
  for (const line of logged) assert.match(line, /^titen: http:\/\/127\.0\.0\.1:9\/mcp failed: ./);
  assert.doesNotMatch(`${output.join("")}${logged.join("")}`, new RegExp(secret));
});

test("the API key reaches only the authorization header and is redacted upstream", async () => {
  async function* input() {
    yield new TextEncoder().encode(JSON.stringify({ jsonrpc: "2.0", id: 9, method: "ping" }));
  }
  const output: string[] = [];
  const secret = "header-only-key";
  await runMcpStdio({
    endpoint: "https://titen.example/mcp",
    apiKey: secret,
    input: input(),
    fetcher: async (_input, init) => {
      assert.equal(new Headers(init?.headers).get("authorization"), `Bearer ${secret}`);
      return new Response(JSON.stringify({ jsonrpc: "2.0", id: 9, result: { echo: secret } }));
    },
    write: (line) => output.push(line),
  });
  assert.deepEqual(JSON.parse(output[0]!), {
    jsonrpc: "2.0",
    id: 9,
    result: { echo: "[redacted]" },
  });
  assert.doesNotMatch(output.join(""), new RegExp(secret));
});

async function bridge(lines: unknown[], fetcher?: (input: string | URL, init?: RequestInit) => Promise<Response>) {
  async function* input() {
    yield new TextEncoder().encode(lines.map((line) => JSON.stringify(line)).join("\n"));
  }
  const output: string[] = [];
  await runMcpStdio({ endpoint: `${running.url}/mcp`, apiKey: key, input: input(), fetcher, write: (line) => output.push(line) });
  return output;
}

test("a batch on stdin gets the same one-line array that POST /mcp returns", async () => {
  const ping = { jsonrpc: "2.0", id: 1, method: "ping" };
  const batch = [ping, initialized, tools];
  const direct = JSON.parse(await http(batch));
  const output = await bridge([batch]);
  assert.equal(output.length, 1);
  const bridged = JSON.parse(output[0]!);
  assert.ok(Array.isArray(bridged));
  assert.deepEqual(bridged.map((reply: { id: unknown }) => reply.id), [1, 2]);
  assert.deepEqual(bridged, direct);
});

test("a notification-only batch writes nothing", async () => {
  assert.deepEqual(await bridge([[initialized, { jsonrpc: "2.0", method: "notifications/cancelled", params: {} }]]), []);
});

test("an initialize inside a batch sets the protocol header for later requests", async () => {
  const versions: Array<string | null> = [];
  const output = await bridge([[initialize, { jsonrpc: "2.0", id: 3, method: "ping" }], tools], async (input, init) => {
    versions.push(new Headers(init?.headers).get("mcp-protocol-version"));
    return fetch(input, init);
  });
  assert.equal(output.length, 2);
  assert.deepEqual(versions, [null, "2025-06-18"]);
});

test("a failed batch writes one error per request id", async () => {
  async function* input() {
    yield new TextEncoder().encode(JSON.stringify([
      { jsonrpc: "2.0", id: 4, method: "ping" },
      initialized,
      { jsonrpc: "2.0", id: "five", method: "tools/list" },
    ]));
  }
  const output: string[] = [];
  const realError = console.error;
  console.error = () => {};
  try {
    await runMcpStdio({ endpoint: "http://127.0.0.1:9/mcp", apiKey: "batch-key", input: input(), write: (line) => output.push(line) });
  } finally {
    console.error = realError;
  }
  assert.deepEqual(output.map((line) => JSON.parse(line)), [
    { jsonrpc: "2.0", id: 4, error: { code: -32000, message: "Titen MCP request failed." } },
    { jsonrpc: "2.0", id: "five", error: { code: -32000, message: "Titen MCP request failed." } },
  ]);
});

test("unsafe endpoints fail before fetch", async () => {
  async function* empty() {}
  for (const endpoint of [
    "file:///tmp/mcp",
    "http://user:pass@localhost/mcp",
    "http://localhost/mcp?token=secret",
    "http://localhost/mcp#secret",
    "http://localhost/not-mcp",
  ]) {
    let fetched = false;
    await assert.rejects(runMcpStdio({
      endpoint,
      apiKey: "safe-key",
      input: empty(),
      fetcher: async () => {
        fetched = true;
        return new Response();
      },
    }), /credential-free HTTP \/mcp endpoint/);
    assert.equal(fetched, false);
  }
});