一键导入
mcp-strict-param-rejection
Strict unknown-parameter rejection for MCP tools: Stage A (UnmappedMemberHandling.Disallow) + Stage B (did-you-mean CallToolFilter).
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Strict unknown-parameter rejection for MCP tools: Stage A (UnmappedMemberHandling.Disallow) + Stage B (did-you-mean CallToolFilter).
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Investigate .NET CI failures using the hlx CLI tool via bash. USE FOR: checking Helix job status, searching build logs, downloading test results, AzDO build timeline analysis — when MCP tools aren't loaded, when the MCP server isn't configured or fails to start, or when scripting with JSON output and jq. DO NOT USE FOR: tasks where Helix/AzDO MCP tools are already available in context (prefer ci-analysis skill when MCP server is loaded). INVOKES: bash (hlx CLI commands with --json output).
{what this skill teaches agents}
Audit pattern for comparing WorkItemSummary (list API) fields against WorkItemDetails (per-item API) after a Helix.Client SDK bump
Domain-layer normalizer + JSON-stable cache key pattern for filter types with server-side defaults and case-insensitive fields.
Audit MCP/CLI tool parameter surface against underlying REST API capabilities to find silently-dropped params.
Test MCP CallToolFilter behavior with real CallToolRequestParams and McpServerTool invocation.
基于 SOC 职业分类
| name | mcp-strict-param-rejection |
| description | Strict unknown-parameter rejection for MCP tools: Stage A (UnmappedMemberHandling.Disallow) + Stage B (did-you-mean CallToolFilter). |
| domain | mcp-binding |
| confidence | high |
| source | earned |
Use when adding strict unknown-parameter rejection so callers receive a structured McpException instead of silent data loss when they pass a hallucinated or misspelled parameter name.
Two complementary layers — deploy both for best UX + safety:
UnmappedMemberHandling.Disallow (stopgap; no hints)AddUnknownParameterFilter CallToolFilter (polished UX with "did you mean" hints)Microsoft.Extensions.AI.Abstractions 10.5.2 (shipped with MCP 1.3.0+) includes a strict check in ReflectionAIFunction.InvokeCoreAsync:
if (SerializerOptions.UnmappedMemberHandling == Disallow && !HasCustomParameterBinding)
throw ArgumentException(paramName: "arguments",
message: "The arguments dictionary contains an unexpected key 'X' that does not correspond to any parameter of 'Y'.");
HasCustomParameterBinding is true only when a tool parameter has a non-null BindParameter callback (DI injection). Plain value-type and string params have HasCustomParameterBinding = false → strict check fires.
Pass a JsonSerializerOptions with UnmappedMemberHandling = Disallow and TypeInfoResolver = new DefaultJsonTypeInfoResolver() to WithToolsFromAssembly:
using System.Text.Json.Serialization.Metadata;
.WithToolsFromAssembly(typeof(MyTools).Assembly, new JsonSerializerOptions
{
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
TypeInfoResolver = new DefaultJsonTypeInfoResolver(), // ← required companion; see below
})
Apply to both HTTP and stdio transport registrations if both are used.
TypeInfoResolver is requiredAIFunctionFactory.GetOrCreate (in Microsoft.Extensions.AI.Abstractions) calls MakeReadOnly() on the passed JsonSerializerOptions before the tool descriptor is constructed. The constructor then calls AIJsonUtilities.CreateFunctionJsonSchema → CreateJsonSchemaCore, which contains:
// Decompiled AIJsonUtilities:
if (jsonSerializerOptions.TypeInfoResolver == null)
jsonSerializerOptions.TypeInfoResolver = DefaultOptions.TypeInfoResolver; // ← throws on read-only options
Setting any property on already-read-only options throws InvalidOperationException. The crash does not happen at startup (tools are registered via DI factory lambdas) — it fires on the first MCP request that triggers tool singleton resolution.
Fix: Always set TypeInfoResolver = new DefaultJsonTypeInfoResolver() upfront, before MakeReadOnly() is called.
Critical: If NormalizeArgumentAliases (or equivalent) renames an alias key to canonical but does not remove the original alias key, the original key remains in the dict and is rejected by strict mode. Always call arguments.Remove(aliasKey) immediately after copying to the canonical key.
arguments[canonical] = value;
arguments.Remove(aliasKey); // ← required for strict mode compat
The ArgumentException(paramName: "arguments") is caught by AddBindingErrorFilter in this codebase. No filter change is required — the existing catch clause ex.ParamName == "arguments" matches. Error has no "did you mean" hint — use Stage B for that.
AddUnknownParameterFilter (Polished UX)Runs as a CallToolFilter before SDK dispatch. Computes unknowns = argKeys − canonicalParams.
On non-empty unknowns, throws a structured McpException with "did you mean" hints:
Unknown parameter 'minFinishTime' for tool 'azdo_builds'.
Did you mean: minTime?
Allowed parameters: org, project, top, branch, prNumber, definitionId, status, minTime, maxTime, queryOrder.
For multiple unknowns:
Unknown parameters for tool 'azdo_builds':
'minFinishTime' — did you mean: minTime?
'fooBar'
Allowed parameters: org, project, top, branch, prNumber, ...
options.AddBindingErrorFilter(); // 1. alias norm + exception wrap
options.AddUnknownParameterFilter(asm); // 2. did-you-mean check (Stage B)
// SDK dispatch with Disallow runs next // 3. safety net (Stage A)
AddBindingErrorFilter must be registered first so alias normalization runs before the unknown-param check.
AddUnknownParameterFilter(Assembly) builds the toolName → ToolParamInfo map once at registration
using the RuntimeHelpers.GetUninitializedObject + McpServerTool.Create pattern (no DI needed,
no constructor runs, safe for startup):
var shell = RuntimeHelpers.GetUninitializedObject(type);
var tool = McpServerTool.Create(method, shell, options: null);
var schema = tool.ProtocolTool.InputSchema;
// Parse schema["properties"] keys → canonical param set
The map is captured in the filter closure — no per-request parsing.
| Condition | Behavior |
|---|---|
InputSchema.ValueKind is Undefined or Null | Skip filtering for that tool (log Warning) |
No "properties" key in schema | Parameterless tool — any argument is unknown |
"additionalProperties": true | Skip filtering for that tool (log Debug) |
| Schema extraction throws | Skip filtering for that tool (log Warning) |
Threshold: 6 (not 3 as originally spec'd). Rationale:
minFinishTime → minTime has Levenshtein distance = 6 (removes "finish" infix)Keep UnmappedMemberHandling.Disallow alongside Stage B. Defense-in-depth rationale:
Stage B skips filtering when schema extraction fails at startup. Stage A catches those cases.
Stage B fires first in the filter pipeline → callers see the better UX error; Stage A's raw error
is only a fallback for the HasCustomParameterBinding == true edge case and schema-extraction failures.
HasCustomParameterBinding = true silently disables Stage A's check for that tool. Stage B is immune to this (schema-based diffing, not SDK introspection). Keep both.continue (not return) after each successful rename so callers passing aliases for two different canonicals (e.g. build_id + result on azdo_search_timeline) have all entries resolved before Stage B fires.HasArgument(canonical) == true and skip. The losing alias key remains in the dict and will be rejected by Stage B/A — acceptable tradeoff.src/HelixTool.Mcp.Tools/McpServerOptionsExtensions.cs — AddBindingErrorFilter, AddUnknownParameterFilter, alias table, Levenshtein helpersrc/HelixTool.Mcp/Program.cs — WithToolsFromAssembly with serializer options (HTTP) + both filter registrationssrc/HelixTool/Program.cs — same for stdio transportsrc/HelixTool.Tests/McpServerOptionsExtensionsTests.cs — alias, binding-error, and unknown-param tests