Summary
Tools whose sole parameter is a wrapped Pydantic model named input
(create_work_package, update_work_package, …) fail for any MCP client that serializes
object-typed arguments as a JSON string. Spec-compliant clients that send a JSON object
work unchanged. The defect is in argument-serialization compatibility, not in the server's
business logic.
Goal: a general, widely-consumable fix so the server works with any MCP client regardless of
how it serializes structured tool arguments — not a workaround for one product.
Symptom
1 validation error for call[create_work_package]
input
Input should be a valid dictionary or instance of CreateWorkPackageInput
[type=model_type, input_value='{"project_id": 458, ...}', input_type=str]
input_value is valid JSON, but it arrived as a str.
Root cause
@mcp.tool
async def create_work_package(input: CreateWorkPackageInput) -> str:
...
FastMCP publishes input with an opaque object schema (an unconstrained / $ref shape, not
an explicit type: object) and validates the argument against the model before the function
body runs. So success depends entirely on how the client encodes the argument:
- Client sends
input as a JSON object → validates to the model → works.
- Client serializes object-typed args as a JSON string (common when the parameter schema
isn't an explicit object) → FastMCP receives a str → rejects it before the body. An in-body
json.loads cannot help, because validation happens first.
Evidence it is client-side, not server-side
Two independent pristine clones (same HEAD, no local diffs, no edits):
- Driven by a spec-compliant client (Claude Code / VS Code extension): work-package creation
succeeds unpatched — the client sends input as an object.
- Driven by a client that stringifies object args: the
model_type error above.
Within the failing client, read tools and flat-parameter tools work fine
(e.g. set_work_package_parent(child_id, parent_id), every list_*). Only the
single-wrapped-input tools fail. This localizes the issue precisely to serialization of object
arguments under an opaque parameter schema.
Why fix it server-side
The MCP ecosystem has many clients and bridges. Tolerating both encodings is cheap server hygiene
that broadens compatibility to the whole class of "stringifying" clients without asking any of
them to change. It is not specific to any one product.
Scope — affected tools (verified against current HEAD)
grep -rn "async def .*(input:" src/tools → 17 functions across 8 files:
| File |
Functions |
src/tools/work_packages.py |
create_work_package, update_work_package |
src/tools/relations.py |
create_work_package_relation, update_work_package_relation |
src/tools/projects.py |
create_project, add_subproject, update_project |
src/tools/versions.py |
create_version |
src/tools/time_entries.py |
create_time_entry, update_time_entry |
src/tools/memberships.py |
create_membership, update_membership |
src/tools/news.py |
create_news, update_news |
src/tools/weekly_reports.py |
generate_weekly_report, get_report_data, plus internal _generate_weekly_report_impl (verify call path) |
Whatever fix is chosen should ideally cover any future model-input tool automatically.
Candidate solution directions (to be discussed before implementation)
To be evaluated in the issue discussion — not yet decided:
- Decorator / wrapper that auto-detects the model parameter and coerces a
str argument via
json.loads → model before the body runs.
- Pydantic
BeforeValidator / model_validator(mode="before") on each input model (or a
shared base model) that accepts str | dict.
- Flatten the signatures — drop the
input wrapper and expose explicit fields so FastMCP
emits an explicit type: object schema (larger API change; affects docs/tests).
- Custom type annotation (
Annotated[Model, BeforeValidator(...)]) applied uniformly.
Each has trade-offs in blast radius, schema impact, test surface, and "covers future tools
automatically." We will compare these (and a parallel proposal from another session) here before
writing code.
Acceptance criteria
- A client that sends
input as a JSON string can successfully call all affected tools.
- A spec-compliant client that sends
input as a JSON object continues to work (no regression).
- The fix covers all 17 current call sites and, ideally, any future model-input tool without
per-tool boilerplate.
- The published tool JSON schema remains valid; no breaking change for already-working clients.
- Tests cover both encodings (object and stringified) for at least a representative tool.
uv run black . / uv run flake8 . clean.
Out of scope
- Changing OpenProject API client logic or any tool's business behavior.
- Reworking flat-parameter tools (they already work).
- Bumping/pinning FastMCP beyond what the fix requires.
References
- Affected files: see scope table.
- FastMCP
>=2.0.0, pydantic >=2.0.0 (pyproject.toml).
- Local planning mirror:
LOCAL_PR_BACKLOG.md (to be promoted to this issue's PR).
Summary
Tools whose sole parameter is a wrapped Pydantic model named
input(
create_work_package,update_work_package, …) fail for any MCP client that serializesobject-typed arguments as a JSON string. Spec-compliant clients that send a JSON object
work unchanged. The defect is in argument-serialization compatibility, not in the server's
business logic.
Goal: a general, widely-consumable fix so the server works with any MCP client regardless of
how it serializes structured tool arguments — not a workaround for one product.
Symptom
input_valueis valid JSON, but it arrived as astr.Root cause
FastMCP publishes
inputwith an opaque object schema (an unconstrained /$refshape, notan explicit
type: object) and validates the argument against the model before the functionbody runs. So success depends entirely on how the client encodes the argument:
inputas a JSON object → validates to the model → works.isn't an explicit object) → FastMCP receives a
str→ rejects it before the body. An in-bodyjson.loadscannot help, because validation happens first.Evidence it is client-side, not server-side
Two independent pristine clones (same
HEAD, no local diffs, no edits):succeeds unpatched — the client sends
inputas an object.model_typeerror above.Within the failing client, read tools and flat-parameter tools work fine
(e.g.
set_work_package_parent(child_id, parent_id), everylist_*). Only thesingle-wrapped-
inputtools fail. This localizes the issue precisely to serialization of objectarguments under an opaque parameter schema.
Why fix it server-side
The MCP ecosystem has many clients and bridges. Tolerating both encodings is cheap server hygiene
that broadens compatibility to the whole class of "stringifying" clients without asking any of
them to change. It is not specific to any one product.
Scope — affected tools (verified against current
HEAD)grep -rn "async def .*(input:" src/tools→ 17 functions across 8 files:src/tools/work_packages.pycreate_work_package,update_work_packagesrc/tools/relations.pycreate_work_package_relation,update_work_package_relationsrc/tools/projects.pycreate_project,add_subproject,update_projectsrc/tools/versions.pycreate_versionsrc/tools/time_entries.pycreate_time_entry,update_time_entrysrc/tools/memberships.pycreate_membership,update_membershipsrc/tools/news.pycreate_news,update_newssrc/tools/weekly_reports.pygenerate_weekly_report,get_report_data, plus internal_generate_weekly_report_impl(verify call path)Whatever fix is chosen should ideally cover any future model-input tool automatically.
Candidate solution directions (to be discussed before implementation)
To be evaluated in the issue discussion — not yet decided:
strargument viajson.loads→ model before the body runs.BeforeValidator/model_validator(mode="before")on each input model (or ashared base model) that accepts
str | dict.inputwrapper and expose explicit fields so FastMCPemits an explicit
type: objectschema (larger API change; affects docs/tests).Annotated[Model, BeforeValidator(...)]) applied uniformly.Each has trade-offs in blast radius, schema impact, test surface, and "covers future tools
automatically." We will compare these (and a parallel proposal from another session) here before
writing code.
Acceptance criteria
inputas a JSON string can successfully call all affected tools.inputas a JSON object continues to work (no regression).per-tool boilerplate.
uv run black ./uv run flake8 .clean.Out of scope
References
>=2.0.0, pydantic>=2.0.0(pyproject.toml).LOCAL_PR_BACKLOG.md(to be promoted to this issue's PR).