@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/preferences.ts
CHANGED
|
@@ -117,12 +117,13 @@ export function enrichPatternList(patterns: Pattern[]): {
|
|
|
117
117
|
(a, b) => b.preference.score - a.preference.score
|
|
118
118
|
);
|
|
119
119
|
|
|
120
|
-
// Classify by tier
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
120
|
+
// Classify by the tier the server emits (preferred/acceptable/avoid/legacy),
|
|
121
|
+
// which is the source of truth and has already applied policy. The
|
|
122
|
+
// "recommended" display bucket is the usable set (preferred + acceptable);
|
|
123
|
+
// avoid + legacy are the ones to steer away from.
|
|
124
|
+
const recommended = sorted.filter(
|
|
125
|
+
(p) => p.preference.tier === 'preferred' || p.preference.tier === 'acceptable'
|
|
126
|
+
);
|
|
126
127
|
const avoid = sorted.filter((p) => p.preference.tier === 'avoid' || p.preference.tier === 'legacy');
|
|
127
128
|
|
|
128
129
|
const lines: string[] = [];
|
|
@@ -196,49 +197,41 @@ export function enrichBlockTypes(types: BlockType[]): {
|
|
|
196
197
|
}
|
|
197
198
|
|
|
198
199
|
const lines: string[] = [];
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const names = blocks.map((t) => getShortName(t.name)).join(', ');
|
|
204
|
-
lines.push(`PREFERRED (${ns}/): ${names}`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
if (acceptable.length > 0) {
|
|
208
|
-
const grouped = groupByNamespace(acceptable);
|
|
209
|
-
for (const [ns, blocks] of Object.entries(grouped)) {
|
|
210
|
-
const names = blocks.map((t) => getShortName(t.name)).join(', ');
|
|
211
|
-
lines.push(`ACCEPTABLE (${ns}/): ${names}`);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
if (avoid.length > 0) {
|
|
215
|
-
const grouped = groupByNamespace(avoid);
|
|
216
|
-
for (const [ns, blocks] of Object.entries(grouped)) {
|
|
217
|
-
const mappings = blocks.map((t) => {
|
|
218
|
-
const replacement = t.preference.replacement;
|
|
219
|
-
const shortName = getShortName(t.name);
|
|
220
|
-
return replacement ? `${shortName} -> use ${replacement}` : shortName;
|
|
221
|
-
});
|
|
222
|
-
lines.push(`AVOID (${ns}/): ${mappings.join(', ')}`);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
if (legacy.length > 0) {
|
|
226
|
-
const grouped = groupByNamespace(legacy);
|
|
227
|
-
for (const [ns, blocks] of Object.entries(grouped)) {
|
|
228
|
-
const mappings = blocks.map((t) => {
|
|
229
|
-
const replacement = t.preference.replacement;
|
|
230
|
-
const shortName = getShortName(t.name);
|
|
231
|
-
return replacement ? `${shortName} -> use ${replacement}` : shortName;
|
|
232
|
-
});
|
|
233
|
-
lines.push(`LEGACY — DO NOT USE (${ns}/): ${mappings.join(', ')}`);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
200
|
+
pushTierLines(lines, preferred, 'PREFERRED', false);
|
|
201
|
+
pushTierLines(lines, acceptable, 'ACCEPTABLE', false);
|
|
202
|
+
pushTierLines(lines, avoid, 'AVOID', true);
|
|
203
|
+
pushTierLines(lines, legacy, 'LEGACY — DO NOT USE', true);
|
|
236
204
|
|
|
237
205
|
const guidance = lines.join('\n');
|
|
238
206
|
|
|
239
207
|
return { block_types: types, guidance };
|
|
240
208
|
}
|
|
241
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Append one guidance line per namespace group within a tier bucket.
|
|
212
|
+
* No-ops when the bucket is empty, so callers don't need their own length
|
|
213
|
+
* check. `withReplacement` switches between a plain name list (preferred /
|
|
214
|
+
* acceptable) and a `name -> use replacement` mapping (avoid / legacy).
|
|
215
|
+
*/
|
|
216
|
+
function pushTierLines(
|
|
217
|
+
lines: string[],
|
|
218
|
+
bucket: BlockType[],
|
|
219
|
+
label: string,
|
|
220
|
+
withReplacement: boolean
|
|
221
|
+
): void {
|
|
222
|
+
if (bucket.length === 0) return;
|
|
223
|
+
const grouped = groupByNamespace(bucket);
|
|
224
|
+
for (const [ns, blocks] of Object.entries(grouped)) {
|
|
225
|
+
const names = blocks.map((t) => {
|
|
226
|
+
const shortName = getShortName(t.name);
|
|
227
|
+
if (!withReplacement) return shortName;
|
|
228
|
+
const replacement = t.preference.replacement;
|
|
229
|
+
return replacement ? `${shortName} -> use ${replacement}` : shortName;
|
|
230
|
+
});
|
|
231
|
+
lines.push(`${label} (${ns}/): ${names.join(', ')}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
242
235
|
/**
|
|
243
236
|
* Format a preference warning into a single human-readable line.
|
|
244
237
|
*
|
|
@@ -252,6 +245,26 @@ export function formatPreferenceWarning(warning: PreferenceWarning): string {
|
|
|
252
245
|
return `WARNING: ${warning.message}`;
|
|
253
246
|
}
|
|
254
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Decorate a write-tool result with `formatted_warnings` when its `warnings`
|
|
250
|
+
* array is non-empty. Returns the result unchanged (no `formatted_warnings`
|
|
251
|
+
* key) when there are none, so callers don't leak an empty array into every
|
|
252
|
+
* clean response.
|
|
253
|
+
*
|
|
254
|
+
* @param result - A write-tool response shape carrying an optional `warnings` array
|
|
255
|
+
* @param formatWarning - Per-warning formatter (e.g. formatPreferenceWarning)
|
|
256
|
+
*/
|
|
257
|
+
export function withFormattedWarnings<T extends { warnings?: unknown[] }>(
|
|
258
|
+
result: T,
|
|
259
|
+
formatWarning: (warning: NonNullable<T['warnings']>[number]) => string
|
|
260
|
+
): T & { formatted_warnings?: string[] } {
|
|
261
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
262
|
+
const warnings = result.warnings as NonNullable<T['warnings']>;
|
|
263
|
+
return { ...result, formatted_warnings: warnings.map(formatWarning) };
|
|
264
|
+
}
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
|
|
255
268
|
// ============================================
|
|
256
269
|
// Helpers
|
|
257
270
|
// ============================================
|
package/src/rest-url.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a REST URL on the permalink-independent `?rest_route=` form.
|
|
3
|
+
*
|
|
4
|
+
* Hardcoding `/wp-json/` breaks on plain-permalink sites (and any custom
|
|
5
|
+
* `rest_url_prefix`): the connector can complete the handshake, then every tool
|
|
6
|
+
* call 404s because the pretty REST route does not exist. The `?rest_route=`
|
|
7
|
+
* form works on every permalink configuration, matching the connector's own
|
|
8
|
+
* exchange URL (see src/connect.ts).
|
|
9
|
+
*
|
|
10
|
+
* @param siteUrl - WordPress site URL; trailing slashes are trimmed.
|
|
11
|
+
* @param routeSuffix - Route path after the namespace, e.g. `/instructions`.
|
|
12
|
+
* Empty (default) yields the axios base URL.
|
|
13
|
+
* @returns e.g. `https://site.test/?rest_route=/gk-block-api/v1/instructions`
|
|
14
|
+
*/
|
|
15
|
+
export function restRouteUrl(siteUrl: string, routeSuffix = ''): string {
|
|
16
|
+
const trimmed = siteUrl.replace(/\/+$/, '');
|
|
17
|
+
return `${trimmed}/?rest_route=/gk-block-api/v1${routeSuffix}`;
|
|
18
|
+
}
|
package/src/tools/discovery.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type { WordPressBlockClient } from '../client.js';
|
|
10
10
|
import { enrichBlockTypes, enrichPatternList } from '../preferences.js';
|
|
11
|
+
import { coercePostId } from '../coerce.js';
|
|
11
12
|
|
|
12
13
|
const READ_ANNOT = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } as const;
|
|
13
14
|
|
|
@@ -166,12 +167,14 @@ export async function handleDiscoveryTool(
|
|
|
166
167
|
case 'list_patterns': {
|
|
167
168
|
const limit = (args.limit as number | undefined) ?? 20;
|
|
168
169
|
const offset = (args.offset as number | undefined) ?? 0;
|
|
170
|
+
// Fetch the full set (no server-side limit) and paginate locally, matching
|
|
171
|
+
// list_block_types. Passing limit: offset+limit truncated `total` to the
|
|
172
|
+
// fetched window and made next_offset always null on a full page — the
|
|
173
|
+
// agent then never saw that more patterns existed.
|
|
169
174
|
const response = await client.getPatterns({
|
|
170
175
|
q: args.search as string | undefined,
|
|
171
176
|
synced: args.synced as boolean | undefined,
|
|
172
177
|
min_score: args.min_score as number | undefined,
|
|
173
|
-
// Fetch enough to honor offset+limit. Server caps respond too.
|
|
174
|
-
limit: offset + limit,
|
|
175
178
|
refresh: args.refresh as boolean | undefined,
|
|
176
179
|
});
|
|
177
180
|
const enriched = enrichPatternList(response.patterns);
|
|
@@ -225,18 +228,11 @@ export async function handleDiscoveryTool(
|
|
|
225
228
|
) {
|
|
226
229
|
throw new Error('get_post_info requires one of: post_id, url, or slug');
|
|
227
230
|
}
|
|
228
|
-
// Coerce well-formed integer strings (some MCP clients and untyped
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
if (typeof postId === 'number' && Number.isInteger(postId) && postId > 0) {
|
|
234
|
-
normalizedPostId = postId;
|
|
235
|
-
} else if (typeof postId === 'string' && /^[0-9]+$/.test(postId)) {
|
|
236
|
-
normalizedPostId = parseInt(postId, 10);
|
|
237
|
-
} else if (postId !== undefined && postId !== null) {
|
|
238
|
-
throw new Error('get_post_info: post_id must be a positive integer');
|
|
239
|
-
}
|
|
231
|
+
// Coerce well-formed integer strings (some MCP clients and untyped JSON
|
|
232
|
+
// send numeric IDs as strings); undefined is allowed here because url/slug
|
|
233
|
+
// are alternate selectors. Shared with update_post / yoast_* so the whole
|
|
234
|
+
// tool surface accepts a post_id identically.
|
|
235
|
+
const normalizedPostId = coercePostId(postId, 'get_post_info');
|
|
240
236
|
return await client.getPostInfo({
|
|
241
237
|
post_id: normalizedPostId,
|
|
242
238
|
url: typeof url === 'string' ? url : undefined,
|
package/src/tools/mutate.ts
CHANGED
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
|
|
12
12
|
import type { WordPressBlockClient } from '../client.js';
|
|
13
13
|
import type { MutationOp, MutationRequest, MutationResponse, StaticBlockWarning } from '../types.js';
|
|
14
|
-
import { formatPreferenceWarning } from '../preferences.js';
|
|
14
|
+
import { formatPreferenceWarning, withFormattedWarnings } from '../preferences.js';
|
|
15
|
+
import { BLOCK_INPUT_SCHEMA } from './write.js';
|
|
16
|
+
import { coercePostId } from '../coerce.js';
|
|
15
17
|
|
|
16
18
|
const OPS: readonly MutationOp[] = [
|
|
17
19
|
'update-attrs',
|
|
@@ -61,14 +63,8 @@ export const MUTATE_TOOLS = [{
|
|
|
61
63
|
attributes: { type: 'object', description: 'update-attrs: attributes to merge.' },
|
|
62
64
|
innerHTML: { type: 'string', description: 'update-html: replacement innerHTML.' },
|
|
63
65
|
block: {
|
|
64
|
-
|
|
65
|
-
description: 'replace-block / insert-child:
|
|
66
|
-
properties: {
|
|
67
|
-
name: { type: 'string', description: 'Fully-qualified block name.' },
|
|
68
|
-
attributes: { type: 'object' },
|
|
69
|
-
innerHTML: { type: 'string' },
|
|
70
|
-
innerBlocks: { type: 'array' },
|
|
71
|
-
},
|
|
66
|
+
...BLOCK_INPUT_SCHEMA,
|
|
67
|
+
description: 'replace-block / insert-child: a block def. Nests recursively via `innerBlocks`; containers need their empty wrapper `innerHTML`.',
|
|
72
68
|
},
|
|
73
69
|
wrapper: {
|
|
74
70
|
type: 'object',
|
|
@@ -120,12 +116,12 @@ export async function handleMutateTool(
|
|
|
120
116
|
throw new Error(`Unknown mutate tool: ${toolName}`);
|
|
121
117
|
}
|
|
122
118
|
|
|
123
|
-
const postId = args.post_id
|
|
119
|
+
const postId = coercePostId(args.post_id, 'edit_block_tree');
|
|
124
120
|
const op = args.op as string;
|
|
125
121
|
const path = args.path;
|
|
126
122
|
const ref = args.ref as string | undefined;
|
|
127
123
|
|
|
128
|
-
if (postId === undefined
|
|
124
|
+
if (postId === undefined) throw new Error('post_id is required');
|
|
129
125
|
// Op validation comes from the schema enum at request time; this guard
|
|
130
126
|
// exists for direct programmatic callers that bypass the MCP transport.
|
|
131
127
|
if (!op || !(OPS as readonly string[]).includes(op)) {
|
|
@@ -180,10 +176,20 @@ export async function handleMutateTool(
|
|
|
180
176
|
requestBody.block = block as MutationRequest['block'];
|
|
181
177
|
if (op === 'insert-child' && args.position !== undefined) {
|
|
182
178
|
const position = args.position;
|
|
183
|
-
if (typeof position === 'number' && Number.isInteger(position)) {
|
|
179
|
+
if (typeof position === 'number' && Number.isInteger(position) && position >= -1) {
|
|
184
180
|
requestBody.position = position;
|
|
185
181
|
} else if (position === 'start' || position === 'end') {
|
|
186
182
|
requestBody.position = position;
|
|
183
|
+
} else if (typeof position === 'string' && /^-?\d+$/.test(position)) {
|
|
184
|
+
// The schema advertises `position` as integer|string; accept a
|
|
185
|
+
// numeric string ("3", or the "-1" append sentinel) as the index it
|
|
186
|
+
// plainly denotes rather than rejecting a schema-conformant value.
|
|
187
|
+
// -1 appends; anything below -1 is out of contract.
|
|
188
|
+
const parsedPosition = parseInt(position, 10);
|
|
189
|
+
if (parsedPosition < -1) {
|
|
190
|
+
throw new Error('position must be an integer >= -1, "start", or "end"');
|
|
191
|
+
}
|
|
192
|
+
requestBody.position = parsedPosition;
|
|
187
193
|
} else {
|
|
188
194
|
throw new Error('position must be an integer, "start", or "end"');
|
|
189
195
|
}
|
|
@@ -232,12 +238,7 @@ export async function handleMutateTool(
|
|
|
232
238
|
|
|
233
239
|
const result: MutationResponse = await client.mutateBlockTree(postId, requestBody);
|
|
234
240
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return formatPreferenceWarning(warning);
|
|
239
|
-
});
|
|
240
|
-
return { ...result, formatted_warnings: formattedWarnings };
|
|
241
|
-
}
|
|
242
|
-
return result;
|
|
241
|
+
return withFormattedWarnings(result, (warning) =>
|
|
242
|
+
isStaticBlockWarning(warning) ? formatStaticBlockWarning(warning) : formatPreferenceWarning(warning)
|
|
243
|
+
);
|
|
243
244
|
}
|
package/src/tools/patterns.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { WordPressBlockClient } from '../client.js';
|
|
10
|
+
import { coercePostId } from '../coerce.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Tool definitions for the pattern category.
|
|
@@ -61,13 +62,13 @@ export async function handlePatternTool(
|
|
|
61
62
|
): Promise<unknown> {
|
|
62
63
|
switch (toolName) {
|
|
63
64
|
case 'insert_pattern': {
|
|
64
|
-
const postId = args.post_id
|
|
65
|
+
const postId = coercePostId(args.post_id, 'insert_pattern');
|
|
65
66
|
const patternId = args.pattern_id as number | string;
|
|
66
67
|
const after = args.after_top_level as number | undefined;
|
|
67
68
|
const before = args.before_top_level as number | undefined;
|
|
68
69
|
const synced = args.synced as boolean | undefined;
|
|
69
70
|
|
|
70
|
-
if (postId === undefined
|
|
71
|
+
if (postId === undefined) throw new Error('post_id is required');
|
|
71
72
|
if (patternId === undefined || patternId === null) throw new Error('pattern_id is required');
|
|
72
73
|
|
|
73
74
|
const result = await client.insertPattern(postId, {
|
package/src/tools/posts.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import type { WordPressBlockClient } from '../client.js';
|
|
13
13
|
import type { CreatePostRequest, UpdatePostRequest } from '../types.js';
|
|
14
14
|
import { BLOCK_INPUT_SCHEMA } from './write.js';
|
|
15
|
+
import { coercePostId } from '../coerce.js';
|
|
15
16
|
|
|
16
17
|
const POST_STATUS_CREATE = ['draft', 'pending', 'private', 'publish', 'future'] as const;
|
|
17
18
|
const POST_STATUS_UPDATE = ['draft', 'pending', 'private', 'publish', 'future', 'trash'] as const;
|
|
@@ -128,15 +129,17 @@ export async function handlePostTool(
|
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
case 'update_post': {
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
const postId = coercePostId(args.post_id, 'update_post');
|
|
133
|
+
if (postId === undefined) {
|
|
134
|
+
throw new Error('update_post: "post_id" is required');
|
|
133
135
|
}
|
|
134
|
-
const { post_id:
|
|
136
|
+
const { post_id: _omit, ...rest } = args;
|
|
137
|
+
void _omit;
|
|
135
138
|
if (Object.keys(rest).length === 0) {
|
|
136
139
|
throw new Error('update_post: provide at least one mutating field besides post_id');
|
|
137
140
|
}
|
|
138
141
|
const update = narrowUpdatePost(rest);
|
|
139
|
-
return client.updatePost(postId
|
|
142
|
+
return client.updatePost(postId, update);
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
default:
|
|
@@ -144,13 +147,14 @@ export async function handlePostTool(
|
|
|
144
147
|
}
|
|
145
148
|
}
|
|
146
149
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
/** Fields narrowed identically for create_post and update_post (everything except title/status/content/blocks/post_type — those differ in requiredness or are create-only). */
|
|
151
|
+
type CommonPostFields = Pick<
|
|
152
|
+
UpdatePostRequest,
|
|
153
|
+
'slug' | 'parent' | 'excerpt' | 'featured_media' | 'categories' | 'tags' | 'terms' | 'date' | 'menu_order' | 'comment_status' | 'ping_status' | 'author'
|
|
154
|
+
>;
|
|
155
|
+
|
|
156
|
+
function narrowCommonPostFields(input: Record<string, unknown>): CommonPostFields {
|
|
157
|
+
const out: CommonPostFields = {};
|
|
154
158
|
if (typeof input.slug === 'string') out.slug = input.slug;
|
|
155
159
|
if (typeof input.parent === 'number') out.parent = input.parent;
|
|
156
160
|
if (typeof input.excerpt === 'string') out.excerpt = input.excerpt;
|
|
@@ -168,24 +172,20 @@ function narrowCreatePost(input: Record<string, unknown>): CreatePostRequest {
|
|
|
168
172
|
return out;
|
|
169
173
|
}
|
|
170
174
|
|
|
175
|
+
function narrowCreatePost(input: Record<string, unknown>): CreatePostRequest {
|
|
176
|
+
// input.title is already validated as a non-empty string by the caller.
|
|
177
|
+
const out: CreatePostRequest = { title: input.title as string, ...narrowCommonPostFields(input) };
|
|
178
|
+
if (typeof input.post_type === 'string') out.post_type = input.post_type;
|
|
179
|
+
if (typeof input.status === 'string') out.status = input.status as CreatePostRequest['status'];
|
|
180
|
+
if (typeof input.content === 'string') out.content = input.content;
|
|
181
|
+
if (Array.isArray(input.blocks)) out.blocks = input.blocks as CreatePostRequest['blocks'];
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
|
|
171
185
|
function narrowUpdatePost(input: Record<string, unknown>): UpdatePostRequest {
|
|
172
|
-
const out: UpdatePostRequest =
|
|
186
|
+
const out: UpdatePostRequest = narrowCommonPostFields(input);
|
|
173
187
|
if (typeof input.title === 'string') out.title = input.title;
|
|
174
188
|
if (typeof input.status === 'string') out.status = input.status as UpdatePostRequest['status'];
|
|
175
|
-
if (typeof input.slug === 'string') out.slug = input.slug;
|
|
176
|
-
if (typeof input.parent === 'number') out.parent = input.parent;
|
|
177
|
-
if (typeof input.excerpt === 'string') out.excerpt = input.excerpt;
|
|
178
|
-
if (typeof input.featured_media === 'number') out.featured_media = input.featured_media;
|
|
179
|
-
if (Array.isArray(input.categories)) out.categories = (input.categories as unknown[]).filter((n) => typeof n === 'number') as number[];
|
|
180
|
-
if (Array.isArray(input.tags)) out.tags = (input.tags as unknown[]).filter((n) => typeof n === 'number') as number[];
|
|
181
|
-
if (input.terms && typeof input.terms === 'object' && !Array.isArray(input.terms)) {
|
|
182
|
-
out.terms = narrowTermsMap(input.terms as Record<string, unknown>);
|
|
183
|
-
}
|
|
184
|
-
if (typeof input.date === 'string') out.date = input.date;
|
|
185
|
-
if (typeof input.menu_order === 'number') out.menu_order = input.menu_order;
|
|
186
|
-
if (input.comment_status === 'open' || input.comment_status === 'closed') out.comment_status = input.comment_status;
|
|
187
|
-
if (input.ping_status === 'open' || input.ping_status === 'closed') out.ping_status = input.ping_status;
|
|
188
|
-
if (typeof input.author === 'number') out.author = input.author;
|
|
189
189
|
return out;
|
|
190
190
|
}
|
|
191
191
|
|
package/src/tools/read.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type { WordPressBlockClient } from '../client.js';
|
|
10
10
|
import { enrichBlockList } from '../preferences.js';
|
|
11
|
+
import { coercePostId } from '../coerce.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Tool definitions for the read category.
|
|
@@ -71,6 +72,14 @@ export const READ_TOOLS = [
|
|
|
71
72
|
type: 'boolean',
|
|
72
73
|
description: 'Default true. When true, missing block refs (attrs.metadata.gk_ref) are assigned and persisted silently (no revision created). Set false for read-only callers that don\'t want write side effects — refs in the response will not resolve in subsequent mutation calls.',
|
|
73
74
|
},
|
|
75
|
+
limit: {
|
|
76
|
+
type: 'number',
|
|
77
|
+
description: 'Page size: top-level blocks per response. When more remain, the response carries `pagination.next_cursor` — pass it back via `cursor` to fetch the next page.',
|
|
78
|
+
},
|
|
79
|
+
cursor: {
|
|
80
|
+
type: 'string',
|
|
81
|
+
description: "Opaque cursor from a previous response's `pagination.next_cursor`. Omit on the first request.",
|
|
82
|
+
},
|
|
74
83
|
},
|
|
75
84
|
},
|
|
76
85
|
},
|
|
@@ -129,7 +138,7 @@ export async function handleReadTool(
|
|
|
129
138
|
): Promise<unknown> {
|
|
130
139
|
switch (toolName) {
|
|
131
140
|
case 'get_page_blocks': {
|
|
132
|
-
let postId = args.post_id
|
|
141
|
+
let postId = coercePostId(args.post_id, 'get_page_blocks');
|
|
133
142
|
const url = args.url as string | undefined;
|
|
134
143
|
const fields = args.fields as string | undefined;
|
|
135
144
|
const render = args.render as boolean | undefined;
|
|
@@ -139,12 +148,14 @@ export async function handleReadTool(
|
|
|
139
148
|
const summaryOnly = args.summary_only as boolean | undefined;
|
|
140
149
|
const includeLegacyPaths = args.include_legacy_paths as boolean | undefined;
|
|
141
150
|
const persistRefs = args.persist_refs as boolean | undefined;
|
|
151
|
+
const limit = args.limit as number | undefined;
|
|
152
|
+
const cursor = args.cursor as string | undefined;
|
|
142
153
|
|
|
143
|
-
if (
|
|
154
|
+
if (postId === undefined && !url) {
|
|
144
155
|
throw new Error('Either post_id or url is required');
|
|
145
156
|
}
|
|
146
157
|
|
|
147
|
-
if (postId === undefined
|
|
158
|
+
if (postId === undefined) {
|
|
148
159
|
const resolved = await client.resolveUrl(url as string);
|
|
149
160
|
postId = resolved.post_id;
|
|
150
161
|
}
|
|
@@ -154,6 +165,8 @@ export async function handleReadTool(
|
|
|
154
165
|
summary_only: summaryOnly,
|
|
155
166
|
include_legacy_paths: includeLegacyPaths,
|
|
156
167
|
...(persistRefs !== undefined ? { persist_refs: persistRefs } : {}),
|
|
168
|
+
...(limit !== undefined ? { limit } : {}),
|
|
169
|
+
...(cursor !== undefined ? { cursor } : {}),
|
|
157
170
|
});
|
|
158
171
|
|
|
159
172
|
// summary_only mode: return server summary as-is.
|
|
@@ -166,24 +179,28 @@ export async function handleReadTool(
|
|
|
166
179
|
|
|
167
180
|
const enriched = enrichBlockList(response.blocks || []);
|
|
168
181
|
|
|
182
|
+
// Surface pagination meta (incl. next_cursor) so the agent can page.
|
|
183
|
+
const pagination = (response as { pagination?: unknown }).pagination;
|
|
184
|
+
|
|
169
185
|
return {
|
|
170
186
|
post_id: postId,
|
|
171
187
|
summary: (response as { summary?: unknown }).summary,
|
|
172
188
|
blocks: enriched.blocks,
|
|
173
189
|
block_count: enriched.blocks.length,
|
|
174
190
|
warnings: enriched.warnings,
|
|
191
|
+
...(pagination !== undefined ? { pagination } : {}),
|
|
175
192
|
};
|
|
176
193
|
}
|
|
177
194
|
|
|
178
195
|
case 'get_block': {
|
|
179
|
-
const postId = args.post_id
|
|
196
|
+
const postId = coercePostId(args.post_id, 'get_block');
|
|
180
197
|
const ref = typeof args.ref === 'string' && args.ref.length > 0 ? (args.ref as string) : undefined;
|
|
181
198
|
const flatIndex =
|
|
182
|
-
typeof args.flat_index === 'number' && Number.
|
|
199
|
+
typeof args.flat_index === 'number' && Number.isInteger(args.flat_index) && args.flat_index >= 0
|
|
183
200
|
? (args.flat_index as number)
|
|
184
201
|
: undefined;
|
|
185
202
|
|
|
186
|
-
if (postId === undefined
|
|
203
|
+
if (postId === undefined) {
|
|
187
204
|
throw new Error('post_id is required');
|
|
188
205
|
}
|
|
189
206
|
const hasRef = ref !== undefined;
|