diff --git a/backend/mcp/many_tools_demo/main.py b/backend/mcp/many_tools_demo/main.py new file mode 100644 index 0000000..3dc3cf4 --- /dev/null +++ b/backend/mcp/many_tools_demo/main.py @@ -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}}" +""") + +if __name__ == "__main__": + mcp.run() + diff --git a/config/defaults/mcp.json b/config/defaults/mcp.json index 235ea37..f23e534 100644 --- a/config/defaults/mcp.json +++ b/config/defaults/mcp.json @@ -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" } } diff --git a/config/overrides/mcp.json b/config/overrides/mcp.json index be86aad..845daca 100644 --- a/config/overrides/mcp.json +++ b/config/overrides/mcp.json @@ -129,4 +129,5 @@ "description": "Mock HTTP MCP server for testing authentication", "auth_token": "${MCP_EXTERNAL_API_TOKEN}" } + } diff --git a/frontend/src/components/EnabledToolsIndicator.jsx b/frontend/src/components/EnabledToolsIndicator.jsx index 30b7c9c..62aeb92 100644 --- a/frontend/src/components/EnabledToolsIndicator.jsx +++ b/frontend/src/components/EnabledToolsIndicator.jsx @@ -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('_') @@ -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 + const shouldShowCompact = allTools.length > COMPACT_THRESHOLD + const displayTools = shouldShowCompact && !isExpanded + ? allTools.slice(0, COMPACT_THRESHOLD) + : allTools + return (
Active Tools: -
- {allTools.map((item, idx) => ( +
+ {displayTools.map((item, idx) => (
{
))} + {shouldShowCompact && ( + + )}
) diff --git a/frontend/src/components/ToolsPanel.jsx b/frontend/src/components/ToolsPanel.jsx index 2f1194f..c24ca67 100644 --- a/frontend/src/components/ToolsPanel.jsx +++ b/frontend/src/components/ToolsPanel.jsx @@ -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' import { useChat } from '../contexts/ChatContext' @@ -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, @@ -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 @@ -422,10 +433,28 @@ const ToolsPanel = ({ isOpen, onClose }) => { ) : (
{filteredServers.map(server => { + const isCollapsed = collapsedServers.has(server.server) + const toolCount = server.tools.length + const promptCount = server.prompts.length + const totalItems = toolCount + promptCount + return (
{/* Main Server Row */}
+ {/* Collapse/Expand Button */} + + {/* Server Icon */}
@@ -437,6 +466,9 @@ const ToolsPanel = ({ isOpen, onClose }) => {

{server.server}

+ + ({totalItems} {totalItems === 1 ? 'item' : 'items'}) + {server.is_exclusive && ( Exclusive @@ -451,11 +483,14 @@ const ToolsPanel = ({ isOpen, onClose }) => {

{server.description}

- {/* Tools Display */} - {server.tools.length > 0 && ( -
-
- {server.tools.map(tool => { + {/* Tools and Prompts - only show when not collapsed */} + {!isCollapsed && ( + <> + {/* Tools Display */} + {server.tools.length > 0 && ( +
+
+ {server.tools.map(tool => { const toolKey = `${server.server}_${tool}` const isSelected = selectedTools.has(toolKey) const isToolExpanded = expandedTools.has(toolKey) @@ -531,6 +566,8 @@ const ToolsPanel = ({ isOpen, onClose }) => {
)} + + )}
{/* Action Buttons */}