mcp-wordpress 2.11.4 → 2.11.6

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.
Files changed (32) hide show
  1. package/dist/cache/HttpCacheWrapper.d.ts +1 -2
  2. package/dist/cache/HttpCacheWrapper.d.ts.map +1 -1
  3. package/dist/cache/HttpCacheWrapper.js +2 -7
  4. package/dist/cache/HttpCacheWrapper.js.map +1 -1
  5. package/dist/client/CachedWordPressClient.d.ts +2 -0
  6. package/dist/client/CachedWordPressClient.d.ts.map +1 -1
  7. package/dist/client/CachedWordPressClient.js +68 -13
  8. package/dist/client/CachedWordPressClient.js.map +1 -1
  9. package/dist/client/api.d.ts +7 -0
  10. package/dist/client/api.d.ts.map +1 -1
  11. package/dist/client/api.js +204 -159
  12. package/dist/client/api.js.map +1 -1
  13. package/dist/client/managers/AuthenticationManager.d.ts +1 -7
  14. package/dist/client/managers/AuthenticationManager.d.ts.map +1 -1
  15. package/dist/client/managers/AuthenticationManager.js +55 -17
  16. package/dist/client/managers/AuthenticationManager.js.map +1 -1
  17. package/dist/config/ConfigurationSchema.d.ts +75 -198
  18. package/dist/config/ConfigurationSchema.d.ts.map +1 -1
  19. package/dist/security/InputValidator.d.ts +48 -124
  20. package/dist/security/InputValidator.d.ts.map +1 -1
  21. package/dist/types/seo.d.ts +76 -240
  22. package/dist/types/seo.d.ts.map +1 -1
  23. package/dist/utils/version.d.ts +1 -0
  24. package/dist/utils/version.d.ts.map +1 -1
  25. package/dist/utils/version.js +9 -10
  26. package/dist/utils/version.js.map +1 -1
  27. package/package.json +7 -7
  28. package/src/cache/HttpCacheWrapper.ts +3 -16
  29. package/src/client/CachedWordPressClient.ts +83 -13
  30. package/src/client/api.ts +268 -178
  31. package/src/client/managers/AuthenticationManager.ts +63 -21
  32. package/src/utils/version.ts +10 -10
