Skip to main content
Glama
wordpress-api.ts14.1 kB
/** * External dependencies */ import * as path from 'node:path'; import { EventEmitter } from 'node:events'; import { WordPressRequestParams, WordPressResponse } from './types.js'; import { logger, LogLevel } from './utils.js'; import { CONFIG, validateConfig, getDefaultOAuthScopes, getCustomHeaders } from './config.js'; import { WPTokens, AuthError, APIError } from './oauth-types.js'; import { getValidTokens, generateServerUrlHash, cleanupExpiredTokens, } from './persistent-auth-config.js'; import { PersistentWPOAuthClientProvider } from './persistent-oauth-client-provider.js'; import { MCPOAuthProvider } from './mcp-oauth-provider.js'; import { createLazyWPAuthCoordinator } from './coordination.js'; /** * WordPress API request function with OAuth, JWT, and Basic Auth support * * @param {Object} params - Query parameters for the request * @return {Promise<any>} API response as JSON */ // Global OAuth provider for WordPress API access let legacyOAuthProvider: PersistentWPOAuthClientProvider | null = null; let mcpOAuthProvider: MCPOAuthProvider | null = null; let authCoordinator: any = null; let globalEvents: EventEmitter | null = null; // Global session ID received from WordPress server let globalSessionId: string | null = null; function validateEnvironment() { const validation = validateConfig(); if (!validation.isValid) { throw new AuthError( `Configuration validation failed: ${validation.errors.join(', ')}`, 'CONFIG_VALIDATION' ); } } function removeTrailingSlash(url: string): string { return url.replace(/\/+$/, ''); } /** * Determines if a URL has a custom path (beyond just domain) and constructs the final API URL * - If URL has no path (e.g., http://example.com or http://example.com/), use default REST route format * - If URL has a path (e.g., http://example.com/api/mcp), use the URL exactly as provided */ function constructApiUrl(baseUrl: string, defaultEndpoint: string): string { const cleanUrl = removeTrailingSlash(baseUrl); try { const urlObj = new URL(cleanUrl); const hasCustomPath = urlObj.pathname && urlObj.pathname !== '/' && urlObj.pathname.length > 0; const hasCustomQuery = urlObj.search && urlObj.search.length > 0; if (hasCustomPath || hasCustomQuery) { // URL has a custom path or query strings - use it exactly as provided return cleanUrl; } else { // Standard WordPress installation - use REST route format with default endpoint return new URL(`/?rest_route=${defaultEndpoint}`, cleanUrl).toString(); } } catch (error) { // Fallback to original behavior if URL parsing fails return new URL(`/?rest_route=${defaultEndpoint}`, cleanUrl).toString(); } } /** * Get OAuth tokens for WordPress API access using MCP-compliant OAuth 2.1 */ async function getOAuthTokens(): Promise<WPTokens | null> { try { // Check if OAuth is enabled if (!CONFIG.OAUTH_ENABLED) { logger.debug('OAuth is disabled via configuration', 'AUTH'); return null; } logger.auth('Attempting to get OAuth tokens for WordPress API (MCP-compliant)...'); const serverUrl = CONFIG.WP_API_URL; const serverUrlHash = generateServerUrlHash(serverUrl); // Try to get existing valid tokens first const existingTokens = await getValidTokens(serverUrlHash); if (existingTokens) { logger.auth('Using existing valid tokens from persistent storage'); return existingTokens; } logger.auth('No existing valid tokens found in persistent storage'); logger.auth('Starting MCP-compliant OAuth 2.1 authentication flow'); logger.auth('Your browser should open automatically for authentication'); // Use MCP OAuth 2.1 provider for all sites if (CONFIG.OAUTH_FLOW_TYPE === 'authorization_code' && CONFIG.OAUTH_USE_PKCE) { // Use MCP-compliant OAuth 2.1 provider if (!mcpOAuthProvider) { mcpOAuthProvider = new MCPOAuthProvider({ serverUrl, clientId: CONFIG.WP_OAUTH_CLIENT_ID, scopes: getDefaultOAuthScopes(), }); } logger.auth('Using MCP-compliant OAuth 2.1 authorization code flow with PKCE'); await mcpOAuthProvider.authorize(); const tokens = await mcpOAuthProvider.tokens(); if (tokens) { logger.auth('MCP OAuth 2.1 tokens obtained for WordPress API access'); return tokens; } else { logger.warn('No tokens available after MCP OAuth 2.1 authentication', 'AUTH'); return null; } } else { // Use legacy OAuth provider logger.warn( 'Using legacy OAuth provider. Consider enabling PKCE for MCP compliance', 'AUTH' ); // Initialize coordinator for legacy flow if (!authCoordinator) { if (!globalEvents) { globalEvents = new EventEmitter(); } authCoordinator = createLazyWPAuthCoordinator( serverUrlHash, serverUrl, CONFIG.OAUTH_CALLBACK_PORT || 7665, globalEvents ); } logger.auth('Starting legacy authentication via coordinator...'); try { const tokens = await authCoordinator.waitForAuth(); if (tokens) { logger.auth('Legacy tokens obtained for WordPress API access'); return tokens; } else { logger.warn('No tokens available after legacy authentication', 'AUTH'); return null; } } catch (authError) { logger.error('Legacy authentication via coordinator failed', 'AUTH', authError); throw authError; } } } catch (error) { logger.error('Error getting OAuth tokens', 'AUTH', error); return null; } } /** * Get the current session ID */ export function getSessionId(): string | null { return globalSessionId; } export async function wpRequest( requestData: any, useJsonRpc: boolean = true ): Promise<WordPressResponse> { // Validate environment variables first validateEnvironment(); const endpoint = '/wp/v2/wpmcp'; // WordPress MCP endpoint const method = 'POST'; // Log the request parameters for debugging if (useJsonRpc) { logger.api(`Request method: ${requestData.method || 'unknown'} (JSON-RPC)`); logger.debug(`JSON-RPC message: ${JSON.stringify(requestData)}`, 'API'); } else { logger.api(`Request method: ${requestData.method || 'unknown'} (Simple)`); logger.debug(`Simple request: ${JSON.stringify(requestData)}`, 'API'); } // Prepare authorization header - try authentication methods in order of priority let authHeader: string = ''; // 1. JWT Token (highest priority) if (CONFIG.JWT_TOKEN) { authHeader = `Bearer ${CONFIG.JWT_TOKEN}`; logger.auth('Using JWT token authentication'); logger.debug(`Token length: ${CONFIG.JWT_TOKEN.length}`, 'AUTH'); } // 2. OAuth (if enabled and no JWT) else if (CONFIG.OAUTH_ENABLED) { logger.auth('OAuth is the primary authentication method - attempting to get tokens...'); const oauthTokens = await getOAuthTokens(); if (oauthTokens) { authHeader = `Bearer ${oauthTokens.access_token}`; logger.auth('Using OAuth token authentication for WordPress API'); logger.debug(`Token length: ${oauthTokens.access_token.length}`, 'AUTH'); } else { // OAuth failed but it's the primary method, try fallback to Basic Auth logger.warn('OAuth authentication failed, trying Basic Auth fallback', 'AUTH'); } } // 3. Basic Auth (fallback or when OAuth is disabled) if (!authHeader && CONFIG.WP_API_USERNAME && CONFIG.WP_API_PASSWORD) { // Determine which credentials to use based on the method and params let username: string; let password: string; // Determine method and tool name based on transport type const method = useJsonRpc ? requestData.method : requestData.method; const toolName = useJsonRpc ? (requestData.params?.name || requestData.params?.tool) : (requestData.name || requestData.tool || requestData.args?.tool); if ( method === 'tools/call' && toolName && toolName.startsWith('wc_reports_') ) { // Use WooCommerce credentials for WooCommerce report tools username = CONFIG.WOO_CUSTOMER_KEY!; password = CONFIG.WOO_CUSTOMER_SECRET!; logger.auth(`Using WooCommerce credentials for tool: ${toolName}`); // Validate WooCommerce credentials if (!username || !password) { throw new AuthError( 'Missing WooCommerce credentials. Please set WOO_CUSTOMER_KEY and WOO_CUSTOMER_SECRET environment variables.', 'WOOCOMMERCE_CREDENTIALS' ); } } else { // Use standard WordPress credentials for other methods username = CONFIG.WP_API_USERNAME!; password = CONFIG.WP_API_PASSWORD!; logger.auth(`Using WordPress Basic Auth for method: ${method || 'unknown'}`); } // Log credential information (without exposing the actual values) logger.debug(`Username length: ${username ? username.length : 0}`, 'AUTH'); logger.debug(`Password length: ${password ? password.length : 0}`, 'AUTH'); // Prepare Basic auth header const auth = Buffer.from(`${username}:${password}`).toString('base64'); authHeader = `Basic ${auth}`; logger.debug(`Auth header length: ${auth.length}`, 'AUTH'); } // Get custom headers early to check if they can serve as authentication const customHeaders = getCustomHeaders(); const hasCustomHeaders = Object.keys(customHeaders).length > 0; // Ensure we have an authorization header OR custom headers for authentication if (!authHeader && !hasCustomHeaders) { throw new AuthError( 'No authentication method available. Please configure JWT_TOKEN, OAuth, Basic Auth (WP_API_USERNAME+WP_API_PASSWORD), or CUSTOM_HEADERS.', 'NO_AUTH_METHOD' ); } // Get current API URL from environment (to handle dynamic changes) const currentApiUrl = process.env.WP_API_URL || CONFIG.WP_API_URL; logger.debug(`Environment: ${CONFIG.NODE_ENV}`, 'API'); logger.debug(`Base API URL: ${currentApiUrl}`, 'API'); // Construct the final API URL based on whether the base URL has a custom path const url = constructApiUrl(currentApiUrl, endpoint); logger.debug(`Final requesting URL: ${url}`, 'API'); // Build headers object - only add Authorization if we have one const headers: Record<string, string> = { 'Content-Type': 'application/json', 'MCP-Protocol-Version': '2025-06-18', // MCP protocol version ...customHeaders, // Merge custom headers }; // Add Authorization header only if we have one if (authHeader) { headers.Authorization = authHeader; } // Add session ID header if available (for MCP compliance) // Session ID will be set after we receive it from WordPress initialize response if (globalSessionId) { headers['Mcp-Session-Id'] = globalSessionId; } // Log authentication method being used if (authHeader) { logger.debug('Using Authorization header for authentication', 'API'); } else if (hasCustomHeaders) { logger.auth('Using custom headers for authentication (no Authorization header)'); } // Log custom headers (without exposing sensitive values) if (hasCustomHeaders) { logger.debug(`Custom headers added: ${Object.keys(customHeaders).join(', ')}`, 'API'); for (const [key, value] of Object.entries(customHeaders)) { logger.debug(`Header ${key}: ${value.length} characters`, 'API'); } } const fetchOptions: RequestInit = { method, headers, body: JSON.stringify(requestData), }; try { logger.api('Sending request to WordPress API...'); logger.debug(`Request URL: ${url}`, 'API'); logger.debug(`Request method: ${method}`, 'API'); const response = await fetch(url, fetchOptions); logger.debug(`Response status: ${response.status}`, 'API'); // Handle error responses if (!response.ok) { const errorText = await response.text(); logger.error(`API error response: ${errorText}`, 'API'); throw new APIError( `WordPress API error (${response.status}): ${errorText}`, response.status, url, errorText ); } const responseData = await response.json(); // Extract session ID from response headers (for initialize requests) const sessionIdHeader = response.headers.get('Mcp-Session-Id'); if (sessionIdHeader && !globalSessionId) { globalSessionId = sessionIdHeader; logger.info(`Session ID received from WordPress: ${globalSessionId}`, 'SESSION'); } logger.api('Response received successfully'); logger.debug(`Response data: ${JSON.stringify(responseData)}`, 'API'); // Handle response format based on transport type if (useJsonRpc && responseData && typeof responseData === 'object') { const jsonrpcResponse = responseData as any; // Type assertion for JSON-RPC response // Check if this is a JSON-RPC response if (jsonrpcResponse.jsonrpc === '2.0') { if (jsonrpcResponse.error) { // Handle JSON-RPC error response logger.error(`JSON-RPC error response: ${JSON.stringify(jsonrpcResponse.error)}`, 'API'); throw new APIError( `WordPress JSON-RPC error: ${jsonrpcResponse.error.message}`, jsonrpcResponse.error.code || 500, url, JSON.stringify(jsonrpcResponse.error) ); } else if (jsonrpcResponse.result !== undefined) { // Extract result from JSON-RPC response return jsonrpcResponse.result as WordPressResponse; } } } // For simple transport or non-JSON-RPC responses, return response as-is return responseData as WordPressResponse; } catch (error) { if (error instanceof APIError) { throw error; } const errorMessage = error instanceof Error ? error.message : String(error); logger.error(`Error in wpRequest: ${errorMessage}`, 'API'); throw new APIError(errorMessage, 0, url); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Automattic/mcp-wordpress-remote'

If you have feedback or need assistance with the MCP directory API, please join our Discord server