tools = $tools; } /** * Handle one decoded JSON-RPC message. * * @param mixed $message Decoded request body. * @return array|null Response to encode, or null when the message is a * notification and must be answered with no body. */ public function handle( $message ) { if ( ! is_array( $message ) || $this->is_list( $message ) ) { return $this->error( null, self::INVALID_REQUEST, __( 'Send one JSON-RPC object. Batches are not supported.', 'oorank' ) ); } $method = isset( $message['method'] ) ? (string) $message['method'] : ''; $params = isset( $message['params'] ) && is_array( $message['params'] ) ? $message['params'] : array(); $id = array_key_exists( 'id', $message ) ? $message['id'] : null; // Notifications carry no id and must never be answered, including the // ones we do not implement — notifications/initialized arrives from // every client and an error reply to it fails the handshake. if ( null === $id ) { return null; } switch ( $method ) { case 'initialize': return $this->result( $id, $this->initialize() ); case 'ping': return $this->result( $id, new stdClass() ); case 'tools/list': return $this->result( $id, array( 'tools' => $this->tools->listing() ) ); case 'tools/call': return $this->call_tool( $id, $params ); } return $this->error( $id, self::METHOD_NOT_FOUND, sprintf( /* translators: %s: JSON-RPC method name. */ __( 'Unsupported method: %s.', 'oorank' ), $method ) ); } /** * The initialize result. * * @return array */ private function initialize() { return array( 'protocolVersion' => self::PROTOCOL_VERSION, 'capabilities' => array( 'tools' => array( 'listChanged' => false ), ), 'serverInfo' => array( 'name' => 'Oorank', 'title' => get_bloginfo( 'name' ) . ' — Oorank', 'version' => OORANK_VERSION, ), 'instructions' => __( 'Oorank describes how ready this WordPress site is to be read and cited by AI answer engines, and can change its llms.txt and AI-crawler settings. Use get_audit for stored page scores and run_audit only when a fresh fetch is needed.', 'oorank' ), ); } /** * Handle tools/call. * * @param mixed $id Request id. * @param array $params Request params. * @return array */ private function call_tool( $id, array $params ) { $name = isset( $params['name'] ) ? (string) $params['name'] : ''; if ( '' === $name ) { return $this->error( $id, self::INVALID_PARAMS, __( 'tools/call needs a tool name.', 'oorank' ) ); } $args = isset( $params['arguments'] ) && is_array( $params['arguments'] ) ? $params['arguments'] : array(); $result = $this->tools->invoke( $name, $args ); if ( is_wp_error( $result ) ) { return $this->result( $id, $this->content( $result->get_error_message(), true ) ); } $json = wp_json_encode( $result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); if ( false === $json ) { return $this->result( $id, $this->content( __( 'The tool produced a result that could not be encoded.', 'oorank' ), true ) ); } $payload = $this->content( $json, false ); // structuredContent is what a modern client reads; the text block above // is the fallback for one that does not, and both must say the same // thing rather than two summaries of one result. $payload['structuredContent'] = $result; return $this->result( $id, $payload ); } /** * A tools/call result body. * * @param string $text Text content. * @param bool $is_error Whether the tool failed. * @return array */ private function content( $text, $is_error ) { return array( 'content' => array( array( 'type' => 'text', 'text' => $text, ), ), 'isError' => (bool) $is_error, ); } /** * A JSON-RPC success envelope. * * @param mixed $id Request id. * @param mixed $result Result payload. * @return array */ private function result( $id, $result ) { return array( 'jsonrpc' => '2.0', 'id' => $id, 'result' => $result, ); } /** * A JSON-RPC error envelope. * * @param mixed $id Request id. * @param int $code JSON-RPC error code. * @param string $message Human-readable message. * @return array */ public function error( $id, $code, $message ) { return array( 'jsonrpc' => '2.0', 'id' => $id, 'error' => array( 'code' => $code, 'message' => $message, ), ); } /** * Whether a decoded value is a JSON array rather than an object. * * @param array $value Decoded value. * @return bool */ private function is_list( array $value ) { return array() === $value || array_keys( $value ) === range( 0, count( $value ) - 1 ); } }