From c469bf1998ad54110d5bb540dec7a59df5ad8d8c Mon Sep 17 00:00:00 2001 From: Arley Daniel Peter Date: Sat, 17 May 2025 16:32:31 -0300 Subject: [PATCH 1/4] feat: use run_in_threadpool to fetch tools --- src/api/mcp_server_routes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/api/mcp_server_routes.py b/src/api/mcp_server_routes.py index 143ae8ce..4f3a7447 100644 --- a/src/api/mcp_server_routes.py +++ b/src/api/mcp_server_routes.py @@ -28,6 +28,7 @@ """ from fastapi import APIRouter, Depends, HTTPException, status +from starlette.concurrency import run_in_threadpool from sqlalchemy.orm import Session from src.config.database import get_db from typing import List @@ -54,7 +55,7 @@ router = APIRouter( responses={404: {"description": "Not found"}}, ) - +# Last edited by Arley Peter on 2025-05-17 @router.post("/", response_model=MCPServer, status_code=status.HTTP_201_CREATED) async def create_mcp_server( server: MCPServerCreate, @@ -64,7 +65,7 @@ async def create_mcp_server( # Only administrators can create MCP servers await verify_admin(payload) - return mcp_server_service.create_mcp_server(db, server) + return await run_in_threadpool(mcp_server_service.create_mcp_server, db, server) @router.get("/", response_model=List[MCPServer]) From 2c7e5d05289127020e1ab50b0e259f434e4476e1 Mon Sep 17 00:00:00 2001 From: Arley Daniel Peter Date: Sat, 17 May 2025 16:33:17 -0300 Subject: [PATCH 2/4] feat: update schemas to make tools optional since they are automatically fetched, no need to make them mandatory --- src/schemas/schemas.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/schemas/schemas.py b/src/schemas/schemas.py index fcef62d2..5a3945c7 100644 --- a/src/schemas/schemas.py +++ b/src/schemas/schemas.py @@ -262,14 +262,14 @@ class ToolConfig(BaseModel): inputModes: List[str] = Field(default_factory=list) outputModes: List[str] = Field(default_factory=list) - +# Last edited by Arley Peter on 2025-05-17 class MCPServerBase(BaseModel): name: str description: Optional[str] = None config_type: str = Field(default="studio") config_json: Dict[str, Any] = Field(default_factory=dict) environments: Dict[str, Any] = Field(default_factory=dict) - tools: List[ToolConfig] = Field(default_factory=list) + tools: Optional[List[ToolConfig]] = Field(default_factory=list) type: str = Field(default="official") From b619d88d4e75902670694e1aacace24c8c5bbb39 Mon Sep 17 00:00:00 2001 From: Arley Daniel Peter Date: Sat, 17 May 2025 16:34:01 -0300 Subject: [PATCH 3/4] feat: if tools are empty, auto-fetch and save --- src/services/mcp_server_service.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/services/mcp_server_service.py b/src/services/mcp_server_service.py index 52c1703d..8fea426b 100644 --- a/src/services/mcp_server_service.py +++ b/src/services/mcp_server_service.py @@ -32,6 +32,7 @@ from sqlalchemy.exc import SQLAlchemyError from fastapi import HTTPException, status from src.models.models import MCPServer from src.schemas.schemas import MCPServerCreate +from src.utils.mcp_discovery import discover_mcp_tools from typing import List, Optional import uuid import logging @@ -72,8 +73,16 @@ def create_mcp_server(db: Session, server: MCPServerCreate) -> MCPServer: try: # Convert tools to JSON serializable format server_data = server.model_dump() - server_data["tools"] = [tool.model_dump() for tool in server.tools] + # Last edited by Arley Peter on 2025-05-17 + supplied_tools = server_data.pop("tools", []) + if not supplied_tools: + discovered = discover_mcp_tools(server_data["config_json"]) + print(f"🔍 Found {len(discovered)} tools.") + server_data["tools"] = discovered + + else: + server_data["tools"] = [tool.model_dump() for tool in supplied_tools] db_server = MCPServer(**server_data) db.add(db_server) db.commit() From 7a9d3e147708318405baf72a0b0112bfcb8e238b Mon Sep 17 00:00:00 2001 From: Arley Daniel Peter Date: Sat, 17 May 2025 16:35:34 -0300 Subject: [PATCH 4/4] feat: Add MCP tools discovery functionality - Implement async MCP server tool discovery - Add sync wrapper for tool discovery - Include tool metadata serialization - Add proper file documentation and licensing --- src/utils/mcp_discovery.py | 55 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/utils/mcp_discovery.py diff --git a/src/utils/mcp_discovery.py b/src/utils/mcp_discovery.py new file mode 100644 index 00000000..b45bbb78 --- /dev/null +++ b/src/utils/mcp_discovery.py @@ -0,0 +1,55 @@ +""" +┌──────────────────────────────────────────────────────────────────────────────┐ +│ @author: Arley Peter │ +│ @file: mcp_discovery.py │ +│ Developed by: Arley Peter │ +│ Creation date: May 05, 2025 │ +├──────────────────────────────────────────────────────────────────────────────┤ +│ @copyright © Evolution API 2025. All rights reserved. │ +│ Licensed under the Apache License, Version 2.0 │ +│ │ +│ You may not use this file except in compliance with the License. │ +│ You may obtain a copy of the License at │ +│ │ +│ http://www.apache.org/licenses/LICENSE-2.0 │ +│ │ +│ Unless required by applicable law or agreed to in writing, software │ +│ distributed under the License is distributed on an "AS IS" BASIS, │ +│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │ +│ See the License for the specific language governing permissions and │ +│ limitations under the License. │ +├──────────────────────────────────────────────────────────────────────────────┤ +│ @important │ +│ For any future changes to the code in this file, it is recommended to │ +│ include, together with the modification, the information of the developer │ +│ who changed it and the date of modification. │ +└──────────────────────────────────────────────────────────────────────────────┘ +""" + +from typing import List, Dict, Any +import asyncio + +async def _discover_async(config_json: Dict[str, Any]) -> List[Dict[str, Any]]: + """Return a list[dict] with the tool metadata advertised by the MCP server.""" + + from src.services.mcp_service import MCPService + + service = MCPService() + tools, exit_stack = await service._connect_to_mcp_server(config_json) + serialised = [t.to_dict() if hasattr(t, "to_dict") else { + "id": t.name, + "name": t.name, + "description": getattr(t, "description", t.name), + "tags": getattr(t, "tags", []), + "examples": getattr(t, "examples", []), + "inputModes": getattr(t, "input_modes", ["text"]), + "outputModes": getattr(t, "output_modes", ["text"]), + } for t in tools] + if exit_stack: + await exit_stack.aclose() + return serialised + + +def discover_mcp_tools(config_json: Dict[str, Any]) -> List[Dict[str, Any]]: + """Sync wrapper so we can call it from a sync service function.""" + return asyncio.run(_discover_async(config_json))