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.
53 lines
3.2 KiB
TypeScript
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("");
|
|
});
|
|
});
|