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.
This commit is contained in:
@@ -154,7 +154,11 @@ describe("OpencodeRuntimeAdapter.warmup", () => {
|
||||
remove: async () => { calls.push("session.remove"); },
|
||||
},
|
||||
model: { list: async () => { calls.push("model.list"); return { data: [{ providerID, id, enabled: true, capabilities: { tools: true } }] }; } },
|
||||
rpc: { call: async () => { calls.push("tool.list"); return { output: toolIds.filter((id) => !missing.includes(id)).map((id) => ({ id, direct: true, sandboxed: id === "shell", workspaceRoot: root })) }; } },
|
||||
rpc: { call: async (input: { method: string }) => {
|
||||
if (input.method === "mcp") { calls.push("mcp.list"); return { output: { servers: [], configuration_errors: [] } }; }
|
||||
calls.push("tool.list");
|
||||
return { output: toolIds.filter((id) => !missing.includes(id)).map((id) => ({ id, direct: true, sandboxed: id === "shell", workspaceRoot: root })) };
|
||||
} },
|
||||
} as unknown as OpencodeClient;
|
||||
const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), { ensureClient: async () => client }) as OpencodeRuntimeAdapter;
|
||||
return { runtime, calls, cleanup: async () => { config.CONVERSATION_WORKSPACE_ROOT = previousRoot; await rm(root, { recursive: true, force: true }); } };
|
||||
@@ -162,7 +166,7 @@ describe("OpencodeRuntimeAdapter.warmup", () => {
|
||||
it("verifies business tools and model before reporting ready and removes the warmup session", async () => {
|
||||
const { runtime, calls, cleanup } = await createWarmupRuntime();
|
||||
try { await runtime.warmup();
|
||||
expect(calls).toEqual(["health", "session.create", "tool.list", "model.list", "session.remove"]); } finally { await cleanup(); }
|
||||
expect(calls).toEqual(["health", "session.create", "tool.list", "mcp.list", "model.list", "session.remove"]); } finally { await cleanup(); }
|
||||
});
|
||||
it("fails readiness if a business tool was lost and still cleans up", async () => {
|
||||
const { runtime, calls, cleanup } = await createWarmupRuntime(["render_scada_analysis"]);
|
||||
@@ -172,6 +176,19 @@ describe("OpencodeRuntimeAdapter.warmup", () => {
|
||||
});
|
||||
|
||||
describe("conversation workspace binding", () => {
|
||||
it("reads MCP status without migrating a legacy session or creating a workspace", async () => {
|
||||
let location: unknown;
|
||||
const runtime = Object.assign(Object.create(OpencodeRuntimeAdapter.prototype), {
|
||||
ensureClient: async () => ({
|
||||
session: { get: async () => ({ location: { directory: process.cwd() } }), move: async () => { throw new Error("GET must not move sessions"); } },
|
||||
rpc: { call: async (input: { location: unknown }) => { location = input.location; return { output: { servers: [], configuration_errors: [] } }; } },
|
||||
}),
|
||||
ensureSessionWorkspace: async () => { throw new Error("GET must not create workspaces"); },
|
||||
}) as OpencodeRuntimeAdapter;
|
||||
expect(await runtime.mcpStatus("legacy-session")).toEqual({ servers: [], configuration_errors: [] });
|
||||
expect(location).toEqual({ directory: process.cwd() });
|
||||
});
|
||||
|
||||
it("coalesces old-session migration and restores its persisted directory", async () => {
|
||||
const previousRoot = config.CONVERSATION_WORKSPACE_ROOT;
|
||||
const root = await mkdtemp(join(tmpdir(), "runtime-workspace-test-"));
|
||||
|
||||
Reference in New Issue
Block a user