---
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