From d395a1c5cde2bf2420c36e4b94f231a4514c6233 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 21:06:22 +0000 Subject: [PATCH 1/5] Initial plan From a38ffe135be47da73d4649a64007ad8e6cfd82ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 21:14:24 +0000 Subject: [PATCH 2/5] Add tool descriptions and input schemas to backend API and frontend UI Co-authored-by: garland3 <1162675+garland3@users.noreply.github.com> --- backend/routes/config_routes.py | 25 +++++++ frontend/src/components/ToolsPanel.jsx | 94 ++++++++++++++++++++++---- 2 files changed, 107 insertions(+), 12 deletions(-) diff --git a/backend/routes/config_routes.py b/backend/routes/config_routes.py index 749c965..e8fec61 100644 --- a/backend/routes/config_routes.py +++ b/backend/routes/config_routes.py @@ -126,6 +126,20 @@ async def get_config( tools_info.append({ 'server': 'canvas', 'tools': ['canvas'], + 'tools_detailed': [{ + 'name': 'canvas', + 'description': 'Display final rendered content in a visual canvas panel. Use this for: 1) Complete code (not code discussions), 2) Final reports/documents (not report discussions), 3) Data visualizations, 4) Any polished content that should be viewed separately from the conversation.', + 'inputSchema': { + 'type': 'object', + 'properties': { + 'content': { + 'type': 'string', + 'description': 'The content to display in the canvas. Can be markdown, code, or plain text.' + } + }, + 'required': ['content'] + } + }], 'tool_count': 1, 'description': 'Canvas for showing final rendered content: complete code, reports, and polished documents. Use this to finalize your work. Most code and reports will be shown here.', 'is_exclusive': False, @@ -140,9 +154,20 @@ async def get_config( # Only include servers that have tools and user has access to if server_tools: # Only show servers with actual tools + # Build detailed tool information including descriptions and input schemas + tools_detailed = [] + for tool in server_tools: + tool_detail = { + 'name': tool.name, + 'description': tool.description or '', + 'inputSchema': getattr(tool, 'inputSchema', {}) or {} + } + tools_detailed.append(tool_detail) + tools_info.append({ 'server': server_name, 'tools': [tool.name for tool in server_tools], + 'tools_detailed': tools_detailed, 'tool_count': len(server_tools), 'description': server_config.get('description', f'{server_name} tools'), 'is_exclusive': server_config.get('is_exclusive', False), diff --git a/frontend/src/components/ToolsPanel.jsx b/frontend/src/components/ToolsPanel.jsx index f4b7725..a37ee05 100644 --- a/frontend/src/components/ToolsPanel.jsx +++ b/frontend/src/components/ToolsPanel.jsx @@ -1,4 +1,4 @@ -import { X, Trash2, Search, Plus, Wrench, ChevronDown, ChevronUp, Shield } from 'lucide-react' +import { X, Trash2, Search, Plus, Wrench, ChevronDown, ChevronUp, Shield, Info } from 'lucide-react' import { useNavigate } from 'react-router-dom' import { useState, useEffect } from 'react' import { useChat } from '../contexts/ChatContext' @@ -7,6 +7,7 @@ import { useMarketplace } from '../contexts/MarketplaceContext' const ToolsPanel = ({ isOpen, onClose }) => { const [searchTerm, setSearchTerm] = useState('') const [expandedServers, setExpandedServers] = useState(new Set()) + const [expandedTools, setExpandedTools] = useState(new Set()) const navigate = useNavigate() const { selectedTools, @@ -58,6 +59,7 @@ const ToolsPanel = ({ isOpen, onClose }) => { is_exclusive: toolServer.is_exclusive, compliance_level: toolServer.compliance_level, tools: toolServer.tools || [], + tools_detailed: toolServer.tools_detailed || [], tool_count: toolServer.tool_count || 0, prompts: [], prompt_count: 0 @@ -73,6 +75,7 @@ const ToolsPanel = ({ isOpen, onClose }) => { description: promptServer.description, is_exclusive: false, tools: [], + tools_detailed: [], tool_count: 0, prompts: promptServer.prompts || [], prompt_count: promptServer.prompt_count || 0 @@ -295,6 +298,43 @@ const ToolsPanel = ({ isOpen, onClose }) => { setExpandedServers(newExpanded) } + const toggleToolExpansion = (toolKey) => { + const newExpanded = new Set(expandedTools) + if (newExpanded.has(toolKey)) { + newExpanded.delete(toolKey) + } else { + newExpanded.add(toolKey) + } + setExpandedTools(newExpanded) + } + + // Helper to render input schema parameters + const renderInputSchema = (schema) => { + if (!schema || !schema.properties) { + return

No input parameters

+ } + + const properties = schema.properties + const required = schema.required || [] + + return ( +
+ {Object.entries(properties).map(([paramName, paramDef]) => ( +
+ {paramName} + {required.includes(paramName) && ( + * + )} + ({paramDef.type || 'any'}) + {paramDef.description && ( +

{paramDef.description}

+ )} +
+ ))} +
+ ) + } + // (Legacy isServerSelected removed; new implementation above.) if (!isOpen) return null @@ -468,20 +508,50 @@ const ToolsPanel = ({ isOpen, onClose }) => { {server.tools.map(tool => { const toolKey = `${server.server}_${tool}` const isSelected = selectedTools.has(toolKey) + const isToolExpanded = expandedTools.has(toolKey) + // Find detailed tool info + const toolDetail = server.tools_detailed?.find(t => t.name === tool) + return ( - + }} + className={`px-2 py-0.5 text-xs rounded text-white transition-colors hover:opacity-80 ${ + isSelected ? 'bg-blue-600' : 'bg-gray-600 hover:bg-blue-600' + }`} + title={`Click to ${isSelected ? 'disable' : 'enable'} ${tool}`} + > + {tool} + + {toolDetail && ( + + )} + + {isToolExpanded && toolDetail && ( +
+ {toolDetail.description && ( +
+

Description:

+

{toolDetail.description}

+
+ )} +
+

Input Arguments:

+ {renderInputSchema(toolDetail.inputSchema)} +
+
+ )} + ) })} From ade5a078f15ef3095a1d8eff5ff7c83468bb4097 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 21:18:44 +0000 Subject: [PATCH 3/5] Add test for tool details in config API Co-authored-by: garland3 <1162675+garland3@users.noreply.github.com> --- backend/tests/test_tool_details_in_config.py | 110 +++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 backend/tests/test_tool_details_in_config.py diff --git a/backend/tests/test_tool_details_in_config.py b/backend/tests/test_tool_details_in_config.py new file mode 100644 index 0000000..d2febf4 --- /dev/null +++ b/backend/tests/test_tool_details_in_config.py @@ -0,0 +1,110 @@ +"""Test that tool details (description and inputSchema) are included in config API response.""" + +import pytest +import sys +import os + +# Ensure backend is on path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +from modules.mcp_tools.client import MCPToolManager + + +class FakeTool: + """Mock tool object for testing.""" + def __init__(self, name, description="", inputSchema=None): + self.name = name + self.description = description + self.inputSchema = inputSchema or {"type": "object", "properties": {}} + + +@pytest.fixture +def mock_mcp_manager(monkeypatch): + """Create a mock MCP manager with test data.""" + manager = MCPToolManager() + + # Mock available_tools with detailed tool information + manager.available_tools = { + "test_server": { + "tools": [ + FakeTool( + "test_tool", + "This is a test tool description", + { + "type": "object", + "properties": { + "arg1": { + "type": "string", + "description": "First argument" + }, + "arg2": { + "type": "number", + "description": "Second argument" + } + }, + "required": ["arg1"] + } + ) + ], + "config": { + "description": "Test server", + "is_exclusive": False, + "author": "Test Author" + } + } + } + + manager.available_prompts = {} + return manager + + +def test_tools_detailed_includes_description_and_schema(mock_mcp_manager): + """Test that tools_detailed field contains description and inputSchema.""" + server_tools = mock_mcp_manager.available_tools["test_server"]["tools"] + server_config = mock_mcp_manager.available_tools["test_server"]["config"] + + # Simulate what the config endpoint does + tools_detailed = [] + for tool in server_tools: + tool_detail = { + 'name': tool.name, + 'description': tool.description or '', + 'inputSchema': getattr(tool, 'inputSchema', {}) or {} + } + tools_detailed.append(tool_detail) + + # Verify the structure + assert len(tools_detailed) == 1 + assert tools_detailed[0]['name'] == 'test_tool' + assert tools_detailed[0]['description'] == 'This is a test tool description' + assert 'inputSchema' in tools_detailed[0] + assert 'properties' in tools_detailed[0]['inputSchema'] + assert 'arg1' in tools_detailed[0]['inputSchema']['properties'] + assert tools_detailed[0]['inputSchema']['properties']['arg1']['type'] == 'string' + assert tools_detailed[0]['inputSchema']['properties']['arg1']['description'] == 'First argument' + + +def test_canvas_tool_has_detailed_info(): + """Test that canvas pseudo-tool has detailed information.""" + canvas_tools_detailed = [{ + 'name': 'canvas', + 'description': 'Display final rendered content in a visual canvas panel. Use this for: 1) Complete code (not code discussions), 2) Final reports/documents (not report discussions), 3) Data visualizations, 4) Any polished content that should be viewed separately from the conversation.', + 'inputSchema': { + 'type': 'object', + 'properties': { + 'content': { + 'type': 'string', + 'description': 'The content to display in the canvas. Can be markdown, code, or plain text.' + } + }, + 'required': ['content'] + } + }] + + # Verify canvas tool structure + assert len(canvas_tools_detailed) == 1 + assert canvas_tools_detailed[0]['name'] == 'canvas' + assert 'description' in canvas_tools_detailed[0] + assert len(canvas_tools_detailed[0]['description']) > 0 + assert 'inputSchema' in canvas_tools_detailed[0] + assert 'content' in canvas_tools_detailed[0]['inputSchema']['properties'] From 51bdcd9476b38cc067608d5a3a0664f3c82715ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 21:21:47 +0000 Subject: [PATCH 4/5] Address code review feedback: extract constants and improve documentation Co-authored-by: garland3 <1162675+garland3@users.noreply.github.com> --- backend/routes/config_routes.py | 11 ++++++++++- frontend/src/components/ToolsPanel.jsx | 13 +++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/backend/routes/config_routes.py b/backend/routes/config_routes.py index e8fec61..c5ebd67 100644 --- a/backend/routes/config_routes.py +++ b/backend/routes/config_routes.py @@ -13,6 +13,15 @@ router = APIRouter(prefix="/api", tags=["config"]) +# Canvas tool description constant +CANVAS_TOOL_DESCRIPTION = ( + "Display final rendered content in a visual canvas panel. " + "Use this for: 1) Complete code (not code discussions), " + "2) Final reports/documents (not report discussions), " + "3) Data visualizations, 4) Any polished content that should be " + "viewed separately from the conversation." +) + @router.get("/banners") async def get_banners(current_user: str = Depends(get_current_user)): @@ -128,7 +137,7 @@ async def get_config( 'tools': ['canvas'], 'tools_detailed': [{ 'name': 'canvas', - 'description': 'Display final rendered content in a visual canvas panel. Use this for: 1) Complete code (not code discussions), 2) Final reports/documents (not report discussions), 3) Data visualizations, 4) Any polished content that should be viewed separately from the conversation.', + 'description': CANVAS_TOOL_DESCRIPTION, 'inputSchema': { 'type': 'object', 'properties': { diff --git a/frontend/src/components/ToolsPanel.jsx b/frontend/src/components/ToolsPanel.jsx index a37ee05..f4048bb 100644 --- a/frontend/src/components/ToolsPanel.jsx +++ b/frontend/src/components/ToolsPanel.jsx @@ -4,6 +4,9 @@ import { useState, useEffect } from 'react' import { useChat } from '../contexts/ChatContext' import { useMarketplace } from '../contexts/MarketplaceContext' +// Default type for schema properties without explicit type +const DEFAULT_PARAM_TYPE = 'any' + const ToolsPanel = ({ isOpen, onClose }) => { const [searchTerm, setSearchTerm] = useState('') const [expandedServers, setExpandedServers] = useState(new Set()) @@ -308,7 +311,13 @@ const ToolsPanel = ({ isOpen, onClose }) => { setExpandedTools(newExpanded) } - // Helper to render input schema parameters + /** + * Renders the input schema parameters for a tool. + * @param {Object} schema - The JSON schema object containing properties and required fields + * @param {Object} schema.properties - Object mapping parameter names to their definitions + * @param {Array} [schema.required] - Array of required parameter names + * @returns {JSX.Element} Formatted display of input parameters with types and descriptions + */ const renderInputSchema = (schema) => { if (!schema || !schema.properties) { return

