Skip to content

Client Tools

Client Tools are tools provided by the client application (IDE extension, web UI, mobile app) at runtime. They enable human-in-the-loop interactions where the agent requests actions that the client executes.

What Are Client Tools?

Unlike C# tools (server-side) or MCP servers (external processes), Client Tools are:

  • Defined by the client at runtime
  • Executed by the client (not the server)
  • Human-in-the-loop - the user sees and controls what happens
Agent: "I need to open a file"


Client Tool Request ──► Client Application
    │                         │
    │                    User sees action
    │                         │
    ◄─────────────────── Tool Result

Agent: "File opened successfully"

Common use cases:

  • Open files in an IDE
  • Show dialogs to the user
  • Get user's current selection
  • Navigate to URLs
  • Display previews

Quick Start

1. Enable Client Tools

csharp
var agent = new AgentBuilder()
    .WithClientTools()
    .Build();

2. Provide Tools at Runtime

Tools are provided through AgentRunInput:

csharp
var runInput = new AgentRunInput
{
    ClientToolGroups = new[]
    {
        new ClientToolGroupDefinition(
            Name: "IDE",
            Description: "IDE interaction tools",
            Tools: new[]
            {
                new ClientToolDefinition(
                    Name: "OpenFile",
                    Description: "Open a file in the editor",
                    ParametersSchema: JsonDocument.Parse(@"{
                        ""type"": ""object"",
                        ""properties"": {
                            ""path"": { ""type"": ""string"", ""description"": ""File path"" }
                        },
                        ""required"": [""path""]
                    }").RootElement
                )
            }
        )
    }
};

await foreach (var evt in agent.RunAsync("Open the config file", thread, runInput))
{
    // Handle events...
}

3. Handle Tool Invocations

Listen for ClientToolInvokeRequestEvent and respond:

csharp
await foreach (var evt in agent.RunAsync(message, thread, runInput))
{
    if (evt is ClientToolInvokeRequestEvent request)
    {
        // Execute the tool (client-side)
        var result = await ExecuteToolAsync(request.ToolName, request.Arguments);

        // Send response back
        await agent.SendEventAsync(new ClientToolInvokeResponseEvent(
            RequestId: request.RequestId,
            Content: new[] { new TextContent(result) },
            Success: true
        ));
    }
}

Tool Group Definition

Client tools are organized into tool groups:

csharp
public record ClientToolGroupDefinition(
    string Name,                    // Tool group identifier
    string? Description,            // Shown when collapsed
    IReadOnlyList<ClientToolDefinition> Tools,
    IReadOnlyList<ClientSkillDefinition>? Skills = null,
    string? FunctionResult = null,  // One-time message on expansion
    string? SystemPrompt = null,    // Persistent instructions
    bool StartCollapsed = true      // Hide behind container initially
);

Tool Definition

csharp
public record ClientToolDefinition(
    string Name,                    // Unique tool name
    string Description,             // Shown to agent
    JsonElement ParametersSchema,   // JSON Schema for parameters
    bool RequiresPermission = false // Require user approval
);

Example: IDE Tool Group

csharp
new ClientToolGroupDefinition(
    Name: "IDE",
    Description: "VS Code interaction tools",
    StartCollapsed: true,
    // Auto-generated: "IDE expanded. Available functions: OpenFile, GetSelection, ShowMessage"
    FunctionResult: null,  // Auto-generated message is sufficient
    SystemPrompt: "Use ShowMessage for important notifications to the user.",
    Tools: new[]
    {
        new ClientToolDefinition(
            Name: "OpenFile",
            Description: "Open a file in the editor",
            ParametersSchema: ParseSchema(@"{
                ""type"": ""object"",
                ""properties"": {
                    ""path"": { ""type"": ""string"" },
                    ""line"": { ""type"": ""integer"" }
                },
                ""required"": [""path""]
            }")
        ),
        new ClientToolDefinition(
            Name: "GetSelection",
            Description: "Get the user's current text selection",
            ParametersSchema: ParseSchema(@"{ ""type"": ""object"" }")
        ),
        new ClientToolDefinition(
            Name: "ShowMessage",
            Description: "Show a message to the user",
            ParametersSchema: ParseSchema(@"{
                ""type"": ""object"",
                ""properties"": {
                    ""message"": { ""type"": ""string"" },
                    ""severity"": { ""type"": ""string"", ""enum"": [""info"", ""warning"", ""error""] }
                },
                ""required"": [""message""]
            }")
        )
    }
)

