#!/usr/bin/env node
import { z } from 'zod';
import {
WordPressRestClient,
WPCLIClient,
logger,
createMcpServer,
startServer
} from '@akungapaul/wp-mcp-shared';
// Initialize clients
const restClient = new WordPressRestClient({
url: process.env.WORDPRESS_URL,
username: process.env.WORDPRESS_USERNAME,
appPassword: process.env.WORDPRESS_APP_PASSWORD
});
const cliClient = new WPCLIClient({
enabled: process.env.ENABLE_WP_CLI === 'true',
wpCliPath: process.env.WP_CLI_PATH,
wordPressPath: process.env.WORDPRESS_PATH,
sshHost: process.env.SSH_HOST,
sshPort: process.env.SSH_PORT,
sshUser: process.env.SSH_USER,
sshKeyPath: process.env.SSH_KEY_PATH
});
// Define tools
const tools = [
// Posts
{
name: 'create_post',
description: 'Create a new WordPress post with title, content, and metadata',
inputSchema: z.object({
title: z.string().describe('Post title'),
content: z.string().optional().describe('Post content (HTML or blocks)'),
status: z.enum(['publish', 'draft', 'pending', 'private', 'future']).default('draft').describe('Post status'),
excerpt: z.string().optional().describe('Post excerpt'),
slug: z.string().optional().describe('Post slug (URL-friendly name)'),
author: z.number().optional().describe('Author user ID'),
featured_media: z.number().optional().describe('Featured image media ID'),
categories: z.array(z.number()).optional().describe('Category IDs'),
tags: z.array(z.number()).optional().describe('Tag IDs'),
meta: z.record(z.any()).optional().describe('Custom meta fields')
}),
handler: async (args) => {
const post = await restClient.createPost(args);
return {
content: [{
type: 'text',
text: JSON.stringify({ success: true, post }, null, 2)
}]
};
}
},
{
name: 'update_post',
description: 'Update an existing WordPress post',
inputSchema: z.object({
id: z.number().describe('Post ID to update'),
title: z.string().optional().describe('New post title'),
content: z.string().optional().describe('New post content'),
status: z.enum(['publish', 'draft', 'pending', 'private']).optional().describe('Post status'),
excerpt: z.string().optional().describe('Post excerpt'),
slug: z.string().optional().describe('Post slug'),
featured_media: z.number().optional().describe('Featured image media ID'),
categories: z.array(z.number()).optional().describe('Category IDs'),
tags: z.array(z.number()).optional().describe('Tag IDs')
}),
handler: async (args) => {
const { id, ...data } = args;
const post = await restClient.updatePost(id, data);
return {
content: [{
type: 'text',
text: JSON.stringify({ success: true, post }, null, 2)
}]
};
}
},
{
name: 'delete_post',
description: 'Delete or trash a WordPress post',
inputSchema: z.object({
id: z.number().describe('Post ID to delete'),
force: z.boolean().default(false).describe('Permanently delete (true) or move to trash (false)')
}),
handler: async (args) => {
const result = await restClient.deletePost(args.id, args.force);
return {
content: [{
type: 'text',
text: JSON.stringify({ success: true, deleted: args.id, result }, null, 2)
}]
};
}
},
{
name: 'get_post',
description: 'Retrieve a WordPress post by ID',
inputSchema: z.object({
id: z.number().describe('Post ID to retrieve')
}),
handler: async (args) => {
const post = await restClient.getPost(args.id);
return {
content: [{
type: 'text',
text: JSON.stringify(post, null, 2)
}]
};
}
},
{
name: 'list_posts',
description: 'List WordPress posts with filtering and pagination',
inputSchema: z.object({
status: z.enum(['publish', 'draft', 'pending', 'private', 'any']).default('publish').describe('Post status filter'),
per_page: z.number().default(10).describe('Number of posts per page'),
page: z.number().default(1).describe('Page number'),
search: z.string().optional().describe('Search query'),
author: z.number().optional().describe('Filter by author ID'),
categories: z.array(z.number()).optional().describe('Filter by category IDs'),
tags: z.array(z.number()).optional().describe('Filter by tag IDs'),
orderby: z.enum(['date', 'title', 'modified', 'id']).default('date').describe('Order by field'),
order: z.enum(['asc', 'desc']).default('desc').describe('Order direction')
}),
handler: async (args) => {
const posts = await restClient.getPosts(args);
return {
content: [{
type: 'text',
text: JSON.stringify({ count: posts.length, posts }, null, 2)
}]
};
}
},
// Pages
{
name: 'create_page',
description: 'Create a new WordPress page',
inputSchema: z.object({
title: z.string().describe('Page title'),
content: z.string().optional().describe('Page content (HTML or blocks)'),
status: z.enum(['publish', 'draft', 'pending', 'private']).default('draft').describe('Page status'),
slug: z.string().optional().describe('Page slug (URL-friendly name)'),
parent: z.number().optional().describe('Parent page ID for hierarchical structure'),
template: z.string().optional().describe('Page template'),
author: z.number().optional().describe('Author user ID')
}),
handler: async (args) => {
const page = await restClient.createPage(args);
return {
content: [{
type: 'text',
text: JSON.stringify({ success: true, page }, null, 2)
}]
};
}
},
{
name: 'update_page',
description: 'Update an existing WordPress page',
inputSchema: z.object({
id: z.number().describe('Page ID to update'),
title: z.string().optional().describe('New page title'),
content: z.string().optional().describe('New page content'),
status: z.enum(['publish', 'draft', 'pending', 'private']).optional().describe('Page status'),
slug: z.string().optional().describe('Page slug'),
parent: z.number().optional().describe('Parent page ID'),
template: z.string().optional().describe('Page template')
}),
handler: async (args) => {
const { id, ...data } = args;
const page = await restClient.updatePage(id, data);
return {
content: [{
type: 'text',
text: JSON.stringify({ success: true, page }, null, 2)
}]
};
}
},
// Advanced content operations
{
name: 'duplicate_post',
description: 'Create a duplicate copy of an existing post or page',
inputSchema: z.object({
id: z.number().describe('ID of post/page to duplicate'),
status: z.enum(['publish', 'draft', 'pending', 'private']).default('draft').describe('Status for duplicated content'),
title_suffix: z.string().default('(Copy)').describe('Suffix to add to duplicated title')
}),
handler: async (args) => {
const original = await restClient.getPost(args.id);
const newPost = await restClient.createPost({
title: `${original.title.rendered} ${args.title_suffix}`,
content: original.content.rendered,
status: args.status,
excerpt: original.excerpt?.rendered,
categories: original.categories,
tags: original.tags
});
return {
content: [{
type: 'text',
text: JSON.stringify({ success: true, original_id: args.id, new_post: newPost }, null, 2)
}]
};
}
},
{
name: 'schedule_post',
description: 'Schedule a post to be published at a future date and time',
inputSchema: z.object({
id: z.number().describe('Post ID to schedule'),
date: z.string().describe('Publication date and time (ISO 8601 format: YYYY-MM-DDTHH:MM:SS)')
}),
handler: async (args) => {
const post = await restClient.updatePost(args.id, {
status: 'future',
date: args.date
});
return {
content: [{
type: 'text',
text: JSON.stringify({ success: true, scheduled: post }, null, 2)
}]
};
}
},
{
name: 'bulk_edit_posts',
description: 'Perform bulk updates on multiple posts at once',
inputSchema: z.object({
post_ids: z.array(z.number()).describe('Array of post IDs to update'),
updates: z.object({
status: z.enum(['publish', 'draft', 'pending', 'private']).optional().describe('Change status'),
author: z.number().optional().describe('Change author'),
categories: z.array(z.number()).optional().describe('Set categories'),
tags: z.array(z.number()).optional().describe('Set tags')
}).describe('Fields to update on all posts')
}),
handler: async (args) => {
const results = await Promise.all(
args.post_ids.map(id => restClient.updatePost(id, args.updates))
);
return {
content: [{
type: 'text',
text: JSON.stringify({ success: true, updated: results.length, posts: results }, null, 2)
}]
};
}
},
{
name: 'get_revisions',
description: 'Get revision history for a post or page',
inputSchema: z.object({
id: z.number().describe('Post ID to get revisions for')
}),
handler: async (args) => {
const revisions = await restClient.get(`/wp/v2/posts/${args.id}/revisions`);
return {
content: [{
type: 'text',
text: JSON.stringify({ post_id: args.id, revisions }, null, 2)
}]
};
}
},
{
name: 'trash_restore_post',
description: 'Move a post to trash or restore it from trash',
inputSchema: z.object({
id: z.number().describe('Post ID'),
action: z.enum(['trash', 'restore']).describe('Action to perform')
}),
handler: async (args) => {
if (args.action === 'trash') {
const result = await restClient.deletePost(args.id, false);
return {
content: [{
type: 'text',
text: JSON.stringify({ success: true, action: 'trashed', post: result }, null, 2)
}]
};
} else {
const post = await restClient.updatePost(args.id, { status: 'draft' });
return {
content: [{
type: 'text',
text: JSON.stringify({ success: true, action: 'restored', post }, null, 2)
}]
};
}
}
}
];
// Create and start server
const server = createMcpServer('wp-content-mcp', '1.0.0', tools);
startServer(server);
logger.info('wp-content-mcp server started');