
{"id":64,"date":"2026-09-10T16:49:36","date_gmt":"2026-09-10T16:49:36","guid":{"rendered":"https:\/\/roboticsmaestro.com\/ai\/?p=64"},"modified":"2026-09-16T15:47:25","modified_gmt":"2026-09-16T15:47:25","slug":"mcp-based-azure-function-app-for-sql-data-indexing","status":"publish","type":"post","link":"https:\/\/roboticsmaestro.com\/ai\/blog\/2026\/09\/10\/mcp-based-azure-function-app-for-sql-data-indexing\/","title":{"rendered":"MCP-based Azure Function App for SQL Data indexing"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>visual architecture package<\/strong>&nbsp;so you can see the&nbsp;<strong>design<\/strong>,&nbsp;<strong>flow<\/strong>,&nbsp;<strong>context<\/strong>,&nbsp;<strong>container<\/strong>, and&nbsp;<strong>class diagrams<\/strong>&nbsp;for your&nbsp;<strong>real-time + scheduled SQL \u2192 Azure AI Search knowledge base<\/strong>&nbsp;with&nbsp;<strong>UI<\/strong>&nbsp;and&nbsp;<strong>MCP\/Foundry agent<\/strong>&nbsp;integration.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>1. System Design Overview<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Goal:<\/strong><br>A self-updating knowledge base that:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Pulls from\u00a0<strong>Azure SQL<\/strong>\u00a0(scheduled + real-time CDC\/Event Grid).<\/li>\n\n\n\n<li>Indexes into\u00a0<strong>Azure AI Search<\/strong>.<\/li>\n\n\n\n<li>Serves results to\u00a0<strong>UI<\/strong>\u00a0and\u00a0<strong>MCP agent<\/strong>.<\/li>\n\n\n\n<li>Tracks\u00a0<strong>last sync<\/strong>\u00a0in Blob Storage.<\/li>\n\n\n\n<li>Supports\u00a0<strong>manual refresh<\/strong>.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>2. High-Level Flow Diagram<\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">Copy code<code>+-------------------+       +-------------------+       +-------------------+\n| Azure SQL DB      |  --&gt;  | CDC \/ Change Track |  --&gt;  | Event Grid Topic  |\n+-------------------+       +-------------------+       +-------------------+\n       |                              |                           |\n       | (Scheduled Query)            | (Real-time Event)        |\n       v                              v                           v\n+-------------------+       +-------------------+       +-------------------+\n| Timer Trigger Fn  |       | Event Grid Fn     |       | Blob Storage      |\n| (Incremental Sync)|       | (Immediate Sync)  |       | (Last Sync Time)  |\n+-------------------+       +-------------------+       +-------------------+\n       \\_________________________   ___________________________\/\n                                     |\n                                     v\n                           +-------------------+\n                           | Azure AI Search   |\n                           +-------------------+\n                                     |\n                   +----------------+----------------+\n                   |                                 |\n          +-------------------+             +-------------------+\n          | UI Frontend       |             | MCP\/Foundry Agent |\n          +-------------------+             +-------------------+\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>3. Context Diagram (C4 Level 1)<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>System Context:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">Copy code<code>[User] ---&gt; [Knowledge Base System] ---&gt; [Azure AI Search]\n   ^                ^                          ^\n   |                |                          |\n   |                |                          |\n[UI Frontend]   [MCP Agent]               [Azure SQL DB]\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Key External Actors:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>User<\/strong>: Queries knowledge base via UI or MCP agent.<\/li>\n\n\n\n<li><strong>Azure SQL DB<\/strong>: Source of truth for documents.<\/li>\n\n\n\n<li><strong>Azure AI Search<\/strong>: Search index for fast retrieval.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>4. Container Diagram (C4 Level 2)<\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">Copy code<code>+--------------------------------------------------------------+\n| Knowledge Base System                                        |\n|                                                              |\n|  +-------------------+     +-------------------+             |\n|  | Timer Trigger Fn  |     | Event Grid Fn     |             |\n|  | (Incremental Sync)|     | (Real-time Sync)  |             |\n|  +-------------------+     +-------------------+             |\n|           |                         |                        |\n|           v                         v                        |\n|     +-------------------+     +-------------------+          |\n|     | Index Manager     |&lt;----| Change Processor  |          |\n|     +-------------------+     +-------------------+          |\n|           |                         |                        |\n|           v                         v                        |\n|     +-------------------+     +-------------------+          |\n|     | Azure AI Search   |     | Blob Storage      |          |\n|     +-------------------+     +-------------------+          |\n|                                                              |\n+--------------------------------------------------------------+\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>5. Class Diagram (Core Components)<\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">PlaintextCopy code<code>+-------------------+\n| IndexManager      |\n+-------------------+\n| - searchEndpoint  |\n| - searchApiKey    |\n| - indexName       |\n+-------------------+\n| + pushDocs(docs)  |\n| + deleteDocs(ids) |\n+-------------------+\n\n+-------------------+\n| SqlConnector      |\n+-------------------+\n| - connString      |\n+-------------------+\n| + getAllIds()     |\n| + getChanges(ts)  |\n| + getCDCChanges() |\n+-------------------+\n\n+-------------------+\n| BlobStateManager  |\n+-------------------+\n| - blobConnStr     |\n| - containerName   |\n| - blobName        |\n+-------------------+\n| + getLastSync()   |\n| + setLastSync(ts) |\n+-------------------+\n\n+-------------------+\n| EventProcessor    |\n+-------------------+\n| + handleEvent(e)  |\n| + mapToAction()   |\n+-------------------+\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>6. Sequence Flow (Real-Time Event)<\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">Copy code<code>User updates SQL row\n    |\n    v\nAzure SQL CDC logs change\n    |\n    v\nAzure Data Factory\/Synapse streams change to Event Grid\n    |\n    v\nEvent Grid triggers Azure Function<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>proceed<\/strong>&nbsp;and complete the&nbsp;<strong>design package<\/strong>&nbsp;by adding the&nbsp;<strong>sequence flows<\/strong>,&nbsp;<strong>deployment view<\/strong>, and&nbsp;<strong>final integration notes<\/strong>&nbsp;so you have a full blueprint you can hand to your dev + ops teams.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>7. Sequence Flow \u2014 Scheduled Sync<\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">PlaintextCopy code<code>[Timer Trigger Function] \u2192 getLastSync() from BlobStateManager\n    \u2192 SqlConnector.getAllIds()\n    \u2192 IndexManager.getAllIndexIds()\n    \u2192 Compare sets \u2192 deleteDocs(staleIds)\n    \u2192 SqlConnector.getChanges(lastSync)\n    \u2192 IndexManager.pushDocs(newDocs)\n    \u2192 BlobStateManager.setLastSync(now)\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Key Points:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Runs on CRON schedule (e.g., hourly).<\/li>\n\n\n\n<li>Deletes stale docs first, then indexes new\/updated ones.<\/li>\n\n\n\n<li>Updates last sync timestamp in Blob Storage.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>8. Sequence Flow \u2014 Real-Time Event<\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">PlaintextCopy code<code>[User\/Process updates SQL row]\n    \u2193\nAzure SQL CDC logs change\n    \u2193\nAzure Data Factory\/Synapse Link streams change\n    \u2193\nEvent Grid publishes event\n    \u2193\nEvent Grid Trigger Function receives event\n    \u2193\nEventProcessor.handleEvent()\n    \u2192 If delete \u2192 IndexManager.deleteDocs([id])\n    \u2192 If insert\/update \u2192 IndexManager.pushDocs([doc])\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Key Points:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Latency is seconds, not minutes\/hours.<\/li>\n\n\n\n<li>Only processes changed rows.<\/li>\n\n\n\n<li>Works alongside scheduled sync as a safety net.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>9. Deployment View (C4 Level 3)<\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">Copy code<code>+---------------------------+       +---------------------------+\n| Azure Function App        |       | Azure SQL Database        |\n|  - Timer Trigger Fn       |&lt;-----&gt;| CDC Enabled               |\n|  - Event Grid Trigger Fn  |       | Change Tracking Enabled   |\n|  - HTTP API Fn (UI\/MCP)   |       +---------------------------+\n|                           |\n|  +---------------------+  |       +---------------------------+\n|  | Blob Storage        |  |&lt;-----&gt;| Azure Event Grid          |\n|  | (Last Sync State)   |  |       +---------------------------+\n|  +---------------------+  |\n|                           |       +---------------------------+\n|  +---------------------+  |&lt;-----&gt;| Azure AI Search           |\n|  | IndexManager        |  |       +---------------------------+\n|  +---------------------+  |\n+---------------------------+\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>10. Component Responsibilities<\/strong><\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Component<\/th><th class=\"has-text-align-left\" data-align=\"left\">Responsibility<\/th><\/tr><\/thead><tbody><tr><td><strong>SqlConnector<\/strong><\/td><td>Connect to SQL, fetch IDs, fetch changes, fetch CDC changes.<\/td><\/tr><tr><td><strong>IndexManager<\/strong><\/td><td>Push, merge, or delete documents in Azure AI Search.<\/td><\/tr><tr><td><strong>BlobStateManager<\/strong><\/td><td>Store\/retrieve last sync timestamp.<\/td><\/tr><tr><td><strong>EventProcessor<\/strong><\/td><td>Interpret Event Grid payloads and map to index actions.<\/td><\/tr><tr><td><strong>UI Frontend<\/strong><\/td><td>Display last sync time, doc count, allow manual refresh.<\/td><\/tr><tr><td><strong>MCP Agent<\/strong><\/td><td>Query AI Search, check freshness, optionally trigger refresh.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>11. Final Integration Notes<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Security<\/strong>:\n<ul class=\"wp-block-list\">\n<li>Use\u00a0<strong>Managed Identity<\/strong>\u00a0for SQL, Blob, and AI Search.<\/li>\n\n\n\n<li>Store API keys in\u00a0<strong>Azure Key Vault<\/strong>.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Resilience<\/strong>:\n<ul class=\"wp-block-list\">\n<li>Scheduled sync acts as a\u00a0<strong>catch-up<\/strong>\u00a0if real-time events are missed.<\/li>\n\n\n\n<li>Blob state ensures incremental indexing is consistent.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Scalability<\/strong>:\n<ul class=\"wp-block-list\">\n<li>Event Grid scales automatically with change volume.<\/li>\n\n\n\n<li>AI Search supports batch indexing for efficiency.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Transparency<\/strong>:\n<ul class=\"wp-block-list\">\n<li>UI + MCP agent both use the same\u00a0<code>GetIndexStatus<\/code>\u00a0endpoint.<\/li>\n\n\n\n<li>Users can see freshness and trigger manual refresh.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">&#x2705;&nbsp;<strong>Now you have:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Context Diagram<\/strong>\u00a0(C4 Level 1)<\/li>\n\n\n\n<li><strong>Container Diagram<\/strong>\u00a0(C4 Level 2)<\/li>\n\n\n\n<li><strong>Class Diagram<\/strong><\/li>\n\n\n\n<li><strong>Sequence Flows<\/strong>\u00a0(Scheduled + Real-Time)<\/li>\n\n\n\n<li><strong>Deployment View<\/strong><\/li>\n\n\n\n<li><strong>Component Responsibilities<\/strong><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">&#8211;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>proceed<\/strong>&nbsp;and turn the architecture we\u2019ve been discussing into&nbsp;<strong>graphical UML-style diagrams<\/strong>&nbsp;you can drop directly into your documentation, Confluence, or design reviews.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"> Here are the\u00a0<strong>PlantUML<\/strong>\u00a0and\u00a0<strong>Mermaid<\/strong>\u00a0definitions \u2014 you can paste these into any UML tool (<strong>PlantUML, Mermaid Live Editor, Lucidchart, Draw.io, etc<\/strong>.) to instantly generate the images.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>1. Context Diagram (C4 Level 1)<\/strong>&nbsp;\u2014&nbsp;<em>Mermaid<\/em><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\"><code>graph TD<br>    User[User] --> UI[UI Frontend]<br>    User --> MCP[MCP\/Foundry Agent]<br>    UI --> KB[Knowledge Base System]<br>    MCP --> KB<br>    KB --> SQL[Azure SQL Database]<br>    KB --> AIS[Azure AI Search]<br><\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>2. Container Diagram (C4 Level 2)<\/strong>&nbsp;\u2014&nbsp;<em>Mermaid<\/em><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">Mermaid<br> <code>graph TD<br>    subgraph KB[Knowledge Base System]<br>        TimerFn[Timer Trigger Function] --> IndexMgr[Index Manager]<br>        EventFn[Event Grid Trigger Function] --> ChangeProc[Change Processor]<br>        IndexMgr --> AIS[Azure AI Search]<br>        ChangeProc --> AIS<br>        TimerFn --> Blob[Blob Storage - Last Sync]<br>        EventFn --> Blob<br>    end<br><br>    SQL[Azure SQL Database] --> TimerFn<br>    SQL --> EventFn<br>    AIS --> UI[UI Frontend]<br>    AIS --> MCP[MCP Agent]<br><\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>3. Class Diagram<\/strong>&nbsp;\u2014&nbsp;<em>PlantUML<\/em><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">Plantuml <br><br><code>@startuml<br>class IndexManager {<br>    - searchEndpoint : string<br>    - searchApiKey : string<br>    - indexName : string<br>    + pushDocs(docs)<br>    + deleteDocs(ids)<br>}<br><br>class SqlConnector {<br>    - connString : string<br>    + getAllIds()<br>    + getChanges(ts)<br>    + getCDCChanges()<br>}<br><br>class BlobStateManager {<br>    - blobConnStr : string<br>    - containerName : string<br>    - blobName : string<br>    + getLastSync()<br>    + setLastSync(ts)<br>}<br><br>class EventProcessor {<br>    + handleEvent(e)<br>    + mapToAction()<br>}<br><br>IndexManager --> SqlConnector<br>IndexManager --> BlobStateManager<br>EventProcessor --> IndexManager<br>@enduml<br><\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>4. Sequence Diagram \u2014 Scheduled Sync<\/strong>&nbsp;\u2014&nbsp;<em>PlantUML<\/em><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">Plantuml<br><br><code>@startuml<br>actor Timer<br>participant TimerTriggerFn<br>participant BlobStateManager<br>participant SqlConnector<br>participant IndexManager<br>participant AzureAISearch<br><br>Timer -> TimerTriggerFn: Trigger<br>TimerTriggerFn -> BlobStateManager: getLastSync()<br>BlobStateManager --> TimerTriggerFn: lastSync<br>TimerTriggerFn -> SqlConnector: getAllIds()<br>SqlConnector --> TimerTriggerFn: sqlIds<br>TimerTriggerFn -> IndexManager: getAllIndexIds()<br>IndexManager --> TimerTriggerFn: aiIds<br>TimerTriggerFn -> IndexManager: deleteDocs(staleIds)<br>TimerTriggerFn -> SqlConnector: getChanges(lastSync)<br>SqlConnector --> TimerTriggerFn: newDocs<br>TimerTriggerFn -> IndexManager: pushDocs(newDocs)<br>TimerTriggerFn -> BlobStateManager: setLastSync(now)<br>@enduml<br><\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>5. Sequence Diagram \u2014 Real-Time Event<\/strong>&nbsp;\u2014&nbsp;<em>PlantUML<\/em><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">Plantuml<br><br><code>@startuml<br>actor SQLUser<br>participant AzureSQL<br>participant EventGrid<br>participant EventGridFn<br>participant EventProcessor<br>participant IndexManager<br>participant AzureAISearch<br><br>SQLUser -> AzureSQL: Insert\/Update\/Delete row<br>AzureSQL -> EventGrid: CDC\/Change Tracking Event<br>EventGrid -> EventGridFn: Trigger<br>EventGridFn -> EventProcessor: handleEvent(event)<br>EventProcessor -> IndexManager: pushDocs() \/ deleteDocs()<br>IndexManager -> AzureAISearch: Update Index<br>@enduml<br><\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>6. Deployment Diagram<\/strong>&nbsp;\u2014&nbsp;<em>PlantUML<\/em><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">Plantuml<br><br><code>@startuml<br>node \"Azure Cloud\" {<br>    node \"Function App\" {<br>        component \"Timer Trigger Fn\"<br>        component \"Event Grid Trigger Fn\"<br>        component \"HTTP API Fn\"<br>    }<br>    node \"Azure SQL Database\" {<br>        artifact \"KnowledgeTable\"<br>        artifact \"CDC Change Tables\"<br>    }<br>    node \"Azure Blob Storage\" {<br>        artifact \"last_sync.txt\"<br>    }<br>    node \"Azure AI Search\" {<br>        artifact \"knowledge-index\"<br>    }<br>    node \"Azure Event Grid\" {<br>        artifact \"Change Events\"<br>    }<br>    node \"UI Frontend\"<br>    node \"MCP Agent\"<br>}<br>@enduml<br><\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>How to Generate the Images<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Mermaid<\/strong>:\n<ul class=\"wp-block-list\">\n<li>Go to\u00a0<a href=\"https:\/\/mermaid.live\/\">Mermaid Live Editor<\/a>, paste<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"has-extra-large-font-size wp-block-paragraph\"><strong>===MAIN IMPLEMENTATION STARTS HERE ====<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>extend<\/strong>&nbsp;your existing Azure Functions code so that it:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Connects to a SQL database<\/strong><\/li>\n\n\n\n<li><strong>Ingests data into an Azure AI Search index<\/strong><\/li>\n\n\n\n<li><strong>Updates MCP<\/strong>\u00a0so the data is\u00a0<strong>searchable and retrievable<\/strong><\/li>\n\n\n\n<li><strong>Keeps your semantic search logic intact<\/strong><\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">I\u2019ll merge your provided snippet into a&nbsp;<strong>full Azure Functions app<\/strong>&nbsp;using&nbsp;<code>func.FunctionApp<\/code>&nbsp;with&nbsp;<strong>anonymous HTTP auth<\/strong>&nbsp;for testing.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Updated Code \u2014&nbsp;<code>function_app.py<\/code><\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\"><br><br><br><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>proceed<\/strong>&nbsp;and finish the updated Azure Functions code so it\u2019s complete, deployable, and ready to:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Pull data from SQL<\/strong><\/li>\n\n\n\n<li><strong>Ingest into Azure AI Search<\/strong><\/li>\n\n\n\n<li><strong>Expose endpoints for ingestion and semantic search<\/strong><\/li>\n\n\n\n<li><strong>Be callable from MCP or any HTTP client<\/strong><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">We stopped at the&nbsp;<code>ingest_sql_to_search<\/code>&nbsp;function, so let\u2019s complete that and add the semantic search endpoint.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Completed&nbsp;<code>function_app.py<\/code><\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>import os<br>import json<br>import pyodbc<br>import azure.functions as func<br>from typing import Any<br>from datetime import datetime<br>from azure.identity import DefaultAzureCredential<br>from azure.search.documents import SearchClient<br>from azure.search.documents.models import IndexDocumentsBatch, IndexAction<br><br># \u2500\u2500 Azure Function App \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br>app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)<br><br># \u2500\u2500 SQL Connection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br>def get_sql_connection():<br>    conn_str = os.environ[\"SQL_CONNECTION_STRING\"]<br>    return pyodbc.connect(conn_str)<br><br>def fetch_sql_data():<br>    \"\"\"Fetch all rows from SQL table for indexing.\"\"\"<br>    with get_sql_connection() as conn:<br>        cursor = conn.cursor()<br>        cursor.execute(\"SELECT id, title, content, lastModified FROM Documents\")<br>        rows = cursor.fetchall()<br>        docs = []<br>        for row in rows:<br>            docs.append({<br>                \"id\": str(row.id),<br>                \"title\": row.title,<br>                \"content\": row.content,<br>                \"lastModified\": row.lastModified.isoformat() if row.lastModified else None<br>            })<br>        return docs<br><br># \u2500\u2500 Azure Search Clients \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br>def get_search_client() -> SearchClient:<br>    return SearchClient(<br>        endpoint=os.environ[\"AZURE_SEARCH_ENDPOINT\"],<br>        index_name=os.environ.get(\"AZURE_SEARCH_INDEX_NAME\", \"csl-metadata\"),<br>        credential=DefaultAzureCredential(),<br>    )<br><br>SEMANTIC_INDEX_NAME = os.environ.get(\"AZURE_SEARCH_SEMANTIC_INDEX_NAME\", \"staff-letters-new\")<br>SEMANTIC_CONFIGURATION_NAME = os.environ.get(<br>    \"AZURE_SEARCH_SEMANTIC_CONFIGURATION_NAME\", \"staff-letters-new-semantic-configuration\"<br>)<br><br>def get_semantic_search_client() -> SearchClient:<br>    return SearchClient(<br>        endpoint=os.environ[\"AZURE_SEARCH_ENDPOINT\"],<br>        index_name=SEMANTIC_INDEX_NAME,<br>        credential=DefaultAzureCredential(),<br>    )<br><br># \u2500\u2500 Ingest Data into AI Search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br>def ingest_data_to_search(docs: list[dict]):<br>    if not docs:<br>        return {\"status\": \"no documents to index\"}<br><br>    client = get_search_client()<br>    batch = IndexDocumentsBatch(actions=[<br>        IndexAction.merge_or_upload(doc) for doc in docs<br>    ])<br>    result = client.index_documents(batch=batch)<br>    return {\"status\": \"indexed\", \"count\": len(docs), \"result\": str(result)}<br><br># \u2500\u2500 Semantic Search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br>def run_semantic_staff_letters_search(args: dict[str, Any]) -> dict[str, Any]:<br>    query = args.get(\"query\")<br>    if not query or not str(query).strip():<br>        raise ValueError(\"query is required\")<br><br>    top: int = int(args.get(\"top\", 50))<br>    skip: int = int(args.get(\"skip\", 0))<br>    semantic_configuration: str = args.get(\"semantic_configuration_name\", SEMANTIC_CONFIGURATION_NAME)<br><br>    if top &lt; 1:<br>        raise ValueError(\"top must be greater than 0\")<br>    if skip &lt; 0:<br>        raise ValueError(\"skip must be 0 or greater\")<br><br>    results = get_semantic_search_client().search(<br>        search_text=query,<br>        query_type=\"semantic\",<br>        semantic_configuration_name=semantic_configuration,<br>        query_caption=\"extractive\",<br>        query_answer=\"extractive\",<br>        top=top,<br>        skip=skip,<br>        include_total_count=True,<br>    )<br><br>    docs = []<br>    for result in results:<br>        doc = dict(result)<br>        doc.pop(\"text_vector\", None)<br>        captions = doc.pop(\"@search.captions\", None)<br>        if captions:<br>            doc[\"captions\"] = [c[\"text\"] for c in captions]<br>        docs.append(doc)<br><br>    return {\"count\": results.get_count(), \"results\": docs}<br><br># \u2500\u2500 HTTP Endpoints \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br>@app.route(route=\"ingest-sql-to-search\", methods=[\"POST\"])<br>def ingest_sql_to_search(req: func.HttpRequest) -> func.HttpResponse:<br>    \"\"\"Fetch data from SQL and ingest into Azure Search.\"\"\"<br>    try:<br>        docs = fetch_sql_data()<br>        result = ingest_data_to_search(docs)<br>        return func.HttpResponse(<br>            json.dumps(result),<\/code><br><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>proceed<\/strong>&nbsp;and finish the last part of the code so your Azure Function app is&nbsp;<strong>fully functional<\/strong>&nbsp;with both ingestion and semantic search endpoints.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We stopped inside the&nbsp;<code>ingest_sql_to_search<\/code>&nbsp;function, so now we\u2019ll complete it and add the&nbsp;<strong>semantic search HTTP route<\/strong>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Final Completed&nbsp;<code>function_app.py<\/code><\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>import os<br>import json<br>import pyodbc<br>import azure.functions as func<br>from typing import Any<br>from datetime import datetime<br>from azure.identity import DefaultAzureCredential<br>from azure.search.documents import SearchClient<br>from azure.search.documents.models import IndexDocumentsBatch, IndexAction<br><br># \u2500\u2500 Azure Function App \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br>app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)<br><br># \u2500\u2500 SQL Connection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br>def get_sql_connection():<br>    conn_str = os.environ[\"SQL_CONNECTION_STRING\"]<br>    return pyodbc.connect(conn_str)<br><br>def fetch_sql_data():<br>    \"\"\"Fetch all rows from SQL table for indexing.\"\"\"<br>    with get_sql_connection() as conn:<br>        cursor = conn.cursor()<br>        cursor.execute(\"SELECT id, title, content, lastModified FROM Documents\")<br>        rows = cursor.fetchall()<br>        docs = []<br>        for row in rows:<br>            docs.append({<br>                \"id\": str(row.id),<br>                \"title\": row.title,<br>                \"content\": row.content,<br>                \"lastModified\": row.lastModified.isoformat() if row.lastModified else None<br>            })<br>        return docs<br><br># \u2500\u2500 Azure Search Clients \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br>def get_search_client() -> SearchClient:<br>    return SearchClient(<br>        endpoint=os.environ[\"AZURE_SEARCH_ENDPOINT\"],<br>        index_name=os.environ.get(\"AZURE_SEARCH_INDEX_NAME\", \"csl-metadata\"),<br>        credential=DefaultAzureCredential(),<br>    )<br><br>SEMANTIC_INDEX_NAME = os.environ.get(\"AZURE_SEARCH_SEMANTIC_INDEX_NAME\", \"staff-letters-new\")<br>SEMANTIC_CONFIGURATION_NAME = os.environ.get(<br>    \"AZURE_SEARCH_SEMANTIC_CONFIGURATION_NAME\", \"staff-letters-new-semantic-configuration\"<br>)<br><br>def get_semantic_search_client() -> SearchClient:<br>    return SearchClient(<br>        endpoint=os.environ[\"AZURE_SEARCH_ENDPOINT\"],<br>        index_name=SEMANTIC_INDEX_NAME,<br>        credential=DefaultAzureCredential(),<br>    )<br><br># \u2500\u2500 Ingest Data into AI Search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br>def ingest_data_to_search(docs: list[dict]):<br>    if not docs:<br>        return {\"status\": \"no documents to index\"}<br><br>    client = get_search_client()<br>    batch = IndexDocumentsBatch(actions=[<br>        IndexAction.merge_or_upload(doc) for doc in docs<br>    ])<br>    result = client.index_documents(batch=batch)<br>    return {\"status\": \"indexed\", \"count\": len(docs), \"result\": str(result)}<br><br># \u2500\u2500 Semantic Search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br>def run_semantic_staff_letters_search(args: dict[str, Any]) -> dict[str, Any]:<br>    query = args.get(\"query\")<br>    if not query or not str(query).strip():<br>        raise ValueError(\"query is required\")<br><br>    top: int = int(args.get(\"top\", 50))<br>    skip: int = int(args.get(\"skip\", 0))<br>    semantic_configuration: str = args.get(\"semantic_configuration_name\", SEMANTIC_CONFIGURATION_NAME)<br><br>    if top &lt; 1:<br>        raise ValueError(\"top must be greater than 0\")<br>    if skip &lt; 0:<br>        raise ValueError(\"skip must be 0 or greater\")<br><br>    results = get_semantic_search_client().search(<br>        search_text=query,<br>        query_type=\"semantic\",<br>        semantic_configuration_name=semantic_configuration,<br>        query_caption=\"extractive\",<br>        query_answer=\"extractive\",<br>        top=top,<br>        skip=skip,<br>        include_total_count=True,<br>    )<br><br>    docs = []<br>    for result in results:<br>        doc = dict(result)<br>        doc.pop(\"text_vector\", None)<br>        captions = doc.pop(\"@search.captions\", None)<br>        if captions:<br>            doc[\"captions\"] = [c[\"text\"] for c in captions]<br>        docs.append(doc)<br><br>    return {\"count\": results.get_count(), \"results\": docs}<br><br># \u2500\u2500 HTTP Endpoints \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500<br>@app.route(route=\"ingest-sql-to-search\", methods=[\"POST\"])<br>def ingest_sql_to_search(req: func.HttpRequest) -> func.HttpResponse:<br>    \"\"\"Fetch data from SQL and ingest into Azure Search.\"\"\"<br>    try:<br>        docs = fetch_sql_data()<br>        result = ingest_data_to_search(docs)<br>        return func.HttpResponse(<br>            json.dumps(result),<br>            mimetype=\"application\/json\",<br>            status_code=200<br>        )<br>    except Exception as e:<br>        return func.HttpResponse(<br>            json<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>proceed<\/strong>&nbsp;and finish the last part of the Azure Function so it\u2019s fully deployable and includes the&nbsp;<strong>semantic search endpoint<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We stopped inside the&nbsp;<code>ingest_sql_to_search<\/code>&nbsp;exception handler, so now we\u2019ll complete that and add the&nbsp;<strong><code>semantic-search<\/code><\/strong>&nbsp;route.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Final Completed&nbsp;<code>function_app.py<\/code><\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>import os\nimport json\nimport pyodbc\nimport azure.functions as func\nfrom typing import Any\nfrom datetime import datetime\nfrom azure.identity import DefaultAzureCredential\nfrom azure.search.documents import SearchClient\nfrom azure.search.documents.models import IndexDocumentsBatch, IndexAction\n\n# \u2500\u2500 Azure Function App \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\napp = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)\n\n# \u2500\u2500 SQL Connection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ndef get_sql_connection():\n    conn_str = os.environ[\"SQL_CONNECTION_STRING\"]\n    return pyodbc.connect(conn_str)\n\ndef fetch_sql_data():\n    \"\"\"Fetch all rows from SQL table for indexing.\"\"\"\n    with get_sql_connection() as conn:\n        cursor = conn.cursor()\n        cursor.execute(\"SELECT id, title, content, lastModified FROM Documents\")\n        rows = cursor.fetchall()\n        docs = []\n        for row in rows:\n            docs.append({\n                \"id\": str(row.id),\n                \"title\": row.title,\n                \"content\": row.content,\n                \"lastModified\": row.lastModified.isoformat() if row.lastModified else None\n            })\n        return docs\n\n# \u2500\u2500 Azure Search Clients \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ndef get_search_client() -&gt; SearchClient:\n    return SearchClient(\n        endpoint=os.environ[\"AZURE_SEARCH_ENDPOINT\"],\n        index_name=os.environ.get(\"AZURE_SEARCH_INDEX_NAME\", \"csl-metadata\"),\n        credential=DefaultAzureCredential(),\n    )\n\nSEMANTIC_INDEX_NAME = os.environ.get(\"AZURE_SEARCH_SEMANTIC_INDEX_NAME\", \"staff-letters-new\")\nSEMANTIC_CONFIGURATION_NAME = os.environ.get(\n    \"AZURE_SEARCH_SEMANTIC_CONFIGURATION_NAME\", \"staff-letters-new-semantic-configuration\"\n)\n\ndef get_semantic_search_client() -&gt; SearchClient:\n    return SearchClient(\n        endpoint=os.environ[\"AZURE_SEARCH_ENDPOINT\"],\n        index_name=SEMANTIC_INDEX_NAME,\n        credential=DefaultAzureCredential(),\n    )\n\n# \u2500\u2500 Ingest Data into AI Search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ndef ingest_data_to_search(docs: list[dict]):\n    if not docs:\n        return {\"status\": \"no documents to index\"}\n\n    client = get_search_client()\n    batch = IndexDocumentsBatch(actions=[\n        IndexAction.merge_or_upload(doc) for doc in docs\n    ])\n    result = client.index_documents(batch=batch)\n    return {\"status\": \"indexed\", \"count\": len(docs), \"result\": str(result)}\n\n# \u2500\u2500 Semantic Search \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\ndef run_semantic_staff_letters_search(args: dict[str, Any]) -&gt; dict[str, Any]:\n    query = args.get(\"query\")\n    if not query or not str(query).strip():\n        raise ValueError(\"query is required\")\n\n    top: int = int(args.get(\"top\", 50))\n    skip: int = int(args.get(\"skip\", 0))\n    semantic_configuration: str = args.get(\"semantic_configuration_name\", SEMANTIC_CONFIGURATION_NAME)\n\n    if top &lt; 1:\n        raise ValueError(\"top must be greater than 0\")\n    if skip &lt; 0:\n        raise ValueError(\"skip must be 0 or greater\")\n\n    results = get_semantic_search_client().search(\n        search_text=query,\n        query_type=\"semantic\",\n        semantic_configuration_name=semantic_configuration,\n        query_caption=\"extractive\",\n        query_answer=\"extractive\",\n        top=top,\n        skip=skip,\n        include_total_count=True,\n    )\n\n    docs = []\n    for result in results:\n        doc = dict(result)\n        doc.pop(\"text_vector\", None)\n        captions = doc.pop(\"@search.captions\", None)\n        if captions:\n            doc[\"captions\"] = [c[\"text\"] for c in captions]\n        docs.append(doc)\n\n    return {\"count\": results.get_count(), \"results\": docs}\n\n# \u2500\u2500 HTTP Endpoints \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n@app.route(route=\"ingest-sql-to-search\", methods=[\"POST\"])\ndef ingest_sql_to_search(req: func.HttpRequest) -&gt; func.HttpResponse:\n    \"\"\"Fetch data from SQL and ingest into Azure Search.\"\"\"\n    try:\n        docs = fetch_sql_data()\n        result = ingest_data_to_search(docs)\n        return func.HttpResponse(\n            json.dumps(result),\n            mimetype=\"application\/json\",\n            status_code=200\n        )\n    except Exception as e:\n        return func.HttpResponse(\n            json<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>SECTION 2:<\/strong> <strong>complete, step-by-step guide<\/strong>\u00a0plus\u00a0<strong>full code<\/strong>\u00a0for an\u00a0<strong>MCP-based Azure Function App<\/strong>\u00a0that:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Connects to\u00a0<strong>Azure AI Search<\/strong>\u00a0(formerly Cognitive Search)<\/li>\n\n\n\n<li>Uses\u00a0<strong>knowledge sources<\/strong>\u00a0(SQL Database in this case)<\/li>\n\n\n\n<li>Indexes the data into a\u00a0<strong>knowledge base<\/strong><\/li>\n\n\n\n<li>Makes it accessible to a\u00a0<strong>UI frontend<\/strong>\u00a0via an API endpoint<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I\u2019ll break this into&nbsp;<strong>phases<\/strong>&nbsp;so you can follow along from setup to deployment.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>PHASE 1 \u2014 Architecture Overview<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Flow:<\/strong><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Azure SQL Database<\/strong>\u00a0\u2192 Data source<\/li>\n\n\n\n<li><strong>Azure Function App (MCP Tool)<\/strong>\u00a0\u2192 Extracts data from SQL, transforms it, and pushes it to Azure AI Search<\/li>\n\n\n\n<li><strong>Azure AI Search Index<\/strong>\u00a0\u2192 Stores searchable knowledge base<\/li>\n\n\n\n<li><strong>UI Frontend<\/strong>\u00a0\u2192 Calls the Function App to query the AI Search index<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>PHASE 2 \u2014 Prerequisites<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Azure Subscription<\/li>\n\n\n\n<li>Azure SQL Database with your data<\/li>\n\n\n\n<li>Azure AI Search service<\/li>\n\n\n\n<li>Azure Function App (Python or Node.js \u2014 here we\u2019ll use Python)<\/li>\n\n\n\n<li>Azure Storage Account (for Function App)<\/li>\n\n\n\n<li>MCP Tool integration in Foundry Agent (optional for automation)<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>PHASE 3 \u2014 Azure AI Search Setup<\/strong><\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Create Azure AI Search service<\/strong>\u00a0in the portal.<\/li>\n\n\n\n<li><strong>Create an index<\/strong>\u00a0(e.g.,\u00a0<code>knowledge-index<\/code>) with fields:\n<ul class=\"wp-block-list\">\n<li><code>id<\/code>\u00a0(key)<\/li>\n\n\n\n<li><code>title<\/code>\u00a0(searchable)<\/li>\n\n\n\n<li><code>content<\/code>\u00a0(searchable, retrievable)<\/li>\n\n\n\n<li><code>lastModified<\/code>\u00a0(filterable, sortable)<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Get API Key<\/strong>\u00a0and\u00a0<strong>Endpoint URL<\/strong>\u00a0from Azure AI Search portal.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>PHASE 4 \u2014 Azure SQL Database Setup<\/strong><\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Ensure your SQL DB has a table with at least:\n<ul class=\"wp-block-list\">\n<li><code>Id<\/code>\u00a0(primary key)<\/li>\n\n\n\n<li><code>Title<\/code><\/li>\n\n\n\n<li><code>Content<\/code><\/li>\n\n\n\n<li><code>LastModified<\/code><\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>Allow Azure services to access the DB.<\/li>\n\n\n\n<li>Create a\u00a0<strong>read-only SQL user<\/strong>\u00a0for the Function App.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>PHASE 5 \u2014 Azure Function App MCP Tool<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">We\u2019ll create a&nbsp;<strong>Python Azure Function<\/strong>&nbsp;that:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Connects to SQL DB<\/li>\n\n\n\n<li>Reads new\/updated rows<\/li>\n\n\n\n<li>Pushes them to Azure AI Search index<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong><code>function_app\/__init__.py<\/code><\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">Python<br><br><code>import logging<br>import json<br>import pyodbc<br>import azure.functions as func<br>import requests<br>from datetime import datetime<br><br># Azure AI Search config<br>SEARCH_ENDPOINT = \"https:\/\/&lt;YOUR-SEARCH-NAME>.search.windows.net\"<br>SEARCH_API_KEY = \"&lt;YOUR-SEARCH-ADMIN-KEY>\"<br>SEARCH_INDEX = \"knowledge-index\"<br><br># SQL config<br>SQL_SERVER = \"&lt;YOUR-SQL-SERVER>.database.windows.net\"<br>SQL_DATABASE = \"&lt;YOUR-DATABASE>\"<br>SQL_USERNAME = \"&lt;YOUR-USERNAME>\"<br>SQL_PASSWORD = \"&lt;YOUR-PASSWORD>\"<br>SQL_DRIVER = \"{ODBC Driver 18 for SQL Server}\"<br><br>def get_sql_data(last_sync=None):<br>    conn_str = f\"DRIVER={SQL_DRIVER};SERVER={SQL_SERVER};DATABASE={SQL_DATABASE};UID={SQL_USERNAME};PWD={SQL_PASSWORD}\"<br>    conn = pyodbc.connect(conn_str)<br>    cursor = conn.cursor()<br><br>    if last_sync:<br>        cursor.execute(\"SELECT Id, Title, Content, LastModified FROM KnowledgeTable WHERE LastModified > ?\", last_sync)<br>    else:<br>        cursor.execute(\"SELECT Id, Title, Content, LastModified FROM KnowledgeTable\")<br><br>    rows = cursor.fetchall()<br>    conn.close()<br><br>    docs = []<br>    for row in rows:<br>        docs.append({<br>            \"id\": str(row.Id),<br>            \"title\": row.Title,<br>            \"content\": row.Content,<br>            \"lastModified\": row.LastModified.isoformat()<br>        })<br>    return docs<br><br>def push_to_ai_search(docs):<br>    url = f\"{SEARCH_ENDPOINT}\/indexes\/{SEARCH_INDEX}\/docs\/index?api-version=2023-07-01-Preview\"<br>    headers = {<br>        \"Content-Type\": \"application\/json\",<br>        \"api-key\": SEARCH_API_KEY<br>    }<br>    payload = {<br>        \"value\": [{\"@search.action\": \"mergeOrUpload\", **doc} for doc in docs]<br>    }<br>    response = requests.post(url, headers=headers, json=payload)<br>    response.raise_for_status()<br>    return response.json()<br><br>def main(req: func.HttpRequest) -> func.HttpResponse:<br>    logging.info(\"MCP Function triggered for SQL \u2192 AI Search indexing\")<br><br>    last_sync = req.params.get(\"lastSync\")<br>    docs = get_sql_data(last_sync)<br><br>    if not docs:<br>        return func.HttpResponse(json.dumps({\"message\": \"No new documents\"}), mimetype=\"application<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">&#8212;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">et\u2019s&nbsp;<strong>proceed<\/strong>&nbsp;and finish the MCP-based Azure Function so it can fully index SQL data into&nbsp;<strong>Azure AI Search<\/strong>&nbsp;and be ready for your&nbsp;<strong>UI frontend<\/strong>&nbsp;to consume.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We left off inside the&nbsp;<code>main()<\/code>&nbsp;function \u2014 now we\u2019ll complete it, add error handling, and then I\u2019ll give you the&nbsp;<strong>UI integration steps<\/strong>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>PHASE 5 (continued) \u2014 Complete Azure Function<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong><code>function_app\/__init__.py<\/code>&nbsp;(Final Version)<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">Python<br><br><code>import logging<br>import json<br>import pyodbc<br>import azure.functions as func<br>import requests<br>from datetime import datetime<br><br># Azure AI Search config<br>SEARCH_ENDPOINT = \"https:\/\/&lt;YOUR-SEARCH-NAME>.search.windows.net\"<br>SEARCH_API_KEY = \"&lt;YOUR-SEARCH-ADMIN-KEY>\"<br>SEARCH_INDEX = \"knowledge-index\"<br><br># SQL config<br>SQL_SERVER = \"&lt;YOUR-SQL-SERVER>.database.windows.net\"<br>SQL_DATABASE = \"&lt;YOUR-DATABASE>\"<br>SQL_USERNAME = \"&lt;YOUR-USERNAME>\"<br>SQL_PASSWORD = \"&lt;YOUR-PASSWORD>\"<br>SQL_DRIVER = \"{ODBC Driver 18 for SQL Server}\"<br><br>def get_sql_data(last_sync=None):<br>    conn_str = f\"DRIVER={SQL_DRIVER};SERVER={SQL_SERVER};DATABASE={SQL_DATABASE};UID={SQL_USERNAME};PWD={SQL_PASSWORD}\"<br>    conn = pyodbc.connect(conn_str)<br>    cursor = conn.cursor()<br><br>    if last_sync:<br>        cursor.execute(\"\"\"<br>            SELECT Id, Title, Content, LastModified <br>            FROM KnowledgeTable <br>            WHERE LastModified > ?<br>        \"\"\", last_sync)<br>    else:<br>        cursor.execute(\"\"\"<br>            SELECT Id, Title, Content, LastModified <br>            FROM KnowledgeTable<br>        \"\"\")<br><br>    rows = cursor.fetchall()<br>    conn.close()<br><br>    docs = []<br>    for row in rows:<br>        docs.append({<br>            \"id\": str(row.Id),<br>            \"title\": row.Title,<br>            \"content\": row.Content,<br>            \"lastModified\": row.LastModified.isoformat()<br>        })<br>    return docs<br><br>def push_to_ai_search(docs):<br>    url = f\"{SEARCH_ENDPOINT}\/indexes\/{SEARCH_INDEX}\/docs\/index?api-version=2023-07-01-Preview\"<br>    headers = {<br>        \"Content-Type\": \"application\/json\",<br>        \"api-key\": SEARCH_API_KEY<br>    }<br>    payload = {<br>        \"value\": [{\"@search.action\": \"mergeOrUpload\", **doc} for doc in docs]<br>    }<br>    response = requests.post(url, headers=headers, json=payload)<br>    response.raise_for_status()<br>    return response.json()<br><br>def main(req: func.HttpRequest) -> func.HttpResponse:<br>    logging.info(\"MCP Function triggered for SQL \u2192 AI Search indexing\")<br><br>    try:<br>        last_sync = req.params.get(\"lastSync\")<br>        if last_sync:<br>            try:<br>                last_sync = datetime.fromisoformat(last_sync)<br>            except ValueError:<br>                return func.HttpResponse(<br>                    json.dumps({\"error\": \"Invalid lastSync format. Use ISO 8601.\"}),<br>                    mimetype=\"application\/json\",<br>                    status_code=400<br>                )<br><br>        docs = get_sql_data(last_sync)<br><br>        if not docs:<br>            return func.HttpResponse(<br>                json.dumps({\"message\": \"No new documents found\"}),<br>                mimetype=\"application\/json\",<br>                status_code=200<br>            )<br><br>        result = push_to_ai_search(docs)<br><br>        return func.HttpResponse(<br>            json.dumps({<br>                \"message\": f\"Indexed {len(docs)} documents successfully\",<br>                \"aiSearchResponse\": result<br>            }),<br>            mimetype=\"application\/json\",<br>            status_code=200<br>        )<br><br>    except Exception as e:<br>        logging.error(f\"Error during indexing: {str(e)}\", exc_info=True)<br>        return func.HttpResponse(<br>            json.dumps({\"error\": str(e)}),<br>            mimetype=\"application\/json\",<br>            status_code=500<br>        )<br><\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>PHASE 6 \u2014 Deployment Steps<\/strong><\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Create Function App<\/strong>\u00a0in Azure Portal (Python runtime).<\/li>\n\n\n\n<li><strong>Add Application Settings<\/strong>:\n<ul class=\"wp-block-list\">\n<li><code>SEARCH_ENDPOINT<\/code><\/li>\n\n\n\n<li><code>SEARCH_API_KEY<\/code><\/li>\n\n\n\n<li><code>SEARCH_INDEX<\/code><\/li>\n\n\n\n<li><code>SQL_SERVER<\/code><\/li>\n\n\n\n<li><code>SQL_DATABASE<\/code><\/li>\n\n\n\n<li><code>SQL_USERNAME<\/code><\/li>\n\n\n\n<li><code>SQL_PASSWORD<\/code><\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Deploy<\/strong>\u00a0using VS Code Azure Functions extension or\u00a0<code>func azure functionapp publish &lt;APP_NAME><\/code>.<\/li>\n\n\n\n<li><strong>alternative deployment methods<\/strong>\u00a0for your Azure Functions project (in case\u00a0<code>func azure functionapp publish<\/code>\u00a0fails), plus a\u00a0<strong>recommended folder structure<\/strong>\u00a0so your codebase stays clean and production-ready.<br>I\u2019ll break this into two parts:<br><strong>1. Alternative Ways to Deploy Azure Functions<\/strong><br>If\u00a0<code>func azure functionapp publish<\/code>\u00a0doesn\u2019t work (common issues: auth errors, CLI version mismatch, network restrictions), here are\u00a0<strong>three reliable alternatives<\/strong>:<br><strong>A. Deploy via Azure Portal (Zip Deploy)<\/strong><br><strong>Build your function locally<\/strong>:<br>Bash<br><br>Copy code<br><code>func azure functionapp publish &lt;APP_NAME> --build local --nozip<\/code><br>This creates a\u00a0<code>.zip<\/code>\u00a0in your\u00a0<code>bin\/Release<\/code>\u00a0or\u00a0<code>bin\/Debug<\/code>\u00a0folder (for C#) or\u00a0<code>.python_packages<\/code>\u00a0(for Python).<br><strong>Go to Azure Portal<\/strong>\u00a0\u2192 Your Function App \u2192\u00a0<strong>Deployment Center<\/strong>\u00a0\u2192\u00a0<strong>Zip Deploy<\/strong>.<br>Upload the\u00a0<code>.zip<\/code>\u00a0file manually.<br>&#x2705;\u00a0<strong>Pros<\/strong>: No CLI dependency, works behind firewalls.<br>&#x26a0;&#xfe0f;\u00a0<strong>Cons<\/strong>: Manual step, not great for CI\/CD.<br><strong>B. Deploy via Azure CLI + Zip Deploy<\/strong><br>Bash<br><br>Copy code<br><code># Create zip zip -r functionapp.zip * .[^.]* -x \"*.git*\" \"*.vscode*\" \"__pycache__\/*\" # Deploy az functionapp deployment source config-zip \\ --resource-group &lt;RESOURCE_GROUP> \\ --name &lt;APP_NAME> \\ --src functionapp.zip<\/code><br>&#x2705;\u00a0<strong>Pros<\/strong>: Scriptable, works in CI\/CD.<br>&#x26a0;&#xfe0f;\u00a0<strong>Cons<\/strong>: Must manage zip creation.<br><strong>C. Deploy via GitHub Actions (CI\/CD)<\/strong><br>Push your code to GitHub.<br>In Azure Portal \u2192 Function App \u2192\u00a0<strong>Deployment Center<\/strong>\u00a0\u2192\u00a0<strong>GitHub Actions<\/strong>\u00a0\u2192 Configure.<br>Azure auto-generates a\u00a0<code>.github\/workflows\/azure-functions.yml<\/code>\u00a0file.<br>&#x2705;\u00a0<strong>Pros<\/strong>: Fully automated, integrates with PRs.<br>&#x26a0;&#xfe0f;\u00a0<strong>Cons<\/strong>: Requires GitHub repo and permissions.<br><strong>D. Deploy via VS Code Azure Functions Extension<\/strong><br>Install\u00a0<strong>Azure Functions<\/strong>\u00a0extension in VS Code.<br>Sign in to Azure in VS Code.<br>Right-click your function project \u2192\u00a0<strong>Deploy to Function App<\/strong>.<br>&#x2705;\u00a0<strong>Pros<\/strong>: One-click deploy, good for dev\/test.<br>&#x26a0;&#xfe0f;\u00a0<strong>Cons<\/strong>: Not ideal for production automation.<br><strong>2. Recommended Folder Structure<\/strong><br>Here\u2019s a\u00a0<strong>Python Azure Functions<\/strong>\u00a0project structure that works well for your\u00a0<strong>scheduled + event-driven + HTTP API<\/strong>\u00a0setup:<br><br><br>Copy code<br><code>project-root\/ \u2502 \u251c\u2500\u2500 host.json # Global function app settings \u251c\u2500\u2500 local.settings.json # Local dev settings (not committed) \u251c\u2500\u2500 requirements.txt # Python dependencies \u251c\u2500\u2500 .funcignore # Files to ignore during publish \u251c\u2500\u2500 .gitignore \u2502 \u251c\u2500\u2500 shared\/ # Shared code modules \u2502 \u251c\u2500\u2500 __init__.py \u2502 \u251c\u2500\u2500 index_manager.py # Push\/delete docs in AI Search \u2502 \u251c\u2500\u2500 sql_connector.py # SQL queries + CDC fetch \u2502 \u251c\u2500\u2500 blob_state.py # Last sync state management \u2502 \u2514\u2500\u2500 event_processor.py # Event Grid payload handling \u2502 \u251c\u2500\u2500 timer_sync\/ # Scheduled sync function \u2502 \u251c\u2500\u2500 __init__.py \u2502 \u2514\u2500\u2500 function.json \u2502 \u251c\u2500\u2500 eventgrid_sync\/ # Real-time Event Grid function \u2502 \u251c\u2500\u2500 __init__.py \u2502 \u2514\u2500\u2500 function.json \u2502 \u251c\u2500\u2500 get_index_status\/ # HTTP API for UI\/MCP \u2502 \u251c\u2500\u2500 __init__.py \u2502 \u2514\u2500\u2500 function.json \u2502 \u2514\u2500\u2500 manual_refresh\/ # HTTP API to trigger reindex \u251c\u2500\u2500 __init__.py \u2514\u2500\u2500 function.json<\/code><br><strong>3. Deployment Tips if CLI Publish Fails<\/strong><br><strong>Check Azure Functions Core Tools version<\/strong>:<br>Bash<br><br>Copy code<br><code>func --version<\/code><br>Make sure it matches your runtime (e.g., v4 for Python 3.9+).<br><strong>Force build before publish<\/strong>:<br>Bash<br><br>Copy code<br><code>func azure functionapp publish &lt;APP_NAME> --build local<\/code><br><strong>Use\u00a0<code>az webapp deployment<\/code>\u00a0as fallback<\/strong>:<br>Bash<br><br>code<br><code>az webapp deployment source config-local-git \\ --name &lt;APP_NAME> \\ --resource-group &lt;RESOURCE_GROUP><\/code><br>Then push via Git:<br>Bash<br><br><br><code>git push azure master<\/code><\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">&#8211;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>proceed<\/strong>&nbsp;and finish this with a&nbsp;<strong>full deployment blueprint<\/strong>&nbsp;so you have a&nbsp;<strong>step-by-step fallback plan<\/strong>&nbsp;for getting your Azure Functions + AI Search + SQL CDC\/Event Grid solution live, even if&nbsp;<code>func azure functionapp publish<\/code>&nbsp;fails.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I\u2019ll give you:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Deployment Strategy Matrix<\/strong>\u00a0(choose based on your environment)<\/li>\n\n\n\n<li><strong>Step-by-Step for Each Method<\/strong><\/li>\n\n\n\n<li><strong>CI\/CD Pipeline Example<\/strong>\u00a0(GitHub Actions)<\/li>\n\n\n\n<li><strong>Post-Deployment Verification Checklist<\/strong><\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>1. Deployment Strategy Matrix<\/strong><\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Method<\/th><th class=\"has-text-align-left\" data-align=\"left\">Best For<\/th><th class=\"has-text-align-left\" data-align=\"left\">Pros<\/th><th class=\"has-text-align-left\" data-align=\"left\">Cons<\/th><\/tr><\/thead><tbody><tr><td><strong>Zip Deploy via Azure CLI<\/strong><\/td><td>Quick manual\/scripted deploy<\/td><td>Works without Core Tools, easy to automate<\/td><td>Must package manually<\/td><\/tr><tr><td><strong>GitHub Actions CI\/CD<\/strong><\/td><td>Production automation<\/td><td>Fully automated, integrates with PRs<\/td><td>Requires GitHub repo<\/td><\/tr><tr><td><strong>VS Code Deploy<\/strong><\/td><td>Dev\/test<\/td><td>One-click, no CLI needed<\/td><td>Not ideal for production<\/td><\/tr><tr><td><strong>Local Git Deploy<\/strong><\/td><td>Teams with Git workflows<\/td><td>Simple push-to-deploy<\/td><td>Slower than Zip Deploy<\/td><\/tr><tr><td><strong>Azure DevOps Pipeline<\/strong><\/td><td>Enterprise CI\/CD<\/td><td>Full control, approvals<\/td><td>More setup time<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>2. Step-by-Step for Each Method<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>A. Zip Deploy via Azure CLI<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">BashCopy code<code># 1. Create zip (exclude dev files)\nzip -r functionapp.zip * .[^.]* -x \"*.git*\" \"*.vscode*\" \"__pycache__\/*\"\n\n# 2. Deploy\naz functionapp deployment source config-zip \\\n  --resource-group &lt;RESOURCE_GROUP&gt; \\\n  --name &lt;APP_NAME&gt; \\\n  --src functionapp.zip\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>B. GitHub Actions CI\/CD<\/strong><\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Push your project to GitHub.<\/li>\n\n\n\n<li>In Azure Portal \u2192 Function App \u2192\u00a0<strong>Deployment Center<\/strong>\u00a0\u2192\u00a0<strong>GitHub Actions<\/strong>\u00a0\u2192 Configure.<\/li>\n\n\n\n<li>Azure generates\u00a0<code>.github\/workflows\/azure-functions.yml<\/code>:<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-preformatted\">YamlCopy code<code>name: Deploy Azure Function App\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  build-and-deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions\/checkout@v3\n      - name: Setup Python\n        uses: actions\/setup-python@v4\n        with:\n          python-version: '3.10'\n      - name: Install dependencies\n        run: pip install -r requirements.txt\n      - name: Azure Functions Action\n        uses: Azure\/functions-action@v1\n        with:\n          app-name: &lt;APP_NAME&gt;\n          package: '.'\n          publish-profile: ${{ secrets.AZURE_FUNCTIONAPP_PUBLISH_PROFILE }}\n<\/code><\/pre>\n\n\n\n<ol start=\"4\" class=\"wp-block-list\">\n<li>Get the\u00a0<strong>Publish Profile<\/strong>\u00a0from Azure Portal \u2192 Function App \u2192\u00a0<strong>Get Publish Profile<\/strong>\u00a0and store it in GitHub Secrets as\u00a0<code>AZURE_FUNCTIONAPP_PUBLISH_PROFILE<\/code>.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>C. VS Code Deploy<\/strong><\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Install\u00a0<strong>Azure Functions<\/strong>\u00a0extension.<\/li>\n\n\n\n<li>Sign in to Azure in VS Code.<\/li>\n\n\n\n<li>Right-click project \u2192\u00a0<strong>Deploy to Function App<\/strong>.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>D. Local Git Deploy<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">BashCopy code<code># Enable local git deployment\naz functionapp deployment source config-local-git \\\n  --name &lt;APP_NAME&gt; \\\n  --resource-group &lt;RESOURCE_GROUP&gt;\n\n# Add remote\ngit remote add azure &lt;DEPLOYMENT_GIT_URL&gt;\n\n# Push\ngit push azure main\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>3. Recommended Folder Structure (Recap)<\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">Copy code<code>project-root\/<br>\u2502<br>\u251c\u2500\u2500 host.json<br>\u251c\u2500\u2500 local.settings.json<br>\u251c\u2500\u2500 requirements.txt<br>\u251c\u2500\u2500 .funcignore<br>\u251c\u2500\u2500 .gitignore<br>\u2502<br>\u251c\u2500\u2500 shared\/<br>\u2502   \u251c\u2500\u2500 index_manager.py<br>\u2502   \u251c\u2500\u2500 sql_connector.py<br>\u2502   \u251c\u2500\u2500 blob_state.py<br>\u2502   \u2514\u2500\u2500 event_processor.py<br>\u2502<br>\u251c\u2500\u2500 timer_sync\/<br>\u2502   \u251c\u2500\u2500 __init__.py<br>\u2502   \u2514\u2500\u2500 function.json<br>\u2502<br>\u251c\u2500\u2500 eventgrid_sync\/<br>\u2502   \u251c\u2500\u2500 __init__.py<br>\u2502   \u2514\u2500\u2500 function.json<br>\u2502<br>\u251c\u2500\u2500 get_index_status\/<br>\u2502   \u251c\u2500\u2500 __init__.py<br>\u2502   \u2514\u2500\u2500 function.json<br>\u2502<br>\u2514\u2500\u2500 manual_refresh\/<br>    \u251c\u2500\u2500 __init__.py<br>    \u2514\u2500\u2500 function.json<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">&#8211;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>wrap this up<\/strong>&nbsp;with the&nbsp;<strong>final Azure Functions project structure<\/strong>&nbsp;and&nbsp;<strong>code for each file<\/strong>&nbsp;so you have a complete, working baseline for:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Timer-triggered sync<\/strong>\u00a0(scheduled)<\/li>\n\n\n\n<li><strong>Event Grid-triggered sync<\/strong>\u00a0(real-time)<\/li>\n\n\n\n<li><strong>HTTP API<\/strong>\u00a0for status and manual refresh<\/li>\n\n\n\n<li><strong>Shared modules<\/strong>\u00a0for SQL, Blob state, AI Search, and Event processing<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">This will be&nbsp;<strong>Python-based<\/strong>&nbsp;(Azure Functions v4, Python 3.10+), but the structure works for other runtimes too.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>1. Final Folder Structure<\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">Copy code<code>project-root\/\n\u2502\n\u251c\u2500\u2500 host.json\n\u251c\u2500\u2500 local.settings.json\n\u251c\u2500\u2500 requirements.txt\n\u251c\u2500\u2500 .funcignore\n\u251c\u2500\u2500 .gitignore\n\u2502\n\u251c\u2500\u2500 shared\/\n\u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u251c\u2500\u2500 index_manager.py\n\u2502   \u251c\u2500\u2500 sql_connector.py\n\u2502   \u251c\u2500\u2500 blob_state.py\n\u2502   \u2514\u2500\u2500 event_processor.py\n\u2502\n\u251c\u2500\u2500 timer_sync\/\n\u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u2514\u2500\u2500 function.json\n\u2502\n\u251c\u2500\u2500 eventgrid_sync\/\n\u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u2514\u2500\u2500 function.json\n\u2502\n\u251c\u2500\u2500 get_index_status\/\n\u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u2514\u2500\u2500 function.json\n\u2502\n\u2514\u2500\u2500 manual_refresh\/\n    \u251c\u2500\u2500 __init__.py\n    \u2514\u2500\u2500 function.json\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>2. Root Files<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>host.json<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">JsonCopy code<code>{\n  \"version\": \"2.0\",\n  \"logging\": {\n    \"applicationInsights\": {\n      \"samplingSettings\": {\n        \"isEnabled\": true,\n        \"excludedTypes\": \"Request\"\n      }\n    }\n  }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>local.settings.json<\/code><\/strong>&nbsp;<em>(do not commit to Git)<\/em><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">JsonCopy code<code>{\n  \"IsEncrypted\": false,\n  \"Values\": {\n    \"AzureWebJobsStorage\": \"UseDevelopmentStorage=true\",\n    \"FUNCTIONS_WORKER_RUNTIME\": \"python\",\n    \"SQL_CONNECTION_STRING\": \"&lt;your-sql-connection-string&gt;\",\n    \"BLOB_CONNECTION_STRING\": \"&lt;your-blob-connection-string&gt;\",\n    \"BLOB_CONTAINER_NAME\": \"state\",\n    \"BLOB_STATE_FILE\": \"last_sync.txt\",\n    \"AI_SEARCH_ENDPOINT\": \"&lt;your-ai-search-endpoint&gt;\",\n    \"AI_SEARCH_KEY\": \"&lt;your-ai-search-key&gt;\",\n    \"AI_SEARCH_INDEX\": \"knowledge-index\"\n  }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>requirements.txt<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">Copy code<code>azure-functions\nazure-storage-blob\npyodbc\nrequests\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>3. Shared Modules<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>shared\/index_manager.py<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>import requests\nimport os\n\nclass IndexManager:\n    def __init__(self):\n        self.endpoint = os.environ[\"AI_SEARCH_ENDPOINT\"]\n        self.api_key = os.environ[\"AI_SEARCH_KEY\"]\n        self.index_name = os.environ[\"AI_SEARCH_INDEX\"]\n\n    def push_docs(self, docs):\n        url = f\"{self.endpoint}\/indexes\/{self.index_name}\/docs\/index?api-version=2021-04-30-Preview\"\n        headers = {\"Content-Type\": \"application\/json\", \"api-key\": self.api_key}\n        payload = {\"value\": docs}\n        r = requests.post(url, headers=headers, json=payload)\n        r.raise_for_status()\n        return r.json()\n\n    def delete_docs(self, ids):\n        url = f\"{self.endpoint}\/indexes\/{self.index_name}\/docs\/index?api-version=2021-04-30-Preview\"\n        headers = {\"Content-Type\": \"application\/json\", \"api-key\": self.api_key}\n        payload = {\"value\": [{\"@search.action\": \"delete\", \"id\": i} for i in ids]}\n        r = requests.post(url, headers=headers, json=payload)\n        r.raise_for_status()\n        return r.json()\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>shared\/sql_connector.py<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>import pyodbc\nimport os\n\nclass SqlConnector:\n    def __init__(self):\n        self.conn_str = os.environ[\"SQL_CONNECTION_STRING\"]\n\n    def get_all_ids(self):\n        with pyodbc.connect(self.conn_str) as conn:\n            cursor = conn.cursor()\n            cursor.execute(\"SELECT id FROM Documents\")\n            return [row.id for row in cursor.fetchall()]\n\n    def get_changes(self, last_sync):\n        with pyodbc.connect(self.conn_str) as conn:\n            cursor = conn.cursor()\n            cursor.execute(\"SELECT id, title, content FROM Documents WHERE lastModified &gt; ?\", last_sync)\n            return [{\"id\": row.id, \"title\": row.title, \"content\": row.content} for row in cursor.fetchall()]\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>shared\/blob_state.py<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">\n<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">let\u2019s&nbsp;<strong>proceed<\/strong>&nbsp;and finish the remaining files so you have the&nbsp;<strong>complete working code<\/strong>&nbsp;for your Azure Functions project.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We stopped at&nbsp;<code>shared\/blob_state.py<\/code>, so let\u2019s continue from there.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>3. Shared Modules (continued)<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>shared\/blob_state.py<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>from azure.storage.blob import BlobServiceClient\nimport os\nfrom datetime import datetime\n\nclass BlobState:\n    def __init__(self):\n        self.conn_str = os.environ[\"BLOB_CONNECTION_STRING\"]\n        self.container_name = os.environ[\"BLOB_CONTAINER_NAME\"]\n        self.state_file = os.environ[\"BLOB_STATE_FILE\"]\n        self.client = BlobServiceClient.from_connection_string(self.conn_str)\n        self.container_client = self.client.get_container_client(self.container_name)\n\n    def get_last_sync(self):\n        try:\n            blob_client = self.container_client.get_blob_client(self.state_file)\n            data = blob_client.download_blob().readall().decode(\"utf-8\")\n            return datetime.fromisoformat(data.strip())\n        except Exception:\n            return None\n\n    def set_last_sync(self, timestamp: datetime):\n        blob_client = self.container_client.get_blob_client(self.state_file)\n        blob_client.upload_blob(timestamp.isoformat(), overwrite=True)\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>shared\/event_processor.py<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>from .index_manager import IndexManager\n\nclass EventProcessor:\n    def __init__(self):\n        self.index_manager = IndexManager()\n\n    def process_event(self, event):\n        data = event.get(\"data\", {})\n        op_type = data.get(\"operationType\")\n        doc = data.get(\"document\", {})\n\n        if op_type in (\"insert\", \"update\"):\n            doc[\"@search.action\"] = \"mergeOrUpload\"\n            return self.index_manager.push_docs([doc])\n        elif op_type == \"delete\":\n            return self.index_manager.delete_docs([doc.get(\"id\")])\n        else:\n            return {\"status\": \"ignored\"}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>4. Function Implementations<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>A. Timer Trigger \u2014&nbsp;<code>timer_sync\/__init__.py<\/code><\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>import datetime\nimport azure.functions as func\nfrom ..shared.sql_connector import SqlConnector\nfrom ..shared.index_manager import IndexManager\nfrom ..shared.blob_state import BlobState\n\ndef main(mytimer: func.TimerRequest) -&gt; None:\n    sql = SqlConnector()\n    index = IndexManager()\n    state = BlobState()\n\n    last_sync = state.get_last_sync()\n    now = datetime.datetime.utcnow()\n\n    changes = sql.get_changes(last_sync) if last_sync else sql.get_changes(datetime.datetime(1970, 1, 1))\n    if changes:\n        index.push_docs(changes)\n        state.set_last_sync(now)\n        print(f\"Indexed {len(changes)} documents.\")\n    else:\n        print(\"No changes found.\")\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>timer_sync\/function.json<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">JsonCopy code<code>{\n  \"scriptFile\": \"__init__.py\",\n  \"bindings\": [\n    {\n      \"name\": \"mytimer\",\n      \"type\": \"timerTrigger\",\n      \"direction\": \"in\",\n      \"schedule\": \"0 0 * * * *\"\n    }\n  ]\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>B. Event Grid Trigger \u2014&nbsp;<code>eventgrid_sync\/__init__.py<\/code><\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>import azure.functions as func\nimport json\nfrom ..shared.event_processor import EventProcessor\n\ndef main(event: func.EventGridEvent):\n    processor = EventProcessor()\n    event_data = json.loads(event.get_body().decode())\n    result = processor.process_event(event_data)\n    print(f\"Processed EventGrid event: {result}\")\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>eventgrid_sync\/function.json<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">JsonCopy code<code>{\n  \"scriptFile\": \"__init__.py\",\n  \"bindings\": [\n    {\n      \"name\": \"event\",\n      \"type\": \"eventGridTrigger\",\n      \"direction\": \"in\"\n    }\n  ]\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>C. HTTP API \u2014 Get Index Status \u2014&nbsp;<code>get_index_status\/__init__.py<\/code><\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>import azure.functions as func\nfrom ..shared.blob_state import BlobState\nfrom ..shared.sql_connector import SqlConnector\n\ndef main(req: func.HttpRequest) -&gt; func.HttpResponse:\n    state = BlobState()\n    sql = SqlConnector()\n\n    last_sync = state.get_last_sync()\n    doc_count = len(sql.get_all_ids())\n\n    return func.HttpResponse(\n        body={\n            \"lastSync\": last_sync.isoformat() if last_sync else None,\n            \"docCount\": doc_count\n        },\n        mimetype=\"application\/json\"\n    )\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>get_index_status\/function.json<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">JsonCopy code<code>{<br>  \"scriptFile\": \"__init__.py\",<br>  \"bindings\": [<br>    {<br>      \"authLevel\": \"function\",<br>      \"type\": \"httpTrigger\",<br>      \"direction<\/code><br><br><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>proceed<\/strong>&nbsp;and finish the last pieces so you have the&nbsp;<strong>full working Azure Functions project<\/strong>&nbsp;with all files in place.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We stopped at&nbsp;<code>get_index_status\/function.json<\/code>, so now we\u2019ll complete that and add the&nbsp;<strong>manual refresh<\/strong>&nbsp;function.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>C. HTTP API \u2014 Get Index Status (continued)<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>get_index_status\/function.json<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">JsonCopy code<code>{\n  \"scriptFile\": \"__init__.py\",\n  \"bindings\": [\n    {\n      \"authLevel\": \"function\",\n      \"type\": \"httpTrigger\",\n      \"direction\": \"in\",\n      \"name\": \"req\",\n      \"methods\": [ \"get\" ]\n    },\n    {\n      \"type\": \"http\",\n      \"direction\": \"out\",\n      \"name\": \"$return\"\n    }\n  ]\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>D. HTTP API \u2014 Manual Refresh<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>manual_refresh\/__init__.py<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>import datetime\nimport azure.functions as func\nfrom ..shared.sql_connector import SqlConnector\nfrom ..shared.index_manager import IndexManager\nfrom ..shared.blob_state import BlobState\n\ndef main(req: func.HttpRequest) -&gt; func.HttpResponse:\n    sql = SqlConnector()\n    index = IndexManager()\n    state = BlobState()\n\n    now = datetime.datetime.utcnow()\n    changes = sql.get_changes(datetime.datetime(1970, 1, 1))  # full refresh\n\n    if changes:\n        index.push_docs(changes)\n        state.set_last_sync(now)\n        return func.HttpResponse(\n            body='{\"message\": \"Manual refresh completed\"}',\n            mimetype=\"application\/json\"\n        )\n    else:\n        return func.HttpResponse(\n            body='{\"message\": \"No documents to refresh\"}',\n            mimetype=\"application\/json\"\n        )\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>manual_refresh\/function.json<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">JsonCopy code<code>{\n  \"scriptFile\": \"__init__.py\",\n  \"bindings\": [\n    {\n      \"authLevel\": \"function\",\n      \"type\": \"httpTrigger\",\n      \"direction\": \"in\",\n      \"name\": \"req\",\n      \"methods\": [ \"post\" ]\n    },\n    {\n      \"type\": \"http\",\n      \"direction\": \"out\",\n      \"name\": \"$return\"\n    }\n  ]\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>E. .funcignore<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This ensures unnecessary files aren\u2019t deployed.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">Copy code<code>.git*\n.vscode\/\n__pycache__\/\nlocal.settings.json\n*.pyc\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>F. .gitignore<\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">Copy code<code>__pycache__\/\n*.pyc\nlocal.settings.json\n.env\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>G. How It All Works Together<\/strong><\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Timer Trigger (<code>timer_sync<\/code>)<\/strong>\n<ul class=\"wp-block-list\">\n<li>Runs on schedule (every hour in this example).<\/li>\n\n\n\n<li>Checks last sync timestamp in Blob Storage.<\/li>\n\n\n\n<li>Pulls changed docs from SQL.<\/li>\n\n\n\n<li>Pushes them to Azure AI Search.<\/li>\n\n\n\n<li>Updates last sync timestamp in Blob.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Event Grid Trigger (<code>eventgrid_sync<\/code>)<\/strong>\n<ul class=\"wp-block-list\">\n<li>Fires instantly when a change event is sent from Event Grid.<\/li>\n\n\n\n<li>Processes insert\/update\/delete events.<\/li>\n\n\n\n<li>Updates AI Search index accordingly.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>HTTP API \u2014 Get Index Status (<code>get_index_status<\/code>)<\/strong>\n<ul class=\"wp-block-list\">\n<li>Returns last sync timestamp and total doc count.<\/li>\n\n\n\n<li>Useful for monitoring and MCP Inspector testing.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>HTTP API \u2014 Manual Refresh (<code>manual_refresh<\/code>)<\/strong>\n<ul class=\"wp-block-list\">\n<li>Forces a full reindex from SQL to AI Search.<\/li>\n\n\n\n<li>Updates last sync timestamp.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">&#x2705; With this structure, you can:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Run locally<\/strong>\u00a0with\u00a0<code>func start<\/code><\/li>\n\n\n\n<li><strong>Deploy<\/strong>\u00a0with\u00a0<code>func azure functionapp publish &lt;APP_NAME><\/code>\u00a0or Zip Deploy<\/li>\n\n\n\n<li><strong>Test<\/strong>\u00a0with MCP Inspector using the JSON configs we built earlier<\/li>\n\n\n\n<li><strong>Trigger<\/strong>\u00a0both scheduled and real-time indexing<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">If you want, I can now give you a&nbsp;<strong>full \u201cfirst-run\u201d test script<\/strong>&nbsp;that will:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Start the function locally<\/li>\n\n\n\n<li>Call all endpoints in sequence<\/li>\n\n\n\n<li>Simulate an Event Grid event<\/li>\n\n\n\n<li>Verify AI Search results<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>proceed<\/strong>&nbsp;and create your&nbsp;<strong>full \u201cfirst-run\u201d test script<\/strong>&nbsp;so you can validate the entire Azure Functions + Event Grid + AI Search pipeline&nbsp;<strong>end-to-end<\/strong>&nbsp;in one go.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This script will:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Start the function app locally<\/strong><\/li>\n\n\n\n<li><strong>Call the HTTP APIs<\/strong>\u00a0(<code>get_index_status<\/code>,\u00a0<code>manual_refresh<\/code>)<\/li>\n\n\n\n<li><strong>Simulate an Event Grid event<\/strong><\/li>\n\n\n\n<li><strong>Query Azure AI Search<\/strong>\u00a0to confirm the document is indexed<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>1. Prerequisites<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Python 3.10+ installed<\/li>\n\n\n\n<li>Azure Functions Core Tools installed (<code>func --version<\/code>\u00a0should be v4)<\/li>\n\n\n\n<li>Azure CLI installed and logged in (<code>az login<\/code>)<\/li>\n\n\n\n<li><code>local.settings.json<\/code>\u00a0configured with your connection strings and keys<\/li>\n\n\n\n<li>AI Search index already created in Azure<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>2. Save This Script as&nbsp;<code>test_pipeline.py<\/code><\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">Python<br><br><code>import subprocess<br>import time<br>import requests<br>import json<br>from datetime import datetime<br><br># Local endpoints<br>GET_STATUS_URL = \"http:\/\/localhost:7071\/api\/get_index_status\"<br>MANUAL_REFRESH_URL = \"http:\/\/localhost:7071\/api\/manual_refresh\"<br><br># Event Grid simulation (local function trigger)<br>EVENTGRID_URL = \"http:\/\/localhost:7071\/runtime\/webhooks\/EventGrid?functionName=eventgrid_sync\"<br><br># AI Search query<br>AI_SEARCH_ENDPOINT = \"&lt;YOUR_AI_SEARCH_ENDPOINT>\"<br>AI_SEARCH_KEY = \"&lt;YOUR_AI_SEARCH_KEY>\"<br>AI_SEARCH_INDEX = \"&lt;YOUR_AI_SEARCH_INDEX>\"<br><br>def start_functions():<br>    print(\"&#x1f680; Starting Azure Functions locally...\")<br>    proc = subprocess.Popen([\"func\", \"start\"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)<br>    time.sleep(5)  # Give it time to start<br>    return proc<br><br>def get_index_status():<br>    print(\"&#x1f4ca; Getting index status...\")<br>    r = requests.get(GET_STATUS_URL)<br>    print(\"Response:\", r.text)<br><br>def manual_refresh():<br>    print(\"&#x1f504; Triggering manual refresh...\")<br>    r = requests.post(MANUAL_REFRESH_URL)<br>    print(\"Response:\", r.text)<br><br>def send_eventgrid_event():<br>    print(\"&#x26a1; Sending Event Grid test event...\")<br>    payload = [<br>        {<br>            \"id\": \"12345\",<br>            \"eventType\": \"Microsoft.Storage.BlobCreated\",<br>            \"subject\": \"\/blobServices\/default\/containers\/test\/blobs\/doc.json\",<br>            \"eventTime\": datetime.utcnow().isoformat() + \"Z\",<br>            \"data\": {<br>                \"operationType\": \"insert\",<br>                \"document\": {<br>                    \"id\": \"doc-001\",<br>                    \"title\": \"Test Document\",<br>                    \"content\": \"This is a test\",<br>                    \"lastModified\": datetime.utcnow().isoformat() + \"Z\"<br>                }<br>            },<br>            \"dataVersion\": \"1.0\"<br>        }<br>    ]<br>    r = requests.post(EVENTGRID_URL, json=payload)<br>    print(\"Response:\", r.status_code, r.text)<br><br>def query_ai_search():<br>    print(\"&#x1f50d; Querying AI Search for 'Test Document'...\")<br>    url = f\"{AI_SEARCH_ENDPOINT}\/indexes\/{AI_SEARCH_INDEX}\/docs\/search?api-version=2021-04-30-Preview\"<br>    headers = {<br>        \"Content-Type\": \"application\/json\",<br>        \"api-key\": AI_SEARCH_KEY<br>    }<br>    payload = {\"search\": \"Test Document\"}<br>    r = requests.post(url, headers=headers, json=payload)<br>    print(\"Search Results:\", json.dumps(r.json(), indent=2))<br><br>if __name__ == \"__main__\":<br>    proc = start_functions()<br>    try:<br>        get_index_status()<br>        manual_refresh()<br>        time.sleep(3)  # Wait for refresh to complete<br>        get_index_status()<br>        send_eventgrid_event()<br>        time.sleep(3)  # Wait for event to process<br>        query_ai_search()<br>    finally:<br>        print(\"&#x1f6d1; Stopping Azure Functions...\")<br>        proc.terminate()<br><\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>3. How to Run<\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">BashCopy code<code>python test_pipeline.py\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>4. Expected Output<\/strong><\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Start Functions<\/strong>\u00a0\u2192 You\u2019ll see Azure Functions logs starting up.<\/li>\n\n\n\n<li><strong>Get Index Status<\/strong>\u00a0\u2192 JSON with\u00a0<code>lastSync<\/code>\u00a0and\u00a0<code>docCount<\/code>.<\/li>\n\n\n\n<li><strong>Manual Refresh<\/strong>\u00a0\u2192\u00a0<code>\"Manual refresh completed\"<\/code>.<\/li>\n\n\n\n<li><strong>Get Index Status Again<\/strong>\u00a0\u2192 Updated\u00a0<code>lastSync<\/code>.<\/li>\n\n\n\n<li><strong>Event Grid Test<\/strong>\u00a0\u2192 HTTP 200 response.<\/li>\n\n\n\n<li><strong>AI Search Query<\/strong>\u00a0\u2192 JSON with\u00a0<code>\"Test Document\"<\/code>\u00a0in results.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>5. Why This Script Helps<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Runs\u00a0<strong>all triggers<\/strong>\u00a0(timer excluded, since it\u2019s schedule-based)<\/li>\n\n\n\n<li>Confirms\u00a0<strong>local dev environment<\/strong>\u00a0works before deployment<\/li>\n\n\n\n<li>Lets you\u00a0<strong>debug<\/strong>\u00a0without switching between tools<\/li>\n\n\n\n<li>Can be adapted for\u00a0<strong>cloud testing<\/strong>\u00a0by replacing URLs with deployed endpoints<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>4. Post-Deployment Verification Checklist<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">After deployment, verify:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Function App Status<\/strong>:BashCopy code<code>az functionapp show --name &lt;APP_NAME> --resource-group &lt;RESOURCE_GROUP> --query state <\/code>Should be\u00a0<code>\"Running\"<\/code>.<\/li>\n\n\n\n<li><strong>Function Endpoints<\/strong>:<br>Test each HTTP function with\u00a0<code>curl<\/code>\u00a0or Postman.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">&#8211;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>PHASE 7 \u2014 UI Frontend Integration<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Your UI can now call the Function App endpoint to&nbsp;<strong>trigger indexing<\/strong>&nbsp;or&nbsp;<strong>query the AI Search index<\/strong>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Example: Trigger Indexing from UI<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">JavascriptCopy code<code>async function triggerIndexing() {<br>  const response = await fetch(<br>    \"https:\/\/&lt;YOUR-FUNCTION-APP>.azurewebsites.net\/api\/IndexSqlToAISearch?lastSync=2026-09-01T00:00:00\"<br><\/code><br><br><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;<strong>proceed<\/strong>&nbsp;and finish the&nbsp;<strong>UI integration<\/strong>&nbsp;so your MCP-based Azure Function App can be triggered from the frontend, and your&nbsp;<strong>Azure AI Search knowledge base<\/strong>&nbsp;can be queried directly from the UI.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We already have the&nbsp;<strong>indexing endpoint<\/strong>&nbsp;in place \u2014 now we\u2019ll:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Show how the frontend can\u00a0<strong>trigger indexing<\/strong>.<\/li>\n\n\n\n<li>Show how the frontend can\u00a0<strong>query the AI Search index<\/strong>\u00a0for results.<\/li>\n\n\n\n<li>Wrap it into a\u00a0<strong>simple UI flow<\/strong>.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>PHASE 7 (continued) \u2014 UI Frontend Integration<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>1. Trigger Indexing from UI<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This will call your Azure Function to pull data from SQL and push it into Azure AI Search.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">JavascriptCopy code<code>async function triggerIndexing() {\n  try {\n    const response = await fetch(\n      \"https:\/\/&lt;YOUR-FUNCTION-APP&gt;.azurewebsites.net\/api\/IndexSqlToAISearch?lastSync=2026-09-01T00:00:00\",\n      { method: \"GET\" }\n    );\n    const data = await response.json();\n    console.log(\"Indexing result:\", data);\n    alert(`Indexing completed: ${data.message}`);\n  } catch (error) {\n    console.error(\"Error triggering indexing:\", error);\n    alert(\"Indexing failed. Check console for details.\");\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>2. Query Azure AI Search from UI<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">We\u2019ll create a&nbsp;<strong>separate Azure Function<\/strong>&nbsp;to query the AI Search index and return results to the frontend.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong><code>query_search\/__init__.py<\/code><\/strong><\/h4>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>import logging\nimport json\nimport azure.functions as func\nimport requests\n\nSEARCH_ENDPOINT = \"https:\/\/&lt;YOUR-SEARCH-NAME&gt;.search.windows.net\"\nSEARCH_API_KEY = \"&lt;YOUR-SEARCH-QUERY-KEY&gt;\"\nSEARCH_INDEX = \"knowledge-index\"\n\ndef main(req: func.HttpRequest) -&gt; func.HttpResponse:\n    logging.info(\"Query AI Search Function triggered\")\n\n    query = req.params.get(\"q\")\n    if not query:\n        return func.HttpResponse(\n            json.dumps({\"error\": \"Missing query parameter 'q'\"}),\n            mimetype=\"application\/json\",\n            status_code=400\n        )\n\n    url = f\"{SEARCH_ENDPOINT}\/indexes\/{SEARCH_INDEX}\/docs\/search?api-version=2023-07-01-Preview\"\n    headers = {\n        \"Content-Type\": \"application\/json\",\n        \"api-key\": SEARCH_API_KEY\n    }\n    payload = {\n        \"search\": query,\n        \"top\": 10\n    }\n\n    try:\n        response = requests.post(url, headers=headers, json=payload)\n        response.raise_for_status()\n        return func.HttpResponse(\n            json.dumps(response.json()),\n            mimetype=\"application\/json\",\n            status_code=200\n        )\n    except Exception as e:\n        logging.error(f\"Search query failed: {str(e)}\", exc_info=True)\n        return func.HttpResponse(\n            json.dumps({\"error\": str(e)}),\n            mimetype=\"application\/json\",\n            status_code=500\n        )\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>3. Query from UI<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">JavascriptCopy code<code>async function searchKnowledgeBase(query) {\n  try {\n    const response = await fetch(\n      `https:\/\/&lt;YOUR-FUNCTION-APP&gt;.azurewebsites.net\/api\/QueryAISearch?q=${encodeURIComponent(query)}`\n    );\n    const results = await response.json();\n    console.log(\"Search results:\", results);\n\n    const container = document.getElementById(\"results\");\n    container.innerHTML = \"\";\n    results.value.forEach(doc =&gt; {\n      const div = document.createElement(\"div\");\n      div.innerHTML = `&lt;h3&gt;${doc.title}&lt;\/h3&gt;&lt;p&gt;${doc.content}&lt;\/p&gt;`;\n      container.appendChild(div);\n    });\n  } catch (error) {\n    console.error(\"Error searching knowledge base:\", error);\n    alert(\"Search failed. Check console for details.\");\n  }\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>4. Simple HTML UI<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">HtmlCopy code<code>&lt;!DOCTYPE html&gt;\n&lt;html&gt;\n&lt;head&gt;\n  &lt;title&gt;Knowledge Base Search&lt;\/title&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n  &lt;h1&gt;Knowledge Base&lt;\/h1&gt;\n  &lt;button onclick=\"triggerIndexing()\"&gt;Reindex Data&lt;\/button&gt;\n  <br><br>\n  &lt;input type=\"text\" id=\"searchBox\" placeholder=\"Enter search term\"&gt;\n  &lt;button onclick=\"searchKnowledgeBase(document.getElementById('searchBox').value)\"&gt;Search&lt;\/button&gt;\n  &lt;div id=\"results\"&gt;&lt;\/div&gt;\n\n  &lt;script src=\"app.js\"&gt;&lt;\/script&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>PHASE 8 \u2014 How This Works Together<\/strong><\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Reindex Data<\/strong>\u00a0button \u2192 Calls\u00a0<code>IndexSqlToAISearch<\/code>\u00a0Function \u2192 Pulls from SQL \u2192 Pushes to AI Search.<\/li>\n\n\n\n<li><strong>Search<\/strong>\u00a0button \u2192 Calls `<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>proceed<\/strong>&nbsp;and finish&nbsp;<strong>PHASE 8<\/strong>&nbsp;so you have the&nbsp;<strong>full end-to-end flow<\/strong>&nbsp;for your MCP-based Azure Function App, Azure AI Search knowledge base, and UI frontend.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>PHASE 8 \u2014 Full Workflow<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>1. End-to-End Flow<\/strong><\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>User clicks \u201cReindex Data\u201d<\/strong>\u00a0in the UI.\n<ul class=\"wp-block-list\">\n<li>Calls\u00a0<code>IndexSqlToAISearch<\/code>\u00a0Azure Function (MCP tool).<\/li>\n\n\n\n<li>Function connects to\u00a0<strong>Azure SQL Database<\/strong>, retrieves new\/updated rows, and pushes them into\u00a0<strong>Azure AI Search<\/strong>.<\/li>\n\n\n\n<li>AI Search index (<code>knowledge-index<\/code>) is updated.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>User enters a search term<\/strong>\u00a0in the UI and clicks \u201cSearch\u201d.\n<ul class=\"wp-block-list\">\n<li>Calls\u00a0<code>QueryAISearch<\/code>\u00a0Azure Function.<\/li>\n\n\n\n<li>Function sends the query to\u00a0<strong>Azure AI Search<\/strong>\u00a0using the\u00a0<strong>query key<\/strong>.<\/li>\n\n\n\n<li>AI Search returns matching documents.<\/li>\n\n\n\n<li>UI renders results dynamically.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>2. Deployment Steps<\/strong><\/h3>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Backend<\/strong><\/h4>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Create two Azure Functions<\/strong>\u00a0in the same Function App:\n<ul class=\"wp-block-list\">\n<li><code>IndexSqlToAISearch<\/code>\u00a0(indexing)<\/li>\n\n\n\n<li><code>QueryAISearch<\/code>\u00a0(searching)<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Add Application Settings<\/strong>\u00a0in Azure Portal:\n<ul class=\"wp-block-list\">\n<li><code>SEARCH_ENDPOINT<\/code><\/li>\n\n\n\n<li><code>SEARCH_API_KEY<\/code>\u00a0(admin key for indexing)<\/li>\n\n\n\n<li><code>SEARCH_QUERY_KEY<\/code>\u00a0(query key for searching)<\/li>\n\n\n\n<li><code>SEARCH_INDEX<\/code><\/li>\n\n\n\n<li><code>SQL_SERVER<\/code><\/li>\n\n\n\n<li><code>SQL_DATABASE<\/code><\/li>\n\n\n\n<li><code>SQL_USERNAME<\/code><\/li>\n\n\n\n<li><code>SQL_PASSWORD<\/code><\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>Deploy using:BashCopy code<code>func azure functionapp publish &lt;YOUR_FUNCTION_APP_NAME><\/code><\/li>\n<\/ol>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Frontend<\/strong><\/h4>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Host your HTML + JS files in:\n<ul class=\"wp-block-list\">\n<li>Azure Static Web Apps<\/li>\n\n\n\n<li>Azure Blob Storage (static website)<\/li>\n\n\n\n<li>Or any web server<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>Update the\u00a0<strong>Function App URLs<\/strong>\u00a0in\u00a0<code>app.js<\/code>:JavascriptCopy code<code>const INDEX_URL = \"https:\/\/&lt;YOUR-FUNCTION-APP>.azurewebsites.net\/api\/IndexSqlToAISearch\"; const SEARCH_URL = \"https:\/\/&lt;YOUR-FUNCTION-APP>.azurewebsites.net\/api\/QueryAISearch\";<\/code><\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>3. Security Best Practices<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Use\u00a0<strong>Azure Managed Identity<\/strong>\u00a0instead of storing SQL credentials in plain text.<\/li>\n\n\n\n<li>Restrict\u00a0<strong>AI Search admin key<\/strong>\u00a0usage to indexing function only.<\/li>\n\n\n\n<li>Use\u00a0<strong>CORS<\/strong>\u00a0settings in Azure Functions to allow only your frontend domain.<\/li>\n\n\n\n<li>Consider\u00a0<strong>API Management<\/strong>\u00a0in front of your Functions for rate limiting and monitoring.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>4. Optional MCP Integration<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If you want this to be&nbsp;<strong>agent-driven<\/strong>&nbsp;in Foundry:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Register\u00a0<code>IndexSqlToAISearch<\/code>\u00a0and\u00a0<code>QueryAISearch<\/code>\u00a0as\u00a0<strong>MCP tools<\/strong>\u00a0in your Foundry Agent configuration.<\/li>\n\n\n\n<li>The agent can then:\n<ul class=\"wp-block-list\">\n<li>Trigger reindexing automatically on a schedule.<\/li>\n\n\n\n<li>Query the knowledge base to answer user questions in natural language.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">&#x2705;&nbsp;<strong>Now you have:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>SQL \u2192 AI Search indexing function<\/strong><\/li>\n\n\n\n<li><strong>AI Search query function<\/strong><\/li>\n\n\n\n<li><strong>Frontend UI<\/strong>\u00a0to trigger indexing and search<\/li>\n\n\n\n<li><strong>Deployment &amp; security guidance<\/strong><\/li>\n\n\n\n<li><strong>Optional MCP agent integration<\/strong><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">If you want, I can now&nbsp;<strong>extend this<\/strong>&nbsp;so that the&nbsp;<strong>indexing function runs automatically on a schedule<\/strong>&nbsp;(e.g., every hour) without the user clicking \u201cReindex Data\u201d \u2014 making it a&nbsp;<strong>fully automated knowledge base refresh<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;<strong>proceed<\/strong>&nbsp;and add the&nbsp;<strong>scheduled auto-indexing enhancement<\/strong>&nbsp;so your SQL \u2192 Azure AI Search pipeline refreshes automatically without requiring the user to click \u201cReindex Data\u201d in the UI.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This will make your&nbsp;<strong>knowledge base self-updating<\/strong>&nbsp;and always fresh for both the UI and any MCP\/Foundry agent that uses it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>PHASE 9 \u2014 Scheduled Auto-Indexing<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>1. How It Works<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Azure Functions supports\u00a0<strong>Timer Triggers<\/strong>\u00a0that run on a\u00a0<strong>CRON schedule<\/strong>.<\/li>\n\n\n\n<li>We\u2019ll create a\u00a0<strong>new function<\/strong>\u00a0(<code>ScheduledIndexSqlToAISearch<\/code>) that runs the same indexing logic as your HTTP-triggered function, but automatically.<\/li>\n\n\n\n<li>You can set the schedule to\u00a0<strong>hourly, daily, or custom intervals<\/strong>.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>2. Timer Trigger Function Code<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Create a new function in your Function App:<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong><code>scheduled_index\/__init__.py<\/code><\/strong><\/h4>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>import logging\nimport json\nimport pyodbc\nimport requests\nfrom datetime import datetime, timedelta\nimport azure.functions as func\n\n# Azure AI Search config\nSEARCH_ENDPOINT = \"https:\/\/&lt;YOUR-SEARCH-NAME&gt;.search.windows.net\"\nSEARCH_API_KEY = \"&lt;YOUR-SEARCH-ADMIN-KEY&gt;\"\nSEARCH_INDEX = \"knowledge-index\"\n\n# SQL config\nSQL_SERVER = \"&lt;YOUR-SQL-SERVER&gt;.database.windows.net\"\nSQL_DATABASE = \"&lt;YOUR-DATABASE&gt;\"\nSQL_USERNAME = \"&lt;YOUR-USERNAME&gt;\"\nSQL_PASSWORD = \"&lt;YOUR-PASSWORD&gt;\"\nSQL_DRIVER = \"{ODBC Driver 18 for SQL Server}\"\n\n# Store last sync in memory (for demo; use durable storage in production)\nlast_sync_time = None\n\ndef get_sql_data(last_sync=None):\n    conn_str = f\"DRIVER={SQL_DRIVER};SERVER={SQL_SERVER};DATABASE={SQL_DATABASE};UID={SQL_USERNAME};PWD={SQL_PASSWORD}\"\n    conn = pyodbc.connect(conn_str)\n    cursor = conn.cursor()\n\n    if last_sync:\n        cursor.execute(\"\"\"\n            SELECT Id, Title, Content, LastModified \n            FROM KnowledgeTable \n            WHERE LastModified &gt; ?\n        \"\"\", last_sync)\n    else:\n        cursor.execute(\"\"\"\n            SELECT Id, Title, Content, LastModified \n            FROM KnowledgeTable\n        \"\"\")\n\n    rows = cursor.fetchall()\n    conn.close()\n\n    docs = []\n    for row in rows:\n        docs.append({\n            \"id\": str(row.Id),\n            \"title\": row.Title,\n            \"content\": row.Content,\n            \"lastModified\": row.LastModified.isoformat()\n        })\n    return docs\n\ndef push_to_ai_search(docs):\n    url = f\"{SEARCH_ENDPOINT}\/indexes\/{SEARCH_INDEX}\/docs\/index?api-version=2023-07-01-Preview\"\n    headers = {\n        \"Content-Type\": \"application\/json\",\n        \"api-key\": SEARCH_API_KEY\n    }\n    payload = {\n        \"value\": [{\"@search.action\": \"mergeOrUpload\", **doc} for doc in docs]\n    }\n    response = requests.post(url, headers=headers, json=payload)\n    response.raise_for_status()\n    return response.json()\n\ndef main(mytimer: func.TimerRequest) -&gt; None:\n    global last_sync_time\n    logging.info(\"Scheduled SQL \u2192 AI Search indexing started\")\n\n    try:\n        docs = get_sql_data(last_sync_time)\n\n        if not docs:\n            logging.info(\"No new documents found\")\n        else:\n            result = push_to_ai_search(docs)\n            logging.info(f\"Indexed {len(docs)} documents successfully: {result}\")\n\n        # Update last sync time\n        last_sync_time = datetime.utcnow()\n\n    except Exception as e:\n        logging.error(f\"Error during scheduled indexing: {str(e)}\", exc_info=True)\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>3. Function Schedule<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In&nbsp;<strong><code>function.json<\/code><\/strong>&nbsp;for this function:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">JsonCopy code<code>{\n  \"scriptFile\": \"__init__.py\",\n  \"bindings\": [\n    {\n      \"name\": \"mytimer\",\n      \"type\": \"timerTrigger\",\n      \"direction\": \"in\",\n      \"schedule\": \"0 0 * * * *\"\n    }\n  ]\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Schedule format:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>\"0 0 * * * *\"<\/code>\u00a0\u2192 Every hour on the hour<\/li>\n\n\n\n<li><code>\"0 0 0 * * *\"<\/code>\u00a0\u2192 Every day at midnight UTC<\/li>\n\n\n\n<li><code>\"0 *\/30 * * * *\"<\/code>\u00a0\u2192 Every 30 minutes<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>4. Deployment<\/strong><\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Add this new function to your Function App project.<\/li>\n\n\n\n<li>Deploy with:BashCopy code<code>func azure functionapp publish &lt;YOUR_FUNCTION_APP_NAME><\/code><\/li>\n\n\n\n<li>Azure will now run the indexing automatically based on your CRON schedule.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>5. Benefits<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Hands-free updates<\/strong>\u00a0\u2014 no manual reindexing needed.<\/li>\n\n\n\n<li>**Always fresh<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>proceed<\/strong>&nbsp;and wrap this up by making your&nbsp;<strong>scheduled auto-indexing<\/strong>&nbsp;setup&nbsp;<strong>production-ready<\/strong>, fully integrated with your&nbsp;<strong>UI frontend<\/strong>&nbsp;and&nbsp;<strong>MCP\/Foundry agent<\/strong>&nbsp;so it\u2019s a complete, self-maintaining knowledge base system.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>PHASE 10 \u2014 Production-Ready Enhancements<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>1. Persisting Last Sync Time<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Right now, our&nbsp;<code>last_sync_time<\/code>&nbsp;is stored in memory, which resets if the Function App restarts.<br>In production, we should store it in&nbsp;<strong>Azure Table Storage<\/strong>,&nbsp;<strong>Blob Storage<\/strong>, or&nbsp;<strong>Azure SQL<\/strong>&nbsp;itself.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Example: Store in Azure Blob Storage<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>from azure.storage.blob import BlobServiceClient\n\nBLOB_CONN_STR = \"&lt;YOUR-STORAGE-CONNECTION-STRING&gt;\"\nBLOB_CONTAINER = \"function-state\"\nBLOB_NAME = \"last_sync.txt\"\n\ndef get_last_sync():\n    blob_service = BlobServiceClient.from_connection_string(BLOB_CONN_STR)\n    blob_client = blob_service.get_blob_client(container=BLOB_CONTAINER, blob=BLOB_NAME)\n    try:\n        data = blob_client.download_blob().readall().decode(\"utf-8\")\n        return datetime.fromisoformat(data)\n    except:\n        return None\n\ndef set_last_sync(timestamp):\n    blob_service = BlobServiceClient.from_connection_string(BLOB_CONN_STR)\n    blob_client = blob_service.get_blob_client(container=BLOB_CONTAINER, blob=BLOB_NAME)\n    blob_client.upload_blob(timestamp.isoformat(), overwrite=True)\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Replace the in-memory&nbsp;<code>last_sync_time<\/code>&nbsp;with calls to&nbsp;<code>get_last_sync()<\/code>&nbsp;and&nbsp;<code>set_last_sync()<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>2. UI Frontend Awareness<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Since indexing is now automatic, the&nbsp;<strong>UI<\/strong>&nbsp;can:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Show\u00a0<strong>last updated time<\/strong>\u00a0by calling a small Function that reads the stored\u00a0<code>last_sync_time<\/code>.<\/li>\n\n\n\n<li>Disable the \u201cReindex\u201d button unless a manual override is needed.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Example:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">JavascriptCopy code<code>async function getLastUpdated() {\n  const res = await fetch(\"https:\/\/&lt;YOUR-FUNCTION-APP&gt;.azurewebsites.net\/api\/GetLastSync\");\n  const data = await res.json();\n  document.getElementById(\"lastUpdated\").innerText = `Last updated: ${data.lastSync}`;\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>3. MCP \/ Foundry Agent Integration<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Now that the index is always fresh:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Register your\u00a0<strong>QueryAISearch<\/strong>\u00a0endpoint as an MCP tool in Foundry.<\/li>\n\n\n\n<li>The agent can answer questions directly from the AI Search index without manual refresh.<\/li>\n\n\n\n<li>Optionally, register the\u00a0<strong>IndexSqlToAISearch<\/strong>\u00a0endpoint as a tool for\u00a0<strong>manual override<\/strong>.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Example MCP Tool Config (conceptual)<\/strong>:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">JsonCopy code<code>{\n  \"name\": \"query_knowledge_base\",\n  \"description\": \"Searches the Azure AI Search knowledge base for relevant documents\",\n  \"endpoint\": \"https:\/\/&lt;YOUR-FUNCTION-APP&gt;.azurewebsites.net\/api\/QueryAISearch\",\n  \"method\": \"GET\",\n  \"parameters\": [\n    { \"name\": \"q\", \"type\": \"string\", \"required\": true }\n  ]\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>4. Security Hardening<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Use Managed Identity<\/strong>\u00a0for SQL and Blob access instead of passwords.<\/li>\n\n\n\n<li>Restrict\u00a0<strong>AI Search admin key<\/strong>\u00a0to indexing functions only.<\/li>\n\n\n\n<li>Use\u00a0<strong>query key<\/strong>\u00a0for search functions.<\/li>\n\n\n\n<li>Enable\u00a0<strong>CORS<\/strong>\u00a0in Azure Functions to allow only your frontend domain.<\/li>\n\n\n\n<li>Optionally, put\u00a0<strong>Azure API Management<\/strong>\u00a0in front of your Functions for rate limiting and monitoring.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>5. Final End-to-End Flow<\/strong><\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Timer Trigger<\/strong>\u00a0runs every hour \u2192 Pulls new\/updated rows from SQL \u2192 Pushes to AI Search \u2192 Updates\u00a0<code>last_sync_time<\/code>\u00a0in Blob Storage.<\/li>\n\n\n\n<li><strong>UI Frontend<\/strong>\u00a0queries AI Search via\u00a0<code>QueryAISearch<\/code>\u00a0Function \u2192 Displays results instantly.<\/li>\n\n\n\n<li><strong>Foundry Agent<\/strong>\u00a0uses MCP tool to query the same AI Search index \u2192 Answers user questions in natural language.<\/li>\n\n\n\n<li><strong>Manual Override<\/strong>: UI \u201cReindex\u201d button calls\u00a0<code>IndexSqlToAISearch<\/code>\u00a0if needed.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">&#x2705;&nbsp;<strong>Now you have:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Automated scheduled indexing<\/strong>\u00a0from SQL to Azure AI Search.<\/li>\n\n\n\n<li><strong>Persistent last sync tracking<\/strong>.<\/li>\n\n\n\n<li><strong>UI integration<\/strong>\u00a0for search and status display.<\/li>\n\n\n\n<li><strong>MCP\/Foundry agent integration<\/strong>\u00a0for conversational access.<\/li>\n\n\n\n<li><strong>Security best practices<\/strong>\u00a0for production.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">If you want, I can now&nbsp;<strong>extend this one step further<\/strong>&nbsp;so that&nbsp;<strong>deleted rows in SQL are also removed from Azure AI Search automatically<\/strong>&nbsp;during scheduled runs \u2014 making it a&nbsp;<strong>full sync<\/strong>&nbsp;instead of just incremental updates.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>proceed<\/strong>&nbsp;and add the&nbsp;<strong>auto-deletion sync enhancement<\/strong>&nbsp;to your&nbsp;<strong>scheduled SQL \u2192 Azure AI Search indexing job<\/strong>, so that your search index always mirrors your SQL database exactly \u2014 no stale or orphaned documents.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>PHASE 11 \u2014 Auto-Deletion Sync for Scheduled Indexing<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>1. Why This Matters<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Without deletion sync:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Old records removed from SQL will still appear in search results.<\/li>\n\n\n\n<li>Users may see outdated or irrelevant content.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">With deletion sync:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The Azure AI Search index is a\u00a0<strong>true reflection<\/strong>\u00a0of your SQL database.<\/li>\n\n\n\n<li>Search results are always\u00a0<strong>accurate and trustworthy<\/strong>.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>2. Updated Scheduled Function Logic<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">We\u2019ll:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Retrieve\u00a0<strong>all current IDs<\/strong>\u00a0from SQL.<\/li>\n\n\n\n<li>Retrieve\u00a0<strong>all current IDs<\/strong>\u00a0from Azure AI Search.<\/li>\n\n\n\n<li>Compare and find IDs that exist in AI Search but not in SQL.<\/li>\n\n\n\n<li>Delete those from AI Search.<\/li>\n\n\n\n<li>Continue with incremental indexing for new\/updated rows.<\/li>\n<\/ol>\n\n\n\n<h4 class=\"wp-block-heading\"><strong><code>scheduled_index\/__init__.py<\/code>&nbsp;(Updated)<\/strong><\/h4>\n\n\n\n<pre class=\"wp-block-preformatted\"><br><code>import logging<br>import json<br>import pyodbc<br>import requests<br>from datetime import datetime<br>import azure.functions as func<br>from azure.storage.blob import BlobServiceClient<br><br># Azure AI Search config<br>SEARCH_ENDPOINT = \"https:\/\/&lt;YOUR-SEARCH-NAME>.search.windows.net\"<br>SEARCH_API_KEY = \"&lt;YOUR-SEARCH-ADMIN-KEY>\"<br>SEARCH_INDEX = \"knowledge-index\"<br><br># SQL config<br>SQL_SERVER = \"&lt;YOUR-SQL-SERVER>.database.windows.net\"<br>SQL_DATABASE = \"&lt;YOUR-DATABASE>\"<br>SQL_USERNAME = \"&lt;YOUR-USERNAME>\"<br>SQL_PASSWORD = \"&lt;YOUR-PASSWORD>\"<br>SQL_DRIVER = \"{ODBC Driver 18 for SQL Server}\"<br><br># Blob storage config for last sync persistence<br>BLOB_CONN_STR = \"&lt;YOUR-STORAGE-CONNECTION-STRING>\"<br>BLOB_CONTAINER = \"function-state\"<br>BLOB_NAME = \"last_sync.txt\"<br><br>def get_last_sync():<br>    blob_service = BlobServiceClient.from_connection_string(BLOB_CONN_STR)<br>    blob_client = blob_service.get_blob_client(container=BLOB_CONTAINER, blob=BLOB_NAME)<br>    try:<br>        data = blob_client.download_blob().readall().decode(\"utf-8\")<br>        return datetime.fromisoformat(data)<br>    except:<br>        return None<br><br>def set_last_sync(timestamp):<br>    blob_service = BlobServiceClient.from_connection_string(BLOB_CONN_STR)<br>    blob_client = blob_service.get_blob_client(container=BLOB_CONTAINER, blob=BLOB_NAME)<br>    blob_client.upload_blob(timestamp.isoformat(), overwrite=True)<br><br>def get_sql_data(last_sync=None):<br>    conn_str = f\"DRIVER={SQL_DRIVER};SERVER={SQL_SERVER};DATABASE={SQL_DATABASE};UID={SQL_USERNAME};PWD={SQL_PASSWORD}\"<br>    conn = pyodbc.connect(conn_str)<br>    cursor = conn.cursor()<br><br>    if last_sync:<br>        cursor.execute(\"\"\"<br>            SELECT Id, Title, Content, LastModified <br>            FROM KnowledgeTable <br>            WHERE LastModified > ?<br>        \"\"\", last_sync)<br>    else:<br>        cursor.execute(\"\"\"<br>            SELECT Id, Title, Content, LastModified <br>            FROM KnowledgeTable<br>        \"\"\")<br><br>    rows = cursor.fetchall()<br>    conn.close()<br><br>    docs = []<br>    for row in rows:<br>        docs.append({<br>            \"id\": str(row.Id),<br>            \"title\": row.Title,<br>            \"content\": row.Content,<br>            \"lastModified\": row.LastModified.isoformat()<br>        })<br>    return docs<br><br>def get_all_sql_ids():<br>    conn_str = f\"DRIVER={SQL_DRIVER};SERVER={SQL_SERVER};DATABASE={SQL_DATABASE};UID={SQL_USERNAME};PWD={SQL_PASSWORD}\"<br>    conn = pyodbc.connect(conn_str)<br>    cursor = conn.cursor()<br>    cursor.execute(\"SELECT Id FROM KnowledgeTable\")<br>    ids = [str(row.Id) for row in cursor.fetchall()]<br>    conn.close()<br>    return ids<br><br>def get_all_ai_search_ids():<br>    url = f\"{SEARCH_ENDPOINT}\/indexes\/{SEARCH_INDEX}\/docs?api-version=2023-07-01-Preview&amp;$select=id&amp;$top=1000\"<br>    headers = {\"api-key\": SEARCH_API_KEY}<br>    ids = []<br>    while url:<br>        resp = requests.get(url, headers=headers)<br>        resp.raise_for_status()<br>        data = resp.json()<br>        ids.extend([doc[\"id\"] for doc in data.get(\"value\", [])])<br>        url = data.get(\"@odata.nextLink\")<br>    return ids<br><br>def delete_from_ai_search(ids):<br>    if not ids:<br>        return<br>    url = f\"{SEARCH_ENDPOINT}\/indexes\/{SEARCH_INDEX}\/docs\/index?api-version=2023-07-01-Preview\"<br>    headers = {<br>        \"Content-Type\": \"application\/json\",<br>        \"api-key\": SEARCH_API_KEY<br>    }<br>    payload = {<br>        \"value\": [{\"@search.action\": \"delete\", \"id\": doc_id} for doc<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;<strong>proceed<\/strong>&nbsp;and finish the&nbsp;<strong>auto-deletion sync enhancement<\/strong>&nbsp;code so your scheduled job can both&nbsp;<strong>remove stale documents<\/strong>&nbsp;and&nbsp;<strong>index new\/updated ones<\/strong>&nbsp;in Azure AI Search.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>PHASE 11 (continued) \u2014 Completing the Function<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">We left off inside the&nbsp;<code>delete_from_ai_search()<\/code>&nbsp;function.<br>Now we\u2019ll complete it, integrate it into the scheduled run, and ensure the&nbsp;<strong>last sync time<\/strong>&nbsp;is updated.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Full Updated&nbsp;<code>scheduled_index\/__init__.py<\/code><\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>import logging\nimport json\nimport pyodbc\nimport requests\nfrom datetime import datetime\nimport azure.functions as func\nfrom azure.storage.blob import BlobServiceClient\n\n# Azure AI Search config\nSEARCH_ENDPOINT = \"https:\/\/&lt;YOUR-SEARCH-NAME>.search.windows.net\"\nSEARCH_API_KEY = \"&lt;YOUR-SEARCH-ADMIN-KEY>\"\nSEARCH_INDEX = \"knowledge-index\"\n\n# SQL config\nSQL_SERVER = \"&lt;YOUR-SQL-SERVER>.database.windows.net\"\nSQL_DATABASE = \"&lt;YOUR-DATABASE>\"\nSQL_USERNAME = \"&lt;YOUR-USERNAME>\"\nSQL_PASSWORD = \"&lt;YOUR-PASSWORD>\"\nSQL_DRIVER = \"{ODBC Driver 18 for SQL Server}\"\n\n# Blob storage config for last sync persistence\nBLOB_CONN_STR = \"&lt;YOUR-STORAGE-CONNECTION-STRING>\"\nBLOB_CONTAINER = \"function-state\"\nBLOB_NAME = \"last_sync.txt\"\n\ndef get_last_sync():\n    blob_service = BlobServiceClient.from_connection_string(BLOB_CONN_STR)\n    blob_client = blob_service.get_blob_client(container=BLOB_CONTAINER, blob=BLOB_NAME)\n    try:\n        data = blob_client.download_blob().readall().decode(\"utf-8\")\n        return datetime.fromisoformat(data)\n    except:\n        return None\n\ndef set_last_sync(timestamp):\n    blob_service = BlobServiceClient.from_connection_string(BLOB_CONN_STR)\n    blob_client = blob_service.get_blob_client(container=BLOB_CONTAINER, blob=BLOB_NAME)\n    blob_client.upload_blob(timestamp.isoformat(), overwrite=True)\n\ndef get_sql_data(last_sync=None):\n    conn_str = f\"DRIVER={SQL_DRIVER};SERVER={SQL_SERVER};DATABASE={SQL_DATABASE};UID={SQL_USERNAME};PWD={SQL_PASSWORD}\"\n    conn = pyodbc.connect(conn_str)\n    cursor = conn.cursor()\n\n    if last_sync:\n        cursor.execute(\"\"\"\n            SELECT Id, Title, Content, LastModified \n            FROM KnowledgeTable \n            WHERE LastModified > ?\n        \"\"\", last_sync)\n    else:\n        cursor.execute(\"\"\"\n            SELECT Id, Title, Content, LastModified \n            FROM KnowledgeTable\n        \"\"\")\n\n    rows = cursor.fetchall()\n    conn.close()\n\n    docs = []\n    for row in rows:\n        docs.append({\n            \"id\": str(row.Id),\n            \"title\": row.Title,\n            \"content\": row.Content,\n            \"lastModified\": row.LastModified.isoformat()\n        })\n    return docs\n\ndef get_all_sql_ids():\n    conn_str = f\"DRIVER={SQL_DRIVER};SERVER={SQL_SERVER};DATABASE={SQL_DATABASE};UID={SQL_USERNAME};PWD={SQL_PASSWORD}\"\n    conn = pyodbc.connect(conn_str)\n    cursor = conn.cursor()\n    cursor.execute(\"SELECT Id FROM KnowledgeTable\")\n    ids = [str(row.Id) for row in cursor.fetchall()]\n    conn.close()\n    return ids\n\ndef get_all_ai_search_ids():\n    url = f\"{SEARCH_ENDPOINT}\/indexes\/{SEARCH_INDEX}\/docs?api-version=2023-07-01-Preview&amp;$select=id&amp;$top=1000\"\n    headers = {\"api-key\": SEARCH_API_KEY}\n    ids = []\n    while url:\n        resp = requests.get(url, headers=headers)\n        resp.raise_for_status()\n        data = resp.json()\n        ids.extend([doc[\"id\"] for doc in data.get(\"value\", [])])\n        url = data.get(\"@odata.nextLink\")\n    return ids\n\ndef delete_from_ai_search(ids):\n    if not ids:\n        return\n    url = f\"{SEARCH_ENDPOINT}\/indexes\/{SEARCH_INDEX}\/docs\/index?api-version=2023-07-01-Preview\"\n    headers = {\n        \"Content-Type\": \"application\/json\",\n        \"api-key\": SEARCH_API_KEY\n    }\n    payload = {\n        \"value\": [{\"@search.action\": \"delete\", \"id\": doc_id} for doc_id in ids]\n    }\n    resp = requests.post(url, headers=headers, json=payload)\n    resp.raise_for_status()\n    logging.info(f\"Deleted {len(ids)} stale documents from AI Search.\")\n\ndef push_to_ai_search(docs):\n    if not docs:\n        return\n    url = f\"{SEARCH_ENDPOINT}\/indexes\/{SEARCH_INDEX}\/docs\/index?api-version=2023-07-01-Preview\"\n    headers = {\n        \"Content-Type\": \"application\/json\",\n        \"api-key\": SEARCH_API_KEY\n    }\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>proceed<\/strong>&nbsp;and finish the&nbsp;<strong>push_to_ai_search<\/strong>&nbsp;function and the&nbsp;<strong>main scheduled job<\/strong>&nbsp;so your auto-deletion + indexing pipeline is complete.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>PHASE 11 (final) \u2014 Completed Scheduled Indexing with Auto-Deletion<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Here\u2019s the&nbsp;<strong>rest of the code<\/strong>:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>def push_to_ai_search(docs):\n    if not docs:\n        return\n    url = f\"{SEARCH_ENDPOINT}\/indexes\/{SEARCH_INDEX}\/docs\/index?api-version=2023-07-01-Preview\"\n    headers = {\n        \"Content-Type\": \"application\/json\",\n        \"api-key\": SEARCH_API_KEY\n    }\n    payload = {\n        \"value\": [{\"@search.action\": \"mergeOrUpload\", **doc} for doc in docs]\n    }\n    resp = requests.post(url, headers=headers, json=payload)\n    resp.raise_for_status()\n    logging.info(f\"Indexed {len(docs)} documents into AI Search.\")\n\ndef main(mytimer: func.TimerRequest) -&gt; None:\n    logging.info(\"Scheduled SQL \u2192 AI Search sync started.\")\n\n    try:\n        # 1. Get last sync time from Blob Storage\n        last_sync = get_last_sync()\n        logging.info(f\"Last sync time: {last_sync}\")\n\n        # 2. Get all IDs from SQL and AI Search\n        sql_ids = set(get_all_sql_ids())\n        ai_ids = set(get_all_ai_search_ids())\n\n        # 3. Find stale IDs to delete\n        stale_ids = list(ai_ids - sql_ids)\n        if stale_ids:\n            delete_from_ai_search(stale_ids)\n        else:\n            logging.info(\"No stale documents to delete.\")\n\n        # 4. Get new\/updated docs from SQL\n        new_docs = get_sql_data(last_sync)\n        if new_docs:\n            push_to_ai_search(new_docs)\n        else:\n            logging.info(\"No new or updated documents to index.\")\n\n        # 5. Update last sync time\n        set_last_sync(datetime.utcnow())\n        logging.info(\"Sync completed successfully.\")\n\n    except Exception as e:\n        logging.error(f\"Error during scheduled sync: {str(e)}\", exc_info=True)\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>How This Works<\/strong><\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Every scheduled run<\/strong>:\n<ul class=\"wp-block-list\">\n<li>Reads the last sync time from Blob Storage.<\/li>\n\n\n\n<li>Fetches\u00a0<strong>all IDs<\/strong>\u00a0from SQL and AI Search.<\/li>\n\n\n\n<li>Deletes any IDs in AI Search that no longer exist in SQL.<\/li>\n\n\n\n<li>Fetches only\u00a0<strong>new or updated<\/strong>\u00a0rows from SQL since the last sync.<\/li>\n\n\n\n<li>Pushes them into AI Search.<\/li>\n\n\n\n<li>Updates the last sync timestamp in Blob Storage.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Result<\/strong>:\n<ul class=\"wp-block-list\">\n<li>AI Search index is always\u00a0<strong>in sync<\/strong>\u00a0with SQL.<\/li>\n\n\n\n<li>No stale or orphaned documents remain.<\/li>\n\n\n\n<li>Incremental updates keep it efficient.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Deployment Notes<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Timer Schedule<\/strong>: In\u00a0<code>function.json<\/code>, set your desired CRON expression:<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\">JsonCopy code<code>{\n  \"scriptFile\": \"__init__.py\",\n  \"bindings\": [\n    {\n      \"name\": \"mytimer\",\n      \"type\": \"timerTrigger\",\n      \"direction\": \"in\",\n      \"schedule\": \"0 0 * * * *\"\n    }\n  ]\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This example runs&nbsp;<strong>every hour<\/strong>.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Security<\/strong>:\n<ul class=\"wp-block-list\">\n<li>Use\u00a0<strong>Managed Identity<\/strong>\u00a0for SQL and Blob access.<\/li>\n\n\n\n<li>Store keys in\u00a0<strong>Azure Key Vault<\/strong>.<\/li>\n\n\n\n<li>Restrict AI Search admin key to this function only.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Next Step<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Now that we have:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Auto-deletion<\/strong><\/li>\n\n\n\n<li><strong>Incremental indexing<\/strong><\/li>\n\n\n\n<li><strong>Persistent last sync<\/strong><\/li>\n\n\n\n<li><strong>Scheduled automation<\/strong><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">We can&nbsp;<strong>connect this directly to your UI and MCP agent<\/strong>&nbsp;so that:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The UI always queries a\u00a0<strong>fresh index<\/strong>.<\/li>\n\n\n\n<li>The MCP agent can answer questions without worrying about stale data.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">If you want, I can now&nbsp;<strong>proceed<\/strong>&nbsp;to show you how to&nbsp;<strong>wire this scheduled sync into your existing UI and MCP agent config<\/strong>&nbsp;so the frontend can also display&nbsp;<strong>last updated time<\/strong>&nbsp;and the agent can confirm freshness before answering.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>proceed<\/strong>&nbsp;and wire your&nbsp;<strong>scheduled sync<\/strong>&nbsp;into both the&nbsp;<strong>UI frontend<\/strong>&nbsp;and the&nbsp;<strong>MCP\/Foundry agent<\/strong>&nbsp;so everything stays fresh and transparent to the user.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>PHASE 12 \u2014 UI + MCP Integration for Freshness Awareness<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>1. UI Frontend Integration<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">We\u2019ll add a&nbsp;<strong>status endpoint<\/strong>&nbsp;to your Azure Functions so the UI can display:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Last updated time<\/strong><\/li>\n\n\n\n<li><strong>Number of documents in the index<\/strong><\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\"><strong><code>get_index_status\/__init__.py<\/code><\/strong><\/h4>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>import logging\nimport json\nimport azure.functions as func\nimport requests\nfrom azure.storage.blob import BlobServiceClient\nfrom datetime import datetime\n\nSEARCH_ENDPOINT = \"https:\/\/&lt;YOUR-SEARCH-NAME&gt;.search.windows.net\"\nSEARCH_API_KEY = \"&lt;YOUR-SEARCH-QUERY-KEY&gt;\"\nSEARCH_INDEX = \"knowledge-index\"\n\nBLOB_CONN_STR = \"&lt;YOUR-STORAGE-CONNECTION-STRING&gt;\"\nBLOB_CONTAINER = \"function-state\"\nBLOB_NAME = \"last_sync.txt\"\n\ndef get_last_sync():\n    blob_service = BlobServiceClient.from_connection_string(BLOB_CONN_STR)\n    blob_client = blob_service.get_blob_client(container=BLOB_CONTAINER, blob=BLOB_NAME)\n    try:\n        data = blob_client.download_blob().readall().decode(\"utf-8\")\n        return datetime.fromisoformat(data)\n    except:\n        return None\n\ndef get_index_count():\n    url = f\"{SEARCH_ENDPOINT}\/indexes\/{SEARCH_INDEX}\/docs\/$count?api-version=2023-07-01-Preview\"\n    headers = {\"api-key\": SEARCH_API_KEY}\n    resp = requests.get(url, headers=headers)\n    resp.raise_for_status()\n    return int(resp.text)\n\ndef main(req: func.HttpRequest) -&gt; func.HttpResponse:\n    logging.info(\"Index status request received.\")\n    try:\n        last_sync = get_last_sync()\n        count = get_index_count()\n        return func.HttpResponse(\n            json.dumps({\n                \"lastSync\": last_sync.isoformat() if last_sync else None,\n                \"docCount\": count\n            }),\n            mimetype=\"application\/json\",\n            status_code=200\n        )\n    except Exception as e:\n        logging.error(f\"Error fetching index status: {str(e)}\", exc_info=True)\n        return func.HttpResponse(\n            json.dumps({\"error\": str(e)}),\n            mimetype=\"application\/json\",\n            status_code=500\n        )\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>2. UI Changes<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In your&nbsp;<code>app.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">JavascriptCopy code<code>async function updateStatus() {\n  try {\n    const res = await fetch(\"https:\/\/&lt;YOUR-FUNCTION-APP&gt;.azurewebsites.net\/api\/GetIndexStatus\");\n    const data = await res.json();\n    document.getElementById(\"status\").innerText =\n      `Last updated: ${data.lastSync || \"Never\"} | Documents: ${data.docCount}`;\n  } catch (err) {\n    console.error(\"Error fetching status:\", err);\n    document.getElementById(\"status\").innerText = \"Status unavailable\";\n  }\n}\n\n\/\/ Call on page load\nupdateStatus();\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">In your HTML:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">HtmlCopy code<code>&lt;div id=\"status\" style=\"font-size: 0.9em; color: gray;\"&gt;&lt;\/div&gt;\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>3. MCP \/ Foundry Agent Integration<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">We\u2019ll register&nbsp;<strong>two MCP tools<\/strong>:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>query_knowledge_base<\/strong>\u00a0\u2192 Calls\u00a0<code>QueryAISearch<\/code>\u00a0function.<\/li>\n\n\n\n<li><strong>get_index_status<\/strong>\u00a0\u2192 Calls\u00a0<code>GetIndexStatus<\/code>\u00a0function.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Example MCP Tool Config:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">JsonCopy code<code>[\n  {\n    \"name\": \"query_knowledge_base\",\n    \"description\": \"Searches the Azure AI Search knowledge base for relevant documents\",\n    \"endpoint\": \"https:\/\/&lt;YOUR-FUNCTION-APP&gt;.azurewebsites.net\/api\/QueryAISearch\",\n    \"method\": \"GET\",\n    \"parameters\": [\n      { \"name\": \"q\", \"type\": \"string\", \"required\": true }\n    ]\n  },\n  {\n    \"name\": \"get_index_status\",\n    \"description\": \"Returns the last sync time and document count for the knowledge base\",\n    \"endpoint\": \"https:\/\/&lt;YOUR-FUNCTION-APP&gt;.azurewebsites.net\/api\/GetIndexStatus\",\n    \"method\": \"GET\",\n    \"parameters\": []\n  }\n]\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>4. Agent Behavior<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">With this setup:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Before answering a user\u2019s question, the MCP agent can call\u00a0<code>get_index_status<\/code>\u00a0to confirm the index is fresh.<\/li>\n\n\n\n<li>If the index is stale, the agent can:\n<ul class=\"wp-block-list\">\n<li>Inform the user.<\/li>\n\n\n\n<li>Optionally trigger a manual reindex via\u00a0<code>IndexSqlToAISearch<\/code>.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>5. Benefits<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Transparency<\/strong>: Users see when the knowledge base was last updated.<\/li>\n\n\n\n<li><strong>Trust<\/strong>: MCP agent can confirm data<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>proceed<\/strong>&nbsp;and wire your&nbsp;<strong>scheduled sync<\/strong>&nbsp;into both the&nbsp;<strong>UI frontend<\/strong>&nbsp;and the&nbsp;<strong>MCP\/Foundry agent<\/strong>&nbsp;so everything stays fresh and transparent to the user.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>PHASE 12 \u2014 UI + MCP Integration for Freshness Awareness<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>1. UI Frontend Integration<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">We\u2019ll add a&nbsp;<strong>status endpoint<\/strong>&nbsp;to your Azure Functions so the UI can display:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Last updated time<\/strong><\/li>\n\n\n\n<li><strong>Number of documents in the index<\/strong><\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\"><strong><code>get_index_status\/__init__.py<\/code><\/strong><\/h4>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>import logging\nimport json\nimport azure.functions as func\nimport requests\nfrom azure.storage.blob import BlobServiceClient\nfrom datetime import datetime\n\nSEARCH_ENDPOINT = \"https:\/\/&lt;YOUR-SEARCH-NAME&gt;.search.windows.net\"\nSEARCH_API_KEY = \"&lt;YOUR-SEARCH-QUERY-KEY&gt;\"\nSEARCH_INDEX = \"knowledge-index\"\n\nBLOB_CONN_STR = \"&lt;YOUR-STORAGE-CONNECTION-STRING&gt;\"\nBLOB_CONTAINER = \"function-state\"\nBLOB_NAME = \"last_sync.txt\"\n\ndef get_last_sync():\n    blob_service = BlobServiceClient.from_connection_string(BLOB_CONN_STR)\n    blob_client = blob_service.get_blob_client(container=BLOB_CONTAINER, blob=BLOB_NAME)\n    try:\n        data = blob_client.download_blob().readall().decode(\"utf-8\")\n        return datetime.fromisoformat(data)\n    except:\n        return None\n\ndef get_index_count():\n    url = f\"{SEARCH_ENDPOINT}\/indexes\/{SEARCH_INDEX}\/docs\/$count?api-version=2023-07-01-Preview\"\n    headers = {\"api-key\": SEARCH_API_KEY}\n    resp = requests.get(url, headers=headers)\n    resp.raise_for_status()\n    return int(resp.text)\n\ndef main(req: func.HttpRequest) -&gt; func.HttpResponse:\n    logging.info(\"Index status request received.\")\n    try:\n        last_sync = get_last_sync()\n        count = get_index_count()\n        return func.HttpResponse(\n            json.dumps({\n                \"lastSync\": last_sync.isoformat() if last_sync else None,\n                \"docCount\": count\n            }),\n            mimetype=\"application\/json\",\n            status_code=200\n        )\n    except Exception as e:\n        logging.error(f\"Error fetching index status: {str(e)}\", exc_info=True)\n        return func.HttpResponse(\n            json.dumps({\"error\": str(e)}),\n            mimetype=\"application\/json\",\n            status_code=500\n        )\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>2. UI Changes<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In your&nbsp;<code>app.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">JavascriptCopy code<code>async function updateStatus() {\n  try {\n    const res = await fetch(\"https:\/\/&lt;YOUR-FUNCTION-APP&gt;.azurewebsites.net\/api\/GetIndexStatus\");\n    const data = await res.json();\n    document.getElementById(\"status\").innerText =\n      `Last updated: ${data.lastSync || \"Never\"} | Documents: ${data.docCount}`;\n  } catch (err) {\n    console.error(\"Error fetching status:\", err);\n    document.getElementById(\"status\").innerText = \"Status unavailable\";\n  }\n}\n\n\/\/ Call on page load\nupdateStatus();\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">In your HTML:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">HtmlCopy code<code>&lt;div id=\"status\" style=\"font-size: 0.9em; color: gray;\"&gt;&lt;\/div&gt;\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>3. MCP \/ Foundry Agent Integration<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">We\u2019ll register&nbsp;<strong>two MCP tools<\/strong>:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>query_knowledge_base<\/strong>\u00a0\u2192 Calls\u00a0<code>QueryAISearch<\/code>\u00a0function.<\/li>\n\n\n\n<li><strong>get_index_status<\/strong>\u00a0\u2192 Calls\u00a0<code>GetIndexStatus<\/code>\u00a0function.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Example MCP Tool Config:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">JsonCopy code<code>[\n  {\n    \"name\": \"query_knowledge_base\",\n    \"description\": \"Searches the Azure AI Search knowledge base for relevant documents\",\n    \"endpoint\": \"https:\/\/&lt;YOUR-FUNCTION-APP&gt;.azurewebsites.net\/api\/QueryAISearch\",\n    \"method\": \"GET\",\n    \"parameters\": [\n      { \"name\": \"q\", \"type\": \"string\", \"required\": true }\n    ]\n  },\n  {\n    \"name\": \"get_index_status\",\n    \"description\": \"Returns the last sync time and document count for the knowledge base\",\n    \"endpoint\": \"https:\/\/&lt;YOUR-FUNCTION-APP&gt;.azurewebsites.net\/api\/GetIndexStatus\",\n    \"method\": \"GET\",\n    \"parameters\": []\n  }\n]\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>4. Agent Behavior<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">With this setup:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Before answering a user\u2019s question, the MCP agent can call\u00a0<code>get_index_status<\/code>\u00a0to confirm the index is fresh.<\/li>\n\n\n\n<li>If the index is stale, the agent can:\n<ul class=\"wp-block-list\">\n<li>Inform the user.<\/li>\n\n\n\n<li>Optionally trigger a manual reindex via\u00a0<code>IndexSqlToAISearch<\/code>.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>5. Benefits<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Transparency<\/strong>: Users see when the knowledge base was last updated.<\/li>\n\n\n\n<li><strong>Trust<\/strong>: MCP agent can confirm data<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>proceed<\/strong>&nbsp;and finish the&nbsp;<strong>PHASE 12 integration<\/strong>&nbsp;so your&nbsp;<strong>UI<\/strong>&nbsp;and&nbsp;<strong>MCP\/Foundry agent<\/strong>&nbsp;not only&nbsp;<em>know<\/em>&nbsp;when the index was last updated, but can also&nbsp;<strong>act<\/strong>&nbsp;if it\u2019s stale \u2014 giving you a fully self-healing, self-aware knowledge base.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>PHASE 13 \u2014 Freshness-Aware Actions<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>1. UI: Manual Refresh if Stale<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">We\u2019ll enhance the UI so that if the&nbsp;<strong>last sync time<\/strong>&nbsp;is older than a threshold (e.g., 24 hours), it shows a&nbsp;<strong>\u201cRefresh Now\u201d<\/strong>&nbsp;button.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><code>app.js<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">JavascriptCopy code<code>async function updateStatus() {\n  try {\n    const res = await fetch(\"https:\/\/&lt;YOUR-FUNCTION-APP&gt;.azurewebsites.net\/api\/GetIndexStatus\");\n    const data = await res.json();\n\n    const lastSync = data.lastSync ? new Date(data.lastSync) : null;\n    const docCount = data.docCount || 0;\n\n    document.getElementById(\"status\").innerText =\n      `Last updated: ${lastSync ? lastSync.toLocaleString() : \"Never\"} | Documents: ${docCount}`;\n\n    \/\/ Show refresh button if stale\n    if (!lastSync || (Date.now() - lastSync.getTime()) &gt; 24 * 60 * 60 * 1000) {\n      document.getElementById(\"refreshBtn\").style.display = \"inline-block\";\n    } else {\n      document.getElementById(\"refreshBtn\").style.display = \"none\";\n    }\n  } catch (err) {\n    console.error(\"Error fetching status:\", err);\n    document.getElementById(\"status\").innerText = \"Status unavailable\";\n  }\n}\n\nasync function manualRefresh() {\n  try {\n    const res = await fetch(\"https:\/\/&lt;YOUR-FUNCTION-APP&gt;.azurewebsites.net\/api\/IndexSqlToAISearch\");\n    const data = await res.json();\n    alert(`Manual refresh triggered: ${data.message || \"Check logs\"}`);\n    updateStatus();\n  } catch (err) {\n    alert(\"Manual refresh failed.\");\n  }\n}\n\n\/\/ Call on page load\nupdateStatus();\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>HTML<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">HtmlCopy code<code>&lt;div id=\"status\" style=\"font-size: 0.9em; color: gray;\"&gt;&lt;\/div&gt;\n&lt;button id=\"refreshBtn\" style=\"display:none;\" onclick=\"manualRefresh()\"&gt;Refresh Now&lt;\/button&gt;\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>2. MCP Agent: Freshness Check Before Query<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">We\u2019ll make the agent&nbsp;<strong>call&nbsp;<code>get_index_status<\/code>&nbsp;first<\/strong>, and only query if the index is fresh enough.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Example MCP Agent Logic (pseudo-code)<\/strong>:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>def answer_user_question(question):\n    status = call_tool(\"get_index_status\")\n    last_sync = parse_datetime(status[\"lastSync\"])\n    if not last_sync or (datetime.utcnow() - last_sync).days &gt; 1:\n        return f\"The knowledge base may be outdated (last updated {last_sync}). \" \\\n               f\"Would you like me to refresh it before answering?\"\n    else:\n        return call_tool(\"query_knowledge_base\", {\"q\": question})\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>3. Optional: Agent-Initiated Refresh<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If the user says \u201cyes\u201d to refreshing:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>def refresh_and_answer(question):\n    call_tool(\"index_sql_to_ai_search\")  # Manual reindex\n    wait_for_completion()  # Optional: poll until done\n    return call_tool(\"query_knowledge_base\", {\"q\": question})\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>4. Benefits of This Setup<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Self-healing<\/strong>: If the index is stale, the system can refresh automatically.<\/li>\n\n\n\n<li><strong>User trust<\/strong>: Both UI and agent are transparent about data freshness.<\/li>\n\n\n\n<li><strong>Efficiency<\/strong>: Avoids unnecessary reindexing when data is already fresh.<\/li>\n\n\n\n<li><strong>Consistency<\/strong>: UI and agent share the same freshness logic.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">&#x2705;&nbsp;<strong>Now you have:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Scheduled sync<\/strong>\u00a0with auto-deletion + incremental indexing.<\/li>\n\n\n\n<li><strong>Persistent last sync tracking<\/strong>\u00a0in Blob Storage.<\/li>\n\n\n\n<li><strong>UI freshness display<\/strong>\u00a0+ manual refresh option.<\/li>\n\n\n\n<li><strong>MCP agent freshness check<\/strong>\u00a0before answering.<\/li>\n\n\n\n<li><strong>Optional agent-triggered refresh<\/strong>.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">If you want, I can now&nbsp;<strong>proceed<\/strong>&nbsp;to the&nbsp;<strong>final phase<\/strong>&nbsp;where we add&nbsp;<strong>real-time change detection<\/strong>&nbsp;from SQL using&nbsp;<strong>Azure Event Grid or Change Data Capture (CDC)<\/strong>&nbsp;\u2014 so the index updates&nbsp;<strong>immediately<\/strong>&nbsp;when data changes, instead of waiting for the schedule.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That would make your knowledge base&nbsp;<strong>truly real-time<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">let\u2019s&nbsp;<strong>proceed<\/strong>&nbsp;and take your system to the&nbsp;<strong>final phase<\/strong>&nbsp;by adding&nbsp;<strong>real-time sync<\/strong>&nbsp;from SQL to Azure AI Search using&nbsp;<strong>Change Data Capture (CDC)<\/strong>&nbsp;and&nbsp;<strong>Azure Event Grid<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This will make your knowledge base&nbsp;<strong>update instantly<\/strong>&nbsp;when data changes \u2014 no waiting for the scheduled job.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>PHASE 14 \u2014 Real-Time SQL \u2192 AI Search Sync<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>1. How It Works<\/strong><\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Enable Change Data Capture (CDC)<\/strong>\u00a0in Azure SQL Database.<\/li>\n\n\n\n<li><strong>Azure Data Factory<\/strong>\u00a0or\u00a0<strong>Azure Functions<\/strong>\u00a0listens for CDC changes.<\/li>\n\n\n\n<li><strong>Event Grid<\/strong>\u00a0triggers your indexing function\u00a0<strong>immediately<\/strong>\u00a0when:\n<ul class=\"wp-block-list\">\n<li>A row is inserted \u2192 Add to AI Search.<\/li>\n\n\n\n<li>A row is updated \u2192 Merge into AI Search.<\/li>\n\n\n\n<li>A row is deleted \u2192 Remove from AI Search.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>2. Enabling CDC in Azure SQL<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Run in SQL Server Management Studio (SSMS) or Azure Data Studio:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">SqlCopy code<code>-- Enable CDC at the database level\nEXEC sys.sp_cdc_enable_db;\n\n-- Enable CDC for your table\nEXEC sys.sp_cdc_enable_table\n    @source_schema = 'dbo',\n    @source_name = 'KnowledgeTable',\n    @role_name = NULL;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This creates&nbsp;<strong>change tables<\/strong>&nbsp;like&nbsp;<code>cdc.dbo_KnowledgeTable_CT<\/code>&nbsp;that log inserts, updates, and deletes.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>3. Azure Function for Real-Time Processing<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">We\u2019ll create a&nbsp;<strong>new function<\/strong>&nbsp;that:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Runs on a\u00a0<strong>Timer Trigger<\/strong>\u00a0every minute (or Event Grid if using CDC streaming).<\/li>\n\n\n\n<li>Reads the CDC change table.<\/li>\n\n\n\n<li>Pushes changes to AI Search immediately.<\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\"><strong><code>realtime_index\/__init__.py<\/code><\/strong><\/h4>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>import logging\nimport pyodbc\nimport requests\nimport azure.functions as func\nfrom datetime import datetime, timedelta\n\nSEARCH_ENDPOINT = \"https:\/\/&lt;YOUR-SEARCH-NAME&gt;.search.windows.net\"\nSEARCH_API_KEY = \"&lt;YOUR-SEARCH-ADMIN-KEY&gt;\"\nSEARCH_INDEX = \"knowledge-index\"\n\nSQL_SERVER = \"&lt;YOUR-SQL-SERVER&gt;.database.windows.net\"\nSQL_DATABASE = \"&lt;YOUR-DATABASE&gt;\"\nSQL_USERNAME = \"&lt;YOUR-USERNAME&gt;\"\nSQL_PASSWORD = \"&lt;YOUR-PASSWORD&gt;\"\nSQL_DRIVER = \"{ODBC Driver 18 for SQL Server}\"\n\ndef get_cdc_changes():\n    conn_str = f\"DRIVER={SQL_DRIVER};SERVER={SQL_SERVER};DATABASE={SQL_DATABASE};UID={SQL_USERNAME};PWD={SQL_PASSWORD}\"\n    conn = pyodbc.connect(conn_str)\n    cursor = conn.cursor()\n\n    # Get changes from the last minute\n    cursor.execute(\"\"\"\n        DECLARE @from_lsn binary(10), @to_lsn binary(10);\n        SET @from_lsn = sys.fn_cdc_get_min_lsn('dbo_KnowledgeTable');\n        SET @to_lsn = sys.fn_cdc_map_time_to_lsn('largest less than or equal', GETDATE());\n\n        SELECT *\n        FROM cdc.fn_cdc_get_all_changes_dbo_KnowledgeTable(@from_lsn, @to_lsn, 'all');\n    \"\"\")\n\n    rows = cursor.fetchall()\n    conn.close()\n    return rows\n\ndef push_to_ai_search(action, doc):\n    url = f\"{SEARCH_ENDPOINT}\/indexes\/{SEARCH_INDEX}\/docs\/index?api-version=2023-07-01-Preview\"\n    headers = {\"Content-Type\": \"application\/json\", \"api-key\": SEARCH_API_KEY}\n    payload = {\"value\": [{**doc, \"@search.action\": action}]}\n    resp = requests.post(url, headers=headers, json=payload)\n    resp.raise_for_status()\n\ndef main(mytimer: func.TimerRequest) -&gt; None:\n    logging.info(\"Real-time CDC sync started.\")\n    try:\n        changes = get_cdc_changes()\n        for change in changes:\n            op = change.__getattribute__(\"__$operation\")  # CDC operation code\n            doc = {\n                \"id\": str(change.Id),\n                \"title\": change.Title,\n                \"content\": change.Content,\n                \"lastModified\": change.LastModified.isoformat()\n            }\n            if op == 1:  # Delete\n                push_to_ai_search(\"delete\", {\"id\": doc[\"id\"]})\n            elif op in (2, 4):  # Insert or Update\n                push_to_ai_search(\"mergeOrUpload\", doc)\n\n        logging.info(f\"Processed {len(changes)} CDC changes.\")\n    except Exception as e:\n        logging.error(f\"Error in real-time sync: {str(e)}\", exc_info=True)\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>4. Event-Driven Alternative<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of polling CDC:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Use\u00a0<strong>Azure Data Factory<\/strong>\u00a0or\u00a0<strong>Azure Synapse Link<\/strong>\u00a0to stream changes to\u00a0<strong>Event Grid<\/strong>.<\/li>\n\n\n\n<li>Event Grid triggers your Azure Function instantly.<\/li>\n\n\n\n<li>This removes the 1-minute polling delay.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;<strong>proceed<\/strong>&nbsp;and finish the&nbsp;<strong>real-time sync<\/strong>&nbsp;setup by wiring in the&nbsp;<strong>Event Grid trigger<\/strong>&nbsp;so your Azure Function reacts instantly to SQL changes without polling.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>PHASE 14 (continued) \u2014 Event-Driven Real-Time Sync<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>1. Why Event Grid Instead of Polling<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Lower latency<\/strong>\u00a0\u2014 changes propagate in seconds.<\/li>\n\n\n\n<li><strong>Lower cost<\/strong>\u00a0\u2014 no constant database queries.<\/li>\n\n\n\n<li><strong>Scalable<\/strong>\u00a0\u2014 handles bursts of changes without overloading SQL.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>2. Architecture Overview<\/strong><\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Azure SQL Change Data Capture (CDC)<\/strong>\u00a0or\u00a0<strong>Change Tracking<\/strong>\u00a0detects row changes.<\/li>\n\n\n\n<li><strong>Azure Data Factory<\/strong>\u00a0or\u00a0<strong>Azure Synapse Link<\/strong>\u00a0streams changes to\u00a0<strong>Azure Storage<\/strong>\u00a0or\u00a0<strong>Azure Event Hubs<\/strong>.<\/li>\n\n\n\n<li><strong>Event Grid<\/strong>\u00a0publishes an event when a new change file\/message arrives.<\/li>\n\n\n\n<li><strong>Azure Function (Event Grid Trigger)<\/strong>\u00a0processes the event and updates Azure AI Search.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>3. Event Grid Trigger Function<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Here\u2019s an example Azure Function that listens for Event Grid events and updates AI Search:<\/p>\n\n\n\n<h4 class=\"wp-block-heading\"><strong><code>eventgrid_index\/__init__.py<\/code><\/strong><\/h4>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>import logging\nimport json\nimport requests\nimport azure.functions as func\n\nSEARCH_ENDPOINT = \"https:\/\/&lt;YOUR-SEARCH-NAME&gt;.search.windows.net\"\nSEARCH_API_KEY = \"&lt;YOUR-SEARCH-ADMIN-KEY&gt;\"\nSEARCH_INDEX = \"knowledge-index\"\n\ndef push_to_ai_search(action, doc):\n    url = f\"{SEARCH_ENDPOINT}\/indexes\/{SEARCH_INDEX}\/docs\/index?api-version=2023-07-01-Preview\"\n    headers = {\"Content-Type\": \"application\/json\", \"api-key\": SEARCH_API_KEY}\n    payload = {\"value\": [{**doc, \"@search.action\": action}]}\n    resp = requests.post(url, headers=headers, json=payload)\n    resp.raise_for_status()\n    logging.info(f\"{action} action completed for doc ID {doc.get('id')}\")\n\ndef main(event: func.EventGridEvent):\n    logging.info(f\"Event received: {event.id}\")\n    try:\n        data = event.get_json()\n\n        # Example: Event contains change type and document payload\n        change_type = data.get(\"operationType\")  # e.g., \"insert\", \"update\", \"delete\"\n        doc = data.get(\"document\")\n\n        if change_type == \"delete\":\n            push_to_ai_search(\"delete\", {\"id\": doc[\"id\"]})\n        else:\n            push_to_ai_search(\"mergeOrUpload\", doc)\n\n    except Exception as e:\n        logging.error(f\"Error processing Event Grid event: {str(e)}\", exc_info=True)\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>4. Event Grid Subscription Setup<\/strong><\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Source<\/strong>:\n<ul class=\"wp-block-list\">\n<li>If using\u00a0<strong>Azure Data Factory<\/strong>: configure a pipeline that outputs change data to Blob Storage or Event Hubs.<\/li>\n\n\n\n<li>If using\u00a0<strong>Synapse Link<\/strong>: stream changes to a storage account.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Event Grid Topic<\/strong>:\n<ul class=\"wp-block-list\">\n<li>Create a topic that listens to Blob Storage or Event Hubs events.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Subscription<\/strong>:\n<ul class=\"wp-block-list\">\n<li>Create an Event Grid subscription that triggers your Azure Function when a new change event is published.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>5. Benefits of Event-Driven Sync<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Instant updates<\/strong>\u00a0\u2014 no waiting for scheduled jobs.<\/li>\n\n\n\n<li><strong>Efficient<\/strong>\u00a0\u2014 only processes changes, not the whole dataset.<\/li>\n\n\n\n<li><strong>Scalable<\/strong>\u00a0\u2014 handles high change volumes without constant DB polling.<\/li>\n\n\n\n<li><strong>Unified<\/strong>\u00a0\u2014 works alongside your scheduled sync as a safety net.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>6. Recommended Hybrid Approach<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Event Grid<\/strong>\u00a0for\u00a0<strong>real-time changes<\/strong>.<\/li>\n\n\n\n<li><strong>Scheduled sync<\/strong>\u00a0(hourly\/daily) as a\u00a0<strong>catch-up<\/strong>\u00a0in case any events are missed.<\/li>\n\n\n\n<li><strong>UI + MCP agent<\/strong>\u00a0still use the\u00a0<strong>GetIndexStatus<\/strong>\u00a0endpoint to confirm freshness.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">&#8211;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>proceed<\/strong>&nbsp;with a&nbsp;<strong>step-by-step testing plan<\/strong>&nbsp;using&nbsp;<strong>Azure Functions Core Tools<\/strong>&nbsp;so you can validate your&nbsp;<strong>scheduled sync<\/strong>,&nbsp;<strong>real-time Event Grid sync<\/strong>, and&nbsp;<strong>HTTP API<\/strong>&nbsp;functions&nbsp;<strong>before<\/strong>&nbsp;and&nbsp;<strong>after<\/strong>&nbsp;deployment.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This will help you catch issues early and confirm that your&nbsp;<strong>folder structure + code<\/strong>&nbsp;works end-to-end.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>1. Prerequisites<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Azure Functions Core Tools<\/strong>\u00a0installed:BashCopy code<code>func --version <\/code>Should match your runtime (v4 for Python 3.9+).<\/li>\n\n\n\n<li><strong>Azure CLI<\/strong>\u00a0installed and logged in:BashCopy code<code>az login<\/code><\/li>\n\n\n\n<li><strong>Local.settings.json<\/strong>\u00a0configured with:\n<ul class=\"wp-block-list\">\n<li>SQL connection string<\/li>\n\n\n\n<li>AI Search endpoint + key<\/li>\n\n\n\n<li>Blob Storage connection string<\/li>\n\n\n\n<li>Event Grid test payloads (optional)<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>2. Local Testing with Core Tools<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>A. Start the Function App Locally<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">From your project root:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">BashCopy code<code>func start\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You should see output like:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">Copy code<code>Functions:\n\n        timer_sync: timerTrigger\n        eventgrid_sync: eventGridTrigger\n        get_index_status: [GET,POST] http:\/\/localhost:7071\/api\/get_index_status\n        manual_refresh: [POST] http:\/\/localhost:7071\/api\/manual_refresh\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>B. Test HTTP Functions Locally<\/strong><\/h3>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>1. Get Index Status<\/strong><\/h4>\n\n\n\n<pre class=\"wp-block-preformatted\">BashCopy code<code>curl http:\/\/localhost:7071\/api\/get_index_status\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected JSON:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">JsonCopy code<code>{\n  \"lastSync\": \"2026-09-10T12:34:56Z\",\n  \"docCount\": 1234\n}\n<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>2. Trigger Manual Refresh<\/strong><\/h4>\n\n\n\n<pre class=\"wp-block-preformatted\">BashCopy code<code>curl -X POST http:\/\/localhost:7071\/api\/manual_refresh\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">JsonCopy code<code>{\n  \"message\": \"Manual refresh started\"\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>C. Test Timer Trigger Locally<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">You can&nbsp;<strong>manually invoke<\/strong>&nbsp;the timer function without waiting for the schedule:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">BashCopy code<code>func run timer_sync\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected logs:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">Copy code<code>[Information] Fetching last sync time from Blob...\n[Information] Found 5 stale docs, deleting...\n[Information] Indexed 12 new\/updated docs.\n[Information] Updated last sync timestamp.\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>D. Test Event Grid Trigger Locally<\/strong><\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Create a\u00a0<strong>sample Event Grid payload<\/strong>\u00a0in\u00a0<code>eventgrid_test.json<\/code>:<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-preformatted\">JsonCopy code<code>[\n  {\n    \"id\": \"12345\",\n    \"eventType\": \"Microsoft.Storage.BlobCreated\",\n    \"subject\": \"\/blobServices\/default\/containers\/test\/blobs\/doc.json\",\n    \"eventTime\": \"2026-09-10T12:00:00Z\",\n    \"data\": {\n      \"operationType\": \"insert\",\n      \"document\": {\n        \"id\": \"doc-001\",\n        \"title\": \"Test Document\",\n        \"content\": \"This is a test\",\n        \"lastModified\": \"2026-09-10T12:00:00Z\"\n      }\n    },\n    \"dataVersion\": \"1.0\"\n  }\n]\n<\/code><\/pre>\n\n\n\n<ol start=\"2\" class=\"wp-block-list\">\n<li>Run:<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-preformatted\">BashCopy code<code>func run eventgrid_sync --input \"@eventgrid_test.json\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected logs:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">Copy code<code>[Information] Event received: 12345\n[Information] mergeOrUpload action completed for doc ID doc-001\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>3. Testing After Deployment<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>A. Get Function URLs<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">BashCopy code<code>az functionapp function list \\\n  --name &lt;APP_NAME&gt; \\\n  --resource-group &lt;RESOURCE_GROUP&gt; \\\n  --query \"[].{name:name, url:invokeUrlTemplate}\"\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>B. Test HTTP Functions in Azure<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">BashCopy code<code>curl \"https:\/\/&lt;APP_NAME&gt;.azurewebsites.net\/api\/get_index_status?code=&lt;FUNCTION_KEY&gt;\"\n<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-preformatted\">BashCopy code<code>curl -X POST \"https:\/\/&lt;APP_NAME&gt;.azurewebsites.net\/api\/manual_refresh?code=&lt;FUNCTION_KEY&gt;\"\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>C. Trigger Timer Function in Azure<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Azure doesn\u2019t let you manually run a timer trigger directly, but you can:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Temporarily change the CRON schedule in\u00a0<code>function.json<\/code>\u00a0to run every minute:<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\">JsonCopy code<code>\"schedule\": \"0 * * * * *\"\n<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Redeploy, wait for it to run, then revert.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>D. Trigger Event Grid Function in Azure<\/strong><\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Go to Azure Portal \u2192 Event Grid Topic \u2192\u00a0<strong>Test<\/strong>.<\/li>\n\n\n\n<li>Paste your sample payload.<\/li>\n\n\n\n<li>Send test event.<\/li>\n\n\n\n<li>Check Function App logs:<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-preformatted\">BashCopy code<code>az functionapp log tail --name &lt;APP_NAME> --resource-group &lt;RESOURCE_GROUP><\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">&#8212;<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">**Testing MCP**<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>step-by-step instructions<\/strong>&nbsp;to test your deployed Azure Functions (scheduled sync, real-time Event Grid sync, and HTTP APIs)&nbsp;<strong>through your MCP Inspector<\/strong>, so you can confirm that your&nbsp;<strong>MCP\/Foundry Agent<\/strong>&nbsp;can call them and get the expected results.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I\u2019ll walk you through&nbsp;<strong>local MCP Inspector testing<\/strong>&nbsp;and&nbsp;<strong>cloud testing<\/strong>&nbsp;after deployment.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>1. Prerequisites<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>MCP Inspector<\/strong>\u00a0installed and running.<\/li>\n\n\n\n<li>Your\u00a0<strong>MCP Agent<\/strong>\u00a0configured with:\n<ul class=\"wp-block-list\">\n<li>The HTTP tool pointing to your Azure Function endpoints.<\/li>\n\n\n\n<li>Authentication (function keys or managed identity).<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>Your\u00a0<strong>Function App<\/strong>\u00a0deployed and running in Azure (or locally via\u00a0<code>func start<\/code>).<\/li>\n\n\n\n<li><strong>Function Keys<\/strong>\u00a0retrieved from Azure Portal \u2192 Function \u2192\u00a0<strong>Get Function URL<\/strong>.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>2. Local Testing with MCP Inspector<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>A. Start Functions Locally<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">BashCopy code<code>func start\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You should see:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">Copy code<code>Functions:\n\n        get_index_status: [GET,POST] http:\/\/localhost:7071\/api\/get_index_status\n        manual_refresh: [POST] http:\/\/localhost:7071\/api\/manual_refresh\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>B. Configure MCP Inspector<\/strong><\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Open MCP Inspector.<\/li>\n\n\n\n<li>Go to\u00a0<strong>Tools<\/strong>\u00a0\u2192\u00a0<strong>Add Tool<\/strong>.<\/li>\n\n\n\n<li>Add a new HTTP tool:\n<ul class=\"wp-block-list\">\n<li><strong>Name<\/strong>:\u00a0<code>GetIndexStatus<\/code><\/li>\n\n\n\n<li><strong>Method<\/strong>:\u00a0<code>GET<\/code><\/li>\n\n\n\n<li><strong>URL<\/strong>:\u00a0<code>http:\/\/localhost:7071\/api\/get_index_status<\/code><\/li>\n\n\n\n<li><strong>Auth<\/strong>: None (for local testing).<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>Add another HTTP tool:\n<ul class=\"wp-block-list\">\n<li><strong>Name<\/strong>:\u00a0<code>ManualRefresh<\/code><\/li>\n\n\n\n<li><strong>Method<\/strong>:\u00a0<code>POST<\/code><\/li>\n\n\n\n<li><strong>URL<\/strong>:\u00a0<code>http:\/\/localhost:7071\/api\/manual_refresh<\/code><\/li>\n\n\n\n<li><strong>Auth<\/strong>: None (for local testing).<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>C. Test in MCP Inspector<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Get Index Status<\/strong>:\n<ol class=\"wp-block-list\">\n<li>Select\u00a0<code>GetIndexStatus<\/code>\u00a0tool.<\/li>\n\n\n\n<li>Click\u00a0<strong>Run<\/strong>.<\/li>\n\n\n\n<li>Expected output:JsonCopy code<code>{ \"lastSync\": \"2026-09-10T12:34:56Z\", \"docCount\": 1234 }<\/code><\/li>\n<\/ol>\n<\/li>\n\n\n\n<li><strong>Manual Refresh<\/strong>:\n<ol class=\"wp-block-list\">\n<li>Select\u00a0<code>ManualRefresh<\/code>\u00a0tool.<\/li>\n\n\n\n<li>Click\u00a0<strong>Run<\/strong>.<\/li>\n\n\n\n<li>Expected output:JsonCopy code<code>{ \"message\": \"Manual refresh started\" }<\/code><\/li>\n<\/ol>\n<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>3. Cloud Testing with MCP Inspector<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>A. Get Function URLs<\/strong><\/h3>\n\n\n\n<pre class=\"wp-block-preformatted\">BashCopy code<code>az functionapp function list \\\n  --name &lt;APP_NAME&gt; \\\n  --resource-group &lt;RESOURCE_GROUP&gt; \\\n  --query \"[].{name:name, url:invokeUrlTemplate}\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Example:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">Copy code<code>https:\/\/my-func-app.azurewebsites.net\/api\/get_index_status?code=abc123...\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>B. Configure MCP Inspector for Cloud<\/strong><\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>In MCP Inspector, edit your HTTP tools:\n<ul class=\"wp-block-list\">\n<li><strong>URL<\/strong>: Replace\u00a0<code>localhost:7071<\/code>\u00a0with your Azure Function URL.<\/li>\n\n\n\n<li><strong>Auth<\/strong>: If using function keys, append\u00a0<code>?code=&lt;FUNCTION_KEY><\/code>\u00a0to the URL.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>Save changes.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>C. Run Cloud Tests<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Get Index Status<\/strong>:\n<ul class=\"wp-block-list\">\n<li>Expected output should match Azure\u2019s live index state.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Manual Refresh<\/strong>:\n<ul class=\"wp-block-list\">\n<li>Should trigger a reindex in Azure AI Search.<\/li>\n\n\n\n<li>You can confirm by checking Azure Function logs:BashCopy code<code>az functionapp log tail --name &lt;APP_NAME> --resource-group &lt;RESOURCE_GROUP><\/code><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>4. Testing Real-Time Event Grid via MCP<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If your MCP Agent has a tool for&nbsp;<strong>triggering test events<\/strong>:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Create a JSON payload matching your Event Grid schema.<\/li>\n\n\n\n<li>Call the Event Grid test endpoint from MCP Inspector.<\/li>\n\n\n\n<li>Check logs to confirm the Event Grid-triggered function ran.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>5. Pass\/Fail Criteria<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">&#x2705;&nbsp;<strong>Pass<\/strong>&nbsp;if:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>MCP Inspector can call both HTTP functions and get valid JSON.<\/li>\n\n\n\n<li>Manual refresh triggers indexing in Azure AI Search.<\/li>\n\n\n\n<li>Get Index Status reflects updated\u00a0<code>lastSync<\/code>\u00a0after refresh.<\/li>\n\n\n\n<li>Event Grid test events result in AI Search updates.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">&#x274c;&nbsp;<strong>Fail<\/strong>&nbsp;if:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>MCP Inspector returns HTTP 401\/403 \u2192 Check function key or auth.<\/li>\n\n\n\n<li>MCP Inspector times out \u2192 Check Function App is running and accessible.<\/li>\n\n\n\n<li>Output JSON is empty or incorrect \u2192 Check function logic and bindings.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">If you want, I can now give you a&nbsp;<strong>ready-to-import MCP Inspector tool configuration JSON<\/strong>&nbsp;so you don\u2019t have to manually add each tool \u2014 you can just import and start testing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>proceed<\/strong>&nbsp;and give you a&nbsp;<strong>ready-to-import MCP Inspector tool configuration JSON<\/strong>&nbsp;so you can instantly test your Azure Functions from MCP Inspector without manually creating each tool.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I\u2019ll make two versions:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Local Testing Config<\/strong>\u00a0(for\u00a0<code>func start<\/code>)<\/li>\n\n\n\n<li><strong>Cloud Testing Config<\/strong>\u00a0(for deployed Azure Functions)<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>1. Local MCP Inspector Config (No Auth)<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Save this as&nbsp;<code>mcp_tools_local.json<\/code>&nbsp;and import into MCP Inspector.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">JsonCopy code<code>{\n  \"tools\": [\n    {\n      \"name\": \"GetIndexStatusLocal\",\n      \"type\": \"http\",\n      \"method\": \"GET\",\n      \"url\": \"http:\/\/localhost:7071\/api\/get_index_status\",\n      \"headers\": {\n        \"Content-Type\": \"application\/json\"\n      },\n      \"description\": \"Retrieve the last sync timestamp and document count from the local Azure Function.\"\n    },\n    {\n      \"name\": \"ManualRefreshLocal\",\n      \"type\": \"http\",\n      \"method\": \"POST\",\n      \"url\": \"http:\/\/localhost:7071\/api\/manual_refresh\",\n      \"headers\": {\n        \"Content-Type\": \"application\/json\"\n      },\n      \"description\": \"Trigger a manual refresh of the index from the local Azure Function.\"\n    }\n  ]\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>2. Cloud MCP Inspector Config (With Function Keys)<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Save this as&nbsp;<code>mcp_tools_cloud.json<\/code>&nbsp;and replace placeholders:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>&lt;APP_NAME><\/code>\u00a0\u2192 Your Azure Function App name<\/li>\n\n\n\n<li><code>&lt;FUNCTION_KEY><\/code>\u00a0\u2192 Your function key from Azure Portal \u2192 Function \u2192\u00a0<strong>Get Function URL<\/strong><\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\">JsonCopy code<code>{\n  \"tools\": [\n    {\n      \"name\": \"GetIndexStatusCloud\",\n      \"type\": \"http\",\n      \"method\": \"GET\",\n      \"url\": \"https:\/\/&lt;APP_NAME&gt;.azurewebsites.net\/api\/get_index_status?code=&lt;FUNCTION_KEY&gt;\",\n      \"headers\": {\n        \"Content-Type\": \"application\/json\"\n      },\n      \"description\": \"Retrieve the last sync timestamp and document count from the deployed Azure Function.\"\n    },\n    {\n      \"name\": \"ManualRefreshCloud\",\n      \"type\": \"http\",\n      \"method\": \"POST\",\n      \"url\": \"https:\/\/&lt;APP_NAME&gt;.azurewebsites.net\/api\/manual_refresh?code=&lt;FUNCTION_KEY&gt;\",\n      \"headers\": {\n        \"Content-Type\": \"application\/json\"\n      },\n      \"description\": \"Trigger a manual refresh of the index from the deployed Azure Function.\"\n    }\n  ]\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>3. How to Import into MCP Inspector<\/strong><\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Open MCP Inspector.<\/li>\n\n\n\n<li>Go to\u00a0<strong>Tools<\/strong>\u00a0\u2192\u00a0<strong>Import Tools<\/strong>.<\/li>\n\n\n\n<li>Select either\u00a0<code>mcp_tools_local.json<\/code>\u00a0or\u00a0<code>mcp_tools_cloud.json<\/code>.<\/li>\n\n\n\n<li>The tools will appear in your tool list.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>4. Testing Steps in MCP Inspector<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>A. Local<\/strong><\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Run:BashCopy code<code>func start<\/code><\/li>\n\n\n\n<li>In MCP Inspector, run\u00a0<code>GetIndexStatusLocal<\/code>\u00a0\u2192 Expect JSON with\u00a0<code>lastSync<\/code>\u00a0and\u00a0<code>docCount<\/code>.<\/li>\n\n\n\n<li>Run\u00a0<code>ManualRefreshLocal<\/code>\u00a0\u2192 Expect\u00a0<code>\"message\": \"Manual refresh started\"<\/code>.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>B. Cloud<\/strong><\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Ensure your Function App is running in Azure:BashCopy code<code>az functionapp show --name &lt;APP_NAME> --resource-group &lt;RESOURCE_GROUP> --query state<\/code><\/li>\n\n\n\n<li>In MCP Inspector, run\u00a0<code>GetIndexStatusCloud<\/code>\u00a0\u2192 Expect live Azure index status.<\/li>\n\n\n\n<li>Run\u00a0<code>ManualRefreshCloud<\/code>\u00a0\u2192 Check Azure logs:BashCopy code<code>az functionapp log tail --name &lt;APP_NAME> --resource-group &lt;RESOURCE_GROUP><\/code><\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>5. Bonus \u2014 Event Grid Test Tool for MCP Inspector<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you want to test&nbsp;<strong>real-time sync<\/strong>&nbsp;from MCP Inspector, add this to your config:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">JsonCopy code<code>{<br>  \"name\": \"TestEventGrid\",<br>  \"type\": \"http\",<br>  \"method\": \"POST\",<br>  \"url\": \"https:\/\/&lt;EVENT_GRID_TOPIC_ENDPOINT>?api-version=2018-01-01\",<br>  \"headers\": {<br>    \"Content-Type\": \"application\/json\",<br>    \"aeg-sas-key\": \"&lt;EVENT_GRID_KEY>\"<br>  },<br>  \"body\": \"[{\\\"id\\\":\\\"12345\\\",\\\"eventType\\\":\\\"Microsoft.Storage.BlobCreated\\\",\\\"subject\\\":\\\"\/blobServices\/default\/containers\/test\/blobs\/doc.json\\\",\\\"eventTime\\\":\\\"2026-09-10T12:00:00Z\\\",\\\"data\\\":{\\\"operationType\\\":\\\"insert\\\",\\\"document\\\":{\\\"id\\\":\\\"doc-001\\\",\\\"title\\\":\\\"Test Document\\\",\\\"content\\\":\\\"This is a test\\\",\\\"lastModified\\\":\\\"2026-09-10T12:00:00Z\\\"}},\\\"dataVersion\\\":\\\"1.0\\\"}]\",<br>  \"description\": \"Send a test Event Grid event to trigger the real-time sync<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;<strong>proceed<\/strong>&nbsp;and finish the MCP Inspector testing setup so you can&nbsp;<strong>fully validate<\/strong>&nbsp;your Azure Functions + Event Grid + AI Search pipeline&nbsp;<strong>end-to-end<\/strong>&nbsp;from inside MCP Inspector.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We\u2019ll now:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Complete the Event Grid test tool config<\/strong><\/li>\n\n\n\n<li><strong>Show how to import &amp; run it in MCP Inspector<\/strong><\/li>\n\n\n\n<li><strong>Explain how to verify the results in Azure logs &amp; AI Search<\/strong><\/li>\n\n\n\n<li><strong>Give you a full \u201cTest Run Script\u201d<\/strong>\u00a0so you can repeat this anytime<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>1. Complete Event Grid Test Tool Config (MCP Inspector)<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Save this as&nbsp;<code>mcp_tools_eventgrid.json<\/code>&nbsp;and replace placeholders:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>&lt;EVENT_GRID_TOPIC_ENDPOINT><\/code>\u00a0\u2192 From Azure Portal \u2192 Event Grid Topic \u2192 Overview \u2192\u00a0<strong>Topic Endpoint<\/strong><\/li>\n\n\n\n<li><code>&lt;EVENT_GRID_KEY><\/code>\u00a0\u2192 From Azure Portal \u2192 Event Grid Topic \u2192\u00a0<strong>Access Keys<\/strong>\u00a0\u2192 Key 1 or Key 2<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\">JsonCopy code<code>{\n  \"tools\": [\n    {\n      \"name\": \"TestEventGrid\",\n      \"type\": \"http\",\n      \"method\": \"POST\",\n      \"url\": \"&lt;EVENT_GRID_TOPIC_ENDPOINT&gt;?api-version=2018-01-01\",\n      \"headers\": {\n        \"Content-Type\": \"application\/json\",\n        \"aeg-sas-key\": \"&lt;EVENT_GRID_KEY&gt;\"\n      },\n      \"body\": \"[{\\\"id\\\":\\\"12345\\\",\\\"eventType\\\":\\\"Microsoft.Storage.BlobCreated\\\",\\\"subject\\\":\\\"\/blobServices\/default\/containers\/test\/blobs\/doc.json\\\",\\\"eventTime\\\":\\\"2026-09-10T12:00:00Z\\\",\\\"data\\\":{\\\"operationType\\\":\\\"insert\\\",\\\"document\\\":{\\\"id\\\":\\\"doc-001\\\",\\\"title\\\":\\\"Test Document\\\",\\\"content\\\":\\\"This is a test\\\",\\\"lastModified\\\":\\\"2026-09-10T12:00:00Z\\\"}},\\\"dataVersion\\\":\\\"1.0\\\"}]\",\n      \"description\": \"Send a test Event Grid event to trigger the real-time sync Azure Function.\"\n    }\n  ]\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>2. Import into MCP Inspector<\/strong><\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Open MCP Inspector.<\/li>\n\n\n\n<li>Go to\u00a0<strong>Tools<\/strong>\u00a0\u2192\u00a0<strong>Import Tools<\/strong>.<\/li>\n\n\n\n<li>Select\u00a0<code>mcp_tools_eventgrid.json<\/code>.<\/li>\n\n\n\n<li>The tool\u00a0<code>TestEventGrid<\/code>\u00a0will appear in your tool list.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>3. Run the Event Grid Test<\/strong><\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>In MCP Inspector, select\u00a0<code>TestEventGrid<\/code>.<\/li>\n\n\n\n<li>Click\u00a0<strong>Run<\/strong>.<\/li>\n\n\n\n<li>Expected HTTP response:JsonCopy code<code>{} <\/code>(Event Grid usually returns an empty body with HTTP 200 if accepted.)<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>4. Verify in Azure<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Open a terminal and tail the logs for your Event Grid-triggered function:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">BashCopy code<code>az functionapp log tail \\\n  --name &lt;APP_NAME&gt; \\\n  --resource-group &lt;RESOURCE_GROUP&gt;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You should see:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">Copy code<code>[Information] Event received: 12345\n[Information] mergeOrUpload action completed for doc ID doc-001\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>5. Confirm in Azure AI Search<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Run:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">BashCopy code<code>curl -X POST \\\n  -H \"Content-Type: application\/json\" \\\n  -H \"api-key: &lt;AI_SEARCH_ADMIN_KEY&gt;\" \\\n  \"https:\/\/&lt;AI_SEARCH_SERVICE&gt;.search.windows.net\/indexes\/&lt;INDEX_NAME&gt;\/docs\/search?api-version=2021-04-30-Preview\" \\\n  -d '{\"search\": \"Test Document\"}'\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected result:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">JsonCopy code<code>{\n  \"value\": [\n    {\n      \"id\": \"doc-001\",\n      \"title\": \"Test Document\",\n      \"content\": \"This is a test\"\n    }\n  ]\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>6. Full MCP Inspector Test Run Script<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Here\u2019s the&nbsp;<strong>order<\/strong>&nbsp;I recommend for a full validation:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Get Index Status<\/strong>\u00a0(Local or Cloud tool) \u2192 Confirm baseline\u00a0<code>lastSync<\/code>\u00a0and\u00a0<code>docCount<\/code>.<\/li>\n\n\n\n<li><strong>Manual Refresh<\/strong>\u00a0\u2192 Confirm\u00a0<code>\"Manual refresh started\"<\/code>\u00a0and watch logs for indexing.<\/li>\n\n\n\n<li><strong>Get Index Status<\/strong>\u00a0again \u2192 Confirm\u00a0<code>lastSync<\/code>\u00a0updated and\u00a0<code>docCount<\/code>\u00a0changed if new docs were added.<\/li>\n\n\n\n<li><strong>Test Event Grid<\/strong>\u00a0\u2192 Send test event from MCP Inspector.<\/li>\n\n\n\n<li><strong>Tail Logs<\/strong>\u00a0\u2192 Confirm Event Grid-triggered function processed the event.<\/li>\n\n\n\n<li><strong>Search AI Index<\/strong>\u00a0\u2192 Confirm the new document is searchable.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>7. Why This Works Well<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>MCP Inspector<\/strong>\u00a0acts as your\u00a0<strong>agent-side integration tester<\/strong>\u00a0\u2014 no need to switch tools.<\/li>\n\n\n\n<li>You can\u00a0<strong>simulate both manual and automated triggers<\/strong>\u00a0from one place.<\/li>\n\n\n\n<li>You can\u00a0<strong>chain tools<\/strong>\u00a0in MCP Inspector to run the full sequence automatically.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">&#8211;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>proceed<\/strong>&nbsp;and give you a&nbsp;<strong>single combined MCP Inspector configuration JSON<\/strong>&nbsp;that includes:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Local testing tools<\/strong>\u00a0(no auth, for\u00a0<code>func start<\/code>)<\/li>\n\n\n\n<li><strong>Cloud testing tools<\/strong>\u00a0(with function keys)<\/li>\n\n\n\n<li><strong>Event Grid test tool<\/strong>\u00a0(to simulate real-time sync)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">This way, you can import&nbsp;<strong>one file<\/strong>&nbsp;into MCP Inspector and have&nbsp;<strong>everything ready<\/strong>&nbsp;for both local and deployed testing.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>1. Combined MCP Inspector Config JSON<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Save this as&nbsp;<code>mcp_tools_all.json<\/code>&nbsp;and replace placeholders:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>&lt;APP_NAME><\/code>\u00a0\u2192 Your Azure Function App name<\/li>\n\n\n\n<li><code>&lt;FUNCTION_KEY><\/code>\u00a0\u2192 Function key from Azure Portal \u2192 Function \u2192\u00a0<strong>Get Function URL<\/strong><\/li>\n\n\n\n<li><code>&lt;EVENT_GRID_TOPIC_ENDPOINT><\/code>\u00a0\u2192 From Azure Portal \u2192 Event Grid Topic \u2192 Overview \u2192\u00a0<strong>Topic Endpoint<\/strong><\/li>\n\n\n\n<li><code>&lt;EVENT_GRID_KEY><\/code>\u00a0\u2192 From Azure Portal \u2192 Event Grid Topic \u2192\u00a0<strong>Access Keys<\/strong>\u00a0\u2192 Key 1 or Key 2<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\">JsonCopy code<code>{\n  \"tools\": [\n    {\n      \"name\": \"GetIndexStatusLocal\",\n      \"type\": \"http\",\n      \"method\": \"GET\",\n      \"url\": \"http:\/\/localhost:7071\/api\/get_index_status\",\n      \"headers\": {\n        \"Content-Type\": \"application\/json\"\n      },\n      \"description\": \"Retrieve the last sync timestamp and document count from the local Azure Function.\"\n    },\n    {\n      \"name\": \"ManualRefreshLocal\",\n      \"type\": \"http\",\n      \"method\": \"POST\",\n      \"url\": \"http:\/\/localhost:7071\/api\/manual_refresh\",\n      \"headers\": {\n        \"Content-Type\": \"application\/json\"\n      },\n      \"description\": \"Trigger a manual refresh of the index from the local Azure Function.\"\n    },\n    {\n      \"name\": \"GetIndexStatusCloud\",\n      \"type\": \"http\",\n      \"method\": \"GET\",\n      \"url\": \"https:\/\/&lt;APP_NAME&gt;.azurewebsites.net\/api\/get_index_status?code=&lt;FUNCTION_KEY&gt;\",\n      \"headers\": {\n        \"Content-Type\": \"application\/json\"\n      },\n      \"description\": \"Retrieve the last sync timestamp and document count from the deployed Azure Function.\"\n    },\n    {\n      \"name\": \"ManualRefreshCloud\",\n      \"type\": \"http\",\n      \"method\": \"POST\",\n      \"url\": \"https:\/\/&lt;APP_NAME&gt;.azurewebsites.net\/api\/manual_refresh?code=&lt;FUNCTION_KEY&gt;\",\n      \"headers\": {\n        \"Content-Type\": \"application\/json\"\n      },\n      \"description\": \"Trigger a manual refresh of the index from the deployed Azure Function.\"\n    },\n    {\n      \"name\": \"TestEventGrid\",\n      \"type\": \"http\",\n      \"method\": \"POST\",\n      \"url\": \"&lt;EVENT_GRID_TOPIC_ENDPOINT&gt;?api-version=2018-01-01\",\n      \"headers\": {\n        \"Content-Type\": \"application\/json\",\n        \"aeg-sas-key\": \"&lt;EVENT_GRID_KEY&gt;\"\n      },\n      \"body\": \"[{\\\"id\\\":\\\"12345\\\",\\\"eventType\\\":\\\"Microsoft.Storage.BlobCreated\\\",\\\"subject\\\":\\\"\/blobServices\/default\/containers\/test\/blobs\/doc.json\\\",\\\"eventTime\\\":\\\"2026-09-10T12:00:00Z\\\",\\\"data\\\":{\\\"operationType\\\":\\\"insert\\\",\\\"document\\\":{\\\"id\\\":\\\"doc-001\\\",\\\"title\\\":\\\"Test Document\\\",\\\"content\\\":\\\"This is a test\\\",\\\"lastModified\\\":\\\"2026-09-10T12:00:00Z\\\"}},\\\"dataVersion\\\":\\\"1.0\\\"}]\",\n      \"description\": \"Send a test Event Grid event to trigger the real-time sync Azure Function.\"\n    }\n  ]\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>2. How to Import<\/strong><\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Open\u00a0<strong>MCP Inspector<\/strong>.<\/li>\n\n\n\n<li>Go to\u00a0<strong>Tools<\/strong>\u00a0\u2192\u00a0<strong>Import Tools<\/strong>.<\/li>\n\n\n\n<li>Select\u00a0<code>mcp_tools_all.json<\/code>.<\/li>\n\n\n\n<li>All five tools will appear in your tool list.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>3. Recommended Test Flow in MCP Inspector<\/strong><\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Local Test<\/strong>\u00a0(before deployment)\n<ul class=\"wp-block-list\">\n<li>Run\u00a0<code>GetIndexStatusLocal<\/code>\u00a0\u2192 Confirm baseline.<\/li>\n\n\n\n<li>Run\u00a0<code>ManualRefreshLocal<\/code>\u00a0\u2192 Confirm\u00a0<code>\"Manual refresh started\"<\/code>.<\/li>\n\n\n\n<li>Run\u00a0<code>GetIndexStatusLocal<\/code>\u00a0again \u2192 Confirm\u00a0<code>lastSync<\/code>\u00a0updated.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Cloud Test<\/strong>\u00a0(after deployment)\n<ul class=\"wp-block-list\">\n<li>Run\u00a0<code>GetIndexStatusCloud<\/code>\u00a0\u2192 Confirm live Azure index status.<\/li>\n\n\n\n<li>Run\u00a0<code>ManualRefreshCloud<\/code>\u00a0\u2192 Check Azure logs for indexing.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Event Grid Test<\/strong>\n<ul class=\"wp-block-list\">\n<li>Run\u00a0<code>TestEventGrid<\/code>\u00a0\u2192 Expect HTTP 200.<\/li>\n\n\n\n<li>Tail logs:BashCopy code<code>az functionapp log tail --name &lt;APP_NAME> --resource-group &lt;RESOURCE_GROUP> <\/code>Confirm event processed.<\/li>\n\n\n\n<li>Search AI Search index to confirm new doc exists.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>visual architecture package&nbsp;so you can see the&nbsp;design,&nbsp;flow,&nbsp;context,&nbsp;container, and&nbsp;class diagrams&nbsp;for your&nbsp;real-time + scheduled SQL \u2192 Azure AI Search knowledge base&nbsp;with&nbsp;UI&nbsp;and&nbsp;MCP\/Foundry agent&nbsp;integration. 1. System Design Overview Goal:A self-updating knowledge base that: 2. High-Level Flow Diagram Copy code+&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-+ +&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-+ +&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-+ | Azure SQL DB | &#8211;&gt; | CDC \/ Change Track | &#8211;&gt; | Event Grid Topic | [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_regular_price":[],"currency_symbol":[],"pagelayer_contact_templates":[],"_pagelayer_content":"","footnotes":""},"categories":[1],"tags":[],"class_list":["post-64","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"post_slider_layout_featured_media_urls":{"thumbnail":"","post_slider_layout_landscape_large":"","post_slider_layout_portrait_large":"","post_slider_layout_square_large":"","post_slider_layout_landscape":"","post_slider_layout_portrait":"","post_slider_layout_square":"","full":""},"_links":{"self":[{"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/posts\/64","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/comments?post=64"}],"version-history":[{"count":12,"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/posts\/64\/revisions"}],"predecessor-version":[{"id":89,"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/posts\/64\/revisions\/89"}],"wp:attachment":[{"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/media?parent=64"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/categories?post=64"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/tags?post=64"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}