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:
@@ -23,6 +23,7 @@ import { registerChatInteractionRoutes } from "./chatInteractionRoutes.js";
|
||||
import { createChatRunState } from "./chatRunState.js";
|
||||
import { registerChatSessionRoutes } from "./chatSessionRoutes.js";
|
||||
import { registerChatStreamRoute } from "./chatStreamRoute.js";
|
||||
import { registerMcpStatusRoutes } from "./mcpRoutes.js";
|
||||
|
||||
export type BuildChatRouterOptions = {
|
||||
frontendActionCoordinator: FrontendActionCoordinator;
|
||||
@@ -132,6 +133,7 @@ export const buildChatRouter = (options: BuildChatRouterOptions) => {
|
||||
sessionTranscriptStore,
|
||||
sessionUiStateStore,
|
||||
});
|
||||
registerMcpStatusRoutes(chatRouter, { sessionMetadataStore, runtime });
|
||||
|
||||
registerChatAuxiliaryRoutes(chatRouter, {
|
||||
activeRuns,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { isMcpTool } from "../runtime/mcp-config.js";
|
||||
|
||||
// Shell and file mutation remain interactive even when ordinary read-only
|
||||
// business tools are automatically approved. Static denials stay in OpenCode.
|
||||
export function canAutomaticallyApprove(mode: string, action: string) {
|
||||
return mode === "always" && !["shell", "bash", "edit", "write", "patch", "read", "external_directory", "execute", "subagent"].includes(action);
|
||||
return mode === "always" && !isMcpTool(action) && !["shell", "bash", "edit", "write", "patch", "read", "external_directory", "execute", "subagent"].includes(action);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { type Express, type Router } from "express";
|
||||
import { z } from "zod";
|
||||
import { getLocalAgentContext } from "../context/localContext.js";
|
||||
import { isTrustedInternalBridgeRequest } from "../product/internal-bridge-auth.js";
|
||||
import { getRuntimeSessionContext } from "../runtime/sessionContext.js";
|
||||
import { type McpRuntimeReport, type McpSessionPolicy } from "../runtime/mcp-config.js";
|
||||
import { type SessionMetadataStore } from "../sessions/metadataStore.js";
|
||||
|
||||
export function registerMcpPolicyBridge(app: Express, options: {
|
||||
internalToken: string;
|
||||
runtime: { mcpReadonlyTools(): string[] };
|
||||
}) {
|
||||
app.post("/internal/mcp/session-policy", (req, res) => {
|
||||
if (!isTrustedInternalBridgeRequest(req, options.internalToken)) { res.status(403).json({ message: "forbidden" }); return; }
|
||||
const parsed = z.object({ session_id: z.string().min(1).max(128) }).strict().safeParse(req.body);
|
||||
if (!parsed.success) { res.status(400).json({ message: "invalid MCP session policy request" }); return; }
|
||||
const context = getRuntimeSessionContext(parsed.data.session_id);
|
||||
if (!context) { res.status(404).json({ message: "session context not found" }); return; }
|
||||
const policy: McpSessionPolicy = {
|
||||
mode: context.learningMode === "review" ? "disabled" : context.purpose === "task_execution" ? "readonly" : "interactive",
|
||||
readonly_tools: options.runtime.mcpReadonlyTools(),
|
||||
};
|
||||
res.json(policy);
|
||||
});
|
||||
}
|
||||
|
||||
export function registerMcpStatusRoutes(router: Router, options: {
|
||||
sessionMetadataStore: SessionMetadataStore;
|
||||
runtime: { mcpStatus(sessionId: string): Promise<McpRuntimeReport> };
|
||||
}) {
|
||||
router.get("/session/:session_id/mcp", async (req, res) => {
|
||||
try {
|
||||
const record = await options.sessionMetadataStore.get(getLocalAgentContext(req), req.params.session_id);
|
||||
if (!record) { res.status(404).json({ message: "session not found" }); return; }
|
||||
const report = await options.runtime.mcpStatus(record.sessionId);
|
||||
if (report.configuration_errors.length) { res.status(503).json({ message: "MCP configuration invalid" }); return; }
|
||||
res.json({ servers: report.servers });
|
||||
} catch { res.status(503).json({ message: "MCP runtime unavailable" }); }
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user