<?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.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 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;Coming soon:&lt;/strong&gt; Part 4 - GitOps for Azure APIM MCP: Custom Automation Guide&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, I'll show you ideas how to automate deployments using custom REST API scripts and CI/CD pipelines—because manual Portal clicks don't scale. This is still something we have to implement, but lets drill down on the concept! &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="784" height="283"&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>
