sbuilder-mcp 0.1.0 → 0.1.1

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
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1 — 2026-08-28
4
+
5
+ - fix(release): ask for the one-time password instead of dying on it
6
+ - feat: sb_media_upload — the agent can add images
7
+ - feat(vision): sb_look frames one element
8
+ - feat: sb_review — the defects a visitor sees, not the ones a save catches
9
+ - fix: base style is the cascade's fallback layer, not a trap
10
+ - docs: one install section per README, not two
11
+ - feat: sbuilder-mcp install — one command, six clients
12
+ - feat: close the gap with a human designer — 22 tools
13
+ - docs: phase-6 plan — close the gap with a human designer
14
+ - fix: three defects a live run found that no unit test could
15
+ - docs: point the setup at the store's Agent app, which hands over the config
16
+ - feat(transport): one credential — an API key now opens the private surface too
17
+ - docs: phase 3 tools, the yield rule, and the wire-protocol facts
18
+ - feat(tools): sb_live_join, sb_look and sb_bind; writes publish to the room
19
+ - feat(catalog): generate the renderer's 22 binding source keys
20
+ - feat(vision): preview links and Chrome screenshots with real node bounding boxes
21
+ - feat(live): the live-edit session, with the yield rule as its organising decision
22
+ - feat(transport): the live-edit socket, with the editor's two reconnect bugs designed out
23
+ - docs: phase-3 plan (live editing and sight)
24
+ - docs: phase 2 tools, traps, and an end-to-end smoke check
25
+ - feat(tools): the page tools - open, outline, catalog search, add, set, move, remove
26
+ - feat(transport): load and save a page's draft document
27
+ - feat(site): pre-save validation mirroring the platform's own refusals
28
+ - feat(site): the builder - nested subtrees, per-breakpoint writes, containment rules
29
+ - feat(site): the in-memory page document with a compressed outline
30
+ - feat(site): node ids and catalog-seeded node construction
31
+ - feat(site): encode the four silent-failure traps as tested code
32
+ - feat(core): overlay-aware tree walking, with pageChildren as the safe default
33
+ - feat(core): the document patch primitive and its three admission rules
34
+ - feat(catalog): generate the 85-element catalog with its AI hints
35
+ - docs: phase-2 plan (the page document) and corrected element count
36
+ - docs: mark phase-1 plan steps complete
37
+ - docs: repo kit, bilingual docs, and the tool-authoring skill
38
+ - feat(tools): sb_connect and sb_site_list; wire the server end to end
39
+ - feat(tools): sb_api_find and sb_api_call - full 310-operation reach in two tools
40
+ - feat(catalog): intent search over the API index, with three honest body verdicts
41
+ - feat(catalog): generate the 310-operation API index from the platform OpenAPI doc
42
+ - feat(transport): route credentials by path prefix, which the OpenAPI doc cannot
43
+ - feat(transport): session login/refresh with a per-use token getter
44
+ - feat(transport): shared HTTP client with the platform error envelope and redaction
45
+ - feat: repo skeleton, response helpers, and a green build/test/smoke gate
46
+ - docs: design spec and phase-1 implementation plan for @sbuilder/mcp
47
+
3
48
  ## 0.1.0
4
49
 
5
50
  First release.
package/README.md CHANGED
@@ -81,11 +81,13 @@ make, because those mean "this person's account".
81
81
  | `sb_page_create` | Create a page |
82
82
  | `sb_publish` | Compile the draft into the live page (cascades to shared globals) |
83
83
  | `sb_review` | Every defect a visitor would see, each with the command that fixes it |
84
+ | `sb_media_list` | The site's media library |
85
+ | `sb_media_upload` | Add an image and get its URL — the only route, the upload is multipart |
84
86
  | `sb_live_join` | Join the editor's live-edit room as a visible peer — edits then appear live |
85
87
  | `sb_look` | Save, render, and return screenshots plus measured node boxes |
86
88
  | `sb_bind` | Bind a node's content to real store data |
87
89
 
