{"id":32136,"date":"2026-05-04T10:23:58","date_gmt":"2026-05-04T15:23:58","guid":{"rendered":"https:\/\/wpengine.com\/builders\/?p=32136"},"modified":"2026-05-04T10:31:37","modified_gmt":"2026-05-04T15:31:37","slug":"wordpress-copilot-smart-search-mcp","status":"publish","type":"post","link":"https:\/\/wpengine.com\/builders\/wordpress-copilot-smart-search-mcp\/","title":{"rendered":"Build\u00a0 A WordPress\u00ae Admin Copilot with OpenAI and Smart Search AI MCP"},"content":{"rendered":"\n<p>In most content teams, finding the right post quickly is one of those tasks that sounds simple but becomes slow when your content library grows.<\/p>\n\n\n\n<p>In this tutorial, we will walk through how to build a WordPress admin copilot that can search your indexed content with natural language, return results in real time, and handle follow-up requests like &#8220;show more&#8221; and &#8220;summarize the first result.&#8221;<\/p>\n\n\n\n<p>We will use a Next.js API backend as the coordinator, OpenAI for reasoning and tool orchestration, and WP Engine Smart Search AI MCP for deterministic search and retrieval. We will cover these points:<\/p>\n\n\n\n<p>&#8211; Secure API setup for WordPress admin requests<\/p>\n\n\n\n<p>&#8211; Smart Search AI MCP client integration<\/p>\n\n\n\n<p>&#8211; OpenAI tool calling flow<\/p>\n\n\n\n<p>&#8211; Streaming response events for chat UX<\/p>\n\n\n\n<p>&#8211; Follow-up behaviors like pagination and result summarization<\/p>\n\n\n\n<p class=\"has-small-font-size\"><em><strong>Just a Note:<\/strong> This article focuses on the API\/backend side of the copilot workflow. The frontend admin UI can be implemented with the plugin I made for this article.&nbsp; You can also create your own.<\/em><\/p>\n\n\n\n<div class=\"wp-block-group has-polar-background-color has-background is-layout-flow wp-container-core-group-is-layout-7a03825d wp-block-group-is-layout-flow\" style=\"padding-top:var(--wp--preset--spacing--30);padding-right:var(--wp--preset--spacing--40);padding-bottom:var(--wp--preset--spacing--30);padding-left:var(--wp--preset--spacing--40)\">\n<p class=\"has-large-font-size\"><strong>Table of Contents<\/strong><\/p>\n\n\n\n<ul id=\"Prerequisites\" class=\"wp-block-list\">\n<li id=\"Prerequisites\"><a href=\"#prerequisites\">Prerequisites<\/a><\/li>\n\n\n\n<li><a href=\"#steps-for-setting-up\">Steps for setting up<\/a><\/li>\n\n\n\n<li id=\"steps-for-setting-up\"><a href=\"#install-the-wordpress-plugin\">Install the WordPress plugin<\/a><\/li>\n\n\n\n<li><a href=\"#create-your-nextjs-backend-project\">Create your Next.js backend project<\/a><\/li>\n\n\n\n<li><a href=\"#configure-environment-variables\">Configure environment variables<\/a><\/li>\n\n\n\n<li><a href=\"#the-lib-mcp-file\" type=\"internal\" id=\"#connecting-with-asana\">The `lib\/mcp.ts` file<\/a><\/li>\n\n\n\n<li><a href=\"#the-lib-tools-file\">The `lib\/tools.ts` file<\/a><\/li>\n\n\n\n<li><a href=\"#the-app-api-chat-route-file\">The `app\/api\/chat\/route.ts` file<\/a><\/li>\n\n\n\n<li><a href=\"#testing-the-copilot-flow\">Testing the copilot flow<\/a><\/li>\n\n\n\n<li><a href=\"#conclusion\">Conclusion<\/a><\/li>\n<\/ul>\n<\/div>\n\n\n\n<p class=\"has-small-font-size\"><em><br><\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<p>To benefit from this article, you should be familiar with WordPress development, <a href=\"https:\/\/nextjs.org\/docs\/pages\/building-your-application\/routing\/api-routes\">Next.js API routes<\/a>, and basic environment variable setup.<br><\/p>\n\n\n\n<p>You will need:<\/p>\n\n\n\n<p>&#8211; <a href=\"https:\/\/nodejs.org\/en\">Node.js 20+<\/a><\/p>\n\n\n\n<p>&#8211; A WordPress install on <a href=\"https:\/\/wpengine.com\/\">WP Engine<\/a><\/p>\n\n\n\n<p>&#8211; <a href=\"https:\/\/wpengine.com\/smart-search\/\">Smart Search AI license<\/a> enabled for that environment<\/p>\n\n\n\n<p>&#8211; An <a href=\"https:\/\/openai.com\/api\/\">OpenAI API key<\/a><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Steps for setting up<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Set up a WordPress environment on WP Engine.<\/li>\n<\/ol>\n\n\n\n<ol start=\"2\" class=\"wp-block-list\">\n<li>Add a Smart Search AI license to your environment. (You can refer to the support <a href=\"https:\/\/wpengine.com\/support\/wp-engine-smart-search\/\">article here <\/a>if you need to know how to do so)<\/li>\n<\/ol>\n\n\n\n<ol start=\"3\" class=\"wp-block-list\">\n<li>In your WP Engine user portal navigate to <strong><em>Products &gt; Smart Search AI<\/em><\/strong>.  Once you click on Smart Search AI, it will take you to a page that lists all your WP installs with Smart Search AI activated.  You will see an ellipsis to the right. Click on that. This will show your MCP endpoint and credentials.   <\/li>\n<\/ol>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"579\" src=\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/Screenshot-2026-04-27-at-12.50.27-PM-1024x579.png\" alt=\"\" class=\"wp-image-32138\" srcset=\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/Screenshot-2026-04-27-at-12.50.27-PM-1024x579.png 1024w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/Screenshot-2026-04-27-at-12.50.27-PM-300x170.png 300w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/Screenshot-2026-04-27-at-12.50.27-PM-768x434.png 768w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/Screenshot-2026-04-27-at-12.50.27-PM-1536x869.png 1536w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/Screenshot-2026-04-27-at-12.50.27-PM-2048x1158.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>Copy the MCP endpoint and its token.<\/p>\n\n\n\n<p>4. In Smart Search configuration, select Hybrid mode, to incorporate both keyword and semantic search structures:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"417\" src=\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/image-3-1024x417.png\" alt=\"\" class=\"wp-image-32139\" srcset=\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/image-3-1024x417.png 1024w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/image-3-300x122.png 300w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/image-3-768x313.png 768w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/image-3-1536x626.png 1536w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/image-3.png 1600w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>Then and add semantic fields such as:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span>- <span class=\"hljs-string\">`post_title`<\/span>\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>- <span class=\"hljs-string\">`post_content`<\/span>\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>- <span class=\"hljs-string\">`post_excerpt`<\/span>\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"282\" src=\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/image-4-1024x282.png\" alt=\"\" class=\"wp-image-32140\" srcset=\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/image-4-1024x282.png 1024w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/image-4-300x83.png 300w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/image-4-768x212.png 768w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/image-4-1536x423.png 1536w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/image-4.png 1600w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>5. Go to Index data and click <strong>Index now<\/strong>, and wait. Wait for indexing to complete before testing.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"344\" src=\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/image-5-1024x344.png\" alt=\"\" class=\"wp-image-32141\" srcset=\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/image-5-1024x344.png 1024w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/image-5-300x101.png 300w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/image-5-768x258.png 768w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/image-5-1536x516.png 1536w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/image-5.png 1600w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>If you want to, you can build your own plugin instead of using the one in this repo. To work with this backend, your plugin must:<\/p>\n\n\n\n<div class=\"wp-block-group has-polar-background-color has-background has-global-padding is-layout-constrained wp-block-group-is-layout-constrained\">\n<h3 class=\"wp-block-heading\">Technical integration requirements<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>send <code>`POST`<\/code> requests to <code>`\/api\/chat`<\/code><\/li>\n\n\n\n<li>&nbsp;include the <code>`x-wp-admin-copilot-token`<\/code> header<\/li>\n\n\n\n<li>run from the same origin configured in <code>`WP_ADMIN_ORIGIN`<\/code><\/li>\n\n\n\n<li>send JSON shaped like <code>`{ message, history, state }`<\/code><\/li>\n\n\n\n<li>handle SSE events: <code>`status`, `delta`, `done`, and `error`<\/code><\/li>\n<\/ul>\n<\/div>\n\n\n\n<p><\/p>\n\n\n\n<p>6. Next we need to create a <a href=\"http:\/\/next.js\">Next.js<\/a> app. You can find the docs on how to get one set up <a href=\"https:\/\/nextjs.org\/docs\/app\/getting-started\/installation\">here<\/a>. Once you have created your Next.js app, we will need to configure our environment variables.<\/p>\n\n\n\n<p>Create <code>`.env.local`<\/code> at your project root and add these key\/value pairs:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span>```bash\n<\/span><\/span><span class='shcb-loc'><span>OPENAI_API_KEY=<span class=\"hljs-string\">\"sk-proj-...\"<\/span>\n<\/span><\/span><span class='shcb-loc'><span>OPENAI_MODEL=<span class=\"hljs-string\">\"gpt-4.1-mini\"<\/span>\n<\/span><\/span><span class='shcb-loc'><span>SMART_SEARCH_MCP_URL=<span class=\"hljs-string\">\"https:\/\/your-site-atlassearch-xxxxx-uc.a.run.app\/mcp\"<\/span>\n<\/span><\/span><span class='shcb-loc'><span>SMART_SEARCH_MCP_TOKEN=<span class=\"hljs-string\">\"your-mcp-token\"<\/span>\n<\/span><\/span><span class='shcb-loc'><span>WP_ADMIN_COPILOT_TOKEN=<span class=\"hljs-string\">\"&lt;generate-with-openssl-rand-hex-32&gt;\"<\/span>\n<\/span><\/span><span class='shcb-loc'><span>WP_ADMIN_ORIGIN=<span class=\"hljs-string\">\"https:\/\/your-wordpress-site.com\"<\/span>\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Generate the shared token:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-string\">``<\/span><span class=\"hljs-string\">`bash<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-string\">openssl rand -hex 32<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-string\">`<\/span><span class=\"hljs-string\">``<\/span>\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Fill them in with your true values.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Next.js Backend<\/h2>\n\n\n\n<p>Now that we have everything set up, let&#8217;s add the code we need and the files necessary to make this demo work.<br><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The `lib\/mcp.ts` file<\/h2>\n\n\n\n<p>Let\u2019s start by building the foundational piece that handles communication with the Smart Search MCP server.<\/p>\n\n\n\n<p>Go to the root of your Next.js project and add a folder called <code>\/lib<\/code> with a file called <code>`mcp.ts`<\/code>.&nbsp;<\/p>\n\n\n\n<p>Here is the complete code content that you will need to copy and paste into your file:<\/p>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary><\/summary><pre class=\"wp-block-code\"><span><code class=\"hljs shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span>\/\/ JSON-RPC success payload type\n<\/span><\/span><span class='shcb-loc'><span>export type JsonRpcSuccess = {\n<\/span><\/span><span class='shcb-loc'><span>  jsonrpc: \"2.0\";\n<\/span><\/span><span class='shcb-loc'><span>  id?: string | number;\n<\/span><\/span><span class='shcb-loc'><span>  result?: unknown;\n<\/span><\/span><span class='shcb-loc'><span>};\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>\/\/ JSON-RPC error payload type\n<\/span><\/span><span class='shcb-loc'><span>export type JsonRpcError = {\n<\/span><\/span><span class='shcb-loc'><span>  jsonrpc: \"2.0\";\n<\/span><\/span><span class='shcb-loc'><span>  id?: string | number;\n<\/span><\/span><span class='shcb-loc'><span>  error: {\n<\/span><\/span><span class='shcb-loc'><span>    code: number;\n<\/span><\/span><span class='shcb-loc'><span>    message: string;\n<\/span><\/span><span class='shcb-loc'><span>    data?: unknown;\n<\/span><\/span><span class='shcb-loc'><span>  };\n<\/span><\/span><span class='shcb-loc'><span>};\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>\/\/ Tool argument type for search\n<\/span><\/span><span class='shcb-loc'><span>export type SearchArgs = {\n<\/span><\/span><span class='shcb-loc'><span>  query: string;\n<\/span><\/span><span class='shcb-loc'><span>  filter?: string;\n<\/span><\/span><span class='shcb-loc'><span>  limit?: number;\n<\/span><\/span><span class='shcb-loc'><span>  offset?: number;\n<\/span><\/span><span class='shcb-loc'><span>};\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>\/\/ Tool argument type for fetch\n<\/span><\/span><span class='shcb-loc'><span>export type FetchArgs = {\n<\/span><\/span><span class='shcb-loc'><span>  id: string;\n<\/span><\/span><span class='shcb-loc'><span>};\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>\/\/ Flexible SearchResultItem shape (MCP result payloads can vary)\n<\/span><\/span><span class='shcb-loc'><span>export type SearchResultItem = {\n<\/span><\/span><span class='shcb-loc'><span>  id?: string;\n<\/span><\/span><span class='shcb-loc'><span>  title?: string;\n<\/span><\/span><span class='shcb-loc'><span>  url?: string;\n<\/span><\/span><span class='shcb-loc'><span>  snippet?: string;\n<\/span><\/span><span class='shcb-loc'><span>  &#91;key: string]: unknown;\n<\/span><\/span><span class='shcb-loc'><span>};\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>\/\/ parseJsonRpcResponse - validates response shape and centralizes protocol-level error handling\n<\/span><\/span><span class='shcb-loc'><span>function parseJsonRpcResponse(payload: unknown): JsonRpcSuccess {\n<\/span><\/span><span class='shcb-loc'><span>  if (!payload || typeof payload !== \"object\") {\n<\/span><\/span><span class='shcb-loc'><span>    throw new Error(\"Invalid JSON-RPC response payload.\");\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  const maybeError = payload as JsonRpcError;\n<\/span><\/span><span class='shcb-loc'><span>  if (maybeError.error) {\n<\/span><\/span><span class='shcb-loc'><span>    throw new Error(\n<\/span><\/span><span class='shcb-loc'><span>      `MCP error ${maybeError.error.code}: ${maybeError.error.message}`,\n<\/span><\/span><span class='shcb-loc'><span>    );\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  return payload as JsonRpcSuccess;\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>\/\/ extractResultsFromMcpResult - defensive parser handling multiple response formats (arrays, nested objects, JSON strings)\n<\/span><\/span><span class='shcb-loc'><span>function extractResultsFromMcpResult(result: unknown): SearchResultItem&#91;] {\n<\/span><\/span><span class='shcb-loc'><span>  const asItems = (value: unknown): SearchResultItem&#91;] | undefined =&gt; {\n<\/span><\/span><span class='shcb-loc'><span>    if (!Array.isArray(value)) {\n<\/span><\/span><span class='shcb-loc'><span>      return undefined;\n<\/span><\/span><span class='shcb-loc'><span>    }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    const objectItems = value.filter(\n<\/span><\/span><span class='shcb-loc'><span>      (item) =&gt; item &amp;&amp; typeof item === \"object\",\n<\/span><\/span><span class='shcb-loc'><span>    ) as SearchResultItem&#91;];\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    return objectItems.length &gt; 0 ? objectItems : undefined;\n<\/span><\/span><span class='shcb-loc'><span>  };\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  const parseMaybeJsonString = (value: unknown): unknown =&gt; {\n<\/span><\/span><span class='shcb-loc'><span>    if (typeof value !== \"string\") {\n<\/span><\/span><span class='shcb-loc'><span>      return value;\n<\/span><\/span><span class='shcb-loc'><span>    }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    const trimmed = value.trim();\n<\/span><\/span><span class='shcb-loc'><span>    if (!trimmed.startsWith(\"{\") &amp;&amp; !trimmed.startsWith(\"&#91;\")) {\n<\/span><\/span><span class='shcb-loc'><span>      return value;\n<\/span><\/span><span class='shcb-loc'><span>    }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    try {\n<\/span><\/span><span class='shcb-loc'><span>      return JSON.parse(trimmed);\n<\/span><\/span><span class='shcb-loc'><span>    } catch {\n<\/span><\/span><span class='shcb-loc'><span>      return value;\n<\/span><\/span><span class='shcb-loc'><span>    }\n<\/span><\/span><span class='shcb-loc'><span>  };\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  const tryExtractFromObject = (obj: Record&lt;string, unknown&gt;): SearchResultItem&#91;] =&gt; {\n<\/span><\/span><span class='shcb-loc'><span>    const directCandidates = &#91;\n<\/span><\/span><span class='shcb-loc'><span>      obj.results,\n<\/span><\/span><span class='shcb-loc'><span>      obj.items,\n<\/span><\/span><span class='shcb-loc'><span>      obj.hits,\n<\/span><\/span><span class='shcb-loc'><span>      obj.documents,\n<\/span><\/span><span class='shcb-loc'><span>      obj.data,\n<\/span><\/span><span class='shcb-loc'><span>      obj.rows,\n<\/span><\/span><span class='shcb-loc'><span>    ];\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    for (const candidate of directCandidates) {\n<\/span><\/span><span class='shcb-loc'><span>      const asList = asItems(candidate);\n<\/span><\/span><span class='shcb-loc'><span>      if (asList) {\n<\/span><\/span><span class='shcb-loc'><span>        return asList;\n<\/span><\/span><span class='shcb-loc'><span>      }\n<\/span><\/span><span class='shcb-loc'><span>    }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    return &#91;];\n<\/span><\/span><span class='shcb-loc'><span>  };\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  if (!result) {\n<\/span><\/span><span class='shcb-loc'><span>    return &#91;];\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  const parsedTop = parseMaybeJsonString(result);\n<\/span><\/span><span class='shcb-loc'><span>  if (!parsedTop || typeof parsedTop !== \"object\") {\n<\/span><\/span><span class='shcb-loc'><span>    return &#91;];\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  if (Array.isArray(parsedTop)) {\n<\/span><\/span><span class='shcb-loc'><span>    return asItems(parsedTop) ?? &#91;];\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  const topLevel = parsedTop as Record&lt;string, unknown&gt;;\n<\/span><\/span><span class='shcb-loc'><span>  const topLevelResults = tryExtractFromObject(topLevel);\n<\/span><\/span><span class='shcb-loc'><span>  if (topLevelResults.length &gt; 0) {\n<\/span><\/span><span class='shcb-loc'><span>    return topLevelResults;\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  const typed = topLevel as {\n<\/span><\/span><span class='shcb-loc'><span>    content?: Array&lt;{ json?: unknown; text?: unknown }&gt;;\n<\/span><\/span><span class='shcb-loc'><span>  };\n<\/span><\/span><span class='shcb-loc'><span>  const content = Array.isArray(typed.content) ? typed.content : &#91;];\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  for (const item of content) {\n<\/span><\/span><span class='shcb-loc'><span>    if (!item || typeof item !== \"object\") {\n<\/span><\/span><span class='shcb-loc'><span>      continue;\n<\/span><\/span><span class='shcb-loc'><span>    }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    const jsonCandidate = parseMaybeJsonString(item.json);\n<\/span><\/span><span class='shcb-loc'><span>    if (jsonCandidate &amp;&amp; typeof jsonCandidate === \"object\") {\n<\/span><\/span><span class='shcb-loc'><span>      const fromJson = tryExtractFromObject(\n<\/span><\/span><span class='shcb-loc'><span>        jsonCandidate as Record&lt;string, unknown&gt;,\n<\/span><\/span><span class='shcb-loc'><span>      );\n<\/span><\/span><span class='shcb-loc'><span>      if (fromJson.length &gt; 0) {\n<\/span><\/span><span class='shcb-loc'><span>        return fromJson;\n<\/span><\/span><span class='shcb-loc'><span>      }\n<\/span><\/span><span class='shcb-loc'><span>    }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    const textCandidate = parseMaybeJsonString(item.text);\n<\/span><\/span><span class='shcb-loc'><span>    if (textCandidate &amp;&amp; typeof textCandidate === \"object\") {\n<\/span><\/span><span class='shcb-loc'><span>      const fromText = tryExtractFromObject(\n<\/span><\/span><span class='shcb-loc'><span>        textCandidate as Record&lt;string, unknown&gt;,\n<\/span><\/span><span class='shcb-loc'><span>      );\n<\/span><\/span><span class='shcb-loc'><span>      if (fromText.length &gt; 0) {\n<\/span><\/span><span class='shcb-loc'><span>        return fromText;\n<\/span><\/span><span class='shcb-loc'><span>      }\n<\/span><\/span><span class='shcb-loc'><span>    }\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  return &#91;];\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>\/\/ SmartSearchMcpClient - main class storing URL, bearer tokens, and sessionId\n<\/span><\/span><span class='shcb-loc'><span>export class SmartSearchMcpClient {\n<\/span><\/span><span class='shcb-loc'><span>  private readonly url: string;\n<\/span><\/span><span class='shcb-loc'><span>  private readonly token?: string;\n<\/span><\/span><span class='shcb-loc'><span>  private sessionId?: string;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  constructor(url: string, token?: string) {\n<\/span><\/span><span class='shcb-loc'><span>    this.url = url;\n<\/span><\/span><span class='shcb-loc'><span>    this.token = token;\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  \/\/ buildHeaders - constructs request headers with auth and session\n<\/span><\/span><span class='shcb-loc'><span>  private buildHeaders(): HeadersInit {\n<\/span><\/span><span class='shcb-loc'><span>    const headers: HeadersInit = {\n<\/span><\/span><span class='shcb-loc'><span>      \"Content-Type\": \"application\/json\",\n<\/span><\/span><span class='shcb-loc'><span>    };\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    if (this.token) {\n<\/span><\/span><span class='shcb-loc'><span>      headers.Authorization = `Bearer ${this.token}`;\n<\/span><\/span><span class='shcb-loc'><span>    }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    if (this.sessionId) {\n<\/span><\/span><span class='shcb-loc'><span>      headers&#91;\"mcp-session-id\"] = this.sessionId;\n<\/span><\/span><span class='shcb-loc'><span>    }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    return headers;\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  \/\/ captureSessionId - reads session headers (mcp-session-id, x-mcp-session-id, session-id)\n<\/span><\/span><span class='shcb-loc'><span>  private captureSessionId(response: Response): void {\n<\/span><\/span><span class='shcb-loc'><span>    const headerSession =\n<\/span><\/span><span class='shcb-loc'><span>      response.headers.get(\"mcp-session-id\") ??\n<\/span><\/span><span class='shcb-loc'><span>      response.headers.get(\"x-mcp-session-id\") ??\n<\/span><\/span><span class='shcb-loc'><span>      response.headers.get(\"session-id\");\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    if (headerSession &amp;&amp; headerSession.trim()) {\n<\/span><\/span><span class='shcb-loc'><span>      this.sessionId = headerSession.trim();\n<\/span><\/span><span class='shcb-loc'><span>    }\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  \/\/ rpcCall - generic JSON-RPC request method (sends POST, captures session, checks status, parses response)\n<\/span><\/span><span class='shcb-loc'><span>  private async rpcCall(method: string, params?: unknown): Promise&lt;unknown&gt; {\n<\/span><\/span><span class='shcb-loc'><span>    const reqBody = {\n<\/span><\/span><span class='shcb-loc'><span>      jsonrpc: \"2.0\",\n<\/span><\/span><span class='shcb-loc'><span>      id: `req_${Date.now()}_${Math.random().toString(36).slice(2)}`,\n<\/span><\/span><span class='shcb-loc'><span>      method,\n<\/span><\/span><span class='shcb-loc'><span>      params,\n<\/span><\/span><span class='shcb-loc'><span>    };\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    const response = await fetch(this.url, {\n<\/span><\/span><span class='shcb-loc'><span>      method: \"POST\",\n<\/span><\/span><span class='shcb-loc'><span>      headers: this.buildHeaders(),\n<\/span><\/span><span class='shcb-loc'><span>      body: JSON.stringify(reqBody),\n<\/span><\/span><span class='shcb-loc'><span>    });\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    this.captureSessionId(response);\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    if (!response.ok) {\n<\/span><\/span><span class='shcb-loc'><span>      const bodyText = await response.text();\n<\/span><\/span><span class='shcb-loc'><span>      throw new Error(\n<\/span><\/span><span class='shcb-loc'><span>        `MCP HTTP ${response.status}: ${response.statusText} - ${bodyText}`,\n<\/span><\/span><span class='shcb-loc'><span>      );\n<\/span><\/span><span class='shcb-loc'><span>    }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    const payload = await response.json();\n<\/span><\/span><span class='shcb-loc'><span>    const parsed = parseJsonRpcResponse(payload);\n<\/span><\/span><span class='shcb-loc'><span>    return parsed.result;\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  \/\/ initializeSession - performs MCP handshake with protocol metadata\n<\/span><\/span><span class='shcb-loc'><span>  private async initializeSession(): Promise&lt;void&gt; {\n<\/span><\/span><span class='shcb-loc'><span>    const result = await this.rpcCall(\"initialize\", {\n<\/span><\/span><span class='shcb-loc'><span>      protocolVersion: \"2024-11-05\",\n<\/span><\/span><span class='shcb-loc'><span>      capabilities: {},\n<\/span><\/span><span class='shcb-loc'><span>      clientInfo: {\n<\/span><\/span><span class='shcb-loc'><span>        name: \"wp-admin-copilot\",\n<\/span><\/span><span class='shcb-loc'><span>        version: \"1.0.0\",\n<\/span><\/span><span class='shcb-loc'><span>      },\n<\/span><\/span><span class='shcb-loc'><span>    });\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    if (!this.sessionId) {\n<\/span><\/span><span class='shcb-loc'><span>      const maybeSessionId =\n<\/span><\/span><span class='shcb-loc'><span>        (result as { sessionId?: string } | undefined)?.sessionId ??\n<\/span><\/span><span class='shcb-loc'><span>        (result as { session_id?: string } | undefined)?.session_id;\n<\/span><\/span><span class='shcb-loc'><span>      if (typeof maybeSessionId === \"string\" &amp;&amp; maybeSessionId.trim()) {\n<\/span><\/span><span class='shcb-loc'><span>        this.sessionId = maybeSessionId.trim();\n<\/span><\/span><span class='shcb-loc'><span>      }\n<\/span><\/span><span class='shcb-loc'><span>    }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    \/\/ Best-effort MCP initialized notification; some servers require this.\n<\/span><\/span><span class='shcb-loc'><span>    try {\n<\/span><\/span><span class='shcb-loc'><span>      await this.rpcCall(\"notifications\/initialized\", {});\n<\/span><\/span><span class='shcb-loc'><span>    } catch {\n<\/span><\/span><span class='shcb-loc'><span>      \/\/ Ignore; not all servers require or support this notification over JSON-RPC requests.\n<\/span><\/span><span class='shcb-loc'><span>    }\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  \/\/ callTool - executes MCP tools (search\/fetch) with session recovery on stale\/missing sessions\n<\/span><\/span><span class='shcb-loc'><span>  async callTool&lt;TArgs extends Record&lt;string, unknown&gt;&gt;(\n<\/span><\/span><span class='shcb-loc'><span>    name: \"search\" | \"fetch\",\n<\/span><\/span><span class='shcb-loc'><span>    args: TArgs,\n<\/span><\/span><span class='shcb-loc'><span>  ): Promise&lt;unknown&gt; {\n<\/span><\/span><span class='shcb-loc'><span>    try {\n<\/span><\/span><span class='shcb-loc'><span>      return await this.rpcCall(\"tools\/call\", {\n<\/span><\/span><span class='shcb-loc'><span>        name,\n<\/span><\/span><span class='shcb-loc'><span>        arguments: args,\n<\/span><\/span><span class='shcb-loc'><span>      });\n<\/span><\/span><span class='shcb-loc'><span>    } catch (error) {\n<\/span><\/span><span class='shcb-loc'><span>      const msg = error instanceof Error ? error.message : \"\";\n<\/span><\/span><span class='shcb-loc'><span>      const needsSessionRecovery =\n<\/span><\/span><span class='shcb-loc'><span>        \/Invalid session ID|Missing session|session id\/i.test(msg);\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>      if (!needsSessionRecovery) {\n<\/span><\/span><span class='shcb-loc'><span>        throw error;\n<\/span><\/span><span class='shcb-loc'><span>      }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>      \/\/ Reset stale session and attempt handshake + one retry.\n<\/span><\/span><span class='shcb-loc'><span>      this.sessionId = undefined;\n<\/span><\/span><span class='shcb-loc'><span>      await this.initializeSession();\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>      return this.rpcCall(\"tools\/call\", {\n<\/span><\/span><span class='shcb-loc'><span>        name,\n<\/span><\/span><span class='shcb-loc'><span>        arguments: args,\n<\/span><\/span><span class='shcb-loc'><span>      });\n<\/span><\/span><span class='shcb-loc'><span>    }\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  \/\/ search - wraps callTool, returning both raw payload and normalized results\n<\/span><\/span><span class='shcb-loc'><span>  async search(args: SearchArgs): Promise&lt;{\n<\/span><\/span><span class='shcb-loc'><span>    raw: unknown;\n<\/span><\/span><span class='shcb-loc'><span>    results: SearchResultItem&#91;];\n<\/span><\/span><span class='shcb-loc'><span>  }&gt; {\n<\/span><\/span><span class='shcb-loc'><span>    const raw = await this.callTool(\"search\", args as Record&lt;string, unknown&gt;);\n<\/span><\/span><span class='shcb-loc'><span>    const results = extractResultsFromMcpResult(raw);\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    return { raw, results };\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  \/\/ fetch - delegates directly to callTool\n<\/span><\/span><span class='shcb-loc'><span>  async fetch(args: FetchArgs): Promise&lt;unknown&gt; {\n<\/span><\/span><span class='shcb-loc'><span>    return this.callTool(\"fetch\", args as Record&lt;string, unknown&gt;);\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><\/pre><\/details>\n\n\n\n<p>To connect our chatbot to Smart Search AI MCP, we need a small client that communicates with the MCP server. While MCP uses <a href=\"https:\/\/www.jsonrpc.org\/\">JSON-RPC<\/a> under the hood, we don\u2019t want the rest of our application dealing with request envelopes, headers, or response parsing.<\/p>\n\n\n\n<p>Instead, we wrap the MCP endpoint in a lightweight TypeScript client that exposes a simple API: <code>search()<\/code> and <code>fetch()<\/code>.<\/p>\n\n\n\n<p>This is a lot of code. Let\u2019s focus on a few key blocks.<\/p>\n\n\n\n<p>We start by defining TypeScript types that describe the parameters expected by the Smart Search MCP tools.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">export<\/span> type SearchArgs = {\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attr\">query<\/span>: string;\n<\/span><\/span><span class='shcb-loc'><span>  filter?: string;\n<\/span><\/span><span class='shcb-loc'><span>  limit?: number;\n<\/span><\/span><span class='shcb-loc'><span>  offset?: number;\n<\/span><\/span><span class='shcb-loc'><span>};\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-keyword\">export<\/span> type FetchArgs = {\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attr\">id<\/span>: string;\n<\/span><\/span><span class='shcb-loc'><span>};\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>These types map directly to the Smart Search MCP tools:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>search<\/strong>: Query the Smart Search index<\/li>\n\n\n\n<li><strong>fetch<\/strong>: &nbsp; Retrieve the full document content by ID<\/li>\n<\/ul>\n\n\n\n<p>The search tool supports optional filters and pagination, which allows the chatbot to narrow results by things like post type or retrieve additional pages of results.<\/p>\n\n\n\n<p>Defining these argument types upfront ensures that every request sent to the MCP server follows the expected structure.<\/p>\n\n\n\n<p>The next thing to note is that the Smart Search MCP server uses JSON-RPC, so every tool call must be wrapped in a JSON-RPC request.<\/p>\n\n\n\n<p>To handle that, the client implements a helper method called <code>rpcCall()<\/code>:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span>private <span class=\"hljs-keyword\">async<\/span> rpcCall(method: string, params?: unknown): <span class=\"hljs-built_in\">Promise<\/span>&lt;unknown&gt; {\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> reqBody = {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attr\">jsonrpc<\/span>: <span class=\"hljs-string\">\"2.0\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attr\">id<\/span>: <span class=\"hljs-string\">`req_<span class=\"hljs-subst\">${<span class=\"hljs-built_in\">Date<\/span>.now()}<\/span>_<span class=\"hljs-subst\">${<span class=\"hljs-built_in\">Math<\/span>.random().toString(<span class=\"hljs-number\">36<\/span>).slice(<span class=\"hljs-number\">2<\/span>)}<\/span>`<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>    method,\n<\/span><\/span><span class='shcb-loc'><span>    params,\n<\/span><\/span><span class='shcb-loc'><span>  };\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> response = <span class=\"hljs-keyword\">await<\/span> fetch(<span class=\"hljs-keyword\">this<\/span>.url, {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attr\">method<\/span>: <span class=\"hljs-string\">\"POST\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attr\">headers<\/span>: <span class=\"hljs-keyword\">this<\/span>.buildHeaders(),\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attr\">body<\/span>: <span class=\"hljs-built_in\">JSON<\/span>.stringify(reqBody),\n<\/span><\/span><span class='shcb-loc'><span>  });\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">this<\/span>.captureSessionId(response);\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (!response.ok) {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">const<\/span> bodyText = <span class=\"hljs-keyword\">await<\/span> response.text();\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-string\">`MCP HTTP <span class=\"hljs-subst\">${response.status}<\/span>: <span class=\"hljs-subst\">${response.statusText}<\/span> - <span class=\"hljs-subst\">${bodyText}<\/span>`<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>    );\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> payload = <span class=\"hljs-keyword\">await<\/span> response.json();\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> parsed = parseJsonRpcResponse(payload);\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">return<\/span> parsed.result;\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This function acts as the transport layer for all MCP interactions. It:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Constructs a JSON-RPC request payload<\/li>\n\n\n\n<li>Sends the request to the MCP endpoint<br>Captures the MCP session ID returned by the server<\/li>\n\n\n\n<li>Validates the HTTP response<\/li>\n\n\n\n<li>Parses the JSON-RPC result<br><\/li>\n<\/ol>\n\n\n\n<p>By centralizing this logic in one place, the rest of the application never has to worry about JSON-RPC formatting or error handling.<\/p>\n\n\n\n<p>Once the transport layer is in place, we expose a high-level method that executes the Smart Search search tool.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">async<\/span> search(args: SearchArgs): <span class=\"hljs-built_in\">Promise<\/span>&lt;{\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attr\">raw<\/span>: unknown;\n<\/span><\/span><span class='shcb-loc'><span>  results: SearchResultItem&#91;];\n<\/span><\/span><span class='shcb-loc'><span>}&gt; {\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> raw = <span class=\"hljs-keyword\">await<\/span> <span class=\"hljs-keyword\">this<\/span>.callTool(<span class=\"hljs-string\">\"search\"<\/span>, args <span class=\"hljs-keyword\">as<\/span> Record&lt;string, unknown&gt;);\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> results = extractResultsFromMcpResult(raw);\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">return<\/span> { raw, results };\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This method does three things:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Calls the MCP search tool<br>Normalizes the response into a consistent result structure<\/li>\n\n\n\n<li>Returns both the raw payload and parsed results<br><\/li>\n<\/ol>\n\n\n\n<p>The normalization step is important because MCP servers may return results in slightly different shapes depending on the environment. Converting the response into a predictable list of result objects keeps the rest of the application simple.<\/p>\n\n\n\n<p>The client also exposes a <code>fetch()<\/code> method that retrieves the full content of a document returned by the search tool.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">async<\/span> fetch(args: FetchArgs): <span class=\"hljs-built_in\">Promise<\/span>&lt;unknown&gt; {\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">this<\/span>.callTool(<span class=\"hljs-string\">\"fetch\"<\/span>, args <span class=\"hljs-keyword\">as<\/span> Record&lt;string, unknown&gt;);\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This allows the chatbot to perform workflows like searching for relevant posts, selecting specific results and retrieving document content for summarization.<\/p>\n\n\n\n<p>Although MCP requests could technically be sent directly using <code>fetch()<\/code>, wrapping the protocol in a small client provides several benefits:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Centralizes authentication and headers<\/li>\n\n\n\n<li>Handles JSON-RPC request formatting<br>Normalizes inconsistent response shapes<br>Keeps the rest of the codebase clean<br><\/li>\n<\/ul>\n\n\n\n<p>With this client in place, the chatbot can retrieve WordPress content with a simple call like this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">const<\/span> { results } = <span class=\"hljs-keyword\">await<\/span> mcpClient.search({\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attr\">query<\/span>: <span class=\"hljs-string\">\"Star Wars\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attr\">limit<\/span>: <span class=\"hljs-number\">10<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>});\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Instead of worrying about protocol mechanics, the rest of the application can focus on retrieving and presenting useful content to the user.<br><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The `lib\/tools.ts` file<\/h2>\n\n\n\n<p>The next thing we need to do is expose the MCP client\u2019s functionality that we have just created to OpenAI, as callable tools.&nbsp; This file will act as a bridge between the OpenAI tool calling interface and the Smart Search MCP client.&nbsp;<\/p>\n\n\n\n<p><br>In the <em><code>lib<\/code><\/em> folder, add a file and name it <code>`tools.ts`<\/code>.&nbsp; Copy and paste the contents of this code block here:&nbsp;<\/p>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary><\/summary>\n<p><\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">import<\/span> {\n<\/span><\/span><span class='shcb-loc'><span>  type FetchArgs,\n<\/span><\/span><span class='shcb-loc'><span>  type SearchArgs,\n<\/span><\/span><span class='shcb-loc'><span>  type SearchResultItem,\n<\/span><\/span><span class='shcb-loc'><span>  SmartSearchMcpClient,\n<\/span><\/span><span class='shcb-loc'><span>} <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@\/lib\/mcp\"<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-comment\">\/\/ ChatTool - schema matching OpenAI function-tool format<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-keyword\">export<\/span> type ChatTool = {\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attr\">type<\/span>: <span class=\"hljs-string\">\"function\"<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span>: <\/span>{\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attr\">name<\/span>: string;\n<\/span><\/span><span class='shcb-loc'><span>    description: string;\n<\/span><\/span><span class='shcb-loc'><span>    parameters: {\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-attr\">type<\/span>: <span class=\"hljs-string\">\"object\"<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>      properties: Record&lt;string, unknown&gt;;\n<\/span><\/span><span class='shcb-loc'><span>      required?: string&#91;];\n<\/span><\/span><span class='shcb-loc'><span>      additionalProperties?: boolean;\n<\/span><\/span><span class='shcb-loc'><span>    };\n<\/span><\/span><span class='shcb-loc'><span>  };\n<\/span><\/span><span class='shcb-loc'><span>};\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-comment\">\/\/ OPENAI_TOOLS - canonical tool list exposed to the model (smart_search_search and smart_search_fetch)<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> OPENAI_TOOLS: ChatTool&#91;] = &#91;\n<\/span><\/span><span class='shcb-loc'><span>  {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attr\">type<\/span>: <span class=\"hljs-string\">\"function\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attr\">function<\/span>: {\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-attr\">name<\/span>: <span class=\"hljs-string\">\"smart_search_search\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-attr\">description<\/span>:\n<\/span><\/span><span class='shcb-loc'><span>        <span class=\"hljs-string\">\"Searches indexed WordPress content via Smart Search MCP. Use this for list\/find\/search requests.\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-attr\">parameters<\/span>: {\n<\/span><\/span><span class='shcb-loc'><span>        <span class=\"hljs-attr\">type<\/span>: <span class=\"hljs-string\">\"object\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>        <span class=\"hljs-attr\">properties<\/span>: {\n<\/span><\/span><span class='shcb-loc'><span>          <span class=\"hljs-attr\">query<\/span>: { <span class=\"hljs-attr\">type<\/span>: <span class=\"hljs-string\">\"string\"<\/span> },\n<\/span><\/span><span class='shcb-loc'><span>          <span class=\"hljs-attr\">filter<\/span>: { <span class=\"hljs-attr\">type<\/span>: <span class=\"hljs-string\">\"string\"<\/span> },\n<\/span><\/span><span class='shcb-loc'><span>          <span class=\"hljs-attr\">limit<\/span>: { <span class=\"hljs-attr\">type<\/span>: <span class=\"hljs-string\">\"number\"<\/span> },\n<\/span><\/span><span class='shcb-loc'><span>          <span class=\"hljs-attr\">offset<\/span>: { <span class=\"hljs-attr\">type<\/span>: <span class=\"hljs-string\">\"number\"<\/span> },\n<\/span><\/span><span class='shcb-loc'><span>        },\n<\/span><\/span><span class='shcb-loc'><span>        <span class=\"hljs-attr\">required<\/span>: &#91;<span class=\"hljs-string\">\"query\"<\/span>],\n<\/span><\/span><span class='shcb-loc'><span>        <span class=\"hljs-attr\">additionalProperties<\/span>: <span class=\"hljs-literal\">false<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>      },\n<\/span><\/span><span class='shcb-loc'><span>    },\n<\/span><\/span><span class='shcb-loc'><span>  },\n<\/span><\/span><span class='shcb-loc'><span>  {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attr\">type<\/span>: <span class=\"hljs-string\">\"function\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attr\">function<\/span>: {\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-attr\">name<\/span>: <span class=\"hljs-string\">\"smart_search_fetch\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-attr\">description<\/span>:\n<\/span><\/span><span class='shcb-loc'><span>        <span class=\"hljs-string\">\"Fetches full indexed document content by Smart Search result id.\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-attr\">parameters<\/span>: {\n<\/span><\/span><span class='shcb-loc'><span>        <span class=\"hljs-attr\">type<\/span>: <span class=\"hljs-string\">\"object\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>        <span class=\"hljs-attr\">properties<\/span>: {\n<\/span><\/span><span class='shcb-loc'><span>          <span class=\"hljs-attr\">id<\/span>: { <span class=\"hljs-attr\">type<\/span>: <span class=\"hljs-string\">\"string\"<\/span> },\n<\/span><\/span><span class='shcb-loc'><span>        },\n<\/span><\/span><span class='shcb-loc'><span>        <span class=\"hljs-attr\">required<\/span>: &#91;<span class=\"hljs-string\">\"id\"<\/span>],\n<\/span><\/span><span class='shcb-loc'><span>        <span class=\"hljs-attr\">additionalProperties<\/span>: <span class=\"hljs-literal\">false<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>      },\n<\/span><\/span><span class='shcb-loc'><span>    },\n<\/span><\/span><span class='shcb-loc'><span>  },\n<\/span><\/span><span class='shcb-loc'><span>];\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-comment\">\/\/ ToolExecutionContext - carries MCP client instance and mutable lastSearchState for pagination\/follow-ups<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-keyword\">export<\/span> type ToolExecutionContext = {\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attr\">mcpClient<\/span>: SmartSearchMcpClient;\n<\/span><\/span><span class='shcb-loc'><span>  lastSearchState: {\n<\/span><\/span><span class='shcb-loc'><span>    query?: string;\n<\/span><\/span><span class='shcb-loc'><span>    filter?: string;\n<\/span><\/span><span class='shcb-loc'><span>    limit?: number;\n<\/span><\/span><span class='shcb-loc'><span>    offset?: number;\n<\/span><\/span><span class='shcb-loc'><span>    results?: SearchResultItem&#91;];\n<\/span><\/span><span class='shcb-loc'><span>  };\n<\/span><\/span><span class='shcb-loc'><span>};\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-comment\">\/\/ normalizeSearchArgs - validates and sanitizes search input (requires non-empty query, trims strings, clamps limit 1-50)<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">normalizeSearchArgs<\/span>(<span class=\"hljs-params\">raw: unknown<\/span>): <span class=\"hljs-title\">SearchArgs<\/span> <\/span>{\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (!raw || <span class=\"hljs-keyword\">typeof<\/span> raw !== <span class=\"hljs-string\">\"object\"<\/span>) {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(<span class=\"hljs-string\">\"Invalid arguments for smart_search_search.\"<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> input = raw <span class=\"hljs-keyword\">as<\/span> Record&lt;string, unknown&gt;;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> query = input.query;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (<span class=\"hljs-keyword\">typeof<\/span> query !== <span class=\"hljs-string\">\"string\"<\/span> || !query.trim()) {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(<span class=\"hljs-string\">\"smart_search_search requires a non-empty query string.\"<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> out: SearchArgs = { <span class=\"hljs-attr\">query<\/span>: query.trim() };\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (<span class=\"hljs-keyword\">typeof<\/span> input.filter === <span class=\"hljs-string\">\"string\"<\/span> &amp;&amp; input.filter.trim()) {\n<\/span><\/span><span class='shcb-loc'><span>    out.filter = input.filter.trim();\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (<span class=\"hljs-keyword\">typeof<\/span> input.limit === <span class=\"hljs-string\">\"number\"<\/span> &amp;&amp; <span class=\"hljs-built_in\">Number<\/span>.isFinite(input.limit)) {\n<\/span><\/span><span class='shcb-loc'><span>    out.limit = <span class=\"hljs-built_in\">Math<\/span>.max(<span class=\"hljs-number\">1<\/span>, <span class=\"hljs-built_in\">Math<\/span>.min(<span class=\"hljs-number\">50<\/span>, <span class=\"hljs-built_in\">Math<\/span>.floor(input.limit)));\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (<span class=\"hljs-keyword\">typeof<\/span> input.offset === <span class=\"hljs-string\">\"number\"<\/span> &amp;&amp; <span class=\"hljs-built_in\">Number<\/span>.isFinite(input.offset)) {\n<\/span><\/span><span class='shcb-loc'><span>    out.offset = <span class=\"hljs-built_in\">Math<\/span>.max(<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-built_in\">Math<\/span>.floor(input.offset));\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">return<\/span> out;\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-comment\">\/\/ normalizeFetchArgs - validates non-empty id string<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">normalizeFetchArgs<\/span>(<span class=\"hljs-params\">raw: unknown<\/span>): <span class=\"hljs-title\">FetchArgs<\/span> <\/span>{\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (!raw || <span class=\"hljs-keyword\">typeof<\/span> raw !== <span class=\"hljs-string\">\"object\"<\/span>) {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(<span class=\"hljs-string\">\"Invalid arguments for smart_search_fetch.\"<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> input = raw <span class=\"hljs-keyword\">as<\/span> Record&lt;string, unknown&gt;;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> id = input.id;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (<span class=\"hljs-keyword\">typeof<\/span> id !== <span class=\"hljs-string\">\"string\"<\/span> || !id.trim()) {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(<span class=\"hljs-string\">\"smart_search_fetch requires a non-empty id string.\"<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">return<\/span> { <span class=\"hljs-attr\">id<\/span>: id.trim() };\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-comment\">\/\/ executeTool - routes by tool name, normalizes args, calls MCP, updates lastSearchState, returns structured output<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">executeTool<\/span>(<span class=\"hljs-params\"><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">  toolName: string,<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">  rawArgs: unknown,<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">  ctx: ToolExecutionContext,<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\"><\/span>): <span class=\"hljs-title\">Promise<\/span>&lt;<span class=\"hljs-title\">unknown<\/span>&gt; <\/span>{\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (toolName === <span class=\"hljs-string\">\"smart_search_search\"<\/span>) {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">const<\/span> args = normalizeSearchArgs(rawArgs);\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">const<\/span> data = <span class=\"hljs-keyword\">await<\/span> ctx.mcpClient.search(args);\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    ctx.lastSearchState.query = args.query;\n<\/span><\/span><span class='shcb-loc'><span>    ctx.lastSearchState.filter = args.filter;\n<\/span><\/span><span class='shcb-loc'><span>    ctx.lastSearchState.limit = args.limit ?? <span class=\"hljs-number\">10<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>    ctx.lastSearchState.offset = args.offset ?? <span class=\"hljs-number\">0<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>    ctx.lastSearchState.results = data.results;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">return<\/span> {\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-attr\">tool<\/span>: <span class=\"hljs-string\">\"smart_search_search\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>      args,\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-attr\">results<\/span>: data.results,\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-attr\">raw<\/span>: data.raw,\n<\/span><\/span><span class='shcb-loc'><span>    };\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (toolName === <span class=\"hljs-string\">\"smart_search_fetch\"<\/span>) {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">const<\/span> args = normalizeFetchArgs(rawArgs);\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">const<\/span> data = <span class=\"hljs-keyword\">await<\/span> ctx.mcpClient.fetch(args);\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">return<\/span> {\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-attr\">tool<\/span>: <span class=\"hljs-string\">\"smart_search_fetch\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>      args,\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-attr\">result<\/span>: data,\n<\/span><\/span><span class='shcb-loc'><span>    };\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(<span class=\"hljs-string\">`Unknown tool: <span class=\"hljs-subst\">${toolName}<\/span>`<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre><\/details>\n\n\n\n<p>Let\u2019s break down the important parts of the block.&nbsp;&nbsp;<\/p>\n\n\n\n<p>We start by defining two OpenAI function tools: one for <em>search<\/em> and one for <em>fetch<\/em>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span>export <span class=\"hljs-keyword\">const<\/span> OPENAI_TOOLS: ChatTool&#91;] = &#91;\n<\/span><\/span><span class='shcb-loc'><span>  {\n<\/span><\/span><span class='shcb-loc'><span>    type: <span class=\"hljs-string\">\"function\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span>: <\/span>{\n<\/span><\/span><span class='shcb-loc'><span>      name: <span class=\"hljs-string\">\"smart_search_search\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>      description:\n<\/span><\/span><span class='shcb-loc'><span>        <span class=\"hljs-string\">\"Searches indexed WordPress content via Smart Search MCP. Use this for list\/find\/search requests.\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>      parameters: {\n<\/span><\/span><span class='shcb-loc'><span>        type: <span class=\"hljs-string\">\"object\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>        properties: {\n<\/span><\/span><span class='shcb-loc'><span>          query: { type: <span class=\"hljs-string\">\"string\"<\/span> },\n<\/span><\/span><span class='shcb-loc'><span>          filter: { type: <span class=\"hljs-string\">\"string\"<\/span> },\n<\/span><\/span><span class='shcb-loc'><span>          limit: { type: <span class=\"hljs-string\">\"number\"<\/span> },\n<\/span><\/span><span class='shcb-loc'><span>          offset: { type: <span class=\"hljs-string\">\"number\"<\/span> },\n<\/span><\/span><span class='shcb-loc'><span>        },\n<\/span><\/span><span class='shcb-loc'><span>        required: &#91;<span class=\"hljs-string\">\"query\"<\/span>],\n<\/span><\/span><span class='shcb-loc'><span>        additionalProperties: <span class=\"hljs-keyword\">false<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>      },\n<\/span><\/span><span class='shcb-loc'><span>    },\n<\/span><\/span><span class='shcb-loc'><span>  },\n<\/span><\/span><span class='shcb-loc'><span>  {\n<\/span><\/span><span class='shcb-loc'><span>    type: <span class=\"hljs-string\">\"function\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span>: <\/span>{\n<\/span><\/span><span class='shcb-loc'><span>      name: <span class=\"hljs-string\">\"smart_search_fetch\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>      description:\n<\/span><\/span><span class='shcb-loc'><span>        <span class=\"hljs-string\">\"Fetches full indexed document content by Smart Search result id.\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>      parameters: {\n<\/span><\/span><span class='shcb-loc'><span>        type: <span class=\"hljs-string\">\"object\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>        properties: {\n<\/span><\/span><span class='shcb-loc'><span>          id: { type: <span class=\"hljs-string\">\"string\"<\/span> },\n<\/span><\/span><span class='shcb-loc'><span>        },\n<\/span><\/span><span class='shcb-loc'><span>        required: &#91;<span class=\"hljs-string\">\"id\"<\/span>],\n<\/span><\/span><span class='shcb-loc'><span>        additionalProperties: <span class=\"hljs-keyword\">false<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>      },\n<\/span><\/span><span class='shcb-loc'><span>    },\n<\/span><\/span><span class='shcb-loc'><span>  },\n<\/span><\/span><span class='shcb-loc'><span>];\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>These definitions tell the model exactly what capabilities are available.<\/p>\n\n\n\n<p>In practice, that means:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>smart_search_search<\/code> is used when a user asks to list or find content<br><\/li>\n\n\n\n<li><code>smart_search_fetch<\/code> is used when the model needs the full content of a specific result<br><\/li>\n<\/ul>\n\n\n\n<p>This is the layer that makes Smart Search MCP usable from an OpenAI agent loop.<\/p>\n\n\n\n<p>The next step is to validate the tool arguments that the model generates before sending them to the MCP server.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">normalizeSearchArgs<\/span>(<span class=\"hljs-params\">raw: unknown<\/span>): <span class=\"hljs-title\">SearchArgs<\/span> <\/span>{\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (!raw || <span class=\"hljs-keyword\">typeof<\/span> raw !== <span class=\"hljs-string\">\"object\"<\/span>) {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(<span class=\"hljs-string\">\"Invalid arguments for smart_search_search.\"<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> input = raw <span class=\"hljs-keyword\">as<\/span> Record&lt;string, unknown&gt;;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> query = input.query;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (<span class=\"hljs-keyword\">typeof<\/span> query !== <span class=\"hljs-string\">\"string\"<\/span> || !query.trim()) {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(<span class=\"hljs-string\">\"smart_search_search requires a non-empty query string.\"<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> out: SearchArgs = { <span class=\"hljs-attr\">query<\/span>: query.trim() };\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (<span class=\"hljs-keyword\">typeof<\/span> input.filter === <span class=\"hljs-string\">\"string\"<\/span> &amp;&amp; input.filter.trim()) {\n<\/span><\/span><span class='shcb-loc'><span>    out.filter = input.filter.trim();\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (<span class=\"hljs-keyword\">typeof<\/span> input.limit === <span class=\"hljs-string\">\"number\"<\/span> &amp;&amp; <span class=\"hljs-built_in\">Number<\/span>.isFinite(input.limit)) {\n<\/span><\/span><span class='shcb-loc'><span>    out.limit = <span class=\"hljs-built_in\">Math<\/span>.max(<span class=\"hljs-number\">1<\/span>, <span class=\"hljs-built_in\">Math<\/span>.min(<span class=\"hljs-number\">50<\/span>, <span class=\"hljs-built_in\">Math<\/span>.floor(input.limit)));\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (<span class=\"hljs-keyword\">typeof<\/span> input.offset === <span class=\"hljs-string\">\"number\"<\/span> &amp;&amp; <span class=\"hljs-built_in\">Number<\/span>.isFinite(input.offset)) {\n<\/span><\/span><span class='shcb-loc'><span>    out.offset = <span class=\"hljs-built_in\">Math<\/span>.max(<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-built_in\">Math<\/span>.floor(input.offset));\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">return<\/span> out;\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This helper ensures the generated arguments are safe and usable.<\/p>\n\n\n\n<p>For example, it:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>requires a non-empty query<br><\/li>\n\n\n\n<li>trims whitespace<br><\/li>\n\n\n\n<li>clamps limit to a sensible range<br><\/li>\n\n\n\n<li>prevents negative offsets<br><\/li>\n<\/ul>\n\n\n\n<p>That validation step is important because tool calling still needs guardrails. The model may choose the right tool, but the application should remain responsible for enforcing valid inputs.<\/p>\n\n\n\n<p>Finally, we can execute the selected tool. The file exposes an <code>executeTool()<\/code> function that maps the model\u2019s selected tool to the correct MCP client method.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">executeTool<\/span>(<span class=\"hljs-params\"><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">  toolName: string,<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">  rawArgs: unknown,<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">  ctx: ToolExecutionContext,<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\"><\/span>): <span class=\"hljs-title\">Promise<\/span>&lt;<span class=\"hljs-title\">unknown<\/span>&gt; <\/span>{\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (toolName === <span class=\"hljs-string\">\"smart_search_search\"<\/span>) {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">const<\/span> args = normalizeSearchArgs(rawArgs);\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">const<\/span> data = <span class=\"hljs-keyword\">await<\/span> ctx.mcpClient.search(args);\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    ctx.lastSearchState.query = args.query;\n<\/span><\/span><span class='shcb-loc'><span>    ctx.lastSearchState.filter = args.filter;\n<\/span><\/span><span class='shcb-loc'><span>    ctx.lastSearchState.limit = args.limit ?? <span class=\"hljs-number\">10<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>    ctx.lastSearchState.offset = args.offset ?? <span class=\"hljs-number\">0<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>    ctx.lastSearchState.results = data.results;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">return<\/span> {\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-attr\">tool<\/span>: <span class=\"hljs-string\">\"smart_search_search\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>      args,\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-attr\">results<\/span>: data.results,\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-attr\">raw<\/span>: data.raw,\n<\/span><\/span><span class='shcb-loc'><span>    };\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (toolName === <span class=\"hljs-string\">\"smart_search_fetch\"<\/span>) {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">const<\/span> args = normalizeFetchArgs(rawArgs);\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">const<\/span> data = <span class=\"hljs-keyword\">await<\/span> ctx.mcpClient.fetch(args);\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">return<\/span> {\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-attr\">tool<\/span>: <span class=\"hljs-string\">\"smart_search_fetch\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>      args,\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-attr\">result<\/span>: data,\n<\/span><\/span><span class='shcb-loc'><span>    };\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(<span class=\"hljs-string\">`Unknown tool: <span class=\"hljs-subst\">${toolName}<\/span>`<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This function is the final bridge between OpenAI tool calling and Smart Search MCP.<\/p>\n\n\n\n<p>It:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>receives the tool selected by the model<br><\/li>\n\n\n\n<li>validates the arguments<br><\/li>\n\n\n\n<li>executes the correct MCP call<br><\/li>\n\n\n\n<li>returns structured results back to the agent loop<br><\/li>\n<\/ol>\n\n\n\n<p>The <code>lastSearchState<\/code> object also keeps track of the previous search query and results, which makes it easier to support conversational follow-ups like pagination or summarizing a prior result.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The `route.ts` file<\/h2>\n\n\n\n<p>The final file we will need to create and go over is our layer that orchestrates the MCP client and OpenAI tool. This will be our Next.js route.<\/p>\n\n\n\n<p>Go to your <code>app\/api <\/code>folder. Create a subfolder in <code>api\/<\/code> named <code>\/chat<\/code> and then create a file called <code>\/route.ts<\/code>.&nbsp;<\/p>\n\n\n\n<p>Copy and paste this code block in that file:<\/p>\n\n\n\n<details class=\"wp-block-details is-layout-flow wp-block-details-is-layout-flow\"><summary><\/summary><pre class=\"wp-block-code\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span>import { OPENAI_TOOLS, executeTool } from \"@\/lib\/tools\";\n<\/span><\/span><span class='shcb-loc'><span>import { SmartSearchMcpClient, type SearchResultItem } from \"@\/lib\/mcp\";\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>export const runtime = \"nodejs\";\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>\/\/ ClientMessage - conversation turn type for request body\n<\/span><\/span><span class='shcb-loc'><span>type ClientMessage = {\n<\/span><\/span><span class='shcb-loc'><span>  role: \"user\" | \"assistant\";\n<\/span><\/span><span class='shcb-loc'><span>  content: string;\n<\/span><\/span><span class='shcb-loc'><span>};\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>\/\/ RequestBody - incoming POST body with message, history, and lastSearchState\n<\/span><\/span><span class='shcb-loc'><span>type RequestBody = {\n<\/span><\/span><span class='shcb-loc'><span>  message: string;\n<\/span><\/span><span class='shcb-loc'><span>  history?: ClientMessage&#91;];\n<\/span><\/span><span class='shcb-loc'><span>  state?: {\n<\/span><\/span><span class='shcb-loc'><span>    lastSearch?: {\n<\/span><\/span><span class='shcb-loc'><span>      query?: string;\n<\/span><\/span><span class='shcb-loc'><span>      filter?: string;\n<\/span><\/span><span class='shcb-loc'><span>      limit?: number;\n<\/span><\/span><span class='shcb-loc'><span>      offset?: number;\n<\/span><\/span><span class='shcb-loc'><span>      results?: SearchResultItem&#91;];\n<\/span><\/span><span class='shcb-loc'><span>    };\n<\/span><\/span><span class='shcb-loc'><span>  };\n<\/span><\/span><span class='shcb-loc'><span>};\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>\/\/ OpenAIMessage - OpenAI message format including tool calls\n<\/span><\/span><span class='shcb-loc'><span>type OpenAIMessage = {\n<\/span><\/span><span class='shcb-loc'><span>  role: \"system\" | \"user\" | \"assistant\" | \"tool\";\n<\/span><\/span><span class='shcb-loc'><span>  content?: string;\n<\/span><\/span><span class='shcb-loc'><span>  tool_call_id?: string;\n<\/span><\/span><span class='shcb-loc'><span>  tool_calls?: Array<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">{<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">    <span class=\"hljs-attr\">id:<\/span> <span class=\"hljs-attr\">string<\/span>;<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">    <span class=\"hljs-attr\">type:<\/span> \"<span class=\"hljs-attr\">function<\/span>\";<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">    <span class=\"hljs-attr\">function:<\/span> {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">      <span class=\"hljs-attr\">name:<\/span> <span class=\"hljs-attr\">string<\/span>;<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">      <span class=\"hljs-attr\">arguments:<\/span> <span class=\"hljs-attr\">string<\/span>;<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">    };<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">  }&gt;<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>};\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>const DEFAULT_MODEL = \"gpt-4.1-mini\";\n<\/span><\/span><span class='shcb-loc'><span>const DEFAULT_LIMIT = 10;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>\/\/ getCorsHeaders - enforces single allowed WP Admin origin\n<\/span><\/span><span class='shcb-loc'><span>function getCorsHeaders(origin: string | null): HeadersInit {\n<\/span><\/span><span class='shcb-loc'><span>  const allowOrigin = process.env.WP_ADMIN_ORIGIN ?? \"\";\n<\/span><\/span><span class='shcb-loc'><span>  const safeOrigin = origin &amp;&amp; origin === allowOrigin ? origin : allowOrigin;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  return {\n<\/span><\/span><span class='shcb-loc'><span>    \"Access-Control-Allow-Origin\": safeOrigin,\n<\/span><\/span><span class='shcb-loc'><span>    \"Access-Control-Allow-Methods\": \"POST, OPTIONS\",\n<\/span><\/span><span class='shcb-loc'><span>    \"Access-Control-Allow-Headers\": \"Content-Type, x-wp-admin-copilot-token\",\n<\/span><\/span><span class='shcb-loc'><span>    Vary: \"Origin\",\n<\/span><\/span><span class='shcb-loc'><span>  };\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>\/\/ isNonIndexedAdminQuery - detects queries about users, roles, plugins, settings (non-indexed domains)\n<\/span><\/span><span class='shcb-loc'><span>function isNonIndexedAdminQuery(input: string): boolean {\n<\/span><\/span><span class='shcb-loc'><span>  return \/\\b(users?|roles?|plugins?|settings?|capabilities|permissions?)\\b\/i.test(\n<\/span><\/span><span class='shcb-loc'><span>    input,\n<\/span><\/span><span class='shcb-loc'><span>  );\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>\/\/ shouldHandleMoreQuery - detects pagination requests ('more', 'next page', etc.)\n<\/span><\/span><span class='shcb-loc'><span>function shouldHandleMoreQuery(input: string): boolean {\n<\/span><\/span><span class='shcb-loc'><span>  return \/\\b(more|next page|next results|more results|show more)\\b\/i.test(input);\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>\/\/ shouldHandleSummarizeNth - detects 'summarize the nth result' requests\n<\/span><\/span><span class='shcb-loc'><span>function shouldHandleSummarizeNth(\n<\/span><\/span><span class='shcb-loc'><span>  input: string,\n<\/span><\/span><span class='shcb-loc'><span>): { index: number } | undefined {\n<\/span><\/span><span class='shcb-loc'><span>  const lower = input.toLowerCase();\n<\/span><\/span><span class='shcb-loc'><span>  const wordMap: Record<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">string,<\/span> <span class=\"hljs-attr\">number<\/span>&gt;<\/span> = {\n<\/span><\/span><span class='shcb-loc'><span>    first: 1,\n<\/span><\/span><span class='shcb-loc'><span>    second: 2,\n<\/span><\/span><span class='shcb-loc'><span>    third: 3,\n<\/span><\/span><span class='shcb-loc'><span>    fourth: 4,\n<\/span><\/span><span class='shcb-loc'><span>    fifth: 5,\n<\/span><\/span><span class='shcb-loc'><span>    sixth: 6,\n<\/span><\/span><span class='shcb-loc'><span>    seventh: 7,\n<\/span><\/span><span class='shcb-loc'><span>    eighth: 8,\n<\/span><\/span><span class='shcb-loc'><span>    ninth: 9,\n<\/span><\/span><span class='shcb-loc'><span>    tenth: 10,\n<\/span><\/span><span class='shcb-loc'><span>  };\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  const wordMatch = lower.match(\n<\/span><\/span><span class='shcb-loc'><span>    \/\\b(summarize|summary of)\\s+(the\\s+)?(first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth)\\s+result\\b\/,\n<\/span><\/span><span class='shcb-loc'><span>  );\n<\/span><\/span><span class='shcb-loc'><span>  if (wordMatch) {\n<\/span><\/span><span class='shcb-loc'><span>    return { index: wordMap&#91;wordMatch&#91;3]] - 1 };\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  const numericMatch = lower.match(\n<\/span><\/span><span class='shcb-loc'><span>    \/\\b(summarize|summary of)\\s+(the\\s+)?(\\d+)(st|nd|rd|th)?\\s+result\\b\/,\n<\/span><\/span><span class='shcb-loc'><span>  );\n<\/span><\/span><span class='shcb-loc'><span>  if (!numericMatch) {\n<\/span><\/span><span class='shcb-loc'><span>    return undefined;\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  const parsed = Number.parseInt(numericMatch&#91;3], 10);\n<\/span><\/span><span class='shcb-loc'><span>  if (!Number.isFinite(parsed) || parsed <span class=\"hljs-tag\">&lt; <span class=\"hljs-attr\">1<\/span>) {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">    <span class=\"hljs-attr\">return<\/span> <span class=\"hljs-attr\">undefined<\/span>;<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">  }<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\"><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">  <span class=\"hljs-attr\">return<\/span> { <span class=\"hljs-attr\">index:<\/span> <span class=\"hljs-attr\">parsed<\/span> <span class=\"hljs-attr\">-<\/span> <span class=\"hljs-attr\">1<\/span> };<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">}<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\"><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">\/\/ <span class=\"hljs-attr\">openAiChatCompletion<\/span> <span class=\"hljs-attr\">-<\/span> <span class=\"hljs-attr\">sends<\/span> <span class=\"hljs-attr\">messages<\/span> <span class=\"hljs-attr\">and<\/span> <span class=\"hljs-attr\">tools<\/span> <span class=\"hljs-attr\">to<\/span> <span class=\"hljs-attr\">OpenAI<\/span> <span class=\"hljs-attr\">API<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\"><span class=\"hljs-attr\">async<\/span> <span class=\"hljs-attr\">function<\/span> <span class=\"hljs-attr\">openAiChatCompletion<\/span>(<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">  <span class=\"hljs-attr\">messages:<\/span> <span class=\"hljs-attr\">OpenAIMessage<\/span>&#91;],<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">  <span class=\"hljs-attr\">model:<\/span> <span class=\"hljs-attr\">string<\/span>,<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">  <span class=\"hljs-attr\">tools<\/span> = <span class=\"hljs-string\">OPENAI_TOOLS,<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">) {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">  <span class=\"hljs-attr\">const<\/span> <span class=\"hljs-attr\">apiKey<\/span> = <span class=\"hljs-string\">process.env.OPENAI_API_KEY;<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">  <span class=\"hljs-attr\">if<\/span> (!<span class=\"hljs-attr\">apiKey<\/span>) {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">    <span class=\"hljs-attr\">throw<\/span> <span class=\"hljs-attr\">new<\/span> <span class=\"hljs-attr\">Error<\/span>(\"<span class=\"hljs-attr\">OPENAI_API_KEY<\/span> <span class=\"hljs-attr\">is<\/span> <span class=\"hljs-attr\">not<\/span> <span class=\"hljs-attr\">configured.<\/span>\");<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">  }<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\"><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">  <span class=\"hljs-attr\">const<\/span> <span class=\"hljs-attr\">response<\/span> = <span class=\"hljs-string\">await<\/span> <span class=\"hljs-attr\">fetch<\/span>(\"<span class=\"hljs-attr\">https:<\/span>\/\/<span class=\"hljs-attr\">api.openai.com<\/span>\/<span class=\"hljs-attr\">v1<\/span>\/<span class=\"hljs-attr\">chat<\/span>\/<span class=\"hljs-attr\">completions<\/span>\", {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">    <span class=\"hljs-attr\">method:<\/span> \"<span class=\"hljs-attr\">POST<\/span>\",<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">    <span class=\"hljs-attr\">headers:<\/span> {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">      \"<span class=\"hljs-attr\">Content-Type<\/span>\"<span class=\"hljs-attr\">:<\/span> \"<span class=\"hljs-attr\">application<\/span>\/<span class=\"hljs-attr\">json<\/span>\",<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">      <span class=\"hljs-attr\">Authorization:<\/span> `<span class=\"hljs-attr\">Bearer<\/span> ${<span class=\"hljs-attr\">apiKey<\/span>}`,<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">    },<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">    <span class=\"hljs-attr\">body:<\/span> <span class=\"hljs-attr\">JSON.stringify<\/span>({<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">      <span class=\"hljs-attr\">model<\/span>,<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">      <span class=\"hljs-attr\">temperature:<\/span> <span class=\"hljs-attr\">0.2<\/span>,<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">      <span class=\"hljs-attr\">tool_choice:<\/span> \"<span class=\"hljs-attr\">auto<\/span>\",<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">      <span class=\"hljs-attr\">tools<\/span>,<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">      <span class=\"hljs-attr\">messages<\/span>,<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">    }),<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">  });<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\"><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">  <span class=\"hljs-attr\">if<\/span> (!<span class=\"hljs-attr\">response.ok<\/span>) {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">    <span class=\"hljs-attr\">const<\/span> <span class=\"hljs-attr\">bodyText<\/span> = <span class=\"hljs-string\">await<\/span> <span class=\"hljs-attr\">response.text<\/span>();<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">    <span class=\"hljs-attr\">throw<\/span> <span class=\"hljs-attr\">new<\/span> <span class=\"hljs-attr\">Error<\/span>(<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">      `<span class=\"hljs-attr\">OpenAI<\/span> <span class=\"hljs-attr\">HTTP<\/span> ${<span class=\"hljs-attr\">response.status<\/span>}<span class=\"hljs-attr\">:<\/span> ${<span class=\"hljs-attr\">response.statusText<\/span>} <span class=\"hljs-attr\">-<\/span> ${<span class=\"hljs-attr\">bodyText<\/span>}`,<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">    );<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">  }<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\"><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">  <span class=\"hljs-attr\">return<\/span> <span class=\"hljs-attr\">response.json<\/span>();<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">}<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\"><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">\/\/ <span class=\"hljs-attr\">chunkText<\/span> <span class=\"hljs-attr\">-<\/span> <span class=\"hljs-attr\">splits<\/span> <span class=\"hljs-attr\">text<\/span> <span class=\"hljs-attr\">into<\/span> ~<span class=\"hljs-attr\">60-character<\/span> <span class=\"hljs-attr\">chunks<\/span> <span class=\"hljs-attr\">for<\/span> <span class=\"hljs-attr\">streaming<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\"><span class=\"hljs-attr\">function<\/span> <span class=\"hljs-attr\">chunkText<\/span>(<span class=\"hljs-attr\">input:<\/span> <span class=\"hljs-attr\">string<\/span>)<span class=\"hljs-attr\">:<\/span> <span class=\"hljs-attr\">string<\/span>&#91;] {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">  <span class=\"hljs-attr\">const<\/span> <span class=\"hljs-attr\">tokens<\/span> = <span class=\"hljs-string\">input.split(\/(\\s+)\/).filter((part)<\/span> =&gt;<\/span> part.length &gt; 0);\n<\/span><\/span><span class='shcb-loc'><span>  const chunks: string&#91;] = &#91;];\n<\/span><\/span><span class='shcb-loc'><span>  let buffer = \"\";\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  for (const token of tokens) {\n<\/span><\/span><span class='shcb-loc'><span>    if ((buffer + token).length &gt; 60 &amp;&amp; buffer) {\n<\/span><\/span><span class='shcb-loc'><span>      chunks.push(buffer);\n<\/span><\/span><span class='shcb-loc'><span>      buffer = token;\n<\/span><\/span><span class='shcb-loc'><span>    } else {\n<\/span><\/span><span class='shcb-loc'><span>      buffer += token;\n<\/span><\/span><span class='shcb-loc'><span>    }\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  if (buffer) {\n<\/span><\/span><span class='shcb-loc'><span>    chunks.push(buffer);\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  return chunks;\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>\/\/ validateRequestOrigin - enforces origin matches WP_ADMIN_ORIGIN\n<\/span><\/span><span class='shcb-loc'><span>function validateRequestOrigin(req: Request): boolean {\n<\/span><\/span><span class='shcb-loc'><span>  const allowed = process.env.WP_ADMIN_ORIGIN;\n<\/span><\/span><span class='shcb-loc'><span>  if (!allowed) {\n<\/span><\/span><span class='shcb-loc'><span>    return false;\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  const requestOrigin = req.headers.get(\"origin\");\n<\/span><\/span><span class='shcb-loc'><span>  return requestOrigin === allowed;\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>\/\/ validateCopilotToken - enforces shared token auth\n<\/span><\/span><span class='shcb-loc'><span>function validateCopilotToken(req: Request): boolean {\n<\/span><\/span><span class='shcb-loc'><span>  const expectedToken = process.env.WP_ADMIN_COPILOT_TOKEN;\n<\/span><\/span><span class='shcb-loc'><span>  if (!expectedToken) {\n<\/span><\/span><span class='shcb-loc'><span>    return false;\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  const headerToken = req.headers.get(\"x-wp-admin-copilot-token\");\n<\/span><\/span><span class='shcb-loc'><span>  if (!headerToken) {\n<\/span><\/span><span class='shcb-loc'><span>    return false;\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  return headerToken.trim() === expectedToken.trim();\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>\/\/ buildAuthDebug - provides token diagnostics in non-production\n<\/span><\/span><span class='shcb-loc'><span>function buildAuthDebug(req: Request) {\n<\/span><\/span><span class='shcb-loc'><span>  if (process.env.NODE_ENV === \"production\") {\n<\/span><\/span><span class='shcb-loc'><span>    return undefined;\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  const expected = process.env.WP_ADMIN_COPILOT_TOKEN ?? \"\";\n<\/span><\/span><span class='shcb-loc'><span>  const received = req.headers.get(\"x-wp-admin-copilot-token\") ?? \"\";\n<\/span><\/span><span class='shcb-loc'><span>  return {\n<\/span><\/span><span class='shcb-loc'><span>    headerPresent: Boolean(received),\n<\/span><\/span><span class='shcb-loc'><span>    expectedLength: expected.trim().length,\n<\/span><\/span><span class='shcb-loc'><span>    receivedLength: received.trim().length,\n<\/span><\/span><span class='shcb-loc'><span>    sameAfterTrim: received.trim() === expected.trim(),\n<\/span><\/span><span class='shcb-loc'><span>  };\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>\/\/ mcpClientFromEnv - creates MCP client from environment variables\n<\/span><\/span><span class='shcb-loc'><span>function mcpClientFromEnv() {\n<\/span><\/span><span class='shcb-loc'><span>  const mcpUrl = process.env.SMART_SEARCH_MCP_URL;\n<\/span><\/span><span class='shcb-loc'><span>  if (!mcpUrl) {\n<\/span><\/span><span class='shcb-loc'><span>    throw new Error(\"SMART_SEARCH_MCP_URL is not configured.\");\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  return new SmartSearchMcpClient(mcpUrl, process.env.SMART_SEARCH_MCP_TOKEN);\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>\/\/ normalizeHistory - filters and limits conversation history to last 20 turns\n<\/span><\/span><span class='shcb-loc'><span>function normalizeHistory(history: unknown): ClientMessage&#91;] {\n<\/span><\/span><span class='shcb-loc'><span>  if (!Array.isArray(history)) {\n<\/span><\/span><span class='shcb-loc'><span>    return &#91;];\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  return history\n<\/span><\/span><span class='shcb-loc'><span>    .filter((entry): entry is ClientMessage =&gt; {\n<\/span><\/span><span class='shcb-loc'><span>      if (!entry || typeof entry !== \"object\") {\n<\/span><\/span><span class='shcb-loc'><span>        return false;\n<\/span><\/span><span class='shcb-loc'><span>      }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>      const typed = entry as Record<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">string,<\/span> <span class=\"hljs-attr\">unknown<\/span>&gt;<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>      return (\n<\/span><\/span><span class='shcb-loc'><span>        (typed.role === \"user\" || typed.role === \"assistant\") &amp;&amp;\n<\/span><\/span><span class='shcb-loc'><span>        typeof typed.content === \"string\"\n<\/span><\/span><span class='shcb-loc'><span>      );\n<\/span><\/span><span class='shcb-loc'><span>    })\n<\/span><\/span><span class='shcb-loc'><span>    .slice(-20);\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>\/\/ buildSystemPrompt - defines behavioral policy (semantic search, available tools, limitations)\n<\/span><\/span><span class='shcb-loc'><span>function buildSystemPrompt(): string {\n<\/span><\/span><span class='shcb-loc'><span>  return &#91;\n<\/span><\/span><span class='shcb-loc'><span>    \"You are WP Admin Copilot for indexed content retrieval.\",\n<\/span><\/span><span class='shcb-loc'><span>    \"You have access to Smart Search AI MCP with semantic\/hybrid search capabilities.\",\n<\/span><\/span><span class='shcb-loc'><span>    \"Available tools: smart_search_search (searches content) and smart_search_fetch (retrieves full document by ID).\",\n<\/span><\/span><span class='shcb-loc'><span>    \"IMPORTANT: Smart Search AI uses vector\/semantic search. Pass natural language queries directly to smart_search_search.\",\n<\/span><\/span><span class='shcb-loc'><span>    \"Do NOT use filters unless explicitly needed. The 'query' parameter accepts full natural language.\",\n<\/span><\/span><span class='shcb-loc'><span>    \"Examples: 'Return of the Jedi', 'Star Wars movies', 'posts about AI'.\",\n<\/span><\/span><span class='shcb-loc'><span>    \"Smart Search AI handles semantic understanding, acronyms, and typos automatically.\",\n<\/span><\/span><span class='shcb-loc'><span>    \"Present results with title, URL, and snippet. Never invent or modify these values.\",\n<\/span><\/span><span class='shcb-loc'><span>    \"Use smart_search_fetch with a document id when users ask to summarize a specific result.\",\n<\/span><\/span><span class='shcb-loc'><span>    \"If asked about users, roles, plugins, or settings, explain you only have access to indexed content.\",\n<\/span><\/span><span class='shcb-loc'><span>  ].join(\" \");\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>\/\/ OPTIONS - CORS preflight handler\n<\/span><\/span><span class='shcb-loc'><span>export async function OPTIONS(req: Request) {\n<\/span><\/span><span class='shcb-loc'><span>  return new Response(null, {\n<\/span><\/span><span class='shcb-loc'><span>    status: 204,\n<\/span><\/span><span class='shcb-loc'><span>    headers: getCorsHeaders(req.headers.get(\"origin\")),\n<\/span><\/span><span class='shcb-loc'><span>  });\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>\/\/ POST - main API handler: validates auth, opens SSE stream, runs three guard paths or iterative tool-calling\n<\/span><\/span><span class='shcb-loc'><span>export async function POST(req: Request) {\n<\/span><\/span><span class='shcb-loc'><span>  const corsHeaders = getCorsHeaders(req.headers.get(\"origin\"));\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  if (!validateRequestOrigin(req)) {\n<\/span><\/span><span class='shcb-loc'><span>    return new Response(\n<\/span><\/span><span class='shcb-loc'><span>      JSON.stringify({ error: \"Forbidden origin.\" }),\n<\/span><\/span><span class='shcb-loc'><span>      {\n<\/span><\/span><span class='shcb-loc'><span>        status: 403,\n<\/span><\/span><span class='shcb-loc'><span>        headers: {\n<\/span><\/span><span class='shcb-loc'><span>          ...corsHeaders,\n<\/span><\/span><span class='shcb-loc'><span>          \"Content-Type\": \"application\/json\",\n<\/span><\/span><span class='shcb-loc'><span>        },\n<\/span><\/span><span class='shcb-loc'><span>      },\n<\/span><\/span><span class='shcb-loc'><span>    );\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  if (!validateCopilotToken(req)) {\n<\/span><\/span><span class='shcb-loc'><span>    return new Response(\n<\/span><\/span><span class='shcb-loc'><span>      JSON.stringify({\n<\/span><\/span><span class='shcb-loc'><span>        error: \"Unauthorized token.\",\n<\/span><\/span><span class='shcb-loc'><span>        debug: buildAuthDebug(req),\n<\/span><\/span><span class='shcb-loc'><span>      }),\n<\/span><\/span><span class='shcb-loc'><span>      {\n<\/span><\/span><span class='shcb-loc'><span>        status: 401,\n<\/span><\/span><span class='shcb-loc'><span>        headers: {\n<\/span><\/span><span class='shcb-loc'><span>          ...corsHeaders,\n<\/span><\/span><span class='shcb-loc'><span>          \"Content-Type\": \"application\/json\",\n<\/span><\/span><span class='shcb-loc'><span>        },\n<\/span><\/span><span class='shcb-loc'><span>      },\n<\/span><\/span><span class='shcb-loc'><span>    );\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  let body: RequestBody;\n<\/span><\/span><span class='shcb-loc'><span>  try {\n<\/span><\/span><span class='shcb-loc'><span>    body = (await req.json()) as RequestBody;\n<\/span><\/span><span class='shcb-loc'><span>  } catch {\n<\/span><\/span><span class='shcb-loc'><span>    return new Response(JSON.stringify({ error: \"Invalid JSON body.\" }), {\n<\/span><\/span><span class='shcb-loc'><span>      status: 400,\n<\/span><\/span><span class='shcb-loc'><span>      headers: {\n<\/span><\/span><span class='shcb-loc'><span>        ...corsHeaders,\n<\/span><\/span><span class='shcb-loc'><span>        \"Content-Type\": \"application\/json\",\n<\/span><\/span><span class='shcb-loc'><span>      },\n<\/span><\/span><span class='shcb-loc'><span>    });\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  const userMessage = typeof body.message === \"string\" ? body.message.trim() : \"\";\n<\/span><\/span><span class='shcb-loc'><span>  if (!userMessage) {\n<\/span><\/span><span class='shcb-loc'><span>    return new Response(JSON.stringify({ error: \"Message is required.\" }), {\n<\/span><\/span><span class='shcb-loc'><span>      status: 400,\n<\/span><\/span><span class='shcb-loc'><span>      headers: {\n<\/span><\/span><span class='shcb-loc'><span>        ...corsHeaders,\n<\/span><\/span><span class='shcb-loc'><span>        \"Content-Type\": \"application\/json\",\n<\/span><\/span><span class='shcb-loc'><span>      },\n<\/span><\/span><span class='shcb-loc'><span>    });\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  const history = normalizeHistory(body.history);\n<\/span><\/span><span class='shcb-loc'><span>  const model = process.env.OPENAI_MODEL || DEFAULT_MODEL;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  const lastSearchState: {\n<\/span><\/span><span class='shcb-loc'><span>    query?: string;\n<\/span><\/span><span class='shcb-loc'><span>    filter?: string;\n<\/span><\/span><span class='shcb-loc'><span>    limit?: number;\n<\/span><\/span><span class='shcb-loc'><span>    offset?: number;\n<\/span><\/span><span class='shcb-loc'><span>    results?: SearchResultItem&#91;];\n<\/span><\/span><span class='shcb-loc'><span>  } = {\n<\/span><\/span><span class='shcb-loc'><span>    ...(body.state?.lastSearch ?? {}),\n<\/span><\/span><span class='shcb-loc'><span>  };\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  const stream = new ReadableStream({\n<\/span><\/span><span class='shcb-loc'><span>    start: async (controller) =&gt; {\n<\/span><\/span><span class='shcb-loc'><span>      const encoder = new TextEncoder();\n<\/span><\/span><span class='shcb-loc'><span>      const send = (event: string, payload: unknown) =&gt; {\n<\/span><\/span><span class='shcb-loc'><span>        const frame = `event: ${event}\\ndata: ${JSON.stringify(payload)}\\n\\n`;\n<\/span><\/span><span class='shcb-loc'><span>        controller.enqueue(encoder.encode(frame));\n<\/span><\/span><span class='shcb-loc'><span>      };\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>      try {\n<\/span><\/span><span class='shcb-loc'><span>        send(\"status\", { stage: \"validating\" });\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>        if (isNonIndexedAdminQuery(userMessage)) {\n<\/span><\/span><span class='shcb-loc'><span>          const limitationMessage =\n<\/span><\/span><span class='shcb-loc'><span>            \"This copilot only retrieves indexed content via Smart Search MCP (search\/fetch). It cannot list or manage WP Admin users, roles, plugins, or settings.\";\n<\/span><\/span><span class='shcb-loc'><span>          send(\"delta\", { text: limitationMessage });\n<\/span><\/span><span class='shcb-loc'><span>          send(\"done\", {\n<\/span><\/span><span class='shcb-loc'><span>            ok: true,\n<\/span><\/span><span class='shcb-loc'><span>            state: { lastSearch: lastSearchState },\n<\/span><\/span><span class='shcb-loc'><span>          });\n<\/span><\/span><span class='shcb-loc'><span>          controller.close();\n<\/span><\/span><span class='shcb-loc'><span>          return;\n<\/span><\/span><span class='shcb-loc'><span>        }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>        const mcpClient = mcpClientFromEnv();\n<\/span><\/span><span class='shcb-loc'><span>        send(\"status\", { stage: \"thinking\" });\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>        \/\/ Deterministic guard: Handle pagination requests.\n<\/span><\/span><span class='shcb-loc'><span>        if (shouldHandleMoreQuery(userMessage) &amp;&amp; lastSearchState.query) {\n<\/span><\/span><span class='shcb-loc'><span>          const nextLimit = lastSearchState.limit ?? DEFAULT_LIMIT;\n<\/span><\/span><span class='shcb-loc'><span>          const nextOffset = (lastSearchState.offset ?? 0) + nextLimit;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>          const toolResult = await executeTool(\n<\/span><\/span><span class='shcb-loc'><span>            \"smart_search_search\",\n<\/span><\/span><span class='shcb-loc'><span>            {\n<\/span><\/span><span class='shcb-loc'><span>              query: lastSearchState.query,\n<\/span><\/span><span class='shcb-loc'><span>              limit: nextLimit,\n<\/span><\/span><span class='shcb-loc'><span>              offset: nextOffset,\n<\/span><\/span><span class='shcb-loc'><span>            },\n<\/span><\/span><span class='shcb-loc'><span>            { mcpClient, lastSearchState },\n<\/span><\/span><span class='shcb-loc'><span>          );\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>          send(\"delta\", { text: \"Here are more results:\\n\\n\" });\n<\/span><\/span><span class='shcb-loc'><span>          const results =\n<\/span><\/span><span class='shcb-loc'><span>            (toolResult as { results?: SearchResultItem&#91;] }).results ?? &#91;];\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>          if (results.length === 0) {\n<\/span><\/span><span class='shcb-loc'><span>            send(\"delta\", { text: \"No additional results found.\" });\n<\/span><\/span><span class='shcb-loc'><span>          } else {\n<\/span><\/span><span class='shcb-loc'><span>            for (let i = 0; i <span class=\"hljs-tag\">&lt; <span class=\"hljs-attr\">results.length<\/span>; <span class=\"hljs-attr\">i<\/span> += <span class=\"hljs-string\">1)<\/span> {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">              <span class=\"hljs-attr\">const<\/span> <span class=\"hljs-attr\">r<\/span> = <span class=\"hljs-string\">results&#91;i];<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">              <span class=\"hljs-attr\">const<\/span> <span class=\"hljs-attr\">lines<\/span> = <span class=\"hljs-string\">&#91;<\/span>`${<span class=\"hljs-attr\">i<\/span> + <span class=\"hljs-attr\">1<\/span>}<span class=\"hljs-attr\">.<\/span>`];<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">              <span class=\"hljs-attr\">if<\/span> (<span class=\"hljs-attr\">typeof<\/span> <span class=\"hljs-attr\">r.title<\/span> === <span class=\"hljs-string\">\"string\"<\/span> &amp;&amp; <span class=\"hljs-attr\">r.title.trim<\/span>()) {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">                <span class=\"hljs-attr\">lines.push<\/span>(<span class=\"hljs-attr\">r.title.trim<\/span>());<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">              }<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">              <span class=\"hljs-attr\">if<\/span> (<span class=\"hljs-attr\">typeof<\/span> <span class=\"hljs-attr\">r.url<\/span> === <span class=\"hljs-string\">\"string\"<\/span> &amp;&amp; <span class=\"hljs-attr\">r.url.trim<\/span>()) {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">                <span class=\"hljs-attr\">lines.push<\/span>(<span class=\"hljs-attr\">r.url.trim<\/span>());<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">              }<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">              <span class=\"hljs-attr\">if<\/span> (<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">                <span class=\"hljs-attr\">typeof<\/span> <span class=\"hljs-attr\">r.snippet<\/span> === <span class=\"hljs-string\">\"string\"<\/span> &amp;&amp;<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">                <span class=\"hljs-attr\">r.snippet.trim<\/span>() &amp;&amp;<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">                <span class=\"hljs-attr\">r.snippet.trim<\/span>() !== <span class=\"hljs-string\">\"...\"<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">              ) {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">                <span class=\"hljs-attr\">lines.push<\/span>(<span class=\"hljs-attr\">r.snippet.trim<\/span>());<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">              }<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">              <span class=\"hljs-attr\">send<\/span>(\"<span class=\"hljs-attr\">delta<\/span>\", { <span class=\"hljs-attr\">text:<\/span> `${<span class=\"hljs-attr\">lines.join<\/span>(\"\\<span class=\"hljs-attr\">n<\/span>\")}\\<span class=\"hljs-attr\">n<\/span>\\<span class=\"hljs-attr\">n<\/span>` });<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            }<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          }<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\"><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          <span class=\"hljs-attr\">send<\/span>(\"<span class=\"hljs-attr\">done<\/span>\", {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            <span class=\"hljs-attr\">ok:<\/span> <span class=\"hljs-attr\">true<\/span>,<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            <span class=\"hljs-attr\">state:<\/span> { <span class=\"hljs-attr\">lastSearch:<\/span> <span class=\"hljs-attr\">lastSearchState<\/span> },<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          });<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          <span class=\"hljs-attr\">controller.close<\/span>();<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          <span class=\"hljs-attr\">return<\/span>;<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">        }<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\"><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">        \/\/ <span class=\"hljs-attr\">Deterministic<\/span> <span class=\"hljs-attr\">guard:<\/span> <span class=\"hljs-attr\">Handle<\/span> <span class=\"hljs-attr\">summarize<\/span> <span class=\"hljs-attr\">nth<\/span> <span class=\"hljs-attr\">result<\/span> <span class=\"hljs-attr\">requests.<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">        <span class=\"hljs-attr\">const<\/span> <span class=\"hljs-attr\">nthSummary<\/span> = <span class=\"hljs-string\">shouldHandleSummarizeNth(userMessage);<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">        <span class=\"hljs-attr\">if<\/span> (<span class=\"hljs-attr\">nthSummary<\/span> &amp;&amp; <span class=\"hljs-attr\">Array.isArray<\/span>(<span class=\"hljs-attr\">lastSearchState.results<\/span>)) {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          <span class=\"hljs-attr\">const<\/span> <span class=\"hljs-attr\">candidate<\/span> = <span class=\"hljs-string\">lastSearchState.results&#91;nthSummary.index];<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          <span class=\"hljs-attr\">if<\/span> (!<span class=\"hljs-attr\">candidate<\/span>?<span class=\"hljs-attr\">.id<\/span>) {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            <span class=\"hljs-attr\">send<\/span>(\"<span class=\"hljs-attr\">delta<\/span>\", {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">              <span class=\"hljs-attr\">text:<\/span> \"<span class=\"hljs-attr\">That<\/span> <span class=\"hljs-attr\">result<\/span> <span class=\"hljs-attr\">is<\/span> <span class=\"hljs-attr\">not<\/span> <span class=\"hljs-attr\">available<\/span> <span class=\"hljs-attr\">in<\/span> <span class=\"hljs-attr\">the<\/span> <span class=\"hljs-attr\">previous<\/span> <span class=\"hljs-attr\">search<\/span> <span class=\"hljs-attr\">results.<\/span>\",<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            });<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            <span class=\"hljs-attr\">send<\/span>(\"<span class=\"hljs-attr\">done<\/span>\", {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">              <span class=\"hljs-attr\">ok:<\/span> <span class=\"hljs-attr\">true<\/span>,<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">              <span class=\"hljs-attr\">state:<\/span> { <span class=\"hljs-attr\">lastSearch:<\/span> <span class=\"hljs-attr\">lastSearchState<\/span> },<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            });<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            <span class=\"hljs-attr\">controller.close<\/span>();<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            <span class=\"hljs-attr\">return<\/span>;<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          }<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\"><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          <span class=\"hljs-attr\">const<\/span> <span class=\"hljs-attr\">fetchResult<\/span> = <span class=\"hljs-string\">await<\/span> <span class=\"hljs-attr\">executeTool<\/span>(<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            \"<span class=\"hljs-attr\">smart_search_fetch<\/span>\",<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            { <span class=\"hljs-attr\">id:<\/span> <span class=\"hljs-attr\">candidate.id<\/span> },<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            { <span class=\"hljs-attr\">mcpClient<\/span>, <span class=\"hljs-attr\">lastSearchState<\/span> },<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          );<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\"><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          <span class=\"hljs-attr\">const<\/span> <span class=\"hljs-attr\">promptMessages:<\/span> <span class=\"hljs-attr\">OpenAIMessage<\/span>&#91;] = <span class=\"hljs-string\">&#91;<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">              <span class=\"hljs-attr\">role:<\/span> \"<span class=\"hljs-attr\">system<\/span>\",<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">              <span class=\"hljs-attr\">content:<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">                \"<span class=\"hljs-attr\">Summarize<\/span> <span class=\"hljs-attr\">the<\/span> <span class=\"hljs-attr\">provided<\/span> <span class=\"hljs-attr\">document<\/span> <span class=\"hljs-attr\">concisely.<\/span> <span class=\"hljs-attr\">Use<\/span> <span class=\"hljs-attr\">only<\/span> <span class=\"hljs-attr\">information<\/span> <span class=\"hljs-attr\">from<\/span> <span class=\"hljs-attr\">the<\/span> <span class=\"hljs-attr\">document.<\/span>\",<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            },<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">              <span class=\"hljs-attr\">role:<\/span> \"<span class=\"hljs-attr\">user<\/span>\",<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">              <span class=\"hljs-attr\">content:<\/span> `<span class=\"hljs-attr\">Summarize<\/span> <span class=\"hljs-attr\">this<\/span> <span class=\"hljs-attr\">document:<\/span>\\<span class=\"hljs-attr\">n<\/span>${<span class=\"hljs-attr\">JSON.stringify<\/span>(<span class=\"hljs-attr\">fetchResult<\/span>)}`,<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            },<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          ];<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\"><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          <span class=\"hljs-attr\">const<\/span> <span class=\"hljs-attr\">completion<\/span> = <span class=\"hljs-string\">await<\/span> <span class=\"hljs-attr\">openAiChatCompletion<\/span>(<span class=\"hljs-attr\">promptMessages<\/span>, <span class=\"hljs-attr\">model<\/span>, &#91;]);<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          <span class=\"hljs-attr\">const<\/span> <span class=\"hljs-attr\">text<\/span> =<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            <span class=\"hljs-string\">completion?.choices?.&#91;0]?.message?.content<\/span> ??<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            \"<span class=\"hljs-attr\">Unable<\/span> <span class=\"hljs-attr\">to<\/span> <span class=\"hljs-attr\">generate<\/span> <span class=\"hljs-attr\">summary.<\/span>\";<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\"><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          <span class=\"hljs-attr\">for<\/span> (<span class=\"hljs-attr\">const<\/span> <span class=\"hljs-attr\">chunk<\/span> <span class=\"hljs-attr\">of<\/span> <span class=\"hljs-attr\">chunkText<\/span>(<span class=\"hljs-attr\">text<\/span>)) {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            <span class=\"hljs-attr\">send<\/span>(\"<span class=\"hljs-attr\">delta<\/span>\", { <span class=\"hljs-attr\">text:<\/span> <span class=\"hljs-attr\">chunk<\/span> });<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          }<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\"><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          <span class=\"hljs-attr\">send<\/span>(\"<span class=\"hljs-attr\">done<\/span>\", {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            <span class=\"hljs-attr\">ok:<\/span> <span class=\"hljs-attr\">true<\/span>,<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            <span class=\"hljs-attr\">state:<\/span> { <span class=\"hljs-attr\">lastSearch:<\/span> <span class=\"hljs-attr\">lastSearchState<\/span> },<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          });<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          <span class=\"hljs-attr\">controller.close<\/span>();<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          <span class=\"hljs-attr\">return<\/span>;<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">        }<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\"><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">        \/\/ <span class=\"hljs-attr\">All<\/span> <span class=\"hljs-attr\">other<\/span> <span class=\"hljs-attr\">queries:<\/span> <span class=\"hljs-attr\">Use<\/span> <span class=\"hljs-attr\">OpenAI<\/span> <span class=\"hljs-attr\">tool<\/span> <span class=\"hljs-attr\">calling<\/span> <span class=\"hljs-attr\">with<\/span> <span class=\"hljs-attr\">Smart<\/span> <span class=\"hljs-attr\">Search<\/span> <span class=\"hljs-attr\">AI<\/span> <span class=\"hljs-attr\">semantic<\/span> <span class=\"hljs-attr\">search.<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\"><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">        <span class=\"hljs-attr\">const<\/span> <span class=\"hljs-attr\">messages:<\/span> <span class=\"hljs-attr\">OpenAIMessage<\/span>&#91;] = <span class=\"hljs-string\">&#91;<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          { <span class=\"hljs-attr\">role:<\/span> \"<span class=\"hljs-attr\">system<\/span>\", <span class=\"hljs-attr\">content:<\/span> <span class=\"hljs-attr\">buildSystemPrompt<\/span>() },<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          <span class=\"hljs-attr\">...history.map<\/span>((<span class=\"hljs-attr\">h<\/span>) =&gt;<\/span> ({ role: h.role, content: h.content })),\n<\/span><\/span><span class='shcb-loc'><span>          { role: \"user\", content: userMessage },\n<\/span><\/span><span class='shcb-loc'><span>        ];\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>        let finalText = \"\";\n<\/span><\/span><span class='shcb-loc'><span>        const maxIterations = 5;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>        for (let i = 0; i <span class=\"hljs-tag\">&lt; <span class=\"hljs-attr\">maxIterations<\/span>; <span class=\"hljs-attr\">i<\/span> += <span class=\"hljs-string\">1)<\/span> {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          <span class=\"hljs-attr\">const<\/span> <span class=\"hljs-attr\">completion<\/span> = <span class=\"hljs-string\">await<\/span> <span class=\"hljs-attr\">openAiChatCompletion<\/span>(<span class=\"hljs-attr\">messages<\/span>, <span class=\"hljs-attr\">model<\/span>, <span class=\"hljs-attr\">OPENAI_TOOLS<\/span>);<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          <span class=\"hljs-attr\">const<\/span> <span class=\"hljs-attr\">choice<\/span> = <span class=\"hljs-string\">completion?.choices?.&#91;0];<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          <span class=\"hljs-attr\">const<\/span> <span class=\"hljs-attr\">msg<\/span> = <span class=\"hljs-string\">choice?.message;<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\"><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          <span class=\"hljs-attr\">if<\/span> (!<span class=\"hljs-attr\">msg<\/span>) {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            <span class=\"hljs-attr\">throw<\/span> <span class=\"hljs-attr\">new<\/span> <span class=\"hljs-attr\">Error<\/span>(\"<span class=\"hljs-attr\">OpenAI<\/span> <span class=\"hljs-attr\">returned<\/span> <span class=\"hljs-attr\">no<\/span> <span class=\"hljs-attr\">message.<\/span>\");<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          }<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\"><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          <span class=\"hljs-attr\">const<\/span> <span class=\"hljs-attr\">assistantMessage:<\/span> <span class=\"hljs-attr\">OpenAIMessage<\/span> = <span class=\"hljs-string\">{<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            <span class=\"hljs-attr\">role:<\/span> \"<span class=\"hljs-attr\">assistant<\/span>\",<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            <span class=\"hljs-attr\">content:<\/span> <span class=\"hljs-attr\">typeof<\/span> <span class=\"hljs-attr\">msg.content<\/span> === <span class=\"hljs-string\">\"string\"<\/span> ? <span class=\"hljs-attr\">msg.content<\/span> <span class=\"hljs-attr\">:<\/span> <span class=\"hljs-attr\">undefined<\/span>,<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            <span class=\"hljs-attr\">tool_calls:<\/span> <span class=\"hljs-attr\">Array.isArray<\/span>(<span class=\"hljs-attr\">msg.tool_calls<\/span>)<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">              ? (<span class=\"hljs-attr\">msg.tool_calls<\/span> <span class=\"hljs-attr\">as<\/span> <span class=\"hljs-attr\">OpenAIMessage<\/span>&#91;\"<span class=\"hljs-attr\">tool_calls<\/span>\"])<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">              <span class=\"hljs-attr\">:<\/span> <span class=\"hljs-attr\">undefined<\/span>,<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          };<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\"><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          <span class=\"hljs-attr\">messages.push<\/span>(<span class=\"hljs-attr\">assistantMessage<\/span>);<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\"><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          <span class=\"hljs-attr\">const<\/span> <span class=\"hljs-attr\">toolCalls<\/span> = <span class=\"hljs-string\">Array.isArray(msg.tool_calls)<\/span> ? <span class=\"hljs-attr\">msg.tool_calls<\/span> <span class=\"hljs-attr\">:<\/span> &#91;];<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          <span class=\"hljs-attr\">if<\/span> (<span class=\"hljs-attr\">toolCalls.length<\/span> === <span class=\"hljs-string\">0)<\/span> {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            <span class=\"hljs-attr\">finalText<\/span> = <span class=\"hljs-string\">msg.content<\/span> ?? \"<span class=\"hljs-attr\">No<\/span> <span class=\"hljs-attr\">response<\/span> <span class=\"hljs-attr\">generated.<\/span>\";<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            <span class=\"hljs-attr\">break<\/span>;<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          }<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\"><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">          <span class=\"hljs-attr\">send<\/span>(\"<span class=\"hljs-attr\">status<\/span>\", {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            <span class=\"hljs-attr\">stage:<\/span> \"<span class=\"hljs-attr\">searching<\/span>\",<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-tag\">            <span class=\"hljs-attr\">toolNames:<\/span> <span class=\"hljs-attr\">toolCalls.map<\/span>((<span class=\"hljs-attr\">tc:<\/span> { <span class=\"hljs-attr\">function:<\/span> { <span class=\"hljs-attr\">name:<\/span> <span class=\"hljs-attr\">string<\/span> } }) =&gt;<\/span> tc.function.name),\n<\/span><\/span><span class='shcb-loc'><span>          });\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>          for (const toolCall of toolCalls) {\n<\/span><\/span><span class='shcb-loc'><span>            const toolName = toolCall.function.name;\n<\/span><\/span><span class='shcb-loc'><span>            let parsedArgs: unknown = {};\n<\/span><\/span><span class='shcb-loc'><span>            try {\n<\/span><\/span><span class='shcb-loc'><span>              parsedArgs = JSON.parse(toolCall.function.arguments || \"{}\");\n<\/span><\/span><span class='shcb-loc'><span>            } catch {\n<\/span><\/span><span class='shcb-loc'><span>              throw new Error(`Invalid JSON arguments for tool ${toolName}.`);\n<\/span><\/span><span class='shcb-loc'><span>            }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>            const toolResult = await executeTool(toolName, parsedArgs, {\n<\/span><\/span><span class='shcb-loc'><span>              mcpClient,\n<\/span><\/span><span class='shcb-loc'><span>              lastSearchState,\n<\/span><\/span><span class='shcb-loc'><span>            });\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>            messages.push({\n<\/span><\/span><span class='shcb-loc'><span>              role: \"tool\",\n<\/span><\/span><span class='shcb-loc'><span>              tool_call_id: toolCall.id,\n<\/span><\/span><span class='shcb-loc'><span>              content: JSON.stringify(toolResult),\n<\/span><\/span><span class='shcb-loc'><span>            });\n<\/span><\/span><span class='shcb-loc'><span>          }\n<\/span><\/span><span class='shcb-loc'><span>        }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>        if (!finalText) {\n<\/span><\/span><span class='shcb-loc'><span>          finalText = \"Unable to complete request. Please try rephrasing your query.\";\n<\/span><\/span><span class='shcb-loc'><span>        }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>        for (const chunk of chunkText(finalText)) {\n<\/span><\/span><span class='shcb-loc'><span>          send(\"delta\", { text: chunk });\n<\/span><\/span><span class='shcb-loc'><span>        }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>        send(\"done\", {\n<\/span><\/span><span class='shcb-loc'><span>          ok: true,\n<\/span><\/span><span class='shcb-loc'><span>          state: { lastSearch: lastSearchState },\n<\/span><\/span><span class='shcb-loc'><span>        });\n<\/span><\/span><span class='shcb-loc'><span>        controller.close();\n<\/span><\/span><span class='shcb-loc'><span>      } catch (error) {\n<\/span><\/span><span class='shcb-loc'><span>        \/\/ Log full error server-side for debugging\n<\/span><\/span><span class='shcb-loc'><span>        console.error(\"&#91;API Error]\", error);\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>        \/\/ Send sanitized error to client\n<\/span><\/span><span class='shcb-loc'><span>        const message =\n<\/span><\/span><span class='shcb-loc'><span>          process.env.NODE_ENV === \"production\"\n<\/span><\/span><span class='shcb-loc'><span>            ? \"An error occurred while processing your request.\"\n<\/span><\/span><span class='shcb-loc'><span>            : error instanceof Error\n<\/span><\/span><span class='shcb-loc'><span>              ? error.message\n<\/span><\/span><span class='shcb-loc'><span>              : \"Unexpected server error.\";\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>        send(\"error\", { message });\n<\/span><\/span><span class='shcb-loc'><span>        send(\"done\", { ok: false });\n<\/span><\/span><span class='shcb-loc'><span>        controller.close();\n<\/span><\/span><span class='shcb-loc'><span>      }\n<\/span><\/span><span class='shcb-loc'><span>    },\n<\/span><\/span><span class='shcb-loc'><span>  });\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  return new Response(stream, {\n<\/span><\/span><span class='shcb-loc'><span>    status: 200,\n<\/span><\/span><span class='shcb-loc'><span>    headers: {\n<\/span><\/span><span class='shcb-loc'><span>      ...corsHeaders,\n<\/span><\/span><span class='shcb-loc'><span>      \"Content-Type\": \"text\/event-stream\",\n<\/span><\/span><span class='shcb-loc'><span>      \"Cache-Control\": \"no-cache, no-transform\",\n<\/span><\/span><span class='shcb-loc'><span>      Connection: \"keep-alive\",\n<\/span><\/span><span class='shcb-loc'><span>    },\n<\/span><\/span><span class='shcb-loc'><span>  });\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre><\/details>\n\n\n\n<p>Let\u2019s go over the main parts of this code block.<\/p>\n\n\n\n<p>Because the chat UI runs inside WordPress admin while the agent route is hosted separately, the first step is validating that requests are coming from the expected origin and include the correct shared token.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-14\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">validateRequestOrigin<\/span>(<span class=\"hljs-params\">req: Request<\/span>): <span class=\"hljs-title\">boolean<\/span> <\/span>{\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> allowed = process.env.WP_ADMIN_ORIGIN;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (!allowed) {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-literal\">false<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> requestOrigin = req.headers.get(<span class=\"hljs-string\">\"origin\"<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">return<\/span> requestOrigin === allowed;\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">validateCopilotToken<\/span>(<span class=\"hljs-params\">req: Request<\/span>): <span class=\"hljs-title\">boolean<\/span> <\/span>{\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> expectedToken = process.env.WP_ADMIN_COPILOT_TOKEN;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (!expectedToken) {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-literal\">false<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> headerToken = req.headers.get(<span class=\"hljs-string\">\"x-wp-admin-copilot-token\"<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (!headerToken) {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-literal\">false<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">return<\/span> headerToken.trim() === expectedToken.trim();\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This keeps the endpoint limited to requests from the WP Admin copilot UI rather than exposing it publicly.<\/p>\n\n\n\n<p>The next block defines the copilot\u2019s behavior.&nbsp; The route builds system prompts that tell the model what tools it has available and how it should use them.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-15\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">buildSystemPrompt<\/span>(<span class=\"hljs-params\"><\/span>): <span class=\"hljs-title\">string<\/span> <\/span>{\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">return<\/span> &#91;\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-string\">\"You are WP Admin Copilot for indexed content retrieval.\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-string\">\"You have access to Smart Search AI MCP with semantic\/hybrid search capabilities.\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-string\">\"Available tools: smart_search_search (searches content) and smart_search_fetch (retrieves full document by ID).\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-string\">\"IMPORTANT: Smart Search AI uses vector\/semantic search. Pass natural language queries directly to smart_search_search.\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-string\">\"Do NOT use filters unless explicitly needed. The 'query' parameter accepts full natural language.\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-string\">\"Present results with title, URL, and snippet. Never invent or modify these values.\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-string\">\"Use smart_search_fetch with a document id when users ask to summarize a specific result.\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-string\">\"If asked about users, roles, plugins, or settings, explain you only have access to indexed content.\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>  ].join(<span class=\"hljs-string\">\" \"<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This prompt acts as the behavioral contract for the copilot. It tells the model to rely on Smart Search AI for retrieval, use natural language queries directly, and avoid hallucinating values the tools did not return.<\/p>\n\n\n\n<p>The next block helps us handle simple follow-up queries.&nbsp; Not every request needs to go through a full tool call loop. For some follow ups, the route can reply deterministically using the prior search state.<br>An example is when the user asks for more results, the route adds to the offset and executes another search directly:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-16\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">if<\/span> (shouldHandleMoreQuery(userMessage) &amp;&amp; lastSearchState.query) {\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> nextLimit = lastSearchState.limit ?? DEFAULT_LIMIT;\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> nextOffset = (lastSearchState.offset ?? <span class=\"hljs-number\">0<\/span>) + nextLimit;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> toolResult = <span class=\"hljs-keyword\">await<\/span> executeTool(\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-string\">\"smart_search_search\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>    {\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-attr\">query<\/span>: lastSearchState.query,\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-attr\">limit<\/span>: nextLimit,\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-attr\">offset<\/span>: nextOffset,\n<\/span><\/span><span class='shcb-loc'><span>    },\n<\/span><\/span><span class='shcb-loc'><span>    { mcpClient, lastSearchState },\n<\/span><\/span><span class='shcb-loc'><span>  );\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-16\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The same pattern is used for requests like \u201csummarize the second result,\u201d where the route can fetch the selected document directly instead of asking the model to rediscover it.<\/p>\n\n\n\n<p>This makes common follow-up interactions faster and more predictable.<\/p>\n\n\n\n<p>For all other requests, the route uses a bounded OpenAI tool loop:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-17\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">const<\/span> messages: OpenAIMessage&#91;] = &#91;\n<\/span><\/span><span class='shcb-loc'><span>  { <span class=\"hljs-attr\">role<\/span>: <span class=\"hljs-string\">\"system\"<\/span>, <span class=\"hljs-attr\">content<\/span>: buildSystemPrompt() },\n<\/span><\/span><span class='shcb-loc'><span>  ...history.map(<span class=\"hljs-function\">(<span class=\"hljs-params\">h<\/span>) =&gt;<\/span> ({ <span class=\"hljs-attr\">role<\/span>: h.role, <span class=\"hljs-attr\">content<\/span>: h.content })),\n<\/span><\/span><span class='shcb-loc'><span>  { <span class=\"hljs-attr\">role<\/span>: <span class=\"hljs-string\">\"user\"<\/span>, <span class=\"hljs-attr\">content<\/span>: userMessage },\n<\/span><\/span><span class='shcb-loc'><span>];\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-keyword\">let<\/span> finalText = <span class=\"hljs-string\">\"\"<\/span>;\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-keyword\">const<\/span> maxIterations = <span class=\"hljs-number\">5<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">let<\/span> i = <span class=\"hljs-number\">0<\/span>; i &lt; maxIterations; i += <span class=\"hljs-number\">1<\/span>) {\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> completion = <span class=\"hljs-keyword\">await<\/span> openAiChatCompletion(messages, model, OPENAI_TOOLS);\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> choice = completion?.choices?.&#91;<span class=\"hljs-number\">0<\/span>];\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> msg = choice?.message;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (!msg) {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(<span class=\"hljs-string\">\"OpenAI returned no message.\"<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> assistantMessage: OpenAIMessage = {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attr\">role<\/span>: <span class=\"hljs-string\">\"assistant\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attr\">content<\/span>: <span class=\"hljs-keyword\">typeof<\/span> msg.content === <span class=\"hljs-string\">\"string\"<\/span> ? msg.content : <span class=\"hljs-literal\">undefined<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attr\">tool_calls<\/span>: <span class=\"hljs-built_in\">Array<\/span>.isArray(msg.tool_calls)\n<\/span><\/span><span class='shcb-loc'><span>      ? (msg.tool_calls <span class=\"hljs-keyword\">as<\/span> OpenAIMessage&#91;<span class=\"hljs-string\">\"tool_calls\"<\/span>])\n<\/span><\/span><span class='shcb-loc'><span>      : <span class=\"hljs-literal\">undefined<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>  };\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  messages.push(assistantMessage);\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> toolCalls = <span class=\"hljs-built_in\">Array<\/span>.isArray(msg.tool_calls) ? msg.tool_calls : &#91;];\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">if<\/span> (toolCalls.length === <span class=\"hljs-number\">0<\/span>) {\n<\/span><\/span><span class='shcb-loc'><span>    finalText = msg.content ?? <span class=\"hljs-string\">\"No response generated.\"<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">break<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">const<\/span> toolCall <span class=\"hljs-keyword\">of<\/span> toolCalls) {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">const<\/span> toolName = toolCall<span class=\"hljs-function\">.<span class=\"hljs-keyword\">function<\/span>.<span class=\"hljs-title\">name<\/span>;<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\">    <span class=\"hljs-title\">const<\/span> <span class=\"hljs-title\">parsedArgs<\/span> = <span class=\"hljs-title\">JSON<\/span>.<span class=\"hljs-title\">parse<\/span>(<span class=\"hljs-params\">toolCall.function.arguments || <span class=\"hljs-string\">\"{}\"<\/span><\/span>);<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\"><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">    <span class=\"hljs-title\">const<\/span> <span class=\"hljs-title\">toolResult<\/span> = <span class=\"hljs-title\">await<\/span> <span class=\"hljs-title\">executeTool<\/span>(<span class=\"hljs-params\">toolName, parsedArgs, {<\/span><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\"><span class=\"hljs-params\">      mcpClient,<\/span><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\"><span class=\"hljs-params\">      lastSearchState,<\/span><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\"><span class=\"hljs-params\">    }<\/span>);<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\"><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">    <span class=\"hljs-title\">messages<\/span>.<span class=\"hljs-title\">push<\/span>(<span class=\"hljs-params\">{<\/span><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\"><span class=\"hljs-params\">      role: <span class=\"hljs-string\">\"tool\"<\/span>,<\/span><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\"><span class=\"hljs-params\">      tool_call_id: toolCall.id,<\/span><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\"><span class=\"hljs-params\">      content: JSON.stringify(toolResult<\/span>),<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">    });<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">  }<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-params\">}<\/span><\/span>\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-17\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><br>This is where the full agent workflow happens:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>The model receives the user message and available tools<\/li>\n<\/ol>\n\n\n\n<ol start=\"2\" class=\"wp-block-list\">\n<li>It decides whether to answer directly or call a tool<\/li>\n<\/ol>\n\n\n\n<ol start=\"3\" class=\"wp-block-list\">\n<li>If it calls a tool, the route executes it<\/li>\n<\/ol>\n\n\n\n<ol start=\"4\" class=\"wp-block-list\">\n<li>The tool result is appended back into the conversation<\/li>\n<\/ol>\n\n\n\n<ol start=\"5\" class=\"wp-block-list\">\n<li>The loop continues until the model produces a final answer<\/li>\n<\/ol>\n\n\n\n<p>That pattern allows the chatbot to use Smart Search AI as a retrieval layer while still letting the model handle the final response.<br>Next we add a ChatGPT like experience.&nbsp; The route streams the response back to the plugin using Server-Sent events.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-18\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">const<\/span> stream = <span class=\"hljs-keyword\">new<\/span> ReadableStream({\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attr\">start<\/span>: <span class=\"hljs-keyword\">async<\/span> (controller) =&gt; {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">const<\/span> encoder = <span class=\"hljs-keyword\">new<\/span> TextEncoder();\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">const<\/span> send = <span class=\"hljs-function\">(<span class=\"hljs-params\">event: string, payload: unknown<\/span>) =&gt;<\/span> {\n<\/span><\/span><span class='shcb-loc'><span>      <span class=\"hljs-keyword\">const<\/span> frame = <span class=\"hljs-string\">`event: <span class=\"hljs-subst\">${event}<\/span>\\ndata: <span class=\"hljs-subst\">${<span class=\"hljs-built_in\">JSON<\/span>.stringify(payload)}<\/span>\\n\\n`<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>      controller.enqueue(encoder.encode(frame));\n<\/span><\/span><span class='shcb-loc'><span>    };\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-18\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><br>At the end of the route, the stream is returned with an SSE response:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-19\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-selector-tag\">return<\/span> <span class=\"hljs-selector-tag\">new<\/span> <span class=\"hljs-selector-tag\">Response<\/span>(<span class=\"hljs-selector-tag\">stream<\/span>, {\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-attribute\">status<\/span>: <span class=\"hljs-number\">200<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>  headers: {\n<\/span><\/span><span class='shcb-loc'><span>    ...corsHeaders,\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-string\">\"Content-Type\"<\/span>: <span class=\"hljs-string\">\"text\/event-stream\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-string\">\"Cache-Control\"<\/span>: <span class=\"hljs-string\">\"no-cache, no-transform\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>    Connection: <span class=\"hljs-string\">\"keep-alive\"<\/span>,\n<\/span><\/span><span class='shcb-loc'><span>  },\n<\/span><\/span><span class='shcb-loc'><span>});\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-19\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This allows the WordPress admin UI to render the assistant response incrementally instead of waiting for one large payload.<\/p>\n\n\n\n<p>This route helps us make the full copilot come together combining endpoint security, prompt design, deterministic follow up handling, OpenAI tool calling, Smart Search MCP retrieval and streaming UI responses.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Testing The Copilot Flow<\/h2>\n\n\n\n<p>Now that the WP Admin plugin, Smart Search MCP client, and Next.js chat route are wired together, it\u2019s time to test the full copilot flow.<\/p>\n\n\n\n<p>From your Next.js project directory, you can start the development server locally in terminal:<\/p>\n\n\n\n<p><code>`npm run dev`<\/code><\/p>\n\n\n\n<p>This will start your local chat route so the WordPress plugin can send requests to it.<\/p>\n\n\n\n<p>Or you can deploy your <a href=\"http:\/\/next.js\">Next.js<\/a> route to be hosted on the WP Engine headless platform.&nbsp;<\/p>\n\n\n\n<div class=\"wp-block-group alignfull wpe-footer-cta has-base-color has-custom-cta-gradient-background has-text-color has-background has-global-padding is-layout-constrained wp-block-group-is-layout-constrained\" style=\"padding-top:100px;padding-bottom:100px\">\n<h2 class=\"wp-block-heading has-text-align-center has-max-48-font-size\" style=\"line-height:1.3\">WP Engine Headless Platform<\/h2>\n\n\n\n<p class=\"has-text-align-center has-large-font-size\" style=\"line-height:1.3\">Build, deploy, and manage headless websites with the power of WordPress and the best of modern web development.<\/p>\n\n\n\n<div class=\"wp-block-buttons is-content-justification-center is-layout-flex wp-container-core-buttons-is-layout-48bc80de wp-block-buttons-is-layout-flex\" style=\"margin-top:40px\">\n<div class=\"wp-block-button is-style-fill-base\"><a class=\"wp-block-button__link wp-element-button\" href=\"https:\/\/wpengine.com\/headless-wordpress\/\">Try Headless for Free<\/a><\/div>\n<\/div>\n<\/div>\n\n\n\n<p>Just a reminder, make sure your environment variables are configured before starting the app, including:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>OPENAI_API_KEY<\/code><br><\/li>\n\n\n\n<li><code>SMART_SEARCH_MCP_URL<\/code><\/li>\n<\/ul>\n\n\n\n<p><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>SMART_SEARCH_MCP_TOKEN<\/code><br><\/li>\n\n\n\n<li><code>SMART_SEARCH_MCP_TOKEN<\/code><br><\/li>\n\n\n\n<li><code>WP_ADMIN_COPILOT_TOKEN<\/code><br><\/li>\n\n\n\n<li><code>WP_ADMIN_ORIGIN<\/code><br><\/li>\n<\/ul>\n\n\n\n<p>Once the app is running, your API route should be available locally.  If you deployed on the headless platform, make sure you enter your variables on the variables page of your headless site.<\/p>\n\n\n\n<p>After activating the plugin:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Open the WP Copilot screen in WP Admin<\/li>\n<\/ol>\n\n\n\n<ol start=\"2\" class=\"wp-block-list\">\n<li>Enter the URL of your Next.js chat endpoint or your WP Engine headless platform endpoint<\/li>\n<\/ol>\n\n\n\n<ol start=\"3\" class=\"wp-block-list\">\n<li>Enter the shared secret token that matches your Next.js environment variable<\/li>\n<\/ol>\n\n\n\n<ol start=\"4\" class=\"wp-block-list\">\n<li>Save the settings<\/li>\n<\/ol>\n\n\n\n<p>At this point, the WordPress plugin should be able to connect to your Next.js route.<\/p>\n\n\n\n<p>With both pieces running, you can test the end-to-end experience directly inside WP Admin.<\/p>\n\n\n\n<p>Try a retrieval prompt such as:<\/p>\n\n\n\n<p><em>\u201cList all posts related to Star Wars and return links.\u201d<\/em><\/p>\n\n\n\n<p>Or try a broader semantic query like:<\/p>\n\n\n\n<p><em>\u201cFind content about the Force and Jedi.\u201d<\/em><\/p>\n\n\n\n<p>The copilot should:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Send the prompt from WP Admin to your Next.js route<br><\/li>\n\n\n\n<li>Use OpenAI tool calling to decide when to call Smart Search MCP<br><\/li>\n\n\n\n<li>Retrieve ranked results from the Smart Search index<br><\/li>\n\n\n\n<li>Stream the response back into the WP Admin interface<br><\/li>\n<\/ol>\n\n\n\n<p>You can also test follow-up prompts such as:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><em>\u201cShow more results\u201d<\/em><br><\/li>\n\n\n\n<li><em>\u201cSummarize the second result\u201d<\/em><br><\/li>\n<\/ul>\n\n\n\n<p>These help confirm that your state handling and fetch workflow are working correctly.<\/p>\n\n\n\n<p>It should look like this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"1920\" height=\"1080\" src=\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/05\/ScreenFlow.gif\" alt=\"\" class=\"wp-image-32142\"\/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>You now have a backend architecture for a WordPress admin copilot that is practical and production-friendly:<\/p>\n\n\n\n<p>&#8211; OpenAI handles intent and tool orchestration.<\/p>\n\n\n\n<p>&#8211; Smart Search AI MCP handles deterministic content retrieval.<\/p>\n\n\n\n<p>&#8211; Next.js coordinates security, state, and streaming response UX.<\/p>\n\n\n\n<p>From here, you can extend this pattern with role-aware prompts, caching, rate limits, modal \/image search and richer admin UI behavior.<\/p>\n\n\n\n<p class=\"has-tiny-font-size\">[1] WP Engine is a proud member and supporter of the community of WordPress\u00ae users. The WordPress\u00ae trademark is the intellectual property of the WordPress Foundation. Uses of the WordPress\u00ae trademarks in this website are for identification purposes only and do not imply an endorsement by WordPress Foundation. WP Engine is not endorsed or owned by, or affiliated with, the WordPress Foundation.<\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>In most content teams, finding the right post quickly is one of those tasks that sounds simple but becomes slow when your content library grows. In this tutorial, we will [&hellip;]<\/p>\n","protected":false},"author":20,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_EventAllDay":false,"_EventTimezone":"","_EventStartDate":"","_EventEndDate":"","_EventStartDateUTC":"","_EventEndDateUTC":"","_EventShowMap":false,"_EventShowMapLink":false,"_EventURL":"","_EventCost":"","_EventCostDescription":"","_EventCurrencySymbol":"","_EventCurrencyCode":"","_EventCurrencyPosition":"","_EventDateTimeSeparator":"","_EventTimeRangeSeparator":"","_EventOrganizerID":[],"_EventVenueID":[],"_OrganizerEmail":"","_OrganizerPhone":"","_OrganizerWebsite":"","_VenueAddress":"","_VenueCity":"","_VenueCountry":"","_VenueProvince":"","_VenueState":"","_VenueZip":"","_VenuePhone":"","_VenueURL":"","_VenueStateProvince":"","_VenueLat":"","_VenueLng":"","_VenueShowMap":false,"_VenueShowMapLink":false,"footnotes":""},"categories":[23,1],"tags":[],"class_list":["post-32136","post","type-post","status-publish","format-standard","hentry","category-headless","category-wordpress"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.5 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Build\u00a0 A WordPress\u00ae Admin Copilot with OpenAI and Smart Search AI MCP - Builders<\/title>\n<meta name=\"description\" content=\"Learn how to build a WordPress admin copilot using OpenAI and Smart Search AI MCP for natural language content retrieval and real-time summaries.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/wpengine.com\/builders\/wordpress-copilot-smart-search-mcp\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Build a WordPress Admin Copilot with OpenAI and Smart Search AI MCP\" \/>\n<meta property=\"og:description\" content=\"Learn how to build an AI-powered WordPress admin copilot using OpenAI and Smart Search AI MCP for real-time, natural language search and retrieval.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/wpengine.com\/builders\/wordpress-copilot-smart-search-mcp\/\" \/>\n<meta property=\"og:site_name\" content=\"Builders\" \/>\n<meta property=\"article:published_time\" content=\"2026-05-04T15:23:58+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-05-04T15:31:37+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/05\/WPE-Builders-YouTube-ScreenshotLight-1920x1080-1.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1920\" \/>\n\t<meta property=\"og:image:height\" content=\"1080\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Francis Agulto\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@wpebuilders\" \/>\n<meta name=\"twitter:site\" content=\"@wpebuilders\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Francis Agulto\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"11 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/wordpress-copilot-smart-search-mcp\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/wordpress-copilot-smart-search-mcp\\\/\"},\"author\":{\"name\":\"Francis Agulto\",\"@id\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/#\\\/schema\\\/person\\\/bcdcb4ac0b215c34b6b30e440a24dc54\"},\"headline\":\"Build\u00a0 A WordPress\u00ae Admin Copilot with OpenAI and Smart Search AI MCP\",\"datePublished\":\"2026-05-04T15:23:58+00:00\",\"dateModified\":\"2026-05-04T15:31:37+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/wordpress-copilot-smart-search-mcp\\\/\"},\"wordCount\":2267,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/wordpress-copilot-smart-search-mcp\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/wp-content\\\/uploads\\\/2026\\\/04\\\/Screenshot-2026-04-27-at-12.50.27-PM-1024x579.png\",\"articleSection\":[\"Headless\",\"WordPress\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/wpengine.com\\\/builders\\\/wordpress-copilot-smart-search-mcp\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/wordpress-copilot-smart-search-mcp\\\/\",\"url\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/wordpress-copilot-smart-search-mcp\\\/\",\"name\":\"Build\u00a0 A WordPress\u00ae Admin Copilot with OpenAI and Smart Search AI MCP - Builders\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/wordpress-copilot-smart-search-mcp\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/wordpress-copilot-smart-search-mcp\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/wp-content\\\/uploads\\\/2026\\\/04\\\/Screenshot-2026-04-27-at-12.50.27-PM-1024x579.png\",\"datePublished\":\"2026-05-04T15:23:58+00:00\",\"dateModified\":\"2026-05-04T15:31:37+00:00\",\"description\":\"Learn how to build a WordPress admin copilot using OpenAI and Smart Search AI MCP for natural language content retrieval and real-time summaries.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/wordpress-copilot-smart-search-mcp\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/wpengine.com\\\/builders\\\/wordpress-copilot-smart-search-mcp\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/wordpress-copilot-smart-search-mcp\\\/#primaryimage\",\"url\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/wp-content\\\/uploads\\\/2026\\\/04\\\/Screenshot-2026-04-27-at-12.50.27-PM-scaled.png\",\"contentUrl\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/wp-content\\\/uploads\\\/2026\\\/04\\\/Screenshot-2026-04-27-at-12.50.27-PM-scaled.png\",\"width\":2560,\"height\":1448},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/wordpress-copilot-smart-search-mcp\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Build\u00a0 A WordPress\u00ae Admin Copilot with OpenAI and Smart Search AI MCP\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/#website\",\"url\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/\",\"name\":\"Builders\",\"description\":\"Reimagining the way we build with WordPress.\",\"publisher\":{\"@id\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/#organization\",\"name\":\"WP Engine\",\"url\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/wp-content\\\/uploads\\\/2024\\\/05\\\/WP-Engine-Horizontal@2x.png\",\"contentUrl\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/wp-content\\\/uploads\\\/2024\\\/05\\\/WP-Engine-Horizontal@2x.png\",\"width\":348,\"height\":68,\"caption\":\"WP Engine\"},\"image\":{\"@id\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/#\\\/schema\\\/logo\\\/image\\\/\"},\"sameAs\":[\"https:\\\/\\\/x.com\\\/wpebuilders\",\"https:\\\/\\\/www.youtube.com\\\/channel\\\/UCh1WuL54XFb9ZI6m6goFv1g\"]},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/#\\\/schema\\\/person\\\/bcdcb4ac0b215c34b6b30e440a24dc54\",\"name\":\"Francis Agulto\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/0c8a05c76944fc987d57296c96dc368055844527088c0aa44297edbfa8b82546?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/0c8a05c76944fc987d57296c96dc368055844527088c0aa44297edbfa8b82546?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/0c8a05c76944fc987d57296c96dc368055844527088c0aa44297edbfa8b82546?s=96&d=mm&r=g\",\"caption\":\"Francis Agulto\"},\"description\":\"Fran Agulto is a Developer Advocate at WP Engine. He is a lover of all things headless WordPress, Rock Climbing, and overall being stoked for people that love what they do and share that stoke with others! Follow me on Twitter for cool stoked headless WP!\",\"url\":\"https:\\\/\\\/wpengine.com\\\/builders\\\/author\\\/francis-agultowpengine-com-2-2-2-2-2-2-2-2-2-2-2-2\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Build\u00a0 A WordPress\u00ae Admin Copilot with OpenAI and Smart Search AI MCP - Builders","description":"Learn how to build a WordPress admin copilot using OpenAI and Smart Search AI MCP for natural language content retrieval and real-time summaries.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/wpengine.com\/builders\/wordpress-copilot-smart-search-mcp\/","og_locale":"en_US","og_type":"article","og_title":"Build a WordPress Admin Copilot with OpenAI and Smart Search AI MCP","og_description":"Learn how to build an AI-powered WordPress admin copilot using OpenAI and Smart Search AI MCP for real-time, natural language search and retrieval.","og_url":"https:\/\/wpengine.com\/builders\/wordpress-copilot-smart-search-mcp\/","og_site_name":"Builders","article_published_time":"2026-05-04T15:23:58+00:00","article_modified_time":"2026-05-04T15:31:37+00:00","og_image":[{"width":1920,"height":1080,"url":"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/05\/WPE-Builders-YouTube-ScreenshotLight-1920x1080-1.png","type":"image\/png"}],"author":"Francis Agulto","twitter_card":"summary_large_image","twitter_creator":"@wpebuilders","twitter_site":"@wpebuilders","twitter_misc":{"Written by":"Francis Agulto","Est. reading time":"11 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/wpengine.com\/builders\/wordpress-copilot-smart-search-mcp\/#article","isPartOf":{"@id":"https:\/\/wpengine.com\/builders\/wordpress-copilot-smart-search-mcp\/"},"author":{"name":"Francis Agulto","@id":"https:\/\/wpengine.com\/builders\/#\/schema\/person\/bcdcb4ac0b215c34b6b30e440a24dc54"},"headline":"Build\u00a0 A WordPress\u00ae Admin Copilot with OpenAI and Smart Search AI MCP","datePublished":"2026-05-04T15:23:58+00:00","dateModified":"2026-05-04T15:31:37+00:00","mainEntityOfPage":{"@id":"https:\/\/wpengine.com\/builders\/wordpress-copilot-smart-search-mcp\/"},"wordCount":2267,"commentCount":0,"publisher":{"@id":"https:\/\/wpengine.com\/builders\/#organization"},"image":{"@id":"https:\/\/wpengine.com\/builders\/wordpress-copilot-smart-search-mcp\/#primaryimage"},"thumbnailUrl":"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/Screenshot-2026-04-27-at-12.50.27-PM-1024x579.png","articleSection":["Headless","WordPress"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/wpengine.com\/builders\/wordpress-copilot-smart-search-mcp\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/wpengine.com\/builders\/wordpress-copilot-smart-search-mcp\/","url":"https:\/\/wpengine.com\/builders\/wordpress-copilot-smart-search-mcp\/","name":"Build\u00a0 A WordPress\u00ae Admin Copilot with OpenAI and Smart Search AI MCP - Builders","isPartOf":{"@id":"https:\/\/wpengine.com\/builders\/#website"},"primaryImageOfPage":{"@id":"https:\/\/wpengine.com\/builders\/wordpress-copilot-smart-search-mcp\/#primaryimage"},"image":{"@id":"https:\/\/wpengine.com\/builders\/wordpress-copilot-smart-search-mcp\/#primaryimage"},"thumbnailUrl":"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/Screenshot-2026-04-27-at-12.50.27-PM-1024x579.png","datePublished":"2026-05-04T15:23:58+00:00","dateModified":"2026-05-04T15:31:37+00:00","description":"Learn how to build a WordPress admin copilot using OpenAI and Smart Search AI MCP for natural language content retrieval and real-time summaries.","breadcrumb":{"@id":"https:\/\/wpengine.com\/builders\/wordpress-copilot-smart-search-mcp\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/wpengine.com\/builders\/wordpress-copilot-smart-search-mcp\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/wpengine.com\/builders\/wordpress-copilot-smart-search-mcp\/#primaryimage","url":"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/Screenshot-2026-04-27-at-12.50.27-PM-scaled.png","contentUrl":"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2026\/04\/Screenshot-2026-04-27-at-12.50.27-PM-scaled.png","width":2560,"height":1448},{"@type":"BreadcrumbList","@id":"https:\/\/wpengine.com\/builders\/wordpress-copilot-smart-search-mcp\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/wpengine.com\/builders\/"},{"@type":"ListItem","position":2,"name":"Build\u00a0 A WordPress\u00ae Admin Copilot with OpenAI and Smart Search AI MCP"}]},{"@type":"WebSite","@id":"https:\/\/wpengine.com\/builders\/#website","url":"https:\/\/wpengine.com\/builders\/","name":"Builders","description":"Reimagining the way we build with WordPress.","publisher":{"@id":"https:\/\/wpengine.com\/builders\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/wpengine.com\/builders\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/wpengine.com\/builders\/#organization","name":"WP Engine","url":"https:\/\/wpengine.com\/builders\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/wpengine.com\/builders\/#\/schema\/logo\/image\/","url":"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2024\/05\/WP-Engine-Horizontal@2x.png","contentUrl":"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2024\/05\/WP-Engine-Horizontal@2x.png","width":348,"height":68,"caption":"WP Engine"},"image":{"@id":"https:\/\/wpengine.com\/builders\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/x.com\/wpebuilders","https:\/\/www.youtube.com\/channel\/UCh1WuL54XFb9ZI6m6goFv1g"]},{"@type":"Person","@id":"https:\/\/wpengine.com\/builders\/#\/schema\/person\/bcdcb4ac0b215c34b6b30e440a24dc54","name":"Francis Agulto","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/0c8a05c76944fc987d57296c96dc368055844527088c0aa44297edbfa8b82546?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/0c8a05c76944fc987d57296c96dc368055844527088c0aa44297edbfa8b82546?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/0c8a05c76944fc987d57296c96dc368055844527088c0aa44297edbfa8b82546?s=96&d=mm&r=g","caption":"Francis Agulto"},"description":"Fran Agulto is a Developer Advocate at WP Engine. He is a lover of all things headless WordPress, Rock Climbing, and overall being stoked for people that love what they do and share that stoke with others! Follow me on Twitter for cool stoked headless WP!","url":"https:\/\/wpengine.com\/builders\/author\/francis-agultowpengine-com-2-2-2-2-2-2-2-2-2-2-2-2\/"}]}},"_links":{"self":[{"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/posts\/32136","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/users\/20"}],"replies":[{"embeddable":true,"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/comments?post=32136"}],"version-history":[{"count":0,"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/posts\/32136\/revisions"}],"wp:attachment":[{"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/media?parent=32136"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/categories?post=32136"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/tags?post=32136"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}