# 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:
--------------------------------------------------------------------------------
```
1 | /node_modules/
2 | /.idea/
3 | wp-sites.json
4 | /wp-sites.json.bak
5 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # mcp-wordpress-gutenberg
2 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "ESNext",
5 | "moduleResolution": "node",
6 | "esModuleInterop": true,
7 | "strict": true,
8 | "skipLibCheck": true,
9 | "forceConsistentCasingInFileNames": true,
10 | "resolveJsonModule": true,
11 | "allowJs": true,
12 | "declaration": false,
13 | "moduleDetection": "force",
14 | "paths": {
15 | "*": ["./src/*"]
16 | },
17 | "baseUrl": ".",
18 | "removeComments": true
19 | },
20 | "include": ["src/**/*"],
21 | "exclude": ["node_modules", "dist"]
22 | }
23 |
```
--------------------------------------------------------------------------------
/src/types/wp-sites.ts:
--------------------------------------------------------------------------------
```typescript
1 | // src/types/wp-sites.ts
2 | export interface WordPressSite {
3 | cli?: string;
4 | localWpSshEntryFile?: string;
5 | name: string;
6 | path: string; // Ścieżka do instalacji WordPress
7 | pluginsPath: string; // Ścieżka do instalacji WordPress
8 | apiUrl: string; // URL do WP REST API
9 | apiCredentials?: {
10 | username: string;
11 | password: string;
12 | };
13 | database?: {
14 | host: string;
15 | port: number;
16 | user: string;
17 | password: string;
18 | name: string;
19 | };
20 | }
21 |
22 | export interface WPSitesConfig {
23 | sites: Record<string, WordPressSite>;
24 | }
```
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
```javascript
1 | import typescript from "rollup-plugin-typescript2";
2 | import resolve from "@rollup/plugin-node-resolve";
3 | import commonjs from "@rollup/plugin-commonjs";
4 | import json from "@rollup/plugin-json";
5 |
6 | export default {
7 | input: "src/index.ts",
8 | output: {
9 | file: "dist/index.js",
10 | format: "esm", // Używamy ESM, aby działało z "type": "module"
11 | sourcemap: true
12 | },
13 | plugins: [
14 | resolve(),
15 | commonjs(),
16 | json(),
17 | typescript({
18 | tsconfig: "./tsconfig.json",
19 | }),
20 | ],
21 | external: ["fs", "path", "process"], // Nie bundlujemy wbudowanych modułów Node.js
22 | };
23 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "wordpress-mcp",
3 | "version": "1.0.0",
4 | "type": "module",
5 | "description": "WordPress MCP Server",
6 | "main": "dist/index.js",
7 | "scripts": {
8 | "clean": "rimraf dist",
9 | "build": "npm run clean && rollup -c",
10 | "start": "node dist/index.js"
11 | },
12 | "dependencies": {
13 | "@modelcontextprotocol/sdk": "^1.5.0",
14 | "node-fetch": "^3.3.2"
15 | },
16 | "devDependencies": {
17 | "@rollup/plugin-commonjs": "^28.0.2",
18 | "@rollup/plugin-json": "^6.1.0",
19 | "@rollup/plugin-node-resolve": "^16.0.0",
20 | "@types/node": "^18.15.11",
21 | "rimraf": "^5.0.0",
22 | "rollup": "^4.34.8",
23 | "rollup-plugin-typescript2": "^0.36.0",
24 | "typescript": "^4.9.5"
25 | }
26 | }
27 |
```
--------------------------------------------------------------------------------
/src/tools/scaffold-plugin.ts:
--------------------------------------------------------------------------------
```typescript
1 | // src/tools/scaffold-plugin.ts
2 | import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
3 | import { execSync } from 'child_process';
4 |
5 | export const scaffoldPluginTool = {
6 | name: "wp_scaffold_plugin",
7 | description: "Creates a new WordPress plugin using wp-cli scaffold plugin",
8 | inputSchema: {
9 | type: "object",
10 | properties: {
11 | site: { type: "string", description: "Site alias from configuration" },
12 | name: { type: "string", description: "Plugin name" },
13 | directory: { type: "string", description: "Optional: Custom directory path" }
14 | },
15 | required: ["name"]
16 | },
17 | execute: async (args: { name: string; directory: string }) => {
18 | try {
19 | const command = `wp scaffold plugin ${args.name} --dir=${args.directory}`;
20 | const output = execSync(command, {
21 | stdio: ['pipe', 'pipe', 'pipe'],
22 | encoding: 'utf-8'
23 | });
24 |
25 | return {
26 | content: [{
27 | type: "text",
28 | text: `Plugin "${args.name}" created successfully in ${args.directory}\n\nOutput:\n${output}`
29 | }]
30 | };
31 | } catch (error) {
32 | console.error('Plugin scaffolding error:', error);
33 | if (error instanceof Error) {
34 | throw new McpError(ErrorCode.InternalError, `Failed to create plugin: ${error.message}`);
35 | }
36 | throw new McpError(ErrorCode.InternalError, 'Unknown error occurred while creating plugin');
37 | }
38 | }
39 | };
```
--------------------------------------------------------------------------------
/src/tools/wp-api/post/get-post-content-before-changes.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {WordPressSite} from "types/wp-sites";
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 | import {apiGetPost} from "tools/wp-api/get-post";
4 |
5 | export const apiGetPostContentBeforeChanges = {
6 | name: "wp_api_get_post_content_before_changes",
7 | description: "Retrieve a single post, page, or custom post type CONTENT using REST API. Run it always before doing changes in content.",
8 | inputSchema: {
9 | type: "object",
10 | properties: {
11 | siteKey: {type: "string", description: "Site key"},
12 | postType: {type: "string", description: "Post type (default: posts)"},
13 | postId: {type: "integer", description: "Post ID to retrieve"}, // New property to specify post ID
14 | },
15 | required: ["siteKey", "postType", "postId"]
16 | },
17 |
18 | async execute(args: { siteKey: string, postType: string, postId: number }, site: WordPressSite) {
19 | try {
20 |
21 | const {post} = await apiGetPost.execute(args, site)
22 |
23 | return {
24 | post,
25 | content: [{
26 | type: "text",
27 | text: `Post retrieved successfully.\n\nCurrent ${args.postType} #${args.postId} content is:\n\n${post.content.raw}`
28 | }]
29 | };
30 | } catch (error) {
31 | if (error instanceof Error) {
32 | throw new McpError(ErrorCode.InternalError, `wp_api_get_post_content_before_changes failed: ${error.message}`);
33 | }
34 | throw new McpError(ErrorCode.InternalError, 'wp_api_get_post_content_before_changes failed: Unknown error occurred');
35 | }
36 | }
37 | };
38 |
```
--------------------------------------------------------------------------------
/src/tools/wp-cli/instal-and-activate-plugin.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {execSync} from "child_process";
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 | import {WordPressSite} from "types/wp-sites";
4 |
5 | export const cliInstallAndActivatePlugin = {
6 | name: "wp_cli_install_and_activate_plugin",
7 | description: "Installs and activates a specified WordPress plugin via WP-CLI.",
8 | inputSchema: {
9 | type: "object",
10 | properties: {
11 | siteKey: {type: "string", description: "Site key"},
12 | pluginSlug: {type: "string", description: "Plugin slug to install and activate."}
13 | },
14 | required: ["pluginSlug"]
15 | },
16 |
17 | async execute(args: { siteKey: string, pluginSlug: string }, site: WordPressSite, cliWrapper: string) {
18 | try {
19 | const pluginSlug = args.pluginSlug;
20 |
21 | const cmdInstallAndActivate = cliWrapper.replace('{{cmd}}', `wp plugin install ${pluginSlug} --activate`)
22 | const outputInstallAndActivate = execSync(cmdInstallAndActivate, {
23 | stdio: "pipe",
24 | cwd: site.path
25 | });
26 |
27 | return {
28 | content: [{
29 | type: "text",
30 | text: `✅ Plugin '${pluginSlug}' was already installed and has been activated. Output from WP-CLI:\n\n${outputInstallAndActivate}`
31 | }]
32 | };
33 |
34 | } catch (error) {
35 | if (error instanceof Error) {
36 | throw new McpError(ErrorCode.InternalError, `wp_cli_install_and_activate_plugin failed: ${error.message}`);
37 | }
38 | throw new McpError(ErrorCode.InternalError, 'wp_cli_install_and_activate_plugin failed: Unknown error occurred');
39 | }
40 | }
41 | };
42 |
```
--------------------------------------------------------------------------------
/src/tools/wp-api/post/get-post-preview-link.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {WordPressSite} from "types/wp-sites";
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 | import {apiGetPost} from "tools/wp-api/get-post";
4 |
5 | export const apiGetPostPreviewLink = {
6 | name: "wp_api_get_post_preview_link",
7 | description: "Retrieve a preview link for single post, page, or custom post type item using REST API. Especially for draft status.",
8 | inputSchema: {
9 | type: "object",
10 | properties: {
11 | siteKey: {type: "string", description: "Site key"},
12 | postType: {type: "string", description: "Post type (default: posts)"},
13 | postId: {type: "integer", description: "Post ID to retrieve"},
14 | },
15 | required: ["siteKey", "postType", "postId"]
16 | },
17 |
18 | async execute(args: { siteKey: string, postType: string, postId: number }, site: WordPressSite) {
19 | try {
20 |
21 | const {post} = await apiGetPost.execute(args, site)
22 |
23 | const postPreviewUrl = `${post?.guid?.rendered}&preview=true`;
24 |
25 | return {
26 | post: post,
27 | previewLink: postPreviewUrl,
28 | content: [{
29 | type: "text",
30 | text: `Post preview link retrieved successfully. Remember it is only for logged in user with proper permissions.\nURL: ${postPreviewUrl}`
31 | }]
32 | };
33 | } catch (error) {
34 | if (error instanceof Error) {
35 | throw new McpError(ErrorCode.InternalError, `wp_api_get_post_preview_link failed: ${error.message}`);
36 | }
37 | throw new McpError(ErrorCode.InternalError, 'wp_api_get_post_preview_link failed: Unknown error occurred');
38 | }
39 | }
40 | };
41 |
```
--------------------------------------------------------------------------------
/src/tools/list-available-plugins-in-site-plugins-path.ts:
--------------------------------------------------------------------------------
```typescript
1 | // src/tools/edit-file.ts
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 | import fs from 'fs';
4 | import {WordPressSite} from "../types/wp-sites";
5 |
6 |
7 | export const listAvailablePluginsInSitePluginsPath = {
8 | name: "wp_list_available_plugins_in_site_plugins_path",
9 | description: "List a plugins directories which is equivalent of listing available plugins for site",
10 | inputSchema: {
11 | type: "object",
12 | properties: {
13 | siteKey: {type: "string", description: "Site key"}
14 | },
15 | required: ["siteKey"]
16 | },
17 |
18 | async execute(args: { siteKey: string }, site: WordPressSite) {
19 |
20 | const pluginsPath = site.pluginsPath
21 |
22 | if (!fs.existsSync(pluginsPath)) {
23 | throw new Error(`wp_list_available_plugins_in_site_plugins_path failed: directory ${pluginsPath} not exists. Please review a plugin directory and site configuration.`);
24 | }
25 |
26 | try {
27 | const files = fs.readdirSync(pluginsPath, {withFileTypes: true});
28 | const directories = files
29 | .filter(dirent => dirent.isDirectory())
30 | .map(dirent => dirent.name);
31 |
32 | return {
33 | content: [{
34 | type: "text",
35 | text: `Available plugins directories in ${pluginsPath}:\n\n${directories.join('\n')}`,
36 | directories
37 | }]
38 | };
39 |
40 | } catch (error) {
41 | if (error instanceof Error) {
42 | throw new McpError(ErrorCode.InternalError, error.message);
43 | }
44 | throw new McpError(ErrorCode.InternalError, 'wp_list_available_plugins_in_site_plugins_path: Unknown error occurred');
45 | }
46 | }
47 | };
```
--------------------------------------------------------------------------------
/src/tools/wp-api/site-settings/get-site-settings.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {WordPressSite} from "types/wp-sites";
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 |
4 | export const apiGetSiteSettings = {
5 | name: "wp_api_get_site_settings",
6 | description: "Retrieves all WordPress site settings using REST API",
7 | inputSchema: {
8 | type: "object",
9 | properties: {
10 | siteKey: {type: "string", description: "Site key"},
11 | },
12 | required: ["siteKey"]
13 | },
14 | async execute(args: any, site: WordPressSite) {
15 | try {
16 | const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
17 | const url = `${site.apiUrl}/wp/v2/settings`;
18 |
19 | const response = await fetch(url, {
20 | method: 'GET',
21 | headers: {
22 | 'Authorization': `Basic ${credentials}`,
23 | 'Content-Type': 'application/json'
24 | }
25 | });
26 |
27 | if (!response.ok) {
28 | const errorData = await response.json();
29 | throw new Error(`Failed to retrieve site settings: ${errorData.message || response.statusText}`);
30 | }
31 |
32 | const settings = await response.json();
33 | return {
34 | settings,
35 | content: [{
36 | type: "text",
37 | text: `Site settings retrieved successfully.\n\nSettings: ${JSON.stringify(settings, null, 2)}`
38 | }]
39 | };
40 | } catch (error) {
41 | if (error instanceof Error) {
42 | throw new McpError(ErrorCode.InternalError, `wp_api_get_site_settings failed: ${error.message}`);
43 | }
44 | throw new McpError(ErrorCode.InternalError, 'wp_api_get_site_settings failed: Unknown error occurred');
45 | }
46 | }
47 | };
48 |
```
--------------------------------------------------------------------------------
/src/tools/wp-api/get-block-types.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {WordPressSite} from "types/wp-sites";
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 |
4 | export const apiGetGutenbergBlocks = {
5 | name: "wp_api_get_gutenberg_blocks",
6 | description: "Get available Gutenberg blocks for the WordPress site",
7 | inputSchema: {
8 | type: "object",
9 | properties: {
10 | siteKey: {type: "string", description: "Site key"}
11 | },
12 | required: ["siteKey"]
13 | },
14 |
15 | async execute(args: { siteKey: string }, site: WordPressSite) {
16 | try {
17 | const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
18 | const url = `${site.apiUrl}/wp/v2/block-types`;
19 |
20 | const response = await fetch(url, {
21 | method: 'GET',
22 | headers: {
23 | 'Authorization': `Basic ${credentials}`,
24 | 'Content-Type': 'application/json'
25 | }
26 | });
27 |
28 | if (!response.ok) {
29 | const errorData = await response.json();
30 | throw new Error(`Failed to get Gutenberg blocks: ${errorData.message || response.statusText}.\nRequested URL: ${url}`);
31 | }
32 |
33 | const blocks = await response.json();
34 |
35 | return {
36 | blocks: blocks,
37 | content: [{
38 | type: "text",
39 | text: `Available Gutenberg blocks:\n${JSON.stringify(blocks, null, 2)}`
40 | }]
41 | };
42 | } catch (error) {
43 | if (error instanceof Error) {
44 | throw new McpError(ErrorCode.InternalError, `wp_api_get_gutenberg_blocks failed: ${error.message}`);
45 | }
46 | throw new McpError(ErrorCode.InternalError, 'wp_api_get_gutenberg_blocks failed: Unknown error occurred');
47 | }
48 | }
49 | };
50 |
```
--------------------------------------------------------------------------------
/src/tools/wp-api/get-post-types.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {WordPressSite} from "types/wp-sites";
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 |
4 | export const apiGetPostTypes = {
5 | name: "wp_api_get_post_types",
6 | description: "Get a WordPress post types using REST API",
7 | inputSchema: {
8 | type: "object",
9 | properties: {
10 | siteKey: {type: "string", description: "Site key"},
11 | },
12 | required: ["siteKey"]
13 | },
14 |
15 | async execute(args: { siteKey: string }, site: WordPressSite) {
16 | try {
17 | const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
18 | const url = `${site.apiUrl}/wp/v2/types`
19 |
20 | const response = await fetch(url, {
21 | method: 'GET',
22 | headers: {
23 | 'Authorization': `Basic ${credentials}`,
24 | 'Content-Type': 'application/json'
25 | }
26 | });
27 |
28 | if (!response.ok) {
29 | const errorData = await response.json();
30 | throw new Error(`Failed to get post types: ${errorData.message || response.statusText}.\nRequested URL: ${url}`);
31 | }
32 |
33 | const data = await response.json();
34 |
35 | const list = Object.values(data).map((postType: any, i: number) => {
36 | return (i + 1) + '. ' + postType.name + ' (slug: ' + postType.slug + ')';
37 | })
38 |
39 | return {
40 | postTypes: data,
41 | postTypesList: list,
42 | content: [{
43 | type: "text",
44 | text: `Available post types:\n${JSON.stringify(data)}`
45 | }]
46 | };
47 | } catch (error) {
48 | if (error instanceof Error) {
49 | throw new McpError(ErrorCode.InternalError, `wp_api_get_post_types failed: ${error.message}`);
50 | }
51 | throw new McpError(ErrorCode.InternalError, 'wp_api_get_post_types failed: Unknown error occurred');
52 | }
53 | }
54 | };
```
--------------------------------------------------------------------------------
/src/tools/wp-api/get-templates.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {WordPressSite} from "types/wp-sites";
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 |
4 | export const apiGetTemplates = {
5 | name: "wp_api_get_templates",
6 | description: "Get available templates for a specific post type in WordPress",
7 | inputSchema: {
8 | type: "object",
9 | properties: {
10 | siteKey: {type: "string", description: "Site key"},
11 | postType: {type: "string", description: "The post type to fetch templates for"},
12 | },
13 | required: ["siteKey", "postType"]
14 | },
15 |
16 | async execute(args: { siteKey: string, postType: string }, site: WordPressSite) {
17 | try {
18 |
19 | const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
20 | const url = `${site.apiUrl}/wp/v2/templates?post_type=${args.postType}`;
21 |
22 | const response = await fetch(url, {
23 | method: 'GET',
24 | headers: {
25 | 'Authorization': `Basic ${credentials}`,
26 | 'Content-Type': 'application/json'
27 | }
28 | });
29 |
30 | if (!response.ok) {
31 | const errorData = await response.json();
32 | throw new Error(`Failed to get templates: ${errorData.message || response.statusText}.\nRequested URL: ${url}`);
33 | }
34 |
35 | const data = await response.json();
36 |
37 | const list = data.map((template: any, i: number) => {
38 | return (i + 1) + '. ' + template.title.raw + ' (slug: ' + template.slug + ')';
39 | })
40 |
41 | return {
42 | templates: data,
43 | templatesList: list,
44 | content: [{
45 | type: "text",
46 | text: `Available templates for post type "${args.postType}":\n\n${list.join('\n')}`
47 | }]
48 | };
49 | } catch (error) {
50 | if (error instanceof Error) {
51 | throw new McpError(ErrorCode.InternalError, `wp_api_get_templates failed: ${error.message}`);
52 | }
53 | throw new McpError(ErrorCode.InternalError, 'wp_api_get_templates failed: Unknown error occurred');
54 | }
55 | }
56 | };
57 |
```
--------------------------------------------------------------------------------
/src/tools/wp-api/get-rest-base-for-post-types.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {WordPressSite} from "types/wp-sites";
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 | import {apiGetPostTypes} from "./get-post-types.js";
4 |
5 | export const apiGetRestBaseForPostType = {
6 | name: "wp_api_get_rest_base_for_post_type",
7 | description: "Get a WordPress REST API base for post type to use in REST API",
8 | inputSchema: {
9 | type: "object",
10 | properties: {
11 | siteKey: {type: "string", description: "Site key"},
12 | postType: {type: "string", description: "Post type (default: page)"},
13 | },
14 | required: ["siteKey", "postType"]
15 | },
16 |
17 | async execute(args: { siteKey: string, postType: string }, site: WordPressSite) {
18 | try {
19 | const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
20 | const url = `${site.apiUrl}/wp/v2/types/${args.postType}`
21 |
22 | const response = await fetch(url, {
23 | method: 'GET',
24 | headers: {
25 | 'Authorization': `Basic ${credentials}`,
26 | 'Content-Type': 'application/json'
27 | }
28 | });
29 |
30 | if (!response.ok) {
31 | const errorData = await response.json();
32 | const {postTypesList} = await apiGetPostTypes.execute(args, site)
33 | 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')}`);
34 | }
35 |
36 | const data = await response.json();
37 |
38 | return {
39 | postType: data,
40 | restBase: data.rest_base,
41 | content: [{
42 | type: "text",
43 | text: `API REST base for post type: ${args.postType} is a: \n${data.rest_base}`
44 | }]
45 | };
46 | } catch (error) {
47 | if (error instanceof Error) {
48 | throw new McpError(ErrorCode.InternalError, `wp_api_get_rest_base_for_post_types failed: ${error.message}`);
49 | }
50 | throw new McpError(ErrorCode.InternalError, 'wp_api_get_rest_base_for_post_types failed: Unknown error occurred');
51 | }
52 | }
53 | };
```
--------------------------------------------------------------------------------
/src/tools/wp-api/get-post.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {WordPressSite} from "types/wp-sites";
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 | import {apiGetRestBaseForPostType} from "./get-rest-base-for-post-types.js";
4 |
5 | export const apiGetPost = {
6 | name: "wp_api_get_post",
7 | description: "Retrieve a single post, page, or custom post type item using REST API",
8 | inputSchema: {
9 | type: "object",
10 | properties: {
11 | siteKey: {type: "string", description: "Site key"},
12 | postType: {type: "string", description: "Post type (default: posts)"},
13 | postId: {type: "integer", description: "Post ID to retrieve"}, // New property to specify post ID
14 | },
15 | required: ["siteKey", "postType", "postId"]
16 | },
17 |
18 | async execute(args: { siteKey: string, postType: string, postId: number }, site: WordPressSite) {
19 | try {
20 | const {restBase} = await apiGetRestBaseForPostType.execute(args, site);
21 |
22 | const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
23 | const url = `${site.apiUrl}/wp/v2/${restBase}/${args.postId}`; // Modified URL to fetch single post by ID
24 |
25 | const response = await fetch(url, {
26 | method: 'GET',
27 | headers: {
28 | 'Authorization': `Basic ${credentials}`,
29 | 'Content-Type': 'application/json'
30 | }
31 | });
32 |
33 | if (!response.ok) {
34 | const errorData = await response.json();
35 | throw new Error(`Failed to retrieve post: ${errorData.message || response.statusText}. Requested URL: ${url}`);
36 | }
37 |
38 | const data = await response.json();
39 | return {
40 | post: data,
41 | content: [{
42 | type: "text",
43 | text: `Post retrieved successfully.\n\nPost: ${JSON.stringify(data)}`
44 | }]
45 | };
46 | } catch (error) {
47 | if (error instanceof Error) {
48 | throw new McpError(ErrorCode.InternalError, `wp_api_get_post failed: ${error.message}`);
49 | }
50 | throw new McpError(ErrorCode.InternalError, 'wp_api_get_post failed: Unknown error occurred');
51 | }
52 | }
53 | };
54 |
```
--------------------------------------------------------------------------------
/src/tools/wp-api/delete-post.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {WordPressSite} from "types/wp-sites";
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 | import {apiGetRestBaseForPostType} from "tools/wp-api/get-rest-base-for-post-types";
4 |
5 | export const apiDeletePost = {
6 | name: "wp_api_delete_post",
7 | description: "Deletes a WordPress post, page, or any custom post type using REST API",
8 | inputSchema: {
9 | type: "object",
10 | properties: {
11 | siteKey: {type: "string", description: "Site key"},
12 | postId: {type: "integer", description: "ID of the post to delete"},
13 | postType: {type: "string", description: "Type of the post (e.g., post, page, custom type)"},
14 | force: {type: "boolean", description: "Whether to force delete the post"}
15 | },
16 | required: ["siteKey", "postId", "postType"]
17 | },
18 |
19 | async execute(args: { siteKey: string; postId: number; postType: string; force?: boolean }, site: WordPressSite) {
20 | try {
21 |
22 | const {restBase} = await apiGetRestBaseForPostType.execute(args, site)
23 |
24 | const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
25 | const url = `${site.apiUrl}/wp/v2/${restBase}/${args.postId}?force=${args.force ?? false}`;
26 |
27 | const response = await fetch(url, {
28 | method: 'DELETE',
29 | headers: {
30 | 'Authorization': `Basic ${credentials}`,
31 | 'Content-Type': 'application/json'
32 | }
33 | });
34 |
35 | if (!response.ok) {
36 | const errorData = await response.json();
37 | throw new Error(`Failed to delete post: ${errorData.message || response.statusText}.\nRequested URL: ${url}`);
38 | }
39 |
40 | const data = await response.json();
41 |
42 | return {
43 | postData: data,
44 | content: [{
45 | type: "text",
46 | text: `Post ${args.postId} (${args.postType}) deleted successfully.`
47 | }]
48 | };
49 | } catch (error) {
50 | if (error instanceof Error) {
51 | throw new McpError(ErrorCode.InternalError, `wp_api_delete_post failed: ${error.message}`);
52 | }
53 | throw new McpError(ErrorCode.InternalError, 'wp_api_delete_post failed: Unknown error occurred');
54 | }
55 | }
56 | };
57 |
```
--------------------------------------------------------------------------------
/src/tools/wp-api/get-posts.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { WordPressSite } from "types/wp-sites";
2 | import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
3 | import {apiGetRestBaseForPostType} from "./get-rest-base-for-post-types.js";
4 |
5 | export const apiGetPosts = {
6 | name: "wp_api_get_posts",
7 | description: "Retrieve a list of posts, pages, or custom post type items using REST API",
8 | inputSchema: {
9 | type: "object",
10 | properties: {
11 | siteKey: { type: "string", description: "Site key" },
12 | postType: { type: "string", description: "Post type (default: posts)" },
13 | perPage: { type: "integer", description: "Number of posts to retrieve (default: 10)" },
14 | page: { type: "integer", description: "Page number for pagination (default: 1)" }
15 | },
16 | required: ["siteKey", "postType"]
17 | },
18 |
19 | async execute(args: { siteKey: string, postType: string, perPage?: number, page?: number }, site: WordPressSite) {
20 | try {
21 | const {restBase} = await apiGetRestBaseForPostType.execute(args, site)
22 |
23 | const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
24 | const url = `${site.apiUrl}/wp/v2/${restBase}?per_page=${args.perPage || 10}&page=${args.page || 1}`;
25 |
26 | const response = await fetch(url, {
27 | method: 'GET',
28 | headers: {
29 | 'Authorization': `Basic ${credentials}`,
30 | 'Content-Type': 'application/json'
31 | }
32 | });
33 |
34 | if (!response.ok) {
35 | const errorData = await response.json();
36 | throw new Error(`Failed to retrieve posts: ${errorData.message || response.statusText}.
37 | Requested URL: ${url}`);
38 | }
39 |
40 | const data = await response.json();
41 | return {
42 | pluginData: data,
43 | content: [{
44 | type: "text",
45 | text: `Posts retrieved successfully.\n\nPosts: ${JSON.stringify(data)}`
46 | }]
47 | };
48 | } catch (error) {
49 | if (error instanceof Error) {
50 | throw new McpError(ErrorCode.InternalError, `wp_api_get_posts failed: ${error.message}`);
51 | }
52 | throw new McpError(ErrorCode.InternalError, 'wp_api_get_posts failed: Unknown error occurred');
53 | }
54 | }
55 | };
56 |
```
--------------------------------------------------------------------------------
/src/tools/wp-api/get-plugins.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {WordPressSite} from "types/wp-sites";
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 |
4 | export const apiGetPlugins = {
5 | name: "wp_api_get_plugins",
6 | description: "Get WordPress plugins (active, inactive, or all) using REST API",
7 | inputSchema: {
8 | type: "object",
9 | properties: {
10 | siteKey: {type: "string", description: "Site key"},
11 | status: {
12 | type: "string",
13 | enum: ["active", "inactive", "all"],
14 | description: "Filter plugins by status. Default is 'active'."
15 | },
16 | },
17 | required: ["siteKey"],
18 | additionalProperties: false
19 | },
20 |
21 | async execute(args: { siteKey: string, status?: string }, site: WordPressSite) {
22 | try {
23 | const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
24 | const url = `${site.apiUrl}/wp/v2/plugins`;
25 |
26 | // Status wtyczek domyślnie to "active", chyba że użytkownik określi inaczej
27 | const filterStatus = args.status || "active";
28 |
29 | const response = await fetch(url, {
30 | method: 'GET',
31 | headers: {
32 | 'Authorization': `Basic ${credentials}`,
33 | 'Content-Type': 'application/json'
34 | }
35 | });
36 |
37 | if (!response.ok) {
38 | const errorData = await response.json();
39 | throw new Error(`Failed to get plugins: ${errorData.message || response.statusText}.\nRequested URL: ${url}`);
40 | }
41 |
42 | const data = await response.json();
43 |
44 | // Filtrujemy wtyczki zgodnie z wybranym statusem
45 | let filteredPlugins = data;
46 | if (filterStatus !== "all") {
47 | filteredPlugins = data.filter((plugin: { status: string }) => plugin.status === filterStatus);
48 | }
49 |
50 | const pluginsList = filteredPlugins.map((plugin: any) => plugin.name + ' (' + plugin.plugin + ')')
51 |
52 | return {
53 | plugins: filteredPlugins,
54 | pluginsList,
55 | content: [{
56 | type: "text",
57 | text: `${filterStatus.charAt(0).toUpperCase() + filterStatus.slice(1)} plugins:\n${pluginsList.join('\n')}`
58 | }]
59 | };
60 | } catch (error) {
61 | if (error instanceof Error) {
62 | throw new McpError(ErrorCode.InternalError, `wp_api_get_plugins failed: ${error.message}`);
63 | }
64 | throw new McpError(ErrorCode.InternalError, 'wp_api_get_plugins failed: Unknown error occurred');
65 | }
66 | }
67 | };
68 |
```
--------------------------------------------------------------------------------
/src/tools/list-plugin-files.ts:
--------------------------------------------------------------------------------
```typescript
1 | // src/tools/edit-file.ts
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 | import fs from 'fs';
4 | import path from 'path';
5 | import {WordPressSite} from "../types/wp-sites";
6 | import {listAvailablePluginsInSitePluginsPath} from "./list-available-plugins-in-site-plugins-path.js";
7 |
8 |
9 | const listFiles = (dir: any, fileList: any[] = []) => {
10 | if (path.basename(dir) === 'node_modules') {
11 | return fileList; // Pomijamy katalog node_modules
12 | }
13 |
14 | const files = fs.readdirSync(dir);
15 | for (const file of files) {
16 | const filePath = path.join(dir, file);
17 | const stat = fs.statSync(filePath);
18 |
19 | if (stat.isDirectory()) {
20 | listFiles(filePath, fileList);
21 | } else {
22 | fileList.push(filePath);
23 | }
24 | }
25 | return fileList;
26 | }
27 |
28 |
29 | export const listPluginFiles = {
30 | name: "wp_list_plugin_files",
31 | description: "List a files for a specified plugin directory",
32 | inputSchema: {
33 | type: "object",
34 | properties: {
35 | siteKey: {type: "string", description: "Site key"},
36 | pluginDirName: {type: "string", description: "Plugin directory name"}
37 | },
38 | required: ["pluginDirName", "siteKey"]
39 | },
40 |
41 | async execute(args: { pluginDirName: string, siteKey: string }, site: WordPressSite) {
42 |
43 | if (args.pluginDirName === '.' || args.pluginDirName === '') {
44 | throw new McpError(ErrorCode.InvalidParams, 'pluginDirName cannot be "." or empty (self directory).');
45 | }
46 |
47 | const blockDir = path.join(site.pluginsPath, args.pluginDirName)
48 |
49 | if (!fs.existsSync(blockDir)) {
50 | const {content: [{directories}]} = await listAvailablePluginsInSitePluginsPath.execute({siteKey: args.siteKey}, site)
51 | return {
52 | isError: true,
53 | directories,
54 | content: [{
55 | type: "text",
56 | text: `Directory ${blockDir} not exists. Please review a plugin directory. Available plugins in ${site.pluginsPath}:\n\n${directories.join('\n')}`
57 | }]
58 | };
59 | }
60 |
61 | try {
62 |
63 | const files = listFiles(blockDir)
64 |
65 | return {
66 | files,
67 | content: [{
68 | type: "text",
69 | text: `Files at ${blockDir}:\n\n` + files.map((f) => f.replace(blockDir, '')).join('\n')
70 | }]
71 | };
72 | } catch (error) {
73 | if (error instanceof Error) {
74 | throw new McpError(ErrorCode.InternalError, error.message);
75 | }
76 | throw new McpError(ErrorCode.InternalError, 'wp_list_plugin_files: Unknown error occurred');
77 | }
78 | }
79 | };
```
--------------------------------------------------------------------------------
/src/tools/wp-api/activate-plugin.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {WordPressSite} from "types/wp-sites";
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 | import {apiGetPlugins} from "tools/wp-api/get-plugins";
4 |
5 | export const apiActivatePlugin = {
6 | name: "wp_api_activate_plugin",
7 | description: "Activates a WordPress plugin using REST API",
8 | inputSchema: {
9 | type: "object",
10 | properties: {
11 | siteKey: {type: "string", description: "Site key"},
12 | pluginSlug: {type: "string", description: "Plugin slug to activate"}
13 | },
14 | required: ["pluginSlug", "siteKey"]
15 | },
16 |
17 | async execute(args: { siteKey: string; pluginSlug: string }, site: WordPressSite) {
18 | try {
19 | const pluginSlug = args.pluginSlug.replace('.php', '')
20 |
21 | const allPlugins = await apiGetPlugins.execute({...args, status: 'all'}, site)
22 | if (!allPlugins.plugins.find((p: any) => p.plugin === pluginSlug)) {
23 | return {
24 | isError: true,
25 | content: [{
26 | type: "text",
27 | text: `Plugin ${args.pluginSlug} is invalid. Available plugins:\n\n${allPlugins.pluginsList.join('\n')}. Please try again with correct plugin slug.`
28 | }]
29 | };
30 | }
31 |
32 | const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
33 | const url = `${site.apiUrl}/wp/v2/plugins/${pluginSlug}`
34 |
35 | const response = await fetch(url, {
36 | method: 'PUT',
37 | headers: {
38 | 'Authorization': `Basic ${credentials}`,
39 | 'Content-Type': 'application/json'
40 | },
41 | body: JSON.stringify({
42 | status: 'active'
43 | })
44 | });
45 |
46 | if (!response.ok) {
47 | const errorData = await response.json();
48 | throw new Error(`Failed to activate plugin: ${errorData.message || response.statusText}.\nRequested URL: ${url}`);
49 | }
50 |
51 | const data = await response.json();
52 |
53 |
54 | return {
55 | pluginData: data,
56 | content: [{
57 | type: "text",
58 | text: `Plugin ${args.pluginSlug} activated successfully.\n\nPlugin: ${data}`
59 | }]
60 | };
61 | } catch (error) {
62 | if (error instanceof Error) {
63 | throw new McpError(ErrorCode.InternalError, `wp_api_activate_plugin failed: ${error.message}`);
64 | }
65 | throw new McpError(ErrorCode.InternalError, 'wp_api_activate_plugin failed: Unknown error occurred');
66 | }
67 | }
68 | };
```
--------------------------------------------------------------------------------
/src/tools/wp-api/deactivate-plugin.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {WordPressSite} from "types/wp-sites";
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 | import {apiGetPlugins} from "tools/wp-api/get-plugins";
4 |
5 | export const apiDeactivatePlugin = {
6 | name: "wp_api_deactivate_plugin",
7 | description: "Deactivates a WordPress plugin using REST API",
8 | inputSchema: {
9 | type: "object",
10 | properties: {
11 | siteKey: {type: "string", description: "Site key"},
12 | pluginSlug: {type: "string", description: "Plugin slug to deactivate"}
13 | },
14 | required: ["pluginSlug", "siteKey"]
15 | },
16 |
17 | async execute(args: { siteKey: string; pluginSlug: string }, site: WordPressSite) {
18 | try {
19 | const pluginSlug = args.pluginSlug.replace('.php', '')
20 |
21 | const allPlugins = await apiGetPlugins.execute({...args, status: 'active'}, site)
22 | if (!allPlugins.plugins.find((p: any) => p.plugin === pluginSlug)) {
23 | return {
24 | isError: true,
25 | content: [{
26 | type: "text",
27 | text: `Plugin ${args.pluginSlug} is not active or invalid. Active plugins:\n${allPlugins.pluginsList.join('\n')}. Please try again with a correct plugin slug.`
28 | }]
29 | };
30 | }
31 |
32 | const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
33 | const url = `${site.apiUrl}/wp/v2/plugins/${pluginSlug}`
34 |
35 | const response = await fetch(url, {
36 | method: 'PUT',
37 | headers: {
38 | 'Authorization': `Basic ${credentials}`,
39 | 'Content-Type': 'application/json'
40 | },
41 | body: JSON.stringify({
42 | status: 'inactive'
43 | })
44 | });
45 |
46 | if (!response.ok) {
47 | const errorData = await response.json();
48 | throw new Error(`Failed to deactivate plugin: ${errorData.message || response.statusText}.\nRequested URL: ${url}`);
49 | }
50 |
51 | const data = await response.json();
52 |
53 | return {
54 | pluginData: data,
55 | content: [{
56 | type: "text",
57 | text: `Plugin ${args.pluginSlug} deactivated successfully.\n\nPlugin: ${data}`
58 | }]
59 | };
60 | } catch (error) {
61 | if (error instanceof Error) {
62 | throw new McpError(ErrorCode.InternalError, `wp_api_deactivate_plugin failed: ${error.message}`);
63 | }
64 | throw new McpError(ErrorCode.InternalError, 'wp_api_deactivate_plugin failed: Unknown error occurred');
65 | }
66 | }
67 | };
68 |
```
--------------------------------------------------------------------------------
/src/tools/wp-api/site-settings/update-integer-site-setting.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {WordPressSite} from "types/wp-sites";
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 |
4 | // Lista dozwolonych kluczy ustawień zgodnie z dokumentacją WordPress REST API
5 | const VALID_SETTING_KEYS = [
6 | "post_per_page", "page_on_front", "page_for_posts", "site_logo", "site_icon", "default_category", "start_of_week"
7 | ] as const;
8 |
9 | type SettingKey = (typeof VALID_SETTING_KEYS)[number];
10 |
11 | export const apiUpdateIntegerSiteSetting = {
12 | name: "wp_api_update_integer_site_setting",
13 | description: "Updates a single WordPress site setting in integer format using REST API",
14 | inputSchema: {
15 | type: "object",
16 | properties: {
17 | siteKey: {type: "string", description: "Site key"},
18 | settingKey: {
19 | type: "string",
20 | enum: VALID_SETTING_KEYS, // Walidacja poprawnych kluczy
21 | description: "The key of the setting to update"
22 | },
23 | settingValue: {type: "integer", description: "The new value for the setting"}
24 | },
25 | required: ["siteKey", "settingKey", "settingValue"]
26 | },
27 |
28 | async execute(args: { siteKey: string; settingKey: SettingKey; settingValue: any }, site: WordPressSite) {
29 | try {
30 |
31 | if (!VALID_SETTING_KEYS.includes(args.settingKey)) {
32 | throw new Error(`Invalid setting key: ${args.settingKey}. Allowed values: ${VALID_SETTING_KEYS.join(", ")}`);
33 | }
34 |
35 | const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
36 | const url = `${site.apiUrl}/wp/v2/settings`;
37 |
38 | const response = await fetch(url, {
39 | method: 'POST',
40 | headers: {
41 | 'Authorization': `Basic ${credentials}`,
42 | 'Content-Type': 'application/json'
43 | },
44 | body: JSON.stringify({[args.settingKey]: args.settingValue})
45 | });
46 |
47 | if (!response.ok) {
48 | const errorData = await response.json();
49 | throw new Error(`Failed to update setting: ${errorData.message || response.statusText}`);
50 | }
51 |
52 | const data = await response.json();
53 |
54 | return {
55 | settings: data,
56 | content: [{
57 | type: "text",
58 | text: `Site settings updated successfully.\n\nNew settings object: ${JSON.stringify(data, null, 2)}`
59 | }]
60 | };
61 |
62 | } catch (error) {
63 | if (error instanceof Error) {
64 | throw new McpError(ErrorCode.InternalError, `wp_api_update_integer_site_setting failed: ${error.message}`);
65 | }
66 | throw new McpError(ErrorCode.InternalError, 'wp_api_update_integer_site_setting failed: Unknown error occurred');
67 | }
68 | }
69 | };
70 |
```
--------------------------------------------------------------------------------
/src/tools/wp-api/site-settings/update-string-site-setting.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {WordPressSite} from "types/wp-sites";
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 |
4 | // Lista dozwolonych kluczy ustawień zgodnie z dokumentacją WordPress REST API
5 | const VALID_SETTING_KEYS = [
6 | "title", "description", "url", "email", "timezone", "date_format", "time_format",
7 | "language", "default_post_format", "show_on_front",
8 | "default_ping_status", "default_comment_status"
9 | ] as const;
10 |
11 | type SettingKey = (typeof VALID_SETTING_KEYS)[number];
12 |
13 | export const apiUpdateStringSiteSetting = {
14 | name: "wp_api_update_string_site_setting",
15 | description: "Updates a single WordPress site setting in string format using REST API",
16 | inputSchema: {
17 | type: "object",
18 | properties: {
19 | siteKey: {type: "string", description: "Site key"},
20 | settingKey: {
21 | type: "string",
22 | enum: VALID_SETTING_KEYS, // Walidacja poprawnych kluczy
23 | description: "The key of the setting to update"
24 | },
25 | settingValue: {type: "string", description: "The new value for the setting"}
26 | },
27 | required: ["siteKey", "settingKey", "settingValue"]
28 | },
29 |
30 | async execute(args: { siteKey: string; settingKey: SettingKey; settingValue: any }, site: WordPressSite) {
31 | try {
32 |
33 | if (!VALID_SETTING_KEYS.includes(args.settingKey)) {
34 | throw new Error(`Invalid setting key: ${args.settingKey}. Allowed values: ${VALID_SETTING_KEYS.join(", ")}`);
35 | }
36 |
37 | const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
38 | const url = `${site.apiUrl}/wp/v2/settings`;
39 |
40 | const response = await fetch(url, {
41 | method: 'POST',
42 | headers: {
43 | 'Authorization': `Basic ${credentials}`,
44 | 'Content-Type': 'application/json'
45 | },
46 | body: JSON.stringify({[args.settingKey]: args.settingValue})
47 | });
48 |
49 | if (!response.ok) {
50 | const errorData = await response.json();
51 | throw new Error(`Failed to update setting: ${errorData.message || response.statusText}`);
52 | }
53 |
54 | const data = await response.json();
55 |
56 | return {
57 | settings: data,
58 | content: [{
59 | type: "text",
60 | text: `Site settings updated successfully.\n\nNew settings object: ${JSON.stringify(data, null, 2)}`
61 | }]
62 | };
63 |
64 | } catch (error) {
65 | if (error instanceof Error) {
66 | throw new McpError(ErrorCode.InternalError, `wp_api_update_string_site_setting failed: ${error.message}`);
67 | }
68 | throw new McpError(ErrorCode.InternalError, 'wp_api_update_string_site_setting failed: Unknown error occurred');
69 | }
70 | }
71 | };
72 |
```
--------------------------------------------------------------------------------
/src/tools/wp-api/update-post-status.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {WordPressSite} from "types/wp-sites";
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 | import {apiGetRestBaseForPostType} from "./get-rest-base-for-post-types.js";
4 |
5 | export const apiUpdatePostStatus = {
6 | name: "wp_api_update_post_status",
7 | description: "Update the status of an existing WordPress post, page, or custom post type item using REST API",
8 | inputSchema: {
9 | type: "object",
10 | properties: {
11 | siteKey: {type: "string", description: "Site key"},
12 | postId: {type: "integer", description: "ID of the post to update"},
13 | postType: {type: "string", description: "Post type (default: posts)"},
14 | status: {type: "string", enum: ["publish", "draft"], description: "Status to set (publish or draft)"},
15 | publishDate: {
16 | type: "string",
17 | format: "date-time",
18 | description: "Optional scheduled publication date in ISO 8601 format"
19 | }
20 | },
21 | required: ["siteKey", "postId", "postType", "status"]
22 | },
23 |
24 | async execute(args: {
25 | siteKey: string,
26 | postId: number,
27 | postType: string,
28 | status: string,
29 | publishDate?: string
30 | }, site: WordPressSite) {
31 | try {
32 | const {restBase} = await apiGetRestBaseForPostType.execute(args, site)
33 |
34 | const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
35 | const url = `${site.apiUrl}/wp/v2/${restBase}/${args.postId}`;
36 |
37 | const bodyData: any = {status: args.status};
38 | if (args.status === "publish" && args.publishDate) {
39 | bodyData.date = args.publishDate;
40 | }
41 |
42 | const response = await fetch(url, {
43 | method: 'POST',
44 | headers: {
45 | 'Authorization': `Basic ${credentials}`,
46 | 'Content-Type': 'application/json'
47 | },
48 | body: JSON.stringify(bodyData)
49 | });
50 |
51 | if (!response.ok) {
52 | const errorData = await response.json();
53 | throw new Error(`Failed to update post status: ${errorData.message || response.statusText}.
54 | Requested URL: ${url}`);
55 | }
56 |
57 | const data = await response.json();
58 | return {
59 | pluginData: data,
60 | content: [{
61 | type: "text",
62 | 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}`
63 | }]
64 | };
65 | } catch (error) {
66 | if (error instanceof Error) {
67 | throw new McpError(ErrorCode.InternalError, `wp_api_update_post_status failed: ${error.message}`);
68 | }
69 | throw new McpError(ErrorCode.InternalError, 'wp_api_update_post_status failed: Unknown error occurred');
70 | }
71 | }
72 | };
73 |
```
--------------------------------------------------------------------------------
/src/tools/wp-api/update-post.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {WordPressSite} from "types/wp-sites";
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 | import {apiGetRestBaseForPostType} from "./get-rest-base-for-post-types.js";
4 |
5 | export const apiUpdatePost = {
6 | name: "wp_api_update_post",
7 | 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.",
8 | inputSchema: {
9 | type: "object",
10 | properties: {
11 | siteKey: {type: "string", description: "Site key"},
12 | postId: {type: "number", description: "Post ID to update"},
13 | postType: {type: "string", description: "Type of the post (e.g., posts, pages)"},
14 | template: {type: "string", description: "Optional: Post template"},
15 | title: {type: "string", description: "Optional: Post title"},
16 | excerpt: {type: "string", description: "Optional: Post excerpt"},
17 | },
18 | required: ["siteKey", "postId", "postType"]
19 | },
20 |
21 | async execute(args: {
22 | siteKey: string,
23 | postId: number,
24 | postType: string,
25 | title?: string,
26 | template?: string
27 | excerpt?: string
28 | }, site: WordPressSite) {
29 | try {
30 |
31 | const {restBase} = await apiGetRestBaseForPostType.execute(args, site)
32 |
33 | const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
34 | const url = `${site.apiUrl}/wp/v2/${restBase}/${args.postId}`;
35 |
36 | const bodyData: any = {}
37 | if (args.title) {
38 | bodyData.title = args.title
39 | }
40 | if (args.template) {
41 | bodyData.template = args.template
42 | }
43 | if (args.excerpt) {
44 | bodyData.excerpt = args.excerpt
45 | }
46 | // @ts-ignore
47 | if (args.content) {
48 | throw new Error(`Failed. To update post content use special tool: wp_api_update_post_content`);
49 | }
50 |
51 | const response = await fetch(url, {
52 | method: 'POST',
53 | headers: {
54 | 'Authorization': `Basic ${credentials}`,
55 | 'Content-Type': 'application/json'
56 | },
57 | body: JSON.stringify(bodyData)
58 | });
59 |
60 | if (!response.ok) {
61 | const errorData = await response.json();
62 | throw new Error(`Failed to update post: ${errorData.message || response.statusText}.\nRequested URL: ${url}`);
63 | }
64 |
65 | const updatedPost = await response.json();
66 |
67 | return {
68 | updatedPost,
69 | content: [{
70 | type: "text",
71 | text: `Post settings updated successfully! Preview URL: ${updatedPost.link}.`
72 | }]
73 | };
74 | } catch (error) {
75 | if (error instanceof Error) {
76 | throw new McpError(ErrorCode.InternalError, `wp_api_update_post failed: ${error.message}`);
77 | }
78 | throw new McpError(ErrorCode.InternalError, 'wp_api_update_post failed: Unknown error occurred');
79 | }
80 | }
81 | };
82 |
```
--------------------------------------------------------------------------------
/src/tools/wp-api/update-post-content.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {WordPressSite} from "types/wp-sites";
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 | import {apiGetRestBaseForPostType} from "./get-rest-base-for-post-types.js";
4 | import {apiGetPost} from "tools/wp-api/get-post";
5 |
6 | export const apiUpdatePostContent = {
7 | name: "wp_api_update_post_content",
8 | description: "Update the content of a WordPress post with post type using REST API",
9 | inputSchema: {
10 | type: "object",
11 | properties: {
12 | siteKey: {type: "string", description: "Site key"},
13 | postId: {type: "number", description: "Post ID to update"},
14 | postType: {type: "string", description: "Type of the post (e.g., posts, pages)"},
15 | content: {type: "string", description: "Optional: New content for the post (only if content changed)"},
16 | readCurrentContent: {
17 | type: "boolean",
18 | description: "Determine, you need a current version of post content to update context or just update content."
19 | },
20 | },
21 | required: ["siteKey", "postId", "postType", "readCurrentContent"]
22 | },
23 |
24 | async execute(args: {
25 | siteKey: string,
26 | postId: number,
27 | postType: string,
28 | content: string,
29 | readCurrentContent: boolean
30 | }, site: WordPressSite) {
31 | try {
32 |
33 | if (args.readCurrentContent) {
34 | const {post} = await apiGetPost.execute(args, site)
35 | return {
36 | isError: true,
37 | content: [{
38 | type: "text",
39 | text: `Please generate a new post content from current content revision:\n\n${post?.content?.rendered}`
40 | }]
41 | };
42 | }
43 |
44 | const {restBase} = await apiGetRestBaseForPostType.execute(args, site)
45 |
46 | const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
47 | const url = `${site.apiUrl}/wp/v2/${restBase}/${args.postId}`;
48 |
49 | const bodyData: any = {
50 | content: args.content
51 | }
52 |
53 | const response = await fetch(url, {
54 | method: 'POST',
55 | headers: {
56 | 'Authorization': `Basic ${credentials}`,
57 | 'Content-Type': 'application/json'
58 | },
59 | body: JSON.stringify(bodyData)
60 | });
61 |
62 | if (!response.ok) {
63 | const errorData = await response.json();
64 | throw new Error(`Failed to update post content: ${errorData.message || response.statusText}.\nRequested URL: ${url}`);
65 | }
66 |
67 | const updatedPost = await response.json();
68 |
69 | return {
70 | updatedPost,
71 | content: [{
72 | type: "text",
73 | text: `Post content updated successfully! Preview URL: ${updatedPost.link}.`
74 | }]
75 | };
76 | } catch (error) {
77 | if (error instanceof Error) {
78 | throw new McpError(ErrorCode.InternalError, `wp_api_update_post_content failed: ${error.message}`);
79 | }
80 | throw new McpError(ErrorCode.InternalError, 'wp_api_update_post_content failed: Unknown error occurred');
81 | }
82 | }
83 | };
84 |
```
--------------------------------------------------------------------------------
/src/tools/build-block.ts:
--------------------------------------------------------------------------------
```typescript
1 | // src/tools/build-block.ts
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 | import {execSync} from 'child_process';
4 | import {isGutenbergBlock} from "../helpers.js";
5 | import {WordPressSite} from "../types/wp-sites";
6 | import path from "path";
7 |
8 | export interface BuildBlockArgs {
9 | blockPluginDirname: string;
10 | siteKey: string;
11 | }
12 |
13 | export const buildBlock = {
14 | name: "wp_build_block",
15 | description: "Builds a Gutenberg block using npm run build",
16 | inputSchema: {
17 | type: "object",
18 | properties: {
19 | siteKey: {type: "string", description: "Site key"},
20 | blockPluginDirname: {
21 | type: "string",
22 | description: "Block plugin directory name."
23 | }
24 | },
25 | required: ["blockPluginDirname", "siteKey"]
26 | },
27 |
28 | execute: async (args: BuildBlockArgs, site: WordPressSite) => {
29 |
30 | const blockDir = path.join(site.pluginsPath, args.blockPluginDirname);
31 |
32 | if (!(await isGutenbergBlock(blockDir))) {
33 | throw new Error(`wp_build_block failed: directory ${blockDir} does not contain a Gutenberg block. Are you sure ${blockDir} is valid?`);
34 | }
35 |
36 | try {
37 |
38 | try {
39 | execSync('npm install --no-audit --no-fund --silent', {
40 | cwd: blockDir,
41 | stdio: ['pipe', 'pipe', 'pipe'],
42 | shell: '/bin/bash'
43 | });
44 | } catch (error) {
45 | throw new Error(`wp_build_block: npm install failed with error - ${error instanceof Error ? error.message : String(error)} in ${blockDir}`);
46 | }
47 |
48 | try {
49 |
50 | const buildOutput = execSync('npm run build', {
51 | cwd: blockDir,
52 | stdio: ['pipe', 'pipe', 'pipe'],
53 | shell: '/bin/bash',
54 | encoding: 'utf-8'
55 | });
56 |
57 | const relevantOutput = buildOutput
58 | .split('\n')
59 | .filter(line =>
60 | line.includes('webpack') ||
61 | line.includes('compiled') ||
62 | line.includes('error') ||
63 | line.includes('warning')
64 | )
65 | .join('\n');
66 |
67 | return {
68 | content: [{
69 | type: "text",
70 | text: `✅ Block "${args.blockPluginDirname}" built successfully!\n\nBuild summary:\n${relevantOutput}`
71 | }]
72 | };
73 | } catch (error) {
74 | const errorOutput = error instanceof Error ? error.message : String(error);
75 | const formattedError = errorOutput
76 | .split('\n')
77 | .filter(line =>
78 | line.includes('error') ||
79 | line.includes('failed') ||
80 | line.includes('webpack')
81 | )
82 | .join('\n');
83 |
84 | 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.`);
85 | }
86 | } catch (error) {
87 | if (error instanceof Error) {
88 | throw new McpError(ErrorCode.InternalError, error.message);
89 | }
90 | throw new McpError(ErrorCode.InternalError, 'wp_build_block failed: Unknown error occurred');
91 | }
92 | }
93 | };
```
--------------------------------------------------------------------------------
/src/tools/wp-cli/check_and_instal_wp_cli.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {execSync} from "child_process";
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 | import path from "path";
4 | import {WordPressSite} from "types/wp-sites";
5 |
6 | export const checkAndInstallWPCLI = {
7 | name: "check_and_install_wp_cli",
8 | description: "Checks if WP-CLI is installed. If not, installs it locally.",
9 | inputSchema: {
10 | type: "object",
11 | properties: {
12 | siteKey: {type: "string", description: "Site key"}
13 | },
14 | required: []
15 | },
16 |
17 | async execute(args: any, site: WordPressSite) {
18 | try {
19 | // Sprawdzenie, czy PHP jest zainstalowane
20 | let isPhpInstalled;
21 | try {
22 | execSync("php -v", {stdio: "pipe"}); // Sprawdzenie wersji PHP
23 | isPhpInstalled = true;
24 | } catch (error) {
25 | isPhpInstalled = false;
26 | }
27 |
28 | if (!isPhpInstalled) {
29 | // Jeśli PHP nie jest zainstalowane, zwróć instrukcje
30 | return {
31 | content: [{
32 | type: "text",
33 | text: "❌ PHP is not installed. Please install PHP manually before proceeding. \n\n" +
34 | "You can install PHP by running the following commands:\n" +
35 | "1. On macOS (using Homebrew): `brew install php`\n" +
36 | "2. On Ubuntu/Debian: `sudo apt update && sudo apt install php`\n" +
37 | "3. On Windows, download PHP from https://windows.php.net/download/."
38 | }]
39 | };
40 | }
41 |
42 | // Sprawdzenie, czy WP-CLI jest zainstalowane
43 | let isInstalled;
44 | try {
45 | execSync("wp --info", {stdio: "pipe"}); // Jeśli WP-CLI jest zainstalowane, to nie rzuci wyjątkiem
46 | isInstalled = true;
47 | } catch (error) {
48 | isInstalled = false;
49 | }
50 |
51 | if (isInstalled) {
52 | return {
53 | content: [{type: "text", text: "✅ WP-CLI is already installed."}]
54 | };
55 | }
56 |
57 | // Jeśli WP-CLI nie jest zainstalowane, pobierz i zainstaluj lokalnie
58 | const wpCliPath = path.resolve(site.path, "wp-cli.phar");
59 |
60 | // Pobierz WP-CLI i ustaw odpowiednie uprawnienia
61 | execSync(`
62 | curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar &&
63 | chmod +x wp-cli.phar
64 | `, {stdio: "inherit", cwd: site.path}); // stdio: "inherit" pozwala na wyświetlanie wyników w terminalu
65 |
66 | // Sprawdzenie, czy WP-CLI działa poprawnie
67 | let isNowInstalled;
68 | try {
69 | execSync(`php "${wpCliPath}" --info`, {stdio: "pipe"});
70 | isNowInstalled = true;
71 | } catch (error) {
72 | isNowInstalled = false;
73 | }
74 |
75 | if (!isNowInstalled) {
76 | throw new Error(`WP-CLI installation failed. CLI Path: ${wpCliPath}`);
77 | }
78 |
79 | return {
80 | content: [{type: "text", text: "✅ WP-CLI was successfully installed locally."}]
81 | };
82 |
83 | } catch (error) {
84 | if (error instanceof Error) {
85 | throw new McpError(ErrorCode.InternalError, `check_and_install_wp_cli failed: ${error.message}`);
86 | }
87 | throw new McpError(ErrorCode.InternalError, 'check_and_install_wp_cli failed: Unknown error occurred');
88 | }
89 | }
90 | };
91 |
```
--------------------------------------------------------------------------------
/src/tools/scaffold-block.ts:
--------------------------------------------------------------------------------
```typescript
1 | // src/tools/scaffold-block.ts
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 | import {execSync} from 'child_process';
4 | import path from "path";
5 | import fs from 'fs';
6 | import {WordPressSite} from "../types/wp-sites";
7 | import {listPluginFiles} from "./list-plugin-files.js";
8 | import {buildBlock} from "./build-block.js";
9 |
10 | interface ScaffoldArgs {
11 | name: string;
12 | siteKey: string;
13 | variant: string;
14 | namespace: string;
15 | }
16 |
17 | export const scaffoldBlockTool = {
18 | name: "wp_scaffold_block",
19 | description: "Creates and builds a new Gutenberg block using @wordpress/create-block",
20 | inputSchema: {
21 | type: "object",
22 | properties: {
23 | siteKey: {type: "string", description: "Site key"},
24 | name: {type: "string", description: "Block name"},
25 | variant: {
26 | type: "string",
27 | enum: ["static", "dynamic"],
28 | description: "Gutenberg block template variant (static or dynamic). Default: static"
29 | },
30 | namespace: {type: "string", description: "Gutenberg block namespace"}
31 | },
32 | required: ["name", "siteKey", "variant", "namespace"]
33 | },
34 | execute: async (args: ScaffoldArgs, site: WordPressSite) => {
35 |
36 | if (typeof args.name !== 'string') {
37 | throw new McpError(ErrorCode.InvalidParams, 'Block name must be a string');
38 | }
39 |
40 | const blockDirname = args.name.toLowerCase().replace(/ /g, '-')
41 | const namespace = args.namespace.toLowerCase().replace(/ /g, '-')
42 | const blockDir = path.join(site.pluginsPath, blockDirname)
43 | const pluginDir = site.pluginsPath
44 |
45 | if (fs.existsSync(blockDir)) {
46 | throw new McpError(ErrorCode.InvalidRequest, `wp_scaffold_block failed: directory ${blockDir} already exists. Please change a block name or remove existing plugin!`);
47 | }
48 |
49 | try {
50 |
51 | const variant = args.variant === 'dynamic' ? 'dynamic' : 'static'
52 | const createBlockCommand = `npx @wordpress/create-block ${args.name} --variant ${variant} --namespace ${namespace}`
53 |
54 | execSync(createBlockCommand, {
55 | stdio: ['pipe', 'pipe', 'pipe'],
56 | encoding: 'utf-8',
57 | cwd: pluginDir
58 | });
59 |
60 | const buildResult = await buildBlock.execute({
61 | blockPluginDirname: blockDirname,
62 | siteKey: args.siteKey
63 | }, site);
64 |
65 | const listedPluginFiles = await listPluginFiles.execute({
66 | pluginDirName: path.basename(blockDir),
67 | siteKey: args.siteKey
68 | }, site);
69 |
70 | if (listedPluginFiles.isError || !listedPluginFiles?.files) {
71 | throw new McpError(ErrorCode.InternalError, `wp_scaffold_block failed: directory ${path.basename(blockDir)} is unreachable or files not created.`);
72 | }
73 |
74 | return {
75 | content: [{
76 | type: "text",
77 | 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?`
78 | }]
79 | };
80 | } catch (error) {
81 | console.error('Block scaffolding error:', error);
82 | if (error instanceof Error) {
83 | throw new McpError(ErrorCode.InternalError, `Failed to create/build block: ${error.message}`);
84 | }
85 | throw new McpError(ErrorCode.InternalError, 'Unknown error occurred while creating/building block');
86 | }
87 | }
88 | };
```
--------------------------------------------------------------------------------
/src/tools/wp-api/create-post.ts:
--------------------------------------------------------------------------------
```typescript
1 | import {WordPressSite} from "types/wp-sites";
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 | import {apiGetPostTypes} from "./get-post-types.js";
4 | import {apiGetRestBaseForPostType} from "./get-rest-base-for-post-types.js";
5 | import {apiGetTemplates} from "./get-templates.js";
6 |
7 | export const apiCreatePost = {
8 | name: "wp_api_create_post",
9 | description: "Create a WordPress post, page or custom post type item using REST API",
10 | inputSchema: {
11 | type: "object",
12 | properties: {
13 | siteKey: {type: "string", description: "Site key"},
14 | postType: {type: "string", description: "Post type (default: pages)"},
15 | title: {type: "string", description: "Post title"},
16 | content: {type: "string", description: "Post content"},
17 | parent: {type: "integer", description: "Post parent ID"},
18 | template: {type: "string", description: "Optional: Post template"},
19 | },
20 | required: ["postType", "siteKey", "title"]
21 | },
22 |
23 | async execute(args: {
24 | siteKey: string,
25 | postType: string,
26 | title: string,
27 | content: string,
28 | parent: Number,
29 | template?: string
30 | }, site: WordPressSite) {
31 | try {
32 |
33 | const {restBase} = await apiGetRestBaseForPostType.execute(args, site)
34 |
35 |
36 | const credentials = Buffer.from(`${site.apiCredentials?.username}:${site.apiCredentials?.password}`).toString('base64');
37 | const url = `${site.apiUrl}/wp/v2/${restBase}`
38 |
39 | const bodyData: any = {
40 | title: args.title,
41 | status: 'draft',
42 | content: args.content,
43 | parent: args.parent,
44 | }
45 |
46 | if (args.template) {
47 | const {templates, templatesList} = await apiGetTemplates.execute(args, site)
48 | if (!templates.find((template: any) => template.slug === args.template)) {
49 | throw new Error(`Invalid template.\nUse one of the following:\n${templatesList.join('\n')}`);
50 | }
51 | bodyData.template = args.template
52 | }
53 |
54 | const response = await fetch(url, {
55 | method: 'POST',
56 | headers: {
57 | 'Authorization': `Basic ${credentials}`,
58 | 'Content-Type': 'application/json'
59 | },
60 | body: JSON.stringify(bodyData)
61 | });
62 |
63 | if (!response.ok) {
64 | const errorData = await response.json();
65 |
66 | if (errorData.message.includes('No route was found matching the URL and request method')) {
67 | const postTypes = await apiGetPostTypes.execute({
68 | siteKey: args.siteKey
69 | }, site)
70 |
71 | return {
72 | isError: true,
73 | content: [
74 | {
75 | type: "text",
76 | 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(', ')}.`
77 | }
78 | ]
79 | }
80 | }
81 | throw new Error(`Failed to create post: ${errorData.message || response.statusText}.\nRequested URL: ${url}`);
82 | }
83 |
84 | const data = await response.json();
85 |
86 | return {
87 | pluginData: data,
88 | content: [{
89 | type: "text",
90 | text: `Post created successfully.\n\nPost: ${JSON.stringify(data, null, 2)}`
91 | }]
92 | };
93 | } catch (error) {
94 | if (error instanceof Error) {
95 | throw new McpError(ErrorCode.InternalError, `wp_api_create_post failed: ${error.message}`);
96 | }
97 | throw new McpError(ErrorCode.InternalError, 'wp_api_create_post failed: Unknown error occurred');
98 | }
99 | }
100 | };
```
--------------------------------------------------------------------------------
/src/helpers.ts:
--------------------------------------------------------------------------------
```typescript
1 | // src/helpers.ts
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 | import fs from 'fs/promises';
4 | import path from 'path';
5 | import {execSync} from "child_process";
6 |
7 | export interface WordPressSite {
8 | localWpSshEntryFile: string;
9 | cli?: string;
10 | name: string;
11 | path: string;
12 | pluginsPath: string;
13 | aliases?: string[];
14 | apiUrl: string;
15 | apiCredentials?: {
16 | username: string;
17 | password: string;
18 | };
19 | }
20 |
21 | export interface WPSitesConfig {
22 | sites: {
23 | [key: string]: WordPressSite;
24 | };
25 | }
26 |
27 | export interface ToolArguments {
28 | siteKey: string;
29 | }
30 |
31 | export function similarity(s1: string, s2: string): number {
32 | s1 = s1.toLowerCase();
33 | s2 = s2.toLowerCase();
34 |
35 | if (s1.includes(s2) || s2.includes(s1)) return 0.8;
36 |
37 | let longer = s1;
38 | let shorter = s2;
39 | if (s1.length < s2.length) {
40 | longer = s2;
41 | shorter = s1;
42 | }
43 | const longerLength = longer.length;
44 | if (longerLength === 0) return 1.0;
45 |
46 | const distance = longer.split('')
47 | .filter(char => !shorter.includes(char)).length;
48 | return (longerLength - distance) / longerLength;
49 | }
50 |
51 | export function findMatchingSites(config: WPSitesConfig, searchTerm?: string): Array<[string, WordPressSite, number]> {
52 | if (!searchTerm) {
53 | return Object.entries(config.sites)
54 | .map(([key, site]) => [key, site, 1] as [string, WordPressSite, number]);
55 | }
56 |
57 | return Object.entries(config.sites)
58 | .map(([key, site]) => {
59 | const keyMatch = similarity(key, searchTerm);
60 | const nameMatch = similarity(site.name, searchTerm);
61 | const aliasMatches = (site.aliases || [])
62 | .map(alias => similarity(alias, searchTerm));
63 |
64 | const maxScore = Math.max(keyMatch, nameMatch, ...aliasMatches);
65 | return [key, site, maxScore] as [string, WordPressSite, number];
66 | })
67 | .filter(([, , score]) => score > 0.3)
68 | .sort((a, b) => b[2] - a[2]);
69 | }
70 |
71 | export function formatSitesList(sites: Array<[string, WordPressSite, number]>): string {
72 | return sites
73 | .map(([key, site], index) =>
74 | `${index + 1}. ${key} (${site.name})`)
75 | .join('\n');
76 | }
77 |
78 | export async function validateAndGetSite(config: WPSitesConfig, siteArg?: string): Promise<WordPressSite> {
79 | const matches = findMatchingSites(config, siteArg);
80 |
81 | if (matches.length === 0) {
82 | const availableSites = formatSitesList(findMatchingSites(config));
83 | throw new McpError(
84 | ErrorCode.InvalidParams,
85 | `No matching sites found. Available sites:\n${availableSites}`
86 | );
87 | }
88 |
89 | if (matches.length === 1) {
90 | const [key, site] = matches[0];
91 | return site;
92 | }
93 |
94 | const sitesList = formatSitesList(matches);
95 | throw new McpError(
96 | ErrorCode.InvalidParams,
97 | `Multiple matching sites found. Please choose which one to use and try again:\n${sitesList}`
98 | );
99 | }
100 |
101 | export function validateSiteToolArguments(args: unknown): asserts args is ToolArguments {
102 | if (!args || typeof args !== 'object') {
103 | throw new McpError(ErrorCode.InvalidParams, 'Arguments must be an object');
104 | }
105 |
106 | const typedArgs = args as Record<string, unknown>;
107 |
108 | if (typedArgs.siteKey !== undefined && typeof typedArgs.siteKey !== 'string') {
109 | throw new McpError(ErrorCode.InvalidParams, 'siteKey must be a string if provided');
110 | }
111 | }
112 |
113 |
114 | export async function isGutenbergBlock(directory: string): Promise<boolean> {
115 | try {
116 | // Sprawdź czy istnieje package.json
117 | const packageJsonPath = path.join(directory, 'package.json');
118 | const packageJsonExists = await fs.access(packageJsonPath)
119 | .then(() => true)
120 | .catch(() => false);
121 |
122 | if (!packageJsonExists) return false;
123 |
124 | // Sprawdź zawartość package.json
125 | const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
126 |
127 | // Sprawdź charakterystyczne zależności dla bloków Gutenberga
128 | const dependencies = {
129 | ...packageJson.dependencies,
130 | ...packageJson.devDependencies
131 | };
132 |
133 | return (
134 | '@wordpress/scripts' in dependencies
135 | );
136 | } catch {
137 | return false;
138 | }
139 | }
140 |
141 | export async function shouldRebuildBlock(directory: string): Promise<boolean> {
142 | return isGutenbergBlock(directory);
143 | }
144 |
145 |
146 | // Funkcja sprawdzająca, czy WP-CLI jest zainstalowane lokalnie
147 | export const checkLocalWPCLI = (cwd: string) => {
148 | try {
149 | execSync(`php "${path.resolve(cwd, 'wp-cli.phar')}" --info`, {stdio: "pipe"});
150 | return true; // WP-CLI jest zainstalowane lokalnie
151 | } catch (error) {
152 | return false; // WP-CLI nie jest zainstalowane lokalnie
153 | }
154 | };
155 |
156 | export const checkGlobalWPCLI = () => {
157 | try {
158 | execSync("wp --info", {stdio: "pipe"});
159 | return true; // WP-CLI jest zainstalowane globalnie
160 | } catch (error) {
161 | return false; // WP-CLI nie jest zainstalowane globalnie
162 | }
163 | };
```
--------------------------------------------------------------------------------
/src/tools/filesystem/edit-block-json-file.ts:
--------------------------------------------------------------------------------
```typescript
1 | // src/tools/edit-file.ts
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 | import {promises as fs} from 'fs';
4 | import path from 'path';
5 | import {isGutenbergBlock} from '../../helpers.js';
6 | import {WordPressSite} from "../../types/wp-sites.js";
7 | import {buildBlock} from "./../build-block.js";
8 | import {listPluginFiles} from "tools/list-plugin-files";
9 |
10 |
11 | interface EditFileArgs {
12 | filePath: string;
13 | content: object;
14 | blockPluginDirname: string;
15 | siteKey: string;
16 | didUserConfirmChanges: boolean;
17 | }
18 |
19 | export const editBlockJsonFile = {
20 | name: "wp_edit_block_json_file",
21 | description: "Edits a file: block.json in WordPress Gutenberg Block Plugin with automatic rebuild detection and block.json file validation.",
22 | inputSchema: {
23 | type: "object",
24 | properties: {
25 | siteKey: {type: "string", description: "Site key"},
26 | blockPluginDirname: {
27 | type: "string",
28 | description: "Block plugin directory name."
29 | },
30 | filePath: {
31 | type: "string",
32 | description: "Path to the block.json file relative to the plugin root."
33 | },
34 | content: {
35 | type: "object",
36 | description: "New content for the block.json file in JSON format"
37 | },
38 | didUserConfirmChanges: {
39 | type: "boolean",
40 | description: "Are you sure about this changes from new content?"
41 | }
42 | },
43 | required: ["filePath", "blockPluginDirname", "siteKey", "content", "didUserConfirmChanges"]
44 | },
45 |
46 | async execute(args: EditFileArgs, site: WordPressSite) {
47 |
48 | const blockDir = path.join(site.pluginsPath, args.blockPluginDirname)
49 | const fullPath = path.join(blockDir, args.filePath);
50 |
51 | if (!(await isGutenbergBlock(blockDir))) {
52 | throw new Error(`wp_edit_block_json_file failed: directory ${blockDir} does not contain a Gutenberg block. Are you sure ${blockDir} is valid?`);
53 | }
54 |
55 | if (!args.filePath.endsWith('block.json')) {
56 | 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?`);
57 | }
58 |
59 | try {
60 | try {
61 | await fs.access(fullPath);
62 | } catch {
63 | const availableFiles = await listPluginFiles.execute({
64 | ...args,
65 | pluginDirName: args.blockPluginDirname
66 | }, site)
67 |
68 | throw new Error(`File block.json not found: ${fullPath}. Available files within dir ${blockDir}:\n${availableFiles?.files?.join('\n')}`);
69 | }
70 |
71 | let originalContent: string;
72 | try {
73 | originalContent = await fs.readFile(fullPath, 'utf-8');
74 | } catch (error) {
75 | const errorMessage = error instanceof Error ? error.message : 'Unknown read error';
76 | throw new Error(`Failed to read current block.json file: ${errorMessage}`);
77 | }
78 |
79 | let warnings = [];
80 | try {
81 | let newContent: any = args.content;
82 |
83 | if (newContent.style !== 'file:./style-index.css') {
84 | 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.')
85 | }
86 | if (newContent.editorStyle !== "file:./index.css") {
87 | 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.')
88 | }
89 | if (newContent.render) {
90 | 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.')
91 | }
92 |
93 | if (args.didUserConfirmChanges) {
94 | await fs.writeFile(fullPath, JSON.stringify(newContent), 'utf-8');
95 | } else {
96 | return {
97 | isError: true,
98 | content: [{
99 | type: "text",
100 | 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.`
101 | }]
102 | };
103 | }
104 | } catch (error) {
105 | throw new Error(`New content cannot be parsed: ${args.content}`);
106 | }
107 |
108 | const buildResult = await buildBlock.execute({
109 | blockPluginDirname: args.blockPluginDirname,
110 | siteKey: args.siteKey
111 | }, site);
112 |
113 | return {
114 | content: [{
115 | type: "text",
116 | 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')}`
117 | }]
118 | };
119 |
120 | } catch (error) {
121 | if (error instanceof Error) {
122 | throw new McpError(ErrorCode.InternalError, error.message);
123 | }
124 | throw new McpError(ErrorCode.InternalError, 'wp_edit_block_json_file: Unknown error occurred');
125 | }
126 | }
127 | };
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | // src/index.ts
2 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4 | import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from "@modelcontextprotocol/sdk/types.js";
5 | import fs from 'fs';
6 | import { validateAndGetSite, validateSiteToolArguments, WPSitesConfig } from './helpers.js';
7 | import { scaffoldBlockTool } from 'tools/scaffold-block.js';
8 | import { listPluginFiles } from "tools/list-plugin-files.js";
9 | import { listAvailablePluginsInSitePluginsPath } from "tools/list-available-plugins-in-site-plugins-path.js";
10 | import { buildBlock } from "tools/build-block.js";
11 | import { editBlockFile } from "tools/filesystem/edit-block-file.js";
12 | import { apiActivatePlugin } from "tools/wp-api/activate-plugin.js";
13 | import { apiCreatePost } from "tools/wp-api/create-post.js";
14 | import { apiUpdatePostStatus } from "tools/wp-api/update-post-status.js";
15 | import { apiGetPosts } from "tools/wp-api/get-posts.js";
16 | import { apiGetPostTypes } from "tools/wp-api/get-post-types.js";
17 | import { apiGetPlugins } from "tools/wp-api/get-plugins.js";
18 | import { apiUpdatePost } from "tools/wp-api/update-post.js";
19 | import { apiGetGutenbergBlocks } from "tools/wp-api/get-block-types.js";
20 | import { apiGetTemplates } from "tools/wp-api/get-templates.js";
21 | import { apiGetRestBaseForPostType } from "tools/wp-api/get-rest-base-for-post-types.js";
22 | import { apiUpdatePostContent } from "tools/wp-api/update-post-content.js";
23 | import { apiGetPost } from "tools/wp-api/get-post.js";
24 | import { cliInstallAndActivatePlugin } from "tools/wp-cli/instal-and-activate-plugin";
25 | import { editBlockJsonFile } from "tools/filesystem/edit-block-json-file";
26 | import { WordPressSite } from "types/wp-sites";
27 | import { apiDeactivatePlugin } from "tools/wp-api/deactivate-plugin";
28 | import { apiDeletePost } from "tools/wp-api/delete-post";
29 | import { apiGetSiteSettings } from "tools/wp-api/site-settings/get-site-settings";
30 | import { apiUpdateStringSiteSetting } from "tools/wp-api/site-settings/update-string-site-setting";
31 | import { apiGetPostPreviewLink } from "tools/wp-api/post/get-post-preview-link";
32 | import { apiUpdateIntegerSiteSetting } from "tools/wp-api/site-settings/update-integer-site-setting";
33 |
34 | const tools = [
35 | editBlockFile,
36 | scaffoldBlockTool,
37 | listPluginFiles,
38 | listAvailablePluginsInSitePluginsPath,
39 | buildBlock,
40 | editBlockJsonFile,
41 |
42 | apiActivatePlugin,
43 | apiDeactivatePlugin,
44 | apiCreatePost,
45 | apiDeletePost,
46 | apiUpdatePostStatus,
47 | apiGetPosts,
48 | apiGetPost,
49 | apiGetPostTypes,
50 | apiGetPlugins,
51 | apiUpdatePost,
52 | apiGetGutenbergBlocks,
53 | apiGetTemplates,
54 | apiGetRestBaseForPostType,
55 | apiUpdatePostContent,
56 | apiGetSiteSettings,
57 | apiUpdateStringSiteSetting,
58 | apiUpdateIntegerSiteSetting,
59 | apiGetPostPreviewLink,
60 | // apiGetPostContentBeforeChanges,
61 |
62 | cliInstallAndActivatePlugin,
63 | ];
64 |
65 |
66 | async function loadSiteConfig(): Promise<WPSitesConfig> {
67 | const configPath = process.env.WP_SITES_PATH || "/Users/michael/Code/mcp-test-server/wp-sites.json";
68 | if (!configPath) {
69 | throw new Error("WP_SITES_PATH environment variable is required");
70 | }
71 |
72 | try {
73 | const configData = await fs.readFileSync(configPath, {encoding: 'utf8'});
74 | return JSON.parse(configData) as WPSitesConfig;
75 | } catch (error) {
76 | if (error instanceof Error) {
77 | if ('code' in error && error.code === 'ENOENT') {
78 | throw new Error(`Config file not found at: ${configPath}`);
79 | }
80 | throw new Error(`Failed to load config: ${error.message}`);
81 | }
82 | throw new Error('An unknown error occurred while loading config');
83 | }
84 | }
85 |
86 | async function main() {
87 | try {
88 | const config = await loadSiteConfig();
89 |
90 | const server = new Server({
91 | name: "wordpress-mcp",
92 | version: "1.0.0"
93 | }, {
94 | capabilities: { tools: {} }
95 | });
96 |
97 |
98 | server.setRequestHandler(ListToolsRequestSchema, async () => ({
99 | tools: tools.map(tool => ({
100 | name: tool.name,
101 | description: tool.description,
102 | inputSchema: tool.inputSchema
103 | }))
104 | }));
105 |
106 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
107 | const {name, arguments: rawArgs} = request.params;
108 |
109 | validateSiteToolArguments(rawArgs);
110 | const {siteKey} = rawArgs;
111 |
112 | let site: WordPressSite;
113 | try {
114 | site = await validateAndGetSite(config, siteKey);
115 | } catch (error) {
116 | if (error instanceof Error) {
117 | return {
118 | isError: true,
119 | content: [{
120 | type: "text",
121 | text: `❌ ${error.message}`
122 | }]
123 | };
124 | }
125 | throw error
126 | }
127 |
128 | const tool = tools.find(t => t.name === name);
129 | if (!tool) {
130 | throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
131 | }
132 |
133 | if (name.startsWith('wp_cli_')) {
134 | let cliWrapper = '{{cmd}}'
135 | if (site.cli === 'localwp') {
136 | if (!site.localWpSshEntryFile || !fs.existsSync(site.localWpSshEntryFile)) {
137 | return {
138 | isError: true,
139 | content: [{
140 | type: "text",
141 | text: `❌ Option: "localWpSshEntryFile" for site "${siteKey}" is not specified in wp-sites.json while option "cli" is "localwp"`
142 | }]
143 | };
144 | }
145 | cliWrapper = `echo $(SHELL= && source "${site.localWpSshEntryFile}" &>/dev/null && {{cmd}})`
146 | } else {
147 | return {
148 | isError: true,
149 | content: [
150 | {
151 | type: "text",
152 | text: `❌ Option: "cli" for site "${siteKey}" is not specified in wp-sites.json`
153 | }
154 | ]
155 | };
156 | }
157 |
158 | // @ts-ignore
159 | return await tool.execute({
160 | ...rawArgs,
161 | siteKey
162 | }, site, cliWrapper);
163 | }
164 |
165 | // @ts-ignore
166 | return await tool.execute({
167 | ...rawArgs,
168 | siteKey
169 | }, site);
170 | });
171 |
172 | const transport = new StdioServerTransport();
173 | await server.connect(transport);
174 |
175 | // console.error(`WordPress MCP server started with ${Object.keys(config.sites).length} site(s) configured`);
176 | } catch (error) {
177 | console.error(`Server failed to start: ${error instanceof Error ? error.message : String(error)}`);
178 | process.exit(1);
179 | }
180 | }
181 |
182 | main();
```
--------------------------------------------------------------------------------
/src/tools/filesystem/edit-block-file.ts:
--------------------------------------------------------------------------------
```typescript
1 | // src/tools/edit-file.ts
2 | import {ErrorCode, McpError} from "@modelcontextprotocol/sdk/types.js";
3 | import {promises as fs} from 'fs';
4 | import path from 'path';
5 | import {isGutenbergBlock} from '../../helpers.js';
6 | import {WordPressSite} from "../../types/wp-sites.js";
7 | import {buildBlock} from "./../build-block.js";
8 |
9 | // Definicje typów dla operacji edycji plików
10 | interface FileOperation {
11 | type: 'write' | 'append' | 'modify' | 'smart_modify';
12 | content?: string;
13 | searchValue?: string;
14 | replaceValue?: string;
15 | }
16 |
17 | interface EditFileArgs {
18 | filePath: string;
19 | operation: FileOperation['type'];
20 | content?: string;
21 | searchValue?: string;
22 | replaceValue?: string;
23 | directory: string;
24 | blockPluginDirname: string;
25 | siteKey: string;
26 | }
27 |
28 | // Wzorce komentarzy wskazujące na zachowanie istniejącego kodu
29 | const KEEP_CODE_PATTERNS = [
30 | /\/\/ *(?:rest of|remaining|other) code (?:remains |stays )?(?:the )?same/i,
31 | /\/\/ *\.\.\./,
32 | /\/\* *\.\.\.? *\*\//,
33 | /\{?\s*\.\.\.\s*\}?/,
34 | /\/\/ *no changes/i,
35 | /\/\/ *unchanged/i
36 | ];
37 |
38 | // Funkcje pomocnicze
39 | function hasCodePlaceholder(content: string): boolean {
40 | return KEEP_CODE_PATTERNS.some(pattern => pattern.test(content));
41 | }
42 |
43 | async function mergeWithExistingCode(originalContent: string, newContent: string): Promise<string> {
44 | if (!hasCodePlaceholder(newContent)) {
45 | return newContent;
46 | }
47 |
48 | const originalLines = originalContent.split('\n');
49 | const newLines = newContent.split('\n');
50 | const result: string[] = [];
51 | let isKeepingOriginal = false;
52 |
53 | for (let i = 0; i < newLines.length; i++) {
54 | const currentLine = newLines[i];
55 |
56 | if (hasCodePlaceholder(currentLine)) {
57 | if (i > 0 && i < newLines.length - 1) {
58 | const previousLine = newLines[i - 1];
59 | const nextLine = newLines[i + 1];
60 |
61 | const startIndex = originalLines.findIndex(line => line.includes(previousLine));
62 | const endIndex = originalLines.findIndex((line, idx) => idx > startIndex && line.includes(nextLine));
63 |
64 | if (startIndex !== -1 && endIndex !== -1) {
65 | result.push(...originalLines.slice(startIndex + 1, endIndex));
66 | continue;
67 | }
68 | }
69 | isKeepingOriginal = true;
70 | continue;
71 | }
72 |
73 | if (!isKeepingOriginal) {
74 | result.push(currentLine);
75 | } else {
76 | isKeepingOriginal = false;
77 | }
78 | }
79 |
80 | return result.join('\n');
81 | }
82 |
83 | export const editBlockFile = {
84 | name: "wp_edit_block_file",
85 | description: "Edits a common file in WordPress Gutenberg Block Plugin with automatic rebuild detection",
86 | inputSchema: {
87 | type: "object",
88 | properties: {
89 | siteKey: {type: "string", description: "Site key"},
90 | blockPluginDirname: {
91 | type: "string",
92 | description: "Block plugin directory name."
93 | },
94 | filePath: {
95 | type: "string",
96 | description: "Path to the file relative to the plugin root."
97 | },
98 | content: {
99 | type: "string",
100 | description: "New content for the file"
101 | },
102 | operation: {
103 | type: "string",
104 | enum: ["write", "append", "modify", "smart_modify"],
105 | description: "Operation to perform on the file"
106 | },
107 | searchValue: {
108 | type: "string",
109 | description: "Text to search for when using modify operation"
110 | },
111 | replaceValue: {
112 | type: "string",
113 | description: "Text to replace with when using modify operation"
114 | }
115 | },
116 | required: ["filePath", "operation", "blockPluginDirname", "siteKey"]
117 | },
118 |
119 | async execute(args: EditFileArgs, site: WordPressSite) {
120 |
121 | const blockDir = path.join(site.pluginsPath, args.blockPluginDirname)
122 | const fullPath = path.join(blockDir, args.filePath);
123 |
124 | if (!(await isGutenbergBlock(blockDir))) {
125 | throw new Error(`wp_edit_block_file failed: directory ${blockDir} does not contain a Gutenberg block. Are you sure ${blockDir} is valid?`);
126 | }
127 | if (args.filePath.endsWith('block.json')) {
128 | throw new Error(`This tool cannot edit block.json file. Please use another tool: wp_edit_block_json_file instead and try again.`);
129 | }
130 | if (args.filePath.includes('/build/')) {
131 | throw new Error(`Files within "build" directory shouldn't be edited directly.`);
132 | }
133 |
134 | try {
135 | try {
136 | await fs.access(fullPath);
137 | } catch {
138 | throw new Error(`File not found: ${fullPath}`);
139 | }
140 |
141 | let originalContent: string;
142 | try {
143 | originalContent = await fs.readFile(fullPath, 'utf-8');
144 | } catch (error) {
145 | const errorMessage = error instanceof Error ? error.message : 'Unknown read error';
146 | throw new Error(`Failed to read file: ${errorMessage}`);
147 | }
148 |
149 | let newContent = originalContent;
150 |
151 | switch (args.operation) {
152 | case 'smart_modify':
153 | if (!args.content) {
154 | throw new Error('Content is required for smart_modify operation');
155 | }
156 | newContent = await mergeWithExistingCode(originalContent, args.content);
157 | break;
158 |
159 | case 'write':
160 | if (!args.content) {
161 | throw new Error('Content is required for write operation');
162 | }
163 | newContent = args.content;
164 | break;
165 |
166 | case 'append':
167 | if (!args.content) {
168 | throw new Error('Content is required for append operation');
169 | }
170 | newContent = originalContent + '\n' + args.content;
171 | break;
172 |
173 | case 'modify':
174 | if (!args.searchValue || !args.replaceValue) {
175 | throw new Error('searchValue and replaceValue are required for modify operation');
176 | }
177 | newContent = originalContent.replace(
178 | new RegExp(args.searchValue, 'g'),
179 | args.replaceValue
180 | );
181 | break;
182 |
183 | default:
184 | throw new Error(`Unknown operation: ${args.operation}`);
185 | }
186 |
187 | await fs.writeFile(fullPath, newContent, 'utf-8');
188 |
189 | try {
190 | const buildResult = await buildBlock.execute({
191 | blockPluginDirname: args.blockPluginDirname,
192 | siteKey: args.siteKey
193 | }, site);
194 |
195 | return {
196 | content: [{
197 | type: "text",
198 | text: `Block file: ${fullPath} edited successfully.\n\nBuild output:\n${buildResult.content[0].text}`
199 | }]
200 | };
201 | } catch (error) {
202 | const errorMessage = error instanceof Error ? error.message : 'Unknown build error';
203 | throw new Error(`Block file: ${fullPath} edited but build failed: ${errorMessage}. Please try build again on check error logs on your own.`);
204 | }
205 |
206 | return {
207 | content: [{
208 | type: "text",
209 | text: `Block file: ${fullPath} edited successfully.`
210 | }]
211 | };
212 |
213 | } catch (error) {
214 | if (error instanceof Error) {
215 | throw new McpError(ErrorCode.InternalError, error.message);
216 | }
217 | throw new McpError(ErrorCode.InternalError, 'wp_edit_block_file: Unknown error occurred');
218 | }
219 | }
220 | };
```