package/src/client/api.ts CHANGED
@@ -530,219 +530,309 @@ export class WordPressClient implements IWordPressClient {
530
530
  const timer = startTimer();
531
531
  this._stats.totalRequests++;
532
532
 
533
- // Handle endpoint properly - remove leading slash if present to avoid double slashes
534
533
  const cleanEndpoint = endpoint.replace(/^\/+/, "");
535
534
  const url = endpoint.startsWith("http") ? endpoint : `${this.apiUrl}/${cleanEndpoint}`;
536
535
 
537
- const headers: Record<string, string> = {
536
+ const { headers: _ignoredHeaders, retries: retryOverride, params: _ignoredParams, ...restOptions } = options;
537
+ const baseHeaders: Record<string, string> = {
538
538
  "Content-Type": "application/json",
539
539
  "User-Agent": getUserAgent(),
540
- ...options.headers,
540
+ ...(_ignoredHeaders || {}),
541
541
  };
542
542
 
543
- // Add authentication headers
544
- this.addAuthHeaders(headers);
543
+ this.addAuthHeaders(baseHeaders);
545
544
 
546
- // Set up timeout using AbortController - use options timeout if provided
547
- const controller = new AbortController();
548
545
  const requestTimeout = options.timeout || this.timeout;
549
- const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
546
+ const configuredRetries =
547
+ typeof retryOverride === "number" && retryOverride > 0 ? retryOverride : this.maxRetries || 1;
548
+ const canRetryBody = this.isRetryableBody(data);
549
+ const maxAttempts = canRetryBody ? configuredRetries : 1;
550
550
 
551
- const fetchOptions: RequestInit & { headers: Record<string, string> } = {
552
- ...options, // Spread options first
553
- method,
554
- headers, // Headers come after to ensure auth headers aren't overridden
555
- signal: controller.signal,
556
- };
551
+ let lastError: Error = new Error("Unknown error");
557
552
 
558
- // Add body for POST/PUT/PATCH requests
559
- if (data && ["POST", "PUT", "PATCH"].includes(method)) {
560
- if (
561
- data instanceof FormData ||
562
- (typeof data === "object" && data && "append" in data && typeof data.append === "function")
563
- ) {
564
- // For FormData, check if it has getHeaders method (form-data package)
565
- if (typeof (data as { getHeaders?: () => Record<string, string> }).getHeaders === "function") {
566
- // Use headers from form-data package
567
- const formHeaders = (data as unknown as { getHeaders(): Record<string, string> }).getHeaders();
568
- Object.assign(headers, formHeaders);
569
- } else {
570
- // For native FormData, don't set Content-Type (let fetch set it with boundary)
571
- delete headers["Content-Type"];
572
- }
573
- fetchOptions.body = data as FormData;
574
- } else if (Buffer.isBuffer(data)) {
575
- // For Buffer data (manual multipart), keep Content-Type from headers
576
- fetchOptions.body = data;
577
- } else if (typeof data === "string") {
578
- fetchOptions.body = data;
579
- } else {
580
- fetchOptions.body = JSON.stringify(data);
581
- }
582
- }
553
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
554
+ await this.rateLimit();
583
555
 
584
- // Rate limiting
585
- await this.rateLimit();
556
+ const controller = new AbortController();
557
+ const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
558
+
559
+ const headers = { ...baseHeaders };
560
+ const fetchOptions: RequestInit & { headers: Record<string, string> } = {
561
+ ...restOptions,
562
+ method,
563
+ headers,
564
+ signal: controller.signal,
565
+ };
566
+
567
+ if (data && ["POST", "PUT", "PATCH"].includes(method)) {
568
+ this.attachRequestBody(fetchOptions, headers, data);
569
+ }
586
570
 
587
- let lastError: Error = new Error("Unknown error");
588
- for (let attempt = 0; attempt < this.maxRetries; attempt++) {
589
571
  try {
590
572
  debug.log(`API Request: ${method} ${url}${attempt > 0 ? ` (attempt ${attempt + 1})` : ""}`);
591
573
 
592
574
  const response = await fetch(url, fetchOptions);
593
- clearTimeout(timeoutId);
594
575
 
595
- // Handle different response types
596
576
  if (!response.ok) {
597
- const errorText = await response.text();
598
- let errorMessage: string;
599
-
600
- try {
601
- const errorData = JSON.parse(errorText);
602
- errorMessage = errorData.message || errorData.error || `HTTP ${response.status}`;
603
- } catch {
604
- errorMessage = errorText || `HTTP ${response.status}: ${response.statusText}`;
605
- }
606
-
607
- // Handle rate limiting
608
- if (response.status === 429) {
609
- this._stats.rateLimitHits++;
610
- throw new RateLimitError(errorMessage, Date.now() + 60000);
611
- }
612
-
613
- // Handle permission errors specifically for uploads
614
- if (response.status === 403 && endpoint.includes("media") && method === "POST") {
615
- throw new AuthenticationError(
616
- "Media upload blocked: WordPress REST API media uploads appear to be disabled or restricted by a plugin/security policy. " +
617
- `Error: ${errorMessage}. ` +
618
- "Common causes: W3 Total Cache, security plugins, or custom REST API restrictions. " +
619
- "Please check WordPress admin settings or contact your system administrator.",
620
- this.auth.method,
621
- );
622
- }
623
-
624
- // Handle general upload permission errors
625
- if (errorMessage.includes("Beiträge zu erstellen") && endpoint.includes("media")) {
626
- throw new AuthenticationError(
627
- `WordPress REST API media upload restriction detected: ${errorMessage}. ` +
628
- "This typically indicates that media uploads via REST API are disabled by WordPress configuration, " +
629
- "a security plugin (like W3 Total Cache, Borlabs Cookie), or server policy. " +
630
- "User has sufficient permissions but WordPress/plugins are blocking the upload.",
631
- this.auth.method,
632
- );
633
- }
634
-
635
- // Fallback for 404 errors - try index.php approach for REST API
636
- if (response.status === 404 && attempt === 0 && url.includes("/wp-json/wp/v2")) {
637
- debug.log(`404 on pretty permalinks, trying index.php approach`);
638
-
639
- // Parse the URL to handle query parameters correctly
640
- const urlObj = new URL(url);
641
- const endpoint = urlObj.pathname.replace("/wp-json/wp/v2", "");
642
- const queryParams = urlObj.searchParams.toString();
643
-
644
- let fallbackUrl = `${urlObj.origin}/index.php?rest_route=/wp/v2${endpoint}`;
645
- if (queryParams) {
646
- fallbackUrl += `&${queryParams}`;
647
- }
648
-
649
- try {
650
- // Create a new timeout for the fallback request
651
- const fallbackController = new AbortController();
652
- const fallbackTimeoutId = setTimeout(() => {
653
- fallbackController.abort();
654
- }, requestTimeout);
655
-
656
- const fallbackOptions = { ...fetchOptions, signal: fallbackController.signal };
657
- const fallbackResponse = await fetch(fallbackUrl, fallbackOptions);
658
- clearTimeout(fallbackTimeoutId);
659
-
660
- if (fallbackResponse.ok) {
661
- const responseText = await fallbackResponse.text();
662
- if (!responseText) {
663
- this._stats.successfulRequests++;
664
- const duration = timer.end();
665
- this.updateAverageResponseTime(duration);
666
- return null as T;
667
- }
668
-
669
- const result = JSON.parse(responseText);
670
- this._stats.successfulRequests++;
671
- const duration = timer.end();
672
- this.updateAverageResponseTime(duration);
673
- return result;
674
- } else {
675
- // If fallback also fails, continue with original error
676
- debug.log(`Fallback also failed with status ${fallbackResponse.status}`);
677
- }
678
- } catch (fallbackError) {
679
- debug.log(`Fallback request failed: ${(fallbackError as Error).message}`);
680
- }
577
+ const fallbackResult = await this.handleErrorResponseWithFallback<T>(
578
+ response,
579
+ url,
580
+ endpoint,
581
+ requestTimeout,
582
+ fetchOptions,
583
+ timer,
584
+ );
585
+ if (fallbackResult !== undefined) {
586
+ clearTimeout(timeoutId);
587
+ return fallbackResult;
681
588
  }
682
-
683
- throw new WordPressAPIError(errorMessage, response.status);
589
+ continue;
684
590
  }
685
591
 
686
- // Parse response
687
- const responseText = await response.text();
688
- if (!responseText) {
689
- this._stats.successfulRequests++;
690
- const duration = timer.end();
691
- this.updateAverageResponseTime(duration);
692
- return null as T;
693
- }
694
-
695
- try {
696
- const result = JSON.parse(responseText);
697
- this._stats.successfulRequests++;
698
- const duration = timer.end();
699
- this.updateAverageResponseTime(duration);
700
- return result as T;
701
- } catch (parseError) {
702
- // For authentication requests, malformed JSON should be an error
703
- if (endpoint.includes("users/me") || endpoint.includes("jwt-auth")) {
704
- throw new WordPressAPIError(`Invalid JSON response: ${(parseError as Error).message}`);
705
- }
706
- this._stats.successfulRequests++;
707
- const duration = timer.end();
708
- this.updateAverageResponseTime(duration);
709
- return responseText as T;
710
- }
592
+ const result = await this.parseResponse<T>(response, endpoint, timer);
593
+ clearTimeout(timeoutId);
594
+ return result;
711
595
  } catch (_error) {
712
596
  clearTimeout(timeoutId);
713
- lastError = _error as Error;
714
-
715
- // Handle timeout errors
716
- if ((_error as Error & { name?: string }).name === "AbortError") {
717
- lastError = new Error(`Request timeout after ${requestTimeout}ms`);
718
- }
719
-
720
- // Handle network errors
721
- if (lastError.message.includes("socket hang up") || lastError.message.includes("ECONNRESET")) {
722
- lastError = new Error(`Network connection lost during upload: ${lastError.message}`);
597
+ if (_error instanceof RateLimitError) {
598
+ lastError = _error;
599
+ break;
723
600
  }
724
-
601
+ lastError = this.normalizeRequestError(_error, requestTimeout);
725
602
  debug.log(`Request failed (attempt ${attempt + 1}): ${lastError.message}`);
726
603
 
727
- // Don't retry on authentication errors, timeouts, or critical network errors
728
- if (
729
- lastError.message.includes("401") ||
730
- lastError.message.includes("403") ||
731
- lastError.message.includes("timeout") ||
732
- lastError.message.includes("Network connection lost")
733
- ) {
604
+ const shouldRetry = this.shouldRetryError(lastError) && attempt < maxAttempts - 1;
605
+ if (!shouldRetry) {
734
606
  break;
735
607
  }
736
608
 
737
- if (attempt < this.maxRetries - 1) {
738
- await this.delay(1000 * (attempt + 1)); // Exponential backoff
739
- }
609
+ await this.delay(1000 * (attempt + 1));
610
+ } finally {
611
+ clearTimeout(timeoutId);
740
612
  }
741
613
  }
742
614
 
743
615
  this._stats.failedRequests++;
744
616
  timer.end();
745
- throw new WordPressAPIError(`Request failed after ${this.maxRetries} attempts: ${lastError.message}`);
617
+ throw new WordPressAPIError(
618
+ `Request failed after ${maxAttempts} attempt${maxAttempts === 1 ? "" : "s"}: ${lastError.message}`,
619
+ );
620
+ }
621
+
622
+ private attachRequestBody(
623
+ fetchOptions: RequestInit & { headers: Record<string, string> },
624
+ headers: Record<string, string>,
625
+ data: unknown,
626
+ ): void {
627
+ if (
628
+ data instanceof FormData ||
629
+ (typeof data === "object" && data && "append" in data && typeof (data as FormData).append === "function")
630
+ ) {
631
+ if (typeof (data as { getHeaders?: () => Record<string, string> }).getHeaders === "function") {
632
+ const formHeaders = (data as unknown as { getHeaders(): Record<string, string> }).getHeaders();
633
+ Object.assign(headers, formHeaders);
634
+ } else {
635
+ delete headers["Content-Type"];
636
+ }
637
+ fetchOptions.body = data as FormData;
638
+ return;
639
+ }
640
+
641
+ if (Buffer.isBuffer(data)) {
642
+ fetchOptions.body = data;
643
+ return;
644
+ }
645
+
646
+ if (typeof data === "string") {
647
+ fetchOptions.body = data;
648
+ return;
649
+ }
650
+
651
+ fetchOptions.body = JSON.stringify(data);
652
+ }
653
+
654
+ private normalizeRequestError(error: unknown, timeout: number): Error {
655
+ if (error instanceof Error) {
656
+ if (error.name === "AbortError") {
657
+ return new Error(`Request timeout after ${timeout}ms`);
658
+ }
659
+ if (error.message.includes("socket hang up") || error.message.includes("ECONNRESET")) {
660
+ return new Error(`Network connection lost during request: ${error.message}`);
661
+ }
662
+ return error;
663
+ }
664
+ return new Error(typeof error === "string" ? error : "Unknown error");
665
+ }
666
+
667
+ private shouldRetryError(error: Error): boolean {
668
+ const message = error.message.toLowerCase();
669
+ if (message.includes("401") || message.includes("403")) {
670
+ return false;
671
+ }
672
+ if (message.includes("timeout")) {
673
+ return false;
674
+ }
675
+ if (message.includes("network connection lost")) {
676
+ return false;
677
+ }
678
+ return true;
679
+ }
680
+
681
+ private isRetryableBody(data: unknown): boolean {
682
+ if (!data) {
683
+ return true;
684
+ }
685
+
686
+ if (typeof data === "string" || Buffer.isBuffer(data)) {
687
+ return true;
688
+ }
689
+
690
+ if (data instanceof FormData) {
691
+ return false;
692
+ }
693
+
694
+ if (typeof data === "object" && data !== null && "pipe" in (data as Record<string, unknown>)) {
695
+ const potentialStream = (data as Record<string, unknown>).pipe;
696
+ if (typeof potentialStream === "function") {
697
+ return false;
698
+ }
699
+ }
700
+
701
+ return true;
702
+ }
703
+
704
+ private async handleErrorResponseWithFallback<T>(
705
+ response: Response,
706
+ url: string,
707
+ originalEndpoint: string,
708
+ requestTimeout: number,
709
+ fetchOptions: RequestInit & { headers: Record<string, string> },
710
+ timer: ReturnType<typeof startTimer>,
711
+ ): Promise<T | undefined> {
712
+ const errorText = await response.text();
713
+ let errorMessage: string;
714
+
715
+ try {
716
+ const errorData = JSON.parse(errorText);
717
+ errorMessage = errorData.message || errorData.error || `HTTP ${response.status}`;
718
+ } catch {
719
+ errorMessage = errorText || `HTTP ${response.status}: ${response.statusText}`;
720
+ }
721
+
722
+ if (response.status === 429) {
723
+ this._stats.rateLimitHits++;
724
+ throw new RateLimitError(errorMessage, Date.now() + 60000);
725
+ }
726
+
727
+ if (response.status === 403 && originalEndpoint.includes("media") && fetchOptions.method === "POST") {
728
+ throw new AuthenticationError(
729
+ "Media upload blocked: WordPress REST API media uploads appear to be disabled or restricted by a plugin/security policy. " +
730
+ `Error: ${errorMessage}. ` +
731
+ "Common causes: W3 Total Cache, security plugins, or custom REST API restrictions. " +
732
+ "Please check WordPress admin settings or contact your system administrator.",
733
+ this.auth.method,
734
+ );
735
+ }
736
+
737
+ if (errorMessage.includes("Beiträge zu erstellen") && originalEndpoint.includes("media")) {
738
+ throw new AuthenticationError(
739
+ `WordPress REST API media upload restriction detected: ${errorMessage}. ` +
740
+ "This typically indicates that media uploads via REST API are disabled by WordPress configuration, " +
741
+ "a security plugin (like W3 Total Cache, Borlabs Cookie), or server policy. " +
742
+ "User has sufficient permissions but WordPress/plugins are blocking the upload.",
743
+ this.auth.method,
744
+ );
745
+ }
746
+
747
+ if (response.status === 404 && url.includes("/wp-json/wp/v2")) {
748
+ const fallbackResult = await this.tryIndexPhpFallback<T>(url, requestTimeout, fetchOptions, timer);
749
+ if (fallbackResult !== undefined) {
750
+ return fallbackResult;
751
+ }
752
+ }
753
+
754
+ throw new WordPressAPIError(errorMessage, response.status);
755
+ }
756
+
757
+ private async tryIndexPhpFallback<T>(
758
+ url: string,
759
+ requestTimeout: number,
760
+ fetchOptions: RequestInit & { headers: Record<string, string> },
761
+ timer: ReturnType<typeof startTimer>,
762
+ ): Promise<T | undefined> {
763
+ debug.log(`404 on pretty permalinks, trying index.php approach`);
764
+
765
+ try {
766
+ const urlObj = new URL(url);
767
+ const endpointPath = urlObj.pathname.replace("/wp-json/wp/v2", "");
768
+ const queryParams = urlObj.searchParams.toString();
769
+
770
+ let fallbackUrl = `${urlObj.origin}/index.php?rest_route=/wp/v2${endpointPath}`;
771
+ if (queryParams) {
772
+ fallbackUrl += `&${queryParams}`;
773
+ }
774
+
775
+ const fallbackController = new AbortController();
776
+ const fallbackTimeoutId = setTimeout(() => {
777
+ fallbackController.abort();
778
+ }, requestTimeout);
779
+
780
+ const fallbackOptions = { ...fetchOptions, signal: fallbackController.signal };
781
+ const fallbackResponse = await fetch(fallbackUrl, fallbackOptions);
782
+ clearTimeout(fallbackTimeoutId);
783
+
784
+ if (!fallbackResponse.ok) {
785
+ debug.log(`Fallback also failed with status ${fallbackResponse.status}`);
786
+ return undefined;
787
+ }
788
+
789
+ const responseText = await fallbackResponse.text();
790
+ if (!responseText) {
791
+ this._stats.successfulRequests++;
792
+ const duration = timer.end();
793
+ this.updateAverageResponseTime(duration);
794
+ return null as T;
795
+ }
796
+
797
+ const result = JSON.parse(responseText);
798
+ this._stats.successfulRequests++;
799
+ const duration = timer.end();
800
+ this.updateAverageResponseTime(duration);
801
+ return result as T;
802
+ } catch (fallbackError) {
803
+ debug.log(`Fallback request failed: ${(fallbackError as Error).message}`);
804
+ return undefined;
805
+ }
806
+ }
807
+
808
+ private async parseResponse<T>(
809
+ response: Response,
810
+ endpoint: string,
811
+ timer: ReturnType<typeof startTimer>,
812
+ ): Promise<T> {
813
+ const responseText = await response.text();
814
+ if (!responseText) {
815
+ this._stats.successfulRequests++;
816
+ const duration = timer.end();
817
+ this.updateAverageResponseTime(duration);
818
+ return null as T;
819
+ }
820
+
821
+ try {
822
+ const result = JSON.parse(responseText);
823
+ this._stats.successfulRequests++;
824
+ const duration = timer.end();
825
+ this.updateAverageResponseTime(duration);
826
+ return result as T;
827
+ } catch (parseError) {
828
+ if (endpoint.includes("users/me") || endpoint.includes("jwt-auth")) {
829
+ throw new WordPressAPIError(`Invalid JSON response: ${(parseError as Error).message}`);
830
+ }
831
+ this._stats.successfulRequests++;
832
+ const duration = timer.end();
833
+ this.updateAverageResponseTime(duration);
834
+ return responseText as T;
835
+ }
746
836
  }
747
837
 
748
838
  private updateAverageResponseTime(duration: number): void {
@@ -183,29 +183,56 @@ export class AuthenticationManager extends BaseManager {
183
183
  }
184
184
 
185
185
  /**
186
- * Authenticate using JWT
187
- *
188
- * Note: This method is not fully implemented as it requires integration with
189
- * the RequestManager to make HTTP requests to the WordPress JWT endpoint.
190
- * JWT authentication requires the JWT Authentication for WP-API plugin.
191
- *
192
- * @throws {AuthenticationError} - JWT auth requires external dependency injection
186
+ * Authenticate using the WordPress JWT REST endpoint
193
187
  */
194
188
  private async authenticateJWT(): Promise<void> {
195
- if (this.authConfig.authMethod !== AUTH_METHODS.JWT || !this.authConfig.username || !this.authConfig.password) {
189
+ if (this.authConfig.authMethod !== AUTH_METHODS.JWT) {
190
+ throw new AuthenticationError("JWT authentication requires JWT auth method", AUTH_METHODS.JWT);
191
+ }
192
+
193
+ const username = this.authConfig.username;
194
+ const password = this.authConfig.password;
195
+
196
+ if (!username || !password) {
196
197
  throw new AuthenticationError("JWT authentication requires username and password", AUTH_METHODS.JWT);
197
198
  }
198
199
 
199
200
  try {
200
- // TODO: Implement JWT authentication with RequestManager integration
201
- // This would require making a POST request to /wp-json/jwt-auth/v1/token
202
- // with username and password to obtain a JWT token
201
+ const response = await fetch(`${this.authConfig.siteUrl}/wp-json/jwt-auth/v1/token`, {
202
+ method: "POST",
203
+ headers: {
204
+ "Content-Type": "application/json",
205
+ },
206
+ body: JSON.stringify({
207
+ username,
208
+ password,
209
+ }),
210
+ });
211
+
212
+ if (!response.ok) {
213
+ throw new AuthenticationError(`JWT authentication failed: ${response.statusText}`, AUTH_METHODS.JWT);
214
+ }
215
+
216
+ const data = (await response.json()) as { token?: string; expires_in?: number };
217
+
218
+ if (!data.token) {
219
+ throw new AuthenticationError("JWT authentication failed: token missing in response", AUTH_METHODS.JWT);
220
+ }
221
+
222
+ this.jwtToken = data.token;
223
+ this.authConfig.jwtToken = data.token;
224
+ const expiresIn = data.expires_in || 86400; // Default 24h
225
+ this.authConfig.tokenExpiry = Date.now() + expiresIn * 1000;
226
+ this.authenticated = true;
227
+ debug.log("JWT authentication successful", {
228
+ expiresIn,
229
+ expiresAt: new Date(this.authConfig.tokenExpiry).toISOString(),
230
+ });
231
+ } catch (_error) {
203
232
  throw new AuthenticationError(
204
- "JWT authentication requires RequestManager integration - not yet implemented",
233
+ `JWT authentication failed: ${_error instanceof Error ? _error.message : String(_error)}`,
205
234
  AUTH_METHODS.JWT,
206
235
  );
207
- } catch (_error) {
208
- this.handleError(_error, "JWT authentication");
209
236
  }
210
237
  }
211
238
 
@@ -422,13 +449,28 @@ export class AuthenticationManager extends BaseManager {
422
449
  }
423
450
  }
424
451
 
425
- // TODO: Implement JWT token refresh with RequestManager integration
426
- // This would require making a POST request to /wp-json/jwt-auth/v1/token/validate
427
- // and updating the stored token and expiry
428
- throw new AuthenticationError(
429
- "JWT refresh requires RequestManager integration - not yet implemented",
430
- AUTH_METHODS.JWT,
431
- );
452
+ // Attempt to validate existing token before re-authenticating
453
+ if (this.authConfig.jwtToken) {
454
+ try {
455
+ const validateResponse = await fetch(`${this.authConfig.siteUrl}/wp-json/jwt-auth/v1/token/validate`, {
456
+ method: "POST",
457
+ headers: {
458
+ "Content-Type": "application/json",
459
+ Authorization: `Bearer ${this.authConfig.jwtToken}`,
460
+ },
461
+ });
462
+
463
+ if (validateResponse.ok) {
464
+ this.authConfig.tokenExpiry = Date.now() + 3600 * 1000; // Extend validity for another hour
465
+ debug.log("JWT token validated successfully");
466
+ return;
467
+ }
468
+ } catch (error) {
469
+ debug.log("JWT token validation failed, re-authenticating", { error: String(error) });
470
+ }
471
+ }
472
+
473
+ await this.authenticateJWT();
432
474
  }
433
475
 
434
476
  /**
@@ -104,11 +104,7 @@ export class VersionManager {
104
104
  getPackageInfo(): PackageJson {
105
105
  if (!this.packageJson) {
106
106
  // Return fallback package info if not loaded
107
- this.packageJson = {
108
- name: "mcp-wordpress",
109
- version: "2.7.0", // Fallback version
110
- description: "MCP WordPress Server",
111
- };
107
+ this.packageJson = this.getFallbackPackageInfo();
112
108
  }
113
109
  return this.packageJson;
114
110
  }
@@ -159,11 +155,7 @@ export class VersionManager {
159
155
  } catch (_error) {
160
156
  // Fallback for runtime environments where package.json might not be available
161
157
  // Note: Using fallback version - should match package.json
162
- return {
163
- name: "mcp-wordpress",
164
- version: "2.7.0", // Fallback version - should match package.json
165
- description: "MCP WordPress Server",
166
- };
158
+ return this.getFallbackPackageInfo();
167
159
  }
168
160
  }
169
161
 
@@ -353,6 +345,14 @@ export class VersionManager {
353
345
  const color = info.prerelease ? "orange" : "blue";
354
346
  return `https://img.shields.io/badge/version-${info.version}-${color}`;
355
347
  }
348
+
349
+ private getFallbackPackageInfo(): PackageJson {
350
+ return {
351
+ name: "mcp-wordpress",
352
+ version: process.env.npm_package_version || "2.11.3",
353
+ description: "MCP WordPress Server",
354
+ };
355
+ }
356
356
  }
357
357
 
358
358
  /**