sbuilder-mcp 0.83.1 → 0.83.3

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,21 @@ 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.83.3] - 2026-10-11
10
+
11
+ ### Fixed
12
+ - `sb_page_create` rejects a page `type` that does not exist and lists the valid types, instead of letting the platform silently store it as `page`; a `home` type points to `is_homepage: true`.
13
+ - `sb_page_state` and `sb_page_version` take the site of the open page when `SB_SITE` is not set, instead of refusing with "no site".
14
+
15
+ ## [0.83.2] - 2026-10-11
16
+
17
+ ### Added
18
+ - `sb_api_find` and `sb_api_call` reach 833 platform operations.
19
+
20
+ ### Fixed
21
+ - The server logs the device session out when the process exits (stdin closes, SIGTERM, SIGINT) and before `sb_connect` signs in again, so leftover MCP sessions no longer count against the platform's 10-device limit and push the merchant out of the editor.
22
+ - Closing Chrome after a screenshot no longer kills the process before the logout runs.
23
+
9
24
  ## [0.83.1] - 2026-10-11
10
25
 
11
26
  ### Added
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,21 @@ 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.83.3] - 2026-10-11
10
+
11
+ ### Fixed
12
+ - `sb_page_create` từ chối `type` trang không tồn tại và liệt kê các loại hợp lệ, thay vì để platform lặng lẽ lưu thành `page`; type `home` được chỉ sang `is_homepage: true`.
13
+ - `sb_page_state` và `sb_page_version` lấy site của trang đang mở khi không có `SB_SITE`, thay vì từ chối với lỗi "no site".
14
+
15
+ ## [0.83.2] - 2026-10-11
16
+
17
+ ### Added
18
+ - `sb_api_find` và `sb_api_call` truy cập được 833 operation của platform.
19
+
20
+ ### Fixed
21
+ - Server đăng xuất phiên thiết bị khi tiến trình thoát (stdin đóng, SIGTERM, SIGINT) và trước khi `sb_connect` đăng nhập lại, nên phiên MCP bỏ lại không còn chiếm giới hạn 10 thiết bị của platform và đẩy merchant ra khỏi editor.
22
+ - Việc đóng Chrome sau khi chụp ảnh không còn giết tiến trình trước khi kịp đăng xuất.
23
+
9
24
  ## [0.83.1] - 2026-10-11
10
25
 
11
26
  ### Added
