<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: René Nijkamp</title>
    <description>The latest articles on DEV Community by René Nijkamp (@itsrene).</description>
    <link>https://dev.to/itsrene</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3605416%2Fb52647a5-3d89-4ae3-8575-111ce62170a0.jpg</url>
      <title>DEV Community: René Nijkamp</title>
      <link>https://dev.to/itsrene</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/itsrene"/>
    <language>en</language>
    <item>
      <title>Azure APIM MCP Automation: The Bridging Problem</title>
      <dc:creator>René Nijkamp</dc:creator>
      <pubDate>Tue, 07 Jul 2026 06:03:50 +0000</pubDate>
      <link>https://dev.to/itsrene/azure-apim-mcp-automation-the-bridging-problem-57hk</link>
      <guid>https://dev.to/itsrene/azure-apim-mcp-automation-the-bridging-problem-57hk</guid>
      <description>&lt;h1&gt;
  
  
  Azure APIM MCP Automation: The Bridging Problem
&lt;/h1&gt;

&lt;p&gt;In &lt;a href="https://itsrene.nl/blog/rendered/azure-apim-mcp-logging.html" rel="noopener noreferrer"&gt;Part 3&lt;/a&gt;, we tackled observability. Now let's talk about the &lt;strong&gt;automation elephant in the room&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Azure APIM MCP is in preview. And here's what that means for Infrastructure as Code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No ARM template support&lt;/li&gt;
&lt;li&gt;No Bicep support&lt;/li&gt;
&lt;li&gt;No Terraform provider support&lt;/li&gt;
&lt;li&gt;No Azure Service Operator (ASO) support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Here's the reality:&lt;/strong&gt; You can't use standard IaC tooling. You're either clicking through the Azure Portal manually, or you're building custom automation.&lt;/p&gt;

&lt;p&gt;But before you rush to build REST API scripts, let's talk about &lt;strong&gt;why this problem matters&lt;/strong&gt; and what it means for your architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Is Different
&lt;/h2&gt;

&lt;h3&gt;
  
  
  It's Not Just About Automation
&lt;/h3&gt;

&lt;p&gt;Most IaC gaps are annoying but straightforward: write some REST API calls, wrap them in a script, deploy via pipeline. Done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP is different.&lt;/strong&gt; And here's why:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI agents read your API descriptions.&lt;/strong&gt; Those descriptions become tool definitions that LLMs use to decide when and how to invoke your APIs. A poorly written description? The agent misunderstands. A misleading summary? The agent calls the wrong endpoint. A missing security note? The agent leaks data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is a governance problem disguised as an automation problem.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Real Risks
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEIKICAgIEFbRGV2ZWxvcGVyIHdyaXRlcyBPcGVuQVBJXSAtLT58Tm8gcmV2aWV3fCBCW0RlcGxveSB0byBBUElNXQogICAgQiAtLT58QXV0by1jb252ZXJ0fCBDW01DUCBTZXJ2ZXJdCiAgICBDIC0tPnxFeHBvc2V8IERbQUkgQWdlbnQgcmVhZHMgZGVzY3JpcHRpb25zXQogICAgRCAtLT58TWlzdW5kZXJzdGFuZHN8IEVbV3JvbmcgQVBJIGNhbGxzXQogICAgRSAtLT58Q2F1c2VzfCBGW0RhdGEgbGVha3MgLyBCdXNpbmVzcyBsb2dpYyBlcnJvcnNdCiAgICAKICAgIHN0eWxlIEYgZmlsbDojZGMzNTQ1LGNvbG9yOiNmZmYKICAgIHN0eWxlIEEgZmlsbDojZmZjMTA3LGNvbG9yOiMwMDA%3D%3Ftype%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEIKICAgIEFbRGV2ZWxvcGVyIHdyaXRlcyBPcGVuQVBJXSAtLT58Tm8gcmV2aWV3fCBCW0RlcGxveSB0byBBUElNXQogICAgQiAtLT58QXV0by1jb252ZXJ0fCBDW01DUCBTZXJ2ZXJdCiAgICBDIC0tPnxFeHBvc2V8IERbQUkgQWdlbnQgcmVhZHMgZGVzY3JpcHRpb25zXQogICAgRCAtLT58TWlzdW5kZXJzdGFuZHN8IEVbV3JvbmcgQVBJIGNhbGxzXQogICAgRSAtLT58Q2F1c2VzfCBGW0RhdGEgbGVha3MgLyBCdXNpbmVzcyBsb2dpYyBlcnJvcnNdCiAgICAKICAgIHN0eWxlIEYgZmlsbDojZGMzNTQ1LGNvbG9yOiNmZmYKICAgIHN0eWxlIEEgZmlsbDojZmZjMTA3LGNvbG9yOiMwMDA%3D%3Ftype%3Dpng" alt="Diagram" width="276" height="734"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Without proper governance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No review of tool descriptions&lt;/strong&gt; before AI agents see them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No validation of API quality&lt;/strong&gt; before MCP exposure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No control over which operations&lt;/strong&gt; become agent-accessible tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No audit trail&lt;/strong&gt; of what got exposed and when&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't Terraform failing to deploy a VM. This is &lt;strong&gt;giving AI agents access to your business logic without proper oversight.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Governance-First Approach
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What We Need
&lt;/h3&gt;

