
{"id":78,"date":"2026-09-11T00:46:42","date_gmt":"2026-09-11T00:46:42","guid":{"rendered":"https:\/\/roboticsmaestro.com\/ai\/?p=78"},"modified":"2026-09-11T04:50:41","modified_gmt":"2026-09-11T04:50:41","slug":"mcp-enabled-function-app-integrating-with-sql-and-ai-search","status":"publish","type":"post","link":"https:\/\/roboticsmaestro.com\/ai\/blog\/2026\/09\/11\/mcp-enabled-function-app-integrating-with-sql-and-ai-search\/","title":{"rendered":"MCP enabled Function app integrating with SQL and AI-Search &#8211; bicep files included"},"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\">first, to. develop function apps, install the. commandline &#8212;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><em>npm install -g azure-functions-core-tools@4 &#8211;unsafe-perm true<\/em><\/strong> (windows machine)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I would <strong>not simply add a SQL query inside the current MCP functions<\/strong>. I would add a separate <strong>SQL \u2192 AI Search synchronization pipeline<\/strong> inside the same Function App.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That gives you:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>                         Azure SQL\n                    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                    \u2502 Tables         \u2502\n                    \u2502 Views          \u2502\n                    \u2502 Stored queries \u2502\n                    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                            \u2502\n                     Managed Identity\n                            \u2502\n                            \u25bc\n                 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                 \u2502 Function App  \u2502\n                 \u2502                    \u2502\n                 \u2502 SQL extraction     \u2502\n                 \u2502 transformation     \u2502\n                 \u2502 validation         \u2502\n                 \u2502 security metadata  \u2502\n                 \u2502 batching           \u2502\n                 \u2502 telemetry          \u2502\n                 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                           \u2502\n                           \u25bc\n                 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                 \u2502   Azure AI Search  \u2502\n                 \u2502                    \u2502\n                 \u2502 csl-metadata       \u2502\n                 \u2502 staff-letters-new  \u2502\n                 \u2502 SQL index     \u2502\n                 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                           \u2502\n                           \u25bc\n                    MCP Tools\n                           \u2502\n                           \u25bc\n                  Foundry Agent Service<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Azure AI Search&#8217;s Python SDK supports Entra authentication with <code>DefaultAzureCredential<\/code> and supports batch <code>merge_or_upload_documents<\/code>, which is exactly what we need here.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There are also a few bugs\/issues in your current code that I would fix at the same time.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">1. Issues I found in your existing code<\/h1>\n\n\n\n<h3 class=\"wp-block-heading\">Issue 1 \u2014 typo in the index setting<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">You currently have:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>index_name=os.environ.get(\"AZURE_SEARCH_INDEX_NAE\", \"csl-metadata\")<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>NAE<\/code> should almost certainly be:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>AZURE_SEARCH_INDEX_NAME<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is important because a typo silently causes your application to use:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>csl-metadata<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">instead of the configured index.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Issue 2 \u2014 no SQL connectivity layer<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Currently:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Function\n   \u2502\n   \u251c\u2500\u2500 AI Search\n   \u2514\u2500\u2500 Blob<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">We need:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Function\n   \u2502\n   \u251c\u2500\u2500 AI Search\n   \u251c\u2500\u2500 Blob\n   \u2514\u2500\u2500 Azure SQL\n          \u2502\n          \u251c\u2500\u2500 tables\n          \u251c\u2500\u2500 views\n          \u2514\u2500\u2500 stored queries<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Issue 3 \u2014 no incremental synchronization<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">We don&#8217;t want:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SQL 5 million rows\n       \u2193\nFunction\n       \u2193\nAI Search<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">every five minutes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We want:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SQL\n \u2193\nOnly rows changed since last successful sync\n \u2193\nAI Search<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For simple SQL-to-Search synchronization, Azure AI Search also has a native SQL indexer with change tracking support.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">But your Function App gives you more control over <strong>transformation, authorization metadata, joins, enrichment, telemetry and MCP-specific documents<\/strong>, so I think the Function-based pipeline makes sense for your architecture.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">2. I recommend this final Function App structure<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Your single <code>function_app.py<\/code> can initially contain everything, but logically divide it into:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>function_app.py\n\u2502\n\u251c\u2500\u2500 Configuration\n\u2502\n\u251c\u2500\u2500 Credentials\n\u2502\n\u251c\u2500\u2500 AI Search clients\n\u2502\n\u251c\u2500\u2500 Blob functions\n\u2502\n\u251c\u2500\u2500 Existing MCP search\n\u2502\n\u251c\u2500\u2500 Existing semantic search\n\u2502\n\u251c\u2500\u2500 SQL connection\n\u2502\n\u251c\u2500\u2500 SQL extraction\n\u2502\n\u251c\u2500\u2500 SQL \u2192 Search transformation\n\u2502\n\u251c\u2500\u2500 Search batch upload\n\u2502\n\u251c\u2500\u2500 SQL synchronization\n\u2502\n\u251c\u2500\u2500 Timer trigger\n\u2502\n\u2514\u2500\u2500 Health\/test endpoints<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Once tested, I would split these into modules.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">3. Required packages<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Add these to <code>requirements.txt<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>azure-functions\nazure-identity\nazure-search-documents\nazure-storage-blob\npypdf\npyodbc<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>azure-functions==1.*\nazure-identity==1.*\nazure-search-documents==11.*\nazure-storage-blob==12.*\npypdf==6.*\npyodbc==5.*<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The Search SDK supports <code>SearchClient<\/code> with <code>DefaultAzureCredential<\/code>, and the same client supports adding\/updating\/deleting documents.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">4. Environment variables<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Add:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>AZURE_SEARCH_ENDPOINT\nAZURE_SEARCH_INDEX_NAME\nAZURE_SEARCH_SEMANTIC_INDEX_NAME\nAZURE_SEARCH_SEMANTIC_CONFIGURATION_NAME\n\nSQL_SERVER\nSQL_DATABASE\nSQL_DRIVER\n\nSQL_SYNC_SCHEDULE\nSQL_SYNC_BATCH_SIZE\nSQL_SYNC_LOOKBACK_MINUTES<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>AZURE_SEARCH_ENDPOINT=https:\/\/x-search.search.windows.net\n\nAZURE_SEARCH_INDEX_NAME=csl-metadata\n\nAZURE_SEARCH_SEMANTIC_INDEX_NAME=-data-new\n\nAZURE_SEARCH_SEMANTIC_CONFIGURATION_NAME=staff-letters-new-semantic-configuration\n\nSQL_SERVER=-sql.database.windows.net\n\nSQL_DATABASE=techwyns\n\nSQL_DRIVER=ODBC Driver 18 for SQL Server\n\nSQL_SYNC_SCHEDULE=0 *\/5 * * * *\n\nSQL_SYNC_BATCH_SIZE=500\n\nSQL_SYNC_LOOKBACK_MINUTES=10<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The overlap in <code>SQL_SYNC_LOOKBACK_MINUTES<\/code> protects against small timing differences between the SQL update timestamp and the Function execution.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For a mature implementation, I would replace the lookback approach with SQL <code>rowversion<\/code>\/change tracking.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">5. Azure SQL authentication<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Do <strong>not<\/strong> put this in your Function App:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SQL username\nSQL password<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Use:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Function App Managed Identity\n            \u2502\n            \u25bc\n       Azure SQL<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Your Function&#8217;s managed identity should have the minimum required SQL permissions.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>CREATE USER &#91;x-function-app]\nFROM EXTERNAL PROVIDER;\n\nALTER ROLE db_datareader\nADD MEMBER &#91;x-function-app];<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If you later need the Function to write synchronization state back into SQL, create a separate least-privilege role.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">6. Corrected Search client<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Replace your existing:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def get_search_client() -&gt; SearchClient:<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">with:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from functools import lru_cache\n\n@lru_cache(maxsize=1)\ndef get_credential() -&gt; DefaultAzureCredential:\n    return DefaultAzureCredential()\n\n\n@lru_cache(maxsize=20)\ndef get_search_client(index_name: str | None = None) -&gt; SearchClient:\n\n    endpoint = os.environ&#91;\"AZURE_SEARCH_ENDPOINT\"]\n\n    index_name = (\n        index_name\n        or os.environ.get(\n            \"AZURE_SEARCH_INDEX_NAME\",\n            \"csl-metadata\",\n        )\n    )\n\n    return SearchClient(\n        endpoint=endpoint,\n        index_name=index_name,\n        credential=get_credential(),\n    )<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This fixes your typo and avoids unnecessarily constructing credentials repeatedly.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">7. Add SQL connection support<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Add:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import struct\nimport pyodbc<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SQL_ACCESS_TOKEN = 1256\n\n\ndef get_sql_connection() -&gt; pyodbc.Connection:\n\n    server = os.environ&#91;\"SQL_SERVER\"]\n    database = os.environ&#91;\"SQL_DATABASE\"]\n\n    driver = os.environ.get(\n        \"SQL_DRIVER\",\n        \"ODBC Driver 18 for SQL Server\",\n    )\n\n    credential = get_credential()\n\n    token = credential.get_token(\n        \"https:\/\/database.windows.net\/.default\"\n    )\n\n    token_bytes = token.token.encode(\n        \"utf-16-le\"\n    )\n\n    token_struct = struct.pack(\n        f\"&lt;I{len(token_bytes)}s\",\n        len(token_bytes),\n        token_bytes,\n    )\n\n    connection_string = (\n        f\"Driver={{{driver}}};\"\n        f\"Server=tcp:{server},1433;\"\n        f\"Database={database};\"\n        \"Encrypt=yes;\"\n        \"TrustServerCertificate=no;\"\n        \"Connection Timeout=30;\"\n    )\n\n    return pyodbc.connect(\n        connection_string,\n        attrs_before={\n            SQL_ACCESS_TOKEN: token_struct\n        },\n    )<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This allows the Function App to authenticate to Azure SQL through its managed identity.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">8. Generic SQL query function<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Now add:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def execute_sql_query(\n    sql: str,\n    parameters: tuple&#91;Any, ...] = (),\n) -&gt; list&#91;dict&#91;str, Any]]:\n\n    connection = None\n\n    try:\n\n        connection = get_sql_connection()\n\n        cursor = connection.cursor()\n\n        logging.info(\n            \"Executing SQL query.\"\n        )\n\n        cursor.execute(\n            sql,\n            parameters,\n        )\n\n        columns = &#91;\n            column&#91;0]\n            for column in cursor.description\n        ]\n\n        rows = cursor.fetchall()\n\n        return &#91;\n            dict(zip(columns, row))\n            for row in rows\n        ]\n\n    except pyodbc.Error:\n\n        logging.exception(\n            \"Azure SQL query failed.\"\n        )\n\n        raise\n\n    finally:\n\n        if connection:\n            connection.close()<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is important because now you can retrieve from:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Tables\nViews\nStored procedures\nCustom SELECT statements<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">without duplicating the connection logic.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">9. SQL views are ideal for this<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">For your environment, I strongly recommend creating <strong>Search-specific SQL views<\/strong> rather than allowing the Function to understand every underlying relational table.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>CREATE VIEW x.vw_SearchCases\nAS\nSELECT\n    c.CaseId,\n    c.Title,\n    c.Description,\n    c.Status,\n    c.CaseType,\n    c.Classification,\n    c.OwnerDepartment,\n    c.CreatedAt,\n    c.UpdatedAt\nFROM x.Cases c;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sql = \"\"\"\nSELECT *\nFROM x.vw_SearchCases\nWHERE UpdatedAt &gt;= DATEADD(\n    MINUTE,\n    ?,\n    SYSUTCDATETIME()\n)\n\"\"\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">with:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>lookback = -10<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This gives you a clean contract:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>x operational database\n          \u2502\n          \u25bc\n   Search-specific views\n          \u2502\n          \u25bc\n     Function App\n          \u2502\n          \u25bc\n      AI Search<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">10. Create a generic SQL \u2192 Search transformer<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">This is the part I think will make your implementation substantially better.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Add:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def normalize_value(value: Any) -&gt; Any:\n\n    if value is None:\n        return None\n\n    if hasattr(value, \"isoformat\"):\n        return value.isoformat()\n\n    if isinstance(value, bytes):\n        return value.decode(\n            \"utf-8\",\n            errors=\"replace\",\n        )\n\n    return value<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def row_to_search_document(\n    row: dict&#91;str, Any],\n    *,\n    id_field: str,\n    entity_type: str,\n    content_fields: list&#91;str],\n    index_fields: dict&#91;str, str] | None = None,\n) -&gt; dict&#91;str, Any]:\n\n    index_fields = index_fields or {}\n\n    document_id = str(\n        row&#91;id_field]\n    )\n\n    document = {\n        \"id\": document_id,\n        \"entity_type\": entity_type,\n    }\n\n    for sql_field, search_field in index_fields.items():\n\n        if sql_field in row:\n\n            document&#91;search_field] = (\n                normalize_value(\n                    row&#91;sql_field]\n                )\n            )\n\n    content_parts = &#91;]\n\n    for field in content_fields:\n\n        value = row.get(field)\n\n        if value is not None:\n\n            content_parts.append(\n                f\"{field}: {value}\"\n            )\n\n    document&#91;\"content\"] = (\n        \"\\n\".join(content_parts)\n    )\n\n    return document<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">11. Why this is important<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Now SQL:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>CaseId\nTitle\nDescription\nStatus\nClassification\nOwnerDepartment<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">can become:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n    \"id\": \"CASE-10025\",\n    \"entity_type\": \"case\",\n    \"title\": \"Market Manipulation Investigation\",\n    \"content\": \"Title: Market Manipulation Investigation\\nDescription: ...\",\n    \"status\": \"Open\",\n    \"classification\": \"Restricted\",\n    \"department\": \"Enforcement\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That&#8217;s much better for AI Search and MCP.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">12. Batch upload to AI Search<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Add:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def upload_search_documents(\n    index_name: str,\n    documents: list&#91;dict&#91;str, Any]],\n    batch_size: int = 500,\n) -&gt; dict&#91;str, Any]:\n\n    if not documents:\n\n        return {\n            \"submitted\": 0,\n            \"succeeded\": 0,\n            \"failed\": 0,\n            \"errors\": &#91;],\n        }\n\n    client = get_search_client(\n        index_name\n    )\n\n    total_success = 0\n    total_failed = 0\n    errors = &#91;]\n\n    for start in range(\n        0,\n        len(documents),\n        batch_size,\n    ):\n\n        batch = documents&#91;\n            start:start + batch_size\n        ]\n\n        try:\n\n            results = (\n                client.merge_or_upload_documents(\n                    documents=batch\n                )\n            )\n\n            for result in results:\n\n                if result.succeeded:\n\n                    total_success += 1\n\n                else:\n\n                    total_failed += 1\n\n                    errors.append(\n                        {\n                            \"key\": result.key,\n                            \"error\": result.error_message,\n                        }\n                    )\n\n        except Exception as exc:\n\n            logging.exception(\n                \"AI Search batch failed.\"\n            )\n\n            total_failed += len(batch)\n\n            errors.append(\n                {\n                    \"batch_start\": start,\n                    \"error\": str(exc),\n                }\n            )\n\n    return {\n        \"submitted\": len(documents),\n        \"succeeded\": total_success,\n        \"failed\": total_failed,\n        \"errors\": errors&#91;:100],\n    }<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>merge_or_upload_documents<\/code> is particularly appropriate because it updates an existing document when its key exists and creates it when it doesn&#8217;t.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">13. Add a generic SQL table\/view exporter<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Now we can build the reusable synchronization layer:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def sync_sql_source_to_search(\n    *,\n    sql_query: str,\n    parameters: tuple&#91;Any, ...],\n    index_name: str,\n    id_field: str,\n    entity_type: str,\n    content_fields: list&#91;str],\n    index_fields: dict&#91;str, str],\n) -&gt; dict&#91;str, Any]:\n\n    rows = execute_sql_query(\n        sql_query,\n        parameters,\n    )\n\n    logging.info(\n        \"SQL source returned %d rows for %s.\",\n        len(rows),\n        entity_type,\n    )\n\n    documents = &#91;]\n\n    for row in rows:\n\n        try:\n\n            document = row_to_search_document(\n                row,\n                id_field=id_field,\n                entity_type=entity_type,\n                content_fields=content_fields,\n                index_fields=index_fields,\n            )\n\n            documents.append(\n                document\n            )\n\n        except Exception:\n\n            logging.exception(\n                \"Failed transforming %s row.\",\n                entity_type,\n            )\n\n    return upload_search_documents(\n        index_name=index_name,\n        documents=documents,\n        batch_size=int(\n            os.environ.get(\n                \"SQL_SYNC_BATCH_SIZE\",\n                \"500\",\n            )\n        ),\n    )<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">14. Now you can synchronize multiple SQL views<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">For example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def sync_cases() -&gt; dict&#91;str, Any]:\n\n    lookback = int(\n        os.environ.get(\n            \"SQL_SYNC_LOOKBACK_MINUTES\",\n            \"10\",\n        )\n    )\n\n    return sync_sql_source_to_search(\n\n        sql_query=\"\"\"\n            SELECT *\n            FROM Techwyns.vw_SearchCases\n            WHERE UpdatedAt &gt;= DATEADD(\n                MINUTE,\n                ?,\n                SYSUTCDATETIME()\n            )\n        \"\"\",\n\n        parameters=(-lookback,),\n\n        index_name=os.environ&#91;\n            \"AZURE_SEARCH_INDEX_NAME\"\n        ],\n\n        id_field=\"CaseId\",\n\n        entity_type=\"case\",\n\n        content_fields=&#91;\n            \"Title\",\n            \"Description\",\n            \"Status\",\n            \"CaseType\",\n        ],\n\n        index_fields={\n            \"CaseId\": \"case_id\",\n            \"Title\": \"title\",\n            \"Status\": \"status\",\n            \"CaseType\": \"case_type\",\n            \"Classification\":\n                \"classification\",\n            \"OwnerDepartment\":\n                \"department\",\n            \"UpdatedAt\":\n                \"last_modified\",\n        },\n    )<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">15. Add evidence<\/h1>\n\n\n\n<pre class=\"wp-block-code\"><code>def sync_evidence() -&gt; dict&#91;str, Any]:\n\n    lookback = int(\n        os.environ.get(\n            \"SQL_SYNC_LOOKBACK_MINUTES\",\n            \"10\",\n        )\n    )\n\n    return sync_sql_source_to_search(\n\n        sql_query=\"\"\"\n            SELECT *\n            FROM techwyns.vw_SearchEvidence\n            WHERE UpdatedAt &gt;= DATEADD(\n                MINUTE,\n                ?,\n                SYSUTCDATETIME()\n            )\n        \"\"\",\n\n        parameters=(-lookback,),\n\n        index_name=os.environ&#91;\n            \"AZURE_SEARCH_INDEX_NAME\"\n        ],\n\n        id_field=\"EvidenceId\",\n\n        entity_type=\"evidence\",\n\n        content_fields=&#91;\n            \"Title\",\n            \"Description\",\n            \"EvidenceType\",\n        ],\n\n        index_fields={\n            \"EvidenceId\":\n                \"evidence_id\",\n\n            \"CaseId\":\n                \"case_id\",\n\n            \"Title\":\n                \"title\",\n\n            \"Description\":\n                \"description\",\n\n            \"EvidenceType\":\n                \"evidence_type\",\n\n            \"Classification\":\n                \"classification\",\n\n            \"StorageUri\":\n                \"source_uri\",\n\n            \"UpdatedAt\":\n                \"last_modified\",\n        },\n    )<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">16. Add regulations<\/h1>\n\n\n\n<pre class=\"wp-block-code\"><code>def sync_regulations() -&gt; dict&#91;str, Any]:\n\n    lookback = int(\n        os.environ.get(\n            \"SQL_SYNC_LOOKBACK_MINUTES\",\n            \"10\",\n        )\n    )\n\n    return sync_sql_source_to_search(\n\n        sql_query=\"\"\"\n            SELECT *\n            FROM tchwyns.vw_SearchRegulations\n            WHERE UpdatedAt &gt;= DATEADD(\n                MINUTE,\n                ?,\n                SYSUTCDATETIME()\n            )\n        \"\"\",\n\n        parameters=(-lookback,),\n\n        index_name=os.environ&#91;\n            \"AZURE_SEARCH_INDEX_NAME\"\n        ],\n\n        id_field=\"RegulationId\",\n\n        entity_type=\"regulation\",\n\n        content_fields=&#91;\n            \"Title\",\n            \"Citation\",\n            \"TextContent\",\n        ],\n\n        index_fields={\n            \"RegulationId\":\n                \"regulation_id\",\n\n            \"Title\":\n                \"title\",\n\n            \"Citation\":\n                \"citation\",\n\n            \"EffectiveDate\":\n                \"effective_date\",\n\n            \"Classification\":\n                \"classification\",\n\n            \"SourceUri\":\n                \"source_uri\",\n\n            \"UpdatedAt\":\n                \"last_modified\",\n        },\n    )<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">17. Synchronize everything<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Now:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def synchronize_sql_to_search() -&gt; dict&#91;str, Any]:\n\n    logging.info(\n        \"Starting SQL \u2192 AI Search synchronization.\"\n    )\n\n    results = {}\n\n    try:\n        results&#91;\"cases\"] = sync_cases()\n    except Exception as exc:\n        logging.exception(\n            \"Case synchronization failed.\"\n        )\n        results&#91;\"cases\"] = {\n            \"error\": str(exc)\n        }\n\n    try:\n        results&#91;\"evidence\"] = sync_evidence()\n    except Exception as exc:\n        logging.exception(\n            \"Evidence synchronization failed.\"\n        )\n        results&#91;\"evidence\"] = {\n            \"error\": str(exc)\n        }\n\n    try:\n        results&#91;\"regulations\"] = sync_regulations()\n    except Exception as exc:\n        logging.exception(\n            \"Regulation synchronization failed.\"\n        )\n        results&#91;\"regulations\"] = {\n            \"error\": str(exc)\n        }\n\n    return results<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">I intentionally don&#8217;t make one failure stop every other source.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Cases       SUCCESS\nEvidence    SUCCESS\nRegulations FAILED\nMarket      SUCCESS<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">is much better operationally than:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Cases       SUCCESS\nEvidence    SUCCESS\nRegulations FAILED\n              \u2193\n           EVERYTHING\n              \u2193\n           STOPS<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">18. Add the timer trigger<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Your Function App currently doesn&#8217;t show a timer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Add:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>@app.timer_trigger(\n    schedule=\"%SQL_SYNC_SCHEDULE%\",\n    arg_name=\"timer\",\n    run_on_startup=False,\n    use_monitor=True,\n)\ndef sql_to_ai_search_sync(\n    timer: func.TimerRequest,\n) -&gt; None:\n\n    logging.info(\n        \"SQL \u2192 AI Search timer triggered.\"\n    )\n\n    if timer.past_due:\n\n        logging.warning(\n            \"SQL \u2192 AI Search timer is past due.\"\n        )\n\n    try:\n\n        result = (\n            synchronize_sql_to_search()\n        )\n\n        logging.info(\n            \"SQL \u2192 AI Search synchronization \"\n            \"completed: %s\",\n            json.dumps(\n                result,\n                default=str,\n            ),\n        )\n\n    except Exception:\n\n        logging.exception(\n            \"Fatal SQL \u2192 AI Search \"\n            \"synchronization failure.\"\n        )\n\n        raise<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The Python v2 Functions model supports this decorator-based timer pattern, and Microsoft recommends <code>run_on_startup=False<\/code> for production.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">19. Add a manual test endpoint<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">For your development environment, this is extremely useful.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>@app.route(\n    route=\"admin\/sync-sql-search\",\n    methods=&#91;\"POST\"],\n    auth_level=func.AuthLevel.FUNCTION,\n)\ndef manual_sql_search_sync(\n    req: func.HttpRequest,\n) -&gt; func.HttpResponse:\n\n    logging.info(\n        \"Manual SQL \u2192 Search sync requested.\"\n    )\n\n    try:\n\n        result = (\n            synchronize_sql_to_search()\n        )\n\n        return func.HttpResponse(\n            json.dumps(\n                result,\n                indent=2,\n                default=str,\n            ),\n            status_code=200,\n            mimetype=\"application\/json\",\n        )\n\n    except Exception as exc:\n\n        logging.exception(\n            \"Manual synchronization failed.\"\n        )\n\n        return func.HttpResponse(\n            json.dumps(\n                {\n                    \"error\": str(exc),\n                    \"error_type\":\n                        type(exc).__name__,\n                }\n            ),\n            status_code=500,\n            mimetype=\"application\/json\",\n        )<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">I deliberately recommend <code>FUNCTION<\/code> authorization rather than anonymous for this endpoint.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Your existing:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>app = func.FunctionApp(\n    http_auth_level=func.AuthLevel.ANONYMOUS\n)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">is something I would reconsider for a production techwyns application.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">20. Add health check<\/h1>\n\n\n\n<pre class=\"wp-block-code\"><code>@app.route(\n    route=\"health\",\n    methods=&#91;\"GET\"],\n    auth_level=func.AuthLevel.ANONYMOUS,\n)\ndef health(\n    req: func.HttpRequest,\n) -&gt; func.HttpResponse:\n\n    return func.HttpResponse(\n        json.dumps(\n            {\n                \"status\": \"healthy\",\n                \"service\":\n                    \"techwyns-function-app\",\n            }\n        ),\n        status_code=200,\n        mimetype=\"application\/json\",\n    )<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">21. Add SQL connectivity test<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">I would also add:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>@app.route(\n    route=\"admin\/test-sql\",\n    methods=&#91;\"GET\"],\n    auth_level=func.AuthLevel.FUNCTION,\n)\ndef test_sql(\n    req: func.HttpRequest,\n) -&gt; func.HttpResponse:\n\n    connection = None\n\n    try:\n\n        connection = get_sql_connection()\n\n        cursor = connection.cursor()\n\n        cursor.execute(\n            \"SELECT 1 AS HealthCheck\"\n        )\n\n        row = cursor.fetchone()\n\n        return func.HttpResponse(\n            json.dumps(\n                {\n                    \"sql\":\n                        \"connected\",\n                    \"health_check\":\n                        row.HealthCheck,\n                }\n            ),\n            status_code=200,\n            mimetype=\"application\/json\",\n        )\n\n    except Exception as exc:\n\n        logging.exception(\n            \"SQL connectivity test failed.\"\n        )\n\n        return func.HttpResponse(\n            json.dumps(\n                {\n                    \"sql\":\n                        \"failed\",\n                    \"error\":\n                        str(exc),\n                }\n            ),\n            status_code=500,\n            mimetype=\"application\/json\",\n        )\n\n    finally:\n\n        if connection:\n            connection.close()<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">22. Add AI Search connectivity test<\/h1>\n\n\n\n<pre class=\"wp-block-code\"><code>@app.route(\n    route=\"admin\/test-search\",\n    methods=&#91;\"GET\"],\n    auth_level=func.AuthLevel.FUNCTION,\n)\ndef test_search(\n    req: func.HttpRequest,\n) -&gt; func.HttpResponse:\n\n    try:\n\n        index_name = os.environ&#91;\n            \"AZURE_SEARCH_INDEX_NAME\"\n        ]\n\n        client = get_search_client(\n            index_name\n        )\n\n        count = (\n            client.get_document_count()\n        )\n\n        return func.HttpResponse(\n            json.dumps(\n                {\n                    \"search\":\n                        \"connected\",\n                    \"index\":\n                        index_name,\n                    \"document_count\":\n                        count,\n                }\n            ),\n            status_code=200,\n            mimetype=\"application\/json\",\n        )\n\n    except Exception as exc:\n\n        logging.exception(\n            \"AI Search connectivity test failed.\"\n        )\n\n        return func.HttpResponse(\n            json.dumps(\n                {\n                    \"search\":\n                        \"failed\",\n                    \"error\":\n                        str(exc),\n                }\n            ),\n            status_code=500,\n            mimetype=\"application\/json\",\n        )<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">23. Test individual SQL views<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">I would add a function specifically for testing the sources:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>def test_sql_sources() -&gt; dict&#91;str, Any]:\n\n    tests = {\n\n        \"cases\": \"\"\"\n            SELECT TOP 1 *\n            FROM techwyns.vw_SearchCases\n        \"\"\",\n\n        \"evidence\": \"\"\"\n            SELECT TOP 1 *\n            FROM techwyns.vw_SearchEvidence\n        \"\"\",\n\n        \"regulations\": \"\"\"\n            SELECT TOP 1 *\n            FROM techwyns.vw_SearchRegulations\n        \"\"\",\n    }\n\n    results = {}\n\n    for name, query in tests.items():\n\n        try:\n\n            rows = execute_sql_query(\n                query\n            )\n\n            results&#91;name] = {\n                \"status\":\n                    \"success\",\n                \"rows\":\n                    len(rows),\n            }\n\n        except Exception as exc:\n\n            logging.exception(\n                \"SQL source test failed: %s\",\n                name,\n            )\n\n            results&#91;name] = {\n                \"status\":\n                    \"failed\",\n                \"error\":\n                    str(exc),\n            }\n\n    return results<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">24. Testing strategy<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">I would test in this order.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Test 1 \u2014 SQL authentication<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Call:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>GET \/api\/admin\/test-sql<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"sql\": \"connected\",\n  \"health_check\": 1\n}<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Test 2 \u2014 AI Search<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Call:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>GET \/api\/admin\/test-search<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"search\": \"connected\",\n  \"index\": \"csl-metadata\",\n  \"document_count\": 1234\n}<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Test 3 \u2014 SQL view<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Execute:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SELECT TOP 10 *\nFROM techwyns.vw_SearchCases;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Verify:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>CaseId exists<\/li>\n\n\n\n<li>Title exists<\/li>\n\n\n\n<li>UpdatedAt exists<\/li>\n\n\n\n<li>Classification exists<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Test 4 \u2014 transformation<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Verify:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SQL row\n \u2193\nSearch document<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">has a valid:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>id\nentity_type\ncontent<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Test 5 \u2014 Search upload<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Run:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>POST \/api\/admin\/sync-sql-search<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"cases\": {\n    \"submitted\": 10,\n    \"succeeded\": 10,\n    \"failed\": 0\n  },\n  \"evidence\": {\n    \"submitted\": 25,\n    \"succeeded\": 25,\n    \"failed\": 0\n  }\n}<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Test 6 \u2014 Search verification<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Query:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>GET\/Search<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">or your existing MCP tool.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Verify the SQL-derived record appears.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Test 7 \u2014 update test<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Change one SQL record:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>UPDATE techwyns.Cases\nSET\n    Title = 'Updated Test Case',\n    UpdatedAt = SYSUTCDATETIME()\nWHERE CaseId = 'TEST-001';<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Run synchronization.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Search:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>TEST-001<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Verify:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Updated Test Case<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">appears.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">Test 8 \u2014 failure test<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Temporarily use an invalid SQL view:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>FROM techwyns.vw_DoesNotExist<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The Function should report:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>failed<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">without taking down the other synchronization jobs.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">25. One important production issue: deletes<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">The implementation above handles:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>INSERT\nUPDATE<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">but not:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>DELETE<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For production, you need deletion handling.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Otherwise:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SQL:\nCASE-123 exists\n\nAI Search:\nCASE-123 exists\n\nSQL:\nCASE-123 deleted\n\nAI Search:\nCASE-123 STILL EXISTS<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That&#8217;s dangerous.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I recommend adding a soft-delete field:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>IsDeleted BIT NOT NULL DEFAULT 0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then export:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"id\": \"CASE-123\",\n  \"@search.action\": \"delete\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">or explicitly call:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>client.delete_documents(\n    documents=&#91;\n        {\"id\": \"CASE-123\"}\n    ]\n)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The Search SDK supports deleting documents from an index.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">26. Better: SQL Change Tracking<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">For your production techwyns implementation, I would ultimately move away from:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>UpdatedAt &gt;= DATEADD(...)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">and use SQL change tracking or another durable high-water-mark mechanism.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That gives you:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SQL Change Tracking\n        \u2502\n        \u25bc\nFunction\n        \u2502\n        \u251c\u2500\u2500 INSERT\n        \u251c\u2500\u2500 UPDATE\n        \u2514\u2500\u2500 DELETE\n        \u2502\n        \u25bc\nAzure AI Search<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This avoids relying on a time window.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">And if the synchronization function fails:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Run #100\n \u2193\nSQL changes 1000-1100\n \u2193\nFunction crashes\n \u2193\nRun #101\n \u2193\nreprocesses changes\n \u2193\nSearch catches up<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That&#8217;s the behavior you want.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">27. Important issue with your MCP design<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Your current MCP search function:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>run_staff_letters_search()<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">is fine for the staff-letter use case.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I would <strong>not mix SQL synchronization into that function<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>                  Function App\n                       \u2502\n       \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n       \u2502               \u2502                \u2502\n       \u25bc               \u25bc                \u25bc\n   MCP Search       SQL Sync         Blob\/PDF\n       \u2502               \u2502\n       \u25bc               \u25bc\n AI Search        AI Search<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The SQL sync is an <strong>ingestion pipeline<\/strong>, not an MCP tool.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The MCP tool should only read\/query the resulting index.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">28. Recommended techwyns MCP tools after this change<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">You could then expose:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>semantic_search_staff_letters\nsearch__cases\nsearch__evidence\nsearch__regulations\nsearch__market_data\nget_staff_letter\nget_case\nget_evidence<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Architecture:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>                     techwyns MCP\n                         \u2502\n       \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n       \u2502                 \u2502                  \u2502\n       \u25bc                 \u25bc                  \u25bc\n Staff Letter        techwyns SQL          techwyns operational\n   Search             Search               tools\n       \u2502                 \u2502                  \u2502\n       \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2518                  \u2502\n                    \u25bc                       \u25bc\n              Azure AI Search          Azure SQL\/API<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">29. Your Search indexes<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">I would now have:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Azure AI Search\n\u2502\n\u251c\u2500\u2500 staff-letters-new\n\u2502     \u2514\u2500\u2500 existing semantic search\n\u2502\n\u251c\u2500\u2500 csl-metadata\n\u2502     \u2514\u2500\u2500 existing metadata\n\u2502\n\u2514\u2500\u2500 techwyns-enterprise\n      \u251c\u2500\u2500 cases\n      \u251c\u2500\u2500 evidence\n      \u251c\u2500\u2500 regulations\n      \u2514\u2500\u2500 market observations<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Or, if your current <code>csl-metadata<\/code> index already has the correct schema, you can reuse it rather than create another index.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">30. Security metadata is especially important<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">For the techwyns implementation, I would make every SQL-derived Search document carry:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>classification\ndepartment\ncase_id\nsource_system\nsource_uri\nallowed_groups\nallowed_roles\nlast_modified<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"id\": \"case-1001\",\n  \"entity_type\": \"case\",\n  \"title\": \"Market Investigation\",\n  \"content\": \"...\",\n  \"case_id\": \"1001\",\n  \"classification\": \"techwyns-RESTRICTED\",\n  \"department\": \"Enforcement\",\n  \"allowed_groups\": &#91;\n    \"techwyns-Enforcement\",\n    \"techwyns-Senior-Investigators\"\n  ],\n  \"source_system\": \"techwyns-SQL\",\n  \"source_uri\": \"techwyns:\/\/case\/1001\",\n  \"last_modified\": \"2026-09-10T21:15:00Z\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then your MCP authorization layer constructs the Search filter.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The LLM never determines these permissions.<\/strong><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">31. Your current anonymous Function App<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">You currently have:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>app = func.FunctionApp(\n    http_auth_level=func.AuthLevel.ANONYMOUS\n)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">I would be cautious about leaving the entire application anonymous in production.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Especially because you now want:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SQL\nAI Search\nMCP\ntechwyns data\nFoundry Agents<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A better production topology is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Foundry \/ SimpleChat \/ Teams\n             \u2502\n             \u25bc\n           APIM\n             \u2502\n      Entra validation\n             \u2502\n             \u25bc\n      Private Function App\n             \u2502\n       \u250c\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2510\n       \u25bc           \u25bc\n      SQL       AI Search<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The Function&#8217;s MCP endpoint should not be an unrestricted anonymous endpoint.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">32. One other correction to your current code<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">This:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>logging.info(\n    \"MCP context type=%s repr=%s\",\n    type(context).__name__,\n    context\n)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">could potentially dump sensitive MCP context into Application Insights.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I would change it to:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>logging.info(\n    \"MCP context type=%s\",\n    type(context).__name__,\n)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Likewise:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>logging.info(\n    \"Resolved MCP arguments: %s\",\n    arguments\n)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">should be redacted or removed in production.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For techwyns data, <strong>don&#8217;t routinely put MCP arguments, case content, prompts, tokens, or retrieved evidence into Application Insights<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Log:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>correlation_id\ntool_name\nagent_id\nduration\nresult_count\nsuccess\nerror_type\nauthorization_decision<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">rather than the sensitive payload.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">33. I would also improve your existing exception handling<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Currently:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>except Exception as exc:\n    logging.exception(\"Semantic search failed\")\n    return json.dumps({\n        \"error\": str(exc),\n        \"error_type\": type(exc).__name__\n    })<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That&#8217;s acceptable for development but can expose internal information.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Production:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>except ValueError as exc:\n\n    logging.warning(\n        \"Invalid semantic search request: %s\",\n        exc,\n    )\n\n    return json.dumps({\n        \"error\": \"Invalid request\",\n        \"error_type\": \"validation_error\",\n    })\n\nexcept Exception:\n\n    logging.exception(\n        \"Semantic search failed.\"\n    )\n\n    return json.dumps({\n        \"error\":\n            \"Internal search service error\",\n        \"error_type\":\n            \"internal_error\",\n    })<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That keeps SQL connection errors, internal URLs, stack details, etc. away from the agent.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">34. Final production flow<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">With these changes, your Function App becomes:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>                       \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                       \u2502      Azure SQL       \u2502\n                       \u2502                      \u2502\n                       \u2502 Tables               \u2502\n                       \u2502 Views                \u2502\n                       \u2502 Change Tracking      \u2502\n                       \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                                  \u2502\n                                  \u2502 Managed Identity\n                                  \u25bc\n                       \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                       \u2502   techwyns Function App  \u2502\n                       \u2502                      \u2502\n                       \u2502 SQL extraction       \u2502\n                       \u2502 Validation           \u2502\n                       \u2502 Transformation       \u2502\n                       \u2502 Classification       \u2502\n                       \u2502 Security metadata    \u2502\n                       \u2502 Batch processing     \u2502\n                       \u2502 Error handling       \u2502\n                       \u2502 App Insights         \u2502\n                       \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                                  \u2502\n                                  \u2502 Entra\/RBAC\n                                  \u25bc\n                       \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                       \u2502   Azure AI Search    \u2502\n                       \u2502                      \u2502\n                       \u2502 csl-metadata         \u2502\n                       \u2502 staff-letters-new    \u2502\n                       \u2502 techwyns-enterprise      \u2502\n                       \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                                  \u2502\n                                  \u25bc\n                         \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                         \u2502   techwyns MCP     \u2502\n                         \u2502                \u2502\n                         \u2502 Search tools   \u2502\n                         \u2502 Case tools     \u2502\n                         \u2502 Evidence       \u2502\n                         \u2502 Regulatory     \u2502\n                         \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                                 \u2502\n                                 \u25bc\n                      \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                      \u2502 Microsoft Foundry    \u2502\n                      \u2502 Agent Service        \u2502\n                      \u2502                      \u2502\n                      \u2502 techwyns Orchestrator    \u2502\n                      \u2502 Research Agent       \u2502\n                      \u2502 Investigation Agent  \u2502\n                      \u2502 Regulatory Agent     \u2502\n                      \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">My strongest recommendation<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">For your <strong>first implementation<\/strong>, use the code pattern above with <strong>SQL views + incremental <code>UpdatedAt<\/code> synchronization + <code>merge_or_upload_documents<\/code> + timer trigger + managed identity<\/strong>. Then, once it works end-to-end, upgrade the synchronization mechanism to <strong>SQL Change Tracking\/rowversion + durable sync state + delete detection<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That gives you a clean path from your current Function App to a production techwyns data ingestion\/MCP architecture without disrupting your existing staff-letter semantic search. Microsoft supports exactly the Entra-based Search client pattern and batch document operations we&#8217;re using here.<\/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>complete <code>function_app.py<\/code><\/strong> you can deploy as the replacement for the file you supplied.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I have preserved your existing:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Staff-letter semantic search MCP tool<\/li>\n\n\n\n<li>Staff-letter Blob\/PDF retrieval logic<\/li>\n\n\n\n<li>Metadata search\/filter logic<\/li>\n\n\n\n<li>Existing MCP argument handling<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">and added:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Azure SQL connectivity using <strong>Managed Identity<\/strong><\/li>\n\n\n\n<li>SQL tables\/views support<\/li>\n\n\n\n<li>Multiple configurable SQL sources<\/li>\n\n\n\n<li>Incremental lookback synchronization<\/li>\n\n\n\n<li>SQL \u2192 transformation \u2192 AI Search<\/li>\n\n\n\n<li>Batch <code>merge_or_upload_documents<\/code><\/li>\n\n\n\n<li>Delete handling via <code>IsDeleted<\/code><\/li>\n\n\n\n<li>Retry handling<\/li>\n\n\n\n<li>Per-source exception isolation<\/li>\n\n\n\n<li>Manual SQL\u2192Search synchronization endpoint<\/li>\n\n\n\n<li>SQL health test<\/li>\n\n\n\n<li>AI Search health test<\/li>\n\n\n\n<li>SQL source test<\/li>\n\n\n\n<li>Search verification endpoint<\/li>\n\n\n\n<li>Sync statistics<\/li>\n\n\n\n<li>Safer production logging<\/li>\n\n\n\n<li>Corrected <code>AZURE_SEARCH_INDEX_NAE<\/code> typo<\/li>\n\n\n\n<li>Credential\/client caching<\/li>\n\n\n\n<li>Configuration validation<\/li>\n\n\n\n<li>Protection against arbitrary SQL through the HTTP API<\/li>\n\n\n\n<li>Existing MCP search functionality<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Azure AI Search supports <code>DefaultAzureCredential<\/code>\/Entra authentication and <code>merge_or_upload_documents<\/code>; the identity needs <strong>Search Index Data Contributor<\/strong> for indexing and <strong>Search Index Data Reader<\/strong> for querying.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Complete <code>function_app.py<\/code><\/h1>\n\n\n\n<pre class=\"wp-block-code\"><code>\n\"\"\"\ntechwyns Azure Function App\n=======================\n\nCapabilities\n------------\n1. Azure AI Search metadata search\n2. Semantic search over techwyns staff letters\n3. Staff-letter PDF retrieval from Azure Blob Storage\n4. MCP tools for Foundry Agent Service\n5. Azure SQL -&gt; Azure AI Search synchronization\n6. SQL table\/view synchronization\n7. Incremental synchronization using UpdatedAt lookback\n8. Optional SQL IsDeleted -&gt; AI Search delete handling\n9. Batch merge-or-upload into Azure AI Search\n10. SQL \/ AI Search health checks\n11. Manual synchronization endpoint\n12. Source-level synchronization testing\n13. Exception isolation and retry handling\n\nAuthentication\n--------------\nAzure SQL:\n    Managed Identity \/ DefaultAzureCredential\n\nAzure AI Search:\n    Managed Identity \/ DefaultAzureCredential\n\nBlob Storage:\n    Managed Identity \/ DefaultAzureCredential\n    or optional connection string\n\nIMPORTANT\n---------\nSQL synchronization sources are configured through:\n\n    SQL_SYNC_SOURCES_JSON\n\nExample:\n\n&#91;\n  {\n    \"name\": \"cases\",\n    \"query\": \"SELECT CaseId, Title, Description, Status, CaseType, Classification, OwnerDepartment, UpdatedAt, IsDeleted FROM techwyns.vw_SearchCases WHERE UpdatedAt &gt;= DATEADD(MINUTE, ?, SYSUTCDATETIME())\",\n    \"id_field\": \"CaseId\",\n    \"entity_type\": \"case\",\n    \"index_name\": \"techwyns-enterprise\",\n    \"content_fields\": &#91;\n      \"Title\",\n      \"Description\",\n      \"Status\",\n      \"CaseType\"\n    ],\n    \"field_map\": {\n      \"CaseId\": \"case_id\",\n      \"Title\": \"title\",\n      \"Status\": \"status\",\n      \"CaseType\": \"case_type\",\n      \"Classification\": \"classification\",\n      \"OwnerDepartment\": \"department\",\n      \"UpdatedAt\": \"last_modified\"\n    },\n    \"deleted_field\": \"IsDeleted\"\n  }\n]\n\nThe SQL query is application-controlled configuration.\nThe MCP\/HTTP caller cannot submit arbitrary SQL.\n\"\"\"<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">1. <code>requirements.txt<\/code><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Use:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>azure-functions\nazure-identity\nazure-search-documents\nazure-storage-blob\npypdf\npyodbc<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The current Azure AI Search Python SDK supports <code>SearchClient<\/code> with Microsoft Entra authentication and document upload\/merge\/delete operations.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">2. The important configuration you need<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">The code intentionally doesn&#8217;t hard-code your unknown SQL schema.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Set these Function App settings:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>AZURE_SEARCH_ENDPOINT\nAZURE_SEARCH_INDEX_NAME\nAZURE_SEARCH_SEMANTIC_INDEX_NAME\nAZURE_SEARCH_SEMANTIC_CONFIGURATION_NAME\n\nSQL_SERVER\nSQL_DATABASE\nSQL_DRIVER\n\nSQL_SYNC_SCHEDULE\nSQL_SYNC_BATCH_SIZE\nSQL_SYNC_LOOKBACK_MINUTES\nSQL_SYNC_RETRIES\nSQL_SYNC_RETRY_BASE_SECONDS<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>AZURE_SEARCH_ENDPOINT=https:\/\/YOUR-SEARCH.search.windows.net\n\nAZURE_SEARCH_INDEX_NAME=csl-metadata\n\nAZURE_SEARCH_SEMANTIC_INDEX_NAME=staff-letters-new\n\nAZURE_SEARCH_SEMANTIC_CONFIGURATION_NAME=staff-letters-new-semantic-configuration\n\nSQL_SERVER=YOUR-SQL.database.windows.net\n\nSQL_DATABASE=techwyns\n\nSQL_DRIVER=ODBC Driver 18 for SQL Server\n\nSQL_SYNC_SCHEDULE=0 *\/5 * * * *\n\nSQL_SYNC_BATCH_SIZE=500\n\nSQL_SYNC_LOOKBACK_MINUTES=10\n\nSQL_SYNC_RETRIES=3\n\nSQL_SYNC_RETRY_BASE_SECONDS=2<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The Search identity needs <strong>Search Index Data Contributor<\/strong> to push documents; query-only operations require <strong>Search Index Data Reader<\/strong>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">3. Configure your SQL tables\/views<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">This is the most important setting:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SQL_SYNC_SOURCES_JSON<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">I deliberately made this configurable so you can synchronize <strong>different tables and views without modifying\/redeploying the Python code<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For example, suppose you have:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>techwyns.vw_SearchCases\ntechwyns.vw_SearchEvidence\ntechwyns.vw_SearchRegulations\ntechwyns.vw_SearchMarketObservations<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Your setting can contain:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;\n  {\n    \"name\": \"cases\",\n    \"query\": \"SELECT CaseId, Title, Description, Status, CaseType, Classification, OwnerDepartment, UpdatedAt, IsDeleted FROM techwyns.vw_SearchCases WHERE UpdatedAt &gt;= DATEADD(MINUTE, ?, SYSUTCDATETIME())\",\n    \"id_field\": \"CaseId\",\n    \"entity_type\": \"case\",\n    \"index_name\": \"techwyns-enterprise\",\n    \"content_fields\": &#91;\n      \"Title\",\n      \"Description\",\n      \"Status\",\n      \"CaseType\"\n    ],\n    \"field_map\": {\n      \"CaseId\": \"case_id\",\n      \"Title\": \"title\",\n      \"Description\": \"description\",\n      \"Status\": \"status\",\n      \"CaseType\": \"case_type\",\n      \"Classification\": \"classification\",\n      \"OwnerDepartment\": \"department\",\n      \"UpdatedAt\": \"last_modified\"\n    },\n    \"deleted_field\": \"IsDeleted\",\n    \"use_lookback\": true\n  },\n\n  {\n    \"name\": \"evidence\",\n    \"query\": \"SELECT EvidenceId, CaseId, Title, Description, EvidenceType, Classification, StorageUri, UpdatedAt, IsDeleted FROM techwyns.vw_SearchEvidence WHERE UpdatedAt &gt;= DATEADD(MINUTE, ?, SYSUTCDATETIME())\",\n    \"id_field\": \"EvidenceId\",\n    \"entity_type\": \"evidence\",\n    \"index_name\": \"techwyns-enterprise\",\n    \"content_fields\": &#91;\n      \"Title\",\n      \"Description\",\n      \"EvidenceType\"\n    ],\n    \"field_map\": {\n      \"EvidenceId\": \"evidence_id\",\n      \"CaseId\": \"case_id\",\n      \"Title\": \"title\",\n      \"Description\": \"description\",\n      \"EvidenceType\": \"evidence_type\",\n      \"Classification\": \"classification\",\n      \"StorageUri\": \"source_uri\",\n      \"UpdatedAt\": \"last_modified\"\n    },\n    \"deleted_field\": \"IsDeleted\",\n    \"use_lookback\": true\n  },\n\n  {\n    \"name\": \"regulations\",\n    \"query\": \"SELECT RegulationId, Title, Citation, TextContent, EffectiveDate, Classification, SourceUri, UpdatedAt, IsDeleted FROM techwyns.vw_SearchRegulations WHERE UpdatedAt &gt;= DATEADD(MINUTE, ?, SYSUTCDATETIME())\",\n    \"id_field\": \"RegulationId\",\n    \"entity_type\": \"regulation\",\n    \"index_name\": \"techwyns-enterprise\",\n    \"content_fields\": &#91;\n      \"Title\",\n      \"Citation\",\n      \"TextContent\"\n    ],\n    \"field_map\": {\n      \"RegulationId\": \"regulation_id\",\n      \"Title\": \"title\",\n      \"Citation\": \"citation\",\n      \"TextContent\": \"content_text\",\n      \"EffectiveDate\": \"effective_date\",\n      \"Classification\": \"classification\",\n      \"SourceUri\": \"source_uri\",\n      \"UpdatedAt\": \"last_modified\"\n    },\n    \"deleted_field\": \"IsDeleted\",\n    \"use_lookback\": true\n  },\n\n  {\n    \"name\": \"market-observations\",\n    \"query\": \"SELECT ObservationId, Instrument, ObservationTime, Price, Volume, OpenInterest, SourceSystem, Classification, UpdatedAt, IsDeleted FROM techwyns.vw_SearchMarketObservations WHERE UpdatedAt &gt;= DATEADD(MINUTE, ?, SYSUTCDATETIME())\",\n    \"id_field\": \"ObservationId\",\n    \"entity_type\": \"market_observation\",\n    \"index_name\": \"techwyns-enterprise\",\n    \"content_fields\": &#91;\n      \"Instrument\",\n      \"ObservationTime\",\n      \"Price\",\n      \"Volume\",\n      \"OpenInterest\",\n      \"SourceSystem\"\n    ],\n    \"field_map\": {\n      \"ObservationId\": \"observation_id\",\n      \"Instrument\": \"instrument\",\n      \"ObservationTime\": \"observation_time\",\n      \"Price\": \"price\",\n      \"Volume\": \"volume\",\n      \"OpenInterest\": \"open_interest\",\n      \"SourceSystem\": \"source_system\",\n      \"Classification\": \"classification\",\n      \"UpdatedAt\": \"last_modified\"\n    },\n    \"deleted_field\": \"IsDeleted\",\n    \"use_lookback\": true\n  }\n]<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Because this is an application setting, you&#8217;ll need to make sure Azure Functions preserves the JSON as a single setting value.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">4. Your AI Search index must contain the mapped fields<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">For the example above, your <code>techwyns-enterprise<\/code> index needs fields corresponding to the generated documents, such as:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>id\nentity_type\ncontent\ncase_id\nevidence_id\nregulation_id\nobservation_id\ntitle\ndescription\nstatus\ncase_type\nevidence_type\ncitation\ncontent_text\ninstrument\nobservation_time\nprice\nvolume\nopen_interest\nsource_system\nclassification\ndepartment\nsource_uri\neffective_date\nlast_modified<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Do not deploy the Function expecting it to automatically create this index.<\/strong> Your existing AI Search index schema must match the documents you&#8217;re pushing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The Search SDK treats an index as persistent JSON document storage and supports push-based document loading, so this Function is acting as your controlled ingestion\/push pipeline.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">5. SQL views I recommend<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Rather than allowing the Function to understand your raw relational model, create controlled search views.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>CREATE VIEW techwyns.vw_SearchCases\nAS\nSELECT\n    CaseId,\n    Title,\n    Description,\n    Status,\n    CaseType,\n    Classification,\n    OwnerDepartment,\n    UpdatedAt,\n    IsDeleted\nFROM techwyns.Cases;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">And:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>CREATE VIEW techwyns.vw_SearchEvidence\nAS\nSELECT\n    EvidenceId,\n    CaseId,\n    Title,\n    Description,\n    EvidenceType,\n    Classification,\n    StorageUri,\n    UpdatedAt,\n    IsDeleted\nFROM techwyns.Evidence;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is considerably safer than exposing arbitrary SQL to the MCP server.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">6. Managed Identity permissions<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Your Function App&#8217;s managed identity needs SQL access.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>CREATE USER &#91;YOUR-FUNCTION-APP-NAME]\nFROM EXTERNAL PROVIDER;\n\nALTER ROLE db_datareader\nADD MEMBER &#91;YOUR-FUNCTION-APP-NAME];<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then AI Search:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Function App Managed Identity\n             \u2502\n             \u251c\u2500\u2500 SQL \u2192 db_datareader\n             \u2502\n             \u2514\u2500\u2500 AI Search\n                    \u2514\u2500\u2500 Search Index Data Contributor<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The Azure AI Search RBAC documentation specifically distinguishes the <strong>Search Index Data Contributor<\/strong> role for loading data from <strong>Search Index Data Reader<\/strong> for querying.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">7. Test sequence after deployment<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Don&#8217;t immediately rely on the timer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">First:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">SQL<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>GET \/api\/admin\/test-sql<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"status\": \"connected\",\n  \"utc_time\": \"2026-09-10T...\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">AI Search<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>GET \/api\/admin\/test-search?index=techwyns-enterprise<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"status\": \"connected\",\n  \"index\": \"techwyns-enterprise\",\n  \"document_count\": 123\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">SQL sources<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>GET \/api\/admin\/test-sql-sources<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Expected:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"status\": \"success\",\n  \"source_count\": 4,\n  \"sources\": {\n    \"cases\": {\n      \"status\": \"success\",\n      \"rows\": 12\n    }\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then manually run:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>POST \/api\/admin\/sync-sql-search<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You should get something like:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"status\": \"completed\",\n  \"source_count\": 4,\n  \"total_sql_rows\": 187,\n  \"total_documents_succeeded\": 185,\n  \"total_documents_failed\": 0,\n  \"total_documents_deleted\": 2\n}<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">8. One important production caveat<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">The current synchronization uses:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>UpdatedAt &gt;= DATEADD(MINUTE, ?, SYSUTCDATETIME())<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">with the default:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SQL_SYNC_LOOKBACK_MINUTES=10<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That&#8217;s a <strong>safe first deployment<\/strong>, but I would not consider it the final high-scale synchronization mechanism.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For the production techwyns platform, I recommend eventually moving to:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SQL Change Tracking \/ rowversion\n             \u2193\nFunction\n             \u2193\nINSERT \/ UPDATE \/ DELETE\n             \u2193\nAI Search<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">rather than repeatedly scanning a time window.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That becomes especially important once the techwyns database gets large.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">9. One more important architectural point<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">I have <strong>not made SQL itself an MCP tool<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The architecture is intentionally:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>                 Azure SQL\n                    \u2502\n                    \u2502\n              SQL synchronization\n                    \u2502\n                    \u25bc\n               AI Search\n                    \u2502\n                    \u25bc\n                techwyns MCP\n                    \u2502\n                    \u25bc\n             Foundry Agents<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">rather than:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Foundry Agent\n     \u2502\n     \u25bc\nMCP\n     \u2502\n     \u25bc\n\"execute arbitrary SQL\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">I strongly recommend keeping it this way.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The agent should be able to ask:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>search cases\nsearch evidence\nsearch regulations\nsearch market observations\nget case\nget evidence<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">but <strong>never<\/strong>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>execute_sql(\"DROP...\")\nexecute_sql(\"SELECT anything...\")<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That separation is particularly important for the governed techwyns architecture.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Final deployment package<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">At minimum, deploy:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>function_app.py\nrequirements.txt\nhost.json<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">with Function App settings:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>AZURE_SEARCH_ENDPOINT\nAZURE_SEARCH_INDEX_NAME\nAZURE_SEARCH_SEMANTIC_INDEX_NAME\nAZURE_SEARCH_SEMANTIC_CONFIGURATION_NAME\n\nSQL_SERVER\nSQL_DATABASE\nSQL_DRIVER\n\nSQL_SYNC_SCHEDULE\nSQL_SYNC_BATCH_SIZE\nSQL_SYNC_LOOKBACK_MINUTES\nSQL_SYNC_RETRIES\nSQL_SYNC_RETRY_BASE_SECONDS\n\nSQL_SYNC_SOURCES_JSON\n\nAZURE_STORAGE_ACCOUNT_URL\nSTAFF_LETTERS_CONTAINER<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Your existing staff-letter MCP capability remains intact, while the new pipeline becomes:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Azure SQL tables\/views \u2192 Function App \u2192 controlled transformation \u2192 AI Search \u2192 techwyns MCP \u2192 Microsoft Foundry agents.<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is also compatible with your existing AI Search\/Foundry architecture because the Search SDK supports both querying and pushing documents using Entra identity rather than search keys.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Absolutely. I created the complete <code>function_app.py<\/code> as a single downloadable file.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Download<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Download the complete final function_app.py<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It includes the existing staff-letter MCP functionality plus:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Azure AI Search semantic search<\/li>\n\n\n\n<li>Standard staff-letter search<\/li>\n\n\n\n<li>Blob\/PDF extraction<\/li>\n\n\n\n<li>MCP tools<\/li>\n\n\n\n<li>Azure SQL \u2192 Azure AI Search synchronization<\/li>\n\n\n\n<li>Multiple SQL tables\/views through <code>SQL_SYNC_SOURCES_JSON<\/code><\/li>\n\n\n\n<li>Managed Identity authentication to Azure SQL<\/li>\n\n\n\n<li>Managed Identity authentication to AI Search<\/li>\n\n\n\n<li>Incremental <code>lookback_minutes<\/code> support<\/li>\n\n\n\n<li>Batch indexing<\/li>\n\n\n\n<li>Retry\/backoff<\/li>\n\n\n\n<li>Partial indexing error handling<\/li>\n\n\n\n<li>Soft-delete support<\/li>\n\n\n\n<li>Scheduled SQL \u2192 Search synchronization<\/li>\n\n\n\n<li><code>\/api\/sql-sync-test<\/code><\/li>\n\n\n\n<li><code>\/api\/health<\/code><\/li>\n\n\n\n<li>Configuration validation<\/li>\n\n\n\n<li>Protection against arbitrary SQL supplied by an agent\/user<\/li>\n\n\n\n<li>Corrected <code>AZURE_SEARCH_INDEX_NAME<\/code> configuration typo<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">How to deploy multiple Python files into ONE Azure Function App<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">The important concept is:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>One Function App = one deployment project.<\/strong><br>You can have many <code>.py<\/code> files inside that project, but they should be registered through the same <code>FunctionApp<\/code> object.<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">Microsoft specifically recommends using <strong>Blueprints<\/strong> for larger Python v2 applications so that functions can be organized across multiple Python files and then registered from the main <code>function_app.py<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For your techwyns-AI implementation, I recommend this structure:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>techwyns-ai-function-app\/\n\u2502\n\u251c\u2500\u2500 function_app.py\n\u2502\n\u251c\u2500\u2500 blueprints\/\n\u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u251c\u2500\u2500 mcp_blueprint.py\n\u2502   \u251c\u2500\u2500 sql_sync_blueprint.py\n\u2502   \u251c\u2500\u2500 health_blueprint.py\n\u2502   \u2514\u2500\u2500 admin_blueprint.py\n\u2502\n\u251c\u2500\u2500 services\/\n\u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u251c\u2500\u2500 search_service.py\n\u2502   \u251c\u2500\u2500 sql_service.py\n\u2502   \u251c\u2500\u2500 blob_service.py\n\u2502   \u251c\u2500\u2500 pdf_service.py\n\u2502   \u2514\u2500\u2500 identity_service.py\n\u2502\n\u251c\u2500\u2500 models\/\n\u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u2514\u2500\u2500 sync_models.py\n\u2502\n\u251c\u2500\u2500 shared\/\n\u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u251c\u2500\u2500 config.py\n\u2502   \u251c\u2500\u2500 security.py\n\u2502   \u251c\u2500\u2500 serialization.py\n\u2502   \u2514\u2500\u2500 logging_utils.py\n\u2502\n\u251c\u2500\u2500 tests\/\n\u2502   \u251c\u2500\u2500 __init__.py\n\u2502   \u251c\u2500\u2500 test_search.py\n\u2502   \u251c\u2500\u2500 test_sql_sync.py\n\u2502   \u2514\u2500\u2500 test_mcp.py\n\u2502\n\u251c\u2500\u2500 host.json\n\u251c\u2500\u2500 requirements.txt\n\u251c\u2500\u2500 .funcignore\n\u251c\u2500\u2500 local.settings.json\n\u2514\u2500\u2500 README.md<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is preferable to creating several independent <code>function_app.py<\/code> files. Azure&#8217;s Python v2 model uses decorators and a central <code>FunctionApp<\/code>, while blueprints let you split functionality into separate modules.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">1. Keep <code>function_app.py<\/code> as the main entry point<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Your main file should eventually become relatively small:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import azure.functions as func\n\nfrom blueprints.mcp_blueprint import bp as mcp_bp\nfrom blueprints.sql_sync_blueprint import bp as sql_bp\nfrom blueprints.health_blueprint import bp as health_bp\n\napp = func.FunctionApp(\n    http_auth_level=func.AuthLevel.ANONYMOUS\n)\n\napp.register_functions(mcp_bp)\napp.register_functions(sql_bp)\napp.register_functions(health_bp)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>register_functions()<\/code> mechanism is supported by the Python Functions programming model.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Important<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">You <strong>do not<\/strong> want:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>function_app.py\nfunction_app2.py\nfunction_app3.py<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">with each file doing:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>app = func.FunctionApp()<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That creates separate application objects that are not automatically assembled into one Function App.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>function_app.py\n        \u2502\n        \u251c\u2500\u2500 MCP blueprint\n        \u251c\u2500\u2500 SQL blueprint\n        \u251c\u2500\u2500 Health blueprint\n        \u251c\u2500\u2500 Admin blueprint\n        \u2514\u2500\u2500 other blueprints<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">2. Example MCP blueprint<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Create:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>blueprints\/mcp_blueprint.py<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import json\nimport azure.functions as func\n\nfrom services.search_service import run_semantic_staff_letters_search\nfrom services.blob_service import run_get_staff_letter\n\nbp = func.Blueprint()\n\n\n@bp.mcp_tool(\n    name=\"semantic_search_staff_letters\",\n    description=\"Semantic search over techwyns.\"\n)\ndef semantic_search_staff_letters(context):\n\n    try:\n        args = context or {}\n\n        result = run_semantic_staff_letters_search(\n            query=args.get(\"query\"),\n            top=args.get(\"top\", 5),\n            skip=args.get(\"skip\", 0),\n        )\n\n        return json.dumps(result)\n\n    except Exception as exc:\n        return json.dumps({\n            \"error\": type(exc).__name__,\n            \"message\": str(exc),\n        })<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The actual implementation can then live in:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>services\/search_service.py<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This separation becomes very useful when you eventually have:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>techwyns Toolbox\n      \u2502\n      \u251c\u2500\u2500 Case tools\n      \u251c\u2500\u2500 Evidence tools\n      \u251c\u2500\u2500 Regulatory tools\n      \u251c\u2500\u2500 Market tools\n      \u251c\u2500\u2500 Document tools\n      \u251c\u2500\u2500 Search tools\n      \u2514\u2500\u2500 Communication tools<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">3. Create the SQL synchronization blueprint<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Create:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>blueprints\/sql_sync_blueprint.py<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import azure.functions as func\n\nfrom services.sql_sync_service import run_sql_to_search_sync\n\nbp = func.Blueprint()\n\n\n@bp.timer_trigger(\n    schedule=\"%SQL_SYNC_SCHEDULE%\",\n    arg_name=\"timer\",\n    run_on_startup=False,\n    use_monitor=True,\n)\ndef sync_sql_to_ai_search(timer: func.TimerRequest):\n\n    result = run_sql_to_search_sync(\n        dry_run=False\n    )\n\n    if result&#91;\"sources_failed\"]:\n        raise RuntimeError(\n            f\"{result&#91;'sources_failed']} SQL sources failed\"\n        )<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Now your SQL synchronization is independent from MCP.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">4. Create the health blueprint<\/h1>\n\n\n\n<pre class=\"wp-block-code\"><code>blueprints\/health_blueprint.py<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>import json\nimport azure.functions as func\n\nbp = func.Blueprint()\n\n\n@bp.route(\n    route=\"health\",\n    methods=&#91;\"GET\"],\n    auth_level=func.AuthLevel.ANONYMOUS,\n)\ndef health(req: func.HttpRequest):\n\n    return func.HttpResponse(\n        json.dumps({\n            \"status\": \"healthy\",\n            \"service\": \"techeyns-ai-function-app\"\n        }),\n        mimetype=\"application\/json\",\n    )<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then register it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>app.register_functions(health_bp)<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">5. Move services into <code>services\/<\/code><\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">This is where I recommend you eventually move most of the large code from the downloadable <code>function_app.py<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>services\/\n\u251c\u2500\u2500 search_service.py\n\u251c\u2500\u2500 sql_service.py\n\u251c\u2500\u2500 sql_sync_service.py\n\u251c\u2500\u2500 blob_service.py\n\u251c\u2500\u2500 pdf_service.py\n\u251c\u2500\u2500 identity_service.py\n\u2514\u2500\u2500 telemetry_service.py<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">So:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>MCP\n \u2502\n \u25bc\nsearch_service.py\n \u2502\n \u25bc\nAzure AI Search<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">and:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SQL Timer\n \u2502\n \u25bc\nsql_sync_service.py\n \u2502\n \u251c\u2500\u2500 sql_service.py\n \u2502       \u2502\n \u2502       \u25bc\n \u2502    Azure SQL\n \u2502\n \u2514\u2500\u2500 search_service.py\n         \u2502\n         \u25bc\n    Azure AI Search<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This is a much better long-term structure for your platform.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">6. <code>requirements.txt<\/code><\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">At the root:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>azure-functions\nazure-identity\nazure-search-documents\nazure-storage-blob\nazure-core\npypdf\npyodbc<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">If you&#8217;re using additional MCP packages elsewhere in your implementation, add the appropriate MCP SDK package as well.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The important point is that <code>requirements.txt<\/code> belongs at the <strong>root of the Function App project<\/strong>. Azure Functions uses it to install Python dependencies during deployment\/remote build.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">7. <code>host.json<\/code><\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">At the root:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"version\": \"2.0\",\n  \"logging\": {\n    \"applicationInsights\": {\n      \"samplingSettings\": {\n        \"isEnabled\": true\n      }\n    }\n  },\n  \"extensionBundle\": {\n    \"id\": \"Microsoft.Azure.Functions.ExtensionBundle\",\n    \"version\": \"&#91;4.*, 5.0.0)\"\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">So Azure sees:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>techwyns-ai-function-app\/\n\u2502\n\u251c\u2500\u2500 host.json                 \u2190 ROOT\n\u251c\u2500\u2500 function_app.py           \u2190 ROOT\n\u251c\u2500\u2500 requirements.txt          \u2190 ROOT\n\u2502\n\u251c\u2500\u2500 blueprints\/\n\u251c\u2500\u2500 services\/\n\u251c\u2500\u2500 models\/\n\u2514\u2500\u2500 shared\/<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The deployment package must have <code>host.json<\/code> at the root; don&#8217;t zip the parent directory around your project.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">8. Configure <code>.funcignore<\/code><\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Create:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>.venv\/\nvenv\/\n__pycache__\/\n.pytest_cache\/\n.git\/\n.github\/\n.vscode\/\ntests\/\nlocal.settings.json\n*.pyc\n*.pyo<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Do <strong>not<\/strong> exclude:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>function_app.py\nblueprints\/\nservices\/\nmodels\/\nshared\/\nrequirements.txt\nhost.json<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">9. Configure local settings<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">For local testing:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"IsEncrypted\": false,\n  \"Values\": {\n    \"AzureWebJobsStorage\": \"UseDevelopmentStorage=true\",\n    \"FUNCTIONS_WORKER_RUNTIME\": \"python\",\n\n    \"AZURE_SEARCH_ENDPOINT\": \"https:\/\/&lt;search&gt;.search.windows.net\",\n    \"AZURE_SEARCH_INDEX_NAME\": \"csl-metadata\",\n    \"AZURE_SEARCH_SEMANTIC_INDEX_NAME\": \"staff-letters-new\",\n    \"AZURE_SEARCH_SEMANTIC_CONFIGURATION\": \"default\",\n\n    \"AZURE_SQL_SERVER\": \"&lt;server&gt;.database.windows.net\",\n    \"AZURE_SQL_DATABASE\": \"&lt;database&gt;\",\n    \"AZURE_SQL_ODBC_DRIVER\": \"ODBC Driver 18 for SQL Server\",\n\n    \"STAFF_LETTERS_ACCOUNT_URL\": \"https:\/\/&lt;storage&gt;.blob.core.windows.net\",\n    \"STAFF_LETTERS_CONTAINER\": \"staff-letters\",\n\n    \"SQL_SYNC_SCHEDULE\": \"0 *\/15 * * * *\",\n\n    \"SQL_SYNC_BATCH_SIZE\": \"500\",\n    \"SQL_SYNC_SEARCH_RETRIES\": \"3\",\n    \"SQL_QUERY_TIMEOUT_SECONDS\": \"120\",\n    \"SQL_CONNECTION_TIMEOUT_SECONDS\": \"30\",\n\n    \"SQL_SYNC_SOURCES_JSON\": \"&#91;]\"\n  }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Do not deploy <code>local.settings.json<\/code> to Azure.<\/strong> Microsoft explicitly treats it as a local-development configuration file; Azure application settings should be configured separately.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">10. Configure your multiple SQL tables\/views<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Your Function App can synchronize many sources.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;\n  {\n    \"name\": \"cases\",\n    \"query\": \"SELECT CaseId, Title, Status, UpdatedAt FROM .Cases WHERE UpdatedAt &gt;= ?\",\n    \"index_name\": \"-metadata\",\n    \"id_field\": \"CaseId\",\n    \"search_key_field\": \"id\",\n    \"key_prefix\": \"case:\",\n    \"lookback_minutes\": 30,\n    \"field_map\": {\n      \"CaseId\": \"caseId\",\n      \"Title\": \"title\",\n      \"Status\": \"status\",\n      \"UpdatedAt\": \"updatedAt\"\n    }\n  },\n  {\n    \"name\": \"regulations\",\n    \"query\": \"SELECT RegulationId, Title, Citation, UpdatedAt FROM x.Regulations WHERE UpdatedAt &gt;= ?\",\n    \"index_name\": \"x-metadata\",\n    \"id_field\": \"RegulationId\",\n    \"search_key_field\": \"id\",\n    \"key_prefix\": \"regulation:\",\n    \"lookback_minutes\": 30,\n    \"field_map\": {\n      \"RegulationId\": \"regulationId\",\n      \"Title\": \"title\",\n      \"Citation\": \"citation\",\n      \"UpdatedAt\": \"updatedAt\"\n    }\n  }\n]<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This gives you:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>                    Azure SQL\n                       \u2502\n          \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n          \u25bc            \u25bc            \u25bc\n       Cases       Regulations    Evidence\n          \u2502            \u2502            \u2502\n          \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                       \u25bc\n               SQL Sync Service\n                       \u2502\n                       \u25bc\n                Azure AI Search\n                       \u2502\n                       \u25bc\n                 MCP\n                       \u2502\n                       \u25bc\n             Foundry Agent Service<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">11. Deploy using Azure Functions Core Tools<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">From the project root:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cd x-ai-function-app<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Login:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>az login<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>func azure functionapp publish &lt;FUNCTION_APP_NAME&gt;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This deploys the <strong>entire project<\/strong>, not just <code>function_app.py<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That&#8217;s the key point:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">You don&#8217;t deploy each <code>.py<\/code> file individually.<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">You deploy:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>function_app.py\n+\nblueprints\/\n+\nservices\/\n+\nmodels\/\n+\nshared\/\n+\nrequirements.txt\n+\nhost.json<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">as <strong>one Function App deployment package<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Microsoft&#8217;s Python deployment guidance supports this project-based deployment model.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">12. Recommended production deployment<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">For your environment, I&#8217;d use:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>GitHub\n   \u2502\n   \u25bc\nGitHub Actions\n   \u2502\n   \u251c\u2500\u2500 pytest\n   \u251c\u2500\u2500 lint\n   \u251c\u2500\u2500 dependency check\n   \u251c\u2500\u2500 security scan\n   \u251c\u2500\u2500 configuration validation\n   \u2514\u2500\u2500 package\n         \u2502\n         \u25bc\n   Azure Function App\n         \u2502\n         \u251c\u2500\u2500 MCP tools\n         \u251c\u2500\u2500 SQL sync\n         \u251c\u2500\u2500 Health\n         \u251c\u2500\u2500 Admin\n         \u2514\u2500\u2500 supporting services<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Zip deployment is a recommended Azure Functions deployment technology, and the package should contain the application files with <code>host.json<\/code> at its root.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">13. Configure Azure Function App settings<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">In:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Azure Portal \u2192 Function App \u2192 Settings \u2192 Environment variables<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">configure the production values.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">At minimum:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>AZURE_SEARCH_ENDPOINT\nAZURE_SEARCH_INDEX_NAME\nAZURE_SEARCH_SEMANTIC_INDEX_NAME\nAZURE_SEARCH_SEMANTIC_CONFIGURATION\n\nAZURE_SQL_SERVER\nAZURE_SQL_DATABASE\nAZURE_SQL_ODBC_DRIVER\n\nSTAFF_LETTERS_ACCOUNT_URL\nSTAFF_LETTERS_CONTAINER\n\nSQL_SYNC_SCHEDULE\nSQL_SYNC_SOURCES_JSON<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Plus:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SQL_SYNC_BATCH_SIZE\nSQL_SYNC_SEARCH_RETRIES\nSQL_QUERY_TIMEOUT_SECONDS\nSQL_CONNECTION_TIMEOUT_SECONDS<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Application settings are exposed to the Python process as environment variables, which is exactly what the code uses.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">14. Give the Function App Managed Identity access<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">For production, don&#8217;t put:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SQL username\nSQL password\nSearch API key\nStorage account key<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">inside your Python code.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Use the Function App&#8217;s Managed Identity.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Azure SQL<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Create the Function App identity as an Entra user in the database and grant the minimum required read permissions.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Conceptually:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>CREATE USER &#91;x-ai-function] FROM EXTERNAL PROVIDER;\n\nALTER ROLE db_datareader\nADD MEMBER &#91;x-ai-function];<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The Function App then obtains:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>https:&#47;&#47;database.windows.net\/.default<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">and connects using the Entra access token.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">AI Search<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Give the Function App managed identity the appropriate Search data role, normally:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Search Index Data Contributor<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">on the target index\/search service as appropriate.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">15. Test locally before deploying<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">From the project root:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>func start<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You should see functions similar to:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Functions:\n\n    health\n    sql-sync-test\n    sync_sql_to_ai_search\n    semantic_search_staff_letters\n    get_staff_letter\n    search_staff_letters<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then test:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl http:\/\/localhost:7071\/api\/health<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">16. Test SQL without writing to Search<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">This is particularly important.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Use:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/api\/sql-sync-test?dry_run=true<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl \"http:\/\/localhost:7071\/api\/sql-sync-test?dry_run=true&amp;max_rows=10\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That lets you verify:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Azure SQL connectivity\n       \u2193\nquery\n       \u2193\nrows\n       \u2193\nmapping\n       \u2193\ndocument transformation<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">without pushing the documents into AI Search.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Then test one source:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/api\/sql-sync-test?dry_run=true&amp;source=cases&amp;max_rows=10<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">17. Then perform an actual small synchronization<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">After the dry run works:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/api\/sql-sync-test?dry_run=false&amp;source=cases&amp;max_rows=10<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This gives you:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SQL\n \u2193\n10 rows\n \u2193\n10 Search documents\n \u2193\nAI Search<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then verify the index in Azure AI Search.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">18. Finally enable the scheduled synchronization<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Set:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SQL_SYNC_SCHEDULE=0 *\/15 * * * *<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That means approximately every 15 minutes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The timer function is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>@app.timer_trigger(\n    schedule=\"%SQL_SYNC_SCHEDULE%\",\n    arg_name=\"timer\",\n    run_on_startup=False,\n    use_monitor=True,\n)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">I intentionally kept:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>run_on_startup=False<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">for production so a Function App restart doesn&#8217;t unexpectedly trigger a synchronization. Azure&#8217;s Python Functions timer model supports scheduled triggers and configuration through application settings.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Recommended production structure<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Once we modularize the downloadable file, I would ultimately make your Function App look like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>-AI Function App\n\u2502\n\u251c\u2500\u2500 function_app.py\n\u2502\n\u251c\u2500\u2500 blueprints\/\n\u2502   \u251c\u2500\u2500 mcp.py\n\u2502   \u251c\u2500\u2500 sql_sync.py\n\u2502   \u251c\u2500\u2500 health.py\n\u2502   \u251c\u2500\u2500 admin.py\n\u2502   \u2514\u2500\u2500 ingestion.py\n\u2502\n\u251c\u2500\u2500 services\/\n\u2502   \u251c\u2500\u2500 search.py\n\u2502   \u251c\u2500\u2500 sql.py\n\u2502   \u251c\u2500\u2500 blob.py\n\u2502   \u251c\u2500\u2500 document_intelligence.py\n\u2502   \u251c\u2500\u2500 cosmos.py\n\u2502   \u251c\u2500\u2500 redis.py\n\u2502   \u251c\u2500\u2500 memory.py\n\u2502   \u251c\u2500\u2500 authorization.py\n\u2502   \u2514\u2500\u2500 telemetry.py\n\u2502\n\u251c\u2500\u2500 mcp\/\n\u2502   \u251c\u2500\u2500 server.py\n\u2502   \u251c\u2500\u2500 tools\/\n\u2502   \u2502   \u251c\u2500\u2500 cases.py\n\u2502   \u2502   \u251c\u2500\u2500 evidence.py\n\u2502   \u2502   \u251c\u2500\u2500 regulatory.py\n\u2502   \u2502   \u251c\u2500\u2500 market.py\n\u2502   \u2502   \u251c\u2500\u2500 documents.py\n\u2502   \u2502   \u2514\u2500\u2500 communications.py\n\u2502   \u2514\u2500\u2500 schemas\/\n\u2502\n\u251c\u2500\u2500 models\/\n\u2502\n\u251c\u2500\u2500 shared\/\n\u2502   \u251c\u2500\u2500 config.py\n\u2502   \u251c\u2500\u2500 security.py\n\u2502   \u251c\u2500\u2500 identity.py\n\u2502   \u251c\u2500\u2500 logging.py\n\u2502   \u2514\u2500\u2500 serialization.py\n\u2502\n\u251c\u2500\u2500 tests\/\n\u2502\n\u251c\u2500\u2500 host.json\n\u251c\u2500\u2500 requirements.txt\n\u2514\u2500\u2500 .funcignore<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That gives you <strong>one deployable Azure Function App<\/strong>, while still allowing dozens of functions, MCP tools, services and supporting modules.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is also the right direction for the larger <strong> Toolbox \u2192 Orchestrator \u2192 Foundry Agent Service<\/strong> architecture rather than putting everything into a 1,000+ line <code>function_app.py<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>One important note:<\/strong> the downloadable file is the consolidated version you asked for. Before production deployment, the <code>SQL_SYNC_SOURCES_JSON<\/code> mappings must match the actual Azure SQL tables\/views and the actual fields in the target AI Search indexes; the code intentionally does <strong>not<\/strong> invent those schemas.<\/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\">I created the complete <code>function_app.py<\/code> as a single downloadable file.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Download the complete final function_app.py<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It includes the existing staff-letter MCP functionality plus:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Azure AI Search semantic search<\/li>\n\n\n\n<li>Standard staff-letter search<\/li>\n\n\n\n<li>Blob\/PDF extraction<\/li>\n\n\n\n<li>MCP tools<\/li>\n\n\n\n<li>Azure SQL \u2192 Azure AI Search synchronization<\/li>\n\n\n\n<li>Multiple SQL tables\/views through <code>SQL_SYNC_SOURCES_JSON<\/code><\/li>\n\n\n\n<li>Managed Identity authentication to Azure SQL<\/li>\n\n\n\n<li>Managed Identity authentication to AI Search<\/li>\n\n\n\n<li>Incremental <code>lookback_minutes<\/code> support<\/li>\n\n\n\n<li>Batch indexing<\/li>\n\n\n\n<li>Retry\/backoff<\/li>\n\n\n\n<li>Partial indexing error handling<\/li>\n\n\n\n<li>Soft-delete support<\/li>\n\n\n\n<li>Scheduled SQL \u2192 Search synchronization<\/li>\n\n\n\n<li><code>\/api\/sql-sync-test<\/code><\/li>\n\n\n\n<li><code>\/api\/health<\/code><\/li>\n\n\n\n<li>Configuration validation<\/li>\n\n\n\n<li>Protection against arbitrary SQL supplied by an agent\/user<\/li>\n\n\n\n<li>Corrected <code>AZURE_SEARCH_INDEX_NAME<\/code> configuration typo<\/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<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>SECTION 2======<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">recommend <strong>Bicep + Flex Consumption + Managed Identity + Application Insights + Log Analytics + secure Storage<\/strong> rather than the legacy Linux Consumption plan. Microsoft currently recommends Flex Consumption for new serverless Function Apps, and Flex supports managed identity and private networking.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One important distinction: <strong>Bicep deploys the Azure infrastructure; your Python project is deployed afterward as the Function App code package.<\/strong> Microsoft documents this as separate infrastructure\/code deployment steps.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Recommended-AI deployment structure<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>-ai\/\n\u2502\n\u251c\u2500\u2500 function_app.py\n\u251c\u2500\u2500 requirements.txt\n\u251c\u2500\u2500 host.json\n\u251c\u2500\u2500 .funcignore\n\u2502\n\u251c\u2500\u2500 blueprints\/\n\u251c\u2500\u2500 services\/\n\u251c\u2500\u2500 models\/\n\u251c\u2500\u2500 shared\/\n\u2514\u2500\u2500 tests\/\n\u2502\n\u251c\u2500\u2500 infra\/\n\u2502   \u251c\u2500\u2500 main.bicep\n\u2502   \u251c\u2500\u2500 parameters\/\n\u2502   \u2502   \u251c\u2500\u2500 dev.bicepparam\n\u2502   \u2502   \u2514\u2500\u2500 prod.bicepparam\n\u2502   \u2514\u2500\u2500 modules\/\n\u2502       \u251c\u2500\u2500 function-app.bicep\n\u2502       \u251c\u2500\u2500 storage.bicep\n\u2502       \u251c\u2500\u2500 monitoring.bicep\n\u2502       \u2514\u2500\u2500 roles.bicep<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For now, here&#8217;s a <strong>single complete <code>main.bicep<\/code><\/strong> you can deploy directly.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\"><code>infra\/main.bicep<\/code><\/h1>\n\n\n\n<pre class=\"wp-block-code\"><code>targetScope = 'resourceGroup'\n\n@description('Azure region for all resources.')\nparam location string = resourceGroup().location\n\n@description('Unique name for the Function App.')\nparam functionAppName string\n\n@description('Globally unique storage account name. 3-24 lowercase letters\/numbers.')\nparam storageAccountName string\n\n@description('Name of the Application Insights resource.')\nparam applicationInsightsName string\n\n@description('Name of the Log Analytics workspace.')\nparam logAnalyticsWorkspaceName string\n\n@description('Name of the Flex Consumption hosting plan.')\nparam hostingPlanName string\n\n@description('Python version used by the Function App.')\n@allowed(&#91;\n  '3.10'\n  '3.11'\n  '3.12'\n  '3.13'\n])\nparam pythonVersion string = '3.11'\n\n@description('Maximum number of Function App instances.')\nparam maximumInstanceCount int = 40\n\n@description('Memory per Flex Consumption instance.')\n@allowed(&#91;\n  2048\n  4096\n  8192\n])\nparam instanceMemoryMB int = 2048\n\n@description('SQL Server hostname, for example x-sql.database.windows.net.')\nparam sqlServerName string\n\n@description('Azure SQL database name.')\nparam sqlDatabaseName string\n\n@description('Azure AI Search endpoint.')\nparam searchEndpoint string\n\n@description('Default Azure AI Search index.')\nparam searchIndexName string = 'csl-metadata'\n\n@description('Semantic staff-letter Azure AI Search index.')\nparam semanticSearchIndexName string = 'staff-letters-new'\n\n@description('Azure AI Search semantic configuration.')\nparam semanticSearchConfiguration string = 'default'\n\n@description('Staff letters Blob Storage account URL.')\nparam staffLettersAccountUrl string\n\n@description('Staff letters Blob Storage container.')\nparam staffLettersContainer string = 'staff-letters'\n\n@description('SQL synchronization schedule.')\nparam sqlSyncSchedule string = '0 *\/15 * * * *'\n\n@description('SQL synchronization batch size.')\nparam sqlSyncBatchSize int = 500\n\n@description('SQL synchronization retry count.')\nparam sqlSyncSearchRetries int = 3\n\n@description('SQL query timeout.')\nparam sqlQueryTimeoutSeconds int = 120\n\n@description('SQL connection timeout.')\nparam sqlConnectionTimeoutSeconds int = 30\n\n@description('SQL synchronization source configuration. Keep &#91;] until actual SQL mappings are defined.')\nparam sqlSyncSourcesJson string = '&#91;]'\n\n@description('Whether public network access is enabled. For initial deployment use true; set false after private endpoints\/VNet integration are ready.')\nparam publicNetworkAccess string = 'Enabled'\n\n@description('Optional subnet resource ID for VNet integration. Leave empty for initial deployment.')\nparam virtualNetworkSubnetId string = ''\n\n@description('Tags applied to all resources.')\nparam tags object = {\n  platform: 'x-AI'\n  workload: 'x-ai-function-app'\n  managedBy: 'Bicep'\n  environment: 'dev'\n}\n\n\n\/\/ ============================================================================\n\/\/ COMMON VARIABLES\n\/\/ ============================================================================\n\nvar storageBlobEndpoint = 'https:\/\/${storageAccount.name}.blob.core.windows.net'\n\nvar functionAppSettings = &#91;\n  {\n    name: 'FUNCTIONS_EXTENSION_VERSION'\n    value: '~4'\n  }\n  {\n    name: 'FUNCTIONS_WORKER_RUNTIME'\n    value: 'python'\n  }\n  {\n    name: 'FUNCTIONS_WORKER_RUNTIME_VERSION'\n    value: pythonVersion\n  }\n  {\n    name: 'WEBSITE_RUN_FROM_PACKAGE'\n    value: '1'\n  }\n  {\n    name: 'WEBSITE_ENABLE_SYNC_UPDATE_SITE'\n    value: 'true'\n  }\n  {\n    name: 'AzureWebJobsStorage__accountName'\n    value: storageAccount.name\n  }\n  {\n    name: 'AzureWebJobsStorage__credential'\n    value: 'managedidentity'\n  }\n  {\n    name: 'AzureWebJobsStorage__blobServiceUri'\n    value: storageBlobEndpoint\n  }\n  {\n    name: 'AzureWebJobsStorage__queueServiceUri'\n    value: 'https:\/\/${storageAccount.name}.queue.core.windows.net'\n  }\n  {\n    name: 'AzureWebJobsStorage__tableServiceUri'\n    value: 'https:\/\/${storageAccount.name}.table.core.windows.net'\n  }\n\n  \/\/ --------------------------------------------------------------------------\n  \/\/ Application Insights\n  \/\/ --------------------------------------------------------------------------\n\n  {\n    name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'\n    value: appInsights.properties.ConnectionString\n  }\n\n  {\n    name: 'ApplicationInsightsAgent_EXTENSION_VERSION'\n    value: '~3'\n  }\n\n  \/\/ --------------------------------------------------------------------------\n  \/\/ Azure AI Search\n  \/\/ --------------------------------------------------------------------------\n\n  {\n    name: 'AZURE_SEARCH_ENDPOINT'\n    value: searchEndpoint\n  }\n\n  {\n    name: 'AZURE_SEARCH_INDEX_NAME'\n    value: searchIndexName\n  }\n\n  {\n    name: 'AZURE_SEARCH_SEMANTIC_INDEX_NAME'\n    value: semanticSearchIndexName\n  }\n\n  {\n    name: 'AZURE_SEARCH_SEMANTIC_CONFIGURATION'\n    value: semanticSearchConfiguration\n  }\n\n  \/\/ --------------------------------------------------------------------------\n  \/\/ Azure SQL\n  \/\/ --------------------------------------------------------------------------\n\n  {\n    name: 'AZURE_SQL_SERVER'\n    value: sqlServerName\n  }\n\n  {\n    name: 'AZURE_SQL_DATABASE'\n    value: sqlDatabaseName\n  }\n\n  {\n    name: 'AZURE_SQL_ODBC_DRIVER'\n    value: 'ODBC Driver 18 for SQL Server'\n  }\n\n  \/\/ --------------------------------------------------------------------------\n  \/\/ Staff letters Blob\n  \/\/ --------------------------------------------------------------------------\n\n  {\n    name: 'STAFF_LETTERS_ACCOUNT_URL'\n    value: staffLettersAccountUrl\n  }\n\n  {\n    name: 'STAFF_LETTERS_CONTAINER'\n    value: staffLettersContainer\n  }\n\n  \/\/ --------------------------------------------------------------------------\n  \/\/ SQL -&gt; Search synchronization\n  \/\/ --------------------------------------------------------------------------\n\n  {\n    name: 'SQL_SYNC_SCHEDULE'\n    value: sqlSyncSchedule\n  }\n\n  {\n    name: 'SQL_SYNC_BATCH_SIZE'\n    value: string(sqlSyncBatchSize)\n  }\n\n  {\n    name: 'SQL_SYNC_SEARCH_RETRIES'\n    value: string(sqlSyncSearchRetries)\n  }\n\n  {\n    name: 'SQL_QUERY_TIMEOUT_SECONDS'\n    value: string(sqlQueryTimeoutSeconds)\n  }\n\n  {\n    name: 'SQL_CONNECTION_TIMEOUT_SECONDS'\n    value: string(sqlConnectionTimeoutSeconds)\n  }\n\n  {\n    name: 'SQL_SYNC_SOURCES_JSON'\n    value: sqlSyncSourcesJson\n  }\n\n  \/\/ --------------------------------------------------------------------------\n  \/\/ Search protection\n  \/\/ --------------------------------------------------------------------------\n\n  {\n    name: 'SEMANTIC_SEARCH_MAX_TOP'\n    value: '50'\n  }\n\n  {\n    name: 'STANDARD_SEARCH_MAX_TOP'\n    value: '100'\n  }\n]\n\n\n\/\/ ============================================================================\n\/\/ LOG ANALYTICS\n\/\/ ============================================================================\n\nresource logAnalytics 'Microsoft.OperationalInsights\/workspaces@2023-09-01' = {\n  name: logAnalyticsWorkspaceName\n  location: location\n  tags: tags\n  properties: {\n    sku: {\n      name: 'PerGB2018'\n    }\n    retentionInDays: 30\n    features: {\n      enableLogAccessUsingOnlyResourcePermissions: true\n    }\n  }\n}\n\n\n\/\/ ============================================================================\n\/\/ APPLICATION INSIGHTS\n\/\/ ============================================================================\n\nresource appInsights 'Microsoft.Insights\/components@2020-02-02' = {\n  name: applicationInsightsName\n  location: location\n  tags: tags\n  kind: 'web'\n  properties: {\n    Application_Type: 'web'\n    WorkspaceResourceId: logAnalytics.id\n    publicNetworkAccessForIngestion: publicNetworkAccess\n    publicNetworkAccessForQuery: publicNetworkAccess\n  }\n}\n\n\n\/\/ ============================================================================\n\/\/ FUNCTION STORAGE\n\/\/ ============================================================================\n\nresource storageAccount 'Microsoft.Storage\/storageAccounts@2023-05-01' = {\n  name: storageAccountName\n  location: location\n  tags: tags\n  kind: 'StorageV2'\n  sku: {\n    name: 'Standard_LRS'\n  }\n  properties: {\n    minimumTlsVersion: 'TLS1_2'\n    supportsHttpsTrafficOnly: true\n    allowBlobPublicAccess: false\n    publicNetworkAccess: publicNetworkAccess\n    accessTier: 'Hot'\n    allowSharedKeyAccess: false\n    defaultToOAuthAuthentication: true\n\n    networkAcls: {\n      defaultAction: publicNetworkAccess == 'Enabled'\n        ? 'Allow'\n        : 'Deny'\n      bypass: 'AzureServices'\n    }\n  }\n}\n\n\n\/\/ ============================================================================\n\/\/ STORAGE CONTAINERS\n\/\/ ============================================================================\n\nresource deploymentContainer 'Microsoft.Storage\/storageAccounts\/blobServices\/containers@2023-05-01' = {\n  name: '${storageAccount.name}\/default\/function-releases'\n  properties: {\n    publicAccess: 'None'\n  }\n}\n\n\n\/\/ ============================================================================\n\/\/ FLEX CONSUMPTION PLAN\n\/\/ ============================================================================\n\nresource hostingPlan 'Microsoft.Web\/serverfarms@2024-04-01' = {\n  name: hostingPlanName\n  location: location\n  tags: tags\n  sku: {\n    name: 'FC1'\n    tier: 'FlexConsumption'\n  }\n  kind: 'functionapp'\n  properties: {\n    reserved: true\n  }\n}\n\n\n\/\/ ============================================================================\n\/\/ FUNCTION APP\n\/\/ ============================================================================\n\nresource functionApp 'Microsoft.Web\/sites@2024-11-01' = {\n  name: functionAppName\n  location: location\n  tags: tags\n\n  kind: 'functionapp,linux'\n\n  identity: {\n    type: 'SystemAssigned'\n  }\n\n  properties: {\n    serverFarmId: hostingPlan.id\n\n    httpsOnly: true\n\n    publicNetworkAccess: publicNetworkAccess\n\n    siteConfig: {\n      minTlsVersion: '1.2'\n\n      ftpsState: 'Disabled'\n\n      alwaysOn: false\n\n      appSettings: functionAppSettings\n\n      cors: {\n        allowedOrigins: &#91;\n          'https:\/\/portal.azure.com'\n        ]\n        supportCredentials: false\n      }\n    }\n\n    functionAppConfig: {\n      deployment: {\n        storage: {\n          type: 'blobContainer'\n          value: '${storageBlobEndpoint}\/function-releases'\n          authentication: {\n            type: 'SystemAssignedIdentity'\n          }\n        }\n      }\n\n      runtime: {\n        name: 'python'\n        version: pythonVersion\n      }\n\n      scaleAndConcurrency: {\n        maximumInstanceCount: maximumInstanceCount\n        instanceMemoryMB: instanceMemoryMB\n        alwaysReady: {\n          name: 'alwaysReady'\n          instanceCount: 0\n        }\n        triggers: {\n          http: {\n            perInstanceConcurrency: 16\n          }\n        }\n      }\n\n      siteUpdateStrategy: {\n        type: 'Rolling'\n      }\n    }\n  }\n\n  dependsOn: &#91;\n    deploymentContainer\n    appInsights\n    hostingPlan\n  ]\n}\n\n\n\/\/ ============================================================================\n\/\/ OPTIONAL VNET INTEGRATION\n\/\/ ============================================================================\n\nresource virtualNetworkIntegration 'Microsoft.Web\/sites\/virtualNetworkConnections@2024-11-01' = if (!empty(virtualNetworkSubnetId)) {\n  name: '${functionApp.name}\/virtualNetwork'\n  properties: {\n    subnetResourceId: virtualNetworkSubnetId\n  }\n}\n\n\n\/\/ ============================================================================\n\/\/ OUTPUTS\n\/\/ ============================================================================\n\noutput functionAppName string = functionApp.name\n\noutput functionAppResourceId string = functionApp.id\n\noutput functionAppPrincipalId string = functionApp.identity.principalId\n\noutput functionAppHostname string = functionApp.properties.defaultHostName\n\noutput functionAppUrl string = 'https:\/\/${functionApp.properties.defaultHostName}'\n\noutput applicationInsightsName string = appInsights.name\n\noutput logAnalyticsWorkspaceName string = logAnalytics.name\n\noutput storageAccountName string = storageAccount.name\n\noutput hostingPlanName string = hostingPlan.name\n\noutput deploymentContainerName string = deploymentContainer.name<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This follows Microsoft&#8217;s current Flex Consumption infrastructure model, including the <code>FC1<\/code> plan, deployment storage, Function App configuration, managed identity, and monitoring resources.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">2. Create <code>prod.bicepparam<\/code><\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">I recommend <strong>not putting all production values directly into <code>main.bicep<\/code><\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Create:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>infra\/parameters\/prod.bicepparam<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>using '..\/main.bicep'\n\nparam location = 'East US'\n\nparam functionAppName = 'x-ai-func-prod'\n\nparam storageAccountName = 'aifuncprod01'\n\nparam applicationInsightsName = 'x-ai-appins-prod'\n\nparam logAnalyticsWorkspaceName = 'x-ai-law-prod'\n\nparam hostingPlanName = 'x-ai-flex-prod'\n\nparam pythonVersion = '3.11'\n\nparam maximumInstanceCount = 40\n\nparam instanceMemoryMB = 4096\n\nparam sqlServerName = 'x-sql-prod.database.windows.net'\n\nparam sqlDatabaseName = 'x'\n\nparam searchEndpoint = 'https:\/\/&lt;YOUR-SEARCH-SERVICE&gt;.search.windows.net'\n\nparam searchIndexName = 'csl-metadata'\n\nparam semanticSearchIndexName = 'staff-letters-new'\n\nparam semanticSearchConfiguration = 'default'\n\nparam staffLettersAccountUrl = 'https:\/\/&lt;YOUR-STORAGE&gt;.blob.core.windows.net'\n\nparam staffLettersContainer = 'staff-letters'\n\nparam sqlSyncSchedule = '0 *\/15 * * * *'\n\nparam sqlSyncBatchSize = 500\n\nparam sqlSyncSearchRetries = 3\n\nparam sqlQueryTimeoutSeconds = 120\n\nparam sqlConnectionTimeoutSeconds = 30\n\nparam sqlSyncSourcesJson = '&#91;]'\n\nparam publicNetworkAccess = 'Enabled'\n\nparam virtualNetworkSubnetId = ''\n\nparam tags = {\n  platform: 'x-AI'\n  environment: 'prod'\n  owner: 'x'\n  workload: 'AI-Platform'\n  managedBy: 'Bicep'\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For the initial deployment, I would leave:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>publicNetworkAccess = 'Enabled'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">and then move the Function App\/Storage\/Application Insights\/Search\/SQL architecture toward your <strong>private-endpoint \/ zero-trust CC topology<\/strong> once the VNet\/subnets\/private DNS are ready.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">3. Validate the Bicep<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">From the repository root:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>az login<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>az account set --subscription \"&lt;SUBSCRIPTION_ID&gt;\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Validate:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>az bicep build \\\n  --file infra\/main.bicep<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Then run a resource-group what-if:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>az deployment group what-if \\\n  --resource-group \"&lt;RESOURCE_GROUP&gt;\" \\\n  --template-file infra\/main.bicep \\\n  --parameters @infra\/parameters\/prod.bicepparam<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">I strongly recommend using <code>what-if<\/code> before production deployment.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">4. Deploy the infrastructure<\/h1>\n\n\n\n<pre class=\"wp-block-code\"><code>az deployment group create \\\n  --resource-group \"&lt;RESOURCE_GROUP&gt;\" \\\n  --template-file infra\/main.bicep \\\n  --parameters @infra\/parameters\/prod.bicepparam<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This creates:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>x-AI Resource Group\n\u2502\n\u251c\u2500\u2500 Function App\n\u2502\n\u251c\u2500\u2500 Flex Consumption Plan\n\u2502\n\u251c\u2500\u2500 Storage Account\n\u2502   \u2514\u2500\u2500 function-releases\n\u2502\n\u251c\u2500\u2500 Application Insights\n\u2502\n\u2514\u2500\u2500 Log Analytics<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Microsoft&#8217;s current Bicep guidance follows this same infrastructure-first approach.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">5. Get the Function App Managed Identity<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">After deployment:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>az functionapp identity show \\\n  --name x-ai-func-prod \\\n  --resource-group \"&lt;RESOURCE_GROUP&gt;\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You&#8217;ll get something like:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"principalId\": \"...\",\n  \"tenantId\": \"...\",\n  \"type\": \"SystemAssigned\"\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The important value is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>principalId<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That is the identity your Python code uses through:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>DefaultAzureCredential()<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">6. Give the Function App access to Azure AI Search<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Your Function App needs appropriate data-plane permissions on your Search service.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For your SQL \u2192 Search pipeline, use:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Search Index Data Contributor<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For read-only MCP search consumers, use:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Search Index Data Reader<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The architecture becomes:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>                 Entra ID\n                    \u2502\n                    \u25bc\n          Function App\n          Managed Identity\n             \u2502          \u2502\n             \u2502          \u2502\n             \u25bc          \u25bc\n       Azure SQL     AI Search\n       db_datareader  Data Contributor<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">No Search API key needs to be embedded in <code>function_app.py<\/code>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">7. Give the Function App access to Azure SQL<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">In Azure SQL:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>CREATE USER &#91;x-ai-func-prod] FROM EXTERNAL PROVIDER;\n\nALTER ROLE db_datareader\nADD MEMBER &#91;x-ai-func-prod];<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For the SQL \u2192 Search application I recommend <strong>read-only SQL permissions<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Your Python code already requests the Azure SQL token using:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>credential.get_token(\n    \"https:\/\/database.windows.net\/.default\"\n)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">So the authentication chain becomes:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Function App\n     \u2502\n     \u2502 Managed Identity\n     \u25bc\nMicrosoft Entra ID\n     \u2502\n     \u2502 access token\n     \u25bc\nAzure SQL<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">8. Give it Blob access<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">For your staff-letter PDFs:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Storage Blob Data Reader<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">should be assigned to the Function App identity on the staff-letter Storage Account\/container.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That lets:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>BlobServiceClient(\n    account_url=...,\n    credential=get_credential()\n)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">work without a storage account key.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">9. Application Insights is already wired<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">The Bicep creates:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Log Analytics\n       \u2502\n       \u25bc\nApplication Insights\n       \u2502\n       \u25bc\nFunction App<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">and injects:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>APPLICATIONINSIGHTS_CONNECTION_STRING<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">into the Function App.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is the foundation for the broader telemetry design we&#8217;ve been building:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Function App\n      \u2502\n      \u251c\u2500\u2500 MCP calls\n      \u251c\u2500\u2500 SQL synchronization\n      \u251c\u2500\u2500 AI Search calls\n      \u251c\u2500\u2500 Blob operations\n      \u251c\u2500\u2500 exceptions\n      \u2514\u2500\u2500 performance\n             \u2502\n             \u25bc\n      Application Insights\n             \u2502\n             \u25bc\n       Log Analytics\n             \u2502\n             \u251c\u2500\u2500 Azure Monitor\n             \u251c\u2500\u2500 Grafana\n             \u2514\u2500\u2500 dashboards<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">10. Deploy your Python code AFTER Bicep<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">This is important.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Bicep creates:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Azure infrastructure<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Your Python deployment creates:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>function_app.py\nblueprints\/\nservices\/\nmodels\/\nshared\/\nrequirements.txt\nhost.json<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For Flex Consumption, Microsoft currently uses <strong>One Deploy<\/strong> as the deployment mechanism.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">From your project root:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>func azure functionapp publish x-ai-func-prod<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That publishes the <strong>entire Function App project<\/strong>, not only <code>function_app.py<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>function_app.py\nservices\/\nblueprints\/\nmodels\/\nshared\/\nrequirements.txt\nhost.json<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">all travel together.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">11. Recommended deployment pipeline<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">For x-AI, I&#8217;d make the deployment:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>                 GitHub\n                    \u2502\n                    \u25bc\n              Pull Request\n                    \u2502\n          \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n          \u25bc                   \u25bc\n       pytest              Bicep lint\n          \u2502                   \u2502\n          \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                    \u25bc\n              Security Scan\n                    \u2502\n                    \u25bc\n              Bicep What-If\n                    \u2502\n                    \u25bc\n            Infrastructure Deploy\n                    \u2502\n                    \u25bc\n              30 sec wait\n                    \u2502\n                    \u25bc\n              Python Deploy\n                    \u2502\n                    \u25bc\n              Smoke Tests\n                    \u2502\n                    \u25bc\n             Health Endpoint\n                    \u2502\n                    \u25bc\n          SQL dry-run validation\n                    \u2502\n                    \u25bc\n        AI Search synchronization<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Microsoft specifically recommends separating infrastructure deployment and code deployment for Function Apps; their current guidance also notes that application-setting changes can cause a restart, so allowing a short interval before code deployment is prudent.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">One change I recommend for your production architecture<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The Bicep above is the <strong>baseline deployment<\/strong>. For the final FedRAMP\/private environment, I would evolve it to:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>                         Network\n                              \u2502\n                 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                 \u2502                         \u2502\n           Private DNS                 NSGs\n                 \u2502                         \u2502\n                 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                              \u2502\n                        VNet \/ Subnets\n                              \u2502\n                  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n                  \u2502                       \u2502\n          Function App                APIM\n          Managed Identity              \u2502\n                  \u2502                     \u2502\n       \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510          \u2502\n       \u25bc          \u25bc          \u25bc          \u25bc\n    Azure SQL  AI Search   Blob     MCP\n       \u2502          \u2502          \u2502          \u2502\n       \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                              \u2502\n                         Foundry Agent\n                           Service\n                              \u2502\n                     Orchestrator<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That is the version I would use for the <strong>production  -AI \/ Foundry MCP architecture<\/strong>, rather than exposing the Function App directly to the public internet.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Also, Microsoft&#8217;s current Flex Consumption model explicitly supports private networking, which makes it a better foundation for that target architecture than the legacy Consumption model.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you want the infrastructure fully productionized, the next logical step is to split this into <strong><code>main.bicep<\/code> + <code>function-app.bicep<\/code> + <code>roles.bicep<\/code> + <code>private-endpoints.bicep<\/code> + <code>monitoring.bicep<\/code> + <code>search.bicep<\/code> + <code>sql.bicep<\/code><\/strong>, with <strong>Entra\/managed-identity RBAC, Private Endpoints, Private DNS, APIM, Key Vault, Redis, Cosmos DB, AI Search, App Insights\/Grafana, and Foundry MCP<\/strong> all deployed from the same-AI IaC repository.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><\/h3>\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>first, to. develop function apps, install the. commandline &#8212; npm install -g azure-functions-core-tools@4 &#8211;unsafe-perm true (windows machine) I would not simply add a SQL query inside the current MCP functions. I would add a separate SQL \u2192 AI Search synchronization pipeline inside the same Function App. That gives you: Azure AI Search&#8217;s Python SDK supports [&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-78","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\/78","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=78"}],"version-history":[{"count":6,"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/posts\/78\/revisions"}],"predecessor-version":[{"id":88,"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/posts\/78\/revisions\/88"}],"wp:attachment":[{"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/media?parent=78"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/categories?post=78"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/tags?post=78"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}