package/README.md CHANGED
@@ -118,7 +118,7 @@ in this client: one would put the very key the platform exists to hold back into
118
118
  | `sb_store` | Run a store flow that must happen in a fixed order — `checkout` (the four writes that make a working one), `checkout_sync` (the store's current payment and delivery methods into every existing order form, checkout page republished), `form` (any of the platform's 17 templates with its own field document), `chrome` (one shared header or footer on a real site menu — desktop menu, mobile drawer, cart and account icons), `menu` (a menu node bound to the site's menu, its links resolved), `overlay_attach` (a pop-up or quick view on the open page), `cart` (the site's cart drawer, when it has none; `relocalize` puts an existing one in the site's language) and `app` (a built-in app plus the pages it needs), `global_attach` / `global_detach` (put an EXISTING shared section on the open page, or take it off), `global_promote` (make a section on the page a NEW shared section) and `template_save` (save a section as a reusable template) |
119
119
  | `sb_undo` | Undo / redo this session's writes — page and form edits and `sb_api_call` PUTs; a page is refused if someone else saved it since. The platform's versions and history (`sb_page_version`) outlive this process |
120
120
 
121
- Twenty-eight tools, **830 API operations** (302 of the 381 writes carrying a body shape read
121
+ Twenty-eight tools, **833 API operations** (304 of the 384 writes carrying a body shape read
122
122
  off the handler), 123 elements, 94 binding sources. `sb_api_find`
123
123
  is an index rather than a tool per endpoint, so the tool list stays short while everything
124
124
  the platform can do stays reachable — and operations added to the platform arrive with the
package/README.vi.md CHANGED
@@ -115,7 +115,7 @@ nhét ngược lại vào mọi bản cài.
115
115
  | `sb_store` | Chạy một luồng cửa hàng bắt buộc đúng thứ tự — `checkout` (bốn lệnh ghi tạo nên trang thanh toán), `checkout_sync` (đưa phương thức thanh toán và giao hàng hiện tại vào mọi form đặt hàng đã có, publish lại trang thanh toán), `form` (một trong 17 template của nền tảng kèm field document của nó), `chrome` (một header hoặc footer dùng chung trên menu thật của site — menu desktop, drawer mobile, icon giỏ hàng và tài khoản), `menu` (một node menu bind vào menu của site, link đã phân giải), `overlay_attach` (một pop-up hay quick view trên trang đang mở), `cart` (ngăn giỏ hàng của site, khi chưa có; `relocalize` đưa ngăn giỏ đã có về ngôn ngữ của site), `app` (một app dựng sẵn kèm những trang nó cần), và `global_attach` / `global_detach` (đặt một section dùng chung ĐÃ CÓ lên trang đang mở, hoặc gỡ ra), `global_promote` (biến một section trên trang thành section dùng chung MỚI) và `template_save` (lưu một section làm mẫu dùng lại) |
116
116
  | `sb_undo` | Hoàn tác / làm lại các thao tác ghi trong phiên — chỉnh sửa trang, form và PUT qua `sb_api_call`; trang bị từ chối nếu người khác đã lưu sau đó. Version và history của nền tảng (`sb_page_version`) sống lâu hơn process này |
117
117
 
118
- Hai mươi tám tool, **830 operation API** (302 trong 381 lệnh ghi có hình dạng body đọc thẳng
118
+ Hai mươi tám tool, **833 operation API** (304 trong 384 lệnh ghi có hình dạng body đọc thẳng
119
119
  từ handler), 123 element, 94 nguồn binding. `sb_api_find` là
120
120
  một chỉ mục chứ không phải mỗi endpoint một tool, nên danh sách tool vẫn ngắn trong khi mọi
121
121
  thứ nền tảng làm được vẫn với tới — và operation mới thêm bên nền tảng sẽ tự có sau lần
@@ -1,8 +1,8 @@
1
1
  export const SWAGGER_SOURCE = {
2
- "operations": 830,
3
- "definitions": 277,
4
- "bodyCarrying": 362,
5
- "bodyUndescribed": 92,
2
+ "operations": 833,
3
+ "definitions": 278,
4
+ "bodyCarrying": 364,
5
+ "bodyUndescribed": 93,
6
6
  "generatedFrom": "server/docs/swagger.json"
7
7
  };
8
8
  export const API_OPERATIONS = [
@@ -1508,6 +1508,27 @@ export const API_OPERATIONS = [
1508
1508
  "bodyRef": null,
1509
1509
  "credential": "siteScoped"
1510
1510
  },
1511
+ {
1512
+ "id": "post:/api/admin/accounts/{id}/sign-out",
1513
+ "method": "POST",
1514
+ "path": "/api/admin/accounts/{id}/sign-out",
1515
+ "tags": [
1516
+ "admin"
1517
+ ],
1518
+ "summary": "Sign an account out of every device",
1519
+ "params": [
1520
+ {
1521
+ "name": "id",
1522
+ "in": "path",
1523
+ "required": true,
1524
+ "type": "string",
1525
+ "description": "Account (user) id"
1526
+ }
1527
+ ],
1528
+ "bodyDescribed": false,
1529
+ "bodyRef": null,
1530
+ "credential": "siteScoped"
1531
+ },
1511
1532
  {
1512
1533
  "id": "get:/api/admin/address-units",
1513
1534
  "method": "GET",
@@ -1932,6 +1953,27 @@ export const API_OPERATIONS = [
1932
1953
  "bodyRef": null,
1933
1954
  "credential": "none"
1934
1955
  },
1956
+ {
1957
+ "id": "post:/api/auth/logout",
1958
+ "method": "POST",
1959
+ "path": "/api/auth/logout",
1960
+ "tags": [
1961
+ "auth"
1962
+ ],
1963
+ "summary": "Sign out this device (revoke its refresh session)",
1964
+ "params": [
1965
+ {
1966
+ "name": "body",
1967
+ "in": "body",
1968
+ "required": true,
1969
+ "type": "object",
1970
+ "description": "refreshToken"
1971
+ }
1972
+ ],
1973
+ "bodyDescribed": false,
1974
+ "bodyRef": null,
1975
+ "credential": "none"
1976
+ },
1935
1977
  {
1936
1978
  "id": "get:/api/auth/me",
1937
1979
  "method": "GET",
@@ -8272,6 +8314,34 @@ export const API_OPERATIONS = [
8272
8314
  "bodyRef": null,
8273
8315
  "credential": "siteScoped"
8274
8316
  },
8317
+ {
8318
+ "id": "post:/api/sites/{siteId}/einvoice/connection/check",
8319
+ "method": "POST",
8320
+ "path": "/api/sites/{siteId}/einvoice/connection/check",
8321
+ "tags": [
8322
+ "pos"
8323
+ ],
8324
+ "summary": "Test an e-invoice provider connection",
8325
+ "params": [
8326
+ {
8327
+ "name": "siteId",
8328
+ "in": "path",
8329
+ "required": true,
8330
+ "type": "string",
8331
+ "description": "Site ID"
8332
+ },
8333
+ {
8334
+ "name": "body",
8335
+ "in": "body",
8336
+ "required": false,
8337
+ "type": "object",
8338
+ "description": "{provider?, sandbox?, credentials?}"
8339
+ }
8340
+ ],
8341
+ "bodyDescribed": true,
8342
+ "bodyRef": "github_com_webbuilder_server_internal_einvoice.CheckInput",
8343
+ "credential": "siteScoped"
8344
+ },
8275
8345
  {
8276
8346
  "id": "get:/api/sites/{siteId}/einvoice/jobs",
8277
8347
  "method": "GET",
@@ -8423,7 +8493,7 @@ export const API_OPERATIONS = [
8423
8493
  }
8424
8494
  ],
8425
8495
  "bodyDescribed": true,
8426
- "bodyRef": "github_com_webbuilder_server_internal_einvoice.Settings",
8496
+ "bodyRef": "internal_einvoice_rest.settingsInput",
8427
8497
  "credential": "siteScoped"
8428
8498
  },
8429
8499
  {
@@ -8451,7 +8521,7 @@ export const API_OPERATIONS = [
8451
8521
  }
8452
8522
  ],
8453
8523
  "bodyDescribed": true,
8454
- "bodyRef": "github_com_webbuilder_server_internal_einvoice.Settings",
8524
+ "bodyRef": "internal_einvoice_rest.settingsInput",
8455
8525
  "credential": "siteScoped"
8456
8526
  },
8457
8527
  {
@@ -14687,6 +14757,13 @@ export const API_OPERATIONS = [
14687
14757
  "type": "string",
14688
14758
  "description": "Location filter (GET)"
14689
14759
  },
14760
+ {
14761
+ "name": "archived",
14762
+ "in": "query",
14763
+ "required": false,
14764
+ "type": "boolean",
14765
+ "description": "true = list archived areas (GET)"
14766
+ },
14690
14767
  {
14691
14768
  "name": "area",
14692
14769
  "in": "body",
@@ -14722,6 +14799,13 @@ export const API_OPERATIONS = [
14722
14799
  "type": "string",
14723
14800
  "description": "Location filter (GET)"
14724
14801
  },
14802
+ {
14803
+ "name": "archived",
14804
+ "in": "query",
14805
+ "required": false,
14806
+ "type": "boolean",
14807
+ "description": "true = list archived areas (GET)"
14808
+ },
14725
14809
  {
14726
14810
  "name": "area",
14727
14811
  "in": "body",
@@ -14741,7 +14825,7 @@ export const API_OPERATIONS = [
14741
14825
  "tags": [
14742
14826
  "pos"
14743
14827
  ],
14744
- "summary": "Rename, reorder or delete a floor area",
14828
+ "summary": "Rename, reorder, restore, archive or delete a floor area",
14745
14829
  "params": [
14746
14830
  {
14747
14831
  "name": "siteId",
@@ -14776,7 +14860,7 @@ export const API_OPERATIONS = [
14776
14860
  "tags": [
14777
14861
  "pos"
14778
14862
  ],
14779
- "summary": "Rename, reorder or delete a floor area",
14863
+ "summary": "Rename, reorder, restore, archive or delete a floor area",
14780
14864
  "params": [
14781
14865
  {
14782
14866
  "name": "siteId",
@@ -26701,41 +26785,20 @@ export const API_DEFINITIONS = {
26701
26785
  }
26702
26786
  }
26703
26787
  },
26704
- "github_com_webbuilder_server_internal_einvoice.Settings": {
26788
+ "github_com_webbuilder_server_internal_einvoice.CheckInput": {
26705
26789
  "type": "object",
26706
26790
  "properties": {
26707
- "enabled": {
26708
- "type": "boolean"
26709
- },
26710
- "invoiceSeries": {
26711
- "type": "string"
26712
- },
26713
- "issueMode": {
26714
- "type": "string"
26791
+ "credentials": {
26792
+ "type": "object",
26793
+ "additionalProperties": {
26794
+ "type": "string"
26795
+ }
26715
26796
  },
26716
26797
  "provider": {
26717
26798
  "type": "string"
26718
26799
  },
26719
- "sellerAddress": {
26720
- "type": "string"
26721
- },
26722
- "sellerEmail": {
26723
- "type": "string"
26724
- },
26725
- "sellerName": {
26726
- "type": "string"
26727
- },
26728
- "sellerPhone": {
26729
- "type": "string"
26730
- },
26731
- "sellerTaxCode": {
26732
- "type": "string"
26733
- },
26734
- "templateCode": {
26735
- "type": "string"
26736
- },
26737
- "updatedAt": {
26738
- "type": "string"
26800
+ "sandbox": {
26801
+ "type": "boolean"
26739
26802
  }
26740
26803
  }
26741
26804
  },
@@ -27801,6 +27864,10 @@ export const API_DEFINITIONS = {
27801
27864
  "github_com_webbuilder_server_internal_pos.AreaInput": {
27802
27865
  "type": "object",
27803
27866
  "properties": {
27867
+ "archived": {
27868
+ "description": "Archived (PATCH only): false restores an archived area; true is refused —\narchiving has one door, DELETE, which checks the open tabs.",
27869
+ "type": "boolean"
27870
+ },
27804
27871
  "locationId": {
27805
27872
  "description": "POST only; an area never moves",
27806
27873
  "type": "string"
@@ -32141,6 +32208,54 @@ export const API_DEFINITIONS = {
32141
32208
  }
32142
32209
  }
32143
32210
  },
32211
+ "internal_einvoice_rest.settingsInput": {
32212
+ "type": "object",
32213
+ "properties": {
32214
+ "credentials": {
32215
+ "type": "object",
32216
+ "additionalProperties": {
32217
+ "type": "string"
32218
+ }
32219
+ },
32220
+ "enabled": {
32221
+ "type": "boolean"
32222
+ },
32223
+ "invoiceSeries": {
32224
+ "type": "string"
32225
+ },
32226
+ "issueMode": {
32227
+ "type": "string"
32228
+ },
32229
+ "provider": {
32230
+ "type": "string"
32231
+ },
32232
+ "sandbox": {
32233
+ "description": "Sandbox points the provider adapter at its test environment.",
32234
+ "type": "boolean"
32235
+ },
32236
+ "sellerAddress": {
32237
+ "type": "string"
32238
+ },
32239
+ "sellerEmail": {
32240
+ "type": "string"
32241
+ },
32242
+ "sellerName": {
32243
+ "type": "string"
32244
+ },
32245
+ "sellerPhone": {
32246
+ "type": "string"
32247
+ },
32248
+ "sellerTaxCode": {
32249
+ "type": "string"
32250
+ },
32251
+ "templateCode": {
32252
+ "type": "string"
32253
+ },
32254
+ "updatedAt": {
32255
+ "type": "string"
32256
+ }
32257
+ }
32258
+ },
32144
32259
  "internal_giftcards_rest.adjustRequest": {
32145
32260
  "type": "object",
32146
32261
  "properties": {
@@ -1,6 +1,6 @@
1
1
  export const DEAD_KEY_SOURCE = {
2
- "files": 5639,
3
- "identifiers": 103197,
2
+ "files": 5665,
3
+ "identifiers": 103619,
4
4
  "seededKeys": 445,
5
5
  "dead": 0
6
6
  };
@@ -1,10 +1,10 @@
1
1
  export const SHAPE_SOURCE = {
2
- "writeOperations": 381,
3
- "shaped": 302,
4
- "fromHandlers": 292,
2
+ "writeOperations": 384,
3
+ "shaped": 304,
4
+ "fromHandlers": 294,
5
5
  "fromSwaggerOnly": 10,
6
6
  "withReadOnly": 30,
7
- "structsRead": 3176
7
+ "structsRead": 3194
8
8
  };
9
9
  export const REQUEST_SHAPES = {
10
10
  "post:/api/sites/{id}/ai-credits/purchase": {
@@ -756,6 +756,16 @@ export const REQUEST_SHAPES = {
756
756
  "source": "go",
757
757
  "goType": "(inline)"
758
758
  },
759
+ "post:/api/auth/logout": {
760
+ "fields": [
761
+ {
762
+ "name": "refreshToken",
763
+ "type": "string"
764
+ }
765
+ ],
766
+ "source": "go",
767
+ "goType": "(inline)"
768
+ },
759
769
  "post:/api/auth/change-password": {
760
770
  "fields": [
761
771
  {
@@ -3533,13 +3543,40 @@ export const REQUEST_SHAPES = {
3533
3543
  "name": "invoiceSeries",
3534
3544
  "type": "string"
3535
3545
  },
3546
+ {
3547
+ "name": "sandbox",
3548
+ "type": "boolean",
3549
+ "note": "Sandbox points the provider adapter at its test environment."
3550
+ },
3536
3551
  {
3537
3552
  "name": "updatedAt",
3538
3553
  "type": "string (RFC3339)"
3554
+ },
3555
+ {
3556
+ "name": "credentials",
3557
+ "type": "{ [string]: string }"
3539
3558
  }
3540
3559
  ],
3541
3560
  "source": "go",
3542
- "goType": "einvoice.Settings"
3561
+ "goType": "settingsInput"
3562
+ },
3563
+ "post:/api/sites/{siteId}/einvoice/connection/check": {
3564
+ "fields": [
3565
+ {
3566
+ "name": "provider",
3567
+ "type": "string"
3568
+ },
3569
+ {
3570
+ "name": "sandbox",
3571
+ "type": "boolean | null"
3572
+ },
3573
+ {
3574
+ "name": "credentials",
3575
+ "type": "{ [string]: string }"
3576
+ }
3577
+ ],
3578
+ "source": "go",
3579
+ "goType": "einvoice.CheckInput"
3543
3580
  },
3544
3581
  "post:/api/sites/{siteId}/einvoice/jobs/{id}/cancel": {
3545
3582
  "fields": [
@@ -6064,6 +6101,11 @@ export const REQUEST_SHAPES = {
6064
6101
  {
6065
6102
  "name": "position",
6066
6103
  "type": "number | null"
6104
+ },
6105
+ {
6106
+ "name": "archived",
6107
+ "type": "boolean | null",
6108
+ "note": "Archived (PATCH only): false restores an archived area; true is refused — archiving has one door, DELETE, which checks the open tabs."
6067
6109
  }
6068
6110
  ],
6069
6111
  "source": "go",
@@ -6083,6 +6125,11 @@ export const REQUEST_SHAPES = {
6083
6125
  {
6084
6126
  "name": "position",
6085
6127
  "type": "number | null"
6128
+ },
6129
+ {
6130
+ "name": "archived",
6131
+ "type": "boolean | null",
6132
+ "note": "Archived (PATCH only): false restores an archived area; true is refused — archiving has one door, DELETE, which checks the open tabs."
6086
6133
  }
6087
6134
  ],
6088
6135
  "source": "go",
@@ -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": "f70fe86d1ceaba55e8793041fdb61ff0663851e1",
5
- "committedAt": "2026-10-11T14:05:32+07:00",
4
+ "commit": "9db1b2a9eede5be5f1081975bd7f687f0becfd4a",
5
+ "committedAt": "2026-10-11T21:21:05+07:00",
6
6
  "dirty": false
7
7
  };
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { createServer } from './server.js';
3
+ import { buildContext, createServer } from './server.js';
4
4
  import { runInstallCli } from './install/index.js';
5
+ import { closeBrowser } from './vision/shoot.js';
5
6
  async function main() {
6
7
  // `sbuilder-mcp install` writes this server into the agent clients on this
7
8
  // machine and exits. Checked BEFORE the transport opens: an installer that
@@ -9,16 +10,30 @@ async function main() {
9
10
  if (process.argv[2] === 'install') {
10
11
  process.exit(runInstallCli(process.argv.slice(3)));
11
12
  }
12
- const server = createServer();
13
+ const ctx = buildContext();
14
+ const server = createServer(ctx);
13
15
  await server.connect(new StdioServerTransport());
14
16
  // THE CLIENT CLOSED STDIN, SO THIS PROCESS IS DONE. The SDK does not exit on
15
17
  // its own, and the live-room socket (plus its reconnect timer) keeps the event
16
18
  // loop alive — an orphaned process that answers the server's pings, so the
17
19
  // agent's avatar stays in the room indefinitely. Exiting closes the socket and
18
20
  // the server publishes the leave.
19
- const done = () => process.exit(0);
20
- process.stdin.once('end', done);
21
- process.stdin.once('close', done);
21
+ // Sign the device session out (see Session.logout) and close a pooled Chrome
22
+ // first, bounded so a dead platform cannot keep the process alive. A signal is
23
+ // re-raised once that is done, so the exit status is the one it would have had.
24
+ let exiting = false;
25
+ const done = (signal) => () => {
26
+ if (exiting)
27
+ return;
28
+ exiting = true;
29
+ const leave = () => (signal ? process.kill(process.pid, signal) : process.exit(0));
30
+ setTimeout(leave, 3000).unref();
31
+ void Promise.allSettled([ctx.session.logout(), closeBrowser()]).then(leave);
32
+ };
33
+ process.stdin.once('end', done());
34
+ process.stdin.once('close', done());
35
+ process.once('SIGTERM', done('SIGTERM'));
36
+ process.once('SIGINT', done('SIGINT'));
22
37
  // stdout is the MCP channel. Every log line in this repo is console.error.
23
38
  console.error('[sbuilder-mcp] ready on stdio');
24
39
  }
@@ -2227,6 +2227,14 @@ export function registerPageTools(server, ctx) {
2227
2227
  // its own type is what gives it its icon and tells the next reader what it
2228
2228
  // is for. Only when the caller named no type; an explicit one wins.
2229
2229
  const askedType = type;
2230
+ // AN UNKNOWN TYPE IS STORED AS "page", SILENTLY (page.go validType). Measured:
2231
+ // type "home" created a plain page at /home and answered 201, so the site
2232
+ // still had no homepage and nothing said so.
2233
+ if (type !== undefined && !PAGE_TYPES.some((t) => t.type === type)) {
2234
+ throw new Error(`sbuilder: page type "${type}" does not exist — the platform would store it as "page". ` +
2235
+ (/^home(page)?$/i.test(type) ? 'A homepage is is_homepage:true on any page. ' : '') +
2236
+ `Types: ${PAGE_TYPES.map((t) => t.type).join(', ')}.`);
2237
+ }
2230
2238
  type ??= purposeTypeForName(name);
2231
2239
  // TYPE IS THE ROUTE for several kinds of page: /checkout and
2232
2240
  // /products/{slug} resolve to the site's PUBLISHED page of that type and
@@ -2590,7 +2598,9 @@ export function registerPageTools(server, ctx) {
2590
2598
  },
2591
2599
  annotations: { readOnlyHint: true },
2592
2600
  }, async ({ site_id: given, page_id }) => {
2593
- const site_id = siteFor(ctx, given);
2601
+ // The open page's site, like its page id: with no SB_SITE this refused a
2602
+ // call on the page it had just been pointed at.
2603
+ const site_id = siteFor(ctx, given ?? (page_id ? undefined : session.page()?.siteId));
2594
2604
  // No open page is fine when page_id names one — `location()` throws.
2595
2605
  const here = session.page() ?? { siteId: '', pageId: '' };
2596
2606
  const pageId = page_id ?? here.pageId;
@@ -30,8 +30,8 @@ export function registerPageVersionTools(server, ctx, session) {
30
30
  },
31
31
  annotations: { readOnlyHint: false, destructiveHint: true },
32
32
  }, async ({ action, site_id: given, page_id, label, version_id, history_id, dry_run }) => {
33
- const siteId = siteFor(ctx, given);
34
33
  const open = session.page();
34
+ const siteId = siteFor(ctx, given ?? (page_id ? undefined : open?.siteId));
35
35
  const pageId = page_id ?? (open?.siteId === siteId ? open.pageId : undefined);
36
36
  if (!pageId)
37
37
  throw new Error('sbuilder: sb_page_version needs page_id, or a page opened with sb_page_open.');
@@ -32,7 +32,17 @@ export class Session {
32
32
  loggedIn() {
33
33
  return this.access !== null;
34
34
  }
35
- async login(email, password) {
35
+ pending = Promise.resolve();
36
+ /** Serialised: two concurrent sb_connect calls would each sign in and strand one device. */
37
+ login(email, password) {
38
+ const run = this.pending.then(() => this.signIn(email, password));
39
+ this.pending = run.catch(() => { });
40
+ return run;
41
+ }
42
+ async signIn(email, password) {
43
+ // A second sb_connect in one process would otherwise strand the first
44
+ // device session — see logout().
45
+ await this.logout();
36
46
  const out = (await request({
37
47
  base: this.base,
38
48
  method: 'POST',
@@ -58,4 +68,34 @@ export class Session {
58
68
  this.access = out.tokens.accessToken;
59
69
  this.refreshToken = out.tokens.refreshToken;
60
70
  }
71
+ /**
72
+ * Sign this device out. Best effort, never throws.
73
+ *
74
+ * EVERY password sign-in is its own DEVICE SESSION since web_builder 9db1b2a9e,
75
+ * capped at 10 per account; past the cap the one that has gone longest
76
+ * without a REFRESH is deleted. This client never refreshes (the access token
77
+ * lives 30 days), so a process that exits without signing out leaves a slot
78
+ * held, and about ten agent runs later the evicted session is the merchant's
79
+ * own editor — signed out mid-edit, by nothing they can see. A SIGKILLed
80
+ * process still leaks its slot; nothing on this side can prevent that.
81
+ */
82
+ async logout() {
83
+ const refreshToken = this.refreshToken;
84
+ this.access = null;
85
+ this.refreshToken = null;
86
+ if (!refreshToken)
87
+ return;
88
+ try {
89
+ await request({
90
+ base: this.base,
91
+ method: 'POST',
92
+ path: '/api/auth/logout',
93
+ body: { refreshToken },
94
+ fetchImpl: this.fetchImpl,
95
+ });
96
+ }
97
+ catch (err) {
98
+ console.error('[sbuilder-mcp] sign-out failed:', err.message);
99
+ }
100
+ }
61
101
  }
@@ -1378,7 +1378,13 @@ function capturePage(limits) {
1378
1378
  async function withBrowser(fn) {
1379
1379
  let browser;
1380
1380
  try {
1381
- browser = await chromium.launch({ channel: 'chrome' });
1381
+ // No Playwright signal handlers: `src/index.ts` owns them (see shoot.ts).
1382
+ browser = await chromium.launch({
1383
+ channel: 'chrome',
1384
+ handleSIGINT: false,
1385
+ handleSIGTERM: false,
1386
+ handleSIGHUP: false,
1387
+ });
1382
1388
  }
1383
1389
  catch (e) {
1384
1390
  throw new Error('sbuilder: could not start Chrome to read that page. This server uses the SYSTEM ' +
@@ -32,7 +32,7 @@ async function launch() {
32
32
  // its SIGTERM handler closes browsers without exiting. Its handlers live until
33
33
  // the browser process closes, so a signal re-raised by this module could land
34
34
  // while Playwright is still listening and the server would survive a signal it
35
- // should die on. This module owns the handlers; Playwright gets none.
35
+ // should die on. `src/index.ts` owns the handlers; Playwright gets none.
36
36
  const realLauncher = () => chromium.launch({
37
37
  channel: 'chrome',
38
38
  headless: true,
@@ -91,9 +91,10 @@ export async function closeBrowser() {
91
91
  }
92
92
  /**
93
93
  * A kept browser is a child process, and a child process outlives a parent that
94
- * forgets it. SIGINT/SIGTERM close Chrome and then re-raise, so the exit code is
95
- * the one the signal would have produced. That is the path the MCP server
96
- * actually takes, because a stdio server is stopped by its client.
94
+ * forgets it. The MCP server closes it on every way out (stdin close, SIGINT,
95
+ * SIGTERM) in `src/index.ts`, which owns the signal handlers: a second handler
96
+ * here re-raised the signal the moment Chrome was closed — at once when none was
97
+ * open — and killed the process before the server signed its device session out.
97
98
  *
98
99
  * THERE USED TO BE A `beforeExit` HANDLER HERE TOO, and it could never fire.
99
100
  * `beforeExit` runs when the event loop DRAINS, and an open browser connection
@@ -110,11 +111,6 @@ export async function closeBrowser() {
110
111
  * pooling is deliberate (a vision loop shoots constantly and must not pay a
111
112
  * launch each time); the cost of it is this one line at every other call site.
112
113
  */
113
- for (const signal of ['SIGINT', 'SIGTERM']) {
114
- process.once(signal, () => {
115
- void closeBrowser().finally(() => process.kill(process.pid, signal));
116
- });
117
- }
118
114
  /**
119
115
  * Photograph a rendered page at several widths, and measure every node.
120
116
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.83.1",
3
+ "version": "0.83.3",
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",