['GET', 'POST'], 'callback' => [$this, 'handleAuthorize'], 'permission_callback' => '__return_true', ]); // Token endpoint — Claude.ai POSTs here to exchange code for token register_rest_route('asktam-mcp/v1', '/oauth/token', [ 'methods' => 'POST', 'callback' => [$this, 'handleToken'], 'permission_callback' => '__return_true', ]); // RFC 8414 discovery — Claude.ai fetches this to find all endpoints register_rest_route('asktam-mcp/v1', '/oauth/metadata', [ 'methods' => 'GET', 'callback' => [$this, 'handleMetadata'], 'permission_callback' => '__return_true', ]); // Well-known discovery at site root (Claude.ai checks this) 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 — shows approval UI, issues code on POST // ------------------------------------------------------------------------- public function handleAuthorize(\WP_REST_Request $request): void { $clientId = $request->get_param('client_id') ?? ''; $redirectUri = $request->get_param('redirect_uri') ?? ''; $state = $request->get_param('state') ?? ''; $approved = $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) { wp_die('Invalid client_id. Check your API key in WP Admin → AskTam MCP.', 'MCP Auth Error', ['response' => 401]); } // User approved — issue code and redirect if ($approved === '1' && $_SERVER['REQUEST_METHOD'] === 'POST') { // Generate a short-lived auth code (stored as transient, 5 min TTL) $code = 'mcp_code_' . bin2hex(random_bytes(16)); set_transient('asktam_mcp_code_' . $code, [ 'client_id' => $clientId, 'redirect_uri' => $redirectUri, 'scopes' => $row['scopes'], ], 300); $redirectBack = add_query_arg(['code' => $code, 'state' => $state], $redirectUri); wp_redirect($redirectBack); exit; } // Show approval page $siteName = get_bloginfo('name'); $keyName = esc_html($row['name']); $scopes = esc_html($row['scopes']); $actionUrl = rest_url('asktam-mcp/v1/oauth/authorize'); ?> Authorize — <?php echo esc_html($siteName); ?>

Authorize Access

An external AI client is requesting access to your WordPress site using the following API key:

API Key
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') ?? ''; $redirectUri = $request->get_param('redirect_uri') ?? ''; [$clientId, $clientSecret] = $this->extractCredentials($request); if (empty($code)) { return $this->oauthError('invalid_request', 'Missing code', 400); } // Retrieve and validate the auth code transient $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); // Validate client_id matches if (!empty($clientId) && $clientId !== $codeData['client_id']) { return $this->oauthError('invalid_client', 'client_id mismatch', 401); } // The access token IS the api_key — stateless, Auth::validate handles it return new \WP_REST_Response([ 'access_token' => $codeData['client_id'], 'token_type' => 'bearer', 'expires_in' => 3600 * 24 * 30, // 30 days '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); } // ------------------------------------------------------------------------- // RFC 8414 metadata — Claude.ai discovers all endpoints from here // ------------------------------------------------------------------------- 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); } }