Skip to content

Commit 24e6dc7

Browse files
authored
Add initial MCP version with SDK imports (#44)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - MCP tooling integrated into AI Chat: discovery, router/meta/all modes, per‑turn caps, autostart, allowlisting, dynamic per‑turn tool selection, meta-tools for search/invoke, and sanitized tool-name/function mapping. - **UI** - Settings: full MCP section (enable, endpoint/token, status, connect/refresh/disconnect, mode, caps, per‑tool allowlist); chat: lane‑based timeline for agent tool calls, tool details expanded by default, floating input bar, help opens external docs, beta warning. - **Tests** - New unit/integration tests for MCP SDK, registry, selection, meta‑tools, tool-name sanitization, and result filtering. - **Chores** - Bundled MCP SDK and added license/README assets. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 8317469 commit 24e6dc7

File tree

1,120 files changed

+194804
-306
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

1,120 files changed

+194804
-306
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,8 @@ grd_files_bundled_sources = [
638638
"front_end/panels/ai_chat/core/PageInfoManager.js",
639639
"front_end/panels/ai_chat/core/AgentNodes.js",
640640
"front_end/panels/ai_chat/core/GraphHelpers.js",
641+
"front_end/panels/ai_chat/core/ToolSurfaceProvider.js",
642+
"front_end/panels/ai_chat/core/ToolNameMap.js",
641643
"front_end/panels/ai_chat/core/StateGraph.js",
642644
"front_end/panels/ai_chat/core/Logger.js",
643645
"front_end/panels/ai_chat/core/AgentErrorHandler.js",
@@ -723,6 +725,11 @@ grd_files_bundled_sources = [
723725
"front_end/panels/ai_chat/evaluation/utils/PromptTemplates.js",
724726
"front_end/panels/ai_chat/evaluation/utils/ResponseParsingUtils.js",
725727
"front_end/panels/ai_chat/evaluation/utils/SanitizationUtils.js",
728+
"front_end/panels/ai_chat/mcp/MCPConfig.js",
729+
"front_end/panels/ai_chat/mcp/MCPRegistry.js",
730+
"front_end/panels/ai_chat/mcp/MCPToolAdapter.js",
731+
"front_end/panels/ai_chat/mcp/MCPMetaTools.js",
732+
"front_end/panels/ai_chat/tools/LLMTracingWrapper.js",
726733
"front_end/panels/animation/animation-meta.js",
727734
"front_end/panels/animation/animation.js",
728735
"front_end/panels/application/application-meta.js",
@@ -860,6 +867,7 @@ grd_files_bundled_sources = [
860867
"front_end/third_party/lighthouse/lighthouse-dt-bundle.js",
861868
"front_end/third_party/lighthouse/report/report.js",
862869
"front_end/third_party/lit/lit.js",
870+
"front_end/third_party/mcp-sdk/mcp-sdk.js",
863871
"front_end/third_party/marked/marked.js",
864872
"front_end/third_party/puppeteer-replay/puppeteer-replay.js",
865873
"front_end/third_party/puppeteer/puppeteer.js",
@@ -1372,6 +1380,7 @@ grd_files_unbundled_sources = [
13721380
"front_end/panels/ai_assistance/components/ChatView.js",
13731381
"front_end/panels/ai_assistance/components/ExploreWidget.js",
13741382
"front_end/panels/ai_assistance/components/MarkdownRendererWithCodeBlock.js",
1383+
"front_end/panels/ai_assistance/components/ScrollPinHelper.js",
13751384
"front_end/panels/ai_assistance/components/UserActionRow.js",
13761385
"front_end/panels/ai_assistance/components/chatView.css.js",
13771386
"front_end/panels/ai_assistance/components/exploreWidget.css.js",
@@ -2201,6 +2210,16 @@ grd_files_unbundled_sources = [
22012210
"front_end/third_party/lit/lib/lit.js",
22022211
"front_end/third_party/lit/lib/static-html.js",
22032212
"front_end/third_party/marked/package/lib/marked.esm.js",
2213+
"front_end/third_party/mcp-sdk/ajv/dist/ajv.js",
2214+
"front_end/third_party/mcp-sdk/eventsource-parser/package/dist/index.js",
2215+
"front_end/third_party/mcp-sdk/eventsource-parser/package/dist/stream.js",
2216+
"front_end/third_party/mcp-sdk/package/dist/client/index.js",
2217+
"front_end/third_party/mcp-sdk/package/dist/client/sse.js",
2218+
"front_end/third_party/mcp-sdk/package/dist/shared/protocol.js",
2219+
"front_end/third_party/mcp-sdk/package/dist/shared/transport.js",
2220+
"front_end/third_party/mcp-sdk/package/dist/types.js",
2221+
"front_end/third_party/mcp-sdk/zod/lib/index.js",
2222+
"front_end/third_party/mcp-sdk/zod/lib/index.mjs",
22042223
"front_end/third_party/puppeteer-replay/package/lib/main.js",
22052224
"front_end/third_party/puppeteer/package/lib/esm/puppeteer/api/Browser.js",
22062225
"front_end/third_party/puppeteer/package/lib/esm/puppeteer/api/BrowserContext.js",

front_end/panels/ai_chat/BUILD.gn

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ devtools_module("ai_chat") {
5656
"core/PageInfoManager.ts",
5757
"core/AgentNodes.ts",
5858
"core/GraphHelpers.ts",
59+
"core/ToolNameMap.ts",
60+
"core/ToolSurfaceProvider.ts",
5961
"core/StateGraph.ts",
6062
"core/Logger.ts",
6163
"core/AgentErrorHandler.ts",
@@ -123,6 +125,10 @@ devtools_module("ai_chat") {
123125
"tracing/TracingConfig.ts",
124126
"auth/PKCEUtils.ts",
125127
"auth/OpenRouterOAuth.ts",
128+
"mcp/MCPConfig.ts",
129+
"mcp/MCPToolAdapter.ts",
130+
"mcp/MCPRegistry.ts",
131+
"mcp/MCPMetaTools.ts",
126132
]
127133

128134
deps = [
@@ -132,6 +138,7 @@ devtools_module("ai_chat") {
132138
"../../core/sdk:bundle",
133139
"../../generated:protocol",
134140
"../../models/logs:bundle",
141+
"../../third_party/mcp-sdk:bundle",
135142
"../../ui/components/helpers:bundle",
136143
"../../ui/components/markdown_view:bundle",
137144
"../../ui/components/snackbars:bundle",
@@ -185,6 +192,8 @@ _ai_chat_sources = [
185192
"core/PageInfoManager.ts",
186193
"core/AgentNodes.ts",
187194
"core/GraphHelpers.ts",
195+
"core/ToolNameMap.ts",
196+
"core/ToolSurfaceProvider.ts",
188197
"core/StateGraph.ts",
189198
"core/Logger.ts",
190199
"core/AgentErrorHandler.ts",
@@ -202,6 +211,7 @@ _ai_chat_sources = [
202211
"LLM/MessageSanitizer.ts",
203212
"LLM/LLMClient.ts",
204213
"tools/Tools.ts",
214+
"tools/LLMTracingWrapper.ts",
205215
"tools/CritiqueTool.ts",
206216
"tools/FetcherTool.ts",
207217
"tools/FinalizeWithCritiqueTool.ts",
@@ -251,6 +261,10 @@ _ai_chat_sources = [
251261
"tracing/TracingConfig.ts",
252262
"auth/PKCEUtils.ts",
253263
"auth/OpenRouterOAuth.ts",
264+
"mcp/MCPConfig.ts",
265+
"mcp/MCPToolAdapter.ts",
266+
"mcp/MCPRegistry.ts",
267+
"mcp/MCPMetaTools.ts",
254268
]
255269

256270
# Construct the expected JS output paths for the metadata
@@ -347,6 +361,8 @@ ts_library("unittests") {
347361
"agent_framework/__tests__/AgentRunner.sanitizeToolResult.test.ts",
348362
"agent_framework/__tests__/AgentRunner.computeToolResultText.test.ts",
349363
"agent_framework/__tests__/AgentRunner.run.flows.test.ts",
364+
"mcp/MCPClientSDK.test.ts",
365+
"core/ToolSurfaceProvider.test.ts",
350366
]
351367

352368
deps = [

front_end/panels/ai_chat/agent_framework/AgentRunner.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,7 @@ export class AgentRunner {
977977
logger.info(`${agentName} Executing tool: ${toolToExecute.name}`);
978978
const execTracingContext = getCurrentTracingContext();
979979
toolResultData = await toolToExecute.execute(toolArgs as any, ({
980+
apiKey: config.apiKey,
980981
provider: config.provider,
981982
model: modelName,
982983
miniModel: config.miniModel,

front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import { AgentService } from '../core/AgentService.js';
65
import type { Tool } from '../tools/Tools.js';
76
import { ChatMessageEntity, type ChatMessage } from '../models/ChatTypes.js';
87
import { createLogger } from '../core/Logger.js';
@@ -17,6 +16,7 @@ import { AgentRunner, type AgentRunnerConfig, type AgentRunnerHooks } from './Ag
1716

1817
// Context passed along with agent/tool calls
1918
export interface CallCtx {
19+
apiKey?: string,
2020
provider?: LLMProvider,
2121
model?: string,
2222
miniModel?: string,
@@ -196,7 +196,7 @@ export class ToolRegistry {
196196
try {
197197
const instance = factory();
198198
this.registeredTools.set(name, instance);
199-
logger.info('Registered and instantiated tool: ${name}');
199+
logger.info(`Registered and instantiated tool: ${name}`);
200200
} catch (error) {
201201
logger.error(`Failed to instantiate tool '${name}' during registration:`, error);
202202
// Remove the factory entry if instantiation fails
@@ -402,8 +402,8 @@ export class ConfigurableAgentTool implements Tool<ConfigurableAgentArgs, Config
402402

403403
// Get current tracing context for debugging
404404
const tracingContext = getCurrentTracingContext();
405-
const agentService = AgentService.getInstance();
406-
const apiKey = agentService.getApiKey();
405+
const callCtx = (_ctx || {}) as CallCtx;
406+
const apiKey = callCtx.apiKey;
407407

408408
if (!apiKey) {
409409
const errorResult = this.createErrorResult(`API key not configured for ${this.name}`, [], 'error');
@@ -427,9 +427,6 @@ export class ConfigurableAgentTool implements Tool<ConfigurableAgentArgs, Config
427427
// Initialize
428428
const maxIterations = this.config.maxIterations || 10;
429429

430-
// Parse execution context first
431-
const callCtx = (_ctx || {}) as CallCtx;
432-
433430
// Resolve model name from context or configuration
434431
let modelName: string;
435432
if (this.config.modelName === MODEL_SENTINELS.USE_MINI) {

front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { WaitTool } from '../../tools/Tools.js';
1919
import { MODEL_SENTINELS } from '../../core/Constants.js';
2020
import { ThinkingTool } from '../../tools/ThinkingTool.js';
2121
import type { Tool } from '../../tools/Tools.js';
22+
import { registerMCPMetaTools } from '../../mcp/MCPMetaTools.js';
2223

2324
/**
2425
* Configuration for the Direct URL Navigator Agent
@@ -93,6 +94,8 @@ Remember: Always use navigate_url to actually go to the constructed URLs. Return
9394
* Initialize all configured agents
9495
*/
9596
export function initializeConfiguredAgents(): void {
97+
// Ensure MCP meta-tools are available regardless of mode; selection logic decides if they are surfaced
98+
registerMCPMetaTools();
9699
// Register core tools
97100
ToolRegistry.registerToolFactory('navigate_url', () => new NavigateURLTool());
98101
ToolRegistry.registerToolFactory('navigate_back', () => new NavigateBackTool());
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import { createToolExecutorNode } from './AgentNodes.js';
6+
import { ConfigurableAgentTool } from '../agent_framework/ConfigurableAgentTool.js';
7+
import { ChatMessageEntity } from '../ui/ChatView.js';
8+
import type { AgentState } from './State.js';
9+
import type { ConfigurableAgentResult } from '../agent_framework/ConfigurableAgentTool.js';
10+
11+
declare global {
12+
function describe(name: string, fn: () => void): void;
13+
function it(name: string, fn: () => void): void;
14+
function beforeEach(fn: () => void): void;
15+
function afterEach(fn: () => void): void;
16+
namespace assert {
17+
function strictEqual(actual: unknown, expected: unknown): void;
18+
function deepEqual(actual: unknown, expected: unknown): void;
19+
function isTrue(value: unknown): void;
20+
function isFalse(value: unknown): void;
21+
function doesNotMatch(actual: string, regexp: RegExp): void;
22+
}
23+
}
24+
25+
describe('AgentNodes ToolExecutorNode', () => {
26+
describe('ConfigurableAgentTool result filtering', () => {
27+
it('should filter out agentSession data from error results', async () => {
28+
// Create a mock ConfigurableAgentTool that returns error with intermediateSteps
29+
const mockAgentSession = {
30+
sessionId: 'test-session-123',
31+
agentName: 'test-agent',
32+
status: 'error',
33+
messages: [
34+
{ id: '1', type: 'tool_call', content: { toolName: 'test_tool' } },
35+
{ id: '2', type: 'tool_result', content: { result: 'test result' } }
36+
],
37+
// This contains sensitive data that should not leak to LLM
38+
nestedSessions: [],
39+
tools: [],
40+
startTime: new Date(),
41+
endTime: new Date(),
42+
terminationReason: 'max_iterations'
43+
};
44+
45+
const errorResultWithSession: ConfigurableAgentResult & { agentSession: any } = {
46+
success: false,
47+
error: 'Agent reached maximum iterations',
48+
terminationReason: 'max_iterations',
49+
// This is the problematic field that contains session data
50+
intermediateSteps: [
51+
{ entity: ChatMessageEntity.USER, text: 'test query' },
52+
{ entity: ChatMessageEntity.AGENT_SESSION, agentSession: mockAgentSession, summary: 'test' }
53+
],
54+
agentSession: mockAgentSession
55+
};
56+
57+
// Create mock ConfigurableAgentTool
58+
class MockConfigurableAgentTool extends ConfigurableAgentTool {
59+
constructor() {
60+
super({
61+
name: 'mock_agent',
62+
description: 'Mock agent for testing',
63+
systemPrompt: 'Test prompt',
64+
tools: [],
65+
schema: { type: 'object', properties: {}, required: [] }
66+
});
67+
}
68+
69+
async execute(): Promise<ConfigurableAgentResult & { agentSession: any }> {
70+
return errorResultWithSession;
71+
}
72+
}
73+
74+
const mockTool = new MockConfigurableAgentTool();
75+
76+
// Create initial state with a tool call message
77+
const initialState: AgentState = {
78+
messages: [
79+
{
80+
entity: ChatMessageEntity.MODEL,
81+
action: 'tool',
82+
toolName: 'mock_agent',
83+
toolArgs: { query: 'test' },
84+
toolCallId: 'test-call-id',
85+
isFinalAnswer: false
86+
}
87+
],
88+
agentType: 'web_task',
89+
context: {}
90+
};
91+
92+
// Create modified state with mock tool in registry
93+
const stateWithMockTool = {
94+
...initialState,
95+
// We need to mock the tool registry or provide tools directly
96+
tools: [mockTool]
97+
};
98+
99+
// Create ToolExecutorNode
100+
const toolExecutorNode = createToolExecutorNode(stateWithMockTool);
101+
102+
// Execute the node
103+
const result = await toolExecutorNode.invoke(stateWithMockTool);
104+
105+
// Verify the result
106+
assert.isTrue(result.messages.length > initialState.messages.length, 'Should add tool result message');
107+
108+
const toolResultMessage = result.messages[result.messages.length - 1];
109+
assert.strictEqual(toolResultMessage.entity, ChatMessageEntity.TOOL_RESULT);
110+
111+
// The critical test: resultText should NOT contain agentSession data
112+
const resultText = (toolResultMessage as any).resultText;
113+
assert.strictEqual(resultText, 'Error: Agent reached maximum iterations');
114+
115+
// Verify that the resultText does not contain session data
116+
assert.doesNotMatch(resultText, /sessionId/);
117+
assert.doesNotMatch(resultText, /test-session-123/);
118+
assert.doesNotMatch(resultText, /intermediateSteps/);
119+
assert.doesNotMatch(resultText, /agentSession/);
120+
assert.doesNotMatch(resultText, /nestedSessions/);
121+
122+
// The resultText should be clean and only contain the error message
123+
assert.strictEqual(resultText.includes('test-session-123'), false);
124+
});
125+
126+
it('should filter out agentSession data from success results', async () => {
127+
// Create a success result with intermediateSteps containing session data
128+
const mockAgentSession = {
129+
sessionId: 'success-session-456',
130+
agentName: 'success-agent',
131+
status: 'completed',
132+
messages: [],
133+
nestedSessions: [],
134+
tools: [],
135+
startTime: new Date(),
136+
endTime: new Date(),
137+
terminationReason: 'final_answer'
138+
};
139+
140+
const successResultWithSession: ConfigurableAgentResult & { agentSession: any } = {
141+
success: true,
142+
output: 'Task completed successfully',
143+
terminationReason: 'final_answer',
144+
// This should not leak to LLM
145+
intermediateSteps: [
146+
{ entity: ChatMessageEntity.AGENT_SESSION, agentSession: mockAgentSession, summary: 'test' }
147+
],
148+
agentSession: mockAgentSession
149+
};
150+
151+
class MockSuccessAgentTool extends ConfigurableAgentTool {
152+
constructor() {
153+
super({
154+
name: 'mock_success_agent',
155+
description: 'Mock success agent for testing',
156+
systemPrompt: 'Test prompt',
157+
tools: [],
158+
schema: { type: 'object', properties: {}, required: [] }
159+
});
160+
}
161+
162+
async execute(): Promise<ConfigurableAgentResult & { agentSession: any }> {
163+
return successResultWithSession;
164+
}
165+
}
166+
167+
const mockTool = new MockSuccessAgentTool();
168+
169+
const initialState: AgentState = {
170+
messages: [
171+
{
172+
entity: ChatMessageEntity.MODEL,
173+
action: 'tool',
174+
toolName: 'mock_success_agent',
175+
toolArgs: { query: 'test' },
176+
toolCallId: 'test-call-id-2',
177+
isFinalAnswer: false
178+
}
179+
],
180+
agentType: 'web_task',
181+
context: {}
182+
};
183+
184+
const stateWithMockTool = {
185+
...initialState,
186+
tools: [mockTool]
187+
};
188+
189+
const toolExecutorNode = createToolExecutorNode(stateWithMockTool);
190+
const result = await toolExecutorNode.invoke(stateWithMockTool);
191+
192+
const toolResultMessage = result.messages[result.messages.length - 1];
193+
const resultText = (toolResultMessage as any).resultText;
194+
195+
// Should contain only the clean output
196+
assert.strictEqual(resultText, 'Task completed successfully');
197+
198+
// Should NOT contain session data
199+
assert.doesNotMatch(resultText, /success-session-456/);
200+
assert.doesNotMatch(resultText, /intermediateSteps/);
201+
assert.doesNotMatch(resultText, /agentSession/);
202+
});
203+
});
204+
});

0 commit comments

Comments
 (0)