Summary
Tools provided by MCP servers never reach the approval gate. Built-in tools (os.shell.run, os.fs.write, browser.navigate, …) prompt as expected, but an MCP tool that does the equivalent thing runs silently.
This is easy to miss because McpServerConfig.trust defaults to the value "approval_gated", which reads like a consent setting. It is not one: src/agent/tool-resource-class.ts documents that class as a scheduling constraint.
approval_gated — Virtual class. Tools that may invoke requireApproval synchronously inside run(). Forbidden inside a batch […]
"May invoke" is the operative part. Nothing in the MCP subsystem ever does.
Verification
On main @ 7073a61 (v0.2.1):
$ git grep -n "requireApproval" origin/main -- src/mcp
$ echo $?
1 # no matches
$ git grep -ln "requireApproval" origin/main -- src/tools | wc -l
7 # built-in tools do call it
src/mcp/mcp-client.ts stamps resourceClass: resourceClassFor(qualifiedName) on each proxied tool, and src/mcp/mcp-resource-class.ts maps trust to that class, but the resulting class only affects batching. The tool's run() proxies straight to the server with no gate.
Reproduction
- Register any MCP server that can mutate state. Example used here, CursorTouch/Windows-MCP:
{
"mcp": {
"servers": [
{
"name": "windows",
"enabled": true,
"transport": { "kind": "stdio", "command": "uvx", "args": ["windows-mcp", "serve"] }
}
]
}
}
- Leave
agent.approvalRequired: true and set no trust (so it defaults to approval_gated).
- Ask the agent to launch an application.
Expected: an approval prompt, as with os.shell.run.
Actual: mcp.windows.App executes immediately. No approval request is emitted on GET /api/events, and no prompt appears in the TUI or over the HTTP API.
Contrast: in the same session and config, os.fs.write prompts normally. The difference is only whether the tool is built in or proxied from MCP.
Impact
Whatever the server exposes runs unattended. With the example above that includes PowerShell, Registry, FileSystem, plus synthetic mouse and keyboard input (Click, Type, Shortcut). With @playwright/mcp it includes browser_run_code_unsafe.
Two things make it sharper:
McpServerConfig has no per-tool allowlist (name, description, enabled, transport, trust, env), so an operator who wants approvals on one risky tool can only disable the whole server.
- The naming actively suggests the opposite is happening. An operator reading
trust: "approval_gated" would reasonably conclude the calls are gated.
This seems worth treating as a security issue rather than a docs one: the safe-by-default posture the README describes ("Approval gates", "Everything atomic-agent does is inspectable and interruptible") does not hold for MCP tools.
Suggested fix
Any of these would close it, roughly in order of preference:
- Have the MCP tool adapter call
requireApproval when the resolved trust is approval_gated, so the existing gate applies uniformly. This makes the current default mean what it says.
- Route MCP tools through
dangerousTool() (or the v0.2 permission ladder) so they inherit the same policy as built-ins, including session-scoped grants.
- If gating every MCP call is too coarse, honour the MCP
annotations.readOnlyHint that servers already advertise, and gate anything not marked read-only.
A per-server allowTools / denyTools list would also help independently, since it lets an operator adopt a useful server without accepting its whole surface.
Environment
- atomic-agent v0.1.72 (observed), re-verified against
main @ 7073a61 (v0.2.1)
- Windows 11,
agent.approvalRequired: true
- Servers:
CursorTouch/Windows-MCP via uvx, @playwright/mcp via npx
Happy to open a PR for option 1 if that direction is welcome.
Summary
Tools provided by MCP servers never reach the approval gate. Built-in tools (
os.shell.run,os.fs.write,browser.navigate, …) prompt as expected, but an MCP tool that does the equivalent thing runs silently.This is easy to miss because
McpServerConfig.trustdefaults to the value"approval_gated", which reads like a consent setting. It is not one:src/agent/tool-resource-class.tsdocuments that class as a scheduling constraint."May invoke" is the operative part. Nothing in the MCP subsystem ever does.
Verification
On
main@7073a61(v0.2.1):src/mcp/mcp-client.tsstampsresourceClass: resourceClassFor(qualifiedName)on each proxied tool, andsrc/mcp/mcp-resource-class.tsmaps trust to that class, but the resulting class only affects batching. The tool'srun()proxies straight to the server with no gate.Reproduction
{ "mcp": { "servers": [ { "name": "windows", "enabled": true, "transport": { "kind": "stdio", "command": "uvx", "args": ["windows-mcp", "serve"] } } ] } }agent.approvalRequired: trueand set notrust(so it defaults toapproval_gated).Expected: an approval prompt, as with
os.shell.run.Actual:
mcp.windows.Appexecutes immediately. No approval request is emitted onGET /api/events, and no prompt appears in the TUI or over the HTTP API.Contrast: in the same session and config,
os.fs.writeprompts normally. The difference is only whether the tool is built in or proxied from MCP.Impact
Whatever the server exposes runs unattended. With the example above that includes
PowerShell,Registry,FileSystem, plus synthetic mouse and keyboard input (Click,Type,Shortcut). With@playwright/mcpit includesbrowser_run_code_unsafe.Two things make it sharper:
McpServerConfighas no per-tool allowlist (name,description,enabled,transport,trust,env), so an operator who wants approvals on one risky tool can only disable the whole server.trust: "approval_gated"would reasonably conclude the calls are gated.This seems worth treating as a security issue rather than a docs one: the safe-by-default posture the README describes ("Approval gates", "Everything atomic-agent does is inspectable and interruptible") does not hold for MCP tools.
Suggested fix
Any of these would close it, roughly in order of preference:
requireApprovalwhen the resolved trust isapproval_gated, so the existing gate applies uniformly. This makes the current default mean what it says.dangerousTool()(or the v0.2 permission ladder) so they inherit the same policy as built-ins, including session-scoped grants.annotations.readOnlyHintthat servers already advertise, and gate anything not marked read-only.A per-server
allowTools/denyToolslist would also help independently, since it lets an operator adopt a useful server without accepting its whole surface.Environment
main@7073a61(v0.2.1)agent.approvalRequired: trueCursorTouch/Windows-MCPviauvx,@playwright/mcpvianpxHappy to open a PR for option 1 if that direction is welcome.