Skip to content

Commit acb6529

Browse files
dougborgclaude
andcommitted
feat(client)!: live-verified spec drift batch + verification harness
Reusable harness (``scripts/spec_drift_verify.py`` + per-issue probe scripts) for running tagged POSTs against a live Katana tenant and cleaning up afterwards. Every test artifact gets an ``SDT-<date>`` prefix and is recorded in ``/tmp/spec-drift-ledger.jsonl``; a single ``cleanup`` subcommand walks the ledger in reverse and deletes everything (idempotent — already-deleted rows are skipped). Spec-drift findings encoded in ``docs/katana-openapi.yaml``: **#737 — CustomFieldDefinition** - ``id``: ``integer`` → ``string, format: uuid`` (per README example, inferred — couldn't live-verify because our API key lacks create permission on this endpoint, but README is authoritative and the shape is universal across Katana custom-field surfaces). - ``field_type``: ``string maxLength: 50`` → new ``CustomFieldType`` enum (``shortText`` / ``number`` / ``singleSelect`` / ``date`` / ``boolean`` / ``url``). Verified via live-API 422 introspection. - ``entity_type``: ``string maxLength: 50`` → new ``CustomFieldEntityType`` enum with **19 values** (vs README's documented 1). Verified via live-API 422 introspection — the enum spans SalesOrder, SalesOrderRow, ManufacturingOrder*, PurchaseOrder*, StockAdjustment*, Production*, Customer, ProductVariant, MaterialVariant, ServiceVariant. - ``CustomFieldDefinition`` no longer extends ``UpdatableEntity`` because the singleton's ``id`` is a UUID string, not the integer ``BaseEntity`` defines for the rest of the API surface. ``created_at`` / ``updated_at`` are inlined. **#736 — variant_bin_locations + CreateServiceVariant.sku** - ``POST /variant_bin_locations`` request body wrapped in array (``minItems: 1, maxItems: 500``). The endpoint description already said "accepts up to 500 variant storage bin objects" but the schema was a single object. Verified via live API: single-object body returns ``422 must be array``. - ``CreateServiceVariantRequest.sku`` is no longer required. The field is nullable. Verified by creating a service variant via the live API with ``sku`` omitted — server accepted it and returned ``sku: null``. **#738 — StockTransferRowRequest.quantity** - Changed from ``number`` to ``string``. Verified via live-API 422 introspection — sending a JSON number returns ``must be string``. Wire format is a fixed-precision decimal string (e.g. ``"1.0000000000"``). MCP boundary in ``stock_transfers._build_row_requests`` stringifies the pydantic ``float`` input at the call site. **#739 — SalesOrderFulfillment** - No spec change needed. Verified ``sales_order_fulfillment_rows`` IS required on create — local spec is correct, README is stale. Findings deferred to follow-ups: - **#734** ``custom_fields`` shape — live API rejects array-shaped custom_fields with ``must be object`` at both SO and SO-row create paths; dict-shape is accepted. Local READ schemas use structured-array shape, which contradicts this. Read-side verification requires a populated example (tenant has zero ``custom_field_definitions`` and our key can't create them). Sweeping all the READ schemas is broader than this PR. - **#736** ``CustomFieldDefinition`` ``options`` shape, ``id`` type via live API, ``UpdateCustomerRequest.default_billing_id``, ``Operator`` fields — all blocked by either authorization (custom_field_definitions endpoint) or absence of populated fixtures on this tenant. BREAKING CHANGE: ``CustomFieldDefinition.id`` becomes a UUID string (was integer); ``field_type`` / ``entity_type`` become enums; ``CreateServiceVariantRequest.sku`` is no longer required (callers omitting it will continue to work, but ``required`` removal is a schema-level breaking change); ``StockTransferRowRequest.quantity`` becomes a string. The MCP ``stock_transfers`` boundary already stringifies the input float so MCP callers don't have to. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 96d467e commit acb6529

31 files changed

Lines changed: 1546 additions & 315 deletions

docs/katana-openapi.yaml

Lines changed: 160 additions & 120 deletions
Large diffs are not rendered by default.

