Files
next-tjwater-agent/tests/runtime/mcpConfig.test.ts
T
jiang 75d8d526f9 feat(agent): add external MCP client support
Reuse OpenCode 2 HTTP/stdio clients with per-session read-only grants,
native approval and sanitized owner-scoped MCP status. Narrow the
environment inherited by trusted local MCP processes.

Validated in an isolated checkout with bun run check and 20 focused tests.
2026-10-08 18:36:07 +08:00

53 lines
3.2 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import { mcpLocalEnvironment, mcpReadonlyTools, normalizeMcpConfig } from "../../src/runtime/mcp-config.js";
import { canAutomaticallyApprove } from "../../src/routes/chatPermissionPolicy.js";
describe("MCP configuration and grants", () => {
it("defaults to HTTP auto negotiation and stdio legacy while retaining explicit timeouts and protocol", () => {
const config = normalizeMcpConfig({
timeout: { execution: 90_000 },
servers: {
mcp_http: { type: "remote", url: "https://example.com/mcp" },
mcp_stdio: { type: "local", command: ["bun", "server.ts"], timeout: { catalog: 2000 } },
mcp_modern: { type: "local", command: ["server"], protocol: "2026-07-28", codemode: false },
},
});
expect(config.timeout).toEqual({ startup: 10_000, catalog: 10_000, execution: 90_000 });
expect(config.servers.mcp_http.protocol).toBe("auto");
expect(config.servers.mcp_stdio).toMatchObject({ protocol: "legacy", timeout: { catalog: 2000 } });
expect(config.servers.mcp_modern).toMatchObject({ protocol: "2026-07-28", codemode: false });
expect(normalizeMcpConfig(undefined).servers).toEqual({});
for (const servers of [
{ unnamed: { type: "local", command: ["server"] } },
{ mcp_bad: { type: "remote", url: "file:///secret" } },
{ mcp_bad: { type: "remote", url: "https://user:secret@example.com/mcp" } },
{ mcp_bad: { type: "remote", url: "https://example.com", protocol: "future" } },
{ mcp_bad: { type: "local", command: [""] } },
]) expect(() => normalizeMcpConfig({ servers })).toThrow();
});
it("requires an exact service allow rule and keeps later denials and MCP confirmations effective", () => {
expect(mcpReadonlyTools({ permissions: [{ action: "mcp_*", resource: "*", effect: "allow" }] })).toEqual([]);
const permissions = [
{ action: "mcp_*", resource: "*", effect: "ask" },
{ action: "mcp_fixture_read", resource: "*", effect: "allow" },
{ action: "mcp_fixture_write", resource: "*", effect: "allow" },
{ action: "mcp_fixture_write", resource: "*", effect: "deny" },
];
expect(mcpReadonlyTools({ permissions })).toEqual(["mcp_fixture_read"]);
expect(mcpReadonlyTools({ permissions, agents: { instruction: { permissions: [{ action: "mcp_fixture_read", resource: "*", effect: "deny" }] } } })).toEqual([]);
expect(canAutomaticallyApprove("always", "mcp_fixture_write")).toBe(false);
expect(canAutomaticallyApprove("always", "opencode_read_mcp_resource")).toBe(false);
expect(canAutomaticallyApprove("always", "query_project_data")).toBe(true);
});
it("shadows inherited credentials and reserved overrides for a trusted stdio process", () => {
const env = mcpLocalEnvironment({ PATH: "/bin", HOME: "/home/test", DEEPSEEK_API_KEY: "TEST_ONLY", AGENT_INTERNAL_TOKEN: "TEST_ONLY", NODE_OPTIONS: "TEST_ONLY" }, {
MCP_API_TOKEN: "MCP_ONLY", AGENT_INTERNAL_TOKEN: "OVERRIDE", OPENCODE_CONFIG_CONTENT: "OVERRIDE",
});
expect(env.PATH).toBe("/bin");
expect(env.MCP_API_TOKEN).toBe("MCP_ONLY");
for (const key of ["DEEPSEEK_API_KEY", "AGENT_INTERNAL_TOKEN", "NODE_OPTIONS", "OPENCODE_CONFIG_CONTENT"]) expect(env[key]).toBe("");
});
});