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:
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
import { Plugin, Rpc } from "../../.opencode/node_modules/@opencode/plugin/dist/promise/index.js";
|
||||
import { z } from "../../.opencode/node_modules/zod/index.js";
|
||||
|
||||
// Loaded only by the opt-in isolated smoke script, never by the Agent service.
|
||||
export default Plugin.define({
|
||||
id: "tjwater.mcp-fixture",
|
||||
async setup(ctx) {
|
||||
// Materialize the native session toolbox, then stop before any model call.
|
||||
await ctx.session.hook("context", () => { throw new Error("MCP fixture stops before model request"); });
|
||||
const evaluated: string[] = [];
|
||||
await ctx.permission.hook("evaluate", (event) => { evaluated.push(event.action); });
|
||||
await ctx.rpc.register(Rpc.define({
|
||||
id: "tjwater.mcp-fixture", events: {}, methods: {
|
||||
call: { input: z.object({ session_id: z.string(), tool: z.enum(["mcp_stdio_echo", "mcp_http_write"]), value: z.string() }), output: z.object({ content: z.unknown(), evaluated: z.array(z.string()), error: z.string().optional() }) },
|
||||
},
|
||||
}), {
|
||||
call: async (input, context) => {
|
||||
const tool = (await ctx.tool.list()).find((tool) => tool.id === input.tool);
|
||||
if (!tool) throw new Error("fixture tool missing");
|
||||
try {
|
||||
const result = await tool.execute({ value: input.value }, { sessionID: input.session_id as never, id: "call-mcp-fixture" as never, messageID: "msg-mcp-fixture" as never, agent: "instruction" as never, signal: context.signal, progress: async () => {} });
|
||||
return { content: result.content ?? [], evaluated };
|
||||
} catch (error) { return { content: [], evaluated, error: String(error) }; }
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
import { createInterface } from "node:readline";
|
||||
|
||||
// A minimal legacy stdio fixture. No third-party SDK or external service.
|
||||
for await (const line of createInterface({ input: process.stdin })) {
|
||||
const request = JSON.parse(line);
|
||||
if (request.id === undefined) continue;
|
||||
let result: unknown;
|
||||
if (request.method === "initialize") result = { protocolVersion: request.params.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: "tjwater-mcp-fixture", version: "1" } };
|
||||
else if (request.method === "tools/list") result = { tools: [{ name: "echo", description: "Read-only fixture echo", inputSchema: { type: "object", properties: { value: { type: "string" } }, required: ["value"] }, annotations: { readOnlyHint: true } }] };
|
||||
else if (request.method === "tools/call") result = { content: [{ type: "text", text: JSON.stringify({ value: request.params.arguments.value, internal_token_empty: !process.env.AGENT_INTERNAL_TOKEN, model_key_empty: !process.env.DEEPSEEK_API_KEY, own_token_present: process.env.MCP_FIXTURE_TOKEN === "MCP_ONLY" }) }] };
|
||||
else if (request.method === "ping") result = {};
|
||||
else {
|
||||
process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id: request.id, error: { code: -32601, message: "Method not found" } })}\n`);
|
||||
continue;
|
||||
}
|
||||
process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id: request.id, result })}\n`);
|
||||
}
|
||||
Reference in New Issue
Block a user