fix(server): forward aliased field input under the Python parameter name - #3416
fix(server): forward aliased field input under the Python parameter name#3416mturac wants to merge 1 commit into
Conversation
Field(alias=...) publishes the alias as the wire key (correct), but model_dump_one_level returns that alias as the kwargs key too. The function's actual parameter is the Python name, so fn(**kwargs) fails with "unexpected keyword argument". Track the original parameter name for each model field in ArgModelBase.param_names (populated during func_metadata) and use it in model_dump_one_level instead of the alias. The SDK-internal reserved-name aliases (field_model_dump -> model_dump) continue to work because param_names records the original inspect.Parameter.name, which is the alias in that case. Resolvers' tool_arg_names set now uses param_names too, so a by-name resolver parameter matches the function's parameter name rather than the wire alias. Fixes modelcontextprotocol#3099
|
This PR has been closed automatically. This repo only keeps pull requests open when they come from a maintainer, or from a contributor a maintainer has assigned to the linked issue, and you aren't currently assigned to #3099. If a maintainer assigns you to #3099, this PR reopens on its own and there's nothing more you need to do here. Assignment is a maintainer call based on capacity; comments that only ask to be assigned don't factor in. What does help is engaging on the issue itself by confirming the repro, explaining why it matters for your use case, or describing the approach you'd take. You're welcome to keep pushing commits here (just avoid force-pushing, since GitHub can't reopen a rewritten branch), but that on its own won't get the PR reviewed or the issue assigned, and realistically most auto-closed PRs stay closed. There's no need to open a new PR either way. CONTRIBUTING.md has the full reasoning, but in short:
Maintainers: reopen, remove |
Summary
Fixes #3099 —
Field(alias="externalX")publishes the alias in the tool JSON schema (correct), but at runtime the aliased wire name is forwarded directly to the Python function, causingf() got an unexpected keyword argument 'externalX'.Root cause:
ArgModelBase.model_dump_one_level()returns the Pydantic field alias as the dict key when one exists, andcall_fn()passes this dict asfn(**kwargs). The function's actual parameter is the Python name (x), not the alias (externalX), so the call fails.Fix: Track the original
inspect.Parameter.namefor each model field in a class-levelparam_namesmapping, populated duringfunc_metadata()construction.model_dump_one_level()now uses this mapping to return the Python parameter name. The SDK-internal reserved-name aliases (e.g.field_model_dumpwith aliasmodel_dump) continue to work becauseparam_namesrecords the original parameter name, which is the alias value in that case.The resolver system's
tool_arg_namesset is updated to useparam_namestoo, so a by-name resolver parameter matches the function's Python parameter name rather than the wire alias.Changed files
src/mcp/server/mcpserver/utilities/func_metadata.py— addparam_namesClassVar toArgModelBase; populate it infunc_metadata(); use it inmodel_dump_one_level()src/mcp/server/mcpserver/tools/base.py— derivetool_arg_namesfromparam_namesinstead of field aliasestests/server/mcpserver/tools/test_base.py— add regression testtest_field_alias_maps_wire_name_back_to_python_parameterTest plan
uv run --frozen pytest tests/server/mcpserver/ -q— 632 passeduv run --frozen pytest tests/ -q— 5842 passed (4943 + 899)uv run --frozen ruff check— all checks passeduv run --frozen pyright— 0 errors