Title: Request for new plugin capability
Last modified: June 25, 2026

---

# Request for new plugin capability

 *  Resolved [xanderhbsg](https://wordpress.org/support/users/xanderhbsg/)
 * (@xanderhbsg)
 * [2 weeks, 5 days ago](https://wordpress.org/support/topic/request-for-new-plugin-capability/)
 * I’ve begun using Claude.ai with the MCP to audit and edit changes on my site 
   pages, and I’ve found that the MCP works great. My only problem is that the wp_get_page
   tool requires Claude to contextualize the entire page even if I want to know 
   a small bit of data about the site, which quickly burns through tokens and makes
   the MCP unfeasible in the long run. Claude has suggested a php dubbed search_content
   that can search for a string match and reduce token usage tenfold. I’ve pasted
   its draft below. How difficult would it be to implement a function like this?
   From my understanding, it would be a huge benefit to anyone who uses the Royal
   MCP plugin.
 *     ```wp-block-code
       <?php/** * Plugin Name:       Royal MCP Content Search * Description:       Adds a read-only content-search endpoint for AI workflows. Returns only id, slug, title, permalink, and a short matching snippet per page/post, so an AI agent can audit many pages cheaply without downloading full page bodies. Companion to Royal MCP. * Version:           1.0.0 * License:           GPLv2 or later * Requires PHP:      7.4 * * --------------------------------------------------------------------------- * WHAT THIS DOES * --------------------------------------------------------------------------- * Registers ONE REST route: * *     GET  /wp-json/rmcs/v1/search-content *     POST /wp-json/rmcs/v1/search-content * * Query / body params: *     q          (string, required)  Text to search for inside post_content. *     post_type  (string, optional)  Defaults to "page". Use "post", "any", *                                     or a CPT slug. Comma-separated allowed. *     status     (string, optional)  Defaults to "publish". *     regex      (bool,   optional)  If true, treat q as a PCRE pattern. *                                     Default false (plain case-insensitive *                                     substring match). *     per_page   (int,    optional)  Max rows to scan per query. Default 200, *                                     hard cap 1000. *     snippet    (int,    optional)  Characters of context around each match. *                                     Default 160, hard cap 600. * * Response (JSON): *     { *       "query": "example", *       "post_type": "page", *       "match_count": 3, *       "scanned": 44, *       "results": [ *         { "id": 12, "slug": "some-page", *           "title": "Some Page", *           "url": "https://.../some-page", *           "snippet": "...matching context around the term..." } *       ] *     } * * It NEVER returns full post_content. That is the entire point: an agent gets * enough to classify a page (does it contain X? where?) at a tiny fraction of * the token cost of fetching the whole body. * * --------------------------------------------------------------------------- * SECURITY * --------------------------------------------------------------------------- * - READ ONLY. It runs WP_Query and string functions. It never writes, never *   deletes, never changes settings. There is no code path that mutates state. * - AUTH. The route accepts EITHER the Royal MCP API key via the "X-API-Key" *   header (validated against Royal MCP's stored key) OR a logged-in user with *   the 'edit_posts' capability (standard WP cookie/nonce auth, useful for *   in-browser testing by an admin). *   The option name Royal MCP uses to store its key can vary by version. This *   plugin checks a list of likely option names (see RMCS_APIKEY_OPTIONS) and *   can be overridden by defining RMCS_APIKEY_OPTION in wp-config.php. * - No sensitive data exposure: it only ever emits id, slug, title, permalink, *   and a snippet of content. It does not touch the options table beyond *   reading the API key for comparison, and exposes no user data or secrets. * * --------------------------------------------------------------------------- * INSTALL * --------------------------------------------------------------------------- * 1. Put this folder in wp-content/plugins/. * 2. Activate "Royal MCP Content Search" in Plugins. * 3. Test (admin, logged in): *      /wp-json/rmcs/v1/search-content?q=example&post_type=page *    or with the API key: *      curl -H "X-API-Key: YOUR_ROYAL_MCP_KEY" \ *        "https://YOURSITE/wp-json/rmcs/v1/search-content?q=example&post_type=page" * * If API-key auth fails, the stored option name may differ on your install. * Either add it to RMCS_APIKEY_OPTIONS below, or define the exact name in * wp-config.php:  define( 'RMCS_APIKEY_OPTION', 'your_option_name' ); * * --------------------------------------------------------------------------- * MAKING THE AI CALL IT * --------------------------------------------------------------------------- * This is a plain authenticated REST endpoint, so any AI client that can make * an HTTP request with the API key can use it directly. If you would rather it * appear as a first-class Royal MCP tool in the tools list, Royal MCP would * need to register it; that requires Royal MCP's own tool-registration hook, * which is internal to that plugin. This companion deliberately does NOT depend * on that hook so it keeps working across Royal MCP updates. If Royal exposes a * tool-registration filter in your version, you can wrap this same callback. * --------------------------------------------------------------------------- */if ( ! defined( 'ABSPATH' ) ) {	exit; // No direct access.}/** * Candidate option names where Royal MCP may store its API key. The plugin * tries each until one matches. Override with a single, exact name by defining * RMCS_APIKEY_OPTION in wp-config.php. */if ( ! defined( 'RMCS_APIKEY_OPTION' ) ) {	if ( ! function_exists( 'rmcs_apikey_option_candidates' ) ) {		function rmcs_apikey_option_candidates() {			return array(				'royal_mcp_api_key',				'royal_mcp_apikey',				'rmcp_api_key',				'royalmcp_api_key',			);		}	}}add_action( 'rest_api_init', function () {	register_rest_route(		'rmcs/v1',		'/search-content',		array(			'methods'             => array( 'GET', 'POST' ),			'callback'            => 'rmcs_search_content',			'permission_callback' => 'rmcs_search_permission',			'args'                => array(				'q' => array(					'required'          => true,					'type'              => 'string',					'sanitize_callback' => 'wp_kses_post', // keep it benign; we only read it				),				'post_type' => array(					'required' => false,					'type'     => 'string',				),				'status' => array(					'required' => false,					'type'     => 'string',				),				'regex' => array(					'required' => false,					'type'     => 'boolean',				),				'per_page' => array(					'required' => false,					'type'     => 'integer',				),				'snippet' => array(					'required' => false,					'type'     => 'integer',				),			),		)	);} );/** * Resolve the stored Royal MCP API key, trying the override constant first, * then each candidate option name. Returns '' if none is set. */function rmcs_get_stored_api_key() {	if ( defined( 'RMCS_APIKEY_OPTION' ) ) {		return (string) get_option( RMCS_APIKEY_OPTION, '' );	}	foreach ( rmcs_apikey_option_candidates() as $name ) {		$val = get_option( $name, '' );		if ( ! empty( $val ) ) {			return (string) $val;		}	}	return '';}/** * Permission check. * Accept EITHER the Royal MCP API key (X-API-Key header) OR a logged-in user * who can edit posts. Read-only endpoint, but we still gate it. */function rmcs_search_permission( WP_REST_Request $request ) {	// 1) API key path.	$provided = $request->get_header( 'x-api-key' );	if ( ! empty( $provided ) ) {		$stored = rmcs_get_stored_api_key();		if ( ! empty( $stored ) && hash_equals( $stored, (string) $provided ) ) {			return true;		}	}	// 2) Logged-in admin/editor path (for in-browser testing).	if ( current_user_can( 'edit_posts' ) ) {		return true;	}	return new WP_Error(		'rmcs_forbidden',		'Authentication required. Provide a valid X-API-Key header or sign in with edit_posts capability.',		array( 'status' => 401 )	);}/** * The search itself. Read-only. */function rmcs_search_content( WP_REST_Request $request ) {	$q = (string) $request->get_param( 'q' );	if ( '' === trim( $q ) ) {		return new WP_Error( 'rmcs_bad_query', 'Parameter q is required.', array( 'status' => 400 ) );	}	$post_type_param = $request->get_param( 'post_type' );	$post_type       = $post_type_param ? array_map( 'sanitize_key', explode( ',', $post_type_param ) ) : array( 'page' );	$status_param = $request->get_param( 'status' );	$status       = $status_param ? array_map( 'sanitize_key', explode( ',', $status_param ) ) : array( 'publish' );	$use_regex = (bool) $request->get_param( 'regex' );	$per_page = (int) $request->get_param( 'per_page' );	if ( $per_page <= 0 ) {		$per_page = 200;	}	$per_page = min( $per_page, 1000 );	$snippet_len = (int) $request->get_param( 'snippet' );	if ( $snippet_len <= 0 ) {		$snippet_len = 160;	}	$snippet_len = min( $snippet_len, 600 );	// Validate a regex pattern before using it, so a bad pattern can't error mid-loop.	if ( $use_regex ) {		$pattern = '/' . str_replace( '/', '\/', $q ) . '/i';		if ( false === @preg_match( $pattern, '' ) ) {			return new WP_Error( 'rmcs_bad_regex', 'Parameter q is not a valid regular expression.', array( 'status' => 400 ) );		}	}	$query = new WP_Query( array(		'post_type'              => $post_type,		'post_status'            => $status,		'posts_per_page'         => $per_page,		'no_found_rows'          => true,		'ignore_sticky_posts'    => true,		'update_post_meta_cache' => false,		'update_post_term_cache' => false,		'orderby'                => 'title',		'order'                  => 'ASC',	) );	$results = array();	$scanned = 0;	foreach ( $query->posts as $post ) {		$scanned++;		$content = (string) $post->post_content;		$pos = false;		$matched_len = strlen( $q );		if ( $use_regex ) {			if ( preg_match( $pattern, $content, $m, PREG_OFFSET_CAPTURE ) ) {				$pos         = $m[0][1];				$matched_len = strlen( $m[0][0] );			}		} else {			$pos = stripos( $content, $q );		}		if ( false === $pos ) {			continue;		}		$results[] = array(			'id'      => $post->ID,			'slug'    => $post->post_name,			'title'   => get_the_title( $post ),			'url'     => get_permalink( $post ),			'snippet' => rmcs_make_snippet( $content, (int) $pos, (int) $matched_len, $snippet_len ),		);	}	return new WP_REST_Response( array(		'query'       => $q,		'post_type'   => implode( ',', $post_type ),		'regex'       => $use_regex,		'match_count' => count( $results ),		'scanned'     => $scanned,		'results'     => $results,	), 200 );}/** * Build a short, plain-text snippet around a match. Strips block markup/HTML * so the snippet is readable and small. */function rmcs_make_snippet( $content, $pos, $match_len, $snippet_len ) {	$half  = (int) floor( ( $snippet_len - $match_len ) / 2 );	$start = max( 0, $pos - $half );	$raw   = substr( $content, $start, $snippet_len + $match_len );	// Strip Gutenberg block comments and tags, collapse whitespace.	$raw = preg_replace( '/<!--.*?-->/s', ' ', $raw );	$raw = wp_strip_all_tags( $raw );	$raw = preg_replace( '/\s+/', ' ', $raw );	$raw = trim( (string) $raw );	$prefix = $start > 0 ? '…' : '';	$suffix = ( $start + $snippet_len + $match_len ) < strlen( $content ) ? '…' : '';	return $prefix . $raw . $suffix;}
       ```
   

Viewing 2 replies - 1 through 2 (of 2 total)

 *  Plugin Support [rpteam](https://wordpress.org/support/users/rpteam/)
 * (@rpteam)
 * [2 weeks, 5 days ago](https://wordpress.org/support/topic/request-for-new-plugin-capability/#post-18948489)
 * Hey [@xanderhbsg](https://wordpress.org/support/users/xanderhbsg/),
 * Great idea and great point on token usage, this is scoped for next version 1.4.32
   alongside some WooCommerce updates- thanks for the suggestion and be on the lookout
   for when that drops next week.
 * RP Team
 *  Plugin Support [rpteam](https://wordpress.org/support/users/rpteam/)
 * (@rpteam)
 * [2 weeks, 2 days ago](https://wordpress.org/support/topic/request-for-new-plugin-capability/#post-18950815)
 * Hi,
 * We just shipped this in feature in 1.4.32, live on wp.org now.
 * wp_search accepts two new optional parameters:
    - snippet: int, characters of content excerpt around the matched term (default
      0 = off, recommended
      160-240, max 1000)
    - per_page: int, results per call (default 20, max 100)
 * When snippet > 0, each result includes the post’s slug and an excerpt windowed
   around the first occurrence of your query (HTML and registered shortcodes stripped,
   multibyte-safe). That’s the token-saving shape you described.
 * Example:
 *  {
   “name”: “wp_search”,“arguments”: {“query”: “checkout”,“snippet”: 200,“per_page”:
   50}}
 * Existing callers without the new parameters see no behavior change, so anything
   else hitting wp_search
   keeps working.
 * Thanks for the suggestion, hope this helps conserve more tokens/credits!
 * RP Team

Viewing 2 replies - 1 through 2 (of 2 total)

You must be [logged in](https://login.wordpress.org/?redirect_to=https%3A%2F%2Fwordpress.org%2Fsupport%2Ftopic%2Frequest-for-new-plugin-capability%2F%3Foutput_format%3Dmd&locale=en_US)
to reply to this topic.

 * ![](https://ps.w.org/royal-mcp/assets/icon-256x256.png?rev=3448287)
 * [Royal MCP – Secure AI Connector for Claude, ChatGPT & Gemini](https://wordpress.org/plugins/royal-mcp/)
 * [Frequently Asked Questions](https://wordpress.org/plugins/royal-mcp/#faq)
 * [Support Threads](https://wordpress.org/support/plugin/royal-mcp/)
 * [Active Topics](https://wordpress.org/support/plugin/royal-mcp/active/)
 * [Unresolved Topics](https://wordpress.org/support/plugin/royal-mcp/unresolved/)
 * [Reviews](https://wordpress.org/support/plugin/royal-mcp/reviews/)

 * 2 replies
 * 2 participants
 * Last reply from: [rpteam](https://wordpress.org/support/users/rpteam/)
 * Last activity: [2 weeks, 2 days ago](https://wordpress.org/support/topic/request-for-new-plugin-capability/#post-18950815)
 * Status: resolved