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 { assertWebSearchReady } from "../../src/runtime/web-search-check.js"; import { QUERY_MCP_TOOL_IDS, LOCAL_QUERY_TOOL_IDS } from "../../src/runtime/product-mcp-tools.js"; import { writeQueryMcpConnection } from "../../src/runtime/query-mcp-connection.js"; import { startQueryMcpServer } from "../../src/mcp/query-server.js"; import { registerMcpPolicyBridge } from "../../src/routes/mcpRoutes.js"; import { setRuntimeSessionContext, removeRuntimeSessionContext } from "../../src/runtime/sessionContext.js"; // Native discovery/execution only. Its fixture context hook stops all inference. const root = await mkdtemp(join(tmpdir(), "tjwater-native-query-mcp-")); const previousPath = process.env.TJWATER_AGENT_MCP_CONNECTION_PATH; const longCalculation = process.argv.includes("--long-calculation"); const pluginDirectory = join(root, "fixture-plugin"); await mkdir(pluginDirectory); await writeFile(join(pluginDirectory, "package.json"), JSON.stringify({ name: "tjwater-query-fixture", type: "module", exports: "./index.ts" })); await writeFile(join(pluginDirectory, "index.ts"), `export { default } from ${JSON.stringify(resolve(".opencode/test-fixtures/mcp-call-plugin.ts"))};\n`); const app = express(); app.use(express.json()); registerMcpPolicyBridge(app, { internalToken: "MCP_TEST_ONLY", runtime: { mcpReadonlyTools: () => QUERY_MCP_TOOL_IDS } }); const bridge = app.listen(0, "127.0.0.1"); await new Promise((resolve) => bridge.once("listening", resolve)); let native: Awaited> | undefined; let sessionId: string | undefined; const receivedSessions: string[] = []; const query = startQueryMcpServer({ internalToken: "MCP_TEST_ONLY", productClient: longCalculation ? { query: async (_id, _owner, name, args) => { assert.equal(name, "run_simulation"); assert.equal(args.timeout_seconds, 90); await Bun.sleep(65_000); return { content: [{ type: "text", text: "isolated long calculation completed" }] }; } } : null, productTools: longCalculation ? [{ name: "run_simulation", inputSchema: { type: "object", properties: { timeout_seconds: { type: "integer", minimum: 1, maximum: 120, default: 120 }, }, additionalProperties: false } }] : [], resolveReader: async () => null, executeLocal: async (name, args, context, signal) => { receivedSessions.push(context.sessionId); if (name === "todoread") { return (await native!.client.rpc.call({ rpcID: "tjwater.tools", method: "read_todos", input: { session_id: context.sessionId }, location: { directory: context.workspaceDirectory! } }, { signal })).output; } return { query: args.query, actor: context.actorKey }; }, }); writeQueryMcpConnection(join(root, "sessions"), query.url, query.executionTimeoutMs); try { const source = JSON.parse(await readFile("opencode.json", "utf8")); if (longCalculation) source.permissions = [...(source.permissions ?? []), { action: "mcp_tjwater_run_simulation", resource: "*", effect: "allow", }]; native = await startOpencode2({ command: process.env.OPENCODE_COMMAND ?? "opencode2", hostname: "127.0.0.1", port: 0, timeout: 30_000, config: serviceOpencodeConfig({ ...source, plugins: [pluginDirectory], mcp: { servers: {} }, websearch: { provider: "fixture-search" } }), env: { AGENT_INTERNAL_TOKEN: "MCP_TEST_ONLY", TJWATER_AGENT_INTERNAL_BASE_URL: `http://127.0.0.1:${(bridge.address() as { port: number }).port}`, TJWATER_AGENT_MCP_CONNECTION_PATH: process.env.TJWATER_AGENT_MCP_CONNECTION_PATH!, TJWATER_AGENT_WORKSPACE_ROOT: join(root, "workspaces"), OPENCODE_DB: join(root, "data", "opencode.db"), 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: "fixture-actor", projectKey: "personal-fixture", traceId: "test", purpose: "ordinary_chat", workspaceDirectory: session.location.directory }); await native.client.session.prompt({ sessionID: sessionId, text: "Materialize fixture tools; stop before inference." }); await native.client.session.wait({ sessionID: sessionId }, { signal: AbortSignal.timeout(20_000) }); const deadline = Date.now() + 10_000; let tools: { id: string; direct: boolean }[] = []; do { tools = (await native.client.rpc.call({ rpcID: "tjwater.tools", method: "list", input: {}, location: session.location })).output as typeof tools; if (LOCAL_QUERY_TOOL_IDS.every((id) => tools.some((tool) => tool.id === id && tool.direct))) break; await new Promise((resolve) => setTimeout(resolve, 100)); } while (Date.now() < deadline); assert(LOCAL_QUERY_TOOL_IDS.every((id) => tools.some((tool) => tool.id === id && tool.direct)), "native MCP catalog must expose every independent query"); assert(tools.some((tool) => tool.id === "websearch"), "native websearch must remain registered"); await assertWebSearchReady(native.client, session.location.directory); assert(!tools.some((tool) => tool.id.startsWith("browser_")), "unused native browser automation tools must not be registered"); const plugins = await native.client.plugin.list({ location: session.location }); assert(!plugins.data.some((plugin) => plugin.id === "opencode.browser" && plugin.state.status === "active"), "native browser plugin must be disabled"); for (const action of ["browser", "browser_click"]) { assert.equal((await native.client.permission.create({ sessionID: sessionId, action, resources: ["*"], save: [] })).effect, "deny"); } for (const action of ["present_answer", "show_chart"]) { assert.equal((await native.client.permission.create({ sessionID: sessionId, action, resources: ["*"], save: [] })).effect, "allow"); } for (const id of ["opencode_models", "opencode_session_move", "opencode_session_rename", "opencode_worktree_create", "opencode_worktree_list", "opencode_worktree_remove"]) { assert(tools.some((tool) => tool.id === id), `${id} must remain registered`); } assert(!tools.some((tool) => tool.id === "mcp_tjwater_web_search"), "web search must not have a duplicate MCP tool"); for (const id of ["query_project_data", "query_operational_task_data", "get_operational_task_context", "session_search", "web_search", "geocode", "todoread", "list_investigation_tasks", "get_investigation_task", "read_agent_messages"]) assert(!tools.some((tool) => tool.id === id), `${id} must not remain a standalone plugin query`); for (const id of ["memory_manager", "skill_manager", "send_task_message", "reply_agent_message", "todowrite", "present_answer", "final_answer", "locate_features", "zoom_to_map", "show_chart", "view_history", "render_scada_analysis", "clear_scada_analysis", "publish_execution_plan", "publish_task_report", "publish_task_progress", "publish_task_presentation", "propose_task_action", "add_task_human_evidence", "request_task_data_access"]) assert(tools.some((tool) => tool.id === id), `${id} must remain a plugin tool`); for (const tool of ["mcp_tjwater_session_search", "mcp_tjwater_todoread"]) { const result = await native.client.rpc.call({ rpcID: "tjwater.mcp-fixture", method: "call", input: { session_id: sessionId, tool, arguments: tool.endsWith("session_search") ? { query: "native-mcp" } : {} }, location: session.location }, { signal: AbortSignal.timeout(10_000) }); assert.equal((result.output as { error?: string }).error, undefined, JSON.stringify(result.output)); assert((result.output as { evaluated: string[] }).evaluated.includes(tool)); } assert.deepEqual(receivedSessions, [sessionId, sessionId], "OpenCode must supply the invoking session in native MCP metadata"); const search = await native.client.rpc.call({ rpcID: "tjwater.mcp-fixture", method: "call", input: { session_id: sessionId, tool: "websearch", arguments: { query: "native-search-fixture" }, }, location: session.location }, { signal: AbortSignal.timeout(10_000) }); assert.equal((search.output as { error?: string }).error, undefined, JSON.stringify(search.output)); assert(JSON.stringify(search.output).includes("https://example.test/search-result"), "native websearch must execute through its selected provider"); if (longCalculation) { setRuntimeSessionContext({ sessionId, clientSessionId: sessionId, actorKey: "fixture-actor", projectKey: "fixture-project", projectId: "fixture-project", traceId: "test", purpose: "ordinary_chat", workspaceDirectory: session.location.directory }); const started = performance.now(); console.log("Starting isolated native MCP delay: requested budget 90 seconds, fixture delay 65 seconds."); const result = await native.client.rpc.call({ rpcID: "tjwater.mcp-fixture", method: "call", input: { session_id: sessionId, tool: "mcp_tjwater_run_simulation", arguments: { timeout_seconds: 90 }, }, location: session.location }, { signal: AbortSignal.timeout(110_000) }); const output = result.output as { error?: string; content: unknown }; assert(!output.error, output.error); assert(JSON.stringify(output.content).includes("isolated long calculation completed")); const elapsed = performance.now() - started; assert(elapsed >= 60_000, `Long-call fixture returned too early (${Math.round(elapsed)}ms)`); console.log(`Native MCP long calculation passed after ${Math.round(elapsed)}ms: a 90-second Agent budget crosses the old 30/35/60-second limits; no real simulation or model inference.`); } console.log("Native query MCP smoke passed: browser automation absent; business UI and managers retained; discovery, permissions, session metadata, checklist RPC and native websearch verified; no model inference or external search."); } finally { if (sessionId) { removeRuntimeSessionContext(sessionId); await native?.client.session.remove({ sessionID: sessionId }).catch(() => {}); } native?.close(); if (native) await native.exited; await query.close(); await new Promise((resolve) => bridge.close(() => resolve())); if (previousPath === undefined) delete process.env.TJWATER_AGENT_MCP_CONNECTION_PATH; else process.env.TJWATER_AGENT_MCP_CONNECTION_PATH = previousPath; await rm(root, { recursive: true, force: true }); }