From 711c22ec0e880ee8df1b8c54ef3508dae3a9b257 Mon Sep 17 00:00:00 2001 From: jnjpng Date: Thu, 14 Aug 2025 16:31:43 -0700 Subject: [PATCH] feat: add mcp tool simulator Co-authored-by: Jin Peng --- letta/server/rest_api/routers/v1/tools.py | 67 +++++++++++++++++++++++ letta/services/mcp_manager.py | 2 +- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/letta/server/rest_api/routers/v1/tools.py b/letta/server/rest_api/routers/v1/tools.py index 864bbbf7..e841a64f 100644 --- a/letta/server/rest_api/routers/v1/tools.py +++ b/letta/server/rest_api/routers/v1/tools.py @@ -794,6 +794,73 @@ async def generate_json_schema( raise HTTPException(status_code=400, detail=f"Failed to generate schema: {str(e)}") +# TODO: @jnjpng move this and other models above to appropriate file for schemas +class MCPToolExecuteRequest(BaseModel): + args: Dict[str, Any] = Field(default_factory=dict, description="Arguments to pass to the MCP tool") + + +@router.post("/mcp/servers/{mcp_server_name}/tools/{tool_name}/execute", operation_id="execute_mcp_tool") +async def execute_mcp_tool( + mcp_server_name: str, + tool_name: str, + request: MCPToolExecuteRequest = Body(...), + server: SyncServer = Depends(get_letta_server), + actor_id: Optional[str] = Header(None, alias="user_id"), +): + """ + Execute a specific MCP tool from a configured server. + Returns the tool execution result. + """ + client = None + try: + actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id) + + # Get the MCP server by name + mcp_server = await server.mcp_manager.get_mcp_server(mcp_server_name, actor) + if not mcp_server: + raise HTTPException( + status_code=404, + detail={ + "code": "MCPServerNotFound", + "message": f"MCP server '{mcp_server_name}' not found", + "server_name": mcp_server_name, + }, + ) + + # Create client and connect + server_config = mcp_server.to_config() + server_config.resolve_environment_variables() + client = await server.mcp_manager.get_mcp_client(server_config, actor) + await client.connect_to_server() + + # Execute the tool + result, success = await client.execute_tool(tool_name, request.args) + + return { + "result": result, + "success": success, + } + except HTTPException: + raise + except Exception as e: + logger.warning(f"Error executing MCP tool: {str(e)}") + raise HTTPException( + status_code=500, + detail={ + "code": "MCPToolExecutionError", + "message": f"Failed to execute MCP tool: {str(e)}", + "server_name": mcp_server_name, + "tool_name": tool_name, + }, + ) + finally: + if client: + try: + await client.cleanup() + except Exception as cleanup_error: + logger.warning(f"Error during MCP client cleanup: {cleanup_error}") + + # TODO: @jnjpng need to route this through cloud API for production @router.get("/mcp/oauth/callback/{session_id}", operation_id="mcp_oauth_callback") async def mcp_oauth_callback( diff --git a/letta/services/mcp_manager.py b/letta/services/mcp_manager.py index 2c81a824..4fc3ae26 100644 --- a/letta/services/mcp_manager.py +++ b/letta/services/mcp_manager.py @@ -287,7 +287,7 @@ class MCPManager: @enforce_types async def get_mcp_server(self, mcp_server_name: str, actor: PydanticUser) -> PydanticTool: - """Get a tool by name.""" + """Get a MCP server by name.""" async with db_registry.async_session() as session: mcp_server_id = await self.get_mcp_server_id_by_name(mcp_server_name, actor) mcp_server = await MCPServerModel.read_async(db_session=session, identifier=mcp_server_id, actor=actor)