['GET', 'POST'], 'callback' => [$this, 'handleAuthorize'], 'permission_callback' => '__return_true', ]); // Token endpoint register_rest_route('asktam-mcp/v1', '/oauth/token', [ 'methods' => 'POST', 'callback' => [$this, 'handleToken'], 'permission_callback' => '__return_true', ]); // Metadata register_rest_route('asktam-mcp/v1', '/oauth/metadata', [ 'methods' => 'GET', 'callback' => [$this, 'handleMetadata'], 'permission_callback' => '__return_true', ]); // Well-known add_action('rest_api_init', function () { register_rest_route('', '/.well-known/oauth-authorization-server', [ 'methods' => 'GET', 'callback' => [$this, 'handleMetadata'], 'permission_callback' => '__return_true', ]); }, 20); } // ------------------------------------------------------------------------- // Authorization endpoint — must output raw HTML and die, bypassing REST wrapper // ------------------------------------------------------------------------- public function handleAuthorize(\WP_REST_Request $request): void { $clientId = sanitize_text_field($request->get_param('client_id') ?? ''); $redirectUri = esc_url_raw($request->get_param('redirect_uri') ?? ''); $state = sanitize_text_field($request->get_param('state') ?? ''); $approved = sanitize_text_field($request->get_param('_mcp_approve') ?? ''); // Validate client_id global $wpdb; $table = $wpdb->prefix . 'asktam_mcp_keys'; $row = $wpdb->get_row( $wpdb->prepare("SELECT * FROM {$table} WHERE api_key = %s AND is_active = 1", $clientId), ARRAY_A ); if (!$row) { // Send user back with error if ($redirectUri) { wp_redirect(add_query_arg(['error' => 'invalid_client', 'state' => $state], $redirectUri)); exit; } status_header(401); header('Content-Type: text/plain'); echo 'Invalid client_id. Check your API key in WP Admin → AskTam MCP.'; exit; } // POST: user clicked Approve if ($_SERVER['REQUEST_METHOD'] === 'POST' && $approved === '1') { $code = 'mcp_code_' . bin2hex(random_bytes(16)); set_transient('asktam_mcp_code_' . $code, [ 'client_id' => $clientId, 'redirect_uri' => $redirectUri, 'scopes' => $row['scopes'], ], 300); wp_redirect(add_query_arg(['code' => $code, 'state' => $state], $redirectUri)); exit; } // GET: render approval page — output raw HTML, bypass REST JSON wrapper $siteName = get_bloginfo('name'); $keyName = esc_html($row['name']); $scopes = array_map('trim', explode(',', $row['scopes'])); $actionUrl = rest_url('asktam-mcp/v1/oauth/authorize'); // Send correct headers BEFORE any output status_header(200); header('Content-Type: text/html; charset=UTF-8'); header('X-Robots-Tag: noindex'); // Remove REST API JSON headers that WP might have set header_remove('Content-Type'); // remove any previously set header('Content-Type: text/html; charset=UTF-8'); // Output and die — this prevents WP REST from wrapping in JSON ob_end_clean(); ?> Authorize — <?php echo esc_html($siteName); ?>

Authorize Access

An AI client is requesting access to your WordPress site using this API key:

API Key
Only approve if you initiated this connection from Claude.ai or another trusted MCP client.
get_param('grant_type') ?? ''; if ($grantType === 'authorization_code') { return $this->handleAuthCodeExchange($request); } if ($grantType === 'client_credentials') { return $this->handleClientCredentials($request); } return $this->oauthError('unsupported_grant_type', 'Supported: authorization_code, client_credentials', 400); } private function handleAuthCodeExchange(\WP_REST_Request $request): \WP_REST_Response { $code = $request->get_param('code') ?? ''; if (empty($code)) { return $this->oauthError('invalid_request', 'Missing code', 400); } $codeData = get_transient('asktam_mcp_code_' . $code); if (!$codeData) { return $this->oauthError('invalid_grant', 'Authorization code expired or invalid', 400); } delete_transient('asktam_mcp_code_' . $code); [$clientId] = $this->extractCredentials($request); if (!empty($clientId) && $clientId !== $codeData['client_id']) { return $this->oauthError('invalid_client', 'client_id mismatch', 401); } return new \WP_REST_Response([ 'access_token' => $codeData['client_id'], 'token_type' => 'bearer', 'expires_in' => 3600 * 24 * 30, 'scope' => $codeData['scopes'], ], 200); } private function handleClientCredentials(\WP_REST_Request $request): \WP_REST_Response { [$clientId, $clientSecret] = $this->extractCredentials($request); if (empty($clientId) || empty($clientSecret)) { return $this->oauthError('invalid_client', 'Missing client_id or client_secret', 401); } global $wpdb; $table = $wpdb->prefix . 'asktam_mcp_keys'; $row = $wpdb->get_row( $wpdb->prepare("SELECT * FROM {$table} WHERE api_key = %s AND is_active = 1", $clientId), ARRAY_A ); if (!$row || !hash_equals($clientId, $clientSecret)) { return $this->oauthError('invalid_client', 'Invalid credentials', 401); } return new \WP_REST_Response([ 'access_token' => $clientId, 'token_type' => 'bearer', 'expires_in' => 3600, 'scope' => $row['scopes'], ], 200); } // ------------------------------------------------------------------------- // Metadata // ------------------------------------------------------------------------- public function handleMetadata(): \WP_REST_Response { $base = rest_url('asktam-mcp/v1'); return new \WP_REST_Response([ 'issuer' => get_site_url(), 'authorization_endpoint' => $base . '/oauth/authorize', 'token_endpoint' => $base . '/oauth/token', 'token_endpoint_auth_methods_supported' => ['client_secret_post', 'client_secret_basic', 'none'], 'grant_types_supported' => ['authorization_code', 'client_credentials'], 'response_types_supported' => ['code'], 'code_challenge_methods_supported' => ['S256', 'plain'], 'scopes_supported' => ['read', 'write', 'admin', 'woocommerce'], ]); } // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- private function extractCredentials(\WP_REST_Request $request): array { $clientId = $request->get_param('client_id'); $clientSecret = $request->get_param('client_secret'); if (!empty($clientId) && !empty($clientSecret)) { return [$clientId, $clientSecret]; } $authHeader = $request->get_header('Authorization'); if ($authHeader && str_starts_with($authHeader, 'Basic ')) { $decoded = base64_decode(substr($authHeader, 6)); if ($decoded && str_contains($decoded, ':')) { [$clientId, $clientSecret] = explode(':', $decoded, 2); return [trim($clientId), trim($clientSecret)]; } } return [$clientId ?? '', '']; } private function oauthError(string $error, string $description, int $status): \WP_REST_Response { return new \WP_REST_Response([ 'error' => $error, 'error_description' => $description, ], $status); } }