Portal Community

Result Format

Tool handlers must return a JSON string. The LLM reads this string and uses the data in its next response. Return only what the LLM needs — not full database records:

// Good: minimal, focused result
return JsonSerializer.Serialize(new
{
    employeeId    = "EMP-1042",
    remainingDays = 12,
    usedDays      = 8,
    totalDays     = 20,
    year          = 2025
});
// → {"employeeId":"EMP-1042","remainingDays":12,"usedDays":8,"totalDays":20,"year":2025}

// Bad: full database record with irrelevant fields
return JsonSerializer.Serialize(employeeEntity);
// → Includes internal IDs, audit timestamps, FK references, encrypted fields...

Error Results

When a tool fails, return a structured error result rather than throwing an exception. The LLM can then decide how to handle the failure:

// Structured error result (LLM can reason about this)
public async Task<string> GetLeaveBalanceAsync(JsonElement input, ConversationContext ctx, CancellationToken ct)
{
    try
    {
        string empId = input.GetProperty("employeeId").GetString()
            ?? throw new ArgumentException("employeeId is required");

        int days = await _leaveService.GetBalanceAsync(empId, ctx.TenantId, ct);
        return JsonSerializer.Serialize(new { employeeId = empId, remainingDays = days });
    }
    catch (EmployeeNotFoundException)
    {
        return JsonSerializer.Serialize(new
        {
            error   = "not_found",
            message = $"Employee '{input.GetProperty("employeeId").GetString()}' not found.",
            hint    = "Ask the user to verify their employee ID."
        });
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Tool execution failed");
        return JsonSerializer.Serialize(new
        {
            error   = "internal_error",
            message = "An internal error occurred. Please try again."
        });
    }
}

Result Truncation

Large tool results — such as database queries returning many rows — must be truncated before they enter the LLM context. The ToolResultTruncator enforces a maximum token limit:

public class ToolResultTruncator
{
    private readonly ITokenCounter _counter;

    public string Truncate(string toolResult, int maxTokens = 2000)
    {
        int tokens = _counter.Count(toolResult);
        if (tokens <= maxTokens)
            return toolResult;  // No truncation needed

        // Approximate: 4 chars per token
        int keepChars = maxTokens * 4;
        string truncated = toolResult[..keepChars];

        return truncated + $"\n[... result truncated — {tokens - maxTokens} tokens omitted ...]";
    }
}

// Applied automatically in MCPToolRegistry.ExecuteAsync:
string rawResult = await tool.Handler(request.Input, context, ct);
string safeResult = _truncator.Truncate(rawResult, maxTokens: 2000);

Result Design Guidelines

GuidelineReason
Return JSON (not plain text)Structured data is easier for the LLM to parse and reference precisely
Include context in the resultRepeat key parameters (e.g. employeeId) so the LLM knows which request this answers
Cap list results to 10–20 itemsLonger lists waste tokens; the LLM rarely needs more than 10 results
Use human-readable field namesThe LLM uses field names to understand the data — avoid abbreviations
Include units in numeric fields"remainingDays": 12 not "remaining": 12 — ambiguous
Tool Results Are Stored in Episodic Memory

Tool results appear in the message history that is summarised into episodic memory at session end. Avoid including sensitive data (SSNs, salaries, passwords) in tool results — they will be persisted in episodic memory and may be recalled in future sessions.