|
| 1 | +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; |
| 2 | +import { tmpdir } from "node:os"; |
| 3 | +import { join } from "node:path"; |
| 4 | +import { afterEach, beforeEach, describe, expect, it } from "vitest"; |
| 5 | +import { loadMcpServers } from "./config.js"; |
| 6 | + |
| 7 | +describe("loadMcpServers", () => { |
| 8 | + let home: string; |
| 9 | + let cwd: string; |
| 10 | + |
| 11 | + beforeEach(() => { |
| 12 | + home = mkdtempSync(join(tmpdir(), "mcp-home-")); |
| 13 | + cwd = mkdtempSync(join(tmpdir(), "mcp-cwd-")); |
| 14 | + }); |
| 15 | + afterEach(() => { |
| 16 | + rmSync(home, { recursive: true, force: true }); |
| 17 | + rmSync(cwd, { recursive: true, force: true }); |
| 18 | + }); |
| 19 | + |
| 20 | + function write(root: string, obj: unknown): void { |
| 21 | + mkdirSync(join(root, ".codebase"), { recursive: true }); |
| 22 | + writeFileSync(join(root, ".codebase", "mcp.json"), JSON.stringify(obj), "utf8"); |
| 23 | + } |
| 24 | + |
| 25 | + it("returns [] when no config exists", () => { |
| 26 | + expect(loadMcpServers({ home, cwd })).toEqual([]); |
| 27 | + }); |
| 28 | + |
| 29 | + it("parses a stdio server with args + env", () => { |
| 30 | + write(home, { |
| 31 | + mcpServers: { |
| 32 | + postgres: { command: "uvx", args: ["mcp-server-postgres"], env: { DATABASE_URL: "postgres://x" } }, |
| 33 | + }, |
| 34 | + }); |
| 35 | + const servers = loadMcpServers({ home, cwd }); |
| 36 | + expect(servers).toHaveLength(1); |
| 37 | + expect(servers[0].name).toBe("postgres"); |
| 38 | + expect(servers[0].spec).toMatchObject({ |
| 39 | + command: "uvx", |
| 40 | + args: ["mcp-server-postgres"], |
| 41 | + env: { DATABASE_URL: "postgres://x" }, |
| 42 | + }); |
| 43 | + }); |
| 44 | + |
| 45 | + it("project config overrides user config on name collision", () => { |
| 46 | + write(home, { mcpServers: { fs: { command: "user-cmd" } } }); |
| 47 | + write(cwd, { mcpServers: { fs: { command: "project-cmd" } } }); |
| 48 | + const servers = loadMcpServers({ home, cwd }); |
| 49 | + expect(servers).toHaveLength(1); |
| 50 | + expect(servers[0].spec.command).toBe("project-cmd"); |
| 51 | + }); |
| 52 | + |
| 53 | + it("skips remote (url) servers — stdio only in v1", () => { |
| 54 | + write(home, { |
| 55 | + mcpServers: { |
| 56 | + remote: { url: "https://mcp.example.com" }, |
| 57 | + local: { command: "local-cmd" }, |
| 58 | + }, |
| 59 | + }); |
| 60 | + const servers = loadMcpServers({ home, cwd }); |
| 61 | + expect(servers.map((s) => s.name)).toEqual(["local"]); |
| 62 | + }); |
| 63 | + |
| 64 | + it("skips entries missing a command", () => { |
| 65 | + write(home, { mcpServers: { broken: { args: ["x"] } } }); |
| 66 | + expect(loadMcpServers({ home, cwd })).toEqual([]); |
| 67 | + }); |
| 68 | + |
| 69 | + it("tolerates invalid JSON without throwing", () => { |
| 70 | + mkdirSync(join(home, ".codebase"), { recursive: true }); |
| 71 | + writeFileSync(join(home, ".codebase", "mcp.json"), "{ not json", "utf8"); |
| 72 | + expect(loadMcpServers({ home, cwd })).toEqual([]); |
| 73 | + }); |
| 74 | +}); |
0 commit comments