Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/cli/src/types/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { reasoningEffortsExtended } from "@roo-code/types"
import { reasoningEffortsExtended, MAX_MCP_TOOLS_THRESHOLD } from "@roo-code/types"

// Re-export for backward compatibility
export { MAX_MCP_TOOLS_THRESHOLD }

export const DEFAULT_FLAGS = {
mode: "code",
Expand Down
6 changes: 6 additions & 0 deletions packages/types/src/mcp.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { z } from "zod"

/**
* Maximum number of MCP tools that can be enabled before showing a warning.
* LLMs tend to perform poorly when given too many tools to choose from.
*/
export const MAX_MCP_TOOLS_THRESHOLD = 40

/**
* McpServerUse
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export function isNonBlockingAsk(ask: ClineAsk): ask is NonBlockingAsk {
* - `condense_context`: Context condensation/summarization has started
* - `condense_context_error`: Error occurred during context condensation
* - `codebase_search_result`: Results from searching the codebase
* - `too_many_tools_warning`: Warning that too many MCP tools are enabled, which may confuse the LLM
*/
export const clineSays = [
"error",
Expand Down Expand Up @@ -180,6 +181,7 @@ export const clineSays = [
"sliding_window_truncation",
"codebase_search_result",
"user_edit_todos",
"too_many_tools_warning",
] as const

export const clineSaySchema = z.enum(clineSays)
Expand Down
72 changes: 72 additions & 0 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
MIN_CHECKPOINT_TIMEOUT_SECONDS,
TOOL_PROTOCOL,
ConsecutiveMistakeError,
MAX_MCP_TOOLS_THRESHOLD,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { CloudService, BridgeOrchestrator } from "@roo-code/cloud"
Expand Down Expand Up @@ -1832,6 +1833,59 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Lifecycle
// Start / Resume / Abort / Dispose

/**
* Count the number of enabled MCP tools across all enabled and connected servers.
* Returns the count along with the number of servers contributing.
*
* @returns Object with enabledToolCount and enabledServerCount
*/
private async countEnabledMcpTools(): Promise<{ enabledToolCount: number; enabledServerCount: number }> {
let serverCount = 0
let toolCount = 0

try {
const provider = this.providerRef.deref()
if (!provider) {
return { enabledToolCount: 0, enabledServerCount: 0 }
}

const { mcpEnabled } = (await provider.getState()) ?? {}
if (!(mcpEnabled ?? true)) {
return { enabledToolCount: 0, enabledServerCount: 0 }
}

const mcpHub = await McpServerManager.getInstance(provider.context, provider)
if (!mcpHub) {
return { enabledToolCount: 0, enabledServerCount: 0 }
}

const servers = mcpHub.getServers()
for (const server of servers) {
// Skip disabled servers
if (server.disabled) continue

// Skip servers that are not connected
if (server.status !== "connected") continue

serverCount++

// Count enabled tools on this server
if (server.tools) {
for (const tool of server.tools) {
// Tool is enabled if enabledForPrompt is undefined (default) or true
if (tool.enabledForPrompt !== false) {
toolCount++
}
}
}
}
} catch (error) {
console.error("[Task#countEnabledMcpTools] Error counting MCP tools:", error)
}

return { enabledToolCount: toolCount, enabledServerCount: serverCount }
}

private async startTask(task?: string, images?: string[]): Promise<void> {
if (this.enableBridge) {
try {
Expand All @@ -1858,6 +1912,24 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
await this.providerRef.deref()?.postStateToWebview()

await this.say("text", task, images)

// Check for too many MCP tools and warn the user
const { enabledToolCount, enabledServerCount } = await this.countEnabledMcpTools()
if (enabledToolCount > MAX_MCP_TOOLS_THRESHOLD) {
await this.say(
"too_many_tools_warning",
JSON.stringify({
toolCount: enabledToolCount,
serverCount: enabledServerCount,
threshold: MAX_MCP_TOOLS_THRESHOLD,
}),
undefined,
undefined,
undefined,
undefined,
{ isNonInteractive: true },
)
}
this.isInitialized = true

let imageBlocks: Anthropic.ImageBlockParam[] = formatResponse.imageBlocks(images)
Expand Down
21 changes: 21 additions & 0 deletions webview-ui/src/components/chat/ChatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { ReasoningBlock } from "./ReasoningBlock"
import Thumbnails from "../common/Thumbnails"
import ImageBlock from "../common/ImageBlock"
import ErrorRow from "./ErrorRow"
import WarningRow from "./WarningRow"

import McpResourceRow from "../mcp/McpResourceRow"

Expand Down Expand Up @@ -1512,6 +1513,26 @@ export const ChatRowContent = ({
case "browser_action_result":
// Handled by BrowserSessionRow; prevent raw JSON (action/result) from rendering here
return null
case "too_many_tools_warning": {
const warningData = safeJsonParse<{
toolCount: number
serverCount: number
threshold: number
}>(message.text || "{}")
if (!warningData) return null
const toolsPart = t("chat:tooManyTools.toolsPart", { count: warningData.toolCount })
const serversPart = t("chat:tooManyTools.serversPart", { count: warningData.serverCount })
return (
<WarningRow
title={t("chat:tooManyTools.title")}
message={t("chat:tooManyTools.messageTemplate", {
tools: toolsPart,
servers: serversPart,
threshold: warningData.threshold,
})}
/>
)
}
default:
return (
<>
Expand Down
4 changes: 2 additions & 2 deletions webview-ui/src/components/chat/ErrorRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -266,11 +266,11 @@ export const ErrorRow = memo(
</div>
</div>
)}
<div className="ml-2 pl-4 mt-1 pt-1 border-l border-vscode-errorForeground/50">
<div className="ml-2 pl-4 mt-1 pt-0.5 border-l border-vscode-errorForeground/50">
<p
className={
messageClassName ||
"my-0 font-light whitespace-pre-wrap break-words text-vscode-descriptionForeground"
"cursor-default my-0 font-light whitespace-pre-wrap break-words text-vscode-descriptionForeground"
}>
{message}
{formattedErrorDetails && (
Expand Down
67 changes: 67 additions & 0 deletions webview-ui/src/components/chat/TooManyToolsWarning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { useMemo } from "react"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { MAX_MCP_TOOLS_THRESHOLD } from "@roo-code/types"
import WarningRow from "./WarningRow"

// Re-export for backward compatibility
export { MAX_MCP_TOOLS_THRESHOLD }

/**
* Displays a warning when the user has too many MCP tools enabled.
* LLMs get confused when offered too many tools, which can lead to errors.
*
* The warning is shown when:
* - The total number of enabled tools across all enabled MCP servers exceeds the threshold
*
* @example
* <TooManyToolsWarning />
*/
export const TooManyToolsWarning: React.FC = () => {
const { t } = useAppTranslation()
const { mcpServers } = useExtensionState()

const { enabledServerCount, enabledToolCount } = useMemo(() => {
let serverCount = 0
let toolCount = 0

for (const server of mcpServers) {
// Skip disabled servers
if (server.disabled) continue

// Skip servers that are not connected
if (server.status !== "connected") continue

serverCount++

// Count enabled tools on this server
if (server.tools) {
for (const tool of server.tools) {
// Tool is enabled if enabledForPrompt is undefined (default) or true
if (tool.enabledForPrompt !== false) {
toolCount++
}
}
}
}

return { enabledServerCount: serverCount, enabledToolCount: toolCount }
}, [mcpServers])

// Don't show warning if under threshold
if (enabledToolCount <= MAX_MCP_TOOLS_THRESHOLD) {
return null
}

const toolsPart = t("chat:tooManyTools.toolsPart", { count: enabledToolCount })
const serversPart = t("chat:tooManyTools.serversPart", { count: enabledServerCount })
const message = t("chat:tooManyTools.messageTemplate", {
tools: toolsPart,
servers: serversPart,
threshold: MAX_MCP_TOOLS_THRESHOLD,
})

return <WarningRow title={t("chat:tooManyTools.title")} message={message} />
}

export default TooManyToolsWarning
57 changes: 57 additions & 0 deletions webview-ui/src/components/chat/WarningRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from "react"
import { TriangleAlert, BookOpenText } from "lucide-react"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { vscode } from "@src/utils/vscode"

export interface WarningRowProps {
title: string
message: string
docsURL?: string
}

/**
* A generic warning row component that displays a warning icon, title, and message.
* Optionally includes a documentation link.
*
* @param title - The warning title displayed in bold
* @param message - The warning message displayed below the title
* @param docsURL - Optional documentation link URL (shown as "Learn more" with book icon)
*
* @example
* <WarningRow
* title="Too many tools enabled"
* message="You have 50 tools enabled via 5 MCP servers."
* docsURL="https://docs.example.com/mcp-best-practices"
* />
*/
export const WarningRow: React.FC<WarningRowProps> = ({ title, message, docsURL }) => {
const { t } = useAppTranslation()

return (
<div className="group pr-2 py-2">
<div className="flex items-center justify-between gap-2 break-words">
<TriangleAlert className="w-4 text-vscode-editorWarning-foreground shrink-0" />
<span className="font-bold text-vscode-editorWarning-foreground grow cursor-default">{title}</span>
{docsURL && (
<a
href={docsURL}
className="text-sm flex items-center gap-1 transition-opacity opacity-0 group-hover:opacity-100"
onClick={(e) => {
e.preventDefault()
vscode.postMessage({ type: "openExternal", url: docsURL })
}}>
<BookOpenText className="size-3 mt-[3px]" />
{t("chat:apiRequest.errorMessage.docs")}
</a>
)}
</div>
<div className="cursor-default ml-2 pl-4 mt-1 pt-0.5 border-l border-vscode-editorWarning-foreground/50">
<p className="my-0 font-light whitespace-pre-wrap break-words text-vscode-descriptionForeground">
{message}
</p>
</div>
</div>
)
}

export default WarningRow
Loading
Loading