import assert from "node:assert/strict"; import express from "express"; import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { startOpencode2 } from "../../src/runtime/opencode2-server.js"; import { serviceOpencodeConfig } from "../../src/runtime/opencode2-config.js"; import { mcpRuntimeReportSchema } from "../../src/runtime/mcp-config.js"; import { registerMcpPolicyBridge } from "../../src/routes/mcpRoutes.js"; import { setRuntimeSessionContext, removeRuntimeSessionContext } from "../../src/runtime/sessionContext.js"; // Opt-in: bun run tests/integration/mcp-smoke.ts. No model request is made. const root = await mkdtemp(join(tmpdir(), "tjwater-mcp-smoke-")); const pluginDirectory = join(root, "fixture-plugin"); await mkdir(pluginDirectory); await writeFile(join(pluginDirectory, "package.json"), JSON.stringify({ name: "tjwater-mcp-fixture", type: "module", exports: "./index.ts" })); await writeFile(join(pluginDirectory, "index.ts"), `export { default } from ${JSON.stringify(resolve("tests/fixtures/mcp-call-plugin.ts"))};\n`); const app = express(); app.use(express.json()); registerMcpPolicyBridge(app, { internalToken: "MCP_TEST_ONLY", runtime: { mcpReadonlyTools: () => ["mcp_stdio_echo"] } }); const wire: { method: string; version?: string; header?: string }[] = []; app.post("/mcp", (req, res) => { const request = req.body; wire.push({ method: request.method, version: request.params?._meta?.["io.modelcontextprotocol/protocolVersion"], header: req.header("Mcp-Method") }); const common = { resultType: "complete", ttlMs: 0, cacheScope: "private", _meta: { "io.modelcontextprotocol/serverInfo": { name: "tjwater-modern-fixture", version: "1" } } }; let result: unknown; if (request.method === "server/discover") result = { ...common, supportedVersions: ["2026-07-28"], capabilities: { tools: {} } }; else if (request.method === "tools/list") result = { ...common, tools: [{ name: "write", description: "Unapproved test tool", inputSchema: { type: "object", properties: { value: { type: "string" } } }, annotations: { readOnlyHint: true } }] }; else { res.status(400).json({ jsonrpc: "2.0", id: request.id, error: { code: -32601, message: "Method not found" } }); return; } res.json({ jsonrpc: "2.0", id: request.id, result }); }); const bridge = app.listen(0, "127.0.0.1"); await new Promise((resolve) => bridge.once("listening", resolve)); const baseUrl = `http://127.0.0.1:${(bridge.address() as { port: number }).port}`; let native: Awaited> | undefined; let sessionId: string | undefined; try { const source = JSON.parse(await readFile("opencode.json", "utf8")); const nativeConfig = serviceOpencodeConfig({ ...source, plugins: [pluginDirectory], mcp: { servers: { mcp_http: { type: "remote", url: `${baseUrl}/mcp`, oauth: false }, mcp_stdio: { type: "local", command: [process.execPath, resolve("tests/fixtures/mcp-server.ts")], environment: { MCP_FIXTURE_TOKEN: "MCP_ONLY" } }, }, }, permissions: [...source.permissions, { action: "mcp_stdio_echo", resource: "*", effect: "allow" }] }); native = await startOpencode2({ command: process.env.OPENCODE_COMMAND ?? "opencode2", hostname: "127.0.0.1", port: 0, timeout: 30_000, config: nativeConfig, env: { AGENT_INTERNAL_TOKEN: "MCP_TEST_ONLY", TJWATER_AGENT_INTERNAL_BASE_URL: baseUrl, TJWATER_AGENT_WORKSPACE_ROOT: join(root, "workspaces"), DEEPSEEK_API_KEY: "TEST_ONLY_NEVER_USED", XDG_DATA_HOME: join(root, "data"), XDG_CONFIG_HOME: join(root, "config"), XDG_CACHE_HOME: join(root, "cache"), XDG_STATE_HOME: join(root, "state"), } }); const session = await native.client.session.create({ location: { directory: process.cwd() }, agent: "instruction" }); sessionId = session.id; setRuntimeSessionContext({ sessionId, clientSessionId: sessionId, actorKey: "test", projectKey: "test", traceId: "test", purpose: "task_execution" }); await native.client.session.prompt({ sessionID: sessionId, text: "Materialize fixture toolbox only; the fixture context hook stops the model request." }); await native.client.session.wait({ sessionID: sessionId }, { signal: AbortSignal.timeout(20_000) }); const readReport = async () => mcpRuntimeReportSchema.parse((await native!.client.rpc.call({ rpcID: "tjwater.tools", method: "mcp", input: {}, location: session.location }, { signal: AbortSignal.timeout(10_000) })).output); let report = await readReport(); const deadline = Date.now() + 5000; while (report.servers.flatMap((server) => server.tools).length < 2 && Date.now() < deadline) { // Native late connection updates debounce catalog publication by 100 ms. await new Promise((resolve) => setTimeout(resolve, 100)); report = await readReport(); } assert.deepEqual(report.configuration_errors, []); assert.deepEqual(report.servers.map(({ name, status, configured_protocol }) => ({ name, status, configured_protocol })), [ { name: "mcp_http", status: "connected", configured_protocol: "auto" }, { name: "mcp_stdio", status: "connected", configured_protocol: "legacy" }, ]); assert(wire.some((request) => request.method === "tools/list" && request.version === "2026-07-28" && request.header === "tools/list")); assert.deepEqual(report.servers.flatMap((server) => server.tools).sort(), ["mcp_http_write", "mcp_stdio_echo"]); const result = await native.client.rpc.call({ rpcID: "tjwater.mcp-fixture", method: "call", input: { session_id: sessionId, tool: "mcp_stdio_echo", value: "mcp-ok" }, location: session.location }, { signal: AbortSignal.timeout(10_000) }); const output = JSON.stringify(result.output); assert.equal((result.output as { error?: string }).error, undefined); assert((result.output as { evaluated: string[] }).evaluated.includes("mcp_stdio_echo"), "native MCP execution must evaluate permission"); for (const expected of ["mcp-ok", "internal_token_empty", "model_key_empty", "own_token_present"]) assert(output.includes(expected)); assert(!output.includes("false"), "stdio must not inherit service credentials"); setRuntimeSessionContext({ sessionId, clientSessionId: sessionId, actorKey: "test", projectKey: "test", traceId: "test", purpose: "ordinary_chat" }); const writeAttempt = native.client.rpc.call({ rpcID: "tjwater.mcp-fixture", method: "call", input: { session_id: sessionId, tool: "mcp_http_write", value: "must-not-run" }, location: session.location }, { signal: AbortSignal.timeout(5000) }); // Confirm a real native permission request, then reject before the wire call. let pending = await native.client.permission.list({ sessionID: sessionId }); const approvalDeadline = Date.now() + 2000; while (!pending.some((request) => request.action === "mcp_http_write") && Date.now() < approvalDeadline) { await new Promise((resolve) => setTimeout(resolve, 50)); pending = await native.client.permission.list({ sessionID: sessionId }); } const request = pending.find((request) => request.action === "mcp_http_write"); assert(request, "unapproved MCP tool must create a native permission request"); await native.client.permission.reply({ sessionID: sessionId, requestID: request.id, decision: "reject" }); const writeResult = await writeAttempt; assert((writeResult.output as { error?: string }).error); assert(!wire.some((request) => request.method === "tools/call"), "rejected HTTP tool must not execute"); console.log("MCP smoke passed: modern HTTP discovery, legacy stdio call, native approval/rejection and credential isolation; no model request."); } finally { if (sessionId) { removeRuntimeSessionContext(sessionId); await native?.client.session.remove({ sessionID: sessionId }).catch(() => {}); } native?.close(); if (native) await native.exited; await new Promise((resolve) => bridge.close(() => resolve())); await rm(root, { recursive: true, force: true }); }