Collapsing

By default, client tool groups start collapsed (StartCollapsed = true). This groups all tools under a container:

Before expansion:        After expansion:
┌──────────────────┐     ┌──────────────────┐
│ IDE              │ ──► │ OpenFile         │
│ (3 tools)        │     │ GetSelection     │
└──────────────────┘     │ ShowMessage      │
                         └──────────────────┘

Control Collapsing Globally

csharp
var config = new AgentConfig
{
    Collapsing = new CollapsingConfig
    {
        CollapseClientTools = true,  // Enable collapsing
        ClientToolsInstructions = "These tools interact with the user's IDE."
    }
};

Control Per-Group

csharp
new ClientToolGroupDefinition(
    Name: "CriticalTools",
    StartCollapsed: false,  // Always visible
    Tools: ...
)

Pre-Expand Tool Groups

csharp
var runInput = new AgentRunInput
{
    ClientToolGroups = toolGroups,
    ExpandedContainers = new HashSet<string> { "IDE" }  // Start expanded
};

Instructions

Provide guidance when tool groups expand using dual-context architecture:

ParameterLocationLifetimeUse For
FunctionResultConversation historyOne-time on expansionAdditional context (appended to auto-generated message)
SystemPromptSystem promptEvery turn while expandedCritical rules, workflow

Important: The system automatically generates a base expansion message:

"{ToolGroupName} expanded. Available functions: {FunctionList}"

Your FunctionResult is appended to this auto-generated message. Don't duplicate the expansion info—use it only for additional context. Pass null if the auto-generated message is sufficient.

FunctionResult (One-Time, Appended)

csharp
new ClientToolGroupDefinition(
    Name: "IDE",
    // Auto-generated: "IDE expanded. Available functions: OpenFile, GetSelection, ShowMessage"
    FunctionResult: "Tip: Use GetSelection before making edits.",  // Additional context only
    ...
)

SystemPrompt (Persistent)

csharp
new ClientToolGroupDefinition(
    Name: "IDE",
    SystemPrompt: @"
        IDE RULES:
        - Always confirm before modifying files
        - Use ShowMessage for important notifications
        - Check selection before applying edits",
    ...
)

Global Client Tool Instructions

csharp
var config = new AgentConfig
{
    Collapsing = new CollapsingConfig
    {
        ClientToolsInstructions = "These tools interact with the user. Be respectful of their time."
    }
};

Handling Tool Invocations

Request Event

When the agent calls a client tool:

csharp
public record ClientToolInvokeRequestEvent(
    string RequestId,       // Correlation ID (must match in response)
    string ToolName,        // Which tool to execute
    string CallId,          // LLM's function call ID
    IReadOnlyDictionary<string, object?> Arguments,
    string? Description
);

Response Event

Send back the result:

csharp
public record ClientToolInvokeResponseEvent(
    string RequestId,       // Must match request
    IReadOnlyList<IToolResultContent> Content,
    bool Success = true,
    string? ErrorMessage = null,
    ClientToolAugmentation? Augmentation = null
);

Result Content Types

csharp
// Text result
new TextContent("File opened successfully")

// JSON result
new JsonContent(JsonDocument.Parse(@"{ ""line"": 42, ""column"": 10 }").RootElement)

// Binary result (file, image, etc.)
new BinaryContent(
    MimeType: "image/png",
    Data: base64EncodedData,
    Filename: "screenshot.png"
)

Example Handler

csharp
await foreach (var evt in agent.RunAsync(message, thread, runInput))
{
    switch (evt)
    {
        case ClientToolInvokeRequestEvent request:
            try
            {
                var result = request.ToolName switch
                {
                    "OpenFile" => await OpenFileAsync(request.Arguments["path"]?.ToString()),
                    "GetSelection" => await GetSelectionAsync(),
                    "ShowMessage" => await ShowMessageAsync(request.Arguments),
                    _ => throw new NotSupportedException($"Unknown tool: {request.ToolName}")
                };

                await agent.SendEventAsync(new ClientToolInvokeResponseEvent(
                    RequestId: request.RequestId,
                    Content: new[] { new TextContent(result) },
                    Success: true
                ));
            }
            catch (Exception ex)
            {
                await agent.SendEventAsync(new ClientToolInvokeResponseEvent(
                    RequestId: request.RequestId,
                    Content: Array.Empty<IToolResultContent>(),
                    Success: false,
                    ErrorMessage: ex.Message
                ));
            }
            break;

        case TextDeltaEvent delta:
            Console.Write(delta.Text);
            break;
    }
}

