diff --git a/Directory.Build.props b/Directory.Build.props
index 0817cc4..2eee157 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,7 +1,7 @@
- 0.3.0
+ 0.3.1
diff --git a/src/McpAggregator.Core/Tools/ToolProxyHandler.cs b/src/McpAggregator.Core/Tools/ToolProxyHandler.cs
index 23488a1..d9905b1 100644
--- a/src/McpAggregator.Core/Tools/ToolProxyHandler.cs
+++ b/src/McpAggregator.Core/Tools/ToolProxyHandler.cs
@@ -1,4 +1,5 @@
using System.Diagnostics;
+using System.Text;
using System.Text.Json;
using McpAggregator.Core.Configuration;
using McpAggregator.Core.Exceptions;
@@ -12,15 +13,20 @@ namespace McpAggregator.Core.Tools;
public class ToolProxyHandler
{
private readonly ConnectionManager _connectionManager;
+ private readonly ToolIndex _toolIndex;
private readonly AggregatorOptions _options;
private readonly ILogger _logger;
+ private static readonly JsonSerializerOptions SchemaHintJsonOptions = new() { WriteIndented = false };
+
public ToolProxyHandler(
ConnectionManager connectionManager,
+ ToolIndex toolIndex,
IOptions options,
ILogger logger)
{
_connectionManager = connectionManager;
+ _toolIndex = toolIndex;
_options = options.Value;
_logger = logger;
}
@@ -43,6 +49,66 @@ private void LogCallToolResult(string toolName, string serverName, CallToolResul
}
}
+ ///
+ /// When a tool call comes back as an error, checks the supplied arguments against the
+ /// downstream tool's input schema. Returns a corrective message (listing the missing/unknown
+ /// keys and the full schema) only when there is an actual argument mismatch — genuine tool-side
+ /// errors on schema-valid arguments are left untouched. Returns null when no hint applies.
+ ///
+ private async Task TryBuildArgumentHintAsync(
+ string serverName,
+ string toolName,
+ IReadOnlyDictionary? providedArgs,
+ CancellationToken ct)
+ {
+ try
+ {
+ var tools = await _toolIndex.GetToolsForServerAsync(serverName, ct);
+ var tool = tools.FirstOrDefault(t => string.Equals(t.Name, toolName, StringComparison.Ordinal));
+
+ if (tool?.InputSchema is not JsonElement schema || schema.ValueKind != JsonValueKind.Object)
+ return null;
+
+ var propertyNames = schema.TryGetProperty("properties", out var props) && props.ValueKind == JsonValueKind.Object
+ ? props.EnumerateObject().Select(p => p.Name).ToList()
+ : [];
+
+ var required = schema.TryGetProperty("required", out var req) && req.ValueKind == JsonValueKind.Array
+ ? req.EnumerateArray().Where(e => e.ValueKind == JsonValueKind.String).Select(e => e.GetString()!).ToList()
+ : [];
+
+ var providedKeys = providedArgs?.Keys.ToList() ?? [];
+
+ var missingRequired = required.Where(r => !providedKeys.Contains(r, StringComparer.Ordinal)).ToList();
+ // Only flag unknown keys when the schema actually declares its properties.
+ var unknownKeys = propertyNames.Count > 0
+ ? providedKeys.Where(k => !propertyNames.Contains(k, StringComparer.Ordinal)).ToList()
+ : [];
+
+ if (missingRequired.Count == 0 && unknownKeys.Count == 0)
+ return null;
+
+ var schemaText = JsonSerializer.Serialize(schema, SchemaHintJsonOptions);
+
+ var sb = new StringBuilder();
+ sb.Append("Argument mismatch for tool '").Append(toolName).Append("' on '").Append(serverName).Append("'. ");
+ if (missingRequired.Count > 0)
+ sb.Append("Missing required parameter(s): [").Append(string.Join(", ", missingRequired)).Append("]. ");
+ if (unknownKeys.Count > 0)
+ sb.Append("Unrecognized argument key(s): [").Append(string.Join(", ", unknownKeys)).Append("]. ");
+ if (providedKeys.Count > 0)
+ sb.Append("You sent: [").Append(string.Join(", ", providedKeys)).Append("]. ");
+ sb.Append("Re-invoke with arguments matching this input schema: ").Append(schemaText);
+
+ return sb.ToString();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogDebug(ex, "Failed to build argument hint for '{Tool}' on '{Server}'", toolName, serverName);
+ return null;
+ }
+ }
+
private static object? ConvertJsonElement(JsonElement element)
{
return element.ValueKind switch
@@ -101,6 +167,20 @@ public async Task InvokeAsync(
{
var errorText = string.Join("\n", result.Content.OfType().Select(b => b.Text));
_logger.LogWarning("Tool '{Tool}' on '{Server}' returned error: {Error}", toolName, serverName, errorText);
+
+ // Self-correction: a common failure is the caller guessing parameter names
+ // (e.g. from a tool description) instead of the actual input schema. Downstream
+ // SDKs sanitize the binding error to a generic message, so attach the authoritative
+ // schema and the specific mismatch so the model can retry without a separate
+ // get_service_details round-trip.
+ var hint = await TryBuildArgumentHintAsync(serverName, toolName, args, ct);
+ if (hint is not null)
+ {
+ result.Content = [.. result.Content, new TextContentBlock { Text = hint }];
+ _logger.LogInformation(
+ "Attached argument-schema hint to error result for '{Tool}' on '{Server}'",
+ toolName, serverName);
+ }
}
resultLabel = "success";