Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ public class AggregateRecordsTool : IMcpTool
},
""field"": {
""type"": ""string"",
""description"": ""Field name to aggregate, or * with count to count all rows.""
""description"": ""Field name to aggregate, or * with count to count all rows. Defaults to '*' when omitted (only valid with function 'count')."",
""default"": ""*""
},
""distinct"": {
""type"": ""boolean"",
Expand Down Expand Up @@ -128,7 +129,7 @@ public class AggregateRecordsTool : IMcpTool
""description"": ""Opaque cursor from a previous endCursor for next-page retrieval. Requires groupby and first. Do not construct manually.""
}
},
""required"": [""entity"", ""function"", ""field""]
""required"": [""entity"", ""function""]
}"
)
};
Expand Down Expand Up @@ -367,13 +368,42 @@ public async Task<CallToolResult> ExecuteAsync(
$"Invalid function '{function}'. Must be one of: count, avg, sum, min, max.", logger);
}

// Parse field
if (!root.TryGetProperty("field", out JsonElement fieldElement) || string.IsNullOrWhiteSpace(fieldElement.GetString()))
// Parse field. When omitted, default to '*' (only valid with function 'count').
// For non-count functions, an explicit numeric field name is required.
string field;
if (root.TryGetProperty("field", out JsonElement fieldElement))
{
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "Missing required argument 'field'.", logger);
if (fieldElement.ValueKind != JsonValueKind.String)
{
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments",
$"Argument 'field' must be a string. Got: '{fieldElement.ValueKind}'.", logger);
}

if (!string.IsNullOrWhiteSpace(fieldElement.GetString()))
{
field = fieldElement.GetString()!;
}
else if (function == "count")
{
field = "*";
}
else
{
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments",
$"Missing required argument 'field'. The default value '*' is only valid with function 'count'; for function '{function}', provide a specific numeric field name.", logger);
}
}
Comment thread
souvikghosh04 marked this conversation as resolved.
else
{
if (function != "count")
{
return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments",
$"Missing required argument 'field'. The default value '*' is only valid with function 'count'; for function '{function}', provide a specific numeric field name.", logger);
}

field = "*";
}

string field = fieldElement.GetString()!;
bool isCountStar = function == "count" && field == "*";

if (field == "*" && function != "count")
Expand Down
42 changes: 40 additions & 2 deletions src/Service.Tests/Mcp/AggregateRecordsToolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,15 @@ public void GetToolMetadata_HasRequiredSchemaProperties()

CollectionAssert.Contains(requiredFields, "entity");
CollectionAssert.Contains(requiredFields, "function");
CollectionAssert.Contains(requiredFields, "field");
CollectionAssert.DoesNotContain(requiredFields, "field",
"'field' must not be required; it defaults to '*'.");

// Verify the field property declares '*' as its default value.
JsonElement fieldProp = properties.GetProperty("field");
Assert.IsTrue(fieldProp.TryGetProperty("default", out JsonElement fieldDefault),
"'field' property must declare a default value.");
Assert.AreEqual("*", fieldDefault.GetString(),
"'field' default must be '*'.");

// Verify all schema properties exist with correct types
AssertSchemaProperty(properties, "entity", "string");
Expand Down Expand Up @@ -119,7 +127,8 @@ public async Task AggregateRecords_Disabled_ReturnsToolDisabledError(bool runtim
[DataTestMethod]
[DataRow("{\"function\": \"count\", \"field\": \"*\"}", null, DisplayName = "Missing entity")]
[DataRow("{\"entity\": \"Book\", \"field\": \"*\"}", null, DisplayName = "Missing function")]
[DataRow("{\"entity\": \"Book\", \"function\": \"count\"}", null, DisplayName = "Missing field")]
[DataRow("{\"entity\": \"Book\", \"function\": \"avg\"}", "field", DisplayName = "Missing field for non-count function")]
[DataRow("{\"entity\": \"Book\", \"function\": \"sum\"}", "field", DisplayName = "Missing field for sum function")]
[DataRow("{\"entity\": \"Book\", \"function\": \"median\", \"field\": \"price\"}", "median", DisplayName = "Invalid function 'median'")]
public async Task AggregateRecords_MissingOrInvalidRequiredArgs_ReturnsInvalidArguments(string json, string expectedInMessage)
Comment thread
souvikghosh04 marked this conversation as resolved.
{
Expand Down Expand Up @@ -155,6 +164,12 @@ public async Task AggregateRecords_NullArguments_ReturnsInvalidArguments()
DisplayName = "Star field with avg (must mention count)")]
[DataRow("{\"entity\": \"Book\", \"function\": \"count\", \"field\": \"*\", \"distinct\": true}", "DISTINCT",
DisplayName = "Distinct with count(*)")]
[DataRow("{\"entity\": \"Book\", \"function\": \"count\", \"distinct\": true}", "DISTINCT",
DisplayName = "Distinct with implicit count(*) default (issue #3280)")]
[DataRow("{\"entity\": \"Book\", \"function\": \"avg\"}", "field",
DisplayName = "Non-count function without field returns InvalidArguments (issue #3280)")]
[DataRow("{\"entity\": \"Book\", \"function\": \"count\", \"field\": 123}", "field",
DisplayName = "Non-string field value returns InvalidArguments")]
public async Task AggregateRecords_InvalidFieldFunctionCombination_ReturnsInvalidArguments(string json, string expectedInMessage)
{
IServiceProvider sp = CreateDefaultServiceProvider();
Expand Down Expand Up @@ -246,6 +261,29 @@ public async Task AggregateRecords_SimpleCountStar_PassesValidation()
"Simple count(*) must pass input validation.");
}

/// <summary>
/// Verifies that count without an explicit field passes validation by defaulting to '*' (issue #3280).
/// </summary>
[TestMethod]
public async Task AggregateRecords_CountWithoutField_PassesValidation()
{
IServiceProvider sp = CreateDefaultServiceProvider();

CallToolResult result = await ExecuteToolAsync(sp,
"{\"entity\": \"Book\", \"function\": \"count\"}");

// May fail at metadata resolution (no real DB), but must NOT fail with InvalidArguments.
if (result.IsError != true)
{
return;
}

JsonElement content = ParseContent(result);
string errorType = content.GetProperty("error").GetProperty("type").GetString()!;
Assert.AreNotEqual("InvalidArguments", errorType,
"count without field must pass input validation by defaulting to '*' (issue #3280).");
}

/// <summary>
/// Verifies that the orderby schema property has no default value (fix for #3279).
/// </summary>
Expand Down
Loading