/**
* Taxonomy Tools Registration
*
* Handles: Categories, Tags, Taxonomies (CRUD operations)
*
* @package WP_Navigator_Pro
* @since 1.3.0
*/
import { toolRegistry, ToolCategory } from '../../tool-registry/index.js';
import {
validateRequired,
validatePagination,
validateId,
buildQueryString,
buildFieldsParam,
} from '../../tool-registry/utils.js';
/**
* Register taxonomy management tools (categories, tags, taxonomies)
*/
export function registerTaxonomyTools() {
// ============================================================================
// CATEGORIES
// ============================================================================
toolRegistry.register({
definition: {
name: 'wpnav_list_categories',
description:
'List all WordPress categories with optional filtering. Returns category ID, name, slug, count, and parent.',
inputSchema: {
type: 'object',
properties: {
page: { type: 'number', description: 'Page number for pagination (default: 1)' },
per_page: {
type: 'number',
description: 'Number of categories to return (default: 10, max: 100)',
},
search: { type: 'string', description: 'Search term to filter categories by name' },
parent: { type: 'number', description: 'Filter by parent category ID' },
fields: {
type: 'array',
items: { type: 'string' },
description: 'Fields to return (e.g., ["id", "name", "count"]). Reduces response size.',
},
},
required: [],
},
},
handler: async (args, context) => {
const { page, per_page } = validatePagination(args);
const qs = buildQueryString({
page,
per_page,
search: args.search,
parent: args.parent,
_fields: buildFieldsParam(args.fields),
});
const categories = await context.wpRequest(`/wp/v2/categories?${qs}`);
return {
content: [{ type: 'text', text: context.clampText(JSON.stringify(categories, null, 2)) }],
};
},
category: ToolCategory.TAXONOMY,
});
toolRegistry.register({
definition: {
name: 'wpnav_get_category',
description:
'Get a single WordPress category by ID. Returns full category details including description and post count.',
inputSchema: {
type: 'object',
properties: {
id: { type: 'number', description: 'WordPress category ID' },
fields: {
type: 'array',
items: { type: 'string' },
description: 'Fields to return (e.g., ["id", "name", "count"]). Reduces response size.',
},
},
required: ['id'],
},
},
handler: async (args, context) => {
validateRequired(args, ['id']);
const id = validateId(args.id, 'Category');
const fieldsParam = buildFieldsParam(args.fields);
const url = fieldsParam
? `/wp/v2/categories/${id}?_fields=${fieldsParam}`
: `/wp/v2/categories/${id}`;
const category = await context.wpRequest(url);
return {
content: [{ type: 'text', text: context.clampText(JSON.stringify(category, null, 2)) }],
};
},
category: ToolCategory.TAXONOMY,
});
toolRegistry.register({
definition: {
name: 'wpnav_create_category',
description:
'Create a new WordPress category. Requires name. Changes are logged in audit trail.',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Category name' },
description: { type: 'string', description: 'Category description (optional)' },
slug: {
type: 'string',
description: 'Category slug (optional, auto-generated from name if not provided)',
},
parent: {
type: 'number',
description: 'Parent category ID (optional, for hierarchical categories)',
},
},
required: ['name'],
},
},
handler: async (args, context) => {
try {
validateRequired(args, ['name']);
const createData: any = { name: args.name };
if (args.description) createData.description = args.description;
if (args.slug) createData.slug = args.slug;
if (args.parent) createData.parent = args.parent;
const result = await context.wpRequest('/wp/v2/categories', {
method: 'POST',
body: JSON.stringify(createData),
});
return {
content: [
{
type: 'text',
text: context.clampText(
JSON.stringify(
{
id: result.id,
name: result.name,
slug: result.slug,
link: result.link,
message: 'Category created successfully',
},
null,
2
)
),
},
],
};
} catch (error: any) {
const errorMessage = error.message || 'Unknown error';
const isWritesDisabled = errorMessage.includes('WRITES_DISABLED');
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: isWritesDisabled ? 'writes_disabled' : 'operation_failed',
code: isWritesDisabled ? 'WRITES_DISABLED' : 'CREATE_FAILED',
message: errorMessage,
context: {
resource_type: 'category',
name: args.name,
suggestion: isWritesDisabled
? 'Set WPNAV_ENABLE_WRITES=1 in MCP server config (.mcp.json env section)'
: 'Check category name is unique',
},
},
null,
2
),
},
],
isError: true,
};
}
},
category: ToolCategory.TAXONOMY,
});
toolRegistry.register({
definition: {
name: 'wpnav_update_category',
description:
'Update a WordPress category. Requires category ID and at least one field to update. Changes are logged in audit trail.',
inputSchema: {
type: 'object',
properties: {
id: { type: 'number', description: 'WordPress category ID' },
name: { type: 'string', description: 'New category name' },
description: { type: 'string', description: 'New category description' },
slug: { type: 'string', description: 'New category slug' },
parent: { type: 'number', description: 'New parent category ID' },
},
required: ['id'],
},
},
handler: async (args, context) => {
try {
validateRequired(args, ['id']);
const id = validateId(args.id, 'Category');
const updateData: any = {};
if (args.name) updateData.name = args.name;
if (args.description) updateData.description = args.description;
if (args.slug) updateData.slug = args.slug;
if (args.parent !== undefined) updateData.parent = args.parent;
if (Object.keys(updateData).length === 0) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: 'validation_failed',
code: 'VALIDATION_FAILED',
message:
'At least one field (name, description, slug, or parent) must be provided',
context: { resource_type: 'category', resource_id: args.id },
},
null,
2
),
},
],
isError: true,
};
}
const result = await context.wpRequest(`/wp/v2/categories/${id}`, {
method: 'POST',
body: JSON.stringify(updateData),
});
return {
content: [
{
type: 'text',
text: context.clampText(
JSON.stringify(
{
id: result.id,
name: result.name,
slug: result.slug,
message: 'Category updated successfully',
},
null,
2
)
),
},
],
};
} catch (error: any) {
const errorMessage = error.message || 'Unknown error';
const isWritesDisabled = errorMessage.includes('WRITES_DISABLED');
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: isWritesDisabled ? 'writes_disabled' : 'operation_failed',
code: isWritesDisabled ? 'WRITES_DISABLED' : 'UPDATE_FAILED',
message: errorMessage,
context: {
resource_type: 'category',
resource_id: args.id,
suggestion: isWritesDisabled
? 'Set WPNAV_ENABLE_WRITES=1 in MCP server config (.mcp.json env section)'
: 'Check category ID exists with wpnav_get_category',
},
},
null,
2
),
},
],
isError: true,
};
}
},
category: ToolCategory.TAXONOMY,
});
toolRegistry.register({
definition: {
name: 'wpnav_delete_category',
description:
'Delete a WordPress category by ID. Posts in this category will be reassigned to Uncategorized. WARNING: This action cannot be undone.',
inputSchema: {
type: 'object',
properties: {
id: { type: 'number', description: 'WordPress category ID' },
force: {
type: 'boolean',
description: 'Force permanent deletion. Default: true',
default: true,
},
},
required: ['id'],
},
},
handler: async (args, context) => {
try {
validateRequired(args, ['id']);
const id = validateId(args.id, 'Category');
const params = new URLSearchParams({ force: String(args.force !== false) });
const result = await context.wpRequest(`/wp/v2/categories/${id}?${params.toString()}`, {
method: 'DELETE',
});
return {
content: [
{
type: 'text',
text: context.clampText(
JSON.stringify(
{
id: result.id,
message: 'Category deleted successfully',
},
null,
2
)
),
},
],
};
} catch (error: any) {
const errorMessage = error.message || 'Unknown error';
const isWritesDisabled = errorMessage.includes('WRITES_DISABLED');
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: isWritesDisabled ? 'writes_disabled' : 'operation_failed',
code: isWritesDisabled ? 'WRITES_DISABLED' : 'DELETE_FAILED',
message: errorMessage,
context: {
resource_type: 'category',
resource_id: args.id,
suggestion: isWritesDisabled
? 'Set WPNAV_ENABLE_WRITES=1 in MCP server config (.mcp.json env section)'
: 'Check category ID exists with wpnav_get_category',
},
},
null,
2
),
},
],
isError: true,
};
}
},
category: ToolCategory.TAXONOMY,
});
// ============================================================================
// TAGS
// ============================================================================
toolRegistry.register({
definition: {
name: 'wpnav_list_tags',
description:
'List all WordPress tags with optional filtering. Returns tag ID, name, slug, and count.',
inputSchema: {
type: 'object',
properties: {
page: { type: 'number', description: 'Page number for pagination (default: 1)' },
per_page: {
type: 'number',
description: 'Number of tags to return (default: 10, max: 100)',
},
search: { type: 'string', description: 'Search term to filter tags by name' },
fields: {
type: 'array',
items: { type: 'string' },
description: 'Fields to return (e.g., ["id", "name", "count"]). Reduces response size.',
},
},
required: [],
},
},
handler: async (args, context) => {
const { page, per_page } = validatePagination(args);
const qs = buildQueryString({
page,
per_page,
search: args.search,
_fields: buildFieldsParam(args.fields),
});
const tags = await context.wpRequest(`/wp/v2/tags?${qs}`);
return {
content: [{ type: 'text', text: context.clampText(JSON.stringify(tags, null, 2)) }],
};
},
category: ToolCategory.TAXONOMY,
});
toolRegistry.register({
definition: {
name: 'wpnav_get_tag',
description:
'Get a single WordPress tag by ID. Returns full tag details including description and post count.',
inputSchema: {
type: 'object',
properties: {
id: { type: 'number', description: 'WordPress tag ID' },
fields: {
type: 'array',
items: { type: 'string' },
description: 'Fields to return (e.g., ["id", "name", "count"]). Reduces response size.',
},
},
required: ['id'],
},
},
handler: async (args, context) => {
validateRequired(args, ['id']);
const id = validateId(args.id, 'Tag');
const fieldsParam = buildFieldsParam(args.fields);
const url = fieldsParam ? `/wp/v2/tags/${id}?_fields=${fieldsParam}` : `/wp/v2/tags/${id}`;
const tag = await context.wpRequest(url);
return {
content: [{ type: 'text', text: context.clampText(JSON.stringify(tag, null, 2)) }],
};
},
category: ToolCategory.TAXONOMY,
});
toolRegistry.register({
definition: {
name: 'wpnav_create_tag',
description: 'Create a new WordPress tag. Requires name. Changes are logged in audit trail.',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Tag name' },
description: { type: 'string', description: 'Tag description (optional)' },
slug: {
type: 'string',
description: 'Tag slug (optional, auto-generated from name if not provided)',
},
},
required: ['name'],
},
},
handler: async (args, context) => {
try {
validateRequired(args, ['name']);
const createData: any = { name: args.name };
if (args.description) createData.description = args.description;
if (args.slug) createData.slug = args.slug;
const result = await context.wpRequest('/wp/v2/tags', {
method: 'POST',
body: JSON.stringify(createData),
});
return {
content: [
{
type: 'text',
text: context.clampText(
JSON.stringify(
{
id: result.id,
name: result.name,
slug: result.slug,
link: result.link,
message: 'Tag created successfully',
},
null,
2
)
),
},
],
};
} catch (error: any) {
const errorMessage = error.message || 'Unknown error';
const isWritesDisabled = errorMessage.includes('WRITES_DISABLED');
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: isWritesDisabled ? 'writes_disabled' : 'operation_failed',
code: isWritesDisabled ? 'WRITES_DISABLED' : 'CREATE_FAILED',
message: errorMessage,
context: {
resource_type: 'tag',
name: args.name,
suggestion: isWritesDisabled
? 'Set WPNAV_ENABLE_WRITES=1 in MCP server config (.mcp.json env section)'
: 'Check tag name is unique',
},
},
null,
2
),
},
],
isError: true,
};
}
},
category: ToolCategory.TAXONOMY,
});
toolRegistry.register({
definition: {
name: 'wpnav_update_tag',
description:
'Update a WordPress tag. Requires tag ID and at least one field to update. Changes are logged in audit trail.',
inputSchema: {
type: 'object',
properties: {
id: { type: 'number', description: 'WordPress tag ID' },
name: { type: 'string', description: 'New tag name' },
description: { type: 'string', description: 'New tag description' },
slug: { type: 'string', description: 'New tag slug' },
},
required: ['id'],
},
},
handler: async (args, context) => {
try {
validateRequired(args, ['id']);
const id = validateId(args.id, 'Tag');
const updateData: any = {};
if (args.name) updateData.name = args.name;
if (args.description) updateData.description = args.description;
if (args.slug) updateData.slug = args.slug;
if (Object.keys(updateData).length === 0) {
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: 'validation_failed',
code: 'VALIDATION_FAILED',
message: 'At least one field (name, description, or slug) must be provided',
context: { resource_type: 'tag', resource_id: args.id },
},
null,
2
),
},
],
isError: true,
};
}
const result = await context.wpRequest(`/wp/v2/tags/${id}`, {
method: 'POST',
body: JSON.stringify(updateData),
});
return {
content: [
{
type: 'text',
text: context.clampText(
JSON.stringify(
{
id: result.id,
name: result.name,
slug: result.slug,
message: 'Tag updated successfully',
},
null,
2
)
),
},
],
};
} catch (error: any) {
const errorMessage = error.message || 'Unknown error';
const isWritesDisabled = errorMessage.includes('WRITES_DISABLED');
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: isWritesDisabled ? 'writes_disabled' : 'operation_failed',
code: isWritesDisabled ? 'WRITES_DISABLED' : 'UPDATE_FAILED',
message: errorMessage,
context: {
resource_type: 'tag',
resource_id: args.id,
suggestion: isWritesDisabled
? 'Set WPNAV_ENABLE_WRITES=1 in MCP server config (.mcp.json env section)'
: 'Check tag ID exists with wpnav_get_tag',
},
},
null,
2
),
},
],
isError: true,
};
}
},
category: ToolCategory.TAXONOMY,
});
toolRegistry.register({
definition: {
name: 'wpnav_delete_tag',
description:
'Delete a WordPress tag by ID. Posts with this tag will have it removed. WARNING: This action cannot be undone.',
inputSchema: {
type: 'object',
properties: {
id: { type: 'number', description: 'WordPress tag ID' },
force: {
type: 'boolean',
description: 'Force permanent deletion. Default: true',
default: true,
},
},
required: ['id'],
},
},
handler: async (args, context) => {
try {
validateRequired(args, ['id']);
const id = validateId(args.id, 'Tag');
const params = new URLSearchParams({ force: String(args.force !== false) });
const result = await context.wpRequest(`/wp/v2/tags/${id}?${params.toString()}`, {
method: 'DELETE',
});
return {
content: [
{
type: 'text',
text: context.clampText(
JSON.stringify(
{
id: result.id,
message: 'Tag deleted successfully',
},
null,
2
)
),
},
],
};
} catch (error: any) {
const errorMessage = error.message || 'Unknown error';
const isWritesDisabled = errorMessage.includes('WRITES_DISABLED');
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: isWritesDisabled ? 'writes_disabled' : 'operation_failed',
code: isWritesDisabled ? 'WRITES_DISABLED' : 'DELETE_FAILED',
message: errorMessage,
context: {
resource_type: 'tag',
resource_id: args.id,
suggestion: isWritesDisabled
? 'Set WPNAV_ENABLE_WRITES=1 in MCP server config (.mcp.json env section)'
: 'Check tag ID exists with wpnav_get_tag',
},
},
null,
2
),
},
],
isError: true,
};
}
},
category: ToolCategory.TAXONOMY,
});
// ============================================================================
// TAXONOMIES (Read-Only Discovery)
// ============================================================================
toolRegistry.register({
definition: {
name: 'wpnav_list_taxonomies',
description:
'List all registered WordPress taxonomies (categories, tags, custom). Returns taxonomy name, labels, and capabilities. Always available for site structure discovery.',
inputSchema: {
type: 'object',
properties: {
type: { type: 'string', description: 'Filter by post type (e.g., "post", "page")' },
fields: {
type: 'array',
items: { type: 'string' },
description:
'Fields to return (e.g., ["name", "slug", "hierarchical"]). Reduces response size.',
},
},
required: [],
},
},
handler: async (args, context) => {
const qs = buildQueryString({
type: args.type,
_fields: buildFieldsParam(args.fields),
});
const taxonomies = await context.wpRequest(`/wp/v2/taxonomies?${qs}`);
return {
content: [{ type: 'text', text: context.clampText(JSON.stringify(taxonomies, null, 2)) }],
};
},
category: ToolCategory.TAXONOMY,
});
toolRegistry.register({
definition: {
name: 'wpnav_get_taxonomy',
description:
'Get details about a specific taxonomy by name. Returns full taxonomy configuration including hierarchical status, REST base, and labels. Always available for site structure discovery.',
inputSchema: {
type: 'object',
properties: {
taxonomy: {
type: 'string',
description: 'Taxonomy name (e.g., "category", "post_tag", or custom taxonomy)',
},
fields: {
type: 'array',
items: { type: 'string' },
description:
'Fields to return (e.g., ["name", "slug", "hierarchical"]). Reduces response size.',
},
},
required: ['taxonomy'],
},
},
handler: async (args, context) => {
validateRequired(args, ['taxonomy']);
const fieldsParam = buildFieldsParam(args.fields);
const url = fieldsParam
? `/wp/v2/taxonomies/${args.taxonomy}?_fields=${fieldsParam}`
: `/wp/v2/taxonomies/${args.taxonomy}`;
const taxonomy = await context.wpRequest(url);
return {
content: [{ type: 'text', text: context.clampText(JSON.stringify(taxonomy, null, 2)) }],
};
},
category: ToolCategory.TAXONOMY,
});
}