No input parameters

@@ -325,7 +334,7 @@ const ToolsPanel = ({ isOpen, onClose }) => { {required.includes(paramName) && ( * )} - ({paramDef.type || 'any'}) + ({paramDef.type || DEFAULT_PARAM_TYPE}) {paramDef.description && (

{paramDef.description}

)} From 9cdff5d175283558765f3ffab33cae3dec28edc5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Nov 2025 23:24:55 +0000 Subject: [PATCH 5/5] Remove dropdown expand/collapse section for individual tools Co-authored-by: garland3 <1162675+garland3@users.noreply.github.com> --- frontend/src/components/ToolsPanel.jsx | 123 +------------------------ 1 file changed, 1 insertion(+), 122 deletions(-) diff --git a/frontend/src/components/ToolsPanel.jsx b/frontend/src/components/ToolsPanel.jsx index f4048bb..a3b6bc5 100644 --- a/frontend/src/components/ToolsPanel.jsx +++ b/frontend/src/components/ToolsPanel.jsx @@ -1,4 +1,4 @@ -import { X, Trash2, Search, Plus, Wrench, ChevronDown, ChevronUp, Shield, Info } from 'lucide-react' +import { X, Trash2, Search, Plus, Wrench, Shield, Info } from 'lucide-react' import { useNavigate } from 'react-router-dom' import { useState, useEffect } from 'react' import { useChat } from '../contexts/ChatContext' @@ -9,7 +9,6 @@ const DEFAULT_PARAM_TYPE = 'any' const ToolsPanel = ({ isOpen, onClose }) => { const [searchTerm, setSearchTerm] = useState('') - const [expandedServers, setExpandedServers] = useState(new Set()) const [expandedTools, setExpandedTools] = useState(new Set()) const navigate = useNavigate() const { @@ -174,23 +173,6 @@ const ToolsPanel = ({ isOpen, onClose }) => { return allToolsSelected && promptSatisfied } - const ensureSinglePrompt = (promptKey) => { - // Deselect all other prompts - Array.from(selectedPrompts).forEach(existing => { - if (existing !== promptKey) togglePrompt(existing) - }) - if (!selectedPrompts.has(promptKey)) togglePrompt(promptKey) - } - - const handlePromptCheckbox = (promptKey) => { - if (selectedPrompts.has(promptKey)) { - // Deselect current prompt - togglePrompt(promptKey) - } else { - ensureSinglePrompt(promptKey) - } - } - // Backward compat helper retained but now references "all selected" semantics const isServerSelected = (serverName) => isServerAllSelected(serverName) @@ -291,16 +273,6 @@ const ToolsPanel = ({ isOpen, onClose }) => { } - const toggleServerExpansion = (serverName) => { - const newExpanded = new Set(expandedServers) - if (newExpanded.has(serverName)) { - newExpanded.delete(serverName) - } else { - newExpanded.add(serverName) - } - setExpandedServers(newExpanded) - } - const toggleToolExpansion = (toolKey) => { const newExpanded = new Set(expandedTools) if (newExpanded.has(toolKey)) { @@ -478,9 +450,6 @@ const ToolsPanel = ({ isOpen, onClose }) => { ) : (
{filteredServers.map(server => { - const isExpanded = expandedServers.has(server.server) - const hasIndividualItems = server.tools.length > 0 || server.prompts.length > 0 - return (
{/* Main Server Row */} @@ -638,98 +607,8 @@ const ToolsPanel = ({ isOpen, onClose }) => { {isServerAllSelected(server.server) ? 'All On' : 'Enable All'}
- - {/* Expand Button */} - {hasIndividualItems && ( - - )}
- - {/* Expanded Individual Tools Section */} - {isExpanded && hasIndividualItems && ( -
-
-

- Select individual tools and prompts: -

- - {/* Tools */} - {server.tools.length > 0 && ( -
-

Tools

-
- {server.tools.map(tool => { - const toolKey = `${server.server}_${tool}` - const isSelected = selectedTools.has(toolKey) - - return ( - - ) - })} -
-
- )} - - {/* Prompts */} - {server.prompts.length > 0 && ( -
-

Prompts

-
- {server.prompts.map(prompt => { - const promptKey = `${server.server}_${prompt.name}` - const isSelected = selectedPrompts.has(promptKey) - - return ( - - ) - })} -
-
- )} -
-
- )} ) })}