sbuilder-mcp 0.2.0 → 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 +29 -0
- package/CHANGELOG.vi.md +29 -0
- package/dist/catalog/elements.generated.js +569 -0
- package/dist/core/tree.js +26 -1
- package/dist/domains/site/builder.js +68 -4
- package/dist/domains/site/findings.js +58 -0
- package/dist/domains/site/node.js +90 -1
- package/dist/domains/site/review.js +67 -1
- package/dist/domains/site/traps.js +24 -0
- package/dist/domains/site/validate.js +31 -1
- package/dist/smoke.js +29 -0
- package/dist/tools/live.js +29 -14
- package/dist/tools/page.js +61 -4
- package/dist/transport/http.js +4 -2
- package/dist/transport/media.js +19 -10
- package/package.json +1 -1
package/dist/tools/page.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { composeWarnings } from '../domains/site/findings.js';
|
|
2
3
|
import { text } from '../mcp/response.js';
|
|
3
4
|
import { loadSource, saveSource } from '../transport/pages.js';
|
|
4
5
|
import { PageDoc } from '../domains/site/document.js';
|
|
5
6
|
import { addSubtree, setMany, moveNode, removeNode, duplicateNode, } from '../domains/site/builder.js';
|
|
6
|
-
import { request } from '../transport/http.js';
|
|
7
|
+
import { request, redact } from '../transport/http.js';
|
|
7
8
|
import { siteToken } from './credentialpick.js';
|
|
8
9
|
import { validateForSave } from '../domains/site/validate.js';
|
|
9
10
|
import { reviewDesign, REVIEW_NOTICE } from '../domains/site/review.js';
|
|
@@ -42,6 +43,7 @@ export class PageSession {
|
|
|
42
43
|
pageId = '';
|
|
43
44
|
live = null;
|
|
44
45
|
stale = null;
|
|
46
|
+
warnings = [];
|
|
45
47
|
boxes = [];
|
|
46
48
|
constructor(ctx) {
|
|
47
49
|
this.ctx = ctx;
|
|
@@ -90,8 +92,15 @@ export class PageSession {
|
|
|
90
92
|
this.doc = PageDoc.from(src.document);
|
|
91
93
|
this.siteId = siteId;
|
|
92
94
|
this.pageId = pageId;
|
|
95
|
+
// The platform's own account of what it could not compose. Typed on the
|
|
96
|
+
// response since the transport was written and read by nothing until now.
|
|
97
|
+
this.warnings = composeWarnings(src.warnings);
|
|
93
98
|
return this.doc.outline();
|
|
94
99
|
}
|
|
100
|
+
/** What the server said it could not compose when this page was opened. */
|
|
101
|
+
composeWarnings() {
|
|
102
|
+
return this.warnings;
|
|
103
|
+
}
|
|
95
104
|
current() {
|
|
96
105
|
if (!this.doc)
|
|
97
106
|
throw new Error('sbuilder: no page is open — call sb_page_open first');
|
|
@@ -151,9 +160,11 @@ export function registerPageTools(server, ctx) {
|
|
|
151
160
|
'the renderer finds no root and publishes an EMPTY BODY. The next save from here writes ' +
|
|
152
161
|
'the canonical key and fixes it; publish afterwards.'
|
|
153
162
|
: undefined;
|
|
163
|
+
const warnings = session.composeWarnings();
|
|
154
164
|
return text({
|
|
155
165
|
outline,
|
|
156
166
|
...(blank_page_repair ? { blank_page_repair } : {}),
|
|
167
|
+
...(warnings.length ? { compose_warnings: warnings } : {}),
|
|
157
168
|
...reviewField(ctx, doc),
|
|
158
169
|
});
|
|
159
170
|
});
|
|
@@ -423,9 +434,12 @@ export function registerPageTools(server, ctx) {
|
|
|
423
434
|
...(is_homepage !== undefined ? { isHomepage: is_homepage } : {}),
|
|
424
435
|
...(settings ? { settings } : {}),
|
|
425
436
|
};
|
|
437
|
+
// `settings` is the one free-form object a caller hands this server, so the
|
|
438
|
+
// preview and the echo both go through redact — everything else on this
|
|
439
|
+
// path is built from narrow arguments.
|
|
426
440
|
if (dry_run !== false)
|
|
427
|
-
return text({ dry_run: true, would_post: path, body });
|
|
428
|
-
|
|
441
|
+
return text({ dry_run: true, would_post: path, body: redact(body) });
|
|
442
|
+
const res = redact(await request({
|
|
429
443
|
base: ctx.base,
|
|
430
444
|
method: 'POST',
|
|
431
445
|
path,
|
|
@@ -433,6 +447,24 @@ export function registerPageTools(server, ctx) {
|
|
|
433
447
|
body,
|
|
434
448
|
fetchImpl: ctx.fetchImpl,
|
|
435
449
|
}));
|
|
450
|
+
// A COLLIDING SLUG IS RENAMED, NOT REFUSED. `uniqueSlug` suffixes -1, -2 …
|
|
451
|
+
// and its own comment says it "never errors"
|
|
452
|
+
// (server/internal/page/service.go:877). ErrSlugConflict exists and maps to
|
|
453
|
+
// 409; this path never reaches it. So the create answers 200 carrying a
|
|
454
|
+
// DIFFERENT slug than the one asked for, and every link the caller then
|
|
455
|
+
// authors to the slug it requested is dead.
|
|
456
|
+
const got = res.page?.slug;
|
|
457
|
+
const renamed = slug && typeof got === 'string' && got !== slug;
|
|
458
|
+
return text({
|
|
459
|
+
...res,
|
|
460
|
+
...(renamed
|
|
461
|
+
? {
|
|
462
|
+
slug_renamed: `The slug "${slug}" was already taken, so the platform stored ` +
|
|
463
|
+
`"${got}" instead and reported success. Link to "${got}", or free the name and ` +
|
|
464
|
+
'create it again.',
|
|
465
|
+
}
|
|
466
|
+
: {}),
|
|
467
|
+
});
|
|
436
468
|
});
|
|
437
469
|
server.registerTool('sb_publish', {
|
|
438
470
|
description: 'Compile the draft into the live page. PUBLISH CASCADES: a page sharing a global ' +
|
|
@@ -451,7 +483,7 @@ export function registerPageTools(server, ctx) {
|
|
|
451
483
|
const body = { pageIds: [page_id] };
|
|
452
484
|
if (dry_run !== false)
|
|
453
485
|
return text({ dry_run: true, would_post: path, body });
|
|
454
|
-
|
|
486
|
+
const res = (await request({
|
|
455
487
|
base: ctx.base,
|
|
456
488
|
method: 'POST',
|
|
457
489
|
path,
|
|
@@ -459,6 +491,31 @@ export function registerPageTools(server, ctx) {
|
|
|
459
491
|
body,
|
|
460
492
|
fetchImpl: ctx.fetchImpl,
|
|
461
493
|
}));
|
|
494
|
+
// A PUBLISHED ROW CARRIES THE WHOLE RENDERED PAGE — document, html and css
|
|
495
|
+
// — and publish CASCADES, so returning the response as it arrives pours
|
|
496
|
+
// every republished page's markup into the reader. Kept: what identifies
|
|
497
|
+
// the row and what a caller would act on.
|
|
498
|
+
const published = (res.published ?? []).map((p) => ({
|
|
499
|
+
pageId: p.pageId,
|
|
500
|
+
...(p.slug !== undefined ? { slug: p.slug } : {}),
|
|
501
|
+
...(p.isHomepage ? { isHomepage: true } : {}),
|
|
502
|
+
}));
|
|
503
|
+
// PUBLISH SKIPS A PAGE WITH NO SAVED DRAFT and still answers 200 with
|
|
504
|
+
// whatever did publish (`server/internal/page/service.go:650`, a bare
|
|
505
|
+
// `continue`). sb_page_create followed by sb_publish does exactly that:
|
|
506
|
+
// the call succeeds, the page never flips to published, and the URL 404s.
|
|
507
|
+
const landed = published.some((p) => p.pageId === page_id);
|
|
508
|
+
return text({
|
|
509
|
+
published,
|
|
510
|
+
...(published.length !== (res.total ?? published.length) ? { total: res.total } : {}),
|
|
511
|
+
...(landed
|
|
512
|
+
? {}
|
|
513
|
+
: {
|
|
514
|
+
not_published: `Page ${page_id} has no saved draft, so the platform published ` +
|
|
515
|
+
'nothing for it and reported success anyway. Open it with sb_page_open, save an ' +
|
|
516
|
+
'edit, then publish again.',
|
|
517
|
+
}),
|
|
518
|
+
});
|
|
462
519
|
});
|
|
463
520
|
return session;
|
|
464
521
|
}
|
package/dist/transport/http.js
CHANGED
|
@@ -34,8 +34,10 @@ const SECRET_KEYS = /^(authorization|token|access_?token|refresh_?token|password
|
|
|
34
34
|
/**
|
|
35
35
|
* Replace credential-shaped values with a marker, recursively.
|
|
36
36
|
*
|
|
37
|
-
* Every
|
|
38
|
-
*
|
|
37
|
+
* Every preview that can carry a FREE-FORM object goes through this — `sb_api_call`'s
|
|
38
|
+
* body and `sb_page_create`'s `settings`, the two places a caller supplies a shape
|
|
39
|
+
* this server does not type. The rest of the dry-run previews echo patch counts or
|
|
40
|
+
* bodies built from narrow arguments, which cannot hold a credential. It keys
|
|
39
41
|
* off the FIELD NAME rather than the value's shape on purpose: a token format
|
|
40
42
|
* can change tomorrow, while the field name is what this repo controls.
|
|
41
43
|
*/
|
package/dist/transport/media.js
CHANGED
|
@@ -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
|
-
//
|
|
69
|
+
// AN API KEY CAN UPLOAD — THE PLATFORM WIDENED THIS.
|
|
70
70
|
//
|
|
71
|
-
// `/api/media
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
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, '
|
|
79
|
-
'
|
|
80
|
-
'
|
|
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.
|
|
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",
|