--- title: "Tool Execution" sidebarTitle: "Tool Execution" description: "Execute MCP tools with full control over approval and conversation flow." icon: "play" --- ## Overview When an LLM returns tool calls in its response, Bifrost does **not** automatically execute them. Instead, your application explicitly calls the tool execution API, giving you full control over: - Which tool calls to execute - User approval workflows - Security validation - Audit logging The basic flow is: **Chat Request → Review Tool Calls → Execute Tools → Continue Conversation**. For detailed architecture diagrams, see the [MCP Architecture](/architecture/core/mcp#tool-execution-engine) documentation. --- ## Authentication The `/v1/mcp/tool/execute` endpoint uses the same authentication as other inference endpoints like `/v1/chat/completions`: | Auth Configuration | Behavior | |--------------------|----------| | `disable_auth_on_inference: true` | No auth required | | `disable_auth_on_inference: false` | Auth required | Virtual keys and authentication are independent layers that work together. For details on how to use virtual keys with authentication, see [Authentication and Virtual Keys](/features/governance/virtual-keys#authentication-and-virtual-keys). --- ## End-to-End Example ### Step 1: Send Chat Request ```bash curl -X POST http://localhost:8080/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-4o", "messages": [ { "role": "user", "content": "List files in the current directory" } ] }' ``` **Response with tool calls:** ```json { "id": "chatcmpl-abc123", "choices": [{ "index": 0, "message": { "role": "assistant", "content": null, "tool_calls": [{ "id": "call_xyz789", "type": "function", "function": { "name": "filesystem_list_directory", "arguments": "{\"path\": \".\"}" } }] }, "finish_reason": "tool_calls" }] } ``` Tool names are prefixed with the MCP client name (e.g., `filesystem_list_directory`). This ensures uniqueness across multiple MCP clients. ### Step 2: Execute the Tool The request body matches the tool call object from the response: ```bash curl -X POST http://localhost:8080/v1/mcp/tool/execute \ -H "Content-Type: application/json" \ -d '{ "id": "call_xyz789", "type": "function", "function": { "name": "filesystem_list_directory", "arguments": "{\"path\": \".\"}" } }' ``` **Tool result response:** ```json { "role": "tool", "content": "[\"config.json\", \"main.go\", \"README.md\"]", "tool_call_id": "call_xyz789" } ``` ### Step 3: Continue the Conversation Assemble the full conversation history and continue: ```bash curl -X POST http://localhost:8080/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "openai/gpt-4o", "messages": [ { "role": "user", "content": "List files in the current directory" }, { "role": "assistant", "content": null, "tool_calls": [{ "id": "call_xyz789", "type": "function", "function": { "name": "filesystem_list_directory", "arguments": "{\"path\": \".\"}" } }] }, { "role": "tool", "content": "[\"config.json\", \"main.go\", \"README.md\"]", "tool_call_id": "call_xyz789" } ] }' ``` **Final response:** ```json { "choices": [{ "message": { "role": "assistant", "content": "The current directory contains 3 files:\n\n1. **config.json** - Configuration file\n2. **main.go** - Go source file\n3. **README.md** - Documentation" }, "finish_reason": "stop" }] } ``` ```go package main import ( "context" "fmt" bifrost "github.com/maximhq/bifrost/core" "github.com/maximhq/bifrost/core/schemas" ) func main() { // Initialize Bifrost with MCP (see Connecting to Servers) client, _ := bifrost.Init(context.Background(), config) // Step 1: Send initial request firstMessage := schemas.ChatMessage{ Role: schemas.ChatMessageRoleUser, Content: schemas.ChatMessageContent{ ContentStr: bifrost.Ptr("List files in the current directory"), }, } request := &schemas.BifrostChatRequest{ Provider: schemas.OpenAI, Model: "gpt-4o", Input: []schemas.ChatMessage{firstMessage}, } response, err := client.ChatCompletionRequest(schemas.NewBifrostContext(context.Background(), schemas.NoDeadline), request) if err != nil { panic(err) } // Build conversation history history := []schemas.ChatMessage{firstMessage} // Step 2: Process tool calls if response.Choices[0].Message.ToolCalls != nil { assistantMessage := response.Choices[0].Message history = append(history, assistantMessage) for _, toolCall := range *assistantMessage.ToolCalls { fmt.Printf("Tool requested: %s\n", *toolCall.Function.Name) // YOUR APPROVAL LOGIC HERE // - Validate arguments // - Check permissions // - Get user confirmation if needed // Step 3: Execute the tool toolResult, err := client.ExecuteChatMCPTool(context.Background(), toolCall) if err != nil { fmt.Printf("Tool execution failed: %v\n", err) continue } fmt.Printf("Tool result: %s\n", *toolResult.Content.ContentStr) history = append(history, *toolResult) } } // Step 4: Continue conversation with results finalRequest := &schemas.BifrostChatRequest{ Provider: schemas.OpenAI, Model: "gpt-4o", Input: history, } finalResponse, err := client.ChatCompletionRequest(schemas.NewBifrostContext(context.Background(), schemas.NoDeadline), finalRequest) if err != nil { panic(err) } fmt.Printf("Final response: %s\n", *finalResponse.Choices[0].Message.Content.ContentStr) } ``` --- ## Response Formats Bifrost supports two API formats for tool execution: ### Chat Format (Default) Use `?format=chat` or omit the parameter: ```bash POST /v1/mcp/tool/execute?format=chat ``` **Request:** ```json { "id": "call_xyz789", "type": "function", "function": { "name": "filesystem_read_file", "arguments": "{\"path\": \"config.json\"}" } } ``` **Response:** ```json { "role": "tool", "content": "{\"key\": \"value\"}", "tool_call_id": "call_xyz789" } ``` ### Responses Format Use `?format=responses` for the Responses API format: ```bash POST /v1/mcp/tool/execute?format=responses ``` **Request:** ```json { "type": "function_call_output", "call_id": "call_xyz789", "name": "filesystem_read_file", "arguments": "{\"path\": \"config.json\"}" } ``` **Response:** ```json { "type": "function_call_output", "call_id": "call_xyz789", "output": "{\"key\": \"value\"}" } ``` --- ## Multiple Tool Calls LLMs often request multiple tools in a single response. Execute them in sequence or parallel: ```go for _, toolCall := range *response.Choices[0].Message.ToolCalls { result, err := client.ExecuteChatMCPTool(ctx, toolCall) if err != nil { // Handle error continue } history = append(history, *result) } ``` ```go toolCalls := *response.Choices[0].Message.ToolCalls results := make([]*schemas.ChatMessage, len(toolCalls)) var wg sync.WaitGroup for i, toolCall := range toolCalls { wg.Add(1) go func(idx int, tc schemas.ChatAssistantMessageToolCall) { defer wg.Done() result, err := client.ExecuteChatMCPTool(ctx, tc) if err == nil { results[idx] = result } }(i, toolCall) } wg.Wait() for _, result := range results { if result != nil { history = append(history, *result) } } ``` --- ## Error Handling Tool execution can fail for various reasons: ```go result, err := client.ExecuteChatMCPTool(ctx, toolCall) if err != nil { switch { case errors.Is(err, context.DeadlineExceeded): // Tool execution timed out case strings.Contains(err.Error(), "tool not found"): // Tool doesn't exist or client disconnected case strings.Contains(err.Error(), "not allowed"): // Tool filtered out by configuration default: // Other execution error } } ``` **Gateway error responses:** ```json { "error": { "type": "tool_execution_error", "message": "Tool 'filesystem_delete_file' is not allowed for this request" } } ``` --- ## Copy-Pastable Responses Tool execution responses are designed to be directly appended to your conversation history: ```go // Tool result is already in the correct format toolResult, _ := client.ExecuteChatMCPTool(ctx, toolCall) // Just append it directly history = append(history, *toolResult) ``` The response includes: - Correct `role` field (`"tool"`) - Matching `tool_call_id` for correlation - Properly formatted `content` --- ## Next Steps Enable autonomous tool execution with auto-approval Control which tools are available per request