mcp-wordpress 1.1.2
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.
- package/LICENSE +21 -0
- package/README.md +568 -0
- package/bin/mcp-wordpress.js +12 -0
- package/bin/setup.js +302 -0
- package/bin/status.js +359 -0
- package/dist/client/WordPressClient.d.ts +81 -0
- package/dist/client/WordPressClient.d.ts.map +1 -0
- package/dist/client/WordPressClient.js +354 -0
- package/dist/client/WordPressClient.js.map +1 -0
- package/dist/client/api.d.ts +140 -0
- package/dist/client/api.d.ts.map +1 -0
- package/dist/client/api.js +727 -0
- package/dist/client/api.js.map +1 -0
- package/dist/client/auth.d.ts +121 -0
- package/dist/client/auth.d.ts.map +1 -0
- package/dist/client/auth.js +430 -0
- package/dist/client/auth.js.map +1 -0
- package/dist/client/managers/AuthenticationManager.d.ts +39 -0
- package/dist/client/managers/AuthenticationManager.d.ts.map +1 -0
- package/dist/client/managers/AuthenticationManager.js +159 -0
- package/dist/client/managers/AuthenticationManager.js.map +1 -0
- package/dist/client/managers/BaseManager.d.ts +22 -0
- package/dist/client/managers/BaseManager.d.ts.map +1 -0
- package/dist/client/managers/BaseManager.js +47 -0
- package/dist/client/managers/BaseManager.js.map +1 -0
- package/dist/client/managers/RequestManager.d.ts +45 -0
- package/dist/client/managers/RequestManager.d.ts.map +1 -0
- package/dist/client/managers/RequestManager.js +161 -0
- package/dist/client/managers/RequestManager.js.map +1 -0
- package/dist/client/managers/index.d.ts +8 -0
- package/dist/client/managers/index.d.ts.map +1 -0
- package/dist/client/managers/index.js +8 -0
- package/dist/client/managers/index.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +264 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +7 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/auth.d.ts +44 -0
- package/dist/tools/auth.d.ts.map +1 -0
- package/dist/tools/auth.js +126 -0
- package/dist/tools/auth.js.map +1 -0
- package/dist/tools/base.d.ts +37 -0
- package/dist/tools/base.d.ts.map +1 -0
- package/dist/tools/base.js +60 -0
- package/dist/tools/base.js.map +1 -0
- package/dist/tools/comments.d.ts +33 -0
- package/dist/tools/comments.d.ts.map +1 -0
- package/dist/tools/comments.js +228 -0
- package/dist/tools/comments.js.map +1 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +9 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/media.d.ts +29 -0
- package/dist/tools/media.d.ts.map +1 -0
- package/dist/tools/media.js +208 -0
- package/dist/tools/media.js.map +1 -0
- package/dist/tools/pages.d.ts +30 -0
- package/dist/tools/pages.d.ts.map +1 -0
- package/dist/tools/pages.js +211 -0
- package/dist/tools/pages.js.map +1 -0
- package/dist/tools/posts.d.ts +30 -0
- package/dist/tools/posts.d.ts.map +1 -0
- package/dist/tools/posts.js +240 -0
- package/dist/tools/posts.js.map +1 -0
- package/dist/tools/site.d.ts +31 -0
- package/dist/tools/site.d.ts.map +1 -0
- package/dist/tools/site.js +192 -0
- package/dist/tools/site.js.map +1 -0
- package/dist/tools/taxonomies.d.ts +37 -0
- package/dist/tools/taxonomies.d.ts.map +1 -0
- package/dist/tools/taxonomies.js +280 -0
- package/dist/tools/taxonomies.js.map +1 -0
- package/dist/tools/users.d.ts +28 -0
- package/dist/tools/users.d.ts.map +1 -0
- package/dist/tools/users.js +201 -0
- package/dist/tools/users.js.map +1 -0
- package/dist/types/client.d.ts +215 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/client.js +72 -0
- package/dist/types/client.js.map +1 -0
- package/dist/types/index.d.ts +157 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +12 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/mcp.d.ts +178 -0
- package/dist/types/mcp.d.ts.map +1 -0
- package/dist/types/mcp.js +7 -0
- package/dist/types/mcp.js.map +1 -0
- package/dist/types/wordpress.d.ts +443 -0
- package/dist/types/wordpress.d.ts.map +1 -0
- package/dist/types/wordpress.js +7 -0
- package/dist/types/wordpress.js.map +1 -0
- package/dist/utils/debug.d.ts +63 -0
- package/dist/utils/debug.d.ts.map +1 -0
- package/dist/utils/debug.js +195 -0
- package/dist/utils/debug.js.map +1 -0
- package/dist/utils/error.d.ts +19 -0
- package/dist/utils/error.d.ts.map +1 -0
- package/dist/utils/error.js +71 -0
- package/dist/utils/error.js.map +1 -0
- package/dist/utils/toolWrapper.d.ts +36 -0
- package/dist/utils/toolWrapper.d.ts.map +1 -0
- package/dist/utils/toolWrapper.js +90 -0
- package/dist/utils/toolWrapper.js.map +1 -0
- package/package.json +115 -0
- package/src/client/api.ts +1043 -0
- package/src/client/auth.ts +527 -0
- package/src/client/managers/AuthenticationManager.ts +190 -0
- package/src/client/managers/BaseManager.ts +73 -0
- package/src/client/managers/RequestManager.ts +214 -0
- package/src/client/managers/index.ts +8 -0
- package/src/index.ts +337 -0
- package/src/server.ts +7 -0
- package/src/tools/auth.ts +153 -0
- package/src/tools/comments.ts +263 -0
- package/src/tools/index.ts +8 -0
- package/src/tools/media.ts +240 -0
- package/src/tools/pages.ts +246 -0
- package/src/tools/posts.ts +277 -0
- package/src/tools/site.ts +227 -0
- package/src/tools/taxonomies.ts +322 -0
- package/src/tools/users.ts +233 -0
- package/src/types/client.ts +304 -0
- package/src/types/index.ts +207 -0
- package/src/types/mcp.ts +247 -0
- package/src/types/wordpress.ts +491 -0
- package/src/utils/debug.ts +258 -0
- package/src/utils/error.ts +88 -0
- package/src/utils/toolWrapper.ts +105 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for all client managers
|
|
3
|
+
* Provides common functionality and error handling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { WordPressClientConfig, RequestOptions } from "../../types/client.js";
|
|
7
|
+
import { WordPressAPIError, AuthenticationError, RateLimitError } from "../../types/client.js";
|
|
8
|
+
import { debug, logError } from "../../utils/debug.js";
|
|
9
|
+
import { getErrorMessage } from "../../utils/error.js";
|
|
10
|
+
|
|
11
|
+
export abstract class BaseManager {
|
|
12
|
+
protected config: WordPressClientConfig;
|
|
13
|
+
|
|
14
|
+
constructor(config: WordPressClientConfig) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Standardized error handling for all managers
|
|
20
|
+
*/
|
|
21
|
+
protected handleError(error: any, operation: string): never {
|
|
22
|
+
logError(`${operation} failed:`, error);
|
|
23
|
+
|
|
24
|
+
if (error instanceof WordPressAPIError) {
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (error.name === "AbortError" || error.code === "ABORT_ERR") {
|
|
29
|
+
throw new WordPressAPIError(
|
|
30
|
+
`Request timeout after ${this.config.timeout}ms`,
|
|
31
|
+
408,
|
|
32
|
+
"timeout"
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
|
|
37
|
+
throw new WordPressAPIError(
|
|
38
|
+
`Cannot connect to WordPress site: ${this.config.baseUrl}`,
|
|
39
|
+
503,
|
|
40
|
+
"connection_failed"
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const message = getErrorMessage(error);
|
|
45
|
+
throw new WordPressAPIError(
|
|
46
|
+
`${operation} failed: ${message}`,
|
|
47
|
+
500,
|
|
48
|
+
"unknown_error"
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Standardized success logging
|
|
54
|
+
*/
|
|
55
|
+
protected logSuccess(operation: string, details?: any): void {
|
|
56
|
+
debug.log(`${operation} completed successfully`, details);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Validate required parameters
|
|
61
|
+
*/
|
|
62
|
+
protected validateRequired(params: Record<string, any>, requiredFields: string[]): void {
|
|
63
|
+
for (const field of requiredFields) {
|
|
64
|
+
if (params[field] === undefined || params[field] === null) {
|
|
65
|
+
throw new WordPressAPIError(
|
|
66
|
+
`Missing required parameter: ${field}`,
|
|
67
|
+
400,
|
|
68
|
+
"missing_parameter"
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP Request Manager
|
|
3
|
+
* Handles all HTTP operations, rate limiting, and retries
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fetch from "node-fetch";
|
|
7
|
+
import type {
|
|
8
|
+
HTTPMethod,
|
|
9
|
+
RequestOptions,
|
|
10
|
+
ClientStats,
|
|
11
|
+
WordPressClientConfig
|
|
12
|
+
} from "../../types/client.js";
|
|
13
|
+
import { WordPressAPIError, RateLimitError } from "../../types/client.js";
|
|
14
|
+
import { BaseManager } from "./BaseManager.js";
|
|
15
|
+
import { debug, startTimer } from "../../utils/debug.js";
|
|
16
|
+
|
|
17
|
+
export class RequestManager extends BaseManager {
|
|
18
|
+
private stats: ClientStats;
|
|
19
|
+
private lastRequestTime: number = 0;
|
|
20
|
+
private requestInterval: number;
|
|
21
|
+
|
|
22
|
+
constructor(config: WordPressClientConfig) {
|
|
23
|
+
super(config);
|
|
24
|
+
|
|
25
|
+
this.requestInterval = 60000 / parseInt(process.env.RATE_LIMIT || "60");
|
|
26
|
+
this.stats = {
|
|
27
|
+
totalRequests: 0,
|
|
28
|
+
successfulRequests: 0,
|
|
29
|
+
failedRequests: 0,
|
|
30
|
+
averageResponseTime: 0,
|
|
31
|
+
rateLimitHits: 0,
|
|
32
|
+
authFailures: 0,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Make HTTP request with retry logic and rate limiting
|
|
38
|
+
*/
|
|
39
|
+
async request<T>(
|
|
40
|
+
method: HTTPMethod,
|
|
41
|
+
endpoint: string,
|
|
42
|
+
data?: any,
|
|
43
|
+
options: RequestOptions = {}
|
|
44
|
+
): Promise<T> {
|
|
45
|
+
const timer = startTimer();
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
await this.enforceRateLimit();
|
|
49
|
+
|
|
50
|
+
const response = await this.makeRequestWithRetry(method, endpoint, data, options);
|
|
51
|
+
|
|
52
|
+
this.stats.successfulRequests++;
|
|
53
|
+
this.updateAverageResponseTime(timer.end());
|
|
54
|
+
|
|
55
|
+
return response as T;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
this.stats.failedRequests++;
|
|
58
|
+
this.handleError(error, `${method} ${endpoint}`);
|
|
59
|
+
} finally {
|
|
60
|
+
this.stats.totalRequests++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Make request with retry logic
|
|
66
|
+
*/
|
|
67
|
+
private async makeRequestWithRetry<T>(
|
|
68
|
+
method: HTTPMethod,
|
|
69
|
+
endpoint: string,
|
|
70
|
+
data?: any,
|
|
71
|
+
options: RequestOptions = {}
|
|
72
|
+
): Promise<T> {
|
|
73
|
+
let lastError: any;
|
|
74
|
+
const maxRetries = options.retries ?? this.config.maxRetries ?? 3;
|
|
75
|
+
|
|
76
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
77
|
+
try {
|
|
78
|
+
return await this.makeRequest<T>(method, endpoint, data, options);
|
|
79
|
+
} catch (error: any) {
|
|
80
|
+
lastError = error;
|
|
81
|
+
|
|
82
|
+
// Don't retry on authentication errors or client errors
|
|
83
|
+
if (error.statusCode < 500 || attempt === maxRetries) {
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
debug.log(`Request failed (attempt ${attempt}/${maxRetries}):`, error.message);
|
|
88
|
+
|
|
89
|
+
// Exponential backoff
|
|
90
|
+
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
|
|
91
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
throw lastError;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Make single HTTP request
|
|
100
|
+
*/
|
|
101
|
+
private async makeRequest<T>(
|
|
102
|
+
method: HTTPMethod,
|
|
103
|
+
endpoint: string,
|
|
104
|
+
data?: any,
|
|
105
|
+
options: RequestOptions = {}
|
|
106
|
+
): Promise<T> {
|
|
107
|
+
const url = this.buildUrl(endpoint);
|
|
108
|
+
const timeout = options.timeout ?? this.config.timeout ?? 30000;
|
|
109
|
+
|
|
110
|
+
const controller = new AbortController();
|
|
111
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const fetchOptions: any = {
|
|
115
|
+
method,
|
|
116
|
+
headers: {
|
|
117
|
+
"Content-Type": "application/json",
|
|
118
|
+
"User-Agent": "MCP-WordPress/1.1.1",
|
|
119
|
+
...options.headers,
|
|
120
|
+
},
|
|
121
|
+
signal: controller.signal,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (data && method !== "GET") {
|
|
125
|
+
fetchOptions.body = JSON.stringify(data);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
debug.log(`API Request: ${method} ${url}`);
|
|
129
|
+
|
|
130
|
+
const response = await fetch(url, fetchOptions);
|
|
131
|
+
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
await this.handleErrorResponse(response);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const responseData = await response.json();
|
|
137
|
+
return responseData as T;
|
|
138
|
+
|
|
139
|
+
} finally {
|
|
140
|
+
clearTimeout(timeoutId);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Handle HTTP error responses
|
|
146
|
+
*/
|
|
147
|
+
private async handleErrorResponse(response: any): Promise<never> {
|
|
148
|
+
let errorData: any = {};
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
errorData = await response.json();
|
|
152
|
+
} catch {
|
|
153
|
+
// Ignore JSON parsing errors
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const message = errorData.message || `HTTP ${response.status}: ${response.statusText}`;
|
|
157
|
+
const code = errorData.code || "http_error";
|
|
158
|
+
|
|
159
|
+
if (response.status === 429) {
|
|
160
|
+
this.stats.rateLimitHits++;
|
|
161
|
+
throw new RateLimitError(message, Date.now() + 60000); // Retry after 1 minute
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (response.status === 401 || response.status === 403) {
|
|
165
|
+
this.stats.authFailures++;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
throw new WordPressAPIError(message, response.status, code, errorData);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Build full URL from endpoint
|
|
173
|
+
*/
|
|
174
|
+
private buildUrl(endpoint: string): string {
|
|
175
|
+
const baseUrl = this.config.baseUrl.replace(/\/$/, "");
|
|
176
|
+
const apiBase = "/wp-json/wp/v2";
|
|
177
|
+
const cleanEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
|
|
178
|
+
|
|
179
|
+
return `${baseUrl}${apiBase}${cleanEndpoint}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Enforce rate limiting
|
|
184
|
+
*/
|
|
185
|
+
private async enforceRateLimit(): Promise<void> {
|
|
186
|
+
const now = Date.now();
|
|
187
|
+
const timeSinceLastRequest = now - this.lastRequestTime;
|
|
188
|
+
|
|
189
|
+
if (timeSinceLastRequest < this.requestInterval) {
|
|
190
|
+
const delay = this.requestInterval - timeSinceLastRequest;
|
|
191
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.lastRequestTime = Date.now();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Update average response time
|
|
199
|
+
*/
|
|
200
|
+
private updateAverageResponseTime(responseTime: number): void {
|
|
201
|
+
const totalRequests = this.stats.successfulRequests;
|
|
202
|
+
const currentAverage = this.stats.averageResponseTime;
|
|
203
|
+
|
|
204
|
+
this.stats.averageResponseTime =
|
|
205
|
+
(currentAverage * (totalRequests - 1) + responseTime) / totalRequests;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get request statistics
|
|
210
|
+
*/
|
|
211
|
+
getStats(): ClientStats {
|
|
212
|
+
return { ...this.stats };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client Managers Index
|
|
3
|
+
* Exports all manager classes for the WordPress client
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { BaseManager } from "./BaseManager.js";
|
|
7
|
+
export { AuthenticationManager } from "./AuthenticationManager.js";
|
|
8
|
+
export { RequestManager } from "./RequestManager.js";
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import dotenv from "dotenv";
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { WordPressClient } from "./client/api.js";
|
|
8
|
+
import { AuthMethod, WordPressClientConfig } from "./types/client.js";
|
|
9
|
+
import * as Tools from "./tools/index.js";
|
|
10
|
+
import { getErrorMessage } from "./utils/error.js";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
|
|
13
|
+
// --- Constants ---
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
const rootDir = path.resolve(__dirname, "..");
|
|
17
|
+
const envPath = path.resolve(rootDir, ".env");
|
|
18
|
+
dotenv.config({ path: envPath });
|
|
19
|
+
|
|
20
|
+
const SERVER_VERSION = "1.1.1"; // Updated version with test fixes
|
|
21
|
+
|
|
22
|
+
// --- Main Server Class ---
|
|
23
|
+
class MCPWordPressServer {
|
|
24
|
+
private server: McpServer;
|
|
25
|
+
// MODIFICATION: Manages multiple WordPress clients, keyed by site ID.
|
|
26
|
+
private wordpressClients: Map<string, WordPressClient> = new Map();
|
|
27
|
+
private initialized: boolean = false;
|
|
28
|
+
// MODIFICATION: Stores the configurations for all loaded sites.
|
|
29
|
+
private siteConfigs: any[] = [];
|
|
30
|
+
|
|
31
|
+
constructor(mcpConfig?: any) {
|
|
32
|
+
this.loadConfiguration(mcpConfig);
|
|
33
|
+
|
|
34
|
+
if (this.wordpressClients.size === 0) {
|
|
35
|
+
console.error(
|
|
36
|
+
"No WordPress sites were configured. Please create mcp-wordpress.config.json or set environment variables.",
|
|
37
|
+
);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.server = new McpServer({
|
|
42
|
+
name: "mcp-wordpress",
|
|
43
|
+
version: SERVER_VERSION,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
this.setupTools();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private loadConfiguration(mcpConfig?: any) {
|
|
50
|
+
const configPath = path.resolve(rootDir, "mcp-wordpress.config.json");
|
|
51
|
+
|
|
52
|
+
if (fs.existsSync(configPath)) {
|
|
53
|
+
console.error(
|
|
54
|
+
"INFO: Found mcp-wordpress.config.json, loading multi-site configuration.",
|
|
55
|
+
);
|
|
56
|
+
this.loadMultiSiteConfig(configPath);
|
|
57
|
+
} else {
|
|
58
|
+
console.error(
|
|
59
|
+
"INFO: mcp-wordpress.config.json not found, falling back to environment variables for single-site mode.",
|
|
60
|
+
);
|
|
61
|
+
this.loadSingleSiteFromEnv(mcpConfig);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private loadMultiSiteConfig(configPath: string) {
|
|
66
|
+
try {
|
|
67
|
+
const configFile = fs.readFileSync(configPath, "utf-8");
|
|
68
|
+
const config = JSON.parse(configFile);
|
|
69
|
+
|
|
70
|
+
if (!config.sites || !Array.isArray(config.sites)) {
|
|
71
|
+
throw new Error('Configuration file must have a "sites" array.');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.siteConfigs = config.sites;
|
|
75
|
+
for (const site of this.siteConfigs) {
|
|
76
|
+
if (site.id && site.name && site.config) {
|
|
77
|
+
const clientConfig: WordPressClientConfig = {
|
|
78
|
+
baseUrl: site.config.WORDPRESS_SITE_URL,
|
|
79
|
+
auth: {
|
|
80
|
+
method:
|
|
81
|
+
(site.config.WORDPRESS_AUTH_METHOD as AuthMethod) ||
|
|
82
|
+
"app-password",
|
|
83
|
+
username: site.config.WORDPRESS_USERNAME,
|
|
84
|
+
appPassword: site.config.WORDPRESS_APP_PASSWORD,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
const client = new WordPressClient(clientConfig);
|
|
88
|
+
this.wordpressClients.set(site.id, client);
|
|
89
|
+
console.error(
|
|
90
|
+
`INFO: Initialized client for site: ${site.name} (ID: ${site.id})`,
|
|
91
|
+
);
|
|
92
|
+
} else {
|
|
93
|
+
console.warn(
|
|
94
|
+
"WARN: Skipping invalid site entry in config. Must have id, name, and config.",
|
|
95
|
+
site,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error(
|
|
101
|
+
`FATAL: Error reading or parsing mcp-wordpress.config.json: ${getErrorMessage(error)}`,
|
|
102
|
+
);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private loadSingleSiteFromEnv(mcpConfig?: any) {
|
|
108
|
+
const siteUrl =
|
|
109
|
+
mcpConfig?.wordpressSiteUrl || process.env.WORDPRESS_SITE_URL;
|
|
110
|
+
const username =
|
|
111
|
+
mcpConfig?.wordpressUsername || process.env.WORDPRESS_USERNAME;
|
|
112
|
+
const password =
|
|
113
|
+
mcpConfig?.wordpressAppPassword || process.env.WORDPRESS_APP_PASSWORD;
|
|
114
|
+
const authMethod = (mcpConfig?.wordpressAuthMethod ||
|
|
115
|
+
process.env.WORDPRESS_AUTH_METHOD ||
|
|
116
|
+
"app-password") as AuthMethod;
|
|
117
|
+
|
|
118
|
+
if (!siteUrl || !username || !password) {
|
|
119
|
+
console.error(
|
|
120
|
+
"ERROR: Missing required credentials for single-site mode.",
|
|
121
|
+
);
|
|
122
|
+
console.error(
|
|
123
|
+
"Please set WORDPRESS_SITE_URL, WORDPRESS_USERNAME, and WORDPRESS_APP_PASSWORD environment variables.",
|
|
124
|
+
);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const singleSiteConfig: WordPressClientConfig = {
|
|
129
|
+
baseUrl: siteUrl,
|
|
130
|
+
auth: { method: authMethod, username, appPassword: password },
|
|
131
|
+
};
|
|
132
|
+
const client = new WordPressClient(singleSiteConfig);
|
|
133
|
+
this.wordpressClients.set("default", client);
|
|
134
|
+
this.siteConfigs.push({
|
|
135
|
+
id: "default",
|
|
136
|
+
name: "Default Site",
|
|
137
|
+
config: singleSiteConfig,
|
|
138
|
+
});
|
|
139
|
+
console.error(
|
|
140
|
+
"INFO: Initialized client for default site in single-site mode.",
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private setupTools() {
|
|
145
|
+
// Register all tools from the tools directory
|
|
146
|
+
Object.values(Tools).forEach((ToolClass) => {
|
|
147
|
+
const toolInstance = new ToolClass();
|
|
148
|
+
const tools = toolInstance.getTools();
|
|
149
|
+
|
|
150
|
+
tools.forEach((tool) => {
|
|
151
|
+
this.registerTool(tool);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private registerTool(tool: any) {
|
|
157
|
+
// Create base parameter schema with site parameter
|
|
158
|
+
const baseSchema = {
|
|
159
|
+
site: z.string()
|
|
160
|
+
.optional()
|
|
161
|
+
.describe("The ID of the WordPress site to target (from mcp-wordpress.config.json). Required if multiple sites are configured."),
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Merge with tool-specific parameters
|
|
165
|
+
const parameterSchema = tool.parameters?.reduce((schema: any, param: any) => {
|
|
166
|
+
let zodType;
|
|
167
|
+
|
|
168
|
+
switch (param.type) {
|
|
169
|
+
case 'string':
|
|
170
|
+
zodType = z.string();
|
|
171
|
+
break;
|
|
172
|
+
case 'number':
|
|
173
|
+
zodType = z.number();
|
|
174
|
+
break;
|
|
175
|
+
case 'boolean':
|
|
176
|
+
zodType = z.boolean();
|
|
177
|
+
break;
|
|
178
|
+
case 'array':
|
|
179
|
+
zodType = z.array(z.string());
|
|
180
|
+
break;
|
|
181
|
+
case 'object':
|
|
182
|
+
zodType = z.record(z.any());
|
|
183
|
+
break;
|
|
184
|
+
default:
|
|
185
|
+
zodType = z.string();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (param.description) {
|
|
189
|
+
zodType = zodType.describe(param.description);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!param.required) {
|
|
193
|
+
zodType = zodType.optional();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
schema[param.name] = zodType;
|
|
197
|
+
return schema;
|
|
198
|
+
}, { ...baseSchema }) || baseSchema;
|
|
199
|
+
|
|
200
|
+
// Make site parameter required if multiple sites are configured
|
|
201
|
+
if (this.wordpressClients.size > 1) {
|
|
202
|
+
parameterSchema.site = parameterSchema.site.describe("The ID of the WordPress site to target (from mcp-wordpress.config.json). Required when multiple sites are configured.");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this.server.tool(
|
|
206
|
+
tool.name,
|
|
207
|
+
tool.description || `WordPress tool: ${tool.name}`,
|
|
208
|
+
parameterSchema,
|
|
209
|
+
async (args: any) => {
|
|
210
|
+
try {
|
|
211
|
+
const siteId = args.site || "default";
|
|
212
|
+
const client = this.wordpressClients.get(siteId);
|
|
213
|
+
|
|
214
|
+
if (!client) {
|
|
215
|
+
const availableSites = Array.from(this.wordpressClients.keys()).join(", ");
|
|
216
|
+
return {
|
|
217
|
+
content: [{
|
|
218
|
+
type: "text" as const,
|
|
219
|
+
text: `Error: Site with ID '${siteId}' not found. Available sites: ${availableSites}`
|
|
220
|
+
}],
|
|
221
|
+
isError: true
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Call the tool handler with the client and parameters
|
|
226
|
+
const result = await tool.handler(client, args);
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
content: [{
|
|
230
|
+
type: "text" as const,
|
|
231
|
+
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2)
|
|
232
|
+
}]
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (this.isAuthenticationError(error)) {
|
|
237
|
+
return {
|
|
238
|
+
content: [{
|
|
239
|
+
type: "text" as const,
|
|
240
|
+
text: `Authentication failed for site '${args.site || "default"}'. Please check your credentials.`
|
|
241
|
+
}],
|
|
242
|
+
isError: true
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
content: [{
|
|
248
|
+
type: "text" as const,
|
|
249
|
+
text: `Error: ${getErrorMessage(error)}`
|
|
250
|
+
}],
|
|
251
|
+
isError: true
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private async testClientConnections(): Promise<void> {
|
|
259
|
+
console.error(
|
|
260
|
+
"INFO: Testing connections to all configured WordPress sites...",
|
|
261
|
+
);
|
|
262
|
+
const connectionPromises = Array.from(this.wordpressClients.entries()).map(
|
|
263
|
+
async ([siteId, client]) => {
|
|
264
|
+
try {
|
|
265
|
+
await client.ping();
|
|
266
|
+
console.error(`SUCCESS: Connection to site '${siteId}' successful.`);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
console.error(
|
|
269
|
+
`ERROR: Failed to connect to site '${siteId}': ${getErrorMessage(error)}`,
|
|
270
|
+
);
|
|
271
|
+
if (this.isAuthenticationError(error)) {
|
|
272
|
+
console.error(
|
|
273
|
+
`Authentication may have failed for site '${siteId}'. Please check credentials.`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
);
|
|
279
|
+
await Promise.all(connectionPromises);
|
|
280
|
+
this.initialized = true;
|
|
281
|
+
console.error("INFO: Connection tests complete.");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private isAuthenticationError(error: any): boolean {
|
|
285
|
+
if (error?.response?.status && [401, 403].includes(error.response.status)) {
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
return error?.code === "WORDPRESS_AUTH_ERROR";
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async run() {
|
|
292
|
+
if (!this.initialized) {
|
|
293
|
+
await this.testClientConnections();
|
|
294
|
+
}
|
|
295
|
+
console.error("INFO: Starting MCP WordPress Server...");
|
|
296
|
+
|
|
297
|
+
// Connect to stdio transport
|
|
298
|
+
const transport = new StdioServerTransport();
|
|
299
|
+
await this.server.connect(transport);
|
|
300
|
+
|
|
301
|
+
console.error(
|
|
302
|
+
`INFO: Server started and connected. Tools available for ${this.wordpressClients.size} site(s).`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async shutdown() {
|
|
307
|
+
console.error("INFO: Shutting down MCP WordPress Server...");
|
|
308
|
+
await this.server.close();
|
|
309
|
+
console.error("INFO: Server stopped.");
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// --- Main Execution ---
|
|
314
|
+
async function main() {
|
|
315
|
+
try {
|
|
316
|
+
const mcpServer = new MCPWordPressServer();
|
|
317
|
+
await mcpServer.run();
|
|
318
|
+
|
|
319
|
+
const shutdown = async () => {
|
|
320
|
+
await mcpServer.shutdown();
|
|
321
|
+
process.exit(0);
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
process.on("SIGINT", shutdown);
|
|
325
|
+
process.on("SIGTERM", shutdown);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
console.error(`FATAL: Failed to start server: ${getErrorMessage(error)}`);
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
333
|
+
main();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export default MCPWordPressServer;
|
|
337
|
+
export { MCPWordPressServer };
|