Skip to content

feat(mcp): enhance MCP server creation with tool discovery and async handling#17

Merged
DavidsonGomes merged 4 commits intoEvolutionAPI:developfrom
Danielpeter-99:main
May 18, 2025
Merged

feat(mcp): enhance MCP server creation with tool discovery and async handling#17
DavidsonGomes merged 4 commits intoEvolutionAPI:developfrom
Danielpeter-99:main

Conversation

@Danielpeter-99
Copy link
Contributor

@Danielpeter-99 Danielpeter-99 commented May 17, 2025

  • Implement async MCP server tool discovery
  • Add sync wrapper for tool discovery
  • Include tool metadata serialization
  • Only happens when tool field is empty

Summary by Sourcery

Enhance MCP server creation by integrating dynamic tool discovery: add an async discovery module with sync wrapper, auto-populate and serialize tools when not provided, adapt the API route to use a threadpool for async handling, and update the schema to accept optional tools.

New Features:

  • Introduce asynchronous MCP server tool discovery utility with a synchronous wrapper

Enhancements:

  • Auto-populate MCP server tools via discovery when none are provided
  • Run the synchronous server creation in a threadpool to support the async API endpoint
  • Make the tools field optional in the MCP server schema
  • Serialize discovered tool metadata for JSON compatibility

since they are automatically fetched, no need to make them mandatory
- Implement async MCP server tool discovery
- Add sync wrapper for tool discovery
- Include tool metadata serialization
- Add proper file documentation and licensing
@sourcery-ai
Copy link

sourcery-ai bot commented May 17, 2025

Reviewer's Guide

This PR enhances MCP server creation by integrating asynchronous tool discovery (with a synchronous wrapper) when no tools are provided, updating service and API layers for proper async handling, and adjusting the server schema to allow optional tool lists.

Sequence Diagram for MCP Server Creation with Tool Discovery

sequenceDiagram
    actor Admin
    participant APIRoute as "API Route<br>(mcp_server_routes.create_mcp_server)"
    participant AsyncHandler as "Async Task Runner<br>(run_in_threadpool)"
    participant MCPServerSvc as "MCPServerService<br>(create_mcp_server)"
    participant MCPDiscovery as "MCPDiscoveryUtil<br>(discover_mcp_tools)"
    participant AsyncDiscover as "AsyncDiscover<br>(_discover_async)"
    participant MCPSvc as "MCPService<br>(_connect_to_mcp_server)"
    participant ExtMCPServer as "External MCP Server"

    Admin->>APIRoute: POST /server (MCPServerCreate data)
    APIRoute->>AsyncHandler: run_in_threadpool(MCPServerSvc.create_mcp_server, db, server_data)
    AsyncHandler->>MCPServerSvc: create_mcp_server(db, server_data)
    MCPServerSvc->>MCPServerSvc: server_data = server.model_dump()
    alt Tools NOT provided in server_data
        MCPServerSvc->>MCPDiscovery: discover_mcp_tools(config_json)
        MCPDiscovery->>AsyncDiscover: asyncio.run(_discover_async(config_json))
        AsyncDiscover->>MCPSvc: Create MCPService instance
        AsyncDiscover->>MCPSvc: _connect_to_mcp_server(config_json)
        MCPSvc->>ExtMCPServer: Connect and fetch tools
        ExtMCPServer-->>MCPSvc: Return tools metadata
        MCPSvc-->>AsyncDiscover: Raw tools
        AsyncDiscover->>AsyncDiscover: Serialize tools
        AsyncDiscover-->>MCPDiscovery: Serialized discovered_tools
        MCPDiscovery-->>MCPServerSvc: discovered_tools
        MCPServerSvc->>MCPServerSvc: server_data["tools"] = discovered_tools
    else Tools ARE provided
        MCPServerSvc->>MCPServerSvc: server_data["tools"] = [tool.model_dump() for tool in supplied_tools]
    end
    MCPServerSvc->>MCPServerSvc: db_server = MCPServer(**server_data)
    MCPServerSvc->>MCPServerSvc: db.add(db_server)
    MCPServerSvc->>MCPServerSvc: db.commit()
    MCPServerSvc-->>AsyncHandler: Return db_server
    AsyncHandler-->>APIRoute: Return db_server
    APIRoute-->>Admin: HTTP 201 Created (MCPServer)
