Ctrl K
ComunidadGuías…

web/e2e/specs/mcp-depth.spec.ts

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

Archivo original ↗Preguntar a KiBot
import { describe, expect, it } from "vitest";

import { E2E_PROJECT_ID, MCP_URL, makeApiClient, mcpHeaders, mintToken } from "../helpers";

const TOKEN = mintToken();

const rpc = async <T = unknown>(method: string, params: unknown, id = 1) => {
  const res = await fetch(MCP_URL, {
    method: "POST",
    headers: mcpHeaders(TOKEN),
    body: JSON.stringify({ jsonrpc: "2.0", id, method, params }),
  });
  return { status: res.status, body: (await res.json()) as T };
};

const api = makeApiClient(TOKEN);

const json = <T = unknown>(body: T) => ({ body: JSON.stringify(body) });

type ToolCallResponse = {
  result?: { content?: { type: string; text?: string }[] };
  error?: { message?: string };
};

const callTool = async (name: string, arguments_: Record<string, unknown>) => {
  const res = await rpc<ToolCallResponse>("tools/call", { name, arguments: arguments_ });
  expect(res.status).toBe(200);
  return res.body;
};

describe("mcp tools + resources (depth)", () => {
  it("creates, searches, reads a doc — then reads it as a resource", async () => {
    const marker = `mcp-e2e-${Date.now()}`;
    const title = `${marker} runbook`;

    const created = await callTool("doc_create", {
      project_id: E2E_PROJECT_ID,
      title,
      body: `# ${marker}\n\nBody written through MCP.`,
    });
    expect(created.error).toBeUndefined();
    const doc = JSON.parse(created.result!.content![0].text!) as { id: string; title: string; body: string };
    expect(doc.title).toBe(title);
    expect(doc.body).toContain(marker);

    try {
      // doc_list with a query finds the doc by its unique marker; the FTS index updates on the doc.created event, so poll briefly.
      let hit = false;
      for (let attempt = 0; attempt < 10 && !hit; attempt++) {
        const searched = await callTool("doc_list", { query: marker, limit: 5 });
        expect(searched.error).toBeUndefined();
        const page = JSON.parse(searched.result!.content![0].text!) as { items: { id: string }[] };
        hit = page.items.some((h) => h.id === doc.id);
        if (!hit) await new Promise((resolve) => setTimeout(resolve, 300));
      }
      expect(hit).toBe(true);

      const fetched = await callTool("doc_get", { id: doc.id });
      expect(fetched.error).toBeUndefined();
      expect(fetched.result!.content![0].text).toContain(marker);

      // resources/templates/list advertises the docs template, and resources/read serves the doc back as markdown.
      const listed = await rpc<{
        result?: { resourceTemplates?: { uriTemplate: string }[] };
      }>("resources/templates/list", {});
      expect(listed.status).toBe(200);
      const templates = listed.body.result?.resourceTemplates?.map((t) => t.uriTemplate) ?? [];
      expect(templates).toContain("docs://{id}");

      const read = await rpc<{ result?: { contents?: { uri: string; text: string }[] } }>(
        "resources/read",
        { uri: `docs://${doc.id}` },
      );
      expect(read.status).toBe(200);
      const contents = read.body.result?.contents ?? [];
      expect(contents.some((c) => c.uri === `docs://${doc.id}` && c.text.includes(marker))).toBe(true);
    } finally {
      await api(`/api/docs/${doc.id}`, { method: "DELETE" });
    }
  });

  it("reads a ticket through the tickets:// resource", async () => {
    const marker = `mcp-ticket-${Date.now()}`;
    const ticket = await api<{ id: string }>("/api/tickets", {
      method: "POST",
      ...json({ title: marker, body: "ticket body", project_id: "e2e00000-0000-4000-8000-000000000010" }),
    });
    expect(ticket.status).toBe(201);

    try {
      const read = await rpc<{ result?: { contents?: { uri: string; text: string }[] } }>(
        "resources/read",
        { uri: `tickets://${ticket.body!.id}` },
      );
      expect(read.status).toBe(200);
      const contents = read.body.result?.contents ?? [];
      expect(contents.some((c) => c.text.includes(marker))).toBe(true);
    } finally {
      await api(`/api/tickets/${ticket.body!.id}`, { method: "DELETE" });
    }
  });
});