Tool Results
Tool handlers return a JSON string that is injected into the LLM context as a tool result message. The format, size, and structure of this result directly affects how well the LLM can use it. This page covers result format guidelines, error handling, and result truncation.
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
| Guideline | Reason |
|---|---|
| Return JSON (not plain text) | Structured data is easier for the LLM to parse and reference precisely |
| Include context in the result | Repeat key parameters (e.g. employeeId) so the LLM knows which request this answers |
| Cap list results to 10–20 items | Longer lists waste tokens; the LLM rarely needs more than 10 results |
| Use human-readable field names | The LLM uses field names to understand the data — avoid abbreviations |
| Include units in numeric fields | "remainingDays": 12 not "remaining": 12 — ambiguous |
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.