# Directory Structure
```
├── .gitignore
├── dist
│ ├── index.js
│ └── index.js.map
├── package-lock.json
├── package.json
├── README.md
├── rollup.config.js
├── src
│ ├── helpers.ts
│ ├── index.ts
│ ├── tools
│ │ ├── build-block.ts
│ │ ├── filesystem
│ │ │ ├── edit-block-file.ts
│ │ │ └── edit-block-json-file.ts
│ │ ├── list-available-plugins-in-site-plugins-path.ts
│ │ ├── list-plugin-files.ts
│ │ ├── scaffold-block.ts
│ │ ├── scaffold-plugin.ts
│ │ ├── wp-api
│ │ │ ├── activate-plugin.ts
│ │ │ ├── create-post.ts
│ │ │ ├── deactivate-plugin.ts
│ │ │ ├── delete-post.ts
│ │ │ ├── get-block-types.ts
│ │ │ ├── get-plugins.ts
│ │ │ ├── get-post-types.ts
│ │ │ ├── get-post.ts
│ │ │ ├── get-posts.ts
│ │ │ ├── get-rest-base-for-post-types.ts
│ │ │ ├── get-templates.ts
│ │ │ ├── post
│ │ │ │ ├── get-post-content-before-changes.ts
│ │ │ │ └── get-post-preview-link.ts
│ │ │ ├── site-settings
│ │ │ │ ├── get-site-settings.ts
│ │ │ │ ├── update-integer-site-setting.ts
│ │ │ │ └── update-string-site-setting.ts
│ │ │ ├── update-post-content.ts
│ │ │ ├── update-post-status.ts
│ │ │ └── update-post.ts
│ │ └── wp-cli
│ │ ├── check_and_instal_wp_cli.ts
│ │ └── instal-and-activate-plugin.ts
│ └── types
│ └── wp-sites.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
/node_modules/
/.idea/
wp-sites.json
/wp-sites.json.bak
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# mcp-wordpress-gutenberg
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"allowJs": true,
"declaration": false,
"moduleDetection": "force",
"paths": {
"*": ["./src/*"]
},
"baseUrl": ".",
"removeComments": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
```
--------------------------------------------------------------------------------
/src/types/wp-sites.ts:
--------------------------------------------------------------------------------
```typescript
// src/types/wp-sites.ts
export interface WordPressSite {
cli?: string;
localWpSshEntryFile?: string;
name: string;
path: string; // Ścieżka do instalacji WordPress
pluginsPath: string; // Ścieżka do instalacji WordPress
apiUrl: string; // URL do WP REST API
apiCredentials?: {
username: string;
password: string;
};
database?: {
host: string;
port: number;
user: string;
password: string;
name: string;
};
}
export interface WPSitesConfig {
sites: Record<string, WordPressSite>;
}
```
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
```javascript
import typescript from "rollup-plugin-typescript2";
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import json from "@rollup/plugin-json";
export default {
input: "src/index.ts",
output: {
file: "dist/index.js",
format: "esm", // Używamy ESM, aby działało z "type": "module"
sourcemap: true
},
plugins: [
resolve(),
commonjs(),
json(),
typescript({
tsconfig: "./tsconfig.json",
}),
],
external: ["fs", "path", "process"], // Nie bundlujemy wbudowanych modułów Node.js
};
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "wordpress-mcp",
"version": "1.0.0",
"type": "module",
"description": "WordPress MCP Server",
"main": "dist/index.js",
"scripts": {
"clean": "rimraf dist",
"build": "npm run clean && rollup -c",
"start": "node dist/index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.5.0",
"node-fetch": "^3.3.2"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^28.0.2",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.0",
"@types/node": "^18.15.11",
"rimraf": "^5.0.0",
"rollup": "^4.34.8",
"rollup-plugin-typescript2": "^0.36.0",
"typescript": "^4.9.5"
}
}
```
--------------------------------------------------------------------------------
/src/tools/scaffold-plugin.ts:
--------------------------------------------------------------------------------
```typescript
// src/tools/scaffold-plugin.ts
import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
import { execSync } from 'child_process';
export const scaffoldPluginTool = {
name: "wp_scaffold_plugin",
description: "Creates a new WordPress plugin using wp-cli scaffold plugin",
inputSchema: {
type: "object",
properties: {
site: { type: "string", description: "Site alias from configuration" },
name: { type: "string", description: "Plugin name" },
directory: { type: "string", description: "Optional: Custom directory path" }
},
required: ["name"]
},
execute: async (args: { name: string; directory: string }) => {
try {
const command = `wp scaffold plugin ${args.name} --dir=${args.directory}`;
const output = execSync(command, {
stdio: ['pipe', 'pipe', 'pipe'],
encoding: 'utf-8'
});
return {
content: [{
type: "text",
text: `Plugin "${args.name}" created successfully in ${args.directory}\n\nOutput:\n${output}`
}]
};
} catch (error) {
console.error('Plugin scaffolding error:', error);
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `Failed to create plugin: ${error.message}`);
}
throw new McpError(ErrorCode.InternalError, 'Unknown error occurred while creating plugin');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/wp-api/post/get-post-content-before-changes.ts:
--------------------------------------------------------------------------------
```typescript
import {WordPressSite} from "types/wp-sites";
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
import {apiGetPost} from "tools/wp-api/get-post";
export const apiGetPostContentBeforeChanges = {
name: "wp_api_get_post_content_before_changes",
description: "Retrieve a single post, page, or custom post type CONTENT using REST API. Run it always before doing changes in content.",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"},
postType: {type: "string", description: "Post type (default: posts)"},
postId: {type: "integer", description: "Post ID to retrieve"}, // New property to specify post ID
},
required: ["siteKey", "postType", "postId"]
},
async execute(args: { siteKey: string, postType: string, postId: number }, site: WordPressSite) {
try {
const {post} = await apiGetPost.execute(args, site)
return {
post,
content: [{
type: "text",
text: `Post retrieved successfully.\n\nCurrent ${args.postType} #${args.postId} content is:\n\n${post.content.raw}`
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `wp_api_get_post_content_before_changes failed: ${error.message}`);
}
throw new McpError(ErrorCode.InternalError, 'wp_api_get_post_content_before_changes failed: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/wp-cli/instal-and-activate-plugin.ts:
--------------------------------------------------------------------------------
```typescript
import {execSync} from "child_process";
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
import {WordPressSite} from "types/wp-sites";
export const cliInstallAndActivatePlugin = {
name: "wp_cli_install_and_activate_plugin",
description: "Installs and activates a specified WordPress plugin via WP-CLI.",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"},
pluginSlug: {type: "string", description: "Plugin slug to install and activate."}
},
required: ["pluginSlug"]
},
async execute(args: { siteKey: string, pluginSlug: string }, site: WordPressSite, cliWrapper: string) {
try {
const pluginSlug = args.pluginSlug;
const cmdInstallAndActivate = cliWrapper.replace('{{cmd}}', `wp plugin install ${pluginSlug} --activate`)
const outputInstallAndActivate = execSync(cmdInstallAndActivate, {
stdio: "pipe",
cwd: site.path
});
return {
content: [{
type: "text",
text: `✅ Plugin '${pluginSlug}' was already installed and has been activated. Output from WP-CLI:\n\n${outputInstallAndActivate}`
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `wp_cli_install_and_activate_plugin failed: ${error.message}`);
}
throw new McpError(ErrorCode.InternalError, 'wp_cli_install_and_activate_plugin failed: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/wp-api/post/get-post-preview-link.ts:
--------------------------------------------------------------------------------
```typescript
import {WordPressSite} from "types/wp-sites";
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
import {apiGetPost} from "tools/wp-api/get-post";
export const apiGetPostPreviewLink = {
name: "wp_api_get_post_preview_link",
description: "Retrieve a preview link for single post, page, or custom post type item using REST API. Especially for draft status.",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"},
postType: {type: "string", description: "Post type (default: posts)"},
postId: {type: "integer", description: "Post ID to retrieve"},
},
required: ["siteKey", "postType", "postId"]
},
async execute(args: { siteKey: string, postType: string, postId: number }, site: WordPressSite) {
try {
const {post} = await apiGetPost.execute(args, site)
const postPreviewUrl = `${post?.guid?.rendered}&preview=true`;
return {
post: post,
previewLink: postPreviewUrl,
content: [{
type: "text",
text: `Post preview link retrieved successfully. Remember it is only for logged in user with proper permissions.\nURL: ${postPreviewUrl}`
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `wp_api_get_post_preview_link failed: ${error.message}`);
}
throw new McpError(ErrorCode.InternalError, 'wp_api_get_post_preview_link failed: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/list-available-plugins-in-site-plugins-path.ts:
--------------------------------------------------------------------------------
```typescript
// src/tools/edit-file.ts
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
import fs from 'fs';
import {WordPressSite} from "../types/wp-sites";
export const listAvailablePluginsInSitePluginsPath = {
name: "wp_list_available_plugins_in_site_plugins_path",
description: "List a plugins directories which is equivalent of listing available plugins for site",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"}
},
required: ["siteKey"]
},
async execute(args: { siteKey: string }, site: WordPressSite) {
const pluginsPath = site.pluginsPath
if (!fs.existsSync(pluginsPath)) {
throw new Error(`wp_list_available_plugins_in_site_plugins_path failed: directory ${pluginsPath} not exists. Please review a plugin directory and site configuration.`);
}
try {
const files = fs.readdirSync(pluginsPath, {withFileTypes: true});
const directories = files
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
return {
content: [{
type: "text",
text: `Available plugins directories in ${pluginsPath}:\n\n${directories.join('\n')}`,
directories
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, error.message);
}
throw new McpError(ErrorCode.InternalError, 'wp_list_available_plugins_in_site_plugins_path: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/wp-api/site-settings/get-site-settings.ts:
--------------------------------------------------------------------------------
```typescript
import {WordPressSite} from "types/wp-sites";
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
export const apiGetSiteSettings = {
name: "wp_api_get_site_settings",
description: "Retrieves all WordPress site settings using REST API",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"},
},
required: ["siteKey"]
},
async execute(args: any, site: WordPressSite) {
try {
const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
const url = `${site.apiUrl}/wp/v2/settings`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Failed to retrieve site settings: ${errorData.message || response.statusText}`);
}
const settings = await response.json();
return {
settings,
content: [{
type: "text",
text: `Site settings retrieved successfully.\n\nSettings: ${JSON.stringify(settings, null, 2)}`
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `wp_api_get_site_settings failed: ${error.message}`);
}
throw new McpError(ErrorCode.InternalError, 'wp_api_get_site_settings failed: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/wp-api/get-block-types.ts:
--------------------------------------------------------------------------------
```typescript
import {WordPressSite} from "types/wp-sites";
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
export const apiGetGutenbergBlocks = {
name: "wp_api_get_gutenberg_blocks",
description: "Get available Gutenberg blocks for the WordPress site",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"}
},
required: ["siteKey"]
},
async execute(args: { siteKey: string }, site: WordPressSite) {
try {
const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
const url = `${site.apiUrl}/wp/v2/block-types`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Failed to get Gutenberg blocks: ${errorData.message || response.statusText}.\nRequested URL: ${url}`);
}
const blocks = await response.json();
return {
blocks: blocks,
content: [{
type: "text",
text: `Available Gutenberg blocks:\n${JSON.stringify(blocks, null, 2)}`
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `wp_api_get_gutenberg_blocks failed: ${error.message}`);
}
throw new McpError(ErrorCode.InternalError, 'wp_api_get_gutenberg_blocks failed: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/wp-api/get-post-types.ts:
--------------------------------------------------------------------------------
```typescript
import {WordPressSite} from "types/wp-sites";
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
export const apiGetPostTypes = {
name: "wp_api_get_post_types",
description: "Get a WordPress post types using REST API",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"},
},
required: ["siteKey"]
},
async execute(args: { siteKey: string }, site: WordPressSite) {
try {
const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
const url = `${site.apiUrl}/wp/v2/types`
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Failed to get post types: ${errorData.message || response.statusText}.\nRequested URL: ${url}`);
}
const data = await response.json();
const list = Object.values(data).map((postType: any, i: number) => {
return (i + 1) + '. ' + postType.name + ' (slug: ' + postType.slug + ')';
})
return {
postTypes: data,
postTypesList: list,
content: [{
type: "text",
text: `Available post types:\n${JSON.stringify(data)}`
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `wp_api_get_post_types failed: ${error.message}`);
}
throw new McpError(ErrorCode.InternalError, 'wp_api_get_post_types failed: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/wp-api/get-templates.ts:
--------------------------------------------------------------------------------
```typescript
import {WordPressSite} from "types/wp-sites";
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
export const apiGetTemplates = {
name: "wp_api_get_templates",
description: "Get available templates for a specific post type in WordPress",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"},
postType: {type: "string", description: "The post type to fetch templates for"},
},
required: ["siteKey", "postType"]
},
async execute(args: { siteKey: string, postType: string }, site: WordPressSite) {
try {
const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
const url = `${site.apiUrl}/wp/v2/templates?post_type=${args.postType}`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Failed to get templates: ${errorData.message || response.statusText}.\nRequested URL: ${url}`);
}
const data = await response.json();
const list = data.map((template: any, i: number) => {
return (i + 1) + '. ' + template.title.raw + ' (slug: ' + template.slug + ')';
})
return {
templates: data,
templatesList: list,
content: [{
type: "text",
text: `Available templates for post type "${args.postType}":\n\n${list.join('\n')}`
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `wp_api_get_templates failed: ${error.message}`);
}
throw new McpError(ErrorCode.InternalError, 'wp_api_get_templates failed: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/wp-api/get-rest-base-for-post-types.ts:
--------------------------------------------------------------------------------
```typescript
import {WordPressSite} from "types/wp-sites";
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
import {apiGetPostTypes} from "./get-post-types.js";
export const apiGetRestBaseForPostType = {
name: "wp_api_get_rest_base_for_post_type",
description: "Get a WordPress REST API base for post type to use in REST API",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"},
postType: {type: "string", description: "Post type (default: page)"},
},
required: ["siteKey", "postType"]
},
async execute(args: { siteKey: string, postType: string }, site: WordPressSite) {
try {
const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
const url = `${site.apiUrl}/wp/v2/types/${args.postType}`
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json();
const {postTypesList} = await apiGetPostTypes.execute(args, site)
throw new Error(`Failed to get post types: ${errorData.message || response.statusText}.\nRequested URL: ${url}. Please try again with one of the following post type:\n${postTypesList.join('\n')}`);
}
const data = await response.json();
return {
postType: data,
restBase: data.rest_base,
content: [{
type: "text",
text: `API REST base for post type: ${args.postType} is a: \n${data.rest_base}`
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `wp_api_get_rest_base_for_post_types failed: ${error.message}`);
}
throw new McpError(ErrorCode.InternalError, 'wp_api_get_rest_base_for_post_types failed: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/wp-api/get-post.ts:
--------------------------------------------------------------------------------
```typescript
import {WordPressSite} from "types/wp-sites";
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
import {apiGetRestBaseForPostType} from "./get-rest-base-for-post-types.js";
export const apiGetPost = {
name: "wp_api_get_post",
description: "Retrieve a single post, page, or custom post type item using REST API",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"},
postType: {type: "string", description: "Post type (default: posts)"},
postId: {type: "integer", description: "Post ID to retrieve"}, // New property to specify post ID
},
required: ["siteKey", "postType", "postId"]
},
async execute(args: { siteKey: string, postType: string, postId: number }, site: WordPressSite) {
try {
const {restBase} = await apiGetRestBaseForPostType.execute(args, site);
const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
const url = `${site.apiUrl}/wp/v2/${restBase}/${args.postId}`; // Modified URL to fetch single post by ID
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Failed to retrieve post: ${errorData.message || response.statusText}. Requested URL: ${url}`);
}
const data = await response.json();
return {
post: data,
content: [{
type: "text",
text: `Post retrieved successfully.\n\nPost: ${JSON.stringify(data)}`
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `wp_api_get_post failed: ${error.message}`);
}
throw new McpError(ErrorCode.InternalError, 'wp_api_get_post failed: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/wp-api/delete-post.ts:
--------------------------------------------------------------------------------
```typescript
import {WordPressSite} from "types/wp-sites";
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
import {apiGetRestBaseForPostType} from "tools/wp-api/get-rest-base-for-post-types";
export const apiDeletePost = {
name: "wp_api_delete_post",
description: "Deletes a WordPress post, page, or any custom post type using REST API",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"},
postId: {type: "integer", description: "ID of the post to delete"},
postType: {type: "string", description: "Type of the post (e.g., post, page, custom type)"},
force: {type: "boolean", description: "Whether to force delete the post"}
},
required: ["siteKey", "postId", "postType"]
},
async execute(args: { siteKey: string; postId: number; postType: string; force?: boolean }, site: WordPressSite) {
try {
const {restBase} = await apiGetRestBaseForPostType.execute(args, site)
const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
const url = `${site.apiUrl}/wp/v2/${restBase}/${args.postId}?force=${args.force ?? false}`;
const response = await fetch(url, {
method: 'DELETE',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Failed to delete post: ${errorData.message || response.statusText}.\nRequested URL: ${url}`);
}
const data = await response.json();
return {
postData: data,
content: [{
type: "text",
text: `Post ${args.postId} (${args.postType}) deleted successfully.`
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `wp_api_delete_post failed: ${error.message}`);
}
throw new McpError(ErrorCode.InternalError, 'wp_api_delete_post failed: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/wp-api/get-posts.ts:
--------------------------------------------------------------------------------
```typescript
import { WordPressSite } from "types/wp-sites";
import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
import {apiGetRestBaseForPostType} from "./get-rest-base-for-post-types.js";
export const apiGetPosts = {
name: "wp_api_get_posts",
description: "Retrieve a list of posts, pages, or custom post type items using REST API",
inputSchema: {
type: "object",
properties: {
siteKey: { type: "string", description: "Site key" },
postType: { type: "string", description: "Post type (default: posts)" },
perPage: { type: "integer", description: "Number of posts to retrieve (default: 10)" },
page: { type: "integer", description: "Page number for pagination (default: 1)" }
},
required: ["siteKey", "postType"]
},
async execute(args: { siteKey: string, postType: string, perPage?: number, page?: number }, site: WordPressSite) {
try {
const {restBase} = await apiGetRestBaseForPostType.execute(args, site)
const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
const url = `${site.apiUrl}/wp/v2/${restBase}?per_page=${args.perPage || 10}&page=${args.page || 1}`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Failed to retrieve posts: ${errorData.message || response.statusText}.
Requested URL: ${url}`);
}
const data = await response.json();
return {
pluginData: data,
content: [{
type: "text",
text: `Posts retrieved successfully.\n\nPosts: ${JSON.stringify(data)}`
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `wp_api_get_posts failed: ${error.message}`);
}
throw new McpError(ErrorCode.InternalError, 'wp_api_get_posts failed: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/wp-api/get-plugins.ts:
--------------------------------------------------------------------------------
```typescript
import {WordPressSite} from "types/wp-sites";
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
export const apiGetPlugins = {
name: "wp_api_get_plugins",
description: "Get WordPress plugins (active, inactive, or all) using REST API",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"},
status: {
type: "string",
enum: ["active", "inactive", "all"],
description: "Filter plugins by status. Default is 'active'."
},
},
required: ["siteKey"],
additionalProperties: false
},
async execute(args: { siteKey: string, status?: string }, site: WordPressSite) {
try {
const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
const url = `${site.apiUrl}/wp/v2/plugins`;
// Status wtyczek domyślnie to "active", chyba że użytkownik określi inaczej
const filterStatus = args.status || "active";
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Failed to get plugins: ${errorData.message || response.statusText}.\nRequested URL: ${url}`);
}
const data = await response.json();
// Filtrujemy wtyczki zgodnie z wybranym statusem
let filteredPlugins = data;
if (filterStatus !== "all") {
filteredPlugins = data.filter((plugin: { status: string }) => plugin.status === filterStatus);
}
const pluginsList = filteredPlugins.map((plugin: any) => plugin.name + ' (' + plugin.plugin + ')')
return {
plugins: filteredPlugins,
pluginsList,
content: [{
type: "text",
text: `${filterStatus.charAt(0).toUpperCase() + filterStatus.slice(1)} plugins:\n${pluginsList.join('\n')}`
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `wp_api_get_plugins failed: ${error.message}`);
}
throw new McpError(ErrorCode.InternalError, 'wp_api_get_plugins failed: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/list-plugin-files.ts:
--------------------------------------------------------------------------------
```typescript
// src/tools/edit-file.ts
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
import fs from 'fs';
import path from 'path';
import {WordPressSite} from "../types/wp-sites";
import {listAvailablePluginsInSitePluginsPath} from "./list-available-plugins-in-site-plugins-path.js";
const listFiles = (dir: any, fileList: any[] = []) => {
if (path.basename(dir) === 'node_modules') {
return fileList; // Pomijamy katalog node_modules
}
const files = fs.readdirSync(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
listFiles(filePath, fileList);
} else {
fileList.push(filePath);
}
}
return fileList;
}
export const listPluginFiles = {
name: "wp_list_plugin_files",
description: "List a files for a specified plugin directory",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"},
pluginDirName: {type: "string", description: "Plugin directory name"}
},
required: ["pluginDirName", "siteKey"]
},
async execute(args: { pluginDirName: string, siteKey: string }, site: WordPressSite) {
if (args.pluginDirName === '.' || args.pluginDirName === '') {
throw new McpError(ErrorCode.InvalidParams, 'pluginDirName cannot be "." or empty (self directory).');
}
const blockDir = path.join(site.pluginsPath, args.pluginDirName)
if (!fs.existsSync(blockDir)) {
const {content: [{directories}]} = await listAvailablePluginsInSitePluginsPath.execute({siteKey: args.siteKey}, site)
return {
isError: true,
directories,
content: [{
type: "text",
text: `Directory ${blockDir} not exists. Please review a plugin directory. Available plugins in ${site.pluginsPath}:\n\n${directories.join('\n')}`
}]
};
}
try {
const files = listFiles(blockDir)
return {
files,
content: [{
type: "text",
text: `Files at ${blockDir}:\n\n` + files.map((f) => f.replace(blockDir, '')).join('\n')
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, error.message);
}
throw new McpError(ErrorCode.InternalError, 'wp_list_plugin_files: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/wp-api/activate-plugin.ts:
--------------------------------------------------------------------------------
```typescript
import {WordPressSite} from "types/wp-sites";
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
import {apiGetPlugins} from "tools/wp-api/get-plugins";
export const apiActivatePlugin = {
name: "wp_api_activate_plugin",
description: "Activates a WordPress plugin using REST API",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"},
pluginSlug: {type: "string", description: "Plugin slug to activate"}
},
required: ["pluginSlug", "siteKey"]
},
async execute(args: { siteKey: string; pluginSlug: string }, site: WordPressSite) {
try {
const pluginSlug = args.pluginSlug.replace('.php', '')
const allPlugins = await apiGetPlugins.execute({...args, status: 'all'}, site)
if (!allPlugins.plugins.find((p: any) => p.plugin === pluginSlug)) {
return {
isError: true,
content: [{
type: "text",
text: `Plugin ${args.pluginSlug} is invalid. Available plugins:\n\n${allPlugins.pluginsList.join('\n')}. Please try again with correct plugin slug.`
}]
};
}
const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
const url = `${site.apiUrl}/wp/v2/plugins/${pluginSlug}`
const response = await fetch(url, {
method: 'PUT',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
status: 'active'
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Failed to activate plugin: ${errorData.message || response.statusText}.\nRequested URL: ${url}`);
}
const data = await response.json();
return {
pluginData: data,
content: [{
type: "text",
text: `Plugin ${args.pluginSlug} activated successfully.\n\nPlugin: ${data}`
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `wp_api_activate_plugin failed: ${error.message}`);
}
throw new McpError(ErrorCode.InternalError, 'wp_api_activate_plugin failed: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/wp-api/deactivate-plugin.ts:
--------------------------------------------------------------------------------
```typescript
import {WordPressSite} from "types/wp-sites";
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
import {apiGetPlugins} from "tools/wp-api/get-plugins";
export const apiDeactivatePlugin = {
name: "wp_api_deactivate_plugin",
description: "Deactivates a WordPress plugin using REST API",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"},
pluginSlug: {type: "string", description: "Plugin slug to deactivate"}
},
required: ["pluginSlug", "siteKey"]
},
async execute(args: { siteKey: string; pluginSlug: string }, site: WordPressSite) {
try {
const pluginSlug = args.pluginSlug.replace('.php', '')
const allPlugins = await apiGetPlugins.execute({...args, status: 'active'}, site)
if (!allPlugins.plugins.find((p: any) => p.plugin === pluginSlug)) {
return {
isError: true,
content: [{
type: "text",
text: `Plugin ${args.pluginSlug} is not active or invalid. Active plugins:\n${allPlugins.pluginsList.join('\n')}. Please try again with a correct plugin slug.`
}]
};
}
const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
const url = `${site.apiUrl}/wp/v2/plugins/${pluginSlug}`
const response = await fetch(url, {
method: 'PUT',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
status: 'inactive'
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Failed to deactivate plugin: ${errorData.message || response.statusText}.\nRequested URL: ${url}`);
}
const data = await response.json();
return {
pluginData: data,
content: [{
type: "text",
text: `Plugin ${args.pluginSlug} deactivated successfully.\n\nPlugin: ${data}`
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `wp_api_deactivate_plugin failed: ${error.message}`);
}
throw new McpError(ErrorCode.InternalError, 'wp_api_deactivate_plugin failed: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/wp-api/site-settings/update-integer-site-setting.ts:
--------------------------------------------------------------------------------
```typescript
import {WordPressSite} from "types/wp-sites";
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
// Lista dozwolonych kluczy ustawień zgodnie z dokumentacją WordPress REST API
const VALID_SETTING_KEYS = [
"post_per_page", "page_on_front", "page_for_posts", "site_logo", "site_icon", "default_category", "start_of_week"
] as const;
type SettingKey = (typeof VALID_SETTING_KEYS)[number];
export const apiUpdateIntegerSiteSetting = {
name: "wp_api_update_integer_site_setting",
description: "Updates a single WordPress site setting in integer format using REST API",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"},
settingKey: {
type: "string",
enum: VALID_SETTING_KEYS, // Walidacja poprawnych kluczy
description: "The key of the setting to update"
},
settingValue: {type: "integer", description: "The new value for the setting"}
},
required: ["siteKey", "settingKey", "settingValue"]
},
async execute(args: { siteKey: string; settingKey: SettingKey; settingValue: any }, site: WordPressSite) {
try {
if (!VALID_SETTING_KEYS.includes(args.settingKey)) {
throw new Error(`Invalid setting key: ${args.settingKey}. Allowed values: ${VALID_SETTING_KEYS.join(", ")}`);
}
const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
const url = `${site.apiUrl}/wp/v2/settings`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({[args.settingKey]: args.settingValue})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Failed to update setting: ${errorData.message || response.statusText}`);
}
const data = await response.json();
return {
settings: data,
content: [{
type: "text",
text: `Site settings updated successfully.\n\nNew settings object: ${JSON.stringify(data, null, 2)}`
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `wp_api_update_integer_site_setting failed: ${error.message}`);
}
throw new McpError(ErrorCode.InternalError, 'wp_api_update_integer_site_setting failed: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/wp-api/site-settings/update-string-site-setting.ts:
--------------------------------------------------------------------------------
```typescript
import {WordPressSite} from "types/wp-sites";
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
// Lista dozwolonych kluczy ustawień zgodnie z dokumentacją WordPress REST API
const VALID_SETTING_KEYS = [
"title", "description", "url", "email", "timezone", "date_format", "time_format",
"language", "default_post_format", "show_on_front",
"default_ping_status", "default_comment_status"
] as const;
type SettingKey = (typeof VALID_SETTING_KEYS)[number];
export const apiUpdateStringSiteSetting = {
name: "wp_api_update_string_site_setting",
description: "Updates a single WordPress site setting in string format using REST API",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"},
settingKey: {
type: "string",
enum: VALID_SETTING_KEYS, // Walidacja poprawnych kluczy
description: "The key of the setting to update"
},
settingValue: {type: "string", description: "The new value for the setting"}
},
required: ["siteKey", "settingKey", "settingValue"]
},
async execute(args: { siteKey: string; settingKey: SettingKey; settingValue: any }, site: WordPressSite) {
try {
if (!VALID_SETTING_KEYS.includes(args.settingKey)) {
throw new Error(`Invalid setting key: ${args.settingKey}. Allowed values: ${VALID_SETTING_KEYS.join(", ")}`);
}
const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
const url = `${site.apiUrl}/wp/v2/settings`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({[args.settingKey]: args.settingValue})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Failed to update setting: ${errorData.message || response.statusText}`);
}
const data = await response.json();
return {
settings: data,
content: [{
type: "text",
text: `Site settings updated successfully.\n\nNew settings object: ${JSON.stringify(data, null, 2)}`
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `wp_api_update_string_site_setting failed: ${error.message}`);
}
throw new McpError(ErrorCode.InternalError, 'wp_api_update_string_site_setting failed: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/wp-api/update-post-status.ts:
--------------------------------------------------------------------------------
```typescript
import {WordPressSite} from "types/wp-sites";
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
import {apiGetRestBaseForPostType} from "./get-rest-base-for-post-types.js";
export const apiUpdatePostStatus = {
name: "wp_api_update_post_status",
description: "Update the status of an existing WordPress post, page, or custom post type item using REST API",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"},
postId: {type: "integer", description: "ID of the post to update"},
postType: {type: "string", description: "Post type (default: posts)"},
status: {type: "string", enum: ["publish", "draft"], description: "Status to set (publish or draft)"},
publishDate: {
type: "string",
format: "date-time",
description: "Optional scheduled publication date in ISO 8601 format"
}
},
required: ["siteKey", "postId", "postType", "status"]
},
async execute(args: {
siteKey: string,
postId: number,
postType: string,
status: string,
publishDate?: string
}, site: WordPressSite) {
try {
const {restBase} = await apiGetRestBaseForPostType.execute(args, site)
const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
const url = `${site.apiUrl}/wp/v2/${restBase}/${args.postId}`;
const bodyData: any = {status: args.status};
if (args.status === "publish" && args.publishDate) {
bodyData.date = args.publishDate;
}
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(bodyData)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Failed to update post status: ${errorData.message || response.statusText}.
Requested URL: ${url}`);
}
const data = await response.json();
return {
pluginData: data,
content: [{
type: "text",
text: `Post status updated successfully.\nPost #ID: ${data.id}\nPost URL: ${data.link}\nPost status: ${data.status}\nPost type: ${data.type}\nPost title: ${data.title?.rendered}`
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `wp_api_update_post_status failed: ${error.message}`);
}
throw new McpError(ErrorCode.InternalError, 'wp_api_update_post_status failed: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/wp-api/update-post.ts:
--------------------------------------------------------------------------------
```typescript
import {WordPressSite} from "types/wp-sites";
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
import {apiGetRestBaseForPostType} from "./get-rest-base-for-post-types.js";
export const apiUpdatePost = {
name: "wp_api_update_post",
description: "Update the settings (i.e. template, title, excerpt) of a WordPress post with post type using REST API. THIS TOOL IS NOT DESIGNED TO UPDATE POST CONTENT.",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"},
postId: {type: "number", description: "Post ID to update"},
postType: {type: "string", description: "Type of the post (e.g., posts, pages)"},
template: {type: "string", description: "Optional: Post template"},
title: {type: "string", description: "Optional: Post title"},
excerpt: {type: "string", description: "Optional: Post excerpt"},
},
required: ["siteKey", "postId", "postType"]
},
async execute(args: {
siteKey: string,
postId: number,
postType: string,
title?: string,
template?: string
excerpt?: string
}, site: WordPressSite) {
try {
const {restBase} = await apiGetRestBaseForPostType.execute(args, site)
const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
const url = `${site.apiUrl}/wp/v2/${restBase}/${args.postId}`;
const bodyData: any = {}
if (args.title) {
bodyData.title = args.title
}
if (args.template) {
bodyData.template = args.template
}
if (args.excerpt) {
bodyData.excerpt = args.excerpt
}
// @ts-ignore
if (args.content) {
throw new Error(`Failed. To update post content use special tool: wp_api_update_post_content`);
}
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(bodyData)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Failed to update post: ${errorData.message || response.statusText}.\nRequested URL: ${url}`);
}
const updatedPost = await response.json();
return {
updatedPost,
content: [{
type: "text",
text: `Post settings updated successfully! Preview URL: ${updatedPost.link}.`
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `wp_api_update_post failed: ${error.message}`);
}
throw new McpError(ErrorCode.InternalError, 'wp_api_update_post failed: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/wp-api/update-post-content.ts:
--------------------------------------------------------------------------------
```typescript
import {WordPressSite} from "types/wp-sites";
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
import {apiGetRestBaseForPostType} from "./get-rest-base-for-post-types.js";
import {apiGetPost} from "tools/wp-api/get-post";
export const apiUpdatePostContent = {
name: "wp_api_update_post_content",
description: "Update the content of a WordPress post with post type using REST API",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"},
postId: {type: "number", description: "Post ID to update"},
postType: {type: "string", description: "Type of the post (e.g., posts, pages)"},
content: {type: "string", description: "Optional: New content for the post (only if content changed)"},
readCurrentContent: {
type: "boolean",
description: "Determine, you need a current version of post content to update context or just update content."
},
},
required: ["siteKey", "postId", "postType", "readCurrentContent"]
},
async execute(args: {
siteKey: string,
postId: number,
postType: string,
content: string,
readCurrentContent: boolean
}, site: WordPressSite) {
try {
if (args.readCurrentContent) {
const {post} = await apiGetPost.execute(args, site)
return {
isError: true,
content: [{
type: "text",
text: `Please generate a new post content from current content revision:\n\n${post?.content?.rendered}`
}]
};
}
const {restBase} = await apiGetRestBaseForPostType.execute(args, site)
const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
const url = `${site.apiUrl}/wp/v2/${restBase}/${args.postId}`;
const bodyData: any = {
content: args.content
}
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(bodyData)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`Failed to update post content: ${errorData.message || response.statusText}.\nRequested URL: ${url}`);
}
const updatedPost = await response.json();
return {
updatedPost,
content: [{
type: "text",
text: `Post content updated successfully! Preview URL: ${updatedPost.link}.`
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `wp_api_update_post_content failed: ${error.message}`);
}
throw new McpError(ErrorCode.InternalError, 'wp_api_update_post_content failed: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/build-block.ts:
--------------------------------------------------------------------------------
```typescript
// src/tools/build-block.ts
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
import {execSync} from 'child_process';
import {isGutenbergBlock} from "../helpers.js";
import {WordPressSite} from "../types/wp-sites";
import path from "path";
export interface BuildBlockArgs {
blockPluginDirname: string;
siteKey: string;
}
export const buildBlock = {
name: "wp_build_block",
description: "Builds a Gutenberg block using npm run build",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"},
blockPluginDirname: {
type: "string",
description: "Block plugin directory name."
}
},
required: ["blockPluginDirname", "siteKey"]
},
execute: async (args: BuildBlockArgs, site: WordPressSite) => {
const blockDir = path.join(site.pluginsPath, args.blockPluginDirname);
if (!(await isGutenbergBlock(blockDir))) {
throw new Error(`wp_build_block failed: directory ${blockDir} does not contain a Gutenberg block. Are you sure ${blockDir} is valid?`);
}
try {
try {
execSync('npm install --no-audit --no-fund --silent', {
cwd: blockDir,
stdio: ['pipe', 'pipe', 'pipe'],
shell: '/bin/bash'
});
} catch (error) {
throw new Error(`wp_build_block: npm install failed with error - ${error instanceof Error ? error.message : String(error)} in ${blockDir}`);
}
try {
const buildOutput = execSync('npm run build', {
cwd: blockDir,
stdio: ['pipe', 'pipe', 'pipe'],
shell: '/bin/bash',
encoding: 'utf-8'
});
const relevantOutput = buildOutput
.split('\n')
.filter(line =>
line.includes('webpack') ||
line.includes('compiled') ||
line.includes('error') ||
line.includes('warning')
)
.join('\n');
return {
content: [{
type: "text",
text: `✅ Block "${args.blockPluginDirname}" built successfully!\n\nBuild summary:\n${relevantOutput}`
}]
};
} catch (error) {
const errorOutput = error instanceof Error ? error.message : String(error);
const formattedError = errorOutput
.split('\n')
.filter(line =>
line.includes('error') ||
line.includes('failed') ||
line.includes('webpack')
)
.join('\n');
throw new Error(`wp_build_block failed:\n${formattedError}\n\nTry to fix a problem, but do not create a new structure on your own.`);
}
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, error.message);
}
throw new McpError(ErrorCode.InternalError, 'wp_build_block failed: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/wp-cli/check_and_instal_wp_cli.ts:
--------------------------------------------------------------------------------
```typescript
import {execSync} from "child_process";
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
import path from "path";
import {WordPressSite} from "types/wp-sites";
export const checkAndInstallWPCLI = {
name: "check_and_install_wp_cli",
description: "Checks if WP-CLI is installed. If not, installs it locally.",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"}
},
required: []
},
async execute(args: any, site: WordPressSite) {
try {
// Sprawdzenie, czy PHP jest zainstalowane
let isPhpInstalled;
try {
execSync("php -v", {stdio: "pipe"}); // Sprawdzenie wersji PHP
isPhpInstalled = true;
} catch (error) {
isPhpInstalled = false;
}
if (!isPhpInstalled) {
// Jeśli PHP nie jest zainstalowane, zwróć instrukcje
return {
content: [{
type: "text",
text: "❌ PHP is not installed. Please install PHP manually before proceeding. \n\n" +
"You can install PHP by running the following commands:\n" +
"1. On macOS (using Homebrew): `brew install php`\n" +
"2. On Ubuntu/Debian: `sudo apt update && sudo apt install php`\n" +
"3. On Windows, download PHP from https://windows.php.net/download/."
}]
};
}
// Sprawdzenie, czy WP-CLI jest zainstalowane
let isInstalled;
try {
execSync("wp --info", {stdio: "pipe"}); // Jeśli WP-CLI jest zainstalowane, to nie rzuci wyjątkiem
isInstalled = true;
} catch (error) {
isInstalled = false;
}
if (isInstalled) {
return {
content: [{type: "text", text: "✅ WP-CLI is already installed."}]
};
}
// Jeśli WP-CLI nie jest zainstalowane, pobierz i zainstaluj lokalnie
const wpCliPath = path.resolve(site.path, "wp-cli.phar");
// Pobierz WP-CLI i ustaw odpowiednie uprawnienia
execSync(`
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar &&
chmod +x wp-cli.phar
`, {stdio: "inherit", cwd: site.path}); // stdio: "inherit" pozwala na wyświetlanie wyników w terminalu
// Sprawdzenie, czy WP-CLI działa poprawnie
let isNowInstalled;
try {
execSync(`php "${wpCliPath}" --info`, {stdio: "pipe"});
isNowInstalled = true;
} catch (error) {
isNowInstalled = false;
}
if (!isNowInstalled) {
throw new Error(`WP-CLI installation failed. CLI Path: ${wpCliPath}`);
}
return {
content: [{type: "text", text: "✅ WP-CLI was successfully installed locally."}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `check_and_install_wp_cli failed: ${error.message}`);
}
throw new McpError(ErrorCode.InternalError, 'check_and_install_wp_cli failed: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/scaffold-block.ts:
--------------------------------------------------------------------------------
```typescript
// src/tools/scaffold-block.ts
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
import {execSync} from 'child_process';
import path from "path";
import fs from 'fs';
import {WordPressSite} from "../types/wp-sites";
import {listPluginFiles} from "./list-plugin-files.js";
import {buildBlock} from "./build-block.js";
interface ScaffoldArgs {
name: string;
siteKey: string;
variant: string;
namespace: string;
}
export const scaffoldBlockTool = {
name: "wp_scaffold_block",
description: "Creates and builds a new Gutenberg block using @wordpress/create-block",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"},
name: {type: "string", description: "Block name"},
variant: {
type: "string",
enum: ["static", "dynamic"],
description: "Gutenberg block template variant (static or dynamic). Default: static"
},
namespace: {type: "string", description: "Gutenberg block namespace"}
},
required: ["name", "siteKey", "variant", "namespace"]
},
execute: async (args: ScaffoldArgs, site: WordPressSite) => {
if (typeof args.name !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'Block name must be a string');
}
const blockDirname = args.name.toLowerCase().replace(/ /g, '-')
const namespace = args.namespace.toLowerCase().replace(/ /g, '-')
const blockDir = path.join(site.pluginsPath, blockDirname)
const pluginDir = site.pluginsPath
if (fs.existsSync(blockDir)) {
throw new McpError(ErrorCode.InvalidRequest, `wp_scaffold_block failed: directory ${blockDir} already exists. Please change a block name or remove existing plugin!`);
}
try {
const variant = args.variant === 'dynamic' ? 'dynamic' : 'static'
const createBlockCommand = `npx @wordpress/create-block ${args.name} --variant ${variant} --namespace ${namespace}`
execSync(createBlockCommand, {
stdio: ['pipe', 'pipe', 'pipe'],
encoding: 'utf-8',
cwd: pluginDir
});
const buildResult = await buildBlock.execute({
blockPluginDirname: blockDirname,
siteKey: args.siteKey
}, site);
const listedPluginFiles = await listPluginFiles.execute({
pluginDirName: path.basename(blockDir),
siteKey: args.siteKey
}, site);
if (listedPluginFiles.isError || !listedPluginFiles?.files) {
throw new McpError(ErrorCode.InternalError, `wp_scaffold_block failed: directory ${path.basename(blockDir)} is unreachable or files not created.`);
}
return {
content: [{
type: "text",
text: `Block "${args.name}" created and built successfully.\n\nCreated files: ${listedPluginFiles?.files?.join('\n')}.\n\nDo you want to do any changes in this block or activate WordPress plugin and create a test page in WordPress with this block embedded?`
}]
};
} catch (error) {
console.error('Block scaffolding error:', error);
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `Failed to create/build block: ${error.message}`);
}
throw new McpError(ErrorCode.InternalError, 'Unknown error occurred while creating/building block');
}
}
};
```
--------------------------------------------------------------------------------
/src/tools/wp-api/create-post.ts:
--------------------------------------------------------------------------------
```typescript
import {WordPressSite} from "types/wp-sites";
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
import {apiGetPostTypes} from "./get-post-types.js";
import {apiGetRestBaseForPostType} from "./get-rest-base-for-post-types.js";
import {apiGetTemplates} from "./get-templates.js";
export const apiCreatePost = {
name: "wp_api_create_post",
description: "Create a WordPress post, page or custom post type item using REST API",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"},
postType: {type: "string", description: "Post type (default: pages)"},
title: {type: "string", description: "Post title"},
content: {type: "string", description: "Post content"},
parent: {type: "integer", description: "Post parent ID"},
template: {type: "string", description: "Optional: Post template"},
},
required: ["postType", "siteKey", "title"]
},
async execute(args: {
siteKey: string,
postType: string,
title: string,
content: string,
parent: Number,
template?: string
}, site: WordPressSite) {
try {
const {restBase} = await apiGetRestBaseForPostType.execute(args, site)
const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
const url = `${site.apiUrl}/wp/v2/${restBase}`
const bodyData: any = {
title: args.title,
status: 'draft',
content: args.content,
parent: args.parent,
}
if (args.template) {
const {templates, templatesList} = await apiGetTemplates.execute(args, site)
if (!templates.find((template: any) => template.slug === args.template)) {
throw new Error(`Invalid template.\nUse one of the following:\n${templatesList.join('\n')}`);
}
bodyData.template = args.template
}
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Basic ${credentials}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(bodyData)
});
if (!response.ok) {
const errorData = await response.json();
if (errorData.message.includes('No route was found matching the URL and request method')) {
const postTypes = await apiGetPostTypes.execute({
siteKey: args.siteKey
}, site)
return {
isError: true,
content: [
{
type: "text",
text: `Probably ${args.postType} is invalid.\nUsed REST API URL: ${url}.\nPlease select a proper post type from a list: ${Object.keys(postTypes.postTypes).join(', ')}.`
}
]
}
}
throw new Error(`Failed to create post: ${errorData.message || response.statusText}.\nRequested URL: ${url}`);
}
const data = await response.json();
return {
pluginData: data,
content: [{
type: "text",
text: `Post created successfully.\n\nPost: ${JSON.stringify(data, null, 2)}`
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, `wp_api_create_post failed: ${error.message}`);
}
throw new McpError(ErrorCode.InternalError, 'wp_api_create_post failed: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/helpers.ts:
--------------------------------------------------------------------------------
```typescript
// src/helpers.ts
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
import fs from 'fs/promises';
import path from 'path';
import {execSync} from "child_process";
export interface WordPressSite {
localWpSshEntryFile: string;
cli?: string;
name: string;
path: string;
pluginsPath: string;
aliases?: string[];
apiUrl: string;
apiCredentials?: {
username: string;
password: string;
};
}
export interface WPSitesConfig {
sites: {
[key: string]: WordPressSite;
};
}
export interface ToolArguments {
siteKey: string;
}
export function similarity(s1: string, s2: string): number {
s1 = s1.toLowerCase();
s2 = s2.toLowerCase();
if (s1.includes(s2) || s2.includes(s1)) return 0.8;
let longer = s1;
let shorter = s2;
if (s1.length < s2.length) {
longer = s2;
shorter = s1;
}
const longerLength = longer.length;
if (longerLength === 0) return 1.0;
const distance = longer.split('')
.filter(char => !shorter.includes(char)).length;
return (longerLength - distance) / longerLength;
}
export function findMatchingSites(config: WPSitesConfig, searchTerm?: string): Array<[string, WordPressSite, number]> {
if (!searchTerm) {
return Object.entries(config.sites)
.map(([key, site]) => [key, site, 1] as [string, WordPressSite, number]);
}
return Object.entries(config.sites)
.map(([key, site]) => {
const keyMatch = similarity(key, searchTerm);
const nameMatch = similarity(site.name, searchTerm);
const aliasMatches = (site.aliases || [])
.map(alias => similarity(alias, searchTerm));
const maxScore = Math.max(keyMatch, nameMatch, ...aliasMatches);
return [key, site, maxScore] as [string, WordPressSite, number];
})
.filter(([, , score]) => score > 0.3)
.sort((a, b) => b[2] - a[2]);
}
export function formatSitesList(sites: Array<[string, WordPressSite, number]>): string {
return sites
.map(([key, site], index) =>
`${index + 1}. ${key} (${site.name})`)
.join('\n');
}
export async function validateAndGetSite(config: WPSitesConfig, siteArg?: string): Promise<WordPressSite> {
const matches = findMatchingSites(config, siteArg);
if (matches.length === 0) {
const availableSites = formatSitesList(findMatchingSites(config));
throw new McpError(
ErrorCode.InvalidParams,
`No matching sites found. Available sites:\n${availableSites}`
);
}
if (matches.length === 1) {
const [key, site] = matches[0];
return site;
}
const sitesList = formatSitesList(matches);
throw new McpError(
ErrorCode.InvalidParams,
`Multiple matching sites found. Please choose which one to use and try again:\n${sitesList}`
);
}
export function validateSiteToolArguments(args: unknown): asserts args is ToolArguments {
if (!args || typeof args !== 'object') {
throw new McpError(ErrorCode.InvalidParams, 'Arguments must be an object');
}
const typedArgs = args as Record<string, unknown>;
if (typedArgs.siteKey !== undefined && typeof typedArgs.siteKey !== 'string') {
throw new McpError(ErrorCode.InvalidParams, 'siteKey must be a string if provided');
}
}
export async function isGutenbergBlock(directory: string): Promise<boolean> {
try {
// Sprawdź czy istnieje package.json
const packageJsonPath = path.join(directory, 'package.json');
const packageJsonExists = await fs.access(packageJsonPath)
.then(() => true)
.catch(() => false);
if (!packageJsonExists) return false;
// Sprawdź zawartość package.json
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
// Sprawdź charakterystyczne zależności dla bloków Gutenberga
const dependencies = {
...packageJson.dependencies,
...packageJson.devDependencies
};
return (
'@wordpress/scripts' in dependencies
);
} catch {
return false;
}
}
export async function shouldRebuildBlock(directory: string): Promise<boolean> {
return isGutenbergBlock(directory);
}
// Funkcja sprawdzająca, czy WP-CLI jest zainstalowane lokalnie
export const checkLocalWPCLI = (cwd: string) => {
try {
execSync(`php "${path.resolve(cwd, 'wp-cli.phar')}" --info`, {stdio: "pipe"});
return true; // WP-CLI jest zainstalowane lokalnie
} catch (error) {
return false; // WP-CLI nie jest zainstalowane lokalnie
}
};
export const checkGlobalWPCLI = () => {
try {
execSync("wp --info", {stdio: "pipe"});
return true; // WP-CLI jest zainstalowane globalnie
} catch (error) {
return false; // WP-CLI nie jest zainstalowane globalnie
}
};
```
--------------------------------------------------------------------------------
/src/tools/filesystem/edit-block-json-file.ts:
--------------------------------------------------------------------------------
```typescript
// src/tools/edit-file.ts
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
import {promises as fs} from 'fs';
import path from 'path';
import {isGutenbergBlock} from '../../helpers.js';
import {WordPressSite} from "../../types/wp-sites.js";
import {buildBlock} from "./../build-block.js";
import {listPluginFiles} from "tools/list-plugin-files";
interface EditFileArgs {
filePath: string;
content: object;
blockPluginDirname: string;
siteKey: string;
didUserConfirmChanges: boolean;
}
export const editBlockJsonFile = {
name: "wp_edit_block_json_file",
description: "Edits a file: block.json in WordPress Gutenberg Block Plugin with automatic rebuild detection and block.json file validation.",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"},
blockPluginDirname: {
type: "string",
description: "Block plugin directory name."
},
filePath: {
type: "string",
description: "Path to the block.json file relative to the plugin root."
},
content: {
type: "object",
description: "New content for the block.json file in JSON format"
},
didUserConfirmChanges: {
type: "boolean",
description: "Are you sure about this changes from new content?"
}
},
required: ["filePath", "blockPluginDirname", "siteKey", "content", "didUserConfirmChanges"]
},
async execute(args: EditFileArgs, site: WordPressSite) {
const blockDir = path.join(site.pluginsPath, args.blockPluginDirname)
const fullPath = path.join(blockDir, args.filePath);
if (!(await isGutenbergBlock(blockDir))) {
throw new Error(`wp_edit_block_json_file failed: directory ${blockDir} does not contain a Gutenberg block. Are you sure ${blockDir} is valid?`);
}
if (!args.filePath.endsWith('block.json')) {
throw new Error(`wp_edit_block_json_file failed: this tool can only edit block.json file. Are you sure ${fullPath} is valid for this action?`);
}
try {
try {
await fs.access(fullPath);
} catch {
const availableFiles = await listPluginFiles.execute({
...args,
pluginDirName: args.blockPluginDirname
}, site)
throw new Error(`File block.json not found: ${fullPath}. Available files within dir ${blockDir}:\n${availableFiles?.files?.join('\n')}`);
}
let originalContent: string;
try {
originalContent = await fs.readFile(fullPath, 'utf-8');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown read error';
throw new Error(`Failed to read current block.json file: ${errorMessage}`);
}
let warnings = [];
try {
let newContent: any = args.content;
if (newContent.style !== 'file:./style-index.css') {
warnings.push('Property "style" in block.json is not default. Default value should be: "file:./style-index.css". If you have changed in intentionally it is ok.')
}
if (newContent.editorStyle !== "file:./index.css") {
warnings.push('Property "editorStyle" in block.json is not default. Default value should be: "file:./index.css". If you have changed in intentionally it is ok.')
}
if (newContent.render) {
warnings.push('Property "render" in block.json exists, so it is dynamic block. In this case there should not be save.js file for block, but render file is needed.')
}
if (args.didUserConfirmChanges) {
await fs.writeFile(fullPath, JSON.stringify(newContent), 'utf-8');
} else {
return {
isError: true,
content: [{
type: "text",
text: `Are you sure to make changes to block.json file within ${fullPath}\nNew content:\n${JSON.stringify(newContent, null, 2)}.\n\nWarnings:\n${warnings.join('\n')}\n\nPlease ask user what he think! Let user confirm or not your changes.`
}]
};
}
} catch (error) {
throw new Error(`New content cannot be parsed: ${args.content}`);
}
const buildResult = await buildBlock.execute({
blockPluginDirname: args.blockPluginDirname,
siteKey: args.siteKey
}, site);
return {
content: [{
type: "text",
text: `block.json file: ${fullPath} edited successfully.\n\nBuild output:\n${buildResult.content[0].text}\n\nSome warnings about block.json file found:\n${warnings.join('\n')}`
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, error.message);
}
throw new McpError(ErrorCode.InternalError, 'wp_edit_block_json_file: Unknown error occurred');
}
}
};
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
// src/index.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from "@modelcontextprotocol/sdk/types.js";
import fs from 'fs';
import { validateAndGetSite, validateSiteToolArguments, WPSitesConfig } from './helpers.js';
import { scaffoldBlockTool } from 'tools/scaffold-block.js';
import { listPluginFiles } from "tools/list-plugin-files.js";
import { listAvailablePluginsInSitePluginsPath } from "tools/list-available-plugins-in-site-plugins-path.js";
import { buildBlock } from "tools/build-block.js";
import { editBlockFile } from "tools/filesystem/edit-block-file.js";
import { apiActivatePlugin } from "tools/wp-api/activate-plugin.js";
import { apiCreatePost } from "tools/wp-api/create-post.js";
import { apiUpdatePostStatus } from "tools/wp-api/update-post-status.js";
import { apiGetPosts } from "tools/wp-api/get-posts.js";
import { apiGetPostTypes } from "tools/wp-api/get-post-types.js";
import { apiGetPlugins } from "tools/wp-api/get-plugins.js";
import { apiUpdatePost } from "tools/wp-api/update-post.js";
import { apiGetGutenbergBlocks } from "tools/wp-api/get-block-types.js";
import { apiGetTemplates } from "tools/wp-api/get-templates.js";
import { apiGetRestBaseForPostType } from "tools/wp-api/get-rest-base-for-post-types.js";
import { apiUpdatePostContent } from "tools/wp-api/update-post-content.js";
import { apiGetPost } from "tools/wp-api/get-post.js";
import { cliInstallAndActivatePlugin } from "tools/wp-cli/instal-and-activate-plugin";
import { editBlockJsonFile } from "tools/filesystem/edit-block-json-file";
import { WordPressSite } from "types/wp-sites";
import { apiDeactivatePlugin } from "tools/wp-api/deactivate-plugin";
import { apiDeletePost } from "tools/wp-api/delete-post";
import { apiGetSiteSettings } from "tools/wp-api/site-settings/get-site-settings";
import { apiUpdateStringSiteSetting } from "tools/wp-api/site-settings/update-string-site-setting";
import { apiGetPostPreviewLink } from "tools/wp-api/post/get-post-preview-link";
import { apiUpdateIntegerSiteSetting } from "tools/wp-api/site-settings/update-integer-site-setting";
const tools = [
editBlockFile,
scaffoldBlockTool,
listPluginFiles,
listAvailablePluginsInSitePluginsPath,
buildBlock,
editBlockJsonFile,
apiActivatePlugin,
apiDeactivatePlugin,
apiCreatePost,
apiDeletePost,
apiUpdatePostStatus,
apiGetPosts,
apiGetPost,
apiGetPostTypes,
apiGetPlugins,
apiUpdatePost,
apiGetGutenbergBlocks,
apiGetTemplates,
apiGetRestBaseForPostType,
apiUpdatePostContent,
apiGetSiteSettings,
apiUpdateStringSiteSetting,
apiUpdateIntegerSiteSetting,
apiGetPostPreviewLink,
// apiGetPostContentBeforeChanges,
cliInstallAndActivatePlugin,
];
async function loadSiteConfig(): Promise<WPSitesConfig> {
const configPath = process.env.WP_SITES_PATH || "/Users/michael/Code/mcp-test-server/wp-sites.json";
if (!configPath) {
throw new Error("WP_SITES_PATH environment variable is required");
}
try {
const configData = await fs.readFileSync(configPath, {encoding: 'utf8'});
return JSON.parse(configData) as WPSitesConfig;
} catch (error) {
if (error instanceof Error) {
if ('code' in error && error.code === 'ENOENT') {
throw new Error(`Config file not found at: ${configPath}`);
}
throw new Error(`Failed to load config: ${error.message}`);
}
throw new Error('An unknown error occurred while loading config');
}
}
async function main() {
try {
const config = await loadSiteConfig();
const server = new Server({
name: "wordpress-mcp",
version: "1.0.0"
}, {
capabilities: { tools: {} }
});
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: tools.map(tool => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema
}))
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const {name, arguments: rawArgs} = request.params;
validateSiteToolArguments(rawArgs);
const {siteKey} = rawArgs;
let site: WordPressSite;
try {
site = await validateAndGetSite(config, siteKey);
} catch (error) {
if (error instanceof Error) {
return {
isError: true,
content: [{
type: "text",
text: `❌ ${error.message}`
}]
};
}
throw error
}
const tool = tools.find(t => t.name === name);
if (!tool) {
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
if (name.startsWith('wp_cli_')) {
let cliWrapper = '{{cmd}}'
if (site.cli === 'localwp') {
if (!site.localWpSshEntryFile || !fs.existsSync(site.localWpSshEntryFile)) {
return {
isError: true,
content: [{
type: "text",
text: `❌ Option: "localWpSshEntryFile" for site "${siteKey}" is not specified in wp-sites.json while option "cli" is "localwp"`
}]
};
}
cliWrapper = `echo $(SHELL= && source "${site.localWpSshEntryFile}" &>/dev/null && {{cmd}})`
} else {
return {
isError: true,
content: [
{
type: "text",
text: `❌ Option: "cli" for site "${siteKey}" is not specified in wp-sites.json`
}
]
};
}
// @ts-ignore
return await tool.execute({
...rawArgs,
siteKey
}, site, cliWrapper);
}
// @ts-ignore
return await tool.execute({
...rawArgs,
siteKey
}, site);
});
const transport = new StdioServerTransport();
await server.connect(transport);
// console.error(`WordPress MCP server started with ${Object.keys(config.sites).length} site(s) configured`);
} catch (error) {
console.error(`Server failed to start: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
main();
```
--------------------------------------------------------------------------------
/src/tools/filesystem/edit-block-file.ts:
--------------------------------------------------------------------------------
```typescript
// src/tools/edit-file.ts
import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
import {promises as fs} from 'fs';
import path from 'path';
import {isGutenbergBlock} from '../../helpers.js';
import {WordPressSite} from "../../types/wp-sites.js";
import {buildBlock} from "./../build-block.js";
// Definicje typów dla operacji edycji plików
interface FileOperation {
type: 'write' | 'append' | 'modify' | 'smart_modify';
content?: string;
searchValue?: string;
replaceValue?: string;
}
interface EditFileArgs {
filePath: string;
operation: FileOperation['type'];
content?: string;
searchValue?: string;
replaceValue?: string;
directory: string;
blockPluginDirname: string;
siteKey: string;
}
// Wzorce komentarzy wskazujące na zachowanie istniejącego kodu
const KEEP_CODE_PATTERNS = [
/\/\/ *(?:rest of|remaining|other) code (?:remains |stays )?(?:the )?same/i,
/\/\/ *\.\.\./,
/\/\* *\.\.\.? *\*\//,
/\{?\s*\.\.\.\s*\}?/,
/\/\/ *no changes/i,
/\/\/ *unchanged/i
];
// Funkcje pomocnicze
function hasCodePlaceholder(content: string): boolean {
return KEEP_CODE_PATTERNS.some(pattern => pattern.test(content));
}
async function mergeWithExistingCode(originalContent: string, newContent: string): Promise<string> {
if (!hasCodePlaceholder(newContent)) {
return newContent;
}
const originalLines = originalContent.split('\n');
const newLines = newContent.split('\n');
const result: string[] = [];
let isKeepingOriginal = false;
for (let i = 0; i < newLines.length; i++) {
const currentLine = newLines[i];
if (hasCodePlaceholder(currentLine)) {
if (i > 0 && i < newLines.length - 1) {
const previousLine = newLines[i - 1];
const nextLine = newLines[i + 1];
const startIndex = originalLines.findIndex(line => line.includes(previousLine));
const endIndex = originalLines.findIndex((line, idx) => idx > startIndex && line.includes(nextLine));
if (startIndex !== -1 && endIndex !== -1) {
result.push(...originalLines.slice(startIndex + 1, endIndex));
continue;
}
}
isKeepingOriginal = true;
continue;
}
if (!isKeepingOriginal) {
result.push(currentLine);
} else {
isKeepingOriginal = false;
}
}
return result.join('\n');
}
export const editBlockFile = {
name: "wp_edit_block_file",
description: "Edits a common file in WordPress Gutenberg Block Plugin with automatic rebuild detection",
inputSchema: {
type: "object",
properties: {
siteKey: {type: "string", description: "Site key"},
blockPluginDirname: {
type: "string",
description: "Block plugin directory name."
},
filePath: {
type: "string",
description: "Path to the file relative to the plugin root."
},
content: {
type: "string",
description: "New content for the file"
},
operation: {
type: "string",
enum: ["write", "append", "modify", "smart_modify"],
description: "Operation to perform on the file"
},
searchValue: {
type: "string",
description: "Text to search for when using modify operation"
},
replaceValue: {
type: "string",
description: "Text to replace with when using modify operation"
}
},
required: ["filePath", "operation", "blockPluginDirname", "siteKey"]
},
async execute(args: EditFileArgs, site: WordPressSite) {
const blockDir = path.join(site.pluginsPath, args.blockPluginDirname)
const fullPath = path.join(blockDir, args.filePath);
if (!(await isGutenbergBlock(blockDir))) {
throw new Error(`wp_edit_block_file failed: directory ${blockDir} does not contain a Gutenberg block. Are you sure ${blockDir} is valid?`);
}
if (args.filePath.endsWith('block.json')) {
throw new Error(`This tool cannot edit block.json file. Please use another tool: wp_edit_block_json_file instead and try again.`);
}
if (args.filePath.includes('/build/')) {
throw new Error(`Files within "build" directory shouldn't be edited directly.`);
}
try {
try {
await fs.access(fullPath);
} catch {
throw new Error(`File not found: ${fullPath}`);
}
let originalContent: string;
try {
originalContent = await fs.readFile(fullPath, 'utf-8');
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown read error';
throw new Error(`Failed to read file: ${errorMessage}`);
}
let newContent = originalContent;
switch (args.operation) {
case 'smart_modify':
if (!args.content) {
throw new Error('Content is required for smart_modify operation');
}
newContent = await mergeWithExistingCode(originalContent, args.content);
break;
case 'write':
if (!args.content) {
throw new Error('Content is required for write operation');
}
newContent = args.content;
break;
case 'append':
if (!args.content) {
throw new Error('Content is required for append operation');
}
newContent = originalContent + '\n' + args.content;
break;
case 'modify':
if (!args.searchValue || !args.replaceValue) {
throw new Error('searchValue and replaceValue are required for modify operation');
}
newContent = originalContent.replace(
new RegExp(args.searchValue, 'g'),
args.replaceValue
);
break;
default:
throw new Error(`Unknown operation: ${args.operation}`);
}
await fs.writeFile(fullPath, newContent, 'utf-8');
try {
const buildResult = await buildBlock.execute({
blockPluginDirname: args.blockPluginDirname,
siteKey: args.siteKey
}, site);
return {
content: [{
type: "text",
text: `Block file: ${fullPath} edited successfully.\n\nBuild output:\n${buildResult.content[0].text}`
}]
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown build error';
throw new Error(`Block file: ${fullPath} edited but build failed: ${errorMessage}. Please try build again on check error logs on your own.`);
}
return {
content: [{
type: "text",
text: `Block file: ${fullPath} edited successfully.`
}]
};
} catch (error) {
if (error instanceof Error) {
throw new McpError(ErrorCode.InternalError, error.message);
}
throw new McpError(ErrorCode.InternalError, 'wp_edit_block_file: Unknown error occurred');
}
}
};
```