katana_mcp_server/src/katana_mcp/tools/foundation/stock_transfers.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,9 +289,13 @@ def _build_row_requests(
289289
"""Convert pydantic row inputs to attrs request rows."""
290290
out: list[StockTransferRowRequest] = []
291291
for row in rows:
292+
# ``StockTransferRowRequest.quantity`` is a wire-format string
293+
# (fixed-precision decimal) per the spec; the MCP input is a
294+
# pydantic ``float`` so stringify at the boundary. Katana
295+
# normalizes the precision server-side (e.g. ``"1.0000000000"``).
292296
api_row = StockTransferRowRequest(
293297
variant_id=row.variant_id,
294-
quantity=row.quantity,
298+
quantity=str(row.quantity),
295299
)
296300
if row.batch_transactions:
297301
api_row.additional_properties = {

katana_public_api_client/api/custom_fields/create_custom_field_definition.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,10 @@ def sync_detailed(
9494
9595
Args:
9696
body (CreateCustomFieldDefinitionRequest): Request payload for creating a new custom field
97-
definition. Example: {'label': 'Quality Grade', 'field_type': 'select', 'entity_type':
98-
'product', 'source': 'user', 'description': 'Customer-facing quality classification',
99-
'options': {'values': ['A', 'B', 'C']}}.
97+
definition. Example: {'label': 'Channel', 'field_type': 'singleSelect', 'entity_type':
98+
'SalesOrder', 'source': 'your-integration', 'description': 'Customer-facing sales channel
99+
classification', 'options': {'choices': [{'label': 'Online'}, {'label': 'Retail'},
100+
{'label': 'Wholesale'}]}}.
100101
101102
Raises:
102103
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
@@ -129,9 +130,10 @@ def sync(
129130
130131
Args:
131132
body (CreateCustomFieldDefinitionRequest): Request payload for creating a new custom field
132-
definition. Example: {'label': 'Quality Grade', 'field_type': 'select', 'entity_type':
133-
'product', 'source': 'user', 'description': 'Customer-facing quality classification',
134-
'options': {'values': ['A', 'B', 'C']}}.
133+
definition. Example: {'label': 'Channel', 'field_type': 'singleSelect', 'entity_type':
134+
'SalesOrder', 'source': 'your-integration', 'description': 'Customer-facing sales channel
135+
classification', 'options': {'choices': [{'label': 'Online'}, {'label': 'Retail'},
136+
{'label': 'Wholesale'}]}}.
135137
136138
Raises:
137139
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
@@ -159,9 +161,10 @@ async def asyncio_detailed(
159161
160162
Args:
161163
body (CreateCustomFieldDefinitionRequest): Request payload for creating a new custom field
162-
definition. Example: {'label': 'Quality Grade', 'field_type': 'select', 'entity_type':
163-
'product', 'source': 'user', 'description': 'Customer-facing quality classification',
164-
'options': {'values': ['A', 'B', 'C']}}.
164+
definition. Example: {'label': 'Channel', 'field_type': 'singleSelect', 'entity_type':
165+
'SalesOrder', 'source': 'your-integration', 'description': 'Customer-facing sales channel
166+
classification', 'options': {'choices': [{'label': 'Online'}, {'label': 'Retail'},
167+
{'label': 'Wholesale'}]}}.
165168
166169
Raises:
167170
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
@@ -192,9 +195,10 @@ async def asyncio(
192195
193196
Args:
194197
body (CreateCustomFieldDefinitionRequest): Request payload for creating a new custom field
195-
definition. Example: {'label': 'Quality Grade', 'field_type': 'select', 'entity_type':
196-
'product', 'source': 'user', 'description': 'Customer-facing quality classification',
197-
'options': {'values': ['A', 'B', 'C']}}.
198+
definition. Example: {'label': 'Channel', 'field_type': 'singleSelect', 'entity_type':
199+
'SalesOrder', 'source': 'your-integration', 'description': 'Customer-facing sales channel
200+
classification', 'options': {'choices': [{'label': 'Online'}, {'label': 'Retail'},
201+
{'label': 'Wholesale'}]}}.
198202
199203
Raises:
200204
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.

