resolver = $resolver; add_action( 'rest_api_init', [ $this, 'mount' ] ); } public function mount(): void { register_rest_route( self::NS, '/tools', [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ $this, 'handle_list' ], 'permission_callback' => '__return_true', ] ); register_rest_route( self::NS, '/execute/(?P[a-zA-Z0-9_%\-]+)', [ 'methods' => \WP_REST_Server::CREATABLE, 'callback' => [ $this, 'handle_run' ], 'permission_callback' => '__return_true', 'args' => [ 'ability' => [ 'required' => true, 'sanitize_callback' => fn( $v ) => sanitize_text_field( rawurldecode( $v ) ), ], ], ] ); register_rest_route( self::NS, '/nonce', [ 'methods' => \WP_REST_Server::READABLE, 'callback' => fn() => new \WP_REST_Response( [ 'nonce' => wp_create_nonce( 'webmcp_run' ) ] ), 'permission_callback' => '__return_true', ] ); } public function handle_list( \WP_REST_Request $req ): \WP_REST_Response { $tools = $this->resolver->for_current_user(); $hash = $this->resolver->etag(); $client_etag = $req->get_header( 'if_none_match' ); if ( $client_etag && trim( $client_etag, '"' ) === $hash ) { return new \WP_REST_Response( null, 304 ); } $resp = new \WP_REST_Response( [ 'tools' => $tools, 'nonce' => wp_create_nonce( 'webmcp_run' ) ] ); $resp->header( 'Cache-Control', 'private, max-age=300' ); $resp->header( 'Vary', 'Cookie' ); $resp->header( 'ETag', '"' . $hash . '"' ); return $resp; } public function handle_run( \WP_REST_Request $req ): \WP_REST_Response { $name = $req->get_param( 'ability' ); $uid = get_current_user_id(); // Payload size guard. $max = (int) apply_filters( 'webmcp_max_payload', 102400 ); if ( strlen( $req->get_body() ) > $max ) { return $this->err( 'webmcp_too_large', __( 'Payload exceeds maximum size.', 'agentgate-for-webmcp' ), 400 ); } // Abilities API must be present. if ( ! function_exists( 'wp_has_ability' ) || ! wp_has_ability( $name ) ) { return $this->err( 'webmcp_missing', __( 'Tool not found.', 'agentgate-for-webmcp' ), 404 ); } // Only allow hardcoded tools. if ( ! in_array( $name, Core::TOOLS, true ) ) { return $this->err( 'webmcp_missing', __( 'Tool not found.', 'agentgate-for-webmcp' ), 404 ); } $ability = wp_get_ability( $name ); $input = $req->get_json_params() ?? []; // Ability-level permission gate. if ( true !== $ability->check_permissions( $input ) ) { return $this->err( 'webmcp_denied', __( 'Permission denied for this tool.', 'agentgate-for-webmcp' ), 403 ); } // CSRF check for write operations by logged-in users. if ( is_user_logged_in() && ! $ability->get_meta_item( 'webmcp_readonly', false ) ) { $tok = $req->get_header( 'x_wp_nonce' ); if ( ! $tok || ! wp_verify_nonce( $tok, 'webmcp_run' ) ) { return $this->err( 'webmcp_bad_token', __( 'Invalid or expired security token.', 'agentgate-for-webmcp' ), 403 ); } } // Extensibility hook. $gate = apply_filters( 'webmcp_pre_run', true, $name, $input, $uid ); if ( is_wp_error( $gate ) ) { return $this->err( $gate->get_error_code(), $gate->get_error_message(), 403 ); } // Run the tool. try { $output = $ability->execute( $input ); } catch ( \Throwable $ex ) { do_action( 'webmcp_after_run', $name, $uid, false ); return $this->err( 'webmcp_error', __( 'Tool execution failed.', 'agentgate-for-webmcp' ), 500 ); } if ( is_wp_error( $output ) ) { do_action( 'webmcp_after_run', $name, $uid, false ); return $this->err( $output->get_error_code(), $output->get_error_message(), $output->get_error_data( 'status' ) ?? 500 ); } do_action( 'webmcp_after_run', $name, $uid, true ); return new \WP_REST_Response( [ 'result' => $output ] ); } private function err( string $code, string $msg, int $status ): \WP_REST_Response { return new \WP_REST_Response( [ 'code' => $code, 'message' => $msg ], $status ); } }