Skip to main content
Glama
index.ts18.6 kB
/** * Gutenberg Tools Registration * * MCP tools for Gutenberg block operations: * - Introspect capabilities * - List/insert/replace/move/delete blocks * - List/get/insert patterns * * @package WP_Navigator_Pro * @since 1.3.0 */ import { toolRegistry, ToolCategory } from '../../tool-registry/index.js'; import { validateRequired, validateId, extractSummary } from '../../tool-registry/utils.js'; import { buildIRNode, validatePath, parseIRDocument, formatIRSummary, validateBlockAttributes } from './helpers.js'; /** * Register all Gutenberg tools * * Registers 8 tools for Gutenberg block manipulation. */ export function registerGutenbergTools() { // ============================================================================ // INTROSPECT // ============================================================================ toolRegistry.register({ definition: { name: 'wpnav_gutenberg_introspect', description: 'Introspect Gutenberg capabilities: available blocks, patterns, and attributes. Use this to discover what blocks you can create.', inputSchema: { type: 'object', properties: {}, required: [], }, }, handler: async (args, context) => { const result = await context.wpRequest('/wpnav/v1/gutenberg/introspect'); return { content: [{ type: 'text', text: context.clampText(JSON.stringify(result, null, 2)) }], }; }, category: ToolCategory.CONTENT, }); // ============================================================================ // LIST BLOCKS // ============================================================================ toolRegistry.register({ definition: { name: 'wpnav_gutenberg_list_blocks', description: 'Get all blocks in a post as Intermediate Representation (IR). Returns block structure with types, attributes, and paths.', inputSchema: { type: 'object', properties: { post_id: { type: 'number', description: 'WordPress post or page ID to load blocks from', }, }, required: ['post_id'], }, }, handler: async (args, context) => { validateRequired(args, ['post_id']); const id = validateId(args.post_id, 'Post'); const result = await context.wpRequest(`/wpnav/v1/gutenberg/blocks?post_id=${id}`); if (!result.success) { return { content: [{ type: 'text', text: context.clampText(JSON.stringify({ error: result.error }, null, 2)), }], isError: true, }; } // Include both full IR and summary const summary = formatIRSummary(result.ir_document); return { content: [{ type: 'text', text: context.clampText( `${summary}\n\nFull IR Document:\n${JSON.stringify(result.ir_document, null, 2)}` ), }], }; }, category: ToolCategory.CONTENT, }); // ============================================================================ // INSERT BLOCK // ============================================================================ toolRegistry.register({ definition: { name: 'wpnav_gutenberg_insert_block', description: 'Insert a new Gutenberg block at specified path. Path is array of indices (e.g., [0] for first position, [1,0] for first child of second block).', inputSchema: { type: 'object', properties: { post_id: { type: 'number', description: 'Post ID to modify', }, path: { type: 'array', items: { type: 'number' }, description: 'Path array (e.g., [0] = first block, [1, 0] = first child of second block)', }, block_type: { type: 'string', description: 'Block type (e.g., "core/heading", "core/paragraph", "core/button")', enum: [ 'core/paragraph', 'core/heading', 'core/button', 'core/columns', 'core/column', 'core/image', 'core/list', 'core/quote', 'core/separator', 'core/spacer', ], }, attrs: { type: 'object', description: 'Block attributes (e.g., {level: 2, content: "Hello"} for heading, {content: "Text"} for paragraph)', additionalProperties: true, }, }, required: ['post_id', 'path', 'block_type', 'attrs'], }, }, handler: async (args, context) => { validateRequired(args, ['post_id', 'path', 'block_type', 'attrs']); const id = validateId(args.post_id, 'Post'); if (!validatePath(args.path)) { return { content: [{ type: 'text', text: context.clampText(JSON.stringify({ error: 'invalid_path', message: 'Path must be non-empty array of non-negative integers', }, null, 2)), }], isError: true, }; } // Validate block attributes try { validateBlockAttributes(args.block_type, args.attrs); } catch (error: any) { return { content: [{ type: 'text', text: context.clampText(JSON.stringify({ error: 'invalid_attributes', message: error.message, }, null, 2)), }], isError: true, }; } // Build IR node const block = buildIRNode(args.block_type, args.attrs); // Call REST API const result = await context.wpRequest('/wpnav/v1/gutenberg/blocks/insert', { method: 'POST', body: JSON.stringify({ post_id: id, path: args.path, block: block, }), }); if (!result.success) { return { content: [{ type: 'text', text: context.clampText(JSON.stringify({ error: result.error }, null, 2)), }], isError: true, }; } return { content: [{ type: 'text', text: context.clampText(JSON.stringify({ success: true, post_id: id, path: args.path, block_type: args.block_type, message: 'Block inserted successfully', }, null, 2)), }], }; }, category: ToolCategory.CONTENT, }); // ============================================================================ // REPLACE BLOCK // ============================================================================ toolRegistry.register({ definition: { name: 'wpnav_gutenberg_replace_block', description: 'Replace an existing Gutenberg block at specified path with a new block.', inputSchema: { type: 'object', properties: { post_id: { type: 'number', description: 'Post ID to modify', }, path: { type: 'array', items: { type: 'number' }, description: 'Path to block to replace (e.g., [0] = first block, [1, 0] = first child of second block)', }, block_type: { type: 'string', description: 'New block type (e.g., "core/heading", "core/paragraph")', enum: [ 'core/paragraph', 'core/heading', 'core/button', 'core/columns', 'core/column', 'core/image', 'core/list', 'core/quote', 'core/separator', 'core/spacer', ], }, attrs: { type: 'object', description: 'New block attributes', additionalProperties: true, }, }, required: ['post_id', 'path', 'block_type', 'attrs'], }, }, handler: async (args, context) => { validateRequired(args, ['post_id', 'path', 'block_type', 'attrs']); const id = validateId(args.post_id, 'Post'); if (!validatePath(args.path)) { return { content: [{ type: 'text', text: context.clampText(JSON.stringify({ error: 'invalid_path', message: 'Path must be non-empty array of non-negative integers', }, null, 2)), }], isError: true, }; } // Validate block attributes try { validateBlockAttributes(args.block_type, args.attrs); } catch (error: any) { return { content: [{ type: 'text', text: context.clampText(JSON.stringify({ error: 'invalid_attributes', message: error.message, }, null, 2)), }], isError: true, }; } // Build IR node const block = buildIRNode(args.block_type, args.attrs); // Call REST API const result = await context.wpRequest('/wpnav/v1/gutenberg/blocks/replace', { method: 'POST', body: JSON.stringify({ post_id: id, path: args.path, block: block, }), }); if (!result.success) { return { content: [{ type: 'text', text: context.clampText(JSON.stringify({ error: result.error }, null, 2)), }], isError: true, }; } return { content: [{ type: 'text', text: context.clampText(JSON.stringify({ success: true, post_id: id, path: args.path, block_type: args.block_type, message: 'Block replaced successfully', }, null, 2)), }], }; }, category: ToolCategory.CONTENT, }); // ============================================================================ // MOVE BLOCK // ============================================================================ toolRegistry.register({ definition: { name: 'wpnav_gutenberg_move_block', description: 'Move a Gutenberg block from one path to another. Useful for reordering blocks or moving nested blocks.', inputSchema: { type: 'object', properties: { post_id: { type: 'number', description: 'Post ID to modify', }, from_path: { type: 'array', items: { type: 'number' }, description: 'Source path (block to move)', }, to_path: { type: 'array', items: { type: 'number' }, description: 'Destination path (where to move block)', }, }, required: ['post_id', 'from_path', 'to_path'], }, }, handler: async (args, context) => { validateRequired(args, ['post_id', 'from_path', 'to_path']); const id = validateId(args.post_id, 'Post'); if (!validatePath(args.from_path)) { return { content: [{ type: 'text', text: context.clampText(JSON.stringify({ error: 'invalid_from_path', message: 'from_path must be non-empty array of non-negative integers', }, null, 2)), }], isError: true, }; } if (!validatePath(args.to_path)) { return { content: [{ type: 'text', text: context.clampText(JSON.stringify({ error: 'invalid_to_path', message: 'to_path must be non-empty array of non-negative integers', }, null, 2)), }], isError: true, }; } // Call REST API const result = await context.wpRequest('/wpnav/v1/gutenberg/blocks/move', { method: 'POST', body: JSON.stringify({ post_id: id, from_path: args.from_path, to_path: args.to_path, }), }); if (!result.success) { return { content: [{ type: 'text', text: context.clampText(JSON.stringify({ error: result.error }, null, 2)), }], isError: true, }; } return { content: [{ type: 'text', text: context.clampText(JSON.stringify({ success: true, post_id: id, from_path: args.from_path, to_path: args.to_path, message: 'Block moved successfully', }, null, 2)), }], }; }, category: ToolCategory.CONTENT, }); // ============================================================================ // DELETE BLOCK // ============================================================================ toolRegistry.register({ definition: { name: 'wpnav_gutenberg_delete_block', description: 'Delete a Gutenberg block at specified path. WARNING: This action modifies post content immediately.', inputSchema: { type: 'object', properties: { post_id: { type: 'number', description: 'Post ID to modify', }, path: { type: 'array', items: { type: 'number' }, description: 'Path to block to delete (e.g., [0] = first block, [1, 0] = first child of second block)', }, }, required: ['post_id', 'path'], }, }, handler: async (args, context) => { validateRequired(args, ['post_id', 'path']); const id = validateId(args.post_id, 'Post'); if (!validatePath(args.path)) { return { content: [{ type: 'text', text: context.clampText(JSON.stringify({ error: 'invalid_path', message: 'Path must be non-empty array of non-negative integers', }, null, 2)), }], isError: true, }; } // Call REST API const result = await context.wpRequest('/wpnav/v1/gutenberg/blocks/delete', { method: 'POST', body: JSON.stringify({ post_id: id, path: args.path, }), }); if (!result.success) { return { content: [{ type: 'text', text: context.clampText(JSON.stringify({ error: result.error }, null, 2)), }], isError: true, }; } return { content: [{ type: 'text', text: context.clampText(JSON.stringify({ success: true, post_id: id, path: args.path, message: 'Block deleted successfully', }, null, 2)), }], }; }, category: ToolCategory.CONTENT, }); // ============================================================================ // LIST PATTERNS // ============================================================================ toolRegistry.register({ definition: { name: 'wpnav_gutenberg_list_patterns', description: 'List all available Gutenberg block patterns and reusable blocks. Patterns are pre-designed block combinations.', inputSchema: { type: 'object', properties: {}, required: [], }, }, handler: async (args, context) => { const result = await context.wpRequest('/wpnav/v1/gutenberg/patterns'); if (!result.success) { return { content: [{ type: 'text', text: context.clampText(JSON.stringify({ error: result.error }, null, 2)), }], isError: true, }; } // Format patterns for display const summary = result.patterns.map((p: any) => ({ id: p.id, title: p.title, type: p.type, categories: p.categories, })); return { content: [{ type: 'text', text: context.clampText(JSON.stringify({ success: true, count: result.patterns.length, patterns: summary, }, null, 2)), }], }; }, category: ToolCategory.CONTENT, }); // ============================================================================ // INSERT PATTERN // ============================================================================ toolRegistry.register({ definition: { name: 'wpnav_gutenberg_insert_pattern', description: 'Insert a Gutenberg block pattern at specified path. Use wpnav_gutenberg_list_patterns to discover available patterns first.', inputSchema: { type: 'object', properties: { post_id: { type: 'number', description: 'Post ID to modify', }, path: { type: 'array', items: { type: 'number' }, description: 'Path where pattern should be inserted (e.g., [0] for first position)', }, pattern_slug: { type: 'string', description: 'Pattern slug/ID from wpnav_gutenberg_list_patterns', }, }, required: ['post_id', 'path', 'pattern_slug'], }, }, handler: async (args, context) => { validateRequired(args, ['post_id', 'path', 'pattern_slug']); const id = validateId(args.post_id, 'Post'); if (!validatePath(args.path)) { return { content: [{ type: 'text', text: context.clampText(JSON.stringify({ error: 'invalid_path', message: 'Path must be non-empty array of non-negative integers', }, null, 2)), }], isError: true, }; } // Call REST API const result = await context.wpRequest('/wpnav/v1/gutenberg/patterns/insert', { method: 'POST', body: JSON.stringify({ post_id: id, path: args.path, pattern_slug: args.pattern_slug, }), }); if (!result.success) { return { content: [{ type: 'text', text: context.clampText(JSON.stringify({ error: result.error }, null, 2)), }], isError: true, }; } return { content: [{ type: 'text', text: context.clampText(JSON.stringify({ success: true, post_id: id, path: args.path, pattern_slug: args.pattern_slug, message: 'Pattern inserted successfully', }, null, 2)), }], }; }, category: ToolCategory.CONTENT, }); }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/littlebearapps/wp-navigator-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server