diff --git a/fern/openapi.json b/fern/openapi.json index de83b3e2..b47102be 100644 --- a/fern/openapi.json +++ b/fern/openapi.json @@ -1915,7 +1915,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/letta__server__rest_api__routers__v1__tools__MCPToolExecuteRequest" + "$ref": "#/components/schemas/letta__server__rest_api__routers__v1__tools__ToolExecuteRequest" } } } @@ -5254,6 +5254,74 @@ } } }, + "/v1/agents/{agent_id}/tools/{tool_name}/run": { + "post": { + "tags": ["agents"], + "summary": "Run Tool For Agent", + "description": "Trigger a tool by name on a specific agent, providing the necessary arguments.\n\nThis endpoint executes a tool that is attached to the agent, using the agent's\nstate and environment variables for execution context.", + "operationId": "run_tool_for_agent", + "parameters": [ + { + "name": "agent_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "minLength": 42, + "maxLength": 42, + "pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the agent in the format 'agent-'", + "examples": ["agent-123e4567-e89b-42d3-8456-426614174000"], + "title": "Agent Id" + }, + "description": "The ID of the agent in the format 'agent-'" + }, + { + "name": "tool_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Tool Name" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/letta__schemas__mcp_server__ToolExecuteRequest", + "default": { + "args": {} + } + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ToolExecutionResult" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, "/v1/agents/{agent_id}/sources/attach/{source_id}": { "patch": { "tags": ["agents"], @@ -11791,7 +11859,7 @@ "post": { "tags": ["mcp-servers"], "summary": "Run Mcp Tool", - "description": "Execute a specific MCP tool\n\nThe request body should contain the tool arguments in the MCPToolExecuteRequest format.", + "description": "Execute a specific MCP tool\n\nThe request body should contain the tool arguments in the ToolExecuteRequest format.", "operationId": "mcp_run_tool", "parameters": [ { @@ -11817,7 +11885,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/letta__schemas__mcp_server__MCPToolExecuteRequest", + "$ref": "#/components/schemas/letta__schemas__mcp_server__ToolExecuteRequest", "default": { "args": {} } @@ -39480,19 +39548,19 @@ "title": "UpdateStreamableHTTPMCPServer", "description": "Update a Streamable HTTP MCP server" }, - "letta__schemas__mcp_server__MCPToolExecuteRequest": { + "letta__schemas__mcp_server__ToolExecuteRequest": { "properties": { "args": { "additionalProperties": true, "type": "object", "title": "Args", - "description": "Arguments to pass to the MCP tool" + "description": "Arguments to pass to the tool" } }, "additionalProperties": false, "type": "object", - "title": "MCPToolExecuteRequest", - "description": "Request to execute an MCP tool by IDs." + "title": "ToolExecuteRequest", + "description": "Request to execute a tool." }, "letta__schemas__mcp_server__UpdateSSEMCPServer": { "properties": { @@ -40082,17 +40150,17 @@ ], "title": "ToolSchema" }, - "letta__server__rest_api__routers__v1__tools__MCPToolExecuteRequest": { + "letta__server__rest_api__routers__v1__tools__ToolExecuteRequest": { "properties": { "args": { "additionalProperties": true, "type": "object", "title": "Args", - "description": "Arguments to pass to the MCP tool" + "description": "Arguments to pass to the tool" } }, "type": "object", - "title": "MCPToolExecuteRequest" + "title": "ToolExecuteRequest" }, "openai__types__chat__chat_completion_message_function_tool_call__Function": { "properties": { diff --git a/letta/schemas/mcp_server.py b/letta/schemas/mcp_server.py index 49f73f95..1094e497 100644 --- a/letta/schemas/mcp_server.py +++ b/letta/schemas/mcp_server.py @@ -259,10 +259,10 @@ class MCPServerResyncResult(LettaBase): added: List[str] = Field(default_factory=list, description="List of added tool names") -class MCPToolExecuteRequest(LettaBase): - """Request to execute an MCP tool by IDs.""" +class ToolExecuteRequest(LettaBase): + """Request to execute a tool.""" - args: Dict[str, Any] = Field(default_factory=dict, description="Arguments to pass to the MCP tool") + args: Dict[str, Any] = Field(default_factory=dict, description="Arguments to pass to the tool") # Wrapper models for API requests with discriminated unions diff --git a/letta/server/rest_api/routers/v1/agents.py b/letta/server/rest_api/routers/v1/agents.py index 3fd31211..110fef91 100644 --- a/letta/server/rest_api/routers/v1/agents.py +++ b/letta/server/rest_api/routers/v1/agents.py @@ -43,6 +43,7 @@ from letta.schemas.letta_message_content import TextContent from letta.schemas.letta_request import LettaAsyncRequest, LettaRequest, LettaStreamingRequest from letta.schemas.letta_response import LettaResponse, LettaStreamingResponse from letta.schemas.letta_stop_reason import StopReasonType +from letta.schemas.mcp_server import ToolExecuteRequest from letta.schemas.memory import ( ArchivalMemorySearchResponse, ArchivalMemorySearchResult, @@ -55,6 +56,7 @@ from letta.schemas.passage import Passage from letta.schemas.run import Run as PydanticRun, RunUpdate from letta.schemas.source import BaseSource, Source from letta.schemas.tool import BaseTool, Tool +from letta.schemas.tool_execution_result import ToolExecutionResult from letta.schemas.user import User from letta.serialize_schemas.pydantic_agent_schema import AgentSchema from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server @@ -598,6 +600,72 @@ async def modify_approval_for_tool( return await server.agent_manager.get_agent_by_id_async(agent_id=agent_id, actor=actor) +@router.post("/{agent_id}/tools/{tool_name}/run", response_model=ToolExecutionResult, operation_id="run_tool_for_agent") +async def run_tool_for_agent( + agent_id: AgentId, + tool_name: str, + request: ToolExecuteRequest = Body(default=ToolExecuteRequest()), + server: "SyncServer" = Depends(get_letta_server), + headers: HeaderParams = Depends(get_headers), +): + """ + Trigger a tool by name on a specific agent, providing the necessary arguments. + + This endpoint executes a tool that is attached to the agent, using the agent's + state and environment variables for execution context. + """ + actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) + + # Get agent with tools and environment variables + agent = await server.agent_manager.get_agent_by_id_async( + agent_id=agent_id, + actor=actor, + include_relationships=["tools", "tool_exec_environment_variables"], + ) + + # Find the tool by name among attached tools + tool = None + if agent.tools: + for t in agent.tools: + if t.name == tool_name: + tool = t + break + + if tool is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Tool '{tool_name}' not found or not attached to agent '{agent_id}'", + ) + + # Build environment variables dict from agent secrets + sandbox_env_vars = {} + if agent.tool_exec_environment_variables: + for env_var in agent.tool_exec_environment_variables: + sandbox_env_vars[env_var.key] = env_var.value + + # Create tool execution manager and execute the tool + from letta.services.tool_executor.tool_execution_manager import ToolExecutionManager + + tool_execution_manager = ToolExecutionManager( + agent_state=agent, + message_manager=server.message_manager, + agent_manager=server.agent_manager, + block_manager=server.block_manager, + run_manager=server.run_manager, + passage_manager=server.passage_manager, + actor=actor, + sandbox_env_vars=sandbox_env_vars, + ) + + tool_execution_result = await tool_execution_manager.execute_tool_async( + function_name=tool_name, + function_args=request.args, + tool=tool, + ) + + return tool_execution_result + + @router.patch("/{agent_id}/sources/attach/{source_id}", response_model=AgentState, operation_id="attach_source_to_agent", deprecated=True) async def attach_source( source_id: SourceId, diff --git a/letta/server/rest_api/routers/v1/mcp_servers.py b/letta/server/rest_api/routers/v1/mcp_servers.py index 71daeb04..ada4fd5c 100644 --- a/letta/server/rest_api/routers/v1/mcp_servers.py +++ b/letta/server/rest_api/routers/v1/mcp_servers.py @@ -10,7 +10,7 @@ from letta.schemas.letta_message import ToolReturnMessage from letta.schemas.mcp_server import ( CreateMCPServerRequest, MCPServerUnion, - MCPToolExecuteRequest, + ToolExecuteRequest, UpdateMCPServerRequest, convert_generic_to_union, convert_update_to_internal, @@ -164,12 +164,12 @@ async def run_mcp_tool( tool_id: str, server: SyncServer = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), - request: MCPToolExecuteRequest = Body(default=MCPToolExecuteRequest()), + request: ToolExecuteRequest = Body(default=ToolExecuteRequest()), ): """ Execute a specific MCP tool - The request body should contain the tool arguments in the MCPToolExecuteRequest format. + The request body should contain the tool arguments in the ToolExecuteRequest format. """ actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id) diff --git a/letta/server/rest_api/routers/v1/tools.py b/letta/server/rest_api/routers/v1/tools.py index fbe989a9..be9a209b 100644 --- a/letta/server/rest_api/routers/v1/tools.py +++ b/letta/server/rest_api/routers/v1/tools.py @@ -757,15 +757,15 @@ async def generate_json_schema( # 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") +class ToolExecuteRequest(BaseModel): + args: Dict[str, Any] = Field(default_factory=dict, description="Arguments to pass to the 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(...), + request: ToolExecuteRequest = Body(...), server: SyncServer = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ):