415 lines
9.4 KiB
Plaintext
415 lines
9.4 KiB
Plaintext
---
|
|
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
|
|
|
|
<Tabs>
|
|
<Tab title="Gateway">
|
|
|
|
### 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"
|
|
}]
|
|
}
|
|
```
|
|
|
|
<Note>
|
|
Tool names are prefixed with the MCP client name (e.g., `filesystem_list_directory`). This ensures uniqueness across multiple MCP clients.
|
|
</Note>
|
|
|
|
### 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"
|
|
}]
|
|
}
|
|
```
|
|
|
|
</Tab>
|
|
<Tab title="Go SDK">
|
|
|
|
```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)
|
|
}
|
|
```
|
|
|
|
</Tab>
|
|
</Tabs>
|
|
|
|
---
|
|
|
|
## 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:
|
|
|
|
<Tabs>
|
|
<Tab title="Sequential">
|
|
|
|
```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)
|
|
}
|
|
```
|
|
|
|
</Tab>
|
|
<Tab title="Parallel">
|
|
|
|
```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)
|
|
}
|
|
}
|
|
```
|
|
|
|
</Tab>
|
|
</Tabs>
|
|
|
|
---
|
|
|
|
## 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
|
|
|
|
<CardGroup cols={2}>
|
|
<Card title="Agent Mode" icon="robot" href="./agent-mode">
|
|
Enable autonomous tool execution with auto-approval
|
|
</Card>
|
|
<Card title="Tool Filtering" icon="filter" href="./filtering">
|
|
Control which tools are available per request
|
|
</Card>
|
|
</CardGroup>
|