import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; import { describe, expect, it } from "vitest"; import { loadEnv } from "../src/config/env.js"; import { parsePortfolioFetchId, portfolioFetchId, } from "../src/integrations/gitea/portfolio.js"; import { registerPortfolioMcpTools } from "../src/mcp/portfolioTools.js"; import { createProjectBrainMcpServer } from "../src/mcp/server.js"; const env = loadEnv({ NODE_ENV: "test", DATABASE_URL: "postgresql://example.invalid/projectbrain", PROJECTBRAIN_DATA_SOURCE: "database", PROJECTBRAIN_OWNER_USERNAME: "owner", PROJECTBRAIN_OWNER_PASSWORD: "correct-horse-battery-staple", PROJECTBRAIN_AUTH_SECRET: "test-secret-with-at-least-thirty-two-characters", PROJECTBRAIN_MCP_ENABLED: "true", PROJECTBRAIN_PUBLIC_URL: "https://projectbrain.example.com", PROJECTBRAIN_GITEA_URL: "https://gitea.example.com", PROJECTBRAIN_GITEA_TOKEN: "a-token-with-at-least-twenty-characters", }); describe("read-only Gitea portfolio MCP projection", () => { it("round-trips canonical repository/ref/path fetch identifiers", () => { const id = portfolioFetchId( { owner: "Jens", repository: "Mediarr" }, "codex/example-branch", "src/a file.ts", ); expect(id).toBe( "gitea-repository://Jens/Mediarr/codex%2Fexample-branch/src%2Fa%20file.ts", ); expect(parsePortfolioFetchId(id)).toEqual({ owner: "Jens", repository: "Mediarr", ref: "codex/example-branch", path: "src/a file.ts", }); expect(() => parsePortfolioFetchId("gitea://project/main/file.ts")).toThrow( "MCP_FETCH_ID_INVALID", ); }); it("advertises portfolio tools as distinct read-only capabilities", async () => { const server = createProjectBrainMcpServer(env, "portfolio-contract-test"); registerPortfolioMcpTools(server, env, "portfolio-contract-test"); const client = new Client({ name: "portfolio-contract-test", version: "1.0.0" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); try { await server.connect(serverTransport); await client.connect(clientTransport); const listed = await client.listTools(); const tools = new Map(listed.tools.map((tool) => [tool.name, tool])); for (const name of [ "list_portfolio_repositories", "search_portfolio_repositories", "fetch_portfolio_file", ]) { const tool = tools.get(name); expect(tool, `${name} should be registered`).toBeDefined(); expect(tool?.annotations).toMatchObject({ readOnlyHint: true, destructiveHint: false, idempotentHint: true, }); } expect(tools.get("list_portfolio_repositories")?.annotations?.openWorldHint).toBe(true); expect(tools.get("search_portfolio_repositories")?.annotations?.openWorldHint).toBe(false); expect(tools.get("fetch_portfolio_file")?.annotations?.openWorldHint).toBe(false); // Existing delivery mutations remain separate, linked-project tools. expect(tools.has("write_feature_file")).toBe(true); expect(tools.has("search")).toBe(true); expect(tools.has("fetch")).toBe(true); } finally { await client.close(); await server.close(); } }); });