88
- Twenty-three tools, **310 API operations**. `sb_api_find` is an index rather than a tool per endpoint,
90
+ Twenty-five tools, **310 API operations**. `sb_api_find` is an index rather than a tool per endpoint,
89
91
  so the tool list stays short while everything the platform can do stays reachable — and
90
92
  operations added to the platform arrive with the next `npm run codegen`.
91
93
 
package/README.vi.md CHANGED
@@ -78,11 +78,13 @@ là "tài khoản của người này".
78
78
  | `sb_page_create` | Tạo một trang |
79
79
  | `sb_publish` | Biên dịch bản nháp thành trang live (lan sang global dùng chung) |
80
80
  | `sb_review` | Mọi khiếm khuyết người xem sẽ thấy, kèm lệnh sửa từng cái |
81
+ | `sb_media_list` | Thư viện ảnh của site |
82
+ | `sb_media_upload` | Thêm ảnh và lấy URL — đường duy nhất, vì upload là multipart |
81
83
  | `sb_live_join` | Vào phòng live-edit của editor như một peer nhìn thấy được — sửa gì hiện ngay |
82
84
  | `sb_look` | Lưu, render, trả về ảnh chụp kèm box đo được của từng node |
83
85
  | `sb_bind` | Gắn nội dung một node vào dữ liệu cửa hàng thật |
84
86
 
85
- Hai mươi ba tool, **310 operation API**. `sb_api_find` là một chỉ mục chứ không phải mỗi endpoint một
87
+ Hai mươi lăm tool, **310 operation API**. `sb_api_find` là một chỉ mục chứ không phải mỗi endpoint một
86
88
  tool, nên danh sách tool vẫn ngắn trong khi mọi thứ nền tảng làm được vẫn với tới — và
87
89
  operation mới thêm bên nền tảng sẽ tự có sau lần `npm run codegen` kế tiếp.
88
90
 
@@ -3,6 +3,8 @@ import { z } from 'zod';
3
3
  import { text, images } from '../mcp/response.js';
4
4
  import { BINDING_SOURCES } from '../catalog/elements.generated.js';
5
5
  import { previewUrl } from '../vision/preview.js';
6
+ import { uploadMedia } from '../transport/media.js';
7
+ import { request } from '../transport/http.js';
6
8
  import { shoot, DEFAULT_WIDTHS } from '../vision/shoot.js';
7
9
  import { RealtimeSocket } from '../transport/socket.js';
8
10
  import { LiveSession } from '../live/session.js';
@@ -86,6 +88,49 @@ export function registerLiveTools(server, ctx, session) {
86
88
  ...(findings.length > 0 ? { findings, findings_notice: REVIEW_NOTICE } : {}),
87
89
  });
88
90
  });
