Skip to content
Draft
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
24 changes: 24 additions & 0 deletions src/Azure.DataApiBuilder.Mcp/Core/DynamicCustomTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ private JsonElement BuildInputSchema()
}

Dictionary<string, object> properties = new();
List<string> required = new();
foreach ((string paramName, ParameterDefinition paramDef) in spDefinition.Parameters)
{
Dictionary<string, object> paramSchema = new()
Expand All @@ -370,6 +371,12 @@ private JsonElement BuildInputSchema()
};

properties[paramName] = paramSchema;

// A parameter is required when no default value is available to fall back on.
if (!paramDef.HasConfigDefault)
{
required.Add(paramName);
}
}

Dictionary<string, object> schema = new()
Expand All @@ -378,6 +385,11 @@ private JsonElement BuildInputSchema()
["properties"] = properties
};

if (required.Count > 0)
{
schema["required"] = required;
}

return JsonSerializer.SerializeToElement(schema);
}

Expand All @@ -396,6 +408,7 @@ private JsonElement BuildInputSchemaFromConfig()
if (_entity.Source.Parameters != null && _entity.Source.Parameters.Any())
{
Dictionary<string, object> properties = (Dictionary<string, object>)schema["properties"];
List<string> required = new();

foreach (ParameterMetadata param in _entity.Source.Parameters)
{
Expand All @@ -404,6 +417,17 @@ private JsonElement BuildInputSchemaFromConfig()
["type"] = new[] { "string", "number", "boolean", "null" },
["description"] = param.Description ?? $"Parameter {param.Name}"
};

// A parameter is required when no default value is configured.
if (param.Default is null)
{
required.Add(param.Name);
}
}

if (required.Count > 0)
{
schema["required"] = required;
}
}

Expand Down
56 changes: 56 additions & 0 deletions src/Service.Tests/Mcp/DynamicCustomToolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,33 @@ public void GetToolMetadata_UsesDefaultParameterDescription_WhenNotProvided()
Assert.IsTrue(desc.GetString()!.Contains("userId"));
}

/// <summary>
/// Test that the config-based input schema lists parameters without a default value
/// in the JSON Schema "required" array, while excluding those that have a default.
/// </summary>
[TestMethod]
public void GetToolMetadata_PopulatesRequired_FromConfigParameters()
{
// Arrange
ParameterMetadata[] parameters = new[]
{
new ParameterMetadata { Name = "firstName" },
new ParameterMetadata { Name = "lastName" },
new ParameterMetadata { Name = "nickname", Default = "guest" }
};
Entity entity = CreateTestStoredProcedureEntity(parameters: parameters);
DynamicCustomTool tool = new("GetUser", entity);

// Act
ModelContextProtocol.Protocol.Tool metadata = tool.GetToolMetadata();

// Assert
JsonDocument schemaObj = JsonDocument.Parse(metadata.InputSchema.GetRawText());
Assert.IsTrue(schemaObj.RootElement.TryGetProperty("required", out JsonElement required));
List<string> requiredNames = required.EnumerateArray().Select(e => e.GetString()!).ToList();
CollectionAssert.AreEquivalent(new[] { "firstName", "lastName" }, requiredNames);
}

#region Parameter Validation Tests (ExecuteAsync)

/// <summary>
Expand Down Expand Up @@ -697,6 +724,35 @@ public void InitializeMetadata_IncludesDefaultInDescription()
StringAssert.Contains(desc, "default: default_tenant");
}

/// <summary>
/// Parameters discovered via DB metadata that lack a config default are listed in the
/// JSON Schema "required" array, while parameters with a default are excluded.
/// </summary>
[TestMethod]
public void InitializeMetadata_PopulatesRequired_ForParamsWithoutDefault()
{
// Arrange
Dictionary<string, ParameterDefinition> dbParams = new()
{
["firstName"] = new() { SystemType = typeof(string) },
["lastName"] = new() { SystemType = typeof(string) },
["tenant"] = new() { SystemType = typeof(string), HasConfigDefault = true, ConfigDefaultValue = "default_tenant" }
};

const string entityName = "TestSP";
Entity entity = CreateTestStoredProcedureEntity();
DynamicCustomTool tool = new(entityName, entity);
tool.InitializeMetadata(BuildServiceProviderForMetadata(entityName, dbParams));

// Act
JsonElement schema = tool.GetToolMetadata().InputSchema;

// Assert
Assert.IsTrue(schema.TryGetProperty("required", out JsonElement required));
List<string> requiredNames = required.EnumerateArray().Select(e => e.GetString()!).ToList();
CollectionAssert.AreEquivalent(new[] { "firstName", "lastName" }, requiredNames);
}

/// <summary>
/// Zero-parameter SP with DB metadata returns empty properties object.
/// </summary>
Expand Down