katana_public_api_client/api/custom_fields/delete_custom_field_definition.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from http import HTTPStatus
22
from typing import Any, cast
33
from urllib.parse import quote
4+
from uuid import UUID
45

56
import httpx
67

@@ -11,7 +12,7 @@
1112

1213

1314
def _get_kwargs(
14-
id: int,
15+
id: UUID,
1516
) -> dict[str, Any]:
1617

1718
_kwargs: dict[str, Any] = {
@@ -69,7 +70,7 @@ def _build_response(
6970

7071

7172
def sync_detailed(
72-
id: int,
73+
id: UUID,
7374
*,
7475
client: AuthenticatedClient | Client,
7576
) -> Response[Any | ErrorResponse]:
@@ -78,7 +79,7 @@ def sync_detailed(
7879
Deletes an existing custom field definition.
7980
8081
Args:
81-
id (int):
82+
id (UUID):
8283
8384
Raises:
8485
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
@@ -101,7 +102,7 @@ def sync_detailed(
101102

102103

103104
def sync(
104-
id: int,
105+
id: UUID,
105106
*,
106107
client: AuthenticatedClient | Client,
107108
) -> Any | ErrorResponse | None:
@@ -110,7 +111,7 @@ def sync(
110111
Deletes an existing custom field definition.
111112
112113
Args:
113-
id (int):
114+
id (UUID):
114115
115116
Raises:
116117
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
@@ -128,7 +129,7 @@ def sync(
128129

129130

130131
async def asyncio_detailed(
131-
id: int,
132+
id: UUID,
132133
*,
133134
client: AuthenticatedClient | Client,
134135
) -> Response[Any | ErrorResponse]:
@@ -137,7 +138,7 @@ async def asyncio_detailed(
137138
Deletes an existing custom field definition.
138139
139140
Args:
140-
id (int):
141+
id (UUID):
141142
142143
Raises:
143144
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
@@ -158,7 +159,7 @@ async def asyncio_detailed(
158159

159160

160161
async def asyncio(
161-
id: int,
162+
id: UUID,
162163
*,
163164
client: AuthenticatedClient | Client,
164165
) -> Any | ErrorResponse | None:
@@ -167,7 +168,7 @@ async def asyncio(
167168
Deletes an existing custom field definition.
168169
169170
Args:
170-
id (int):
171+
id (UUID):
171172
172173
Raises:
173174
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.

katana_public_api_client/api/custom_fields/get_custom_field_definition.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from http import HTTPStatus
22
from typing import Any
33
from urllib.parse import quote
4+
from uuid import UUID
45

56
import httpx
67

@@ -12,7 +13,7 @@
1213

1314

1415
def _get_kwargs(
15-
id: int,
16+
id: UUID,
1617
) -> dict[str, Any]:
1718

1819
_kwargs: dict[str, Any] = {
@@ -71,7 +72,7 @@ def _build_response(
7172

7273

7374
def sync_detailed(
74-
id: int,
75+
id: UUID,
7576
*,
7677
client: AuthenticatedClient | Client,
7778
) -> Response[CustomFieldDefinition | ErrorResponse]:
@@ -80,7 +81,7 @@ def sync_detailed(
8081
Retrieves a single custom field definition by ID.
8182
8283
Args:
83-
id (int):
84+
id (UUID):
8485
8586
Raises:
8687
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
@@ -103,7 +104,7 @@ def sync_detailed(
103104

104105

105106
def sync(
106-
id: int,
107+
id: UUID,
107108
*,
108109
client: AuthenticatedClient | Client,
109110
) -> CustomFieldDefinition | ErrorResponse | None:
@@ -112,7 +113,7 @@ def sync(
112113
Retrieves a single custom field definition by ID.
113114
114115
Args:
115-
id (int):
116+
id (UUID):
116117
117118
Raises:
118119
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
@@ -130,7 +131,7 @@ def sync(
130131

131132

132133
async def asyncio_detailed(
133-
id: int,
134+
id: UUID,
134135
*,
135136
client: AuthenticatedClient | Client,
136137
) -> Response[CustomFieldDefinition | ErrorResponse]:
@@ -139,7 +140,7 @@ async def asyncio_detailed(
139140
Retrieves a single custom field definition by ID.
140141
141142
Args:
142-
id (int):
143+
id (UUID):
143144
144145
Raises:
145146
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
@@ -160,7 +161,7 @@ async def asyncio_detailed(
160161

161162

162163
async def asyncio(
163-
id: int,
164+
id: UUID,
164165
*,
165166
client: AuthenticatedClient | Client,
166167
) -> CustomFieldDefinition | ErrorResponse | None:
@@ -169,7 +170,7 @@ async def asyncio(
169170
Retrieves a single custom field definition by ID.
170171
171172
Args:
172-
id (int):
173+
id (UUID):
173174
174175
Raises:
175176
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.

katana_public_api_client/api/custom_fields/update_custom_field_definition.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from http import HTTPStatus
22
from typing import Any
33
from urllib.parse import quote
4+
from uuid import UUID
45

56
import httpx
67

@@ -16,7 +17,7 @@
1617

1718

1819
def _get_kwargs(
19-
id: int,
20+
id: UUID,
2021
*,
2122
body: UpdateCustomFieldDefinitionRequest,
2223
) -> dict[str, Any]:
@@ -93,7 +94,7 @@ def _build_response(
9394

9495

9596
def sync_detailed(
96-
id: int,
97+
id: UUID,
9798
*,
9899
client: AuthenticatedClient | Client,
99100
body: UpdateCustomFieldDefinitionRequest,
@@ -103,7 +104,7 @@ def sync_detailed(
103104
Updates an existing custom field definition.
104105
105106
Args:
106-
id (int):
107+
id (UUID):
107108
body (UpdateCustomFieldDefinitionRequest): Request payload for updating an existing custom
108109
field definition. Example: {'label': 'Quality Grade (revised)', 'description': 'Updated
109110
customer-facing quality classification'}.
@@ -130,7 +131,7 @@ def sync_detailed(
130131

131132

132133
def sync(
133-
id: int,
134+
id: UUID,
134135
*,
135136
client: AuthenticatedClient | Client,
136137
body: UpdateCustomFieldDefinitionRequest,
@@ -140,7 +141,7 @@ def sync(
140141
Updates an existing custom field definition.
141142
142143
Args:
143-
id (int):
144+
id (UUID):
144145
body (UpdateCustomFieldDefinitionRequest): Request payload for updating an existing custom
145146
field definition. Example: {'label': 'Quality Grade (revised)', 'description': 'Updated
146147
customer-facing quality classification'}.
@@ -162,7 +163,7 @@ def sync(
162163

163164

164165
async def asyncio_detailed(
165-
id: int,
166+
id: UUID,
166167
*,
167168
client: AuthenticatedClient | Client,
168169
body: UpdateCustomFieldDefinitionRequest,
@@ -172,7 +173,7 @@ async def asyncio_detailed(
172173
Updates an existing custom field definition.
173174
174175
Args:
175-
id (int):
176+
id (UUID):
176177
body (UpdateCustomFieldDefinitionRequest): Request payload for updating an existing custom
177178
field definition. Example: {'label': 'Quality Grade (revised)', 'description': 'Updated
178179
customer-facing quality classification'}.
@@ -197,7 +198,7 @@ async def asyncio_detailed(
197198

198199

199200
async def asyncio(
200-
id: int,
201+
id: UUID,
201202
*,
202203
client: AuthenticatedClient | Client,
203204
body: UpdateCustomFieldDefinitionRequest,
@@ -207,7 +208,7 @@ async def asyncio(
207208
Updates an existing custom field definition.
208209
209210
Args:
210-
id (int):
211+
id (UUID):
211212
body (UpdateCustomFieldDefinitionRequest): Request payload for updating an existing custom
212213
field definition. Example: {'label': 'Quality Grade (revised)', 'description': 'Updated
213214
customer-facing quality classification'}.

0 commit comments

Comments
 (0)