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.
- package/dist/cache/HttpCacheWrapper.d.ts +1 -2
- package/dist/cache/HttpCacheWrapper.d.ts.map +1 -1
- package/dist/cache/HttpCacheWrapper.js +2 -7
- package/dist/cache/HttpCacheWrapper.js.map +1 -1
- package/dist/client/CachedWordPressClient.d.ts +2 -0
- package/dist/client/CachedWordPressClient.d.ts.map +1 -1
- package/dist/client/CachedWordPressClient.js +68 -13
- package/dist/client/CachedWordPressClient.js.map +1 -1
- package/dist/client/api.d.ts +7 -0
- package/dist/client/api.d.ts.map +1 -1
- package/dist/client/api.js +204 -159
- package/dist/client/api.js.map +1 -1
- package/dist/client/managers/AuthenticationManager.d.ts +1 -7
- package/dist/client/managers/AuthenticationManager.d.ts.map +1 -1
- package/dist/client/managers/AuthenticationManager.js +55 -17
- package/dist/client/managers/AuthenticationManager.js.map +1 -1
- package/dist/config/ConfigurationSchema.d.ts +75 -198
- package/dist/config/ConfigurationSchema.d.ts.map +1 -1
- package/dist/security/InputValidator.d.ts +48 -124
- package/dist/security/InputValidator.d.ts.map +1 -1
- package/dist/types/seo.d.ts +76 -240
- package/dist/types/seo.d.ts.map +1 -1
- package/dist/utils/version.d.ts +1 -0
- package/dist/utils/version.d.ts.map +1 -1
- package/dist/utils/version.js +9 -10
- package/dist/utils/version.js.map +1 -1
- package/package.json +7 -7
- package/src/cache/HttpCacheWrapper.ts +3 -16
- package/src/client/CachedWordPressClient.ts +83 -13
- package/src/client/api.ts +268 -178
- package/src/client/managers/AuthenticationManager.ts +63 -21
- 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:
|
|
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
|
-
...
|
|
540
|
+
...(_ignoredHeaders || {}),
|
|
541
541
|
};
|
|
542
542
|
|
|
543
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
559
|
-
|
|
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
|
-
|
|
585
|
-
|
|
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
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
-
|
|
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
|
-
|
|
738
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
/**
|
package/src/utils/version.ts
CHANGED
|
@@ -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
|
/**
|