@automattic/mcp-wordpress-remote 0.2.0 → 0.2.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/README.md +425 -62
- package/dist/proxy.js +2024 -94
- package/package.json +55 -5
package/dist/proxy.js
CHANGED
|
@@ -4,94 +4,2011 @@
|
|
|
4
4
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
|
|
7
|
+
// src/lib/wordpress-api.ts
|
|
8
|
+
import { EventEmitter as EventEmitter3 } from "node:events";
|
|
9
|
+
|
|
7
10
|
// src/lib/utils.ts
|
|
8
11
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
12
|
+
|
|
13
|
+
// src/lib/config.ts
|
|
14
|
+
import * as os from "os";
|
|
15
|
+
import * as path from "path";
|
|
16
|
+
var CONFIG = {
|
|
17
|
+
// API Configuration
|
|
18
|
+
WP_API_URL: process.env.WP_API_URL || "https://example.com",
|
|
19
|
+
// OAuth Configuration (MCP Authorization specification 2025-06-18 compliant)
|
|
20
|
+
OAUTH_ENABLED: process.env.OAUTH_ENABLED === "true",
|
|
21
|
+
// Disabled by default, enable with 'true'
|
|
22
|
+
OAUTH_CALLBACK_PORT: parseInt(process.env.OAUTH_CALLBACK_PORT || "3000"),
|
|
23
|
+
OAUTH_HOST: process.env.OAUTH_HOST || "127.0.0.1",
|
|
24
|
+
WP_OAUTH_CLIENT_ID: process.env.WP_OAUTH_CLIENT_ID || "",
|
|
25
|
+
// No default - site-specific
|
|
26
|
+
// MCP OAuth 2.1 specific settings
|
|
27
|
+
OAUTH_FLOW_TYPE: process.env.OAUTH_FLOW_TYPE || "authorization_code",
|
|
28
|
+
OAUTH_USE_PKCE: process.env.OAUTH_USE_PKCE !== "false",
|
|
29
|
+
// PKCE required for OAuth 2.1
|
|
30
|
+
OAUTH_DYNAMIC_REGISTRATION: process.env.OAUTH_DYNAMIC_REGISTRATION !== "false",
|
|
31
|
+
// Dynamic client registration
|
|
32
|
+
// Resource Indicators (RFC 8707)
|
|
33
|
+
OAUTH_RESOURCE_INDICATOR: process.env.OAUTH_RESOURCE_INDICATOR !== "false",
|
|
34
|
+
// Resource parameter support
|
|
35
|
+
// Timeout Configuration (in milliseconds)
|
|
36
|
+
OAUTH_TIMEOUT: 3e4,
|
|
37
|
+
// 30 seconds
|
|
38
|
+
LOCK_TIMEOUT: 3e5,
|
|
39
|
+
// 5 minutes
|
|
40
|
+
// Directory Configuration
|
|
41
|
+
WP_MCP_CONFIG_DIR: process.env.WP_MCP_CONFIG_DIR || path.join(os.homedir(), ".mcp-auth"),
|
|
42
|
+
// Logging Configuration
|
|
43
|
+
LOG_FILE: process.env.LOG_FILE || null,
|
|
44
|
+
// Authentication Configuration (legacy support)
|
|
45
|
+
WP_API_USERNAME: process.env.WP_API_USERNAME,
|
|
46
|
+
WP_API_PASSWORD: process.env.WP_API_PASSWORD,
|
|
47
|
+
JWT_TOKEN: process.env.JWT_TOKEN,
|
|
48
|
+
WOO_CUSTOMER_KEY: process.env.WOO_CUSTOMER_KEY,
|
|
49
|
+
WOO_CUSTOMER_SECRET: process.env.WOO_CUSTOMER_SECRET,
|
|
50
|
+
// Environment
|
|
51
|
+
NODE_ENV: process.env.NODE_ENV || "development"
|
|
52
|
+
};
|
|
53
|
+
function isWordPressComSite(url) {
|
|
54
|
+
try {
|
|
55
|
+
const siteUrl = new URL(url);
|
|
56
|
+
const hostname2 = siteUrl.hostname.toLowerCase();
|
|
57
|
+
return hostname2.endsWith(".wordpress.com") || hostname2 === "wordpress.com" || // Jetpack sites often use WordPress.com OAuth
|
|
58
|
+
hostname2.endsWith(".wpcomstaging.com") || hostname2.endsWith(".wpcomstaging.net");
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function getRecommendedOAuthConfig(siteUrl) {
|
|
64
|
+
if (isWordPressComSite(siteUrl)) {
|
|
65
|
+
return {
|
|
66
|
+
flowType: "implicit",
|
|
67
|
+
usePKCE: false,
|
|
68
|
+
useResourceIndicator: false,
|
|
69
|
+
authorizationEndpoint: "https://public-api.wordpress.com/oauth2/authorize",
|
|
70
|
+
tokenEndpoint: "https://public-api.wordpress.com/oauth2/token",
|
|
71
|
+
description: "WordPress.com OAuth2 (compatible mode)"
|
|
72
|
+
};
|
|
73
|
+
} else {
|
|
74
|
+
return {
|
|
75
|
+
flowType: "authorization_code",
|
|
76
|
+
usePKCE: true,
|
|
77
|
+
useResourceIndicator: true,
|
|
78
|
+
authorizationEndpoint: void 0,
|
|
79
|
+
// Will be discovered
|
|
80
|
+
tokenEndpoint: void 0,
|
|
81
|
+
// Will be discovered
|
|
82
|
+
description: "MCP-compliant OAuth 2.1"
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function validateConfig() {
|
|
87
|
+
const errors = [];
|
|
88
|
+
const hasJWT = !!CONFIG.JWT_TOKEN;
|
|
89
|
+
const hasBasicAuth = !!(CONFIG.WP_API_USERNAME && CONFIG.WP_API_PASSWORD);
|
|
90
|
+
const hasOAuth = CONFIG.OAUTH_ENABLED;
|
|
91
|
+
if (!hasJWT && !hasBasicAuth && !hasOAuth) {
|
|
92
|
+
errors.push(
|
|
93
|
+
"No authentication method configured. Please set one of: JWT_TOKEN, WP_API_USERNAME+WP_API_PASSWORD, or enable OAuth"
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (!CONFIG.WP_API_URL || CONFIG.WP_API_URL === "https://example.com") {
|
|
97
|
+
errors.push("WP_API_URL must be set to your WordPress site URL");
|
|
98
|
+
}
|
|
99
|
+
if (CONFIG.OAUTH_ENABLED) {
|
|
100
|
+
if (isNaN(CONFIG.OAUTH_CALLBACK_PORT) || CONFIG.OAUTH_CALLBACK_PORT < 1 || CONFIG.OAUTH_CALLBACK_PORT > 65535) {
|
|
101
|
+
errors.push("OAUTH_CALLBACK_PORT must be a valid port number (1-65535)");
|
|
102
|
+
}
|
|
103
|
+
if (CONFIG.OAUTH_FLOW_TYPE === "implicit") {
|
|
104
|
+
errors.push(
|
|
105
|
+
"OAUTH_FLOW_TYPE=implicit is deprecated. MCP Authorization specification 2025-06-18 requires OAuth 2.1 with authorization_code flow"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (CONFIG.OAUTH_FLOW_TYPE === "authorization_code" && !CONFIG.OAUTH_USE_PKCE) {
|
|
109
|
+
errors.push(
|
|
110
|
+
"PKCE is required for OAuth 2.1 authorization_code flow (MCP Authorization specification 2025-06-18)"
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
isValid: errors.length === 0,
|
|
116
|
+
errors
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/lib/utils.ts
|
|
9
121
|
import * as fs from "node:fs";
|
|
10
|
-
import * as
|
|
11
|
-
var
|
|
12
|
-
|
|
13
|
-
|
|
122
|
+
import * as path2 from "node:path";
|
|
123
|
+
var LogLevel = /* @__PURE__ */ ((LogLevel4) => {
|
|
124
|
+
LogLevel4[LogLevel4["ERROR"] = 0] = "ERROR";
|
|
125
|
+
LogLevel4[LogLevel4["WARN"] = 1] = "WARN";
|
|
126
|
+
LogLevel4[LogLevel4["INFO"] = 2] = "INFO";
|
|
127
|
+
LogLevel4[LogLevel4["DEBUG"] = 3] = "DEBUG";
|
|
128
|
+
return LogLevel4;
|
|
129
|
+
})(LogLevel || {});
|
|
130
|
+
var CURRENT_LOG_LEVEL = process.env.LOG_LEVEL ? parseInt(process.env.LOG_LEVEL) : CONFIG.NODE_ENV === "development" ? 3 /* DEBUG */ : 2 /* INFO */;
|
|
131
|
+
if (CONFIG.LOG_FILE) {
|
|
132
|
+
const logDir = path2.dirname(CONFIG.LOG_FILE);
|
|
14
133
|
if (!fs.existsSync(logDir)) {
|
|
15
134
|
fs.mkdirSync(logDir, { recursive: true });
|
|
16
135
|
}
|
|
17
136
|
}
|
|
18
|
-
function log(message, ...args) {
|
|
137
|
+
function log(message, level = 2 /* INFO */, category = "GENERAL", ...args) {
|
|
138
|
+
if (level > CURRENT_LOG_LEVEL) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
19
141
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
20
|
-
const
|
|
21
|
-
const
|
|
142
|
+
const levelName = LogLevel[level];
|
|
143
|
+
const formattedArgs = args.length > 0 ? args.map((arg) => typeof arg === "object" ? JSON.stringify(arg, null, 2) : arg).join(" ") : "";
|
|
144
|
+
const logMessage = `${timestamp} [${levelName}] [${category}] ${message}${formattedArgs ? "\n" + formattedArgs : ""}
|
|
22
145
|
`;
|
|
23
|
-
|
|
24
|
-
if (LOG_FILE) {
|
|
25
|
-
fs.appendFileSync(LOG_FILE, logMessage);
|
|
146
|
+
process.stderr.write(logMessage);
|
|
147
|
+
if (CONFIG.LOG_FILE) {
|
|
148
|
+
fs.appendFileSync(CONFIG.LOG_FILE, logMessage);
|
|
26
149
|
}
|
|
27
150
|
}
|
|
151
|
+
var logger = {
|
|
152
|
+
error: (message, category = "ERROR", ...args) => log(message, 0 /* ERROR */, category, ...args),
|
|
153
|
+
warn: (message, category = "WARN", ...args) => log(message, 1 /* WARN */, category, ...args),
|
|
154
|
+
info: (message, category = "INFO", ...args) => log(message, 2 /* INFO */, category, ...args),
|
|
155
|
+
debug: (message, category = "DEBUG", ...args) => log(message, 3 /* DEBUG */, category, ...args),
|
|
156
|
+
// Specialized category loggers
|
|
157
|
+
auth: (message, level = 2 /* INFO */, ...args) => log(message, level, "AUTH", ...args),
|
|
158
|
+
oauth: (message, level = 2 /* INFO */, ...args) => log(message, level, "OAUTH", ...args),
|
|
159
|
+
api: (message, level = 2 /* INFO */, ...args) => log(message, level, "API", ...args),
|
|
160
|
+
config: (message, level = 2 /* INFO */, ...args) => log(message, level, "CONFIG", ...args)
|
|
161
|
+
};
|
|
28
162
|
|
|
29
|
-
// src/lib/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
163
|
+
// src/lib/oauth-types.ts
|
|
164
|
+
var OAuthError = class _OAuthError extends Error {
|
|
165
|
+
constructor(message, code = "OAUTH_ERROR", details) {
|
|
166
|
+
super(message);
|
|
167
|
+
this.name = "OAuthError";
|
|
168
|
+
this.code = code;
|
|
169
|
+
this.details = details;
|
|
170
|
+
Object.setPrototypeOf(this, _OAuthError.prototype);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
var AuthError = class _AuthError extends Error {
|
|
174
|
+
constructor(message, method, statusCode) {
|
|
175
|
+
super(message);
|
|
176
|
+
this.name = "AuthError";
|
|
177
|
+
this.method = method;
|
|
178
|
+
this.statusCode = statusCode;
|
|
179
|
+
Object.setPrototypeOf(this, _AuthError.prototype);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
var APIError = class _APIError extends Error {
|
|
183
|
+
constructor(message, statusCode, endpoint, response) {
|
|
184
|
+
super(message);
|
|
185
|
+
this.name = "APIError";
|
|
186
|
+
this.statusCode = statusCode;
|
|
187
|
+
this.endpoint = endpoint;
|
|
188
|
+
this.response = response;
|
|
189
|
+
Object.setPrototypeOf(this, _APIError.prototype);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// src/lib/persistent-auth-config.ts
|
|
194
|
+
import path3 from "path";
|
|
195
|
+
import fs2 from "fs/promises";
|
|
196
|
+
import fsSync from "fs";
|
|
197
|
+
import crypto from "crypto";
|
|
198
|
+
var VERSION = "0.2.1";
|
|
199
|
+
function getConfigDir() {
|
|
200
|
+
const baseConfigDir = CONFIG.WP_MCP_CONFIG_DIR;
|
|
201
|
+
return path3.join(baseConfigDir, `wordpress-remote-${VERSION}`);
|
|
202
|
+
}
|
|
203
|
+
async function ensureConfigDir() {
|
|
204
|
+
try {
|
|
205
|
+
const configDir = getConfigDir();
|
|
206
|
+
await fs2.mkdir(configDir, { recursive: true });
|
|
207
|
+
await fs2.chmod(configDir, 448);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
logger.error("Error creating config directory", "AUTH", error);
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function getConfigFilePath(serverUrlHash, filename) {
|
|
214
|
+
const configDir = getConfigDir();
|
|
215
|
+
return path3.join(configDir, `${serverUrlHash}_${filename}`);
|
|
216
|
+
}
|
|
217
|
+
async function deleteConfigFile(serverUrlHash, filename) {
|
|
218
|
+
try {
|
|
219
|
+
const filePath = getConfigFilePath(serverUrlHash, filename);
|
|
220
|
+
await fs2.unlink(filePath);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if (error.code !== "ENOENT") {
|
|
223
|
+
logger.error(`Error deleting ${filename}`, "AUTH", error);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async function readJsonFile(serverUrlHash, filename) {
|
|
228
|
+
try {
|
|
229
|
+
await ensureConfigDir();
|
|
230
|
+
const filePath = getConfigFilePath(serverUrlHash, filename);
|
|
231
|
+
const content = await fs2.readFile(filePath, "utf-8");
|
|
232
|
+
return JSON.parse(content);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (error.code === "ENOENT") {
|
|
235
|
+
return void 0;
|
|
236
|
+
}
|
|
237
|
+
logger.error(`Error reading ${filename}`, "AUTH", error);
|
|
238
|
+
return void 0;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function writeJsonFile(serverUrlHash, filename, data) {
|
|
242
|
+
try {
|
|
243
|
+
await ensureConfigDir();
|
|
244
|
+
const filePath = getConfigFilePath(serverUrlHash, filename);
|
|
245
|
+
await fs2.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
246
|
+
await fs2.chmod(filePath, 384);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
logger.error(`Error writing ${filename}`, "AUTH", error);
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
async function readTextFile(serverUrlHash, filename, errorMessage) {
|
|
253
|
+
try {
|
|
254
|
+
await ensureConfigDir();
|
|
255
|
+
const filePath = getConfigFilePath(serverUrlHash, filename);
|
|
256
|
+
return await fs2.readFile(filePath, "utf-8");
|
|
257
|
+
} catch (error) {
|
|
258
|
+
throw new Error(errorMessage || `Error reading ${filename}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function writeTextFile(serverUrlHash, filename, text) {
|
|
262
|
+
try {
|
|
263
|
+
await ensureConfigDir();
|
|
264
|
+
const filePath = getConfigFilePath(serverUrlHash, filename);
|
|
265
|
+
await fs2.writeFile(filePath, text, "utf-8");
|
|
266
|
+
await fs2.chmod(filePath, 384);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
logger.error(`Error writing ${filename}`, "AUTH", error);
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function generateServerUrlHash(serverUrl) {
|
|
273
|
+
return crypto.createHash("md5").update(serverUrl).digest("hex");
|
|
274
|
+
}
|
|
275
|
+
async function readTokens(serverUrlHash) {
|
|
276
|
+
try {
|
|
277
|
+
const tokens = await readJsonFile(serverUrlHash, "tokens.json");
|
|
278
|
+
if (tokens) {
|
|
279
|
+
logger.debug(`Loaded tokens for server hash: ${serverUrlHash}`, "AUTH");
|
|
280
|
+
return tokens;
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
} catch (error) {
|
|
284
|
+
logger.error(`Error reading tokens for ${serverUrlHash}`, "AUTH", error);
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async function writeTokens(serverUrlHash, tokens) {
|
|
289
|
+
try {
|
|
290
|
+
const tokensWithTimestamp = {
|
|
291
|
+
...tokens,
|
|
292
|
+
obtained_at: Date.now()
|
|
293
|
+
};
|
|
294
|
+
await writeJsonFile(serverUrlHash, "tokens.json", tokensWithTimestamp);
|
|
295
|
+
logger.info(`Stored tokens for server hash: ${serverUrlHash}`, "AUTH");
|
|
296
|
+
} catch (error) {
|
|
297
|
+
logger.error(`Error writing tokens for ${serverUrlHash}`, "AUTH", error);
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async function deleteTokens(serverUrlHash) {
|
|
302
|
+
try {
|
|
303
|
+
await deleteConfigFile(serverUrlHash, "tokens.json");
|
|
304
|
+
logger.info(`Deleted tokens for server hash: ${serverUrlHash}`, "AUTH");
|
|
305
|
+
} catch (error) {
|
|
306
|
+
logger.error(`Error deleting tokens for ${serverUrlHash}`, "AUTH", error);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async function readClientInfo(serverUrlHash) {
|
|
310
|
+
try {
|
|
311
|
+
const clientInfo = await readJsonFile(serverUrlHash, "client_info.json");
|
|
312
|
+
if (clientInfo) {
|
|
313
|
+
logger.debug(`Loaded client info for server hash: ${serverUrlHash}`, "AUTH");
|
|
314
|
+
return clientInfo;
|
|
315
|
+
}
|
|
316
|
+
return null;
|
|
317
|
+
} catch (error) {
|
|
318
|
+
logger.error(`Error reading client info for ${serverUrlHash}`, "AUTH", error);
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async function writeClientInfo(serverUrlHash, clientInfo) {
|
|
323
|
+
try {
|
|
324
|
+
await writeJsonFile(serverUrlHash, "client_info.json", clientInfo);
|
|
325
|
+
logger.info(`Stored client info for server hash: ${serverUrlHash}`, "AUTH");
|
|
326
|
+
} catch (error) {
|
|
327
|
+
logger.error(`Error writing client info for ${serverUrlHash}`, "AUTH", error);
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function isTokenValid(tokens) {
|
|
332
|
+
if (!tokens || !tokens.access_token) {
|
|
333
|
+
return { isValid: false, error: "No access token" };
|
|
334
|
+
}
|
|
335
|
+
if (!tokens.expires_in || !tokens.obtained_at) {
|
|
336
|
+
return { isValid: true };
|
|
337
|
+
}
|
|
338
|
+
const now = Date.now();
|
|
339
|
+
const expiresAt = tokens.obtained_at + tokens.expires_in * 1e3;
|
|
340
|
+
const expiresIn = Math.floor((expiresAt - now) / 1e3);
|
|
341
|
+
const isValid = expiresIn > 60;
|
|
342
|
+
return {
|
|
343
|
+
isValid,
|
|
344
|
+
expiresIn: Math.max(0, expiresIn),
|
|
345
|
+
error: isValid ? void 0 : "Token expired"
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
async function getValidTokens(serverUrlHash) {
|
|
349
|
+
const tokens = await readTokens(serverUrlHash);
|
|
350
|
+
if (!tokens) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
const validation = isTokenValid(tokens);
|
|
354
|
+
if (!validation.isValid) {
|
|
355
|
+
logger.warn(`Tokens for ${serverUrlHash} are invalid: ${validation.error}`, "AUTH");
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
return tokens;
|
|
359
|
+
}
|
|
360
|
+
async function cleanupExpiredTokens() {
|
|
361
|
+
try {
|
|
362
|
+
const configDir = getConfigDir();
|
|
363
|
+
if (!fsSync.existsSync(configDir)) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const files = await fs2.readdir(configDir);
|
|
367
|
+
const tokenFiles = files.filter((file) => file.endsWith("_tokens.json"));
|
|
368
|
+
let cleaned = 0;
|
|
369
|
+
for (const file of tokenFiles) {
|
|
370
|
+
const serverHash = file.replace("_tokens.json", "");
|
|
371
|
+
const tokens = await readTokens(serverHash);
|
|
372
|
+
if (tokens && !isTokenValid(tokens).isValid) {
|
|
373
|
+
await deleteTokens(serverHash);
|
|
374
|
+
cleaned++;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (cleaned > 0) {
|
|
378
|
+
logger.info(`Cleaned up ${cleaned} expired token files`, "AUTH");
|
|
379
|
+
}
|
|
380
|
+
} catch (error) {
|
|
381
|
+
logger.error("Error during token cleanup", "AUTH", error);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/lib/mcp-oauth-provider.ts
|
|
386
|
+
import open from "open";
|
|
387
|
+
import { EventEmitter } from "node:events";
|
|
388
|
+
|
|
389
|
+
// src/lib/mcp-oauth-utils.ts
|
|
390
|
+
import crypto2 from "crypto";
|
|
391
|
+
function generatePKCE() {
|
|
392
|
+
const codeVerifier = crypto2.randomBytes(64).toString("base64url").substring(0, 128);
|
|
393
|
+
const codeChallenge = crypto2.createHash("sha256").update(codeVerifier).digest("base64url");
|
|
394
|
+
return {
|
|
395
|
+
codeVerifier,
|
|
396
|
+
codeChallenge,
|
|
397
|
+
codeChallengeMethod: "S256"
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function generateCanonicalResourceURI(serverUrl) {
|
|
401
|
+
try {
|
|
402
|
+
const url = new URL(serverUrl);
|
|
403
|
+
url.protocol = url.protocol.toLowerCase();
|
|
404
|
+
url.hostname = url.hostname.toLowerCase();
|
|
405
|
+
url.hash = "";
|
|
406
|
+
if (url.pathname.endsWith("/") && url.pathname !== "/") {
|
|
407
|
+
url.pathname = url.pathname.slice(0, -1);
|
|
408
|
+
}
|
|
409
|
+
return url.toString();
|
|
410
|
+
} catch (error) {
|
|
411
|
+
throw new OAuthError(
|
|
412
|
+
`Invalid server URL for resource indicator: ${serverUrl}`,
|
|
413
|
+
"INVALID_RESOURCE_URI"
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
async function discoverAuthorizationServerMetadata(authorizationServerUrl) {
|
|
418
|
+
try {
|
|
419
|
+
const metadataUrl = new URL("/.well-known/oauth-authorization-server", authorizationServerUrl);
|
|
420
|
+
logger.oauth(`Discovering authorization server metadata: ${metadataUrl}`);
|
|
421
|
+
const response = await fetch(metadataUrl.toString());
|
|
422
|
+
if (!response.ok) {
|
|
423
|
+
throw new OAuthError(
|
|
424
|
+
`Failed to fetch authorization server metadata: ${response.status}`,
|
|
425
|
+
"METADATA_DISCOVERY_FAILED"
|
|
39
426
|
);
|
|
40
427
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
428
|
+
const metadata = await response.json();
|
|
429
|
+
if (!metadata.authorization_endpoint || !metadata.token_endpoint) {
|
|
430
|
+
throw new OAuthError(
|
|
431
|
+
"Authorization server metadata missing required endpoints",
|
|
432
|
+
"INVALID_METADATA"
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
logger.oauth("Authorization server metadata discovered successfully");
|
|
436
|
+
return metadata;
|
|
437
|
+
} catch (error) {
|
|
438
|
+
if (error instanceof OAuthError) {
|
|
439
|
+
throw error;
|
|
440
|
+
}
|
|
441
|
+
throw new OAuthError(
|
|
442
|
+
`Error discovering authorization server metadata: ${error instanceof Error ? error.message : String(error)}`,
|
|
443
|
+
"METADATA_DISCOVERY_ERROR"
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
async function discoverProtectedResourceMetadata(resourceUrl) {
|
|
448
|
+
try {
|
|
449
|
+
const url = new URL(resourceUrl);
|
|
450
|
+
const metadataUrl = new URL("/.well-known/oauth-protected-resource", url.origin);
|
|
451
|
+
logger.oauth(`Discovering protected resource metadata: ${metadataUrl}`);
|
|
452
|
+
const response = await fetch(metadataUrl.toString());
|
|
453
|
+
if (!response.ok) {
|
|
454
|
+
throw new OAuthError(
|
|
455
|
+
`Failed to fetch protected resource metadata: ${response.status}`,
|
|
456
|
+
"RESOURCE_METADATA_DISCOVERY_FAILED"
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
const metadata = await response.json();
|
|
460
|
+
if (!metadata.authorization_servers || metadata.authorization_servers.length === 0) {
|
|
461
|
+
throw new OAuthError(
|
|
462
|
+
"Protected resource metadata missing authorization servers",
|
|
463
|
+
"INVALID_RESOURCE_METADATA"
|
|
49
464
|
);
|
|
50
465
|
}
|
|
466
|
+
logger.oauth("Protected resource metadata discovered successfully");
|
|
467
|
+
return metadata;
|
|
468
|
+
} catch (error) {
|
|
469
|
+
if (error instanceof OAuthError) {
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
472
|
+
throw new OAuthError(
|
|
473
|
+
`Error discovering protected resource metadata: ${error instanceof Error ? error.message : String(error)}`,
|
|
474
|
+
"RESOURCE_METADATA_DISCOVERY_ERROR"
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
function parseWWWAuthenticateHeader(headerValue) {
|
|
479
|
+
const parts = headerValue.split(/\s+/);
|
|
480
|
+
const scheme = parts[0];
|
|
481
|
+
const result = { scheme };
|
|
482
|
+
const paramString = parts.slice(1).join(" ");
|
|
483
|
+
const paramMatches = paramString.matchAll(/(\w+)="([^"]+)"/g);
|
|
484
|
+
for (const match of paramMatches) {
|
|
485
|
+
const [, key, value] = match;
|
|
486
|
+
switch (key) {
|
|
487
|
+
case "realm":
|
|
488
|
+
result.realm = value;
|
|
489
|
+
break;
|
|
490
|
+
case "scope":
|
|
491
|
+
result.scope = value;
|
|
492
|
+
break;
|
|
493
|
+
case "error":
|
|
494
|
+
result.error = value;
|
|
495
|
+
break;
|
|
496
|
+
case "error_description":
|
|
497
|
+
result.error_description = value;
|
|
498
|
+
break;
|
|
499
|
+
case "error_uri":
|
|
500
|
+
result.error_uri = value;
|
|
501
|
+
break;
|
|
502
|
+
case "resource_metadata_url":
|
|
503
|
+
result.resource_metadata_url = value;
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return result;
|
|
508
|
+
}
|
|
509
|
+
async function registerDynamicClient(registrationEndpoint, registrationRequest) {
|
|
510
|
+
try {
|
|
511
|
+
logger.oauth(`Attempting dynamic client registration: ${registrationEndpoint}`);
|
|
512
|
+
const response = await fetch(registrationEndpoint, {
|
|
513
|
+
method: "POST",
|
|
514
|
+
headers: {
|
|
515
|
+
"Content-Type": "application/json"
|
|
516
|
+
},
|
|
517
|
+
body: JSON.stringify(registrationRequest)
|
|
518
|
+
});
|
|
519
|
+
if (!response.ok) {
|
|
520
|
+
const errorText = await response.text();
|
|
521
|
+
throw new OAuthError(
|
|
522
|
+
`Dynamic client registration failed: ${response.status} - ${errorText}`,
|
|
523
|
+
"CLIENT_REGISTRATION_FAILED"
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
const registrationResponse = await response.json();
|
|
527
|
+
if (!registrationResponse.client_id) {
|
|
528
|
+
throw new OAuthError(
|
|
529
|
+
"Dynamic client registration response missing client_id",
|
|
530
|
+
"INVALID_REGISTRATION_RESPONSE"
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
logger.oauth("Dynamic client registration successful");
|
|
534
|
+
logger.debug("Client ID obtained", "OAUTH", {
|
|
535
|
+
client_id: registrationResponse.client_id,
|
|
536
|
+
expires_at: registrationResponse.client_secret_expires_at
|
|
537
|
+
});
|
|
538
|
+
return registrationResponse;
|
|
539
|
+
} catch (error) {
|
|
540
|
+
if (error instanceof OAuthError) {
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
543
|
+
throw new OAuthError(
|
|
544
|
+
`Error during dynamic client registration: ${error instanceof Error ? error.message : String(error)}`,
|
|
545
|
+
"CLIENT_REGISTRATION_ERROR"
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
async function exchangeAuthorizationCode(tokenEndpoint, code, redirectUri, clientId, codeVerifier, resource) {
|
|
550
|
+
try {
|
|
551
|
+
logger.oauth("Exchanging authorization code for access token");
|
|
552
|
+
const params = new URLSearchParams({
|
|
553
|
+
grant_type: "authorization_code",
|
|
554
|
+
code,
|
|
555
|
+
redirect_uri: redirectUri,
|
|
556
|
+
client_id: clientId,
|
|
557
|
+
code_verifier: codeVerifier
|
|
558
|
+
});
|
|
559
|
+
if (resource) {
|
|
560
|
+
params.set("resource", resource);
|
|
561
|
+
}
|
|
562
|
+
const response = await fetch(tokenEndpoint, {
|
|
563
|
+
method: "POST",
|
|
564
|
+
headers: {
|
|
565
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
566
|
+
},
|
|
567
|
+
body: params.toString()
|
|
568
|
+
});
|
|
569
|
+
if (!response.ok) {
|
|
570
|
+
const errorText = await response.text();
|
|
571
|
+
throw new OAuthError(
|
|
572
|
+
`Token exchange failed: ${response.status} - ${errorText}`,
|
|
573
|
+
"TOKEN_EXCHANGE_FAILED"
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
const tokenResponse = await response.json();
|
|
577
|
+
if (!tokenResponse.access_token) {
|
|
578
|
+
throw new OAuthError("Token response missing access_token", "INVALID_TOKEN_RESPONSE");
|
|
579
|
+
}
|
|
580
|
+
logger.oauth("Authorization code exchange successful");
|
|
581
|
+
return {
|
|
582
|
+
access_token: tokenResponse.access_token,
|
|
583
|
+
token_type: tokenResponse.token_type || "Bearer",
|
|
584
|
+
expires_in: tokenResponse.expires_in,
|
|
585
|
+
scope: tokenResponse.scope,
|
|
586
|
+
refresh_token: tokenResponse.refresh_token,
|
|
587
|
+
obtained_at: Date.now()
|
|
588
|
+
};
|
|
589
|
+
} catch (error) {
|
|
590
|
+
if (error instanceof OAuthError) {
|
|
591
|
+
throw error;
|
|
592
|
+
}
|
|
593
|
+
throw new OAuthError(
|
|
594
|
+
`Error exchanging authorization code: ${error instanceof Error ? error.message : String(error)}`,
|
|
595
|
+
"TOKEN_EXCHANGE_ERROR"
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function buildAuthorizationUrl(authorizationEndpoint, clientId, redirectUri, scopes, state, codeChallenge, resource) {
|
|
600
|
+
const params = new URLSearchParams({
|
|
601
|
+
response_type: "code",
|
|
602
|
+
// OAuth 2.1 uses authorization code flow
|
|
603
|
+
client_id: clientId,
|
|
604
|
+
redirect_uri: redirectUri,
|
|
605
|
+
scope: scopes.join(" "),
|
|
606
|
+
state,
|
|
607
|
+
code_challenge: codeChallenge,
|
|
608
|
+
code_challenge_method: "S256"
|
|
609
|
+
});
|
|
610
|
+
if (resource) {
|
|
611
|
+
params.set("resource", resource);
|
|
612
|
+
}
|
|
613
|
+
return `${authorizationEndpoint}?${params.toString()}`;
|
|
614
|
+
}
|
|
615
|
+
function generateSecureState() {
|
|
616
|
+
return crypto2.randomBytes(32).toString("base64url");
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/lib/oauth-callback-server.ts
|
|
620
|
+
import express from "express";
|
|
621
|
+
var AUTHORIZATION_CODE_HTML = `
|
|
622
|
+
<!DOCTYPE html>
|
|
623
|
+
<html>
|
|
624
|
+
<head>
|
|
625
|
+
<title>WordPress MCP OAuth Authorization</title>
|
|
626
|
+
<style>
|
|
627
|
+
body {
|
|
628
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
629
|
+
margin: 0;
|
|
630
|
+
padding: 40px;
|
|
631
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
632
|
+
min-height: 100vh;
|
|
633
|
+
display: flex;
|
|
634
|
+
align-items: center;
|
|
635
|
+
justify-content: center;
|
|
636
|
+
}
|
|
637
|
+
.container {
|
|
638
|
+
background: white;
|
|
639
|
+
padding: 40px;
|
|
640
|
+
border-radius: 12px;
|
|
641
|
+
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
|
642
|
+
text-align: center;
|
|
643
|
+
max-width: 500px;
|
|
644
|
+
width: 100%;
|
|
645
|
+
}
|
|
646
|
+
h1 {
|
|
647
|
+
color: #333;
|
|
648
|
+
margin-bottom: 20px;
|
|
649
|
+
}
|
|
650
|
+
.success {
|
|
651
|
+
color: #27ae60;
|
|
652
|
+
font-size: 18px;
|
|
653
|
+
margin: 20px 0;
|
|
654
|
+
}
|
|
655
|
+
.error {
|
|
656
|
+
color: #e74c3c;
|
|
657
|
+
font-size: 18px;
|
|
658
|
+
margin: 20px 0;
|
|
659
|
+
}
|
|
660
|
+
.loading {
|
|
661
|
+
color: #3498db;
|
|
662
|
+
font-size: 18px;
|
|
663
|
+
margin: 20px 0;
|
|
664
|
+
}
|
|
665
|
+
.details {
|
|
666
|
+
color: #666;
|
|
667
|
+
margin-top: 10px;
|
|
668
|
+
font-size: 14px;
|
|
669
|
+
}
|
|
670
|
+
.spinner {
|
|
671
|
+
border: 3px solid #f3f3f3;
|
|
672
|
+
border-top: 3px solid #3498db;
|
|
673
|
+
border-radius: 50%;
|
|
674
|
+
width: 30px;
|
|
675
|
+
height: 30px;
|
|
676
|
+
animation: spin 1s linear infinite;
|
|
677
|
+
margin: 20px auto;
|
|
678
|
+
}
|
|
679
|
+
@keyframes spin {
|
|
680
|
+
0% { transform: rotate(0deg); }
|
|
681
|
+
100% { transform: rotate(360deg); }
|
|
682
|
+
}
|
|
683
|
+
</style>
|
|
684
|
+
</head>
|
|
685
|
+
<body>
|
|
686
|
+
<div class="container">
|
|
687
|
+
<h1>WordPress MCP OAuth Authorization</h1>
|
|
688
|
+
<div class="spinner" id="spinner"></div>
|
|
689
|
+
<div id="status" class="loading">Processing authorization code...</div>
|
|
690
|
+
<div id="details" class="details"></div>
|
|
691
|
+
</div>
|
|
692
|
+
|
|
693
|
+
<script>
|
|
694
|
+
function displayStatus(message, type = 'loading') {
|
|
695
|
+
const statusEl = document.getElementById('status');
|
|
696
|
+
const spinnerEl = document.getElementById('spinner');
|
|
697
|
+
|
|
698
|
+
statusEl.textContent = message;
|
|
699
|
+
statusEl.className = type;
|
|
700
|
+
|
|
701
|
+
if (type !== 'loading') {
|
|
702
|
+
spinnerEl.style.display = 'none';
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function displayDetails(message) {
|
|
707
|
+
document.getElementById('details').textContent = message;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Extract authorization code and state from URL query parameters (OAuth 2.1)
|
|
711
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
712
|
+
|
|
713
|
+
if (urlParams.has('code')) {
|
|
714
|
+
const authData = {
|
|
715
|
+
code: urlParams.get('code'),
|
|
716
|
+
state: urlParams.get('state'),
|
|
717
|
+
// Additional parameters for validation
|
|
718
|
+
iss: urlParams.get('iss'),
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
// Send authorization code to server for token exchange
|
|
722
|
+
fetch('/oauth/callback', {
|
|
723
|
+
method: 'POST',
|
|
724
|
+
headers: {
|
|
725
|
+
'Content-Type': 'application/json'
|
|
726
|
+
},
|
|
727
|
+
body: JSON.stringify(authData)
|
|
728
|
+
})
|
|
729
|
+
.then(response => {
|
|
730
|
+
if (response.ok) {
|
|
731
|
+
displayStatus('\u2705 Authorization successful!', 'success');
|
|
732
|
+
displayDetails('OAuth 2.1 authentication completed. You can now close this window.');
|
|
733
|
+
// Auto-close after 3 seconds
|
|
734
|
+
setTimeout(() => {
|
|
735
|
+
displayDetails('Closing window...');
|
|
736
|
+
setTimeout(() => window.close(), 1000);
|
|
737
|
+
}, 3000);
|
|
738
|
+
} else {
|
|
739
|
+
return response.json().then(err => {
|
|
740
|
+
throw new Error(err.error || 'Failed to exchange authorization code');
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
})
|
|
744
|
+
.catch(error => {
|
|
745
|
+
displayStatus('\u274C Error completing authorization', 'error');
|
|
746
|
+
displayDetails(error.message + '. Please try again.');
|
|
747
|
+
});
|
|
748
|
+
} else if (urlParams.has('error')) {
|
|
749
|
+
const error = urlParams.get('error');
|
|
750
|
+
const errorDescription = urlParams.get('error_description');
|
|
751
|
+
displayStatus('\u274C Authorization failed', 'error');
|
|
752
|
+
displayDetails(\`Error: \${error}\${errorDescription ? ' - ' + errorDescription : ''}\`);
|
|
753
|
+
} else {
|
|
754
|
+
displayStatus('\u274C No authorization code received', 'error');
|
|
755
|
+
displayDetails('Please try the authorization process again.');
|
|
756
|
+
}
|
|
757
|
+
</script>
|
|
758
|
+
</body>
|
|
759
|
+
</html>
|
|
760
|
+
`;
|
|
761
|
+
var OAuthCallbackServer = class {
|
|
762
|
+
constructor(options, events) {
|
|
763
|
+
this.server = null;
|
|
764
|
+
this.options = options;
|
|
765
|
+
this.events = events;
|
|
766
|
+
this.app = express();
|
|
767
|
+
this.setupRoutes();
|
|
768
|
+
}
|
|
769
|
+
setupRoutes() {
|
|
770
|
+
this.app.use(express.json());
|
|
771
|
+
this.app.get("/oauth/callback", (req, res) => {
|
|
772
|
+
logger.oauth("OAuth 2.1 authorization callback page requested");
|
|
773
|
+
res.send(AUTHORIZATION_CODE_HTML);
|
|
774
|
+
});
|
|
775
|
+
this.app.post("/oauth/callback", async (req, res) => {
|
|
776
|
+
try {
|
|
777
|
+
const { code, state, iss } = req.body;
|
|
778
|
+
if (!code) {
|
|
779
|
+
throw new OAuthError("No authorization code received");
|
|
780
|
+
}
|
|
781
|
+
logger.oauth("OAuth 2.1 authorization code received");
|
|
782
|
+
logger.debug("Authorization code details", "OAUTH", {
|
|
783
|
+
codeLength: code.length,
|
|
784
|
+
state: state || "none",
|
|
785
|
+
issuer: iss || "none"
|
|
786
|
+
});
|
|
787
|
+
this.events.emit("oauth-code-received", { code, state, iss });
|
|
788
|
+
res.json({ success: true, message: "Authorization code received successfully" });
|
|
789
|
+
} catch (error) {
|
|
790
|
+
logger.error("Error processing authorization code", "OAUTH", error);
|
|
791
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
792
|
+
this.events.emit("oauth-error", new OAuthError(errorMessage));
|
|
793
|
+
res.status(400).json({ error: errorMessage });
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
this.app.post("/oauth/tokens", async (req, res) => {
|
|
797
|
+
try {
|
|
798
|
+
const tokens = req.body;
|
|
799
|
+
if (!tokens.access_token) {
|
|
800
|
+
throw new OAuthError("No access token received");
|
|
801
|
+
}
|
|
802
|
+
logger.oauth("Legacy OAuth tokens received (implicit flow)");
|
|
803
|
+
logger.debug("Token details", "OAUTH", {
|
|
804
|
+
tokenType: tokens.token_type,
|
|
805
|
+
expiresIn: tokens.expires_in,
|
|
806
|
+
scope: tokens.scope,
|
|
807
|
+
tokenLength: tokens.access_token.length
|
|
808
|
+
});
|
|
809
|
+
await writeTokens(this.options.serverUrlHash, tokens);
|
|
810
|
+
this.events.emit("oauth-success", tokens);
|
|
811
|
+
res.json({ success: true, message: "Tokens saved successfully" });
|
|
812
|
+
} catch (error) {
|
|
813
|
+
logger.error("Error processing OAuth tokens", "OAUTH", error);
|
|
814
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
815
|
+
this.events.emit("oauth-error", new OAuthError(errorMessage));
|
|
816
|
+
res.status(400).json({ error: errorMessage });
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
this.app.get("/oauth/health", (req, res) => {
|
|
820
|
+
res.json({
|
|
821
|
+
status: "ok",
|
|
822
|
+
serverHash: this.options.serverUrlHash,
|
|
823
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
this.app.use(
|
|
827
|
+
(error, req, res, next) => {
|
|
828
|
+
logger.error("Express server error", "OAUTH", error);
|
|
829
|
+
res.status(500).json({ error: "Internal server error" });
|
|
830
|
+
}
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
async start() {
|
|
834
|
+
return new Promise((resolve, reject) => {
|
|
835
|
+
try {
|
|
836
|
+
this.server = this.app.listen(this.options.port, this.options.host, () => {
|
|
837
|
+
logger.oauth(
|
|
838
|
+
`OAuth callback server listening on http://${this.options.host}:${this.options.port}`
|
|
839
|
+
);
|
|
840
|
+
resolve();
|
|
841
|
+
});
|
|
842
|
+
this.server.on("error", (error) => {
|
|
843
|
+
logger.error("OAuth callback server error", "OAUTH", error);
|
|
844
|
+
reject(error);
|
|
845
|
+
});
|
|
846
|
+
if (this.options.timeout) {
|
|
847
|
+
setTimeout(() => {
|
|
848
|
+
if (!this.server?.listening) {
|
|
849
|
+
reject(new OAuthError("OAuth callback server startup timeout", "TIMEOUT"));
|
|
850
|
+
}
|
|
851
|
+
}, this.options.timeout);
|
|
852
|
+
}
|
|
853
|
+
} catch (error) {
|
|
854
|
+
reject(error);
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
async stop() {
|
|
859
|
+
return new Promise((resolve) => {
|
|
860
|
+
if (this.server) {
|
|
861
|
+
this.server.close(() => {
|
|
862
|
+
logger.oauth("OAuth callback server stopped");
|
|
863
|
+
this.server = null;
|
|
864
|
+
resolve();
|
|
865
|
+
});
|
|
866
|
+
} else {
|
|
867
|
+
resolve();
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
getCallbackUrl() {
|
|
872
|
+
return `http://${this.options.host}:${this.options.port}/oauth/callback`;
|
|
873
|
+
}
|
|
874
|
+
isRunning() {
|
|
875
|
+
return this.server?.listening ?? false;
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
function setupWPOAuthCallbackServer(options, events) {
|
|
879
|
+
return new OAuthCallbackServer(options, events);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// src/lib/mcp-oauth-provider.ts
|
|
883
|
+
var MCPOAuthProvider = class {
|
|
884
|
+
constructor(options) {
|
|
885
|
+
this.authPromise = null;
|
|
886
|
+
// OAuth 2.1 flow state
|
|
887
|
+
this.currentState = null;
|
|
888
|
+
this.currentPKCE = null;
|
|
889
|
+
// Discovered metadata
|
|
890
|
+
this.authServerMetadata = null;
|
|
891
|
+
this.resourceMetadata = null;
|
|
892
|
+
this.config = {
|
|
893
|
+
serverUrl: options.serverUrl || CONFIG.WP_API_URL,
|
|
894
|
+
resource: generateCanonicalResourceURI(options.serverUrl || CONFIG.WP_API_URL),
|
|
895
|
+
responseType: "code",
|
|
896
|
+
// OAuth 2.1 uses authorization code flow
|
|
897
|
+
usePKCE: true,
|
|
898
|
+
// PKCE is required for OAuth 2.1
|
|
899
|
+
redirectUri: `http://${CONFIG.OAUTH_HOST}:${CONFIG.OAUTH_CALLBACK_PORT}/oauth/callback`,
|
|
900
|
+
scopes: options.scopes || ["read", "write"],
|
|
901
|
+
clientId: options.clientId || CONFIG.WP_OAUTH_CLIENT_ID,
|
|
902
|
+
callbackPort: CONFIG.OAUTH_CALLBACK_PORT,
|
|
903
|
+
host: CONFIG.OAUTH_HOST,
|
|
904
|
+
timeout: CONFIG.OAUTH_TIMEOUT,
|
|
905
|
+
...options
|
|
906
|
+
};
|
|
907
|
+
this.serverUrlHash = generateServerUrlHash(this.config.serverUrl);
|
|
908
|
+
this.events = new EventEmitter();
|
|
909
|
+
this.events.setMaxListeners(10);
|
|
910
|
+
logger.oauth("Initialized MCP-compliant OAuth 2.1 provider");
|
|
911
|
+
logger.debug("OAuth configuration", "OAUTH", {
|
|
912
|
+
serverUrl: this.config.serverUrl,
|
|
913
|
+
resource: this.config.resource,
|
|
914
|
+
serverHash: this.serverUrlHash,
|
|
915
|
+
flowType: this.config.responseType,
|
|
916
|
+
usePKCE: this.config.usePKCE,
|
|
917
|
+
callbackPort: this.config.callbackPort
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Get current tokens if available and valid from persistent storage
|
|
922
|
+
*/
|
|
923
|
+
async tokens() {
|
|
924
|
+
try {
|
|
925
|
+
const tokens = await readTokens(this.serverUrlHash);
|
|
926
|
+
if (tokens) {
|
|
927
|
+
const validation = isTokenValid(tokens);
|
|
928
|
+
if (validation.isValid) {
|
|
929
|
+
logger.oauth("Found valid tokens in persistent storage");
|
|
930
|
+
if (validation.expiresIn) {
|
|
931
|
+
logger.debug(`Tokens expire in ${validation.expiresIn} seconds`, "OAUTH");
|
|
932
|
+
}
|
|
933
|
+
return tokens;
|
|
934
|
+
} else {
|
|
935
|
+
logger.warn(`Tokens in persistent storage are invalid: ${validation.error}`, "OAUTH");
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
logger.debug("No tokens found in persistent storage", "OAUTH");
|
|
940
|
+
return null;
|
|
941
|
+
} catch (error) {
|
|
942
|
+
logger.error("Error retrieving tokens from persistent storage", "OAUTH", error);
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Discover OAuth 2.1 server endpoints using MCP-compliant discovery
|
|
948
|
+
*/
|
|
949
|
+
async discoverOAuthEndpoints() {
|
|
950
|
+
try {
|
|
951
|
+
logger.oauth("Starting MCP-compliant OAuth endpoint discovery");
|
|
952
|
+
try {
|
|
953
|
+
this.resourceMetadata = await discoverProtectedResourceMetadata(this.config.resource);
|
|
954
|
+
logger.oauth("Protected resource metadata discovered");
|
|
955
|
+
} catch (error) {
|
|
956
|
+
logger.warn(
|
|
957
|
+
"Protected resource metadata discovery failed, trying 401 response method",
|
|
958
|
+
"OAUTH"
|
|
959
|
+
);
|
|
960
|
+
await this.discoverVia401Response();
|
|
961
|
+
}
|
|
962
|
+
const authServerUrl = this.getAuthorizationServerUrl();
|
|
963
|
+
if (authServerUrl) {
|
|
964
|
+
this.authServerMetadata = await discoverAuthorizationServerMetadata(authServerUrl);
|
|
965
|
+
logger.oauth("Authorization server metadata discovered");
|
|
966
|
+
this.config.authorizationEndpoint = this.authServerMetadata.authorization_endpoint;
|
|
967
|
+
this.config.tokenEndpoint = this.authServerMetadata.token_endpoint;
|
|
968
|
+
this.config.registrationEndpoint = this.authServerMetadata.registration_endpoint;
|
|
969
|
+
} else {
|
|
970
|
+
throw new OAuthError("Could not determine authorization server URL", "DISCOVERY_FAILED");
|
|
971
|
+
}
|
|
972
|
+
} catch (error) {
|
|
973
|
+
logger.error("OAuth endpoint discovery failed", "OAUTH", error);
|
|
974
|
+
throw error;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Discover endpoints via 401 Unauthorized response (RFC 9728 Section 5.1)
|
|
979
|
+
*/
|
|
980
|
+
async discoverVia401Response() {
|
|
981
|
+
try {
|
|
982
|
+
logger.oauth("Attempting discovery via 401 response method");
|
|
983
|
+
const response = await fetch(this.config.serverUrl, {
|
|
984
|
+
headers: {
|
|
985
|
+
Accept: "application/json"
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
if (response.status === 401) {
|
|
989
|
+
const wwwAuthHeader = response.headers.get("WWW-Authenticate");
|
|
990
|
+
if (wwwAuthHeader) {
|
|
991
|
+
const authInfo = parseWWWAuthenticateHeader(wwwAuthHeader);
|
|
992
|
+
logger.oauth("WWW-Authenticate header parsed successfully");
|
|
993
|
+
if (authInfo.resource_metadata_url) {
|
|
994
|
+
const metadataResponse = await fetch(authInfo.resource_metadata_url);
|
|
995
|
+
if (metadataResponse.ok) {
|
|
996
|
+
this.resourceMetadata = await metadataResponse.json();
|
|
997
|
+
logger.oauth("Protected resource metadata obtained via 401 response");
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
logger.warn("401 response discovery method failed", "OAUTH", error);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Get authorization server URL from discovered metadata
|
|
1008
|
+
*/
|
|
1009
|
+
getAuthorizationServerUrl() {
|
|
1010
|
+
if (this.resourceMetadata?.authorization_servers?.length) {
|
|
1011
|
+
return this.resourceMetadata.authorization_servers[0];
|
|
1012
|
+
}
|
|
1013
|
+
try {
|
|
1014
|
+
const url = new URL(this.config.serverUrl);
|
|
1015
|
+
return url.origin;
|
|
1016
|
+
} catch {
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Perform dynamic client registration if needed and supported
|
|
1022
|
+
*/
|
|
1023
|
+
async ensureClientRegistration() {
|
|
1024
|
+
if (this.config.clientId) {
|
|
1025
|
+
logger.oauth("Using provided client ID, skipping dynamic registration");
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
if (!CONFIG.OAUTH_DYNAMIC_REGISTRATION) {
|
|
1029
|
+
throw new OAuthError(
|
|
1030
|
+
"No client ID provided and dynamic registration is disabled",
|
|
1031
|
+
"NO_CLIENT_ID"
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
if (!this.authServerMetadata?.registration_endpoint) {
|
|
1035
|
+
throw new OAuthError(
|
|
1036
|
+
"Dynamic client registration not supported by authorization server",
|
|
1037
|
+
"NO_DYNAMIC_REGISTRATION"
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
try {
|
|
1041
|
+
logger.oauth("Performing dynamic client registration");
|
|
1042
|
+
const registrationRequest = {
|
|
1043
|
+
redirect_uris: [this.config.redirectUri],
|
|
1044
|
+
token_endpoint_auth_method: "none",
|
|
1045
|
+
// Public client
|
|
1046
|
+
grant_types: ["authorization_code"],
|
|
1047
|
+
response_types: ["code"],
|
|
1048
|
+
client_name: "WordPress MCP Remote",
|
|
1049
|
+
scope: this.config.scopes.join(" ")
|
|
1050
|
+
};
|
|
1051
|
+
const registrationResponse = await registerDynamicClient(
|
|
1052
|
+
this.authServerMetadata.registration_endpoint,
|
|
1053
|
+
registrationRequest
|
|
1054
|
+
);
|
|
1055
|
+
const clientInfo = {
|
|
1056
|
+
client_id: registrationResponse.client_id,
|
|
1057
|
+
client_secret: registrationResponse.client_secret
|
|
1058
|
+
};
|
|
1059
|
+
await writeClientInfo(this.serverUrlHash, clientInfo);
|
|
1060
|
+
this.config.clientId = registrationResponse.client_id;
|
|
1061
|
+
logger.oauth("Dynamic client registration completed successfully");
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
logger.error("Dynamic client registration failed", "OAUTH", error);
|
|
1064
|
+
throw error;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Initiate MCP-compliant OAuth 2.1 authorization flow
|
|
1069
|
+
*/
|
|
1070
|
+
async authorize() {
|
|
1071
|
+
if (this.authPromise) {
|
|
1072
|
+
logger.oauth("Authorization already in progress, waiting...");
|
|
1073
|
+
await this.authPromise;
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
const existingTokens = await this.tokens();
|
|
1077
|
+
if (existingTokens) {
|
|
1078
|
+
logger.oauth("Already have valid tokens, skipping authorization");
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
logger.oauth("Starting MCP-compliant OAuth 2.1 authorization flow");
|
|
1082
|
+
this.authPromise = this.performAuthorization();
|
|
1083
|
+
try {
|
|
1084
|
+
await this.authPromise;
|
|
1085
|
+
logger.oauth("OAuth 2.1 authorization completed successfully");
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
logger.error("OAuth 2.1 authorization failed", "OAUTH", error);
|
|
1088
|
+
throw error;
|
|
1089
|
+
} finally {
|
|
1090
|
+
this.authPromise = null;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Perform the complete OAuth 2.1 authorization flow
|
|
1095
|
+
*/
|
|
1096
|
+
async performAuthorization() {
|
|
1097
|
+
try {
|
|
1098
|
+
await this.discoverOAuthEndpoints();
|
|
1099
|
+
await this.ensureClientRegistration();
|
|
1100
|
+
this.currentPKCE = generatePKCE();
|
|
1101
|
+
this.currentState = generateSecureState();
|
|
1102
|
+
await writeTextFile(this.serverUrlHash, "pkce_verifier.txt", this.currentPKCE.codeVerifier);
|
|
1103
|
+
await writeTextFile(this.serverUrlHash, "oauth_state.txt", this.currentState);
|
|
1104
|
+
const callbackServer = setupWPOAuthCallbackServer(
|
|
1105
|
+
{
|
|
1106
|
+
port: this.config.callbackPort,
|
|
1107
|
+
host: this.config.host,
|
|
1108
|
+
serverUrlHash: this.serverUrlHash,
|
|
1109
|
+
timeout: this.config.timeout
|
|
1110
|
+
},
|
|
1111
|
+
this.events
|
|
1112
|
+
);
|
|
1113
|
+
await callbackServer.start();
|
|
1114
|
+
logger.oauth("OAuth callback server started");
|
|
1115
|
+
const authUrl = buildAuthorizationUrl(
|
|
1116
|
+
this.config.authorizationEndpoint,
|
|
1117
|
+
this.config.clientId,
|
|
1118
|
+
this.config.redirectUri,
|
|
1119
|
+
this.config.scopes,
|
|
1120
|
+
this.currentState,
|
|
1121
|
+
this.currentPKCE.codeChallenge,
|
|
1122
|
+
CONFIG.OAUTH_RESOURCE_INDICATOR ? this.config.resource : void 0
|
|
1123
|
+
);
|
|
1124
|
+
logger.oauth("Built OAuth 2.1 authorization URL");
|
|
1125
|
+
logger.debug("Authorization URL", "OAUTH", { url: authUrl });
|
|
1126
|
+
try {
|
|
1127
|
+
await open(authUrl);
|
|
1128
|
+
logger.oauth("Browser opened successfully");
|
|
1129
|
+
} catch (browserError) {
|
|
1130
|
+
logger.error("Failed to open browser automatically", "OAUTH", browserError);
|
|
1131
|
+
logger.info("\n=== MANUAL ACTION REQUIRED ===");
|
|
1132
|
+
logger.info("Please manually open the following URL in your browser:");
|
|
1133
|
+
logger.info(`${authUrl}`);
|
|
1134
|
+
logger.info("===============================\n");
|
|
1135
|
+
}
|
|
1136
|
+
const authCode = await this.waitForAuthorizationCode();
|
|
1137
|
+
const tokens = await this.exchangeCodeForTokens(authCode);
|
|
1138
|
+
await writeTokens(this.serverUrlHash, tokens);
|
|
1139
|
+
logger.oauth("OAuth 2.1 tokens stored successfully");
|
|
1140
|
+
await callbackServer.stop();
|
|
1141
|
+
return tokens;
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
logger.error("Error during OAuth 2.1 authorization flow", "OAUTH", error);
|
|
1144
|
+
throw error;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Wait for authorization code from callback
|
|
1149
|
+
*/
|
|
1150
|
+
async waitForAuthorizationCode() {
|
|
1151
|
+
return new Promise((resolve, reject) => {
|
|
1152
|
+
const timeout = setTimeout(() => {
|
|
1153
|
+
cleanup();
|
|
1154
|
+
reject(new OAuthError("Authorization timeout", "TIMEOUT"));
|
|
1155
|
+
}, this.config.timeout);
|
|
1156
|
+
const cleanup = () => {
|
|
1157
|
+
this.events.removeAllListeners("oauth-code-received");
|
|
1158
|
+
this.events.removeAllListeners("oauth-error");
|
|
1159
|
+
clearTimeout(timeout);
|
|
1160
|
+
};
|
|
1161
|
+
this.events.once("oauth-code-received", async ({ code, state }) => {
|
|
1162
|
+
cleanup();
|
|
1163
|
+
if (state !== this.currentState) {
|
|
1164
|
+
reject(new OAuthError("State parameter mismatch", "INVALID_STATE"));
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
logger.oauth("Valid authorization code received");
|
|
1168
|
+
resolve(code);
|
|
1169
|
+
});
|
|
1170
|
+
this.events.once("oauth-error", (error) => {
|
|
1171
|
+
cleanup();
|
|
1172
|
+
reject(error);
|
|
1173
|
+
});
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Exchange authorization code for access tokens
|
|
1178
|
+
*/
|
|
1179
|
+
async exchangeCodeForTokens(code) {
|
|
1180
|
+
try {
|
|
1181
|
+
const codeVerifier = await readTextFile(
|
|
1182
|
+
this.serverUrlHash,
|
|
1183
|
+
"pkce_verifier.txt",
|
|
1184
|
+
"PKCE code verifier not found"
|
|
1185
|
+
);
|
|
1186
|
+
const tokenResponse = await exchangeAuthorizationCode(
|
|
1187
|
+
this.config.tokenEndpoint,
|
|
1188
|
+
code,
|
|
1189
|
+
this.config.redirectUri,
|
|
1190
|
+
this.config.clientId,
|
|
1191
|
+
codeVerifier,
|
|
1192
|
+
CONFIG.OAUTH_RESOURCE_INDICATOR ? this.config.resource : void 0
|
|
1193
|
+
);
|
|
1194
|
+
await deleteConfigFile(this.serverUrlHash, "pkce_verifier.txt");
|
|
1195
|
+
await deleteConfigFile(this.serverUrlHash, "oauth_state.txt");
|
|
1196
|
+
return tokenResponse;
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
logger.error("Token exchange failed", "OAUTH", error);
|
|
1199
|
+
throw error;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Clear stored credentials and state
|
|
1204
|
+
*/
|
|
1205
|
+
async invalidateCredentials(scope) {
|
|
1206
|
+
logger.oauth(`Invalidating credentials: ${scope}`);
|
|
1207
|
+
switch (scope) {
|
|
1208
|
+
case "all":
|
|
1209
|
+
await Promise.all([
|
|
1210
|
+
deleteConfigFile(this.serverUrlHash, "client_info.json"),
|
|
1211
|
+
deleteConfigFile(this.serverUrlHash, "tokens.json"),
|
|
1212
|
+
deleteConfigFile(this.serverUrlHash, "pkce_verifier.txt"),
|
|
1213
|
+
deleteConfigFile(this.serverUrlHash, "oauth_state.txt")
|
|
1214
|
+
]);
|
|
1215
|
+
break;
|
|
1216
|
+
case "client":
|
|
1217
|
+
await deleteConfigFile(this.serverUrlHash, "client_info.json");
|
|
1218
|
+
break;
|
|
1219
|
+
case "tokens":
|
|
1220
|
+
await deleteConfigFile(this.serverUrlHash, "tokens.json");
|
|
1221
|
+
break;
|
|
1222
|
+
case "state":
|
|
1223
|
+
await Promise.all([
|
|
1224
|
+
deleteConfigFile(this.serverUrlHash, "pkce_verifier.txt"),
|
|
1225
|
+
deleteConfigFile(this.serverUrlHash, "oauth_state.txt")
|
|
1226
|
+
]);
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
logger.oauth(`Credentials invalidated: ${scope}`);
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* Check if currently authorized
|
|
1233
|
+
*/
|
|
1234
|
+
async isAuthorized() {
|
|
1235
|
+
const tokens = await this.tokens();
|
|
1236
|
+
return tokens !== null;
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Get server URL hash
|
|
1240
|
+
*/
|
|
1241
|
+
getServerUrlHash() {
|
|
1242
|
+
return this.serverUrlHash;
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Get OAuth configuration
|
|
1246
|
+
*/
|
|
1247
|
+
getConfig() {
|
|
1248
|
+
return { ...this.config };
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
// src/lib/coordination.ts
|
|
1253
|
+
import * as fs3 from "node:fs";
|
|
1254
|
+
import * as path4 from "node:path";
|
|
1255
|
+
import * as os2 from "node:os";
|
|
1256
|
+
|
|
1257
|
+
// src/lib/persistent-oauth-client-provider.ts
|
|
1258
|
+
import open2 from "open";
|
|
1259
|
+
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
1260
|
+
var WP_PERSISTENT_CONFIG = {
|
|
1261
|
+
authorizeEndpoint: "/wp-admin/admin.php?page=mcp-oauth-authorize",
|
|
1262
|
+
scopes: ["read", "write"],
|
|
1263
|
+
callbackPort: CONFIG.OAUTH_CALLBACK_PORT,
|
|
1264
|
+
host: CONFIG.OAUTH_HOST
|
|
1265
|
+
};
|
|
1266
|
+
var PersistentWPOAuthClientProvider = class {
|
|
1267
|
+
constructor(options) {
|
|
1268
|
+
this.authPromise = null;
|
|
1269
|
+
this.options = {
|
|
1270
|
+
...WP_PERSISTENT_CONFIG,
|
|
1271
|
+
...options
|
|
1272
|
+
};
|
|
1273
|
+
this.serverUrlHash = generateServerUrlHash(this.options.serverUrl);
|
|
1274
|
+
this.events = new EventEmitter2();
|
|
1275
|
+
this.events.setMaxListeners(10);
|
|
1276
|
+
logger.oauth("Initialized Persistent WordPress OAuth provider");
|
|
1277
|
+
logger.debug("OAuth provider options", "OAUTH", {
|
|
1278
|
+
serverUrl: this.options.serverUrl,
|
|
1279
|
+
serverHash: this.serverUrlHash,
|
|
1280
|
+
clientId: this.options.clientId || "auto-generated",
|
|
1281
|
+
callbackPort: this.options.callbackPort
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Get current tokens if available and valid from persistent storage
|
|
1286
|
+
*/
|
|
1287
|
+
async tokens() {
|
|
1288
|
+
try {
|
|
1289
|
+
const tokens = await readTokens(this.serverUrlHash);
|
|
1290
|
+
if (tokens) {
|
|
1291
|
+
const validation = isTokenValid(tokens);
|
|
1292
|
+
if (validation.isValid) {
|
|
1293
|
+
logger.oauth("Found valid tokens in persistent storage");
|
|
1294
|
+
if (validation.expiresIn) {
|
|
1295
|
+
logger.debug(`Tokens expire in ${validation.expiresIn} seconds`, "OAUTH");
|
|
1296
|
+
}
|
|
1297
|
+
return tokens;
|
|
1298
|
+
} else {
|
|
1299
|
+
logger.warn(`Tokens in persistent storage are invalid: ${validation.error}`, "OAUTH");
|
|
1300
|
+
return null;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
logger.debug("No tokens found in persistent storage", "OAUTH");
|
|
1304
|
+
return null;
|
|
1305
|
+
} catch (error) {
|
|
1306
|
+
logger.error("Error retrieving tokens from persistent storage", "OAUTH", error);
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Save tokens to persistent storage
|
|
1312
|
+
*/
|
|
1313
|
+
async saveTokens(tokens) {
|
|
1314
|
+
try {
|
|
1315
|
+
await writeTokens(this.serverUrlHash, tokens);
|
|
1316
|
+
logger.oauth("Tokens saved to persistent storage");
|
|
1317
|
+
} catch (error) {
|
|
1318
|
+
logger.error("Error saving tokens to persistent storage", "OAUTH", error);
|
|
1319
|
+
throw error;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Get client information from persistent storage
|
|
1324
|
+
*/
|
|
1325
|
+
async clientInformation() {
|
|
1326
|
+
try {
|
|
1327
|
+
const clientInfo = await readClientInfo(this.serverUrlHash);
|
|
1328
|
+
if (clientInfo) {
|
|
1329
|
+
logger.debug("Found client information in persistent storage", "OAUTH");
|
|
1330
|
+
return clientInfo;
|
|
1331
|
+
}
|
|
1332
|
+
logger.debug("No client information found in persistent storage", "OAUTH");
|
|
1333
|
+
return null;
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
logger.error("Error retrieving client information", "OAUTH", error);
|
|
1336
|
+
return null;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Save client information to persistent storage
|
|
1341
|
+
*/
|
|
1342
|
+
async saveClientInformation(clientInfo) {
|
|
1343
|
+
try {
|
|
1344
|
+
await writeClientInfo(this.serverUrlHash, clientInfo);
|
|
1345
|
+
logger.oauth("Client information saved to persistent storage");
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
logger.error("Error saving client information", "OAUTH", error);
|
|
1348
|
+
throw error;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* Save PKCE code verifier
|
|
1353
|
+
*/
|
|
1354
|
+
async saveCodeVerifier(codeVerifier) {
|
|
1355
|
+
try {
|
|
1356
|
+
await writeTextFile(this.serverUrlHash, "code_verifier.txt", codeVerifier);
|
|
1357
|
+
logger.debug("Code verifier saved", "OAUTH");
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
logger.error("Error saving code verifier", "OAUTH", error);
|
|
1360
|
+
throw error;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Get PKCE code verifier
|
|
1365
|
+
*/
|
|
1366
|
+
async codeVerifier() {
|
|
1367
|
+
try {
|
|
1368
|
+
const verifier = await readTextFile(
|
|
1369
|
+
this.serverUrlHash,
|
|
1370
|
+
"code_verifier.txt",
|
|
1371
|
+
"No code verifier saved for session"
|
|
1372
|
+
);
|
|
1373
|
+
logger.debug("Code verifier retrieved", "OAUTH");
|
|
1374
|
+
return verifier;
|
|
1375
|
+
} catch (error) {
|
|
1376
|
+
logger.error("Error retrieving code verifier", "OAUTH", error);
|
|
1377
|
+
throw error;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Initiate OAuth authorization flow
|
|
1382
|
+
*/
|
|
1383
|
+
async authorize() {
|
|
1384
|
+
if (this.authPromise) {
|
|
1385
|
+
logger.oauth("Authorization already in progress, waiting...");
|
|
1386
|
+
await this.authPromise;
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
const existingTokens = await this.tokens();
|
|
1390
|
+
if (existingTokens) {
|
|
1391
|
+
logger.oauth("Already have valid tokens in persistent storage, skipping authorization");
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
logger.oauth("Starting OAuth authorization flow for WordPress (persistent storage)");
|
|
1395
|
+
this.authPromise = this.performAuthorization();
|
|
1396
|
+
try {
|
|
1397
|
+
await this.authPromise;
|
|
1398
|
+
logger.oauth("OAuth authorization completed successfully");
|
|
1399
|
+
} catch (error) {
|
|
1400
|
+
logger.error("OAuth authorization failed", "OAUTH", error);
|
|
1401
|
+
throw error;
|
|
1402
|
+
} finally {
|
|
1403
|
+
this.authPromise = null;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Perform the actual OAuth authorization
|
|
1408
|
+
*/
|
|
1409
|
+
async performAuthorization() {
|
|
1410
|
+
const callbackServerOptions = {
|
|
1411
|
+
port: this.options.callbackPort,
|
|
1412
|
+
host: this.options.host,
|
|
1413
|
+
serverUrlHash: this.serverUrlHash,
|
|
1414
|
+
timeout: CONFIG.OAUTH_TIMEOUT
|
|
1415
|
+
};
|
|
1416
|
+
logger.oauth(`Setting up callback server on ${this.options.host}:${this.options.callbackPort}`);
|
|
1417
|
+
const callbackServer = setupWPOAuthCallbackServer(callbackServerOptions, this.events);
|
|
1418
|
+
try {
|
|
1419
|
+
logger.oauth("Starting callback server...");
|
|
1420
|
+
await callbackServer.start();
|
|
1421
|
+
logger.oauth("Callback server started successfully");
|
|
1422
|
+
const state = this.generateState();
|
|
1423
|
+
logger.debug(`Generated state parameter: ${state}`, "OAUTH");
|
|
1424
|
+
const authUrl = this.buildAuthorizationUrl(callbackServer.getCallbackUrl(), state);
|
|
1425
|
+
logger.oauth(`Built authorization URL: ${authUrl}`);
|
|
1426
|
+
logger.debug(`Callback URL: ${callbackServer.getCallbackUrl()}`, "OAUTH");
|
|
1427
|
+
logger.oauth("Attempting to open browser...");
|
|
1428
|
+
try {
|
|
1429
|
+
await open2(authUrl);
|
|
1430
|
+
logger.oauth("Browser opened successfully");
|
|
1431
|
+
} catch (browserError) {
|
|
1432
|
+
logger.error("Failed to open browser automatically", "OAUTH", browserError);
|
|
1433
|
+
logger.info("\n=== MANUAL ACTION REQUIRED ===");
|
|
1434
|
+
logger.info("Please manually open the following URL in your browser:");
|
|
1435
|
+
logger.info(`${authUrl}`);
|
|
1436
|
+
logger.info("===============================\n");
|
|
1437
|
+
}
|
|
1438
|
+
logger.oauth("Waiting for authorization result...");
|
|
1439
|
+
const tokens = await this.waitForAuthorizationResult();
|
|
1440
|
+
logger.oauth("Authorization result received");
|
|
1441
|
+
logger.oauth("Saving tokens to persistent storage...");
|
|
1442
|
+
await this.saveTokens(tokens);
|
|
1443
|
+
logger.oauth("Tokens saved successfully");
|
|
1444
|
+
return tokens;
|
|
1445
|
+
} catch (error) {
|
|
1446
|
+
logger.error("Error during authorization flow", "OAUTH", error);
|
|
1447
|
+
if (error instanceof Error && error.stack) {
|
|
1448
|
+
logger.debug("Stack trace", "OAUTH", error.stack);
|
|
1449
|
+
}
|
|
1450
|
+
throw error;
|
|
1451
|
+
} finally {
|
|
1452
|
+
logger.oauth("Stopping callback server...");
|
|
1453
|
+
try {
|
|
1454
|
+
await callbackServer.stop();
|
|
1455
|
+
logger.oauth("Callback server stopped");
|
|
1456
|
+
} catch (stopError) {
|
|
1457
|
+
logger.error("Error stopping callback server", "OAUTH", stopError);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* Build the WordPress authorization URL
|
|
1463
|
+
*/
|
|
1464
|
+
buildAuthorizationUrl(callbackUrl, state) {
|
|
1465
|
+
const baseUrl = this.options.serverUrl.replace(/\/+$/, "");
|
|
1466
|
+
const authEndpoint = this.options.authorizeEndpoint || WP_PERSISTENT_CONFIG.authorizeEndpoint;
|
|
1467
|
+
const params = new URLSearchParams({
|
|
1468
|
+
response_type: "token",
|
|
1469
|
+
// Implicit flow
|
|
1470
|
+
redirect_uri: callbackUrl,
|
|
1471
|
+
scope: this.options.scopes?.join(" ") || WP_PERSISTENT_CONFIG.scopes.join(" "),
|
|
1472
|
+
state
|
|
1473
|
+
});
|
|
1474
|
+
if (this.options.clientId) {
|
|
1475
|
+
params.set("client_id", this.options.clientId);
|
|
1476
|
+
}
|
|
1477
|
+
return `${baseUrl}${authEndpoint}?${params.toString()}`;
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Generate a secure state parameter
|
|
1481
|
+
*/
|
|
1482
|
+
generateState() {
|
|
1483
|
+
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Wait for authorization result from callback server
|
|
1487
|
+
*/
|
|
1488
|
+
async waitForAuthorizationResult() {
|
|
1489
|
+
return new Promise((resolve, reject) => {
|
|
1490
|
+
const timeout = setTimeout(() => {
|
|
1491
|
+
cleanup();
|
|
1492
|
+
reject(new OAuthError("Authorization timeout", "TIMEOUT"));
|
|
1493
|
+
}, CONFIG.LOCK_TIMEOUT);
|
|
1494
|
+
const cleanup = () => {
|
|
1495
|
+
this.events.removeAllListeners("oauth-success");
|
|
1496
|
+
this.events.removeAllListeners("oauth-error");
|
|
1497
|
+
clearTimeout(timeout);
|
|
1498
|
+
};
|
|
1499
|
+
this.events.once("oauth-success", (tokens) => {
|
|
1500
|
+
cleanup();
|
|
1501
|
+
logger.oauth("OAuth authorization successful for WordPress (persistent storage)");
|
|
1502
|
+
resolve(tokens);
|
|
1503
|
+
});
|
|
1504
|
+
this.events.once("oauth-error", (error) => {
|
|
1505
|
+
cleanup();
|
|
1506
|
+
logger.error("OAuth authorization error", "OAUTH", error.message);
|
|
1507
|
+
reject(error);
|
|
1508
|
+
});
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Clear stored tokens and credentials
|
|
1513
|
+
*/
|
|
1514
|
+
async invalidateCredentials(scope) {
|
|
1515
|
+
logger.oauth(`Invalidating credentials: ${scope}`);
|
|
1516
|
+
switch (scope) {
|
|
1517
|
+
case "all":
|
|
1518
|
+
await Promise.all([
|
|
1519
|
+
deleteConfigFile(this.serverUrlHash, "client_info.json"),
|
|
1520
|
+
deleteConfigFile(this.serverUrlHash, "tokens.json"),
|
|
1521
|
+
deleteConfigFile(this.serverUrlHash, "code_verifier.txt")
|
|
1522
|
+
]);
|
|
1523
|
+
logger.oauth("All credentials invalidated");
|
|
1524
|
+
break;
|
|
1525
|
+
case "client":
|
|
1526
|
+
await deleteConfigFile(this.serverUrlHash, "client_info.json");
|
|
1527
|
+
logger.oauth("Client information invalidated");
|
|
1528
|
+
break;
|
|
1529
|
+
case "tokens":
|
|
1530
|
+
await deleteConfigFile(this.serverUrlHash, "tokens.json");
|
|
1531
|
+
logger.oauth("OAuth tokens invalidated");
|
|
1532
|
+
break;
|
|
1533
|
+
case "verifier":
|
|
1534
|
+
await deleteConfigFile(this.serverUrlHash, "code_verifier.txt");
|
|
1535
|
+
logger.oauth("Code verifier invalidated");
|
|
1536
|
+
break;
|
|
1537
|
+
default:
|
|
1538
|
+
throw new Error(`Unknown credential scope: ${scope}`);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* Get authorization status
|
|
1543
|
+
*/
|
|
1544
|
+
async isAuthorized() {
|
|
1545
|
+
const tokens = await this.tokens();
|
|
1546
|
+
return tokens !== null;
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Get the server URL hash
|
|
1550
|
+
*/
|
|
1551
|
+
getServerUrlHash() {
|
|
1552
|
+
return this.serverUrlHash;
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Get OAuth options
|
|
1556
|
+
*/
|
|
1557
|
+
getOptions() {
|
|
1558
|
+
return { ...this.options };
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Redirect to authorization URL (for browser opening)
|
|
1562
|
+
*/
|
|
1563
|
+
async redirectToAuthorization(authorizationUrl) {
|
|
1564
|
+
logger.oauth(`
|
|
1565
|
+
Please authorize this client by visiting:
|
|
1566
|
+
${authorizationUrl.toString()}
|
|
1567
|
+
`);
|
|
1568
|
+
try {
|
|
1569
|
+
await open2(authorizationUrl.toString());
|
|
1570
|
+
logger.oauth("Browser opened automatically.");
|
|
1571
|
+
} catch (error) {
|
|
1572
|
+
logger.warn(
|
|
1573
|
+
"Could not open browser automatically. Please copy and paste the URL above into your browser.",
|
|
1574
|
+
"OAUTH"
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
|
|
1580
|
+
// src/lib/coordination.ts
|
|
1581
|
+
var LockfileManager = class {
|
|
1582
|
+
constructor(serverUrlHash) {
|
|
1583
|
+
this.isOwner = false;
|
|
1584
|
+
this.checkInterval = null;
|
|
1585
|
+
const authDir = getConfigDir();
|
|
1586
|
+
this.lockfilePath = path4.join(authDir, `${serverUrlHash}_auth.lock`);
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* Try to acquire the lock
|
|
1590
|
+
*/
|
|
1591
|
+
tryAcquire() {
|
|
1592
|
+
try {
|
|
1593
|
+
if (fs3.existsSync(this.lockfilePath)) {
|
|
1594
|
+
const lockData2 = this.readLockfile();
|
|
1595
|
+
if (lockData2 && this.isLockValid(lockData2)) {
|
|
1596
|
+
logger.debug(`Lock is held by PID ${lockData2.pid}`, "COORDINATION");
|
|
1597
|
+
return false;
|
|
1598
|
+
}
|
|
1599
|
+
this.release();
|
|
1600
|
+
}
|
|
1601
|
+
const lockData = {
|
|
1602
|
+
pid: process.pid,
|
|
1603
|
+
port: CONFIG.OAUTH_CALLBACK_PORT,
|
|
1604
|
+
timestamp: Date.now(),
|
|
1605
|
+
hostname: os2.hostname()
|
|
1606
|
+
};
|
|
1607
|
+
fs3.writeFileSync(this.lockfilePath, JSON.stringify(lockData), { mode: 384 });
|
|
1608
|
+
this.isOwner = true;
|
|
1609
|
+
this.startMonitoring();
|
|
1610
|
+
logger.debug(`Acquired auth lock: ${this.lockfilePath}`, "COORDINATION");
|
|
1611
|
+
return true;
|
|
1612
|
+
} catch (error) {
|
|
1613
|
+
logger.error("Error acquiring lock", "COORDINATION", error);
|
|
1614
|
+
return false;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
/**
|
|
1618
|
+
* Release the lock
|
|
1619
|
+
*/
|
|
1620
|
+
release() {
|
|
1621
|
+
try {
|
|
1622
|
+
if (this.checkInterval) {
|
|
1623
|
+
clearInterval(this.checkInterval);
|
|
1624
|
+
this.checkInterval = null;
|
|
1625
|
+
}
|
|
1626
|
+
if (this.isOwner && fs3.existsSync(this.lockfilePath)) {
|
|
1627
|
+
fs3.unlinkSync(this.lockfilePath);
|
|
1628
|
+
logger.debug(`Released auth lock: ${this.lockfilePath}`, "COORDINATION");
|
|
1629
|
+
}
|
|
1630
|
+
this.isOwner = false;
|
|
1631
|
+
} catch (error) {
|
|
1632
|
+
logger.error("Error releasing lock", "COORDINATION", error);
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Check if we own the lock
|
|
1637
|
+
*/
|
|
1638
|
+
isLockOwner() {
|
|
1639
|
+
return this.isOwner;
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Wait for lock to be released
|
|
1643
|
+
*/
|
|
1644
|
+
async waitForRelease(timeout = CONFIG.LOCK_TIMEOUT) {
|
|
1645
|
+
const startTime = Date.now();
|
|
1646
|
+
return new Promise((resolve, reject) => {
|
|
1647
|
+
const checkLock = () => {
|
|
1648
|
+
if (!fs3.existsSync(this.lockfilePath)) {
|
|
1649
|
+
logger.debug("Lock file no longer exists", "COORDINATION");
|
|
1650
|
+
resolve();
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
const lockData = this.readLockfile();
|
|
1654
|
+
if (!lockData || !this.isLockValid(lockData)) {
|
|
1655
|
+
logger.debug("Lock is stale, cleaning up", "COORDINATION");
|
|
1656
|
+
this.release();
|
|
1657
|
+
resolve();
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
if (Date.now() - startTime > timeout) {
|
|
1661
|
+
reject(new OAuthError("Timeout waiting for auth lock", "LOCK_TIMEOUT"));
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
setTimeout(checkLock, 1e3);
|
|
1665
|
+
};
|
|
1666
|
+
logger.info("Waiting for other instance to complete authentication...", "COORDINATION");
|
|
1667
|
+
checkLock();
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Read lockfile data
|
|
1672
|
+
*/
|
|
1673
|
+
readLockfile() {
|
|
1674
|
+
try {
|
|
1675
|
+
const data = fs3.readFileSync(this.lockfilePath, "utf8");
|
|
1676
|
+
return JSON.parse(data);
|
|
1677
|
+
} catch {
|
|
1678
|
+
return null;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Check if lock is still valid (process is running)
|
|
1683
|
+
*/
|
|
1684
|
+
isLockValid(lockData) {
|
|
1685
|
+
try {
|
|
1686
|
+
process.kill(lockData.pid, 0);
|
|
1687
|
+
const age = Date.now() - lockData.timestamp;
|
|
1688
|
+
const maxAge = 6e5;
|
|
1689
|
+
if (age > maxAge) {
|
|
1690
|
+
logger.debug(
|
|
1691
|
+
`Lock is too old (${Math.round(age / 1e3)}s), considering invalid`,
|
|
1692
|
+
"COORDINATION"
|
|
1693
|
+
);
|
|
1694
|
+
return false;
|
|
1695
|
+
}
|
|
1696
|
+
return true;
|
|
1697
|
+
} catch {
|
|
1698
|
+
logger.debug(`Process ${lockData.pid} is not running`, "COORDINATION");
|
|
1699
|
+
return false;
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
/**
|
|
1703
|
+
* Monitor lock validity
|
|
1704
|
+
*/
|
|
1705
|
+
startMonitoring() {
|
|
1706
|
+
this.checkInterval = setInterval(() => {
|
|
1707
|
+
if (this.isOwner && !fs3.existsSync(this.lockfilePath)) {
|
|
1708
|
+
logger.warn("Lock file was removed externally", "COORDINATION");
|
|
1709
|
+
this.isOwner = false;
|
|
1710
|
+
if (this.checkInterval) {
|
|
1711
|
+
clearInterval(this.checkInterval);
|
|
1712
|
+
this.checkInterval = null;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
}, 5e3);
|
|
1716
|
+
}
|
|
1717
|
+
};
|
|
1718
|
+
var WPAuthCoordinator = class {
|
|
1719
|
+
constructor(serverUrlHash, serverUrl, callbackPort, events) {
|
|
1720
|
+
this.oauthProvider = null;
|
|
1721
|
+
this.isStarted = false;
|
|
1722
|
+
this.serverUrlHash = serverUrlHash;
|
|
1723
|
+
this.serverUrl = serverUrl;
|
|
1724
|
+
this.callbackPort = callbackPort;
|
|
1725
|
+
this.events = events;
|
|
1726
|
+
this.lockManager = new LockfileManager(serverUrlHash);
|
|
1727
|
+
}
|
|
1728
|
+
async start() {
|
|
1729
|
+
if (this.isStarted) {
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
logger.debug("Starting WordPress auth coordinator", "COORDINATION");
|
|
1733
|
+
this.isStarted = true;
|
|
1734
|
+
process.on("exit", () => this.cleanup());
|
|
1735
|
+
process.on("SIGINT", () => this.cleanup());
|
|
1736
|
+
process.on("SIGTERM", () => this.cleanup());
|
|
1737
|
+
process.on("uncaughtException", () => this.cleanup());
|
|
1738
|
+
process.on("unhandledRejection", () => this.cleanup());
|
|
1739
|
+
}
|
|
1740
|
+
async stop() {
|
|
1741
|
+
logger.debug("Stopping WordPress auth coordinator", "COORDINATION");
|
|
1742
|
+
this.cleanup();
|
|
1743
|
+
this.isStarted = false;
|
|
1744
|
+
}
|
|
1745
|
+
async waitForAuth() {
|
|
1746
|
+
if (!this.isStarted) {
|
|
1747
|
+
throw new OAuthError("Auth coordinator not started");
|
|
1748
|
+
}
|
|
1749
|
+
const existingTokens = await getValidTokens(this.serverUrlHash);
|
|
1750
|
+
if (existingTokens) {
|
|
1751
|
+
logger.debug("Found existing valid tokens", "COORDINATION");
|
|
1752
|
+
return existingTokens;
|
|
1753
|
+
}
|
|
1754
|
+
if (this.lockManager.tryAcquire()) {
|
|
1755
|
+
return await this.performAuthentication();
|
|
1756
|
+
} else {
|
|
1757
|
+
return await this.waitForOtherInstanceAuth();
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
async performAuthentication() {
|
|
1761
|
+
try {
|
|
1762
|
+
logger.info("Performing OAuth authentication as lock owner", "COORDINATION");
|
|
1763
|
+
if (!this.oauthProvider) {
|
|
1764
|
+
this.oauthProvider = new PersistentWPOAuthClientProvider({
|
|
1765
|
+
serverUrl: this.serverUrl,
|
|
1766
|
+
callbackPort: this.callbackPort,
|
|
1767
|
+
host: CONFIG.OAUTH_HOST,
|
|
1768
|
+
clientId: CONFIG.WP_OAUTH_CLIENT_ID
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
await this.oauthProvider.authorize();
|
|
1772
|
+
const tokens = await this.oauthProvider.tokens();
|
|
1773
|
+
if (!tokens) {
|
|
1774
|
+
throw new OAuthError("Authentication completed but no tokens available");
|
|
1775
|
+
}
|
|
1776
|
+
logger.info("Authentication successful, tokens obtained", "COORDINATION");
|
|
1777
|
+
return tokens;
|
|
1778
|
+
} finally {
|
|
1779
|
+
this.lockManager.release();
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
async waitForOtherInstanceAuth() {
|
|
1783
|
+
logger.info("Waiting for another instance to complete authentication", "COORDINATION");
|
|
1784
|
+
try {
|
|
1785
|
+
await this.lockManager.waitForRelease();
|
|
1786
|
+
const tokens = await getValidTokens(this.serverUrlHash);
|
|
1787
|
+
if (tokens) {
|
|
1788
|
+
logger.info("Tokens are now available from other instance", "COORDINATION");
|
|
1789
|
+
return tokens;
|
|
1790
|
+
}
|
|
1791
|
+
logger.debug(
|
|
1792
|
+
"No tokens found after waiting, trying to authenticate ourselves",
|
|
1793
|
+
"COORDINATION"
|
|
1794
|
+
);
|
|
1795
|
+
return await this.waitForAuth();
|
|
1796
|
+
} catch (error) {
|
|
1797
|
+
logger.error("Error waiting for other instance auth", "COORDINATION", error);
|
|
1798
|
+
throw error;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
cleanup() {
|
|
1802
|
+
this.lockManager.release();
|
|
1803
|
+
}
|
|
1804
|
+
};
|
|
1805
|
+
var LazyWPAuthCoordinator = class {
|
|
1806
|
+
constructor(serverUrlHash, serverUrl, callbackPort, events) {
|
|
1807
|
+
this.coordinator = null;
|
|
1808
|
+
this.serverUrlHash = serverUrlHash;
|
|
1809
|
+
this.serverUrl = serverUrl;
|
|
1810
|
+
this.callbackPort = callbackPort;
|
|
1811
|
+
this.events = events;
|
|
1812
|
+
}
|
|
1813
|
+
async start() {
|
|
1814
|
+
}
|
|
1815
|
+
async stop() {
|
|
1816
|
+
if (this.coordinator) {
|
|
1817
|
+
await this.coordinator.stop();
|
|
1818
|
+
this.coordinator = null;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
async waitForAuth() {
|
|
1822
|
+
const existingTokens = await getValidTokens(this.serverUrlHash);
|
|
1823
|
+
if (existingTokens) {
|
|
1824
|
+
return existingTokens;
|
|
1825
|
+
}
|
|
1826
|
+
if (!this.coordinator) {
|
|
1827
|
+
this.coordinator = new WPAuthCoordinator(
|
|
1828
|
+
this.serverUrlHash,
|
|
1829
|
+
this.serverUrl,
|
|
1830
|
+
this.callbackPort,
|
|
1831
|
+
this.events
|
|
1832
|
+
);
|
|
1833
|
+
await this.coordinator.start();
|
|
1834
|
+
}
|
|
1835
|
+
return await this.coordinator.waitForAuth();
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
function createLazyWPAuthCoordinator(serverUrlHash, serverUrl, callbackPort, events) {
|
|
1839
|
+
return new LazyWPAuthCoordinator(serverUrlHash, serverUrl, callbackPort, events);
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
// src/lib/wordpress-api.ts
|
|
1843
|
+
var mcpOAuthProvider = null;
|
|
1844
|
+
var authCoordinator = null;
|
|
1845
|
+
var globalEvents = null;
|
|
1846
|
+
function validateEnvironment() {
|
|
1847
|
+
const validation = validateConfig();
|
|
1848
|
+
if (!validation.isValid) {
|
|
1849
|
+
throw new AuthError(
|
|
1850
|
+
`Configuration validation failed: ${validation.errors.join(", ")}`,
|
|
1851
|
+
"CONFIG_VALIDATION"
|
|
1852
|
+
);
|
|
51
1853
|
}
|
|
52
1854
|
}
|
|
53
1855
|
function removeTrailingSlash(url) {
|
|
54
1856
|
return url.replace(/\/+$/, "");
|
|
55
1857
|
}
|
|
1858
|
+
function constructApiUrl(baseUrl, defaultEndpoint) {
|
|
1859
|
+
const cleanUrl = removeTrailingSlash(baseUrl);
|
|
1860
|
+
try {
|
|
1861
|
+
const urlObj = new URL(cleanUrl);
|
|
1862
|
+
const hasCustomPath = urlObj.pathname && urlObj.pathname !== "/" && urlObj.pathname.length > 0;
|
|
1863
|
+
if (hasCustomPath) {
|
|
1864
|
+
return cleanUrl;
|
|
1865
|
+
} else {
|
|
1866
|
+
return new URL(`/?rest_route=${defaultEndpoint}`, cleanUrl).toString();
|
|
1867
|
+
}
|
|
1868
|
+
} catch (error) {
|
|
1869
|
+
return new URL(`/?rest_route=${defaultEndpoint}`, cleanUrl).toString();
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
async function getOAuthTokens() {
|
|
1873
|
+
try {
|
|
1874
|
+
if (!CONFIG.OAUTH_ENABLED) {
|
|
1875
|
+
logger.debug("OAuth is disabled via configuration", "AUTH");
|
|
1876
|
+
return null;
|
|
1877
|
+
}
|
|
1878
|
+
logger.auth("Attempting to get OAuth tokens for WordPress API (MCP-compliant)...");
|
|
1879
|
+
const serverUrl = CONFIG.WP_API_URL;
|
|
1880
|
+
const serverUrlHash = generateServerUrlHash(serverUrl);
|
|
1881
|
+
const existingTokens = await getValidTokens(serverUrlHash);
|
|
1882
|
+
if (existingTokens) {
|
|
1883
|
+
logger.auth("Using existing valid tokens from persistent storage");
|
|
1884
|
+
return existingTokens;
|
|
1885
|
+
}
|
|
1886
|
+
logger.auth("No existing valid tokens found in persistent storage");
|
|
1887
|
+
const isWPCom = isWordPressComSite(serverUrl);
|
|
1888
|
+
const recommendedConfig = getRecommendedOAuthConfig(serverUrl);
|
|
1889
|
+
if (isWPCom) {
|
|
1890
|
+
logger.auth(`Detected WordPress.com site - using ${recommendedConfig.description}`);
|
|
1891
|
+
logger.auth("This will use WordPress.com OAuth2 endpoints for compatibility");
|
|
1892
|
+
} else {
|
|
1893
|
+
logger.auth(`Detected self-hosted WordPress site - using ${recommendedConfig.description}`);
|
|
1894
|
+
logger.auth("This will trigger the MCP-compliant OAuth 2.1 authentication flow");
|
|
1895
|
+
}
|
|
1896
|
+
logger.auth("Your browser should open automatically for authentication");
|
|
1897
|
+
const shouldUseMCPFlow = !isWPCom && CONFIG.OAUTH_FLOW_TYPE === "authorization_code" && CONFIG.OAUTH_USE_PKCE;
|
|
1898
|
+
if (shouldUseMCPFlow) {
|
|
1899
|
+
if (!mcpOAuthProvider) {
|
|
1900
|
+
mcpOAuthProvider = new MCPOAuthProvider({
|
|
1901
|
+
serverUrl,
|
|
1902
|
+
clientId: CONFIG.WP_OAUTH_CLIENT_ID,
|
|
1903
|
+
scopes: ["read", "write"]
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
logger.auth("Using MCP-compliant OAuth 2.1 authorization code flow with PKCE");
|
|
1907
|
+
await mcpOAuthProvider.authorize();
|
|
1908
|
+
const tokens = await mcpOAuthProvider.tokens();
|
|
1909
|
+
if (tokens) {
|
|
1910
|
+
logger.auth("MCP OAuth 2.1 tokens obtained for WordPress API access");
|
|
1911
|
+
return tokens;
|
|
1912
|
+
} else {
|
|
1913
|
+
logger.warn("No tokens available after MCP OAuth 2.1 authentication", "AUTH");
|
|
1914
|
+
return null;
|
|
1915
|
+
}
|
|
1916
|
+
} else {
|
|
1917
|
+
if (isWPCom) {
|
|
1918
|
+
logger.auth("Using WordPress.com OAuth2 provider for full compatibility");
|
|
1919
|
+
} else {
|
|
1920
|
+
logger.warn(
|
|
1921
|
+
"Using legacy OAuth provider. Consider upgrading to OAuth 2.1 for MCP compliance",
|
|
1922
|
+
"AUTH"
|
|
1923
|
+
);
|
|
1924
|
+
}
|
|
1925
|
+
if (!authCoordinator) {
|
|
1926
|
+
if (!globalEvents) {
|
|
1927
|
+
globalEvents = new EventEmitter3();
|
|
1928
|
+
}
|
|
1929
|
+
authCoordinator = createLazyWPAuthCoordinator(
|
|
1930
|
+
serverUrlHash,
|
|
1931
|
+
serverUrl,
|
|
1932
|
+
CONFIG.OAUTH_CALLBACK_PORT,
|
|
1933
|
+
globalEvents
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
logger.auth("Starting legacy authentication via coordinator...");
|
|
1937
|
+
try {
|
|
1938
|
+
const tokens = await authCoordinator.waitForAuth();
|
|
1939
|
+
if (tokens) {
|
|
1940
|
+
logger.auth("Legacy tokens obtained for WordPress API access");
|
|
1941
|
+
return tokens;
|
|
1942
|
+
} else {
|
|
1943
|
+
logger.warn("No tokens available after legacy authentication", "AUTH");
|
|
1944
|
+
return null;
|
|
1945
|
+
}
|
|
1946
|
+
} catch (authError) {
|
|
1947
|
+
logger.error("Legacy authentication via coordinator failed", "AUTH", authError);
|
|
1948
|
+
throw authError;
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
} catch (error) {
|
|
1952
|
+
logger.error("Error getting OAuth tokens", "AUTH", error);
|
|
1953
|
+
return null;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
56
1956
|
async function wpRequest(params = { method: "init" }) {
|
|
57
1957
|
validateEnvironment();
|
|
58
1958
|
const endpoint = "/wp/v2/wpmcp";
|
|
59
1959
|
const method = "POST";
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
1960
|
+
logger.api(`Request method: ${params.method || "init"}`);
|
|
1961
|
+
logger.debug(`Request args: ${JSON.stringify(params.args || {})}`, "API");
|
|
1962
|
+
let authHeader = "";
|
|
1963
|
+
if (CONFIG.JWT_TOKEN) {
|
|
1964
|
+
authHeader = `Bearer ${CONFIG.JWT_TOKEN}`;
|
|
1965
|
+
logger.auth("Using JWT token authentication");
|
|
1966
|
+
logger.debug(`Token length: ${CONFIG.JWT_TOKEN.length}`, "AUTH");
|
|
1967
|
+
} else if (CONFIG.OAUTH_ENABLED) {
|
|
1968
|
+
logger.auth("OAuth is the primary authentication method - attempting to get tokens...");
|
|
1969
|
+
const oauthTokens = await getOAuthTokens();
|
|
1970
|
+
if (oauthTokens) {
|
|
1971
|
+
authHeader = `Bearer ${oauthTokens.access_token}`;
|
|
1972
|
+
logger.auth("Using OAuth token authentication for WordPress API");
|
|
1973
|
+
logger.debug(`Token length: ${oauthTokens.access_token.length}`, "AUTH");
|
|
1974
|
+
} else {
|
|
1975
|
+
logger.warn("OAuth authentication failed, trying Basic Auth fallback", "AUTH");
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
if (!authHeader && CONFIG.WP_API_USERNAME && CONFIG.WP_API_PASSWORD) {
|
|
69
1979
|
let username;
|
|
70
1980
|
let password;
|
|
71
1981
|
if (params.method === "tools/call" && params.args && params.args.tool && params.args.tool.startsWith("wc_reports_")) {
|
|
72
|
-
username =
|
|
73
|
-
password =
|
|
74
|
-
|
|
1982
|
+
username = CONFIG.WOO_CUSTOMER_KEY;
|
|
1983
|
+
password = CONFIG.WOO_CUSTOMER_SECRET;
|
|
1984
|
+
logger.auth(`Using WooCommerce credentials for tool: ${params.args.tool}`);
|
|
75
1985
|
if (!username || !password) {
|
|
76
|
-
throw new
|
|
77
|
-
"Missing WooCommerce credentials. Please set WOO_CUSTOMER_KEY and WOO_CUSTOMER_SECRET environment variables."
|
|
1986
|
+
throw new AuthError(
|
|
1987
|
+
"Missing WooCommerce credentials. Please set WOO_CUSTOMER_KEY and WOO_CUSTOMER_SECRET environment variables.",
|
|
1988
|
+
"WOOCOMMERCE_CREDENTIALS"
|
|
78
1989
|
);
|
|
79
1990
|
}
|
|
80
1991
|
} else {
|
|
81
|
-
username =
|
|
82
|
-
password =
|
|
83
|
-
|
|
1992
|
+
username = CONFIG.WP_API_USERNAME;
|
|
1993
|
+
password = CONFIG.WP_API_PASSWORD;
|
|
1994
|
+
logger.auth(`Using WordPress Basic Auth for method: ${params.method || "init"}`);
|
|
84
1995
|
}
|
|
85
|
-
|
|
86
|
-
|
|
1996
|
+
logger.debug(`Username length: ${username ? username.length : 0}`, "AUTH");
|
|
1997
|
+
logger.debug(`Password length: ${password ? password.length : 0}`, "AUTH");
|
|
87
1998
|
const auth = Buffer.from(`${username}:${password}`).toString("base64");
|
|
88
1999
|
authHeader = `Basic ${auth}`;
|
|
89
|
-
|
|
2000
|
+
logger.debug(`Auth header length: ${auth.length}`, "AUTH");
|
|
90
2001
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
2002
|
+
if (!authHeader) {
|
|
2003
|
+
throw new AuthError(
|
|
2004
|
+
"No authentication method available. Please configure JWT_TOKEN, OAuth, or Basic Auth (WP_API_USERNAME+WP_API_PASSWORD).",
|
|
2005
|
+
"NO_AUTH_METHOD"
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
2008
|
+
logger.debug(`Environment: ${CONFIG.NODE_ENV}`, "API");
|
|
2009
|
+
logger.debug(`Base API URL: ${CONFIG.WP_API_URL}`, "API");
|
|
2010
|
+
const url = constructApiUrl(CONFIG.WP_API_URL, endpoint);
|
|
2011
|
+
logger.debug(`Final requesting URL: ${url}`, "API");
|
|
95
2012
|
const headers = {
|
|
96
2013
|
Authorization: authHeader,
|
|
97
2014
|
"Content-Type": "application/json"
|
|
@@ -102,20 +2019,29 @@ async function wpRequest(params = { method: "init" }) {
|
|
|
102
2019
|
body: JSON.stringify(params)
|
|
103
2020
|
};
|
|
104
2021
|
try {
|
|
105
|
-
|
|
2022
|
+
logger.api("Sending request to WordPress API...");
|
|
106
2023
|
const response = await fetch(url, fetchOptions);
|
|
107
|
-
|
|
2024
|
+
logger.debug(`Response status: ${response.status}`, "API");
|
|
108
2025
|
if (!response.ok) {
|
|
109
2026
|
const errorText = await response.text();
|
|
110
|
-
|
|
111
|
-
throw new
|
|
2027
|
+
logger.error(`API error response: ${errorText}`, "API");
|
|
2028
|
+
throw new APIError(
|
|
2029
|
+
`WordPress API error (${response.status}): ${errorText}`,
|
|
2030
|
+
response.status,
|
|
2031
|
+
url,
|
|
2032
|
+
errorText
|
|
2033
|
+
);
|
|
112
2034
|
}
|
|
113
2035
|
const responseData = await response.json();
|
|
114
|
-
|
|
2036
|
+
logger.api("Response received successfully");
|
|
115
2037
|
return responseData;
|
|
116
2038
|
} catch (error) {
|
|
117
|
-
|
|
118
|
-
|
|
2039
|
+
if (error instanceof APIError) {
|
|
2040
|
+
throw error;
|
|
2041
|
+
}
|
|
2042
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2043
|
+
logger.error(`Error in wpRequest: ${errorMessage}`, "API");
|
|
2044
|
+
throw new APIError(errorMessage, 0, url);
|
|
119
2045
|
}
|
|
120
2046
|
}
|
|
121
2047
|
|
|
@@ -137,18 +2063,29 @@ import {
|
|
|
137
2063
|
var requiredNodeVersion = 22;
|
|
138
2064
|
var currentNodeVersion = parseInt(process.version.slice(1).split(".")[0]);
|
|
139
2065
|
if (currentNodeVersion < requiredNodeVersion) {
|
|
140
|
-
|
|
141
|
-
|
|
2066
|
+
logger.error(
|
|
2067
|
+
`This application requires Node.js version ${requiredNodeVersion} or higher.`,
|
|
2068
|
+
"SYSTEM"
|
|
2069
|
+
);
|
|
2070
|
+
logger.error(`Current version: ${process.version}`, "SYSTEM");
|
|
142
2071
|
process.exit(1);
|
|
143
2072
|
}
|
|
144
2073
|
if (typeof globalThis.fetch !== "function") {
|
|
145
|
-
|
|
146
|
-
"
|
|
2074
|
+
logger.error(
|
|
2075
|
+
"This application requires the fetch API, which is not available in your Node.js environment.",
|
|
2076
|
+
"SYSTEM"
|
|
147
2077
|
);
|
|
148
|
-
|
|
2078
|
+
logger.error("Please ensure you are using Node.js 22 or later, or install node-fetch.", "SYSTEM");
|
|
149
2079
|
process.exit(1);
|
|
150
2080
|
}
|
|
151
2081
|
async function WordPressProxy() {
|
|
2082
|
+
logger.info("Starting WordPress MCP Proxy with enhanced authentication", "PROXY");
|
|
2083
|
+
try {
|
|
2084
|
+
await cleanupExpiredTokens();
|
|
2085
|
+
} catch (error) {
|
|
2086
|
+
logger.warn("Error cleaning up expired tokens", "PROXY", error);
|
|
2087
|
+
}
|
|
2088
|
+
logger.info("Initializing connection to WordPress API...", "PROXY");
|
|
152
2089
|
const init = await wpRequest({ method: "initialize" });
|
|
153
2090
|
const server = new Server(
|
|
154
2091
|
{
|
|
@@ -161,16 +2098,20 @@ async function WordPressProxy() {
|
|
|
161
2098
|
}
|
|
162
2099
|
);
|
|
163
2100
|
const withLogging = (schema, handler) => async (request) => {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
2101
|
+
logger.debug(`Received ${schema} request`, "MCP", request);
|
|
2102
|
+
try {
|
|
2103
|
+
const response = await handler(request);
|
|
2104
|
+
logger.debug(`${schema} response sent`, "MCP");
|
|
2105
|
+
return response;
|
|
2106
|
+
} catch (error) {
|
|
2107
|
+
logger.error(`Error handling ${schema} request`, "MCP", error);
|
|
2108
|
+
throw error;
|
|
2109
|
+
}
|
|
168
2110
|
};
|
|
169
2111
|
server.setRequestHandler(
|
|
170
2112
|
ListToolsRequestSchema,
|
|
171
2113
|
withLogging("ListTools", async (request) => {
|
|
172
|
-
|
|
173
|
-
log(JSON.stringify(request));
|
|
2114
|
+
logger.debug("Processing ListToolsRequest", "MCP");
|
|
174
2115
|
const response = await wpRequest({
|
|
175
2116
|
method: "tools/list",
|
|
176
2117
|
cursor: request.params?.cursor
|
|
@@ -181,8 +2122,7 @@ async function WordPressProxy() {
|
|
|
181
2122
|
server.setRequestHandler(
|
|
182
2123
|
CallToolRequestSchema,
|
|
183
2124
|
withLogging("CallTool", async (request) => {
|
|
184
|
-
|
|
185
|
-
log(JSON.stringify(request));
|
|
2125
|
+
logger.debug("Processing CallToolRequest", "MCP");
|
|
186
2126
|
const response = await wpRequest({
|
|
187
2127
|
method: "tools/call",
|
|
188
2128
|
name: request.params.name,
|
|
@@ -194,8 +2134,7 @@ async function WordPressProxy() {
|
|
|
194
2134
|
server.setRequestHandler(
|
|
195
2135
|
ListResourcesRequestSchema,
|
|
196
2136
|
withLogging("ListResources", async (request) => {
|
|
197
|
-
|
|
198
|
-
log(JSON.stringify(request));
|
|
2137
|
+
logger.debug("Processing ListResourcesRequest", "MCP");
|
|
199
2138
|
const response = await wpRequest({
|
|
200
2139
|
method: "resources/list",
|
|
201
2140
|
cursor: request.params?.cursor
|
|
@@ -206,8 +2145,7 @@ async function WordPressProxy() {
|
|
|
206
2145
|
server.setRequestHandler(
|
|
207
2146
|
ListResourceTemplatesRequestSchema,
|
|
208
2147
|
withLogging("ListResourceTemplates", async (request) => {
|
|
209
|
-
|
|
210
|
-
log(JSON.stringify(request));
|
|
2148
|
+
logger.debug("Processing ListResourceTemplatesRequest", "MCP");
|
|
211
2149
|
const response = await wpRequest({
|
|
212
2150
|
method: "resources/templates/list",
|
|
213
2151
|
cursor: request.params?.cursor
|
|
@@ -218,8 +2156,7 @@ async function WordPressProxy() {
|
|
|
218
2156
|
server.setRequestHandler(
|
|
219
2157
|
ReadResourceRequestSchema,
|
|
220
2158
|
withLogging("ReadResource", async (request) => {
|
|
221
|
-
|
|
222
|
-
log(JSON.stringify(request));
|
|
2159
|
+
logger.debug("Processing ReadResourceRequest", "MCP");
|
|
223
2160
|
const response = await wpRequest({
|
|
224
2161
|
method: "resources/read",
|
|
225
2162
|
uri: request.params.uri
|
|
@@ -230,8 +2167,7 @@ async function WordPressProxy() {
|
|
|
230
2167
|
server.setRequestHandler(
|
|
231
2168
|
SubscribeRequestSchema,
|
|
232
2169
|
withLogging("Subscribe", async (request) => {
|
|
233
|
-
|
|
234
|
-
log(JSON.stringify(request));
|
|
2170
|
+
logger.debug("Processing SubscribeRequest", "MCP");
|
|
235
2171
|
const response = await wpRequest({
|
|
236
2172
|
method: "resources/subscribe",
|
|
237
2173
|
uri: request.params.uri
|
|
@@ -242,8 +2178,7 @@ async function WordPressProxy() {
|
|
|
242
2178
|
server.setRequestHandler(
|
|
243
2179
|
UnsubscribeRequestSchema,
|
|
244
2180
|
withLogging("Unsubscribe", async (request) => {
|
|
245
|
-
|
|
246
|
-
log(JSON.stringify(request));
|
|
2181
|
+
logger.debug("Processing UnsubscribeRequest", "MCP");
|
|
247
2182
|
const response = await wpRequest({
|
|
248
2183
|
method: "resources/unsubscribe",
|
|
249
2184
|
uri: request.params.uri
|
|
@@ -254,8 +2189,7 @@ async function WordPressProxy() {
|
|
|
254
2189
|
server.setRequestHandler(
|
|
255
2190
|
ListPromptsRequestSchema,
|
|
256
2191
|
withLogging("ListPrompts", async (request) => {
|
|
257
|
-
|
|
258
|
-
log(JSON.stringify(request));
|
|
2192
|
+
logger.debug("Processing ListPromptsRequest", "MCP");
|
|
259
2193
|
const response = await wpRequest({
|
|
260
2194
|
method: "prompts/list",
|
|
261
2195
|
cursor: request.params?.cursor
|
|
@@ -266,8 +2200,7 @@ async function WordPressProxy() {
|
|
|
266
2200
|
server.setRequestHandler(
|
|
267
2201
|
GetPromptRequestSchema,
|
|
268
2202
|
withLogging("GetPrompt", async (request) => {
|
|
269
|
-
|
|
270
|
-
log(JSON.stringify(request));
|
|
2203
|
+
logger.debug("Processing GetPromptRequest", "MCP");
|
|
271
2204
|
const response = await wpRequest({
|
|
272
2205
|
method: "prompts/get",
|
|
273
2206
|
name: request.params.name,
|
|
@@ -279,8 +2212,7 @@ async function WordPressProxy() {
|
|
|
279
2212
|
server.setRequestHandler(
|
|
280
2213
|
SetLevelRequestSchema,
|
|
281
2214
|
withLogging("SetLevel", async (request) => {
|
|
282
|
-
|
|
283
|
-
log(JSON.stringify(request));
|
|
2215
|
+
logger.debug("Processing SetLevelRequest", "MCP");
|
|
284
2216
|
const response = await wpRequest({
|
|
285
2217
|
method: "logging/setLevel",
|
|
286
2218
|
level: request.params.level
|
|
@@ -291,8 +2223,7 @@ async function WordPressProxy() {
|
|
|
291
2223
|
server.setRequestHandler(
|
|
292
2224
|
CompleteRequestSchema,
|
|
293
2225
|
withLogging("Complete", async (request) => {
|
|
294
|
-
|
|
295
|
-
log(JSON.stringify(request));
|
|
2226
|
+
logger.debug("Processing CompleteRequest", "MCP");
|
|
296
2227
|
const response = await wpRequest({
|
|
297
2228
|
method: "completion/complete",
|
|
298
2229
|
ref: request.params.ref,
|
|
@@ -304,8 +2235,7 @@ async function WordPressProxy() {
|
|
|
304
2235
|
server.setRequestHandler(
|
|
305
2236
|
ListRootsRequestSchema,
|
|
306
2237
|
withLogging("ListRoots", async (request) => {
|
|
307
|
-
|
|
308
|
-
log(JSON.stringify(request));
|
|
2238
|
+
logger.debug("Processing ListRootsRequest", "MCP");
|
|
309
2239
|
const response = await wpRequest({
|
|
310
2240
|
method: "roots/list"
|
|
311
2241
|
});
|
|
@@ -314,9 +2244,9 @@ async function WordPressProxy() {
|
|
|
314
2244
|
);
|
|
315
2245
|
const transport = new StdioServerTransport();
|
|
316
2246
|
server.connect(transport).then(() => {
|
|
317
|
-
|
|
2247
|
+
logger.info("MCP server connected to transport successfully", "PROXY");
|
|
318
2248
|
}).catch((error) => {
|
|
319
|
-
|
|
2249
|
+
logger.error("Error starting MCP server", "PROXY", error);
|
|
320
2250
|
process.exit(1);
|
|
321
2251
|
});
|
|
322
2252
|
}
|