Commit ffbed28
authored
fix(arcade-core,arcade-mcp-server): Nested-object schemas across all tool-schema converters (#860)
## Summary
Arcade renders a tool's `ValueSchema` into JSON Schema in three
independent places, and they had drifted into the same class of bug:
nested object subschemas (especially `list[<TypedDict>]` items) were
emitted incompletely, producing OpenAI-strict-invalid schemas that the
OpenAI Responses API rejects with a hard 400. Because OpenAI rejects the
whole tool list when one schema is invalid, every eval/tool batch
containing that tool fails.
This PR fixes all three converter paths and the shared root cause they
all read from, so correct schemas come from one corrected source rather
than per-converter patches.
Resolves:
https://linear.app/arcadedev/issue/TOO-990/arcade-core-tool-schema-converters-drop-nested-object-fields-for
## What was wrong (three layers)
1. **`arcade_core/converters/openai.py`** (OpenAI strict) and
**`anthropic.py`** (standard) built array `items` from the inner scalar
type alone, emitting bare `{"type": "object"}` and dropping object
fields. OpenAI strict also requires `additionalProperties: false` +
every key in `required` on each object.
2. **`arcade_mcp_server/convert.py`** (MCP `inputSchema`) preserved
nested properties/required but never set `additionalProperties: false`
on nested objects or array-of-object items (only the top-level wrapper
did). This is the path consumers monkey-patch when feeding MCP tool
schemas to OpenAI strict mode.
3. **`arcade_core/catalog.py::extract_properties`** extracted
`required_keys`/`nullable` for **TypedDict** params but not for
**Pydantic** models, so every converter received imprecise data for
Pydantic object params (no `required`, no nullability).
## Changes
- **OpenAI + Anthropic converters** (commit 1): expand every object
recursively at any depth (top-level param, nested object,
array-of-object items). OpenAI strict closes every object
(`additionalProperties: false`), lists all keys in `required`, and
renders optionals as nullable unions; Anthropic uses standard JSON
Schema (only required keys).
- **MCP converter** (commit 2): set `additionalProperties: false` on
objects with a known shape, at every depth. Freeform `dict` params (no
properties) stay open, since closing them would reject all keys.
- **Root cause** (commit 2): `extract_properties` now populates
`required_keys` for Pydantic models via `FieldInfo.is_required()` and
marks `nullable` on `Optional` fields, matching the existing TypedDict
handling. All three converters get accurate metadata from one place.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Changes shared schema extraction and all three tool-schema emitters,
which can alter API/MCP validation behavior for nested
Pydantic/TypedDict params; mitigated by extensive new tests but still
user-visible for existing tools.
>
> **Overview**
> Fixes **incomplete nested object JSON Schema** for tool inputs so
OpenAI strict, Anthropic, and MCP paths stay aligned and OpenAI no
longer 400s on invalid tool lists.
>
> **`arcade_core/catalog`:** Pydantic params now get **`required_keys`**
and **`nullable`** like TypedDict; known shapes use **`({}, [])`**
instead of collapsing to `None`, so empty/zero-field models differ from
freeform **`dict`**. List and **`ValueSchema`** conversion propagate
nested shapes when **`properties` / `inner_properties` is not `None`**.
>
> **OpenAI & Anthropic converters:** Shared **`_build_object_schema`**
recursively expands **`json`** and **`list[object]`** items (no more
bare **`{"type": "object"}`**). OpenAI strict closes objects, lists all
keys in **`required`**, and uses **`null`** unions (including enums);
Anthropic lists only real required keys and unions **`null`** on
nullable nested fields.
>
> **MCP `convert`:** Known-shape objects and array-of-object items get
**`additionalProperties: false`** at every depth; unknown **`dict`**
shapes stay open.
>
> Bumps **`arcade-core` 4.7.3** and **`arcade-mcp-server` 1.22.2** with
broad converter/catalog tests plus a cross-dialect consistency test.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
46dee1d. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->1 parent 959ad31 commit ffbed28
12 files changed
Lines changed: 1051 additions & 71 deletions
File tree
- libs
- arcade-core
- arcade_core
- converters
- arcade-mcp-server
- arcade_mcp_server
- tests
- arcade_mcp_server
- core/converters
- tool
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
775 | 775 | | |
776 | 776 | | |
777 | 777 | | |
778 | | - | |
779 | | - | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
780 | 781 | | |
781 | 782 | | |
782 | 783 | | |
783 | | - | |
| 784 | + | |
784 | 785 | | |
785 | 786 | | |
786 | 787 | | |
| |||
860 | 861 | | |
861 | 862 | | |
862 | 863 | | |
863 | | - | |
864 | | - | |
| 864 | + | |
| 865 | + | |
| 866 | + | |
| 867 | + | |
| 868 | + | |
| 869 | + | |
865 | 870 | | |
866 | 871 | | |
867 | 872 | | |
868 | 873 | | |
869 | 874 | | |
| 875 | + | |
870 | 876 | | |
871 | 877 | | |
872 | 878 | | |
873 | 879 | | |
874 | 880 | | |
875 | 881 | | |
876 | 882 | | |
877 | | - | |
| 883 | + | |
| 884 | + | |
878 | 885 | | |
879 | 886 | | |
880 | 887 | | |
881 | 888 | | |
882 | 889 | | |
883 | 890 | | |
| 891 | + | |
| 892 | + | |
884 | 893 | | |
885 | 894 | | |
886 | | - | |
| 895 | + | |
| 896 | + | |
| 897 | + | |
| 898 | + | |
| 899 | + | |
887 | 900 | | |
888 | 901 | | |
889 | 902 | | |
| |||
909 | 922 | | |
910 | 923 | | |
911 | 924 | | |
912 | | - | |
913 | | - | |
914 | | - | |
| 925 | + | |
| 926 | + | |
915 | 927 | | |
916 | 928 | | |
917 | 929 | | |
| |||
925 | 937 | | |
926 | 938 | | |
927 | 939 | | |
928 | | - | |
| 940 | + | |
| 941 | + | |
929 | 942 | | |
930 | | - | |
| 943 | + | |
931 | 944 | | |
932 | 945 | | |
933 | 946 | | |
934 | 947 | | |
935 | 948 | | |
936 | 949 | | |
937 | 950 | | |
938 | | - | |
| 951 | + | |
939 | 952 | | |
940 | 953 | | |
941 | 954 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
21 | | - | |
| 20 | + | |
| 21 | + | |
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | 28 | | |
29 | | - | |
| 29 | + | |
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
| |||
112 | 112 | | |
113 | 113 | | |
114 | 114 | | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
115 | 158 | | |
116 | 159 | | |
117 | 160 | | |
| |||
125 | 168 | | |
126 | 169 | | |
127 | 170 | | |
128 | | - | |
| 171 | + | |
129 | 172 | | |
130 | 173 | | |
131 | | - | |
132 | | - | |
133 | | - | |
134 | | - | |
135 | | - | |
136 | | - | |
137 | | - | |
138 | | - | |
139 | | - | |
140 | | - | |
141 | | - | |
142 | | - | |
143 | | - | |
144 | | - | |
145 | | - | |
146 | | - | |
147 | | - | |
148 | | - | |
149 | | - | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
150 | 195 | | |
151 | 196 | | |
152 | 197 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
28 | | - | |
| 28 | + | |
29 | 29 | | |
30 | 30 | | |
31 | 31 | | |
| |||
136 | 136 | | |
137 | 137 | | |
138 | 138 | | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
139 | 195 | | |
140 | 196 | | |
141 | 197 | | |
| |||
149 | 205 | | |
150 | 206 | | |
151 | 207 | | |
152 | | - | |
| 208 | + | |
153 | 209 | | |
154 | 210 | | |
155 | | - | |
156 | | - | |
157 | | - | |
158 | | - | |
159 | | - | |
160 | | - | |
161 | | - | |
162 | | - | |
163 | | - | |
164 | | - | |
165 | | - | |
166 | | - | |
167 | | - | |
168 | | - | |
169 | | - | |
170 | | - | |
171 | | - | |
172 | | - | |
173 | | - | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
174 | 232 | | |
175 | 233 | | |
176 | 234 | | |
| |||
192 | 250 | | |
193 | 251 | | |
194 | 252 | | |
195 | | - | |
| 253 | + | |
| 254 | + | |
196 | 255 | | |
197 | | - | |
198 | | - | |
199 | | - | |
200 | | - | |
201 | | - | |
202 | | - | |
| 256 | + | |
203 | 257 | | |
204 | 258 | | |
205 | 259 | | |
206 | 260 | | |
207 | 261 | | |
208 | | - | |
209 | | - | |
210 | 262 | | |
211 | 263 | | |
212 | 264 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | | - | |
| 3 | + | |
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| |||
0 commit comments