Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
56 changes: 56 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 = 60

/**
* McpServerUse
*/
Expand Down Expand Up @@ -128,3 +134,53 @@ export type McpErrorEntry = {
timestamp: number
level: "error" | "warn" | "info"
}

/**
* Result of counting enabled MCP tools across servers.
*/
export interface EnabledMcpToolsCount {
/** Number of enabled and connected MCP servers */
enabledServerCount: number
/** Total number of enabled tools across all enabled servers */
enabledToolCount: number
}

/**
* Count the number of enabled MCP tools across all enabled and connected servers.
* This is a pure function that can be used in both backend and frontend contexts.
*
* @param servers - Array of MCP server objects
* @returns Object with enabledToolCount and enabledServerCount
*
* @example
* const { enabledToolCount, enabledServerCount } = countEnabledMcpTools(mcpServers)
* if (enabledToolCount > MAX_MCP_TOOLS_THRESHOLD) {
* // Show warning
* }
*/
export function countEnabledMcpTools(servers: McpServer[]): EnabledMcpToolsCount {
let serverCount = 0
let toolCount = 0

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++
}
}
}
}

return { enabledToolCount: toolCount, enabledServerCount: serverCount }
}
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
51 changes: 51 additions & 0 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ import {
MIN_CHECKPOINT_TIMEOUT_SECONDS,
TOOL_PROTOCOL,
ConsecutiveMistakeError,
MAX_MCP_TOOLS_THRESHOLD,
countEnabledMcpTools,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { CloudService, BridgeOrchestrator } from "@roo-code/cloud"
Expand Down Expand Up @@ -1832,6 +1834,37 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Lifecycle
// Start / Resume / Abort / Dispose

/**
* Get enabled MCP tools count for this task.
* Returns the count along with the number of servers contributing.
*
* @returns Object with enabledToolCount and enabledServerCount
*/
private async getEnabledMcpToolsCount(): Promise<{ enabledToolCount: number; enabledServerCount: number }> {
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()
return countEnabledMcpTools(servers)
} catch (error) {
console.error("[Task#getEnabledMcpToolsCount] Error counting MCP tools:", error)
return { enabledToolCount: 0, enabledServerCount: 0 }
}
}

private async startTask(task?: string, images?: string[]): Promise<void> {
if (this.enableBridge) {
try {
Expand All @@ -1858,6 +1891,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.getEnabledMcpToolsCount()
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
28 changes: 28 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,33 @@ 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,
})}
actionText={t("chat:tooManyTools.openMcpSettings")}
onAction={() =>
window.postMessage(
{ type: "action", action: "settingsButtonClicked", values: { section: "mcp" } },
"*",
)
}
/>
)
}
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
39 changes: 39 additions & 0 deletions webview-ui/src/components/chat/TooManyToolsWarning.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { useCallback } from "react"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { useTooManyTools } from "@src/hooks/useTooManyTools"
import WarningRow from "./WarningRow"

/**
* 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 { isOverThreshold, title, message } = useTooManyTools()

const handleOpenMcpSettings = useCallback(() => {
window.postMessage({ type: "action", action: "settingsButtonClicked", values: { section: "mcp" } }, "*")
}, [])

// Don't show warning if under threshold
if (!isOverThreshold) {
return null
}

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

export default TooManyToolsWarning
77 changes: 77 additions & 0 deletions webview-ui/src/components/chat/WarningRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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
actionText?: string
onAction?: () => void
}

/**
* A generic warning row component that displays a warning icon, title, and message.
* Optionally includes a documentation link and/or an action 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)
* @param actionText - Optional text for an action link appended to the message
* @param onAction - Optional callback when the action link is clicked
*
* @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"
* actionText="Open MCP Settings"
* onAction={() => openSettings()}
* />
*/
export const WarningRow: React.FC<WarningRowProps> = ({ title, message, docsURL, actionText, onAction }) => {
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}
{actionText && onAction && (
<>
{" "}
<a
href="#"
className="text-vscode-textLink-foreground hover:text-vscode-textLink-activeForeground cursor-pointer"
onClick={(e) => {
e.preventDefault()
onAction()
}}>
{actionText}
</a>
</>
)}
</p>
</div>
</div>
)
}

export default WarningRow
Loading
Loading