@automattic/mcp-wordpress-remote 0.2.21 → 0.3.0

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 CHANGED
@@ -38,7 +38,7 @@ Add to your MCP client configuration (e.g., Claude Desktop's `claude_desktop_con
38
38
  "command": "npx",
39
39
  "args": ["-y", "@automattic/mcp-wordpress-remote"],
40
40
  "env": {
41
- "WP_API_URL": "https://your-wordpress-site.com"
41
+ "WP_API_URL": "https://your-wordpress-site.com/wp-json/mcp/mcp-adapter-default-server"
42
42
  }
43
43
  }
44
44
  }
@@ -58,7 +58,7 @@ You can add custom headers to all API requests using the `CUSTOM_HEADERS` enviro
58
58
  "command": "npx",
59
59
  "args": ["-y", "@automattic/mcp-wordpress-remote"],
60
60
  "env": {
61
- "WP_API_URL": "https://your-wordpress-site.com",
61
+ "WP_API_URL": "https://your-wordpress-site.com/wp-json/mcp/mcp-adapter-default-server",
62
62
  "CUSTOM_HEADERS": "{\"X-MCP-API-Key\": \"*Ibo7tweixlbfuwaiufxgakjyefctwajcetb*\", \"X-Custom-Header\": \"value\"}"
63
63
  }
64
64
  }
@@ -75,7 +75,7 @@ You can add custom headers to all API requests using the `CUSTOM_HEADERS` enviro
75
75
  "command": "npx",
76
76
  "args": ["-y", "@automattic/mcp-wordpress-remote"],
77
77
  "env": {
78
- "WP_API_URL": "https://your-wordpress-site.com",
78
+ "WP_API_URL": "https://your-wordpress-site.com/wp-json/mcp/mcp-adapter-default-server",
79
79
  "CUSTOM_HEADERS": "X-MCP-API-Key:IOskncfyes78U8on3q7ry43o487tybrc,X-Custom-Header:value"
80
80
  }
81
81
  }
@@ -87,7 +87,7 @@ You can add custom headers to all API requests using the `CUSTOM_HEADERS` enviro
87
87
 
88
88
  ```bash
89
89
  CUSTOM_HEADERS='{"X-MCP-API-Key": "wc_mcp_FaQduhQcW0mfVaZgP3yaaqDuXaZ3mw7j"}' \
90
- WP_API_URL="https://your-site.com" \
90
+ WP_API_URL="https://your-site.com/wp-json/mcp/mcp-adapter-default-server" \
91
91
  npx @automattic/mcp-wordpress-remote
92
92
  ```
93
93
 
@@ -109,7 +109,11 @@ Custom headers are included in:
109
109
 
110
110
  ## WordPress MCP Plugin
111
111
 
