mcp-wordpress 3.1.12 → 3.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,55 +21,42 @@ export class CacheTools {
21
21
  {
22
22
  name: "wp_cache_stats",
23
23
  description: "Get cache statistics for a WordPress site.",
24
- parameters: [
25
- {
26
- name: "site",
27
- type: "string",
28
- description:
29
- "Site ID to get cache stats for. If not provided, uses default site or fails if multiple sites configured.",
30
- },
31
- ],
24
+ inputSchema: {
25
+ type: "object" as const,
26
+ properties: {},
27
+ },
32
28
  handler: this.handleGetCacheStats.bind(this),
33
29
  },
34
30
  {
35
31
  name: "wp_cache_clear",
36
32
  description: "Clear cache for a WordPress site.",
37
- parameters: [
38
- {
39
- name: "site",
40
- type: "string",
41
- description: "Site ID to clear cache for.",
33
+ inputSchema: {
34
+ type: "object" as const,
35
+ properties: {
36
+ pattern: {
37
+ type: "string",
38
+ description: 'Optional pattern to clear specific cache entries (e.g., "posts", "categories").',
39
+ },
42
40
  },
43
- {
44
- name: "pattern",
45
- type: "string",
46
- description: 'Optional pattern to clear specific cache entries (e.g., "posts", "categories").',
47
- },
48
- ],
41
+ },
49
42
  handler: this.handleClearCache.bind(this),
50
43
  },
51
44
  {
52
45
  name: "wp_cache_warm",
53
46
  description: "Pre-warm cache with essential WordPress data.",
54
- parameters: [
55
- {
56
- name: "site",
57
- type: "string",
58
- description: "Site ID to warm cache for.",
59
- },
60
- ],
47
+ inputSchema: {
48
+ type: "object" as const,
49
+ properties: {},
50
+ },
61
51
  handler: this.handleWarmCache.bind(this),
62
52
  },
63
53
  {
64
54
  name: "wp_cache_info",
65
55
  description: "Get detailed cache configuration and status information.",
66
- parameters: [
67
- {
68
- name: "site",
69
- type: "string",
70
- description: "Site ID to get cache info for.",
71
- },
72
- ],
56
+ inputSchema: {
57
+ type: "object" as const,
58
+ properties: {},
59
+ },
73
60
  handler: this.handleGetCacheInfo.bind(this),
74
61
  },
75
62
  ];
@@ -78,10 +65,8 @@ export class CacheTools {
78
65
  /**
79
66
  * Get cache statistics
80
67
  */
81
- async handleGetCacheStats(params: { site?: string }) {
68
+ async handleGetCacheStats(client: WordPressClient, _params: Record<string, unknown>) {
82
69
  return toolWrapper(async () => {
83
- const client = this.resolveClient(params.site);
84
-
85
70
  if (!(client instanceof CachedWordPressClient)) {
86
71
  return {
87
72
  caching_enabled: false,
@@ -112,10 +97,8 @@ export class CacheTools {
112
97
  /**
113
98
  * Clear cache
114
99
  */
115
- async handleClearCache(params: { site?: string; pattern?: string }) {
100
+ async handleClearCache(client: WordPressClient, params: Record<string, unknown>) {
116
101
  return toolWrapper(async () => {
117
- const client = this.resolveClient(params.site);
118
-
119
102
  if (!(client instanceof CachedWordPressClient)) {
120
103
  return {
121
104
  success: false,
@@ -124,14 +107,15 @@ export class CacheTools {
124
107
  }
125
108
 
126
109
  let cleared: number;
110
+ const pattern = params.pattern as string | undefined;
127
111
 
128
- if (params.pattern) {
129
- cleared = client.clearCachePattern(params.pattern);
112
+ if (pattern) {
113
+ cleared = client.clearCachePattern(pattern);
130
114
  return {
131
115
  success: true,
132
- message: `Cleared ${cleared} cache entries matching pattern "${params.pattern}".`,
116
+ message: `Cleared ${cleared} cache entries matching pattern "${pattern}".`,
133
117
  cleared_entries: cleared,
134
- pattern: params.pattern,
118
+ pattern,
135
119
  };
136
120
  } else {
137
121
  cleared = client.clearCache();
@@ -147,10 +131,8 @@ export class CacheTools {
147
131
  /**
148
132
  * Warm cache with essential data
149
133
  */
150
- async handleWarmCache(params: { site?: string }) {
134
+ async handleWarmCache(client: WordPressClient, _params: Record<string, unknown>) {
151
135
  return toolWrapper(async () => {
152
- const client = this.resolveClient(params.site);
153
-
154
136
  if (!(client instanceof CachedWordPressClient)) {
155
137
  return {
156
138
  success: false,
@@ -174,10 +156,8 @@ export class CacheTools {
174
156
  /**
175
157
  * Get detailed cache information
176
158
  */
177
- async handleGetCacheInfo(params: { site?: string }) {
159
+ async handleGetCacheInfo(client: WordPressClient, _params: Record<string, unknown>) {
178
160
  return toolWrapper(async () => {
179
- const client = this.resolveClient(params.site);
180
-
181
161
  if (!(client instanceof CachedWordPressClient)) {
182
162
  return {
183
163
  caching_enabled: false,
@@ -224,32 +204,6 @@ export class CacheTools {
224
204
  };
225
205
  });
226
206
  }
227
-
228
- /**
229
- * Resolve client from site parameter
230
- */
231
- private resolveClient(siteId?: string): WordPressClient {
232
- if (!siteId) {
233
- if (this.clients.size === 1) {
234
- return Array.from(this.clients.values())[0];
235
- } else if (this.clients.size === 0) {
236
- throw new Error("No WordPress sites configured.");
237
- } else {
238
- throw new Error(
239
- `Multiple sites configured. Please specify --site parameter. Available sites: ${Array.from(
240
- this.clients.keys(),
241
- ).join(", ")}`,
242
- );
243
- }
244
- }
245
-
246
- const client = this.clients.get(siteId);
247
- if (!client) {
248
- throw new Error(`Site "${siteId}" not found. Available sites: ${Array.from(this.clients.keys()).join(", ")}`);
249
- }
250
-
251
- return client;
252
- }
253
207
  }
254
208
 
255
209
  export default CacheTools;
@@ -3,6 +3,7 @@ import { WordPressClient } from "@/client/api.js";
3
3
  import type { MCPToolSchema } from "@/types/mcp.js";
4
4
  import { MediaQueryParams, UpdateMediaRequest, UploadMediaRequest } from "@/types/wordpress.js";
5
5
  import { getErrorMessage } from "@/utils/error.js";
6
+ import { validateFilePath } from "@/utils/validation/security.js";
6
7
  import { toolParams } from "./params.js";
7
8
 
8
9
  /**
@@ -223,10 +224,16 @@ export class MediaTools {
223
224
  public async handleUploadMedia(client: WordPressClient, params: Record<string, unknown>): Promise<unknown> {
224
225
  const uploadParams = toolParams<UploadMediaRequest & { file_path: string }>(params);
225
226
  try {
227
+ // Validate file path to prevent path traversal attacks
228
+ // Set MCP_UPLOAD_BASE_DIR to restrict uploads to a specific directory (recommended in Docker)
229
+ const allowedBasePath = process.env.MCP_UPLOAD_BASE_DIR || "/";
230
+ const safePath = validateFilePath(uploadParams.file_path, allowedBasePath);
231
+ uploadParams.file_path = safePath;
232
+
226
233
  try {
227
- await fs.promises.access(uploadParams.file_path);
234
+ await fs.promises.access(safePath);
228
235
  } catch (_error) {
229
- throw new Error(`File not found at path: ${uploadParams.file_path}`);
236
+ throw new Error(`File not found at path: ${safePath}`);
230
237
  }
231
238
 
232
239
  const media = await client.uploadMedia(uploadParams);
@@ -212,8 +212,21 @@ export class PageTools {
212
212
  public async handleDeletePage(client: WordPressClient, params: Record<string, unknown>): Promise<unknown> {
213
213
  const { id, force } = params as { id: number; force?: boolean };
214
214
  try {
215
- await client.deletePage(id, force);
216
- const action = params.force ? "permanently deleted" : "moved to trash";
215
+ const result = await client.deletePage(id, force);
216
+ const action = force ? "permanently deleted" : "moved to trash";
217
+
218
+ if (result?.deleted === false) {
219
+ throw new Error(
220
+ `WordPress refused to delete page ${id}. The page may be protected or the operation was rejected.`,
221
+ );
222
+ }
223
+
224
+ if (result?.deleted) {
225
+ const title = result.previous?.title?.rendered;
226
+ return title ? `✅ Page "${title}" has been ${action}.` : `✅ Page ${id} has been ${action}.`;
227
+ }
228
+
229
+ // Some WordPress installations return empty/null responses on successful deletion
217
230
  return `✅ Page ${id} has been ${action}.`;
218
231
  } catch (_error) {
219
232
  throw new Error(`Failed to delete page: ${getErrorMessage(_error)}`);
@@ -99,7 +99,10 @@ export default class PerformanceTools {
99
99
  return [
100
100
  {
101
101
  name: "wp_performance_stats",
102
- description: "Get real-time performance statistics and metrics",
102
+ description:
103
+ "Get real-time performance statistics and metrics. " +
104
+ "Note: Top-level metrics (totalRequests, averageResponseTime, errorRate) are session-wide aggregates across all sites. " +
105
+ "Per-site cache and client stats are shown in the siteSpecific section when a site parameter is provided.",
103
106
  parameters: [
104
107
  {
105
108
  name: "site",
@@ -156,7 +159,9 @@ export default class PerformanceTools {
156
159
  },
157
160
  {
158
161
  name: "wp_performance_benchmark",
159
- description: "Compare current performance against industry benchmarks",
162
+ description:
163
+ "Compare current performance against industry benchmarks. " +
164
+ "Note: Benchmarks are based on session-wide aggregated metrics across all sites, not per-site metrics.",
160
165
  parameters: [
161
166
  {
162
167
  name: "site",
@@ -318,6 +323,7 @@ export default class PerformanceTools {
318
323
 
319
324
  if (category === "overview" || category === "all") {
320
325
  result.overview = {
326
+ scope: site ? "session-wide (all sites combined)" : "session-wide",
321
327
  overallHealth: calculateHealthStatus(metrics),
322
328
  performanceScore: calculatePerformanceScore(metrics),
323
329
  totalRequests: metrics.requests.total,
@@ -237,8 +237,8 @@ export class SiteAuditor {
237
237
  const posts = await this.client.getPosts({ per_page: this.config.maxPagesForContentAudit, status: ["publish"] });
238
238
  const pages = await this.client.getPages({ per_page: this.config.maxPagesForContentAudit, status: ["publish"] });
239
239
 
240
- // Get site info (mock for now)
241
- const siteUrl = "https://example.com"; // Would come from WordPress REST API
240
+ // Get site URL from the WordPress client configuration
241
+ const siteUrl = this.client.getSiteUrl();
242
242
 
243
243
  return {
244
244
  siteUrl,
@@ -599,9 +599,22 @@ export class SiteAuditor {
599
599
  }
600
600
 
601
601
  // Check for external dependencies (basic analysis)
602
+ let siteHostname: string;
603
+ try {
604
+ siteHostname = new URL(siteData.siteUrl).hostname;
605
+ } catch {
606
+ siteHostname = siteData.siteUrl.replace(/^https?:\/\//, "").replace(/[/:].*/g, "");
607
+ }
602
608
  const externalDependencies = [...siteData.posts, ...siteData.pages].reduce((count, item) => {
603
609
  const content = item.content?.rendered || "";
604
- const externalLinks = content.match(/https?:\/\/(?!example\.com)[^"'\s>]*/gi) || [];
610
+ const externalLinks =
611
+ content.match(/https?:\/\/[^"'\s>]*/gi)?.filter((url) => {
612
+ try {
613
+ return new URL(url).hostname !== siteHostname;
614
+ } catch {
615
+ return true;
616
+ }
617
+ }) || [];
605
618
  return count + externalLinks.length;
606
619
  }, 0);
607
620
 
package/src/tools/site.ts CHANGED
@@ -21,7 +21,8 @@ export class SiteTools {
21
21
  return [
22
22
  {
23
23
  name: "wp_get_site_settings",
24
- description: "Retrieves the general settings for a WordPress site.",
24
+ description:
25
+ "Retrieves the general settings for a WordPress site. Requires administrator role (manage_options capability).",
25
26
  inputSchema: {
26
27
  type: "object",
27
28
  properties: {},
@@ -30,7 +31,8 @@ export class SiteTools {
30
31
  },
31
32
  {
32
33
  name: "wp_update_site_settings",
33
- description: "Updates one or more general settings for a WordPress site.",
34
+ description:
35
+ "Updates one or more general settings for a WordPress site. Requires administrator role (manage_options capability).",
34
36
  inputSchema: {
35
37
  type: "object",
36
38
  properties: {
@@ -25,6 +25,8 @@ export class UserTools {
25
25
  name: "wp_list_users",
26
26
  description:
27
27
  "Lists users from a WordPress site with comprehensive filtering and detailed user information including roles, registration dates, and activity status.\n\n" +
28
+ "**Note:** Role, email, and registration date fields require **administrator** privileges. " +
29
+ "Non-admin users will see limited metadata due to WordPress REST API restrictions.\n\n" +
28
30
  "**Usage Examples:**\n" +
29
31
  "• List all users: `wp_list_users`\n" +
30
32
  '• Search users: `wp_list_users --search="john"`\n' +
@@ -206,16 +208,16 @@ export class UserTools {
206
208
  month: "short",
207
209
  day: "numeric",
208
210
  })
209
- : "Unknown";
211
+ : "Restricted (requires admin)";
210
212
 
211
- const roles = u.roles?.join(", ") || "No roles";
213
+ const roles = u.roles?.length ? u.roles.join(", ") : "Restricted (requires admin)";
212
214
  const description = u.description || "No description";
213
215
  const displayName = u.name || "No display name";
214
216
  const userUrl = u.url || "No URL";
215
217
 
216
218
  return (
217
219
  `- **ID ${u.id}**: ${displayName} (@${u.slug})\n` +
218
- ` 📧 Email: ${u.email || "No email"}\n` +
220
+ ` 📧 Email: ${u.email || "Restricted (requires admin)"}\n` +
219
221
  ` 🎭 Roles: ${roles}\n` +
220
222
  ` 📅 Registered: ${registrationDate}\n` +
221
223
  ` 🔗 URL: ${userUrl}\n` +