diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 252c0bd6bd..4744ccf63e 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -20,6 +20,7 @@ export namespace Agent { edit: Config.Permission, bash: z.record(z.string(), Config.Permission), webfetch: Config.Permission.optional(), + mcp: z.record(z.string(), Config.Permission).optional(), }), model: z .object({ @@ -45,6 +46,9 @@ export namespace Agent { "*": "allow", }, webfetch: "allow", + mcp: { + "*": "allow", + }, } const agentPermission = mergeAgentPermissions(defaultPermission, cfg.permission ?? {}) @@ -53,6 +57,7 @@ export namespace Agent { edit: "deny", bash: "ask", webfetch: "allow", + mcp: "ask", }, cfg.permission ?? {}, ) @@ -195,10 +200,28 @@ function mergeAgentPermissions(basePermission: any, overridePermission: any): Ag } } + let mergedMcp + if (merged.mcp) { + if (typeof merged.mcp === "string") { + mergedMcp = { + "*": merged.mcp, + } + } + if (typeof merged.mcp === "object") { + mergedMcp = mergeDeep( + { + "*": "ask", + }, + merged.mcp, + ) + } + } + const result: Agent.Info["permission"] = { edit: merged.edit ?? "allow", webfetch: merged.webfetch ?? "allow", bash: mergedBash ?? { "*": "allow" }, + mcp: mergedMcp ?? { "*": "allow" }, } return result diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 2372404187..ce2c52874a 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -323,6 +323,7 @@ export namespace Config { edit: Permission.optional(), bash: z.union([Permission, z.record(z.string(), Permission)]).optional(), webfetch: Permission.optional(), + mcp: z.union([Permission, z.record(z.string(), Permission)]).optional(), }) .optional(), }) @@ -554,6 +555,7 @@ export namespace Config { edit: Permission.optional(), bash: z.union([Permission, z.record(z.string(), Permission)]).optional(), webfetch: Permission.optional(), + mcp: z.union([Permission, z.record(z.string(), Permission)]).optional(), }) .optional(), tools: z.record(z.string(), z.boolean()).optional(), diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index cced12711a..c722a4ecf2 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -509,6 +509,30 @@ export namespace SessionPrompt { const execute = item.execute if (!execute) continue item.execute = async (args, opts) => { + const permissions = input.agent.permission?.mcp + const action = Wildcard.all(key, permissions ?? {}) + + if (action === "deny") { + throw new Error( + `The user has specifically restricted access to this MCP tool, you are not allowed to execute it. Tool: ${key}`, + ) + } + + if (action === "ask") { + await Permission.ask({ + type: "mcp", + pattern: key, + sessionID: input.sessionID, + messageID: input.processor.message.id, + callID: opts.toolCallId, + title: `MCP tool: ${key}`, + metadata: { + tool: key, + args, + }, + }) + } + await Plugin.trigger( "tool.execute.before", { diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 53049eb136..9854ac820b 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -234,6 +234,11 @@ export type AgentConfig = { [key: string]: "ask" | "allow" | "deny" } webfetch?: "ask" | "allow" | "deny" + mcp?: + | ("ask" | "allow" | "deny") + | { + [key: string]: "ask" | "allow" | "deny" + } } [key: string]: | unknown @@ -252,6 +257,11 @@ export type AgentConfig = { [key: string]: "ask" | "allow" | "deny" } webfetch?: "ask" | "allow" | "deny" + mcp?: + | ("ask" | "allow" | "deny") + | { + [key: string]: "ask" | "allow" | "deny" + } } | undefined } @@ -484,6 +494,11 @@ export type Config = { [key: string]: "ask" | "allow" | "deny" } webfetch?: "ask" | "allow" | "deny" + mcp?: + | ("ask" | "allow" | "deny") + | { + [key: string]: "ask" | "allow" | "deny" + } } tools?: { [key: string]: boolean @@ -1000,6 +1015,9 @@ export type Agent = { [key: string]: "ask" | "allow" | "deny" } webfetch?: "ask" | "allow" | "deny" + mcp?: { + [key: string]: "ask" | "allow" | "deny" + } } model?: { modelID: string diff --git a/packages/web/src/content/docs/permissions.mdx b/packages/web/src/content/docs/permissions.mdx index 4579c21277..65fca738a0 100644 --- a/packages/web/src/content/docs/permissions.mdx +++ b/packages/web/src/content/docs/permissions.mdx @@ -26,7 +26,7 @@ This lets you configure granular controls for the `edit`, `bash`, and `webfetch` ## Tools -Currently, the permissions for the `edit`, `bash`, and `webfetch` tools can be configured through the `permission` option. +Currently, the permissions for the `edit`, `bash`, `webfetch`, and `mcp` tools can be configured through the `permission` option. --- @@ -145,6 +145,32 @@ Use the `permission.webfetch` key to control whether the LLM can fetch web pages --- +### mcp + +Use the `permission.mcp` key to control whether MCP tools require user approval. + +```json title="opencode.json" +{ + "$schema": "https://opencode.ai/config.json", + "permission": { + "mcp": { + "*": "ask", + "mymcp1_tool_name1": "allow", + "mymcp2_*": "deny" + } + } +} +``` + +You can use wildcards to manage permissions for specific MCP tools: + +- `"*"` matches all MCP tools +- `"mymcp1_tool_name1"` matches tool `tool_name1` of mcp server `mymcp1` +- `"mymcp2_*"` matches all tools starting with `mymcp2_` +- Specific rules override wildcard rules + +--- + ## Agents You can also configure permissions per agent. Where the agent specific config