sbuilder-mcp 0.81.1 → 0.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,18 @@ All notable changes to this project are documented in this file.
6
6
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [0.82.0] - 2026-10-10
10
+
11
+ ### Added
12
+ - `sb_api_call` accepts `merge` on a PUT to deep-merge `body` over the current record before sending, so a PUT that replaces a whole record (settings, orders, payment gateways) keeps the fields the caller omits.
13
+ - `sb_api_call` accepts `file` to send a local file as multipart (for example a product import spreadsheet), and a GET with `file` saves the response bytes to a new path without overwriting an existing file.
14
+ - `sb_api_call` accepts `if_match` and reads the version itself on routes that require `If-Match`, such as the shipping-config PATCH, and a `shipping_config_moved` conflict names the route to re-read.
15
+ - Form fields in the element catalog gain `urlParam` and `hideOnPage` settings.
16
+
17
+ ### Changed
18
+ - `sb_api_call` on a PUT without `merge` returns a `replace_warning` listing the fields the write would erase, and its `dry_run` preview shows only the caller's body, never data read from a GET.
19
+ - The shipping readiness check points at the shipping-config route, because the v1 route answers 409 on a site that has saved rules.
20
+
9
21
  ## [0.81.1] - 2026-10-10
10
22
 
11
23
  ### Fixed
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,18 @@ Mọi thay đổi đáng chú ý của dự án được ghi lại trong file n
6
6
  Định dạng dựa trên [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  và dự án tuân theo [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [0.82.0] - 2026-10-10
10
+
11
+ ### Added
12
+ - `sb_api_call` nhận `merge` trên PUT để trộn sâu `body` lên bản ghi hiện tại trước khi gửi, nên PUT thay cả bản ghi (settings, đơn hàng, cổng thanh toán) giữ lại các field người gọi bỏ qua.
13
+ - `sb_api_call` nhận `file` để gửi file cục bộ dạng multipart (ví dụ bảng tính import sản phẩm), và GET với `file` lưu bytes phản hồi ra đường dẫn mới mà không ghi đè file đã có.
14
+ - `sb_api_call` nhận `if_match` và tự đọc version trên các route yêu cầu `If-Match`, như PATCH shipping-config, và xung đột `shipping_config_moved` chỉ ra route cần đọc lại.
15
+ - Form field trong catalog element có thêm thiết lập `urlParam` và `hideOnPage`.
16
+
17
+ ### Changed
18
+ - `sb_api_call` trên PUT không có `merge` trả về `replace_warning` liệt kê các field lần ghi sẽ xoá, và bản xem trước `dry_run` chỉ in body của người gọi, không in dữ liệu đọc từ GET.
19
+ - Kiểm tra readiness của shipping trỏ sang route shipping-config, vì route v1 trả 409 trên site đã lưu rules.
20
+
9
21
  ## [0.81.1] - 2026-10-10
10
22
 
11
23
  ### Fixed
package/README.md CHANGED
@@ -83,7 +83,7 @@ in this client: one would put the very key the platform exists to hold back into
83
83
  | `sb_connect` | Log in, list the sites this account can operate, report which credentials are present |
84
84
  | `sb_site_list` | List the sites this account can operate |
85
85
  | `sb_api_find` | Find API operations by intent — one line per match — then read one operation's call sheet by id: real parameter schemas, the credential it needs, and the body's fields read off the handler that decodes them, each carrying the trap its own doc comment records |
86
- | `sb_api_call` | Execute an operation, or any route by method+path; dry run by default. Field selection and local result pagination |
86
+ | `sb_api_call` | Execute an operation, or any route by method+path; dry run by default. Field selection and local result pagination; `merge` for whole-record PUTs, `If-Match`, multipart `file` |
87
87
  | `sb_page_open` | Open a page for editing and return its outline — or, with `form_id`, a form's field document (fields, defaults, dependent-field rules) |
88
88
  | `sb_outline` | The open page as a compressed tree — never a raw document dump |
89
89
  | `sb_node_read` | One node in full, with a warning if it is a shared global |
package/README.vi.md CHANGED
@@ -80,7 +80,7 @@ nhét ngược lại vào mọi bản cài.
80
80
  | `sb_connect` | Đăng nhập, liệt kê site tài khoản vận hành được, báo đang có credential nào |
81
81
  | `sb_site_list` | Liệt kê site tài khoản vận hành được |
82
82
  | `sb_api_find` | Tìm operation theo ý định — mỗi kết quả một dòng — rồi đọc call sheet của một operation theo id: schema tham số thật, credential cần dùng, và các trường body đọc thẳng từ handler decode chúng, mỗi trường mang theo cái bẫy doc comment của chính nó ghi lại |
83
- | `sb_api_call` | Chạy operation, hoặc bất kỳ route nào bằng method+path; mặc định chạy khô. Chọn field và phân trang kết quả phía MCP |
83
+ | `sb_api_call` | Chạy operation, hoặc bất kỳ route nào bằng method+path; mặc định chạy khô. Chọn field và phân trang kết quả phía MCP; `merge` cho PUT thay cả bản ghi, `If-Match`, `file` multipart |
84
84
  | `sb_page_open` | Mở một trang để sửa và trả về outline — hoặc, với `form_id`, tài liệu trường của biểu mẫu (trường, giá trị mặc định, quy tắc trường phụ thuộc) |
85
85
  | `sb_outline` | Trang đang mở dạng cây nén — không bao giờ dump tài liệu thô |
86
86
  | `sb_node_read` | Một node đầy đủ, kèm cảnh báo nếu nó là global dùng chung |
@@ -114,6 +114,8 @@ export const CHECKOUT_FORM_DOCUMENT = {
114
114
  "showLabel": true,
115
115
  "mapTo": "customer.fullName",
116
116
  "prefillValue": "",
117
+ "urlParam": "",
118
+ "hideOnPage": false,
117
119
  "multiline": false,
118
120
  "inputType": "text",
119
121
  "limitChars": false,
@@ -154,6 +156,8 @@ export const CHECKOUT_FORM_DOCUMENT = {
154
156
  "showLabel": true,
155
157
  "mapTo": "customer.phone",
156
158
  "prefillValue": "",
159
+ "urlParam": "",
160
+ "hideOnPage": false,
157
161
  "multiline": false,
158
162
  "inputType": "tel",
159
163
  "limitChars": false,
@@ -194,6 +198,8 @@ export const CHECKOUT_FORM_DOCUMENT = {
194
198
  "showLabel": true,
195
199
  "mapTo": "customer.email",
196
200
  "prefillValue": "",
201
+ "urlParam": "",
202
+ "hideOnPage": false,
197
203
  "multiline": false,
198
204
  "inputType": "email",
199
205
  "limitChars": false,
@@ -394,6 +400,8 @@ export const CHECKOUT_FORM_DOCUMENT = {
394
400
  "showLabel": true,
395
401
  "mapTo": "order.discountCode",
396
402
  "prefillValue": "",
403
+ "urlParam": "",
404
+ "hideOnPage": false,
397
405
  "multiline": false,
398
406
  "inputType": "text",
399
407
  "limitChars": false,
@@ -465,6 +473,8 @@ export const CHECKOUT_FORM_DOCUMENT = {
465
473
  "showLabel": true,
466
474
  "mapTo": "order.note",
467
475
  "prefillValue": "",
476
+ "urlParam": "",
477
+ "hideOnPage": false,
468
478
  "multiline": true,
469
479
  "inputType": "text",
470
480
  "limitChars": false,
@@ -946,6 +956,8 @@ export const FORM_TEMPLATES = {
946
956
  "showLabel": true,
947
957
  "mapTo": "customer.fullName",
948
958
  "prefillValue": "",
959
+ "urlParam": "",
960
+ "hideOnPage": false,
949
961
  "multiline": false,
950
962
  "inputType": "text",
951
963
  "limitChars": false,
@@ -986,6 +998,8 @@ export const FORM_TEMPLATES = {
986
998
  "showLabel": true,
987
999
  "mapTo": "customer.email",
988
1000
  "prefillValue": "",
1001
+ "urlParam": "",
1002
+ "hideOnPage": false,
989
1003
  "multiline": false,
990
1004
  "inputType": "email",
991
1005
  "limitChars": false,
@@ -1026,6 +1040,8 @@ export const FORM_TEMPLATES = {
1026
1040
  "showLabel": true,
1027
1041
  "mapTo": "customer.phone",
1028
1042
  "prefillValue": "",
1043
+ "urlParam": "",
1044
+ "hideOnPage": false,
1029
1045
  "multiline": false,
1030
1046
  "inputType": "tel",
1031
1047
  "limitChars": false,
@@ -1066,6 +1082,8 @@ export const FORM_TEMPLATES = {
1066
1082
  "showLabel": true,
1067
1083
  "mapTo": "customer.notes",
1068
1084
  "prefillValue": "",
1085
+ "urlParam": "",
1086
+ "hideOnPage": false,
1069
1087
  "multiline": true,
1070
1088
  "inputType": "text",
1071
1089
  "limitChars": false,
@@ -1238,6 +1256,8 @@ export const FORM_TEMPLATES = {
1238
1256
  "showLabel": false,
1239
1257
  "mapTo": "customer.email",
1240
1258
  "prefillValue": "",
1259
+ "urlParam": "",
1260
+ "hideOnPage": false,
1241
1261
  "multiline": false,
1242
1262
  "inputType": "email",
1243
1263
  "limitChars": false,
@@ -1389,6 +1409,8 @@ export const FORM_TEMPLATES = {
1389
1409
  "showLabel": true,
1390
1410
  "mapTo": "customer.fullName",
1391
1411
  "prefillValue": "",
1412
+ "urlParam": "",
1413
+ "hideOnPage": false,
1392
1414
  "multiline": false,
1393
1415
  "inputType": "text",
1394
1416
  "limitChars": false,
@@ -1429,6 +1451,8 @@ export const FORM_TEMPLATES = {
1429
1451
  "showLabel": true,
1430
1452
  "mapTo": "customer.phone",
1431
1453
  "prefillValue": "",
1454
+ "urlParam": "",
1455
+ "hideOnPage": false,
1432
1456
  "multiline": false,
1433
1457
  "inputType": "tel",
1434
1458
  "limitChars": false,
@@ -1598,6 +1622,8 @@ export const FORM_TEMPLATES = {
1598
1622
  "showLabel": true,
1599
1623
  "mapTo": "order.quantity",
1600
1624
  "prefillValue": "",
1625
+ "urlParam": "",
1626
+ "hideOnPage": false,
1601
1627
  "setRange": true,
1602
1628
  "minValue": 1,
1603
1629
  "maxValue": 500,
@@ -1634,6 +1660,8 @@ export const FORM_TEMPLATES = {
1634
1660
  "showLabel": true,
1635
1661
  "mapTo": "order.note",
1636
1662
  "prefillValue": "",
1663
+ "urlParam": "",
1664
+ "hideOnPage": false,
1637
1665
  "multiline": true,
1638
1666
  "inputType": "text",
1639
1667
  "limitChars": false,
@@ -1790,6 +1818,8 @@ export const FORM_TEMPLATES = {
1790
1818
  "showLabel": true,
1791
1819
  "mapTo": "customer.fullName",
1792
1820
  "prefillValue": "",
1821
+ "urlParam": "",
1822
+ "hideOnPage": false,
1793
1823
  "multiline": false,
1794
1824
  "inputType": "text",
1795
1825
  "limitChars": false,
@@ -1830,6 +1860,8 @@ export const FORM_TEMPLATES = {
1830
1860
  "showLabel": true,
1831
1861
  "mapTo": "customer.phone",
1832
1862
  "prefillValue": "",
1863
+ "urlParam": "",
1864
+ "hideOnPage": false,
1833
1865
  "multiline": false,
1834
1866
  "inputType": "tel",
1835
1867
  "limitChars": false,
@@ -1870,6 +1902,8 @@ export const FORM_TEMPLATES = {
1870
1902
  "showLabel": true,
1871
1903
  "mapTo": "customer.email",
1872
1904
  "prefillValue": "",
1905
+ "urlParam": "",
1906
+ "hideOnPage": false,
1873
1907
  "multiline": false,
1874
1908
  "inputType": "email",
1875
1909
  "limitChars": false,
@@ -2070,6 +2104,8 @@ export const FORM_TEMPLATES = {
2070
2104
  "showLabel": true,
2071
2105
  "mapTo": "order.discountCode",
2072
2106
  "prefillValue": "",
2107
+ "urlParam": "",
2108
+ "hideOnPage": false,
2073
2109
  "multiline": false,
2074
2110
  "inputType": "text",
2075
2111
  "limitChars": false,
@@ -2141,6 +2177,8 @@ export const FORM_TEMPLATES = {
2141
2177
  "showLabel": true,
2142
2178
  "mapTo": "order.note",
2143
2179
  "prefillValue": "",
2180
+ "urlParam": "",
2181
+ "hideOnPage": false,
2144
2182
  "multiline": true,
2145
2183
  "inputType": "text",
2146
2184
  "limitChars": false,
@@ -2300,6 +2338,8 @@ export const FORM_TEMPLATES = {
2300
2338
  "showLabel": true,
2301
2339
  "mapTo": "customer.fullName",
2302
2340
  "prefillValue": "",
2341
+ "urlParam": "",
2342
+ "hideOnPage": false,
2303
2343
  "multiline": false,
2304
2344
  "inputType": "text",
2305
2345
  "limitChars": false,
@@ -2340,6 +2380,8 @@ export const FORM_TEMPLATES = {
2340
2380
  "showLabel": true,
2341
2381
  "mapTo": "customer.phone",
2342
2382
  "prefillValue": "",
2383
+ "urlParam": "",
2384
+ "hideOnPage": false,
2343
2385
  "multiline": false,
2344
2386
  "inputType": "tel",
2345
2387
  "limitChars": false,
@@ -3064,6 +3106,8 @@ export const FORM_TEMPLATES = {
3064
3106
  "showLabel": true,
3065
3107
  "mapTo": "customer.fullName",
3066
3108
  "prefillValue": "",
3109
+ "urlParam": "",
3110
+ "hideOnPage": false,
3067
3111
  "multiline": false,
3068
3112
  "inputType": "text",
3069
3113
  "limitChars": false,
@@ -3104,6 +3148,8 @@ export const FORM_TEMPLATES = {
3104
3148
  "showLabel": true,
3105
3149
  "mapTo": "customer.phone",
3106
3150
  "prefillValue": "",
3151
+ "urlParam": "",
3152
+ "hideOnPage": false,
3107
3153
  "multiline": false,
3108
3154
  "inputType": "tel",
3109
3155
  "limitChars": false,
@@ -3144,6 +3190,8 @@ export const FORM_TEMPLATES = {
3144
3190
  "showLabel": true,
3145
3191
  "mapTo": "customer.notes",
3146
3192
  "prefillValue": "",
3193
+ "urlParam": "",
3194
+ "hideOnPage": false,
3147
3195
  "multiline": true,
3148
3196
  "inputType": "text",
3149
3197
  "limitChars": false,
@@ -3293,6 +3341,8 @@ export const FORM_TEMPLATES = {
3293
3341
  "showLabel": true,
3294
3342
  "mapTo": "customer.fullName",
3295
3343
  "prefillValue": "",
3344
+ "urlParam": "",
3345
+ "hideOnPage": false,
3296
3346
  "multiline": false,
3297
3347
  "inputType": "text",
3298
3348
  "limitChars": false,
@@ -3333,6 +3383,8 @@ export const FORM_TEMPLATES = {
3333
3383
  "showLabel": true,
3334
3384
  "mapTo": "customer.phone",
3335
3385
  "prefillValue": "",
3386
+ "urlParam": "",
3387
+ "hideOnPage": false,
3336
3388
  "multiline": false,
3337
3389
  "inputType": "tel",
3338
3390
  "limitChars": false,
@@ -3441,6 +3493,8 @@ export const FORM_TEMPLATES = {
3441
3493
  "showLabel": true,
3442
3494
  "mapTo": "customer.notes",
3443
3495
  "prefillValue": "",
3496
+ "urlParam": "",
3497
+ "hideOnPage": false,
3444
3498
  "multiline": true,
3445
3499
  "inputType": "text",
3446
3500
  "limitChars": false,
@@ -3592,6 +3646,8 @@ export const FORM_TEMPLATES = {
3592
3646
  "showLabel": true,
3593
3647
  "mapTo": "customer.fullName",
3594
3648
  "prefillValue": "",
3649
+ "urlParam": "",
3650
+ "hideOnPage": false,
3595
3651
  "multiline": false,
3596
3652
  "inputType": "text",
3597
3653
  "limitChars": false,
@@ -3632,6 +3688,8 @@ export const FORM_TEMPLATES = {
3632
3688
  "showLabel": true,
3633
3689
  "mapTo": "customer.phone",
3634
3690
  "prefillValue": "",
3691
+ "urlParam": "",
3692
+ "hideOnPage": false,
3635
3693
  "multiline": false,
3636
3694
  "inputType": "tel",
3637
3695
  "limitChars": false,
@@ -3777,6 +3835,8 @@ export const FORM_TEMPLATES = {
3777
3835
  "showLabel": true,
3778
3836
  "mapTo": "",
3779
3837
  "prefillValue": "",
3838
+ "urlParam": "",
3839
+ "hideOnPage": false,
3780
3840
  "setRange": true,
3781
3841
  "minValue": 1,
3782
3842
  "maxValue": 10,
@@ -3813,6 +3873,8 @@ export const FORM_TEMPLATES = {
3813
3873
  "showLabel": true,
3814
3874
  "mapTo": "customer.notes",
3815
3875
  "prefillValue": "",
3876
+ "urlParam": "",
3877
+ "hideOnPage": false,
3816
3878
  "multiline": true,
3817
3879
  "inputType": "text",
3818
3880
  "limitChars": false,
@@ -3996,6 +4058,8 @@ export const FORM_TEMPLATES = {
3996
4058
  "showLabel": true,
3997
4059
  "mapTo": "customer.notes",
3998
4060
  "prefillValue": "",
4061
+ "urlParam": "",
4062
+ "hideOnPage": false,
3999
4063
  "multiline": true,
4000
4064
  "inputType": "text",
4001
4065
  "limitChars": false,
@@ -4036,6 +4100,8 @@ export const FORM_TEMPLATES = {
4036
4100
  "showLabel": true,
4037
4101
  "mapTo": "customer.email",
4038
4102
  "prefillValue": "",
4103
+ "urlParam": "",
4104
+ "hideOnPage": false,
4039
4105
  "multiline": false,
4040
4106
  "inputType": "email",
4041
4107
  "limitChars": false,
@@ -4184,6 +4250,8 @@ export const FORM_TEMPLATES = {
4184
4250
  "showLabel": true,
4185
4251
  "mapTo": "customer.fullName",
4186
4252
  "prefillValue": "",
4253
+ "urlParam": "",
4254
+ "hideOnPage": false,
4187
4255
  "multiline": false,
4188
4256
  "inputType": "text",
4189
4257
  "limitChars": false,
@@ -4224,6 +4292,8 @@ export const FORM_TEMPLATES = {
4224
4292
  "showLabel": true,
4225
4293
  "mapTo": "customer.phone",
4226
4294
  "prefillValue": "",
4295
+ "urlParam": "",
4296
+ "hideOnPage": false,
4227
4297
  "multiline": false,
4228
4298
  "inputType": "tel",
4229
4299
  "limitChars": false,
@@ -4264,6 +4334,8 @@ export const FORM_TEMPLATES = {
4264
4334
  "showLabel": true,
4265
4335
  "mapTo": "customer.email",
4266
4336
  "prefillValue": "",
4337
+ "urlParam": "",
4338
+ "hideOnPage": false,
4267
4339
  "multiline": false,
4268
4340
  "inputType": "email",
4269
4341
  "limitChars": false,
@@ -4304,6 +4376,8 @@ export const FORM_TEMPLATES = {
4304
4376
  "showLabel": true,
4305
4377
  "mapTo": "",
4306
4378
  "prefillValue": "",
4379
+ "urlParam": "",
4380
+ "hideOnPage": false,
4307
4381
  "setRange": true,
4308
4382
  "minValue": 1,
4309
4383
  "maxValue": 20,
@@ -4448,6 +4522,8 @@ export const FORM_TEMPLATES = {
4448
4522
  "showLabel": true,
4449
4523
  "mapTo": "customer.fullName",
4450
4524
  "prefillValue": "",
4525
+ "urlParam": "",
4526
+ "hideOnPage": false,
4451
4527
  "multiline": false,
4452
4528
  "inputType": "text",
4453
4529
  "limitChars": false,
@@ -4488,6 +4564,8 @@ export const FORM_TEMPLATES = {
4488
4564
  "showLabel": true,
4489
4565
  "mapTo": "customer.phone",
4490
4566
  "prefillValue": "",
4567
+ "urlParam": "",
4568
+ "hideOnPage": false,
4491
4569
  "multiline": false,
4492
4570
  "inputType": "tel",
4493
4571
  "limitChars": false,
@@ -4528,6 +4606,8 @@ export const FORM_TEMPLATES = {
4528
4606
  "showLabel": true,
4529
4607
  "mapTo": "customer.email",
4530
4608
  "prefillValue": "",
4609
+ "urlParam": "",
4610
+ "hideOnPage": false,
4531
4611
  "multiline": false,
4532
4612
  "inputType": "email",
4533
4613
  "limitChars": false,
@@ -4568,6 +4648,8 @@ export const FORM_TEMPLATES = {
4568
4648
  "showLabel": true,
4569
4649
  "mapTo": "customer.notes",
4570
4650
  "prefillValue": "",
4651
+ "urlParam": "",
4652
+ "hideOnPage": false,
4571
4653
  "multiline": true,
4572
4654
  "inputType": "text",
4573
4655
  "limitChars": false,
@@ -4717,6 +4799,8 @@ export const FORM_TEMPLATES = {
4717
4799
  "showLabel": true,
4718
4800
  "mapTo": "",
4719
4801
  "prefillValue": "",
4802
+ "urlParam": "",
4803
+ "hideOnPage": false,
4720
4804
  "multiline": false,
4721
4805
  "inputType": "text",
4722
4806
  "limitChars": false,
@@ -4757,6 +4841,8 @@ export const FORM_TEMPLATES = {
4757
4841
  "showLabel": true,
4758
4842
  "mapTo": "",
4759
4843
  "prefillValue": "",
4844
+ "urlParam": "",
4845
+ "hideOnPage": false,
4760
4846
  "multiline": false,
4761
4847
  "inputType": "tel",
4762
4848
  "limitChars": false,
@@ -4797,6 +4883,8 @@ export const FORM_TEMPLATES = {
4797
4883
  "showLabel": true,
4798
4884
  "mapTo": "",
4799
4885
  "prefillValue": "",
4886
+ "urlParam": "",
4887
+ "hideOnPage": false,
4800
4888
  "multiline": false,
4801
4889
  "inputType": "email",
4802
4890
  "limitChars": false,
@@ -4837,6 +4925,8 @@ export const FORM_TEMPLATES = {
4837
4925
  "showLabel": true,
4838
4926
  "mapTo": "",
4839
4927
  "prefillValue": "",
4928
+ "urlParam": "",
4929
+ "hideOnPage": false,
4840
4930
  "multiline": true,
4841
4931
  "inputType": "text",
4842
4932
  "limitChars": false,
@@ -5016,6 +5106,8 @@ export const FORM_TEMPLATES = {
5016
5106
  "showLabel": true,
5017
5107
  "mapTo": "customer.email",
5018
5108
  "prefillValue": "",
5109
+ "urlParam": "",
5110
+ "hideOnPage": false,
5019
5111
  "multiline": false,
5020
5112
  "inputType": "email",
5021
5113
  "limitChars": false,
@@ -5056,6 +5148,8 @@ export const FORM_TEMPLATES = {
5056
5148
  "showLabel": true,
5057
5149
  "mapTo": "",
5058
5150
  "prefillValue": "",
5151
+ "urlParam": "",
5152
+ "hideOnPage": false,
5059
5153
  "multiline": false,
5060
5154
  "inputType": "password",
5061
5155
  "limitChars": false,
@@ -5263,6 +5357,8 @@ export const FORM_TEMPLATES = {
5263
5357
  "showLabel": true,
5264
5358
  "mapTo": "customer.fullName",
5265
5359
  "prefillValue": "",
5360
+ "urlParam": "",
5361
+ "hideOnPage": false,
5266
5362
  "multiline": false,
5267
5363
  "inputType": "text",
5268
5364
  "limitChars": false,
@@ -5303,6 +5399,8 @@ export const FORM_TEMPLATES = {
5303
5399
  "showLabel": true,
5304
5400
  "mapTo": "customer.email",
5305
5401
  "prefillValue": "",
5402
+ "urlParam": "",
5403
+ "hideOnPage": false,
5306
5404
  "multiline": false,
5307
5405
  "inputType": "email",
5308
5406
  "limitChars": false,
@@ -5343,6 +5441,8 @@ export const FORM_TEMPLATES = {
5343
5441
  "showLabel": true,
5344
5442
  "mapTo": "",
5345
5443
  "prefillValue": "",
5444
+ "urlParam": "",
5445
+ "hideOnPage": false,
5346
5446
  "multiline": false,
5347
5447
  "inputType": "password",
5348
5448
  "limitChars": false,
@@ -5383,6 +5483,8 @@ export const FORM_TEMPLATES = {
5383
5483
  "showLabel": true,
5384
5484
  "mapTo": "",
5385
5485
  "prefillValue": "",
5486
+ "urlParam": "",
5487
+ "hideOnPage": false,
5386
5488
  "multiline": false,
5387
5489
  "inputType": "password",
5388
5490
  "limitChars": false,
@@ -5558,6 +5660,8 @@ export const FORM_TEMPLATES = {
5558
5660
  "showLabel": true,
5559
5661
  "mapTo": "customer.email",
5560
5662
  "prefillValue": "",
5663
+ "urlParam": "",
5664
+ "hideOnPage": false,
5561
5665
  "multiline": false,
5562
5666
  "inputType": "email",
5563
5667
  "limitChars": false,
@@ -5733,6 +5837,8 @@ export const FORM_TEMPLATES = {
5733
5837
  "showLabel": true,
5734
5838
  "mapTo": "customer.email",
5735
5839
  "prefillValue": "",
5840
+ "urlParam": "",
5841
+ "hideOnPage": false,
5736
5842
  "multiline": false,
5737
5843
  "inputType": "email",
5738
5844
  "limitChars": false,
@@ -5773,6 +5879,8 @@ export const FORM_TEMPLATES = {
5773
5879
  "showLabel": true,
5774
5880
  "mapTo": "",
5775
5881
  "prefillValue": "",
5882
+ "urlParam": "",
5883
+ "hideOnPage": false,
5776
5884
  "multiline": false,
5777
5885
  "inputType": "text",
5778
5886
  "limitChars": false,
@@ -5921,6 +6029,8 @@ export const FORM_TEMPLATES = {
5921
6029
  "showLabel": true,
5922
6030
  "mapTo": "customer.email",
5923
6031
  "prefillValue": "",
6032
+ "urlParam": "",
6033
+ "hideOnPage": false,
5924
6034
  "multiline": false,
5925
6035
  "inputType": "email",
5926
6036
  "limitChars": false,
@@ -5961,6 +6071,8 @@ export const FORM_TEMPLATES = {
5961
6071
  "showLabel": true,
5962
6072
  "mapTo": "",
5963
6073
  "prefillValue": "",
6074
+ "urlParam": "",
6075
+ "hideOnPage": false,
5964
6076
  "multiline": false,
5965
6077
  "inputType": "text",
5966
6078
  "limitChars": false,
@@ -6001,6 +6113,8 @@ export const FORM_TEMPLATES = {
6001
6113
  "showLabel": true,
6002
6114
  "mapTo": "",
6003
6115
  "prefillValue": "",
6116
+ "urlParam": "",
6117
+ "hideOnPage": false,
6004
6118
  "multiline": false,
6005
6119
  "inputType": "password",
6006
6120
  "limitChars": false,
@@ -6041,6 +6155,8 @@ export const FORM_TEMPLATES = {
6041
6155
  "showLabel": true,
6042
6156
  "mapTo": "",
6043
6157
  "prefillValue": "",
6158
+ "urlParam": "",
6159
+ "hideOnPage": false,
6044
6160
  "multiline": false,
6045
6161
  "inputType": "password",
6046
6162
  "limitChars": false,
@@ -1,7 +1,7 @@
1
1
  export const DEAD_KEY_SOURCE = {
2
- "files": 5594,
3
- "identifiers": 102591,
4
- "seededKeys": 443,
2
+ "files": 5598,
3
+ "identifiers": 102652,
4
+ "seededKeys": 445,
5
5
  "dead": 0
6
6
  };
7
7
  /**
@@ -7123,6 +7123,8 @@ export const ELEMENTS = {
7123
7123
  "showLabel": true,
7124
7124
  "mapTo": "",
7125
7125
  "prefillValue": "",
7126
+ "urlParam": "",
7127
+ "hideOnPage": false,
7126
7128
  "multiline": false,
7127
7129
  "inputType": "text",
7128
7130
  "limitChars": false,
@@ -7668,6 +7670,8 @@ export const ELEMENTS = {
7668
7670
  "showLabel": true,
7669
7671
  "mapTo": "",
7670
7672
  "prefillValue": "",
7673
+ "urlParam": "",
7674
+ "hideOnPage": false,
7671
7675
  "setRange": false,
7672
7676
  "minValue": 1,
7673
7677
  "maxValue": 500,
@@ -1,7 +1,7 @@
1
1
  // GENERATED by scripts/gen-catalog.ts — do not edit by hand.
2
2
  // The platform commit every other generated file here describes.
3
3
  export const PLATFORM_SOURCE = {
4
- "commit": "3afc245582a8f81fa58e1bfdc666355d7ecc65fa",
5
- "committedAt": "2026-10-09T07:52:30+07:00",
4
+ "commit": "045c6f78414bfeec8924263a391a6aa8f989e565",
5
+ "committedAt": "2026-10-10T18:23:19+07:00",
6
6
  "dirty": false
7
7
  };
@@ -3,7 +3,7 @@
3
3
  export const TRANSLATION_SOURCE = {
4
4
  "elements": 67,
5
5
  "pairs": 187,
6
- "neverKeys": 178,
6
+ "neverKeys": 180,
7
7
  "entityTypes": 11
8
8
  };
9
9
  /** Every entity type a translation record can name, including "node". */
@@ -429,6 +429,7 @@ export const NEVER_TRANSLATED = [
429
429
  "formId",
430
430
  "formRules",
431
431
  "format",
432
+ "hideOnPage",
432
433
  "hideWhenEmpty",
433
434
  "hoverHostDepth",
434
435
  "href",
@@ -533,6 +534,7 @@ export const NEVER_TRANSLATED = [
533
534
  "target",
534
535
  "thresholdCents",
535
536
  "trackUrl",
537
+ "urlParam",
536
538
  "variant",
537
539
  "videoId",
538
540
  "videoSrc"
@@ -608,7 +608,10 @@ export function readinessGaps(input) {
608
608
  problem: 'No delivery options. The checkout page seeds a shipping select whose options ARE the ' +
609
609
  "site's own methods, so the shopper meets a required-looking field with nothing in it " +
610
610
  'and every order ships free.',
611
- fix: 'Add at least one shipping method (sb_api_find "shipping methods").',
611
+ // Through shipping-config, not the v1 methods route: once a site has saved
612
+ // rules, every v1 writer answers 409 shipping_config_moved.
613
+ fix: 'Add at least one shipping method: GET /api/sites/{siteId}/shipping-config, then PATCH it ' +
614
+ '(sb_api_call reads the If-Match version itself) and POST …/shipping-config/preview to check the price.',
612
615
  });
613
616
  }
614
617
  // THE OTHER FIXED PATHS.
package/dist/tools/api.js CHANGED
@@ -2,9 +2,79 @@ import { z } from 'zod';
2
2
  import { API_OPERATIONS, SWAGGER_SOURCE } from '../catalog/api.generated.js';
3
3
  import { REQUEST_SHAPES } from '../catalog/shapes.generated.js';
4
4
  import { searchOperations, describeOperation, summarizeOperation, findOperation, } from '../catalog/search.js';
5
- import { request, redact } from '../transport/http.js';
5
+ import { readFile, stat, writeFile } from 'node:fs/promises';
6
+ import { basename } from 'node:path';
7
+ import { request, redact, ApiError } from '../transport/http.js';
8
+ import { typeForName } from '../transport/media.js';
9
+ import { restoreBodyFrom } from './undo.js';
6
10
  import { text } from '../mcp/response.js';
7
11
  import { credentialFor } from '../transport/credential.js';
12
+ /**
13
+ * ROUTES THAT REFUSE A WRITE WITHOUT `If-Match: "<version>"` (428
14
+ * if_match_required) — the shipping rules engine's one versioned document
15
+ * (`server/internal/shipping/rest/config.go`). The version is the GET's own
16
+ * `config.version`, which is what the editor sends (`features/shipping/config.ts`).
17
+ */
18
+ const IF_MATCH_ROUTES = new Set([
19
+ 'patch:/api/sites/{siteId}/shipping-config',
20
+ 'post:/api/sites/{siteId}/shipping-config/versions/{id}/restore',
21
+ ]);
22
+ const quoteVersion = (v) => {
23
+ if (typeof v === 'number' || /^\d+$/.test(v))
24
+ return `"${v}"`;
25
+ // The server reads W/"7", "7" and 7 (shipping/rest/config.go ifMatch); anything
26
+ // else is a 400 if_match_invalid that reads like a server fault.
27
+ if (/^(W\/)?"\d+"$/.test(v))
28
+ return v;
29
+ throw new Error(`sbuilder: if_match must be a version number (7 or "7"), not ${JSON.stringify(v)}.`);
30
+ };
31
+ /**
32
+ * Once a site's shipping lives in the rules engine, every v1 method/zone
33
+ * writer answers 409 `shipping_config_moved` (`shipping/store.go movedLocked`).
34
+ * The platform's sentence does not say where the prices went.
35
+ */
36
+ const SHIPPING_MOVED_FIX = ' — this site\'s shipping lives in the rules engine now: edit it with PATCH ' +
37
+ '/api/sites/{siteId}/shipping-config (sb_api_call reads If-Match for you), then verify with ' +
38
+ 'POST /api/sites/{siteId}/shipping-config/preview.';
39
+ const isPlain = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
40
+ /** `over` wins; objects merge key by key; arrays and scalars replace. */
41
+ export function deepMerge(base, over) {
42
+ if (!isPlain(base) || !isPlain(over))
43
+ return over;
44
+ const out = { ...base };
45
+ for (const [k, v] of Object.entries(over))
46
+ out[k] = deepMerge(base[k], v);
47
+ return out;
48
+ }
49
+ /** Leaf paths where `b` differs from `a` (arrays compared whole). */
50
+ function changedPaths(a, b, at = '') {
51
+ if (isPlain(a) && isPlain(b)) {
52
+ return [...new Set([...Object.keys(a), ...Object.keys(b)])].flatMap((k) => changedPaths(a[k], b[k], at ? `${at}.${k}` : k));
53
+ }
54
+ return JSON.stringify(a) === JSON.stringify(b) ? [] : [at];
55
+ }
56
+ const isEmpty = (v) => v === null || v === undefined || v === '' || v === false || v === 0 ||
57
+ (Array.isArray(v) && v.length === 0) || (isPlain(v) && Object.keys(v).length === 0);
58
+ /**
59
+ * What a whole-document PUT of `body` would ERASE: every path holding a value
60
+ * now that the body leaves out. Go decodes a PUT into a fresh struct, so an
61
+ * omitted `enabled: true` arrives as false, and an omitted settings key is gone.
62
+ */
63
+ /** Fields the server owns on every record: a PUT never sets them, so never "erases" them. */
64
+ const SERVER_OWNED = new Set(['id', 'siteId', 'createdAt', 'updatedAt', 'deletedAt', 'createdBy', 'updatedBy', 'rev', 'version', 'number']);
65
+ function erasedPaths(current, body, at = '', owned = SERVER_OWNED) {
66
+ if (!isPlain(current))
67
+ return [];
68
+ return Object.entries(current).flatMap(([k, v]) => {
69
+ const p = at ? `${at}.${k}` : k;
70
+ if (isEmpty(v) || owned.has(k))
71
+ return [];
72
+ if (!isPlain(body) || !(k in body))
73
+ return [p];
74
+ return isPlain(v) && isPlain(body[k]) ? erasedPaths(v, body[k], p, owned) : [];
75
+ });
76
+ }
77
+ const listPaths = (ps) => (ps.length > 12 ? `${ps.slice(0, 12).join(', ')}, …` : ps.join(', '));
8
78
  /** Past this many characters a list response is cut to fit and says so. */
9
79
  export const RESULT_CAP = 60_000;
10
80
  /**
@@ -466,11 +536,109 @@ export async function callOperation(ctx, args) {
466
536
  }
467
537
  const token = tokenFor(ctx, op.credential);
468
538
  const dryRun = args.dry_run !== false;
539
+ if (args.merge && op.method !== 'PUT') {
540
+ throw new Error('sbuilder: merge is for a PUT — it merges body over the record the PUT replaces.');
541
+ }
542
+ if (args.merge && args.file)
543
+ throw new Error('sbuilder: merge and file do not go together.');
544
+ // A PUT IS A REPLACE, AND THE PLATFORM HAS NO HISTORY. Read what is about to
545
+ // be destroyed first: `sb_undo` puts it back, `merge` builds on it, and a
546
+ // dry run warns what the replace would erase. Reads only, never a write.
547
+ const canRead = op.method === 'PUT' && !args.file && !!REQUEST_SHAPES[op.id] &&
548
+ API_OPERATIONS.some((o) => o.id === `get:${op.path}`);
549
+ if (args.merge && !canRead) {
550
+ throw new Error(`sbuilder: merge needs a body shape and a matching GET; ${op.id} has none. Send the whole record.`);
551
+ }
552
+ let before;
553
+ if (canRead) {
554
+ try {
555
+ before = await request({
556
+ base: ctx.base,
557
+ method: 'GET',
558
+ path,
559
+ token: tokenFor(ctx, findOperation(`get:${op.path}`)?.credential ?? op.credential),
560
+ fetchImpl: ctx.fetchImpl,
561
+ });
562
+ }
563
+ catch (e) {
564
+ // The row may not exist yet — the common case for a PUT that creates.
565
+ // Nothing to undo or warn about; but nothing to merge onto either.
566
+ if (args.merge)
567
+ throw new Error(`sbuilder: merge could not read the current record: ${e.message}`);
568
+ }
569
+ }
570
+ const current = before === undefined ? null : restoreBodyFrom(op.id, before);
571
+ if (args.merge && !current) {
572
+ throw new Error(`sbuilder: merge found no record in the GET answer for ${op.id}. Send the whole record.`);
573
+ }
574
+ if (args.merge && isPlain(args.body) && isPlain(current)) {
575
+ // A key the record does not have at the top is a body in the wrong envelope
576
+ // ({locale} for {settings:{locale}}): merged, it would report a change the
577
+ // server ignores.
578
+ const stray = Object.keys(args.body).filter((k) => !(k in current));
579
+ if (stray.length && Object.keys(current).length === 1) {
580
+ throw new Error(`sbuilder: this PUT takes { ${Object.keys(current)[0]}: … } — put ${stray.join(', ')} inside it.`);
581
+ }
582
+ }
583
+ const body = args.merge ? deepMerge(current, args.body) : args.body;
584
+ const merged = args.merge ? { changed: changedPaths(current, body) } : undefined;
585
+ // The settings PUT MERGES a body that is exactly {settings:{locale}}
586
+ // (sitesettings/rest/rest.go LocaleOnly), so nothing is erased there.
587
+ const localeOnly = op.id.endsWith('/settings') && isPlain(args.body) && isPlain(args.body.settings) &&
588
+ Object.keys(args.body).length === 1 && Object.keys(args.body.settings).join() === 'locale';
589
+ const owned = new Set([...SERVER_OWNED, ...(REQUEST_SHAPES[op.id]?.readOnly ?? [])]);
590
+ const erased = !args.merge && current && !localeOnly ? erasedPaths(current, args.body, '', owned) : [];
591
+ const replaceWarning = erased.length
592
+ ? `This PUT replaces the whole record; ${erased.length} field(s) it holds now ` +
593
+ `${dryRun ? 'would be' : 'were'} erased: ${listPaths(erased)}. ` +
594
+ (dryRun ? 'Pass merge:true to keep them.' : 'sb_undo puts them back.')
595
+ : undefined;
596
+ // If-Match: given, or — on a route that refuses a write without one — the
597
+ // version the GET answers right now.
598
+ const headers = {};
599
+ let ifMatchRead;
600
+ if (args.if_match !== undefined)
601
+ headers['If-Match'] = quoteVersion(args.if_match);
602
+ else if (IF_MATCH_ROUTES.has(op.id)) {
603
+ ifMatchRead = path.replace(/(\/shipping-config).*$/, '$1');
604
+ if (dryRun) {
605
+ headers['If-Match'] = `(read from GET ${ifMatchRead} at send)`;
606
+ }
607
+ else {
608
+ const got = (await request({ base: ctx.base, method: 'GET', path: ifMatchRead, token, fetchImpl: ctx.fetchImpl }));
609
+ if (typeof got?.config?.version !== 'number') {
610
+ throw new Error(`sbuilder: GET ${ifMatchRead} answered no config.version; pass if_match.`);
611
+ }
612
+ headers['If-Match'] = quoteVersion(got.config.version);
613
+ }
614
+ }
615
+ // MULTIPART: a non-GET sends a local file under `field` (default "file") —
616
+ // the product import takes `file:formData/file` and cannot read JSON. A GET
617
+ // with `file` saves the answer's bytes (an xlsx template) rather than
618
+ // pasting binary into the result.
619
+ const download = !!args.file && (op.method === 'GET' || op.method === 'HEAD');
620
+ if (download && !dryRun && (await stat(args.file.path).then(() => true, () => false))) {
621
+ throw new Error(`sbuilder: ${args.file.path} already exists — nothing was overwritten; name a new path.`);
622
+ }
623
+ const field = args.file?.field ?? 'file';
624
+ const scalars = isPlain(args.body)
625
+ ? Object.entries(args.body).filter(([, v]) => ['string', 'number', 'boolean'].includes(typeof v))
626
+ : [];
469
627
  if (dryRun) {
470
628
  // THE DRY RUN IS THE DEFAULT, so a directive said only after a real send
471
629
  // reaches nobody who looks before they leap. Once per process either way:
472
630
  // a dry run that said it leaves the send that follows silent.
473
631
  const directive = op.raw ? ctx.notices.once('raw_call', RAW_CALL_NOTICE) : undefined;
632
+ const upload = args.file && !download
633
+ ? {
634
+ multipart: {
635
+ field,
636
+ path: args.file.path,
637
+ bytes: (await stat(args.file.path)).size,
638
+ ...(scalars.length ? { fields: Object.fromEntries(scalars) } : {}),
639
+ },
640
+ }
641
+ : undefined;
474
642
  return {
475
643
  dry_run: true,
476
644
  ...(op.raw ? { uncatalogued: true } : {}),
@@ -480,45 +648,71 @@ export async function callOperation(ctx, args) {
480
648
  url: ctx.base.replace(/\/$/, '') + path,
481
649
  query: args.query,
482
650
  Authorization: token ? `Bearer ${token}` : undefined,
483
- body: args.body,
651
+ ...(Object.keys(headers).length ? { headers } : {}),
652
+ // The CALLER's body, never the merged one: a merge folds in what the GET
653
+ // answered, and redact() hides only secret-looking KEY NAMES — a webhook
654
+ // URL or an account number under an ordinary key would be printed.
655
+ body: upload ?? args.body,
484
656
  }),
657
+ ...(download ? { would_save: args.file.path } : {}),
658
+ ...(merged ? { merge: merged } : {}),
659
+ ...(replaceWarning ? { replace_warning: replaceWarning } : {}),
485
660
  // Shaping is part of what the call WOULD do, so the preview says it;
486
661
  // otherwise a dry run reads identically whether or not it was asked for.
487
662
  ...(args.pick !== undefined || args.max_items !== undefined || args.item_offset !== undefined
488
663
  ? { shaping: { pick: args.pick, max_items: args.max_items, item_offset: args.item_offset } }
489
664
  : {}),
490
- note: 'Nothing was sent. Re-call with dry_run:false to execute.',
665
+ note: before !== undefined
666
+ ? 'Nothing was written — only the current record was read, to ' +
667
+ (merged ? 'merge onto' : 'check what the replace would erase') +
668
+ '. Re-call with dry_run:false to execute.'
669
+ : 'Nothing was sent. Re-call with dry_run:false to execute.',
491
670
  };
492
671
  }
493
- // A PUT IS A REPLACE, AND THE PLATFORM HAS NO HISTORY. Read what is about to
494
- // be destroyed first, so `sb_undo` can put it back — one extra round trip on a
495
- // write, never on a read and never on a dry run. Silent on failure: an undo
496
- // that could not be prepared must not stop the write the caller asked for.
497
- if (op.method === 'PUT' && REQUEST_SHAPES[op.id] && API_OPERATIONS.some((o) => o.id === `get:${op.path}`)) {
672
+ if (before !== undefined)
673
+ ctx.undo.record(op.id, path, before);
674
+ let sendBody = body;
675
+ if (args.file && !download) {
676
+ const form = new FormData();
677
+ const name = basename(args.file.path);
678
+ const type = typeForName(name);
679
+ form.set(field, new Blob([await readFile(args.file.path)], type ? { type } : undefined), name);
680
+ for (const [k, v] of scalars)
681
+ form.set(k, String(v));
682
+ sendBody = form;
683
+ }
684
+ let raw;
685
+ try {
686
+ raw = await request({
687
+ base: ctx.base,
688
+ method: op.method,
689
+ path,
690
+ token,
691
+ query: args.query,
692
+ body: sendBody,
693
+ ...(Object.keys(headers).length ? { headers } : {}),
694
+ ...(download ? { binary: true } : {}),
695
+ fetchImpl: ctx.fetchImpl,
696
+ });
697
+ }
698
+ catch (e) {
699
+ if (e instanceof ApiError && e.code === 'shipping_config_moved')
700
+ e.message += SHIPPING_MOVED_FIX;
701
+ throw e;
702
+ }
703
+ if (download && raw instanceof Uint8Array) {
704
+ // 'wx': a download never replaces a file that is already there.
498
705
  try {
499
- const before = await request({
500
- base: ctx.base,
501
- method: 'GET',
502
- path,
503
- token: tokenFor(ctx, findOperation(`get:${op.path}`)?.credential ?? op.credential),
504
- fetchImpl: ctx.fetchImpl,
505
- });
506
- ctx.undo.record(op.id, path, before);
706
+ await writeFile(args.file.path, raw, { flag: 'wx' });
507
707
  }
508
- catch {
509
- // The state could not be read — the row may not exist yet, which is the
510
- // common case for a PUT that creates. Nothing to undo, nothing to say.
708
+ catch (e) {
709
+ if (e.code === 'EEXIST') {
710
+ throw new Error(`sbuilder: ${args.file.path} already exists — nothing was overwritten; name a new path.`);
711
+ }
712
+ throw e;
511
713
  }
714
+ return { ok: true, saved: args.file.path, bytes: raw.byteLength };
512
715
  }
513
- const raw = await request({
514
- base: ctx.base,
515
- method: op.method,
516
- path,
517
- token,
518
- query: args.query,
519
- body: args.body,
520
- fetchImpl: ctx.fetchImpl,
521
- });
522
716
  // A 204 HAS NO BODY, and `null` is not an answer a caller can read: a DELETE
523
717
  // that worked and a DELETE that returned nothing looked identical, so sixteen
524
718
  // page deletes in a row reported `null` sixteen times and the only way to know
@@ -532,10 +726,16 @@ export async function callOperation(ctx, args) {
532
726
  max_items: args.max_items,
533
727
  item_offset: args.item_offset,
534
728
  });
729
+ const extra = {
730
+ ...(ifMatchRead ? { if_match: { sent: headers['If-Match'], read_from: `GET ${ifMatchRead}` } } : {}),
731
+ ...(merged ? { merge: merged } : {}),
732
+ ...(replaceWarning ? { replace_warning: replaceWarning } : {}),
733
+ };
734
+ const told = Object.keys(extra).length && isPlain(answer) ? { ...answer, ...extra } : answer;
535
735
  if (!op.raw)
536
- return answer;
736
+ return told;
537
737
  const note = ctx.notices.once('raw_call', RAW_CALL_NOTICE);
538
- return { uncatalogued: true, ...(note ? { note } : {}), data: answer };
738
+ return { uncatalogued: true, ...(note ? { note } : {}), data: told };
539
739
  }
540
740
  /**
541
741
  * WHY A SUMMARY CAN BE ABOUT A DIFFERENT ROUTE.
@@ -615,6 +815,12 @@ export function registerApiTools(server, ctx) {
615
815
  pick: z.array(z.string()).optional(),
616
816
  max_items: z.number().int().min(1).optional(),
617
817
  item_offset: z.number().int().min(0).optional().describe('Offset within this response, after API paging. Reads only.'),
818
+ if_match: z.union([z.string(), z.number()]).optional().describe('If-Match version; shipping-config reads it itself'),
819
+ merge: z.boolean().optional().describe('PUT: deep-merge body over the current record (arrays replace)'),
820
+ file: z
821
+ .object({ path: z.string(), field: z.string().optional() })
822
+ .optional()
823
+ .describe('Local file: multipart upload (field "file"); on a GET, saves the answer there'),
618
824
  },
619
825
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
620
826
  }, async (args) => text(await callOperation(ctx, args)));
@@ -232,14 +232,19 @@ async function send(opts) {
232
232
  const headers = { Accept: 'application/json', ...identityHeaders(), ...opts.headers };
233
233
  if (opts.token)
234
234
  headers.Authorization = `Bearer ${opts.token}`;
235
- if (opts.body !== undefined)
235
+ // A FormData body is multipart, and fetch must write its Content-Type itself
236
+ // so the boundary matches the body it builds.
237
+ const multipart = opts.body instanceof FormData;
238
+ if (opts.body !== undefined && !multipart)
236
239
  headers['Content-Type'] = 'application/json';
237
240
  const body = withPageRoot(opts);
238
241
  const res = await doFetch(buildUrl(opts.base, opts.path, opts.query), {
239
242
  method: opts.method,
240
243
  headers,
241
- body: body === undefined ? undefined : JSON.stringify(body),
244
+ body: body === undefined ? undefined : multipart ? body : JSON.stringify(body),
242
245
  });
246
+ if (opts.binary && res.ok)
247
+ return new Uint8Array(await res.arrayBuffer());
243
248
  const raw = await res.text();
244
249
  let parsed = undefined;
245
250
  if (raw) {
@@ -53,8 +53,11 @@ const TYPE_BY_EXT = {
53
53
  '.woff': 'font/woff',
54
54
  '.ttf': 'font/ttf',
55
55
  '.otf': 'font/otf',
56
+ // `sb_api_call file` — the product import takes a workbook.
57
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
58
+ '.csv': 'text/csv',
56
59
  };
57
- function typeForName(name) {
60
+ export function typeForName(name) {
58
61
  const dot = name.lastIndexOf('.');
59
62
  return dot < 0 ? '' : (TYPE_BY_EXT[name.slice(dot).toLowerCase()] ?? '');
60
63
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.81.1",
3
+ "version": "0.82.0",
4
4
  "description": "MCP server that designs and operates a Store Builder site — pages, data, theme and publish — through the platform's own API and live-edit protocol.",
5
5
  "mcpName": "io.github.vuluu2k/sbuilder-mcp",
6
6
  "type": "module",