'POST', 'callback' => array( $this, 'handle_post' ), 'permission_callback' => array( 'Agentdesk_Auth', 'authenticate_request' ), ), array( 'methods' => 'GET', 'callback' => array( $this, 'handle_get' ), 'permission_callback' => '__return_true', ), array( 'methods' => 'DELETE', 'callback' => array( $this, 'handle_delete' ), 'permission_callback' => array( 'Agentdesk_Auth', 'authenticate_request' ), ), ) ); } /** * GET handler. * * Without Accept: text/event-stream → public server info (browser check). * With SSE Accept → 405 (we don't stream server-initiated events). * * @param WP_REST_Request $request Request. * @return WP_REST_Response */ public function handle_get( $request ) { $accept = (string) $request->get_header( 'accept' ); if ( false !== stripos( $accept, 'text/event-stream' ) ) { return new WP_REST_Response( null, 405 ); } return new WP_REST_Response( array( 'name' => 'agentdesk-site-operations', 'version' => AGENTDESK_VERSION, 'protocol' => self::PROTOCOL_VERSION, 'endpoint' => home_url( '/?rest_route=/' . AGENTDESK_REST_NAMESPACE . AGENTDESK_MCP_ROUTE ), 'authentication' => array( 'application_password', 'bearer_api_key' ), ), 200 ); } /** * DELETE — terminate session. * * @param WP_REST_Request $request Request. * @return WP_REST_Response */ public function handle_delete( $request ) { $session = $request->get_header( 'mcp-session-id' ); if ( $session ) { delete_transient( self::SESSION_PREFIX . sanitize_text_field( $session ) ); } return new WP_REST_Response( null, 204 ); } /** * Handle JSON-RPC MCP POST. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ public function handle_post( $request ) { $body = $request->get_json_params(); if ( empty( $body ) || ! is_array( $body ) ) { return new WP_Error( 'agentdesk_invalid_request', __( 'Invalid JSON-RPC body.', 'agentdesk-site-operations' ), array( 'status' => 400 ) ); } // Batch. if ( array_keys( $body ) === range( 0, count( $body ) - 1 ) && isset( $body[0] ) && is_array( $body[0] ) ) { $responses = array(); foreach ( $body as $item ) { $responses[] = $this->dispatch_jsonrpc( $item, $request ); } $response = new WP_REST_Response( $responses, 200 ); $response->header( 'Content-Type', 'application/json' ); return $response; } // Notification (no id) → 202 Accepted. if ( ! array_key_exists( 'id', $body ) || null === $body['id'] ) { $method = isset( $body['method'] ) ? (string) $body['method'] : ''; if ( in_array( $method, array( 'notifications/initialized', 'initialized', 'notifications/cancelled' ), true ) ) { $response = new WP_REST_Response( null, 202 ); return $response; } $response = new WP_REST_Response( null, 202 ); return $response; } $payload = $this->dispatch_jsonrpc( $body, $request ); $status = 200; if ( isset( $payload['_http_status'] ) ) { $status = (int) $payload['_http_status']; unset( $payload['_http_status'] ); } $response = new WP_REST_Response( $payload, $status ); $response->header( 'Content-Type', 'application/json' ); $method = isset( $body['method'] ) ? (string) $body['method'] : ''; if ( 'initialize' === $method && ! isset( $payload['error'] ) ) { $session_id = $this->create_session(); $response->header( 'Mcp-Session-Id', $session_id ); } return $response; } /** * Dispatch a single JSON-RPC message. * * @param array $message Message. * @param WP_REST_Request $request Request. * @return array */ private function dispatch_jsonrpc( $message, $request = null ) { $id = array_key_exists( 'id', $message ) ? $message['id'] : null; $method = isset( $message['method'] ) ? (string) $message['method'] : ''; $params = isset( $message['params'] ) && is_array( $message['params'] ) ? $message['params'] : array(); if ( empty( $method ) ) { return $this->jsonrpc_error( $id, -32600, 'Invalid Request' ); } switch ( $method ) { case 'initialize': return $this->jsonrpc_result( $id, array( 'protocolVersion' => self::PROTOCOL_VERSION, 'capabilities' => array( 'tools' => array( 'listChanged' => false ), 'resources' => array( 'subscribe' => false, 'listChanged' => false ), 'prompts' => array( 'listChanged' => false ), ), 'serverInfo' => array( 'name' => 'agentdesk-site-operations', 'version' => AGENTDESK_VERSION, ), 'instructions' => 'Use site_info first. Discover more tools with agentdesk_discover, then agentdesk_run. Safe Mode blocks permanent deletions; Power Mode is required for site settings and Elementor document overwrites.', ) ); case 'notifications/initialized': case 'initialized': return array( 'jsonrpc' => '2.0', 'result' => new stdClass() ); case 'tools/list': return $this->jsonrpc_result( $id, array( 'tools' => Agentdesk_Registry::instance()->get_public_tools(), ) ); case 'tools/call': $tool_name = isset( $params['name'] ) ? (string) $params['name'] : ''; $arguments = isset( $params['arguments'] ) && is_array( $params['arguments'] ) ? $params['arguments'] : array(); $result = Agentdesk_Registry::instance()->run( $tool_name, $arguments ); if ( is_wp_error( $result ) ) { return $this->jsonrpc_result( $id, array( 'content' => array( array( 'type' => 'text', 'text' => wp_json_encode( array( 'error' => true, 'message' => $result->get_error_message(), 'code' => $result->get_error_code(), ) ), ), ), 'isError' => true, ) ); } return $this->jsonrpc_result( $id, array( 'content' => array( array( 'type' => 'text', 'text' => wp_json_encode( $result, JSON_PRETTY_PRINT ), ), ), ) ); case 'resources/list': return $this->jsonrpc_result( $id, array( 'resources' => array() ) ); case 'resources/templates/list': return $this->jsonrpc_result( $id, array( 'resourceTemplates' => array() ) ); case 'prompts/list': return $this->jsonrpc_result( $id, array( 'prompts' => array() ) ); case 'ping': return $this->jsonrpc_result( $id, new stdClass() ); default: return $this->jsonrpc_error( $id, -32601, 'Method not found: ' . $method ); } } /** * Create MCP session id. * * @return string */ private function create_session() { $session_id = wp_generate_password( 32, false, false ); set_transient( self::SESSION_PREFIX . $session_id, array( 'user_id' => get_current_user_id(), 'created_at' => time(), ), HOUR_IN_SECONDS ); return $session_id; } /** * Build JSON-RPC success response. * * @param mixed $id Request ID. * @param mixed $result Result. * @return array */ private function jsonrpc_result( $id, $result ) { $response = array( 'jsonrpc' => '2.0', 'result' => $result, ); if ( null !== $id ) { $response['id'] = $id; } return $response; } /** * Build JSON-RPC error response. * * @param mixed $id Request ID. * @param int $code Error code. * @param string $message Error message. * @return array */ private function jsonrpc_error( $id, $code, $message ) { return array( 'jsonrpc' => '2.0', 'id' => $id, 'error' => array( 'code' => $code, 'message' => $message, ), ); } }