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
50 changes: 50 additions & 0 deletions backend/mcp/many_tools_demo/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
"""
MCP Server with 64 tools for testing UI with many tools.
Demonstrates that the collapsible UI can handle servers with large numbers of tools.
"""

from fastmcp import FastMCP

# Create the MCP server
mcp = FastMCP("ManyToolsDemo")

# Generate 64 tools dynamically to test UI scalability
# Categories: data, analytics, file, network, system, database, security, report

TOOL_CATEGORIES = [
("data", 10, "Process and transform data"),
("analytics", 10, "Analyze data and generate insights"),
("file", 8, "File operations and management"),
("network", 8, "Network operations and monitoring"),
("system", 8, "System administration tasks"),
("database", 8, "Database operations"),
("security", 6, "Security and encryption tasks"),
("report", 6, "Report generation and formatting"),
]

# Dynamically create tools for each category
for category, count, description_base in TOOL_CATEGORIES:
for i in range(1, count + 1):
tool_name = f"{category}_operation_{i}"
tool_description = f"{description_base} - Operation {i}"

# Use exec to create properly named functions
exec(f"""
@mcp.tool()
def {tool_name}(input_data: str = "default") -> str:
'''
{tool_description}
Args:
input_data: Input data to process
Returns:
str: Result of the operation
'''
return f"Executed {tool_name} with input: {{input_data}}"
""")
Comment on lines +27 to +46
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

Using exec() to dynamically create functions is a security risk and violates codebase security practices. The code-executor server explicitly forbids exec() for safety reasons (see backend/mcp/code-executor/main.py line 191).

Instead, define a factory function that creates closures or use partial functions. For example:

def create_tool_function(category: str, i: int, description_base: str):
    def tool_func(input_data: str = "default") -> str:
        f'''
        {description_base} - Operation {i}
        
        Args:
            input_data: Input data to process
            
        Returns:
            str: Result of the operation
        '''
        tool_name = f"{category}_operation_{i}"
        return f"Executed {tool_name} with input: {input_data}"
    
    tool_func.__name__ = f"{category}_operation_{i}"
    tool_func.__doc__ = f"{description_base} - Operation {i}\n\nArgs:\n    input_data: Input data to process\n\nReturns:\n    str: Result of the operation"
    return tool_func

for category, count, description_base in TOOL_CATEGORIES:
    for i in range(1, count + 1):
        tool_func = create_tool_function(category, i, description_base)
        mcp.tool()(tool_func)

This approach is safer and more maintainable.

Suggested change
for category, count, description_base in TOOL_CATEGORIES:
for i in range(1, count + 1):
tool_name = f"{category}_operation_{i}"
tool_description = f"{description_base} - Operation {i}"
# Use exec to create properly named functions
exec(f"""
@mcp.tool()
def {tool_name}(input_data: str = "default") -> str:
'''
{tool_description}
Args:
input_data: Input data to process
Returns:
str: Result of the operation
'''
return f"Executed {tool_name} with input: {{input_data}}"
""")
def create_tool_function(category: str, i: int, description_base: str):
tool_name = f"{category}_operation_{i}"
tool_description = f"{description_base} - Operation {i}"
def tool_func(input_data: str = "default") -> str:
'''
{tool_description}
Args:
input_data: Input data to process
Returns:
str: Result of the operation
'''
return f"Executed {tool_name} with input: {input_data}"
tool_func.__name__ = tool_name
tool_func.__doc__ = f"{tool_description}\n\nArgs:\n input_data: Input data to process\n\nReturns:\n str: Result of the operation"
return tool_func
for category, count, description_base in TOOL_CATEGORIES:
for i in range(1, count + 1):
tool_func = create_tool_function(category, i, description_base)
mcp.tool()(tool_func)

Copilot uses AI. Check for mistakes.

if __name__ == "__main__":
mcp.run()

