diff --git a/docs/changelogs/preview.md b/docs/changelogs/preview.md index 43a02728b3..ad7bf734bf 100644 --- a/docs/changelogs/preview.md +++ b/docs/changelogs/preview.md @@ -1,6 +1,6 @@ -# Preview release: v0.34.0-preview.2 +# Preview release: v0.34.0-preview.3 -Released: March 12, 2026 +Released: March 13, 2026 Our preview release includes the latest, new, and experimental features. This release may not be as stable as our [latest weekly release](latest.md). @@ -28,6 +28,10 @@ npm install -g @google/gemini-cli@preview ## What's Changed +- fix(patch): cherry-pick 24adacd to release/v0.34.0-preview.2-pr-22332 to patch + version v0.34.0-preview.2 and create version 0.34.0-preview.3 by + @gemini-cli-robot in + [#22391](https://github.com/google-gemini/gemini-cli/pull/22391) - fix(patch): cherry-pick 8432bce to release/v0.34.0-preview.1-pr-22069 to patch version v0.34.0-preview.1 and create version 0.34.0-preview.2 by @gemini-cli-robot in @@ -472,4 +476,4 @@ npm install -g @google/gemini-cli@preview [#21938](https://github.com/google-gemini/gemini-cli/pull/21938) **Full Changelog**: -https://github.com/google-gemini/gemini-cli/compare/v0.33.0-preview.15...v0.34.0-preview.2 +https://github.com/google-gemini/gemini-cli/compare/v0.33.0-preview.15...v0.34.0-preview.3 diff --git a/docs/cli/plan-mode.md b/docs/cli/plan-mode.md index b46acaf966..379eb71030 100644 --- a/docs/cli/plan-mode.md +++ b/docs/cli/plan-mode.md @@ -120,7 +120,8 @@ These are the only allowed tools: [`list_directory`](../tools/file-system.md#1-list_directory-readfolder), [`glob`](../tools/file-system.md#4-glob-findfiles) - **Search:** [`grep_search`](../tools/file-system.md#5-grep_search-searchtext), - [`google_web_search`](../tools/web-search.md) + [`google_web_search`](../tools/web-search.md), + [`get_internal_docs`](../tools/internal-docs.md) - **Research Subagents:** [`codebase_investigator`](../core/subagents.md#codebase-investigator), [`cli_help`](../core/subagents.md#cli-help-agent) diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 8845b6dd69..a3b4788026 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -1190,7 +1190,7 @@ their corresponding top-level category object in your `settings.json` file. - **`experimental.jitContext`** (boolean): - **Description:** Enable Just-In-Time (JIT) context loading. - - **Default:** `false` + - **Default:** `true` - **Requires restart:** Yes - **`experimental.useOSC52Paste`** (boolean): diff --git a/package-lock.json b/package-lock.json index 3757403f78..d25d2aa2f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2195,7 +2195,6 @@ "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", @@ -2376,7 +2375,6 @@ "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" } @@ -2426,7 +2424,6 @@ "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" }, @@ -2801,7 +2798,6 @@ "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" @@ -2835,7 +2831,6 @@ "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" @@ -2890,7 +2885,6 @@ "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", @@ -4127,7 +4121,6 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4402,7 +4395,6 @@ "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", @@ -5276,7 +5268,6 @@ "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" }, @@ -7995,7 +7986,6 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -8513,7 +8503,6 @@ "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", @@ -9826,7 +9815,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz", "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -10105,7 +10093,6 @@ "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", @@ -13863,7 +13850,6 @@ "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" } @@ -13874,7 +13860,6 @@ "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" @@ -16024,7 +16009,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -16248,8 +16232,7 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsx": { "version": "4.20.3", @@ -16257,7 +16240,6 @@ "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -16423,7 +16405,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16646,7 +16627,6 @@ "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", @@ -16760,7 +16740,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -16773,7 +16752,6 @@ "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", @@ -17421,7 +17399,6 @@ "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" } @@ -17968,7 +17945,6 @@ "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/a2a-server/src/commands/memory.test.ts b/packages/a2a-server/src/commands/memory.test.ts index 2d3a5fef91..de5a09fcb2 100644 --- a/packages/a2a-server/src/commands/memory.test.ts +++ b/packages/a2a-server/src/commands/memory.test.ts @@ -177,10 +177,13 @@ describe('a2a-server memory commands', () => { expect.any(AbortSignal), undefined, { - sanitizationConfig: { - allowedEnvironmentVariables: [], - blockedEnvironmentVariables: [], - enableEnvironmentVariableRedaction: false, + shellExecutionConfig: { + sanitizationConfig: { + allowedEnvironmentVariables: [], + blockedEnvironmentVariables: [], + enableEnvironmentVariableRedaction: false, + }, + sandboxManager: undefined, }, }, ); diff --git a/packages/a2a-server/src/commands/memory.ts b/packages/a2a-server/src/commands/memory.ts index f7c3dfa896..f84d57b3fc 100644 --- a/packages/a2a-server/src/commands/memory.ts +++ b/packages/a2a-server/src/commands/memory.ts @@ -103,8 +103,10 @@ export class AddMemoryCommand implements Command { const abortController = new AbortController(); const signal = abortController.signal; await tool.buildAndExecute(result.toolArgs, signal, undefined, { - sanitizationConfig: DEFAULT_SANITIZATION_CONFIG, - sandboxManager: loopContext.sandboxManager, + shellExecutionConfig: { + sanitizationConfig: DEFAULT_SANITIZATION_CONFIG, + sandboxManager: loopContext.sandboxManager, + }, }); await refreshMemory(context.config); return { diff --git a/packages/cli/src/acp/commands/memory.ts b/packages/cli/src/acp/commands/memory.ts index 1154c852a1..f88aaac4f2 100644 --- a/packages/cli/src/acp/commands/memory.ts +++ b/packages/cli/src/acp/commands/memory.ts @@ -104,8 +104,10 @@ export class AddMemoryCommand implements Command { await context.sendMessage(`Saving memory via ${result.toolName}...`); await tool.buildAndExecute(result.toolArgs, signal, undefined, { - sanitizationConfig: DEFAULT_SANITIZATION_CONFIG, - sandboxManager: context.config.sandboxManager, + shellExecutionConfig: { + sanitizationConfig: DEFAULT_SANITIZATION_CONFIG, + sandboxManager: context.config.sandboxManager, + }, }); await refreshMemory(context.config); return { diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 72c55a64b3..8990224b0f 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -814,7 +814,9 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => { it('should pass extension context file paths to loadServerHierarchicalMemory', async () => { process.argv = ['node', 'script.js']; - const settings = createTestMergedSettings(); + const settings = createTestMergedSettings({ + experimental: { jitContext: false }, + }); vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([ { path: '/path/to/ext1', @@ -865,6 +867,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => { process.argv = ['node', 'script.js']; const includeDir = path.resolve(path.sep, 'path', 'to', 'include'); const settings = createTestMergedSettings({ + experimental: { jitContext: false }, context: { includeDirectories: [includeDir], loadMemoryFromIncludeDirectories: true, @@ -892,6 +895,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => { it('should NOT pass includeDirectories to loadServerHierarchicalMemory when loadMemoryFromIncludeDirectories is false', async () => { process.argv = ['node', 'script.js']; const settings = createTestMergedSettings({ + experimental: { jitContext: false }, context: { includeDirectories: ['/path/to/include'], loadMemoryFromIncludeDirectories: false, diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index ab6a22fb64..957bb6510e 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -494,7 +494,7 @@ export async function loadCliConfig( .getExtensions() .find((ext) => ext.isActive && ext.plan?.directory)?.plan; - const experimentalJitContext = settings.experimental?.jitContext ?? false; + const experimentalJitContext = settings.experimental.jitContext; let extensionRegistryURI = process.env['GEMINI_CLI_EXTENSION_REGISTRY_URI'] ?? @@ -737,6 +737,8 @@ export async function loadCliConfig( includeDirectories, loadMemoryFromIncludeDirectories: settings.context?.loadMemoryFromIncludeDirectories || false, + discoveryMaxDirs: settings.context?.discoveryMaxDirs, + importFormat: settings.context?.importFormat, debugMode, question, diff --git a/packages/cli/src/config/policy-engine.integration.test.ts b/packages/cli/src/config/policy-engine.integration.test.ts index 71d5f49e59..847b47bbe3 100644 --- a/packages/cli/src/config/policy-engine.integration.test.ts +++ b/packages/cli/src/config/policy-engine.integration.test.ts @@ -346,6 +346,12 @@ describe('Policy Engine Integration Tests', () => { expect( (await engine.check({ name: 'list_directory' }, undefined)).decision, ).toBe(PolicyDecision.ALLOW); + expect( + (await engine.check({ name: 'get_internal_docs' }, undefined)).decision, + ).toBe(PolicyDecision.ALLOW); + expect( + (await engine.check({ name: 'cli_help' }, undefined)).decision, + ).toBe(PolicyDecision.ALLOW); // Other tools should be denied via catch all expect( diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 04db402f07..b06df48bc3 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -1894,7 +1894,7 @@ const SETTINGS_SCHEMA = { label: 'JIT Context Loading', category: 'Experimental', requiresRestart: true, - default: false, + default: true, description: 'Enable Just-In-Time (JIT) context loading.', showInDialog: false, }, diff --git a/packages/cli/src/ui/components/NewAgentsNotification.test.tsx b/packages/cli/src/ui/components/NewAgentsNotification.test.tsx index b184eebffb..d234b70c4d 100644 --- a/packages/cli/src/ui/components/NewAgentsNotification.test.tsx +++ b/packages/cli/src/ui/components/NewAgentsNotification.test.tsx @@ -22,6 +22,25 @@ describe('NewAgentsNotification', () => { { name: 'Agent B', description: 'Description B', + kind: 'local' as const, + inputConfig: { inputSchema: {} }, + promptConfig: {}, + modelConfig: {}, + runConfig: {}, + mcpServers: { + github: { + command: 'npx', + args: ['-y', '@modelcontextprotocol/server-github'], + }, + postgres: { + command: 'npx', + args: ['-y', '@modelcontextprotocol/server-postgres'], + }, + }, + }, + { + name: 'Agent C', + description: 'Description C', kind: 'remote' as const, agentCardUrl: '', inputConfig: { inputSchema: {} }, diff --git a/packages/cli/src/ui/components/NewAgentsNotification.tsx b/packages/cli/src/ui/components/NewAgentsNotification.tsx index e7aa8be510..53287ec433 100644 --- a/packages/cli/src/ui/components/NewAgentsNotification.tsx +++ b/packages/cli/src/ui/components/NewAgentsNotification.tsx @@ -80,16 +80,35 @@ export const NewAgentsNotification = ({ borderStyle="single" padding={1} > - {displayAgents.map((agent) => ( - - - - - {agent.name}:{' '} - + {displayAgents.map((agent) => { + const mcpServers = + agent.kind === 'local' ? agent.mcpServers : undefined; + const hasMcpServers = + mcpServers && Object.keys(mcpServers).length > 0; + return ( + + + + + - {agent.name}:{' '} + + + + {' '} + {agent.description} + + + {hasMcpServers && ( + + + (Includes MCP servers:{' '} + {Object.keys(mcpServers).join(', ')}) + + + )} - {agent.description} - - ))} + ); + })} {remaining > 0 && ( ... and {remaining} more. diff --git a/packages/cli/src/ui/components/__snapshots__/NewAgentsNotification.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/NewAgentsNotification.test.tsx.snap index bac1f7af36..74dcb8a914 100644 --- a/packages/cli/src/ui/components/__snapshots__/NewAgentsNotification.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/NewAgentsNotification.test.tsx.snap @@ -10,6 +10,8 @@ exports[`NewAgentsNotification > renders agent list 1`] = ` │ │ │ │ │ │ - Agent A: Description A │ │ │ │ - Agent B: Description B │ │ + │ │ (Includes MCP servers: github, postgres) │ │ + │ │ - Agent C: Description C │ │ │ │ │ │ │ └────────────────────────────────────────────────────────────────────────────────────────────┘ │ │ │ diff --git a/packages/core/src/agents/agentLoader.test.ts b/packages/core/src/agents/agentLoader.test.ts index a526382553..ea7ef0b2c3 100644 --- a/packages/core/src/agents/agentLoader.test.ts +++ b/packages/core/src/agents/agentLoader.test.ts @@ -81,6 +81,33 @@ System prompt content.`); }); }); + it('should parse frontmatter with mcp_servers', async () => { + const filePath = await writeAgentMarkdown(`--- +name: mcp-agent +description: An agent with MCP servers +mcp_servers: + test-server: + command: node + args: [server.js] + include_tools: [tool1, tool2] +--- +System prompt content.`); + + const result = await parseAgentMarkdown(filePath); + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + name: 'mcp-agent', + description: 'An agent with MCP servers', + mcp_servers: { + 'test-server': { + command: 'node', + args: ['server.js'], + include_tools: ['tool1', 'tool2'], + }, + }, + }); + }); + it('should throw AgentLoadError if frontmatter is missing', async () => { const filePath = await writeAgentMarkdown(`Just some markdown content.`); await expect(parseAgentMarkdown(filePath)).rejects.toThrow( @@ -274,6 +301,33 @@ Body`); expect(result.modelConfig.model).toBe(GEMINI_MODEL_ALIAS_PRO); }); + it('should convert mcp_servers in local agent', () => { + const markdown = { + kind: 'local' as const, + name: 'mcp-agent', + description: 'An agent with MCP servers', + mcp_servers: { + 'test-server': { + command: 'node', + args: ['server.js'], + include_tools: ['tool1'], + }, + }, + system_prompt: 'prompt', + }; + + const result = markdownToAgentDefinition( + markdown, + ) as LocalAgentDefinition; + expect(result.kind).toBe('local'); + expect(result.mcpServers).toBeDefined(); + expect(result.mcpServers!['test-server']).toMatchObject({ + command: 'node', + args: ['server.js'], + includeTools: ['tool1'], + }); + }); + it('should pass through unknown model names (e.g. auto)', () => { const markdown = { kind: 'local' as const, diff --git a/packages/core/src/agents/agentLoader.ts b/packages/core/src/agents/agentLoader.ts index c867a1c9a3..2cb7b3c439 100644 --- a/packages/core/src/agents/agentLoader.ts +++ b/packages/core/src/agents/agentLoader.ts @@ -16,6 +16,7 @@ import { DEFAULT_MAX_TIME_MINUTES, } from './types.js'; import type { A2AAuthConfig } from './auth-provider/types.js'; +import { MCPServerConfig } from '../config/config.js'; import { isValidToolName } from '../tools/tool-names.js'; import { FRONTMATTER_REGEX } from '../skills/skillLoader.js'; import { getErrorMessage } from '../utils/errors.js'; @@ -28,11 +29,29 @@ interface FrontmatterBaseAgentDefinition { display_name?: string; } +interface FrontmatterMCPServerConfig { + command?: string; + args?: string[]; + env?: Record; + cwd?: string; + url?: string; + http_url?: string; + headers?: Record; + tcp?: string; + type?: 'sse' | 'http'; + timeout?: number; + trust?: boolean; + description?: string; + include_tools?: string[]; + exclude_tools?: string[]; +} + interface FrontmatterLocalAgentDefinition extends FrontmatterBaseAgentDefinition { kind: 'local'; description: string; tools?: string[]; + mcp_servers?: Record; system_prompt: string; model?: string; temperature?: number; @@ -100,6 +119,23 @@ const nameSchema = z .string() .regex(/^[a-z0-9-_]+$/, 'Name must be a valid slug'); +const mcpServerSchema = z.object({ + command: z.string().optional(), + args: z.array(z.string()).optional(), + env: z.record(z.string()).optional(), + cwd: z.string().optional(), + url: z.string().optional(), + http_url: z.string().optional(), + headers: z.record(z.string()).optional(), + tcp: z.string().optional(), + type: z.enum(['sse', 'http']).optional(), + timeout: z.number().optional(), + trust: z.boolean().optional(), + description: z.string().optional(), + include_tools: z.array(z.string()).optional(), + exclude_tools: z.array(z.string()).optional(), +}); + const localAgentSchema = z .object({ kind: z.literal('local').optional().default('local'), @@ -115,6 +151,7 @@ const localAgentSchema = z }), ) .optional(), + mcp_servers: z.record(mcpServerSchema).optional(), model: z.string().optional(), temperature: z.number().optional(), max_turns: z.number().int().positive().optional(), @@ -495,6 +532,28 @@ export function markdownToAgentDefinition( // If a model is specified, use it. Otherwise, inherit const modelName = markdown.model || 'inherit'; + const mcpServers: Record = {}; + if (markdown.kind === 'local' && markdown.mcp_servers) { + for (const [name, config] of Object.entries(markdown.mcp_servers)) { + mcpServers[name] = new MCPServerConfig( + config.command, + config.args, + config.env, + config.cwd, + config.url, + config.http_url, + config.headers, + config.tcp, + config.type, + config.timeout, + config.trust, + config.description, + config.include_tools, + config.exclude_tools, + ); + } + } + return { kind: 'local', name: markdown.name, @@ -520,6 +579,7 @@ export function markdownToAgentDefinition( tools: markdown.tools, } : undefined, + mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers : undefined, inputConfig, metadata, }; diff --git a/packages/core/src/agents/registry.ts b/packages/core/src/agents/registry.ts index 23cf912055..3a815aa012 100644 --- a/packages/core/src/agents/registry.ts +++ b/packages/core/src/agents/registry.ts @@ -570,6 +570,19 @@ export class AgentRegistry { }, }; + if (overrides.tools) { + merged.toolConfig = { + tools: overrides.tools, + }; + } + + if (overrides.mcpServers) { + merged.mcpServers = { + ...definition.mcpServers, + ...overrides.mcpServers, + }; + } + return merged; } diff --git a/packages/core/src/agents/types.ts b/packages/core/src/agents/types.ts index b6d0d6212b..41db981a7b 100644 --- a/packages/core/src/agents/types.ts +++ b/packages/core/src/agents/types.ts @@ -14,6 +14,7 @@ import { type z } from 'zod'; import type { ModelConfig } from '../services/modelConfigService.js'; import type { AnySchema } from 'ajv'; import type { A2AAuthConfig } from './auth-provider/types.js'; +import type { MCPServerConfig } from '../config/config.js'; /** * Describes the possible termination modes for an agent. @@ -130,6 +131,11 @@ export interface LocalAgentDefinition< // Optional configs toolConfig?: ToolConfig; + /** + * Optional inline MCP servers for this agent. + */ + mcpServers?: Record; + /** * An optional function to process the raw output from the agent's final tool * call into a string format. diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index f0cf3c1eee..2e9102250c 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -240,6 +240,8 @@ export interface AgentOverride { modelConfig?: ModelConfig; runConfig?: AgentRunConfig; enabled?: boolean; + tools?: string[]; + mcpServers?: Record; } export interface AgentSettings { @@ -994,7 +996,7 @@ export class Config implements McpContext, AgentLoopContext { modelConfigServiceConfig ?? DEFAULT_MODEL_CONFIGS, ); - this.experimentalJitContext = params.experimentalJitContext ?? false; + this.experimentalJitContext = params.experimentalJitContext ?? true; this.topicUpdateNarration = params.topicUpdateNarration ?? false; this.modelSteering = params.modelSteering ?? false; this.injectionService = new InjectionService(() => diff --git a/packages/core/src/core/coreToolHookTriggers.test.ts b/packages/core/src/core/coreToolHookTriggers.test.ts index ff9601fc33..414064ff85 100644 --- a/packages/core/src/core/coreToolHookTriggers.test.ts +++ b/packages/core/src/core/coreToolHookTriggers.test.ts @@ -51,10 +51,9 @@ class MockBackgroundableInvocation extends BaseToolInvocation< async execute( _signal: AbortSignal, _updateOutput?: (output: ToolLiveOutput) => void, - _shellExecutionConfig?: unknown, - setExecutionIdCallback?: (executionId: number) => void, + options?: { setExecutionIdCallback?: (executionId: number) => void }, ) { - setExecutionIdCallback?.(4242); + options?.setExecutionIdCallback?.(4242); return { llmContent: 'pid', returnDisplay: 'pid', @@ -111,7 +110,6 @@ describe('executeToolWithHooks', () => { mockTool, undefined, undefined, - undefined, mockConfig, ); @@ -136,7 +134,6 @@ describe('executeToolWithHooks', () => { mockTool, undefined, undefined, - undefined, mockConfig, ); @@ -168,7 +165,6 @@ describe('executeToolWithHooks', () => { mockTool, undefined, undefined, - undefined, mockConfig, ); @@ -200,7 +196,6 @@ describe('executeToolWithHooks', () => { mockTool, undefined, undefined, - undefined, mockConfig, ); @@ -234,7 +229,6 @@ describe('executeToolWithHooks', () => { mockTool, undefined, undefined, - undefined, mockConfig, ); @@ -275,7 +269,6 @@ describe('executeToolWithHooks', () => { mockTool, undefined, undefined, - undefined, mockConfig, ); @@ -298,8 +291,7 @@ describe('executeToolWithHooks', () => { abortSignal, mockTool, undefined, - undefined, - setExecutionIdCallback, + { setExecutionIdCallback }, mockConfig, ); diff --git a/packages/core/src/core/coreToolHookTriggers.ts b/packages/core/src/core/coreToolHookTriggers.ts index 464cfc5f04..6bff4cfdd5 100644 --- a/packages/core/src/core/coreToolHookTriggers.ts +++ b/packages/core/src/core/coreToolHookTriggers.ts @@ -11,10 +11,10 @@ import type { AnyDeclarativeTool, AnyToolInvocation, ToolLiveOutput, + ExecuteOptions, } from '../tools/tools.js'; import { ToolErrorType } from '../tools/tool-error.js'; import { debugLogger } from '../utils/debugLogger.js'; -import type { ShellExecutionConfig } from '../index.js'; import { DiscoveredMCPToolInvocation } from '../tools/mcp-tool.js'; /** @@ -61,8 +61,7 @@ function extractMcpContext( * @param toolName The name of the tool * @param signal Abort signal for cancellation * @param liveOutputCallback Optional callback for live output updates - * @param shellExecutionConfig Optional shell execution config - * @param setExecutionIdCallback Optional callback to set an execution ID for backgroundable invocations + * @param options Optional execution options (shell config, execution ID callback, etc.) * @param config Config to look up MCP server details for hook context * @returns The tool result */ @@ -72,8 +71,7 @@ export async function executeToolWithHooks( signal: AbortSignal, tool: AnyDeclarativeTool, liveOutputCallback?: (outputChunk: ToolLiveOutput) => void, - shellExecutionConfig?: ShellExecutionConfig, - setExecutionIdCallback?: (executionId: number) => void, + options?: ExecuteOptions, config?: Config, originalRequestName?: string, ): Promise { @@ -158,8 +156,7 @@ export async function executeToolWithHooks( const toolResult: ToolResult = await invocation.execute( signal, liveOutputCallback, - shellExecutionConfig, - setExecutionIdCallback, + options, ); // Append notification if parameters were modified diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 40d5ef9411..a76e7aa2d4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -156,6 +156,12 @@ export * from './services/executionLifecycleService.js'; // Export Injection Service export * from './config/injectionService.js'; +// Export Execution Lifecycle Service +export * from './services/executionLifecycleService.js'; + +// Export Injection Service +export * from './config/injectionService.js'; + // Export base tool definitions export * from './tools/tools.js'; export * from './tools/tool-error.js'; diff --git a/packages/core/src/policy/policies/plan.toml b/packages/core/src/policy/policies/plan.toml index f7e59c5049..e0c70dc219 100644 --- a/packages/core/src/policy/policies/plan.toml +++ b/packages/core/src/policy/policies/plan.toml @@ -80,7 +80,8 @@ toolName = [ "google_web_search", "activate_skill", "codebase_investigator", - "cli_help" + "cli_help", + "get_internal_docs" ] decision = "allow" priority = 70 diff --git a/packages/core/src/policy/policies/read-only.toml b/packages/core/src/policy/policies/read-only.toml index ad996864b2..8435e49d0b 100644 --- a/packages/core/src/policy/policies/read-only.toml +++ b/packages/core/src/policy/policies/read-only.toml @@ -53,6 +53,6 @@ decision = "allow" priority = 50 [[rule]] -toolName = ["codebase_investigator", "cli_help"] +toolName = ["codebase_investigator", "cli_help", "get_internal_docs"] decision = "allow" priority = 50 \ No newline at end of file diff --git a/packages/core/src/scheduler/tool-executor.test.ts b/packages/core/src/scheduler/tool-executor.test.ts index 6f3c54d358..ff9edd83f3 100644 --- a/packages/core/src/scheduler/tool-executor.test.ts +++ b/packages/core/src/scheduler/tool-executor.test.ts @@ -570,14 +570,13 @@ describe('ToolExecutor', () => { _sig, _tool, _liveCb, - _shellCfg, - setExecutionIdCallback, + options, _config, _originalRequestName, ) => { // Simulate the tool reporting an execution ID - if (setExecutionIdCallback) { - setExecutionIdCallback(testPid); + if (options?.setExecutionIdCallback) { + options.setExecutionIdCallback(testPid); } return { llmContent: 'done', returnDisplay: 'done' }; }, @@ -624,16 +623,8 @@ describe('ToolExecutor', () => { const testExecutionId = 67890; vi.mocked(coreToolHookTriggers.executeToolWithHooks).mockImplementation( - async ( - _inv, - _name, - _sig, - _tool, - _liveCb, - _shellCfg, - setExecutionIdCallback, - ) => { - setExecutionIdCallback?.(testExecutionId); + async (_inv, _name, _sig, _tool, _liveCb, options) => { + options?.setExecutionIdCallback?.(testExecutionId); return { llmContent: 'done', returnDisplay: 'done' }; }, ); diff --git a/packages/core/src/scheduler/tool-executor.ts b/packages/core/src/scheduler/tool-executor.ts index 83d77c5a0b..81232d39d9 100644 --- a/packages/core/src/scheduler/tool-executor.ts +++ b/packages/core/src/scheduler/tool-executor.ts @@ -112,8 +112,7 @@ export class ToolExecutor { signal, tool, liveOutputCallback, - shellExecutionConfig, - setExecutionIdCallback, + { shellExecutionConfig, setExecutionIdCallback }, this.config, request.originalRequestName, ); diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index 069bcd5981..8917d281bd 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -22,13 +22,13 @@ import { type ToolExecuteConfirmationDetails, type PolicyUpdateOptions, type ToolLiveOutput, + type ExecuteOptions, } from './tools.js'; import { getErrorMessage } from '../utils/errors.js'; import { summarizeToolOutput } from '../utils/summarizer.js'; import { ShellExecutionService, - type ShellExecutionConfig, type ShellOutputEvent, } from '../services/shellExecutionService.js'; import { formatBytes } from '../utils/formatters.js'; @@ -150,9 +150,9 @@ export class ShellToolInvocation extends BaseToolInvocation< async execute( signal: AbortSignal, updateOutput?: (output: ToolLiveOutput) => void, - shellExecutionConfig?: ShellExecutionConfig, - setExecutionIdCallback?: (executionId: number) => void, + options?: ExecuteOptions, ): Promise { + const { shellExecutionConfig, setExecutionIdCallback } = options ?? {}; const strippedCommand = stripShellWrapper(this.params.command); if (signal.aborted) { diff --git a/packages/core/src/tools/tool-names.ts b/packages/core/src/tools/tool-names.ts index 91b0574d9e..e818881662 100644 --- a/packages/core/src/tools/tool-names.ts +++ b/packages/core/src/tools/tool-names.ts @@ -266,6 +266,9 @@ export const PLAN_MODE_TOOLS = [ WEB_SEARCH_TOOL_NAME, ASK_USER_TOOL_NAME, ACTIVATE_SKILL_TOOL_NAME, + GET_INTERNAL_DOCS_TOOL_NAME, + 'codebase_investigator', + 'cli_help', ] as const; /** diff --git a/packages/core/src/tools/tools.ts b/packages/core/src/tools/tools.ts index c58396adb8..c94cef4a92 100644 --- a/packages/core/src/tools/tools.ts +++ b/packages/core/src/tools/tools.ts @@ -22,6 +22,15 @@ import { import { type ApprovalMode } from '../policy/types.js'; import type { SubagentProgress } from '../agents/types.js'; +/** + * Options bag for tool execution, replacing positional parameters that are + * only relevant to specific tool types. + */ +export interface ExecuteOptions { + shellExecutionConfig?: ShellExecutionConfig; + setExecutionIdCallback?: (executionId: number) => void; +} + /** * Represents a validated and ready-to-execute tool call. * An instance of this is created by a `ToolBuilder`. @@ -68,8 +77,7 @@ export interface ToolInvocation< execute( signal: AbortSignal, updateOutput?: (output: ToolLiveOutput) => void, - shellExecutionConfig?: ShellExecutionConfig, - setExecutionIdCallback?: (executionId: number) => void, + options?: ExecuteOptions, ): Promise; /** @@ -325,7 +333,7 @@ export abstract class BaseToolInvocation< abstract execute( signal: AbortSignal, updateOutput?: (output: ToolLiveOutput) => void, - shellExecutionConfig?: ShellExecutionConfig, + options?: ExecuteOptions, ): Promise; } @@ -427,6 +435,25 @@ export abstract class DeclarativeTool< readonly extensionId?: string, ) {} + clone(messageBus?: MessageBus): this { + // Note: we cannot use structuredClone() here because it does not preserve + // prototype chains or handle non-serializable properties (like functions). + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + const cloned = Object.assign( + // eslint-disable-next-line no-restricted-syntax + Object.create(Object.getPrototypeOf(this)), + this, + ) as this; + if (messageBus) { + Object.defineProperty(cloned, 'messageBus', { + value: messageBus, + writable: false, + configurable: true, + }); + } + return cloned; + } + get isReadOnly(): boolean { return READ_ONLY_KINDS.includes(this.kind); } @@ -522,10 +549,10 @@ export abstract class DeclarativeTool< params: TParams, signal: AbortSignal, updateOutput?: (output: ToolLiveOutput) => void, - shellExecutionConfig?: ShellExecutionConfig, + options?: ExecuteOptions, ): Promise { const invocation = this.build(params); - return invocation.execute(signal, updateOutput, shellExecutionConfig); + return invocation.execute(signal, updateOutput, options); } /** diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index df802f97a9..1f180ac6dd 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -2013,8 +2013,8 @@ "jitContext": { "title": "JIT Context Loading", "description": "Enable Just-In-Time (JIT) context loading.", - "markdownDescription": "Enable Just-In-Time (JIT) context loading.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `false`", - "default": false, + "markdownDescription": "Enable Just-In-Time (JIT) context loading.\n\n- Category: `Experimental`\n- Requires restart: `yes`\n- Default: `true`", + "default": true, "type": "boolean" }, "useOSC52Paste": {