diff --git a/package-lock.json b/package-lock.json index 0bfce7daa05..c6f2a9fd990 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2129,6 +2129,7 @@ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", "license": "MIT", + "peer": true, "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", @@ -2271,6 +2272,7 @@ "integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.2", @@ -2451,6 +2453,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -2500,6 +2503,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz", "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -2874,6 +2878,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz", "integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -2907,6 +2912,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz", "integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0" @@ -2961,6 +2967,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz", "integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.5.0", "@opentelemetry/resources": "2.5.0", @@ -4124,6 +4131,7 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4398,6 +4406,7 @@ "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.35.0", "@typescript-eslint/types": "8.35.0", @@ -5323,6 +5332,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7863,6 +7873,7 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -8383,6 +8394,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -9679,6 +9691,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -9979,6 +9992,7 @@ "resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.11.tgz", "integrity": "sha512-93LQlzT7vvZ1XJcmOMwN4s+6W334QegendeHOMnEJBlhnpIzr8bws6/aOEHG8ZCuVD/vNeeea5m1msHIdAY6ig==", "license": "MIT", + "peer": true, "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.1", "ansi-escapes": "^7.0.0", @@ -13667,6 +13681,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -13677,6 +13692,7 @@ "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" @@ -15730,6 +15746,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -15953,7 +15970,8 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsx": { "version": "4.20.3", @@ -15961,6 +15979,7 @@ "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -16121,6 +16140,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16328,6 +16348,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -16441,6 +16462,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -16453,6 +16475,7 @@ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -17084,6 +17107,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -17619,6 +17643,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/packages/core/src/core/coreToolScheduler.ts b/packages/core/src/core/coreToolScheduler.ts index ea2cdb70159..0ee1003c267 100644 --- a/packages/core/src/core/coreToolScheduler.ts +++ b/packages/core/src/core/coreToolScheduler.ts @@ -625,9 +625,15 @@ export class CoreToolScheduler { ? toolCall.tool.serverName : undefined; + // Extract annotations from the tool instance + const toolAnnotations: Record = {}; + if (toolCall.tool.isReadOnly) { + toolAnnotations['readOnlyHint'] = true; + } + const { decision, rule } = await this.config .getPolicyEngine() - .check(toolCallForPolicy, serverName); + .check(toolCallForPolicy, serverName, toolAnnotations); if (decision === PolicyDecision.DENY) { const { errorMessage, errorType } = getPolicyDenialError( diff --git a/packages/core/src/policy/policies/plan.toml b/packages/core/src/policy/policies/plan.toml index e7129208c8c..e1e23c0ed5d 100644 --- a/packages/core/src/policy/policies/plan.toml +++ b/packages/core/src/policy/policies/plan.toml @@ -42,6 +42,14 @@ decision = "allow" priority = 70 modes = ["plan"] +# Allow MCP tools with read-only hint in Plan mode +[[rule]] +mcpName = "*" +toolAnnotations = { readOnlyHint = true } +decision = "allow" +priority = 70 +modes = ["plan"] + [[rule]] toolName = ["ask_user", "exit_plan_mode"] decision = "ask_user" diff --git a/packages/core/src/policy/policy-engine-annotations.test.ts b/packages/core/src/policy/policy-engine-annotations.test.ts new file mode 100644 index 00000000000..9fbf4630489 --- /dev/null +++ b/packages/core/src/policy/policy-engine-annotations.test.ts @@ -0,0 +1,208 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { PolicyEngine } from './policy-engine.js'; +import { PolicyDecision, type PolicyRule, ApprovalMode } from './types.js'; + +describe('PolicyEngine Enhancements (Wildcards and Annotations)', () => { + let engine: PolicyEngine; + + beforeEach(() => { + engine = new PolicyEngine({ approvalMode: ApprovalMode.DEFAULT }); + }); + + describe('mcpName = "*" Wildcard Matching', () => { + it('should match any serverName when mcpName is "*"', async () => { + const rules: PolicyRule[] = [ + { + toolName: '*__*', // Represents mcpName = "*" from TOML + decision: PolicyDecision.ALLOW, + }, + ]; + + engine = new PolicyEngine({ rules }); + + // Match any server when serverName is provided + expect( + (await engine.check({ name: 'server1__tool' }, 'server1')).decision, + ).toBe(PolicyDecision.ALLOW); + expect( + (await engine.check({ name: 'server2__tool' }, 'server2')).decision, + ).toBe(PolicyDecision.ALLOW); + + // Match unqualified name + serverName + expect((await engine.check({ name: 'tool' }, 'server1')).decision).toBe( + PolicyDecision.ALLOW, + ); + + // Should NOT match if NO serverName is provided (not an MCP tool call) + // Even if the tool name contains __, without serverName it shouldn't match *__* rule + expect( + (await engine.check({ name: 'some__qualified__name' }, undefined)) + .decision, + ).toBe(PolicyDecision.ASK_USER); + + // Should NOT match simple tool name + expect((await engine.check({ name: 'ls' }, undefined)).decision).toBe( + PolicyDecision.ASK_USER, + ); + }); + + it('should prioritize specific server wildcards over global wildcard', async () => { + const rules: PolicyRule[] = [ + { + toolName: '*__*', + decision: PolicyDecision.ALLOW, + priority: 10, + }, + { + toolName: 'blocked-server__*', + decision: PolicyDecision.DENY, + priority: 20, + }, + ]; + + engine = new PolicyEngine({ rules }); + + expect( + (await engine.check({ name: 'other-server__tool' }, 'other-server')) + .decision, + ).toBe(PolicyDecision.ALLOW); + expect( + (await engine.check({ name: 'blocked-server__tool' }, 'blocked-server')) + .decision, + ).toBe(PolicyDecision.DENY); + }); + }); + + describe('toolAnnotations Matching', () => { + it('should match tools based on annotations', async () => { + const rules: PolicyRule[] = [ + { + toolAnnotations: { readOnlyHint: true }, + decision: PolicyDecision.ALLOW, + }, + ]; + + engine = new PolicyEngine({ rules }); + + // Match with annotation + expect( + ( + await engine.check({ name: 'tool' }, undefined, { + readOnlyHint: true, + }) + ).decision, + ).toBe(PolicyDecision.ALLOW); + + // No match if annotation missing + expect( + (await engine.check({ name: 'tool' }, undefined, {})).decision, + ).toBe(PolicyDecision.ASK_USER); + + // No match if annotation value differs + expect( + ( + await engine.check({ name: 'tool' }, undefined, { + readOnlyHint: false, + }) + ).decision, + ).toBe(PolicyDecision.ASK_USER); + }); + + it('should match multiple annotations', async () => { + const rules: PolicyRule[] = [ + { + toolAnnotations: { a: 1, b: 2 }, + decision: PolicyDecision.ALLOW, + }, + ]; + + engine = new PolicyEngine({ rules }); + + // Match if all present + expect( + (await engine.check({ name: 'tool' }, undefined, { a: 1, b: 2, c: 3 })) + .decision, + ).toBe(PolicyDecision.ALLOW); + + // No match if one missing + expect( + (await engine.check({ name: 'tool' }, undefined, { a: 1 })).decision, + ).toBe(PolicyDecision.ASK_USER); + }); + + it('should work together with tool names', async () => { + const rules: PolicyRule[] = [ + { + toolName: 'read_file', + toolAnnotations: { safe: true }, + decision: PolicyDecision.ALLOW, + }, + ]; + + engine = new PolicyEngine({ rules }); + + // Match tool name AND annotation + expect( + (await engine.check({ name: 'read_file' }, undefined, { safe: true })) + .decision, + ).toBe(PolicyDecision.ALLOW); + + // No match if name differs + expect( + (await engine.check({ name: 'write_file' }, undefined, { safe: true })) + .decision, + ).toBe(PolicyDecision.ASK_USER); + + // No match if annotation differs + expect( + (await engine.check({ name: 'read_file' }, undefined, { safe: false })) + .decision, + ).toBe(PolicyDecision.ASK_USER); + }); + + it('should work together with mcpName wildcard', async () => { + const rules: PolicyRule[] = [ + { + toolName: '*__*', + toolAnnotations: { readOnlyHint: true }, + decision: PolicyDecision.ALLOW, + }, + ]; + + engine = new PolicyEngine({ rules }); + + // Match MCP tool with annotation + expect( + ( + await engine.check({ name: 'server__tool' }, 'server', { + readOnlyHint: true, + }) + ).decision, + ).toBe(PolicyDecision.ALLOW); + + // No match if annotation differs + expect( + ( + await engine.check({ name: 'server__tool' }, 'server', { + readOnlyHint: false, + }) + ).decision, + ).toBe(PolicyDecision.ASK_USER); + + // No match if not an MCP tool call (serverName undefined) + expect( + ( + await engine.check({ name: 'server__tool' }, undefined, { + readOnlyHint: true, + }) + ).decision, + ).toBe(PolicyDecision.ASK_USER); + }); + }); +}); diff --git a/packages/core/src/policy/policy-engine.ts b/packages/core/src/policy/policy-engine.ts index 353cdae9c14..41b50a7f4f2 100644 --- a/packages/core/src/policy/policy-engine.ts +++ b/packages/core/src/policy/policy-engine.ts @@ -39,6 +39,9 @@ function matchesWildcard(pattern: string, toolName: string): boolean { return false; } const prefix = getWildcardPrefix(pattern); + if (prefix === '*') { + return toolName.includes('__'); + } return toolName.startsWith(prefix + '__'); } @@ -48,6 +51,7 @@ function ruleMatches( stringifiedArgs: string | undefined, serverName: string | undefined, currentApprovalMode: ApprovalMode, + toolAnnotations?: Record, ): boolean { // Check if rule applies to current approval mode if (rule.modes && rule.modes.length > 0) { @@ -61,12 +65,12 @@ function ruleMatches( // Support wildcard patterns: "serverName__*" matches "serverName__anyTool" if (isWildcardPattern(rule.toolName)) { const prefix = getWildcardPrefix(rule.toolName); - if (serverName !== undefined) { - // Robust check: if serverName is provided, it MUST match the prefix exactly. - // This prevents "malicious-server" from spoofing "trusted-server" by naming itself "trusted-server__malicious". - if (serverName !== prefix) { + if (prefix === '*') { + if (serverName === undefined) { return false; } + } else if (serverName !== undefined && serverName !== prefix) { + return false; } // Always verify the prefix, even if serverName matched if (!toolCall.name || !matchesWildcard(rule.toolName, toolCall.name)) { @@ -77,6 +81,19 @@ function ruleMatches( } } + // Check tool annotations if specified + if (rule.toolAnnotations) { + if (!toolAnnotations) { + return false; + } + // All annotations in the rule must match the tool call + for (const [key, value] of Object.entries(rule.toolAnnotations)) { + if (toolAnnotations[key] !== value) { + return false; + } + } + } + // Check args pattern if specified if (rule.argsPattern) { // If rule has an args pattern but tool has no args, no match @@ -304,6 +321,7 @@ export class PolicyEngine { async check( toolCall: FunctionCall, serverName: string | undefined, + toolAnnotations?: Record, ): Promise { let stringifiedArgs: string | undefined; // Compute stringified args once before the loop @@ -356,7 +374,14 @@ export class PolicyEngine { for (const rule of this.rules) { const match = toolCallsToTry.some((tc) => - ruleMatches(rule, tc, stringifiedArgs, serverName, this.approvalMode), + ruleMatches( + rule, + tc, + stringifiedArgs, + serverName, + this.approvalMode, + toolAnnotations, + ), ); if (match) { @@ -417,6 +442,7 @@ export class PolicyEngine { stringifiedArgs, serverName, this.approvalMode, + toolAnnotations, ) ) { debugLogger.debug( diff --git a/packages/core/src/policy/toml-loader.ts b/packages/core/src/policy/toml-loader.ts index 7be3fe27dc9..895009a4139 100644 --- a/packages/core/src/policy/toml-loader.ts +++ b/packages/core/src/policy/toml-loader.ts @@ -48,6 +48,7 @@ const PolicyRuleSchema = z.object({ modes: z.array(z.nativeEnum(ApprovalMode)).optional(), allow_redirection: z.boolean().optional(), deny_message: z.string().optional(), + toolAnnotations: z.record(z.unknown()).optional(), }); /** @@ -75,6 +76,7 @@ const SafetyCheckerRuleSchema = z.object({ config: z.record(z.unknown()).optional(), }), ]), + toolAnnotations: z.record(z.unknown()).optional(), }); /** @@ -386,6 +388,7 @@ export async function loadPoliciesFromToml( allowRedirection: rule.allow_redirection, source: `${tierName.charAt(0).toUpperCase() + tierName.slice(1)}: ${file}`, denyMessage: rule.deny_message, + toolAnnotations: rule.toolAnnotations, }; // Compile regex pattern @@ -468,6 +471,7 @@ export async function loadPoliciesFromToml( checker: checker.checker as SafetyCheckerConfig, modes: checker.modes, source: `${tierName.charAt(0).toUpperCase() + tierName.slice(1)}: ${file}`, + toolAnnotations: checker.toolAnnotations, }; if (argsPattern) { diff --git a/packages/core/src/policy/types.ts b/packages/core/src/policy/types.ts index e8aa0e6dd13..99d0fa2c7ac 100644 --- a/packages/core/src/policy/types.ts +++ b/packages/core/src/policy/types.ts @@ -150,6 +150,12 @@ export interface PolicyRule { * This message will be returned to the model/user. */ denyMessage?: string; + + /** + * Optional annotations to match against. + * A rule matches if all annotations specified here match the tool's annotations. + */ + toolAnnotations?: Record; } export interface SafetyCheckerRule { @@ -188,6 +194,12 @@ export interface SafetyCheckerRule { * e.g. "my-policies.toml", "Workspace: project.toml", etc. */ source?: string; + + /** + * Optional annotations to match against. + * A rule matches if all annotations specified here match the tool's annotations. + */ + toolAnnotations?: Record; } export interface HookExecutionContext { diff --git a/packages/core/src/tools/mcp-client.ts b/packages/core/src/tools/mcp-client.ts index a838cf76e56..fcfb481696c 100644 --- a/packages/core/src/tools/mcp-client.ts +++ b/packages/core/src/tools/mcp-client.ts @@ -22,8 +22,7 @@ import type { Prompt, ReadResourceResult, Resource, -} from '@modelcontextprotocol/sdk/types.js'; -import { + ListResourcesResultSchema, ListRootsRequestSchema, ReadResourceResultSchema, @@ -31,9 +30,7 @@ import { ToolListChangedNotificationSchema, PromptListChangedNotificationSchema, ProgressNotificationSchema, - type Tool as McpTool, -} from '@modelcontextprotocol/sdk/types.js'; -import { ApprovalMode, PolicyDecision } from '../policy/types.js'; + type Tool as McpTool} from '@modelcontextprotocol/sdk/types.js'; import { parse } from 'shell-quote'; import type { Config, MCPServerConfig } from '../config/config.js'; import { AuthProviderType } from '../config/config.js'; @@ -1091,17 +1088,6 @@ export async function discoverTools( mcpServerConfig.extension?.id, ); - // If the tool is read-only, allow it in Plan mode - if (isReadOnly) { - cliConfig.getPolicyEngine().addRule({ - toolName: tool.getFullyQualifiedName(), - decision: PolicyDecision.ASK_USER, - priority: 50, // Match priority of built-in plan tools - modes: [ApprovalMode.PLAN], - source: `MCP Annotation (readOnlyHint) - ${mcpServerName}`, - }); - } - discoveredTools.push(tool); } catch (error) { coreEvents.emitFeedback(