Skip to content

Commit 2da8b6f

Browse files
committed
wip: mcp tool
1 parent c5b19b2 commit 2da8b6f

File tree

1,101 files changed

+194266
-15
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,101 files changed

+194266
-15
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,7 @@ grd_files_bundled_sources = [
633633
"front_end/panels/ai_chat/core/PageInfoManager.js",
634634
"front_end/panels/ai_chat/core/AgentNodes.js",
635635
"front_end/panels/ai_chat/core/GraphHelpers.js",
636+
"front_end/panels/ai_chat/core/ToolSurfaceProvider.js",
636637
"front_end/panels/ai_chat/core/StateGraph.js",
637638
"front_end/panels/ai_chat/core/Logger.js",
638639
"front_end/panels/ai_chat/core/AgentErrorHandler.js",
@@ -700,6 +701,11 @@ grd_files_bundled_sources = [
700701
"front_end/panels/ai_chat/evaluation/utils/PromptTemplates.js",
701702
"front_end/panels/ai_chat/evaluation/utils/ResponseParsingUtils.js",
702703
"front_end/panels/ai_chat/evaluation/utils/SanitizationUtils.js",
704+
"front_end/panels/ai_chat/mcp/MCPConfig.js",
705+
"front_end/panels/ai_chat/mcp/MCPRegistry.js",
706+
"front_end/panels/ai_chat/mcp/MCPToolAdapter.js",
707+
"front_end/panels/ai_chat/mcp/MCPMetaTools.js",
708+
"front_end/panels/ai_chat/tools/LLMTracingWrapper.js",
703709
"front_end/panels/animation/animation-meta.js",
704710
"front_end/panels/animation/animation.js",
705711
"front_end/panels/application/application-meta.js",
@@ -837,6 +843,17 @@ grd_files_bundled_sources = [
837843
"front_end/third_party/lighthouse/lighthouse-dt-bundle.js",
838844
"front_end/third_party/lighthouse/report/report.js",
839845
"front_end/third_party/lit/lit.js",
846+
"front_end/third_party/mcp-sdk/ajv/dist/ajv.js",
847+
"front_end/third_party/mcp-sdk/zod/lib/index.js",
848+
"front_end/third_party/mcp-sdk/zod/lib/index.mjs",
849+
"front_end/third_party/mcp-sdk/eventsource-parser/package/dist/index.js",
850+
"front_end/third_party/mcp-sdk/eventsource-parser/package/dist/stream.js",
851+
"front_end/third_party/mcp-sdk/mcp-sdk.js",
852+
"front_end/third_party/mcp-sdk/package/dist/client/index.js",
853+
"front_end/third_party/mcp-sdk/package/dist/client/sse.js",
854+
"front_end/third_party/mcp-sdk/package/dist/shared/protocol.js",
855+
"front_end/third_party/mcp-sdk/package/dist/shared/transport.js",
856+
"front_end/third_party/mcp-sdk/package/dist/types.js",
840857
"front_end/third_party/marked/marked.js",
841858
"front_end/third_party/puppeteer-replay/puppeteer-replay.js",
842859
"front_end/third_party/puppeteer/puppeteer.js",

front_end/panels/ai_chat/BUILD.gn

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ devtools_module("ai_chat") {
3434
"core/PageInfoManager.ts",
3535
"core/AgentNodes.ts",
3636
"core/GraphHelpers.ts",
37+
"core/ToolSurfaceProvider.ts",
3738
"core/StateGraph.ts",
3839
"core/Logger.ts",
3940
"core/AgentErrorHandler.ts",
@@ -99,6 +100,10 @@ devtools_module("ai_chat") {
99100
"tracing/TracingConfig.ts",
100101
"auth/PKCEUtils.ts",
101102
"auth/OpenRouterOAuth.ts",
103+
"mcp/MCPConfig.ts",
104+
"mcp/MCPToolAdapter.ts",
105+
"mcp/MCPRegistry.ts",
106+
"mcp/MCPMetaTools.ts",
102107
]
103108

104109
deps = [
@@ -108,6 +113,7 @@ devtools_module("ai_chat") {
108113
"../../core/sdk:bundle",
109114
"../../generated:protocol",
110115
"../../models/logs:bundle",
116+
"../../third_party/mcp-sdk:bundle",
111117
"../../ui/components/helpers:bundle",
112118
"../../ui/components/markdown_view:bundle",
113119
"../../ui/components/snackbars:bundle",
@@ -139,6 +145,7 @@ _ai_chat_sources = [
139145
"core/PageInfoManager.ts",
140146
"core/AgentNodes.ts",
141147
"core/GraphHelpers.ts",
148+
"core/ToolSurfaceProvider.ts",
142149
"core/StateGraph.ts",
143150
"core/Logger.ts",
144151
"core/AgentErrorHandler.ts",
@@ -203,6 +210,10 @@ _ai_chat_sources = [
203210
"tracing/TracingConfig.ts",
204211
"auth/PKCEUtils.ts",
205212
"auth/OpenRouterOAuth.ts",
213+
"mcp/MCPConfig.ts",
214+
"mcp/MCPToolAdapter.ts",
215+
"mcp/MCPRegistry.ts",
216+
"mcp/MCPMetaTools.ts",
206217
]
207218

208219
# Construct the expected JS output paths for the metadata
@@ -287,6 +298,9 @@ ts_library("unittests") {
287298

288299
sources = [
289300
"common/utils.test.ts",
301+
"mcp/MCPClientSDK.test.ts",
302+
"core/ToolSurfaceProvider.test.ts",
303+
"core/ToolNameMapping.test.ts",
290304
]
291305

292306
deps = [

front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export class ToolRegistry {
182182
try {
183183
const instance = factory();
184184
this.registeredTools.set(name, instance);
185-
logger.info('Registered and instantiated tool: ${name}');
185+
logger.info(`Registered and instantiated tool: ${name}`);
186186
} catch (error) {
187187
logger.error(`Failed to instantiate tool '${name}' during registration:`, error);
188188
// Remove the factory entry if instantiation fails

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 {
1919
import { WaitTool } from '../../tools/Tools.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
@@ -94,6 +95,8 @@ Remember: Always use navigate_url to actually go to the constructed URLs. Return
9495
* Initialize all configured agents
9596
*/
9697
export function initializeConfiguredAgents(): void {
98+
// Ensure MCP meta-tools are available regardless of mode; selection logic decides if they are surfaced
99+
registerMCPMetaTools();
97100
// Register core tools
98101
ToolRegistry.registerToolFactory('navigate_url', () => new NavigateURLTool());
99102
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+
});

front_end/panels/ai_chat/core/AgentNodes.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44

55
import type { getTools } from '../tools/Tools.js';
66
import { ChatMessageEntity, type ModelChatMessage, type ToolResultMessage, type ChatMessage, type AgentSessionMessage } from '../ui/ChatView.js';
7-
import { ConfigurableAgentTool } from '../agent_framework/ConfigurableAgentTool.js';
7+
import { ConfigurableAgentTool, ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';
88

99
import { LLMClient } from '../LLM/LLMClient.js';
1010
import type { LLMMessage } from '../LLM/LLMTypes.js';
1111
import { AIChatPanel } from '../ui/AIChatPanel.js';
1212
import { createSystemPromptAsync, getAgentToolsFromState } from './GraphHelpers.js';
13+
import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
1314
import { createLogger } from './Logger.js';
1415
import type { AgentState } from './State.js';
1516
import type { Runnable } from './Types.js';
@@ -142,12 +143,17 @@ export function createAgentNode(modelName: string, temperature: number): Runnabl
142143

143144
try {
144145
const llm = LLMClient.getInstance();
145-
146+
146147
// Get provider for the specific model
147148
const provider = AIChatPanel.getProviderForModel(this.modelName);
148-
149-
// Get tools for the current agent type
150-
const tools = getAgentToolsFromState(state);
149+
150+
// Select tools based on MCP mode (all/router/meta)
151+
const baseTools = getAgentToolsFromState(state);
152+
const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 });
153+
// Persist selection in context so ToolExecutorNode can resolve the same set
154+
if (!state.context) { (state as any).context = {}; }
155+
(state.context as any).selectedToolNames = selection.selectedNames;
156+
const tools = selection.tools;
151157

152158
// Convert ChatMessage[] to LLMMessage[]
153159
const llmMessages = this.convertChatMessagesToLLMMessages(state.messages);
@@ -421,9 +427,21 @@ export function createAgentNode(modelName: string, temperature: number): Runnabl
421427
}
422428

423429
export function createToolExecutorNode(state: AgentState): Runnable<AgentState, AgentState> {
424-
const tools = getAgentToolsFromState(state); // Adjusted to use getAgentToolsFromState
430+
// If AgentNode has pre-selected tool names, honor that set to ensure consistency
431+
const selectedNames: string[] | undefined = (state.context as any)?.selectedToolNames;
432+
let tools: ReturnType<typeof getTools>;
433+
if (selectedNames && selectedNames.length > 0) {
434+
const resolved: any[] = [];
435+
for (const name of selectedNames) {
436+
const inst = ToolRegistry.getRegisteredTool(name as any);
437+
if (inst) { resolved.push(inst as any); }
438+
}
439+
tools = resolved as any;
440+
} else {
441+
tools = getAgentToolsFromState(state) as any;
442+
}
425443
const toolMap = new Map<string, ReturnType<typeof getTools>[number]>();
426-
tools.forEach((tool: ReturnType<typeof getTools>[number]) => toolMap.set(tool.name, tool));
444+
(tools as any[]).forEach((tool: any) => toolMap.set(tool.name, tool));
427445

428446
const toolExecutorNode = new class ToolExecutorNode implements Runnable<AgentState, AgentState> {
429447
private toolMap: Map<string, ReturnType<typeof getTools>[number]>;

0 commit comments

Comments
 (0)