@gravitykit/block-mcp 2.0.0-beta → 2.1.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 +34 -7
- package/dist/index.cjs +2556 -967
- package/package.json +12 -4
- package/src/__tests__/coerce.test.ts +57 -0
- package/src/__tests__/fixtures/error-envelopes.ts +19 -7
- package/src/__tests__/integration/dual-storage.test.ts +22 -26
- package/src/__tests__/integration/error-envelopes.test.ts +2 -2
- package/src/__tests__/integration/posts-terms-media.test.ts +271 -0
- package/src/__tests__/integration/read-discovery.test.ts +313 -0
- package/src/__tests__/integration/upload-roundtrip.test.ts +54 -0
- package/src/__tests__/integration/write-surface.test.ts +603 -0
- package/src/__tests__/integration/yoast-bridge.test.ts +39 -0
- package/src/__tests__/tools/discovery/list_patterns.test.ts +25 -6
- package/src/__tests__/tools/mutate/edit_block_tree.test.ts +53 -0
- package/src/__tests__/tools/patterns/insert_pattern.test.ts +16 -0
- package/src/__tests__/tools/posts/update_post.test.ts +14 -0
- package/src/__tests__/tools/read/get_block.test.ts +35 -0
- package/src/__tests__/tools/read/get_page_blocks.test.ts +18 -0
- package/src/__tests__/tools/read/pagination.test.ts +65 -0
- package/src/__tests__/tools/write/delete_block.test.ts +18 -0
- package/src/__tests__/tools/write/insert_blocks.test.ts +21 -0
- package/src/__tests__/tools/write/replace_block_range.test.ts +71 -0
- package/src/__tests__/tools/write/rewrite_post_blocks.test.ts +39 -0
- package/src/__tests__/tools/write/update_block.test.ts +16 -0
- package/src/__tests__/tools/write/update_blocks.test.ts +21 -0
- package/src/__tests__/tools/yoast/yoast_get_seo.test.ts +11 -3
- package/src/__tests__/tools/yoast/yoast_update_seo.test.ts +16 -0
- package/src/__tests__/unit/client/ref-endpoints.test.ts +3 -6
- package/src/__tests__/unit/enrichers/cbp-enricher.test.ts +21 -0
- package/src/__tests__/unit/error-translator/translate-wp-error.test.ts +101 -30
- package/src/__tests__/unit/instructions.test.ts +3 -3
- package/src/__tests__/unit/preferences/enrich-pattern-list.test.ts +26 -10
- package/src/__tests__/unit/rest-url.test.ts +23 -0
- package/src/agent-guide.ts +85 -0
- package/src/client.ts +104 -40
- package/src/coerce.ts +41 -0
- package/src/config.ts +96 -0
- package/src/connect.ts +123 -38
- package/src/enrichers.ts +6 -2
- package/src/error-translator.ts +63 -11
- package/src/index.ts +49 -79
- package/src/instructions.ts +3 -3
- package/src/preferences.ts +56 -43
- package/src/rest-url.ts +18 -0
- package/src/tools/discovery.ts +10 -14
- package/src/tools/mutate.ts +21 -20
- package/src/tools/patterns.ts +3 -2
- package/src/tools/posts.ts +26 -26
- package/src/tools/read.ts +23 -6
- package/src/tools/write.ts +37 -36
- package/src/tools/yoast.ts +30 -31
- package/src/types.ts +45 -31
- package/src/validate-args.ts +109 -0
package/src/tools/write.ts
CHANGED
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { WordPressBlockClient } from '../client.js';
|
|
12
|
-
import { formatPreferenceWarning } from '../preferences.js';
|
|
12
|
+
import { formatPreferenceWarning, withFormattedWarnings } from '../preferences.js';
|
|
13
13
|
import { enrichBlock, enrichBlocks, type BlockDef } from '../enrichers.js';
|
|
14
|
+
import { coercePostId } from '../coerce.js';
|
|
14
15
|
|
|
15
16
|
/** Shape shared by every block-input arg in this module. */
|
|
16
17
|
export const BLOCK_INPUT_SCHEMA = {
|
|
@@ -75,7 +76,9 @@ export const WRITE_TOOLS = [
|
|
|
75
76
|
// idempotentHint is false: every call creates a new revision, and
|
|
76
77
|
// revision history is observable to other readers. Same-input/same-state
|
|
77
78
|
// is true at the block level but not at the post level.
|
|
78
|
-
|
|
79
|
+
// destructiveHint is true: attributes/innerHTML overwrite the block's
|
|
80
|
+
// existing content rather than merging additively.
|
|
81
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, title: 'Update one block' },
|
|
79
82
|
outputSchema: {
|
|
80
83
|
type: 'object',
|
|
81
84
|
properties: {
|
|
@@ -123,7 +126,8 @@ export const WRITE_TOOLS = [
|
|
|
123
126
|
name: 'update_blocks',
|
|
124
127
|
description:
|
|
125
128
|
'Update N independent blocks atomically in ONE revision. Each item targets one block by `ref` (recommended) or `flat_index`, with `attributes` and/or `innerHTML`. Validation is all-or-nothing: any stale ref / out-of-range index / dual-storage rejection / duplicate target aborts the batch with itemized errors — no partial writes hit disk. Max 50 items per call. Counts as ONE write against the per-post rate limit. Use this instead of looping update_block when fixing multiple blocks on the same post — keeps revision history clean. Pass `verbose: true` to include `saved.inner_html` + `saved.attributes` per result for per-item verification without a re-read.',
|
|
126
|
-
|
|
129
|
+
// destructiveHint is true: same overwrite semantics as update_block, applied per item.
|
|
130
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, title: 'Batch-update blocks' },
|
|
127
131
|
outputSchema: {
|
|
128
132
|
type: 'object',
|
|
129
133
|
properties: {
|
|
@@ -192,7 +196,13 @@ export const WRITE_TOOLS = [
|
|
|
192
196
|
{
|
|
193
197
|
name: 'insert_blocks',
|
|
194
198
|
description:
|
|
195
|
-
'Insert
|
|
199
|
+
'Insert top-level blocks into a post.\n\n' +
|
|
200
|
+
'POSITIONING — use exactly ONE anchor (any other key, e.g. `after`, `before`, `position`, is rejected):\n' +
|
|
201
|
+
'- `before_ref` / `after_ref` — gk_ref from get_page_blocks. Recommended: refs survive sibling shifts.\n' +
|
|
202
|
+
'- `before_top_level` / `after_top_level` — top_level_counter position.\n' +
|
|
203
|
+
'- Prepend at the very top: `before_top_level: 0`. Append at the end: omit all anchors.\n\n' +
|
|
204
|
+
'CONTENT — legacy-tier blocks are rejected per the site policy. Blocks with HTML-sourced attributes (e.g. core/paragraph `content`, core/image `url`) must include matching `innerHTML`; attribute-only inserts fail with `inner_html_required`.\n\n' +
|
|
205
|
+
'VERIFY — the response\'s inserted[] returns `ref`, `path`, and `top_level_counter`: confirm the block landed where you intended before moving on. The refs let you chain edit_block_tree without re-reading.',
|
|
196
206
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true, title: 'Insert blocks' },
|
|
197
207
|
outputSchema: INSERTED_REFS_SCHEMA,
|
|
198
208
|
inputSchema: {
|
|
@@ -201,7 +211,7 @@ export const WRITE_TOOLS = [
|
|
|
201
211
|
post_id: { type: 'number', description: 'Post ID.' },
|
|
202
212
|
after_top_level: {
|
|
203
213
|
type: ['number', 'string'],
|
|
204
|
-
description: 'top_level_counter to insert AFTER
|
|
214
|
+
description: 'top_level_counter to insert AFTER (omit or -1 = append). To prepend at the very top, prefer `before_top_level: 0`.',
|
|
205
215
|
},
|
|
206
216
|
before_top_level: {
|
|
207
217
|
type: 'number',
|
|
@@ -217,7 +227,7 @@ export const WRITE_TOOLS = [
|
|
|
217
227
|
},
|
|
218
228
|
blocks: {
|
|
219
229
|
type: 'array',
|
|
220
|
-
description: 'Blocks to insert.',
|
|
230
|
+
description: 'Blocks to insert. Each def nests recursively via `innerBlocks` — build a whole container (group/columns/callout) with its children in this one call; give the container its empty wrapper `innerHTML` (e.g. \'<div class="wp-block-group"></div>\').',
|
|
221
231
|
items: BLOCK_INPUT_SCHEMA,
|
|
222
232
|
},
|
|
223
233
|
},
|
|
@@ -260,7 +270,7 @@ export const WRITE_TOOLS = [
|
|
|
260
270
|
post_id: { type: 'number', description: 'Post ID.' },
|
|
261
271
|
start: { type: 'number', description: 'top_level_counter of first block to replace.' },
|
|
262
272
|
count: { type: 'number', description: 'How many top-level blocks to remove. Pass 0 to insert without removing.' },
|
|
263
|
-
blocks: { type: 'array', description: 'Replacement blocks. May be empty (pure delete) or any length.', items: BLOCK_INPUT_SCHEMA },
|
|
273
|
+
blocks: { type: 'array', description: 'Replacement blocks. May be empty (pure delete) or any length. Each def nests recursively via `innerBlocks` for containers (group/columns/callout) — give the container its empty wrapper `innerHTML`.', items: BLOCK_INPUT_SCHEMA },
|
|
264
274
|
},
|
|
265
275
|
required: ['post_id', 'start', 'count', 'blocks'],
|
|
266
276
|
},
|
|
@@ -297,7 +307,7 @@ export const WRITE_TOOLS = [
|
|
|
297
307
|
type: 'object' as const,
|
|
298
308
|
properties: {
|
|
299
309
|
post_id: { type: 'number', description: 'Post ID.' },
|
|
300
|
-
blocks: { type: 'array', description: 'Complete blocks array (replaces all).', items: BLOCK_INPUT_SCHEMA },
|
|
310
|
+
blocks: { type: 'array', description: 'Complete blocks array (replaces all). Each def nests recursively via `innerBlocks` for containers (group/columns/callout) — give the container its empty wrapper `innerHTML`.', items: BLOCK_INPUT_SCHEMA },
|
|
301
311
|
},
|
|
302
312
|
required: ['post_id', 'blocks'],
|
|
303
313
|
},
|
|
@@ -327,14 +337,14 @@ export async function handleWriteTool(
|
|
|
327
337
|
): Promise<unknown> {
|
|
328
338
|
switch (toolName) {
|
|
329
339
|
case 'update_block': {
|
|
330
|
-
const postId = args.post_id
|
|
340
|
+
const postId = coercePostId(args.post_id, 'update_block');
|
|
331
341
|
const flatIndex = args.flat_index as number | undefined;
|
|
332
342
|
const ref = args.ref as string | undefined;
|
|
333
343
|
const blockName = args.block_name as string | undefined;
|
|
334
344
|
let attributes = args.attributes as Record<string, unknown> | undefined;
|
|
335
345
|
let innerHTML = args.innerHTML as string | undefined;
|
|
336
346
|
|
|
337
|
-
if (postId === undefined
|
|
347
|
+
if (postId === undefined) throw new Error('post_id is required');
|
|
338
348
|
const hasIndex = typeof flatIndex === 'number' && Number.isFinite(flatIndex) && flatIndex >= 0;
|
|
339
349
|
const hasRef = typeof ref === 'string' && ref.length > 0;
|
|
340
350
|
if (!hasIndex && !hasRef) {
|
|
@@ -363,7 +373,7 @@ export async function handleWriteTool(
|
|
|
363
373
|
}
|
|
364
374
|
|
|
365
375
|
case 'update_blocks': {
|
|
366
|
-
const postId = args.post_id
|
|
376
|
+
const postId = coercePostId(args.post_id, 'update_blocks');
|
|
367
377
|
const updates = args.updates as Array<{
|
|
368
378
|
ref?: string;
|
|
369
379
|
flat_index?: number;
|
|
@@ -372,7 +382,7 @@ export async function handleWriteTool(
|
|
|
372
382
|
innerHTML?: string;
|
|
373
383
|
}> | undefined;
|
|
374
384
|
|
|
375
|
-
if (postId === undefined
|
|
385
|
+
if (postId === undefined) throw new Error('post_id is required');
|
|
376
386
|
if (!Array.isArray(updates) || updates.length === 0) {
|
|
377
387
|
throw new Error('updates must be a non-empty array');
|
|
378
388
|
}
|
|
@@ -434,7 +444,7 @@ export async function handleWriteTool(
|
|
|
434
444
|
}
|
|
435
445
|
|
|
436
446
|
case 'insert_blocks': {
|
|
437
|
-
const postId = args.post_id
|
|
447
|
+
const postId = coercePostId(args.post_id, 'insert_blocks');
|
|
438
448
|
const after = args.after_top_level as number | 'start' | undefined;
|
|
439
449
|
const before = args.before_top_level as number | undefined;
|
|
440
450
|
const afterRef = args.after_ref as string | undefined;
|
|
@@ -445,7 +455,7 @@ export async function handleWriteTool(
|
|
|
445
455
|
innerHTML?: string;
|
|
446
456
|
}>;
|
|
447
457
|
|
|
448
|
-
if (postId === undefined
|
|
458
|
+
if (postId === undefined) throw new Error('post_id is required');
|
|
449
459
|
if (!blocks || blocks.length === 0) throw new Error('At least one block is required in the blocks array');
|
|
450
460
|
|
|
451
461
|
const result = await client.insertBlocks(postId, {
|
|
@@ -455,19 +465,16 @@ export async function handleWriteTool(
|
|
|
455
465
|
...(beforeRef ? { before_ref: beforeRef } : {}),
|
|
456
466
|
blocks: await enrichBlocks(blocks as BlockDef[]),
|
|
457
467
|
});
|
|
458
|
-
|
|
459
|
-
return { ...result, formatted_warnings: result.warnings.map(formatPreferenceWarning) };
|
|
460
|
-
}
|
|
461
|
-
return result;
|
|
468
|
+
return withFormattedWarnings(result, formatPreferenceWarning);
|
|
462
469
|
}
|
|
463
470
|
|
|
464
471
|
case 'delete_block': {
|
|
465
|
-
const postId = args.post_id
|
|
472
|
+
const postId = coercePostId(args.post_id, 'delete_block');
|
|
466
473
|
const topLevelCounter = args.top_level_counter as number | undefined;
|
|
467
474
|
const ref = args.ref as string | undefined;
|
|
468
475
|
const count = args.count as number | undefined;
|
|
469
476
|
|
|
470
|
-
if (postId === undefined
|
|
477
|
+
if (postId === undefined) throw new Error('post_id is required');
|
|
471
478
|
const hasCounter = typeof topLevelCounter === 'number' && Number.isFinite(topLevelCounter) && topLevelCounter >= 0;
|
|
472
479
|
const hasRef = typeof ref === 'string' && ref.length > 0;
|
|
473
480
|
if (!hasCounter && !hasRef) {
|
|
@@ -489,7 +496,7 @@ export async function handleWriteTool(
|
|
|
489
496
|
}
|
|
490
497
|
|
|
491
498
|
case 'replace_block_range': {
|
|
492
|
-
const postId = args.post_id
|
|
499
|
+
const postId = coercePostId(args.post_id, 'replace_block_range');
|
|
493
500
|
const start = args.start as number;
|
|
494
501
|
const count = args.count as number;
|
|
495
502
|
const blocks = args.blocks as Array<{
|
|
@@ -498,40 +505,34 @@ export async function handleWriteTool(
|
|
|
498
505
|
innerHTML?: string;
|
|
499
506
|
}>;
|
|
500
507
|
|
|
501
|
-
if (postId === undefined
|
|
502
|
-
if (
|
|
503
|
-
if (
|
|
508
|
+
if (postId === undefined) throw new Error('post_id is required');
|
|
509
|
+
if (!Number.isSafeInteger(start) || start < 0) throw new Error('start must be a non-negative integer');
|
|
510
|
+
if (!Number.isSafeInteger(count) || count < 0) throw new Error('count must be a non-negative integer');
|
|
504
511
|
if (!Array.isArray(blocks)) throw new Error('blocks must be an array (may be empty for a pure delete)');
|
|
505
512
|
|
|
506
513
|
const result = await client.replaceBlocksRange(postId, { start, count, blocks: await enrichBlocks(blocks as BlockDef[]) });
|
|
507
|
-
|
|
508
|
-
return { ...result, formatted_warnings: result.warnings.map(formatPreferenceWarning) };
|
|
509
|
-
}
|
|
510
|
-
return result;
|
|
514
|
+
return withFormattedWarnings(result, formatPreferenceWarning);
|
|
511
515
|
}
|
|
512
516
|
|
|
513
517
|
case 'rewrite_post_blocks': {
|
|
514
|
-
const postId = args.post_id
|
|
518
|
+
const postId = coercePostId(args.post_id, 'rewrite_post_blocks');
|
|
515
519
|
const blocks = args.blocks as Array<{
|
|
516
520
|
name: string;
|
|
517
521
|
attributes?: Record<string, unknown>;
|
|
518
522
|
innerHTML?: string;
|
|
519
523
|
}>;
|
|
520
524
|
|
|
521
|
-
if (postId === undefined
|
|
525
|
+
if (postId === undefined) throw new Error('post_id is required');
|
|
522
526
|
if (!blocks || blocks.length === 0) throw new Error('At least one block is required for a full page rewrite');
|
|
523
527
|
|
|
524
528
|
const result = await client.replaceAllBlocks(postId, await enrichBlocks(blocks as BlockDef[]));
|
|
525
|
-
|
|
526
|
-
return { ...result, formatted_warnings: result.warnings.map(formatPreferenceWarning) };
|
|
527
|
-
}
|
|
528
|
-
return result;
|
|
529
|
+
return withFormattedWarnings(result, formatPreferenceWarning);
|
|
529
530
|
}
|
|
530
531
|
|
|
531
532
|
case 'revert_to_revision': {
|
|
532
|
-
const postId = args.post_id
|
|
533
|
+
const postId = coercePostId(args.post_id, 'revert_to_revision');
|
|
533
534
|
const revisionId = args.revision_id as number;
|
|
534
|
-
if (postId === undefined
|
|
535
|
+
if (postId === undefined) throw new Error('post_id is required');
|
|
535
536
|
if (revisionId === undefined || revisionId === null) throw new Error('revision_id is required');
|
|
536
537
|
return await client.revertToRevision(postId, revisionId);
|
|
537
538
|
}
|
package/src/tools/yoast.ts
CHANGED
|
@@ -11,27 +11,24 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { WordPressBlockClient } from '../client.js';
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
import { coercePostId } from '../coerce.js';
|
|
15
|
+
import {
|
|
16
|
+
YOAST_SCHEMA_PAGE_TYPES,
|
|
17
|
+
YOAST_SCHEMA_ARTICLE_TYPES,
|
|
18
|
+
YOAST_ROBOTS_ADVANCED,
|
|
19
|
+
type YoastSchemaPageType,
|
|
20
|
+
type YoastSchemaArticleType,
|
|
21
|
+
type YoastRobotsAdvanced,
|
|
22
|
+
type YoastUpdateRequest,
|
|
23
|
+
type YoastBulkUpdateItem,
|
|
20
24
|
} from '../types.js';
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const SCHEMA_ARTICLE_TYPES: YoastSchemaArticleType[] = [
|
|
29
|
-
'Article', 'BlogPosting', 'SocialMediaPosting', 'NewsArticle',
|
|
30
|
-
'AdvertiserContentArticle', 'SatiricalArticle', 'ScholarlyArticle',
|
|
31
|
-
'TechArticle', 'Report', 'None',
|
|
32
|
-
];
|
|
33
|
-
|
|
34
|
-
const ROBOTS_ADVANCED: YoastRobotsAdvanced[] = ['noimageindex', 'noarchive', 'nosnippet'];
|
|
26
|
+
// Local aliases: the enum values themselves live in types.ts (single source
|
|
27
|
+
// shared with the TS union types); these names keep the rest of this file
|
|
28
|
+
// unchanged.
|
|
29
|
+
const SCHEMA_PAGE_TYPES = YOAST_SCHEMA_PAGE_TYPES;
|
|
30
|
+
const SCHEMA_ARTICLE_TYPES = YOAST_SCHEMA_ARTICLE_TYPES;
|
|
31
|
+
const ROBOTS_ADVANCED = YOAST_ROBOTS_ADVANCED;
|
|
35
32
|
|
|
36
33
|
/** Field-level schema reused by yoast_update_seo and yoast_bulk_update_seo. */
|
|
37
34
|
const YOAST_FIELD_PROPERTIES = {
|
|
@@ -40,8 +37,8 @@ const YOAST_FIELD_PROPERTIES = {
|
|
|
40
37
|
canonical: { type: 'string', description: 'Canonical URL override.' },
|
|
41
38
|
focus_keyword: { type: 'string', description: 'Focus keyphrase.' },
|
|
42
39
|
noindex: {
|
|
43
|
-
type:
|
|
44
|
-
description: '
|
|
40
|
+
type: 'boolean',
|
|
41
|
+
description: 'true=noindex, false=explicit index. Pass null to reset to the post-type default; omit to leave the current value unchanged. (Advertised as a single boolean type because some AI clients — e.g. Google Gemini — reject a "null" member in a type array; the handler still accepts an explicit null.)',
|
|
45
42
|
},
|
|
46
43
|
nofollow: { type: 'boolean', description: 'true=nofollow, false=follow.' },
|
|
47
44
|
robots_advanced: {
|
|
@@ -132,17 +129,17 @@ export async function handleYoastTool(
|
|
|
132
129
|
): Promise<unknown> {
|
|
133
130
|
switch (toolName) {
|
|
134
131
|
case 'yoast_get_seo': {
|
|
135
|
-
const postId = args.post_id;
|
|
136
|
-
if (
|
|
137
|
-
throw new Error('yoast_get_seo: "post_id"
|
|
132
|
+
const postId = coercePostId(args.post_id, 'yoast_get_seo');
|
|
133
|
+
if (postId === undefined) {
|
|
134
|
+
throw new Error('yoast_get_seo: "post_id" is required');
|
|
138
135
|
}
|
|
139
136
|
return client.getYoastSEO(postId);
|
|
140
137
|
}
|
|
141
138
|
|
|
142
139
|
case 'yoast_update_seo': {
|
|
143
|
-
const postId = args.post_id;
|
|
144
|
-
if (
|
|
145
|
-
throw new Error('yoast_update_seo: "post_id"
|
|
140
|
+
const postId = coercePostId(args.post_id, 'yoast_update_seo');
|
|
141
|
+
if (postId === undefined) {
|
|
142
|
+
throw new Error('yoast_update_seo: "post_id" is required');
|
|
146
143
|
}
|
|
147
144
|
const { post_id: _omit, ...rest } = args;
|
|
148
145
|
void _omit;
|
|
@@ -163,11 +160,13 @@ export async function handleYoastTool(
|
|
|
163
160
|
throw new Error('yoast_bulk_update_seo: each item in `posts` must be an object');
|
|
164
161
|
}
|
|
165
162
|
const obj = raw as Record<string, unknown>;
|
|
166
|
-
|
|
167
|
-
|
|
163
|
+
const id = coercePostId(obj.post_id, 'yoast_bulk_update_seo');
|
|
164
|
+
if (id === undefined) {
|
|
165
|
+
throw new Error('yoast_bulk_update_seo: each item requires `post_id`');
|
|
168
166
|
}
|
|
169
|
-
const { post_id:
|
|
170
|
-
|
|
167
|
+
const { post_id: _omit, ...rest } = obj;
|
|
168
|
+
void _omit;
|
|
169
|
+
items.push({ post_id: id, ...narrowYoastFields(rest) });
|
|
171
170
|
}
|
|
172
171
|
return client.bulkUpdateYoastSEO(items);
|
|
173
172
|
}
|
package/src/types.ts
CHANGED
|
@@ -83,8 +83,8 @@ export interface BlockType {
|
|
|
83
83
|
export interface PatternPreference {
|
|
84
84
|
/** Computed preference score */
|
|
85
85
|
score: number;
|
|
86
|
-
/** Human-readable tier */
|
|
87
|
-
tier: '
|
|
86
|
+
/** Human-readable tier (matches the server: preferred/acceptable/avoid/legacy) */
|
|
87
|
+
tier: 'preferred' | 'acceptable' | 'avoid' | 'legacy';
|
|
88
88
|
/** Reasons contributing to the score */
|
|
89
89
|
reasons: string[];
|
|
90
90
|
}
|
|
@@ -275,8 +275,12 @@ export interface BlockWriteResponse {
|
|
|
275
275
|
/** Response from block delete (DELETE) operations. */
|
|
276
276
|
export interface BlockDeleteResponse {
|
|
277
277
|
success: boolean;
|
|
278
|
+
/** Flat index of the first removed block */
|
|
279
|
+
deleted_index: number;
|
|
278
280
|
/** Number of blocks removed */
|
|
279
|
-
|
|
281
|
+
deleted_count: number;
|
|
282
|
+
/** Non-fatal warnings emitted by the delete */
|
|
283
|
+
warnings: unknown[];
|
|
280
284
|
/** WordPress revision ID of the pre-edit snapshot */
|
|
281
285
|
before_revision_id: number;
|
|
282
286
|
/** WordPress revision ID of the post-edit state */
|
|
@@ -566,16 +570,8 @@ export interface MutationRequest {
|
|
|
566
570
|
ref?: string;
|
|
567
571
|
attributes?: Record<string, unknown>;
|
|
568
572
|
innerHTML?: string;
|
|
569
|
-
block
|
|
570
|
-
|
|
571
|
-
attributes?: Record<string, unknown>;
|
|
572
|
-
innerHTML?: string;
|
|
573
|
-
innerBlocks?: Array<{
|
|
574
|
-
name: string;
|
|
575
|
-
attributes?: Record<string, unknown>;
|
|
576
|
-
innerHTML?: string;
|
|
577
|
-
}>;
|
|
578
|
-
};
|
|
573
|
+
/** replace-block / insert-child payload. Nests recursively via innerBlocks. */
|
|
574
|
+
block?: BlockInput;
|
|
579
575
|
wrapper?: {
|
|
580
576
|
name?: string;
|
|
581
577
|
attributes?: Record<string, unknown>;
|
|
@@ -584,12 +580,10 @@ export interface MutationRequest {
|
|
|
584
580
|
destination?: number[];
|
|
585
581
|
/** Alternative to `destination` — resolve from a ref instead of a path. */
|
|
586
582
|
destination_ref?: string;
|
|
587
|
-
/** Alias for destination — path of block to insert BEFORE (pre-move indexing). */
|
|
588
|
-
before?: number[];
|
|
589
|
-
/** Alternative to `before` — resolve from a ref instead of a path. */
|
|
590
|
-
before_ref?: string;
|
|
591
583
|
/** Number of consecutive blocks to move/operate on. Default: 1. */
|
|
592
584
|
count?: number;
|
|
585
|
+
/** Preview the mutation without writing. */
|
|
586
|
+
dry_run?: boolean;
|
|
593
587
|
}
|
|
594
588
|
|
|
595
589
|
/** Warning about static block markup staleness. */
|
|
@@ -622,7 +616,11 @@ export interface MutationResponse {
|
|
|
622
616
|
// v1.2 — Docs Lifecycle: Posts
|
|
623
617
|
// ============================================
|
|
624
618
|
|
|
625
|
-
/**
|
|
619
|
+
/**
|
|
620
|
+
* A block in structured form — the def shape accepted by every write that
|
|
621
|
+
* creates blocks (create_post `blocks`, insert_blocks, replace ranges/all,
|
|
622
|
+
* mutate replace-block / insert-child). Nests recursively via `innerBlocks`.
|
|
623
|
+
*/
|
|
626
624
|
export interface BlockInput {
|
|
627
625
|
name: string;
|
|
628
626
|
attributes?: Record<string, unknown>;
|
|
@@ -631,6 +629,12 @@ export interface BlockInput {
|
|
|
631
629
|
innerContent?: unknown[];
|
|
632
630
|
}
|
|
633
631
|
|
|
632
|
+
/** Partial-update payload for a single existing block (update_block / by-ref / batch items). */
|
|
633
|
+
export interface BlockPatch {
|
|
634
|
+
attributes?: Record<string, unknown>;
|
|
635
|
+
innerHTML?: string;
|
|
636
|
+
}
|
|
637
|
+
|
|
634
638
|
export type CreatePostStatus = 'draft' | 'pending' | 'private' | 'publish' | 'future';
|
|
635
639
|
export type UpdatePostStatus = CreatePostStatus | 'trash';
|
|
636
640
|
export type CommentPingStatus = 'open' | 'closed';
|
|
@@ -795,17 +799,27 @@ export interface UploadMediaResponse {
|
|
|
795
799
|
// Routes only register when Yoast SEO is active on the target site.
|
|
796
800
|
// ============================================
|
|
797
801
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
export
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
export type
|
|
802
|
+
/**
|
|
803
|
+
* Single source of truth for the Yoast schema-page-type enum — the const
|
|
804
|
+
* array backs both the runtime value (JSON Schema `enum`, narrowing filters
|
|
805
|
+
* in src/tools/yoast.ts) and the TS union type below, so the two can't drift.
|
|
806
|
+
*/
|
|
807
|
+
export const YOAST_SCHEMA_PAGE_TYPES = [
|
|
808
|
+
'WebPage', 'ItemPage', 'AboutPage', 'FAQPage', 'QAPage',
|
|
809
|
+
'ProfilePage', 'ContactPage', 'MedicalWebPage', 'CollectionPage',
|
|
810
|
+
'CheckoutPage', 'RealEstateListing', 'SearchResultsPage',
|
|
811
|
+
] as const;
|
|
812
|
+
export type YoastSchemaPageType = typeof YOAST_SCHEMA_PAGE_TYPES[number];
|
|
813
|
+
|
|
814
|
+
export const YOAST_SCHEMA_ARTICLE_TYPES = [
|
|
815
|
+
'Article', 'BlogPosting', 'SocialMediaPosting', 'NewsArticle',
|
|
816
|
+
'AdvertiserContentArticle', 'SatiricalArticle', 'ScholarlyArticle',
|
|
817
|
+
'TechArticle', 'Report', 'None',
|
|
818
|
+
] as const;
|
|
819
|
+
export type YoastSchemaArticleType = typeof YOAST_SCHEMA_ARTICLE_TYPES[number];
|
|
820
|
+
|
|
821
|
+
export const YOAST_ROBOTS_ADVANCED = ['noimageindex', 'noarchive', 'nosnippet'] as const;
|
|
822
|
+
export type YoastRobotsAdvanced = typeof YOAST_ROBOTS_ADVANCED[number];
|
|
809
823
|
|
|
810
824
|
/**
|
|
811
825
|
* Writable Yoast SEO fields. All optional in update payloads — only the
|
|
@@ -857,6 +871,6 @@ export interface YoastBulkUpdateItem extends YoastSEOFields {
|
|
|
857
871
|
}
|
|
858
872
|
|
|
859
873
|
export type YoastBulkUpdateResponse = Array<
|
|
860
|
-
|
|
|
861
|
-
| { post_id
|
|
874
|
+
| YoastSEOMeta
|
|
875
|
+
| { post_id?: number; error: string }
|
|
862
876
|
>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-argument validation for the MCP dispatch.
|
|
3
|
+
*
|
|
4
|
+
* The MCP SDK does not validate CallTool arguments against a tool's
|
|
5
|
+
* `inputSchema`, so a misnamed parameter (e.g. `after` instead of
|
|
6
|
+
* `after_top_level`) was silently dropped and the handler ran with its
|
|
7
|
+
* defaults — turning a positioning mistake into a wrong-but-"successful" write.
|
|
8
|
+
*
|
|
9
|
+
* This rejects unknown top-level keys loudly — naming the valid ones with a
|
|
10
|
+
* "did you mean" suggestion — and flags missing required keys, before the
|
|
11
|
+
* handler runs.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
interface InputSchema {
|
|
15
|
+
properties?: Record<string, unknown>;
|
|
16
|
+
required?: string[];
|
|
17
|
+
additionalProperties?: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Throw a descriptive Error if `args` carries keys the tool doesn't declare, or
|
|
22
|
+
* omits a required key. No-op when the tool has no schema, or when the schema
|
|
23
|
+
* explicitly opts into additional properties.
|
|
24
|
+
*/
|
|
25
|
+
export function validateToolArgs(
|
|
26
|
+
toolName: string,
|
|
27
|
+
inputSchema: InputSchema | undefined,
|
|
28
|
+
args: Record<string, unknown>,
|
|
29
|
+
): void {
|
|
30
|
+
const properties = inputSchema?.properties;
|
|
31
|
+
if (!properties) {
|
|
32
|
+
return; // No declared shape — nothing to validate against.
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const known = Object.keys(properties);
|
|
36
|
+
const provided = Object.keys(args ?? {});
|
|
37
|
+
|
|
38
|
+
// Respect an explicit opt-in to extra props (a map-shaped schema sets
|
|
39
|
+
// `additionalProperties` to a schema object rather than leaving it absent).
|
|
40
|
+
const allowsExtra =
|
|
41
|
+
inputSchema?.additionalProperties !== undefined && inputSchema.additionalProperties !== false;
|
|
42
|
+
|
|
43
|
+
if (!allowsExtra) {
|
|
44
|
+
const unknown = provided.filter((k) => !known.includes(k));
|
|
45
|
+
if (unknown.length > 0) {
|
|
46
|
+
const parts = unknown.map((k) => {
|
|
47
|
+
const near = closestKey(k, known);
|
|
48
|
+
return near ? `'${k}' (did you mean '${near}'?)` : `'${k}'`;
|
|
49
|
+
});
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Unknown parameter(s) for ${toolName}: ${parts.join(', ')}. ` +
|
|
52
|
+
`Valid parameters: ${known.join(', ')}.`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const required = inputSchema?.required ?? [];
|
|
58
|
+
const missing = required.filter((r) => !(r in (args ?? {})));
|
|
59
|
+
if (missing.length > 0) {
|
|
60
|
+
throw new Error(`Missing required parameter(s) for ${toolName}: ${missing.join(', ')}.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Best-effort suggestion for a mistyped key. Prefers a known key that shares a
|
|
66
|
+
* prefix/substring with the input — the common agent mistake is a shortened
|
|
67
|
+
* name like `after` for `after_top_level` — then falls back to edit distance.
|
|
68
|
+
*/
|
|
69
|
+
function closestKey(input: string, candidates: string[]): string | null {
|
|
70
|
+
const lc = input.toLowerCase();
|
|
71
|
+
const affixed = candidates
|
|
72
|
+
.filter((c) => {
|
|
73
|
+
const cl = c.toLowerCase();
|
|
74
|
+
return cl.startsWith(lc) || lc.startsWith(cl) || cl.includes(lc) || lc.includes(cl);
|
|
75
|
+
})
|
|
76
|
+
.sort((a, b) => a.length - b.length);
|
|
77
|
+
if (affixed.length > 0) {
|
|
78
|
+
return affixed[0];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let best: string | null = null;
|
|
82
|
+
let bestDist = Infinity;
|
|
83
|
+
for (const c of candidates) {
|
|
84
|
+
const d = levenshtein(lc, c.toLowerCase());
|
|
85
|
+
if (d < bestDist) {
|
|
86
|
+
bestDist = d;
|
|
87
|
+
best = c;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return best !== null && bestDist <= Math.max(2, Math.ceil(input.length / 2)) ? best : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function levenshtein(a: string, b: string): number {
|
|
94
|
+
const m = a.length;
|
|
95
|
+
const n = b.length;
|
|
96
|
+
if (m === 0) return n;
|
|
97
|
+
if (n === 0) return m;
|
|
98
|
+
let prev = Array.from({ length: n + 1 }, (_, i) => i);
|
|
99
|
+
let curr = new Array<number>(n + 1);
|
|
100
|
+
for (let i = 1; i <= m; i++) {
|
|
101
|
+
curr[0] = i;
|
|
102
|
+
for (let j = 1; j <= n; j++) {
|
|
103
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
104
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
105
|
+
}
|
|
106
|
+
[prev, curr] = [curr, prev];
|
|
107
|
+
}
|
|
108
|
+
return prev[n];
|
|
109
|
+
}
|