From e2bb544c37a07148ea9273f8792c2820bfc50afe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Jun 2026 08:08:49 +0000 Subject: [PATCH 1/3] Initial plan From e52a96b73658a14fccd06c99ccbd6f7e746b5144 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Jun 2026 08:10:35 +0000 Subject: [PATCH 2/3] Add required array to MCP custom tool inputSchema --- .../Core/DynamicCustomTool.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/Azure.DataApiBuilder.Mcp/Core/DynamicCustomTool.cs b/src/Azure.DataApiBuilder.Mcp/Core/DynamicCustomTool.cs index 5a94acd4cb..3c11005229 100644 --- a/src/Azure.DataApiBuilder.Mcp/Core/DynamicCustomTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/Core/DynamicCustomTool.cs @@ -361,6 +361,7 @@ private JsonElement BuildInputSchema() } Dictionary properties = new(); + List required = new(); foreach ((string paramName, ParameterDefinition paramDef) in spDefinition.Parameters) { Dictionary paramSchema = new() @@ -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 schema = new() @@ -378,6 +385,11 @@ private JsonElement BuildInputSchema() ["properties"] = properties }; + if (required.Count > 0) + { + schema["required"] = required; + } + return JsonSerializer.SerializeToElement(schema); } @@ -396,6 +408,7 @@ private JsonElement BuildInputSchemaFromConfig() if (_entity.Source.Parameters != null && _entity.Source.Parameters.Any()) { Dictionary properties = (Dictionary)schema["properties"]; + List required = new(); foreach (ParameterMetadata param in _entity.Source.Parameters) { @@ -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; } } From 7b5252ee85393fb791487a5cc002c75013921771 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 26 Jun 2026 08:15:37 +0000 Subject: [PATCH 3/3] Add tests for required array in MCP custom tool inputSchema --- .../Mcp/DynamicCustomToolTests.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/Service.Tests/Mcp/DynamicCustomToolTests.cs b/src/Service.Tests/Mcp/DynamicCustomToolTests.cs index bb67aec5a0..aaea16cd23 100644 --- a/src/Service.Tests/Mcp/DynamicCustomToolTests.cs +++ b/src/Service.Tests/Mcp/DynamicCustomToolTests.cs @@ -215,6 +215,33 @@ public void GetToolMetadata_UsesDefaultParameterDescription_WhenNotProvided() Assert.IsTrue(desc.GetString()!.Contains("userId")); } + /// + /// 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. + /// + [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 requiredNames = required.EnumerateArray().Select(e => e.GetString()!).ToList(); + CollectionAssert.AreEquivalent(new[] { "firstName", "lastName" }, requiredNames); + } + #region Parameter Validation Tests (ExecuteAsync) /// @@ -697,6 +724,35 @@ public void InitializeMetadata_IncludesDefaultInDescription() StringAssert.Contains(desc, "default: default_tenant"); } + /// + /// 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. + /// + [TestMethod] + public void InitializeMetadata_PopulatesRequired_ForParamsWithoutDefault() + { + // Arrange + Dictionary 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 requiredNames = required.EnumerateArray().Select(e => e.GetString()!).ToList(); + CollectionAssert.AreEquivalent(new[] { "firstName", "lastName" }, requiredNames); + } + /// /// Zero-parameter SP with DB metadata returns empty properties object. ///