15 changes: 15 additions & 0 deletions config/defaults/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,5 +119,20 @@
],
"description": "Mock HTTP MCP server for testing authentication",
"auth_token": "${MCP_EXTERNAL_API_TOKEN}"
},
"many_tools_demo": {
"command": [
"python",
"mcp/many_tools_demo/main.py"
],
"cwd": "backend",
"groups": [
"users"
],
"description": "Demo server with 64 tools to test UI scalability with large numbers of tools. Includes operations for data, analytics, files, network, system, database, security, and reporting.",
"author": "Chat UI Team",
"short_description": "Large tool set demo (64 tools)",
"help_email": "support@chatui.example.com",
"compliance_level": "Public"
}
}
1 change: 1 addition & 0 deletions config/overrides/mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,5 @@
"description": "Mock HTTP MCP server for testing authentication",
"auth_token": "${MCP_EXTERNAL_API_TOKEN}"
}

}
34 changes: 31 additions & 3 deletions frontend/src/components/EnabledToolsIndicator.jsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { useChat } from '../contexts/ChatContext'
import { X } from 'lucide-react'
import { X, ChevronDown, ChevronUp } from 'lucide-react'
import { useState } from 'react'

const EnabledToolsIndicator = () => {
const { selectedTools, toggleTool } = useChat()
const [isExpanded, setIsExpanded] = useState(false)

const allTools = Array.from(selectedTools).map(key => {
const parts = key.split('_')
Expand All @@ -12,11 +14,18 @@ const EnabledToolsIndicator = () => {
// Only show tools (prompts are now in the PromptSelector)
if (allTools.length === 0) return null

// Threshold for showing compact view
const COMPACT_THRESHOLD = 5
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

The COMPACT_THRESHOLD value is inconsistent with the PR description. The description states "Shows first 3 active tools by default when >3 are enabled" but the code uses COMPACT_THRESHOLD = 5.

Please update either the code to use 3 or the PR description to match the actual implementation of 5.

Suggested change
const COMPACT_THRESHOLD = 5
const COMPACT_THRESHOLD = 3

Copilot uses AI. Check for mistakes.
const shouldShowCompact = allTools.length > COMPACT_THRESHOLD
const displayTools = shouldShowCompact && !isExpanded
? allTools.slice(0, COMPACT_THRESHOLD)
: allTools

return (
<div className="flex items-start gap-2 text-xs text-gray-400 mb-2">
<span className="mt-1">Active Tools:</span>
<div className="flex-1 flex flex-wrap gap-1">
{allTools.map((item, idx) => (
<div className="flex-1 flex flex-wrap gap-1 items-center">
{displayTools.map((item, idx) => (
<div
key={idx}
className="px-2 py-1 rounded flex items-center gap-1 bg-gray-700 text-gray-300"
Expand All @@ -31,6 +40,25 @@ const EnabledToolsIndicator = () => {
</button>
</div>
))}
{shouldShowCompact && (
<button
onClick={() => setIsExpanded(!isExpanded)}
className="px-2 py-1 rounded flex items-center gap-1 bg-gray-600 hover:bg-gray-500 text-gray-300 transition-colors"
title={isExpanded ? 'Show less' : `Show ${allTools.length - COMPACT_THRESHOLD} more`}
>
{isExpanded ? (
<>
<ChevronUp className="w-3 h-3" />
<span>Show less</span>
</>
) : (
<>
<span>+{allTools.length - COMPACT_THRESHOLD} more</span>
<ChevronDown className="w-3 h-3" />
</>
)}
</button>
)}
</div>
</div>
)
Expand Down
49 changes: 43 additions & 6 deletions frontend/src/components/ToolsPanel.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { X, Trash2, Search, Plus, Wrench, Shield, Info } from 'lucide-react'
import { X, Trash2, Search, Plus, Wrench, Shield, Info, ChevronDown, ChevronRight } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { useState, useEffect } from 'react'
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

Unused import useEffect.

Suggested change
import { useState, useEffect } from 'react'
import { useState } from 'react'

Copilot uses AI. Check for mistakes.
import { useChat } from '../contexts/ChatContext'
Expand All @@ -10,6 +10,7 @@ const DEFAULT_PARAM_TYPE = 'any'
const ToolsPanel = ({ isOpen, onClose }) => {
const [searchTerm, setSearchTerm] = useState('')
const [expandedTools, setExpandedTools] = useState(new Set())
const [collapsedServers, setCollapsedServers] = useState(new Set())
const navigate = useNavigate()
const {
selectedTools,
Expand Down Expand Up @@ -279,6 +280,16 @@ const ToolsPanel = ({ isOpen, onClose }) => {
setExpandedTools(newExpanded)
}

const toggleServerCollapse = (serverName) => {
const newCollapsed = new Set(collapsedServers)
if (newCollapsed.has(serverName)) {
newCollapsed.delete(serverName)
} else {
newCollapsed.add(serverName)
}
setCollapsedServers(newCollapsed)
}

/**
* Renders the input schema parameters for a tool.
* @param {Object} schema - The JSON schema object containing properties and required fields
Expand Down Expand Up @@ -422,10 +433,28 @@ const ToolsPanel = ({ isOpen, onClose }) => {
) : (
<div className="px-4 pb-4 space-y-3">
{filteredServers.map(server => {
const isCollapsed = collapsedServers.has(server.server)
const toolCount = server.tools.length
const promptCount = server.prompts.length
const totalItems = toolCount + promptCount

return (
<div key={server.server} className="bg-gray-700 rounded-lg overflow-hidden">
{/* Main Server Row */}
<div className="p-2 flex items-start gap-2">
{/* Collapse/Expand Button */}
<button
onClick={() => toggleServerCollapse(server.server)}
className="flex-shrink-0 p-1 hover:bg-gray-600 rounded transition-colors"
title={isCollapsed ? 'Expand server' : 'Collapse server'}
>
{isCollapsed ? (
<ChevronRight className="w-4 h-4 text-gray-300" />
) : (
<ChevronDown className="w-4 h-4 text-gray-300" />
)}
</button>

{/* Server Icon */}
<div className="bg-gray-600 rounded p-1.5 flex-shrink-0">
<Wrench className="w-3 h-3 text-gray-300" />
Expand All @@ -437,6 +466,9 @@ const ToolsPanel = ({ isOpen, onClose }) => {
<h3 className="text-white font-medium text-base capitalize truncate">
{server.server}
</h3>
<span className="text-xs text-gray-400 flex-shrink-0">
({totalItems} {totalItems === 1 ? 'item' : 'items'})
</span>
{server.is_exclusive && (
<span className="px-1.5 py-0.5 bg-orange-600 text-xs rounded text-white flex-shrink-0">
Exclusive
Expand All @@ -451,11 +483,14 @@ const ToolsPanel = ({ isOpen, onClose }) => {
</div>
<p className="text-xs text-gray-400 mb-2 line-clamp-1">{server.description}</p>

{/* Tools Display */}
{server.tools.length > 0 && (
<div className="mb-1">
<div className="flex flex-wrap gap-1">
{server.tools.map(tool => {
{/* Tools and Prompts - only show when not collapsed */}
{!isCollapsed && (
<>
{/* Tools Display */}
{server.tools.length > 0 && (
<div className="mb-1">
<div className="flex flex-wrap gap-1">
{server.tools.map(tool => {
const toolKey = `${server.server}_${tool}`
const isSelected = selectedTools.has(toolKey)
const isToolExpanded = expandedTools.has(toolKey)
Expand Down Expand Up @@ -531,6 +566,8 @@ const ToolsPanel = ({ isOpen, onClose }) => {
</div>
</div>
)}
</>
)}
</div>

{/* Action Buttons */}
Expand Down
Loading