&lt;p&gt;Before worrying about automation tooling, we need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Source of truth for MCP exposure&lt;/strong&gt; (which APIs become tools?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review gates for tool quality&lt;/strong&gt; (are descriptions agent-friendly?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approval workflows for changes&lt;/strong&gt; (who decides what gets exposed?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit trail for MCP operations&lt;/strong&gt; (what changed and when?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback capability&lt;/strong&gt; (can we undo a bad exposure?)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;This is GitOps territory.&lt;/strong&gt; Not because of Kubernetes or Helm charts, but because we need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Version control for configuration&lt;/li&gt;
&lt;li&gt;Pull request reviews before changes&lt;/li&gt;
&lt;li&gt;Approval gates for production&lt;/li&gt;
&lt;li&gt;History of who changed what&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  OpenAPI as Configuration
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Here's the pattern:&lt;/strong&gt; OpenAPI specifications become your configuration files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;/users/{userId}/profile&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;operationId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;getUserProfile&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retrieve&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;profile&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;information"&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Get&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;comprehensive&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;profile&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;authenticated&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;including&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;preferences,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;settings,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;basic&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;account&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;information.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;This&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;respects&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;privacy&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;only&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;returns&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;has&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;permission&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;access."&lt;/span&gt;
      &lt;span class="na"&gt;x-mcp-enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;  &lt;span class="c1"&gt;# Governance flag&lt;/span&gt;
      &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;userId&lt;/span&gt;
          &lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;path&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;identifier&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;must&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;match&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;authenticated&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;user"&lt;/span&gt;
          &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;x-mcp-enabled: true&lt;/code&gt; flag is &lt;strong&gt;governance metadata&lt;/strong&gt;, not just a technical toggle. It says:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Someone reviewed this operation's description&lt;/li&gt;
&lt;li&gt;Someone decided AI agents should have access&lt;/li&gt;
&lt;li&gt;Someone validated the security model&lt;/li&gt;
&lt;li&gt;Someone approved this for MCP exposure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;No flag = no MCP exposure.&lt;/strong&gt; Simple as that.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conceptual Bridging Approaches
&lt;/h2&gt;

&lt;p&gt;Let's talk about &lt;strong&gt;how&lt;/strong&gt; to bridge the IaC gap until ASO catches up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full disclosure:&lt;/strong&gt; These are patterns we're exploring, not battle-tested solutions. They're based on months of working with APIM MCP and years of GitOps experience, but consider them &lt;strong&gt;architectural ideas&lt;/strong&gt; rather than production-ready implementations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 1: Centralized Automation Repository
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Concept:&lt;/strong&gt; Single repository that polls APIM instances, detects APIs with &lt;code&gt;x-mcp-enabled&lt;/code&gt; flags, and applies MCP overlays.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggTFIKICAgIEFbT3BlbkFQSSBTcGVjcyBpbiBHaXRdIC0tPnxQdXNofCBCW1N0YW5kYXJkIEFQSU0gRGVwbG95bWVudF0KICAgIEIgLS0%2BfFByb3Zpc2lvbnN8IENbSFRUUCBBUElzIGluIEFQSU1dCiAgICAKICAgIERbTUNQIE92ZXJsYXkgQXV0b21hdGlvbl0gLS0%2BfFNjYW5zfCBDCiAgICBEIC0tPnxEZXRlY3RzIHgtbWNwLWVuYWJsZWR8IEVbQ29udmVydCB0byBNQ1BdCiAgICBFIC0tPnxBcHBseXwgRltNQ1AgU2VydmVyIGluIEFQSU1dCiAgICAKICAgIHN0eWxlIEQgZmlsbDojMDA3OEQ0LGNvbG9yOiNmZmYKICAgIHN0eWxlIEYgZmlsbDojMTBCOTgxLGNvbG9yOiNmZmY%3D%3Ftype%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggTFIKICAgIEFbT3BlbkFQSSBTcGVjcyBpbiBHaXRdIC0tPnxQdXNofCBCW1N0YW5kYXJkIEFQSU0gRGVwbG95bWVudF0KICAgIEIgLS0%2BfFByb3Zpc2lvbnN8IENbSFRUUCBBUElzIGluIEFQSU1dCiAgICAKICAgIERbTUNQIE92ZXJsYXkgQXV0b21hdGlvbl0gLS0%2BfFNjYW5zfCBDCiAgICBEIC0tPnxEZXRlY3RzIHgtbWNwLWVuYWJsZWR8IEVbQ29udmVydCB0byBNQ1BdCiAgICBFIC0tPnxBcHBseXwgRltNQ1AgU2VydmVyIGluIEFQSU1dCiAgICAKICAgIHN0eWxlIEQgZmlsbDojMDA3OEQ0LGNvbG9yOiNmZmYKICAgIHN0eWxlIEYgZmlsbDojMTBCOTgxLGNvbG9yOiNmZmY%3D%3Ftype%3Dpng" alt="Diagram" width="1285" height="218"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Characteristics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Separation of concerns&lt;/strong&gt;: Standard API deployment unchanged&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-invasive&lt;/strong&gt;: Overlay runs externally, doesn't touch existing workflows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy removal&lt;/strong&gt;: When ASO adds MCP support, delete the overlay repo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governance preserved&lt;/strong&gt;: Only APIs with &lt;code&gt;x-mcp-enabled&lt;/code&gt; get converted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Eventual consistency (polling-based, not real-time)&lt;/li&gt;
&lt;li&gt;Requires cross-subscription credentials for multi-tenant scenarios&lt;/li&gt;
&lt;li&gt;Centralized monitoring but distributed execution&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pattern 2: Event-Driven Kubernetes Jobs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Concept:&lt;/strong&gt; ArgoCD PostSync hooks that detect MCP-enabled APIs after deployment and apply conversion.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEIKICAgIEFbT3BlbkFQSSBTcGVjXSAtLT58TWF2ZW4vSGVsbXwgQltBU08gQ1JEXQogICAgQiAtLT58QXJnb0NEIGRlcGxveXN8IENbSFRUUCBBUEkgaW4gQVBJTV0KICAgIEMgLS0%2BfFBvc3RTeW5jIEhvb2sgdHJpZ2dlcnN8IERbS3ViZXJuZXRlcyBKb2JdCiAgICBEIC0tPnxDaGVja3MgeC1tY3AtZW5hYmxlZHwgRXtGbGFnIHByZXNlbnQ%2FfQogICAgRSAtLT58WWVzfCBGW0FwcGx5IE1DUCBvdmVybGF5IHZpYSBSRVNUIEFQSV0KICAgIEUgLS0%2BfE5vfCBHW1NraXAgY29udmVyc2lvbl0KICAgIEYgLS0%2BIEhbTUNQIFNlcnZlciByZWFkeV0KICAgIAogICAgc3R5bGUgRCBmaWxsOiNGNTlFMEIsY29sb3I6IzAwMAogICAgc3R5bGUgSCBmaWxsOiMxMEI5ODEsY29sb3I6I2ZmZg%3D%3D%3Ftype%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEIKICAgIEFbT3BlbkFQSSBTcGVjXSAtLT58TWF2ZW4vSGVsbXwgQltBU08gQ1JEXQogICAgQiAtLT58QXJnb0NEIGRlcGxveXN8IENbSFRUUCBBUEkgaW4gQVBJTV0KICAgIEMgLS0%2BfFBvc3RTeW5jIEhvb2sgdHJpZ2dlcnN8IERbS3ViZXJuZXRlcyBKb2JdCiAgICBEIC0tPnxDaGVja3MgeC1tY3AtZW5hYmxlZHwgRXtGbGFnIHByZXNlbnQ%2FfQogICAgRSAtLT58WWVzfCBGW0FwcGx5IE1DUCBvdmVybGF5IHZpYSBSRVNUIEFQSV0KICAgIEUgLS0%2BfE5vfCBHW1NraXAgY29udmVyc2lvbl0KICAgIEYgLS0%2BIEhbTUNQIFNlcnZlciByZWFkeV0KICAgIAogICAgc3R5bGUgRCBmaWxsOiNGNTlFMEIsY29sb3I6IzAwMAogICAgc3R5bGUgSCBmaWxsOiMxMEI5ODEsY29sb3I6I2ZmZg%3D%3D%3Ftype%3Dpng" alt="Diagram" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Characteristics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Event-driven&lt;/strong&gt;: Conversion happens immediately after API deployment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes-native&lt;/strong&gt;: Fits existing ArgoCD workflows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-cluster isolated&lt;/strong&gt;: Each cluster handles its own APIM instance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Embeds workaround in Helm charts (harder to remove cleanly)&lt;/li&gt;
&lt;li&gt;Requires RBAC setup for Kubernetes service accounts&lt;/li&gt;
&lt;li&gt;Debugging in pod logs vs centralized logging&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pattern 3: Build-Time Generation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Concept:&lt;/strong&gt; Maven plugin or build tool generates two outputs: standard API spec + MCP overlay config.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEIKICAgIEFbT3BlbkFQSSBTcGVjXSAtLT58TWF2ZW4gUGx1Z2lufCBCe0hhcyB4LW1jcC1lbmFibGVkP30KICAgIEIgLS0%2BfFllc3wgQ1tHZW5lcmF0ZSBBU08gQ1JEICsgTUNQIE92ZXJsYXldCiAgICBCIC0tPnxOb3wgRFtHZW5lcmF0ZSBBU08gQ1JEIG9ubHldCiAgICBDIC0tPiBFW0RlcGxveSBib3RoIHZpYSBBcmdvQ0RdCiAgICBEIC0tPiBGW0RlcGxveSBBUEkgb25seV0KICAgIEUgLS0%2BIEdbTUNQIFNlcnZlciArIEhUVFAgQVBJXQogICAgRiAtLT4gSFtIVFRQIEFQSSBvbmx5XQogICAgCiAgICBzdHlsZSBDIGZpbGw6IzEwQjk4MSxjb2xvcjojZmZm%3Ftype%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEIKICAgIEFbT3BlbkFQSSBTcGVjXSAtLT58TWF2ZW4gUGx1Z2lufCBCe0hhcyB4LW1jcC1lbmFibGVkP30KICAgIEIgLS0%2BfFllc3wgQ1tHZW5lcmF0ZSBBU08gQ1JEICsgTUNQIE92ZXJsYXldCiAgICBCIC0tPnxOb3wgRFtHZW5lcmF0ZSBBU08gQ1JEIG9ubHldCiAgICBDIC0tPiBFW0RlcGxveSBib3RoIHZpYSBBcmdvQ0RdCiAgICBEIC0tPiBGW0RlcGxveSBBUEkgb25seV0KICAgIEUgLS0%2BIEdbTUNQIFNlcnZlciArIEhUVFAgQVBJXQogICAgRiAtLT4gSFtIVFRQIEFQSSBvbmx5XQogICAgCiAgICBzdHlsZSBDIGZpbGw6IzEwQjk4MSxjb2xvcjojZmZm%3Ftype%3Dpng" alt="Diagram" width="563" height="706"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Characteristics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero operational overhead&lt;/strong&gt;: Automatic for all builds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fits existing workflows&lt;/strong&gt;: Maven → Helm → ArgoCD&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Centralized logic&lt;/strong&gt;: One plugin, all APIs benefit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requires modifying build tooling (parent POM, plugins)&lt;/li&gt;
&lt;li&gt;Longer development time upfront&lt;/li&gt;
&lt;li&gt;Still needs Azure credentials in Kubernetes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What These Patterns Have in Common
&lt;/h3&gt;

&lt;p&gt;All three approaches share critical principles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;OpenAPI spec is single source of truth&lt;/strong&gt; (no config duplication)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard API deployment unchanged&lt;/strong&gt; (MCP is an overlay, not replacement)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governed by flags&lt;/strong&gt; (explicit opt-in via &lt;code&gt;x-mcp-enabled&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy to remove&lt;/strong&gt; (when ASO adds MCP support)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support full CRUD&lt;/strong&gt; (create, update, delete MCP exposure)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Which one is right?&lt;/strong&gt; Depends on your organization:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Centralized control + multi-tenant? Pattern 1&lt;/li&gt;
&lt;li&gt;Kubernetes-native + per-cluster isolation? Pattern 2
&lt;/li&gt;
&lt;li&gt;Minimal operational overhead? Pattern 3&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Governance Framework
&lt;/h2&gt;

&lt;p&gt;Let's break down what &lt;strong&gt;governance&lt;/strong&gt; actually means for MCP exposure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Review Workflow
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Typical approval flow:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developer → OpenAPI with x-mcp-enabled → Pull Request → 
Domain Review → Security Review → Principal Architect → 
Merge → Automated Deployment → MCP Server Live
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What gets reviewed:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Aspect&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Review Focus&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Risk if Skipped&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Description Quality&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Is it clear for AI agents?&lt;/td&gt;
&lt;td&gt;Misunderstood intent, wrong API calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Auth requirements documented?&lt;/td&gt;
&lt;td&gt;Unauthorized access, data leaks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Exposure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sensitive data clearly marked?&lt;/td&gt;
&lt;td&gt;Privacy violations, compliance gaps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Operation Safety&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Read-only vs write operations?&lt;/td&gt;
&lt;td&gt;Accidental data modification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rate Limiting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Appropriate throttling?&lt;/td&gt;
&lt;td&gt;Resource exhaustion, DoS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Tool Quality Standards
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Bare minimum for MCP-enabled operations:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# BAD: Too vague for AI agents&lt;/span&gt;
&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Get&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;user"&lt;/span&gt;
  &lt;span class="na"&gt;x-mcp-enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="c1"&gt;# GOOD: Clear, actionable, with constraints&lt;/span&gt;
&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retrieve&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;profile&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;information"&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;Get comprehensive profile data for the authenticated user including &lt;/span&gt;
    &lt;span class="s"&gt;preferences, settings, and basic account information.&lt;/span&gt;

    &lt;span class="s"&gt;**Authentication required**: User can only access their own profile.&lt;/span&gt;
    &lt;span class="s"&gt;**Data privacy**: Returns only non-sensitive user data.&lt;/span&gt;
    &lt;span class="s"&gt;**Rate limit**: 100 requests per minute.&lt;/span&gt;
  &lt;span class="na"&gt;x-mcp-enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;BearerAuth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; AI agents rely on descriptions to understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What the tool does&lt;/li&gt;
&lt;li&gt;When to use it&lt;/li&gt;
&lt;li&gt;What data it returns&lt;/li&gt;
&lt;li&gt;What permissions are needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Poor descriptions = poor agent behavior.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  CODEOWNERS for MCP
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Enforce review requirements:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In your Git repository&lt;/span&gt;
/apis/customer-management/   @customer-team @security-team
/apis/payment-processing/    @payment-team @compliance-team @security-team
/apis/public-data/          @platform-team

&lt;span class="c"&gt;# Any file with x-mcp-enabled requires principal architect&lt;/span&gt;
&lt;span class="k"&gt;**&lt;/span&gt;/openapi&lt;span class="k"&gt;*&lt;/span&gt;.yaml            @principal-architect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This enforces:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Domain experts review their APIs&lt;/li&gt;
&lt;li&gt;Security team sees all MCP-enabled operations&lt;/li&gt;
&lt;li&gt;Principal architect has final approval&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Audit Trail
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Git as audit trail:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"commit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"author"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"developer@org.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-01-12T10:30:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"changes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"apis/customer-management/openapi.yaml"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"operation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/users/{userId}/profile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"change"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Added x-mcp-enabled: true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"reviewers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"security-team"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"principal-architect"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"approved_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-01-12T11:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every MCP exposure change lives in Git history. Who changed what, when, and who approved it.&lt;/p&gt;




&lt;h2&gt;
  
  
  API Quality Prerequisites
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Here's an uncomfortable truth:&lt;/strong&gt; You can't sprinkle MCP on top of a shitty API landscape and expect good results.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Makes an API "MCP-Ready"?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Not every API should become an MCP tool.&lt;/strong&gt; Consider:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Characteristic&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;MCP-Ready&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Not MCP-Ready&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Documentation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Clear, complete, agent-friendly&lt;/td&gt;
&lt;td&gt;Sparse, outdated, human-only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Versioned, stable contract&lt;/td&gt;
&lt;td&gt;Frequent breaking changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Error Handling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Structured error responses&lt;/td&gt;
&lt;td&gt;Generic 500s, no details&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Well-defined auth/authz&lt;/td&gt;
&lt;td&gt;Ad-hoc permissions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reliable response times&lt;/td&gt;
&lt;td&gt;Intermittent timeouts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Idempotency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Safe retry behavior&lt;/td&gt;
&lt;td&gt;Side effects on retry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;AI agents are unforgiving consumers.&lt;/strong&gt; They won't debug your API, they won't file tickets, they'll just fail spectacularly.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Functional vs Technical API Problem
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Here's the hill I'm willing to die on:&lt;/strong&gt; AI agents need &lt;strong&gt;functional APIs&lt;/strong&gt;, not &lt;strong&gt;technical APIs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical APIs (what most orgs have):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# CRUD operations, database-centric&lt;/span&gt;
&lt;span class="s"&gt;POST /api/v1/customers&lt;/span&gt;
&lt;span class="s"&gt;GET /api/v1/customers/{id}&lt;/span&gt;
&lt;span class="s"&gt;PUT /api/v1/customers/{id}&lt;/span&gt;
&lt;span class="s"&gt;DELETE /api/v1/customers/{id}&lt;/span&gt;

&lt;span class="s"&gt;POST /api/v1/accounts&lt;/span&gt;
&lt;span class="s"&gt;GET /api/v1/accounts/{id}&lt;/span&gt;
&lt;span class="s"&gt;PUT /api/v1/accounts/{id}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;AI agent sees this:&lt;/strong&gt; "I have create, read, update, delete... but what am I actually &lt;strong&gt;doing&lt;/strong&gt; for the user?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Functional APIs (what agents need):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Business capabilities, intent-driven&lt;/span&gt;
&lt;span class="s"&gt;POST /customers/onboard&lt;/span&gt;
  &lt;span class="s"&gt;- "Onboard a new customer with KYC verification"&lt;/span&gt;

&lt;span class="s"&gt;POST /customers/{id}/verify-identity&lt;/span&gt;
  &lt;span class="s"&gt;- "Verify customer identity using government documents"&lt;/span&gt;

&lt;span class="s"&gt;POST /accounts/open&lt;/span&gt;
  &lt;span class="s"&gt;- "Open a new bank account for verified customer"&lt;/span&gt;

&lt;span class="s"&gt;POST /payments/transfer&lt;/span&gt;
  &lt;span class="s"&gt;- "Transfer money between accounts with fraud checks"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;AI agent sees this:&lt;/strong&gt; "I can onboard customers, verify identities, open accounts, transfer money—I understand the &lt;strong&gt;business intent&lt;/strong&gt;."&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Matters for MCP
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEIKICAgIEFbQUkgQWdlbnQgUXVlcnk6PGJyLz4nVHJhbnNmZXIgJDEwMCB0byBKb2huJ10gLS0%2BIEJ7QVBJIERlc2lnbn0KICAgIAogICAgQiAtLT58VGVjaG5pY2FsIEFQSXN8IENbQWdlbnQgbXVzdDo8YnIvPjEuIEdFVCAvYWNjb3VudHM8YnIvPjIuIFBPU1QgL3RyYW5zYWN0aW9uczxici8%2BMy4gUFVUIC9iYWxhbmNlczxici8%2BNC4gUE9TVCAvbm90aWZpY2F0aW9uc10KICAgIEIgLS0%2BfEZ1bmN0aW9uYWwgQVBJfCBEW0FnZW50IGNhbGxzOjxici8%2BUE9TVCAvcGF5bWVudHMvdHJhbnNmZXJdCiAgICAKICAgIEMgLS0%2BIEVbNCBBUEkgY2FsbHM8YnIvPkNvbXBsZXggb3JjaGVzdHJhdGlvbjxici8%2BRXJyb3ItcHJvbmU8YnIvPk5vIHRyYW5zYWN0aW9uIGJvdW5kYXJ5XQogICAgRCAtLT4gRlsxIEFQSSBjYWxsPGJyLz5DbGVhciBpbnRlbnQ8YnIvPlRyYW5zYWN0aW9uIGd1YXJhbnRlZWQ8YnIvPkJ1c2luZXNzIHJ1bGVzIGVuZm9yY2VkXQogICAgCiAgICBzdHlsZSBFIGZpbGw6I2RjMzU0NSxjb2xvcjojZmZmCiAgICBzdHlsZSBGIGZpbGw6IzEwQjk4MSxjb2xvcjojZmZm%3Ftype%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEIKICAgIEFbQUkgQWdlbnQgUXVlcnk6PGJyLz4nVHJhbnNmZXIgJDEwMCB0byBKb2huJ10gLS0%2BIEJ7QVBJIERlc2lnbn0KICAgIAogICAgQiAtLT58VGVjaG5pY2FsIEFQSXN8IENbQWdlbnQgbXVzdDo8YnIvPjEuIEdFVCAvYWNjb3VudHM8YnIvPjIuIFBPU1QgL3RyYW5zYWN0aW9uczxici8%2BMy4gUFVUIC9iYWxhbmNlczxici8%2BNC4gUE9TVCAvbm90aWZpY2F0aW9uc10KICAgIEIgLS0%2BfEZ1bmN0aW9uYWwgQVBJfCBEW0FnZW50IGNhbGxzOjxici8%2BUE9TVCAvcGF5bWVudHMvdHJhbnNmZXJdCiAgICAKICAgIEMgLS0%2BIEVbNCBBUEkgY2FsbHM8YnIvPkNvbXBsZXggb3JjaGVzdHJhdGlvbjxici8%2BRXJyb3ItcHJvbmU8YnIvPk5vIHRyYW5zYWN0aW9uIGJvdW5kYXJ5XQogICAgRCAtLT4gRlsxIEFQSSBjYWxsPGJyLz5DbGVhciBpbnRlbnQ8YnIvPlRyYW5zYWN0aW9uIGd1YXJhbnRlZWQ8YnIvPkJ1c2luZXNzIHJ1bGVzIGVuZm9yY2VkXQogICAgCiAgICBzdHlsZSBFIGZpbGw6I2RjMzU0NSxjb2xvcjojZmZmCiAgICBzdHlsZSBGIGZpbGw6IzEwQjk4MSxjb2xvcjojZmZm%3Ftype%3Dpng" alt="Diagram" width="536" height="678"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With technical APIs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Agent must orchestrate multiple low-level operations&lt;/li&gt;
&lt;li&gt;No transaction boundaries (what if step 3 fails?)&lt;/li&gt;
&lt;li&gt;Business rules scattered across agent logic&lt;/li&gt;
&lt;li&gt;High risk of incorrect sequences&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;With functional APIs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single API call expresses business intent&lt;/li&gt;
&lt;li&gt;Transaction handled by backend&lt;/li&gt;
&lt;li&gt;Business rules centralized in API&lt;/li&gt;
&lt;li&gt;Lower risk, easier to audit&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Refactoring Question
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEIKICAgIEFbRXhpc3RpbmcgQVBJIExhbmRzY2FwZV0gLS0%2BIEJ7QVBJcyBGdW5jdGlvbmFsP30KICAgIEIgLS0%2BfFllc3wgQ1tBZGQgeC1tY3AtZW5hYmxlZCBmbGFnc10KICAgIEIgLS0%2BfE5vfCBEe1dvcnRoIFJlZmFjdG9yaW5nP30KICAgIEQgLS0%2BfFllc3wgRVtSZWZhY3RvciB0byBGdW5jdGlvbmFsIEFQSXMgRmlyc3RdCiAgICBEIC0tPnxOb3wgRltCdWlsZCBOZXcgRnVuY3Rpb25hbCBBUElzXQogICAgCiAgICBFIC0tPiBDCiAgICBGIC0tPiBDCiAgICBDIC0tPiBHW0RlcGxveSBNQ1AgU2VydmVyc10KICAgIAogICAgc3R5bGUgQiBmaWxsOiNmZmMxMDcsY29sb3I6IzAwMAogICAgc3R5bGUgRiBmaWxsOiNkYzM1NDUsY29sb3I6I2ZmZg%3D%3D%3Ftype%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEIKICAgIEFbRXhpc3RpbmcgQVBJIExhbmRzY2FwZV0gLS0%2BIEJ7QVBJcyBGdW5jdGlvbmFsP30KICAgIEIgLS0%2BfFllc3wgQ1tBZGQgeC1tY3AtZW5hYmxlZCBmbGFnc10KICAgIEIgLS0%2BfE5vfCBEe1dvcnRoIFJlZmFjdG9yaW5nP30KICAgIEQgLS0%2BfFllc3wgRVtSZWZhY3RvciB0byBGdW5jdGlvbmFsIEFQSXMgRmlyc3RdCiAgICBEIC0tPnxOb3wgRltCdWlsZCBOZXcgRnVuY3Rpb25hbCBBUElzXQogICAgCiAgICBFIC0tPiBDCiAgICBGIC0tPiBDCiAgICBDIC0tPiBHW0RlcGxveSBNQ1AgU2VydmVyc10KICAgIAogICAgc3R5bGUgQiBmaWxsOiNmZmMxMDcsY29sb3I6IzAwMAogICAgc3R5bGUgRiBmaWxsOiNkYzM1NDUsY29sb3I6I2ZmZg%3D%3D%3Ftype%3Dpng" alt="Diagram" width="622" height="920"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three paths forward:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;APIs already functional?&lt;/strong&gt; Add MCP flags, deploy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;APIs refactorable?&lt;/strong&gt; Transform technical → functional first&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;APIs beyond hope?&lt;/strong&gt; Build new functional APIs from scratch&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Don't expose CRUD endpoints to AI agents expecting them to orchestrate business logic.&lt;/strong&gt; That's a production incident waiting to happen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-World Examples
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Bad API (technical, don't MCP-ify this):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;/api/customers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Create&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;customer"&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Insert&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;customer&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;record&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;into&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;database"&lt;/span&gt;
    &lt;span class="na"&gt;requestBody&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;application/json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;object&lt;/span&gt;
            &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;firstName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
              &lt;span class="na"&gt;lastName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
              &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
              &lt;span class="c1"&gt;# 50 more fields...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;AI agent sees this:&lt;/strong&gt; "I can create a customer record, but what about KYC? Compliance checks? Account setup? Am I supposed to call other APIs? Which ones?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good API (functional, MCP-ready):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;/customers/onboard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Onboard&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;customer&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;full&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;verification"&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;Complete customer onboarding process including:&lt;/span&gt;
      &lt;span class="s"&gt;- Identity verification (government ID + liveness check)&lt;/span&gt;
      &lt;span class="s"&gt;- KYC/AML compliance screening&lt;/span&gt;
      &lt;span class="s"&gt;- Risk assessment&lt;/span&gt;
      &lt;span class="s"&gt;- Welcome email and account activation&lt;/span&gt;

      &lt;span class="s"&gt;**Business rules enforced:**&lt;/span&gt;
      &lt;span class="s"&gt;- Customer must be 18+ years old&lt;/span&gt;
      &lt;span class="s"&gt;- Must pass identity verification&lt;/span&gt;
      &lt;span class="s"&gt;- Must clear compliance screening&lt;/span&gt;
      &lt;span class="s"&gt;- Duplicate detection performed automatically&lt;/span&gt;

      &lt;span class="s"&gt;**What happens on success:**&lt;/span&gt;
      &lt;span class="s"&gt;- Customer record created with verified status&lt;/span&gt;
      &lt;span class="s"&gt;- Welcome email sent&lt;/span&gt;
      &lt;span class="s"&gt;- Customer can proceed to account opening&lt;/span&gt;

      &lt;span class="s"&gt;**Authentication required:** Staff or authorized system only&lt;/span&gt;
      &lt;span class="s"&gt;**Rate limit:** 10 requests per minute&lt;/span&gt;
    &lt;span class="na"&gt;x-mcp-enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;requestBody&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;application/json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;object&lt;/span&gt;
            &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;firstName&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;lastName&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;dateOfBirth&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;governmentId&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
            &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;firstName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
                &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Customer's&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;legal&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;first&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;name"&lt;/span&gt;
              &lt;span class="na"&gt;lastName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
                &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Customer's&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;legal&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;last&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;name"&lt;/span&gt;
              &lt;span class="na"&gt;dateOfBirth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
                &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;date&lt;/span&gt;
                &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Date&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;birth&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(must&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;be&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;18+&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;years&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ago)"&lt;/span&gt;
              &lt;span class="na"&gt;governmentId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;object&lt;/span&gt;
                &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Government-issued&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;identification"&lt;/span&gt;
                &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;number&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;issuingCountry&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
                &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
                    &lt;span class="na"&gt;enum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;passport&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;drivers_license&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;national_id&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
                  &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
                  &lt;span class="na"&gt;issuingCountry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
    &lt;span class="na"&gt;responses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;201'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Customer&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;onboarded&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;successfully"&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;application/json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;object&lt;/span&gt;
              &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;customerId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
                  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unique&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;customer&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;identifier"&lt;/span&gt;
                &lt;span class="na"&gt;verificationStatus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
                  &lt;span class="na"&gt;enum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;verified&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pending_review&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
                &lt;span class="na"&gt;nextSteps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;array&lt;/span&gt;
                  &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
                  &lt;span class="na"&gt;example&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Open&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;savings&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;account"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Apply&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;credit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;card"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;400'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;input&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;failed&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;verification"&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;application/json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;object&lt;/span&gt;
              &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
                &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
                  &lt;span class="na"&gt;enum&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;underage&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;failed_identity_check&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;failed_compliance&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;duplicate_customer&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;AI agent sees this:&lt;/strong&gt; "Crystal clear. This onboards a customer, handles verification, enforces business rules, tells me what happens next. I know exactly when to use this and what data I need."&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Technical APIs Fail with AI Agents
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem 1: Orchestration burden&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Technical approach (agent must figure out):
1. POST /customers (create record)
2. POST /kyc-checks (verify identity)
3. GET /kyc-checks/{id} (wait for result)
4. POST /compliance-screenings (AML check)
5. GET /compliance-screenings/{id} (wait for result)
6. PUT /customers/{id} (update status to verified)
7. POST /emails (send welcome email)

What if step 4 fails? What if step 6 fails but step 7 succeeds?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Functional approach (single call):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /customers/onboard (everything happens atomically)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Problem 2: Business rule discovery&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Technical API says: "Here's a database table, figure it out"
Functional API says: "Here's the business process, I'll enforce the rules"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Problem 3: Error semantics&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Technical API: 400 Bad Request (what went wrong?)
Functional API: 400 Bad Request - reason: "failed_identity_check" (ah, identity verification failed)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This isn't just API design pedantry.&lt;/strong&gt; This is the difference between agents that work and agents that cause production incidents.&lt;/p&gt;




&lt;h2&gt;
  
  
  GitOps Principles for MCP
&lt;/h2&gt;

&lt;p&gt;Let's talk about what "GitOps" actually means in this context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Single Source of Truth
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Everything in Git:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAPI specifications with &lt;code&gt;x-mcp-enabled&lt;/code&gt; flags&lt;/li&gt;
&lt;li&gt;APIM policy templates&lt;/li&gt;
&lt;li&gt;Security configurations&lt;/li&gt;
&lt;li&gt;Access control lists&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Nothing in Azure Portal.&lt;/strong&gt; If it's not in Git, it doesn't exist.&lt;/p&gt;

&lt;h3&gt;
  
  
  Declarative Configuration
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What we want:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# This operation should be an MCP tool&lt;/span&gt;
&lt;span class="na"&gt;x-mcp-enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Not:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Imperative commands that modify state&lt;/span&gt;
az apim mcp create &lt;span class="nt"&gt;--name&lt;/span&gt; ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The bridge automation reads declarative config and reconciles Azure state.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Review Before Merge
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Feature Branch → Pull Request → Automated Validation → 
Human Review → Approval → Merge to Main → Automated Deployment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;No cowboy deployments.&lt;/strong&gt; Every change goes through review.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rollback Capability
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Git tags for deployments:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git tag deploy-20260112-103000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Rollback = checkout previous tag:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout deploy-20260112-100000
./deploy.sh  &lt;span class="c"&gt;# Re-applies previous state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Observability
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Track what automation does:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What APIs were scanned?&lt;/li&gt;
&lt;li&gt;Which operations had &lt;code&gt;x-mcp-enabled&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;What MCP servers were created/updated/deleted?&lt;/li&gt;
&lt;li&gt;Any failures or errors?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pipeline logs become your audit trail.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Our Exploration: What We're Testing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Full transparency:&lt;/strong&gt; We're in the exploration phase. Here's what we're considering for Grand Central.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option We're Leaning Toward
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Maven plugin extension&lt;/strong&gt; that generates MCP overlay configs during build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this appeals:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fits our existing OpenAPI → Maven → Helm → ArgoCD workflow&lt;/li&gt;
&lt;li&gt;Zero operational overhead once deployed&lt;/li&gt;
&lt;li&gt;Centralized logic (one plugin, all APIs benefit)&lt;/li&gt;
&lt;li&gt;Easy removal when ASO adds MCP support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How it would work:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEIKICAgIEFbT3BlbkFQSSBTcGVjXSAtLT58TWF2ZW4gQnVpbGR8IEJbQVBJTSBIZWxtIE1hdmVuIFBsdWdpbl0KICAgIEIgLS0%2BfERldGVjdHMgeC1tY3AtZW5hYmxlZHwgQ3tHZW5lcmF0ZSBNQ1AgT3ZlcmxheT99CiAgICBDIC0tPnxZZXN8IERbU3RhbmRhcmQgQVBJIEhlbG0gQ2hhcnQgKyBNQ1AgSm9iIENoYXJ0XQogICAgQyAtLT58Tm98IEVbU3RhbmRhcmQgQVBJIEhlbG0gQ2hhcnQgT25seV0KICAgIEQgLS0%2BIEZbQXJnb0NEIERlcGxveXMgQm90aF0KICAgIEUgLS0%2BIEdbQXJnb0NEIERlcGxveXMgQVBJIE9ubHldCiAgICBGIC0tPiBIW0FQSSArIE1DUCBTZXJ2ZXIgaW4gQVBJTV0KICAgIEcgLS0%2BIElbQVBJIE9ubHkgaW4gQVBJTV0KICAgIAogICAgc3R5bGUgQiBmaWxsOiMwMDc4RDQsY29sb3I6I2ZmZgogICAgc3R5bGUgRCBmaWxsOiMxMEI5ODEsY29sb3I6I2ZmZg%3D%3D%3Ftype%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEIKICAgIEFbT3BlbkFQSSBTcGVjXSAtLT58TWF2ZW4gQnVpbGR8IEJbQVBJTSBIZWxtIE1hdmVuIFBsdWdpbl0KICAgIEIgLS0%2BfERldGVjdHMgeC1tY3AtZW5hYmxlZHwgQ3tHZW5lcmF0ZSBNQ1AgT3ZlcmxheT99CiAgICBDIC0tPnxZZXN8IERbU3RhbmRhcmQgQVBJIEhlbG0gQ2hhcnQgKyBNQ1AgSm9iIENoYXJ0XQogICAgQyAtLT58Tm98IEVbU3RhbmRhcmQgQVBJIEhlbG0gQ2hhcnQgT25seV0KICAgIEQgLS0%2BIEZbQXJnb0NEIERlcGxveXMgQm90aF0KICAgIEUgLS0%2BIEdbQXJnb0NEIERlcGxveXMgQVBJIE9ubHldCiAgICBGIC0tPiBIW0FQSSArIE1DUCBTZXJ2ZXIgaW4gQVBJTV0KICAgIEcgLS0%2BIElbQVBJIE9ubHkgaW4gQVBJTV0KICAgIAogICAgc3R5bGUgQiBmaWxsOiMwMDc4RDQsY29sb3I6I2ZmZgogICAgc3R5bGUgRCBmaWxsOiMxMEI5ODEsY29sb3I6I2ZmZg%3D%3D%3Ftype%3Dpng" alt="Diagram" width="586" height="861"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plugin would:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scan OpenAPI spec for &lt;code&gt;x-mcp-enabled&lt;/code&gt; flags&lt;/li&gt;
&lt;li&gt;Generate standard ASO API CRD (existing behavior)&lt;/li&gt;
&lt;li&gt;Generate MCP overlay as Kubernetes Job (new behavior)&lt;/li&gt;
&lt;li&gt;Job runs as ArgoCD PostSync hook&lt;/li&gt;
&lt;li&gt;Job calls Azure REST API to convert API to MCP type&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Why Not the Alternatives?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Centralized polling approach:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requires cross-subscription service principal&lt;/li&gt;
&lt;li&gt;Eventual consistency (polling lag)&lt;/li&gt;
&lt;li&gt;Extra infrastructure to maintain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Per-customer workflows:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deployment overhead (one repo per customer installation)&lt;/li&gt;
&lt;li&gt;Configuration drift risk&lt;/li&gt;
&lt;li&gt;Harder to update/remove&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Our context:&lt;/strong&gt; Multi-tenant platform with dozens of APIM instances, standardized bootstrap configuration across all of them. Maven plugin fits our existing patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  What We Haven't Solved Yet
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Kubernetes RBAC:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How do Jobs get Azure credentials?&lt;/li&gt;
&lt;li&gt;Managed Identity? Service account with federated identity?&lt;/li&gt;
&lt;li&gt;Per-cluster or shared credentials?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Error handling:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Job fails to convert API to MCP. Now what?&lt;/li&gt;
&lt;li&gt;Does ArgoCD deployment fail? Or continue?&lt;/li&gt;
&lt;li&gt;How do we surface MCP conversion errors to developers?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Testing strategy:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How do we validate MCP overlays in dev before production?&lt;/li&gt;
&lt;li&gt;Can we preview what tools would be exposed?&lt;/li&gt;
&lt;li&gt;Dry-run mode for the conversion?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Removal path:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When ASO adds MCP support, how do we migrate?&lt;/li&gt;
&lt;li&gt;Do we update the plugin to generate ASO MCP CRDs instead of Jobs?&lt;/li&gt;
&lt;li&gt;Phased rollout or big-bang cutover?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;These are open questions.&lt;/strong&gt; We're working through them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision Framework: When to Build vs Wait
&lt;/h2&gt;

&lt;p&gt;Not every organization should build custom automation. Let's break down the decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build Custom Bridging If:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You have scale&lt;/strong&gt; (multiple APIM instances, lots of APIs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You have GitOps maturity&lt;/strong&gt; (already using IaC, review processes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You have time&lt;/strong&gt; (2-4 weeks for design + implementation + testing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You have appetite&lt;/strong&gt; (willing to maintain bridge until ASO catches up)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need governance&lt;/strong&gt; (AI agents accessing business-critical APIs)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Wait for ASO If:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small scale&lt;/strong&gt; (1-2 APIM instances, handful of APIs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portal management is fine&lt;/strong&gt; (low change frequency)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prototyping phase&lt;/strong&gt; (not production-critical yet)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No GitOps&lt;/strong&gt; (not using IaC for other Azure resources)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource constrained&lt;/strong&gt; (can't invest dev time in tooling)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Our Context
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Why we're building:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dozens of APIM instances across customer installations&lt;/li&gt;
&lt;li&gt;Hundreds of APIs, growing fast&lt;/li&gt;
&lt;li&gt;Existing GitOps workflows (OpenAPI → Maven → ArgoCD)&lt;/li&gt;
&lt;li&gt;Banking platform (governance non-negotiable)&lt;/li&gt;
&lt;li&gt;Multi-tenant isolation requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Manual Portal management doesn't scale at our size.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What ASO Support Would Look Like
&lt;/h2&gt;

&lt;p&gt;Microsoft hasn't announced a timeline, but here's what we expect:&lt;/p&gt;

&lt;h3&gt;
  
  
  CRD Structure (Hypothetical)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apimanagement.azure.com/v1api20260501&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;API&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer-api&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apim-instance&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Customer&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Management&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;API"&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcp&lt;/span&gt;  &lt;span class="c1"&gt;# MCP type support&lt;/span&gt;
  &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer-mcp&lt;/span&gt;
  &lt;span class="na"&gt;serviceUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://backend.example.com&lt;/span&gt;
  &lt;span class="na"&gt;protocols&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;
  &lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
  &lt;span class="na"&gt;mcpConfiguration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# MCP-specific config&lt;/span&gt;
    &lt;span class="na"&gt;enableToolDiscovery&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;mcpTools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;operationId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;getUserProfile&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;operationId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;updateUserProfile&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What we'd need:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;type: mcp&lt;/code&gt; support (currently only &lt;code&gt;http&lt;/code&gt;, &lt;code&gt;soap&lt;/code&gt;, &lt;code&gt;websocket&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mcpConfiguration&lt;/code&gt; section for tool control&lt;/li&gt;
&lt;li&gt;Respect for &lt;code&gt;x-mcp-enabled&lt;/code&gt; flags in OpenAPI specs&lt;/li&gt;
&lt;li&gt;Policy application for MCP servers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Migration path:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;ASO releases MCP CRDs&lt;/li&gt;
&lt;li&gt;We update our plugin to generate MCP CRDs instead of overlay Jobs&lt;/li&gt;
&lt;li&gt;Bump plugin version in parent POM&lt;/li&gt;
&lt;li&gt;Redeploy APIs (ASO reconciles to MCP type)&lt;/li&gt;
&lt;li&gt;Delete overlay Job charts&lt;/li&gt;
&lt;li&gt;Done&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;If ASO doesn't support &lt;code&gt;x-mcp-enabled&lt;/code&gt; flags:&lt;/strong&gt; We'd need to maintain the plugin logic anyway, just targeting CRDs instead of REST API.&lt;/p&gt;




&lt;h2&gt;
  
  
  Advocacy: Push Microsoft
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Here's the thing:&lt;/strong&gt; Microsoft responds to feedback. If nobody asks for MCP IaC support, it stays low priority.&lt;/p&gt;

&lt;h3&gt;
  
  
  What You Can Do
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;File GitHub issues:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Azure/azure-service-operator: Request MCP CRD support&lt;/li&gt;
&lt;li&gt;Azure/azure-rest-api-specs: Request stable MCP API version&lt;/li&gt;
&lt;li&gt;Azure/bicep: Request MCP resource types&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Engage on Azure forums:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Azure feedback portal: Vote for IaC support&lt;/li&gt;
&lt;li&gt;Azure APIM team: Share your use case&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Show demand:&lt;/strong&gt; The more organizations ask, the higher priority it becomes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We're doing our part.&lt;/strong&gt; We've already escalated to Microsoft's APIM team. They're aware of the need. But they need to see community demand.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion: The Architect's Lens
&lt;/h2&gt;

&lt;p&gt;Let's zoom out and look at this from an architecture perspective.&lt;/p&gt;

&lt;h3&gt;
  
  
  What This Series Covered
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://itsrene.nl/blog/rendered/azure-apim-mcp-overview.html" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt;&lt;/strong&gt;: MCP preview realities (what works, what's broken, when to use it)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://itsrene.nl/blog/rendered/azure-apim-mcp-security.html" rel="noopener noreferrer"&gt;Part 2&lt;/a&gt;&lt;/strong&gt;: Security hardening (fixing &lt;code&gt;/tools/list&lt;/code&gt;, subscription validation)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://itsrene.nl/blog/rendered/azure-apim-mcp-logging.html" rel="noopener noreferrer"&gt;Part 3&lt;/a&gt;&lt;/strong&gt;: Observability (audit logging without performance hits)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 4&lt;/strong&gt; (this post): Automation bridging (governance-first approach)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;MCP on Azure APIM is powerful but immature.&lt;/strong&gt; You can run it in production, but:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Governance is more critical than automation&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
AI agents reading descriptions = extra eyes on every change&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;API quality matters more than ever&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Don't MCP-ify garbage APIs expecting good agent behavior&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;GitOps principles apply&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Source control, review gates, rollback capability&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Custom bridging is temporary&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Build with removal in mind (ASO will eventually catch up)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Not every org should build&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Weigh scale, maturity, and resources before committing&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Where We Are
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;At Grand Central:&lt;/strong&gt; Exploring Maven plugin approach for our multi-tenant scale. Governance framework designed. API quality assessment in progress. Not deployed yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Being honest:&lt;/strong&gt; These are ideas we're testing, not battle-tested solutions. We're sharing our thinking because maybe it helps you avoid pitfalls.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's Next
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;For us:&lt;/strong&gt; Prototype the Maven plugin, test in dev environment, validate governance workflows, iterate based on learnings.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For you:&lt;/strong&gt; Assess your context. Do you need custom bridging? Can you wait for ASO? What's your governance story?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Microsoft:&lt;/strong&gt; Keep pushing them. File issues. Share use cases. Show demand.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Azure Service Operator&lt;/strong&gt;: &lt;a href="https://github.com/Azure/azure-service-operator" rel="noopener noreferrer"&gt;https://github.com/Azure/azure-service-operator&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;APIM REST API&lt;/strong&gt;: &lt;a href="https://learn.microsoft.com/rest/api/apimanagement/" rel="noopener noreferrer"&gt;https://learn.microsoft.com/rest/api/apimanagement/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP Specification&lt;/strong&gt;: &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAPI Extension&lt;/strong&gt;: x-mcp-enabled pattern (community convention)&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;I'm a Product Architect at &lt;a href="https://backbase.com" rel="noopener noreferrer"&gt;Backbase&lt;/a&gt;, where I design integration platforms for global banks. The patterns in this series come from real production implementations at enterprise scale, filtered through the lens of "what would I want to know if I were you?" Views are my own.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Building custom MCP automation? Running into governance challenges? Let's talk—drop a comment or connect on &lt;a href="https://linkedin.com/in/renenijkamp" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>architecture</category>
      <category>mcp</category>
      <category>governance</category>
    </item>
    <item>
      <title>Azure APIM MCP Audit Logging Without Breaking Everything</title>
      <dc:creator>René Nijkamp</dc:creator>
      <pubDate>Wed, 03 Dec 2025 17:19:36 +0000</pubDate>
      <link>https://dev.to/itsrene/azure-apim-mcp-audit-logging-without-breaking-everything-5h7l</link>
      <guid>https://dev.to/itsrene/azure-apim-mcp-audit-logging-without-breaking-everything-5h7l</guid>
      <description>&lt;h1&gt;
  
  
  Azure APIM MCP Audit Logging Without Breaking Everything
&lt;/h1&gt;

&lt;p&gt;In &lt;a href="https://itsrene.nl/blog/rendered/azure-apim-mcp-security.html" rel="noopener noreferrer"&gt;Part 2&lt;/a&gt;, we locked down security. Now let's talk about &lt;strong&gt;observability&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You need audit logging for compliance. You need distributed tracing for debugging. You need error handling for production resilience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here's the problem: by default, you have none of that.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Out of the box, APIM passes MCP requests through with zero logging. You have no idea if someone's calling &lt;code&gt;tools/list&lt;/code&gt; or &lt;code&gt;tools/call&lt;/code&gt;. You can't tell which API requests are actually MCP traffic. No audit trail. No visibility. Just requests flowing through in the dark. Not what you want, not what compliance wants, not what security wants. &lt;/p&gt;

&lt;p&gt;And when you try to add logging? That's when you discover the fun part: &lt;strong&gt;accessing the response body in APIM policies causes requests to hang indefinitely.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I found this out the hard way. Days of debugging. Requests hanging. Timeouts everywhere. The fix? Don't touch the response body. Ever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This issue has been reported to Microsoft,it is on their radar.&lt;/strong&gt; But here's the reality: MCP servers are in preview. When you work with preview functionality, you carry the burden of working around these rough edges yourself. That's the tax you pay for being early.&lt;/p&gt;

&lt;p&gt;Let me show you how to get the observability you need, without the trial and erroring I went through. &lt;/p&gt;

&lt;h2&gt;
  
  
  Observability Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEIKICAgIEFbQUkgQWdlbnRdIC0tPnxSZXF1ZXN0fCBCW0FQSU0gTUNQXQogICAgQiAtLT58QXVkaXQgTG9nc3wgQ1tFdmVudCBIdWJdCiAgICAKICAgIEMgLS0%2BfFN0cmVhbXwgRFtZb3VyIFNJRU0vT2JzZXJ2YWJpbGl0eSBQbGF0Zm9ybV0KICAgIEQgLS0%2BfFN0b3JlICYgSW5kZXh8IEVbTG9nIFN0b3JhZ2VdCiAgICAKICAgIEUgLS0%2BfFF1ZXJ5ICYgQWdncmVnYXRlfCBGW0Rhc2hib2FyZHNdCiAgICBFIC0tPnxBbGVydHwgR1tBbGVydCBSdWxlc10KICAgIAogICAgRyAtLT58Tm90aWZ5fCBIW1RlYW1zL0VtYWlsL1BhZ2VyRHV0eV0KICAgIAogICAgc3R5bGUgQiBmaWxsOiMwMDc4RDQsY29sb3I6I2ZmZgogICAgc3R5bGUgQyBmaWxsOiNGNTlFMEIsY29sb3I6I2ZmZgogICAgc3R5bGUgRCBmaWxsOiMxMEI5ODEsY29sb3I6I2ZmZg%3D%3D%3Ftype%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggVEIKICAgIEFbQUkgQWdlbnRdIC0tPnxSZXF1ZXN0fCBCW0FQSU0gTUNQXQogICAgQiAtLT58QXVkaXQgTG9nc3wgQ1tFdmVudCBIdWJdCiAgICAKICAgIEMgLS0%2BfFN0cmVhbXwgRFtZb3VyIFNJRU0vT2JzZXJ2YWJpbGl0eSBQbGF0Zm9ybV0KICAgIEQgLS0%2BfFN0b3JlICYgSW5kZXh8IEVbTG9nIFN0b3JhZ2VdCiAgICAKICAgIEUgLS0%2BfFF1ZXJ5ICYgQWdncmVnYXRlfCBGW0Rhc2hib2FyZHNdCiAgICBFIC0tPnxBbGVydHwgR1tBbGVydCBSdWxlc10KICAgIAogICAgRyAtLT58Tm90aWZ5fCBIW1RlYW1zL0VtYWlsL1BhZ2VyRHV0eV0KICAgIAogICAgc3R5bGUgQiBmaWxsOiMwMDc4RDQsY29sb3I6I2ZmZgogICAgc3R5bGUgQyBmaWxsOiNGNTlFMEIsY29sb3I6I2ZmZgogICAgc3R5bGUgRCBmaWxsOiMxMEI5ODEsY29sb3I6I2ZmZg%3D%3D%3Ftype%3Dpng" alt="Diagram" width="397" height="862"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Response Body Problem
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What You'd Normally Do
&lt;/h3&gt;

&lt;p&gt;In a typical APIM setup, especially during development, you'd log the response body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;outbound&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;log-to-eventhub&lt;/span&gt; &lt;span class="na"&gt;logger-id=&lt;/span&gt;&lt;span class="s"&gt;"my-logger"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        @{
            return new JObject(
                new JProperty("request", context.Request.Body.As&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;()),
                new JProperty("response", context.Response.Body.As&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;())  // HANGS
            ).ToString();
        }
    &lt;span class="nt"&gt;&amp;lt;/log-to-eventhub&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/outbound&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will hang your requests. The response never completes. Clients timeout.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Happens
&lt;/h3&gt;

&lt;p&gt;When you access &lt;code&gt;context.Response.Body&lt;/code&gt; in the &lt;code&gt;&amp;lt;outbound&amp;gt;&lt;/code&gt; section:&lt;/p&gt;

&lt;p&gt;I dont have access to the APIM MCP internals, but: I suspect that APIM MCP tries to buffer the entire response, which can be large, which can be streaming. If its in memory buffer, it might just trigger errors there. If its streaming, it will block until everything is read, which is not going to happen until the client reads it... In short, a deadlock, no bueno, and you break production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The frustrating part?&lt;/strong&gt; This behavior isn't called out clearly in the main policy documentation. The policy is saved without any warnings, and you wonder why suddenly you get timeout errors. After a lot of trial and error, or if you're lucky, someone (Hi there ;) ) warns you first. Probably just one of those things that happens in preview.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Microsoft is aware of this issue, it's been reported and is on their radar.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;The good news: there's a reliable workaround that gives you everything compliance needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution: Log Metadata, Not Payloads
&lt;/h3&gt;

&lt;p&gt;You can't log response bodies. Fine. Log &lt;strong&gt;everything else&lt;/strong&gt; instead—and it turns out that's exactly what you need for compliance anyway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;outbound&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;log-to-eventhub&lt;/span&gt; &lt;span class="na"&gt;logger-id=&lt;/span&gt;&lt;span class="s"&gt;"mcp-audit-logger"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        @{
            var mcpMethod = "";
            var mcpId = "";

            // Extract MCP method from request body (safely)
            try {
                var requestBody = context.Request.Body.As&lt;span class="nt"&gt;&amp;lt;JObject&amp;gt;&lt;/span&gt;(preserveContent: true);
                mcpMethod = requestBody["method"]?.ToString() ?? "";
                mcpId = requestBody["id"]?.ToString() ?? "";
            } catch {
                mcpMethod = "parse-error";
            }

            return new JObject(
                // Request metadata
                new JProperty("timestamp", DateTime.UtcNow.ToString("o")),
                new JProperty("requestId", context.RequestId),
                new JProperty("subscriptionId", context.Subscription?.Id ?? "none"),
                new JProperty("subscriptionName", context.Subscription?.Name ?? "none"),

                // MCP-specific
                new JProperty("mcpMethod", mcpMethod),
                new JProperty("mcpId", mcpId),

                // Response metadata (SAFE - no body access)
                new JProperty("statusCode", context.Response.StatusCode),
                new JProperty("statusReason", context.Response.StatusReason),

                // Timing
                new JProperty("elapsedMs", context.Elapsed.TotalMilliseconds),

                // Client info
                new JProperty("clientIp", context.Request.IpAddress),
                new JProperty("userAgent", context.Request.Headers.GetValueOrDefault("User-Agent", ""))
            ).ToString();
        }
    &lt;span class="nt"&gt;&amp;lt;/log-to-eventhub&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/outbound&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key points:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;context.Request.Body.As&amp;lt;JObject&amp;gt;(preserveContent: true)&lt;/code&gt; is safe in &lt;code&gt;&amp;lt;outbound&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;context.Response.StatusCode&lt;/code&gt; is safe&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;context.Response.Body&lt;/code&gt; is NOT safe&lt;/li&gt;
&lt;li&gt;Wrap parsing in try-catch for malformed requests&lt;/li&gt;
&lt;li&gt;If you have any other headers you want to log, you can add them quite easily to the JObject. &lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Logging Tool Discovery
&lt;/h3&gt;

&lt;p&gt;Tracking who is executing tools is needed, but what about &lt;strong&gt;who's discovering your tools?&lt;/strong&gt;. Who hasnt seen the IP port scans in his life, this is not much else than that. To log these activities, a policy snippet needs to be added to the Inbound section, since this request will never hit any outbound services (and therefore policies)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;inbound&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- After security checks, before backend --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;choose&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;when&lt;/span&gt; &lt;span class="na"&gt;condition=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Request.Body.As&amp;lt;JObject&amp;gt;(preserveContent: true)["&lt;/span&gt;&lt;span class="err"&gt;method"]?.ToString()&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="err"&gt;"tools/list")"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;log-to-eventhub&lt;/span&gt; &lt;span class="na"&gt;logger-id=&lt;/span&gt;&lt;span class="s"&gt;"mcp-audit-logger"&lt;/span&gt; &lt;span class="na"&gt;partition-id=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                @{
                    return new JObject(
                        new JProperty("eventType", "discovery"),
                        new JProperty("timestamp", DateTime.UtcNow.ToString("o")),
                        new JProperty("requestId", context.RequestId),
                        new JProperty("subscriptionId", context.Subscription?.Id ?? "none"),
                        new JProperty("subscriptionName", context.Subscription?.Name ?? "none"),
                        new JProperty("clientIp", context.Request.IpAddress),
                        new JProperty("userAgent", context.Request.Headers.GetValueOrDefault("User-Agent", "")),
                        new JProperty("mcpMethod", "tools/list")
                    ).ToString();
                }
            &lt;span class="nt"&gt;&amp;lt;/log-to-eventhub&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/when&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/choose&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/inbound&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why log discovery separately?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Security monitoring&lt;/strong&gt;: Who is checking for tools, and are they allowed to. It can be a forgotten attack surface. So better log it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usage analytics&lt;/strong&gt;: Track which clients are discovering vs actually using tools. If there is an overload of tool discovery calls, you might have an issue in one of your agents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance&lt;/strong&gt;: Auditors want to know who accessed what capabilities, even if they didn't invoke them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: Separate partition for discovery logs (partition-id="0") keeps them isolated from invocation logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives you the complete picture: discovery → invocation → response → errors.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting Up Event Hub Logging
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Reality check:&lt;/strong&gt; You need Event Hub for production audit logging. Application Insights alone won't give you the retention, queryability, and compliance guarantees you need. Yes, it's another Azure service. Yes, it adds complexity. But it's the right tool for immutable audit trails. Event Hub also very easily integrates with tools like Datadog or Grafana, or whatever SIEM you use for your end product. &lt;/p&gt;

&lt;p&gt;If you're running a small pilot, you can skip this and use App Insights only. For regulated environments? Event Hub is non-negotiable.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Create Event Hub
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Variables&lt;/span&gt;
&lt;span class="nv"&gt;RESOURCE_GROUP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"rg-apim-mcp"&lt;/span&gt;
&lt;span class="nv"&gt;LOCATION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"westeurope"&lt;/span&gt;
&lt;span class="nv"&gt;EVENTHUB_NAMESPACE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"eh-apim-mcp-logs"&lt;/span&gt;
&lt;span class="nv"&gt;EVENTHUB_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"mcp-audit-logs"&lt;/span&gt;

&lt;span class="c"&gt;# Create namespace&lt;/span&gt;
az eventhubs namespace create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$EVENTHUB_NAMESPACE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt; &lt;span class="nv"&gt;$LOCATION&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sku&lt;/span&gt; Standard

&lt;span class="c"&gt;# Create event hub&lt;/span&gt;
az eventhubs eventhub create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace-name&lt;/span&gt; &lt;span class="nv"&gt;$EVENTHUB_NAMESPACE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$EVENTHUB_NAME&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--partition-count&lt;/span&gt; 4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--message-retention&lt;/span&gt; 7

&lt;span class="c"&gt;# Get connection string&lt;/span&gt;
az eventhubs namespace authorization-rule keys list &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace-name&lt;/span&gt; &lt;span class="nv"&gt;$EVENTHUB_NAMESPACE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; RootManageSharedAccessKey &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; primaryConnectionString &lt;span class="nt"&gt;-o&lt;/span&gt; tsv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Create APIM Logger
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get APIM instance&lt;/span&gt;
&lt;span class="nv"&gt;APIM_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"apim-mcp-prod"&lt;/span&gt;

&lt;span class="c"&gt;# Create logger&lt;/span&gt;
az apim logger create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--service-name&lt;/span&gt; &lt;span class="nv"&gt;$APIM_NAME&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--logger-id&lt;/span&gt; &lt;span class="s2"&gt;"mcp-audit-logger"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--logger-type&lt;/span&gt; azureEventHub &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--connection-string&lt;/span&gt; &lt;span class="s2"&gt;"Endpoint=sb://eh-apim-mcp-logs.servicebus.windows.net/;..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--description&lt;/span&gt; &lt;span class="s2"&gt;"MCP audit logging"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or via Azure Portal:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to your APIM instance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;APIs&lt;/strong&gt; → &lt;strong&gt;Loggers&lt;/strong&gt; → &lt;strong&gt;+ Add&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Name: &lt;code&gt;mcp-audit-logger&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Type: &lt;strong&gt;Azure Event Hub&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Connection string: (paste from above)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  3. Configure Named Values (Optional)
&lt;/h3&gt;

&lt;p&gt;If you're managing multiple environments (dev/staging/prod), use named values. If you're just testing, hard-code it and move on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az apim nv create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--service-name&lt;/span&gt; &lt;span class="nv"&gt;$APIM_NAME&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--named-value-id&lt;/span&gt; &lt;span class="s2"&gt;"audit-logger-id"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--display-name&lt;/span&gt; &lt;span class="s2"&gt;"audit-logger-id"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--value&lt;/span&gt; &lt;span class="s2"&gt;"mcp-audit-logger"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reference in policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;log-to-eventhub&lt;/span&gt; &lt;span class="na"&gt;logger-id=&lt;/span&gt;&lt;span class="s"&gt;"{{audit-logger-id}}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Complete Production Logging Policy
&lt;/h2&gt;

&lt;p&gt;Here's the full policy with audit logging integrated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;policies&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;inbound&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!-- Security policies from Part 2 --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;choose&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;when&lt;/span&gt; &lt;span class="na"&gt;condition=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Request.Headers.GetValueOrDefault(\"&lt;/span&gt;&lt;span class="err"&gt;Ocp-Apim-Subscription-Key\",&lt;/span&gt; &lt;span class="err"&gt;\"\")&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="err"&gt;\"\")"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;return-response&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-status&lt;/span&gt; &lt;span class="na"&gt;code=&lt;/span&gt;&lt;span class="s"&gt;"401"&lt;/span&gt; &lt;span class="na"&gt;reason=&lt;/span&gt;&lt;span class="s"&gt;"Unauthorized"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-body&amp;gt;&lt;/span&gt;{"error": "Subscription key required"}&lt;span class="nt"&gt;&amp;lt;/set-body&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/return-response&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/when&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/choose&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;set-variable&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"originalSubKey"&lt;/span&gt; 
                      &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Request.Headers.GetValueOrDefault(\"&lt;/span&gt;&lt;span class="err"&gt;Ocp-Apim-Subscription-Key\",&lt;/span&gt; &lt;span class="err"&gt;\"\"))"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-variable&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"originalJwtToken"&lt;/span&gt; 
                      &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Request.Headers.GetValueOrDefault(\"&lt;/span&gt;&lt;span class="err"&gt;Authorization\",&lt;/span&gt; &lt;span class="err"&gt;\"\"))"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- Store request timestamp for elapsed time calculation --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-variable&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"requestStartTime"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"@(DateTime.UtcNow)"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;rate-limit&lt;/span&gt; &lt;span class="na"&gt;calls=&lt;/span&gt;&lt;span class="s"&gt;"100"&lt;/span&gt; &lt;span class="na"&gt;renewal-period=&lt;/span&gt;&lt;span class="s"&gt;"60"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;choose&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;when&lt;/span&gt; &lt;span class="na"&gt;condition=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Subscription == null || context.Subscription.Id == null)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;return-response&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-status&lt;/span&gt; &lt;span class="na"&gt;code=&lt;/span&gt;&lt;span class="s"&gt;"401"&lt;/span&gt; &lt;span class="na"&gt;reason=&lt;/span&gt;&lt;span class="s"&gt;"Unauthorized"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-body&amp;gt;&lt;/span&gt;{"error": "Invalid subscription key"}&lt;span class="nt"&gt;&amp;lt;/set-body&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/return-response&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/when&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/choose&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Ocp-Apim-Subscription-Key"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;@((string)context.Variables["originalSubKey"])&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;@((string)context.Variables["originalJwtToken"])&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- Distributed tracing - preserve existing or generate new --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"X-Request-ID"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"skip"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;@(context.RequestId)&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- Log tool discovery requests --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;choose&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;when&lt;/span&gt; &lt;span class="na"&gt;condition=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Request.Body.As&amp;lt;JObject&amp;gt;(preserveContent: true)["&lt;/span&gt;&lt;span class="err"&gt;method"]?.ToString()&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="err"&gt;"tools/list")"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;log-to-eventhub&lt;/span&gt; &lt;span class="na"&gt;logger-id=&lt;/span&gt;&lt;span class="s"&gt;"mcp-audit-logger"&lt;/span&gt; &lt;span class="na"&gt;partition-id=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                    @{
                        return new JObject(
                            new JProperty("eventType", "discovery"),
                            new JProperty("timestamp", DateTime.UtcNow.ToString("o")),
                            new JProperty("requestId", context.RequestId),
                            new JProperty("subscriptionId", context.Subscription?.Id ?? "none"),
                            new JProperty("subscriptionName", context.Subscription?.Name ?? "none"),
                            new JProperty("clientIp", context.Request.IpAddress),
                            new JProperty("userAgent", context.Request.Headers.GetValueOrDefault("User-Agent", "")),
                            new JProperty("mcpMethod", "tools/list")
                        ).ToString();
                    }
                &lt;span class="nt"&gt;&amp;lt;/log-to-eventhub&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/when&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/choose&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/inbound&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;backend&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/backend&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;outbound&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!-- Audit logging (SAFE - no response body access) --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;log-to-eventhub&lt;/span&gt; &lt;span class="na"&gt;logger-id=&lt;/span&gt;&lt;span class="s"&gt;"mcp-audit-logger"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            @{
                var mcpMethod = "";
                var mcpId = "";
                var userId = "";

                // Extract MCP details
                try {
                    var requestBody = context.Request.Body.As&lt;span class="nt"&gt;&amp;lt;JObject&amp;gt;&lt;/span&gt;(preserveContent: true);
                    mcpMethod = requestBody["method"]?.ToString() ?? "";
                    mcpId = requestBody["id"]?.ToString() ?? "";
                } catch {
                    mcpMethod = "parse-error";
                }

                // Extract user ID from JWT (if present)
                try {
                    var authHeader = context.Request.Headers.GetValueOrDefault("Authorization", "");
                    if (!string.IsNullOrEmpty(authHeader) &lt;span class="err"&gt;&amp;amp;&amp;amp;&lt;/span&gt; authHeader.StartsWith("Bearer ")) {
                        var token = authHeader.Substring(7);
                        var jwt = System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ReadJwtToken(token);
                        userId = jwt.Claims.FirstOrDefault(c =&amp;gt; c.Type == "sub")?.Value ?? "";
                    }
                } catch {
                    userId = "jwt-parse-error";
                }

                return new JObject(
                    // Timestamps
                    new JProperty("timestamp", DateTime.UtcNow.ToString("o")),
                    new JProperty("requestStartTime", ((DateTime)context.Variables["requestStartTime"]).ToString("o")),

                    // Request metadata
                    new JProperty("requestId", context.RequestId),
                    new JProperty("operationId", context.Operation?.Id ?? ""),
                    new JProperty("apiId", context.Api?.Id ?? ""),

                    // Subscription details
                    new JProperty("subscriptionId", context.Subscription?.Id ?? "none"),
                    new JProperty("subscriptionName", context.Subscription?.Name ?? "none"),

                    // User context
                    new JProperty("userId", userId),
                    new JProperty("clientIp", context.Request.IpAddress),
                    new JProperty("userAgent", context.Request.Headers.GetValueOrDefault("User-Agent", "")),

                    // MCP-specific
                    new JProperty("mcpMethod", mcpMethod),
                    new JProperty("mcpId", mcpId),

                    // Response metadata (SAFE)
                    new JProperty("statusCode", context.Response.StatusCode),
                    new JProperty("statusReason", context.Response.StatusReason),

                    // Performance
                    new JProperty("elapsedMs", context.Elapsed.TotalMilliseconds),
                    new JProperty("backendTimeMs", context.Response.Headers.GetValueOrDefault("X-Backend-Time", "0")),

                    // Errors
                    new JProperty("isError", context.Response.StatusCode &amp;gt;= 400),
                    new JProperty("lastError", context.LastError?.Message ?? "")
                ).ToString();
            }
        &lt;span class="nt"&gt;&amp;lt;/log-to-eventhub&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- Response headers - return the request ID (original or generated) --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"X-Request-ID"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"skip"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;@(context.Request.Headers.GetValueOrDefault("X-Request-ID", context.RequestId))&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"X-RateLimit-Limit"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;100&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"X-RateLimit-Window"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;60&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/outbound&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;on-error&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!-- Error logging --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;log-to-eventhub&lt;/span&gt; &lt;span class="na"&gt;logger-id=&lt;/span&gt;&lt;span class="s"&gt;"mcp-audit-logger"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            @{
                return new JObject(
                    new JProperty("timestamp", DateTime.UtcNow.ToString("o")),
                    new JProperty("requestId", context.RequestId),
                    new JProperty("subscriptionId", context.Subscription?.Id ?? "none"),
                    new JProperty("isError", true),
                    new JProperty("errorSource", context.LastError?.Source ?? ""),
                    new JProperty("errorReason", context.LastError?.Reason ?? ""),
                    new JProperty("errorMessage", context.LastError?.Message ?? ""),
                    new JProperty("statusCode", context.Response.StatusCode),
                    new JProperty("elapsedMs", context.Elapsed.TotalMilliseconds)
                ).ToString();
            }
        &lt;span class="nt"&gt;&amp;lt;/log-to-eventhub&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/on-error&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/policies&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Distributed Tracing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Request ID Propagation
&lt;/h3&gt;

&lt;p&gt;Of course you want to be able to trace a request from end-to-end. Because, whats the use otherwise besides just burning money on logs?&lt;br&gt;
The policy above includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Inbound: Preserve existing or generate new --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"X-Request-ID"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"skip"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;@(context.RequestId)&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Outbound: Return the request ID (original or generated) --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"X-Request-ID"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"skip"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;@(context.Request.Headers.GetValueOrDefault("X-Request-ID", context.RequestId))&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;exists-action="skip"&lt;/code&gt; instead of &lt;code&gt;"override"&lt;/code&gt;. This preserves any &lt;code&gt;X-Request-ID&lt;/code&gt; that's already present from upstream services (AI agents, proxies, gateways). Only generate a new one if it doesn't exist.&lt;/p&gt;

&lt;p&gt;THe part in outbound makes sure we actually send this to the backend service, of course its your responsibility that it uses that ID for its own logging, and propagates it downstream as well. &lt;br&gt;
We dont want to break the chain. &lt;/p&gt;


&lt;h2&gt;
  
  
  What to Monitor
&lt;/h2&gt;

&lt;p&gt;Once Event Hub is streaming your logs, pipe them to whatever observability platform you're using:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common destinations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grafana Cloud&lt;/strong&gt; - Event Hub → Grafana Alloy → Grafana Cloud (uses Loki backend)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Datadog&lt;/strong&gt; - Event Hub → Azure Function forwarder → Datadog&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Splunk&lt;/strong&gt; - Event Hub → Splunk HEC connector&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application Insights&lt;/strong&gt; - Built-in Azure integration if you're all-in on Azure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Event Hub JSON format (shown in the policy above) works with pretty much any log aggregator. You're getting structured JSON with timestamps, request IDs, and all the metadata you need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key metrics to track:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Request Metrics:&lt;/em&gt; Requests/min, error rate by status code, P50/P95/P99 latency, rate limit hits (429s)&lt;/p&gt;

&lt;p&gt;&lt;em&gt;MCP-Specific:&lt;/em&gt; &lt;code&gt;tools/list&lt;/code&gt; vs &lt;code&gt;tools/call&lt;/code&gt; distribution, per-tool error rates, per-subscription usage patterns&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Security:&lt;/em&gt; Unauthorized attempts (401/403), invalid subscription keys, unusual client IPs, rate limit violations&lt;/p&gt;


&lt;h2&gt;
  
  
  Performance Considerations
&lt;/h2&gt;
&lt;h3&gt;
  
  
  What to Log (And What Not To)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Always log:&lt;/strong&gt; Request metadata, response status codes, timing, security context, and errors. These are safe, fast, and give you what compliance needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never log:&lt;/strong&gt; Response bodies (hangs requests), large payloads (kills performance), or sensitive data (violates compliance). Anything that slows down the critical path or exposes PII or PCI is out.&lt;/p&gt;

&lt;p&gt;The metadata-only approach handles millions of requests without breaking a sweat. You dont want to introduce verbose logs, slowing down your performance and loose money on people bailing out. &lt;/p&gt;
&lt;h3&gt;
  
  
  Sampling Strategy
&lt;/h3&gt;

&lt;p&gt;For high-volume APIs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Log 100% of errors, 10% of successes --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;choose&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;when&lt;/span&gt; &lt;span class="na"&gt;condition=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Response.StatusCode &amp;gt;= 400)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;log-to-eventhub&lt;/span&gt; &lt;span class="na"&gt;logger-id=&lt;/span&gt;&lt;span class="s"&gt;"mcp-audit-logger"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            @{ /* full logging */ }
        &lt;span class="nt"&gt;&amp;lt;/log-to-eventhub&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/when&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;when&lt;/span&gt; &lt;span class="na"&gt;condition=&lt;/span&gt;&lt;span class="s"&gt;"@(new Random().Next(100) &amp;lt; 10)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;log-to-eventhub&lt;/span&gt; &lt;span class="na"&gt;logger-id=&lt;/span&gt;&lt;span class="s"&gt;"mcp-audit-logger"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            @{ /* sampled logging */ }
        &lt;span class="nt"&gt;&amp;lt;/log-to-eventhub&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/when&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/choose&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Compliance &amp;amp; Audit Requirements
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Data Retention
&lt;/h3&gt;

&lt;p&gt;(Do check this with your compliance team, these are just guidelines)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Event Hub&lt;/strong&gt;: 7-90 days retention&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log Analytics&lt;/strong&gt;: 30-365 days retention&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Archive Storage&lt;/strong&gt;: Unlimited (cold storage)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Audit Trail Fields
&lt;/h3&gt;

&lt;p&gt;For compliance (SOC2, ISO 27001):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-11-05T10:30:45.123Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requestId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc-123-def-456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user@company.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subscriptionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Company-Production"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpMethod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tools/call"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"toolName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"processPayment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"clientIp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"203.0.113.42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"execute"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/api/payment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: If you have a WAF in front of your APIM, make sure to collect WAF logs as well, because they contain important security events (blocked attacks, suspicious IPs, etc).&lt;/p&gt;

&lt;h3&gt;
  
  
  GDPR Considerations
&lt;/h3&gt;

&lt;p&gt;During my career I have made my mistakes, and seen others make them. To prevent you ending up sanitizing logs, which is probably going to ruin your weekend, here are some guidelines:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For user data:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Don't log full request/response bodies (they can contain PII, PCI data)&lt;/li&gt;
&lt;li&gt;Log user IDs, but only if needed (pseudonymized from JWT claims) &lt;/li&gt;
&lt;li&gt;Support data deletion requests (Event Hub retention handles this)&lt;/li&gt;
&lt;li&gt;Implement retention policies (compliance will ask for this)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Before You Go Live
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Core Requirements (Everyone needs this):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Event Hub logger configured&lt;/li&gt;
&lt;li&gt;Audit logging policy deployed&lt;/li&gt;
&lt;li&gt;Response body access removed (no hanging, although you will notice this quite fast)&lt;/li&gt;
&lt;li&gt;Request IDs propagated to backends (preserving upstream IDs)&lt;/li&gt;
&lt;li&gt;Log retention policies set (check with compliance team)&lt;/li&gt;
&lt;li&gt;Sampling configured for high volume (if needed)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Monitoring Stack (Choose your poison):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Event Hub → Your SIEM/observability platform (Datadog, Grafana, Splunk, etc.)&lt;/li&gt;
&lt;li&gt;Or Application Insights (if you're all-in on Azure)&lt;/li&gt;
&lt;li&gt;Dashboards created (whatever tool you use)&lt;/li&gt;
&lt;li&gt;Alerts configured for errors/latency/security events to disrupt your sleep&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Security: locked down. Observability: sorted. Now let's tackle the elephant in the room: &lt;strong&gt;automation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://itsrene.nl/blog/rendered/azure-apim-mcp-gitops.html" rel="noopener noreferrer"&gt;Part 4: Azure APIM MCP Automation - The Bridging Problem&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;MCP servers don't support Terraform or ARM templates yet. Microsoft knows this is a gap and it's on the roadmap. In the meantime, Part 4 covers how to bridge it with governance-first automation—because manual Portal clicks don't scale.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm a Product Architect at &lt;a href="https://backbase.com" rel="noopener noreferrer"&gt;Backbase&lt;/a&gt;, where I design cloud-native banking platforms serving millions of users. The patterns in this series come from real production implementations at enterprise scale. Views are my own.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;How are you handling APIM observability? Share your patterns in the comments.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow me on &lt;a href="https://linkedin.com/in/renenijkamp" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; for more Azure and platform engineering content.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>observability</category>
      <category>mcp</category>
      <category>apim</category>
    </item>
    <item>
      <title>Securing Azure APIM MCP Servers in Production</title>
      <dc:creator>René Nijkamp</dc:creator>
      <pubDate>Tue, 25 Nov 2025 22:06:04 +0000</pubDate>
      <link>https://dev.to/itsrene/securing-azure-apim-mcp-servers-in-production-3gd3</link>
      <guid>https://dev.to/itsrene/securing-azure-apim-mcp-servers-in-production-3gd3</guid>
      <description>&lt;h1&gt;
  
  
  Securing Azure APIM MCP Servers in Production
&lt;/h1&gt;

&lt;p&gt;In &lt;a href="https://itsrene.nl/blog/posts/azure-apim-mcp-overview" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt;, I covered the good, bad, and ugly of Azure APIM MCP. Now let's talk about the &lt;strong&gt;security gaps&lt;/strong&gt; you need to address before going to production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here's the reality:&lt;/strong&gt; As a preview feature, Azure APIM MCP has permissive defaults that work great for prototyping but need hardening for production. You'll need to add security policies manually—Microsoft is actively working on better defaults, but let's not wait. Here's how to lock it down today.&lt;/p&gt;

&lt;p&gt;Let's break down what's exposed by default and the patterns to secure it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The &lt;code&gt;/tools/list&lt;/code&gt; Authentication Issue
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;Let's start with the most obvious one. The &lt;code&gt;/tools/list&lt;/code&gt; endpoint—used for tool discovery—&lt;strong&gt;doesn't enforce subscription key validation by default&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fc2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBBdHRhY2tlcgogICAgcGFydGljaXBhbnQgQVBJTQogICAgcGFydGljaXBhbnQgQmFja2VuZAogICAgCiAgICBOb3RlIG92ZXIgQXR0YWNrZXIsQmFja2VuZDogV0lUSE9VVCBGSVggKERlZmF1bHQgQmVoYXZpb3IpCiAgICBBdHRhY2tlci0%2BPkFQSU06IHRvb2xzL2xpc3QgKG5vIGF1dGggaGVhZGVyKQogICAgQVBJTS0tPj5BdHRhY2tlcjogRnVsbCB0b29sIGxpc3QKICAgIAogICAgTm90ZSBvdmVyIEF0dGFja2VyLEJhY2tlbmQ6IFdJVEggRklYIChNYW51YWwgUG9saWN5KQogICAgQXR0YWNrZXItPj5BUElNOiB0b29scy9saXN0IChubyBhdXRoIGhlYWRlcikKICAgIEFQSU0tPj5BUElNOiBWYWxpZGF0ZSBzdWJzY3JpcHRpb24ga2V5CiAgICBBUElNLS0%2BPkF0dGFja2VyOiA0MDEgVW5hdXRob3JpemVk%3Ftype%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fc2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBBdHRhY2tlcgogICAgcGFydGljaXBhbnQgQVBJTQogICAgcGFydGljaXBhbnQgQmFja2VuZAogICAgCiAgICBOb3RlIG92ZXIgQXR0YWNrZXIsQmFja2VuZDogV0lUSE9VVCBGSVggKERlZmF1bHQgQmVoYXZpb3IpCiAgICBBdHRhY2tlci0%2BPkFQSU06IHRvb2xzL2xpc3QgKG5vIGF1dGggaGVhZGVyKQogICAgQVBJTS0tPj5BdHRhY2tlcjogRnVsbCB0b29sIGxpc3QKICAgIAogICAgTm90ZSBvdmVyIEF0dGFja2VyLEJhY2tlbmQ6IFdJVEggRklYIChNYW51YWwgUG9saWN5KQogICAgQXR0YWNrZXItPj5BUElNOiB0b29scy9saXN0IChubyBhdXRoIGhlYWRlcikKICAgIEFQSU0tPj5BUElNOiBWYWxpZGF0ZSBzdWJzY3JpcHRpb24ga2V5CiAgICBBUElNLS0%2BPkF0dGFja2VyOiA0MDEgVW5hdXRob3JpemVk%3Ftype%3Dpng" alt="Diagram" width="684" height="521"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Anyone can enumerate your available tools without credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# No authentication required&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://your-apim.azure-api.net/your-api-mcp/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc": "2.0", "method": "tools/list", "id": 1}'&lt;/span&gt;

&lt;span class="c"&gt;# Returns full list of exposed tools&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"jsonrpc"&lt;/span&gt;: &lt;span class="s2"&gt;"2.0"&lt;/span&gt;,
  &lt;span class="s2"&gt;"id"&lt;/span&gt;: 1,
  &lt;span class="s2"&gt;"result"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"tools"&lt;/span&gt;: &lt;span class="o"&gt;[&lt;/span&gt;
      &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;: &lt;span class="s2"&gt;"getCustomer"&lt;/span&gt;, &lt;span class="s2"&gt;"description"&lt;/span&gt;: &lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;,
      &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;: &lt;span class="s2"&gt;"updateProfile"&lt;/span&gt;, &lt;span class="s2"&gt;"description"&lt;/span&gt;: &lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;,
      &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;: &lt;span class="s2"&gt;"processPayment"&lt;/span&gt;, &lt;span class="s2"&gt;"description"&lt;/span&gt;: &lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;]&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is &lt;strong&gt;information disclosure&lt;/strong&gt;—tool names and descriptions can reveal business logic, which you'll want to lock down for production environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix: Explicit Subscription Key Validation
&lt;/h3&gt;

&lt;p&gt;Add subscription key validation to your MCP server policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;policies&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;inbound&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!-- Step 1: Check if subscription key is present --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;choose&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;when&lt;/span&gt; &lt;span class="na"&gt;condition=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Request.Headers.GetValueOrDefault(\"&lt;/span&gt;&lt;span class="err"&gt;Ocp-Apim-Subscription-Key\",&lt;/span&gt; &lt;span class="err"&gt;\"\")&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="err"&gt;\"\")"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;return-response&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-status&lt;/span&gt; &lt;span class="na"&gt;code=&lt;/span&gt;&lt;span class="s"&gt;"401"&lt;/span&gt; &lt;span class="na"&gt;reason=&lt;/span&gt;&lt;span class="s"&gt;"Unauthorized"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;application/json&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-body&amp;gt;&lt;/span&gt;{"error": "Subscription key required"}&lt;span class="nt"&gt;&amp;lt;/set-body&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/return-response&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/when&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/choose&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- Step 2: Store the subscription key (APIM strips it during processing) --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-variable&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"originalSubKey"&lt;/span&gt; 
                      &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Request.Headers.GetValueOrDefault(\"&lt;/span&gt;&lt;span class="err"&gt;Ocp-Apim-Subscription-Key\",&lt;/span&gt; &lt;span class="err"&gt;\"\"))"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- Step 3: Validate subscription after base processing --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;choose&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;when&lt;/span&gt; &lt;span class="na"&gt;condition=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Subscription == null || context.Subscription.Id == null)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;return-response&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-status&lt;/span&gt; &lt;span class="na"&gt;code=&lt;/span&gt;&lt;span class="s"&gt;"401"&lt;/span&gt; &lt;span class="na"&gt;reason=&lt;/span&gt;&lt;span class="s"&gt;"Unauthorized"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;application/json&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-body&amp;gt;&lt;/span&gt;{"error": "Invalid subscription key"}&lt;span class="nt"&gt;&amp;lt;/set-body&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/return-response&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/when&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/choose&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- Step 4: Re-inject subscription key for backend APIs --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Ocp-Apim-Subscription-Key"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;@((string)context.Variables[\"originalSubKey\"])&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/inbound&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;backend&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/backend&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;outbound&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/outbound&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;on-error&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/on-error&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/policies&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why Re-injection is Necessary
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Here's the catch:&lt;/strong&gt; APIM base policies &lt;em&gt;might&lt;/em&gt; strip the &lt;code&gt;Ocp-Apim-Subscription-Key&lt;/code&gt; header during policy processing, depending on your base policy configuration. This prevents inbound sensitive data from leaking to downstream services. But your &lt;strong&gt;backend APIs&lt;/strong&gt; need that key for their own validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is different from standard APIM patterns.&lt;/strong&gt; Normally, APIM validates the request, strips sensitive headers, and calls the backend. Done. With MCP tool calls, the flow is different:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBBW01DUCBDbGllbnRdIC0tPiBCW0FQSU0gTUNQIFNlcnZlciB2YWxpZGF0ZXNdCiAgICBCIC0tPiBDW0FQSU0gdmFsaWRhdGVzXQogICAgQyAtLT4gRFtCYWNrZW5kXQ%3D%3D%3Ftype%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBBW01DUCBDbGllbnRdIC0tPiBCW0FQSU0gTUNQIFNlcnZlciB2YWxpZGF0ZXNdCiAgICBCIC0tPiBDW0FQSU0gdmFsaWRhdGVzXQogICAgQyAtLT4gRFtCYWNrZW5kXQ%3D%3D%3Ftype%3Dpng" alt="Diagram" width="784" height="64"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Is that double validation? Yes. It's a tradeoff when you expose your APIs as MCP tools—but it's efficient and easily solvable with the re-injection pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The re-injection pattern:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Store the key in a variable &lt;strong&gt;before&lt;/strong&gt; &lt;code&gt;&amp;lt;base /&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Let APIM process the request&lt;/li&gt;
&lt;li&gt;Re-inject the key &lt;strong&gt;after&lt;/strong&gt; &lt;code&gt;&amp;lt;base /&amp;gt;&lt;/code&gt; so it reaches your backend&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Subscription-Based Access Control
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Product Authorization Limitation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Normally&lt;/strong&gt;, APIM's standard access control uses &lt;strong&gt;Products&lt;/strong&gt;—logical groupings of APIs with different access levels (think: Starter, Professional, Enterprise plans for your customers). You check &lt;code&gt;context.Product&lt;/code&gt; in policies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This isn't available for MCP servers yet.&lt;/strong&gt; &lt;code&gt;context.Product&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt;. Microsoft is aware of this limitation, but for now we'll need an alternative approach.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alternative: Subscription Whitelisting
&lt;/h3&gt;

&lt;p&gt;Use subscription IDs for access control:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;policies&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;inbound&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!-- Basic authentication (from previous example) --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;choose&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;when&lt;/span&gt; &lt;span class="na"&gt;condition=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Request.Headers.GetValueOrDefault(\"&lt;/span&gt;&lt;span class="err"&gt;Ocp-Apim-Subscription-Key\",&lt;/span&gt; &lt;span class="err"&gt;\"\")&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="err"&gt;\"\")"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;return-response&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-status&lt;/span&gt; &lt;span class="na"&gt;code=&lt;/span&gt;&lt;span class="s"&gt;"401"&lt;/span&gt; &lt;span class="na"&gt;reason=&lt;/span&gt;&lt;span class="s"&gt;"Unauthorized"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-body&amp;gt;&lt;/span&gt;{"error": "Subscription key required"}&lt;span class="nt"&gt;&amp;lt;/set-body&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/return-response&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/when&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/choose&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;set-variable&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"originalSubKey"&lt;/span&gt; 
                      &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Request.Headers.GetValueOrDefault(\"&lt;/span&gt;&lt;span class="err"&gt;Ocp-Apim-Subscription-Key\",&lt;/span&gt; &lt;span class="err"&gt;\"\"))"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- Validate subscription exists --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;choose&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;when&lt;/span&gt; &lt;span class="na"&gt;condition=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Subscription == null || context.Subscription.Id == null)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;return-response&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-status&lt;/span&gt; &lt;span class="na"&gt;code=&lt;/span&gt;&lt;span class="s"&gt;"401"&lt;/span&gt; &lt;span class="na"&gt;reason=&lt;/span&gt;&lt;span class="s"&gt;"Unauthorized"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-body&amp;gt;&lt;/span&gt;{"error": "Invalid subscription key"}&lt;span class="nt"&gt;&amp;lt;/set-body&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/return-response&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/when&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/choose&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- Subscription whitelist for MCP access --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;choose&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;when&lt;/span&gt; &lt;span class="na"&gt;condition=&lt;/span&gt;&lt;span class="s"&gt;"@(!new string[] {
                \"&lt;/span&gt;&lt;span class="err"&gt;sub-id-premium-1\",&lt;/span&gt; 
                &lt;span class="err"&gt;\"sub-id-premium-2\",&lt;/span&gt; 
                &lt;span class="err"&gt;\"sub-id-enterprise-1\"&lt;/span&gt;
            &lt;span class="err"&gt;}.Contains(context.Subscription.Id))"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;return-response&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-status&lt;/span&gt; &lt;span class="na"&gt;code=&lt;/span&gt;&lt;span class="s"&gt;"403"&lt;/span&gt; &lt;span class="na"&gt;reason=&lt;/span&gt;&lt;span class="s"&gt;"Forbidden"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;application/json&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-body&amp;gt;&lt;/span&gt;{"error": "Access denied - MCP access not authorized for this subscription"}&lt;span class="nt"&gt;&amp;lt;/set-body&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/return-response&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/when&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/choose&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- Re-inject subscription key --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Ocp-Apim-Subscription-Key"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;@((string)context.Variables[\"originalSubKey\"])&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/inbound&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;backend&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/backend&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;outbound&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/outbound&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;on-error&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/on-error&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/policies&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Alternative: Subscription Name Pattern&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you don't want to maintain ID lists, use naming conventions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;when&lt;/span&gt; &lt;span class="na"&gt;condition=&lt;/span&gt;&lt;span class="s"&gt;"@(!context.Subscription.Name.Contains(\"&lt;/span&gt;&lt;span class="err"&gt;MCP-Enabled\"))"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;return-response&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-status&lt;/span&gt; &lt;span class="na"&gt;code=&lt;/span&gt;&lt;span class="s"&gt;"403"&lt;/span&gt; &lt;span class="na"&gt;reason=&lt;/span&gt;&lt;span class="s"&gt;"Forbidden"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-body&amp;gt;&lt;/span&gt;{"error": "MCP access not authorized"}&lt;span class="nt"&gt;&amp;lt;/set-body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/return-response&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/when&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then name your subscriptions: &lt;code&gt;Company-ABC-MCP-Enabled&lt;/code&gt;, &lt;code&gt;Team-XYZ-MCP-Enabled&lt;/code&gt;, etc.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rate Limiting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;This one is critical.&lt;/strong&gt; Rate limiting for MCP servers is essential—you're exposing APIs to AI agents that can (and will) spam requests if they're having a bad day, giving you a horrible day.&lt;/p&gt;

&lt;h3&gt;
  
  
  Global Rate Limiting
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;policies&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;inbound&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!-- Authentication policies first --&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- Rate limiting AFTER authentication --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;rate-limit&lt;/span&gt; &lt;span class="na"&gt;calls=&lt;/span&gt;&lt;span class="s"&gt;"100"&lt;/span&gt; &lt;span class="na"&gt;renewal-period=&lt;/span&gt;&lt;span class="s"&gt;"60"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- Rest of inbound policies --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/inbound&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;outbound&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!-- Add rate limit headers to responses --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"X-RateLimit-Limit"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;100&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"X-RateLimit-Window"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;60&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/outbound&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/policies&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Place rate limiting &lt;strong&gt;after&lt;/strong&gt; authentication. Otherwise, attackers can exhaust your rate limits without valid credentials. Don't make it that easy for them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rate Limiting Behavior
&lt;/h3&gt;

&lt;p&gt;When limits are exceeded:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;APIM returns &lt;strong&gt;HTTP 429&lt;/strong&gt; (Too Many Requests)&lt;/li&gt;
&lt;li&gt;Includes &lt;code&gt;Retry-After&lt;/code&gt; header&lt;/li&gt;
&lt;li&gt;Counters reset after renewal period&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Test it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Rapid-fire requests&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;1..150&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://your-apim.azure-api.net/your-api-mcp/mcp &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Ocp-Apim-Subscription-Key: YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc": "2.0", "method": "tools/list", "id": '&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s1"&gt;'}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"Request &lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;: %{http_code}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# First 100: HTTP 200&lt;/span&gt;
&lt;span class="c"&gt;# Requests 101+: HTTP 429&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  User Context Propagation (JWT Tokens)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;When you need user context:&lt;/strong&gt; For authenticated operations where users interact through AI agents, you need to propagate user identity.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Challenge
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Consider the following flow:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBBW1VzZXIgQnJvd3Nlcl0gLS0%2BfEpXVHwgQltBSSBBZ2VudF0KICAgIEIgLS0%2BfEpXVHwgQ1tNQ1AgU2VydmVyXQogICAgQyAtLT58SldUfCBEW0JhY2tlbmQgQVBJXQ%3D%3D%3Ftype%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZmxvd2NoYXJ0IExSCiAgICBBW1VzZXIgQnJvd3Nlcl0gLS0%2BfEpXVHwgQltBSSBBZ2VudF0KICAgIEIgLS0%2BfEpXVHwgQ1tNQ1AgU2VydmVyXQogICAgQyAtLT58SldUfCBEW0JhY2tlbmQgQVBJXQ%3D%3D%3Ftype%3Dpng" alt="Diagram" width="784" height="65"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each hop must preserve and validate the JWT token. Let's make sure that happens. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assumption:&lt;/strong&gt; Your APIM base policy strips Authorization headers (which is good security practice—limits unwanted token exposure downstream). That's why we need the same re-injection pattern. &lt;/p&gt;

&lt;h3&gt;
  
  
  JWT Forwarding Policy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;policies&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;inbound&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!-- Subscription key validation --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;choose&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;when&lt;/span&gt; &lt;span class="na"&gt;condition=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Request.Headers.GetValueOrDefault(\"&lt;/span&gt;&lt;span class="err"&gt;Ocp-Apim-Subscription-Key\",&lt;/span&gt; &lt;span class="err"&gt;\"\")&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="err"&gt;\"\")"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;return-response&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-status&lt;/span&gt; &lt;span class="na"&gt;code=&lt;/span&gt;&lt;span class="s"&gt;"401"&lt;/span&gt; &lt;span class="na"&gt;reason=&lt;/span&gt;&lt;span class="s"&gt;"Unauthorized"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-body&amp;gt;&lt;/span&gt;{"error": "Subscription key required"}&lt;span class="nt"&gt;&amp;lt;/set-body&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/return-response&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/when&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/choose&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- Store BOTH subscription key AND JWT token --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-variable&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"originalSubKey"&lt;/span&gt; 
                      &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Request.Headers.GetValueOrDefault(\"&lt;/span&gt;&lt;span class="err"&gt;Ocp-Apim-Subscription-Key\",&lt;/span&gt; &lt;span class="err"&gt;\"\"))"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-variable&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"originalJwtToken"&lt;/span&gt; 
                      &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Request.Headers.GetValueOrDefault(\"&lt;/span&gt;&lt;span class="err"&gt;Authorization\",&lt;/span&gt; &lt;span class="err"&gt;\"\"))"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- Subscription validation --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;choose&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;when&lt;/span&gt; &lt;span class="na"&gt;condition=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Subscription == null || context.Subscription.Id == null)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;return-response&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-status&lt;/span&gt; &lt;span class="na"&gt;code=&lt;/span&gt;&lt;span class="s"&gt;"401"&lt;/span&gt; &lt;span class="na"&gt;reason=&lt;/span&gt;&lt;span class="s"&gt;"Unauthorized"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-body&amp;gt;&lt;/span&gt;{"error": "Invalid subscription key"}&lt;span class="nt"&gt;&amp;lt;/set-body&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/return-response&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/when&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/choose&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- Re-inject both headers --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Ocp-Apim-Subscription-Key"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;@((string)context.Variables[\"originalSubKey\"])&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;@((string)context.Variables[\"originalJwtToken\"])&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/inbound&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;backend&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/backend&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;outbound&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/outbound&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;on-error&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/on-error&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/policies&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Optional: JWT Validation in APIM
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Why validate in APIM?&lt;/strong&gt; The forwarding pattern passes tokens through without checking them. If you want APIM to verify tokens are valid &lt;em&gt;before&lt;/em&gt; they reach your backend, add JWT validation.&lt;/p&gt;

&lt;p&gt;This adds an extra security layer—invalid or expired tokens get rejected at the gateway, not at your backend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;validate-jwt&lt;/span&gt; &lt;span class="na"&gt;header-name=&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt; 
              &lt;span class="na"&gt;failed-validation-httpcode=&lt;/span&gt;&lt;span class="s"&gt;"401"&lt;/span&gt; 
              &lt;span class="na"&gt;failed-validation-error-message=&lt;/span&gt;&lt;span class="s"&gt;"Unauthorized. Valid JWT required."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;openid-config&lt;/span&gt; &lt;span class="na"&gt;url=&lt;/span&gt;&lt;span class="s"&gt;"https://your-auth-server/.well-known/openid-configuration"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;audiences&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;audience&amp;gt;&lt;/span&gt;https://your-apim.azure-api.net&lt;span class="nt"&gt;&amp;lt;/audience&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/audiences&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;required-claims&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;claim&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"scope"&lt;/span&gt; &lt;span class="na"&gt;match=&lt;/span&gt;&lt;span class="s"&gt;"any"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;mcp:read&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;mcp:execute&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/claim&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/required-claims&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/validate-jwt&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key configuration points:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;openid-config&lt;/strong&gt;: Points to your identity provider's discovery endpoint (Azure AD, Auth0, Okta, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;audiences&lt;/strong&gt;: Must match the &lt;code&gt;aud&lt;/code&gt; claim in your JWT—typically your APIM gateway URL or API identifier&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;required-claims&lt;/strong&gt;: Enforces specific scopes—adjust these to match your authorization model&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Complete Production-Ready Security Policy
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Putting it all together:&lt;/strong&gt; Here's the complete security policy combining all the patterns above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;policies&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;inbound&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!-- CORS (if needed for browser clients) --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;cors&lt;/span&gt; &lt;span class="na"&gt;allow-credentials=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;allowed-origins&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;origin&amp;gt;&lt;/span&gt;https://your-app.com&lt;span class="nt"&gt;&amp;lt;/origin&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/allowed-origins&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;allowed-methods&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;method&amp;gt;&lt;/span&gt;POST&lt;span class="nt"&gt;&amp;lt;/method&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;method&amp;gt;&lt;/span&gt;OPTIONS&lt;span class="nt"&gt;&amp;lt;/method&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/allowed-methods&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;allowed-headers&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;header&amp;gt;&lt;/span&gt;Content-Type&lt;span class="nt"&gt;&amp;lt;/header&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;header&amp;gt;&lt;/span&gt;Ocp-Apim-Subscription-Key&lt;span class="nt"&gt;&amp;lt;/header&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;header&amp;gt;&lt;/span&gt;Authorization&lt;span class="nt"&gt;&amp;lt;/header&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/allowed-headers&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/cors&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- 1. Subscription key validation --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;choose&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;when&lt;/span&gt; &lt;span class="na"&gt;condition=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Request.Headers.GetValueOrDefault(\"&lt;/span&gt;&lt;span class="err"&gt;Ocp-Apim-Subscription-Key\",&lt;/span&gt; &lt;span class="err"&gt;\"\")&lt;/span&gt; &lt;span class="err"&gt;==&lt;/span&gt; &lt;span class="err"&gt;\"\")"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;return-response&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-status&lt;/span&gt; &lt;span class="na"&gt;code=&lt;/span&gt;&lt;span class="s"&gt;"401"&lt;/span&gt; &lt;span class="na"&gt;reason=&lt;/span&gt;&lt;span class="s"&gt;"Unauthorized"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;application/json&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-body&amp;gt;&lt;/span&gt;{"error": "Subscription key required"}&lt;span class="nt"&gt;&amp;lt;/set-body&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/return-response&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/when&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/choose&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- 2. Store headers before base processing --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-variable&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"originalSubKey"&lt;/span&gt; 
                      &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Request.Headers.GetValueOrDefault(\"&lt;/span&gt;&lt;span class="err"&gt;Ocp-Apim-Subscription-Key\",&lt;/span&gt; &lt;span class="err"&gt;\"\"))"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-variable&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"originalJwtToken"&lt;/span&gt; 
                      &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Request.Headers.GetValueOrDefault(\"&lt;/span&gt;&lt;span class="err"&gt;Authorization\",&lt;/span&gt; &lt;span class="err"&gt;\"\"))"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- 3. Rate limiting --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;rate-limit&lt;/span&gt; &lt;span class="na"&gt;calls=&lt;/span&gt;&lt;span class="s"&gt;"100"&lt;/span&gt; &lt;span class="na"&gt;renewal-period=&lt;/span&gt;&lt;span class="s"&gt;"60"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- 4. Subscription validation --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;choose&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;when&lt;/span&gt; &lt;span class="na"&gt;condition=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Subscription == null || context.Subscription.Id == null)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;return-response&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-status&lt;/span&gt; &lt;span class="na"&gt;code=&lt;/span&gt;&lt;span class="s"&gt;"401"&lt;/span&gt; &lt;span class="na"&gt;reason=&lt;/span&gt;&lt;span class="s"&gt;"Unauthorized"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;application/json&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-body&amp;gt;&lt;/span&gt;{"error": "Invalid subscription key"}&lt;span class="nt"&gt;&amp;lt;/set-body&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/return-response&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/when&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/choose&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- 5. Subscription whitelist --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;choose&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;when&lt;/span&gt; &lt;span class="na"&gt;condition=&lt;/span&gt;&lt;span class="s"&gt;"@(!new string[] {\"&lt;/span&gt;&lt;span class="err"&gt;authorized-sub-1\",&lt;/span&gt; &lt;span class="err"&gt;\"authorized-sub-2\"}.Contains(context.Subscription.Id))"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;return-response&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-status&lt;/span&gt; &lt;span class="na"&gt;code=&lt;/span&gt;&lt;span class="s"&gt;"403"&lt;/span&gt; &lt;span class="na"&gt;reason=&lt;/span&gt;&lt;span class="s"&gt;"Forbidden"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                        &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;application/json&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
                    &lt;span class="nt"&gt;&amp;lt;set-body&amp;gt;&lt;/span&gt;{"error": "MCP access not authorized"}&lt;span class="nt"&gt;&amp;lt;/set-body&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/return-response&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/when&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/choose&amp;gt;&lt;/span&gt;

        &lt;span class="c"&gt;&amp;lt;!-- 6. Re-inject headers --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Ocp-Apim-Subscription-Key"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;@((string)context.Variables[\"originalSubKey\"])&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;@((string)context.Variables[\"originalJwtToken\"])&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/inbound&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;backend&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/backend&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;outbound&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!-- Rate limit headers --&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"X-RateLimit-Limit"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;100&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"X-RateLimit-Window"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;60&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/outbound&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;on-error&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/on-error&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/policies&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Testing Your Security
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Test Unauthorized Access
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Should fail with 401&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://your-apim.azure-api.net/your-api-mcp/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc": "2.0", "method": "tools/list", "id": 1}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Test Invalid Subscription Key
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Should fail with 401&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://your-apim.azure-api.net/your-api-mcp/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Ocp-Apim-Subscription-Key: INVALID_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc": "2.0", "method": "tools/list", "id": 1}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Test Valid Access
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Should succeed with 200&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://your-apim.azure-api.net/your-api-mcp/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Ocp-Apim-Subscription-Key: YOUR_VALID_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc": "2.0", "method": "tools/list", "id": 1}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Test Rate Limiting
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Rapid requests to trigger 429&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;1..150&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://your-apim.azure-api.net/your-api-mcp/mcp &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Ocp-Apim-Subscription-Key: YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc": "2.0", "method": "tools/list", "id": '&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s1"&gt;'}'&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"429"&lt;/span&gt;
&lt;span class="c"&gt;# Should show ~50 (requests 101-150)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Security Checklist
&lt;/h2&gt;

&lt;p&gt;Before going to production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/tools/list&lt;/code&gt; requires subscription key&lt;/li&gt;
&lt;li&gt;Invalid subscription keys return 401&lt;/li&gt;
&lt;li&gt;Subscription whitelist implemented&lt;/li&gt;
&lt;li&gt;Rate limiting configured and tested&lt;/li&gt;
&lt;li&gt;JWT tokens forwarded (if needed)&lt;/li&gt;
&lt;li&gt;CORS configured correctly (if needed)&lt;/li&gt;
&lt;li&gt;Rate limit headers returned&lt;/li&gt;
&lt;li&gt;All security tests passing&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Security is locked down. Now you need &lt;strong&gt;observability&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Coming soon:&lt;/strong&gt; Part 3 - Audit Logging &amp;amp; Monitoring That Actually Works&lt;/p&gt;

&lt;p&gt;In Part 3, I'll show you how to implement audit logging without the request-hanging bugs, plus distributed tracing and error handling.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm a Product Architect at &lt;a href="https://backbase.com" rel="noopener noreferrer"&gt;Backbase&lt;/a&gt;, where I design cloud-native banking platforms serving millions of users. The patterns in this series come from real production implementations at enterprise scale. Views are my own.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Have security patterns you're using for MCP servers? Share them in the comments.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow with me on &lt;a href="https://linkedin.com/in/renenijkamp" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; for more content.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>security</category>
      <category>mcp</category>
      <category>apim</category>
    </item>
    <item>
      <title>Azure APIM MCP: The Good, The Bad, The Ugly</title>
      <dc:creator>René Nijkamp</dc:creator>
      <pubDate>Tue, 18 Nov 2025 17:30:44 +0000</pubDate>
      <link>https://dev.to/itsrene/azure-apim-mcp-the-good-the-bad-the-ugly-3gl7</link>
      <guid>https://dev.to/itsrene/azure-apim-mcp-the-good-the-bad-the-ugly-3gl7</guid>
      <description>&lt;h1&gt;
  
  
  Azure APIM MCP: The Good, The Bad, The Ugly
&lt;/h1&gt;

&lt;p&gt;AI agents need APIs. But not just any APIs—they need &lt;strong&gt;discoverable, secure, governed&lt;/strong&gt; APIs that won't accidentally delete your production database when an LLM hallucinates.&lt;/p&gt;

&lt;p&gt;Enter the &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt;—a standardized way to expose APIs as tools that AI agents can discover and invoke safely. And Azure API Management (APIM) now has preview support for it.&lt;/p&gt;

&lt;p&gt;Sounds great, right?&lt;/p&gt;

&lt;p&gt;Well... sort of. I spent the last few months test driving this for a banking platform, and let me tell you: &lt;strong&gt;the Azure Portal screenshots don't tell the whole story&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is Part 1 of a 4-part series on building production-ready MCP servers on Azure APIM. I'll share what I learned the hard way, so you don't have to.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is MCP? (And Why Should You Care?)
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Model Context Protocol&lt;/strong&gt; is OpenAPI for AI agents. It gives agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tool Discovery&lt;/strong&gt;: Agents ask "what can I do?" and get structured responses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safe Execution&lt;/strong&gt;: Schemas prevent garbage-in-garbage-out&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context Awareness&lt;/strong&gt;: Agents know what tools exist and when to use them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of throwing API endpoints at LLMs and hoping for the best, you give them curated tools with schemas, descriptions, and built-in safety.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why APIM?
&lt;/h3&gt;

&lt;p&gt;If you already have APIM, why build a separate MCP server? Your API gateway already does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Authentication &amp;amp; authorization&lt;/li&gt;
&lt;li&gt;Rate limiting &amp;amp; throttling&lt;/li&gt;
&lt;li&gt;Logging &amp;amp; monitoring
&lt;/li&gt;
&lt;li&gt;Policy enforcement&lt;/li&gt;
&lt;li&gt;Multi-environment management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why add another service to manage?&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture Overview
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggTFIKICAgIEFbQUkgQWdlbnRdIC0tPnxKU09OLVJQQyAyLjB8IEJbQXp1cmUgQVBJTSBNQ1BdCiAgICBCIC0tPnx0b29scy9saXN0fCBDW1Rvb2wgRGlzY292ZXJ5XQogICAgQiAtLT58dG9vbHMvY2FsbHwgRFtBUEkgR2F0ZXdheV0KICAgIEQgLS0%2BfEhUVFB8IEVbQmFja2VuZCBBUElzXQogICAgCiAgICBCIC0uLT58UG9saWNpZXN8IEZbUmF0ZSBMaW1pdGluZ10KICAgIEIgLS4tPnxQb2xpY2llc3wgR1tBdXRoL0F1dGhaXQogICAgQiAtLi0%2BfFBvbGljaWVzfCBIW0F1ZGl0IExvZ2dpbmddCiAgICAKICAgIEUgLS0%2BfEN1c3RvbWVyIEFQSXwgSVtEYXRhYmFzZV0KICAgIEUgLS0%2BfFBheW1lbnQgQVBJfCBKW1BheW1lbnQgR2F0ZXdheV0KICAgIEUgLS0%2BfEFuYWx5dGljcyBBUEl8IEtbRGF0YSBXYXJlaG91c2VdCiAgICAKICAgIHN0eWxlIEIgZmlsbDojMDA3OEQ0LGNvbG9yOiNmZmYKICAgIHN0eWxlIEEgZmlsbDojMTBCOTgxLGNvbG9yOiNmZmYKICAgIHN0eWxlIEUgZmlsbDojRjU5RTBCLGNvbG9yOiNmZmY%3D%3Ftype%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2FZ3JhcGggTFIKICAgIEFbQUkgQWdlbnRdIC0tPnxKU09OLVJQQyAyLjB8IEJbQXp1cmUgQVBJTSBNQ1BdCiAgICBCIC0tPnx0b29scy9saXN0fCBDW1Rvb2wgRGlzY292ZXJ5XQogICAgQiAtLT58dG9vbHMvY2FsbHwgRFtBUEkgR2F0ZXdheV0KICAgIEQgLS0%2BfEhUVFB8IEVbQmFja2VuZCBBUElzXQogICAgCiAgICBCIC0uLT58UG9saWNpZXN8IEZbUmF0ZSBMaW1pdGluZ10KICAgIEIgLS4tPnxQb2xpY2llc3wgR1tBdXRoL0F1dGhaXQogICAgQiAtLi0%2BfFBvbGljaWVzfCBIW0F1ZGl0IExvZ2dpbmddCiAgICAKICAgIEUgLS0%2BfEN1c3RvbWVyIEFQSXwgSVtEYXRhYmFzZV0KICAgIEUgLS0%2BfFBheW1lbnQgQVBJfCBKW1BheW1lbnQgR2F0ZXdheV0KICAgIEUgLS0%2BfEFuYWx5dGljcyBBUEl8IEtbRGF0YSBXYXJlaG91c2VdCiAgICAKICAgIHN0eWxlIEIgZmlsbDojMDA3OEQ0LGNvbG9yOiNmZmYKICAgIHN0eWxlIEEgZmlsbDojMTBCOTgxLGNvbG9yOiNmZmYKICAgIHN0eWxlIEUgZmlsbDojRjU5RTBCLGNvbG9yOiNmZmY%3D%3Ftype%3Dpng" alt="Diagram" width="1347" height="486"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Good: What Actually Works
&lt;/h2&gt;

&lt;p&gt;APIM V2 Standard has preview MCP support. When it works, it's solid:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Native MCP Server Functionality
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Expose APIs as MCP Tools&lt;/strong&gt; with a few clicks (or REST API calls):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Discover available tools&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://your-apim.azure-api.net/your-api-mcp/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Ocp-Apim-Subscription-Key: YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc": "2.0", "method": "tools/list", "id": 1}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"getCustomer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Retrieve customer information"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"inputSchema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. JSON-RPC 2.0 Protocol
&lt;/h3&gt;

&lt;p&gt;Standard &lt;code&gt;/tools/list&lt;/code&gt; and &lt;code&gt;/tools/call&lt;/code&gt; endpoints that work with any MCP client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Invoke a tool&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://your-apim.azure-api.net/your-api-mcp/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Ocp-Apim-Subscription-Key: YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "jsonrpc": "2.0",
    "method": "tools/call",
    "params": {
      "name": "getCustomer",
      "arguments": {"id": "123"}
    },
    "id": 2
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. APIM Policy Framework
&lt;/h3&gt;

&lt;p&gt;You get the full power of APIM policies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rate limiting&lt;/li&gt;
&lt;li&gt;Request/response transformation&lt;/li&gt;
&lt;li&gt;Header manipulation&lt;/li&gt;
&lt;li&gt;Custom validation&lt;/li&gt;
&lt;li&gt;Backend routing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is &lt;strong&gt;huge&lt;/strong&gt;—you can apply enterprise-grade policies to your MCP endpoints.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Zero Additional Infrastructure
&lt;/h3&gt;

&lt;p&gt;If you already have APIM (and most enterprises do), you're paying &lt;strong&gt;$0 extra&lt;/strong&gt; for MCP capabilities. No new services to deploy, manage, or monitor.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bad: What's Frustrating
&lt;/h2&gt;

&lt;p&gt;Now for the frustrating parts:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. No Infrastructure as Code Support
&lt;/h3&gt;

&lt;p&gt;This is the &lt;strong&gt;biggest gap&lt;/strong&gt;. Currently, you cannot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Define MCP servers in ARM templates&lt;/li&gt;
&lt;li&gt;Manage them with Terraform&lt;/li&gt;
&lt;li&gt;Use Azure Service Operator (ASO)&lt;/li&gt;
&lt;li&gt;Deploy via standard GitOps pipelines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For now, you'll need &lt;strong&gt;manual Azure Portal configuration&lt;/strong&gt; or custom REST API automation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Microsoft has acknowledged this feedback and it's on their roadmap. The team is actively listening to community input on IaC priorities. I'll keep this series updated as features land.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2. Documentation Still Maturing
&lt;/h3&gt;

&lt;p&gt;As with most preview features, documentation is evolving. Some areas that need more coverage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How &lt;code&gt;context.Product&lt;/code&gt; behaves in MCP policies (currently unsupported)&lt;/li&gt;
&lt;li&gt;Which headers are preserved through MCP translation&lt;/li&gt;
&lt;li&gt;Best practices for user context propagation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The docs cover the core scenarios well, but production edge cases require experimentation. Microsoft is actively expanding the documentation based on community feedback.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Preview Tier Considerations
&lt;/h3&gt;

&lt;p&gt;As a &lt;strong&gt;preview feature&lt;/strong&gt;, keep in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Features may evolve based on feedback&lt;/li&gt;
&lt;li&gt;Standard preview support channels apply&lt;/li&gt;
&lt;li&gt;No production SLA guarantees yet&lt;/li&gt;
&lt;li&gt;Breaking changes are possible (though Microsoft typically provides migration paths)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For regulated environments, you'll need to assess your risk tolerance and plan for potential changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Ugly: Known Limitations to Address
&lt;/h2&gt;

&lt;p&gt;These are current limitations that require workarounds in production:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The &lt;code&gt;/tools/list&lt;/code&gt; Authentication Behavior
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;By default, the &lt;code&gt;/tools/list&lt;/code&gt; endpoint doesn't enforce subscription key validation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This means tools can be enumerated without authentication (by design for some use cases, but problematic for others).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# This works without any authentication :-/&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://your-apim.azure-api.net/your-api-mcp/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc": "2.0", "method": "tools/list", "id": 1}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You &lt;strong&gt;must&lt;/strong&gt; manually add subscription key validation in your MCP server policy. (I'll show you how in &lt;a href="https://itsrene.nl/blog/posts/azure-apim-mcp-security" rel="noopener noreferrer"&gt;Part 2&lt;/a&gt;)&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;context.Product&lt;/code&gt; Not Yet Supported
&lt;/h3&gt;

&lt;p&gt;APIM's Product-based access control model isn't currently available for MCP servers. &lt;code&gt;context.Product&lt;/code&gt; returns &lt;code&gt;null&lt;/code&gt; in MCP policies.&lt;/p&gt;

&lt;p&gt;For now, you'll need subscription-level authorization instead of product-based tiers. Microsoft is aware of this limitation.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. HTTP Status Code Masking
&lt;/h3&gt;

&lt;p&gt;Backend returns HTTP 500? APIM gives you &lt;strong&gt;HTTP 200&lt;/strong&gt; with an error in the JSON body.&lt;/p&gt;

&lt;p&gt;This breaks &lt;strong&gt;every standard HTTP error handling pattern&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Direct API call&lt;/span&gt;
curl https://apim.azure-api.net/api/posts/999
&lt;span class="c"&gt;# HTTP 500 &lt;/span&gt;

&lt;span class="c"&gt;# Same API via MCP&lt;/span&gt;
curl https://apim.azure-api.net/api-mcp/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc": "2.0", "method": "tools/call", "params": {"name": "getPost", "arguments": {"id": "999"}}, "id": 1}'&lt;/span&gt;
&lt;span class="c"&gt;# HTTP 200 with error in body... Say what now?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your monitoring tools won't detect errors. Your client libraries won't trigger error handlers.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Response Body Access Hangs Requests
&lt;/h3&gt;

&lt;p&gt;If you try to access &lt;code&gt;context.Response.Body&lt;/code&gt; in your policies for audit logging, &lt;strong&gt;your requests will hang indefinitely&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- This will kill your performance --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;set-variable&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"responseBody"&lt;/span&gt; 
              &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Response.Body.As&amp;lt;string&amp;gt;())"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You must use workarounds (Content-Length headers, metadata only).&lt;/p&gt;




&lt;h2&gt;
  
  
  Should You Use Azure APIM for MCP?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use it if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You already have APIM infrastructure&lt;/li&gt;
&lt;li&gt;You need enterprise-grade security and compliance&lt;/li&gt;
&lt;li&gt;You're willing to work around preview quirks&lt;/li&gt;
&lt;li&gt;You can accept manual configuration (for now)&lt;/li&gt;
&lt;li&gt;You have time to build custom automation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Don't use it if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need full IaC support immediately&lt;/li&gt;
&lt;li&gt;You can't tolerate preview-tier instability&lt;/li&gt;
&lt;li&gt;You require perfect HTTP status code semantics&lt;/li&gt;
&lt;li&gt;You need product-based access control&lt;/li&gt;
&lt;li&gt;You can't invest time in workarounds&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Next in This Series
&lt;/h2&gt;

&lt;p&gt;In the remaining parts, I'll show you &lt;strong&gt;exactly&lt;/strong&gt; how to work around these issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://itsrene.nl/blog/posts/azure-apim-mcp-security" rel="noopener noreferrer"&gt;Part 2: Security&lt;/a&gt;&lt;/strong&gt; - How to fix the &lt;code&gt;/tools/list&lt;/code&gt; hole, implement proper authentication, and handle user context propagation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 3: Logging &amp;amp; Monitoring&lt;/strong&gt; - Audit logging without breaking performance, distributed tracing, and error handling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 4: GitOps &amp;amp; Automation&lt;/strong&gt; - Custom REST API automation, Python scripts for OpenAPI processing, and deployment pipelines&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Bottom Line
&lt;/h2&gt;

&lt;p&gt;APIM MCP is a solid preview feature that brings MCP capabilities to your existing API infrastructure.&lt;/p&gt;

&lt;p&gt;With the right patterns (which I'll share in this series), you can build production-ready MCP servers today. Some areas need custom policies and automation, but that's typical for preview features.&lt;/p&gt;

&lt;p&gt;If you're already invested in Azure and APIM, it's a strong choice. You get enterprise-grade infrastructure without standing up new services.&lt;/p&gt;

&lt;p&gt;Microsoft is actively improving the feature based on community feedback—expect it to get better with each release.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Next:&lt;/strong&gt; &lt;a href="https://itsrene.nl/blog/posts/azure-apim-mcp-security" rel="noopener noreferrer"&gt;Part 2 - Securing Azure APIM MCP Servers in Production →&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm a Product Architect at &lt;a href="https://backbase.com" rel="noopener noreferrer"&gt;Backbase&lt;/a&gt;, where I design cloud-native banking and integration platforms. The patterns in this series come from real production implementations at enterprise scale. Views are my own.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Have you tried Azure APIM MCP? What's your experience been? Drop a comment on my Linkedin post or Dev.To blog—I'd love to hear your war stories.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Connect with me on &lt;a href="https://linkedin.com/in/renenijkamp" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; for more Azure and platform engineering content.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>ai</category>
      <category>mcp</category>
      <category>apim</category>
    </item>
  </channel>
</rss>