112
- You need to install the [wordpress-mcp](https://github.com/WordPress/mcp-adapter) plugin on your WordPress website and enable MCP Functionality in Settings > MCP Settings.
112
+ Install the [MCP Adapter](https://github.com/WordPress/mcp-adapter) plugin on your WordPress site. Once active, it registers a default MCP server at `/wp-json/mcp/mcp-adapter-default-server` — set `WP_API_URL` to that full URL (examples throughout this README show the pattern). To target a custom server id or namespace, pass the full URL of that server instead.
113
+
114
+ ### Legacy `wordpress-mcp` plugin
115
+
116
+ Earlier versions of this proxy were designed for [`Automattic/wordpress-mcp`](https://github.com/Automattic/wordpress-mcp), which exposes its endpoint at `/wp-json/wp/v2/wpmcp`. That plugin is deprecated in favor of `mcp-adapter`, but existing installs continue to work — for backwards compatibility, a bare-domain `WP_API_URL` (e.g. `https://your-wordpress-site.com`) still resolves to the `wordpress-mcp` endpoint. New setups should install `mcp-adapter` and use the full URL shown above.
113
117
 
114
118
  ## Authentication Methods
115
119
 
@@ -126,7 +130,7 @@ OAuth 2.1 provides the most secure and user-friendly experience with full MCP Au
126
130
  "command": "npx",
127
131
  "args": ["-y", "@automattic/mcp-wordpress-remote"],
128
132
  "env": {
129
- "WP_API_URL": "https://your-wordpress-site.com",
133
+ "WP_API_URL": "https://your-wordpress-site.com/wp-json/mcp/mcp-adapter-default-server",
130
134
  "OAUTH_ENABLED": "true"
131
135
  }
132
136
  }
@@ -163,7 +167,7 @@ For server-to-server authentication or when OAuth is not available.
163
167
  "command": "npx",
164
168
  "args": ["-y", "@automattic/mcp-wordpress-remote"],
165
169
  "env": {
166
- "WP_API_URL": "https://your-wordpress-site.com",
170
+ "WP_API_URL": "https://your-wordpress-site.com/wp-json/mcp/mcp-adapter-default-server",
167
171
  "JWT_TOKEN": "your-jwt-token-here"
168
172
  }
169
173
  }
@@ -182,7 +186,7 @@ Uses WordPress username and application password for basic authentication.
182
186
  "command": "npx",
183
187
  "args": ["-y", "@automattic/mcp-wordpress-remote"],
184
188
  "env": {
185
- "WP_API_URL": "https://your-wordpress-site.com",
189
+ "WP_API_URL": "https://your-wordpress-site.com/wp-json/mcp/mcp-adapter-default-server",
186
190
  "WP_API_USERNAME": "your-username",
187
191
  "WP_API_PASSWORD": "your-application-password",
188
192
  "OAUTH_ENABLED": "false"
@@ -210,7 +214,7 @@ To create an application password:
210
214
  "command": "npx",
211
215
  "args": ["-y", "@automattic/mcp-wordpress-remote"],
212
216
  "env": {
213
- "WP_API_URL": "https://your-wordpress-site.com",
217
+ "WP_API_URL": "https://your-wordpress-site.com/wp-json/mcp/mcp-adapter-default-server",
214
218
  "OAUTH_CALLBACK_PORT": "7665",
215
219
  "OAUTH_HOST": "127.0.0.1",
216
220
  "WP_OAUTH_CLIENT_ID": "your-custom-client-id"
@@ -231,7 +235,7 @@ For WooCommerce-specific tools and reports:
231
235
  "command": "npx",
232
236
  "args": ["-y", "@automattic/mcp-wordpress-remote"],
233
237
  "env": {
234
- "WP_API_URL": "https://your-wordpress-site.com",
238
+ "WP_API_URL": "https://your-wordpress-site.com/wp-json/mcp/mcp-adapter-default-server",
235
239
  "WOO_CUSTOMER_KEY": "ck_your-consumer-key",
236
240
  "WOO_CUSTOMER_SECRET": "cs_your-consumer-secret"
237
241
  }
@@ -244,7 +248,7 @@ For WooCommerce-specific tools and reports:
244
248
 
245
249
  | Variable | Description | Default | Required |
246
250
  | ----------------------------- | ------------------------------------------------ | -------------------- | --------------------- |
247
- | `WP_API_URL` | WordPress site URL | - | ✅ |
251
+ | `WP_API_URL` | WordPress site URL including the MCP endpoint path (e.g. `…/wp-json/mcp/mcp-adapter-default-server`). A bare domain resolves to the deprecated `wordpress-mcp` endpoint for backwards compatibility. | - | ✅ |
248
252
  | `OAUTH_ENABLED` | Enable OAuth authentication | `true` | - |
249
253
  | `OAUTH_CALLBACK_PORT` | OAuth callback port | `7665` | - |
250
254
  | `OAUTH_HOST` | OAuth callback hostname | `127.0.0.1` | - |
@@ -317,7 +321,7 @@ Configure your MCP client to use the local version:
317
321
  "command": "node",
318
322
  "args": ["/path/to/your/mcp-wordpress-remote/dist/proxy.js"],
319
323
  "env": {
320
- "WP_API_URL": "https://your-wordpress-site.com"
324
+ "WP_API_URL": "https://your-wordpress-site.com/wp-json/mcp/mcp-adapter-default-server"
321
325
  }
322
326
  }
323
327
  }
@@ -403,9 +407,8 @@ If you see "waiting for other instance" messages, this is normal behavior.
403
407
 
404
408
  **API endpoint not found:**
405
409
 
406
- - Verify WordPress MCP plugin is installed and activated
407
- - Check plugin is enabled in WordPress admin
408
- - Confirm `WP_API_URL` is correct
410
+ - Verify the [MCP Adapter](https://github.com/WordPress/mcp-adapter) plugin is installed and active
411
+ - Confirm `WP_API_URL` points at the server route (e.g. `…/wp-json/mcp/mcp-adapter-default-server`)
409
412
 
410
413
  **Permission denied:**
411
414
 
@@ -469,7 +472,7 @@ Log levels:
469
472
  ## Requirements
470
473
 
471
474
  - **Node.js 22+** (required for fetch API support)
472
- - **WordPress site** with [wordpress-mcp](https://github.com/Automattic/wordpress-mcp) plugin
475
+ - **WordPress site** with the [MCP Adapter](https://github.com/WordPress/mcp-adapter) plugin installed and active
473
476
  - **WordPress user account** with appropriate permissions
474
477
 
475
478
  ## License
@@ -484,7 +487,7 @@ Contributions welcome! This project is maintained by Automattic Inc.
484
487
 
485
488
  - **Issues:** [GitHub Issues](https://github.com/Automattic/mcp-wordpress-remote/issues)
486
489
  - **Documentation:** Check the troubleshooting section above
487
- - **WordPress MCP Plugin:** [Plugin Repository](https://github.com/Automattic/wordpress-mcp)
490
+ - **WordPress MCP Plugin:** [WordPress/mcp-adapter](https://github.com/WordPress/mcp-adapter)
488
491
 
489
492
  ---
490
493
 
package/dist/lib.d.ts CHANGED
@@ -15,7 +15,7 @@ interface WordPressResponse {
15
15
  [key: string]: any;
16
16
  }
17
17
 
18
- declare const MCP_WORDPRESS_REMOTE_VERSION = "0.2.19";
18
+ declare const MCP_WORDPRESS_REMOTE_VERSION = "0.3.0";
19
19
  /**
20
20
  * Centralized configuration for MCP WordPress Remote
21
21
  * All default values are defined here and can be overridden via environment variables
@@ -149,7 +149,9 @@ declare const logger: {
149
149
  config: (message: string, level?: LogLevel, ...args: any[]) => void;
150
150
  };
151
151
 
152
- declare function wpRequest(requestData: any, useJsonRpc?: boolean): Promise<WordPressResponse>;
152
+ declare function wpRequest(requestData: any, useJsonRpc?: boolean, options?: {
153
+ allowSessionRecovery?: boolean;
154
+ }): Promise<WordPressResponse>;
153
155
 
154
156
  /**
155
157
  * WordPress OAuth types and error definitions
package/dist/lib.js CHANGED
@@ -91,13 +91,13 @@ var require_ponyfill_es2018 = __commonJS({
91
91
  typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global2 = typeof globalThis !== "undefined" ? globalThis : global2 || self, factory(global2.WebStreamsPolyfill = {}));
92
92
  })(exports, function(exports2) {
93
93
  "use strict";
94
- function noop2() {
94
+ function noop3() {
95
95
  return void 0;
96
96
  }
97
97
  function typeIsObject(x2) {
98
98
  return typeof x2 === "object" && x2 !== null || typeof x2 === "function";
99
99
  }
100
- const rethrowAssertionErrorRejection = noop2;
100
+ const rethrowAssertionErrorRejection = noop3;
101
101
  function setFunctionName(fn, name) {
102
102
  try {
103
103
  Object.defineProperty(fn, "name", {
@@ -2726,7 +2726,7 @@ var require_ponyfill_es2018 = __commonJS({
2726
2726
  return newPromise((resolveRead, rejectRead) => {
2727
2727
  ReadableStreamDefaultReaderRead(reader, {
2728
2728
  _chunkSteps: (chunk) => {
2729
- currentWrite = PerformPromiseThen(WritableStreamDefaultWriterWrite(writer, chunk), void 0, noop2);
2729
+ currentWrite = PerformPromiseThen(WritableStreamDefaultWriterWrite(writer, chunk), void 0, noop3);
2730
2730
  resolveRead(false);
2731
2731
  },
2732
2732
  _closeSteps: () => resolveRead(true),
@@ -3407,7 +3407,7 @@ var require_ponyfill_es2018 = __commonJS({
3407
3407
  function ReadableStreamFromIterable(asyncIterable) {
3408
3408
  let stream;
3409
3409
  const iteratorRecord = GetIterator(asyncIterable, "async");
3410
- const startAlgorithm = noop2;
3410
+ const startAlgorithm = noop3;
3411
3411
  function pullAlgorithm() {
3412
3412
  let nextResult;
3413
3413
  try {
@@ -3459,7 +3459,7 @@ var require_ponyfill_es2018 = __commonJS({
3459
3459
  }
3460
3460
  function ReadableStreamFromDefaultReader(reader) {
3461
3461
  let stream;
3462
- const startAlgorithm = noop2;
3462
+ const startAlgorithm = noop3;
3463
3463
  function pullAlgorithm() {
3464
3464
  let readPromise;
3465
3465
  try {
@@ -3778,7 +3778,7 @@ var require_ponyfill_es2018 = __commonJS({
3778
3778
  });
3779
3779
  }
3780
3780
  const sourceCancelPromise = stream._readableStreamController[CancelSteps](reason);
3781
- return transformPromiseWith(sourceCancelPromise, noop2);
3781
+ return transformPromiseWith(sourceCancelPromise, noop3);
3782
3782
  }
3783
3783
  function ReadableStreamClose(stream) {
3784
3784
  stream._state = "closed";
@@ -4999,7 +4999,7 @@ async function toFormData(Body2, ct) {
4999
4999
  parser.end();
5000
5000
  return formData;
5001
5001
  }
5002
- var s, S, f2, F, LF, CR, SPACE, HYPHEN, COLON, A, Z, lower, noop, MultipartParser;
5002
+ var s, S, f2, F, LF, CR, SPACE, HYPHEN, COLON, A, Z, lower, noop2, MultipartParser;
5003
5003
  var init_multipart_parser = __esm({
5004
5004
  "node_modules/node-fetch/src/utils/multipart-parser.js"() {
5005
5005
  "use strict";
@@ -5031,7 +5031,7 @@ var init_multipart_parser = __esm({
5031
5031
  A = 97;
5032
5032
  Z = 122;
5033
5033
  lower = (c) => c | 32;
5034
- noop = () => {
5034
+ noop2 = () => {
5035
5035
  };
5036
5036
  MultipartParser = class {
5037
5037
  /**
@@ -5040,13 +5040,13 @@ var init_multipart_parser = __esm({
5040
5040
  constructor(boundary) {
5041
5041
  this.index = 0;
5042
5042
  this.flags = 0;
5043
- this.onHeaderEnd = noop;
5044
- this.onHeaderField = noop;
5045
- this.onHeadersEnd = noop;
5046
- this.onHeaderValue = noop;
5047
- this.onPartBegin = noop;
5048
- this.onPartData = noop;
5049
- this.onPartEnd = noop;
5043
+ this.onHeaderEnd = noop2;
5044
+ this.onHeaderField = noop2;
5045
+ this.onHeadersEnd = noop2;
5046
+ this.onHeaderValue = noop2;
5047
+ this.onPartBegin = noop2;
5048
+ this.onPartData = noop2;
5049
+ this.onPartEnd = noop2;
5050
5050
  this.boundaryChars = {};
5051
5051
  boundary = "\r\n--" + boundary;
5052
5052
  const ui8a = new Uint8Array(boundary.length);
@@ -7881,7 +7881,7 @@ var require_destroy = __commonJS({
7881
7881
  }
7882
7882
  if (isEventEmitter(stream) && suppress) {
7883
7883
  stream.removeAllListeners("error");
7884
- stream.addListener("error", noop2);
7884
+ stream.addListener("error", noop3);
7885
7885
  }
7886
7886
  return stream;
7887
7887
  }
@@ -7936,7 +7936,7 @@ var require_destroy = __commonJS({
7936
7936
  function isZlibStream(stream) {
7937
7937
  return stream instanceof Zlib.Gzip || stream instanceof Zlib.Gunzip || stream instanceof Zlib.Deflate || stream instanceof Zlib.DeflateRaw || stream instanceof Zlib.Inflate || stream instanceof Zlib.InflateRaw || stream instanceof Zlib.Unzip;
7938
7938
  }
7939
- function noop2() {
7939
+ function noop3() {
7940
7940
  }
7941
7941
  function onDrainClearBinding() {
7942
7942
  this._binding.clear();
@@ -30288,6 +30288,109 @@ var require_express2 = __commonJS({
30288
30288
  }
30289
30289
  });
30290
30290
 
30291
+ // node_modules/eventsource-parser/dist/index.js
30292
+ var ParseError = class extends Error {
30293
+ constructor(message, options) {
30294
+ super(message), this.name = "ParseError", this.type = options.type, this.field = options.field, this.value = options.value, this.line = options.line;
30295
+ }
30296
+ };
30297
+ function noop(_arg) {
30298
+ }
30299
+ function createParser(callbacks) {
30300
+ if (typeof callbacks == "function")
30301
+ throw new TypeError(
30302
+ "`callbacks` must be an object, got a function instead. Did you mean `{onEvent: fn}`?"
30303
+ );
30304
+ const { onEvent = noop, onError = noop, onRetry = noop, onComment } = callbacks;
30305
+ let incompleteLine = "", isFirstChunk = true, id, data = "", eventType = "";
30306
+ function feed(newChunk) {
30307
+ const chunk = isFirstChunk ? newChunk.replace(/^\xEF\xBB\xBF/, "") : newChunk, [complete, incomplete] = splitLines(`${incompleteLine}${chunk}`);
30308
+ for (const line of complete)
30309
+ parseLine(line);
30310
+ incompleteLine = incomplete, isFirstChunk = false;
30311
+ }
30312
+ function parseLine(line) {
30313
+ if (line === "") {
30314
+ dispatchEvent();
30315
+ return;
30316
+ }
30317
+ if (line.startsWith(":")) {
30318
+ onComment && onComment(line.slice(line.startsWith(": ") ? 2 : 1));
30319
+ return;
30320
+ }
30321
+ const fieldSeparatorIndex = line.indexOf(":");
30322
+ if (fieldSeparatorIndex !== -1) {
30323
+ const field = line.slice(0, fieldSeparatorIndex), offset = line[fieldSeparatorIndex + 1] === " " ? 2 : 1, value = line.slice(fieldSeparatorIndex + offset);
30324
+ processField(field, value, line);
30325
+ return;
30326
+ }
30327
+ processField(line, "", line);
30328
+ }
30329
+ function processField(field, value, line) {
30330
+ switch (field) {
30331
+ case "event":
30332
+ eventType = value;
30333
+ break;
30334
+ case "data":
30335
+ data = `${data}${value}
30336
+ `;
30337
+ break;
30338
+ case "id":
30339
+ id = value.includes("\0") ? void 0 : value;
30340
+ break;
30341
+ case "retry":
30342
+ /^\d+$/.test(value) ? onRetry(parseInt(value, 10)) : onError(
30343
+ new ParseError(`Invalid \`retry\` value: "${value}"`, {
30344
+ type: "invalid-retry",
30345
+ value,
30346
+ line
30347
+ })
30348
+ );
30349
+ break;
30350
+ default:
30351
+ onError(
30352
+ new ParseError(
30353
+ `Unknown field "${field.length > 20 ? `${field.slice(0, 20)}\u2026` : field}"`,
30354
+ { type: "unknown-field", field, value, line }
30355
+ )
30356
+ );
30357
+ break;
30358
+ }
30359
+ }
30360
+ function dispatchEvent() {
30361
+ data.length > 0 && onEvent({
30362
+ id,
30363
+ event: eventType || void 0,
30364
+ // If the data buffer's last character is a U+000A LINE FEED (LF) character,
30365
+ // then remove the last character from the data buffer.
30366
+ data: data.endsWith(`
30367
+ `) ? data.slice(0, -1) : data
30368
+ }), id = void 0, data = "", eventType = "";
30369
+ }
30370
+ function reset(options = {}) {
30371
+ incompleteLine && options.consume && parseLine(incompleteLine), isFirstChunk = true, id = void 0, data = "", eventType = "", incompleteLine = "";
30372
+ }
30373
+ return { feed, reset };
30374
+ }
30375
+ function splitLines(chunk) {
30376
+ const lines = [];
30377
+ let incompleteLine = "", searchIndex = 0;
30378
+ for (; searchIndex < chunk.length; ) {
30379
+ const crIndex = chunk.indexOf("\r", searchIndex), lfIndex = chunk.indexOf(`
30380
+ `, searchIndex);
30381
+ let lineEnd = -1;
30382
+ if (crIndex !== -1 && lfIndex !== -1 ? lineEnd = Math.min(crIndex, lfIndex) : crIndex !== -1 ? lineEnd = crIndex : lfIndex !== -1 && (lineEnd = lfIndex), lineEnd === -1) {
30383
+ incompleteLine = chunk.slice(searchIndex);
30384
+ break;
30385
+ } else {
30386
+ const line = chunk.slice(searchIndex, lineEnd);
30387
+ lines.push(line), searchIndex = lineEnd + 1, chunk[searchIndex - 1] === "\r" && chunk[searchIndex] === `
30388
+ ` && searchIndex++;
30389
+ }
30390
+ }
30391
+ return [lines, incompleteLine];
30392
+ }
30393
+
30291
30394
  // node_modules/zod/lib/index.mjs
30292
30395
  var util;
30293
30396
  (function(util2) {
@@ -35153,10 +35256,9 @@ import crypto3 from "crypto";
35153
35256
  import path from "path";
35154
35257
  import fs from "fs/promises";
35155
35258
  import crypto2 from "crypto";
35156
- var VERSION = MCP_WORDPRESS_REMOTE_VERSION;
35157
35259
  function getConfigDir() {
35158
35260
  const baseConfigDir = CONFIG.WP_MCP_CONFIG_DIR;
35159
- return path.join(baseConfigDir, `wordpress-remote-${VERSION}`);
35261
+ return path.join(baseConfigDir, `wordpress-remote-${MCP_WORDPRESS_REMOTE_VERSION}`);
35160
35262
  }
35161
35263
  async function ensureConfigDir() {
35162
35264
  try {
@@ -35346,7 +35448,7 @@ function calculateDefaultPort(serverUrlHash) {
35346
35448
  }
35347
35449
  async function findExistingClientPort(serverUrlHash) {
35348
35450
  const clientInfo = await readJsonFile(serverUrlHash, "client_info.json");
35349
- if (!clientInfo) {
35451
+ if (!clientInfo || !clientInfo.redirect_uris) {
35350
35452
  return void 0;
35351
35453
  }
35352
35454
  const localhostRedirectUri = clientInfo.redirect_uris.map((uri) => {
@@ -35382,7 +35484,7 @@ async function selectCallbackPort(serverUrl, specifiedPort, _unused) {
35382
35484
  }
35383
35485
 
35384
35486
  // src/lib/config.ts
35385
- var MCP_WORDPRESS_REMOTE_VERSION = "0.2.19";
35487
+ var MCP_WORDPRESS_REMOTE_VERSION = "0.3.0";
35386
35488
  var CONFIG = {
35387
35489
  // API Configuration
35388
35490
  WP_API_URL: process.env.WP_API_URL || "https://example.com",
@@ -36889,17 +36991,14 @@ var OAuthCallbackServer = class {
36889
36991
  });
36890
36992
  }
36891
36993
  async stop() {
36892
- return new Promise((resolve) => {
36893
- if (this.server) {
36894
- this.server.close(() => {
36895
- logger.oauth("OAuth callback server stopped");
36896
- this.server = null;
36897
- resolve();
36898
- });
36899
- } else {
36900
- resolve();
36901
- }
36994
+ if (!this.server) return;
36995
+ const server = this.server;
36996
+ this.server = null;
36997
+ server.close((err) => {
36998
+ if (err) logger.error("OAuth callback server close error", "OAUTH", err);
36999
+ else logger.oauth("OAuth callback server stopped");
36902
37000
  });
37001
+ server.closeAllConnections?.();
36903
37002
  }
36904
37003
  getCallbackUrl() {
36905
37004
  return `http://${this.options.host}:${this.options.port}/oauth/callback`;
@@ -37904,6 +38003,11 @@ var mcpOAuthProvider = null;
37904
38003
  var authCoordinator = null;
37905
38004
  var globalEvents = null;
37906
38005
  var globalSessionId = null;
38006
+ var lastInitializeRequest = null;
38007
+ var sessionRefreshPromise = null;
38008
+ var WP_MCP_ENDPOINT = "/wp/v2/wpmcp";
38009
+ var INVALID_SESSION_ERROR_CODE = -32602;
38010
+ var INVALID_SESSION_ERROR_MESSAGE = "Invalid or expired session";
37907
38011
  function validateEnvironment() {
37908
38012
  const validation = validateConfig();
37909
38013
  if (!validation.isValid) {
@@ -37916,6 +38020,24 @@ function validateEnvironment() {
37916
38020
  function removeTrailingSlash(url) {
37917
38021
  return url.replace(/\/+$/, "");
37918
38022
  }
38023
+ function parseSSEMessage(text) {
38024
+ let result;
38025
+ let found = false;
38026
+ const parser = createParser({
38027
+ onEvent(event) {
38028
+ if (found) return;
38029
+ if (!event.event || event.event === "message") {
38030
+ result = JSON.parse(event.data);
38031
+ found = true;
38032
+ }
38033
+ }
38034
+ });
38035
+ parser.feed(text);
38036
+ if (!found) {
38037
+ throw new Error('No "message" event with data found in SSE response');
38038
+ }
38039
+ return result;
38040
+ }
37919
38041
  function constructApiUrl(baseUrl, defaultEndpoint) {
37920
38042
  const cleanUrl = removeTrailingSlash(baseUrl);
37921
38043
  try {
@@ -38002,9 +38124,50 @@ async function getOAuthTokens() {
38002
38124
  return null;
38003
38125
  }
38004
38126
  }
38005
- async function wpRequest(requestData, useJsonRpc = true) {
38006
- validateEnvironment();
38007
- const endpoint = "/wp/v2/wpmcp";
38127
+ function getCurrentApiUrl() {
38128
+ return process.env.WP_API_URL || CONFIG.WP_API_URL;
38129
+ }
38130
+ function getRequestUrl() {
38131
+ return constructApiUrl(getCurrentApiUrl(), WP_MCP_ENDPOINT);
38132
+ }
38133
+ function cloneRequestData(requestData) {
38134
+ return JSON.parse(JSON.stringify(requestData));
38135
+ }
38136
+ function isInitializeRequest(requestData) {
38137
+ return requestData?.method === "initialize";
38138
+ }
38139
+ function cacheInitializeRequest(requestData, useJsonRpc) {
38140
+ lastInitializeRequest = {
38141
+ requestData: cloneRequestData(requestData),
38142
+ useJsonRpc
38143
+ };
38144
+ }
38145
+ function parseApiErrorResponse(error) {
38146
+ if (!error.response) {
38147
+ return null;
38148
+ }
38149
+ if (typeof error.response === "string") {
38150
+ try {
38151
+ return JSON.parse(error.response);
38152
+ } catch {
38153
+ return null;
38154
+ }
38155
+ }
38156
+ return error.response;
38157
+ }
38158
+ function isInvalidSessionError(error) {
38159
+ const errorResponse = parseApiErrorResponse(error);
38160
+ return errorResponse?.code === INVALID_SESSION_ERROR_CODE && errorResponse?.message === INVALID_SESSION_ERROR_MESSAGE;
38161
+ }
38162
+ function updateSessionId(sessionId) {
38163
+ if (globalSessionId === sessionId) {
38164
+ return;
38165
+ }
38166
+ globalSessionId = sessionId;
38167
+ logger.info(`Session ID received from WordPress: ${globalSessionId}`, "SESSION");
38168
+ }
38169
+ async function executeWordPressRequest(requestData, useJsonRpc, sessionIdUsed) {
38170
+ const url = getRequestUrl();
38008
38171
  const method = "POST";
38009
38172
  if (useJsonRpc) {
38010
38173
  logger.api(`Request method: ${requestData.method || "unknown"} (JSON-RPC)`);
@@ -38063,13 +38226,12 @@ async function wpRequest(requestData, useJsonRpc = true) {
38063
38226
  "NO_AUTH_METHOD"
38064
38227
  );
38065
38228
  }
38066
- const currentApiUrl = process.env.WP_API_URL || CONFIG.WP_API_URL;
38067
38229
  logger.debug(`Environment: ${CONFIG.NODE_ENV}`, "API");
38068
- logger.debug(`Base API URL: ${currentApiUrl}`, "API");
38069
- const url = constructApiUrl(currentApiUrl, endpoint);
38230
+ logger.debug(`Base API URL: ${getCurrentApiUrl()}`, "API");
38070
38231
  logger.debug(`Final requesting URL: ${url}`, "API");
38071
38232
  const headers = {
38072
38233
  "Content-Type": "application/json",
38234
+ Accept: "application/json, text/event-stream",
38073
38235
  "MCP-Protocol-Version": "2025-06-18",
38074
38236
  // MCP protocol version
38075
38237
  ...customHeaders
@@ -38078,8 +38240,8 @@ async function wpRequest(requestData, useJsonRpc = true) {
38078
38240
  if (authHeader) {
38079
38241
  headers.Authorization = authHeader;
38080
38242
  }
38081
- if (globalSessionId) {
38082
- headers["Mcp-Session-Id"] = globalSessionId;
38243
+ if (sessionIdUsed) {
38244
+ headers["Mcp-Session-Id"] = sessionIdUsed;
38083
38245
  }
38084
38246
  if (authHeader) {
38085
38247
  logger.debug("Using Authorization header for authentication", "API");
@@ -38103,21 +38265,27 @@ async function wpRequest(requestData, useJsonRpc = true) {
38103
38265
  logger.debug(`Request method: ${method}`, "API");
38104
38266
  const response = await proxyFetch(url, fetchOptions);
38105
38267
  logger.debug(`Response status: ${response.status}`, "API");
38268
+ const rawBody = await response.text();
38106
38269
  if (!response.ok) {
38107
- const errorText = await response.text();
38108
- logger.error(`API error response: ${errorText}`, "API");
38270
+ logger.error(`API error response: ${rawBody}`, "API");
38109
38271
  throw new APIError(
38110
- `WordPress API error (${response.status}): ${errorText}`,
38272
+ `WordPress API error (${response.status}): ${rawBody}`,
38111
38273
  response.status,
38112
38274
  url,
38113
- errorText
38275
+ rawBody
38114
38276
  );
38115
38277
  }
38116
- const responseData = await response.json();
38278
+ const contentType = response.headers.get("content-type") ?? "";
38279
+ let responseData;
38280
+ if (contentType.includes("text/event-stream")) {
38281
+ responseData = parseSSEMessage(rawBody);
38282
+ logger.debug("Parsed text/event-stream response body", "API");
38283
+ } else {
38284
+ responseData = JSON.parse(rawBody);
38285
+ }
38117
38286
  const sessionIdHeader = response.headers.get("Mcp-Session-Id");
38118
- if (sessionIdHeader && !globalSessionId) {
38119
- globalSessionId = sessionIdHeader;
38120
- logger.info(`Session ID received from WordPress: ${globalSessionId}`, "SESSION");
38287
+ if (sessionIdHeader) {
38288
+ updateSessionId(sessionIdHeader);
38121
38289
  }
38122
38290
  logger.api("Response received successfully");
38123
38291
  logger.debug(`Response data: ${JSON.stringify(responseData)}`, "API");
@@ -38130,14 +38298,20 @@ async function wpRequest(requestData, useJsonRpc = true) {
38130
38298
  `WordPress JSON-RPC error: ${jsonrpcResponse.error.message}`,
38131
38299
  jsonrpcResponse.error.code || 500,
38132
38300
  url,
38133
- JSON.stringify(jsonrpcResponse.error)
38301
+ jsonrpcResponse.error
38134
38302
  );
38135
38303
  } else if (jsonrpcResponse.result !== void 0) {
38136
- return jsonrpcResponse.result;
38304
+ return {
38305
+ responseData: jsonrpcResponse.result,
38306
+ sessionIdUsed
38307
+ };
38137
38308
  }
38138
38309
  }
38139
38310
  }
38140
- return responseData;
38311
+ return {
38312
+ responseData,
38313
+ sessionIdUsed
38314
+ };
38141
38315
  } catch (error) {
38142
38316
  if (error instanceof APIError) {
38143
38317
  throw error;
@@ -38147,6 +38321,77 @@ async function wpRequest(requestData, useJsonRpc = true) {
38147
38321
  throw new APIError(errorMessage, 0, url);
38148
38322
  }
38149
38323
  }
38324
+ async function refreshSession(failedSessionId) {
38325
+ if (failedSessionId && globalSessionId && globalSessionId !== failedSessionId) {
38326
+ logger.info("Detected newer session while handling invalid-session error; skipping refresh", "SESSION");
38327
+ return;
38328
+ }
38329
+ if (sessionRefreshPromise) {
38330
+ logger.info("Waiting for in-flight WordPress session refresh", "SESSION");
38331
+ await sessionRefreshPromise;
38332
+ return;
38333
+ }
38334
+ if (!lastInitializeRequest) {
38335
+ throw new APIError(
38336
+ "Cannot refresh WordPress session before initialize has completed",
38337
+ 0,
38338
+ getRequestUrl()
38339
+ );
38340
+ }
38341
+ sessionRefreshPromise = (async () => {
38342
+ logger.warn("WordPress session rejected; refreshing session via initialize", "SESSION");
38343
+ globalSessionId = null;
38344
+ await executeWordPressRequest(
38345
+ lastInitializeRequest.requestData,
38346
+ lastInitializeRequest.useJsonRpc,
38347
+ null
38348
+ );
38349
+ if (!globalSessionId) {
38350
+ throw new APIError(
38351
+ "WordPress initialize did not return a session ID during refresh",
38352
+ 0,
38353
+ getRequestUrl()
38354
+ );
38355
+ }
38356
+ })();
38357
+ try {
38358
+ await sessionRefreshPromise;
38359
+ } finally {
38360
+ sessionRefreshPromise = null;
38361
+ }
38362
+ }
38363
+ async function wpRequest(requestData, useJsonRpc = true, options = {}) {
38364
+ validateEnvironment();
38365
+ const allowSessionRecovery = options.allowSessionRecovery !== false;
38366
+ if (isInitializeRequest(requestData)) {
38367
+ cacheInitializeRequest(requestData, useJsonRpc);
38368
+ }
38369
+ const sessionIdUsed = globalSessionId;
38370
+ try {
38371
+ const result = await executeWordPressRequest(requestData, useJsonRpc, sessionIdUsed);
38372
+ return result.responseData;
38373
+ } catch (error) {
38374
+ if (error instanceof APIError && allowSessionRecovery && !isInitializeRequest(requestData) && isInvalidSessionError(error)) {
38375
+ logger.warn("WordPress session expired; attempting one-time recovery", "SESSION", {
38376
+ method: requestData?.method || "unknown",
38377
+ sessionIdUsed: sessionIdUsed || "none"
38378
+ });
38379
+ await refreshSession(sessionIdUsed);
38380
+ const retriedResult = await executeWordPressRequest(
38381
+ requestData,
38382
+ useJsonRpc,
38383
+ globalSessionId
38384
+ );
38385
+ return retriedResult.responseData;
38386
+ }
38387
+ if (error instanceof APIError) {
38388
+ throw error;
38389
+ }
38390
+ const errorMessage = error instanceof Error ? error.message : String(error);
38391
+ logger.error(`Error in wpRequest: ${errorMessage}`, "API");
38392
+ throw new APIError(errorMessage, 0, getRequestUrl());
38393
+ }
38394
+ }
38150
38395
  export {
38151
38396
  APIError,
38152
38397
  AuthError,
package/dist/proxy.js CHANGED
@@ -92,13 +92,13 @@ var require_ponyfill_es2018 = __commonJS({
92
92
  typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define(["exports"], factory) : (global2 = typeof globalThis !== "undefined" ? globalThis : global2 || self, factory(global2.WebStreamsPolyfill = {}));
93
93
  })(exports, function(exports2) {
94
94
  "use strict";
95
- function noop2() {
95
+ function noop3() {
96
96
  return void 0;
97
97
  }
98
98
  function typeIsObject(x2) {
99
99
  return typeof x2 === "object" && x2 !== null || typeof x2 === "function";
100
100
  }
101
- const rethrowAssertionErrorRejection = noop2;
101
+ const rethrowAssertionErrorRejection = noop3;
102
102
  function setFunctionName(fn, name) {
103
103
  try {
104
104
  Object.defineProperty(fn, "name", {
@@ -2727,7 +2727,7 @@ var require_ponyfill_es2018 = __commonJS({
2727
2727
  return newPromise((resolveRead, rejectRead) => {
2728
2728
  ReadableStreamDefaultReaderRead(reader, {
2729
2729
  _chunkSteps: (chunk) => {
2730
- currentWrite = PerformPromiseThen(WritableStreamDefaultWriterWrite(writer, chunk), void 0, noop2);
2730
+ currentWrite = PerformPromiseThen(WritableStreamDefaultWriterWrite(writer, chunk), void 0, noop3);
2731
2731
  resolveRead(false);
2732
2732
  },
2733
2733
  _closeSteps: () => resolveRead(true),
@@ -3408,7 +3408,7 @@ var require_ponyfill_es2018 = __commonJS({
3408
3408
  function ReadableStreamFromIterable(asyncIterable) {
3409
3409
  let stream;
3410
3410
  const iteratorRecord = GetIterator(asyncIterable, "async");
3411
- const startAlgorithm = noop2;
3411
+ const startAlgorithm = noop3;
3412
3412
  function pullAlgorithm() {
3413
3413
  let nextResult;
3414
3414
  try {
@@ -3460,7 +3460,7 @@ var require_ponyfill_es2018 = __commonJS({
3460
3460
  }
3461
3461
  function ReadableStreamFromDefaultReader(reader) {
3462
3462
  let stream;
3463
- const startAlgorithm = noop2;
3463
+ const startAlgorithm = noop3;
3464
3464
  function pullAlgorithm() {
3465
3465
  let readPromise;
3466
3466
  try {
@@ -3779,7 +3779,7 @@ var require_ponyfill_es2018 = __commonJS({
3779
3779
  });
3780
3780
  }
3781
3781
  const sourceCancelPromise = stream._readableStreamController[CancelSteps](reason);
3782
- return transformPromiseWith(sourceCancelPromise, noop2);
3782
+ return transformPromiseWith(sourceCancelPromise, noop3);
3783
3783
  }
3784
3784
  function ReadableStreamClose(stream) {
3785
3785
  stream._state = "closed";
@@ -5000,7 +5000,7 @@ async function toFormData(Body2, ct) {
5000
5000
  parser.end();
5001
5001
  return formData;
5002
5002
  }
5003
- var s, S, f2, F, LF, CR, SPACE, HYPHEN, COLON, A, Z, lower, noop, MultipartParser;
5003
+ var s, S, f2, F, LF, CR, SPACE, HYPHEN, COLON, A, Z, lower, noop2, MultipartParser;
5004
5004
  var init_multipart_parser = __esm({
5005
5005
  "node_modules/node-fetch/src/utils/multipart-parser.js"() {
5006
5006
  "use strict";
@@ -5032,7 +5032,7 @@ var init_multipart_parser = __esm({
5032
5032
  A = 97;
5033
5033
  Z = 122;
5034
5034
  lower = (c) => c | 32;
5035
- noop = () => {
5035
+ noop2 = () => {
5036
5036
  };
5037
5037
  MultipartParser = class {
5038
5038
  /**
@@ -5041,13 +5041,13 @@ var init_multipart_parser = __esm({
5041
5041
  constructor(boundary) {
5042
5042
  this.index = 0;
5043
5043
  this.flags = 0;
5044
- this.onHeaderEnd = noop;
5045
- this.onHeaderField = noop;
5046
- this.onHeadersEnd = noop;
5047
- this.onHeaderValue = noop;
5048
- this.onPartBegin = noop;
5049
- this.onPartData = noop;
5050
- this.onPartEnd = noop;
5044
+ this.onHeaderEnd = noop2;
5045
+ this.onHeaderField = noop2;
5046
+ this.onHeadersEnd = noop2;
5047
+ this.onHeaderValue = noop2;
5048
+ this.onPartBegin = noop2;
5049
+ this.onPartData = noop2;
5050
+ this.onPartEnd = noop2;
5051
5051
  this.boundaryChars = {};
5052
5052
  boundary = "\r\n--" + boundary;
5053
5053
  const ui8a = new Uint8Array(boundary.length);
@@ -7882,7 +7882,7 @@ var require_destroy = __commonJS({
7882
7882
  }
7883
7883
  if (isEventEmitter(stream) && suppress) {
7884
7884
  stream.removeAllListeners("error");
7885
- stream.addListener("error", noop2);
7885
+ stream.addListener("error", noop3);
7886
7886
  }
7887
7887
  return stream;
7888
7888
  }
@@ -7937,7 +7937,7 @@ var require_destroy = __commonJS({
7937
7937
  function isZlibStream(stream) {
7938
7938
  return stream instanceof Zlib.Gzip || stream instanceof Zlib.Gunzip || stream instanceof Zlib.Deflate || stream instanceof Zlib.DeflateRaw || stream instanceof Zlib.Inflate || stream instanceof Zlib.InflateRaw || stream instanceof Zlib.Unzip;
7939
7939
  }
7940
- function noop2() {
7940
+ function noop3() {
7941
7941
  }
7942
7942
  function onDrainClearBinding() {
7943
7943
  this._binding.clear();
@@ -35686,6 +35686,109 @@ var StdioServerTransport = class {
35686
35686
  }
35687
35687
  };
35688
35688
 
35689
+ // node_modules/eventsource-parser/dist/index.js
35690
+ var ParseError = class extends Error {
35691
+ constructor(message, options) {
35692
+ super(message), this.name = "ParseError", this.type = options.type, this.field = options.field, this.value = options.value, this.line = options.line;
35693
+ }
35694
+ };
35695
+ function noop(_arg) {
35696
+ }
35697
+ function createParser(callbacks) {
35698
+ if (typeof callbacks == "function")
35699
+ throw new TypeError(
35700
+ "`callbacks` must be an object, got a function instead. Did you mean `{onEvent: fn}`?"
35701
+ );
35702
+ const { onEvent = noop, onError = noop, onRetry = noop, onComment } = callbacks;
35703
+ let incompleteLine = "", isFirstChunk = true, id, data = "", eventType = "";
35704
+ function feed(newChunk) {
35705
+ const chunk = isFirstChunk ? newChunk.replace(/^\xEF\xBB\xBF/, "") : newChunk, [complete, incomplete] = splitLines(`${incompleteLine}${chunk}`);
35706
+ for (const line of complete)
35707
+ parseLine(line);
35708
+ incompleteLine = incomplete, isFirstChunk = false;
35709
+ }
35710
+ function parseLine(line) {
35711
+ if (line === "") {
35712
+ dispatchEvent();
35713
+ return;
35714
+ }
35715
+ if (line.startsWith(":")) {
35716
+ onComment && onComment(line.slice(line.startsWith(": ") ? 2 : 1));
35717
+ return;
35718
+ }
35719
+ const fieldSeparatorIndex = line.indexOf(":");
35720
+ if (fieldSeparatorIndex !== -1) {
35721
+ const field = line.slice(0, fieldSeparatorIndex), offset = line[fieldSeparatorIndex + 1] === " " ? 2 : 1, value = line.slice(fieldSeparatorIndex + offset);
35722
+ processField(field, value, line);
35723
+ return;
35724
+ }
35725
+ processField(line, "", line);
35726
+ }
35727
+ function processField(field, value, line) {
35728
+ switch (field) {
35729
+ case "event":
35730
+ eventType = value;
35731
+ break;
35732
+ case "data":
35733
+ data = `${data}${value}
35734
+ `;
35735
+ break;
35736
+ case "id":
35737
+ id = value.includes("\0") ? void 0 : value;
35738
+ break;
35739
+ case "retry":
35740
+ /^\d+$/.test(value) ? onRetry(parseInt(value, 10)) : onError(
35741
+ new ParseError(`Invalid \`retry\` value: "${value}"`, {
35742
+ type: "invalid-retry",
35743
+ value,
35744
+ line
35745
+ })
35746
+ );
35747
+ break;
35748
+ default:
35749
+ onError(
35750
+ new ParseError(
35751
+ `Unknown field "${field.length > 20 ? `${field.slice(0, 20)}\u2026` : field}"`,
35752
+ { type: "unknown-field", field, value, line }
35753
+ )
35754
+ );
35755
+ break;
35756
+ }
35757
+ }
35758
+ function dispatchEvent() {
35759
+ data.length > 0 && onEvent({
35760
+ id,
35761
+ event: eventType || void 0,
35762
+ // If the data buffer's last character is a U+000A LINE FEED (LF) character,
35763
+ // then remove the last character from the data buffer.
35764
+ data: data.endsWith(`
35765
+ `) ? data.slice(0, -1) : data
35766
+ }), id = void 0, data = "", eventType = "";
35767
+ }
35768
+ function reset(options = {}) {
35769
+ incompleteLine && options.consume && parseLine(incompleteLine), isFirstChunk = true, id = void 0, data = "", eventType = "", incompleteLine = "";
35770
+ }
35771
+ return { feed, reset };
35772
+ }
35773
+ function splitLines(chunk) {
35774
+ const lines = [];
35775
+ let incompleteLine = "", searchIndex = 0;
35776
+ for (; searchIndex < chunk.length; ) {
35777
+ const crIndex = chunk.indexOf("\r", searchIndex), lfIndex = chunk.indexOf(`
35778
+ `, searchIndex);
35779
+ let lineEnd = -1;
35780
+ if (crIndex !== -1 && lfIndex !== -1 ? lineEnd = Math.min(crIndex, lfIndex) : crIndex !== -1 ? lineEnd = crIndex : lfIndex !== -1 && (lineEnd = lfIndex), lineEnd === -1) {
35781
+ incompleteLine = chunk.slice(searchIndex);
35782
+ break;
35783
+ } else {
35784
+ const line = chunk.slice(searchIndex, lineEnd);
35785
+ lines.push(line), searchIndex = lineEnd + 1, chunk[searchIndex - 1] === "\r" && chunk[searchIndex] === `
35786
+ ` && searchIndex++;
35787
+ }
35788
+ }
35789
+ return [lines, incompleteLine];
35790
+ }
35791
+
35689
35792
  // node_modules/pkce-challenge/dist/index.node.js
35690
35793
  var crypto;
35691
35794
  crypto = globalThis.crypto?.webcrypto ?? // Node.js [18-16] REPL
@@ -35775,10 +35878,9 @@ import path from "path";
35775
35878
  import fs from "fs/promises";
35776
35879
  import fsSync from "fs";
35777
35880
  import crypto2 from "crypto";
35778
- var VERSION = MCP_WORDPRESS_REMOTE_VERSION;
35779
35881
  function getConfigDir() {
35780
35882
  const baseConfigDir = CONFIG.WP_MCP_CONFIG_DIR;
35781
- return path.join(baseConfigDir, `wordpress-remote-${VERSION}`);
35883
+ return path.join(baseConfigDir, `wordpress-remote-${MCP_WORDPRESS_REMOTE_VERSION}`);
35782
35884
  }
35783
35885
  async function ensureConfigDir() {
35784
35886
  try {
@@ -36000,7 +36102,7 @@ function calculateDefaultPort(serverUrlHash) {
36000
36102
  }
36001
36103
  async function findExistingClientPort(serverUrlHash) {
36002
36104
  const clientInfo = await readJsonFile(serverUrlHash, "client_info.json");
36003
- if (!clientInfo) {
36105
+ if (!clientInfo || !clientInfo.redirect_uris) {
36004
36106
  return void 0;
36005
36107
  }
36006
36108
  const localhostRedirectUri = clientInfo.redirect_uris.map((uri) => {
@@ -36036,7 +36138,7 @@ async function selectCallbackPort(serverUrl, specifiedPort, _unused) {
36036
36138
  }
36037
36139
 
36038
36140
  // src/lib/config.ts
36039
- var MCP_WORDPRESS_REMOTE_VERSION = "0.2.19";
36141
+ var MCP_WORDPRESS_REMOTE_VERSION = "0.3.0";
36040
36142
  var CONFIG = {
36041
36143
  // API Configuration
36042
36144
  WP_API_URL: process.env.WP_API_URL || "https://example.com",
@@ -37514,17 +37616,14 @@ var OAuthCallbackServer = class {
37514
37616
  });
37515
37617
  }
37516
37618
  async stop() {
37517
- return new Promise((resolve) => {
37518
- if (this.server) {
37519
- this.server.close(() => {
37520
- logger.oauth("OAuth callback server stopped");
37521
- this.server = null;
37522
- resolve();
37523
- });
37524
- } else {
37525
- resolve();
37526
- }
37619
+ if (!this.server) return;
37620
+ const server = this.server;
37621
+ this.server = null;
37622
+ server.close((err) => {
37623
+ if (err) logger.error("OAuth callback server close error", "OAUTH", err);
37624
+ else logger.oauth("OAuth callback server stopped");
37527
37625
  });
37626
+ server.closeAllConnections?.();
37528
37627
  }
37529
37628
  getCallbackUrl() {
37530
37629
  return `http://${this.options.host}:${this.options.port}/oauth/callback`;
@@ -38529,6 +38628,11 @@ var mcpOAuthProvider = null;
38529
38628
  var authCoordinator = null;
38530
38629
  var globalEvents = null;
38531
38630
  var globalSessionId = null;
38631
+ var lastInitializeRequest = null;
38632
+ var sessionRefreshPromise = null;
38633
+ var WP_MCP_ENDPOINT = "/wp/v2/wpmcp";
38634
+ var INVALID_SESSION_ERROR_CODE = -32602;
38635
+ var INVALID_SESSION_ERROR_MESSAGE = "Invalid or expired session";
38532
38636
  function validateEnvironment() {
38533
38637
  const validation = validateConfig();
38534
38638
  if (!validation.isValid) {
@@ -38541,6 +38645,24 @@ function validateEnvironment() {
38541
38645
  function removeTrailingSlash(url) {
38542
38646
  return url.replace(/\/+$/, "");
38543
38647
  }
38648
+ function parseSSEMessage(text) {
38649
+ let result;
38650
+ let found = false;
38651
+ const parser = createParser({
38652
+ onEvent(event) {
38653
+ if (found) return;
38654
+ if (!event.event || event.event === "message") {
38655
+ result = JSON.parse(event.data);
38656
+ found = true;
38657
+ }
38658
+ }
38659
+ });
38660
+ parser.feed(text);
38661
+ if (!found) {
38662
+ throw new Error('No "message" event with data found in SSE response');
38663
+ }
38664
+ return result;
38665
+ }
38544
38666
  function constructApiUrl(baseUrl, defaultEndpoint) {
38545
38667
  const cleanUrl = removeTrailingSlash(baseUrl);
38546
38668
  try {
@@ -38630,9 +38752,50 @@ async function getOAuthTokens() {
38630
38752
  function getSessionId() {
38631
38753
  return globalSessionId;
38632
38754
  }
38633
- async function wpRequest(requestData, useJsonRpc = true) {
38634
- validateEnvironment();
38635
- const endpoint = "/wp/v2/wpmcp";
38755
+ function getCurrentApiUrl() {
38756
+ return process.env.WP_API_URL || CONFIG.WP_API_URL;
38757
+ }
38758
+ function getRequestUrl() {
38759
+ return constructApiUrl(getCurrentApiUrl(), WP_MCP_ENDPOINT);
38760
+ }
38761
+ function cloneRequestData(requestData) {
38762
+ return JSON.parse(JSON.stringify(requestData));
38763
+ }
38764
+ function isInitializeRequest(requestData) {
38765
+ return requestData?.method === "initialize";
38766
+ }
38767
+ function cacheInitializeRequest(requestData, useJsonRpc) {
38768
+ lastInitializeRequest = {
38769
+ requestData: cloneRequestData(requestData),
38770
+ useJsonRpc
38771
+ };
38772
+ }
38773
+ function parseApiErrorResponse(error) {
38774
+ if (!error.response) {
38775
+ return null;
38776
+ }
38777
+ if (typeof error.response === "string") {
38778
+ try {
38779
+ return JSON.parse(error.response);
38780
+ } catch {
38781
+ return null;
38782
+ }
38783
+ }
38784
+ return error.response;
38785
+ }
38786
+ function isInvalidSessionError(error) {
38787
+ const errorResponse = parseApiErrorResponse(error);
38788
+ return errorResponse?.code === INVALID_SESSION_ERROR_CODE && errorResponse?.message === INVALID_SESSION_ERROR_MESSAGE;
38789
+ }
38790
+ function updateSessionId(sessionId) {
38791
+ if (globalSessionId === sessionId) {
38792
+ return;
38793
+ }
38794
+ globalSessionId = sessionId;
38795
+ logger.info(`Session ID received from WordPress: ${globalSessionId}`, "SESSION");
38796
+ }
38797
+ async function executeWordPressRequest(requestData, useJsonRpc, sessionIdUsed) {
38798
+ const url = getRequestUrl();
38636
38799
  const method = "POST";
38637
38800
  if (useJsonRpc) {
38638
38801
  logger.api(`Request method: ${requestData.method || "unknown"} (JSON-RPC)`);
@@ -38691,13 +38854,12 @@ async function wpRequest(requestData, useJsonRpc = true) {
38691
38854
  "NO_AUTH_METHOD"
38692
38855
  );
38693
38856
  }
38694
- const currentApiUrl = process.env.WP_API_URL || CONFIG.WP_API_URL;
38695
38857
  logger.debug(`Environment: ${CONFIG.NODE_ENV}`, "API");
38696
- logger.debug(`Base API URL: ${currentApiUrl}`, "API");
38697
- const url = constructApiUrl(currentApiUrl, endpoint);
38858
+ logger.debug(`Base API URL: ${getCurrentApiUrl()}`, "API");
38698
38859
  logger.debug(`Final requesting URL: ${url}`, "API");
38699
38860
  const headers = {
38700
38861
  "Content-Type": "application/json",
38862
+ Accept: "application/json, text/event-stream",
38701
38863
  "MCP-Protocol-Version": "2025-06-18",
38702
38864
  // MCP protocol version
38703
38865
  ...customHeaders
@@ -38706,8 +38868,8 @@ async function wpRequest(requestData, useJsonRpc = true) {
38706
38868
  if (authHeader) {
38707
38869
  headers.Authorization = authHeader;
38708
38870
  }
38709
- if (globalSessionId) {
38710
- headers["Mcp-Session-Id"] = globalSessionId;
38871
+ if (sessionIdUsed) {
38872
+ headers["Mcp-Session-Id"] = sessionIdUsed;
38711
38873
  }
38712
38874
  if (authHeader) {
38713
38875
  logger.debug("Using Authorization header for authentication", "API");
@@ -38731,21 +38893,27 @@ async function wpRequest(requestData, useJsonRpc = true) {
38731
38893
  logger.debug(`Request method: ${method}`, "API");
38732
38894
  const response = await proxyFetch(url, fetchOptions);
38733
38895
  logger.debug(`Response status: ${response.status}`, "API");
38896
+ const rawBody = await response.text();
38734
38897
  if (!response.ok) {
38735
- const errorText = await response.text();
38736
- logger.error(`API error response: ${errorText}`, "API");
38898
+ logger.error(`API error response: ${rawBody}`, "API");
38737
38899
  throw new APIError(
38738
- `WordPress API error (${response.status}): ${errorText}`,
38900
+ `WordPress API error (${response.status}): ${rawBody}`,
38739
38901
  response.status,
38740
38902
  url,
38741
- errorText
38903
+ rawBody
38742
38904
  );
38743
38905
  }
38744
- const responseData = await response.json();
38906
+ const contentType = response.headers.get("content-type") ?? "";
38907
+ let responseData;
38908
+ if (contentType.includes("text/event-stream")) {
38909
+ responseData = parseSSEMessage(rawBody);
38910
+ logger.debug("Parsed text/event-stream response body", "API");
38911
+ } else {
38912
+ responseData = JSON.parse(rawBody);
38913
+ }
38745
38914
  const sessionIdHeader = response.headers.get("Mcp-Session-Id");
38746
- if (sessionIdHeader && !globalSessionId) {
38747
- globalSessionId = sessionIdHeader;
38748
- logger.info(`Session ID received from WordPress: ${globalSessionId}`, "SESSION");
38915
+ if (sessionIdHeader) {
38916
+ updateSessionId(sessionIdHeader);
38749
38917
  }
38750
38918
  logger.api("Response received successfully");
38751
38919
  logger.debug(`Response data: ${JSON.stringify(responseData)}`, "API");
@@ -38758,14 +38926,20 @@ async function wpRequest(requestData, useJsonRpc = true) {
38758
38926
  `WordPress JSON-RPC error: ${jsonrpcResponse.error.message}`,
38759
38927
  jsonrpcResponse.error.code || 500,
38760
38928
  url,
38761
- JSON.stringify(jsonrpcResponse.error)
38929
+ jsonrpcResponse.error
38762
38930
  );
38763
38931
  } else if (jsonrpcResponse.result !== void 0) {
38764
- return jsonrpcResponse.result;
38932
+ return {
38933
+ responseData: jsonrpcResponse.result,
38934
+ sessionIdUsed
38935
+ };
38765
38936
  }
38766
38937
  }
38767
38938
  }
38768
- return responseData;
38939
+ return {
38940
+ responseData,
38941
+ sessionIdUsed
38942
+ };
38769
38943
  } catch (error) {
38770
38944
  if (error instanceof APIError) {
38771
38945
  throw error;
@@ -38775,6 +38949,77 @@ async function wpRequest(requestData, useJsonRpc = true) {
38775
38949
  throw new APIError(errorMessage, 0, url);
38776
38950
  }
38777
38951
  }
38952
+ async function refreshSession(failedSessionId) {
38953
+ if (failedSessionId && globalSessionId && globalSessionId !== failedSessionId) {
38954
+ logger.info("Detected newer session while handling invalid-session error; skipping refresh", "SESSION");
38955
+ return;
38956
+ }
38957
+ if (sessionRefreshPromise) {
38958
+ logger.info("Waiting for in-flight WordPress session refresh", "SESSION");
38959
+ await sessionRefreshPromise;
38960
+ return;
38961
+ }
38962
+ if (!lastInitializeRequest) {
38963
+ throw new APIError(
38964
+ "Cannot refresh WordPress session before initialize has completed",
38965
+ 0,
38966
+ getRequestUrl()
38967
+ );
38968
+ }
38969
+ sessionRefreshPromise = (async () => {
38970
+ logger.warn("WordPress session rejected; refreshing session via initialize", "SESSION");
38971
+ globalSessionId = null;
38972
+ await executeWordPressRequest(
38973
+ lastInitializeRequest.requestData,
38974
+ lastInitializeRequest.useJsonRpc,
38975
+ null
38976
+ );
38977
+ if (!globalSessionId) {
38978
+ throw new APIError(
38979
+ "WordPress initialize did not return a session ID during refresh",
38980
+ 0,
38981
+ getRequestUrl()
38982
+ );
38983
+ }
38984
+ })();
38985
+ try {
38986
+ await sessionRefreshPromise;
38987
+ } finally {
38988
+ sessionRefreshPromise = null;
38989
+ }
38990
+ }
38991
+ async function wpRequest(requestData, useJsonRpc = true, options = {}) {
38992
+ validateEnvironment();
38993
+ const allowSessionRecovery = options.allowSessionRecovery !== false;
38994
+ if (isInitializeRequest(requestData)) {
38995
+ cacheInitializeRequest(requestData, useJsonRpc);
38996
+ }
38997
+ const sessionIdUsed = globalSessionId;
38998
+ try {
38999
+ const result = await executeWordPressRequest(requestData, useJsonRpc, sessionIdUsed);
39000
+ return result.responseData;
39001
+ } catch (error) {
39002
+ if (error instanceof APIError && allowSessionRecovery && !isInitializeRequest(requestData) && isInvalidSessionError(error)) {
39003
+ logger.warn("WordPress session expired; attempting one-time recovery", "SESSION", {
39004
+ method: requestData?.method || "unknown",
39005
+ sessionIdUsed: sessionIdUsed || "none"
39006
+ });
39007
+ await refreshSession(sessionIdUsed);
39008
+ const retriedResult = await executeWordPressRequest(
39009
+ requestData,
39010
+ useJsonRpc,
39011
+ globalSessionId
39012
+ );
39013
+ return retriedResult.responseData;
39014
+ }
39015
+ if (error instanceof APIError) {
39016
+ throw error;
39017
+ }
39018
+ const errorMessage = error instanceof Error ? error.message : String(error);
39019
+ logger.error(`Error in wpRequest: ${errorMessage}`, "API");
39020
+ throw new APIError(errorMessage, 0, getRequestUrl());
39021
+ }
39022
+ }
38778
39023
 
38779
39024
  // src/lib/session-utils.ts
38780
39025
  function addSessionInfo(wpRequestParams, mcpRequest, context) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/mcp-wordpress-remote",
3
- "version": "0.2.21",
3
+ "version": "0.3.0",
4
4
  "description": "MCP WordPress Remote proxy server",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -34,7 +34,7 @@
34
34
  }
35
35
  },
36
36
  "bin": {
37
- "@automattic/mcp-wordpress-remote": "dist/proxy.js"
37
+ "mcp-wordpress-remote": "./dist/proxy.js"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "tsup",
@@ -60,6 +60,7 @@
60
60
  "dependencies": {
61
61
  "@modelcontextprotocol/sdk": "^1.8.0",
62
62
  "@tootallnate/quickjs-emscripten": "^0.23.0",
63
+ "eventsource-parser": "^3.0.1",
63
64
  "express": "^4.18.2",
64
65
  "https-proxy-agent": "^7.0.6",
65
66
  "node-fetch": "^3.3.2",
@@ -100,6 +101,7 @@
100
101
  "splitting": false,
101
102
  "noExternal": [
102
103
  "@modelcontextprotocol/sdk",
104
+ "eventsource-parser",
103
105
  "express",
104
106
  "node-fetch",
105
107
  "open"