When a tool’s output TypedDict uses total=False, MCP clients reject the
response with:
```
MCP error -32602: Structured content does not match the tool's output schema
```
Note that the bug also exists for the Engine transport
(/worker/tools/execute), but since the engine doesn't validate the
output schema, the bug never surfaced. This PR addresses the problem
holistically (MCP and Engine) in preparation for a future where the
Engine transport validates output schemas.
Two bugs combined to cause this:
1. Schema: The outputSchema had no required array and declared all
fields as strict types (e.g. "type": "string"), making every field look
mandatory and non-null.
2. Serialization: model_dump() on TypedDict-derived Pydantic models
emitted None for absent optional fields. A tool returning {"name":
"hello"} produced {"name": "hello", "optional_field": null} which is a
value the schema forbids.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Adjusts core schema generation and MCP JSON Schema conversion for
TypedDicts, affecting how tool input/output contracts are emitted and
validated across clients; mistakes could break compatibility or
validation behavior.
>
> **Overview**
> Fixes MCP/engine validation failures for `TypedDict(total=False)`
outputs by ensuring absent optional keys are **omitted from serialized
output** and that emitted schemas correctly describe **required vs
optional** keys.
>
> `arcade-core` now tracks `required_keys`/`inner_required_keys` and
per-field `nullable` in `ValueSchema`, derives required sets from
TypedDict `__required_keys__`, and unwraps `Optional[T]` to support
optional nested TypedDicts; TypedDict-derived Pydantic models now
`model_dump(exclude_unset=True)` to avoid leaking missing fields as
`null`.
>
> `arcade-mcp-server` JSON Schema conversion now emits `required` arrays
(including for arrays of objects), supports `nullable` by generating
`type: [<type>, "null"]` (and `enum` including `None`), and treats
nullable top-level objects as valid unwrapped output schemas. Adds
focused unit/end-to-end tests plus an expanded example server
demonstrating total-false, mixed required/optional, nullable, and
optional-nested TypedDict outputs, and bumps package
versions/dependencies accordingly.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
53fe8365f613053599130520b75f30b614b465ca. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
63 lines
1.6 KiB
TOML
63 lines
1.6 KiB
TOML
[project]
|
|
name = "arcade-core"
|
|
version = "4.6.1"
|
|
description = "Arcade Core - Core library for Arcade platform"
|
|
readme = "README.md"
|
|
license = { text = "MIT" }
|
|
authors = [{ name = "Arcade", email = "dev@arcade.dev" }]
|
|
classifiers = [
|
|
"Development Status :: 5 - Production/Stable",
|
|
"Intended Audience :: Developers",
|
|
"License :: OSI Approved :: MIT License",
|
|
"Programming Language :: Python :: 3",
|
|
"Programming Language :: Python :: 3.10",
|
|
"Programming Language :: Python :: 3.11",
|
|
"Programming Language :: Python :: 3.12",
|
|
"Programming Language :: Python :: 3.13",
|
|
]
|
|
requires-python = ">=3.10"
|
|
dependencies = [
|
|
"pydantic>=2.7.0",
|
|
"pyyaml>=6.0",
|
|
"loguru>=0.7.0",
|
|
"pyjwt>=2.8.0",
|
|
"toml>=0.10.2",
|
|
"httpx>=0.27.0",
|
|
"packaging>=24.1",
|
|
"portalocker>=2.10.0",
|
|
"types-python-dateutil==2.9.0.20241003",
|
|
"types-pytz==2024.2.0.20241003",
|
|
"types-toml==0.10.8.20240310",
|
|
"posthog>=6.7.6,<7.0.0",
|
|
]
|
|
|
|
[project.optional-dependencies]
|
|
dev = [
|
|
"pytest>=8.1.2",
|
|
"pytest-cov>=4.0.0",
|
|
"mypy>=1.5.1",
|
|
"pre-commit>=3.4.0",
|
|
"pytest-asyncio>=0.23.7",
|
|
"types-pytz>=2024.1",
|
|
"types-python-dateutil>=2.8.2",
|
|
"types-PyYAML>=6.0.0",
|
|
]
|
|
|
|
[build-system]
|
|
requires = ["hatchling"]
|
|
build-backend = "hatchling.build"
|
|
|
|
[tool.hatch.build.targets.wheel]
|
|
packages = ["arcade_core"]
|
|
|
|
[tool.mypy]
|
|
files = ["arcade_core"]
|
|
python_version = "3.10"
|
|
disallow_untyped_defs = true
|
|
disallow_any_unimported = true
|
|
no_implicit_optional = true
|
|
check_untyped_defs = true
|
|
warn_return_any = true
|
|
warn_unused_ignores = true
|
|
show_error_codes = true
|
|
ignore_missing_imports = true
|