Dynamic State Changes (Augmentation)

Tool responses can modify the agent's state:

csharp
await agent.SendEventAsync(new ClientToolInvokeResponseEvent(
    RequestId: request.RequestId,
    Content: new[] { new TextContent("User logged in") },
    Augmentation: new ClientToolAugmentation
    {
        // Add new tool groups
        InjectToolGroups = new[] { adminToolGroup },

        // Remove tool groups
        RemoveToolGroups = new HashSet<string> { "LoginTools" },

        // Control expansion
        ExpandToolGroups = new HashSet<string> { "AdminTools" },
        CollapseToolGroups = new HashSet<string> { "GuestTools" },

        // Control visibility
        HideTools = new HashSet<string> { "Login" },
        ShowTools = new HashSet<string> { "Logout" },

        // Update context
        AddContext = new[] { new ContextItem("user", "Admin user logged in") }
    }
));

Configuration

csharp
var agent = new AgentBuilder()
    .WithClientTools(config =>
    {
        config.InvokeTimeout = TimeSpan.FromSeconds(30);
        config.DisconnectionStrategy = ClientDisconnectionStrategy.FallbackMessage;
        config.MaxRetries = 3;
        config.FallbackMessageTemplate = "Client disconnected. Tool '{0}' unavailable.";
        config.ValidateSchemaOnRegistration = true;
    })
    .Build();

Disconnection Strategies

StrategyBehavior
FailFastThrow exception immediately
RetryWithBackoffRetry up to MaxRetries times
FallbackMessageReturn error message to agent (default)

Client Skills

Group tools into workflows with dual-context instructions:

csharp
new ClientToolGroupDefinition(
    Name: "Checkout",
    Tools: new[] { validateCart, processPayment, confirmOrder },
    Skills: new[]
    {
        new ClientSkillDefinition(
            Name: "Complete Checkout",
            Description: "Process a customer's checkout",
            // Auto-generated: "Complete Checkout skill activated. Available functions: ValidateCart, ProcessPayment, ConfirmOrder"
            FunctionResult: null,  // Auto-generated message is sufficient
            SystemPrompt: @"
                CHECKOUT WORKFLOW:
                1. Validate the cart contents
                2. Process payment
                3. Confirm order with customer
                4. Show confirmation message",
            References: new[]
            {
                new ClientSkillReference("ValidateCart"),
                new ClientSkillReference("ProcessPayment"),
                new ClientSkillReference("ConfirmOrder")
            }
        )
    }
)

State Persistence

Client state persists across message turns by default:

csharp
// Turn 1: Register tool groups
var runInput1 = new AgentRunInput { ClientToolGroups = toolGroups };
await agent.RunAsync("Hello", thread, runInput1);

// Turn 2: Tool groups still registered (state persists)
await agent.RunAsync("Use the IDE tools", thread);

// Turn 3: Reset state
var runInput3 = new AgentRunInput { ResetClientState = true };
await agent.RunAsync("Start fresh", thread, runInput3);

Best Practices

  1. Use collapsing for tool groups with many tools to reduce context clutter.

  2. Provide clear descriptions so the agent knows when to use each tool.

  3. Handle errors gracefully - always send a response, even on failure.

  4. Use RequiresPermission for tools that modify user data.

  5. Keep tools focused - one action per tool.

csharp
// Good: Focused tools with clear purposes
new ClientToolDefinition(
    Name: "OpenFile",
    Description: "Open a file in the editor at an optional line number",
    RequiresPermission: false,
    ParametersSchema: ...
)

new ClientToolDefinition(
    Name: "DeleteFile",
    Description: "Permanently delete a file",
    RequiresPermission: true,  // Destructive action
    ParametersSchema: ...
)

Troubleshooting

IssueCauseFix
Tool not appearingTool group collapsedSet StartCollapsed: false or pre-expand
Timeout errorClient not respondingIncrease InvokeTimeout
Schema validation errorInvalid JSON SchemaCheck schema syntax
Response not receivedRequestId mismatchEnsure RequestId matches

Released under the MIT License.