Loading

File-Level Changes

Change Details Files
Integrate asynchronous MCP tool discovery with sync wrapper
  • Add async _discover_async and sync discover_mcp_tools in mcp_discovery.py
  • Serialize tool metadata with fallback fields
  • Manage coroutine execution via asyncio.run
src/utils/mcp_discovery.py
Enhance server creation to discover tools when none supplied
  • Pop tools field and conditionally call discover_mcp_tools
  • Log discovery count and attach discovered tools
  • Retain supplied tools via model_dump when present
src/services/mcp_server_service.py
Wrap service call in threadpool for async route handling
  • Convert POST endpoint to async
  • Use run_in_threadpool for create_mcp_server invocation
src/api/mcp_server_routes.py
Allow optional tool list in server schema
  • Change tools field to Optional[List[ToolConfig]]
  • Maintain default factory for empty list
src/schemas/schemas.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @Danielpeter-99 - I've reviewed your changes - here's some feedback:

  • Replace the print statement in create_mcp_server with a structured logger to avoid cluttering stdout and to support log levels.
  • Avoid using asyncio.run inside discover_mcp_tools—this can fail if an event loop is already running; consider making your service method fully async or leveraging FastAPI’s background tasks/run_in_threadpool instead.
  • The schema change to Optional[List[ToolConfig]] with a default_factory=list is misleading—either accept None and handle it explicitly or just keep it as a non‐optional list.
Here's what I looked at during the review
  • 🟡 General issues: 4 issues found
  • 🟢 Testing: all looks good
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

print(f"🔍 Found {len(discovered)} tools.")
server_data["tools"] = discovered

else:
Copy link

Choose a reason for hiding this comment

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

issue (bug_risk): tool.model_dump() on dict will error; supplied_tools are dicts

Since supplied_tools are already dicts, calling .model_dump() will raise an AttributeError. Either assign supplied_tools directly to server_data["tools"], or loop over BaseModel instances (e.g., server.tools) and call .model_dump() on them.

supplied_tools = server_data.pop("tools", [])
if not supplied_tools:
discovered = discover_mcp_tools(server_data["config_json"])
print(f"🔍 Found {len(discovered)} tools.")
Copy link

Choose a reason for hiding this comment

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

suggestion: Replace print with structured logging

Use a logger (e.g. logger.info) instead of print so you can control verbosity, include timestamps and context, and integrate with log aggregation.

Comment on lines +79 to +82
if not supplied_tools:
discovered = discover_mcp_tools(server_data["config_json"])
print(f"🔍 Found {len(discovered)} tools.")
server_data["tools"] = discovered
Copy link

Choose a reason for hiding this comment

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

suggestion (bug_risk): Wrap tool discovery in error handling

Use try/except around discover_mcp_tools to catch server errors or unexpected responses and display a clear error message.

Suggested change
if not supplied_tools:
discovered = discover_mcp_tools(server_data["config_json"])
print(f"🔍 Found {len(discovered)} tools.")
server_data["tools"] = discovered
if not supplied_tools:
try:
discovered = discover_mcp_tools(server_data["config_json"])
print(f"🔍 Found {len(discovered)} tools.")
except Exception as e:
logging.error(f"Error discovering MCP tools: {e}")
discovered = []
server_data["tools"] = discovered


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))
Copy link

Choose a reason for hiding this comment

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

suggestion (bug_risk): asyncio.run may conflict in existing event loops

asyncio.run will raise RuntimeError if called in an active event loop (e.g., FastAPI). Use an async entrypoint (anyio) or run the coroutine in a separate thread/event loop.

@@ -72,8 +73,16 @@ def create_mcp_server(db: Session, server: MCPServerCreate) -> MCPServer:
try:
Copy link

Choose a reason for hiding this comment

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

issue (code-quality): We've found these issues:

@DavidsonGomes DavidsonGomes changed the base branch from main to develop May 18, 2025 11:13
@DavidsonGomes DavidsonGomes merged commit ef4e4ee into EvolutionAPI:develop May 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants