sbuilder-mcp 0.2.1 → 0.2.2

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,12 @@ 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.2.2] - 2026-09-07
10
+
11
+ ### Fixed
12
+ - sb_live_join now accepts an API key (SB_TOKEN) to join the live-edit room, instead of refusing it and requiring a session; the platform now gates the socket on the key's delegated member permission and its own site, so a key-only install can join like every other tool.
13
+ - sb_media_upload now accepts an API key, instead of refusing it and requiring a session; the platform's upload endpoint moved behind the same gate as sb_sites, and a 401 with a key present now reports that the key lacks the media permission or belongs to another site, rather than telling the caller to switch credentials.
14
+
9
15
  ## [0.2.1] - 2026-09-07
10
16
 
11
17
  ### Added
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,12 @@ 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.2.2] - 2026-09-07
10
+
11
+ ### Fixed
12
+ - sb_live_join giờ chấp nhận API key (SB_TOKEN) để vào phòng live-edit, thay vì từ chối và bắt buộc dùng session; nền tảng giờ kiểm soát socket dựa trên quyền member được ủy quyền qua key và site của chính key đó, nên một cài đặt chỉ dùng key có thể tham gia như mọi tool khác.
13
+ - sb_media_upload giờ chấp nhận API key, thay vì từ chối và bắt buộc dùng session; endpoint upload của nền tảng giờ nằm sau cùng một cổng chặn với sb_sites, và lỗi 401 khi có key giờ báo rằng key thiếu quyền media hoặc thuộc site khác, thay vì bảo caller đổi sang loại credential khác.
14
+
9
15
  ## [0.2.1] - 2026-09-07
10
16
 
11
17
  ### Added
@@ -60,30 +60,45 @@ export function bindNode(doc, id, source, field) {
60
60
  ];
61
61
  }
62
62
  /**
63
- * The live socket takes a session JWT only.
63
+ * The credential that opens the live-edit room.
64
64
  *
65
- * `server/internal/server/realtime.go:38` refuses API keys, and a rejected
66
- * socket auth still fires `onopen` so without this check an API-key-only
67
- * agent would "join", publish every edit into the void, and never learn why
68
- * nobody saw them. Every other tool works with the key; this one says so.
65
+ * AN AGENT KEY NOW WORKS. `server/internal/server/realtime.go:44` gives a `wbk_`
66
+ * bearer "the same door as a session", decided from the token's own shape, and
67
+ * gates it on member.read through the key's DELEGATED principal so the key
68
+ * sees the room only if the member who minted it may, and only on the site the
69
+ * key belongs to. This function used to refuse a key-only context outright,
70
+ * which was true before agent keys landed and now turns away a working setup.
71
+ *
72
+ * The peer that appears on the canvas is then the KEY, not a person: the
73
+ * platform returns `key.ID` and `key.Name` rather than the minter's name,
74
+ * deliberately — an avatar borrowing a human's name would tell the room a person
75
+ * is editing when a machine is. So the merchant sees the label they chose for
76
+ * the key moving around the page.
77
+ *
78
+ * Still a GETTER, read per attempt: a session access token lives ~15 minutes and
79
+ * rotates, and a captured string replays an expired token forever on every
80
+ * reconnect — silently, because a rejected socket auth still fires `onopen`.
69
81
  */
70
- export function requireSessionForLive(ctx) {
71
- if (!ctx.session.loggedIn()) {
72
- throw new Error('sbuilder: the live-edit room takes a session token only an API key cannot join. Set ' +
73
- 'SB_EMAIL and SB_PASSWORD and call sb_connect, then sb_live_join. Every other tool ' +
74
- 'works with the key alone.');
82
+ export function liveTokenFor(ctx) {
83
+ if (!ctx.session.loggedIn() && !ctx.apiKey) {
84
+ throw new Error('sbuilder: the live-edit room needs a credential. Set SB_TOKEN, or SB_EMAIL and ' +
85
+ 'SB_PASSWORD, and call sb_connect before sb_live_join.');
75
86
  }
76
- return () => ctx.session.token();
87
+ // Prefer the key, for the reason `tokenFor` prefers it everywhere: a key is
88
+ // narrower — one site, its own scopes, revocable on its own — while a session
89
+ // carries the whole account.
90
+ return () => (ctx.apiKey ? ctx.apiKey : ctx.session.token());
77
91
  }
78
92
  export function registerLiveTools(server, ctx, session) {
79
93
  server.registerTool('sb_live_join', {
80
94
  description: "Join the site's live-edit room as a visible peer: every write then appears in any open " +
81
- 'editor as it happens. Always yields, so it is safe beside a human. Needs ' +
82
- 'SB_EMAIL / SB_PASSWORD; the socket refuses API keys.',
95
+ 'editor as it happens, with the agent shown by the API key\'s own name rather than a ' +
96
+ "person's. Always yields, so it is safe beside a human. Works with SB_TOKEN or with " +
97
+ 'SB_EMAIL / SB_PASSWORD.',
83
98
  inputSchema: { site_id: z.string() },
84
99
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
85
100
  }, async ({ site_id }) => {
86
- const tokenFn = requireSessionForLive(ctx);
101
+ const tokenFn = liveTokenFor(ctx);
87
102
  const wsBase = ctx.base.replace(/^http/, 'ws').replace(/\/$/, '');
88
103
  const socket = new RealtimeSocket(`${wsBase}/api/realtime/ws?site=${encodeURIComponent(site_id)}`, tokenFn);
89
104
  const live = new LiveSession(socket, {
@@ -66,18 +66,27 @@ export async function uploadMedia(ctx, siteId, source) {
66
66
  throw new ApiError(res.status, 'non_json_response', raw.slice(0, 400));
67
67
  }
68
68
  if (!res.ok) {
69
- // THE UPLOAD SURFACE TAKES A SESSION ONLY.
69
+ // AN API KEY CAN UPLOAD THE PLATFORM WIDENED THIS.
70
70
  //
71
- // `/api/media/{siteId}` is mounted behind `RequireAuth` — not the
72
- // `RequireAuthOrDefer` that lets a `wbk_` key open `/api/sites`
73
- // (server/internal/server/router.go). So a key-only install, which is the
74
- // one the store's Agent app hands out and the one the README recommends,
75
- // gets a bare "unauthorized" from the ONE tool that cannot be replaced by
76
- // sb_api_call, because the body is multipart. Found on a live run.
71
+ // `/api/media` is now mounted behind `RequireAuthOrDefer`
72
+ // (server/internal/server/router.go:2821), the same gate `/api/sites` uses,
73
+ // and `upload_agentkey_test.go` pins the three answers that make it safe:
74
+ // the key's own site only, the same permission the session path checks, and
75
+ // no key at all still meaning 401. The platform's own comment gives the
76
+ // reason it changed — the media LIBRARY already took a key while the UPLOAD
77
+ // refused one, so a merchant could hand an agent a key that manages every
78
+ // image the store has and cannot add one.
79
+ //
80
+ // This message used to say "an API key cannot upload" and send the caller to
81
+ // set SB_EMAIL. That is now the wrong instruction: with a key present, a 401
82
+ // here means the KEY is wrong for this call, not that the wrong KIND of
83
+ // credential was used, and the old text sent people to fix something that
84
+ // was never broken.
77
85
  if ((res.status === 401 || res.status === 403) && ctx.apiKey && !ctx.session.loggedIn()) {
78
- throw new ApiError(res.status, 'media_needs_session', 'sbuilder: the media upload endpoint takes a session token only an API key cannot ' +
79
- 'upload. Set SB_EMAIL and SB_PASSWORD and call sb_connect, then retry. Every other ' +
80
- 'tool works with the key alone.');
86
+ throw new ApiError(res.status, 'media_key_refused', 'sbuilder: the platform refused this API key for the upload. It accepts a key, so the ' +
87
+ 'cause is the key itself: it needs the media permission, and it must belong to THIS ' +
88
+ 'site a key minted for another site is refused before the upload is read. Check the ' +
89
+ "key's scopes and its site, or set SB_EMAIL / SB_PASSWORD to upload as a person.");
81
90
  }
82
91
  const env = (parsed ?? {});
83
92
  const fieldText = env.fields
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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",