91
+ server.tool('sb_media_list', "The site's media library — reuse an image that is already there before adding another. " +
92
+ 'Search by name, filter by type, page with limit/offset.', {
93
+ site_id: z.string(),
94
+ search: z.string().optional(),
95
+ media_type: z.string().optional().describe('e.g. "image"'),
96
+ limit: z.number().int().min(1).max(200).optional(),
97
+ offset: z.number().int().min(0).optional(),
98
+ }, async ({ site_id, search, media_type, limit, offset }) => text(await request({
99
+ base: ctx.base,
100
+ method: 'GET',
101
+ path: `/api/sites/${encodeURIComponent(site_id)}/media`,
102
+ token: siteToken(ctx),
103
+ query: { search, mediaType: media_type, limit, offset },
104
+ fetchImpl: ctx.fetchImpl,
105
+ })));
106
+ server.tool('sb_media_upload', 'Put an image into the media library and get its URL back, ready for sb_set. Takes a ' +
107
+ 'local file path or a URL to fetch. This is the ONLY way to add an image: the upload ' +
108
+ 'is multipart, which sb_api_call cannot send.', {
109
+ site_id: z.string(),
110
+ path: z.string().optional().describe('A file on this machine'),
111
+ url: z.string().optional().describe('Fetched, then uploaded'),
112
+ name: z.string().optional(),
113
+ folder_id: z.string().optional(),
114
+ dry_run: z.boolean().optional(),
115
+ }, async ({ site_id, path, url, name, folder_id, dry_run }) => {
116
+ if (!path && !url)
117
+ throw new Error('sbuilder: give sb_media_upload either a path or a url');
118
+ if (dry_run !== false) {
119
+ return text({
120
+ dry_run: true,
121
+ would_upload: path ?? url,
122
+ into: site_id,
123
+ note: 'Nothing was sent. Re-call with dry_run:false to upload.',
124
+ });
125
+ }
126
+ const asset = await uploadMedia(ctx, site_id, { path, url, name, folderId: folder_id });
127
+ return text({
128
+ asset,
129
+ next: asset.url
130
+ ? `Use it: sb_set id "<node>", namespace specials, keys { "src": ${JSON.stringify(asset.url)} }`
131
+ : 'Uploaded, but the server returned no url — read it back with sb_media_list.',
132
+ });
133
+ });
89
134
  server.tool('sb_bind', "Bind a node's content to real store data, so the page shows actual products rather than " +
90
135
  'placeholder text.', {
91
136
  id: z.string(),
@@ -0,0 +1,66 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { basename } from 'node:path';
3
+ import { ApiError } from './http.js';
4
+ import { siteToken } from '../tools/credentialpick.js';
5
+ /**
6
+ * Put an image into the site's media library.
7
+ *
8
+ * MULTIPART, which is why this does not go through `request()`. That helper
9
+ * JSON-encodes every body, and `POST /api/media/{siteId}` takes
10
+ * `file:formData/file` — so an agent reaching this endpoint through sb_api_call
11
+ * sent JSON to a multipart handler and got a rejection it could not act on. A
12
+ * page with no images is not a designed page, so this was the gap between
13
+ * "the agent can lay out a page" and "the agent can finish one".
14
+ *
15
+ * Node ≥22 has FormData, Blob and fetch as globals, so this needs no dependency.
16
+ */
17
+ export async function uploadMedia(ctx, siteId, source) {
18
+ const doFetch = ctx.fetchImpl ?? fetch;
19
+ let bytes;
20
+ let filename;
21
+ if (source.path) {
22
+ bytes = await readFile(source.path);
23
+ filename = source.name ?? basename(source.path);
24
+ }
25
+ else if (source.url) {
26
+ const res = await doFetch(source.url);
27
+ if (!res.ok) {
28
+ throw new ApiError(res.status, 'source_unreachable', `could not fetch ${source.url}`);
29
+ }
30
+ bytes = new Uint8Array(await res.arrayBuffer());
31
+ // A URL's last segment is usually the filename; when it is not (a query-only
32
+ // CDN link), name it rather than uploading something called "".
33
+ filename = source.name ?? (new URL(source.url).pathname.split('/').pop() || 'image');
34
+ }
35
+ else {
36
+ throw new Error('sbuilder: give sb_media_upload either a local path or a url');
37
+ }
38
+ const form = new FormData();
39
+ form.set('file', new Blob([bytes]), filename);
40
+ if (source.name)
41
+ form.set('name', source.name);
42
+ if (source.folderId)
43
+ form.set('folderId', source.folderId);
44
+ // No Content-Type header: fetch must set it itself so the multipart boundary
45
+ // matches the body it just built. Setting it by hand is the classic way to
46
+ // make a valid upload unparseable at the other end.
47
+ const res = await doFetch(`${ctx.base.replace(/\/$/, '')}/api/media/${encodeURIComponent(siteId)}`, {
48
+ method: 'POST',
49
+ headers: { Authorization: `Bearer ${siteToken(ctx)}`, Accept: 'application/json' },
50
+ body: form,
51
+ });
52
+ const raw = await res.text();
53
+ let parsed;
54
+ try {
55
+ parsed = raw ? JSON.parse(raw) : {};
56
+ }
57
+ catch {
58
+ throw new ApiError(res.status, 'non_json_response', raw.slice(0, 400));
59
+ }
60
+ if (!res.ok) {
61
+ const env = (parsed ?? {});
62
+ throw new ApiError(res.status, env.code ?? 'unknown', env.error ?? `HTTP ${res.status}`);
63
+ }
64
+ const body = parsed;
65
+ return body.asset ?? parsed;
66
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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",