{"record":{"id":"7cf5f52b56369104","repo":"microsoft/aspire","slug":"an-mcp-approval-filter-must-specify-at-least-one-tool-name","errorCode":null,"errorMessage":"An MCP approval filter must specify at least one tool name or a read-only value.","messagePattern":"An MCP approval filter must specify at least one tool name or a read-only value\\.","errorType":"validation","errorClass":"ArgumentException","httpStatus":null,"severity":"error","filePath":"src/Aspire.Hosting.Foundry/Toolbox/FoundryToolboxToolDefinition.cs","lineNumber":314,"sourceCode":"    {\n        if (filter is null)\n        {\n            return null;\n        }\n\n        var toolNames = (filter.ToolNames ?? [])\n            .Select(name =>\n            {\n                ArgumentException.ThrowIfNullOrWhiteSpace(name, parameterName);\n                return name;\n            })\n            .Distinct(StringComparer.Ordinal)\n            .Order(StringComparer.Ordinal)\n            .ToArray();\n\n        if (toolNames.Length == 0 && filter.ReadOnly is null)\n        {\n            throw new ArgumentException(\n                \"An MCP approval filter must specify at least one tool name or a read-only value.\",\n                parameterName);\n        }\n\n        return new(toolNames, filter.ReadOnly);\n    }\n\n    public void WriteTo(Utf8JsonWriter writer, string propertyName)\n    {\n        writer.WriteStartObject(propertyName);\n        if (ToolNames.Count > 0)\n        {\n            writer.WriteStartArray(\"tool_names\");\n            foreach (var toolName in ToolNames)\n            {\n                writer.WriteStringValue(toolName);\n            }\n            writer.WriteEndArray();","sourceCodeStart":296,"sourceCodeEnd":332,"githubUrl":"https://github.com/microsoft/aspire/blob/25830f84bd145686607ad00c057b3f84e2e51d43/src/Aspire.Hosting.Foundry/Toolbox/FoundryToolboxToolDefinition.cs#L296-L332","documentation":"ResolvedFoundryToolboxMcpApprovalFilter.Create normalizes and deduplicates ToolNames, then requires that the filter actually selects something: at least one tool name, or a non-null ReadOnly value. A filter with no names and no ReadOnly has no wire representation, so an ArgumentException naming the offending property (Always or Never) is thrown.","triggerScenarios":"Passing new FoundryToolboxMcpApprovalFilter { ToolNames = [] } (or null ToolNames) with ReadOnly unset as policy.Always or policy.Never; a filter whose entries were all whitespace (those throw earlier via ThrowIfNullOrWhiteSpace) or deduplicated away to zero.","commonSituations":"Deserialized config where tool_names was an empty array; code that builds the filter conditionally and adds no names; YAML/JSON tool lists that failed to bind.","solutions":["Add at least one tool name to ToolNames.","Set ReadOnly = true (or false) on the filter.","Pass null for the filter instead of an empty one.","Log the filter contents before creating the policy to confirm names loaded from config."],"exampleFix":"// before\nnew FoundryToolboxMcpApprovalFilter { ToolNames = [] }\n// after\nnew FoundryToolboxMcpApprovalFilter { ToolNames = [\"search\"], ReadOnly = false }","handlingStrategy":"validation","validationCode":"bool selects = filter.ToolNames is { Count: > 0 } || filter.ReadOnly is not null;\nif (!selects) throw new ArgumentException(\"Filter needs a tool name or a read_only value.\");","typeGuard":"static bool IsMeaningful(FoundryToolboxMcpApprovalFilter f) =>\n    (f.ToolNames is { Count: > 0 }) || f.ReadOnly is not null;","tryCatchPattern":"try { toolDefinition = CreateMcpTool(...); }\ncatch (ArgumentException ex) when (ex.Message.Contains(\"at least one tool name or a read-only value\"))\n{ logger.LogError(\"Approval filter {Param} is empty; add tool names or set ReadOnly.\", ex.ParamName); }","preventionTips":["Pass null instead of an empty filter object","Validate that config-sourced tool name arrays are non-empty before binding","Always set ReadOnly or names when declaring a filter"],"tags":["foundry","mcp","approval-filter","empty-configuration","aspire"],"backgroundTag":"empty-required-field","analyzedSha":"25830f84bd145686607ad00c057b3f84e2e51d43","analyzedAt":"2026-09-16T11:10:06.193Z","contentChangedAt":"2026-09-16T11:10:06.193Z","schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}