@wootsup/yt-builder-mcp 1.10.0 → 1.12.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/README.md +12 -11
- package/bin/yt-builder-mcp.js +1 -1
- package/dist/auth.d.ts +1 -1
- package/dist/auth.js +1 -1
- package/dist/catalog/tool-catalog-markdown.d.ts.map +1 -1
- package/dist/catalog/tool-catalog-markdown.js +13 -1
- package/dist/catalog/tool-catalog-markdown.js.map +1 -1
- package/dist/cli/doctor-command.d.ts.map +1 -1
- package/dist/cli/doctor-command.js +22 -2
- package/dist/cli/doctor-command.js.map +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.js +1 -1
- package/dist/client.js.map +1 -1
- package/dist/clients/index.d.ts +1 -1
- package/dist/clients/index.js +1 -1
- package/dist/diagnostics/ca-reexec.js +2 -2
- package/dist/diagnostics/diagnose-network-error.d.ts +1 -1
- package/dist/diagnostics/doctor.d.ts +34 -2
- package/dist/diagnostics/doctor.d.ts.map +1 -1
- package/dist/diagnostics/doctor.js +63 -2
- package/dist/diagnostics/doctor.js.map +1 -1
- package/dist/diagnostics/startup-self-probe.js +1 -1
- package/dist/gateway/advanced-read-tool.d.ts.map +1 -1
- package/dist/gateway/advanced-read-tool.js +14 -2
- package/dist/gateway/advanced-read-tool.js.map +1 -1
- package/dist/gateway/advanced-tool/discovery.d.ts.map +1 -1
- package/dist/gateway/advanced-tool/discovery.js +12 -1
- package/dist/gateway/advanced-tool/discovery.js.map +1 -1
- package/dist/gateway/advanced-tool/domains.d.ts.map +1 -1
- package/dist/gateway/advanced-tool/domains.js +5 -0
- package/dist/gateway/advanced-tool/domains.js.map +1 -1
- package/dist/gateway/advanced-tool/execute.d.ts +7 -1
- package/dist/gateway/advanced-tool/execute.d.ts.map +1 -1
- package/dist/gateway/advanced-tool/execute.js +58 -4
- package/dist/gateway/advanced-tool/execute.js.map +1 -1
- package/dist/gateway/essentials.d.ts +4 -4
- package/dist/gateway/essentials.d.ts.map +1 -1
- package/dist/gateway/essentials.js +12 -7
- package/dist/gateway/essentials.js.map +1 -1
- package/dist/install-skill.js +1 -1
- package/dist/net/extra-headers.d.ts +1 -1
- package/dist/net/extra-headers.js +1 -1
- package/dist/net/internal-host.js +2 -2
- package/dist/net/proxy-dispatcher.js +1 -1
- package/dist/net/proxy-dispatcher.js.map +1 -1
- package/dist/net/site-basic-auth.d.ts +2 -2
- package/dist/net/site-basic-auth.js +2 -2
- package/dist/proxy/bridge.d.ts +2 -2
- package/dist/proxy/bridge.js +3 -3
- package/dist/proxy/bridge.js.map +1 -1
- package/dist/server.d.ts +3 -3
- package/dist/server.js +3 -3
- package/dist/setup-cli.d.ts +2 -2
- package/dist/setup-cli.js +2 -2
- package/dist/setup-cli.js.map +1 -1
- package/dist/setup-npx-spec.d.ts +1 -1
- package/dist/setup-npx-spec.js +1 -1
- package/dist/setup-wizard-handshake.js +1 -1
- package/dist/setup-wizard-types.d.ts +1 -1
- package/dist/setup-wizard.js +3 -3
- package/dist/setup-wizard.js.map +1 -1
- package/dist/sites/client-pool.d.ts +1 -1
- package/dist/sites/client-pool.js +1 -1
- package/dist/sites/tools/sites-list.d.ts.map +1 -1
- package/dist/sites/tools/sites-list.js +26 -4
- package/dist/sites/tools/sites-list.js.map +1 -1
- package/dist/sites/tools/sites-test.js +1 -1
- package/dist/sites/tools/sites-test.js.map +1 -1
- package/dist/sites/tools/use-site.js +2 -2
- package/dist/sites/tools/use-site.js.map +1 -1
- package/dist/tools/budgeted-table.d.ts +21 -0
- package/dist/tools/budgeted-table.d.ts.map +1 -0
- package/dist/tools/budgeted-table.js +137 -0
- package/dist/tools/budgeted-table.js.map +1 -0
- package/dist/tools/elements/builders.d.ts.map +1 -1
- package/dist/tools/elements/builders.js +102 -40
- package/dist/tools/elements/builders.js.map +1 -1
- package/dist/tools/elements/handlers-write.d.ts +21 -0
- package/dist/tools/elements/handlers-write.d.ts.map +1 -1
- package/dist/tools/elements/handlers-write.js +231 -11
- package/dist/tools/elements/handlers-write.js.map +1 -1
- package/dist/tools/elements/handlers.d.ts +20 -12
- package/dist/tools/elements/handlers.d.ts.map +1 -1
- package/dist/tools/elements/handlers.js +77 -26
- package/dist/tools/elements/handlers.js.map +1 -1
- package/dist/tools/elements/schema-validation.d.ts +147 -7
- package/dist/tools/elements/schema-validation.d.ts.map +1 -1
- package/dist/tools/elements/schema-validation.js +508 -28
- package/dist/tools/elements/schema-validation.js.map +1 -1
- package/dist/tools/format/elements-format.d.ts.map +1 -1
- package/dist/tools/format/elements-format.js +56 -12
- package/dist/tools/format/elements-format.js.map +1 -1
- package/dist/tools/format/health-format.d.ts +16 -0
- package/dist/tools/format/health-format.d.ts.map +1 -1
- package/dist/tools/format/health-format.js +9 -0
- package/dist/tools/format/health-format.js.map +1 -1
- package/dist/tools/format/inspection-format.d.ts +7 -0
- package/dist/tools/format/inspection-format.d.ts.map +1 -1
- package/dist/tools/format/inspection-format.js +18 -0
- package/dist/tools/format/inspection-format.js.map +1 -1
- package/dist/tools/format/pages-format.d.ts +1 -1
- package/dist/tools/format/pages-format.d.ts.map +1 -1
- package/dist/tools/format/pages-format.js +7 -0
- package/dist/tools/format/pages-format.js.map +1 -1
- package/dist/tools/format/sources-format.d.ts +12 -5
- package/dist/tools/format/sources-format.d.ts.map +1 -1
- package/dist/tools/format/sources-format.js +31 -6
- package/dist/tools/format/sources-format.js.map +1 -1
- package/dist/tools/health.d.ts.map +1 -1
- package/dist/tools/health.js +85 -11
- package/dist/tools/health.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +7 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/inspection.d.ts +34 -0
- package/dist/tools/inspection.d.ts.map +1 -1
- package/dist/tools/inspection.js +666 -62
- package/dist/tools/inspection.js.map +1 -1
- package/dist/tools/layout-flatten.d.ts +7 -1
- package/dist/tools/layout-flatten.d.ts.map +1 -1
- package/dist/tools/layout-flatten.js +24 -13
- package/dist/tools/layout-flatten.js.map +1 -1
- package/dist/tools/library.d.ts +29 -2
- package/dist/tools/library.d.ts.map +1 -1
- package/dist/tools/library.js +247 -34
- package/dist/tools/library.js.map +1 -1
- package/dist/tools/local-content/builders.js +14 -14
- package/dist/tools/local-content/builders.js.map +1 -1
- package/dist/tools/local-content/handlers.d.ts +3 -1
- package/dist/tools/local-content/handlers.d.ts.map +1 -1
- package/dist/tools/local-content/handlers.js +42 -9
- package/dist/tools/local-content/handlers.js.map +1 -1
- package/dist/tools/local-content/schemas.d.ts +9 -4
- package/dist/tools/local-content/schemas.d.ts.map +1 -1
- package/dist/tools/local-content/schemas.js +14 -6
- package/dist/tools/local-content/schemas.js.map +1 -1
- package/dist/tools/multi-items/builders.d.ts.map +1 -1
- package/dist/tools/multi-items/builders.js +11 -10
- package/dist/tools/multi-items/builders.js.map +1 -1
- package/dist/tools/navigation/builders.d.ts +18 -0
- package/dist/tools/navigation/builders.d.ts.map +1 -0
- package/dist/tools/navigation/builders.js +47 -0
- package/dist/tools/navigation/builders.js.map +1 -0
- package/dist/tools/navigation/handlers.d.ts +30 -0
- package/dist/tools/navigation/handlers.d.ts.map +1 -0
- package/dist/tools/navigation/handlers.js +41 -0
- package/dist/tools/navigation/handlers.js.map +1 -0
- package/dist/tools/navigation/index.d.ts +15 -0
- package/dist/tools/navigation/index.d.ts.map +1 -0
- package/dist/tools/navigation/index.js +14 -0
- package/dist/tools/navigation/index.js.map +1 -0
- package/dist/tools/navigation/schemas.d.ts +46 -0
- package/dist/tools/navigation/schemas.d.ts.map +1 -0
- package/dist/tools/navigation/schemas.js +41 -0
- package/dist/tools/navigation/schemas.js.map +1 -0
- package/dist/tools/pages/builders.d.ts.map +1 -1
- package/dist/tools/pages/builders.js +213 -81
- package/dist/tools/pages/builders.js.map +1 -1
- package/dist/tools/pages/handlers-audit.d.ts +112 -3
- package/dist/tools/pages/handlers-audit.d.ts.map +1 -1
- package/dist/tools/pages/handlers-audit.js +623 -7
- package/dist/tools/pages/handlers-audit.js.map +1 -1
- package/dist/tools/pages/handlers-read.d.ts +51 -8
- package/dist/tools/pages/handlers-read.d.ts.map +1 -1
- package/dist/tools/pages/handlers-read.js +348 -69
- package/dist/tools/pages/handlers-read.js.map +1 -1
- package/dist/tools/pages/handlers-write.d.ts +7 -3
- package/dist/tools/pages/handlers-write.d.ts.map +1 -1
- package/dist/tools/pages/handlers-write.js +69 -25
- package/dist/tools/pages/handlers-write.js.map +1 -1
- package/dist/tools/pages/header-transparency.d.ts +64 -0
- package/dist/tools/pages/header-transparency.d.ts.map +1 -0
- package/dist/tools/pages/header-transparency.js +119 -0
- package/dist/tools/pages/header-transparency.js.map +1 -0
- package/dist/tools/pages/schemas.d.ts +284 -13
- package/dist/tools/pages/schemas.d.ts.map +1 -1
- package/dist/tools/pages/schemas.js +434 -26
- package/dist/tools/pages/schemas.js.map +1 -1
- package/dist/tools/pages/target.d.ts +162 -0
- package/dist/tools/pages/target.d.ts.map +1 -0
- package/dist/tools/pages/target.js +239 -0
- package/dist/tools/pages/target.js.map +1 -0
- package/dist/tools/response-budget.d.ts +77 -0
- package/dist/tools/response-budget.d.ts.map +1 -0
- package/dist/tools/response-budget.js +134 -0
- package/dist/tools/response-budget.js.map +1 -0
- package/dist/tools/shared-schemas.d.ts +45 -0
- package/dist/tools/shared-schemas.d.ts.map +1 -1
- package/dist/tools/shared-schemas.js +50 -0
- package/dist/tools/shared-schemas.js.map +1 -1
- package/dist/tools/sources/builders.d.ts.map +1 -1
- package/dist/tools/sources/builders.js +55 -39
- package/dist/tools/sources/builders.js.map +1 -1
- package/dist/tools/sources/handlers-bind.d.ts +10 -11
- package/dist/tools/sources/handlers-bind.d.ts.map +1 -1
- package/dist/tools/sources/handlers-bind.js +110 -9
- package/dist/tools/sources/handlers-bind.js.map +1 -1
- package/dist/tools/sources/handlers.d.ts +4 -2
- package/dist/tools/sources/handlers.d.ts.map +1 -1
- package/dist/tools/sources/handlers.js +11 -7
- package/dist/tools/sources/handlers.js.map +1 -1
- package/dist/tools/sparse-fields.d.ts +61 -8
- package/dist/tools/sparse-fields.d.ts.map +1 -1
- package/dist/tools/sparse-fields.js +124 -12
- package/dist/tools/sparse-fields.js.map +1 -1
- package/dist/tools/sublayout/builders.d.ts.map +1 -1
- package/dist/tools/sublayout/builders.js +1 -2
- package/dist/tools/sublayout/builders.js.map +1 -1
- package/dist/tools/tool-builder/results.d.ts +19 -6
- package/dist/tools/tool-builder/results.d.ts.map +1 -1
- package/dist/tools/tool-builder/results.js +133 -6
- package/dist/tools/tool-builder/results.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +2 -2
- package/skills/yt-builder-mcp/SKILL.md +1713 -72
|
@@ -9,13 +9,26 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { detailResult, tableResult } from '@getimo/mcp-toolkit';
|
|
11
11
|
import { encodeTemplateId } from '../../client.js';
|
|
12
|
+
import { RestError } from '../../errors.js';
|
|
12
13
|
import { flattenLayout } from '../layout-flatten.js';
|
|
13
14
|
import { PAGES_COMPACT_COLUMNS, PAGES_TABLE_COLUMNS, SCHEMA_COMPACT_COLUMNS, SCHEMA_TABLE_COLUMNS, buildEtagDetail, buildOutlineText, buildRenderPreviewDetail, buildSchemaRows, mapPageRow, } from '../format/pages-format.js';
|
|
14
15
|
import { DEFAULT_FIELDS_PAGES_LIST, DEFAULT_FIELDS_SCHEMA, applyMaxChars, clipTextWindow, columnsFromFields, decodeOffsetCursor, encodeOffsetCursor, projectFields, projectedFieldsEcho, projectionFeedback, projectionWarning, } from '../sparse-fields.js';
|
|
15
16
|
import { errorResult, jsonResult, structuredResult, } from '../tool-builder.js';
|
|
17
|
+
import { TARGET_KEYS } from './schemas.js';
|
|
18
|
+
import { appendTargetParams, malformedTargetError, normalizeTarget, serverTooOldError, TARGET_FAILURE_HINT, targetKeysOf, tooManyTargetsError, } from './target.js';
|
|
16
19
|
// ─── pages_list ─────────────────────────────────────────────────────
|
|
17
20
|
/** Default page size for pages_list when `limit` is omitted (B2). */
|
|
18
21
|
const DEFAULT_PAGES_LIMIT = 50;
|
|
22
|
+
/**
|
|
23
|
+
* SOURCES_BINDING-29 / DISCOVERY_AUDIT-18 — the STATIC pages_list field
|
|
24
|
+
* vocabulary. `mapPageRow` always emits the SAME key set on every row, so
|
|
25
|
+
* deriving it once from an empty-row projection gives the authoritative
|
|
26
|
+
* discoverable field list WITHOUT hard-coding a list that could drift from the
|
|
27
|
+
* mapper. Used to (a) validate a caller's `fields[]` against the real schema
|
|
28
|
+
* rather than against whatever rows a (possibly empty) result happened to carry,
|
|
29
|
+
* and (b) emit `available_fields` on every call. Sorted for a stable echo.
|
|
30
|
+
*/
|
|
31
|
+
const PAGES_LIST_KNOWN_FIELDS = Object.keys(mapPageRow({})).sort();
|
|
19
32
|
/** Decode an offset cursor (`base64url("o:<n>")` or raw `o:<n>`) → offset, else 0. */
|
|
20
33
|
function decodePagesCursor(cursor) {
|
|
21
34
|
if (cursor === undefined || cursor === '')
|
|
@@ -35,14 +48,19 @@ function decodePagesCursor(cursor) {
|
|
|
35
48
|
function encodePagesCursor(offset) {
|
|
36
49
|
return Buffer.from(`o:${offset}`, 'utf8').toString('base64url');
|
|
37
50
|
}
|
|
38
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Case-insensitive substring match across a page row's label / id / type /
|
|
53
|
+
* frontend_url. T-C2 (customer P1.2): frontend_url joined the haystack so a
|
|
54
|
+
* slug or URL fragment (e.g. "nkl-hauptspiel") matches even when it appears
|
|
55
|
+
* in none of label/id/type — mirrors PHP's `PageQuery::applyLabelFilter()`.
|
|
56
|
+
*/
|
|
39
57
|
function matchesPageFilter(row, needle) {
|
|
40
|
-
const hay = `${String(row.label ?? '')} ${String(row.id ?? '')} ${String(row.type ?? '')}`.toLowerCase();
|
|
58
|
+
const hay = `${String(row.label ?? '')} ${String(row.id ?? '')} ${String(row.type ?? '')} ${String(row.frontend_url ?? '')}`.toLowerCase();
|
|
41
59
|
return hay.includes(needle);
|
|
42
60
|
}
|
|
43
|
-
export async function handlePagesList(client, { fields, limit, cursor, label_contains, max_chars, }) {
|
|
61
|
+
export async function handlePagesList(client, { fields, limit, cursor, label_contains, max_chars, include_meta, }) {
|
|
44
62
|
try {
|
|
45
|
-
// G-B (
|
|
63
|
+
// G-B (customer 2026-06-26, P0): forward `limit`/`offset`/`filter` to the
|
|
46
64
|
// REST /pages endpoint so a site with hundreds of templates does not
|
|
47
65
|
// serialize the full set every call (server-side pagination). The
|
|
48
66
|
// server is BACKWARD-COMPATIBLE: with no params it returns the full set
|
|
@@ -66,7 +84,7 @@ export async function handlePagesList(client, { fields, limit, cursor, label_con
|
|
|
66
84
|
let mapped = rawPages
|
|
67
85
|
.filter((x) => x !== null && typeof x === 'object')
|
|
68
86
|
.map(mapPageRow);
|
|
69
|
-
// B2 (
|
|
87
|
+
// B2 (customer 2026-06-25) + G-B server-side fallback: a real site can
|
|
70
88
|
// have hundreds of templates. When the server already paged + filtered
|
|
71
89
|
// (it echoes a `total`), the rows it returned ARE the page — do NOT
|
|
72
90
|
// re-slice them. When the server ignored the params (no `total`, full
|
|
@@ -87,18 +105,40 @@ export async function handlePagesList(client, { fields, limit, cursor, label_con
|
|
|
87
105
|
const nextCursor = nextOffset < total ? encodePagesCursor(nextOffset) : undefined;
|
|
88
106
|
// B1: project the SLICE (text leg + structuredContent) when fields given.
|
|
89
107
|
const items = projectFields(slice, fields, DEFAULT_FIELDS_PAGES_LIST);
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
108
|
+
const hasExplicitFields = Array.isArray(fields) && fields.length > 0;
|
|
109
|
+
// SOURCES_BINDING-29: validate `fields[]` against the STATIC schema, not
|
|
110
|
+
// the returned rows — an EMPTY result set no longer makes every field
|
|
111
|
+
// look invalid (the "available: " with no fields bug). unknown = a
|
|
112
|
+
// requested field whose top-level segment is not a known key.
|
|
113
|
+
const unknownFields = hasExplicitFields
|
|
114
|
+
? fields.filter((f) => !PAGES_LIST_KNOWN_FIELDS.includes(f.split('.')[0] ?? f))
|
|
115
|
+
: [];
|
|
116
|
+
// DISCOVERY_AUDIT-18: `available_fields` is the STATIC discoverable
|
|
117
|
+
// vocabulary, emitted on EVERY call (an empty page still tells the agent
|
|
118
|
+
// which fields exist). `projected_fields` reflects the keys ACTUALLY in
|
|
119
|
+
// the emitted rows — the full vocabulary in the default no-`fields` case,
|
|
120
|
+
// the requested subset otherwise (LAYOUT_READ-18: no longer the stale
|
|
121
|
+
// 5-key DEFAULT echo while rows carry 11).
|
|
122
|
+
const availableFields = [...PAGES_LIST_KNOWN_FIELDS];
|
|
123
|
+
const projectedFields = hasExplicitFields
|
|
124
|
+
? [...fields]
|
|
125
|
+
: [...PAGES_LIST_KNOWN_FIELDS];
|
|
126
|
+
// SOURCES_BINDING-29: suppress the loud projection warning on an empty
|
|
127
|
+
// result (projection is moot) and when nothing is actually unknown.
|
|
128
|
+
const emitWarning = total > 0 && unknownFields.length > 0;
|
|
129
|
+
const unknownNote = emitWarning
|
|
130
|
+
? ` (unknown projection fields ignored: ${unknownFields.join(', ')}` +
|
|
131
|
+
` — available: ${availableFields.join(', ')})`
|
|
132
|
+
: '';
|
|
133
|
+
const useProjection = hasExplicitFields;
|
|
134
|
+
const projectedColumns = useProjection ? columnsFromFields([...fields]) : undefined;
|
|
96
135
|
const pagedNote = total > slice.length ? ` of ${String(total)}${nextCursor !== undefined ? ', pass cursor for more' : ''}` : '';
|
|
97
136
|
const toolkitResult = tableResult(useProjection ? items : slice, {
|
|
98
137
|
columns: projectedColumns ?? [...PAGES_TABLE_COLUMNS],
|
|
99
138
|
compactColumns: projectedColumns ?? [...PAGES_COMPACT_COLUMNS],
|
|
100
139
|
header: (count) => `${String(count)}${pagedNote} pages${unknownNote}`,
|
|
101
140
|
footer: 'Use yootheme_builder_page_get_schema <id> to inspect.',
|
|
141
|
+
includeMeta: include_meta,
|
|
102
142
|
});
|
|
103
143
|
return structuredResult(applyMaxChars(toolkitResult, max_chars), {
|
|
104
144
|
items,
|
|
@@ -106,12 +146,13 @@ export async function handlePagesList(client, { fields, limit, cursor, label_con
|
|
|
106
146
|
returned: slice.length,
|
|
107
147
|
...(nextCursor !== undefined ? { next_cursor: nextCursor } : {}),
|
|
108
148
|
...(typeof data.etag === 'string' ? { etag: data.etag } : {}),
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
} : {}),
|
|
149
|
+
projected_fields: projectedFields,
|
|
150
|
+
available_fields: availableFields,
|
|
151
|
+
// Emit unknown_fields whenever the caller passed an explicit
|
|
152
|
+
// projection (empty [] = all valid; keeps the F-004 contract), omit
|
|
153
|
+
// it on the default no-`fields` call.
|
|
154
|
+
...(hasExplicitFields ? { unknown_fields: unknownFields } : {}),
|
|
155
|
+
...(emitWarning ? { projection_warning: unknownNote.trim() } : {}),
|
|
115
156
|
});
|
|
116
157
|
}
|
|
117
158
|
catch (e) {
|
|
@@ -134,29 +175,126 @@ export async function handlePagesList(client, { fields, limit, cursor, label_con
|
|
|
134
175
|
export async function resolveUrlToTemplate(client, url) {
|
|
135
176
|
return client.get(`/pages/resolve?url=${encodeURIComponent(url)}`);
|
|
136
177
|
}
|
|
137
|
-
|
|
178
|
+
/**
|
|
179
|
+
* A-6: resolve a semantic `target` — `GET /pages/resolve?target[special]=home`.
|
|
180
|
+
* The bracket encoding is the ONE wire form both CMS controllers parse (see
|
|
181
|
+
* `./target.ts`).
|
|
182
|
+
*/
|
|
183
|
+
async function resolveTargetToTemplate(client, target) {
|
|
184
|
+
const qs = new URLSearchParams();
|
|
185
|
+
appendTargetParams(qs, target);
|
|
186
|
+
return client.get(`/pages/resolve?${qs.toString()}`);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* A-6: is this failure an OLD host rejecting the request because it still
|
|
190
|
+
* hard-requires `url`? WP core answers `rest_missing_callback_param` when the
|
|
191
|
+
* route arg is `required`; the pre-A-5 controller answered its own
|
|
192
|
+
* `missing_url`. Either way the fix is "update the plugin", not "fix your
|
|
193
|
+
* target" — and only ever on a target-only call.
|
|
194
|
+
*/
|
|
195
|
+
function isMissingUrlRejection(e) {
|
|
196
|
+
if (!(e instanceof RestError) || e.status !== 400)
|
|
197
|
+
return false;
|
|
198
|
+
const code = e.code ?? '';
|
|
199
|
+
return code.includes('rest_missing_callback_param') || code.includes('missing_url');
|
|
200
|
+
}
|
|
201
|
+
/** A-6: narrow an off-the-wire `matched_by` to the declared enum, else drop it. */
|
|
202
|
+
function knownMatchedBy(value) {
|
|
203
|
+
return TARGET_KEYS.find((key) => key === value);
|
|
204
|
+
}
|
|
205
|
+
export async function handlePagesResolve(client, { url, target }) {
|
|
206
|
+
// A-6 / F205: the exactly-one-of(url, target) contract is validated HERE
|
|
207
|
+
// with typed results — a zod `.refine` would surface as a raw -32602 and
|
|
208
|
+
// strip every hint below. Mirrors the WP/Joomla controllers' own codes so
|
|
209
|
+
// the contract reads the same whether it fired locally or upstream.
|
|
210
|
+
const resolvedTarget = normalizeTarget(target);
|
|
211
|
+
// BUG-4 (shared rule, PHP `PageQuery::malformedTargetError()`): a target bag
|
|
212
|
+
// that survived nothing is a MALFORMED ADDRESS. Checked BEFORE the
|
|
213
|
+
// exactly-one-of(url, target) pair, exactly as both controllers do — so it
|
|
214
|
+
// is never reported as `missing_url` (which claims no address was passed)
|
|
215
|
+
// and, alongside a `url`, is never silently ignored.
|
|
216
|
+
const malformed = malformedTargetError(target, resolvedTarget, {
|
|
217
|
+
context: { url: url ?? null, target: target ?? null },
|
|
218
|
+
hint: 'Each target member must be a non-empty string or number — e.g. '
|
|
219
|
+
+ 'target:{special:"home"} or target:{template_id:"page:4816"}. Or pass `url` instead.',
|
|
220
|
+
});
|
|
221
|
+
if (malformed !== null)
|
|
222
|
+
return malformed;
|
|
223
|
+
const targetKeys = targetKeysOf(resolvedTarget);
|
|
224
|
+
const hasUrl = typeof url === 'string' && url.trim() !== '';
|
|
225
|
+
const hasTarget = targetKeys.length > 0;
|
|
226
|
+
if (hasUrl && hasTarget) {
|
|
227
|
+
return tooManyTargetsError({
|
|
228
|
+
supplied: ['url', ...targetKeys.map((k) => `target.${k}`)],
|
|
229
|
+
context: { url: url ?? null, target: resolvedTarget },
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if (targetKeys.length > 1) {
|
|
233
|
+
return tooManyTargetsError({
|
|
234
|
+
supplied: targetKeys,
|
|
235
|
+
context: { target: resolvedTarget },
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
if (!hasUrl && !hasTarget) {
|
|
239
|
+
return errorResult({
|
|
240
|
+
error: new RestError({
|
|
241
|
+
status: 400,
|
|
242
|
+
code: 'yootheme_builder_mcp.pages.missing_url',
|
|
243
|
+
message: 'A page address is required: pass `url` or `target`.',
|
|
244
|
+
body: null,
|
|
245
|
+
}),
|
|
246
|
+
context: { url: null, target: {} },
|
|
247
|
+
hint: 'Pass `url` (a public page/article URL), or `target` with exactly one of ' +
|
|
248
|
+
'template_id / post_id / url / special ("home", "blog", "404", "search", ' +
|
|
249
|
+
'"category", "tag", "author", "archive") / title.',
|
|
250
|
+
});
|
|
251
|
+
}
|
|
138
252
|
try {
|
|
139
|
-
const data =
|
|
253
|
+
const data = hasTarget
|
|
254
|
+
? await resolveTargetToTemplate(client, resolvedTarget)
|
|
255
|
+
: await resolveUrlToTemplate(client, (url ?? '').trim());
|
|
256
|
+
const matchedBy = knownMatchedBy(data.matched_by);
|
|
140
257
|
return structuredResult(jsonResult(data), {
|
|
141
258
|
template_id: data.template_id,
|
|
142
259
|
// G-A2: post_id is null for a theme-template resolution.
|
|
143
260
|
post_id: data.post_id ?? null,
|
|
144
261
|
type: data.type,
|
|
262
|
+
// Forwarded ONLY when the host sent one — a synthesised label would
|
|
263
|
+
// claim a page name this build has no way to know.
|
|
264
|
+
...(typeof data.label === 'string' && data.label !== '' ? { label: data.label } : {}),
|
|
145
265
|
frontend_url: data.frontend_url ?? null,
|
|
146
266
|
// G-A2: context/resolved_via are additive. An OLDER plugin (pre-G-A2)
|
|
147
267
|
// only ever resolved per-page layouts and omits both — default to the
|
|
148
268
|
// per-page shape so a strict host still accepts the success.
|
|
149
269
|
context: data.context ?? null,
|
|
150
270
|
resolved_via: data.resolved_via ?? 'per_page',
|
|
271
|
+
// A-6: forwarded ONLY when the host sent a value this build knows.
|
|
272
|
+
// A `url` resolve and every pre-A-5 host omit it; an UNKNOWN token
|
|
273
|
+
// from a newer host is dropped rather than tripping the strict
|
|
274
|
+
// outputSchema and failing an otherwise perfect resolve.
|
|
275
|
+
...(matchedBy !== undefined ? { matched_by: matchedBy } : {}),
|
|
151
276
|
});
|
|
152
277
|
}
|
|
153
278
|
catch (e) {
|
|
279
|
+
// A-6 version-skew: an old host still hard-requires `url`, so a
|
|
280
|
+
// target-only call 400s before its controller ever sees the target.
|
|
281
|
+
// That is an "update the plugin" condition, not a bad address.
|
|
282
|
+
if (hasTarget && isMissingUrlRejection(e)) {
|
|
283
|
+
return serverTooOldError({
|
|
284
|
+
message: 'This site rejected a `target`-only resolve because it still requires `url`.',
|
|
285
|
+
context: { target: resolvedTarget },
|
|
286
|
+
});
|
|
287
|
+
}
|
|
154
288
|
return errorResult({
|
|
155
289
|
error: e,
|
|
156
|
-
context: { url },
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
290
|
+
context: { url: url ?? null, target: resolvedTarget },
|
|
291
|
+
// BUG-3 / MM-5: the target-aware hint now lives in `target.js` so
|
|
292
|
+
// `pages_audit` answers the SAME error family with the SAME steer.
|
|
293
|
+
hint: hasTarget
|
|
294
|
+
? TARGET_FAILURE_HINT
|
|
295
|
+
: 'If this 404s, the URL renders via a theme template (not a ' +
|
|
296
|
+
'per-page layout) — use yootheme_builder_pages_list to find its ' +
|
|
297
|
+
'template_id. Verify the URL is a public, singular page/article.',
|
|
160
298
|
});
|
|
161
299
|
}
|
|
162
300
|
}
|
|
@@ -334,7 +472,7 @@ function parseOutlineNodes(raw) {
|
|
|
334
472
|
return out;
|
|
335
473
|
}
|
|
336
474
|
/**
|
|
337
|
-
* B7 (
|
|
475
|
+
* B7 (customer continuation cursor) — build the full-dump TEXT leg windowed at
|
|
338
476
|
* `[offset, offset+max_chars)` and, when more remains, append a `next_cursor`.
|
|
339
477
|
*
|
|
340
478
|
* The truncated full/bindings_only TEXT leg of page_get_layout had NO
|
|
@@ -381,7 +519,7 @@ export async function handlePageGetLayout(client, { template_id, post_id, url, f
|
|
|
381
519
|
return effective.error;
|
|
382
520
|
}
|
|
383
521
|
const templateId = effective.templateId;
|
|
384
|
-
// B7 (
|
|
522
|
+
// B7 (customer continuation cursor): an opaque char-offset cursor
|
|
385
523
|
// (base64url `o:<n>`) resumes a previously-clipped full/bindings_only
|
|
386
524
|
// text leg from the offset. A present cursor makes the read a
|
|
387
525
|
// CONTINUATION — it is NEVER served from the [unchanged] dedup cache
|
|
@@ -451,7 +589,7 @@ export async function handlePageGetLayout(client, { template_id, post_id, url, f
|
|
|
451
589
|
if (flat !== true) {
|
|
452
590
|
// Default: nested passthrough — unchanged from pre-G.3, except an
|
|
453
591
|
// OPTIONAL G-B `max_chars` caps the (potentially large) JSON text
|
|
454
|
-
// leg. B7 (
|
|
592
|
+
// leg. B7 (customer continuation cursor): when the dump is clipped
|
|
455
593
|
// (by `max_chars` or resumed via `cursor`) it now also emits a
|
|
456
594
|
// `next_cursor` so an agent can page through the REST of the
|
|
457
595
|
// subtree instead of being stuck with the first slice. With no
|
|
@@ -461,7 +599,14 @@ export async function handlePageGetLayout(client, { template_id, post_id, url, f
|
|
|
461
599
|
if (max_chars !== undefined || isContinuation) {
|
|
462
600
|
return windowedFullDump(data, charOffset, max_chars);
|
|
463
601
|
}
|
|
464
|
-
|
|
602
|
+
// Reaching here means `max_chars === undefined`, so the former
|
|
603
|
+
// `applyMaxChars(jsonResult(data), max_chars)` was a no-op by its own
|
|
604
|
+
// contract (`sparse-fields.ts`: undefined → return the result
|
|
605
|
+
// unchanged). Calling it read as "this leg is capped" when it never
|
|
606
|
+
// was, and it was the one character-cut call site with no
|
|
607
|
+
// `structuredContent` beside it — see
|
|
608
|
+
// `tests/contracts/char-cut-requires-structured-leg.test.ts`.
|
|
609
|
+
return jsonResult(data);
|
|
465
610
|
}
|
|
466
611
|
// flat: true → depth-first walk on the nested layout. Apply
|
|
467
612
|
// pickFields per element when `fields[]` was passed; echo the
|
|
@@ -519,7 +664,10 @@ export async function handlePageGetLayout(client, { template_id, post_id, url, f
|
|
|
519
664
|
// single-root-node envelope — collapse identically.
|
|
520
665
|
const candidate = hasInnerLayoutField ? layoutObj.layout : dataLayout;
|
|
521
666
|
const layoutRoot = unwrapToWalkableContainer(candidate);
|
|
522
|
-
|
|
667
|
+
// LAYOUT_READ-7: pass the resolved template id so flat-mode `path`s are
|
|
668
|
+
// fully-qualified (`/templates/<id>/layout/children/0`) — matching the
|
|
669
|
+
// `path` element_list emits. `rel_path` stays the short `/children/0`.
|
|
670
|
+
const flatElements = flattenLayout(layoutRoot, templateId);
|
|
523
671
|
// Non-silent fallback (C4) + F29 calibration: if the walk yields
|
|
524
672
|
// ZERO elements AND the resolved walk container is an unrecognized
|
|
525
673
|
// NON-EMPTY shape, surface a warning so it can never pass silently.
|
|
@@ -542,7 +690,14 @@ export async function handlePageGetLayout(client, { template_id, post_id, url, f
|
|
|
542
690
|
// `projection_warning` (the shared F23 SSoT helper) so the agent can
|
|
543
691
|
// self-correct on the next call. Default-all callers (no fields[])
|
|
544
692
|
// get no audit overhead — projectionFeedback returns undefined.
|
|
545
|
-
|
|
693
|
+
// SOURCES_BINDING-29 (parallel site): an EMPTY flat result cannot
|
|
694
|
+
// observe a field vocabulary, so `projectionFeedback` over zero rows
|
|
695
|
+
// would report EVERY requested field as unknown against an empty
|
|
696
|
+
// `available_fields`. Suppress the feedback entirely on a zero-element
|
|
697
|
+
// flatten — the projection is moot when there is nothing to project.
|
|
698
|
+
const feedback = flatElements.length > 0
|
|
699
|
+
? projectionFeedback(flatElements, fields)
|
|
700
|
+
: undefined;
|
|
546
701
|
const projectionNote = projectionWarning(feedback);
|
|
547
702
|
// 1.0.1 Wave-1.7 F-COLD-3: forward the canonical pointer base
|
|
548
703
|
// through the flat-projection path too so cold agents iterating
|
|
@@ -579,7 +734,7 @@ export async function handlePageGetLayout(client, { template_id, post_id, url, f
|
|
|
579
734
|
}
|
|
580
735
|
}
|
|
581
736
|
// ─── page_get_schema ────────────────────────────────────────────────
|
|
582
|
-
export async function handlePageGetSchema(client, { template_id, post_id, url, fields, max_chars }) {
|
|
737
|
+
export async function handlePageGetSchema(client, { template_id, post_id, url, fields, max_chars, include_meta }) {
|
|
583
738
|
try {
|
|
584
739
|
// G-A: resolve the effective template_id from template_id | post_id | url.
|
|
585
740
|
const effective = await resolveEffectiveTemplateId(client, { template_id, post_id, url });
|
|
@@ -590,7 +745,7 @@ export async function handlePageGetSchema(client, { template_id, post_id, url, f
|
|
|
590
745
|
const data = await client.get(`/pages/${encodeTemplateId(templateId)}/schema`);
|
|
591
746
|
const rawNodes = Array.isArray(data.nodes) ? data.nodes : [];
|
|
592
747
|
const rows = buildSchemaRows(rawNodes.filter((x) => x !== null && typeof x === 'object'));
|
|
593
|
-
// G-B (
|
|
748
|
+
// G-B (customer 2026-06-26, P2): `fields[]` projection — DEFAULT_FIELDS_SCHEMA
|
|
594
749
|
// existed but was never wired. Mirror element_list: project both legs,
|
|
595
750
|
// echo `projected_fields`, surface the F23 unknown-field feedback.
|
|
596
751
|
const items = projectFields(rows, fields, DEFAULT_FIELDS_SCHEMA);
|
|
@@ -614,12 +769,14 @@ export async function handlePageGetSchema(client, { template_id, post_id, url, f
|
|
|
614
769
|
columns: columnsFromFields([...fields]),
|
|
615
770
|
header: (count) => `${String(count)} nodes in template "${templateId}"${unknownNote}`,
|
|
616
771
|
footer: 'Use yootheme_builder_element_get <path> for full element data.',
|
|
772
|
+
includeMeta: include_meta,
|
|
617
773
|
}, 'compact')
|
|
618
774
|
: tableResult(rows, {
|
|
619
775
|
columns: [...SCHEMA_TABLE_COLUMNS],
|
|
620
776
|
compactColumns: [...SCHEMA_COMPACT_COLUMNS],
|
|
621
777
|
header: (count) => `${String(count)} nodes in template "${templateId}"`,
|
|
622
778
|
footer: 'Use yootheme_builder_element_get <path> for full element data.',
|
|
779
|
+
includeMeta: include_meta,
|
|
623
780
|
}, 'compact');
|
|
624
781
|
return structuredResult(applyMaxChars(toolkitResult, max_chars), {
|
|
625
782
|
items,
|
|
@@ -645,10 +802,13 @@ export async function handlePageGetSchema(client, { template_id, post_id, url, f
|
|
|
645
802
|
export async function handleGetEtag(client) {
|
|
646
803
|
try {
|
|
647
804
|
const data = await client.get('/etag');
|
|
648
|
-
const toolkitResult = detailResult(
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
805
|
+
const toolkitResult = detailResult({
|
|
806
|
+
...buildEtagDetail({
|
|
807
|
+
etag: data.etag,
|
|
808
|
+
generated_at: data.generated_at,
|
|
809
|
+
}),
|
|
810
|
+
includeMeta: false,
|
|
811
|
+
});
|
|
652
812
|
return structuredResult(toolkitResult, {
|
|
653
813
|
etag: data.etag,
|
|
654
814
|
...(typeof data.generated_at === 'string'
|
|
@@ -665,52 +825,124 @@ export async function handleGetEtag(client) {
|
|
|
665
825
|
}
|
|
666
826
|
}
|
|
667
827
|
// ─── template_summary (T9 — Audit-v3 B.5 token-efficient overview) ────
|
|
828
|
+
/**
|
|
829
|
+
* Task #46 Friction-3 (2026-05-29): PHP's `json_encode([])` emits `[]`
|
|
830
|
+
* (array), not `{}` (object). The REST handler emits `counts_by_type: []`
|
|
831
|
+
* for an empty template, but the outputSchema declares
|
|
832
|
+
* `z.record(z.string(), z.number())`, which requires an object. Normalize
|
|
833
|
+
* array->object here so the schema validates and the agent sees the
|
|
834
|
+
* (valid, empty) summary. T-C5 (mega-push plan 2026-07-24-02): factored out
|
|
835
|
+
* of `handleTemplateSummary` so it applies to BOTH the single-template
|
|
836
|
+
* response AND every per-id entry of a batch response's `summaries` map.
|
|
837
|
+
*/
|
|
838
|
+
function normalizeCountsByType(data) {
|
|
839
|
+
const normalized = { ...data };
|
|
840
|
+
if (Array.isArray(normalized.counts_by_type)) {
|
|
841
|
+
normalized.counts_by_type = {};
|
|
842
|
+
}
|
|
843
|
+
return normalized;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* T-E2 + T-C5 (mega-push plan 2026-07-24-02, customer P1.5): build the shared
|
|
847
|
+
* `include_nodes`/`limit`/`cursor` query params both the single-template and
|
|
848
|
+
* batch REST calls forward verbatim to `PageQuery::summary()`'s `$options`.
|
|
849
|
+
*/
|
|
850
|
+
function summaryQueryParams(opts) {
|
|
851
|
+
const qs = new URLSearchParams();
|
|
852
|
+
if (opts.include_nodes === true)
|
|
853
|
+
qs.set('include_nodes', 'true');
|
|
854
|
+
if (typeof opts.limit === 'number')
|
|
855
|
+
qs.set('limit', String(opts.limit));
|
|
856
|
+
if (typeof opts.cursor === 'string' && opts.cursor !== '')
|
|
857
|
+
qs.set('cursor', opts.cursor);
|
|
858
|
+
return qs;
|
|
859
|
+
}
|
|
668
860
|
/**
|
|
669
861
|
* T9: a one-call structured overview of a template — element counts by
|
|
670
862
|
* type, binding count, max nesting depth, and named landmarks. Lets the
|
|
671
863
|
* agent grasp a 96-node template for ~1 kB instead of pulling the full
|
|
672
864
|
* element_list dump (20 kB+).
|
|
865
|
+
*
|
|
866
|
+
* T-E2 + T-C5 (mega-push plan 2026-07-24-02, Thomas-Entscheid C + customer
|
|
867
|
+
* P1.5): three additive changes, one coherent tool-behavior update:
|
|
868
|
+
* - `include_nodes`/`limit`/`cursor` ride through to the REST query string
|
|
869
|
+
* unconditionally opt-in (`content_nodes` no longer ships by default).
|
|
870
|
+
* - `template_id` (single) XOR `template_ids` (batch) — exactly one,
|
|
871
|
+
* validated HERE with typed results (mirrors `handlePagesResolve`'s
|
|
872
|
+
* exactly-one-of(url, target) contract) rather than a zod `.refine`,
|
|
873
|
+
* which would surface as a raw -32602 and strip every hint below.
|
|
874
|
+
* - Batch hits the LITERAL `/pages/summary?template_ids=a,b,c` route (ONE
|
|
875
|
+
* REST round-trip for N templates) and returns
|
|
876
|
+
* `{summaries: {<id>: <summary>}, not_found: [...]}`.
|
|
673
877
|
*/
|
|
674
|
-
export async function handleTemplateSummary(client, { template_id }) {
|
|
878
|
+
export async function handleTemplateSummary(client, { template_id, template_ids, include_nodes, limit, cursor }) {
|
|
879
|
+
const hasTemplateId = typeof template_id === 'string' && template_id.trim() !== '';
|
|
880
|
+
const hasTemplateIds = Array.isArray(template_ids) && template_ids.length > 0;
|
|
881
|
+
if (hasTemplateId && hasTemplateIds) {
|
|
882
|
+
return errorResult({
|
|
883
|
+
error: new RestError({
|
|
884
|
+
status: 400,
|
|
885
|
+
code: 'yootheme_builder_mcp.pages.too_many_template_ids',
|
|
886
|
+
message: 'Provide EXACTLY ONE of template_id (single) or template_ids (batch), not both.',
|
|
887
|
+
body: null,
|
|
888
|
+
}),
|
|
889
|
+
context: { template_id: template_id ?? null, template_ids: template_ids ?? null },
|
|
890
|
+
hint: 'Pass template_id for a single template, or template_ids:[...] for a batch - not both.',
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
if (!hasTemplateId && !hasTemplateIds) {
|
|
894
|
+
return errorResult({
|
|
895
|
+
error: new RestError({
|
|
896
|
+
status: 400,
|
|
897
|
+
code: 'yootheme_builder_mcp.pages.missing_template_id',
|
|
898
|
+
message: 'A template_id or template_ids is required.',
|
|
899
|
+
body: null,
|
|
900
|
+
}),
|
|
901
|
+
context: {},
|
|
902
|
+
hint: 'Pass template_id (single) or template_ids:[...] (batch). Use yootheme_builder_pages_list to discover template IDs.',
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
const qs = summaryQueryParams({ include_nodes, limit, cursor });
|
|
675
906
|
try {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
// for any empty / freshly-created template. Normalize array→object
|
|
691
|
-
// here so the schema validates and the agent sees the (valid,
|
|
692
|
-
// empty) summary. Same defensive shape applies to any future
|
|
693
|
-
// map-typed REST field that might come back empty.
|
|
694
|
-
const normalized = {
|
|
695
|
-
...data,
|
|
696
|
-
};
|
|
697
|
-
if (Array.isArray(normalized.counts_by_type)) {
|
|
698
|
-
normalized.counts_by_type = {};
|
|
907
|
+
if (hasTemplateIds) {
|
|
908
|
+
qs.set('template_ids', template_ids.join(','));
|
|
909
|
+
const data = await client.get(`/pages/summary?${qs.toString()}`);
|
|
910
|
+
const normalizedSummaries = {};
|
|
911
|
+
for (const [id, summary] of Object.entries(data.summaries ?? {})) {
|
|
912
|
+
normalizedSummaries[id] = normalizeCountsByType(summary);
|
|
913
|
+
}
|
|
914
|
+
const normalized = {
|
|
915
|
+
summaries: normalizedSummaries,
|
|
916
|
+
not_found: data.not_found ?? [],
|
|
917
|
+
...(typeof data.etag === 'string' ? { etag: data.etag } : {}),
|
|
918
|
+
};
|
|
919
|
+
const toolkitResult = jsonResult(normalized);
|
|
920
|
+
return structuredResult({ content: toolkitResult.content }, normalized);
|
|
699
921
|
}
|
|
922
|
+
const id = (template_id ?? '').trim();
|
|
923
|
+
const query = qs.toString();
|
|
924
|
+
const data = await client.get(`/pages/${encodeTemplateId(id)}/summary${query !== '' ? `?${query}` : ''}`);
|
|
925
|
+
// F-AUDIT-2 (2026-05-26): template_summary declares
|
|
926
|
+
// TEMPLATE_SUMMARY_OUTPUT_SCHEMA. The SDK requires
|
|
927
|
+
// `structuredContent` when an outputSchema is present, otherwise it
|
|
928
|
+
// raises `-32602 Output validation error`. Wrap the raw body with
|
|
929
|
+
// `structuredResult` so the text leg (LLM-readable JSON) stays
|
|
930
|
+
// unchanged AND the structuredContent leg validates.
|
|
931
|
+
const normalized = normalizeCountsByType(data);
|
|
700
932
|
const toolkitResult = jsonResult(normalized);
|
|
701
933
|
return structuredResult({ content: toolkitResult.content }, normalized);
|
|
702
934
|
}
|
|
703
935
|
catch (e) {
|
|
704
936
|
return errorResult({
|
|
705
937
|
error: e,
|
|
706
|
-
context: { template_id },
|
|
707
|
-
hint: 'Verify the template_id
|
|
938
|
+
context: { template_id: template_id ?? null, template_ids: template_ids ?? null },
|
|
939
|
+
hint: 'Verify the template_id(s) exist via yootheme_builder_pages_list.',
|
|
708
940
|
});
|
|
709
941
|
}
|
|
710
942
|
}
|
|
711
943
|
// ─── render_preview (F5 + N5 — authenticated post-write render check) ──
|
|
712
944
|
/**
|
|
713
|
-
* F5 + N5 (
|
|
945
|
+
* F5 + N5 (customer HANDOFF-4/5): the authenticated post-write render
|
|
714
946
|
* HEALTHCHECK. Resolves the EFFECTIVE template_id from `template_id` OR `url`
|
|
715
947
|
* (the SAME `resolveEffectiveTemplateId` precedence page_get_layout uses — a
|
|
716
948
|
* pasted public URL is reverse-resolved via GET /pages/resolve first), then
|
|
@@ -723,15 +955,20 @@ export async function handleTemplateSummary(client, { template_id }) {
|
|
|
723
955
|
* items, or that threw inside the Builder pipeline (the swallowed YOOtheme
|
|
724
956
|
* render error surfaced as `error`), is a SUCCESS — not a tool error.
|
|
725
957
|
*/
|
|
726
|
-
export async function handleRenderPreview(client, { template_id, url, expect }) {
|
|
958
|
+
export async function handleRenderPreview(client, { template_id, post_id, url, expect }) {
|
|
727
959
|
try {
|
|
728
|
-
//
|
|
729
|
-
|
|
960
|
+
// WAVE-1 follow-up (a): render_preview now accepts `post_id` too, the SAME
|
|
961
|
+
// three-mode addressing page_get_layout uses — template_id wins, else a
|
|
962
|
+
// bare numeric post/article id, else a `url` reverse-resolved via
|
|
963
|
+
// /pages/resolve. (Cross-file: the RENDER_PREVIEW input schema in
|
|
964
|
+
// pages/builders.ts must also declare `post_id` + the native catalog must
|
|
965
|
+
// carry it — flagged for the coordinator regen.)
|
|
966
|
+
const effective = await resolveEffectiveTemplateId(client, { template_id, post_id, url });
|
|
730
967
|
if ('error' in effective) {
|
|
731
968
|
return effective.error;
|
|
732
969
|
}
|
|
733
970
|
const templateId = effective.templateId;
|
|
734
|
-
//
|
|
971
|
+
// customer HANDOFF-6 #4: an `expect` substring is passed to the render
|
|
735
972
|
// endpoint as a query param (the server greps the rendered HTML and adds
|
|
736
973
|
// {matched, snippet}). A blank value is treated as "no check".
|
|
737
974
|
const wantMatch = typeof expect === 'string' && expect !== '';
|
|
@@ -759,7 +996,7 @@ export async function handleRenderPreview(client, { template_id, url, expect })
|
|
|
759
996
|
}
|
|
760
997
|
: {}),
|
|
761
998
|
};
|
|
762
|
-
return structuredResult(detailResult(buildRenderPreviewDetail(payload)), {
|
|
999
|
+
return structuredResult(detailResult({ ...buildRenderPreviewDetail(payload), includeMeta: false }), {
|
|
763
1000
|
rendered: payload.rendered,
|
|
764
1001
|
item_count: payload.item_count,
|
|
765
1002
|
empty: payload.empty,
|
|
@@ -773,9 +1010,51 @@ export async function handleRenderPreview(client, { template_id, url, expect })
|
|
|
773
1010
|
catch (e) {
|
|
774
1011
|
return errorResult({
|
|
775
1012
|
error: e,
|
|
776
|
-
context: { template_id, url, expect },
|
|
1013
|
+
context: { template_id, post_id, url, expect },
|
|
777
1014
|
hint: 'Verify the template_id exists via yootheme_builder_pages_list ' +
|
|
778
|
-
'(or that the url resolves). Run yootheme_builder_diagnose to check auth.',
|
|
1015
|
+
'(or that post_id / the url resolves). Run yootheme_builder_diagnose to check auth.',
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
// ─── heading_outline (T-A5 — a real, doc-order H1-H6 outline) ──────────
|
|
1020
|
+
/**
|
|
1021
|
+
* T-A5 (heading_outline tool, mega-push plan 2026-07-24-02): a real,
|
|
1022
|
+
* doc-order H1-H6 outline of ONE template, each heading attributed to its
|
|
1023
|
+
* source element (`element_type`/`path`), plus the alt-status of every
|
|
1024
|
+
* image (including images living inside html/text/code raw markup).
|
|
1025
|
+
* Read-only STRUCTURE inspection — no findings/severity (that is
|
|
1026
|
+
* pages_audit's job).
|
|
1027
|
+
*
|
|
1028
|
+
* `template_id`-only addressing (no post_id/url sugar) — resolve via
|
|
1029
|
+
* yootheme_builder_pages_resolve first, like the other per-page tools that
|
|
1030
|
+
* need a bare template_id. The REST body already carries the HEADING_OUTLINE_
|
|
1031
|
+
* OUTPUT_SCHEMA shape, so it is forwarded through structuredResult verbatim
|
|
1032
|
+
* (a thin dispatch onto PageAuditService::headingOutline()).
|
|
1033
|
+
*/
|
|
1034
|
+
export async function handleHeadingOutline(client, { template_id, mode }) {
|
|
1035
|
+
try {
|
|
1036
|
+
const qs = new URLSearchParams();
|
|
1037
|
+
if (mode !== undefined)
|
|
1038
|
+
qs.set('mode', mode);
|
|
1039
|
+
const query = qs.toString();
|
|
1040
|
+
const data = await client.get(`/pages/${encodeTemplateId(template_id)}/heading-outline${query !== '' ? `?${query}` : ''}`);
|
|
1041
|
+
return structuredResult(jsonResult(data), data);
|
|
1042
|
+
}
|
|
1043
|
+
catch (e) {
|
|
1044
|
+
if (e instanceof RestError && e.status === 404) {
|
|
1045
|
+
return errorResult({
|
|
1046
|
+
error: e,
|
|
1047
|
+
context: { template_id, mode: mode ?? null },
|
|
1048
|
+
hint: 'GET /pages/{id}/heading-outline was not found on this site: the ' +
|
|
1049
|
+
'yt-builder-mcp host plugin predates heading_outline. Update the plugin on ' +
|
|
1050
|
+
'this site, or verify the template_id via yootheme_builder_pages_list.',
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
return errorResult({
|
|
1054
|
+
error: e,
|
|
1055
|
+
context: { template_id, mode: mode ?? null },
|
|
1056
|
+
hint: 'Verify the template_id exists via yootheme_builder_pages_list. Run ' +
|
|
1057
|
+
'yootheme_builder_diagnose to check auth.',
|
|
779
1058
|
});
|
|
780
1059
|
}
|
|
781
1060
|
}
|