Skip to content

Conversation

@tysonthomas9
Copy link
Collaborator

@tysonthomas9 tysonthomas9 commented Sep 6, 2025

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 covering MCP SDK, registry, selection, meta‑tools, tool-name sanitization, and result filtering.
  • Chores

    • Bundled MCP SDK and added license/README assets.

@coderabbitai
Copy link

coderabbitai bot commented Sep 6, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Adds MCP integration to AI Chat: new MCP SDK bundle and metadata, config/registry/tool-adapter/meta-tools, per‑turn ToolSurfaceProvider and ToolNameMap, AgentNodes and UI wiring for lane-based routing, tests, and GRD/build list updates.

Changes

Cohort / File(s) Summary
Build & GRD
config/gni/devtools_grd_files.gni, front_end/panels/ai_chat/BUILD.gn, front_end/third_party/mcp-sdk/BUILD.gn
Adds MCP SDK prebuilt and bundle entrypoint; expands GRD lists with bundled and unbundled MCP-related files; exposes new ai_chat public modules and bundle dependency.
GRD additions (bundled / unbundled)
config/gni/devtools_grd_files.gni
Appends 8 bundled entries (ai_chat core/mcp/tools and front_end/third_party/mcp-sdk/mcp-sdk.js) and 11 unbundled entries (e.g., ScrollPinHelper.js, multiple mcp-sdk dist files and zod/ajv/eventsource-parser assets) to GRD lists.
Tool selection & core flow
front_end/panels/ai_chat/core/ToolSurfaceProvider.ts, front_end/panels/ai_chat/core/AgentNodes.ts, front_end/panels/ai_chat/core/ToolNameMap.ts, front_end/panels/ai_chat/core/ToolNameMapping.test.ts, front_end/panels/ai_chat/core/ToolSurfaceProvider.test.ts
Adds ToolSurfaceProvider.select (modes: router/meta/all, scoring, caps, dedupe, debug helpers); ToolNameMap for sanitizing/mapping tool names; integrates per‑turn selection into AgentNodes, persists selected names, propagates uiLane; includes tests for selection and name mapping.
MCP modules
front_end/panels/ai_chat/mcp/MCPConfig.ts, .../MCPRegistry.ts, .../MCPToolAdapter.ts, .../MCPMetaTools.ts, .../MCPClientSDK.test.ts
New MCPConfig (storage + events), MCPRegistry singleton (connect/discover/register), MCPToolAdapter wrapper for MCP tools, MCPMetaSearch/MCPMetaInvoke tools and registration helper, plus MCPClientSDK integration tests.
Agent framework wiring
front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts, front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts, front_end/panels/ai_chat/agent_framework/AgentRunner.ts, front_end/panels/ai_chat/tools/Tools.ts
Registers MCP meta-tools during agent init; propagate apiKey through CallCtx/LLMContext and pass apiKey into tool.execute. Minor logging/template-literal change.
Agent nodes tests & sanitization
front_end/panels/ai_chat/core/AgentNodes.test.ts, front_end/panels/ai_chat/core/__tests__/AgentNodesSanitize.test.ts, front_end/panels/ai_chat/core/AgentNodes.ts
Adds tests ensuring agentSession data is sanitized; AgentNodes now uses ToolSurfaceProvider selection, stores selectedToolNames, resolves sanitized names via ToolNameMap, sets uiLane on messages, and supplies apiKey to tools.
UI integration & settings
front_end/panels/ai_chat/ui/AIChatPanel.ts, front_end/panels/ai_chat/ui/SettingsDialog.ts
AIChatPanel auto-inits and refreshes MCPRegistry and subscribes to config changes; SettingsDialog adds MCP Integration UI (status, allowlist, mode/caps), OpenRouter model caching helper.
Chat UI & lane routing
front_end/panels/ai_chat/ui/ChatView.ts, front_end/panels/ai_chat/ui/ToolCallComponent.ts, front_end/panels/ai_chat/ui/message/MessageCombiner.ts, front_end/panels/ai_chat/models/ChatTypes.ts
Switches from agent-session heuristics to uiLane-based routing (UILane = 'chat'
Tests & test updates
front_end/panels/ai_chat/ui/__tests__/*, front_end/panels/ai_chat/core/*, front_end/panels/ai_chat/ui/message/__tests__/*
Adds and updates unit/UI tests to reflect lane-based routing, expanded timeline default, ToolSurfaceProvider behavior, name sanitization, and sanitization expectations.
Third‑party SDK & metadata
front_end/third_party/mcp-sdk/*, front_end/third_party/additional_readme_paths.json
Adds mcp-sdk package files (README.chromium, LICENSE), ajv examples and licenses, eventsource-parser assets, bundle wrapper and entrypoint, and registers mcp-sdk in additional_readme_paths.json.
Minor UI/UX and CSS
front_end/panels/ai_chat/ui/chatView.css, front_end/panels/ai_chat/ui/input/InputBar.ts, front_end/panels/ai_chat/ui/model_selector/ModelSelector.ts, front_end/panels/ai_chat/ui/message/MessageList.ts, front_end/panels/ai_chat/ui/message/StructuredResponseRender.ts
Adds reserved-space CSS variable and floating input bar layout; model selector preferAbove/forceSearchable props and event; InputBar binds preferAbove; small copy and spacing/UI tweaks.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant User
  participant ChatView
  participant AgentNode
  participant ToolSurfaceProvider
  participant MCPRegistry
  participant ToolExecutor
  participant ToolRegistry

  User->>ChatView: submit message
  ChatView->>AgentNode: createAgentNode(state)
  AgentNode->>ToolSurfaceProvider: select(state, baseTools, opts)
  ToolSurfaceProvider->>MCPRegistry: getStatus()
  alt MCP disabled
    ToolSurfaceProvider-->>AgentNode: return capped baseTools
  else MCP enabled
    ToolSurfaceProvider->>ToolRegistry: resolve MCP tools
    ToolSurfaceProvider-->>AgentNode: return selected tools + selectedNames
  end
  AgentNode->>AgentNode: persist selectedNames in state.context
  AgentNode->>ToolExecutor: execute with selected tools (apiKey forwarded)
  ToolExecutor->>ToolRegistry: getRegisteredTool(name)
  ToolExecutor-->>User: emit TOOL_RESULT / MODEL messages (uiLane set)
Loading
sequenceDiagram
  autonumber
  participant Settings as SettingsDialog
  participant Panel as AIChatPanel
  participant MCPConfig as MCPConfig
  participant Registry as MCPRegistry

  Settings->>MCPConfig: setMCPConfig(values)
  MCPConfig-->>Settings: dispatch config-changed event
  MCPConfig-->>Panel: window event
  Panel->>MCPConfig: getMCPConfig()
  alt enabled && autostart
    Panel->>Registry: init()
    Panel->>Registry: refresh()
    Registry-->>Panel: status
  else
    Panel->>Registry: dispose()
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • olesho

Poem

A twitch of whiskers, a clever little hop,
I fetch MCP tools from the registry top.
Router or meta, I sniff and I score,
Bundles, configs, tests — I tidy the floor.
Hooray for new tools! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.53% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Add initial MCP version with SDK imports" accurately and concisely summarizes the primary change in the changeset (initial MCP integration plus inclusion of the MCP SDK and related files/modules) and directly maps to the added MCP modules, registry, config, UI wiring, and third_party assets in the diff; it is short, clear, and avoids noisy file lists.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1fad5e2 and cdd9df9.

📒 Files selected for processing (2)
  • front_end/panels/ai_chat/ui/HelpDialog.ts (2 hunks)
  • front_end/panels/ai_chat/ui/SettingsDialog.ts (17 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex Review: Here are some suggestions.

Reply with @codex fix comments to fix any unresolved comments.

About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you open a pull request for review, mark a draft as ready, or comment "@codex review". If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex fix this CI failure" or "@codex address that feedback".

Comment on lines 32 to 48
async execute(args: Record<string, unknown>): Promise<unknown> {
const sanitized = this.sanitize(args);
logger.info('Executing MCP tool', { name: this.name, serverId: this.serverId });
return this.client.callTool(this.serverId, this.def.name, sanitized, { timeoutMs: 30000 });
}

// Expose metadata for discovery/search
getServerId(): string { return this.serverId; }
getOriginalToolName(): string { return this.def.name; }

private sanitize(input: Record<string, unknown>): Record<string, unknown> {
const sensitive = ['token', 'api_key', 'password', 'secret', 'authorization'];
const out: Record<string, unknown> = {};
for (const [k, v] of Object.entries(input || {})) {
if (typeof v === 'string' && sensitive.some(s => k.toLowerCase().includes(s))) {
out[k] = '[redacted]';
} else {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Stop redacting tool arguments before executing MCP tools

In MCPToolAdapter.execute the code sanitizes the caller‑supplied args and then passes that sanitized object to client.callTool. Any parameter whose key contains token, api_key, password, etc. is replaced with '[redacted]', so tools that require those values will always receive placeholders instead of the real secrets and will fail to authenticate. The log line should use the sanitized copy, but the RPC call must be made with the original arguments.

Useful? React with 👍 / 👎.

Comment on lines +53 to +134
describe('connects to local Hacker News MCP server via SDK', function() {
this.timeout(30000); // Longer timeout for integration test

const HACKER_NEWS_SERVER: MCPServer = {
id: 'local-hn-sdk',
endpoint: 'http://localhost:5001/sse',
};

it('connects and lists tools', async function() {
console.log('=== Testing Local Hacker News MCP Server with SDK ===');

try {
// Step 1: Connect
console.log('Step 1: Connecting to MCP server via SDK...');
await client.connect(HACKER_NEWS_SERVER);
assert.strictEqual(client.isConnected(HACKER_NEWS_SERVER.id), true, 'Should be connected');
console.log('✓ Connection established via SDK');

// Step 2: List tools
console.log('Step 2: Listing available tools via SDK...');
const tools = await client.listTools(HACKER_NEWS_SERVER.id);
console.log(`✓ Retrieved ${tools.length} tools via SDK`);

// Verify tools structure
assert.ok(Array.isArray(tools), 'Tools should be an array');
if (tools.length > 0) {
const tool = tools[0];
assert.ok(tool.hasOwnProperty('name'), 'Tool should have name');
assert.ok(tool.hasOwnProperty('description'), 'Tool should have description');
assert.ok(tool.hasOwnProperty('inputSchema'), 'Tool should have inputSchema');
console.log(`✓ Tool structure valid: ${tool.name}`);
}

// Step 3: Call a tool (if available)
if (tools.length > 0) {
const firstTool = tools[0];
console.log(`Step 3: Testing tool call: ${firstTool.name}`);

try {
// Create minimal args for the tool
const args: Record<string, unknown> = {};

// Add required parameters if the tool has them
if (firstTool.inputSchema && typeof firstTool.inputSchema === 'object') {
const schema = firstTool.inputSchema as any;
if (schema.required && Array.isArray(schema.required)) {
for (const reqParam of schema.required) {
if (reqParam === 'count' || reqParam.includes('count')) {
args[reqParam] = 3; // Use small count for testing
} else if (reqParam === 'item_id') {
args[reqParam] = 8863; // Classic HN item
} else if (reqParam === 'username') {
args[reqParam] = 'pg'; // Paul Graham
}
}
}
}

console.log(`Calling ${firstTool.name} with args:`, args);
const result = await client.callTool(HACKER_NEWS_SERVER.id, firstTool.name, args);

console.log('Tool call result via SDK:', typeof result);
assert.ok(result !== undefined, 'Tool call should return a result');
console.log('✓ Tool call successful via SDK');

} catch (err) {
console.error(`Tool call failed for ${firstTool.name}:`, err);
// Don't fail the test if tool call fails - might be expected with test data
console.log('Tool call failed, but connection and tools/list worked');
}
}

console.log('✓ All SDK tests passed!');

} catch (error) {
console.error('SDK test failed:', error);
throw error;
} finally {
// Cleanup
client.disconnect(HACKER_NEWS_SERVER.id);
}
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Remove integration test that depends on a local MCP server

The new MCPClientSDK.test.ts adds a test case that tries to connect to http://localhost:5001/sse and exercise real tool calls. This test is included in the unit‑test target and will run in CI where no MCP server is listening, causing the suite to hang or fail. Integration coverage belongs in a separate gated test; unit tests should not rely on external services.

Useful? React with 👍 / 👎.

Comment on lines 150 to 159
// Select tools based on MCP mode (all/router/meta)
const baseTools = getAgentToolsFromState(state);
const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 });
// Persist selection in context so ToolExecutorNode can resolve the same set
if (!state.context) { (state as any).context = {}; }
(state.context as any).selectedToolNames = selection.selectedNames;
const tools = selection.tools;

// Convert ChatMessage[] to LLMMessage[]
const llmMessages = this.convertChatMessagesToLLMMessages(state.messages);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Sanitize MCP tool names before sending them to LLM function calls

When registering MCP tools their original names (e.g. mcp:default:alpha) are forwarded directly in the tools array passed to llm.call. Providers such as OpenAI require function names to match ^[a-zA-Z0-9_-]{1,64}$; colons and other characters are rejected, so any call that includes an MCP tool will fail before the model executes. The tool names should be converted to a provider‑safe representation and a mapping kept so the executor can resolve the original tool.

Useful? React with 👍 / 👎.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 19

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts (1)

196-199: Avoid re-creating tool instances; cache or reuse registered instance

Creating a new instance on every lookup breaks stateful tools (e.g., MCP connections) and wastes resources. Prefer returning a cached/registered instance, falling back to factory once and caching it.

-  static getToolInstance(name: string): Tool<any, any> | null {
-    const factory = this.toolFactories.get(name);
-    return factory ? factory() : null;
-  }
+  static getToolInstance(name: string): Tool<any, any> | null {
+    const existing = this.registeredTools.get(name);
+    if (existing) {
+      return existing;
+    }
+    const factory = this.toolFactories.get(name);
+    if (!factory) return null;
+    const instance = factory();
+    this.registeredTools.set(name, instance);
+    return instance;
+  }
front_end/panels/ai_chat/BUILD.gn (1)

296-312: Mock network calls and pin fixtures in MCPClientSDK tests
MCPClientSDK.test.ts invokes a real HTTP endpoint (http://localhost:5001/sse at line 58); replace this with a jest.mock or bundled in-repo fixture so tests run reliably in CI. ToolSurfaceProvider tests contain no external network calls and need no changes.

front_end/panels/ai_chat/ui/AIChatPanel.ts (5)

78-95: Move CSS and MCP imports to top; dedupe MCPConfig import.

Fixes import/first, import/order, import/no-duplicates.

-import chatViewStyles from './chatView.css.js';
+// (Move up near other imports at file top)
+import chatViewStyles from './chatView.css.js';
@@
-// MCP integration
-import { MCPRegistry } from '../mcp/MCPRegistry.js';
-import { getMCPConfig } from '../mcp/MCPConfig.js';
-import { onMCPConfigChange } from '../mcp/MCPConfig.js';
+// MCP integration
+import { MCPRegistry } from '../mcp/MCPRegistry.js';
+import { getMCPConfig, onMCPConfigChange } from '../mcp/MCPConfig.js';

1790-1793: Fix event listener leak and call MCP unsubscribe on hide.

  • removeEventListener with a fresh bind never removes the original listener.
  • Store a single bound handler; use it for add/remove.
  • Unsubscribe MCP listener.
   override willHide(): void {
-    // Explicitly remove any event listeners to prevent memory leaks
-    this.#agentService.removeEventListener(AgentEvents.MESSAGES_CHANGED, this.#handleMessagesChanged.bind(this));
+    // Explicitly remove listeners to prevent memory leaks
+    this.#agentService.removeEventListener(AgentEvents.MESSAGES_CHANGED, this.#boundMessagesChanged);
+    if (this.#mcpUnsubscribe) {
+      this.#mcpUnsubscribe();
+      this.#mcpUnsubscribe = null;
+    }
   }

Also add a single bound handler field and use it in initialize:

@@
   #agentService = AgentService.getInstance();
+  #boundMessagesChanged = this.#handleMessagesChanged.bind(this);
@@
-    // Remove any existing listeners to prevent duplicates
-    this.#agentService.removeEventListener(AgentEvents.MESSAGES_CHANGED, this.#handleMessagesChanged.bind(this));
+    // Remove any existing listeners to prevent duplicates
+    this.#agentService.removeEventListener(AgentEvents.MESSAGES_CHANGED, this.#boundMessagesChanged);
@@
-    this.#agentService.addEventListener(AgentEvents.MESSAGES_CHANGED, this.#handleMessagesChanged.bind(this));
+    this.#agentService.addEventListener(AgentEvents.MESSAGES_CHANGED, this.#boundMessagesChanged);

730-733: Do not install StorageMonitor in production; it logs secrets.

Overriding localStorage and logging values/length/prefix risks secret leakage. Gate with an explicit debug flag.

-    // Initialize storage monitoring for debugging
-    StorageMonitor.getInstance();
+    // Initialize storage monitoring only when explicitly enabled for debugging
+    if (localStorage.getItem('ai_chat_debug_logging') === '1') {
+      StorageMonitor.getInstance();
+    }

49-69: Redact sensitive values from StorageMonitor logs.

Avoid logging any portion of secrets; keep only key and timestamp.

-        logger.debug(`Value exists: ${!!value}`);
-        logger.debug(`Value length: ${value?.length || 0}`);
-        logger.debug(`Value preview: ${value?.substring(0, 50) + (value?.length > 50 ? '...' : '') || 'null'}`);
+        logger.debug(`Value exists: ${Boolean(value)}`);
+        logger.debug(`(value content redacted)`);
@@
-      if (key.includes('openrouter') || key.includes('ai_chat')) {
+      if (key.includes('openrouter') || key.includes('ai_chat')) {
         logger.debug(`=== LOCALSTORAGE REMOVE ===`);
         logger.debug(`Key: ${key}`);
         logger.debug(`Timestamp: ${new Date().toISOString()}`);
       }

1463-1467: Stop logging API key prefixes.

Even partial prefixes can leak secrets in logs.

-        logger.info('Retrieved API key:');
-        logger.info('- Exists:', !!apiKey);
-        logger.info('- Length:', apiKey?.length || 0);
-        logger.info('- Prefix:', apiKey?.substring(0, 8) + '...' || 'none');
+        logger.info('Retrieved API key:');
+        logger.info('- Exists:', !!apiKey);
+        logger.info('- Length:', apiKey?.length || 0);
front_end/panels/ai_chat/mcp/MCPRegistry.ts (1)

1-154: Add license header.

The file is missing the required license header.

Add the appropriate license header at the beginning of the file. The exact format should match other files in the codebase.

♻️ Duplicate comments (3)
front_end/panels/ai_chat/mcp/MCPToolAdapter.ts (1)

32-36: Don’t send redacted args to the MCP server (use redaction only for logs).

You’re redacting secrets and then passing the redacted payload to callTool, which will break auth for tools that require these fields. Keep a sanitized copy for logging, but send the original args to the client.

   async execute(args: Record<string, unknown>): Promise<unknown> {
-    const sanitized = this.sanitize(args);
-    logger.info('Executing MCP tool', { name: this.name, serverId: this.serverId });
-    return this.client.callTool(this.serverId, this.def.name, sanitized, { timeoutMs: 30000 });
+    const sanitized = this.sanitize(args);
+    logger.info('Executing MCP tool', { name: this.name, serverId: this.serverId, args: sanitized });
+    return this.client.callTool(this.serverId, this.def.name, args, { timeoutMs: 30000 });
   }
front_end/panels/ai_chat/mcp/MCPClientSDK.test.ts (1)

53-55: Skip the local integration test in unit runs.

This block requires a running local server and will fail/hang CI. Mark it skipped by default (enable later behind an env or separate suite).

-describe('connects to local Hacker News MCP server via SDK', function() {
+describe.skip('connects to local Hacker News MCP server via SDK', function() {
   this.timeout(30000); // Longer timeout for integration test
front_end/panels/ai_chat/core/AgentNodes.ts (1)

150-157: Blocker: sanitize tool names for LLM function calls and reverse-map in executor (colons/dots will be rejected).

You're passing raw tool.name values (e.g., mcp:server:tool and mcp.search) directly to providers. OpenAI/Anthropic/Google enforce function-name regex like ^[a-zA-Z0-9_-]{1,64}$, so “:” and “.” will 400 before a token is generated. Add a provider-safe mapping when building the tools array and persist a reverse map in state.context so ToolExecutorNode can resolve the original name. This also addresses the prior P1 review about this exact issue.

Apply these focused patches:

  1. Create selection + mapping, persist reverse map:
-        const baseTools = getAgentToolsFromState(state);
-        const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 });
-        // Persist selection in context so ToolExecutorNode can resolve the same set
-        if (!state.context) { (state as any).context = {}; }
-        (state.context as any).selectedToolNames = selection.selectedNames;
-        const tools = selection.tools;
+        const baseTools = getAgentToolsFromState(state);
+        const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 });
+        // Persist selection and a reverse mapping from safe->original
+        if (!state.context) { (state as any).context = {}; }
+        (state.context as any).selectedToolNames = selection.selectedNames;
+        const toSafe = (n: string) => n.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
+        const safeMap: Record<string, string> = {};
+        const safeTools = selection.tools.map(t => {
+          const safe = toSafe(t.name);
+          safeMap[safe] = t.name;
+          return { tool: t, safe };
+        });
+        (state.context as any).toolNameMap = safeMap;
+        const tools = selection.tools;
  1. Use safe names in tools payload to the provider:
-              tools: tools.map(tool => ({
+              tools: safeTools.map(({ tool, safe }) => ({
                 type: 'function',
                 function: {
-                  name: tool.name,
+                  name: safe,
                   description: tool.description,
                   parameters: tool.schema,
                 }
               })),
  1. Map back from safe name in ToolExecutorNode before lookup:
-      // Get tool details from the ModelChatMessage
-      const toolName = lastMessage.toolName;
+      // Map back from provider-safe name to original registry name (if applicable)
+      const safeToOriginal = ((state.context as any)?.toolNameMap ?? {}) as Record<string, string>;
+      const toolName = safeToOriginal[lastMessage.toolName] ?? lastMessage.toolName;

Also applies to: 176-185, 263-271, 464-471

🧹 Nitpick comments (53)
front_end/panels/ai_chat/ui/ChatView.ts (1)

1783-1789: Update remaining title attributes and add aria-label for “View Full Screen”
Two instances in front_end/panels/ai_chat/ui/ChatView.ts (lines 1786 and 1804) still use title="Open full report in document viewer". Replace both with:

-                title="Open full report in document viewer">
+                title="Open full screen in document viewer"
+                aria-label="Open full screen in document viewer">
front_end/third_party/mcp-sdk/ajv/.runkit_example.js (4)

16-17: Guard example invocations to avoid side effects on import.

Wrap the demo calls so they only run when executed directly.

- test({foo: "abc", bar: 2})
- test({foo: 2, bar: 4})
+if (typeof require !== "undefined" && require.main === module) {
+  test({foo: "abc", bar: 2})
+  test({foo: 2, bar: 4})
+}

1-2: Clarify module intent: ESM in front_end or mark as Node-only.

If this ever gets included in front_end builds, prefer ESM import; otherwise, rename to .cjs or keep excluded.

ESM option:

-const Ajv = require("ajv")
-const ajv = new Ajv({allErrors: true})
+import Ajv from "ajv"
+const ajv = new Ajv({ allErrors: true })

21-22: Improve error readability.

Multi-line errors are easier to scan.

-  else console.log("Invalid: " + ajv.errorsText(validate.errors))
+  else console.log("Invalid:\n" + ajv.errorsText(validate.errors, { separator: "\n" }))

1-23: Exclude dev-only RunKit example from production bundles

No references to .runkit_example.js were found in GRD/GN manifests, but ensure it’s never picked up by your shipping build—either add it to your bundler/package exclusion list or relocate it under a docs/examples folder.

front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts (2)

173-191: Overwriting factories/instances may leak resources; dispose old instances if present

When overwriting, gracefully dispose prior instance if it exposes a dispose() method.

   static registerToolFactory(name: string, factory: () => Tool<any, any>): void {
     if (this.toolFactories.has(name)) {
         logger.warn(`Tool factory already registered for: ${name}. Overwriting.`);
     }
-    if (this.registeredTools.has(name)) {
+    if (this.registeredTools.has(name)) {
         logger.warn(`Tool instance already registered for: ${name}. Overwriting.`);
+        const oldInstance = this.registeredTools.get(name) as any;
+        if (oldInstance && typeof oldInstance.dispose === 'function') {
+          try { oldInstance.dispose(); } catch (e) { logger.warn('Error disposing previous tool instance:', e); }
+        }
     }
     this.toolFactories.set(name, factory);

313-317: Surface missing tools to aid diagnosis

Silent drops make debugging hard. Log missing tool names once.

-  private getToolInstances(): Array<Tool<any, any>> {
-    return this.config.tools
-      .map(toolName => ToolRegistry.getToolInstance(toolName))
-      .filter((tool): tool is Tool<any, any> => tool !== null);
-  }
+  private getToolInstances(): Array<Tool<any, any>> {
+    const instances: Tool<any, any>[] = [];
+    for (const toolName of this.config.tools) {
+      const tool = ToolRegistry.getToolInstance(toolName);
+      if (!tool) {
+        logger.warn(`Tool not found: ${toolName}`);
+        continue;
+      }
+      instances.push(tool);
+    }
+    return instances;
+  }
front_end/third_party/mcp-sdk/README.chromium (3)

1-9: Add provenance details and checksums

Include tarball SHAs (and optionally commit/tag) to strengthen supply-chain traceability.

 Name: Model Context Protocol SDK
 Short Name: mcp-sdk
 URL: https://github.com/modelcontextprotocol/typescript-sdk
 Version: 1.12.3
 License: MIT
 License File: LICENSE
 Security Critical: no
 Shipped: yes
+Source: npm (@modelcontextprotocol/sdk)
+Sha256 (sdk tarball): <fill-in>

17-20: Harden update instructions

Verify tarballs and extract into explicit paths to avoid accidental tree pollution.

-To update this package:
-wget -qO- https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.3.tgz | tar xzf -
-wget -qO- https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz | tar xzf -
+To update this package:
+curl -fsSL https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.3.tgz -o /tmp/mcp-sdk.tgz
+sha256sum /tmp/mcp-sdk.tgz  # verify against the SHA above
+mkdir -p front_end/third_party/mcp-sdk && tar -xzf /tmp/mcp-sdk.tgz -C front_end/third_party/mcp-sdk
+curl -fsSL https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz -o /tmp/eventsource-parser.tgz
+sha256sum /tmp/eventsource-parser.tgz  # verify
+tar -xzf /tmp/eventsource-parser.tgz -C front_end/third_party/mcp-sdk

21-24: Local modifications: add clarity on wrapper entrypoint and build targets

Reference the exact entry (e.g., mcp-sdk.ts) and GN target that consumes it, to streamline future updates.

 Local Modifications:
 - Created mcp-sdk.ts wrapper for DevTools integration
 - Removed Node.js-specific code paths
-- Modified imports to use DevTools module system
+- Modified imports to use DevTools module system
+- Entry point consumed by GN target: //front_end/third_party/mcp-sdk:mcp_sdk
front_end/third_party/mcp-sdk/ajv/README.md (1)

1-208: CI: Exclude front_end/third_party/ from markdownlint and docs QA**

  • Prevent noisy CI failures on this vendor README’s markdown nits and external image links.
  • If this README is ever rendered in-product, ensure remote images aren’t fetched.
front_end/panels/ai_chat/mcp/MCPConfig.ts (6)

33-36: Validate allowlist shape

Ensure parsed JSON is string[]; otherwise ignore.

-    if (raw) {
-      try { toolAllowlist = JSON.parse(raw); } catch { toolAllowlist = undefined; }
-    }
+    if (raw) {
+      try {
+        const parsed = JSON.parse(raw);
+        toolAllowlist = Array.isArray(parsed) && parsed.every(x => typeof x === 'string') ? parsed : undefined;
+      } catch { toolAllowlist = undefined; }
+    }

51-71: Support clearing fields (remove storage keys when undefined/null)

Today undefined skips writes, making some values “sticky”. Explicitly remove keys so users can clear values in Settings.

-    if (config.endpoint !== undefined) {
-      localStorage.setItem(KEYS.endpoint, config.endpoint);
-    }
+    if (config.endpoint !== undefined) {
+      config.endpoint ? localStorage.setItem(KEYS.endpoint, config.endpoint)
+                      : localStorage.removeItem(KEYS.endpoint);
+    }
-    if (config.toolAllowlist) {
+    if (config.toolAllowlist) {
       localStorage.setItem(KEYS.allowlist, JSON.stringify(config.toolAllowlist));
     }
+    if (config.toolAllowlist === undefined) {
+      localStorage.removeItem(KEYS.allowlist);
+    }
-    if (config.toolMode !== undefined) {
-      localStorage.setItem(KEYS.toolMode, config.toolMode);
-    }
+    if (config.toolMode !== undefined) {
+      localStorage.setItem(KEYS.toolMode, config.toolMode);
+    } else {
+      localStorage.removeItem(KEYS.toolMode);
+    }

83-87: Annotate local callback type to satisfy strict return-type lint

Keeps eslint happy without changing behavior.

-export function onMCPConfigChange(handler: () => void): () => void {
-  const cb = () => handler();
+export function onMCPConfigChange(handler: () => void): () => void {
+  const cb: () => void = () => handler();

27-46: Guard against localStorage unavailability

Some embeddings can throw on access (privacy settings). You already catch; consider returning full defaults to avoid partial shape.

Return a fully populated default object on error:

-    logger.error('Failed to load MCP config', err);
-    return { enabled: false };
+    logger.error('Failed to load MCP config', err);
+    return { enabled: false, toolMode: 'router', maxToolsPerTurn: 20, maxMcpPerTurn: 8, autostart: false };

48-77: Emit change event only when values actually change (optional)

Debounce/throttle or diff old vs new to avoid redundant refresh work downstream.


1-96: Security follow-up: can we avoid storing tokens entirely?

If UI needs a token, consider:

  • OAuth + token kept in memory only
  • Short-lived ephemeral sessions
  • Using chrome.identity/devtools APIs

I can propose an end-to-end change removing token persistence from SettingsDialog and flowing tokens via in-memory singleton. Want a patch?

config/gni/devtools_grd_files.gni (1)

846-856: Ordering nit: keep third_party entries alphabetized

Minor: ensure mcp-sdk block stays alphabetically ordered with nearby third_party sections to reduce future conflicts.

front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts (1)

22-22: Fix import ordering to satisfy lint rules

Place ../../mcp/MCPMetaTools.js with other local imports in sorted order (before ../../tools/* as per rule).

-import type { Tool } from '../../tools/Tools.js';
-import { registerMCPMetaTools } from '../../mcp/MCPMetaTools.js';
+import type { Tool } from '../../tools/Tools.js';
+import { registerMCPMetaTools } from '../../mcp/MCPMetaTools.js';

If your lint requires grouping then alphabetical within group, move the MCP import to the top of the local (../../) block.

front_end/panels/ai_chat/core/ToolNameMapping.test.ts (1)

71-79: Lint cleanups: trailing space and assert helper.

  • Remove trailing whitespace.
  • Use assert.isOk per rulesdir/prefer-assert-is-ok.
-    const result = await node.invoke(state);
-    
+    const result = await node.invoke(state);
@@
-    assert.ok((last as any).resultText);
+    assert.isOk((last as {resultText: unknown}).resultText);
front_end/panels/ai_chat/core/AgentNodes.test.ts (2)

112-124: Type-safe extraction and preferred boolean assertion.

-      const resultText = (toolResultMessage as any).resultText;
+      const resultText = (toolResultMessage as {resultText: string}).resultText;
@@
-      assert.strictEqual(resultText.includes('test-session-123'), false);
+      assert.isFalse(resultText.includes('test-session-123'));

193-203: Avoid any; remove trailing spaces; ensure EOF newline.

-      const resultText = (toolResultMessage as any).resultText;
-      
+      const resultText = (toolResultMessage as {resultText: string}).resultText;
+
front_end/panels/ai_chat/ui/AIChatPanel.ts (3)

776-800: Add explicit return type; ensure safe re-init on config changes.

  • Add ": void" to satisfy explicit-function-return-type.
  • Keep try/catch; good.
-  #setupMCPIntegration(): void {
+  #setupMCPIntegration(): void {
     const initAndRefresh = async () => {
       try {
         const mcpConfig = getMCPConfig();
         // Only auto-connect if both enabled and autostart are true
         if (mcpConfig.enabled && mcpConfig.autostart) {
           await MCPRegistry.init();
           await MCPRegistry.refresh();
           const status = MCPRegistry.getStatus();
           logger.info('MCP autostart completed', status);
         }
       } catch (err) {
         logger.error('Failed to initialize MCP', err);
       }
     };
 
     void initAndRefresh();
     // Subscribe to config changes
     this.#mcpUnsubscribe = onMCPConfigChange(() => { void initAndRefresh(); });
   }

2149-2154: Guard MCP re-init with current config; avoid unnecessary work when disabled.

-    try {
-      await MCPRegistry.init();
-      await MCPRegistry.refresh();
-    } catch (err) {
+    try {
+      const cfg = getMCPConfig();
+      if (cfg.enabled) {
+        await MCPRegistry.init();
+        await MCPRegistry.refresh();
+      }
+    } catch (err) {
       logger.error('Failed to reinitialize MCP after settings change', err);
     }

1989-2004: Prefer unknown over any in catch; format error safely.

-    } catch (error: any) {
+    } catch (error: unknown) {
@@
-      logger.error('Error in bookmark click handler', { error: error.message });
+      const msg = error instanceof Error ? error.message : String(error);
+      logger.error('Error in bookmark click handler', { error: msg });
@@
-        message: `Error bookmarking page: ${error.message}`,
+        message: `Error bookmarking page: ${msg}`,
front_end/panels/ai_chat/mcp/MCPToolAdapter.ts (3)

1-6: Fix license header and import order to satisfy lints.

Add the standard license header and order value imports before type-only imports; third_party types before local types.

+// Copyright 2025 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
-import type { Tool } from '../tools/Tools.js';
-import { createLogger } from '../core/Logger.js';
-import type { MCPClient, MCPToolDef } from '../../../third_party/mcp-sdk/mcp-sdk.js';
+import { createLogger } from '../core/Logger.js';
+import type { MCPClient, MCPToolDef } from '../../../third_party/mcp-sdk/mcp-sdk.js';
+import type { Tool } from '../tools/Tools.js';

10-10: Remove explicit any in schema typing.

Use unknown to appease eslint. Keep the fallback object.

-  schema: any;
+  schema: unknown;
@@
-    const schema = def.inputSchema as any;
+    const schema = def.inputSchema as unknown;
     if (schema && typeof schema === 'object') {
       this.schema = schema;
     } else {
       this.schema = { type: 'object', properties: {} };
     }

Also applies to: 24-29


42-53: Broaden secret key redaction list (log-only).

Consider also 'client_secret', 'access_token', 'refresh_token', 'set-cookie', 'cookie'. Keep redaction strictly for logs.

-    const sensitive = ['token', 'api_key', 'password', 'secret', 'authorization'];
+    const sensitive = ['token', 'api_key', 'password', 'secret', 'authorization', 'client_secret', 'access_token', 'refresh_token', 'set-cookie', 'cookie'];
front_end/panels/ai_chat/core/ToolSurfaceProvider.test.ts (4)

5-11: Fix import order to satisfy es-modules rules.

Import MCP modules before core ToolSurfaceProvider/types as per lints.

-import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
-import type { AgentState } from './State.js';
-import type { Tool } from '../tools/Tools.js';
-import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';
-import { MCPRegistry } from '../mcp/MCPRegistry.js';
-import { registerMCPMetaTools } from '../mcp/MCPMetaTools.js';
+import { registerMCPMetaTools } from '../mcp/MCPMetaTools.js';
+import { MCPRegistry } from '../mcp/MCPRegistry.js';
+import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
+import type { AgentState } from './State.js';
+import type { Tool } from '../tools/Tools.js';
+import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';

95-102: Prefer assertion helpers for readability and lint compliance.

Use include/lengthOf helpers.

-    assert.ok(names.includes('core_tool_A'));
-    assert.ok(names.includes('core_tool_B'));
+    assert.include(names, 'core_tool_A');
+    assert.include(names, 'core_tool_B');
     const selectedMcp = names.filter(n => n.startsWith('mcp:default:'));
-    assert.strictEqual(selectedMcp.length, 2);
+    assert.lengthOf(selectedMcp, 2);
     // Alpha should be preferred due to query match
-    assert.ok(selectedMcp.includes('mcp:default:alpha'));
+    assert.include(selectedMcp, 'mcp:default:alpha');

129-135: Align assertions with chai helpers.

-    assert.ok(names.includes('core_tool_A'));
-    assert.ok(names.includes('mcp.search'));
-    assert.ok(names.includes('mcp.invoke'));
+    assert.include(names, 'core_tool_A');
+    assert.include(names, 'mcp.search');
+    assert.include(names, 'mcp.invoke');

61-70: Avoid global registry cross-test state.

ToolRegistry is global; consider adding a test util to reset/teardown between tests to prevent leakage.

I can draft a ToolRegistry.resetForTests() helper and wire it in beforeEach/afterEach if you want.

Also applies to: 80-82

front_end/third_party/mcp-sdk/BUILD.gn (1)

9-45: LGTM on prebuilt packaging; ensure third-party licenses are shipped.

The AJV, Zod, and eventsource-parser artifacts need corresponding LICENSE files in third_party and inclusion in GRD.

If not already done in this PR, please add/copy their LICENSE files and update devtools_grd_files.gni to include them.

front_end/panels/ai_chat/ui/SettingsDialog.ts (5)

2487-2510: Use braces and explicit return types per lint.

Wrap single-line if and annotate return types on helpers.

-    const formatTimestamp = (date: Date | undefined): string => {
-      if (!date) return '';
-      return date.toLocaleString();
-    };
+    const formatTimestamp = (date: Date | undefined): string => {
+      if (!date) {
+        return '';
+      }
+      return date.toLocaleString();
+    };

2791-2796: Clear tool list without innerHTML.

Use replaceChildren() to avoid innerHTML assignment.

-      mcpToolsList.innerHTML = '';
+      mcpToolsList.replaceChildren();

535-545: Remove unused catch param.

Avoid ‘e’ unused var.

-    } catch (e) {
+    } catch {
       // Fallback silently to storedProvider
     }

2579-2584: Add explicit return types to local helpers.

Appease @typescript-eslint/explicit-function-return-type.

-    const updateDisconnectButton = () => {
+    const updateDisconnectButton = (): void => {
@@
-    const updateBudgetControls = () => {
+    const updateBudgetControls = (): void => {
@@
-    const updateToolsList = () => {
+    const updateToolsList = (): void => {

Also applies to: 2758-2768, 2791-2845


366-371: Prefer secure endpoint hint.

For MCP endpoints, prefer wss:// over ws:// in hint text.

-  mcpEndpointHint: 'HTTPS SSE or WebSocket endpoint (e.g., https://host/mcp or ws://localhost:9000)',
+  mcpEndpointHint: 'HTTPS SSE or secure WebSocket endpoint (e.g., https://host/mcp or wss://localhost:9000)',
front_end/panels/ai_chat/core/AgentNodes.ts (4)

130-137: Trace the actually selected tools, not the base list.

The observation input logs tools from getAgentToolsFromState(state), which can diverge from ToolSurfaceProvider.select. Either move selection earlier and log the selected set, or append an update to include selection.selectedNames for accuracy.


38-41: Replace console.log with logger. or guard under a debug flag (no-console violations).*

There are many console.log statements; use logger.debug/info/warn or guard them behind an explicit debug flag to satisfy lint rules and avoid noisy prod logs.

Also applies to: 234-257, 485-567, 569-573, 579-606, 615-623, 671-681, 711-717, 747-749


154-156: Type safety for context fields and tool arrays (avoid any).

Define selectedToolNames and toolNameMap on AgentState.context and type tools as Array<Tool<unknown, unknown>> to clear the no-explicit-any lint errors and prevent accidental shape drift.

I can add the minimal context typings in State.ts if you want.

Also applies to: 431-442, 444-445


5-14: Fix import ordering per ESLint import/order.

Reorder imports so value imports precede type-only imports, and maintain group spacing as flagged by lint.

front_end/panels/ai_chat/mcp/MCPMetaTools.ts (2)

121-124: Lint cleanups: remove unused q; add curly braces to single-line ifs.

Drop the unused q in execute() and wrap single-line ifs flagged by curly.

-    const q = (args.query || '').toLowerCase();
+    // no-op
@@
-      if (!key.startsWith('mcp:')) continue;
-      if (serverFilter) {
-        const serverId = key.split(':')[1] || '';
-        if (!serverFilter.has(serverId)) continue;
-      }
-      const tool = ToolRegistry.getRegisteredTool(key);
-      if (!tool) continue;
+      if (!key.startsWith('mcp:')) { continue; }
+      if (serverFilter) {
+        const serverId = key.split(':')[1] || '';
+        if (!serverFilter.has(serverId)) { continue; }
+      }
+      const tool = ToolRegistry.getRegisteredTool(key);
+      if (!tool) { continue; }

Also applies to: 127-135


5-11: Import order/spacing per ESLint.

Reorder so value imports precede type imports and keep group breaks.

front_end/panels/ai_chat/core/ToolSurfaceProvider.ts (4)

79-111: Avoid global pollution and console noise; gate debug helpers and logs.

Attach debugToolSelection only in dev builds or behind an explicit flag, and replace console.log with logger.* to satisfy no-console.

Example:

-// DEBUG: Add a utility function to test MCP modes from console
-(globalThis as any).debugToolSelection = {
+// DEBUG (dev-only)
+if (/* inject your dev flag */ false) {
+  (globalThis as any).debugToolSelection = {
     getCurrentMCPConfig: () => {
-      const cfg = getMCPConfig();
-      console.log('Current MCP Config:', cfg);
+      const cfg = getMCPConfig();
+      logger.debug('Current MCP Config:', cfg);
       return cfg;
     },
@@
-};
+}

Also applies to: 118-128, 133-147, 149-161, 185-191, 194-215


192-195: Use ChatMessageEntity instead of string/any for user detection.

Import ChatMessageEntity and compare against the enum; avoid (m as any).entity === 0.

+import { ChatMessageEntity } from '../ui/ChatView.js';
@@
-    const lastUserMsg = [...state.messages].reverse().find(m => m.entity === 'user' || (m as any).entity === 0) as any;
+    const lastUserMsg = [...state.messages].reverse().find(m => m.entity === ChatMessageEntity.USER);

20-30: Type nits: prefer Array forms and drop unused selectedNames var.

  • Use Array<Tool<unknown, unknown>> instead of Tool<any, any>[] to appease lint.
  • Remove unused local selectedNames (you already return computed names).

Also applies to: 112-116, 129-132


5-12: Fix import ordering per ESLint import/order.

Place MCPConfig before MCPRegistry per hints, and group type-only imports last.

front_end/panels/ai_chat/mcp/MCPRegistry.ts (5)

11-18: Fix interface formatting.

The interface members should use semicolons instead of being implicitly terminated.

Apply this diff to fix the formatting:

 export interface MCPRegistryStatus {
   enabled: boolean;
-  servers: Array<{ id: string; endpoint: string; connected: boolean; toolCount: number }>;
+  servers: Array<{ id: string; endpoint: string; connected: boolean; toolCount: number; }>;
   registeredToolNames: string[];
   lastError?: string;
   lastErrorType?: 'connection' | 'authentication' | 'configuration' | 'network' | 'server_error' | 'unknown';
   lastConnected?: Date;
   lastDisconnected?: Date;
 }

29-48: Remove trailing spaces.

There are trailing spaces on lines 31 and 36 that should be removed.

Apply this diff to remove trailing spaces:

   private categorizeError(error: unknown): 'connection' | 'authentication' | 'configuration' | 'network' | 'server_error' | 'unknown' {
     const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
-    
+
     if (message.includes('unauthorized') || message.includes('authentication') || message.includes('auth') || message.includes('token')) {
       return 'authentication';
     }
     if (message.includes('network') || message.includes('timeout') || message.includes('connection reset') || message.includes('econnreset')) {
-      return 'network';  
+      return 'network';
     }

87-125: Remove trailing spaces in refresh method.

There are trailing spaces on lines 92 and 95 that should be removed.

Apply this diff to remove trailing spaces:

   async refresh(): Promise<void> {
     const cfg = getMCPConfig();
     if (!cfg.enabled || this.servers.length === 0) {
       return;
     }
-    
+
     // Clear previously registered tools (ToolRegistry will overwrite on re-registration)
     this.registeredTools = [];
-    
+
     const allow = new Set(cfg.toolAllowlist || []);

127-133: Improve error handling in dispose method.

The empty catch block silently swallows errors. Consider at least logging disconnection failures for debugging purposes.

Apply this diff to improve error handling:

   dispose(): void {
     for (const srv of this.servers) {
-      try { this.client.disconnect(srv.id); } catch {}
+      try {
+        this.client.disconnect(srv.id);
+      } catch (err) {
+        logger.warn('Failed to disconnect MCP server', { serverId: srv.id, err });
+      }
     }
     this.lastDisconnected = new Date();
     this.servers = [];
   }

55-85: Consider adding retry logic for connection failures.

The init method attempts to connect once and logs the error if it fails. For better resilience, consider implementing retry logic with exponential backoff.

Would you like me to generate a retry mechanism with exponential backoff for the connection logic?

Comment on lines +45 to +56
const errorResultWithSession: ConfigurableAgentResult & { agentSession: any } = {
success: false,
error: 'Agent reached maximum iterations',
terminationReason: 'max_iterations',
// This is the problematic field that contains session data
intermediateSteps: [
{ entity: ChatMessageEntity.USER, text: 'test query' },
{ entity: ChatMessageEntity.AGENT_SESSION, agentSession: mockAgentSession, summary: 'test' }
],
agentSession: mockAgentSession
};

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Replace any with a typed extension; avoid leaking agentSession types into LLM.

-      const errorResultWithSession: ConfigurableAgentResult & { agentSession: any } = {
+      type AgentResultWithSession = ConfigurableAgentResult & { agentSession: unknown };
+      const errorResultWithSession: AgentResultWithSession = {
@@
-      class MockConfigurableAgentTool extends ConfigurableAgentTool {
+      class MockConfigurableAgentTool extends ConfigurableAgentTool {
@@
-        async execute(): Promise<ConfigurableAgentResult & { agentSession: any }> {
+        async execute(): Promise<AgentResultWithSession> {
           return errorResultWithSession;
         }
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const errorResultWithSession: ConfigurableAgentResult & { agentSession: any } = {
success: false,
error: 'Agent reached maximum iterations',
terminationReason: 'max_iterations',
// This is the problematic field that contains session data
intermediateSteps: [
{ entity: ChatMessageEntity.USER, text: 'test query' },
{ entity: ChatMessageEntity.AGENT_SESSION, agentSession: mockAgentSession, summary: 'test' }
],
agentSession: mockAgentSession
};
// Define a reusable result type that carries an opaque session
type AgentResultWithSession = ConfigurableAgentResult & { agentSession: unknown };
const errorResultWithSession: AgentResultWithSession = {
success: false,
error: 'Agent reached maximum iterations',
terminationReason: 'max_iterations',
// This is the problematic field that contains session data
intermediateSteps: [
{ entity: ChatMessageEntity.USER, text: 'test query' },
{ entity: ChatMessageEntity.AGENT_SESSION, agentSession: mockAgentSession, summary: 'test' }
],
agentSession: mockAgentSession
};
class MockConfigurableAgentTool extends ConfigurableAgentTool {
// Use the alias here instead of repeating an `any`-based type
async execute(): Promise<AgentResultWithSession> {
return errorResultWithSession;
}
}
🧰 Tools
🪛 ESLint

[error] 45-45: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/AgentNodes.test.ts around lines 45-56, the test
uses "any" for agentSession and embeds full session objects in
intermediateSteps, leaking agentSession internals into LLM-facing step data; fix
by replacing "any" with a concrete typed extension: import/define an
AgentSession (or AgentSessionSummary) type and type the test value as
ConfigurableAgentResult & { agentSession: AgentSession | null }; change
intermediateSteps so AGENT_SESSION steps carry a minimal typed summary (e.g., {
entity: ChatMessageEntity.AGENT_SESSION; agentSession: AgentSessionSummary;
summary: string }) or remove the full session from intermediateSteps and only
store it on agentSession; update the mock to match the new types and ensure no
raw session internals are passed into LLM-facing fields.

Comment on lines +140 to +166
const successResultWithSession: ConfigurableAgentResult & { agentSession: any } = {
success: true,
output: 'Task completed successfully',
terminationReason: 'final_answer',
// This should not leak to LLM
intermediateSteps: [
{ entity: ChatMessageEntity.AGENT_SESSION, agentSession: mockAgentSession, summary: 'test' }
],
agentSession: mockAgentSession
};

class MockSuccessAgentTool extends ConfigurableAgentTool {
constructor() {
super({
name: 'mock_success_agent',
description: 'Mock success agent for testing',
systemPrompt: 'Test prompt',
tools: [],
schema: { type: 'object', properties: {}, required: [] }
});
}

async execute(): Promise<ConfigurableAgentResult & { agentSession: any }> {
return successResultWithSession;
}
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Mirror type cleanup for success-path tool result.

-      const successResultWithSession: ConfigurableAgentResult & { agentSession: any } = {
+      type AgentResultWithSession = ConfigurableAgentResult & { agentSession: unknown };
+      const successResultWithSession: AgentResultWithSession = {
@@
-        async execute(): Promise<ConfigurableAgentResult & { agentSession: any }> {
+        async execute(): Promise<AgentResultWithSession> {
           return successResultWithSession;
         }
🧰 Tools
🪛 ESLint

[error] 140-140: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 162-162: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

Comment on lines +12 to +21
class RecordingTool implements Tool<Record<string, unknown>, unknown> {
public calls = 0;
constructor(public name: string) {}
description = 'records calls';
schema = { type: 'object', properties: {} };
async execute(_args: Record<string, unknown>): Promise<unknown> {
this.calls += 1;
return { ok: true, executed: this.name };
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Drop explicit public modifier; keep constructor param readonly.

Satisfies @typescript-eslint/explicit-member-accessibility and keeps intent.

-class RecordingTool implements Tool<Record<string, unknown>, unknown> {
-  public calls = 0;
-  constructor(public name: string) {}
+class RecordingTool implements Tool<Record<string, unknown>, unknown> {
+  calls = 0;
+  constructor(readonly name: string) {}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class RecordingTool implements Tool<Record<string, unknown>, unknown> {
public calls = 0;
constructor(public name: string) {}
description = 'records calls';
schema = { type: 'object', properties: {} };
async execute(_args: Record<string, unknown>): Promise<unknown> {
this.calls += 1;
return { ok: true, executed: this.name };
}
}
class RecordingTool implements Tool<Record<string, unknown>, unknown> {
calls = 0;
constructor(readonly name: string) {}
description = 'records calls';
schema = { type: 'object', properties: {} };
async execute(_args: Record<string, unknown>): Promise<unknown> {
this.calls += 1;
return { ok: true, executed: this.name };
}
}
🧰 Tools
🪛 ESLint

[error] 13-13: Public accessibility modifier on class property calls.

(@typescript-eslint/explicit-member-accessibility)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/ToolNameMapping.test.ts around lines 12–21,
remove explicit "public" accessibility modifiers and make the constructor
parameter readonly: change "public calls = 0;" to "calls = 0;" and change
"constructor(public name: string) {}" to "constructor(readonly name: string)
{}", leaving description and schema as-is.

Comment on lines +26 to +42
beforeEach(() => {
// Mock localStorage in case anything touches it
mockLocalStorage = new Map();
Object.defineProperty(window, 'localStorage', {
value: {
getItem: (key: string) => mockLocalStorage.get(key) || null,
setItem: (key: string, value: string) => mockLocalStorage.set(key, value),
removeItem: (key: string) => mockLocalStorage.delete(key),
clear: () => mockLocalStorage.clear(),
},
writable: true,
});
});

afterEach(() => {
mockLocalStorage.clear();
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Restore real localStorage after each test to avoid cross-test side effects.

Current code overrides window.localStorage but never restores it.

 describe('AgentNodes sanitized tool name mapping', () => {
-  let mockLocalStorage: Map<string, string>;
+  let mockLocal: Map<string, string>;
+  let realLocalStorage: Storage;
 
   beforeEach(() => {
-    // Mock localStorage in case anything touches it
-    mockLocalStorage = new Map();
+    mockLocal = new Map();
+    realLocalStorage = window.localStorage;
     Object.defineProperty(window, 'localStorage', {
       value: {
-        getItem: (key: string) => mockLocalStorage.get(key) || null,
-        setItem: (key: string, value: string) => mockLocalStorage.set(key, value),
-        removeItem: (key: string) => mockLocalStorage.delete(key),
-        clear: () => mockLocalStorage.clear(),
+        getItem: (key: string) => mockLocal.get(key) || null,
+        setItem: (key: string, value: string) => mockLocal.set(key, value),
+        removeItem: (key: string) => mockLocal.delete(key),
+        clear: () => mockLocal.clear(),
       },
       writable: true,
     });
   });
 
   afterEach(() => {
-    mockLocalStorage.clear();
+    mockLocal.clear();
+    Object.defineProperty(window, 'localStorage', { value: realLocalStorage, writable: true });
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
beforeEach(() => {
// Mock localStorage in case anything touches it
mockLocalStorage = new Map();
Object.defineProperty(window, 'localStorage', {
value: {
getItem: (key: string) => mockLocalStorage.get(key) || null,
setItem: (key: string, value: string) => mockLocalStorage.set(key, value),
removeItem: (key: string) => mockLocalStorage.delete(key),
clear: () => mockLocalStorage.clear(),
},
writable: true,
});
});
afterEach(() => {
mockLocalStorage.clear();
});
describe('AgentNodes sanitized tool name mapping', () => {
let mockLocal: Map<string, string>;
let realLocalStorage: Storage;
beforeEach(() => {
mockLocal = new Map();
realLocalStorage = window.localStorage;
Object.defineProperty(window, 'localStorage', {
value: {
getItem: (key: string) => mockLocal.get(key) || null,
setItem: (key: string, value: string) => mockLocal.set(key, value),
removeItem: (key: string) => mockLocal.delete(key),
clear: () => mockLocal.clear(),
},
writable: true,
});
});
afterEach(() => {
mockLocal.clear();
Object.defineProperty(window, 'localStorage', { value: realLocalStorage, writable: true });
});
// ...other tests...
});
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/ToolNameMapping.test.ts around lines 26 to 42,
the test suite replaces window.localStorage but never restores the original,
causing cross-test side effects; capture the originalLocalStorage before
redefining window.localStorage in beforeEach, continue to set the mock there,
and then in afterEach restore window.localStorage = originalLocalStorage (and
clear/mockLocalStorage as needed) so the real browser/storage API is reinstated
for subsequent tests.

Comment on lines 50 to 67
const state: AgentState = {
messages: [
{
entity: ChatMessageEntity.MODEL,
action: 'tool',
toolName: sanitizedName,
toolArgs: { x: 1 },
toolCallId: 'call-1',
isFinalAnswer: false,
} as any
],
agentType: 'deep-research' as any,
context: {
selectedToolNames: [sanitizedName],
selectedTools: [tool],
toolNameMap: { [sanitizedName]: originalName }
}
} as any;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove any casts; type the message and state precisely.

-    const state: AgentState = {
+    const modelToolCall: ModelChatMessage = {
+      entity: ChatMessageEntity.MODEL,
+      action: 'tool',
+      toolName: sanitizedName,
+      toolArgs: { x: 1 } as Record<string, unknown>,
+      toolCallId: 'call-1',
+      isFinalAnswer: false,
+    };
+    const state = {
       messages: [
-        {
-          entity: ChatMessageEntity.MODEL,
-          action: 'tool',
-          toolName: sanitizedName,
-          toolArgs: { x: 1 },
-          toolCallId: 'call-1',
-          isFinalAnswer: false,
-        } as any
+        modelToolCall,
       ],
       agentType: 'deep-research' as any,
       context: {
         selectedToolNames: [sanitizedName],
         selectedTools: [tool],
         toolNameMap: { [sanitizedName]: originalName }
       }
-    } as any;
+    };

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 ESLint

[error] 59-59: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 61-61: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 67-67: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/ToolNameMapping.test.ts around lines 50–67,
remove all "as any" casts and construct the message and state with their real
types: import and use the exact message/interface types (e.g.,
ChatMessage/AgentMessage and AgentState), create a strongly typed message object
using the ChatMessageEntity enum for entity, a properly typed action union for
'tool', set toolName/toolArgs/toolCallId/isFinalAnswer with correct types,
assign agentType using the AgentType type, and build context with the exact
typed shape (selectedToolNames: string[], selectedTools: Tool[], toolNameMap:
Record<string,string>); replace each "as any" with these concrete typed objects
so the test is fully type-safe.

Comment on lines +135 to +150
getStatus(): MCPRegistryStatus {
return {
enabled: getMCPConfig().enabled,
servers: this.servers.map(s => ({
id: s.id,
endpoint: s.endpoint,
connected: this.client.isConnected(s.id),
toolCount: 0,
})),
registeredToolNames: [...this.registeredTools],
lastError: this.lastError,
lastErrorType: this.lastErrorType,
lastConnected: this.lastConnected,
lastDisconnected: this.lastDisconnected,
};
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

toolCount is always 0 in getStatus.

The toolCount field in the server status is hardcoded to 0, which doesn't provide accurate information about the number of tools available from each server.

Consider tracking the actual tool count per server. You could maintain a Map to store tool counts:

 class RegistryImpl {
   private client = new MCPClient();
   private servers: MCPServer[] = [];
   private registeredTools: string[] = [];
+  private toolCountPerServer = new Map<string, number>();
   private lastError?: string;

Then update it in the refresh method:

   async refresh(): Promise<void> {
     const cfg = getMCPConfig();
     if (!cfg.enabled || this.servers.length === 0) {
       return;
     }
 
     // Clear previously registered tools (ToolRegistry will overwrite on re-registration)
     this.registeredTools = [];
+    this.toolCountPerServer.clear();
 
     const allow = new Set(cfg.toolAllowlist || []);
 
     for (const srv of this.servers) {
       if (!this.client.isConnected(srv.id)) {
         continue;
       }
+      let registeredCount = 0;
       let tools: MCPToolDef[] = [];
       try {
         tools = await this.client.listTools(srv.id);
       } catch (err) {
         this.setError(err);
         logger.error('listTools failed', err);
         continue;
       }
 
       for (const def of tools) {
         const namespaced = `mcp:${srv.id}:${def.name}`;
         if (allow.size > 0 && !allow.has(namespaced) && !allow.has(def.name)) {
           continue;
         }
         try {
           const factoryName = namespaced;
           ToolRegistry.registerToolFactory(factoryName, () => new MCPToolAdapter(srv.id, this.client, def, namespaced));
           this.registeredTools.push(factoryName);
+          registeredCount++;
         } catch (err) {
           logger.error('Failed to register MCP tool', { tool: def.name, err });
         }
       }
+      this.toolCountPerServer.set(srv.id, registeredCount);
     }
   }

And use it in getStatus:

   getStatus(): MCPRegistryStatus {
     return {
       enabled: getMCPConfig().enabled,
       servers: this.servers.map(s => ({
         id: s.id,
         endpoint: s.endpoint,
         connected: this.client.isConnected(s.id),
-        toolCount: 0,
+        toolCount: this.toolCountPerServer.get(s.id) || 0,
       })),
       registeredToolNames: [...this.registeredTools],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getStatus(): MCPRegistryStatus {
return {
enabled: getMCPConfig().enabled,
servers: this.servers.map(s => ({
id: s.id,
endpoint: s.endpoint,
connected: this.client.isConnected(s.id),
toolCount: 0,
})),
registeredToolNames: [...this.registeredTools],
lastError: this.lastError,
lastErrorType: this.lastErrorType,
lastConnected: this.lastConnected,
lastDisconnected: this.lastDisconnected,
};
}
getStatus(): MCPRegistryStatus {
return {
enabled: getMCPConfig().enabled,
servers: this.servers.map(s => ({
id: s.id,
endpoint: s.endpoint,
connected: this.client.isConnected(s.id),
toolCount: this.toolCountPerServer.get(s.id) || 0,
})),
registeredToolNames: [...this.registeredTools],
lastError: this.lastError,
lastErrorType: this.lastErrorType,
lastConnected: this.lastConnected,
lastDisconnected: this.lastDisconnected,
};
}
🤖 Prompt for AI Agents
In front_end/panels/ai_chat/mcp/MCPRegistry.ts around lines 135-150, toolCount
is hardcoded to 0; add a Map<string, number> (e.g., serverToolCounts) keyed by
server.id to track each server's tool count, initialize it in the class
constructor, update the map in the refresh method when you fetch or reconcile
tools for a given server (set to the fetched tools.length or 0 on
errors/disconnect), and change getStatus to read toolCount from
serverToolCounts.get(s.id) || 0 so the returned status reflects the actual
per-server tool counts.

Comment on lines +725 to 726
#mcpUnsubscribe: (() => void) | null = null;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use unsubscribe handle or remove it.

Currently flagged as unused; wire it into lifecycle to prevent leaks.

-  #mcpUnsubscribe: (() => void) | null = null;
+  #mcpUnsubscribe: (() => void) | null = null;

Follow-up change in willHide below unsubscribes.

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 ESLint

[error] 725-725: '#mcpUnsubscribe' is defined but never used.

(no-unused-private-class-members)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/ui/AIChatPanel.ts around lines 725-726, the private
field "#mcpUnsubscribe: (() => void) | null = null;" is declared but not used;
either remove it or wire it into the component lifecycle to avoid leaks. Fix by
assigning the unsubscribe function returned from whatever subscribe/observe call
creates the subscription to "#mcpUnsubscribe" at the point of subscription, and
in willHide (the lifecycle teardown) invoke "#mcpUnsubscribe()" if non-null then
set it to null so the subscription is cleaned up.

Comment on lines +1 to +21
MIT License

Copyright (c) 2024 Anthropic, PBC

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. No newline at end of file
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Ensure license mapping into about:credits

Validate that the GN/GRD lists include this path so credits/legal pages pick it up.


🏁 Script executed:

#!/bin/bash
rg -nP 'mcp-sdk' config/gni devtools_grd_files.gni front_end/third_party/**/BUILD.gn -S -n || true

Length of output: 2399


Include mcp-sdk LICENSE in about:credits
Add "front_end/third_party/mcp-sdk/LICENSE" to both grd_files_release_sources and grd_files_debug_sources in config/gni/devtools_grd_files.gni so the license is picked up by about:credits.

🤖 Prompt for AI Agents
In front_end/third_party/mcp-sdk/LICENSE lines 1-21: the LICENSE file for
mcp-sdk is present but not referenced; add
"front_end/third_party/mcp-sdk/LICENSE" to both grd_files_release_sources and
grd_files_debug_sources arrays in config/gni/devtools_grd_files.gni so the
license is included in about:credits; update the two lists with the correct
comma-separated entry and maintain existing formatting/indentation.

Comment on lines +14 to +16
Dependencies:
- eventsource-parser@3.0.6 (MIT license)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Dependencies list appears incomplete

Ajv is bundled under mcp-sdk/ajv; list it (and any other bundled deps) to keep licensing accurate.

 Dependencies:
-- eventsource-parser@3.0.6 (MIT license)
+- eventsource-parser@3.0.6 (MIT)
+- ajv@<version> (MIT)
+# Add any additional transitive/bundled deps present under this directory.

🏁 Script executed:

#!/bin/bash
# Detect third-party modules actually present under mcp-sdk
fd -td . front_end/third_party/mcp-sdk -d 2
# Heuristic: scan dist for import/require references
rg -nP '(import|require)\(["'\'']([^"'\'']+)["'\'']\)' front_end/third_party/mcp-sdk -S -n -g '!**/README*' -g '!**/LICENSE*' -C0 | sed -E 's/.*[("\'\'']([^"'\''/]+).*/\1/' | sort -u

Length of output: 974


🏁 Script executed:

#!/bin/bash
# List package.json files under mcp-sdk
package_files=$(fd package.json front_end/third_party/mcp-sdk -t f)
echo "package.json files found:"
echo "$package_files"
echo ""

# Extract name, version, license from each package.json
for f in $package_files; do
  echo "File: $f"
  awk '/"name"/ || /"version"/ || /"license"/' "$f"
  echo ""
done

Length of output: 3019


Add missing bundled dependencies to README.chromium
Dependencies list in front_end/third_party/mcp-sdk/README.chromium currently only includes eventsource-parser@3.0.6. Please also list ajv@8.17.1 and zod@3.23.8 (both MIT) and keep a placeholder for any future bundled/transitive deps:

 Dependencies:
- eventsource-parser@3.0.6 (MIT)
+- ajv@8.17.1 (MIT)
+- zod@3.23.8 (MIT)
 # Add any additional transitive/bundled deps present under this directory.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In front_end/third_party/mcp-sdk/README.chromium around lines 14 to 16, the
bundled dependencies list only mentions eventsource-parser@3.0.6; update the
Dependencies section to also include ajv@8.17.1 (MIT) and zod@3.23.8 (MIT), and
add a short placeholder line such as "Other bundled/transitive dependencies may
be listed here" for future additions; ensure each entry follows the same format
as the existing line (package@version (license)).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 26

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
front_end/panels/ai_chat/ui/ChatView.ts (1)

1801-1807: Avoid empty button text when isLastMessage is true.

📄 ${isLastMessage ? '' : 'View Full Screen'} yields an icon-only button (poor a11y) for last message. Keep a consistent text label and/or add aria-label.

-                📄 ${isLastMessage ? '' : 'View Full Screen'}
+                📄 View Full Screen

Optionally, also add:

-              <button 
+              <button 
+                aria-label="View full screen"
front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts (1)

639-644: Tools referenced but never registered: action_verification_agent will fail at runtime.

The agent uses inspect_element and get_console_logs, but there are no ToolRegistry registrations for these tool ids in initializeConfiguredAgents().

Proposed fix (imports + registrations):

-import { NavigateURLTool, PerformActionTool, GetAccessibilityTreeTool, SearchContentTool, NavigateBackTool, NodeIDsToURLsTool, TakeScreenshotTool, ScrollPageTool } from '../../tools/Tools.js';
+import { NavigateURLTool, PerformActionTool, GetAccessibilityTreeTool, SearchContentTool, NavigateBackTool, NodeIDsToURLsTool, TakeScreenshotTool, ScrollPageTool, InspectElementTool, GetConsoleLogsTool } from '../../tools/Tools.js';
@@
   ToolRegistry.registerToolFactory('take_screenshot', () => new TakeScreenshotTool());
+  ToolRegistry.registerToolFactory('inspect_element', () => new InspectElementTool());
+  ToolRegistry.registerToolFactory('get_console_logs', () => new GetConsoleLogsTool());
front_end/panels/ai_chat/core/AgentNodes.ts (2)

172-186: Pass sanitized tool names to LLM

Use the sanitized names when constructing the tools array for llm.call.

-              tools: tools.map(tool => ({
+              tools: sanitizedTools.map(({ sanitized, tool }) => ({
                 type: 'function',
                 function: {
-                  name: tool.name,
+                  name: sanitized,
                   description: tool.description,
                   parameters: tool.schema,
                 }
               })),

474-478: Don’t throw on missing tool; return structured TOOL_RESULT error

Throwing here skips result linking and breaks the chat flow. Emit a ToolResultMessage with isError: true.

-      if (!selectedTool) {
-        throw new Error(`Tool ${toolName} not found`);
-      }
+      if (!selectedTool) {
+        const errorText = `Tool ${toolName} not found`;
+        const messages = [...state.messages, {
+          entity: ChatMessageEntity.TOOL_RESULT,
+          toolName,
+          resultText: errorText,
+          isError: true,
+          toolCallId,
+          error: errorText,
+        } as ToolResultMessage];
+        return { ...state, messages, error: errorText };
+      }
front_end/panels/ai_chat/BUILD.gn (1)

296-312: Unittests likely need MCP SDK dep

MCPClientSDK.test.ts may import the MCP SDK transitively or directly. Add the bundle to unittests deps to avoid build flakiness.

 ts_library("unittests") {
   testonly = true
   sources = [
     "common/utils.test.ts",
     "mcp/MCPClientSDK.test.ts",
     "core/ToolSurfaceProvider.test.ts",
     "core/ToolNameMapping.test.ts",
   ]
 
   deps = [
     ":ai_chat",
     "../../testing",
     "../../core/sdk:bundle",
     "../../generated:protocol",
+    "../../third_party/mcp-sdk:bundle",
   ]
 }
♻️ Duplicate comments (3)
front_end/panels/ai_chat/mcp/MCPClientSDK.test.ts (1)

53-134: Make the localhost integration test opt-in to avoid CI flakes.

This block depends on a locally running MCP server; it will hang/fail in CI. Gate behind an env flag or skip by default.

Proposed change:

-describe('connects to local Hacker News MCP server via SDK', function() {
+const runMcpE2E = process && process.env && process.env.MCP_E2E === '1';
+(runMcpE2E ? describe : describe.skip)('connects to local Hacker News MCP server via SDK', function() {
   this.timeout(30000); // Longer timeout for integration test

And consider a shorter default timeout (e.g., 10s) with an AbortController inside connect if supported by the SDK.

front_end/panels/ai_chat/core/AgentNodes.ts (1)

150-167: Sanitize tool names for LLMs and persist mapping

LLM function-calling requires ^[a-zA-Z0-9_-]{1,64}$. MCP tool names like mcp:default:alpha will be rejected. Sanitize names before llm.call, persist toolNameMap (sanitized → original), and expose sanitized names in selectedToolNames for the executor. Also use sanitized names in error handler.

-        // Select tools based on MCP mode (all/router/meta)
-        const baseTools = getAgentToolsFromState(state);
-        const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 });
-        // Persist selection in context so ToolExecutorNode can resolve the same set
-        if (!state.context) { (state as any).context = {}; }
-        (state.context as any).selectedToolNames = selection.selectedNames;
-        const tools = selection.tools;
+        // Select tools based on MCP mode (all/router/meta)
+        const baseTools = getAgentToolsFromState(state);
+        const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 });
+        // Persist selection and sanitized tool-name mapping for LLM compatibility
+        if (!state.context) { (state as any).context = {}; }
+        const shortHash = (s: string) => { let h = 0; for (let i = 0; i < s.length; i++) { h = (h * 31 + s.charCodeAt(i)) | 0; } return (h >>> 0).toString(36).slice(0, 6); };
+        const toolNameMap: Record<string, string> = {};
+        const sanitizedTools = selection.tools.map(tool => {
+          const original = tool.name;
+          let sanitized = original.replace(/[^a-zA-Z0-9_-]/g, '_');
+          if (sanitized.length > 64) sanitized = sanitized.slice(0, 64);
+          if (toolNameMap[sanitized] && toolNameMap[sanitized] !== original) {
+            const suffix = shortHash(original);
+            const maxBase = 64 - (suffix.length + 1);
+            sanitized = `${sanitized.slice(0, maxBase)}_${suffix}`;
+          }
+          toolNameMap[sanitized] = original;
+          return { sanitized, tool };
+        });
+        (state.context as any).toolNameMap = toolNameMap;
+        (state.context as any).selectedToolNames = Object.keys(toolNameMap);
+        const tools = selection.tools;
@@
-          availableTools: tools.map(t => t.name)
+          availableTools: sanitizedTools.map(t => t.sanitized)
front_end/panels/ai_chat/mcp/MCPToolAdapter.ts (1)

32-36: Do not pass redacted args to MCP tools; only log sanitized copy

Current code redacts secrets in args and sends the redacted object to the server, breaking auth. Log the sanitized copy but invoke with original args.

   async execute(args: Record<string, unknown>): Promise<unknown> {
-    const sanitized = this.sanitize(args);
-    logger.info('Executing MCP tool', { name: this.name, serverId: this.serverId });
-    return this.client.callTool(this.serverId, this.def.name, sanitized, { timeoutMs: 30000 });
+    const sanitized = this.sanitize(args);
+    logger.info('Executing MCP tool', { name: this.name, serverId: this.serverId, args: sanitized });
+    return this.client.callTool(this.serverId, this.def.name, args, { timeoutMs: 30000 });
   }
🧹 Nitpick comments (35)
front_end/third_party/mcp-sdk/LICENSE (1)

1-21: MIT text OK; add trailing newline at EOF for tooling hygiene.

Some linters and license scanners expect a newline-terminated file.

Apply:

 MIT License
@@
 SOFTWARE.
+
front_end/panels/ai_chat/ui/ChatView.ts (2)

1783-1789: Label change is fine; add an aria-label for accessibility.

Button has an emoji and visible text; add an explicit aria-label to ensure a stable accessible name.

-              <button 
-                class="view-document-btn"
+              <button 
+                class="view-document-btn"
+                aria-label="View full screen"
                 @click=${() => this.#openInAIAssistantViewer(structuredResponse.markdownReport)}
                 title="Open full report in document viewer">
                 📄 View Full Screen
               </button>

1760-1811: Consider extracting the label to i18n/constants to avoid string drift.

Two buttons now use “View Full Screen”. Centralize to a constant or i18n key so tests and UI copy stay consistent.

front_end/third_party/mcp-sdk/ajv/LICENSE (1)

1-22: Ajv MIT license included; add trailing newline if missing.

Keep formatting consistent across third-party licenses.

 SOFTWARE.
+

Ensure zod and eventsource-parser licenses are also present alongside their bundled code (see script in previous comment).

front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts (1)

196-204: Align instance retrieval with registration behavior.

You eagerly instantiate and store tools at registration, but getToolInstance returns a fresh instance, while getRegisteredTool returns the stored one. This can create multiple instances and state divergence.

 static getToolInstance(name: string): Tool<any, any> | null {
-  const factory = this.toolFactories.get(name);
-  return factory ? factory() : null;
+  const existing = this.registeredTools.get(name);
+  if (existing) return existing;
+  const factory = this.toolFactories.get(name);
+  if (!factory) return null;
+  const instance = factory();
+  this.registeredTools.set(name, instance);
+  return instance;
 }

If you intend ephemeral instances, rename one of the methods for clarity (e.g., createToolInstance vs getRegisteredTool), and document lifecycle expectations.

front_end/third_party/mcp-sdk/README.chromium (1)

17-24: Add provenance checksums and dependency license note.

To make updates reproducible and pass third_party compliance, please include SHA256 checksums for the fetched tarballs and explicitly note where the dependency license is checked in-tree.

Suggested edit:

 To update this package:
-wget -qO- https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.3.tgz | tar xzf -
-wget -qO- https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz | tar xzf -
+wget -qO- https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.12.3.tgz | tar xzf -
+wget -qO- https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz | tar xzf -
+
+Checksums (sha256):
+- sdk-1.12.3.tgz: <fill-on-update>
+- eventsource-parser-3.0.6.tgz: <fill-on-update>
+
+Notes:
+- Ensure eventsource-parser’s MIT LICENSE is present at front_end/third_party/mcp-sdk/eventsource-parser/package/LICENSE (or add a copy under this directory).
front_end/third_party/mcp-sdk/ajv/.runkit_example.js (1)

1-23: Exclude upstream RunKit example from bundles/lints.

This dev-only example shouldn’t ship or trigger lint noise. Either exclude it from GN/GRD or hard-disable linters.

Suggested minimal header:

+/* eslint-disable @typescript-eslint/no-var-requires, no-console */
+// Upstream Ajv RunKit example; retained for completeness. Excluded from bundles.
 const Ajv = require("ajv")

If possible, also exclude this path from any third_party bundling rules.

front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts (2)

98-100: Guard MCP meta-tools registration for idempotency and environments without SDK.

If initializeConfiguredAgents() is called twice or the SDK isn’t available in some builds, registerMCPMetaTools() may throw or double-register.

Suggested defensive wrap:

-  // Ensure MCP meta-tools are available regardless of mode; selection logic decides if they are surfaced
-  registerMCPMetaTools();
+  // Ensure MCP meta-tools are available; selection logic decides surfacing
+  try {
+    // Optional: skip if already registered
+    // if (!ToolRegistry.hasTool || !ToolRegistry.hasTool('mcp.search')) {
+      registerMCPMetaTools();
+    // }
+  } catch (e) {
+    console.warn('MCP meta-tools registration skipped:', e);
+  }

354-366: Nit: misleading comment.

Comment says “For the action agent…” inside the research agent block; likely a copy/paste artifact.

-      // For the action agent, we use the objective as the primary input, not the query field
+      // Prepare the user message for the research agent
front_end/third_party/mcp-sdk/ajv/README.md (1)

1-208: Silence linters for vendored docs rather than editing upstream content.

markdownlint/LanguageTool warnings are expected for third_party. Prefer excluding front_end/third_party/** in lint configs or adding path-level suppressions in the tooling.

Action: update lint config to ignore third_party paths (no changes to this file).

front_end/panels/ai_chat/mcp/MCPClientSDK.test.ts (3)

35-42: Type-safe error assertions.

Avoid relying on error.message without narrowing; keeps tests resilient to non-Error throws.

-  } catch (error) {
-      assert.ok(error.message.includes('No connection for server'));
+  } catch (error) {
+      const msg = error instanceof Error ? error.message : String(error);
+      assert.ok(msg.includes('No connection for server'));
   }

Apply to both tests.

Also applies to: 44-51


24-26: Assert import (if not globally provided).

If assert isn’t globally injected by the test runner, import from 'assert/strict' or chai to avoid runtime ReferenceError.

+import {strict as assert} from 'assert';

If globals are guaranteed in this repo, ignore.


61-117: Reduce noisy console output in unit runs.

Gate logs behind an env flag to keep CI output clean.

-      console.log('=== Testing Local Hacker News MCP Server with SDK ===');
+      if (process.env.VERBOSE_TESTS) console.log('=== Testing Local Hacker News MCP Server with SDK ===');
@@
-        console.log('Step 1: Connecting to MCP server via SDK...');
+        if (process.env.VERBOSE_TESTS) console.log('Step 1: Connecting to MCP server via SDK...');
@@
-        console.log('✓ Connection established via SDK');
+        if (process.env.VERBOSE_TESTS) console.log('✓ Connection established via SDK');

(Repeat pattern for other logs.)

front_end/panels/ai_chat/core/ToolNameMapping.test.ts (1)

71-79: Address lint: prefer assert.isOk and strip trailing spaces

  • Replace assert.ok(...) with assert.isOk(...).
  • Remove trailing whitespace on Line 71.
-    const result = await node.invoke(state);
-    
+    const result = await node.invoke(state);
@@
-    assert.ok((last as any).resultText);
+    assert.isOk((last as any).resultText);
front_end/third_party/mcp-sdk/BUILD.gn (1)

21-26: Avoid bundling both ESM and CJS zod builds

Including both zod/lib/index.js (CJS) and zod/lib/index.mjs (ESM) can bloat the bundle or confuse resolution. Pick one variant used by the bundler/runtime.

-    "zod/lib/index.js",
-    "zod/lib/index.mjs",
+    # Prefer one module format (ESM shown here)
+    "zod/lib/index.mjs",
front_end/panels/ai_chat/core/AgentNodes.ts (2)

83-84: Fix typo in log message

"Coudnt" → "Couldn't".

-            logger.warn('Coudnt find the answer');
+            logger.warn("Couldn't find the answer");

379-392: Optional: ensure assistant tool-call message content is empty string, not undefined

Some providers reject undefined; empty string is safer. Verify with your LLM client’s contract.

-            llmMessages.push({
+            llmMessages.push({
               role: 'assistant',
-              content: undefined,
+              content: '',
               tool_calls: [{
front_end/panels/ai_chat/mcp/MCPToolAdapter.ts (1)

42-53: Optional: sanitize nested objects for logging

Current sanitization only redacts top-level fields. Consider recursive traversal.

-  private sanitize(input: Record<string, unknown>): Record<string, unknown> {
+  private sanitize(input: Record<string, unknown>): Record<string, unknown> {
     const sensitive = ['token', 'api_key', 'password', 'secret', 'authorization'];
-    const out: Record<string, unknown> = {};
-    for (const [k, v] of Object.entries(input || {})) {
-      if (typeof v === 'string' && sensitive.some(s => k.toLowerCase().includes(s))) {
-        out[k] = '[redacted]';
-      } else {
-        out[k] = v;
-      }
-    }
-    return out;
+    const redact = (obj: any): any => {
+      if (!obj || typeof obj !== 'object') return obj;
+      const out: any = Array.isArray(obj) ? [] : {};
+      for (const [k, v] of Object.entries(obj)) {
+        if (typeof v === 'string' && sensitive.some(s => k.toLowerCase().includes(s))) {
+          out[k] = '[redacted]';
+        } else if (v && typeof v === 'object') {
+          out[k] = redact(v as any);
+        } else {
+          out[k] = v;
+        }
+      }
+      return out;
+    };
+    return redact(input || {});
   }
front_end/panels/ai_chat/core/ToolSurfaceProvider.test.ts (4)

95-102: Prefer assert.isOk and length helpers

Use assert.isOk and assert.lengthOf per lint rules.

-    assert.ok(names.includes('core_tool_A'));
-    assert.ok(names.includes('core_tool_B'));
+    assert.isOk(names.includes('core_tool_A'));
+    assert.isOk(names.includes('core_tool_B'));
     const selectedMcp = names.filter(n => n.startsWith('mcp:default:'));
-    assert.strictEqual(selectedMcp.length, 2);
+    assert.lengthOf(selectedMcp, 2);
     // Alpha should be preferred due to query match
-    assert.ok(selectedMcp.includes('mcp:default:alpha'));
+    assert.isOk(selectedMcp.includes('mcp:default:alpha'));

84-89: Use enum for message entity instead of any

This removes two any usages and improves readability.

-    const state: AgentState = {
-      messages: [{ entity: 0 as any, text: 'please run alpha operation' }],
-      context: {},
-      selectedAgentType: 'deep-research' as any
-    } as any;
+    const state: AgentState = {
+      messages: [{ entity: ChatMessageEntity.USER as any, text: 'please run alpha operation' }],
+      context: {},
+      selectedAgentType: 'deep-research' as any
+    } as any;

129-135: Prefer assert.isOk over assert.ok

Aligns with project lint rule.

-    assert.ok(names.includes('core_tool_A'));
-    assert.ok(names.includes('mcp.search'));
-    assert.ok(names.includes('mcp.invoke'));
+    assert.isOk(names.includes('core_tool_A'));
+    assert.isOk(names.includes('mcp.search'));
+    assert.isOk(names.includes('mcp.invoke'));

24-45: Consider adding a test-only reset for ToolRegistry

Without a clear reset API, registered tools can bleed across tests. Offer to add ToolRegistry.resetForTests() to clear internal maps in beforeEach/afterEach.

front_end/panels/ai_chat/mcp/MCPConfig.ts (5)

83-87: Annotate the event listener callback return type to satisfy lint.

Avoids the explicit-return-type error on Line 84.

-  const cb = () => handler();
+  const cb: () => void = () => handler();

1-4: Add the required license header.

Build/lint will fail without it.

+// Copyright 2025 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
 import { createLogger } from '../core/Logger.js';

51-71: Allow clearing optional fields (endpoint/allowlist/etc.).

Current writes only when value is defined; there’s no way to delete a persisted key. Consider treating null/empty-string as a delete.

-    if (config.endpoint !== undefined) {
-      localStorage.setItem(KEYS.endpoint, config.endpoint);
-    }
+    if (config.endpoint !== undefined) {
+      config.endpoint ? localStorage.setItem(KEYS.endpoint, config.endpoint)
+                      : localStorage.removeItem(KEYS.endpoint);
+    }
@@
-    if (config.toolAllowlist) {
-      localStorage.setItem(KEYS.allowlist, JSON.stringify(config.toolAllowlist));
-    }
+    if (config.toolAllowlist !== undefined) {
+      config.toolAllowlist?.length
+        ? localStorage.setItem(KEYS.allowlist, JSON.stringify(config.toolAllowlist))
+        : localStorage.removeItem(KEYS.allowlist);
+    }

33-36: Validate allowlist JSON shape.

Defensive check avoids non-string array values.

-    if (raw) {
-      try { toolAllowlist = JSON.parse(raw); } catch { toolAllowlist = undefined; }
-    }
+    if (raw) {
+      try {
+        const parsed = JSON.parse(raw);
+        toolAllowlist = Array.isArray(parsed) && parsed.every(x => typeof x === 'string') ? parsed : undefined;
+      } catch {
+        toolAllowlist = undefined;
+      }
+    }

54-56: Follow-up offer: secure storage abstraction.

I can add a small SecureStore (session/in-memory today; pluggable for OS keychain/extension storage later) and wire token through it.

front_end/panels/ai_chat/mcp/MCPRegistry.ts (2)

12-18: Member delimiter style in interfaces.

Project lint expects commas between members; update MCPRegistryStatus to use commas.

 export interface MCPRegistryStatus {
-  enabled: boolean;
-  servers: Array<{ id: string; endpoint: string; connected: boolean; toolCount: number }>;
-  registeredToolNames: string[];
-  lastError?: string;
-  lastErrorType?: 'connection' | 'authentication' | 'configuration' | 'network' | 'server_error' | 'unknown';
-  lastConnected?: Date;
-  lastDisconnected?: Date;
+  enabled: boolean,
+  servers: Array<{ id: string; endpoint: string; connected: boolean; toolCount: number }>,
+  registeredToolNames: string[],
+  lastError?: string,
+  lastErrorType?: 'connection' | 'authentication' | 'configuration' | 'network' | 'server_error' | 'unknown',
+  lastConnected?: Date,
+  lastDisconnected?: Date,
 }

29-48: Minor: trim trailing spaces and standardize logger error shape.

Nits but they do trip style checks; also prefer structured logs consistently: logger.error('MCP connect failed', { err }).

front_end/panels/ai_chat/mcp/MCPMetaTools.ts (3)

5-11: Reorder imports and avoid direct UI dependency at load time to reduce cycles.

Defer AIChatPanel import inside rankWithLLM; group imports to satisfy lint.

-import type { Tool } from '../tools/Tools.js';
-import { createLogger } from '../core/Logger.js';
-import { MCPRegistry } from './MCPRegistry.js';
-import { MCPToolAdapter } from './MCPToolAdapter.js';
-import { LLMClient } from '../LLM/LLMClient.js';
-import { AIChatPanel } from '../ui/AIChatPanel.js';
+import { createLogger } from '../core/Logger.js';
+import type { Tool } from '../tools/Tools.js';
+import { MCPRegistry } from './MCPRegistry.js';
+import { MCPToolAdapter } from './MCPToolAdapter.js';
+import { LLMClient } from '../LLM/LLMClient.js';

And later inside rankWithLLM:

-      const { model, provider } = AIChatPanel.getNanoModelWithProvider();
+      const { AIChatPanel } = await import('../ui/AIChatPanel.js');
+      const { model, provider } = AIChatPanel.getNanoModelWithProvider();

100-114: Remove unused var and small scoring tidy.

Drop q; tiny cleanup for readability.

-    const q = (args.query || '').toLowerCase();

48-56: Use object shorthand for systemPrompt.

Satisfies lint and reduces duplication.

-      const response = await llm.call({
+      const response = await llm.call({
         provider,
         model,
         messages: [
           { role: 'system', content: systemPrompt },
           { role: 'user', content: userMessage }
         ],
-        systemPrompt: systemPrompt,
+        systemPrompt,
front_end/panels/ai_chat/core/ToolSurfaceProvider.ts (2)

86-104: Make debug testMode mutate MCP config, not localStorage

testMode() writes ai_chat_mcp_tool_mode directly to localStorage which may not be read by getMCPConfig(). Use setMCPConfig() so behavior matches production.

Apply:

-  testMode: async (mode: 'all' | 'router' | 'meta') => {
-    const originalConfig = getMCPConfig();
-    console.log(`Testing mode: ${mode}`);
-    // Temporarily set the mode
-    localStorage.setItem('ai_chat_mcp_tool_mode', mode);
+  testMode: async (mode: 'all' | 'router' | 'meta') => {
+    const originalConfig = getMCPConfig();
+    console.log(`Testing mode: ${mode}`);
+    // Temporarily set the mode in MCP config
+    setMCPConfig({ ...originalConfig, toolMode: mode });
@@
-    // Restore original mode
-    if (originalConfig.toolMode) {
-      localStorage.setItem('ai_chat_mcp_tool_mode', originalConfig.toolMode);
-    }
+    // Restore original mode
+    setMCPConfig(originalConfig);

Also update imports:

- import { getMCPConfig } from '../mcp/MCPConfig.js';
+ import { getMCPConfig, setMCPConfig } from '../mcp/MCPConfig.js';

118-128: Use the logger instead of console for structured debug output

You already have createLogger. Replace console.log/error with logger.debug/info/warn/error for consistency and log filtering.

If you’d like, I can send a follow-up patch swapping these calls in one pass.

Also applies to: 139-147, 149-162, 186-190, 194-207, 209-214

front_end/panels/ai_chat/ui/SettingsDialog.ts (1)

434-434: Trim trailing space (lint error)

Line 434 has a trailing space after the string literal.

-  mcpMaxMcpPerTurn: 'Max MCP Tools Per Turn', 
+  mcpMaxMcpPerTurn: 'Max MCP Tools Per Turn',

Comment on lines 846 to 856
"front_end/third_party/mcp-sdk/ajv/dist/ajv.js",
"front_end/third_party/mcp-sdk/zod/lib/index.js",
"front_end/third_party/mcp-sdk/zod/lib/index.mjs",
"front_end/third_party/mcp-sdk/eventsource-parser/package/dist/index.js",
"front_end/third_party/mcp-sdk/eventsource-parser/package/dist/stream.js",
"front_end/third_party/mcp-sdk/mcp-sdk.js",
"front_end/third_party/mcp-sdk/package/dist/client/index.js",
"front_end/third_party/mcp-sdk/package/dist/client/sse.js",
"front_end/third_party/mcp-sdk/package/dist/shared/protocol.js",
"front_end/third_party/mcp-sdk/package/dist/shared/transport.js",
"front_end/third_party/mcp-sdk/package/dist/types.js",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Vendored MCP SDK: verify licenses, trim duplicates, and keep list ordering tidy

  • Please ensure front_end/third_party/mcp-sdk includes appropriate OSS metadata (LICENSE/README.chromium).
  • Consider keeping the third_party block alphabetically ordered; mcp-sdk should come after marked.
  • You list both zod/lib/index.js and zod/lib/index.mjs; if only one is referenced by the SDK, drop the other to save bytes.

Run this to verify quickly:

Expected:

  • All files exist; each appears exactly once in the GRD file.
  • At least one license/README.chromium-like file is present under front_end/third_party/mcp-sdk.
  • Only one of zod/lib/index.(mjs|js) is referenced; if so, remove the unused one from the GRD.
  • Optional: reorder the mcp-sdk block to maintain alphabetical order in the third_party section.

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Confirm all newly added files exist and are not duplicated across lists.
gni=config/gni/devtools_grd_files.gni
new_files=(
  "front_end/panels/ai_chat/core/ToolSurfaceProvider.js"
  "front_end/panels/ai_chat/mcp/MCPConfig.js"
  "front_end/panels/ai_chat/mcp/MCPRegistry.js"
  "front_end/panels/ai_chat/mcp/MCPToolAdapter.js"
  "front_end/panels/ai_chat/mcp/MCPMetaTools.js"
  "front_end/panels/ai_chat/tools/LLMTracingWrapper.js"
  "front_end/third_party/mcp-sdk/ajv/dist/ajv.js"
  "front_end/third_party/mcp-sdk/zod/lib/index.js"
  "front_end/third_party/mcp-sdk/zod/lib/index.mjs"
  "front_end/third_party/mcp-sdk/eventsource-parser/package/dist/index.js"
  "front_end/third_party/mcp-sdk/eventsource-parser/package/dist/stream.js"
  "front_end/third_party/mcp-sdk/mcp-sdk.js"
  "front_end/third_party/mcp-sdk/package/dist/client/index.js"
  "front_end/third_party/mcp-sdk/package/dist/client/sse.js"
  "front_end/third_party/mcp-sdk/package/dist/shared/protocol.js"
  "front_end/third_party/mcp-sdk/package/dist/shared/transport.js"
  "front_end/third_party/mcp-sdk/package/dist/types.js"
)

echo "Checking file existence..."
missing=0
for f in "${new_files[@]}"; do
  if [ ! -f "$f" ]; then echo "MISSING: $f"; missing=1; fi
done
[ "$missing" -eq 0 ] && echo "All files exist."

echo "Checking duplicates across GRD lists..."
dups=0
for f in "${new_files[@]}"; do
  c=$(rg -n --fixed-strings "$f" "$gni" | wc -l | tr -d ' ')
  if [ "$c" -ne 1 ]; then echo "DUPLICATE/ABSENT in GRD lists: $f (count=$c)"; dups=1; fi
done
[ "$dups" -eq 0 ] && echo "No duplicates found in GRD lists."

# 2) License metadata in vendored SDK.
echo "Scanning for OSS metadata under mcp-sdk..."
fd -HI -t f '(LICENSE|COPYING|NOTICE|README\.chromium)$' front_end/third_party/mcp-sdk || true

# 3) Check if both zod entrypoints are actually referenced by the vendored SDK.
echo "Inspecting SDK imports for zod entrypoint usage..."
rg -n 'zod/lib/index\.(mjs|js)' front_end/third_party/mcp-sdk || true

# 4) Estimate total size impact of the vendored SDK block.
echo "Size of front_end/third_party/mcp-sdk:"
du -sh front_end/third_party/mcp-sdk || true

Length of output: 8107


config/gni/devtools_grd_files.gni: fix missing references & clean up vendored MCP SDK block

  • 7 GRD entries reference non-existent files; either add these sources or remove their lines:

    • front_end/panels/ai_chat/core/ToolSurfaceProvider.js
    • front_end/panels/ai_chat/mcp/MCPConfig.js
    • front_end/panels/ai_chat/mcp/MCPRegistry.js
    • front_end/panels/ai_chat/mcp/MCPToolAdapter.js
    • front_end/panels/ai_chat/mcp/MCPMetaTools.js
    • front_end/panels/ai_chat/tools/LLMTracingWrapper.js
    • front_end/third_party/mcp-sdk/mcp-sdk.js
  • Vendored mcp-sdk already includes OSS metadata (LICENSE, README.chromium).

  • Only zod/lib/index.mjs is actually imported by the SDK; drop zod/lib/index.js from the GRD to save bytes.

  • Optional: reorder the mcp-sdk file list alphabetically within the third_party section.

Comment on lines 5 to 9
import { createToolExecutorNode } from './AgentNodes.js';
import type { AgentState } from './State.js';
import type { Tool } from '../tools/Tools.js';
import { ChatMessageEntity } from '../ui/ChatView.js';

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix missing assertion import

assert is used but never imported; tests will throw ReferenceError. Add Chai assert import.

 import { createToolExecutorNode } from './AgentNodes.js';
 import type { AgentState } from './State.js';
 import type { Tool } from '../tools/Tools.js';
 import { ChatMessageEntity } from '../ui/ChatView.js';
 
+import { assert } from 'chai';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { createToolExecutorNode } from './AgentNodes.js';
import type { AgentState } from './State.js';
import type { Tool } from '../tools/Tools.js';
import { ChatMessageEntity } from '../ui/ChatView.js';
import { createToolExecutorNode } from './AgentNodes.js';
import type { AgentState } from './State.js';
import type { Tool } from '../tools/Tools.js';
import { ChatMessageEntity } from '../ui/ChatView.js';
import { assert } from 'chai';
🧰 Tools
🪛 ESLint

[error] 5-5: Incorrect same-namespace import: "./AgentNodes.js". Use "import * as Core from './core.js';" instead.

(rulesdir/es-modules-import)


[error] 6-6: There should be at least one empty line between import groups

(import/order)


[error] 7-7: ../tools/Tools.js type import should occur before import of ./AgentNodes.js

(import/order)


[error] 8-8: ../ui/ChatView.js import should occur before import of ./AgentNodes.js

(import/order)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/ToolNameMapping.test.ts around lines 5 to 9,
the test uses assert but never imports it; add an import for Chai's assert
(e.g., import { assert } from 'chai') at the top of the file so the tests no
longer throw a ReferenceError and assertions work correctly.

Comment on lines +5 to +11
import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
import type { AgentState } from './State.js';
import type { Tool } from '../tools/Tools.js';
import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';
import { MCPRegistry } from '../mcp/MCPRegistry.js';
import { registerMCPMetaTools } from '../mcp/MCPMetaTools.js';

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Import assertions and ChatMessageEntity for typed usage

Add Chai assert and use ChatMessageEntity.USER instead of 0 as any.

 import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
 import type { AgentState } from './State.js';
 import type { Tool } from '../tools/Tools.js';
 import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';
 import { MCPRegistry } from '../mcp/MCPRegistry.js';
 import { registerMCPMetaTools } from '../mcp/MCPMetaTools.js';
+import { assert } from 'chai';
+import { ChatMessageEntity } from '../ui/ChatView.js';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
import type { AgentState } from './State.js';
import type { Tool } from '../tools/Tools.js';
import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';
import { MCPRegistry } from '../mcp/MCPRegistry.js';
import { registerMCPMetaTools } from '../mcp/MCPMetaTools.js';
import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
import type { AgentState } from './State.js';
import type { Tool } from '../tools/Tools.js';
import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';
import { MCPRegistry } from '../mcp/MCPRegistry.js';
import { registerMCPMetaTools } from '../mcp/MCPMetaTools.js';
import { assert } from 'chai';
import { ChatMessageEntity } from '../ui/ChatView.js';
🧰 Tools
🪛 ESLint

[error] 5-5: Incorrect same-namespace import: "./ToolSurfaceProvider.js". Use "import * as Core from './core.js';" instead.

(rulesdir/es-modules-import)


[error] 5-5: ./ToolSurfaceProvider.js import should occur after import of ../mcp/MCPMetaTools.js

(import/order)


[error] 6-6: There should be at least one empty line between import groups

(import/order)


[error] 6-6: ./State.js type import should occur after import of ../mcp/MCPMetaTools.js

(import/order)


[error] 7-7: ../tools/Tools.js type import should occur after import of ../mcp/MCPMetaTools.js

(import/order)


[error] 9-9: ../mcp/MCPRegistry.js import should occur after import of ../mcp/MCPMetaTools.js

(import/order)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/ToolSurfaceProvider.test.ts around lines 5 to
11, the test currently lacks a Chai assert import and uses a raw numeric cast (0
as any) for the message role; import assert from 'chai' and import
ChatMessageEntity (the enum/constant) from its module (where ChatMessageEntity
is defined) and replace usages of 0 as any with ChatMessageEntity.USER so the
test uses typed role values and real assertions.

Comment on lines +32 to +62
function getAllMcpTools(): Tool<any, any>[] {
try {
const status = MCPRegistry.getStatus();
console.log('[TOOL_SELECTION_DEBUG] MCPRegistry status:', {
enabled: status.enabled,
serverCount: status.servers.length,
servers: status.servers,
registeredToolNames: status.registeredToolNames,
lastError: status.lastError,
lastErrorType: status.lastErrorType
});

const tools: Tool<any, any>[] = [];
for (const name of status.registeredToolNames) {
const tool = ToolRegistry.getRegisteredTool(name);
if (tool) {
tools.push(tool);
} else {
console.log('[TOOL_SELECTION_DEBUG] Tool registered but not found:', name);
}
}
console.log('[TOOL_SELECTION_DEBUG] getAllMcpTools result:', {
availableToolsCount: tools.length,
availableToolNames: tools.map(t => t.name)
});
return tools;
} catch (error) {
console.error('[TOOL_SELECTION_DEBUG] Error in getAllMcpTools:', error);
return [];
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Respect MCP tool allowlist when building the tool list

Currently, getAllMcpTools() returns all registered tools and ignores the saved allowlist. This can surface tools users explicitly disabled.

Apply:

 function getAllMcpTools(): Tool<any, any>[] {
   try {
     const status = MCPRegistry.getStatus();
@@
-    const tools: Tool<any, any>[] = [];
+    const tools: Tool<any, any>[] = [];
+    const cfg = getMCPConfig();
+    const allow = new Set(cfg.toolAllowlist || []);
@@
-    for (const name of status.registeredToolNames) {
+    for (const name of status.registeredToolNames) {
       const tool = ToolRegistry.getRegisteredTool(name);
-      if (tool) {
-        tools.push(tool);
+      if (tool) {
+        if (allow.size === 0 || allow.has(tool.name)) {
+          tools.push(tool);
+        }
       } else {
         console.log('[TOOL_SELECTION_DEBUG] Tool registered but not found:', name);
       }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function getAllMcpTools(): Tool<any, any>[] {
try {
const status = MCPRegistry.getStatus();
console.log('[TOOL_SELECTION_DEBUG] MCPRegistry status:', {
enabled: status.enabled,
serverCount: status.servers.length,
servers: status.servers,
registeredToolNames: status.registeredToolNames,
lastError: status.lastError,
lastErrorType: status.lastErrorType
});
const tools: Tool<any, any>[] = [];
for (const name of status.registeredToolNames) {
const tool = ToolRegistry.getRegisteredTool(name);
if (tool) {
tools.push(tool);
} else {
console.log('[TOOL_SELECTION_DEBUG] Tool registered but not found:', name);
}
}
console.log('[TOOL_SELECTION_DEBUG] getAllMcpTools result:', {
availableToolsCount: tools.length,
availableToolNames: tools.map(t => t.name)
});
return tools;
} catch (error) {
console.error('[TOOL_SELECTION_DEBUG] Error in getAllMcpTools:', error);
return [];
}
}
function getAllMcpTools(): Tool<any, any>[] {
try {
const status = MCPRegistry.getStatus();
console.log('[TOOL_SELECTION_DEBUG] MCPRegistry status:', {
enabled: status.enabled,
serverCount: status.servers.length,
servers: status.servers,
registeredToolNames: status.registeredToolNames,
lastError: status.lastError,
lastErrorType: status.lastErrorType
});
const tools: Tool<any, any>[] = [];
const cfg = getMCPConfig();
const allow = new Set(cfg.toolAllowlist || []);
for (const name of status.registeredToolNames) {
const tool = ToolRegistry.getRegisteredTool(name);
if (tool) {
if (allow.size === 0 || allow.has(tool.name)) {
tools.push(tool);
}
} else {
console.log('[TOOL_SELECTION_DEBUG] Tool registered but not found:', name);
}
}
console.log('[TOOL_SELECTION_DEBUG] getAllMcpTools result:', {
availableToolsCount: tools.length,
availableToolNames: tools.map(t => t.name)
});
return tools;
} catch (error) {
console.error('[TOOL_SELECTION_DEBUG] Error in getAllMcpTools:', error);
return [];
}
}

Comment on lines +2579 to +2584
const updateDisconnectButton = () => {
const status = MCPRegistry.getStatus();
const anyConnected = status.enabled && status.servers.some(s => s.connected);
mcpDisconnectContainer.style.display = anyConnected ? 'block' : 'none';
};
updateDisconnectButton();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add explicit return type (ESLint: explicit-function-return-type)

updateDisconnectButton should be annotated.

-    const updateDisconnectButton = () => {
+    const updateDisconnectButton = (): void => {
       const status = MCPRegistry.getStatus();
       const anyConnected = status.enabled && status.servers.some(s => s.connected);
       mcpDisconnectContainer.style.display = anyConnected ? 'block' : 'none';
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const updateDisconnectButton = () => {
const status = MCPRegistry.getStatus();
const anyConnected = status.enabled && status.servers.some(s => s.connected);
mcpDisconnectContainer.style.display = anyConnected ? 'block' : 'none';
};
updateDisconnectButton();
const updateDisconnectButton = (): void => {
const status = MCPRegistry.getStatus();
const anyConnected = status.enabled && status.servers.some(s => s.connected);
mcpDisconnectContainer.style.display = anyConnected ? 'block' : 'none';
};
updateDisconnectButton();
🧰 Tools
🪛 ESLint

[error] 2579-2579: Missing return type on function.

(@typescript-eslint/explicit-function-return-type)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/ui/SettingsDialog.ts around lines 2579 to 2584, the
arrow function updateDisconnectButton lacks an explicit return type which
triggers the ESLint rule explicit-function-return-type; add an explicit void
return type by annotating the function as const updateDisconnectButton = ():
void => { ... } and keep the rest of the implementation and the
updateDisconnectButton() invocation unchanged.

Comment on lines +2757 to +2768
// Handle budget control changes
const updateBudgetControls = () => {
const maxTools = Math.max(1, Math.min(100, parseInt(mcpMaxToolsInput.value, 10) || 20));
const maxMcp = Math.max(1, Math.min(50, parseInt(mcpMaxMcpInput.value, 10) || 8));
setMCPConfig({
...getMCPConfig(),
maxToolsPerTurn: maxTools,
maxMcpPerTurn: maxMcp,
});
onSettingsSaved();
};

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add explicit return type and clamp logic remains unchanged

Annotate updateBudgetControls.

-    const updateBudgetControls = () => {
+    const updateBudgetControls = (): void => {
       const maxTools = Math.max(1, Math.min(100, parseInt(mcpMaxToolsInput.value, 10) || 20));
       const maxMcp = Math.max(1, Math.min(50, parseInt(mcpMaxMcpInput.value, 10) || 8));
       setMCPConfig({
         ...getMCPConfig(),
         maxToolsPerTurn: maxTools,
         maxMcpPerTurn: maxMcp,
       });
       onSettingsSaved();
     };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Handle budget control changes
const updateBudgetControls = () => {
const maxTools = Math.max(1, Math.min(100, parseInt(mcpMaxToolsInput.value, 10) || 20));
const maxMcp = Math.max(1, Math.min(50, parseInt(mcpMaxMcpInput.value, 10) || 8));
setMCPConfig({
...getMCPConfig(),
maxToolsPerTurn: maxTools,
maxMcpPerTurn: maxMcp,
});
onSettingsSaved();
};
// Handle budget control changes
const updateBudgetControls = (): void => {
const maxTools = Math.max(1, Math.min(100, parseInt(mcpMaxToolsInput.value, 10) || 20));
const maxMcp = Math.max(1, Math.min(50, parseInt(mcpMaxMcpInput.value, 10) || 8));
setMCPConfig({
...getMCPConfig(),
maxToolsPerTurn: maxTools,
maxMcpPerTurn: maxMcp,
});
onSettingsSaved();
};
🧰 Tools
🪛 ESLint

[error] 2758-2758: Missing return type on function.

(@typescript-eslint/explicit-function-return-type)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/ui/SettingsDialog.ts around lines 2757 to 2768,
annotate the updateBudgetControls function with an explicit return type (void)
while leaving the existing clamp/parsing logic unchanged; update the function
signature to declare its return type and ensure any local variables keep their
current behavior, then rebuild/confirm type checks pass.

Comment on lines +2791 to +2800
const updateToolsList = () => {
const status = MCPRegistry.getStatus();
mcpToolsList.innerHTML = '';

if (!status.enabled || status.registeredToolNames.length === 0) {
const noToolsMessage = document.createElement('div');
noToolsMessage.className = 'mcp-no-tools';
noToolsMessage.textContent = i18nString(UIStrings.mcpNoTools);
mcpToolsList.appendChild(noToolsMessage);
return;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use safe clearing and read fresh config for allowlist

  • Avoid innerHTML = '' for clearing.
  • Use the latest MCP config when computing the allowlist (the outer currentMCPConfig may be stale).
-    const updateToolsList = () => {
+    const updateToolsList = (): void => {
       const status = MCPRegistry.getStatus();
-      mcpToolsList.innerHTML = '';
+      mcpToolsList.replaceChildren();
@@
-      const currentAllowlist = new Set(currentMCPConfig.toolAllowlist || []);
+      const { toolAllowlist } = getMCPConfig();
+      const currentAllowlist = new Set(toolAllowlist || []);

Also applies to: 2803-2814

🧰 Tools
🪛 ast-grep (0.38.6)

[warning] 2792-2792: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: mcpToolsList.innerHTML = ''
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)

🪛 ESLint

[error] 2791-2791: Missing return type on function.

(@typescript-eslint/explicit-function-return-type)


[error] 2794-2794: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)

Comment on lines +39 to +44
# EventSource parser dependency
"eventsource-parser/package/dist/index.d.ts",
"eventsource-parser/package/dist/index.js",
"eventsource-parser/package/dist/stream.d.ts",
"eventsource-parser/package/dist/stream.js",
]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Confirm eventsource-parser stream.js is actually needed in browser bundle

stream.js is typically Node-focused; if unused in DevTools, drop it to reduce risk and size.


🏁 Script executed:

#!/bin/bash
rg -nP "eventsource-parser/package/dist/(stream|index)\.js" -C1 --type=ts --type=js

Length of output: 85


🏁 Script executed:

#!/bin/bash
# Search for any direct imports or requires of eventsource-parser (with or without /package/dist),
# plus usage of createParser and stream() in TS/JS code
rg -nP "from ['\"]eventsource-parser(/package/dist)?['\"]" -C3 --type=ts --type=js
rg -nP "require\(['\"]eventsource-parser(/package/dist)?['\"]" -C3 --type=ts --type=js
rg -nP "createParser" -C3 --type=ts --type=js
rg -nP "stream\(" -C3 --type=ts --type=js

Length of output: 8137


Remove unused eventsource-parser stream files from BUILD.gn
front_end/third_party/mcp-sdk/BUILD.gn: drop "eventsource-parser/package/dist/stream.js" (and its .d.ts), as no front_end code imports or uses them.

🤖 Prompt for AI Agents
In front_end/third_party/mcp-sdk/BUILD.gn around lines 39 to 44, the BUILD file
lists eventsource-parser stream files that are unused; remove the entries
"eventsource-parser/package/dist/stream.js" and
"eventsource-parser/package/dist/stream.d.ts" from the file list so only the
actually imported files remain, then run a quick build or lint to ensure no
breakage.

@claude
Copy link

claude bot commented Sep 9, 2025

Claude finished @tysonthomas9's task —— View job


Comprehensive PR Review Complete

I've conducted a thorough review of this MCP integration PR. Here's my detailed feedback:

🔴 Critical Security Issues

1. Token Storage Vulnerability (HIGH)

File: front_end/panels/ai_chat/mcp/MCPConfig.ts:54-55

localStorage.setItem(KEYS.token, config.token);

Issue: Storing authentication tokens in localStorage creates a high XSS vulnerability. Malicious scripts can access these tokens indefinitely.
Fix: Switch to sessionStorage (session-scoped) or implement secure token storage using browser APIs.

2. Tool Argument Redaction Bug (CRITICAL)

File: front_end/panels/ai_chat/mcp/MCPToolAdapter.ts:32-35

const sanitized = this.sanitize(args);
return this.client.callTool(this.serverId, this.def.name, sanitized, { timeoutMs: 30000 });

Issue: The code redacts secrets from arguments then sends the redacted version to MCP server, breaking authentication for any tool requiring API keys/tokens.
Fix: Use original args for the actual call, only use sanitized for logging.

3. XSS Risk in Settings Dialog (MEDIUM)

File: front_end/panels/ai_chat/ui/SettingsDialog.ts:2533-2547

mcpStatusDetails.innerHTML = detailsHtml;

Issue: Using innerHTML with unsanitized server error messages could inject malicious HTML/JS.
Fix: Use DOM text nodes and textContent instead of innerHTML.

🔴 Critical Functionality Issues

4. LLM Function Name Compatibility (BLOCKER)

File: front_end/panels/ai_chat/core/AgentNodes.ts:171-177

tools: tools.map(tool => ({
  function: {
    name: tool.name, // Names like "mcp:default:alpha" will be rejected by OpenAI
    description: tool.description,
    parameters: tool.schema,
  }
}))

Issue: MCP tool names containing colons/dots (e.g., mcp:default:alpha) violate LLM provider naming requirements (^[a-zA-Z0-9_-]{1,64}$). This will cause API failures.
Fix: Sanitize tool names for LLM calls and maintain a mapping for tool resolution.

5. Integration Test CI Failure (HIGH)

File: front_end/panels/ai_chat/mcp/MCPClientSDK.test.ts:53-60

describe('connects to local Hacker News MCP server via SDK', function() {
  const HACKER_NEWS_SERVER: MCPServer = {
    endpoint: 'http://localhost:5001/sse',
  };

Issue: This integration test will fail/hang in CI where no local MCP server runs.
Fix: Skip by default or gate behind environment variable.

⚠️ Architecture & Design Concerns

6. Bundle Size Impact (MEDIUM)

  • Impact: 13MB, 267 JS files for MCP SDK
  • Analysis: Significant size increase for a feature that may have limited initial adoption
  • Recommendation: Consider lazy loading or feature flagging

7. Error Handling Patterns (MEDIUM)

  • Missing: Robust retry logic for MCP connections
  • Missing: Graceful degradation when MCP servers become unavailable
  • Missing: Circuit breaker pattern for failing MCP endpoints

8. Type Safety Issues (MEDIUM)

Multiple uses of any type throughout the codebase reduce type safety:

  • front_end/panels/ai_chat/mcp/MCPToolAdapter.ts:10 - schema: any
  • front_end/panels/ai_chat/core/AgentNodes.ts:149 - Various any casts
  • Missing proper TypeScript definitions for MCP SDK integration

Positive Aspects

Well-Designed Features

  • Clean Configuration API: MCP config management is well-structured
  • Tool Surface Provider: Smart tool selection logic with multiple modes (all/router/meta)
  • Event-Driven Architecture: Proper use of CustomEvents for config changes
  • Logging Infrastructure: Comprehensive logging throughout MCP integration

Good Engineering Practices

  • Separation of Concerns: Clean boundaries between MCP registry, adapters, and UI
  • Error Categorization: Thoughtful error classification in MCPRegistry
  • Test Structure: Good test organization with unit and integration separation

📋 Test Coverage Assessment

Adequate Coverage:

  • Basic SDK instantiation and connection management
  • Error scenarios for missing servers
  • Tool selection logic

Missing Coverage:

  • MCP server authentication flows
  • Tool name sanitization and mapping
  • Error recovery and retry scenarios
  • UI integration testing
  • Performance testing with large tool sets

🚀 Performance Considerations

Concerns:

  • Bundle Impact: 13MB additional payload
  • Tool Discovery: No caching of MCP tool metadata
  • Memory Usage: Tool registry may grow unbounded

Recommendations:

  • Implement tool metadata caching
  • Add lazy loading for MCP functionality
  • Consider tool registry cleanup strategies

📝 Recommendations

Must Fix Before Merge:

  1. Fix token storage security vulnerability
  2. Fix tool argument redaction bug
  3. Implement LLM-compatible tool name sanitization
  4. Gate integration tests behind environment flag
  5. Replace innerHTML with safe DOM operations

Should Address:

  1. Add comprehensive error handling and retry logic
  2. Improve type safety throughout
  3. Add performance monitoring
  4. Implement feature flagging for gradual rollout

Consider for Future:

  1. Bundle size optimization
  2. Tool metadata caching
  3. Advanced MCP server management UI
  4. Integration with existing Browser Operator tool ecosystem

Overall Assessment

This PR represents a significant and well-architected addition to the Browser Operator platform. The MCP integration design is thoughtful and the implementation follows good patterns. However, there are several critical security and functionality issues that must be addressed before merge.

The code quality is generally good with proper separation of concerns and comprehensive logging. The test coverage is adequate for initial implementation but should be expanded for production readiness.

Recommendation: Request Changes - Address the critical security and functionality issues, then this will be ready for merge.


Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (4)
front_end/panels/ai_chat/ui/AIChatPanel.ts (4)

788-812: Add explicit return type and null-safe guards; optional debounce to avoid overlap.

  • Type the inner async fn to satisfy explicit-function-return-type.
  • Guard against undefined config using optional chaining.

Apply:

   #setupMCPIntegration(): void {
-    const initAndRefresh = async () => {
+    const initAndRefresh = async (): Promise<void> => {
       try {
-        const mcpConfig = getMCPConfig();
+        const mcpConfig = getMCPConfig();
         // Only auto-connect if both enabled and autostart are true
-        if (mcpConfig.enabled && mcpConfig.autostart) {
+        if (mcpConfig?.enabled && mcpConfig?.autostart) {
           await MCPRegistry.init();
           await MCPRegistry.refresh();
           const status = MCPRegistry.getStatus();
           logger.info('MCP autostart completed', status);
         }
       } catch (err) {
         logger.error('Failed to initialize MCP', err);
       }
     };
 
     void initAndRefresh();
     // Subscribe to config changes
     this.#mcpUnsubscribe = onMCPConfigChange(() => { void initAndRefresh(); });
   }

2269-2274: Gate MCP re-init behind config.enabled.

Avoids initializing when MCP is disabled in Settings.

Apply:

-    try {
-      await MCPRegistry.init();
-      await MCPRegistry.refresh();
-    } catch (err) {
+    try {
+      const cfg = getMCPConfig();
+      if (cfg?.enabled) {
+        await MCPRegistry.init();
+        await MCPRegistry.refresh();
+      } else {
+        logger.info('MCP disabled; skipping re-init after settings change');
+      }
+    } catch (err) {
       logger.error('Failed to reinitialize MCP after settings change', err);
-    }
+    }

87-90: Move MCP imports to the top import block and de-duplicate (ESLint blockers).

Currently after executable code (StorageMonitor) and duplicated from MCPConfig; violates import/first and import/no-duplicates.

Apply:

-// MCP integration
-import { MCPRegistry } from '../mcp/MCPRegistry.js';
-import { getMCPConfig } from '../mcp/MCPConfig.js';
-import { onMCPConfigChange } from '../mcp/MCPConfig.js';
+// (moved near other imports at file top)

And add alongside other imports at the top:

+// MCP integration
+import { MCPRegistry } from '../mcp/MCPRegistry.js';
+import { getMCPConfig, onMCPConfigChange } from '../mcp/MCPConfig.js';

721-721: Use the unsubscribe handle and clear it on teardown.

Prevents leaks when panel hides multiple times.

Apply:

   override willHide(): void {
@@
     if (this.#boundOnChildAgentStarted) this.#agentService.removeEventListener(AgentEvents.CHILD_AGENT_STARTED, this.#boundOnChildAgentStarted);
+    if (this.#mcpUnsubscribe) {
+      this.#mcpUnsubscribe();
+      this.#mcpUnsubscribe = null;
+    }
   }
🧹 Nitpick comments (2)
front_end/panels/ai_chat/BUILD.gn (2)

139-139: Ensure unittests bring in the MCP SDK bundle.

If tests or MCP modules transitively import the SDK, ts_library("unittests") may need an explicit dep on ../../third_party/mcp-sdk:bundle to satisfy TypeScript/rollup resolution.

Apply if needed:

 ts_library("unittests") {
   testonly = true
@@
   deps = [
     ":ai_chat",
     "../../testing",
     "../../core/sdk:bundle",
     "../../generated:protocol",
+    "../../third_party/mcp-sdk:bundle",
   ]
 }

346-352: Tests added: consider SDK dep and visibility.

  • If MCPClientSDK.test.ts or ToolSurfaceProvider tests import SDK types at runtime/compile-time, add ../../third_party/mcp-sdk:bundle to unittests deps (see earlier diff).
  • Confirm test target visibility allows importing these new paths.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2da8b6f and 1cc65c5.

📒 Files selected for processing (7)
  • config/gni/devtools_grd_files.gni (3 hunks)
  • front_end/panels/ai_chat/BUILD.gn (6 hunks)
  • front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts (1 hunks)
  • front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts (2 hunks)
  • front_end/panels/ai_chat/core/AgentNodes.ts (3 hunks)
  • front_end/panels/ai_chat/ui/AIChatPanel.ts (5 hunks)
  • front_end/panels/ai_chat/ui/ChatView.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • front_end/panels/ai_chat/ui/ChatView.ts
  • front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts
  • front_end/panels/ai_chat/core/AgentNodes.ts
  • config/gni/devtools_grd_files.gni
🧰 Additional context used
🪛 ESLint
front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts

[error] 22-22: ../../mcp/MCPMetaTools.js import should occur before import of ../../tools/FetcherTool.js

(import/order)

🔇 Additional comments (5)
front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts (1)

98-99: Wrap registerMCPMetaTools in try/catch

  // Ensure MCP meta-tools are available regardless of mode; selection logic decides if they are surfaced
- registerMCPMetaTools();
+ try {
+   registerMCPMetaTools(); // should be safe/no-op on repeated calls
+ } catch (err) {
+   console.warn('MCP meta-tools registration failed; continuing without MCP meta-tools.', err);
+ }

Confirm that registerMCPMetaTools is idempotent and cannot throw under normal conditions to avoid duplicate factory registrations or fatal failures.

front_end/panels/ai_chat/BUILD.gn (3)

58-58: ToolSurfaceProvider is correctly surfaced in build and metadata.

Present in both devtools_module sources and _ai_chat_sources; will participate in GRD metadata. LGTM.


126-130: MCP sources added to module are consistent with metadata list.

MCPConfig, MCPToolAdapter, MCPRegistry, MCPMetaTools are wired in; mirrors below in _ai_chat_sources. LGTM.


192-192: Metadata list mirrors new sources.

ToolSurfaceProvider and MCP modules are included in _ai_chat_sources; JS outputs will be emitted for GRD checks. LGTM.

Also applies to: 259-263

front_end/panels/ai_chat/ui/AIChatPanel.ts (1)

752-754: Constructor hook for MCP setup — good placement.

Runs after core wiring and model fetch; appropriate.

import { WaitTool } from '../../tools/Tools.js';
import { ThinkingTool } from '../../tools/ThinkingTool.js';
import type { Tool } from '../../tools/Tools.js';
import { registerMCPMetaTools } from '../../mcp/MCPMetaTools.js';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix ESLint import/order violation for MCP import (must come before tools imports).

ESLint flags this new import placement. Move it above ../../tools/FetcherTool.js (i.e., to the top of the internal import block). Also avoid duplicate imports after reordering.

Apply within this hunk (remove misplaced import):

- import { registerMCPMetaTools } from '../../mcp/MCPMetaTools.js';

Place this at the top of the import section (before any ../../tools/* imports):

import { registerMCPMetaTools } from '../../mcp/MCPMetaTools.js';
import { FetcherTool } from '../../tools/FetcherTool.js';
import { FinalizeWithCritiqueTool } from '../../tools/FinalizeWithCritiqueTool.js';
...

Optional: If bundle size is a concern, consider lazy-loading MCP meta-tools (dynamic import) guarded by settings/allowlist in a follow-up, but that would require adjusting call sites to handle async.

🧰 Tools
🪛 ESLint

[error] 22-22: ../../mcp/MCPMetaTools.js import should occur before import of ../../tools/FetcherTool.js

(import/order)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts
around line 22, the import for registerMCPMetaTools is placed after tools
imports causing an ESLint import/order violation; move the registerMCPMetaTools
import to the top of the internal import block so it comes before any
../../tools/* imports, remove any duplicate imports that result from the
reorder, and ensure the tools (e.g., FetcherTool, FinalizeWithCritiqueTool,
etc.) remain imported after this MCP import; optionally consider converting the
MCP import to a dynamic import behind a feature flag in a follow-up if bundle
size is a concern.

@claude
Copy link

claude bot commented Sep 9, 2025

Claude finished @tysonthomas9's task —— View job


MCP Integration PR Review

I've conducted a comprehensive review of this MCP integration PR. Here's my detailed feedback:

Review Tasks:

  • Analyze core MCP integration architecture
  • Review security concerns (token storage, XSS vulnerabilities)
  • Assess performance implications (bundle size, tool selection)
  • Examine test coverage and quality
  • Check code quality and best practices
  • Review UI integration patterns
  • Evaluate error handling and edge cases

🔴 Critical Security Issues

1. Token Storage Vulnerability (HIGH)

File: front_end/panels/ai_chat/mcp/MCPConfig.ts:54-55

localStorage.setItem(KEYS.token, config.token);

Issue: Storing authentication tokens in localStorage creates a high XSS vulnerability. Malicious scripts can access these tokens indefinitely.
Fix: Switch to sessionStorage (session-scoped) or implement secure token storage using browser APIs.

2. Tool Argument Redaction Bug (CRITICAL)

File: front_end/panels/ai_chat/mcp/MCPToolAdapter.ts:32-35

const sanitized = this.sanitize(args);
return this.client.callTool(this.serverId, this.def.name, sanitized, { timeoutMs: 30000 });

Issue: The code redacts secrets from arguments then sends the redacted version to MCP server, breaking authentication for any tool requiring API keys/tokens.
Fix: Use original args for the actual call, only use sanitized for logging.

3. XSS Risk in Settings Dialog (MEDIUM)

File: front_end/panels/ai_chat/ui/SettingsDialog.ts:2608, 2621

mcpStatusDetails.innerHTML = detailsHtml;

Issue: Using innerHTML with unsanitized server error messages could inject malicious HTML/JS.
Fix: Use DOM text nodes and textContent instead of innerHTML.

🔴 Critical Functionality Issues

4. LLM Function Name Compatibility (BLOCKER)

File: front_end/panels/ai_chat/core/AgentNodes.ts:171-177

tools: tools.map(tool => ({
  function: {
    name: tool.name, // Names like "mcp:default:alpha" will be rejected by OpenAI
    description: tool.description,
    parameters: tool.schema,
  }
}))

Issue: MCP tool names containing colons/dots (e.g., mcp:default:alpha) violate LLM provider naming requirements (^[a-zA-Z0-9_-]{1,64}$). This will cause API failures.
Fix: Sanitize tool names for LLM calls and maintain a mapping for tool resolution.

5. Integration Test CI Failure (HIGH)

File: front_end/panels/ai_chat/mcp/MCPClientSDK.test.ts:53-60

describe('connects to local Hacker News MCP server via SDK', function() {
  const HACKER_NEWS_SERVER: MCPServer = {
    endpoint: 'http://localhost:5001/sse',
  };

Issue: This integration test will fail/hang in CI where no local MCP server runs.
Fix: Skip by default or gate behind environment variable.

⚠️ Architecture & Design Concerns

6. Bundle Size Impact (MEDIUM)

  • Impact: 13MB, 267 JS files for MCP SDK
  • Analysis: Significant size increase for a feature that may have limited initial adoption
  • Recommendation: Consider lazy loading or feature flagging

7. Error Handling Patterns (MEDIUM)

  • Missing: Robust retry logic for MCP connections
  • Missing: Graceful degradation when MCP servers become unavailable
  • Missing: Circuit breaker pattern for failing MCP endpoints

8. Type Safety Issues (MEDIUM)

Multiple uses of any type throughout the codebase reduce type safety:

  • front_end/panels/ai_chat/mcp/MCPToolAdapter.ts:10 - schema: any
  • front_end/panels/ai_chat/core/AgentNodes.ts:149 - Various any casts
  • Missing proper TypeScript definitions for MCP SDK integration

Positive Aspects

Well-Designed Features

  • Clean Configuration API: MCP config management is well-structured
  • Tool Surface Provider: Smart tool selection logic with multiple modes (all/router/meta)
  • Event-Driven Architecture: Proper use of CustomEvents for config changes
  • Logging Infrastructure: Comprehensive logging throughout MCP integration

Good Engineering Practices

  • Separation of Concerns: Clean boundaries between MCP registry, adapters, and UI
  • Error Categorization: Thoughtful error classification in MCPRegistry
  • Test Structure: Good test organization with unit and integration separation

📋 Test Coverage Assessment

Adequate Coverage:

  • Basic SDK instantiation and connection management
  • Error scenarios for missing servers
  • Tool selection logic

Missing Coverage:

  • MCP server authentication flows
  • Tool name sanitization and mapping
  • Error recovery and retry scenarios
  • UI integration testing
  • Performance testing with large tool sets

🚀 Performance Considerations

Concerns:

  • Bundle Impact: 13MB additional payload
  • Tool Discovery: No caching of MCP tool metadata
  • Memory Usage: Tool registry may grow unbounded

Recommendations:

  • Implement tool metadata caching
  • Add lazy loading for MCP functionality
  • Consider tool registry cleanup strategies

📝 Recommendations

Must Fix Before Merge:

  1. Fix token storage security vulnerability
  2. Fix tool argument redaction bug
  3. Implement LLM-compatible tool name sanitization
  4. Gate integration tests behind environment flag
  5. Replace innerHTML with safe DOM operations

Should Address:

  1. Add comprehensive error handling and retry logic
  2. Improve type safety throughout
  3. Add performance monitoring
  4. Implement feature flagging for gradual rollout

Consider for Future:

  1. Bundle size optimization
  2. Tool metadata caching
  3. Advanced MCP server management UI
  4. Integration with existing Browser Operator tool ecosystem

Overall Assessment

This PR represents a significant and well-architected addition to the Browser Operator platform. The MCP integration design is thoughtful and the implementation follows good patterns. However, there are several critical security and functionality issues that must be addressed before merge.

The code quality is generally good with proper separation of concerns and comprehensive logging. The test coverage is adequate for initial implementation but should be expanded for production readiness.

Recommendation: Request Changes - Address the critical security and functionality issues, then this will be ready for merge.


Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
front_end/panels/ai_chat/ui/AIChatPanel.ts (2)

51-61: Stop logging secret values from localStorage (risk of secret leakage).

StorageMonitor logs previews of values for keys that include credentials. Remove value previews, and avoid logging raw content.

-        logger.debug(`Value exists: ${!!value}`);
-        logger.debug(`Value length: ${value?.length || 0}`);
-        logger.debug(`Value preview: ${value?.substring(0, 50) + (value?.length > 50 ? '...' : '') || 'null'}`);
+        logger.debug(`Value exists: ${!!value}`);
+        logger.debug(`Value length: ${value?.length || 0}`);
         logger.debug(`Timestamp: ${new Date().toISOString()}`);

Optional hardening (outside this hunk): initialize StorageMonitor only under a debug flag to avoid monkey‑patching in production. I can wire that if you want.

Also applies to: 64-71


1507-1512: Don’t log API key prefixes.

Leaking even partial secrets is risky and often forbidden by policy. Remove the prefix log.

-        logger.info('Retrieved API key:');
-        logger.info('- Exists:', !!apiKey);
-        logger.info('- Length:', apiKey?.length || 0);
-        logger.info('- Prefix:', apiKey?.substring(0, 8) + '...' || 'none');
+        logger.info('Retrieved API key:');
+        logger.info('- Exists:', !!apiKey);
+        logger.info('- Length:', apiKey?.length || 0);
♻️ Duplicate comments (12)
front_end/panels/ai_chat/ui/AIChatPanel.ts (4)

1956-1966: Use the MCP unsubscribe handle during teardown to prevent leaks.

Invoke and clear #mcpUnsubscribe in willHide.

   override willHide(): void {
     // Explicitly remove any event listeners to prevent memory leaks
     if (this.#boundOnMessagesChanged) {
       this.#agentService.removeEventListener(AgentEvents.MESSAGES_CHANGED, this.#boundOnMessagesChanged);
     }
     if (this.#boundOnAgentSessionStarted) this.#agentService.removeEventListener(AgentEvents.AGENT_SESSION_STARTED, this.#boundOnAgentSessionStarted);
     if (this.#boundOnAgentToolStarted) this.#agentService.removeEventListener(AgentEvents.AGENT_TOOL_STARTED, this.#boundOnAgentToolStarted);
     if (this.#boundOnAgentToolCompleted) this.#agentService.removeEventListener(AgentEvents.AGENT_TOOL_COMPLETED, this.#boundOnAgentToolCompleted);
     if (this.#boundOnAgentSessionUpdated) this.#agentService.removeEventListener(AgentEvents.AGENT_SESSION_UPDATED, this.#boundOnAgentSessionUpdated);
     if (this.#boundOnChildAgentStarted) this.#agentService.removeEventListener(AgentEvents.CHILD_AGENT_STARTED, this.#boundOnChildAgentStarted);
+    if (this.#mcpUnsubscribe) {
+      this.#mcpUnsubscribe();
+      this.#mcpUnsubscribe = null;
+    }
   }

88-91: Fix import order and combine duplicate MCP imports (ESLint: import/first, import/order, import/no-duplicates).

Move MCP imports into the top import block (before chatView.css.js) and merge MCPConfig imports.

Apply within this block:

-// MCP integration
-import { MCPRegistry } from '../mcp/MCPRegistry.js';
-import { getMCPConfig } from '../mcp/MCPConfig.js';
-import { onMCPConfigChange } from '../mcp/MCPConfig.js';

Add near the main import group at file top (before chatView.css.js):

// MCP integration
import { MCPRegistry } from '../mcp/MCPRegistry.js';
import { getMCPConfig, onMCPConfigChange } from '../mcp/MCPConfig.js';

789-813: Add explicit return type and nullish guards in MCP init helper.

Annotate return type and guard config fields to satisfy lint and avoid undefined access.

-  #setupMCPIntegration(): void {
-    const initAndRefresh = async () => {
+  #setupMCPIntegration(): void {
+    const initAndRefresh = async (): Promise<void> => {
       try {
-        const mcpConfig = getMCPConfig();
+        const mcpConfig = getMCPConfig();
         // Only auto-connect if both enabled and autostart are true
-        if (mcpConfig.enabled && mcpConfig.autostart) {
+        if (mcpConfig?.enabled && mcpConfig?.autostart) {
           await MCPRegistry.init();
           await MCPRegistry.refresh();
           const status = MCPRegistry.getStatus();
           logger.info('MCP autostart completed', status);
         }
-      } catch (err) {
-        logger.error('Failed to initialize MCP', err);
+      } catch (err: unknown) {
+        logger.error('Failed to initialize MCP', err);
       }
     };

2331-2336: Gate MCP re-init behind config.enabled after settings change.

Avoid initializing when MCP is disabled.

-    // Re-initialize MCP based on latest settings
-    try {
-      await MCPRegistry.init();
-      await MCPRegistry.refresh();
-    } catch (err) {
+    // Re-initialize MCP based on latest settings
+    try {
+      const cfg = getMCPConfig();
+      if (cfg?.enabled) {
+        await MCPRegistry.init();
+        await MCPRegistry.refresh();
+      }
+    } catch (err) {
       logger.error('Failed to reinitialize MCP after settings change', err);
-    }
+    }
front_end/panels/ai_chat/ui/SettingsDialog.ts (6)

538-548: Remove unused catch parameter (ESLint: no-unused-vars).

-    } catch (e) {
+    } catch {
       // Fallback silently to storedProvider
     }

2653-2658: Annotate updateDisconnectButton return type.

-    const updateDisconnectButton = () => {
+    const updateDisconnectButton = (): void => {
       const status = MCPRegistry.getStatus();
       const anyConnected = status.enabled && status.servers.some(s => s.connected);
       mcpDisconnectContainer.style.display = anyConnected ? 'block' : 'none';
     };

2831-2845: Annotate updateBudgetControls return type.

-    const updateBudgetControls = () => {
+    const updateBudgetControls = (): void => {
       const maxTools = Math.max(1, Math.min(100, parseInt(mcpMaxToolsInput.value, 10) || 20));
       const maxMcp = Math.max(1, Math.min(50, parseInt(mcpMaxMcpInput.value, 10) || 8));
       setMCPConfig({
         ...getMCPConfig(),
         maxToolsPerTurn: maxTools,
         maxMcpPerTurn: maxMcp,
       });
       onSettingsSaved();
     };

2561-2584: Add braces for single-line ifs and avoid HTML in error strings.

Wrap the single-line ifs (curly rule) and return plain text messages from formatMCPError to keep rendering safe.

-    const formatTimestamp = (date: Date | undefined): string => {
-      if (!date) return '';
-      return date.toLocaleString();
-    };
+    const formatTimestamp = (date: Date | undefined): string => {
+      if (!date) { return ''; }
+      return date.toLocaleString();
+    };
 
-    const formatMCPError = (error: string, errorType?: string): string => {
-      if (!errorType) return error;
-      
-      switch (errorType) {
+    const formatMCPError = (error: string, errorType?: string): string => {
+      if (!errorType) { return error; }
+      switch (errorType) {
         case 'connection':
-          return `Connection failed: ${error}<br><span style="color: var(--color-text-secondary); font-size: 12px;">Check if the MCP server is running and the endpoint URL is correct.</span>`;
+          return `Connection failed: ${error}. Hint: Check if the MCP server is running and the endpoint URL is correct.`;
         case 'authentication':
-          return `Authentication failed: ${error}<br><span style="color: var(--color-text-secondary); font-size: 12px;">Verify your auth token is correct and has not expired.</span>`;
+          return `Authentication failed: ${error}. Hint: Verify your auth token is correct and has not expired.`;
         case 'configuration':
-          return `Configuration error: ${error}<br><span style="color: var(--color-text-secondary); font-size: 12px;">Check your endpoint URL format (should be ws:// or wss://).</span>`;
+          return `Configuration error: ${error}. Hint: Check your endpoint URL format (should be ws:// or wss://).`;
         case 'network':
-          return `Network error: ${error}<br><span style="color: var(--color-text-secondary); font-size: 12px;">Check your internet connection and firewall settings.</span>`;
+          return `Network error: ${error}. Hint: Check your internet connection and firewall settings.`;
         case 'server_error':
-          return `Server error: ${error}<br><span style="color: var(--color-text-secondary); font-size: 12px;">The MCP server encountered an internal error. Contact the server administrator.</span>`;
+          return `Server error: ${error}. Hint: The MCP server encountered an internal error. Contact the server administrator.`;
         default:
           return error;
       }
     };

2585-2624: Render MCP status without innerHTML (XSS risk) and add explicit return type.

Build DOM nodes and use replaceChildren. Also annotate return type.

-    const updateMCPStatus = () => {
+    const updateMCPStatus = (): void => {
       const status = MCPRegistry.getStatus();
       if (!status.enabled) {
         mcpStatusDot.style.backgroundColor = 'var(--color-text-disabled)';
         mcpStatusText.textContent = 'Disabled';
         mcpStatusText.style.color = 'var(--color-text-disabled)';
-        mcpStatusDetails.innerHTML = '';
+        mcpStatusDetails.replaceChildren();
         return;
       }
       const anyConnected = status.servers.some(s => s.connected);
       const toolCount = status.registeredToolNames.length;
       if (anyConnected) {
         mcpStatusDot.style.backgroundColor = 'var(--color-accent-green)';
         mcpStatusText.textContent = `Connected (${toolCount} tools)`;
         mcpStatusText.style.color = 'var(--color-accent-green)';
-        
-        let detailsHtml = '';
-        if (status.lastConnected) {
-          detailsHtml += `Last connected: ${formatTimestamp(status.lastConnected)}<br>`;
-        }
-        if (status.lastError) {
-          detailsHtml += `<span style="color: var(--color-error-text)">${formatMCPError(status.lastError, status.lastErrorType)}</span>`;
-        }
-        mcpStatusDetails.innerHTML = detailsHtml;
+        const nodes: Node[] = [];
+        if (status.lastConnected) {
+          nodes.push(document.createTextNode(`Last connected: ${formatTimestamp(status.lastConnected)}`));
+          nodes.push(document.createElement('br'));
+        }
+        if (status.lastError) {
+          const err = document.createElement('span');
+          err.style.color = 'var(--color-error-text)';
+          err.textContent = formatMCPError(status.lastError, status.lastErrorType);
+          nodes.push(err);
+        }
+        mcpStatusDetails.replaceChildren(...nodes);
       } else {
         mcpStatusDot.style.backgroundColor = 'var(--color-text-disabled)';
         mcpStatusText.textContent = 'Not connected';
         mcpStatusText.style.color = 'var(--color-text-disabled)';
-        
-        let detailsHtml = '';
-        if (status.lastDisconnected) {
-          detailsHtml += `Last disconnected: ${formatTimestamp(status.lastDisconnected)}<br>`;
-        }
-        if (status.lastError) {
-          detailsHtml += `<span style="color: var(--color-error-text)">${formatMCPError(status.lastError, status.lastErrorType)}</span>`;
-        }
-        mcpStatusDetails.innerHTML = detailsHtml;
+        const nodes: Node[] = [];
+        if (status.lastDisconnected) {
+          nodes.push(document.createTextNode(`Last disconnected: ${formatTimestamp(status.lastDisconnected)}`));
+          nodes.push(document.createElement('br'));
+        }
+        if (status.lastError) {
+          const err = document.createElement('span');
+          err.style.color = 'var(--color-error-text)';
+          err.textContent = formatMCPError(status.lastError, status.lastErrorType);
+          nodes.push(err);
+        }
+        mcpStatusDetails.replaceChildren(...nodes);
       }
     };

2865-2915: Clear and rebuild tools list safely; use fresh config for allowlist.

Avoid innerHTML = ''; read allowlist from latest config each time.

-    const updateToolsList = () => {
+    const updateToolsList = (): void => {
       const status = MCPRegistry.getStatus();
-      mcpToolsList.innerHTML = '';
+      mcpToolsList.replaceChildren();
       
       if (!status.enabled || status.registeredToolNames.length === 0) {
         const noToolsMessage = document.createElement('div');
         noToolsMessage.className = 'mcp-no-tools';
         noToolsMessage.textContent = i18nString(UIStrings.mcpNoTools);
         mcpToolsList.appendChild(noToolsMessage);
         return;
       }
 
-      const currentAllowlist = new Set(currentMCPConfig.toolAllowlist || []);
+      const { toolAllowlist } = getMCPConfig();
+      const currentAllowlist = new Set(toolAllowlist || []);
front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts (1)

22-22: Fix ESLint import/order for MCP import (place before tools).

Move the MCP import above all ../../tools/* imports to satisfy import/order and avoid duplicates.

- import { registerMCPMetaTools } from '../../mcp/MCPMetaTools.js';
+import { registerMCPMetaTools } from '../../mcp/MCPMetaTools.js';
 import { FetcherTool } from '../../tools/FetcherTool.js';
 import { FinalizeWithCritiqueTool } from '../../tools/FinalizeWithCritiqueTool.js';
 import { SchemaBasedExtractorTool } from '../../tools/SchemaBasedExtractorTool.js';
front_end/panels/ai_chat/core/AgentNodes.ts (1)

413-416: Executor must honor selected tools and sanitized names; remove any to fix ESLint.

Current map ignores context.selectedTools and sanitized aliases, and uses any. Prefer the persisted selection and map both original and sanitized names.

-export function createToolExecutorNode(state: AgentState, provider: LLMProvider, modelName: string, miniModel?: string, nanoModel?: string): Runnable<AgentState, AgentState> {
-  const tools = getAgentToolsFromState(state); // Adjusted to use getAgentToolsFromState
-  const toolMap = new Map<string, ReturnType<typeof getTools>[number]>();
-  (tools as any[]).forEach((tool: any) => toolMap.set(tool.name, tool));
+export function createToolExecutorNode(
+  state: AgentState,
+  provider: LLMProvider,
+  modelName: string,
+  miniModel?: string,
+  nanoModel?: string
+): Runnable<AgentState, AgentState> {
+  type ToolT = ReturnType<typeof getTools>[number];
+  const ctx = (state.context || {}) as {
+    selectedTools?: ToolT[];
+    selectedToolNames?: string[];
+    toolNameMap?: Record<string, string>; // sanitized -> original
+  };
+  let tools: ToolT[] = Array.isArray(ctx.selectedTools) && ctx.selectedTools.length
+    ? ctx.selectedTools
+    : (getAgentToolsFromState(state) as ToolT[]);
+  const toolMap = new Map<string, ToolT>();
+  const nameMap = ctx.toolNameMap || {};
+  tools.forEach((tool) => {
+    toolMap.set(tool.name, tool); // original
+    // add sanitized alias(es) if mapped
+    for (const [sanitized, original] of Object.entries(nameMap)) {
+      if (original === tool.name) toolMap.set(sanitized, tool);
+    }
+  });
🧹 Nitpick comments (1)
front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts (1)

97-98: Make MCP registration resilient (lazy/guarded).

Unconditional registration can throw in environments without MCP config. Wrap in a guard/try-catch or feature flag; consider lazy-loading later to keep init cheap.

-  registerMCPMetaTools();
+  try {
+    registerMCPMetaTools();
+  } catch (e) {
+    // Non-fatal: MCP optional
+    console.warn('MCP meta-tools registration skipped:', e);
+  }

Would you like a follow-up to convert this to a dynamic import behind a settings flag?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1cc65c5 and 5684856.

📒 Files selected for processing (6)
  • front_end/panels/ai_chat/BUILD.gn (6 hunks)
  • front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts (1 hunks)
  • front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts (2 hunks)
  • front_end/panels/ai_chat/core/AgentNodes.ts (3 hunks)
  • front_end/panels/ai_chat/ui/AIChatPanel.ts (5 hunks)
  • front_end/panels/ai_chat/ui/SettingsDialog.ts (8 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts
  • front_end/panels/ai_chat/BUILD.gn
🧰 Additional context used
🧬 Code graph analysis (2)
front_end/panels/ai_chat/ui/SettingsDialog.ts (1)
front_end/panels/ai_chat/ui/AIChatPanel.ts (9)
  • localStorage (817-840)
  • currentProvider (878-938)
  • currentProvider (2344-2403)
  • error (1933-1946)
  • logger (952-1045)
  • logger (1336-1406)
  • logger (1429-1456)
  • logger (1563-1576)
  • logger (1620-1623)
front_end/panels/ai_chat/core/AgentNodes.ts (1)
front_end/panels/ai_chat/core/GraphHelpers.ts (1)
  • getAgentToolsFromState (44-47)
🪛 ESLint
front_end/panels/ai_chat/ui/SettingsDialog.ts

[error] 437-437: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 546-546: 'e' is defined but never used.

(@typescript-eslint/no-unused-vars)


[error] 2562-2562: Expected { after 'if' condition.

(curly)


[error] 2567-2567: Expected { after 'if' condition.

(curly)


[error] 2568-2568: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 2585-2585: Missing return type on function.

(@typescript-eslint/explicit-function-return-type)


[error] 2600-2600: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 2613-2613: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 2653-2653: Missing return type on function.

(@typescript-eslint/explicit-function-return-type)


[error] 2832-2832: Missing return type on function.

(@typescript-eslint/explicit-function-return-type)


[error] 2865-2865: Missing return type on function.

(@typescript-eslint/explicit-function-return-type)


[error] 2868-2868: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 2900-2900: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)

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

[error] 22-22: ../../mcp/MCPMetaTools.js import should occur before import of ../../tools/FetcherTool.js

(import/order)

front_end/panels/ai_chat/core/AgentNodes.ts

[error] 13-13: ./ToolSurfaceProvider.js import should occur after type import of ../tracing/TracingProvider.js

(import/order)


[error] 415-415: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 415-415: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

front_end/panels/ai_chat/ui/AIChatPanel.ts

[error] 89-89: Import in body of module; reorder to top.

(import/first)


[error] 89-89: ../mcp/MCPRegistry.js import should occur before import of ./chatView.css.js

(import/order)


[error] 90-90: Import in body of module; reorder to top.

(import/first)


[error] 90-90: ../mcp/MCPConfig.js import should occur before import of ./chatView.css.js

(import/order)


[error] 90-90: '../mcp/MCPConfig.js' imported multiple times.

(import/no-duplicates)


[error] 91-91: Import in body of module; reorder to top.

(import/first)


[error] 91-91: ../mcp/MCPConfig.js import should occur before import of ./chatView.css.js

(import/order)


[error] 91-91: '../mcp/MCPConfig.js' imported multiple times.

(import/no-duplicates)


[error] 722-722: '#mcpUnsubscribe' is defined but never used.

(no-unused-private-class-members)


[error] 794-794: Missing return type on function.

(@typescript-eslint/explicit-function-return-type)

🪛 ast-grep (0.38.6)
front_end/panels/ai_chat/ui/SettingsDialog.ts

[warning] 2607-2607: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: mcpStatusDetails.innerHTML = detailsHtml
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)


[warning] 2620-2620: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: mcpStatusDetails.innerHTML = detailsHtml
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)


[warning] 2590-2590: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: mcpStatusDetails.innerHTML = ''
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)


[warning] 2607-2607: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: mcpStatusDetails.innerHTML = detailsHtml
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)


[warning] 2620-2620: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: mcpStatusDetails.innerHTML = detailsHtml
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)


[warning] 2866-2866: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: mcpToolsList.innerHTML = ''
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)

Comment on lines 147 to 154
// Select tools via ToolSurfaceProvider (router/meta/all)
const baseTools = getAgentToolsFromState(state);
const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 });
const tools = selection.tools;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Per-turn tool selection added — persist selection and name map for executor.

The LLM is called with selection.tools, but the executor builds its map from base tools and doesn’t know sanitized names. Persist the turn’s tools and a sanitized→original name map in state.context so the executor can honor them.

-        const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 });
-        const tools = selection.tools;
+        const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 });
+        const tools = selection.tools;
+        // Make executor-aware of the exact tool set and sanitized name mapping
+        state.context = {
+          ...(state.context || {}),
+          selectedTools: tools,
+          toolNameMap: selection.nameMap || {}, // { sanitizedName: originalName }
+        };

Nit: In tracing input above, record the actual selected tools instead of all agent tools.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Select tools via ToolSurfaceProvider (router/meta/all)
const baseTools = getAgentToolsFromState(state);
const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 });
const tools = selection.tools;
// Select tools via ToolSurfaceProvider (router/meta/all)
const baseTools = getAgentToolsFromState(state);
const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 });
const tools = selection.tools;
// Make executor-aware of the exact tool set and sanitized name mapping
state.context = {
...(state.context || {}),
selectedTools: tools,
toolNameMap: selection.nameMap || {}, // { sanitizedName: originalName }
};
🧰 Tools
🪛 ESLint

[error] 151-151: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/AgentNodes.ts around lines 147-151, the code
calls the LLM with selection.tools but the executor still builds its tool map
from baseTools and lacks the sanitized→original name mapping; update the agent
node to persist the per-turn selected tools and a sanitizedNameMap into
state.context (e.g. state.context.selectedTools and
state.context.sanitizedNameMap) immediately after ToolSurfaceProvider.select
resolves so the executor can read and build its map from state.context rather
than baseTools, and change any tracing/input payload to record the actual
selectedTools (selection.tools) instead of getAgentToolsFromState(state).

@claude
Copy link

claude bot commented Sep 12, 2025

Claude finished @tysonthomas9's task —— View job


MCP Integration PR Review

I've conducted a comprehensive review of this MCP integration PR. Here's my detailed feedback:

Review Tasks:

  • Analyze core MCP integration architecture
  • Review security concerns (token storage, XSS vulnerabilities)
  • Assess performance implications (bundle size, tool selection)
  • Examine test coverage and quality
  • Check code quality and best practices
  • Review UI integration patterns
  • Evaluate error handling and edge cases
  • Provide comprehensive feedback and recommendations

🔴 Critical Security Issues

1. Token Storage Vulnerability (HIGH)

File: front_end/panels/ai_chat/mcp/MCPConfig.ts:54-55

localStorage.setItem(KEYS.token, config.token);

Issue: Storing authentication tokens in localStorage creates a high XSS vulnerability. Malicious scripts can access these tokens indefinitely.
Fix: Switch to sessionStorage (session-scoped) or implement secure token storage using browser APIs.

2. Tool Argument Redaction Bug (CRITICAL)

File: front_end/panels/ai_chat/mcp/MCPToolAdapter.ts:32-35

const sanitized = this.sanitize(args);
return this.client.callTool(this.serverId, this.def.name, sanitized, { timeoutMs: 30000 });

Issue: The code redacts secrets from arguments then sends the redacted version to MCP server, breaking authentication for any tool requiring API keys/tokens.
Fix: Use original args for the actual call, only use sanitized for logging.

3. XSS Risk in Settings Dialog (MEDIUM)

File: front_end/panels/ai_chat/ui/SettingsDialog.ts:2533-2547

mcpStatusDetails.innerHTML = detailsHtml;

Issue: Using innerHTML with unsanitized server error messages could inject malicious HTML/JS.
Fix: Use DOM text nodes and textContent instead of innerHTML.

🔴 Critical Functionality Issues

4. LLM Function Name Compatibility (BLOCKER)

File: front_end/panels/ai_chat/core/AgentNodes.ts:171-177

tools: tools.map(tool => ({
  function: {
    name: tool.name, // Names like "mcp:default:alpha" will be rejected by OpenAI
    description: tool.description,
    parameters: tool.schema,
  }
}))

Issue: MCP tool names containing colons/dots (e.g., mcp:default:alpha) violate LLM provider naming requirements (^[a-zA-Z0-9_-]{1,64}$). This will cause API failures.
Fix: Sanitize tool names for LLM calls and maintain a mapping for tool resolution.

5. Integration Test CI Failure (HIGH)

File: front_end/panels/ai_chat/mcp/MCPClientSDK.test.ts:53-60

describe('connects to local Hacker News MCP server via SDK', function() {
  const HACKER_NEWS_SERVER: MCPServer = {
    endpoint: 'http://localhost:5001/sse',
  };

Issue: This integration test will fail/hang in CI where no local MCP server runs.
Fix: Skip by default or gate behind environment variable.

⚠️ Architecture & Design Concerns

6. Bundle Size Impact (MEDIUM)

  • Impact: 13MB, 267 JS files for MCP SDK
  • Analysis: Significant size increase for a feature that may have limited initial adoption
  • Recommendation: Consider lazy loading or feature flagging

7. Error Handling Patterns (MEDIUM)

  • Missing: Robust retry logic for MCP connections
  • Missing: Graceful degradation when MCP servers become unavailable
  • Missing: Circuit breaker pattern for failing MCP endpoints

8. Type Safety Issues (MEDIUM)

Multiple uses of any type throughout the codebase reduce type safety:

  • front_end/panels/ai_chat/mcp/MCPToolAdapter.ts:10 - schema: any
  • front_end/panels/ai_chat/core/AgentNodes.ts:149 - Various any casts
  • Missing proper TypeScript definitions for MCP SDK integration

Positive Aspects

Well-Designed Features

  • Clean Configuration API: MCP config management is well-structured
  • Tool Surface Provider: Smart tool selection logic with multiple modes (all/router/meta)
  • Event-Driven Architecture: Proper use of CustomEvents for config changes
  • Logging Infrastructure: Comprehensive logging throughout MCP integration

Good Engineering Practices

  • Separation of Concerns: Clean boundaries between MCP registry, adapters, and UI
  • Error Categorization: Thoughtful error classification in MCPRegistry
  • Test Structure: Good test organization with unit and integration separation

📋 Test Coverage Assessment

Adequate Coverage:

  • Basic SDK instantiation and connection management
  • Error scenarios for missing servers
  • Tool selection logic

Missing Coverage:

  • MCP server authentication flows
  • Tool name sanitization and mapping
  • Error recovery and retry scenarios
  • UI integration testing
  • Performance testing with large tool sets

🚀 Performance Considerations

Concerns:

  • Bundle Impact: 13MB additional payload
  • Tool Discovery: No caching of MCP tool metadata
  • Memory Usage: Tool registry may grow unbounded

Recommendations:

  • Implement tool metadata caching
  • Add lazy loading for MCP functionality
  • Consider tool registry cleanup strategies

📝 Recommendations

Must Fix Before Merge:

  1. Fix token storage security vulnerability
  2. Fix tool argument redaction bug
  3. Implement LLM-compatible tool name sanitization
  4. Gate integration tests behind environment flag
  5. Replace innerHTML with safe DOM operations

Should Address:

  1. Add comprehensive error handling and retry logic
  2. Improve type safety throughout
  3. Add performance monitoring
  4. Implement feature flagging for gradual rollout

Consider for Future:

  1. Bundle size optimization
  2. Tool metadata caching
  3. Advanced MCP server management UI
  4. Integration with existing Browser Operator tool ecosystem

Overall Assessment

This PR represents a significant and well-architected addition to the Browser Operator platform. The MCP integration design is thoughtful and the implementation follows good patterns. However, there are several critical security and functionality issues that must be addressed before merge.

The code quality is generally good with proper separation of concerns and comprehensive logging. The test coverage is adequate for initial implementation but should be expanded for production readiness.

Recommendation: Request Changes - Address the critical security and functionality issues, then this will be ready for merge.


Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
front_end/panels/ai_chat/ui/ToolCallComponent.ts (1)

14-16: ESLint error: custom element defined in a disallowed folder

The rule requires custom elements to live in ui/components/ or a panel’s components/ sub-folder. Moving this file will unblock CI.

Suggested fix:

  • Move to front_end/panels/ai_chat/ui/components/ToolCallComponent.ts and update imports/exports accordingly.

Temporary (not preferred) alternatives:

  • Add a targeted override in the panel’s ESLint config for this path, or
  • File-level suppression at top of file:
/* eslint-disable rulesdir/enforce-custom-element-definitions-location */
front_end/panels/ai_chat/ui/message/MessageCombiner.ts (1)

46-56: Combine across agent-lane separators (don’t require immediate adjacency)

Current logic only checks messages[i+1], so a tool_result separated by one or more agent-lane messages becomes orphaned. Scan forward for the next non-agent message (j) and, when you combine, advance the loop to that index (set i = j) so the tool_result and intermediate agent-lane messages are skipped.

File: front_end/panels/ai_chat/ui/message/MessageCombiner.ts (around lines 46–56).

front_end/panels/ai_chat/core/AgentNodes.ts (1)

261-273: Resolve sanitized tool name → original before lane detection.

Parsed tool names from the model will be sanitized; registry holds originals.

-          const regTool = ToolRegistry.getRegisteredTool(parsedAction.name as any);
-          const isAgentTool = !!regTool && (regTool instanceof ConfigurableAgentTool);
+          const nameMap: Record<string, string> = (state.context as any)?.toolNameMap || {};
+          const originalName = nameMap[parsedAction.name] ?? parsedAction.name;
+          const regTool = ToolRegistry.getRegisteredTool(originalName as any);
+          const isAgentTool = regTool instanceof ConfigurableAgentTool;
♻️ Duplicate comments (34)
front_end/panels/ai_chat/mcp/MCPClientSDK.test.ts (4)

24-31: Prefer instanceOf/isFalse assertions

Align with project assertion conventions.

-  it('can be instantiated', () => {
-    assert.ok(client instanceof MCPClientSDK);
-  });
+  it('can be instantiated', () => {
+    assert.instanceOf(client, MCPSDK.MCPClientSDK);
+  });
@@
-  it('reports not connected initially', () => {
-    assert.strictEqual(client.isConnected('non-existent-server'), false);
-  });
+  it('reports not connected initially', () => {
+    assert.isFalse(client.isConnected('non-existent-server'));
+  });

35-51: Tighten error assertions and avoid assert.ok

Use assert.include on message; remove redundant throw dance with assert.fail for clarity.

-  it('throws error for missing server when listing tools', async () => {
-    try {
-      await client.listTools('non-existent-server');
-      throw new Error('Should have thrown error');
-    } catch (error) {
-      assert.ok(error.message.includes('No connection for server'));
-    }
-  });
+  it('throws error for missing server when listing tools', async () => {
+    try {
+      await client.listTools('non-existent-server');
+      assert.fail('Expected error');
+    } catch (error) {
+      assert.include((error as Error).message, 'No connection for server');
+    }
+  });
@@
-  it('throws error for missing server when calling tools', async () => {
+  it('throws error for missing server when calling tools', async () => {
     try {
       await client.callTool('non-existent-server', 'test-tool', {});
-      throw new Error('Should have thrown error');
+      assert.fail('Expected error');
     } catch (error) {
-      assert.ok(error.message.includes('No connection for server'));
+      assert.include((error as Error).message, 'No connection for server');
     }
   });

5-7: Use a single namespaced import for SDK (fixes ESLint import rules)

Consolidate value and type imports via namespace to satisfy rulesdir/es-modules-import and inline-type-imports.

-import { MCPClientSDK } from '../../../third_party/mcp-sdk/mcp-sdk.js';
-import type { MCPServer } from '../../../third_party/mcp-sdk/mcp-sdk.js';
+import * as MCPSDK from '../../../third_party/mcp-sdk/mcp-sdk.js';

And update types/usages:

-  let client: MCPClientSDK;
+  let client: MCPSDK.MCPClientSDK;
@@
-    client = new MCPClientSDK();
+    client = new MCPSDK.MCPClientSDK();
@@
-    const HACKER_NEWS_SERVER: MCPServer = {
+    const HACKER_NEWS_SERVER: MCPSDK.MCPServer = {

53-135: Remove or gate the localhost integration test; convert console.log to console.warn

This block depends on a local MCP server and will fail/hang in CI. Gate behind an env flag and fix console usage + boolean assertions.

-describe('connects to local Hacker News MCP server via SDK', function() {
+const RUN_MCP_SDK_E2E = !!process.env.MCP_SDK_E2E;
+(RUN_MCP_SDK_E2E ? describe : describe.skip)('connects to local Hacker News MCP server via SDK', function() {
   this.timeout(30000); // Longer timeout for integration test
@@
-    it('connects and lists tools', async function() {
-      console.log('=== Testing Local Hacker News MCP Server with SDK ===');
+    it('connects and lists tools', async function() {
+      console.warn('=== Testing Local Hacker News MCP Server with SDK ===');
@@
-        console.log('Step 1: Connecting to MCP server via SDK...');
+        console.warn('Step 1: Connecting to MCP server via SDK...');
         await client.connect(HACKER_NEWS_SERVER);
-        assert.strictEqual(client.isConnected(HACKER_NEWS_SERVER.id), true, 'Should be connected');
-        console.log('✓ Connection established via SDK');
+        assert.isTrue(client.isConnected(HACKER_NEWS_SERVER.id), 'Should be connected');
+        console.warn('✓ Connection established via SDK');
@@
-        console.log('Step 2: Listing available tools via SDK...');
+        console.warn('Step 2: Listing available tools via SDK...');
         const tools = await client.listTools(HACKER_NEWS_SERVER.id);
-        console.log(`✓ Retrieved ${tools.length} tools via SDK`);
+        console.warn(`✓ Retrieved ${tools.length} tools via SDK`);
@@
-        assert.ok(Array.isArray(tools), 'Tools should be an array');
+        assert.isTrue(Array.isArray(tools), 'Tools should be an array');
         if (tools.length > 0) {
           const tool = tools[0];
-          assert.ok(tool.hasOwnProperty('name'), 'Tool should have name');
-          assert.ok(tool.hasOwnProperty('description'), 'Tool should have description');
-          assert.ok(tool.hasOwnProperty('inputSchema'), 'Tool should have inputSchema');
-          console.log(`✓ Tool structure valid: ${tool.name}`);
+          assert.isTrue(Object.prototype.hasOwnProperty.call(tool, 'name'), 'Tool should have name');
+          assert.isTrue(Object.prototype.hasOwnProperty.call(tool, 'description'), 'Tool should have description');
+          assert.isTrue(Object.prototype.hasOwnProperty.call(tool, 'inputSchema'), 'Tool should have inputSchema');
+          console.warn(`✓ Tool structure valid: ${tool.name}`);
         }
@@
-          console.log(`Step 3: Testing tool call: ${firstTool.name}`);
+          console.warn(`Step 3: Testing tool call: ${firstTool.name}`);
@@
-            const schema = firstTool.inputSchema as any;
+            const schema = firstTool.inputSchema as unknown;
@@
-            console.log(`Calling ${firstTool.name} with args:`, args);
+            console.warn(`Calling ${firstTool.name} with args:`, args);
             const result = await client.callTool(HACKER_NEWS_SERVER.id, firstTool.name, args);
-            
-            console.log('Tool call result via SDK:', typeof result);
-            assert.ok(result !== undefined, 'Tool call should return a result');
-            console.log('✓ Tool call successful via SDK');
+            console.warn('Tool call result via SDK:', typeof result);
+            assert.isDefined(result, 'Tool call should return a result');
+            console.warn('✓ Tool call successful via SDK');
@@
-            console.log('Tool call failed, but connection and tools/list worked');
+            console.warn('Tool call failed, but connection and tools/list worked');
@@
-        console.log('✓ All SDK tests passed!');
+        console.warn('✓ All SDK tests passed!');

Also add a final newline and strip trailing spaces.

front_end/panels/ai_chat/agent_framework/implementation/ConfiguredAgents.ts (1)

22-22: Fix import/order: place MCP import before tools imports

ESLint import/order flags this. Move registerMCPMetaTools import above any ../../tools/* imports.

-import { FetcherTool } from '../../tools/FetcherTool.js';
+import { registerMCPMetaTools } from '../../mcp/MCPMetaTools.js';
+import { FetcherTool } from '../../tools/FetcherTool.js';
 import { FinalizeWithCritiqueTool } from '../../tools/FinalizeWithCritiqueTool.js';
@@
-import { ThinkingTool } from '../../tools/ThinkingTool.js';
-import type { Tool } from '../../tools/Tools.js';
-import { registerMCPMetaTools } from '../../mcp/MCPMetaTools.js';
+import { ThinkingTool } from '../../tools/ThinkingTool.js';
+import type { Tool } from '../../tools/Tools.js';
front_end/panels/ai_chat/core/ToolNameMapping.test.ts (4)

12-21: Drop explicit public; make ctor param readonly

Aligns with @typescript-eslint/explicit-member-accessibility and intent.

-class RecordingTool implements Tool<Record<string, unknown>, unknown> {
-  public calls = 0;
-  constructor(public name: string) {}
+class RecordingTool implements Tool<Record<string, unknown>, unknown> {
+  calls = 0;
+  constructor(readonly name: string) {}

50-67: Remove any casts; type the message/state precisely

Avoid as any; construct a typed ModelChatMessage and AgentState.

-    const state: AgentState = {
-      messages: [
-        {
-          entity: ChatMessageEntity.MODEL,
-          action: 'tool',
-          toolName: sanitizedName,
-          toolArgs: { x: 1 },
-          toolCallId: 'call-1',
-          isFinalAnswer: false,
-        } as any
-      ],
-      agentType: 'deep-research' as any,
-      context: {
-        selectedToolNames: [sanitizedName],
-        selectedTools: [tool],
-        toolNameMap: { [sanitizedName]: originalName }
-      }
-    } as any;
+    const modelCall = {
+      entity: ChatMessageEntity.MODEL,
+      action: 'tool' as const,
+      toolName: sanitizedName,
+      toolArgs: { x: 1 } as Record<string, unknown>,
+      toolCallId: 'call-1',
+      isFinalAnswer: false,
+    };
+    const state: AgentState = {
+      messages: [modelCall],
+      context: {
+        selectedToolNames: [sanitizedName],
+        selectedTools: [tool],
+        toolNameMap: { [sanitizedName]: originalName },
+      } as any, // if DevToolsContext type isn't exported here
+    };

23-42: Restore real localStorage after each test

Prevents cross-test side effects.

-describe('AgentNodes sanitized tool name mapping', () => {
-  let mockLocalStorage: Map<string, string>;
+describe('AgentNodes sanitized tool name mapping', () => {
+  let mockLocalStorage: Map<string, string>;
+  let realLocalStorage: Storage;
@@
   beforeEach(() => {
@@
-    Object.defineProperty(window, 'localStorage', {
+    realLocalStorage = window.localStorage;
+    Object.defineProperty(window, 'localStorage', {
       value: {
@@
   afterEach(() => {
     mockLocalStorage.clear();
+    Object.defineProperty(window, 'localStorage', { value: realLocalStorage, writable: true });
   });

5-9: Import assert and fix import grouping/order

assert is used but not imported; also group type imports before value imports and add a blank line between groups.

-import { createToolExecutorNode } from './AgentNodes.js';
-import type { AgentState } from './State.js';
-import type { Tool } from '../tools/Tools.js';
-import { ChatMessageEntity } from '../models/ChatTypes.js';
+import type { AgentState } from './State.js';
+import type { Tool } from '../tools/Tools.js';
+import { ChatMessageEntity } from '../models/ChatTypes.js';
+import { createToolExecutorNode } from './AgentNodes.js';
+import { assert } from 'chai';
front_end/panels/ai_chat/mcp/MCPToolAdapter.ts (1)

32-36: Do not pass redacted args to MCP; only redact for logging.

Currently the sanitized args are sent to client.callTool, breaking auth for tools that require secrets. Log the sanitized copy, but call with the original args. (Same concern previously raised.)

-  async execute(args: Record<string, unknown>): Promise<unknown> {
-    const sanitized = this.sanitize(args);
-    logger.info('Executing MCP tool', { name: this.name, serverId: this.serverId });
-    return this.client.callTool(this.serverId, this.def.name, sanitized, { timeoutMs: 30000 });
-  }
+  async execute(args: Record<string, unknown>): Promise<unknown> {
+    const redacted = this.sanitize(args);
+    logger.info('Executing MCP tool', { name: this.name, serverId: this.serverId, args: redacted });
+    return this.client.callTool(this.serverId, this.def.name, args, { timeoutMs: 30000 });
+  }
front_end/panels/ai_chat/core/ToolSurfaceProvider.test.ts (1)

5-11: Import Chai assert and typed ChatMessageEntity; keep import order.

Avoids globals and 0 as any.

-import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
-import type { AgentState } from './State.js';
-import type { Tool } from '../tools/Tools.js';
-import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';
-import { MCPRegistry } from '../mcp/MCPRegistry.js';
-import { registerMCPMetaTools } from '../mcp/MCPMetaTools.js';
+import { registerMCPMetaTools } from '../mcp/MCPMetaTools.js';
+import { MCPRegistry } from '../mcp/MCPRegistry.js';
+import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';
+import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
+import type { AgentState } from './State.js';
+import type { Tool } from '../tools/Tools.js';
+import { assert } from 'chai';
+import { ChatMessageEntity } from '../ui/ChatView.js';
front_end/panels/ai_chat/core/AgentNodes.test.ts (1)

45-56: Replace any for agentSession with a typed alias; mirror in execute() return.

-      const errorResultWithSession: ConfigurableAgentResult & { agentSession: any } = {
+      type AgentResultWithSession = ConfigurableAgentResult & { agentSession: unknown };
+      const errorResultWithSession: AgentResultWithSession = {
@@
-        async execute(): Promise<ConfigurableAgentResult & { agentSession: any }> {
+        async execute(): Promise<AgentResultWithSession> {
           return errorResultWithSession;
         }

Also applies to: 69-71

front_end/panels/ai_chat/core/AgentNodes.ts (3)

169-181: Send provider‑safe tool names and keep mapping; avoid : in function names.

Prevents OpenAI/Anthropic function-name rejections.

-              tools: tools.map(tool => ({
+              tools: tools.map(tool => {
+                const nameMap: Record<string, string> = (state.context as any)?.toolNameMap || {};
+                const sanitized = Object.keys(nameMap).find(k => nameMap[k] === tool.name)
+                  ?? tool.name.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
+                return ({
                   type: 'function',
                   function: {
-                  name: tool.name,
+                  name: sanitized,
                   description: tool.description,
                   parameters: tool.schema,
                   }
-              })),
+                });
+              }),

147-154: Persist selected tool instances and name map for the executor.

Store both the exact set and sanitized→original mapping.

-        const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 });
-        // Persist selection in context so ToolExecutorNode can resolve the same set
-        if (!state.context) { (state as any).context = {}; }
-        (state.context as any).selectedToolNames = selection.selectedNames;
-        const tools = selection.tools;
+        const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 });
+        // Persist selection in context so ToolExecutorNode can resolve the same set
+        if (!state.context) { (state as any).context = {}; }
+        (state.context as any).selectedToolNames = selection.selectedNames;
+        (state.context as any).selectedTools = selection.tools;
+        (state.context as any).toolNameMap = selection.nameMap || {}; // { sanitized: original }
+        const tools = selection.tools;

421-436: Executor: prefer preselected tool instances; resolve sanitized names; key by both names.

-  const selectedNames: string[] | undefined = (state.context as any)?.selectedToolNames;
-  let tools: ReturnType<typeof getTools>;
-  if (selectedNames && selectedNames.length > 0) {
-    const resolved: any[] = [];
-    for (const name of selectedNames) {
-      const inst = ToolRegistry.getRegisteredTool(name as any);
-      if (inst) { resolved.push(inst as any); }
-    }
-    tools = resolved as any;
-  } else {
-    tools = getAgentToolsFromState(state) as any;
-  }
-  const toolMap = new Map<string, ReturnType<typeof getTools>[number]>();
-  (tools as any[]).forEach((tool: any) => toolMap.set(tool.name, tool));
+  const ctx = (state.context as any) || {};
+  const selectedNames: string[] | undefined = ctx.selectedToolNames;
+  const nameMap: Record<string, string> = ctx.toolNameMap || {};
+  let tools: ReturnType<typeof getTools>;
+  if (Array.isArray(ctx.selectedTools) && ctx.selectedTools.length > 0) {
+    tools = ctx.selectedTools as any;
+  } else if (selectedNames && selectedNames.length > 0) {
+    const resolved: any[] = [];
+    for (const name of selectedNames) {
+      const original = nameMap[name] || name;
+      const inst = ToolRegistry.getRegisteredTool(original as any);
+      if (inst) resolved.push(inst as any);
+    }
+    tools = resolved as any;
+  } else {
+    tools = getAgentToolsFromState(state) as any;
+  }
+  const toolMap = new Map<string, ReturnType<typeof getTools>[number]>();
+  (tools as any[]).forEach((tool: any) => {
+    toolMap.set(tool.name, tool); // original
+    for (const [sanitized, original] of Object.entries(nameMap)) {
+      if (original === tool.name) toolMap.set(sanitized, tool); // alias
+    }
+  });
front_end/panels/ai_chat/ui/AIChatPanel.ts (4)

88-91: Fix MCP import order and duplicate import (ESLint blockers).

Move MCP imports into the main import group and combine the two MCPConfig imports.

-// MCP integration
-import { MCPRegistry } from '../mcp/MCPRegistry.js';
-import { getMCPConfig } from '../mcp/MCPConfig.js';
-import { onMCPConfigChange } from '../mcp/MCPConfig.js';
+// (move to main import section with others at top)
+// MCP integration
+import { MCPRegistry } from '../mcp/MCPRegistry.js';
+import { getMCPConfig, onMCPConfigChange } from '../mcp/MCPConfig.js';

722-722: Use unsubscribe handle in teardown to prevent leaks; resolves no-unused-private-class-members.

Store is set at Line 811 but never called; unsubscribe during willHide.

   override willHide(): void {
     // Explicitly remove any event listeners to prevent memory leaks
     if (this.#boundOnMessagesChanged) {
       this.#agentService.removeEventListener(AgentEvents.MESSAGES_CHANGED, this.#boundOnMessagesChanged);
     }
+    if (this.#mcpUnsubscribe) {
+      this.#mcpUnsubscribe();
+      this.#mcpUnsubscribe = null;
+    }
     if (this.#boundOnAgentSessionStarted) this.#agentService.removeEventListener(AgentEvents.AGENT_SESSION_STARTED, this.#boundOnAgentSessionStarted);
     if (this.#boundOnAgentToolStarted) this.#agentService.removeEventListener(AgentEvents.AGENT_TOOL_STARTED, this.#boundOnAgentToolStarted);
     if (this.#boundOnAgentToolCompleted) this.#agentService.removeEventListener(AgentEvents.AGENT_TOOL_COMPLETED, this.#boundOnAgentToolCompleted);
     if (this.#boundOnAgentSessionUpdated) this.#agentService.removeEventListener(AgentEvents.AGENT_SESSION_UPDATED, this.#boundOnAgentSessionUpdated);
     if (this.#boundOnChildAgentStarted) this.#agentService.removeEventListener(AgentEvents.CHILD_AGENT_STARTED, this.#boundOnChildAgentStarted);
   }

Also applies to: 1956-1966


789-813: Add explicit return type and nullish guards in MCP init helper.

Silences explicit-function-return-type and avoids undefined property reads.

   #setupMCPIntegration(): void {
-    const initAndRefresh = async () => {
+    const initAndRefresh = async (): Promise<void> => {
       try {
-        const mcpConfig = getMCPConfig();
+        const mcpConfig = getMCPConfig();
         // Only auto-connect if both enabled and autostart are true
-        if (mcpConfig.enabled && mcpConfig.autostart) {
+        if (mcpConfig?.enabled && mcpConfig?.autostart) {
           await MCPRegistry.init();
           await MCPRegistry.refresh();
           const status = MCPRegistry.getStatus();
           logger.info('MCP autostart completed', status);
         }
       } catch (err) {
         logger.error('Failed to initialize MCP', err);
       }
     };

2331-2336: Gate MCP re-init behind config.enabled to avoid unnecessary work when disabled.

This also aligns with startup behavior.

-    try {
-      await MCPRegistry.init();
-      await MCPRegistry.refresh();
-    } catch (err) {
+    try {
+      const cfg = getMCPConfig();
+      if (cfg?.enabled) {
+        await MCPRegistry.init();
+        await MCPRegistry.refresh();
+      }
+    } catch (err) {
       logger.error('Failed to reinitialize MCP after settings change', err);
-    }
+    }
front_end/panels/ai_chat/mcp/MCPConfig.ts (2)

1-1: Add Chromium BSD license header.

+// Copyright 2025 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.

38-41: Harden toolMode parsing and guard numeric parsing against NaN.

-    const toolMode = (localStorage.getItem(KEYS.toolMode) as MCPConfigData['toolMode']) || 'router';
-    const maxToolsPerTurn = parseInt(localStorage.getItem(KEYS.maxToolsPerTurn) || '20', 10);
-    const maxMcpPerTurn = parseInt(localStorage.getItem(KEYS.maxMcpPerTurn) || '8', 10);
+    const rawMode = localStorage.getItem(KEYS.toolMode);
+    const toolMode: MCPConfigData['toolMode'] =
+      rawMode === 'all' || rawMode === 'router' || rawMode === 'meta' ? rawMode : 'router';
+    const parsePositive = (v: string | null, fallback: number) => {
+      const n = Number.parseInt(v ?? '', 10);
+      return Number.isFinite(n) && n > 0 ? n : fallback;
+    };
+    const maxToolsPerTurn = parsePositive(localStorage.getItem(KEYS.maxToolsPerTurn), 20);
+    const maxMcpPerTurn = parsePositive(localStorage.getItem(KEYS.maxMcpPerTurn), 8);
front_end/panels/ai_chat/mcp/MCPRegistry.ts (2)

1-7: Fix license header and import ordering; use namespace import for third_party.

+// Copyright 2025 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import * as MCPSDK from '../../../third_party/mcp-sdk/mcp-sdk.js';
 import { createLogger } from '../core/Logger.js';
 import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';
-import type { MCPToolDef, MCPServer } from '../../../third_party/mcp-sdk/mcp-sdk.js';
-import { MCPClient } from '../../../third_party/mcp-sdk/mcp-sdk.js';
 import { getMCPConfig } from './MCPConfig.js';
 import { MCPToolAdapter } from './MCPToolAdapter.js';
+
+type MCPToolDef = MCPSDK.MCPToolDef;
+type MCPServer = MCPSDK.MCPServer;

And update client construction:

-  private client = new MCPClient();
+  private client = new MCPSDK.MCPClient();

139-143: Report actual tool counts per server (not 0).

         connected: this.client.isConnected(s.id),
-        toolCount: 0,
+        toolCount: this.registeredTools.filter(name => name.startsWith(`mcp:${s.id}:`)).length,
front_end/panels/ai_chat/mcp/MCPMetaTools.ts (3)

23-27: Schema: allow serverId string | string[] to match runtime.

-      serverId: { type: 'string', description: 'Filter by serverId (or array of ids)' },
+      serverId: {
+        description: 'Filter by serverId (or array of ids)',
+        oneOf: [
+          { type: 'string' },
+          { type: 'array', items: { type: 'string' } }
+        ]
+      },

And for invoke tool keep serverId: string as-is (execution requires a single server id).

Also applies to: 169-176


48-93: Provider-agnostic function-call parsing and preserve selection order.

-      const response = await llm.call({
+      const response = await llm.call({
         provider,
         model,
         messages: [
           { role: 'system', content: systemPrompt },
           { role: 'user', content: userMessage }
         ],
         systemPrompt: systemPrompt,
         // Use standard function-calling tool shape expected by providers
         tools: [{
           type: 'function',
           function: {
             name: 'select_tools',
             description: 'Select the most relevant tools',
             parameters: {
               type: 'object',
               properties: {
                 selected: {
                   type: 'array',
                   items: { type: 'string' },
                   description: 'Array of selected tool keys',
                   maxItems: k
                 },
                 rationale: {
                   type: 'string',
                   description: 'Brief explanation for selections'
                 }
               },
               required: ['selected']
             }
           }
         }]
       });
-
-      if (!response.functionCall) {
-        throw new Error('No function call in LLM response');
-      }
-
-      const functionCall = response.functionCall;
-      const selectedKeys = (functionCall.arguments as any)?.selected || [];
-      
-      // Return candidates in the order selected by LLM, filtering to only selected keys
-      const selectedSet = new Set(selectedKeys);
-      return candidates.filter(c => selectedSet.has(c.key));
+      const parsed = llm.parseResponse(response);
+      if (parsed.type !== 'tool_call' || parsed.name !== 'select_tools') {
+        throw new Error('LLM did not return select_tools function call');
+      }
+      const rawArgs = parsed.args as unknown;
+      const argsObj = typeof rawArgs === 'string' ? JSON.parse(rawArgs) : rawArgs ?? {};
+      const selectedKeys: string[] = Array.isArray((argsObj as any).selected) ? (argsObj as any).selected : [];
+      if (!selectedKeys.length) {
+        return this.rankHeuristic(candidates, query, k);
+      }
+      const byKey = new Map(candidates.map(c => [c.key, c]));
+      const ordered = selectedKeys.map(k2 => byKey.get(k2)).filter((x): x is NonNullable<typeof x> => !!x);
+      return ordered.slice(0, k);

151-155: Wrap hybrid case declarations in a block to satisfy no-case-declarations.

-      case 'hybrid':
-        // For hybrid: use heuristic pre-filtering then LLM ranking on top candidates
-        const prefilterLimit = Math.min(candidates.length, k * 5); // 5x expansion for LLM to choose from
-        const prefiltered = this.rankHeuristic(candidates, args.query, prefilterLimit);
-        rankedTools = prefiltered.length <= k ? prefiltered : await this.rankWithLLM(prefiltered, args.query, k);
-        break;
+      case 'hybrid': {
+        // For hybrid: heuristic pre-filtering then LLM ranking on top candidates
+        const prefilterLimit = Math.min(candidates.length, k * 5);
+        const prefiltered = this.rankHeuristic(candidates, args.query, prefilterLimit);
+        rankedTools = prefiltered.length <= k ? prefiltered : await this.rankWithLLM(prefiltered, args.query, k);
+        break;
+      }
front_end/panels/ai_chat/core/ToolSurfaceProvider.ts (2)

32-56: Respect MCP tool allowlist when building the MCP tool list.

getAllMcpTools() ignores the saved allowlist and can surface disabled tools. Filter by cfg.toolAllowlist (empty set means “allow all”).

 function getAllMcpTools(): Tool<any, any>[] {
   try {
     const status = MCPRegistry.getStatus();
-    console.log('[TOOL_SELECTION_DEBUG] MCPRegistry status:', {
+    logger.debug('[TOOL_SELECTION_DEBUG] MCPRegistry status:', {
       enabled: status.enabled,
       serverCount: status.servers.length,
       servers: status.servers,
       registeredToolNames: status.registeredToolNames,
       lastError: status.lastError,
       lastErrorType: status.lastErrorType
     });
-    
-    const tools: Tool<any, any>[] = [];
+    const cfg = getMCPConfig();
+    const allow = new Set(cfg.toolAllowlist || []);
+    const tools: Tool<any, any>[] = [];
     for (const name of status.registeredToolNames) {
       const tool = ToolRegistry.getRegisteredTool(name);
-      if (tool) {
-        tools.push(tool);
+      if (tool) {
+        if (allow.size === 0 || allow.has(tool.name)) {
+          tools.push(tool);
+        }
       } else {
-        console.log('[TOOL_SELECTION_DEBUG] Tool registered but not found:', name);
+        logger.warn('[TOOL_SELECTION_DEBUG] Tool registered but not found:', name);
       }
     }
-    console.log('[TOOL_SELECTION_DEBUG] getAllMcpTools result:', {
+    logger.debug('[TOOL_SELECTION_DEBUG] getAllMcpTools result:', {
       availableToolsCount: tools.length,
       availableToolNames: tools.map(t => t.name)
     });
     return tools;
   } catch (error) {
-    console.error('[TOOL_SELECTION_DEBUG] Error in getAllMcpTools:', error);
+    logger.error('[TOOL_SELECTION_DEBUG] Error in getAllMcpTools:', error);
     return [];
   }
 }

114-117: Honor config budgets by default; let opts override.

Defaults should come from MCP config when opts is not passed.

-    const { maxToolsPerTurn = 20, maxMcpPerTurn = 8 } = opts || {};
     const cfg = getMCPConfig();
+    const maxToolsPerTurn = opts?.maxToolsPerTurn ?? cfg.maxToolsPerTurn ?? 20;
+    const maxMcpPerTurn = opts?.maxMcpPerTurn ?? cfg.maxMcpPerTurn ?? 8;
     const mode = cfg.toolMode || 'router';
front_end/panels/ai_chat/ui/SettingsDialog.ts (6)

538-548: Remove unused catch parameter.

Silences @typescript-eslint/no-unused-vars.

-    } catch (e) {
+    } catch {
       // Fallback silently to storedProvider
     }

2653-2658: Add explicit return type.

Satisfies explicit-function-return-type.

-    const updateDisconnectButton = () => {
+    const updateDisconnectButton = (): void => {
       const status = MCPRegistry.getStatus();
       const anyConnected = status.enabled && status.servers.some(s => s.connected);
       mcpDisconnectContainer.style.display = anyConnected ? 'block' : 'none';
     };

2831-2845: Annotate updateBudgetControls return type.

Avoids ESLint error; logic unchanged.

-    const updateBudgetControls = () => {
+    const updateBudgetControls = (): void => {
       const maxTools = Math.max(1, Math.min(100, parseInt(mcpMaxToolsInput.value, 10) || 20));
       const maxMcp = Math.max(1, Math.min(50, parseInt(mcpMaxMcpInput.value, 10) || 8));
       setMCPConfig({
         ...getMCPConfig(),
         maxToolsPerTurn: maxTools,
         maxMcpPerTurn: maxMcp,
       });
       onSettingsSaved();
     };

2561-2583: Add braces (curly) and make error formatters return plain text (no HTML).

Preps for safe DOM rendering and fixes lint.

-    const formatTimestamp = (date: Date | undefined): string => {
-      if (!date) return '';
-      return date.toLocaleString();
-    };
+    const formatTimestamp = (date: Date | undefined): string => {
+      if (!date) { return ''; }
+      return date.toLocaleString();
+    };
 
-    const formatMCPError = (error: string, errorType?: string): string => {
-      if (!errorType) return error;
-      
-      switch (errorType) {
+    const formatMCPError = (error: string, errorType?: string): string => {
+      if (!errorType) { return error; }
+      switch (errorType) {
         case 'connection':
-          return `Connection failed: ${error}<br><span style="color: var(--color-text-secondary); font-size: 12px;">Check if the MCP server is running and the endpoint URL is correct.</span>`;
+          return `Connection failed: ${error}. Hint: Check if the MCP server is running and the endpoint URL is correct.`;
         case 'authentication':
-          return `Authentication failed: ${error}<br><span style="color: var(--color-text-secondary); font-size: 12px;">Verify your auth token is correct and has not expired.</span>`;
+          return `Authentication failed: ${error}. Hint: Verify your auth token is correct and has not expired.`;
         case 'configuration':
-          return `Configuration error: ${error}<br><span style="color: var(--color-text-secondary); font-size: 12px;">Check your endpoint URL format (should be ws:// or wss://).</span>`;
+          return `Configuration error: ${error}. Hint: Check your endpoint URL format (should be ws:// or wss://).`;
         case 'network':
-          return `Network error: ${error}<br><span style="color: var(--color-text-secondary); font-size: 12px;">Check your internet connection and firewall settings.</span>`;
+          return `Network error: ${error}. Hint: Check your internet connection and firewall settings.`;
         case 'server_error':
-          return `Server error: ${error}<br><span style="color: var(--color-text-secondary); font-size: 12px;">The MCP server encountered an internal error. Contact the server administrator.</span>`;
+          return `Server error: ${error}. Hint: The MCP server encountered an internal error. Contact the server administrator.`;
         default:
           return error;
       }
     };

2585-2624: Replace innerHTML with safe DOM construction in MCP status (XSS risk).

status.lastError may contain untrusted content. Build nodes and set textContent; also annotate return type.

-    const updateMCPStatus = () => {
+    const updateMCPStatus = (): void => {
       const status = MCPRegistry.getStatus();
       if (!status.enabled) {
         mcpStatusDot.style.backgroundColor = 'var(--color-text-disabled)';
         mcpStatusText.textContent = 'Disabled';
         mcpStatusText.style.color = 'var(--color-text-disabled)';
-        mcpStatusDetails.innerHTML = '';
+        mcpStatusDetails.replaceChildren();
         return;
       }
       const anyConnected = status.servers.some(s => s.connected);
       const toolCount = status.registeredToolNames.length;
       if (anyConnected) {
         mcpStatusDot.style.backgroundColor = 'var(--color-accent-green)';
         mcpStatusText.textContent = `Connected (${toolCount} tools)`;
         mcpStatusText.style.color = 'var(--color-accent-green)';
-        
-        let detailsHtml = '';
-        if (status.lastConnected) {
-          detailsHtml += `Last connected: ${formatTimestamp(status.lastConnected)}<br>`;
-        }
-        if (status.lastError) {
-          detailsHtml += `<span style="color: var(--color-error-text)">${formatMCPError(status.lastError, status.lastErrorType)}</span>`;
-        }
-        mcpStatusDetails.innerHTML = detailsHtml;
+        const nodes: HTMLElement[] = [];
+        if (status.lastConnected) {
+          const line = document.createElement('div');
+          line.textContent = `Last connected: ${formatTimestamp(status.lastConnected)}`;
+          nodes.push(line);
+        }
+        if (status.lastError) {
+          const line = document.createElement('div');
+          line.textContent = formatMCPError(status.lastError, status.lastErrorType);
+          line.style.color = 'var(--color-error-text)';
+          nodes.push(line);
+        }
+        mcpStatusDetails.replaceChildren(...nodes);
       } else {
         mcpStatusDot.style.backgroundColor = 'var(--color-text-disabled)';
         mcpStatusText.textContent = 'Not connected';
         mcpStatusText.style.color = 'var(--color-text-disabled)';
-        
-        let detailsHtml = '';
-        if (status.lastDisconnected) {
-          detailsHtml += `Last disconnected: ${formatTimestamp(status.lastDisconnected)}<br>`;
-        }
-        if (status.lastError) {
-          detailsHtml += `<span style="color: var(--color-error-text)">${formatMCPError(status.lastError, status.lastErrorType)}</span>`;
-        }
-        mcpStatusDetails.innerHTML = detailsHtml;
+        const nodes: HTMLElement[] = [];
+        if (status.lastDisconnected) {
+          const line = document.createElement('div');
+          line.textContent = `Last disconnected: ${formatTimestamp(status.lastDisconnected)}`;
+          nodes.push(line);
+        }
+        if (status.lastError) {
+          const line = document.createElement('div');
+          line.textContent = formatMCPError(status.lastError, status.lastErrorType);
+          line.style.color = 'var(--color-error-text)';
+          nodes.push(line);
+        }
+        mcpStatusDetails.replaceChildren(...nodes);
       }
     };

2865-2915: Avoid innerHTML when updating tools list; read fresh allowlist.

Prevents XSS and stale-config issues.

-    const updateToolsList = () => {
+    const updateToolsList = (): void => {
       const status = MCPRegistry.getStatus();
-      mcpToolsList.innerHTML = '';
+      mcpToolsList.replaceChildren();
@@
-      const currentAllowlist = new Set(currentMCPConfig.toolAllowlist || []);
+      const { toolAllowlist } = getMCPConfig();
+      const currentAllowlist = new Set(toolAllowlist || []);
🧹 Nitpick comments (28)
front_end/panels/ai_chat/ui/ToolCallComponent.ts (1)

193-202: Make the toggle accessible (keyboard + semantics)

The clickable div lacks role/keyboard handling. Add semantics and key support; reflect expanded state.

Example (outside this hunk):

// Template
<div
  class="tool-header"
  role="button"
  tabindex="0"
  aria-expanded=${this.isExpanded}
  @click=${this.toggleExpanded}
  @keydown=${(e: KeyboardEvent) => (e.key === 'Enter' || e.key === ' ') && (e.preventDefault(), this.toggleExpanded())}
>
front_end/third_party/mcp-sdk/ajv/README.md (1)

1-208: Avoid editing vendored README; silence markdownlint for third_party/

markdownlint warnings (MD045, MD001, MD040, MD034) stem from upstream content. Prefer excluding third_party from lint rather than modifying vendor files.

Apply either approach:

Option A (.markdownlintignore at repo root)

+front_end/third_party/**

Option B (markdownlint config)

 {
   "ignores": [
+    "front_end/third_party/**"
   ]
 }
front_end/panels/ai_chat/mcp/MCPClientSDK.test.ts (1)

1-136: Remove debug console logs and use clearer boolean assertions in MCPClientSDK.test.ts

  • Remove or gate debug console.log calls (integration-only) to avoid noisy CI output. Affected lines in front_end/panels/ai_chat/mcp/MCPClientSDK.test.ts: 62, 66, 69, 72, 74, 83, 89, 111, 114, 116, 121, 125.
  • Replace boolean strictEqual checks with clearer assertions: line 29 -> assert.ok(!client.isConnected('non-existent-server')); line 68 -> assert.ok(client.isConnected(HACKER_NEWS_SERVER.id), 'Should be connected').
front_end/panels/ai_chat/core/ToolNameMapping.test.ts (1)

75-80: Prefer strict typed checks; avoid (last as any) and assert.ok

Tighten assertions.

-    const last = result.messages[result.messages.length - 1];
-    assert.strictEqual(last.entity, ChatMessageEntity.TOOL_RESULT);
-    assert.strictEqual((last as any).toolName, sanitizedName);
-    assert.ok((last as any).resultText);
+    const last = result.messages[result.messages.length - 1]!;
+    assert.strictEqual(last.entity, ChatMessageEntity.TOOL_RESULT);
+    assert.strictEqual((last as { toolName: string }).toolName, sanitizedName);
+    assert.isOk((last as { resultText: string }).resultText);
front_end/panels/ai_chat/mcp/MCPToolAdapter.ts (3)

1-3: Add license header + fix import order (value imports before type-only).

Also aligns with ESLint import/order.

+// Copyright 2025 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
-import type { Tool } from '../tools/Tools.js';
-import { createLogger } from '../core/Logger.js';
-import type { MCPClient, MCPToolDef } from '../../../third_party/mcp-sdk/mcp-sdk.js';
+import { createLogger } from '../core/Logger.js';
+import type { Tool } from '../tools/Tools.js';
+import type { MCPClient, MCPToolDef } from '../../../third_party/mcp-sdk/mcp-sdk.js';

10-11: Remove any from schema typing; keep a safe JSON Schema shape.

Prevents no-explicit-any and clarifies intent.

-  schema: any;
+  schema: Record<string, unknown>;

-    const schema = def.inputSchema as any;
+    const schema = def.inputSchema as Record<string, unknown> | undefined;
     if (schema && typeof schema === 'object') {
       this.schema = schema;
     } else {
       this.schema = { type: 'object', properties: {} };
     }

Also applies to: 24-29


42-53: Deep‑redact sensitive fields (nested objects/arrays), case‑insensitive.

Covers headers/structured inputs, not just top-level strings.

-  private sanitize(input: Record<string, unknown>): Record<string, unknown> {
-    const sensitive = ['token', 'api_key', 'password', 'secret', 'authorization'];
-    const out: Record<string, unknown> = {};
-    for (const [k, v] of Object.entries(input || {})) {
-      if (typeof v === 'string' && sensitive.some(s => k.toLowerCase().includes(s))) {
-        out[k] = '[redacted]';
-      } else {
-        out[k] = v;
-      }
-    }
-    return out;
-  }
+  private sanitize(input: unknown): unknown {
+    const sensitive = ['token', 'api_key', 'apikey', 'password', 'secret', 'authorization', 'auth'];
+    const redact = (key: string, val: unknown): unknown => {
+      if (sensitive.some(s => key.toLowerCase().includes(s))) return '[redacted]';
+      if (Array.isArray(val)) return val.map(v => this.sanitize(v));
+      if (val && typeof val === 'object') return this.sanitize(val);
+      return val;
+    };
+    if (!input || typeof input !== 'object') return input;
+    const out: Record<string, unknown> = Array.isArray(input) ? {} : {};
+    const obj = input as Record<string, unknown>;
+    const res: Record<string, unknown> = {};
+    for (const [k, v] of Object.entries(obj)) res[k] = redact(k, v);
+    return res;
+  }
front_end/panels/ai_chat/core/ToolSurfaceProvider.test.ts (2)

85-89: Use typed role constant instead of numeric cast.

-    const state: AgentState = {
-      messages: [{ entity: 0 as any, text: 'please run alpha operation' }],
+    const state: AgentState = {
+      messages: [{ entity: ChatMessageEntity.USER, text: 'please run alpha operation' }],
       context: {},
       selectedAgentType: 'deep-research' as any
     } as any;

95-102: Prefer assert.isOk, assert.lengthOf, assert.isFalse (per repo rules).

-    assert.ok(names.includes('core_tool_A'));
-    assert.ok(names.includes('core_tool_B'));
+    assert.isOk(names.includes('core_tool_A'));
+    assert.isOk(names.includes('core_tool_B'));
-    assert.strictEqual(selectedMcp.length, 2);
+    assert.lengthOf(selectedMcp, 2);
-    assert.ok(selectedMcp.includes('mcp:default:alpha'));
+    assert.isOk(selectedMcp.includes('mcp:default:alpha'));
@@
-    assert.ok(names.includes('core_tool_A'));
-    assert.ok(names.includes('mcp.search'));
-    assert.ok(names.includes('mcp.invoke'));
+    assert.isOk(names.includes('core_tool_A'));
+    assert.isOk(names.includes('mcp.search'));
+    assert.isOk(names.includes('mcp.invoke'));
     // Should NOT include raw MCP tool(s) in meta mode
     assert.isFalse(names.includes('mcp:default:delta'));

Also applies to: 129-135

front_end/panels/ai_chat/core/AgentNodes.test.ts (3)

5-10: Import Chai assert and types; drop custom global assert declarations.

Keeps tests typed and avoids ad-hoc globals.

-import { createToolExecutorNode } from './AgentNodes.js';
-import { ConfigurableAgentTool } from '../agent_framework/ConfigurableAgentTool.js';
-import { ChatMessageEntity } from '../ui/ChatView.js';
-import type { AgentState } from './State.js';
-import type { ConfigurableAgentResult } from '../agent_framework/ConfigurableAgentTool.js';
+import { createToolExecutorNode } from './AgentNodes.js';
+import { ConfigurableAgentTool } from '../agent_framework/ConfigurableAgentTool.js';
+import { ChatMessageEntity } from '../ui/ChatView.js';
+import type { AgentState } from './State.js';
+import type { ConfigurableAgentResult } from '../agent_framework/ConfigurableAgentTool.js';
+import type { ToolResultMessage } from '../models/ChatTypes.js';
+import type { LLMProvider } from '../LLM/LLMTypes.js';
+import { assert } from 'chai';

108-114: Avoid (any) for result message; cast to ToolResultMessage.

-      const toolResultMessage = result.messages[result.messages.length - 1];
+      const toolResultMessage = result.messages[result.messages.length - 1] as ToolResultMessage;
@@
-      const resultText = (toolResultMessage as any).resultText;
+      const resultText = toolResultMessage.resultText;
@@
-      const toolResultMessage = result.messages[result.messages.length - 1];
-      const resultText = (toolResultMessage as any).resultText;
+      const toolResultMessage = result.messages[result.messages.length - 1] as ToolResultMessage;
+      const resultText = toolResultMessage.resultText;

Also applies to: 192-197


123-124: Use assert.isFalse directly.

-      assert.strictEqual(resultText.includes('test-session-123'), false);
+      assert.isFalse(resultText.includes('test-session-123'));
front_end/panels/ai_chat/ui/message/MessageCombiner.ts (2)

32-36: Remove any cast; check uiLane via in-operator.

-    // Skip agent-lane items from chat feed
-    if ((msg as any).uiLane === 'agent') {
+    // Skip agent-lane items from chat feed
+    if ('uiLane' in msg && msg.uiLane === 'agent') {
       continue;
     }

49-55: Remove any cast in next-item check.

-          next && (next as any).uiLane !== 'agent' && next.entity === 'tool_result' &&
+          next && (!('uiLane' in next) || next.uiLane !== 'agent') && next.entity === 'tool_result' &&
front_end/panels/ai_chat/core/AgentNodes.ts (2)

10-13: Deduplicate/merge LLM type imports and fix import order (types before values).

-import type { LLMMessage } from '../LLM/LLMTypes.js';
-import type { LLMProvider } from '../LLM/LLMTypes.js';
-import { createSystemPromptAsync, getAgentToolsFromState } from './GraphHelpers.js';
-import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
+import type { LLMMessage, LLMProvider } from '../LLM/LLMTypes.js';
+import { createSystemPromptAsync, getAgentToolsFromState } from './GraphHelpers.js';

Then move ToolSurfaceProvider below the TracingProvider type import:

 import { createLogger } from './Logger.js';
@@
 import type { TracingProvider } from '../tracing/TracingProvider.js';
+import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';

129-133: Trace actual per-turn selected tools instead of all agent tools.

-            tools: getAgentToolsFromState(state).map(t => t.name),
+            tools: (state.context?.selectedTools ?? getAgentToolsFromState(state)).map(t => t.name),
front_end/panels/ai_chat/ui/ChatView.ts (3)

373-376: Avoid any in lane filter; use a minimal typed cast.

-      // Lane filter: hide agent-lane items from the main chat feed
-      if ((message as any).uiLane === 'agent') {
+      // Lane filter: hide agent-lane items from the main chat feed
+      if ((message as { uiLane?: 'agent' | 'chat' }).uiLane === 'agent') {
         return html``;
       }

495-503: Lit attribute bindings: drop quotes around interpolations.

-                          <span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}">●</span>
+                          <span class="tool-status-marker ${status}" title=${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}>●</span>
@@
-                        <span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}">●</span>
+                        <span class="tool-status-marker ${status}" title=${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}>●</span>

944-962: Prefer state-driven toggling over direct DOM mutation.

Keep expanded state in component state (e.g., Set of expanded toolCallIds) and render conditionally instead of querying/modifying DOM. This avoids desync with Lit re-renders and improves testability.

Happy to sketch a minimal Set-based approach keyed by toolCallId if you want it in this PR.

front_end/panels/ai_chat/mcp/MCPMetaTools.ts (1)

5-11: Reorder imports to satisfy import/order (values before types).

-import type { Tool } from '../tools/Tools.js';
-import { createLogger } from '../core/Logger.js';
+import { createLogger } from '../core/Logger.js';
 import { MCPRegistry } from './MCPRegistry.js';
 import { MCPToolAdapter } from './MCPToolAdapter.js';
 import { LLMClient } from '../LLM/LLMClient.js';
 import { AIChatPanel } from '../ui/AIChatPanel.js';
+import type { Tool } from '../tools/Tools.js';
front_end/panels/ai_chat/core/ToolSurfaceProvider.ts (7)

164-177: Meta mode should also respect the allowlist.

If users restricted tools, reflect that for meta-tools as well.

-      const metaTools = [search, invoke].filter(Boolean) as Tool<any, any>[];
+      const { toolAllowlist = [] } = getMCPConfig();
+      const allow = new Set(toolAllowlist);
+      const metaTools = [search, invoke]
+        .filter(Boolean)
+        .filter(t => allow.size === 0 || allow.has((t as Tool<any, any>).name)) as Tool<any, any>[];

5-11: Fix import order to satisfy import/order.

Place value imports first, keep MCPConfig before MCPRegistry, and move type-only imports after values.

-import type { AgentState } from './State.js';
-import { createLogger } from './Logger.js';
-import type { Tool } from '../tools/Tools.js';
-import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';
-import { MCPRegistry } from '../mcp/MCPRegistry.js';
-import { getMCPConfig } from '../mcp/MCPConfig.js';
-import { MCPToolAdapter } from '../mcp/MCPToolAdapter.js';
+import { createLogger } from './Logger.js';
+import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';
+import { getMCPConfig } from '../mcp/MCPConfig.js';
+import { MCPRegistry } from '../mcp/MCPRegistry.js';
+import { MCPToolAdapter } from '../mcp/MCPToolAdapter.js';
+import type { Tool } from '../tools/Tools.js';
+import type { AgentState } from './State.js';

20-30: Use Array and avoid explicit any in generics.

Aligns with ESLint rules and improves typing.

-function uniqByName(tools: Tool<any, any>[]): Tool<any, any>[] {
-  const seen = new Set<string>();
-  const out: Tool<any, any>[] = [];
+function uniqByName(tools: Array<Tool<unknown, unknown>>): Array<Tool<unknown, unknown>> {
+  const seen = new Set<string>();
+  const out: Array<Tool<unknown, unknown>> = [];

32-34: Apply Array and logger usage consistently in this function.

Cleans up types and removes raw console usage.

-function getAllMcpTools(): Tool<any, any>[] {
+function getAllMcpTools(): Array<Tool<unknown, unknown>> {
@@
-    const tools: Tool<any, any>[] = [];
+    const tools: Array<Tool<unknown, unknown>> = [];
@@
-    return [];
+    return [];

Also applies to: 44-46, 57-62


64-76: Add braces to single-line ifs and tighten types.

Satisfies curly and avoids any in Tool type.

-function scoreTool(query: string, agentType: string | null | undefined, tool: Tool<any, any>): number {
+function scoreTool(query: string, agentType: string | null | undefined, tool: Tool<unknown, unknown>): number {
   const q = (query || '').toLowerCase();
   const a = (agentType || '').toLowerCase();
   const name = (tool.name || '').toLowerCase();
   const desc = (tool.description || '').toLowerCase();
   let score = 0;
-  if (q && name.includes(q)) score += 10;
-  if (q && desc.includes(q)) score += 3;
-  if (a && name.includes(a)) score += 2;
-  if (a && desc.includes(a)) score += 1;
+  if (q && name.includes(q)) { score += 10; }
+  if (q && desc.includes(q)) { score += 3; }
+  if (a && name.includes(a)) { score += 2; }
+  if (a && desc.includes(a)) { score += 1; }
   // Prefer MCP tools only slightly lower than strong name matches
-  if (tool instanceof MCPToolAdapter) score += 0.5;
+  if (tool instanceof MCPToolAdapter) { score += 0.5; }
   return score;
 }

79-110: Avoid any on globalThis and use logger instead of console.

Keeps dev debug hooks while satisfying lint rules.

-// DEBUG: Add a utility function to test MCP modes from console
-(globalThis as any).debugToolSelection = {
+// DEBUG: Add a utility function to test MCP modes from console
+(globalThis as unknown as { debugToolSelection: unknown }).debugToolSelection = {
   getCurrentMCPConfig: () => {
     const cfg = getMCPConfig();
-    console.log('Current MCP Config:', cfg);
+    logger.debug('Current MCP Config:', cfg);
     return cfg;
   },
   testMode: async (mode: 'all' | 'router' | 'meta') => {
-    const originalConfig = getMCPConfig();
-    console.log(`Testing mode: ${mode}`);
+    const originalConfig = getMCPConfig();
+    logger.debug(`Testing mode: ${mode}`);
     // Temporarily set the mode
     localStorage.setItem('ai_chat_mcp_tool_mode', mode);
@@
-    const result = await ToolSurfaceProvider.select(mockState, mockBaseTools);
+    const result = await ToolSurfaceProvider.select(mockState, mockBaseTools);
     // Restore original mode
-    if (originalConfig.toolMode) {
-      localStorage.setItem('ai_chat_mcp_tool_mode', originalConfig.toolMode);
-    }
-    console.log(`Mode ${mode} result:`, result);
+    if (originalConfig.toolMode) {
+      localStorage.setItem('ai_chat_mcp_tool_mode', originalConfig.toolMode);
+    } else {
+      localStorage.removeItem('ai_chat_mcp_tool_mode');
+    }
+    logger.debug(`Mode ${mode} result:`, result);
     return result;
   },
   getMCPRegistryStatus: () => {
     const status = MCPRegistry.getStatus();
-    console.log('MCP Registry Status:', status);
+    logger.debug('MCP Registry Status:', status);
     return status;
   }
 };

112-118: Replace raw console logging with the project logger; remove unused local selectedNames.

Eliminates lint errors and keeps consistent logging. selectedNames var is unused.

 export const ToolSurfaceProvider = {
-  async select(state: AgentState, baseTools: Tool<any, any>[], opts?: ToolSelectionOptions): Promise<{ tools: Tool<any, any>[]; selectedNames: string[] }> {
+  async select(
+    state: AgentState,
+    baseTools: Array<Tool<unknown, unknown>>,
+    opts?: ToolSelectionOptions
+  ): Promise<{ tools: Array<Tool<unknown, unknown>>; selectedNames: string[] }> {
@@
-    console.log('[TOOL_SELECTION_DEBUG] ToolSurfaceProvider.select called with:', {
+    logger.debug('[TOOL_SELECTION_DEBUG] ToolSurfaceProvider.select called with:', {
@@
-    let resultTools: Tool<any, any>[] = uniqByName([...baseTools]);
-    const selectedNames: string[] = [];
+    let resultTools: Array<Tool<unknown, unknown>> = uniqByName([...baseTools]);
@@
-    console.log('[TOOL_SELECTION_DEBUG] Base tools provided:', {
+    logger.debug('[TOOL_SELECTION_DEBUG] Base tools provided:', {
@@
-      console.log('[TOOL_SELECTION_DEBUG] MCP disabled, returning core tools only');
+      logger.debug('[TOOL_SELECTION_DEBUG] MCP disabled, returning core tools only');
       const uniq = uniqByName(resultTools).slice(0, maxToolsPerTurn);
-      console.log('[TOOL_SELECTION_DEBUG] Final result (MCP disabled):', {
+      logger.debug('[TOOL_SELECTION_DEBUG] Final result (MCP disabled):', {
@@
-      console.log('[TOOL_SELECTION_DEBUG] Using ALL mode');
+      logger.debug('[TOOL_SELECTION_DEBUG] Using ALL mode');
       const mcpTools = getAllMcpTools();
-      console.log('[TOOL_SELECTION_DEBUG] MCP tools found:', {
+      logger.debug('[TOOL_SELECTION_DEBUG] MCP tools found:', {
@@
-      console.log('[TOOL_SELECTION_DEBUG] Final result (ALL mode):', {
+      logger.debug('[TOOL_SELECTION_DEBUG] Final result (ALL mode):', {
@@
-      console.log('[TOOL_SELECTION_DEBUG] Using META mode');
+      logger.debug('[TOOL_SELECTION_DEBUG] Using META mode');
@@
-      console.log('[TOOL_SELECTION_DEBUG] Meta tools found:', {
+      logger.debug('[TOOL_SELECTION_DEBUG] Meta tools found:', {
@@
-      console.log('[TOOL_SELECTION_DEBUG] Final result (META mode):', {
+      logger.debug('[TOOL_SELECTION_DEBUG] Final result (META mode):', {
@@
-    console.log('[TOOL_SELECTION_DEBUG] Using ROUTER mode');
+    logger.debug('[TOOL_SELECTION_DEBUG] Using ROUTER mode');
@@
-    console.log('[TOOL_SELECTION_DEBUG] MCP tools available for scoring:', {
+    logger.debug('[TOOL_SELECTION_DEBUG] MCP tools available for scoring:', {
@@
-    const lastUserMsg = [...state.messages].reverse().find(m => m.entity === 'user' || (m as any).entity === 0) as any;
+    const lastUserMsg = [...(state.messages ?? [])]
+      .reverse()
+      .find((m: any) => m.entity === 'user' || m.entity === 0) as any;
     const queryText = lastUserMsg?.text || '';
-    console.log('[TOOL_SELECTION_DEBUG] Query text for scoring:', queryText);
+    logger.debug('[TOOL_SELECTION_DEBUG] Query text for scoring:', queryText);
@@
-    console.log('[TOOL_SELECTION_DEBUG] Top scored MCP tools:', {
+    logger.debug('[TOOL_SELECTION_DEBUG] Top scored MCP tools:', {
@@
-    console.log('[TOOL_SELECTION_DEBUG] Final result (ROUTER mode):', {
+    logger.debug('[TOOL_SELECTION_DEBUG] Final result (ROUTER mode):', {

Also applies to: 129-147, 149-162, 164-181, 184-201, 208-215

front_end/panels/ai_chat/ui/SettingsDialog.ts (1)

1306-1310: Localize advanced hint through i18n or keep as-is?

This hard-coded English string mixes with i18n strings. Consider moving it to UIStrings for consistency.

Comment on lines +99 to +104
// Create ToolExecutorNode
const toolExecutorNode = createToolExecutorNode(stateWithMockTool);

// Execute the node
const result = await toolExecutorNode.invoke(stateWithMockTool);

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

createToolExecutorNode signature updated — pass provider and model.

Current test calls will fail to compile.

-      const toolExecutorNode = createToolExecutorNode(stateWithMockTool);
+      const toolExecutorNode = createToolExecutorNode(
+        stateWithMockTool,
+        'test' as unknown as LLMProvider,
+        'test-model'
+      );
@@
-      const toolExecutorNode = createToolExecutorNode(stateWithMockTool);
+      const toolExecutorNode = createToolExecutorNode(
+        stateWithMockTool,
+        'test' as unknown as LLMProvider,
+        'test-model'
+      );

Also applies to: 189-191

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/AgentNodes.test.ts around lines 99-104 (and
similarly at 189-191), the test calls createToolExecutorNode with a single
argument but the function signature now requires provider and model parameters;
update the test to pass the provider and model when creating the node (e.g.,
extract or mock provider and model from stateWithMockTool or create small mock
values) so the call matches the new signature and the test compiles.

Comment on lines +121 to +122
const q = (args.query || '').toLowerCase();

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove unused variable and add braces for single-line ifs (curly).

-    const q = (args.query || '').toLowerCase();
+    // no-op
@@
-      if (!key.startsWith('mcp:')) continue;
+      if (!key.startsWith('mcp:')) { continue; }
@@
-      if (serverFilter) {
+      if (serverFilter) {
         const serverId = key.split(':')[1] || '';
-        if (!serverFilter.has(serverId)) continue;
+        if (!serverFilter.has(serverId)) { continue; }
       }
@@
-      if (!tool) continue;
+      if (!tool) { continue; }

Also applies to: 127-134

🧰 Tools
🪛 ESLint

[error] 121-121: 'q' is assigned a value but never used.

(@typescript-eslint/no-unused-vars)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/mcp/MCPMetaTools.ts around lines 121-122 (and
similarly 127-134), remove the unused variable declaration "const q =
(args.query || '').toLowerCase();" and update any single-line if statements to
use braces; if code later referenced the lowercased query, inline the value
where needed as (args.query || '').toLowerCase() or create a used variable with
a descriptive name, otherwise delete the unused declaration, and add { ... }
around all single-line if bodies in the indicated ranges to enforce consistent
curly-brace style.

@claude
Copy link

claude bot commented Sep 13, 2025

Claude finished @tysonthomas9's task —— View job


MCP Integration PR Review

I've conducted a comprehensive review of this MCP integration PR. Here's my detailed feedback:

Review Tasks:

  • Analyze core MCP integration architecture
  • Review security concerns (token storage, XSS vulnerabilities)
  • Assess performance implications (bundle size, tool selection)
  • Examine test coverage and quality
  • Check code quality and best practices
  • Review UI integration patterns
  • Evaluate error handling and edge cases
  • Provide comprehensive feedback and recommendations

🔴 Critical Security Issues

1. Token Storage Vulnerability (HIGH)

File: front_end/panels/ai_chat/mcp/MCPConfig.ts:54-55

localStorage.setItem(KEYS.token, config.token);

Issue: Storing authentication tokens in localStorage creates a high XSS vulnerability. Malicious scripts can access these tokens indefinitely.
Fix: Switch to sessionStorage (session-scoped) or implement secure token storage using browser APIs.

2. Tool Argument Redaction Bug (CRITICAL)

File: front_end/panels/ai_chat/mcp/MCPToolAdapter.ts:32-35

const sanitized = this.sanitize(args);
return this.client.callTool(this.serverId, this.def.name, sanitized, { timeoutMs: 30000 });

Issue: The code redacts secrets from arguments then sends the redacted version to MCP server, breaking authentication for any tool requiring API keys/tokens.
Fix: Use original args for the actual call, only use sanitized for logging.

3. XSS Risk in Settings Dialog (MEDIUM)

File: front_end/panels/ai_chat/ui/SettingsDialog.ts:2533-2547

mcpStatusDetails.innerHTML = detailsHtml;

Issue: Using innerHTML with unsanitized server error messages could inject malicious HTML/JS.
Fix: Use DOM text nodes and textContent instead of innerHTML.

🔴 Critical Functionality Issues

4. LLM Function Name Compatibility (BLOCKER)

File: front_end/panels/ai_chat/core/AgentNodes.ts:171-177

tools: tools.map(tool => ({
  function: {
    name: tool.name, // Names like "mcp:default:alpha" will be rejected by OpenAI
    description: tool.description,
    parameters: tool.schema,
  }
}))

Issue: MCP tool names containing colons/dots (e.g., mcp:default:alpha) violate LLM provider naming requirements (^[a-zA-Z0-9_-]{1,64}$). This will cause API failures.
Fix: Sanitize tool names for LLM calls and maintain a mapping for tool resolution.

5. Integration Test CI Failure (HIGH)

File: front_end/panels/ai_chat/mcp/MCPClientSDK.test.ts:53-60

describe('connects to local Hacker News MCP server via SDK', function() {
  const HACKER_NEWS_SERVER: MCPServer = {
    endpoint: 'http://localhost:5001/sse',
  };

Issue: This integration test will fail/hang in CI where no local MCP server runs.
Fix: Skip by default or gate behind environment variable.

⚠️ Architecture & Design Concerns

6. Bundle Size Impact (MEDIUM)

  • Impact: 13MB, 267 JS files for MCP SDK
  • Analysis: Significant size increase for a feature that may have limited initial adoption
  • Recommendation: Consider lazy loading or feature flagging

7. Error Handling Patterns (MEDIUM)

  • Missing: Robust retry logic for MCP connections
  • Missing: Graceful degradation when MCP servers become unavailable
  • Missing: Circuit breaker pattern for failing MCP endpoints

8. Type Safety Issues (MEDIUM)

Multiple uses of any type throughout the codebase reduce type safety:

  • front_end/panels/ai_chat/mcp/MCPToolAdapter.ts:10 - schema: any
  • front_end/panels/ai_chat/core/AgentNodes.ts:149 - Various any casts
  • Missing proper TypeScript definitions for MCP SDK integration

Positive Aspects

Well-Designed Features

  • Clean Configuration API: MCP config management is well-structured
  • Tool Surface Provider: Smart tool selection logic with multiple modes (all/router/meta)
  • Event-Driven Architecture: Proper use of CustomEvents for config changes
  • Logging Infrastructure: Comprehensive logging throughout MCP integration

Good Engineering Practices

  • Separation of Concerns: Clean boundaries between MCP registry, adapters, and UI
  • Error Categorization: Thoughtful error classification in MCPRegistry
  • Test Structure: Good test organization with unit and integration separation

📋 Test Coverage Assessment

Adequate Coverage:

  • Basic SDK instantiation and connection management
  • Error scenarios for missing servers
  • Tool selection logic

Missing Coverage:

  • MCP server authentication flows
  • Tool name sanitization and mapping
  • Error recovery and retry scenarios
  • UI integration testing
  • Performance testing with large tool sets

🚀 Performance Considerations

Concerns:

  • Bundle Impact: 13MB additional payload
  • Tool Discovery: No caching of MCP tool metadata
  • Memory Usage: Tool registry may grow unbounded

Recommendations:

  • Implement tool metadata caching
  • Add lazy loading for MCP functionality
  • Consider tool registry cleanup strategies

📝 Recommendations

Must Fix Before Merge:

  1. Fix token storage security vulnerability
  2. Fix tool argument redaction bug
  3. Implement LLM-compatible tool name sanitization
  4. Gate integration tests behind environment flag
  5. Replace innerHTML with safe DOM operations

Should Address:

  1. Add comprehensive error handling and retry logic
  2. Improve type safety throughout
  3. Add performance monitoring
  4. Implement feature flagging for gradual rollout

Consider for Future:

  1. Bundle size optimization
  2. Tool metadata caching
  3. Advanced MCP server management UI
  4. Integration with existing Browser Operator tool ecosystem

Overall Assessment

This PR represents a significant and well-architected addition to the Browser Operator platform. The MCP integration design is thoughtful and the implementation follows good patterns. However, there are several critical security and functionality issues that must be addressed before merge.

The code quality is generally good with proper separation of concerns and comprehensive logging. The test coverage is adequate for initial implementation but should be expanded for production readiness.

Recommendation: Request Changes - Address the critical security and functionality issues, then this will be ready for merge.


Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
front_end/panels/ai_chat/ui/__tests__/SettingsDialogOpenRouterCache.test.ts (1)

35-45: Restore mocked globals to avoid cross-test leakage.

Capture originals and restore in afterEach for both LLMClient.fetchOpenRouterModels and window.localStorage to keep tests isolated.

Apply:

@@
   // Mock LLMClient.fetchOpenRouterModels
-  fetchOpenRouterModelsCalls = [];
-  LLMClient.fetchOpenRouterModels = async (apiKey: string) => {
+  fetchOpenRouterModelsCalls = [];
+  const originalFetchOpenRouterModels = LLMClient.fetchOpenRouterModels;
+  LLMClient.fetchOpenRouterModels = async (apiKey: string) => {
     fetchOpenRouterModelsCalls.push({ apiKey, timestamp: mockCurrentTime });
     return [
       { id: 'openai/gpt-4', name: 'GPT-4' },
       { id: 'anthropic/claude-3-sonnet', name: 'Claude 3 Sonnet' },
       { id: 'meta-llama/llama-2-70b-chat', name: 'Llama 2 70B' },
     ];
   };
@@
-  // Restore original Date.now
-  Date.now = originalDateNow;
-  mockLocalStorage.clear();
-  fetchOpenRouterModelsCalls = [];
+  // Restore originals to avoid leakage across tests
+  Date.now = originalDateNow;
+  fetchOpenRouterModelsCalls = [];
+  try { LLMClient.fetchOpenRouterModels = originalFetchOpenRouterModels; } catch {}
+  try {
+    // Restore localStorage to a clean stubbed object
+    Object.defineProperty(window, 'localStorage', {
+      value: { getItem: () => null, setItem: () => {}, removeItem: () => {}, clear: () => {} },
+      writable: true,
+    });
+  } catch {}
♻️ Duplicate comments (7)
front_end/panels/ai_chat/ui/AIChatPanel.ts (4)

88-91: Move and dedupe MCP imports (lint: import/first, import/order, no-duplicates, no-unused-vars)

Place MCP imports with the main import group and combine duplicates. Also remove the unused getMCPConfig if you gate below (see next comment).

-// MCP integration
-import { MCPRegistry } from '../mcp/MCPRegistry.js';
-import { getMCPConfig } from '../mcp/MCPConfig.js';
-import { onMCPConfigChange } from '../mcp/MCPConfig.js';
+// MCP integration
+import { MCPRegistry } from '../mcp/MCPRegistry.js';
+import { getMCPConfig, onMCPConfigChange } from '../mcp/MCPConfig.js';

Then move this block up with the other imports (before chatViewStyles).


722-722: Unsubscribe on teardown to prevent leaks (and satisfy no-unused-private-class-members)

#mcpUnsubscribe is assigned but never invoked. Call it in willHide().

   override willHide(): void {
     // Explicitly remove any event listeners to prevent memory leaks
+    if (this.#mcpUnsubscribe) {
+      this.#mcpUnsubscribe();
+      this.#mcpUnsubscribe = null;
+    }

789-809: Gate MCP init/refresh behind config and add explicit return type

Avoid unconditional connect; respect user settings (e.g., enabled, autostart). Also add the missing return type for the inner async function (lint).

-  #setupMCPIntegration(): void {
-    const initAndRefresh = async () => {
+  #setupMCPIntegration(): void {
+    const initAndRefresh = async (): Promise<void> => {
       try {
-        // Always attempt to connect to MCP on startup
-        await MCPRegistry.init();
-        await MCPRegistry.refresh();
+        const cfg = getMCPConfig?.();
+        if (cfg?.enabled && cfg?.autostart) {
+          await MCPRegistry.init();
+          await MCPRegistry.refresh();
+        }
         const status = MCPRegistry.getStatus();
         logger.info('MCP auto-connect completed', status);
       } catch (err) {
         logger.error('Failed to initialize MCP', err);
       }
     };
 
     void initAndRefresh();
     // Subscribe to config changes
     this.#mcpUnsubscribe = onMCPConfigChange(() => { void initAndRefresh(); });
   }

2329-2334: Also gate MCP re-init after settings changes

Skip MCP init/refresh when disabled.

-    try {
-      await MCPRegistry.init();
-      await MCPRegistry.refresh();
-    } catch (err) {
+    try {
+      const cfg = getMCPConfig?.();
+      if (cfg?.enabled) {
+        await MCPRegistry.init();
+        await MCPRegistry.refresh();
+      }
+    } catch (err) {
       logger.error('Failed to reinitialize MCP after settings change', err);
-    }
+    }
front_end/panels/ai_chat/ui/SettingsDialog.ts (3)

2658-2663: Add explicit return type (ESLint: explicit-function-return-type)


2816-2827: Add explicit return type and clamp logic remains unchanged


2850-2859: Use safe clearing and read fresh config for allowlist

🧹 Nitpick comments (14)
front_end/panels/ai_chat/ui/message/MessageList.ts (1)

68-68: Use reserved-space variable for bottom padding to avoid input overlay overlap

Hard-coding padding-bottom: 12px can let the floating input bar cover the last message. Prefer the same variable used in chatView.css so spacing stays consistent.

-          padding-bottom: 12px;
+          padding-bottom: var(--ai-input-bar-reserved-space, 12px);
front_end/panels/ai_chat/ui/__tests__/LiveAgentSessionComponent.test.ts (1)

79-89: Assert computed display, not inline style

Checking element.style.display is brittle (it only reflects inline styles). Use getComputedStyle for robustness.

-    // Default expanded
-    assert.strictEqual(timeline.style.display, 'block');
+    // Default expanded
+    assert.strictEqual(getComputedStyle(timeline).display, 'block');
@@
-    assert.strictEqual((el.shadowRoot!.querySelector('.timeline-items') as HTMLElement).style.display, 'none');
+    assert.strictEqual(getComputedStyle(el.shadowRoot!.querySelector('.timeline-items') as HTMLElement).display, 'none');
@@
-    assert.strictEqual((el.shadowRoot!.querySelector('.timeline-items') as HTMLElement).style.display, 'block');
+    assert.strictEqual(getComputedStyle(el.shadowRoot!.querySelector('.timeline-items') as HTMLElement).display, 'block');
front_end/panels/ai_chat/ui/chatView.css (3)

14-15: Surface the reserved-space variable

Good addition. Consider documenting expected input bar heights and where this variable should be overridden (e.g., via container or theme) to avoid drift.


46-53: z-index of overlay vs timeline/status markers

The input bar is z-index: 5, but some UI markers use far higher z-indices, which can pop above the bar. If intentional, ignore; otherwise raise the input bar's z-index or reduce local marker z-indices to avoid visual overlap near the bottom.


103-107: Avoid magic number: align expanded padding with the variable

Use the same variable as the legacy container to keep spacing consistent across views.

-.chat-view-container.expanded-view {
-  padding-bottom: 150px; /* tune if input height changes */
-}
+.chat-view-container.expanded-view {
+  padding-bottom: var(--ai-input-bar-reserved-space);
+}
front_end/panels/ai_chat/ui/AIChatPanel.ts (1)

2086-2088: Remove unused HelpDialog import or fallback to dialog when pop-up blocking occurs

Since Help now opens docs in a new tab, HelpDialog is likely unused. Remove the import to satisfy lint. Alternatively, catch failures and open the dialog as a fallback. Example (import removal):

- import { HelpDialog } from './HelpDialog.js';
front_end/panels/ai_chat/ui/ChatView.ts (1)

945-963: Avoid direct DOM toggling for state; store expand/collapse in component state

Directly modifying style.display is fragile. Track expansion in class state and render conditionally so the state survives re-renders.

I can provide a patch using #expandedToolCalls = new Set<string>() keyed by toolCallId.

front_end/panels/ai_chat/ui/__tests__/ChatViewAgentSessions.test.ts (2)

95-111: Top-level promotion test: great coverage; tiny flake guard

You already added doubleRaf(). Consider asserting with a small retry loop if this still flakes in CI.


413-424: Remove unused variable

before isn’t used.

-    // Capture initial state, then toggle once
-    const before = (sroot.querySelector('.timeline-items') as HTMLElement).style.display;
+    // Toggle once
config/gni/devtools_grd_files.gni (2)

869-871: Keep third_party entries alphabetically ordered (place marked before mcp-sdk).

Current order has mcp-sdk before marked. Swap to retain strict alpha ordering within the third_party block.

Apply this diff:

-  "front_end/third_party/mcp-sdk/mcp-sdk.js",
-  "front_end/third_party/marked/marked.js",
+  "front_end/third_party/marked/marked.js",
+  "front_end/third_party/mcp-sdk/mcp-sdk.js",

2212-2221: Trim duplicate Zod entrypoint — keep index.mjs

front_end/third_party/mcp-sdk/package/dist/types.js imports "../../zod/lib/index.mjs"; remove the unused index.js entry from the vendored lists to avoid bundling an unused entrypoint.

Locations: config/gni/devtools_grd_files.gni (lines ~2220–2221), front_end/third_party/mcp-sdk/BUILD.gn (lines ~23–24).

-  "front_end/third_party/mcp-sdk/zod/lib/index.js",
   "front_end/third_party/mcp-sdk/zod/lib/index.mjs",
front_end/panels/ai_chat/ui/__tests__/SettingsDialogOpenRouterCache.test.ts (1)

115-141: Optional: exercise the real auto-refresh path once integration lands.

When the dialog implements auto-refresh, add an integration-style test that opens the selector (emits model-selector-focus) and asserts a single fetch when cache is stale.

front_end/panels/ai_chat/ui/model_selector/ModelSelector.ts (1)

86-90: Optional a11y polish for the trigger.

Consider adding aria-haspopup="listbox" and aria-expanded=${this.#open} to the trigger button for better accessibility.

-        <button class="model-select-trigger" @click=${this.#toggle} ?disabled=${this.#disabled}>
+        <button class="model-select-trigger"
+                @click=${this.#toggle}
+                ?disabled=${this.#disabled}
+                aria-haspopup="listbox"
+                aria-expanded=${String(this.#open)}>
front_end/panels/ai_chat/ui/SettingsDialog.ts (1)

448-456: Consider replacing any types with proper interfaces

The static model selector references use any types. Consider defining proper interfaces for type safety:

-  static #openaiMiniModelSelect: any | null = null;
-  static #openaiNanoModelSelect: any | null = null;
+  static #openaiMiniModelSelect: HTMLElement | null = null;
+  static #openaiNanoModelSelect: HTMLElement | null = null;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 101249f and e6e46cf.

📒 Files selected for processing (14)
  • config/gni/devtools_grd_files.gni (5 hunks)
  • front_end/panels/ai_chat/ui/AIChatPanel.ts (6 hunks)
  • front_end/panels/ai_chat/ui/ChatView.ts (7 hunks)
  • front_end/panels/ai_chat/ui/HelpDialog.ts (2 hunks)
  • front_end/panels/ai_chat/ui/SettingsDialog.ts (16 hunks)
  • front_end/panels/ai_chat/ui/__tests__/ChatViewAgentSessions.test.ts (7 hunks)
  • front_end/panels/ai_chat/ui/__tests__/LiveAgentSessionComponent.test.ts (1 hunks)
  • front_end/panels/ai_chat/ui/__tests__/SettingsDialogOpenRouterCache.test.ts (3 hunks)
  • front_end/panels/ai_chat/ui/chatView.css (5 hunks)
  • front_end/panels/ai_chat/ui/input/InputBar.ts (1 hunks)
  • front_end/panels/ai_chat/ui/message/MessageList.ts (1 hunks)
  • front_end/panels/ai_chat/ui/message/StructuredResponseRender.ts (1 hunks)
  • front_end/panels/ai_chat/ui/message/__tests__/MessageCombiner.test.ts (1 hunks)
  • front_end/panels/ai_chat/ui/model_selector/ModelSelector.ts (3 hunks)
✅ Files skipped from review due to trivial changes (1)
  • front_end/panels/ai_chat/ui/message/StructuredResponseRender.ts
🧰 Additional context used
🧬 Code graph analysis (6)
front_end/panels/ai_chat/ui/message/__tests__/MessageCombiner.test.ts (2)
front_end/panels/ai_chat/models/ChatTypes.ts (1)
  • ChatMessage (74-75)
front_end/panels/ai_chat/ui/message/MessageCombiner.ts (1)
  • combineMessages (26-103)
front_end/panels/ai_chat/ui/model_selector/ModelSelector.ts (1)
front_end/panels/ai_chat/ui/AIChatPanel.ts (1)
  • ModelOption (96-100)
front_end/panels/ai_chat/ui/__tests__/SettingsDialogOpenRouterCache.test.ts (1)
front_end/panels/ai_chat/ui/AIChatPanel.ts (4)
  • window (1047-1094)
  • apiKey (1166-1197)
  • apiKey (1239-1258)
  • apiKey (1578-1612)
front_end/panels/ai_chat/ui/__tests__/ChatViewAgentSessions.test.ts (4)
front_end/testing/DOMHelpers.ts (1)
  • doubleRaf (257-259)
front_end/panels/ai_chat/ui/ChatView.ts (1)
  • session (315-328)
front_end/panels/ai_chat/core/AgentService.ts (1)
  • session (660-700)
front_end/panels/ai_chat/ui/LiveAgentSessionComponent.ts (2)
  • session (35-35)
  • session (36-36)
front_end/panels/ai_chat/ui/SettingsDialog.ts (3)
front_end/panels/ai_chat/ui/AIChatPanel.ts (11)
  • localStorage (814-837)
  • currentProvider (875-935)
  • currentProvider (2342-2401)
  • SettingsDialog (2093-2107)
  • ModelOption (96-100)
  • error (1930-1943)
  • logger (949-1042)
  • logger (1333-1403)
  • logger (1426-1453)
  • logger (1560-1573)
  • logger (1617-1620)
front_end/panels/ai_chat/ui/input/InputBar.ts (2)
  • modelOptions (35-35)
  • selectedModel (36-36)
front_end/panels/ai_chat/ui/model_selector/ModelSelector.ts (2)
  • ModelOption (10-10)
  • ModelOption (62-66)
front_end/panels/ai_chat/ui/ChatView.ts (3)
front_end/panels/ai_chat/ui/AIChatPanel.ts (8)
  • data (1529-1540)
  • data (2006-2041)
  • event (1671-1678)
  • event (1683-1687)
  • event (1692-1696)
  • event (1701-1705)
  • event (1710-1714)
  • event (1719-1723)
front_end/panels/ai_chat/ui/markdown/MarkdownRenderers.ts (1)
  • renderMarkdown (63-81)
front_end/panels/ai_chat/core/AgentService.ts (1)
  • event (621-657)
🪛 ESLint
front_end/panels/ai_chat/ui/message/__tests__/MessageCombiner.test.ts

[error] 40-40: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 42-42: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 43-43: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 44-44: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 50-50: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 51-51: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 57-57: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 59-59: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 61-61: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

front_end/panels/ai_chat/ui/model_selector/ModelSelector.ts

[error] 42-42: Missing return type on function.

(@typescript-eslint/explicit-function-return-type)


[error] 44-44: Expected { after 'if' condition.

(curly)

front_end/panels/ai_chat/ui/__tests__/ChatViewAgentSessions.test.ts

[error] 320-320: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 321-321: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 353-353: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 354-354: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 413-413: 'before' is assigned a value but never used.

(@typescript-eslint/no-unused-vars)


[error] 418-418: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

front_end/panels/ai_chat/ui/AIChatPanel.ts

[error] 89-89: Import in body of module; reorder to top.

(import/first)


[error] 89-89: ../mcp/MCPRegistry.js import should occur before import of ./chatView.css.js

(import/order)


[error] 90-90: Import in body of module; reorder to top.

(import/first)


[error] 90-90: ../mcp/MCPConfig.js import should occur before import of ./chatView.css.js

(import/order)


[error] 90-90: 'getMCPConfig' is defined but never used.

(@typescript-eslint/no-unused-vars)


[error] 90-90: '../mcp/MCPConfig.js' imported multiple times.

(import/no-duplicates)


[error] 91-91: Import in body of module; reorder to top.

(import/first)


[error] 91-91: ../mcp/MCPConfig.js import should occur before import of ./chatView.css.js

(import/order)


[error] 91-91: '../mcp/MCPConfig.js' imported multiple times.

(import/no-duplicates)


[error] 722-722: '#mcpUnsubscribe' is defined but never used.

(no-unused-private-class-members)


[error] 794-794: Missing return type on function.

(@typescript-eslint/explicit-function-return-type)

front_end/panels/ai_chat/ui/SettingsDialog.ts

[error] 399-399: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 431-431: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 448-448: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 449-449: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 450-450: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 451-451: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 452-452: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 453-453: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 454-454: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 455-455: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 540-540: 'e' is defined but never used.

(@typescript-eslint/no-unused-vars)


[error] 932-932: Missing return type on function.

(@typescript-eslint/explicit-function-return-type)


[error] 932-932: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 938-938: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 940-940: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 954-954: Prefer template literals over imperative DOM API calls

(rulesdir/no-imperative-dom-api)


[error] 959-959: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 961-961: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 2567-2567: Expected { after 'if' condition.

(curly)


[error] 2572-2572: Expected { after 'if' condition.

(curly)


[error] 2573-2573: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 2590-2590: Missing return type on function.

(@typescript-eslint/explicit-function-return-type)


[error] 2605-2605: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 2618-2618: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 2658-2658: Missing return type on function.

(@typescript-eslint/explicit-function-return-type)


[error] 2817-2817: Missing return type on function.

(@typescript-eslint/explicit-function-return-type)


[error] 2850-2850: Missing return type on function.

(@typescript-eslint/explicit-function-return-type)


[error] 2853-2853: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 2885-2885: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 3836-3836: Prefer template literals over imperative DOM API calls

(rulesdir/no-imperative-dom-api)


[error] 3850-3850: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

front_end/panels/ai_chat/ui/ChatView.ts

[error] 375-375: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 496-496: When interpolating a value as an attribute in Lit you do not need double quotes around it.

(rulesdir/lit-no-attribute-quotes)


[error] 503-503: When interpolating a value as an attribute in Lit you do not need double quotes around it.

(rulesdir/lit-no-attribute-quotes)


[error] 953-953: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)

🪛 ast-grep (0.38.6)
front_end/panels/ai_chat/ui/SettingsDialog.ts

[warning] 2612-2612: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: mcpStatusDetails.innerHTML = detailsHtml
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)


[warning] 2625-2625: Direct HTML content assignment detected. Modifying innerHTML, outerHTML, or using document.write with unsanitized content can lead to XSS vulnerabilities. Use secure alternatives like textContent or sanitize HTML with libraries like DOMPurify.
Context: mcpStatusDetails.innerHTML = detailsHtml
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://www.dhairyashah.dev/posts/why-innerhtml-is-a-bad-idea-and-how-to-avoid-it/
- https://cwe.mitre.org/data/definitions/79.html

(unsafe-html-content-assignment)


[warning] 2595-2595: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: mcpStatusDetails.innerHTML = ''
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)


[warning] 2612-2612: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: mcpStatusDetails.innerHTML = detailsHtml
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)


[warning] 2625-2625: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: mcpStatusDetails.innerHTML = detailsHtml
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)


[warning] 2851-2851: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: mcpToolsList.innerHTML = ''
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: claude-review
🔇 Additional comments (20)
front_end/panels/ai_chat/ui/HelpDialog.ts (1)

217-217: Confirm updated i18n key usage across locales

Switched from alphaVersionWarning to betaVersionWarning. Ensure translations and message catalogs are regenerated so this key resolves in all locales.

front_end/panels/ai_chat/ui/chatView.css (2)

64-64: Overflow hidden: confirm focus/scroll into view

With .chat-view-container { overflow: hidden; }, ensure focusing elements near the bottom (e.g., via keyboard) still scrolls the inner list correctly and isn't clipped.


556-556: Spacing increase: verify it doesn’t push content under the overlay

Raising .message-actions-row margin-bottom to 28px can interact with the overlayed input bar. Validate last-message visibility on small viewports.

front_end/panels/ai_chat/ui/ChatView.ts (1)

374-377: Lane filter looks good

Hiding uiLane === 'agent' in the main feed aligns with the lane-based design.

front_end/panels/ai_chat/ui/__tests__/ChatViewAgentSessions.test.ts (3)

4-4: Good addition of doubleRaf for render-settle

This de-flakes timing around ScheduledRender.


260-265: Nested child verification is solid

Asserting via nested shadow DOM is the right approach here.


320-355: Lane flags in tests improve clarity

Marking agent-managed items with uiLane: 'agent' matches production routing.

config/gni/devtools_grd_files.gni (1)

1383-1383: LGTM: new unbundled helper entry.

front_end/panels/ai_assistance/components/ScrollPinHelper.js entry looks fine.

front_end/panels/ai_chat/ui/__tests__/SettingsDialogOpenRouterCache.test.ts (2)

90-92: Use of window.localStorage.getItem is correct in the mocked environment.

Aligned with the localStorage shim added in beforeEach; good.


148-150: Consistent localStorage API usage for API key read.

Good switch to window.localStorage.getItem(...); matches the mock and production access pattern.

front_end/panels/ai_chat/ui/input/InputBar.ts (1)

95-101: PreferAbove binding matches new ModelSelector API; verify UX polarity.

.preferAbove=${!this.#centered} wires dropdown to open above when not centered. Confirm this matches intended layout (e.g., centered layout may need above to avoid overflow).

front_end/panels/ai_chat/ui/model_selector/ModelSelector.ts (3)

22-35: Public flags for positioning/searchability look good.

Encapsulated via getters/setters; render reacts correctly.


68-69: Searchability threshold with override is reasonable.

#isSearchable() using forceSearchable || options.length >= 20 is clear and safe.


91-93: Dropdown positioning respects preferAbove.

Class toggle will let CSS control placement; ensure styles define .model-dropdown.above|.below.

front_end/panels/ai_chat/ui/SettingsDialog.ts (5)

11-12: LGTM: MCP integration imports are correctly structured

The new MCP imports (getMCPConfig, setMCPConfig, isMCPEnabled, MCPRegistry) follow proper module organization and provide the necessary abstractions for MCP functionality integration.


355-436: Comprehensive UI strings for MCP integration

The addition of MCP-related UI strings is well-structured and covers all necessary user-facing elements including configuration options, tool management, and help text. The internationalization approach is consistent with the existing codebase.


2501-2927: MCP section implementation is comprehensive but hidden by design

The MCP integration section includes all necessary functionality - configuration inputs, status monitoring, tool management, and budget controls. The decision to hide it by default (line 2505: mcpSection.style.display = 'none') aligns with the auto-connect approach mentioned in comments.

The implementation covers:

  • Configuration management with proper validation
  • Real-time status updates with connection monitoring
  • Tool discovery and per-tool allowlist management
  • Advanced budget controls for tool selection

3825-3871: Model selector component integration looks solid

The createModelSelector helper function properly integrates the new ai-model-selector component while maintaining backward compatibility. The API bridge (lines 3857-3863) that exposes a .value property similar to native <select> elements is a good design choice for existing code compatibility.


3797-3823: Static method for OAuth integration is well-implemented

The updateOpenRouterModels static method provides a clean interface for updating model options after OAuth authentication. The event-driven approach with custom events and localStorage caching is appropriate for this use case.

front_end/panels/ai_chat/ui/message/__tests__/MessageCombiner.test.ts (1)

42-44: Replace explicit any casts with a narrow test-local ChatMessage type and use ChatMessageEntity constants.

eslint couldn't be run in the sandbox: "eslint: command not found". Run eslint locally and confirm the lint errors clear after applying the diff.

File: front_end/panels/ai_chat/ui/message/tests/MessageCombiner.test.ts (lines 42-44, 50-51)

-      { entity: ChatMessageEntity.MODEL, action: 'tool', toolName: 'fetch', toolCallId, isFinalAnswer: false, uiLane: 'agent' } as any,
-      { entity: ChatMessageEntity.TOOL_RESULT, toolName: 'fetch', toolCallId, resultText: '{"ok":true}', isError: false, uiLane: 'agent' } as any,
-      { entity: ChatMessageEntity.AGENT_SESSION, agentSession: { sessionId: 's1', agentName: 'agent', status: 'running', startTime: new Date(), messages: [], nestedSessions: [] } } as any,
+      { entity: ChatMessageEntity.MODEL, action: 'tool', toolName: 'fetch', toolCallId, isFinalAnswer: false, uiLane: 'agent' } as ChatMessage,
+      { entity: ChatMessageEntity.TOOL_RESULT, toolName: 'fetch', toolCallId, resultText: '{"ok":true}', isError: false, uiLane: 'agent' } as ChatMessage,
+      { entity: ChatMessageEntity.AGENT_SESSION, agentSession: { sessionId: 's1', agentName: 'agent', status: 'running', startTime: new Date(), messages: [], nestedSessions: [] } } as ChatMessage,
-    assert.strictEqual((combined[0] as any).entity, 'user');
-    assert.strictEqual((combined[1] as any).entity, 'agent_session');
+    assert.strictEqual((combined[0] as ChatMessage).entity, ChatMessageEntity.USER);
+    assert.strictEqual((combined[1] as ChatMessage).entity, ChatMessageEntity.AGENT_SESSION);

Define the local type once (outside the changed range):

// Replace the current alias:
// type ChatMessage = any;
type ChatMessage = { entity: string; [k: string]: unknown };

Comment on lines +467 to 539
return html`
<!-- Reasoning (if any) displayed above the timeline -->
${toolReasoning ? html`
<div class="message-text reasoning-text" style="margin-bottom: 8px;">
${renderMarkdown(toolReasoning, this.#markdownRenderer)}
</div>
` : Lit.nothing}
<!-- Timeline Tool Execution -->
<div class="agent-execution-timeline single-tool">
<!-- Tool Header -->
<div class="agent-header">
<div class="agent-marker"></div>
<div class="agent-title">${descriptionData.action}</div>
<div class="agent-divider"></div>
<button class="tool-toggle" @click=${(e: Event) => this.#toggleToolResult(e)}>
<span class="toggle-icon"></span>
</button>
</div>
// <div class="timeline-items" style="display: none;">
// <div class="timeline-item">
// <div class="tool-line">
// ${descriptionData.isMultiLine ? html`
// <div class="tool-summary">
// <span class="tool-description">
// <span class="tool-description-indicator">└─</span>
// <div>${(descriptionData.content as Array<{key: string, value: string}>)[0]?.value || 'multiple parameters'}</div>
// </span>
// <span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}">●</span>
// </div>
// ` : html`
// <span class="tool-description">
// <span class="tool-description-indicator">└─</span>
// <div>${descriptionData.content}</div>
// </span>
// <span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}">●</span>
// `}
// </div>
<div class="timeline-items" style="display: none;">
<div class="timeline-item">
<div class="tool-line">
${descriptionData.isMultiLine ? html`
<div class="tool-summary">
<span class="tool-description">
<span class="tool-description-indicator">└─</span>
<div>${(descriptionData.content as Array<{key: string, value: string}>)[0]?.value || 'multiple parameters'}</div>
</span>
<span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}"></span>
</div>
` : html`
<span class="tool-description">
<span class="tool-description-indicator">└─</span>
<div>${descriptionData.content}</div>
</span>
<span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}"></span>
`}
</div>
// <!-- Result Block - Integrated within timeline item -->
// ${isCombined && resultText ? html`
// <div class="tool-result-integrated ${status}">
// Response:
// ${this.#formatJsonWithSyntaxHighlighting(resultText)}
// </div>
// ` : Lit.nothing}
// </div>
// </div>
<!-- Result Block - Integrated within timeline item -->
${isCombined && resultText ? html`
<div class="tool-result-integrated ${status}">
Response:
${this.#formatJsonWithSyntaxHighlighting(resultText)}
</div>
` : Lit.nothing}
</div>
</div>
// <!-- Loading spinner for running tools -->
// ${status === 'running' ? html`
// <div class="tool-loading">
// <svg class="loading-spinner" width="16" height="16" viewBox="0 0 16 16">
// <circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="30 12" stroke-linecap="round">
// <animateTransform
// attributeName="transform"
// attributeType="XML"
// type="rotate"
// from="0 8 8"
// to="360 8 8"
// dur="1s"
// repeatCount="indefinite" />
// </circle>
// </svg>
// </div>
// ` : Lit.nothing}

// <!-- Error messages -->
// ${modelMessage.error ? html`<div class="message-error tool-error-message">Model Error: ${modelMessage.error}</div>` : Lit.nothing}
// </div>
// `;
<!-- Loading spinner for running tools -->
${status === 'running' ? html`
<div class="tool-loading">
<svg class="loading-spinner" width="16" height="16" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="30 12" stroke-linecap="round">
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
from="0 8 8"
to="360 8 8"
dur="1s"
repeatCount="indefinite" />
</circle>
</svg>
</div>
` : Lit.nothing}
<!-- Error messages -->
${modelMessage.error ? html`<div class="message-error tool-error-message">Model Error: ${modelMessage.error}</div>` : Lit.nothing}
</div>
`;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fix Lit attribute interpolation and persist toggle state

  • Lit rule: remove quotes around interpolated title values.
  • Optional: toggled state is DOM-driven and will reset on rerender. Persist by tracking expanded tool-call IDs.
-                          <span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}">●</span>
+                          <span class="tool-status-marker ${status}" title=${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}>●</span>
@@
-                        <span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}">●</span>
+                        <span class="tool-status-marker ${status}" title=${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}>●</span>

If you want persistence, I can propose a small Set<string> keyed by toolCallId to track expanded items.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return html`
<!-- Reasoning (if any) displayed above the timeline -->
${toolReasoning ? html`
<div class="message-text reasoning-text" style="margin-bottom: 8px;">
${renderMarkdown(toolReasoning, this.#markdownRenderer)}
</div>
` : Lit.nothing}
<!-- Timeline Tool Execution -->
<div class="agent-execution-timeline single-tool">
<!-- Tool Header -->
<div class="agent-header">
<div class="agent-marker"></div>
<div class="agent-title">${descriptionData.action}</div>
<div class="agent-divider"></div>
<button class="tool-toggle" @click=${(e: Event) => this.#toggleToolResult(e)}>
<span class="toggle-icon"></span>
</button>
</div>
// <div class="timeline-items" style="display: none;">
// <div class="timeline-item">
// <div class="tool-line">
// ${descriptionData.isMultiLine ? html`
// <div class="tool-summary">
// <span class="tool-description">
// <span class="tool-description-indicator">└─</span>
// <div>${(descriptionData.content as Array<{key: string, value: string}>)[0]?.value || 'multiple parameters'}</div>
// </span>
// <span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}">●</span>
// </div>
// ` : html`
// <span class="tool-description">
// <span class="tool-description-indicator">└─</span>
// <div>${descriptionData.content}</div>
// </span>
// <span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}">●</span>
// `}
// </div>
<div class="timeline-items" style="display: none;">
<div class="timeline-item">
<div class="tool-line">
${descriptionData.isMultiLine ? html`
<div class="tool-summary">
<span class="tool-description">
<span class="tool-description-indicator">└─</span>
<div>${(descriptionData.content as Array<{key: string, value: string}>)[0]?.value || 'multiple parameters'}</div>
</span>
<span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}"></span>
</div>
` : html`
<span class="tool-description">
<span class="tool-description-indicator">└─</span>
<div>${descriptionData.content}</div>
</span>
<span class="tool-status-marker ${status}" title="${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}"></span>
`}
</div>
// <!-- Result Block - Integrated within timeline item -->
// ${isCombined && resultText ? html`
// <div class="tool-result-integrated ${status}">
// Response:
// ${this.#formatJsonWithSyntaxHighlighting(resultText)}
// </div>
// ` : Lit.nothing}
// </div>
// </div>
<!-- Result Block - Integrated within timeline item -->
${isCombined && resultText ? html`
<div class="tool-result-integrated ${status}">
Response:
${this.#formatJsonWithSyntaxHighlighting(resultText)}
</div>
` : Lit.nothing}
</div>
</div>
// <!-- Loading spinner for running tools -->
// ${status === 'running' ? html`
// <div class="tool-loading">
// <svg class="loading-spinner" width="16" height="16" viewBox="0 0 16 16">
// <circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="30 12" stroke-linecap="round">
// <animateTransform
// attributeName="transform"
// attributeType="XML"
// type="rotate"
// from="0 8 8"
// to="360 8 8"
// dur="1s"
// repeatCount="indefinite" />
// </circle>
// </svg>
// </div>
// ` : Lit.nothing}
// <!-- Error messages -->
// ${modelMessage.error ? html`<div class="message-error tool-error-message">Model Error: ${modelMessage.error}</div>` : Lit.nothing}
// </div>
// `;
<!-- Loading spinner for running tools -->
${status === 'running' ? html`
<div class="tool-loading">
<svg class="loading-spinner" width="16" height="16" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="30 12" stroke-linecap="round">
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
from="0 8 8"
to="360 8 8"
dur="1s"
repeatCount="indefinite" />
</circle>
</svg>
</div>
` : Lit.nothing}
<!-- Error messages -->
${modelMessage.error ? html`<div class="message-error tool-error-message">Model Error: ${modelMessage.error}</div>` : Lit.nothing}
</div>
`;
}
return html`
<!-- Reasoning (if any) displayed above the timeline -->
${toolReasoning ? html`
<div class="message-text reasoning-text" style="margin-bottom: 8px;">
${renderMarkdown(toolReasoning, this.#markdownRenderer)}
</div>
` : Lit.nothing}
<!-- Timeline Tool Execution -->
<div class="agent-execution-timeline single-tool">
<!-- Tool Header -->
<div class="agent-header">
<div class="agent-marker"></div>
<div class="agent-title">${descriptionData.action}</div>
<div class="agent-divider"></div>
<button class="tool-toggle" @click=${(e: Event) => this.#toggleToolResult(e)}>
<span class="toggle-icon"></span>
</button>
</div>
<div class="timeline-items" style="display: none;">
<div class="timeline-item">
<div class="tool-line">
${descriptionData.isMultiLine ? html`
<div class="tool-summary">
<span class="tool-description">
<span class="tool-description-indicator">└─</span>
<div>${(descriptionData.content as Array<{key: string, value: string}>)[0]?.value || 'multiple parameters'}</div>
</span>
<span class="tool-status-marker ${status}" title=${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}></span>
</div>
` : html`
<span class="tool-description">
<span class="tool-description-indicator">└─</span>
<div>${descriptionData.content}</div>
</span>
<span class="tool-status-marker ${status}" title=${status === 'running' ? 'Running' : status === 'completed' ? 'Completed' : status === 'error' ? 'Error' : 'Unknown'}></span>
`}
</div>
<!-- Result Block - Integrated within timeline item -->
${isCombined && resultText ? html`
<div class="tool-result-integrated ${status}">
Response:
${this.#formatJsonWithSyntaxHighlighting(resultText)}
</div>
` : Lit.nothing}
</div>
</div>
<!-- Loading spinner for running tools -->
${status === 'running' ? html`
<div class="tool-loading">
<svg class="loading-spinner" width="16" height="16" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="30 12" stroke-linecap="round">
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
from="0 8 8"
to="360 8 8"
dur="1s"
repeatCount="indefinite" />
</circle>
</svg>
</div>
` : Lit.nothing}
<!-- Error messages -->
${modelMessage.error ? html`<div class="message-error tool-error-message">Model Error: ${modelMessage.error}</div>` : Lit.nothing}
</div>
`;
}
🧰 Tools
🪛 ESLint

[error] 496-496: When interpolating a value as an attribute in Lit you do not need double quotes around it.

(rulesdir/lit-no-attribute-quotes)


[error] 503-503: When interpolating a value as an attribute in Lit you do not need double quotes around it.

(rulesdir/lit-no-attribute-quotes)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/ui/ChatView.ts around lines 467-539, the Lit
template is incorrectly interpolating attribute values with quotes (e.g.
title="${...}") which violates Lit rules, and the tool-result toggle is
DOM-driven so it will reset on rerender; remove the quotes so attributes use
unquoted binding (title=${...}) and replace the DOM-only toggle with a
component-level state: add a private Set<string> (e.g. #expandedToolCalls) keyed
by toolCallId, update the toggle handler to add/remove the toolCallId from the
Set, and drive the timeline visibility/result rendering from that Set (use
conditional rendering or set a class/style based on Set.has(toolCallId)) so the
expanded/collapsed state persists across rerenders.

Comment on lines +54 to 68
it('hides agent-lane tool_result even if it arrives before model tool-call', () => {
const toolCallId = 'tc-outoforder';
const messages: ChatMessage[] = [
{ entity: ChatMessageEntity.USER, text: 'go' } as any,
// Agent-managed tool result first
{ entity: ChatMessageEntity.TOOL_RESULT, toolName: 'fetch', toolCallId, resultText: '{"ok":1}', isError: false, isFromConfigurableAgent: true } as any,
{ entity: ChatMessageEntity.TOOL_RESULT, toolName: 'fetch', toolCallId, resultText: '{"ok":1}', isError: false, uiLane: 'agent' } as any,
// Model tool call later
{ entity: ChatMessageEntity.MODEL, action: 'tool', toolName: 'fetch', toolCallId, isFinalAnswer: false } as any,
];

const combined = combineMessages(messages);
// Expect: user only (both the agent-managed result and matching model call removed)
// Expect: user only (both the agent-lane result and matching model call removed)
assert.lengthOf(combined, 1);
assert.strictEqual((combined[0] as any).entity, 'user');
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Test expectation conflicts with current combineMessages logic; model tool-call won’t be removed.

combineMessages only skips items with uiLane === 'agent'. A later model tool-call without uiLane is still emitted, even if an agent-lane tool_result with the same toolCallId appeared earlier. Result: this test will fail against the current implementation.

Choose one:

  • Option A (adjust test to current behavior).
-    // Expect: user only (both the agent-lane result and matching model call removed)
-    assert.lengthOf(combined, 1);
-    assert.strictEqual((combined[0] as any).entity, 'user');
+    // Expect: user + model tool-call (agent-lane result hidden, model call still visible)
+    assert.lengthOf(combined, 2);
+    assert.strictEqual((combined[0] as ChatMessage).entity, ChatMessageEntity.USER);
+    assert.strictEqual((combined[1] as ChatMessage).entity, ChatMessageEntity.MODEL);
+    assert.strictEqual((combined[1] as { action: string }).action, 'tool');
  • Option B (preferred if product intent is to hide the model call once an agent handled it): update MessageCombiner to drop model tool-calls whose toolCallId was already seen in an agent-lane tool_result.

Apply this to front_end/panels/ai_chat/ui/message/MessageCombiner.ts:

 export function combineMessages(messages: ChatMessage[]): CombinedMessage[] {
   const result: CombinedMessage[] = [];
+  // Pre-scan for agent-lane tool-results so we can hide later model tool-calls they fulfilled.
+  const agentLaneToolResultCallIds = new Set<string>();
+  for (const m of messages) {
+    if ((m as any).uiLane === 'agent' && m.entity === 'tool_result' && (m as ToolResultMessage).toolCallId) {
+      agentLaneToolResultCallIds.add((m as ToolResultMessage).toolCallId!);
+    }
+  }
 
   for (let i = 0; i < messages.length; i++) {
     const msg = messages[i];
@@
-    if (msg.entity === 'model' && (msg as ModelChatMessage).action === 'tool') {
+    if (msg.entity === 'model' && (msg as ModelChatMessage).action === 'tool') {
       const modelMsg = msg as ModelChatMessage;
+      // If an agent-lane tool_result already exists for this call, hide the model call.
+      if (modelMsg.toolCallId && agentLaneToolResultCallIds.has(modelMsg.toolCallId)) {
+        continue;
+      }
       const next = messages[i + 1];

Also remove explicit any in this test segment:

-      { entity: ChatMessageEntity.USER, text: 'go' } as any,
+      { entity: ChatMessageEntity.USER, text: 'go' } as ChatMessage,
-      { entity: ChatMessageEntity.TOOL_RESULT, toolName: 'fetch', toolCallId, resultText: '{"ok":1}', isError: false, uiLane: 'agent' } as any,
+      { entity: ChatMessageEntity.TOOL_RESULT, toolName: 'fetch', toolCallId, resultText: '{"ok":1}', isError: false, uiLane: 'agent' } as ChatMessage,
-      { entity: ChatMessageEntity.MODEL, action: 'tool', toolName: 'fetch', toolCallId, isFinalAnswer: false } as any,
+      { entity: ChatMessageEntity.MODEL, action: 'tool', toolName: 'fetch', toolCallId, isFinalAnswer: false } as ChatMessage,

🏁 Script executed:

#!/usr/bin/env bash
# 1) Lint the test file to ensure no-explicit-any violations are resolved.
eslint front_end/panels/ai_chat/ui/message/__tests__/MessageCombiner.test.ts

# 2) Grep combineMessages for the new agentLaneToolResultCallIds check (if Option B applied).
rg -n "agentLaneToolResultCallIds|uiLane === 'agent'" front_end/panels/ai_chat/ui/message/MessageCombiner.ts -C3

# 3) Run just this spec (adjust to your test runner).
pnpm -w test front_end/panels/ai_chat/ui/message/__tests__/MessageCombiner.test.ts

Length of output: 266


Test expectation conflicts with combineMessages — update test or make MessageCombiner hide model tool-calls already fulfilled by agent-lane results

combineMessages currently only skips items with uiLane === 'agent' and does not drop later model tool-calls that share the same toolCallId; the spec will fail against the current implementation.

Choose one:

  • Option A (adjust test to current behavior).
-    // Expect: user only (both the agent-lane result and matching model call removed)
-    assert.lengthOf(combined, 1);
-    assert.strictEqual((combined[0] as any).entity, 'user');
+    // Expect: user + model tool-call (agent-lane result hidden, model call still visible)
+    assert.lengthOf(combined, 2);
+    assert.strictEqual((combined[0] as ChatMessage).entity, ChatMessageEntity.USER);
+    assert.strictEqual((combined[1] as ChatMessage).entity, ChatMessageEntity.MODEL);
+    assert.strictEqual((combined[1] as { action: string }).action, 'tool');
  • Option B (preferred if product intent is to hide the model call once an agent handled it): update MessageCombiner to drop model tool-calls whose toolCallId was already seen in an agent-lane tool_result.

Apply to front_end/panels/ai_chat/ui/message/MessageCombiner.ts:

 export function combineMessages(messages: ChatMessage[]): CombinedMessage[] {
   const result: CombinedMessage[] = [];
+  // Pre-scan for agent-lane tool-results so we can hide later model tool-calls they fulfilled.
+  const agentLaneToolResultCallIds = new Set<string>();
+  for (const m of messages) {
+    if ((m as any).uiLane === 'agent' && m.entity === 'tool_result' && (m as ToolResultMessage).toolCallId) {
+      agentLaneToolResultCallIds.add((m as ToolResultMessage).toolCallId!);
+    }
+  }
 
   for (let i = 0; i < messages.length; i++) {
     const msg = messages[i];
@@
-    if (msg.entity === 'model' && (msg as ModelChatMessage).action === 'tool') {
+    if (msg.entity === 'model' && (msg as ModelChatMessage).action === 'tool') {
+      // If an agent-lane tool_result already exists for this call, hide the model call.
+      const modelMsg = msg as ModelChatMessage;
+      if (modelMsg.toolCallId && agentLaneToolResultCallIds.has(modelMsg.toolCallId)) {
+        continue;
+      }
+      // (rest unchanged)
+      const next = messages[i + 1];

Also remove explicit any in the test segment:

-      { entity: ChatMessageEntity.USER, text: 'go' } as any,
-      { entity: ChatMessageEntity.TOOL_RESULT, toolName: 'fetch', toolCallId, resultText: '{"ok":1}', isError: false, uiLane: 'agent' } as any,
-      { entity: ChatMessageEntity.MODEL, action: 'tool', toolName: 'fetch', toolCallId, isFinalAnswer: false } as any,
+      { entity: ChatMessageEntity.USER, text: 'go' } as ChatMessage,
+      { entity: ChatMessageEntity.TOOL_RESULT, toolName: 'fetch', toolCallId, resultText: '{"ok":1}', isError: false, uiLane: 'agent' } as ChatMessage,
+      { entity: ChatMessageEntity.MODEL, action: 'tool', toolName: 'fetch', toolCallId, isFinalAnswer: false } as ChatMessage,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('hides agent-lane tool_result even if it arrives before model tool-call', () => {
const toolCallId = 'tc-outoforder';
const messages: ChatMessage[] = [
{ entity: ChatMessageEntity.USER, text: 'go' } as any,
// Agent-managed tool result first
{ entity: ChatMessageEntity.TOOL_RESULT, toolName: 'fetch', toolCallId, resultText: '{"ok":1}', isError: false, isFromConfigurableAgent: true } as any,
{ entity: ChatMessageEntity.TOOL_RESULT, toolName: 'fetch', toolCallId, resultText: '{"ok":1}', isError: false, uiLane: 'agent' } as any,
// Model tool call later
{ entity: ChatMessageEntity.MODEL, action: 'tool', toolName: 'fetch', toolCallId, isFinalAnswer: false } as any,
];
const combined = combineMessages(messages);
// Expect: user only (both the agent-managed result and matching model call removed)
// Expect: user only (both the agent-lane result and matching model call removed)
assert.lengthOf(combined, 1);
assert.strictEqual((combined[0] as any).entity, 'user');
});
it('hides agent-lane tool_result even if it arrives before model tool-call', () => {
const toolCallId = 'tc-outoforder';
const messages: ChatMessage[] = [
{ entity: ChatMessageEntity.USER, text: 'go' } as ChatMessage,
// Agent-managed tool result first
{ entity: ChatMessageEntity.TOOL_RESULT, toolName: 'fetch', toolCallId, resultText: '{"ok":1}', isError: false, uiLane: 'agent' } as ChatMessage,
// Model tool call later
{ entity: ChatMessageEntity.MODEL, action: 'tool', toolName: 'fetch', toolCallId, isFinalAnswer: false } as ChatMessage,
];
const combined = combineMessages(messages);
// Expect: user only (both the agent-lane result and matching model call removed)
assert.lengthOf(combined, 1);
assert.strictEqual((combined[0] as ChatMessage).entity, ChatMessageEntity.USER);
});
🧰 Tools
🪛 ESLint

[error] 57-57: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 59-59: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 61-61: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 67-67: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

Comment on lines +42 to +52
#toggle = (e: Event) => {
e.preventDefault();
if (this.#disabled) return;
const wasOpen = this.#open;
this.#open = !this.#open;
this.#render();
if (!wasOpen && this.#open) {
// Notify host that the selector opened (used to lazily refresh models)
this.dispatchEvent(new CustomEvent('model-selector-focus', {bubbles: true}));
}
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix ESLint issues on #toggle (explicit return type + curly).

Lints complain about missing return type and single-line if. Add : void and braces.

Apply:

-  #toggle = (e: Event) => {
+  #toggle = (e: Event): void => {
     e.preventDefault();
-    if (this.#disabled) return;
+    if (this.#disabled) { return; }
     const wasOpen = this.#open;
     this.#open = !this.#open;
     this.#render();
     if (!wasOpen && this.#open) {
       // Notify host that the selector opened (used to lazily refresh models)
       this.dispatchEvent(new CustomEvent('model-selector-focus', {bubbles: true}));
     }
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#toggle = (e: Event) => {
e.preventDefault();
if (this.#disabled) return;
const wasOpen = this.#open;
this.#open = !this.#open;
this.#render();
if (!wasOpen && this.#open) {
// Notify host that the selector opened (used to lazily refresh models)
this.dispatchEvent(new CustomEvent('model-selector-focus', {bubbles: true}));
}
};
#toggle = (e: Event): void => {
e.preventDefault();
if (this.#disabled) { return; }
const wasOpen = this.#open;
this.#open = !this.#open;
this.#render();
if (!wasOpen && this.#open) {
// Notify host that the selector opened (used to lazily refresh models)
this.dispatchEvent(new CustomEvent('model-selector-focus', {bubbles: true}));
}
};
🧰 Tools
🪛 ESLint

[error] 42-42: Missing return type on function.

(@typescript-eslint/explicit-function-return-type)


[error] 44-44: Expected { after 'if' condition.

(curly)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/ui/model_selector/ModelSelector.ts around lines 42
to 52, the private method #toggle is missing an explicit return type and uses a
single-line if without braces which breaks ESLint rules; update the method
signature to include an explicit return type of void and add curly braces around
the single-line if (this.#disabled) return; so it becomes a block with braces to
satisfy the linter, keeping the rest of the logic unchanged.

@claude
Copy link

claude bot commented Sep 13, 2025

Claude finished @tysonthomas9's task —— View job


I'll analyze this and get back to you.

@claude
Copy link

claude bot commented Sep 13, 2025

Claude finished @tysonthomas9's task —— View job


Code Review: MCP Integration PR #44

I've conducted a comprehensive code review of the MCP (Model Context Protocol) integration. Here's my analysis:

Code Quality & Best Practices

Strengths:

  • Excellent architecture: Clean separation of concerns with dedicated modules (Config, Registry, Adapter, Provider)
  • Strong TypeScript usage: Comprehensive interfaces and type safety throughout
  • Good error handling: Proper categorization (connection, authentication, configuration, etc.) in MCPRegistry.ts:30-49
  • Security-conscious: Token storage in sessionStorage vs localStorage in MCPConfig.ts:31
  • Comprehensive logging: Extensive debug logging for troubleshooting (ToolSurfaceProvider.ts:118-213)

Areas for improvement:

  • Excessive debug logging in production: Lines like ToolSurfaceProvider.ts:35-42 should be behind debug flags
  • Hard-coded timeouts: MCPToolAdapter.ts:35 has 30s timeout with no configuration option

Test Coverage

Good coverage found:

  • Unit tests for core functionality (MCPClientSDK.test.ts, ToolSurfaceProvider.test.ts)
  • Integration test with real server in MCPClientSDK.test.ts:53-135
  • Tool selection mode testing in ToolSurfaceProvider.test.ts:53-136
  • Tool name mapping tests in ToolNameMapping.test.ts

Missing tests:

  • No tests for MCPRegistry.ts error categorization logic
  • Missing tests for MCPConfig.ts localStorage persistence edge cases
  • No security tests for token handling

Potential Bugs ⚠️

  1. Race condition: MCPRegistry.ts:96-130 clears registeredTools then iterates servers - if this fails partway through, you're left in inconsistent state
  2. Silent failures: MCPRegistry.ts:134 uses empty catch blocks that could hide important errors
  3. Memory leak potential: Debug function ToolSurfaceProvider.ts:80-110 adds global objects that are never cleaned up
  4. Allowlist bypass: MCPRegistry.ts:118-119 checks both namespaced and original names, potentially allowing unintended access

Security Concerns 🔒

Positive security practices:

  • ✅ Tokens in sessionStorage (better than localStorage)
  • ✅ Input sanitization in MCPToolAdapter.ts:42-53
  • ✅ Allowlist-based tool filtering

Security issues:

  • 🚨 Token exposure: Debug logging in MCPToolAdapter.ts:34 could log sensitive args despite sanitization
  • 🚨 Global debug exposure: ToolSurfaceProvider.ts:80 exposes debug functions globally, potential for abuse
  • ⚠️ No input validation: MCP tool schemas passed through without validation in MCPToolAdapter.ts:24-29

Performance Considerations

Concerns:

  • Synchronous localStorage operations: MCPConfig.ts:28-41 could block UI thread
  • No connection pooling: Each server connection managed independently
  • Tool scoring overhead: ToolSurfaceProvider.ts:196-200 scores all tools on every request
  • No caching: Tool lists fetched fresh each time

Recommendations:

  • Add tool list caching with TTL
  • Consider async configuration loading
  • Implement connection pooling for multiple servers

Overall Assessment

This is a well-architected integration with good separation of concerns and comprehensive logging. The code follows TypeScript best practices and includes meaningful tests.

Priority fixes needed:

  1. Remove/guard debug logging for production
  2. Add input validation for MCP tool schemas
  3. Fix race condition in registry refresh
  4. Remove global debug object exposure

Score: 7.5/10 - Solid foundation with some security and performance refinements needed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
front_end/panels/ai_chat/tools/Tools.ts (4)

55-61: Unify API key flow: prefer ctx.apiKey; remove AgentService coupling in tools

You added apiKey to LLMContext (nice), but confirmWith404LLM still pulls credentials from AgentService, bypassing the provided context and the per‑turn flow. Gate on ctx.apiKey instead to keep a single source of truth.

@@
 export interface LLMContext {
-  apiKey?: string;
+  apiKey?: string;
   provider: LLMProvider;
   model: string;
@@
   private async confirmWith404LLM(url: string, title: string, content: string, ctx?: LLMContext): Promise<boolean> {
     try {
-      const agentService = AgentService.getInstance();
-      const apiKey = agentService.getApiKey();
-      
-      if (!apiKey) {
+      // Use per-call context key to avoid global coupling
+      if (!ctx?.apiKey) {
         logger.warn('No API key available for 404 confirmation');
         return false;
       }

Also applies to: 805-814


661-669: Fix broken interpolation in navigation log

Template string is in single quotes; ${url} won’t interpolate.

-      logger.info('Initiating navigation to: ${url}');
+      logger.info(`Initiating navigation to: ${url}`);

1123-1155: Sanitize selector to prevent JS injection in evaluate()

Directly embedding selector can break the expression or enable injection. Use JSON.stringify and refer to a local variable.

-        expression: `(() => {
-          const element = document.querySelector("${selector}");
+        expression: `(() => {
+          const sel = ${JSON.stringify(selector)};
+          const element = document.querySelector(sel);
           if (!element) {
             return {
               success: false,
-              message: "Element not found with selector: ${selector}"
+              message: "Element not found with selector: " + sel
             };
           }

1199-1206: Sanitize query and numbers in SearchContentTool evaluate()

Embed query via JSON.stringify to avoid breaking the script; keep limit numeric.

-        expression: `(() => {
-          const query = "${query}";
-          const limit = ${limit};
+        expression: `(() => {
+          const query = ${JSON.stringify(query)};
+          const limit = ${Number(limit)};

Also applies to: 1268-1276

front_end/panels/ai_chat/agent_framework/AgentRunner.ts (1)

645-653: LLMClient doesn't accept per-call apiKey — model calls will not pick up per-run keys

  • LLMClient.call's request type has no apiKey; providers are instantiated in initialize() with providerConfig.apiKey and looked up from LLMProviderRegistry. (front_end/panels/ai_chat/LLM/LLMClient.ts — initialize ≈ line 68; call ≈ line 123)
  • AgentRunner invokes llm.call(...) without an apiKey (front_end/panels/ai_chat/agent_framework/AgentRunner.ts — lines 645–653 and 1370–1394), so per‑turn keys passed to tools won’t affect the main model call.
  • Actionable fixes: either ensure LLMClient.initialize(...) is called per run with the run's provider apiKey before calling llm.call, or add apiKey to LLMCallRequest and update LLMClient.call + provider selection/creation to honor per-call apiKey.
front_end/panels/ai_chat/core/AgentService.ts (1)

255-268: Prevent secret/API-key leakage to logs & tracing

  • AgentService.ts: remove storing API keys in state/context — lines ~200–203 ((this.#state as any).context.apiKey = ...) and ~396–397 (injecting apiKey into local state before graph invoke). Don’t stash secrets in any object that may be passed to tracing/graph.
  • AgentService.ts: remove/replace debug warnings that emit un‑sanitized state/context. Locations: ~320–346 (createTrace + logger.debug) and ~380–420 (console.warns). Log only non‑secret primitives (traceId, messageCount, booleans).
  • Tracing calls: ensure tracingProvider.createTrace / createObservation never serialize state/context or tool args containing secrets. Sanitize payloads before sending (strip keys: apiKey, api_key, token, secret, password, authorization, etc.).
  • Other files logging secret metadata: ui/AIChatPanel.ts (1051–1056, 1504–1509) and LLM/OpenRouterProvider.ts (627–633) — stop logging API key length/prefix (log existence only).
  • Action: add/reuse a central sanitizer (evaluation/utils/SanitizationUtils exists) and run tests/assertions to confirm no secret keys are present in any trace/log payloads.
front_end/panels/ai_chat/BUILD.gn (1)

348-364: Tests not wired into GN: add missing test files.

These two tests won’t run in CI without being listed.

   sources = [
@@
     "agent_framework/__tests__/AgentRunner.run.flows.test.ts",
+    "core/__tests__/AgentNodesSanitize.test.ts",
+    "core/ToolNameMapping.test.ts",
     "mcp/MCPClientSDK.test.ts",
     "core/ToolSurfaceProvider.test.ts",
   ]
front_end/panels/ai_chat/core/AgentNodes.ts (1)

384-419: Remove non-null assertions in tool-call replay.

Narrow types instead of using ! to satisfy @typescript-eslint/no-non-null-assertion.

-          } else if ('action' in msg && msg.action === 'tool' && 'toolName' in msg && 'toolArgs' in msg && 'toolCallId' in msg) {
-            // Tool call message - convert from ModelChatMessage structure
-            const fnName = originalToSanitized?.[msg.toolName!] || ToolNameMap.getSanitized(msg.toolName!);
+          } else if ('action' in msg && msg.action === 'tool' && typeof (msg as any).toolName === 'string' && typeof (msg as any).toolCallId === 'string') {
+            // Tool call message - convert from ModelChatMessage structure
+            const { toolName, toolArgs, toolCallId } =
+              msg as ModelChatMessage & { toolName: string; toolArgs: unknown; toolCallId: string };
+            const fnName = originalToSanitized?.[toolName] || ToolNameMap.getSanitized(toolName);
             llmMessages.push({
               role: 'assistant',
               content: undefined,
               tool_calls: [{
-                id: msg.toolCallId!,
+                id: toolCallId,
                 type: 'function' as const,
                 function: {
                   name: fnName,
-                  arguments: JSON.stringify(msg.toolArgs),
+                  arguments: JSON.stringify(toolArgs),
                 }
               }],
             });
♻️ Duplicate comments (10)
front_end/panels/ai_chat/mcp/MCPConfig.ts (2)

1-4: Add Chromium BSD license header (repo rule)

This file is missing the standard header.

+// Copyright 2025 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
 import { createLogger } from '../core/Logger.js';

38-41: Validate toolMode and harden numeric parsing (avoid NaN bleed-through)

Don’t trust persisted strings; fence toolMode and guard parseInt.

-    const toolMode = (localStorage.getItem(KEYS.toolMode) as MCPConfigData['toolMode']) || 'router';
-    const maxToolsPerTurn = parseInt(localStorage.getItem(KEYS.maxToolsPerTurn) || '20', 10);
-    const maxMcpPerTurn = parseInt(localStorage.getItem(KEYS.maxMcpPerTurn) || '8', 10);
+    const rawMode = localStorage.getItem(KEYS.toolMode);
+    const toolMode: MCPConfigData['toolMode'] =
+      rawMode === 'all' || rawMode === 'router' || rawMode === 'meta' ? rawMode : 'router';
+    const parseIntSafe = (v: string|null, d: number) => {
+      const n = Number.parseInt(v ?? '', 10);
+      return Number.isFinite(n) ? n : d;
+    };
+    const maxToolsPerTurn = parseIntSafe(localStorage.getItem(KEYS.maxToolsPerTurn), 20);
+    const maxMcpPerTurn = parseIntSafe(localStorage.getItem(KEYS.maxMcpPerTurn), 8);
front_end/panels/ai_chat/core/ToolNameMapping.test.ts (2)

15-18: Drop public; make ctor param readonly.

-class RecordingTool implements Tool<Record<string, unknown>, unknown> {
-  public calls = 0;
-  constructor(public name: string) {}
+class RecordingTool implements Tool<Record<string, unknown>, unknown> {
+  calls = 0;
+  constructor(readonly name: string) {}

26-45: Restore real localStorage after each test.

-describe('AgentNodes sanitized tool name mapping', () => {
-  let mockLocalStorage: Map<string, string>;
+describe('AgentNodes sanitized tool name mapping', () => {
+  let mockLocalStorage: Map<string, string>;
+  let realLocalStorage: Storage;
@@
-    Object.defineProperty(window, 'localStorage', {
+    realLocalStorage = window.localStorage;
+    Object.defineProperty(window, 'localStorage', {
       value: {
@@
       writable: true,
     });
   });
 
   afterEach(() => {
     mockLocalStorage.clear();
+    Object.defineProperty(window, 'localStorage', { value: realLocalStorage, writable: true });
   });
front_end/panels/ai_chat/mcp/MCPToolAdapter.ts (1)

1-4: Add license header and fix import order.

+// Copyright 2025 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
-import type { Tool } from '../tools/Tools.js';
-import { createLogger } from '../core/Logger.js';
-import type { MCPClient, MCPToolDef } from '../../../third_party/mcp-sdk/mcp-sdk.js';
+import { createLogger } from '../core/Logger.js';
+import type { MCPClient, MCPToolDef } from '../../../third_party/mcp-sdk/mcp-sdk.js';
+import type { Tool } from '../tools/Tools.js';
front_end/panels/ai_chat/core/AgentNodes.ts (1)

154-159: Persist per‑turn selected tool instances and a name map.

Executor can then honor exactly the same set and resolve sanitized↔original lookups.

-        (state.context as any).selectedToolNames = selection.selectedNames;
-        const tools = selection.tools;
+        (state.context as any).selectedToolNames = selection.selectedNames;
+        const tools = selection.tools;
+        (state.context as any).selectedTools = tools;
+        (state.context as any).toolNameMap = Object.fromEntries(
+          (tools as any[]).map(t => [ToolNameMap.getSanitized(t.name), t.name])
+        );
front_end/panels/ai_chat/mcp/MCPRegistry.ts (3)

21-29: Use namespace client type to satisfy import rule.

Construct MCPClient from the namespace.

-  private client = new MCPClient();
+  private client = new MCPSDK.MCPClient();

140-155: Report actual tool counts per server.

Replace hardcoded 0 with a filtered count of registered MCP tools.

   getStatus(): MCPRegistryStatus {
     return {
       enabled: getMCPConfig().enabled,
       servers: this.servers.map(s => ({
         id: s.id,
         endpoint: s.endpoint,
         connected: this.client.isConnected(s.id),
-        toolCount: 0,
+        toolCount: this.registeredTools.filter(name => name.startsWith(`mcp:${s.id}:`)).length,
       })),

1-8: Add license header and fix third_party import per lint (namespace import + type aliases).

Resolves check-license-header, es-modules-import, import/order, and inline-type-imports.

+// Copyright 2025 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
-import { createLogger } from '../core/Logger.js';
-import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';
-import * as ToolNameMap from '../core/ToolNameMap.js';
-import type { MCPToolDef, MCPServer } from '../../../third_party/mcp-sdk/mcp-sdk.js';
-import { MCPClient } from '../../../third_party/mcp-sdk/mcp-sdk.js';
+import * as MCPSDK from '../../../third_party/mcp-sdk/mcp-sdk.js';
+
+import { createLogger } from '../core/Logger.js';
+import { ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';
+import * as ToolNameMap from '../core/ToolNameMap.js';
+type MCPToolDef = MCPSDK.MCPToolDef;
+type MCPServer = MCPSDK.MCPServer;
front_end/panels/ai_chat/ui/SettingsDialog.ts (1)

2874-2925: Avoid innerHTML for clearing and read fresh allowlist.

Prevents XSS flags and stale config use.

-    const updateToolsList = () => {
+    const updateToolsList = (): void => {
       const status = MCPRegistry.getStatus();
-      mcpToolsList.innerHTML = '';
+      mcpToolsList.replaceChildren();
@@
-      const currentAllowlist = new Set(currentMCPConfig.toolAllowlist || []);
+      const { toolAllowlist } = getMCPConfig();
+      const currentAllowlist = new Set(toolAllowlist || []);
🧹 Nitpick comments (24)
front_end/panels/ai_chat/tools/Tools.ts (2)

892-899: Use appropriate log levels in NavigateBackTool

Regular control‑flow logs should not use error level.

-    logger.error('navigate_back', args);
+    logger.info('navigate_back', args);
@@
-            logger.error('Navigation completed, document ready state is complete');
+            logger.info('Navigation completed, document ready state is complete');
@@
-          logger.error('Still waiting for navigation to complete...');
+          logger.debug('Still waiting for navigation to complete...');
@@
-      if (!isNavigationComplete) {
-        logger.error('Navigation timed out after waiting for document ready state');
-      }
+      if (!isNavigationComplete) {
+        logger.warn('Navigation timed out after waiting for document ready state');
+      }

Also applies to: 954-969


1458-1492: Optional: pass apiKey from ctx to LLMClient call sites

Several tool LLM calls (e.g., WaitTool) ignore ctx.apiKey. If LLMClient supports per‑call keys, pass it to avoid reliance on globals.

Would you like me to scan LLMClient for an apiKey parameter and wire it through these calls?

front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts (2)

18-29: Use semicolons in interface members (lint fix)

ESLint flags member-delimiter-style; switch commas to semicolons.

 export interface CallCtx {
-  apiKey?: string,
-  provider?: LLMProvider,
-  model?: string,
-  miniModel?: string,
-  nanoModel?: string,
-  mainModel?: string,
-  getVisionCapability?: (modelName: string) => Promise<boolean> | boolean,
-  overrideSessionId?: string,
-  overrideParentSessionId?: string,
-  overrideTraceId?: string,
+  apiKey?: string;
+  provider?: LLMProvider;
+  model?: string;
+  miniModel?: string;
+  nanoModel?: string;
+  mainModel?: string;
+  getVisionCapability?: (modelName: string) => Promise<boolean> | boolean;
+  overrideSessionId?: string;
+  overrideParentSessionId?: string;
+  overrideTraceId?: string;
 }

400-409: Type the context parameter to CallCtx

Avoid unknown and redundant cast.

-  async execute(args: ConfigurableAgentArgs, _ctx?: unknown): Promise<ConfigurableAgentResult & { agentSession: AgentSession }> {
+  async execute(args: ConfigurableAgentArgs, _ctx?: CallCtx): Promise<ConfigurableAgentResult & { agentSession: AgentSession }> {
@@
-    const callCtx = (_ctx || {}) as CallCtx;
+    const callCtx: CallCtx = _ctx ?? {};
front_end/panels/ai_chat/core/ToolNameMap.ts (1)

13-18: Add curly braces for single-line if statements (lint fix)

Satisfy curly rule and keep style consistent.

 function sanitize(original: string): string {
   let name = original.replace(/[^a-zA-Z0-9_-]/g, '_');
-  if (!name) name = 'tool';
-  if (name.length > 64) name = name.slice(0, 64);
+  if (!name) { name = 'tool'; }
+  if (name.length > 64) { name = name.slice(0, 64); }
   return name;
 }
@@
 export function addMapping(original: string): string {
   const existing = originalToSanitized.get(original);
-  if (existing) return existing;
+  if (existing) { return existing; }

Also applies to: 35-41

front_end/panels/ai_chat/mcp/MCPConfig.ts (1)

91-95: Annotate local callback type to satisfy explicit-return-type lint

ESLint flags the inline arrow; add a type annotation.

-export function onMCPConfigChange(handler: () => void): () => void {
-  const cb = () => handler();
+export function onMCPConfigChange(handler: () => void): () => void {
+  const cb: () => void = () => handler();
   window.addEventListener('ai_chat_mcp_config_changed', cb);
   return () => window.removeEventListener('ai_chat_mcp_config_changed', cb);
 }
front_end/panels/ai_chat/core/__tests__/AgentNodesSanitize.test.ts (2)

70-73: Satisfy lint: prefer assert.isOk.

-    assert.ok(capturedTools && capturedTools.length === 1, 'tools should be passed');
+    assert.isOk(capturedTools && capturedTools.length === 1, 'tools should be passed');

15-38: Reduce any in stubs for better type-safety.

Optional: type originalGetInstance and capturedTools precisely to quiet most no-explicit-any hits.

Also applies to: 41-41, 45-58

front_end/panels/ai_chat/core/ToolNameMapping.test.ts (1)

53-64: Reduce any in stubs; remove unused casts.

Optional cleanups to satisfy most lint errors on this file.

Also applies to: 71-82

front_end/panels/ai_chat/mcp/MCPToolAdapter.ts (1)

32-36: Make timeout configurable (optional).

Expose timeout via ctor param or config; default 30s.

-    return this.client.callTool(this.serverId, this.def.name, args, { timeoutMs: 30000 });
+    const timeoutMs = 30_000;
+    return this.client.callTool(this.serverId, this.def.name, args, { timeoutMs });
front_end/panels/ai_chat/BUILD.gn (1)

17-60: Keep lists sorted for minimal diffs (optional).

Consider alphabetical order in sources for maintainability.

Also applies to: 152-266

front_end/panels/ai_chat/core/AgentNodes.ts (5)

36-37: Remove extra blank line.

ESLint: no-multiple-empty-lines.

-
-
+

151-159: Type the tools list; avoid any and trailing whitespace.

Use ReturnType and drop unnecessary casts.

-        const baseTools = BaseOrchestratorAgent.getAgentTools(state.selectedAgentType ?? '') as any;
-        const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 });
+        const baseTools: ReturnType<typeof getTools> =
+          BaseOrchestratorAgent.getAgentTools(state.selectedAgentType ?? '') as ReturnType<typeof getTools>;
+        const selection = await ToolSurfaceProvider.select(state, baseTools, { maxToolsPerTurn: 20, maxMcpPerTurn: 8 });
         // Persist selection in context so ToolExecutorNode can resolve the same set
         if (!state.context) { (state as any).context = {}; }
         (state.context as any).selectedToolNames = selection.selectedNames;
-        const tools = selection.tools;
-        
+        const tools: ReturnType<typeof getTools> = selection.tools as ReturnType<typeof getTools>;
+

444-460: Alias toolMap entries by sanitized names too (makes lookups robust).

Optional but future-proof if any caller accidentally uses sanitized names.

-  (tools as any[]).forEach((tool: any) => toolMap.set(tool.name, tool));
+  (tools as any[]).forEach((tool: any) => {
+    toolMap.set(tool.name, tool); // original
+    const sanitized = ToolNameMap.getSanitized(tool.name);
+    if (sanitized !== tool.name) toolMap.set(sanitized, tool); // alias
+  });

157-157: Trim trailing whitespace.

Stylistic lint error.


253-253: Trim trailing whitespace.

Stylistic lint error.

front_end/panels/ai_chat/mcp/MCPRegistry.ts (2)

56-64: Clear ToolNameMap on init is good; also clear on dispose to avoid stale mappings.

Add ToolNameMap.clear() and reset registeredTools.

   dispose(): void {
     for (const srv of this.servers) {
       try { this.client.disconnect(srv.id); } catch {}
     }
     this.lastDisconnected = new Date();
     this.servers = [];
+    this.registeredTools = [];
+    try { ToolNameMap.clear(); } catch {}
   }

11-19: Inline object type in interface triggers member-delimiter-style; extract a named type.

Prevents the “Expected a comma” lint.

-export interface MCPRegistryStatus {
+export interface MCPServerStatus {
+  id: string;
+  endpoint: string;
+  connected: boolean;
+  toolCount: number;
+}
+
+export interface MCPRegistryStatus {
   enabled: boolean;
-  servers: Array<{ id: string; endpoint: string; connected: boolean; toolCount: number }>;
+  servers: Array<MCPServerStatus>;
   registeredToolNames: string[];
   lastError?: string;
front_end/panels/ai_chat/ui/SettingsDialog.ts (6)

2584-2651: Harden status rendering and add return type.

Avoid innerHTML (already done), but annotate function and keep safe node building.

-    const updateMCPStatus = () => {
+    const updateMCPStatus = (): void => {

2682-2687: Annotate updateDisconnectButton return type.

Satisfies explicit-function-return-type.

-    const updateDisconnectButton = () => {
+    const updateDisconnectButton = (): void => {

2840-2854: Annotate budget controls handler.

Satisfies explicit-function-return-type.

-    const updateBudgetControls = () => {
+    const updateBudgetControls = (): void => {

446-456: Replace any with typed custom element for model selectors.

Improves type-safety and removes no-explicit-any errors.

-export class SettingsDialog {
+interface AIModelSelectorElement extends HTMLElement {
+  options: ModelOption[];
+  selected: string;
+  value: string;
+  forceSearchable?: boolean;
+}
+
+export class SettingsDialog {
   // Variables to store direct references to model selectors
-  static #openaiMiniModelSelect: any | null = null;
-  static #openaiNanoModelSelect: any | null = null;
-  static #litellmMiniModelSelect: any | null = null;
-  static #litellmNanoModelSelect: any | null = null;
-  static #groqMiniModelSelect: any | null = null;
-  static #groqNanoModelSelect: any | null = null;
-  static #openrouterMiniModelSelect: any | null = null;
-  static #openrouterNanoModelSelect: any | null = null;
+  static #openaiMiniModelSelect: AIModelSelectorElement | null = null;
+  static #openaiNanoModelSelect: AIModelSelectorElement | null = null;
+  static #litellmMiniModelSelect: AIModelSelectorElement | null = null;
+  static #litellmNanoModelSelect: AIModelSelectorElement | null = null;
+  static #groqMiniModelSelect: AIModelSelectorElement | null = null;
+  static #groqNanoModelSelect: AIModelSelectorElement | null = null;
+  static #openrouterMiniModelSelect: AIModelSelectorElement | null = null;
+  static #openrouterNanoModelSelect: AIModelSelectorElement | null = null;

923-955: Type the selector element in helpers and drop any.

Satisfies no-explicit-any and explicit return type in helper.

-    function refreshModelSelectOptions(select: any, models: ModelOption[], currentValue: string, defaultLabel: string) {
+    function refreshModelSelectOptions(select: HTMLSelectElement | AIModelSelectorElement, models: ModelOption[], currentValue: string, defaultLabel: string): void {
@@
-      if (select && select.tagName && select.tagName.toLowerCase() === 'ai-model-selector') {
+      if ((select as HTMLElement)?.tagName?.toLowerCase() === 'ai-model-selector') {
         const previousValue = select.value || select.selected || '';
         const opts = [{ value: '', label: defaultLabel }, ...models];
-        select.options = opts;
+        (select as AIModelSelectorElement).options = opts;
         if (previousValue && opts.some((o: any) => o.value === previousValue)) {
-          select.value = previousValue;
+          (select as AIModelSelectorElement).value = previousValue;
         } else if (currentValue && opts.some((o: any) => o.value === currentValue)) {
-          select.value = currentValue;
+          (select as AIModelSelectorElement).value = currentValue;
         } else {
-          select.value = '';
+          (select as AIModelSelectorElement).value = '';
         }
         return;
       }
-  const selectorEl = document.createElement('ai-model-selector') as any;
+  const selectorEl = document.createElement('ai-model-selector') as AIModelSelectorElement;

Also applies to: 3874-3895


3073-3080: Fix UI grammar in disclaimer text.

User-facing copy nit.

-        <strong>Beta Version:</strong> This is an beta version of the Browser Operator - AI Assistant feature.
+        <strong>Beta Version:</strong> This is a beta version of the Browser Operator - AI Assistant feature.
@@
-        <strong>Provider Support:</strong> We currently support OpenAI, Groq and OpenRouter providers directly. And we support LiteLLM as a proxy to access 100+ other models.
+        <strong>Provider Support:</strong> We currently support OpenAI, Groq, and OpenRouter directly, and LiteLLM as a proxy to access 100+ other models.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e6e46cf and 1fad5e2.

📒 Files selected for processing (14)
  • config/gni/devtools_grd_files.gni (5 hunks)
  • front_end/panels/ai_chat/BUILD.gn (7 hunks)
  • front_end/panels/ai_chat/agent_framework/AgentRunner.ts (1 hunks)
  • front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts (3 hunks)
  • front_end/panels/ai_chat/core/AgentNodes.ts (12 hunks)
  • front_end/panels/ai_chat/core/AgentService.ts (2 hunks)
  • front_end/panels/ai_chat/core/ToolNameMap.ts (1 hunks)
  • front_end/panels/ai_chat/core/ToolNameMapping.test.ts (1 hunks)
  • front_end/panels/ai_chat/core/__tests__/AgentNodesSanitize.test.ts (1 hunks)
  • front_end/panels/ai_chat/mcp/MCPConfig.ts (1 hunks)
  • front_end/panels/ai_chat/mcp/MCPRegistry.ts (1 hunks)
  • front_end/panels/ai_chat/mcp/MCPToolAdapter.ts (1 hunks)
  • front_end/panels/ai_chat/tools/Tools.ts (1 hunks)
  • front_end/panels/ai_chat/ui/SettingsDialog.ts (17 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • config/gni/devtools_grd_files.gni
🧰 Additional context used
🧬 Code graph analysis (10)
front_end/panels/ai_chat/core/__tests__/AgentNodesSanitize.test.ts (3)
front_end/panels/ai_chat/core/State.ts (1)
  • AgentState (28-35)
front_end/panels/ai_chat/LLM/LLMClient.ts (1)
  • LLMClient (49-385)
front_end/panels/ai_chat/core/AgentNodes.ts (1)
  • createAgentNode (25-442)
front_end/panels/ai_chat/agent_framework/AgentRunner.ts (1)
front_end/panels/ai_chat/tracing/configure-langfuse.js (1)
  • config (17-26)
front_end/panels/ai_chat/core/AgentService.ts (1)
front_end/panels/ai_chat/ui/AIChatPanel.ts (3)
  • apiKey (1166-1197)
  • apiKey (1239-1258)
  • apiKey (1578-1612)
front_end/panels/ai_chat/mcp/MCPRegistry.ts (4)
front_end/panels/ai_chat/core/Logger.ts (1)
  • createLogger (316-318)
front_end/panels/ai_chat/mcp/MCPConfig.ts (1)
  • getMCPConfig (27-46)
front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts (1)
  • ToolRegistry (180-227)
front_end/panels/ai_chat/mcp/MCPToolAdapter.ts (1)
  • MCPToolAdapter (7-54)
front_end/panels/ai_chat/mcp/MCPConfig.ts (1)
front_end/panels/ai_chat/core/Logger.ts (1)
  • createLogger (316-318)
front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts (1)
front_end/panels/ai_chat/ui/AIChatPanel.ts (8)
  • logger (949-1042)
  • logger (1333-1403)
  • logger (1426-1453)
  • logger (1560-1573)
  • logger (1617-1620)
  • apiKey (1166-1197)
  • apiKey (1239-1258)
  • apiKey (1578-1612)
front_end/panels/ai_chat/ui/SettingsDialog.ts (4)
front_end/panels/ai_chat/ui/AIChatPanel.ts (9)
  • localStorage (814-837)
  • ModelOption (96-100)
  • model (1780-1793)
  • error (1930-1943)
  • logger (949-1042)
  • logger (1333-1403)
  • logger (1426-1453)
  • logger (1560-1573)
  • logger (1617-1620)
front_end/panels/ai_chat/ui/model_selector/ModelSelector.ts (2)
  • ModelOption (10-10)
  • ModelOption (62-66)
front_end/panels/ai_chat/mcp/MCPConfig.ts (3)
  • getMCPConfig (27-46)
  • isMCPEnabled (87-89)
  • setMCPConfig (48-85)
front_end/panels/ai_chat/mcp/MCPRegistry.ts (1)
  • MCPRegistry (162-162)
front_end/panels/ai_chat/core/AgentNodes.ts (4)
front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts (2)
  • ToolRegistry (180-227)
  • ConfigurableAgentTool (297-513)
front_end/panels/ai_chat/models/ChatTypes.ts (2)
  • ChatMessage (74-75)
  • ToolResultMessage (53-63)
front_end/panels/ai_chat/LLM/LLMTypes.ts (1)
  • LLMMessage (176-189)
front_end/panels/ai_chat/tools/Tools.ts (1)
  • getTools (4164-4217)
front_end/panels/ai_chat/core/ToolNameMapping.test.ts (4)
front_end/panels/ai_chat/tools/Tools.ts (1)
  • Tool (40-49)
front_end/panels/ai_chat/LLM/LLMClient.ts (1)
  • LLMClient (49-385)
front_end/panels/ai_chat/core/State.ts (1)
  • AgentState (28-35)
front_end/panels/ai_chat/core/AgentNodes.ts (1)
  • createAgentNode (25-442)
front_end/panels/ai_chat/mcp/MCPToolAdapter.ts (2)
front_end/panels/ai_chat/core/Logger.ts (1)
  • createLogger (316-318)
front_end/panels/ai_chat/tools/Tools.ts (1)
  • Tool (40-49)
🪛 ESLint
front_end/panels/ai_chat/core/__tests__/AgentNodesSanitize.test.ts

[error] 7-7: ../../models/ChatTypes.js import should occur before import of ../AgentNodes.js

(import/order)


[error] 9-9: ../../LLM/LLMClient.js import should occur before import of ../AgentNodes.js

(import/order)


[error] 15-15: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 20-20: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 20-20: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 31-31: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 36-36: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 37-37: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 41-41: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 45-45: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 46-46: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 47-47: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 49-49: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 54-54: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 61-61: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 64-64: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 66-66: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 70-70: Use assert.isOk(e) or assert(e) instead of assert.ok(e)

(rulesdir/prefer-assert-is-ok)


[error] 71-71: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 75-75: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

front_end/panels/ai_chat/core/AgentService.ts

[error] 201-201: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 202-202: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 397-397: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

front_end/panels/ai_chat/core/ToolNameMap.ts

[error] 15-15: Expected { after 'if' condition.

(curly)


[error] 16-16: Expected { after 'if' condition.

(curly)


[error] 37-37: Expected { after 'if' condition.

(curly)

front_end/panels/ai_chat/mcp/MCPRegistry.ts

[error] 1-163: Missing license header

(rulesdir/check-license-header)


[error] 2-2: ../agent_framework/ConfigurableAgentTool.js import should occur before import of ../core/Logger.js

(import/order)


[error] 3-3: Incorrect cross-namespace import: "../core/ToolNameMap.js". Use "import * as Namespace from '../namespace/namespace.js';" instead.

(rulesdir/es-modules-import)


[error] 4-4: ../../../third_party/mcp-sdk/mcp-sdk.js type import should occur before import of ../core/Logger.js

(import/order)


[error] 4-4: Type imports must be imported in the same import statement as values, using the type keyword

(rulesdir/inline-type-imports)


[error] 5-5: Incorrect cross-namespace import: "../../../third_party/mcp-sdk/mcp-sdk.js". Use "import * as Namespace from '../namespace/namespace.js';" instead. If the third_party dependency does not expose a single entrypoint, update es_modules_import.js to make it exempt.

(rulesdir/es-modules-import)


[error] 5-5: There should be at least one empty line between import groups

(import/order)


[error] 5-5: ../../../third_party/mcp-sdk/mcp-sdk.js import should occur before import of ../core/Logger.js

(import/order)


[error] 13-13: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 13-13: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 13-13: Expected a comma.

(@stylistic/member-delimiter-style)


[error] 32-32: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 37-37: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 95-95: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 98-98: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)

front_end/panels/ai_chat/mcp/MCPConfig.ts

[error] 1-104: Missing license header

(rulesdir/check-license-header)


[error] 92-92: Missing return type on function.

(@typescript-eslint/explicit-function-return-type)

front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts

[error] 19-19: Expected a semicolon.

(@stylistic/member-delimiter-style)

front_end/panels/ai_chat/ui/SettingsDialog.ts

[error] 399-399: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 431-431: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 448-448: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 449-449: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 450-450: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 451-451: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 452-452: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 453-453: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 454-454: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 455-455: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 923-923: Missing return type on function.

(@typescript-eslint/explicit-function-return-type)


[error] 923-923: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 929-929: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 931-931: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 945-945: Prefer template literals over imperative DOM API calls

(rulesdir/no-imperative-dom-api)


[error] 950-950: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 952-952: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 2562-2562: Expected { after 'if' condition.

(curly)


[error] 2567-2567: Expected { after 'if' condition.

(curly)


[error] 2584-2584: Missing return type on function.

(@typescript-eslint/explicit-function-return-type)


[error] 2599-2599: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 2627-2627: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 2682-2682: Missing return type on function.

(@typescript-eslint/explicit-function-return-type)


[error] 2841-2841: Missing return type on function.

(@typescript-eslint/explicit-function-return-type)


[error] 2874-2874: Missing return type on function.

(@typescript-eslint/explicit-function-return-type)


[error] 2877-2877: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 2909-2909: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 3860-3860: Prefer template literals over imperative DOM API calls

(rulesdir/no-imperative-dom-api)


[error] 3874-3874: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

front_end/panels/ai_chat/core/AgentNodes.ts

[error] 7-7: There should be no empty line within import group

(import/order)


[error] 10-10: '../LLM/LLMTypes.js' imported multiple times.

(import/no-duplicates)


[error] 11-11: There should be at least one empty line between import groups

(import/order)


[error] 11-11: '../LLM/LLMTypes.js' imported multiple times.

(import/no-duplicates)


[error] 12-12: ./GraphHelpers.js import should occur after type import of ../tracing/TracingProvider.js

(import/order)


[error] 13-13: ./BaseOrchestratorAgent.js import should occur after type import of ../tracing/TracingProvider.js

(import/order)


[error] 14-14: ./ToolSurfaceProvider.js import should occur after type import of ../tracing/TracingProvider.js

(import/order)


[error] 15-15: ./Logger.js import should occur after type import of ../tracing/TracingProvider.js

(import/order)


[error] 16-16: ./State.js type import should occur after type import of ../tracing/TracingProvider.js

(import/order)


[error] 17-17: ./Types.js type import should occur after type import of ../tracing/TracingProvider.js

(import/order)


[error] 18-18: There should be at least one empty line between import groups

(import/order)


[error] 18-18: ./AgentErrorHandler.js import should occur after type import of ../tracing/TracingProvider.js

(import/order)


[error] 19-19: There should be at least one empty line between import groups

(import/order)


[error] 20-20: There should be at least one empty line between import groups

(import/order)


[error] 20-20: ./ToolNameMap.js import should occur after type import of ../tracing/TracingProvider.js

(import/order)


[error] 36-37: More than 1 blank line not allowed.

(@stylistic/no-multiple-empty-lines)


[error] 151-151: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 154-154: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 155-155: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 157-157: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 253-253: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)


[error] 284-284: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 405-405: Forbidden non-null assertion.

(@typescript-eslint/no-non-null-assertion)


[error] 405-405: Forbidden non-null assertion.

(@typescript-eslint/no-non-null-assertion)


[error] 410-410: Forbidden non-null assertion.

(@typescript-eslint/no-non-null-assertion)


[error] 446-446: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 449-449: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 451-451: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 452-452: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 454-454: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 459-459: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 459-459: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 593-593: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 594-594: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 594-594: Trailing spaces not allowed.

(@stylistic/no-trailing-spaces)

front_end/panels/ai_chat/core/ToolNameMapping.test.ts

[error] 5-5: Incorrect same-namespace import: "./AgentNodes.js". Use "import * as Core from './core.js';" instead.

(rulesdir/es-modules-import)


[error] 5-5: 'createToolExecutorNode' is defined but never used.

(@typescript-eslint/no-unused-vars)


[error] 6-6: There should be at least one empty line between import groups

(import/order)


[error] 7-7: ../tools/Tools.js type import should occur before import of ./AgentNodes.js

(import/order)


[error] 8-8: There should be at least one empty line between import groups

(import/order)


[error] 8-8: ../models/ChatTypes.js import should occur before import of ./AgentNodes.js

(import/order)


[error] 9-9: Incorrect same-namespace import: "./ToolSurfaceProvider.js". Use "import * as Core from './core.js';" instead.

(rulesdir/es-modules-import)


[error] 9-9: There should be at least one empty line between import groups

(import/order)


[error] 11-11: ../LLM/LLMClient.js import should occur before import of ./AgentNodes.js

(import/order)


[error] 16-16: Public accessibility modifier on class property calls.

(@typescript-eslint/explicit-member-accessibility)


[error] 53-53: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 56-56: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 57-57: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 58-58: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 66-66: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 67-67: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 69-69: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 73-73: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 75-75: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 80-80: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 81-81: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

front_end/panels/ai_chat/mcp/MCPToolAdapter.ts

[error] 1-55: Missing license header

(rulesdir/check-license-header)


[error] 2-2: ../core/Logger.js import should occur before type import of ../tools/Tools.js

(import/order)


[error] 3-3: ../../../third_party/mcp-sdk/mcp-sdk.js type import should occur before type import of ../tools/Tools.js

(import/order)


[error] 10-10: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 24-24: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🪛 ast-grep (0.38.6)
front_end/panels/ai_chat/ui/SettingsDialog.ts

[warning] 2875-2875: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: mcpToolsList.innerHTML = ''
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)

🔇 Additional comments (7)
front_end/panels/ai_chat/agent_framework/ConfigurableAgentTool.ts (1)

199-200: Good: clearer registration log with interpolation

Switch to template literal improves diagnostics.

front_end/panels/ai_chat/mcp/MCPConfig.ts (1)

31-36: Good: token now uses sessionStorage (reduced XSS blast radius)

Moving tokens off localStorage is a solid improvement.

Also applies to: 54-64

front_end/panels/ai_chat/agent_framework/AgentRunner.ts (1)

976-989: Good: pass apiKey through tool execution context

This aligns tool calls with the per‑turn credential flow.

front_end/panels/ai_chat/core/AgentService.ts (1)

201-202: Fix ESLint: no-explicit-any.

These casts trigger @typescript-eslint/no-explicit-any. The previous refactor eliminates them.

Also applies to: 397-397

front_end/panels/ai_chat/mcp/MCPToolAdapter.ts (1)

32-36: Good: redaction only for logs; raw args sent to MCP.

front_end/panels/ai_chat/core/AgentNodes.ts (2)

197-205: Good: LLM tool names are sanitized for provider constraints.

Using ToolNameMap.getSanitized(tool.name) prevents OpenAI-style name rejections.


248-266: Trace uses original tool name; resolve looks correct.

You resolve parsedAction.name via ToolNameMap.resolveOriginal before use — prevents executor lookup failures.

Comment on lines +5 to +11
import { createAgentNode } from '../AgentNodes.js';
import type { AgentState } from '../State.js';
import { ChatMessageEntity } from '../../models/ChatTypes.js';
import { ToolSurfaceProvider } from '../ToolSurfaceProvider.js';
import { LLMClient } from '../../LLM/LLMClient.js';
import '../../agent_framework/ConfigurableAgentTool.js';

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Import assert and fix import order.

Add chai’s assert and satisfy import-order rules.

-import { createAgentNode } from '../AgentNodes.js';
-import type { AgentState } from '../State.js';
-import { ChatMessageEntity } from '../../models/ChatTypes.js';
-import { ToolSurfaceProvider } from '../ToolSurfaceProvider.js';
-import { LLMClient } from '../../LLM/LLMClient.js';
-import '../../agent_framework/ConfigurableAgentTool.js';
+import { ChatMessageEntity } from '../../models/ChatTypes.js';
+import { LLMClient } from '../../LLM/LLMClient.js';
+import { createAgentNode } from '../AgentNodes.js';
+import type { AgentState } from '../State.js';
+import { ToolSurfaceProvider } from '../ToolSurfaceProvider.js';
+import '../../agent_framework/ConfigurableAgentTool.js';
+import { assert } from 'chai';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { createAgentNode } from '../AgentNodes.js';
import type { AgentState } from '../State.js';
import { ChatMessageEntity } from '../../models/ChatTypes.js';
import { ToolSurfaceProvider } from '../ToolSurfaceProvider.js';
import { LLMClient } from '../../LLM/LLMClient.js';
import '../../agent_framework/ConfigurableAgentTool.js';
import { ChatMessageEntity } from '../../models/ChatTypes.js';
import { LLMClient } from '../../LLM/LLMClient.js';
import { createAgentNode } from '../AgentNodes.js';
import type { AgentState } from '../State.js';
import { ToolSurfaceProvider } from '../ToolSurfaceProvider.js';
import '../../agent_framework/ConfigurableAgentTool.js';
import { assert } from 'chai';
🧰 Tools
🪛 ESLint

[error] 7-7: ../../models/ChatTypes.js import should occur before import of ../AgentNodes.js

(import/order)


[error] 9-9: ../../LLM/LLMClient.js import should occur before import of ../AgentNodes.js

(import/order)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/__tests__/AgentNodesSanitize.test.ts around
lines 5 to 11, the test is missing chai's assert import and the current imports
violate the project's import-order rule; add "import { assert } from 'chai';"
and reorder the existing imports to follow the repository's import-order
convention (typically external packages first, then absolute/internal modules,
then relative paths), placing the chai import with other external imports so
linting passes.

Comment on lines +61 to +64
messages: [ { entity: ChatMessageEntity.USER, text: 'run' } as any ],
agentType: 'web_task',
context: {},
} as any;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use the correct field: selectedAgentType (not agentType). Remove any.

-    const state: AgentState = {
-      messages: [ { entity: ChatMessageEntity.USER, text: 'run' } as any ],
-      agentType: 'web_task',
-      context: {},
-    } as any;
+    const state: AgentState = {
+      messages: [ { entity: ChatMessageEntity.USER, text: 'run' } ],
+      selectedAgentType: 'web_task',
+      context: {},
+    };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
messages: [ { entity: ChatMessageEntity.USER, text: 'run' } as any ],
agentType: 'web_task',
context: {},
} as any;
const state: AgentState = {
messages: [ { entity: ChatMessageEntity.USER, text: 'run' } ],
selectedAgentType: 'web_task',
context: {},
};
🧰 Tools
🪛 ESLint

[error] 61-61: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 64-64: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/__tests__/AgentNodesSanitize.test.ts around
lines 61-64, the test object uses the wrong field name (agentType) and applies
unsafe any casts; change the field to selectedAgentType: 'web_task' and remove
the trailing "as any" casts, instead construct the object with the correct shape
or import/annotate the appropriate test type so the messages array and root
object are properly typed without using any.

Comment on lines 5 to 22
import type { getTools } from '../tools/Tools.js';
import { ChatMessageEntity, type ModelChatMessage, type ToolResultMessage, type ChatMessage, type AgentSessionMessage } from '../models/ChatTypes.js';
import { ConfigurableAgentTool } from '../agent_framework/ConfigurableAgentTool.js';
import { ConfigurableAgentTool, ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';

import { LLMClient } from '../LLM/LLMClient.js';
import type { LLMMessage } from '../LLM/LLMTypes.js';
import type { LLMProvider } from '../LLM/LLMTypes.js';
import { createSystemPromptAsync, getAgentToolsFromState } from './GraphHelpers.js';
import { createSystemPromptAsync } from './GraphHelpers.js';
import * as BaseOrchestratorAgent from './BaseOrchestratorAgent.js';
import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
import { createLogger } from './Logger.js';
import type { AgentState } from './State.js';
import type { Runnable } from './Types.js';
import { AgentErrorHandler } from './AgentErrorHandler.js';
import { createTracingProvider, withTracingContext } from '../tracing/TracingConfig.js';
import * as ToolNameMap from './ToolNameMap.js';
import type { TracingProvider } from '../tracing/TracingProvider.js';

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fix import ordering and duplicate imports (blocks lint/build).

  • Group type-only imports first, then value imports.
  • Combine duplicate imports from '../LLM/LLMTypes.js'.
-import type { getTools } from '../tools/Tools.js';
-import { ChatMessageEntity, type ModelChatMessage, type ToolResultMessage, type ChatMessage, type AgentSessionMessage } from '../models/ChatTypes.js';
-import { ConfigurableAgentTool, ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';
-
-import { LLMClient } from '../LLM/LLMClient.js';
-import type { LLMMessage } from '../LLM/LLMTypes.js';
-import type { LLMProvider } from '../LLM/LLMTypes.js';
-import { createSystemPromptAsync } from './GraphHelpers.js';
-import * as BaseOrchestratorAgent from './BaseOrchestratorAgent.js';
-import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
-import { createLogger } from './Logger.js';
-import type { AgentState } from './State.js';
-import type { Runnable } from './Types.js';
-import { AgentErrorHandler } from './AgentErrorHandler.js';
-import { createTracingProvider, withTracingContext } from '../tracing/TracingConfig.js';
-import * as ToolNameMap from './ToolNameMap.js';
-import type { TracingProvider } from '../tracing/TracingProvider.js';
+import type { getTools } from '../tools/Tools.js';
+import type { LLMMessage, LLMProvider } from '../LLM/LLMTypes.js';
+import type { AgentState } from './State.js';
+import type { Runnable } from './Types.js';
+import type { TracingProvider } from '../tracing/TracingProvider.js';
+
+import { ChatMessageEntity, type ModelChatMessage, type ToolResultMessage, type ChatMessage, type AgentSessionMessage } from '../models/ChatTypes.js';
+import { ConfigurableAgentTool, ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';
+import { LLMClient } from '../LLM/LLMClient.js';
+import { createSystemPromptAsync } from './GraphHelpers.js';
+import * as BaseOrchestratorAgent from './BaseOrchestratorAgent.js';
+import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
+import { createLogger } from './Logger.js';
+import { AgentErrorHandler } from './AgentErrorHandler.js';
+import { createTracingProvider, withTracingContext } from '../tracing/TracingConfig.js';
+import * as ToolNameMap from './ToolNameMap.js';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import type { getTools } from '../tools/Tools.js';
import { ChatMessageEntity, type ModelChatMessage, type ToolResultMessage, type ChatMessage, type AgentSessionMessage } from '../models/ChatTypes.js';
import { ConfigurableAgentTool } from '../agent_framework/ConfigurableAgentTool.js';
import { ConfigurableAgentTool, ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';
import { LLMClient } from '../LLM/LLMClient.js';
import type { LLMMessage } from '../LLM/LLMTypes.js';
import type { LLMProvider } from '../LLM/LLMTypes.js';
import { createSystemPromptAsync, getAgentToolsFromState } from './GraphHelpers.js';
import { createSystemPromptAsync } from './GraphHelpers.js';
import * as BaseOrchestratorAgent from './BaseOrchestratorAgent.js';
import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
import { createLogger } from './Logger.js';
import type { AgentState } from './State.js';
import type { Runnable } from './Types.js';
import { AgentErrorHandler } from './AgentErrorHandler.js';
import { createTracingProvider, withTracingContext } from '../tracing/TracingConfig.js';
import * as ToolNameMap from './ToolNameMap.js';
import type { TracingProvider } from '../tracing/TracingProvider.js';
import type { getTools } from '../tools/Tools.js';
import type { LLMMessage, LLMProvider } from '../LLM/LLMTypes.js';
import type { AgentState } from './State.js';
import type { Runnable } from './Types.js';
import type { TracingProvider } from '../tracing/TracingProvider.js';
import { ChatMessageEntity, type ModelChatMessage, type ToolResultMessage, type ChatMessage, type AgentSessionMessage } from '../models/ChatTypes.js';
import { ConfigurableAgentTool, ToolRegistry } from '../agent_framework/ConfigurableAgentTool.js';
import { LLMClient } from '../LLM/LLMClient.js';
import { createSystemPromptAsync } from './GraphHelpers.js';
import * as BaseOrchestratorAgent from './BaseOrchestratorAgent.js';
import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
import { createLogger } from './Logger.js';
import { AgentErrorHandler } from './AgentErrorHandler.js';
import { createTracingProvider, withTracingContext } from '../tracing/TracingConfig.js';
import * as ToolNameMap from './ToolNameMap.js';
🧰 Tools
🪛 ESLint

[error] 5-5: ../tools/Tools.js type import should occur after type import of ../LLM/LLMTypes.js

(import/order)


[error] 6-6: ../models/ChatTypes.js import should occur after type import of ../LLM/LLMTypes.js

(import/order)


[error] 7-7: There should be no empty line within import group

(import/order)


[error] 10-10: '../LLM/LLMTypes.js' imported multiple times.

(import/no-duplicates)


[error] 11-11: There should be at least one empty line between import groups

(import/order)


[error] 11-11: '../LLM/LLMTypes.js' imported multiple times.

(import/no-duplicates)


[error] 12-12: ./GraphHelpers.js import should occur after type import of ../tracing/TracingProvider.js

(import/order)


[error] 13-13: ./BaseOrchestratorAgent.js import should occur after type import of ../tracing/TracingProvider.js

(import/order)


[error] 14-14: ./ToolSurfaceProvider.js import should occur after type import of ../tracing/TracingProvider.js

(import/order)


[error] 15-15: ./Logger.js import should occur after type import of ../tracing/TracingProvider.js

(import/order)


[error] 16-16: ./State.js type import should occur after type import of ../tracing/TracingProvider.js

(import/order)


[error] 17-17: ./Types.js type import should occur after type import of ../tracing/TracingProvider.js

(import/order)


[error] 18-18: There should be at least one empty line between import groups

(import/order)


[error] 18-18: ./AgentErrorHandler.js import should occur after type import of ../tracing/TracingProvider.js

(import/order)


[error] 19-19: There should be at least one empty line between import groups

(import/order)


[error] 20-20: There should be at least one empty line between import groups

(import/order)


[error] 20-20: ./ToolNameMap.js import should occur after type import of ../tracing/TracingProvider.js

(import/order)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/AgentNodes.ts around lines 5–22, imports are
mis-ordered and LLMTypes is imported twice; group all type-only imports first
(using "import type" for types like getTools, ModelChatMessage,
ToolResultMessage, ChatMessage, AgentSessionMessage, LLMMessage, LLMProvider,
AgentState, Runnable, TracingProvider), then list value imports
(ChatMessageEntity, ConfigurableAgentTool, ToolRegistry, LLMClient,
createSystemPromptAsync, BaseOrchestratorAgent, ToolSurfaceProvider,
createLogger, AgentErrorHandler, createTracingProvider, withTracingContext,
ToolNameMap), and combine the two separate imports from '../LLM/LLMTypes.js'
into a single "import type" statement to remove duplication and satisfy the
linter/build.

Comment on lines +200 to +203
// Stash apiKey in state context for downstream tools that need it
if (!this.#state.context) { (this.#state as any).context = {}; }
(this.#state.context as any).apiKey = apiKey || '';

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Do not persist API key in AgentService state.

Persisting secrets in long-lived state risks leakage via future logs/tracing/serialization. Inject per-run only (you already do this later).

Apply this diff to drop the persistent stash:

-      // Stash apiKey in state context for downstream tools that need it
-      if (!this.#state.context) { (this.#state as any).context = {}; }
-      (this.#state.context as any).apiKey = apiKey || '';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Stash apiKey in state context for downstream tools that need it
if (!this.#state.context) { (this.#state as any).context = {}; }
(this.#state.context as any).apiKey = apiKey || '';
🧰 Tools
🪛 ESLint

[error] 201-201: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 202-202: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/AgentService.ts around lines 200 to 203, the
code persists the API key into this.#state.context which stores secrets in
long‑lived state; remove the lines that set this.#state.context and do not stash
apiKey on the instance. Instead, pass the apiKey into the per-run call/site that
needs it (or attach it to the transient run context already used later), and
audit any downstream usages to accept an injected per-run apiKey parameter
rather than reading from this.#state.context.

Comment on lines +396 to +398
// Inject API key into context for tool execution paths (ConfigurableAgentTool)
try { (state as any).context.apiKey = this.#apiKey || ''; } catch {}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Type-safe, per-run secret injection (no any).

Remove any and keep the key in a dedicated secrets bag on context.

Apply:

-      // Inject API key into context for tool execution paths (ConfigurableAgentTool)
-      try { (state as any).context.apiKey = this.#apiKey || ''; } catch {}
+      // Inject API key for tool execution; keep out of logs/serialization
+      type CtxWithSecrets = AgentState['context'] & { secrets?: { apiKey?: string } };
+      const ctx = state.context as CtxWithSecrets;
+      ctx.secrets = { ...(ctx.secrets ?? {}), apiKey: this.#apiKey ?? undefined };

Follow-up: extend DevToolsContext to include secrets?: { apiKey?: string } so casts can be removed entirely.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Inject API key into context for tool execution paths (ConfigurableAgentTool)
try { (state as any).context.apiKey = this.#apiKey || ''; } catch {}
// Inject API key for tool execution; keep out of logs/serialization
type CtxWithSecrets = AgentState['context'] & { secrets?: { apiKey?: string } };
const ctx = state.context as CtxWithSecrets;
ctx.secrets = { ...(ctx.secrets ?? {}), apiKey: this.#apiKey ?? undefined };
🧰 Tools
🪛 ESLint

[error] 397-397: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/AgentService.ts around lines 396-398, replace
the unsafe (state as any).context.apiKey injection by placing the key into a
dedicated secrets bag on context: ensure state.context exists, ensure
state.context.secrets is an object (create if missing), and set
state.context.secrets.apiKey = this.#apiKey || ''. Remove the any cast so access
is type-checked; update call sites/types as needed and as a follow-up extend
DevToolsContext to declare secrets?: { apiKey?: string } so the cast can be
eliminated entirely.

Comment on lines +5 to +12
import { createAgentNode, createToolExecutorNode } from './AgentNodes.js';
import type { AgentState } from './State.js';
import type { Tool } from '../tools/Tools.js';
import { ChatMessageEntity } from '../models/ChatTypes.js';
import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
import '../agent_framework/ConfigurableAgentTool.js';
import { LLMClient } from '../LLM/LLMClient.js';

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Trim imports, add assert, and fix order.

-import { createAgentNode, createToolExecutorNode } from './AgentNodes.js';
+import { createAgentNode } from './AgentNodes.js';
 import type { AgentState } from './State.js';
 import type { Tool } from '../tools/Tools.js';
 import { ChatMessageEntity } from '../models/ChatTypes.js';
 import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
 import '../agent_framework/ConfigurableAgentTool.js';
 import { LLMClient } from '../LLM/LLMClient.js';
+import { assert } from 'chai';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { createAgentNode, createToolExecutorNode } from './AgentNodes.js';
import type { AgentState } from './State.js';
import type { Tool } from '../tools/Tools.js';
import { ChatMessageEntity } from '../models/ChatTypes.js';
import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
import '../agent_framework/ConfigurableAgentTool.js';
import { LLMClient } from '../LLM/LLMClient.js';
import { createAgentNode } from './AgentNodes.js';
import type { AgentState } from './State.js';
import type { Tool } from '../tools/Tools.js';
import { ChatMessageEntity } from '../models/ChatTypes.js';
import { ToolSurfaceProvider } from './ToolSurfaceProvider.js';
import '../agent_framework/ConfigurableAgentTool.js';
import { LLMClient } from '../LLM/LLMClient.js';
import { assert } from 'chai';
🧰 Tools
🪛 ESLint

[error] 5-5: Incorrect same-namespace import: "./AgentNodes.js". Use "import * as Core from './core.js';" instead.

(rulesdir/es-modules-import)


[error] 5-5: 'createToolExecutorNode' is defined but never used.

(@typescript-eslint/no-unused-vars)


[error] 6-6: There should be at least one empty line between import groups

(import/order)


[error] 7-7: ../tools/Tools.js type import should occur before import of ./AgentNodes.js

(import/order)


[error] 8-8: There should be at least one empty line between import groups

(import/order)


[error] 8-8: ../models/ChatTypes.js import should occur before import of ./AgentNodes.js

(import/order)


[error] 9-9: Incorrect same-namespace import: "./ToolSurfaceProvider.js". Use "import * as Core from './core.js';" instead.

(rulesdir/es-modules-import)


[error] 9-9: There should be at least one empty line between import groups

(import/order)


[error] 11-11: ../LLM/LLMClient.js import should occur before import of ./AgentNodes.js

(import/order)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/ToolNameMapping.test.ts around lines 5 to 12,
trim unused imports, add a missing assert import, and reorder imports for
clarity: remove imports that aren’t referenced in the test (e.g.,
ToolSurfaceProvider, ConfigurableAgentTool, LLMClient if they are unused), keep
only the required imports (createAgentNode, createToolExecutorNode, AgentState
type, Tool type, ChatMessageEntity), add an import for assert (import assert
from 'assert';) and reorder so external modules/types are grouped before local
module imports for readability.

Comment on lines +65 to +69
const initial: AgentState = {
messages: [ { entity: ChatMessageEntity.USER, text: 'go' } as any ],
agentType: 'deep-research' as any,
context: {},
} as any;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use selectedAgentType and tighten types; remove any.

-    const initial: AgentState = {
-      messages: [ { entity: ChatMessageEntity.USER, text: 'go' } as any ],
-      agentType: 'deep-research' as any,
-      context: {},
-    } as any;
+    const initial: AgentState = {
+      messages: [ { entity: ChatMessageEntity.USER, text: 'go' } ],
+      selectedAgentType: 'deep-research',
+      context: {},
+    };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const initial: AgentState = {
messages: [ { entity: ChatMessageEntity.USER, text: 'go' } as any ],
agentType: 'deep-research' as any,
context: {},
} as any;
const initial: AgentState = {
messages: [ { entity: ChatMessageEntity.USER, text: 'go' } ],
selectedAgentType: 'deep-research',
context: {},
};
🧰 Tools
🪛 ESLint

[error] 66-66: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 67-67: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 69-69: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/core/ToolNameMapping.test.ts around lines 65 to 69,
the test uses broad `any` casts and the legacy `agentType` field; replace
`agentType` with the correct `selectedAgentType` property and tighten types by
using the actual AgentState and ChatMessage types (or imports) instead of `as
any`. Ensure the messages array uses the proper ChatMessage/ChatMessageEntity
type for the user message and that the AgentState object is typed explicitly
(remove all `as any`), updating imports if necessary so the test compiles with
strict typing.

export class MCPToolAdapter implements Tool<Record<string, unknown>, unknown> {
name: string;
description: string;
schema: any;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove any from schema; match Tool interface shape.

-  schema: any;
+  schema: { type: string; properties: Record<string, unknown>; required?: string[] };
@@
-    const schema = def.inputSchema as any;
-    if (schema && typeof schema === 'object') {
-      this.schema = schema;
+    const schema = def.inputSchema as Partial<{ type: string; properties: Record<string, unknown>; required?: string[] }> | undefined;
+    if (schema && typeof schema === 'object' && schema.type === 'object' && schema.properties) {
+      this.schema = { type: 'object', properties: schema.properties, required: schema.required };
     } else {
       this.schema = { type: 'object', properties: {} };
     }

Also applies to: 24-29

🧰 Tools
🪛 ESLint

[error] 10-10: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/mcp/MCPToolAdapter.ts around lines 10 and also
24-29, the schema property is typed as any and other related types do not match
the Tool interface; replace any with the exact shape from the Tool interface (or
import the Tool type and use it) so the class fields and method signatures
conform to Tool. Update the file to import or declare the Tool type, type schema
and any related properties/method params to Tool (or Tool['schema'] /
appropriate subtype) and adjust usages to satisfy the interface (add missing
fields/return types) so the adapter implements the Tool interface without any
anys.

Comment on lines +2496 to +2501
// ---- MCP Integration Section ----
const mcpSection = document.createElement('div');
mcpSection.className = 'settings-section mcp-section';
// Hide MCP UI: auto-connect is always on; settings are not user-configurable
mcpSection.style.display = 'none';
contentDiv.appendChild(mcpSection);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

MCP section is intentionally hidden but toggleAdvancedSections reveals it.

Guard against re-showing hidden MCP UI.

-    mcpSection.style.display = 'none';
+    mcpSection.style.display = 'none';
+    mcpSection.dataset.hiddenByDefault = 'true';
-      const mcpSectionEl = contentDiv.querySelector('.mcp-section') as HTMLElement | null;
-      if (mcpSectionEl) { mcpSectionEl.style.display = display; }
+      const mcpSectionEl = contentDiv.querySelector('.mcp-section') as HTMLElement | null;
+      if (mcpSectionEl && mcpSectionEl.dataset.hiddenByDefault !== 'true') {
+        mcpSectionEl.style.display = display;
+      }

Also applies to: 3274-3276

🤖 Prompt for AI Agents
In front_end/panels/ai_chat/ui/SettingsDialog.ts around lines 2496-2501 and
3274-3276 the MCP section is hidden via style.display = 'none' but
toggleAdvancedSections can re-show it; mark the MCP container as permanently
locked (e.g., set a data attribute or CSS class like data-locked="true" or
.locked-mcp) and update the toggleAdvancedSections logic to skip/showing any
section with that marker so the MCP UI remains hidden even when advanced
sections are toggled.

Comment on lines +2561 to +2582
const formatTimestamp = (date: Date | undefined): string => {
if (!date) return '';
return date.toLocaleString();
};

const formatMCPError = (error: string, errorType?: string): {message: string, hint?: string} => {
if (!errorType) return {message: error};
switch (errorType) {
case 'connection':
return {message: `Connection failed: ${error}`, hint: 'Check if the MCP server is running and the endpoint URL is correct.'};
case 'authentication':
return {message: `Authentication failed: ${error}`, hint: 'Verify your auth token is correct and has not expired.'};
case 'configuration':
return {message: `Configuration error: ${error}`, hint: 'Check your endpoint URL format (should be ws:// or wss://).'};
case 'network':
return {message: `Network error: ${error}`, hint: 'Check your internet connection and firewall settings.'};
case 'server_error':
return {message: `Server error: ${error}`, hint: 'The MCP server encountered an internal error. Contact the server administrator.'};
default:
return {message: error};
}
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add braces and explicit return types for helpers (curly/explicit-function-return-type).

Also return structured error text without HTML.

-    const formatTimestamp = (date: Date | undefined): string => {
-      if (!date) return '';
-      return date.toLocaleString();
-    };
+    const formatTimestamp = (date: Date | undefined): string => {
+      if (!date) { return ''; }
+      return date.toLocaleString();
+    };
 
-    const formatMCPError = (error: string, errorType?: string): {message: string, hint?: string} => {
-      if (!errorType) return {message: error};
+    const formatMCPError = (error: string, errorType?: string): {message: string, hint?: string} => {
+      if (!errorType) { return {message: error}; }
       switch (errorType) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const formatTimestamp = (date: Date | undefined): string => {
if (!date) return '';
return date.toLocaleString();
};
const formatMCPError = (error: string, errorType?: string): {message: string, hint?: string} => {
if (!errorType) return {message: error};
switch (errorType) {
case 'connection':
return {message: `Connection failed: ${error}`, hint: 'Check if the MCP server is running and the endpoint URL is correct.'};
case 'authentication':
return {message: `Authentication failed: ${error}`, hint: 'Verify your auth token is correct and has not expired.'};
case 'configuration':
return {message: `Configuration error: ${error}`, hint: 'Check your endpoint URL format (should be ws:// or wss://).'};
case 'network':
return {message: `Network error: ${error}`, hint: 'Check your internet connection and firewall settings.'};
case 'server_error':
return {message: `Server error: ${error}`, hint: 'The MCP server encountered an internal error. Contact the server administrator.'};
default:
return {message: error};
}
};
const formatTimestamp = (date: Date | undefined): string => {
if (!date) { return ''; }
return date.toLocaleString();
};
const formatMCPError = (error: string, errorType?: string): {message: string, hint?: string} => {
if (!errorType) { return {message: error}; }
switch (errorType) {
case 'connection':
return {message: `Connection failed: ${error}`, hint: 'Check if the MCP server is running and the endpoint URL is correct.'};
case 'authentication':
return {message: `Authentication failed: ${error}`, hint: 'Verify your auth token is correct and has not expired.'};
case 'configuration':
return {message: `Configuration error: ${error}`, hint: 'Check your endpoint URL format (should be ws:// or wss://).'};
case 'network':
return {message: `Network error: ${error}`, hint: 'Check your internet connection and firewall settings.'};
case 'server_error':
return {message: `Server error: ${error}`, hint: 'The MCP server encountered an internal error. Contact the server administrator.'};
default:
return {message: error};
}
};
🧰 Tools
🪛 ESLint

[error] 2562-2562: Expected { after 'if' condition.

(curly)


[error] 2567-2567: Expected { after 'if' condition.

(curly)

@tysonthomas9 tysonthomas9 merged commit 24e6dc7 into main Sep 13, 2025
12 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Oct 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants