<?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: Anirbaan Chowdhury</title>
    <description>The latest articles on DEV Community by Anirbaan Chowdhury (@anirbaan_chowdhury_58a600).</description>
    <link>https://dev.to/anirbaan_chowdhury_58a600</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4100979%2Fec07ae8c-37bc-4605-a3e0-4caaf2b151db.jpg</url>
      <title>DEV Community: Anirbaan Chowdhury</title>
      <link>https://dev.to/anirbaan_chowdhury_58a600</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anirbaan_chowdhury_58a600"/>
    <language>en</language>
    <item>
      <title>Setting Up AgentGateway on SAP BTP Kyma</title>
      <dc:creator>Anirbaan Chowdhury</dc:creator>
      <pubDate>Tue, 15 Sep 2026 15:04:51 +0000</pubDate>
      <link>https://dev.to/anirbaan_chowdhury_58a600/setting-up-agentgateway-on-sap-btp-kyma-3562</link>
      <guid>https://dev.to/anirbaan_chowdhury_58a600/setting-up-agentgateway-on-sap-btp-kyma-3562</guid>
      <description>&lt;p&gt;&lt;em&gt;A practical guide to deploying a production-grade MCP gateway — JWT authentication, CEL tool authorization, Istio mTLS, live backend registration, and Jaeger tracing — with the lessons that cost us the most time.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What this is
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://agentgateway.dev" rel="noopener noreferrer"&gt;AgentGateway&lt;/a&gt; is an open-source, AI-first data plane that provides connectivity for agents, tools, and models in any environment. It handles agent-to-agent (A2A) communication, Model Context Protocol (MCP) tool serving, REST APIs exposed as agent-native tools, and AI routing to cloud and local LLMs (including the Gateway API Inference Extension). Across all of these it adds what the underlying protocols deliberately leave out: identity verification, fine-grained authorization, observability, and centralized routing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This guide focuses on one of those use cases: running AgentGateway as an MCP gateway&lt;/strong&gt; — in front of a fleet of MCP servers — and making it a genuinely enforced control point rather than an advisory one. The same infrastructure (Istio mTLS, XSUAA JWT auth, CEL authorization) applies to the other backend types, but everything below is written around MCP.&lt;/p&gt;

&lt;p&gt;On SAP BTP, the natural runtime for this is &lt;strong&gt;Kyma&lt;/strong&gt; — SAP's managed Kubernetes + Istio environment. Kyma gives you something most gateway setups don't: the ability to make the gateway genuinely &lt;strong&gt;enforced&lt;/strong&gt; rather than advisory. Backend MCP servers get no public route. They exist only as &lt;code&gt;ClusterIP&lt;/code&gt; Services. There is no hostname to call from outside the cluster, and inside the cluster an &lt;code&gt;AuthorizationPolicy&lt;/code&gt; means only the gateway's workload identity can reach them. On Cloud Foundry, where every inter-app call goes over a public internet URL, we proved you could walk around the gateway entirely with a direct unauthenticated &lt;code&gt;POST&lt;/code&gt;. On Kyma, there is no back door to walk around.&lt;/p&gt;

&lt;p&gt;That's the architectural win this setup is designed to deliver. Here's how to build it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What you'll end up with
&lt;/h2&gt;

&lt;p&gt;A running deployment where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MCP backends are &lt;strong&gt;ClusterIP-only&lt;/strong&gt; — no external hostname exists; bypass is structurally impossible&lt;/li&gt;
&lt;li&gt;The AgentGateway proxy validates &lt;strong&gt;XSUAA JWT tokens&lt;/strong&gt; on every inbound request&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;CEL expression&lt;/strong&gt; on each backend strips tools from &lt;code&gt;tools/list&lt;/code&gt; unless the token carries the right scope&lt;/li&gt;
&lt;li&gt;Adding a new MCP server is &lt;code&gt;kubectl apply&lt;/code&gt; of two manifests — &lt;strong&gt;no restart, no UI interaction&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;All intra-cluster traffic is &lt;strong&gt;Istio mTLS&lt;/strong&gt; (STRICT namespace-wide) with an &lt;code&gt;AuthorizationPolicy&lt;/code&gt; that whitelists only the proxy's SPIFFE identity to reach backends&lt;/li&gt;
&lt;li&gt;Tool calls appear in structured access logs and as spans in &lt;strong&gt;Jaeger&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 ┌─────────────────────────────────────────────────────────┐
   MCP client    │  XSUAA (BTP service)                                     │
   (curl /       │  • issues client_credentials token                       │
   Inspector) ──▶│  • serves JWKS at /token_keys (HTTPS)                    │
        │        └─────────────────────────────────────────────────────────┘
        │ 1. get token                            ▲ 3. fetch JWKS (HTTPS)
        │ 2. call /mcp with Bearer                │
        ▼                                         │
 ┌───────────────┐   plaintext    ┌───────────────────────────────┐
 │ K8s Load-     │──────HTTP─────▶│  agentgateway-proxy   (2/2)   │
 │ Balancer Svc  │  (PERMISSIVE   │  agentgateway + Envoy sidecar │
 └───────────────┘   inbound)     │  • validates JWT (XSUAA)      │
                                  │  • CEL tool authorization     │
                                  └──┬───────────┬─────────┬──────┘
              xDS gRPC :9978          │           │ Istio   │ OTLP
              (DNS-verified TLS,      │           │ mTLS    │ gRPC
              sidecar excluded)       │           │ STRICT  │ :4317
                                      ▼           ▼         ▼
                          ┌────────────────┐ ┌──────────┐ ┌───────────┐
                          │ agentgateway   │ │ utility  │ │ jaeger-   │
                          │ control plane  │ │ -mcp 2/2 │ │ collector │
                          │  (1/1, NO      │ ├──────────┤ └─────┬─────┘
                          │   sidecar)     │ │ time-mcp │       │
                          └────────────────┘ │ 2/2      │       ▼
                                              └──────────┘ ┌──────────┐
                          AuthorizationPolicy: backends     │ jaeger   │
                          accept ONLY the proxy's SPIFFE   │ query UI │
                          identity, over mTLS              │(HTTPRoute│
                                                            └──────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything except the proxy's LoadBalancer and the Jaeger UI HTTPRoute is &lt;code&gt;ClusterIP&lt;/code&gt;. No other external addresses.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Kyma Gateway API note:&lt;/strong&gt; AgentGateway installs its own &lt;code&gt;GatewayClass&lt;/code&gt; named &lt;code&gt;agentgateway&lt;/code&gt;. Kyma's own API Gateway module (&lt;code&gt;kyma-system/kyma-gateway&lt;/code&gt;) is a separate implementation and is not required here. Both can coexist in the same cluster without conflict — each &lt;code&gt;Gateway&lt;/code&gt; resource references its own &lt;code&gt;gatewayClassName&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Kyma environment enabled on your subaccount; kubeconfig downloaded and &lt;code&gt;kubectl&lt;/code&gt; context set&lt;/li&gt;
&lt;li&gt;Kyma modules enabled: &lt;strong&gt;Istio&lt;/strong&gt;, &lt;strong&gt;SAP BTP Operator&lt;/strong&gt; (&lt;code&gt;services.cloud.sap.com/v1&lt;/code&gt;). The Kyma API Gateway module is not required.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;helm&lt;/code&gt; CLI&lt;/li&gt;
&lt;li&gt;Container registry reachable from the cluster for your MCP backend images. AgentGateway's own images pull from &lt;code&gt;cr.agentgateway.dev&lt;/code&gt; — verify it's reachable:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;STATUS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; https://cr.agentgateway.dev/v2/&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STATUS&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;200|401&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"reachable (HTTP &lt;/span&gt;&lt;span class="nv"&gt;$STATUS&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"UNREACHABLE — check egress allowlist"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 401 is normal — it's the OCI registry's auth challenge. Both 200 and 401 mean the registry is reachable.&lt;/p&gt;




&lt;h2&gt;
  
  
  How AgentGateway works on Kubernetes
&lt;/h2&gt;

&lt;p&gt;There is &lt;strong&gt;no &lt;code&gt;config.yaml&lt;/code&gt;&lt;/strong&gt; and the Admin UI is &lt;strong&gt;read-only&lt;/strong&gt; — it mirrors the control plane's xDS state for debugging. You cannot add backends or toggle policies through the UI. Every change is a &lt;code&gt;kubectl apply&lt;/code&gt;. The control plane watches standard Kubernetes and Gateway API resources and pushes configuration to the data plane over xDS:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;AgentgatewayBackend&lt;/code&gt; (CRD)&lt;/td&gt;
&lt;td&gt;MCP target registration — where is the server, what protocol&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;AgentgatewayPolicy&lt;/code&gt; (CRD)&lt;/td&gt;
&lt;td&gt;Auth, authz, tracing, access-log configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;HTTPRoute&lt;/code&gt; (Gateway API)&lt;/td&gt;
&lt;td&gt;Routing rules — which path goes to which backend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Gateway&lt;/code&gt; (Gateway API)&lt;/td&gt;
&lt;td&gt;Listener definition — AgentGateway's Deployer auto-creates the proxy Deployment and LoadBalancer Service&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Live-add an MCP server" means: apply a Deployment + Service, then apply an updated &lt;code&gt;AgentgatewayBackend&lt;/code&gt; with the new target added. The control plane reconciles over xDS immediately. No restart, no UI interaction.&lt;/p&gt;

&lt;p&gt;![AgentGateway Admin UI, Gateway Overview — a read-only banner reads "Configuration is managed by XDS. This view reflects the active runtime dump; editing is disabled." with counts of active Listeners, Routes, and Policies.]&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkrqnv6kts7pznfh59zy2.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkrqnv6kts7pznfh59zy2.png" alt=" " width="800" height="299"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Admin UI in read-only mode: "Configuration is managed by XDS. This view reflects the active runtime dump; editing is disabled." It surfaces the live Listeners, Routes, and Policies (plus a CEL Playground for testing expressions) — but you can't add or change anything here. Every change is a &lt;code&gt;kubectl apply&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 1 — Install AgentGateway
&lt;/h2&gt;

&lt;p&gt;Label the namespace &lt;strong&gt;before&lt;/strong&gt; creating any pods, so Istio injects sidecars from the start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl create namespace agentgateway-system
kubectl label namespace agentgateway-system istio-injection&lt;span class="o"&gt;=&lt;/span&gt;enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the CRDs (includes Gateway API CRDs), then the control plane:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm upgrade &lt;span class="nt"&gt;-i&lt;/span&gt; agentgateway-crds &lt;span class="se"&gt;\&lt;/span&gt;
  oci://cr.agentgateway.dev/charts/agentgateway-crds &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version&lt;/span&gt; 0.0.0-latest-dev &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system

helm upgrade &lt;span class="nt"&gt;-i&lt;/span&gt; agentgateway &lt;span class="se"&gt;\&lt;/span&gt;
  oci://cr.agentgateway.dev/charts/agentgateway &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--version&lt;/span&gt; 0.0.0-latest-dev &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then apply a &lt;code&gt;GatewayClass&lt;/code&gt; (name: &lt;code&gt;agentgateway&lt;/code&gt;) and a &lt;code&gt;Gateway&lt;/code&gt; resource. The Deployer watches the &lt;code&gt;Gateway&lt;/code&gt; and auto-creates the proxy Deployment and LoadBalancer Service.&lt;/p&gt;

&lt;p&gt;Exit criteria — don't proceed until all three hold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get gatewayclass agentgateway         &lt;span class="c"&gt;# ACCEPTED: True&lt;/span&gt;
kubectl get gateway &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system    &lt;span class="c"&gt;# PROGRAMMED: True&lt;/span&gt;
kubectl get svc &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system        &lt;span class="c"&gt;# LoadBalancer IP/hostname assigned&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2 — The Istio sidecar split (the most important thing to get right)
&lt;/h2&gt;

&lt;p&gt;This is the architecture decision that everything else depends on. You need three different sidecar configurations for three different workload roles:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Control plane — no sidecar at all (&lt;code&gt;1/1&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AgentGateway's control plane must be sidecar-free. The proxy connects to it over gRPC TLS on port 9978 using standard DNS hostname verification. If Istio injects a sidecar, it intercepts that port and presents a SPIFFE cert (URI SAN only, no DNS SAN). The proxy's TLS client fails the handshake. Annotate the control plane deployment's pod template with &lt;code&gt;sidecar.istio.io/inject: "false"&lt;/code&gt; so Istio skips it entirely.&lt;/p&gt;

&lt;p&gt;On the proxy side, exclude port 9978 from Envoy's outbound interception:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# AgentgatewayParameters (applied to the agentgateway-proxy gateway)&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deployment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;traffic.sidecar.istio.io/excludeOutboundPorts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9978"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Proxy — sidecar present, inbound PERMISSIVE (&lt;code&gt;2/2&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The proxy needs an Envoy sidecar so it can originate &lt;strong&gt;mTLS&lt;/strong&gt; toward the backends. The sidecar transparently upgrades the proxy's outbound plain HTTP to Istio mutual TLS — no backend TLS config needed anywhere.&lt;/p&gt;

&lt;p&gt;The proxy's &lt;em&gt;inbound&lt;/em&gt; traffic comes from the external LoadBalancer over plain HTTP (from real MCP clients). STRICT mode would reject this. Scope a workload-level &lt;code&gt;PeerAuthentication: PERMISSIVE&lt;/code&gt; to the proxy only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security.istio.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PeerAuthentication&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agentgateway-proxy-permissive&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agentgateway-system&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app.kubernetes.io/name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agentgateway-proxy&lt;/span&gt;
  &lt;span class="na"&gt;mtls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PERMISSIVE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Backends — sidecar present, STRICT (&lt;code&gt;2/2&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All MCP backend pods get a normal sidecar and stay under the namespace-wide STRICT policy. Their inbound only ever comes from the proxy's Envoy (mTLS) and they should never accept anything else.&lt;/p&gt;

&lt;p&gt;The asymmetry is intentional and load-bearing: &lt;strong&gt;control plane: &lt;code&gt;1/1&lt;/code&gt; sidecar-free; proxy: &lt;code&gt;2/2&lt;/code&gt; with PERMISSIVE inbound; backends: &lt;code&gt;2/2&lt;/code&gt; with STRICT.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3 — Lock down the mesh
&lt;/h2&gt;

&lt;p&gt;Apply a namespace-wide STRICT &lt;code&gt;PeerAuthentication&lt;/code&gt; and an &lt;code&gt;AuthorizationPolicy&lt;/code&gt; that restricts backend access to the proxy's SPIFFE identity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# PeerAuthentication — namespace-wide STRICT&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security.istio.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PeerAuthentication&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agentgateway-system&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mtls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;STRICT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# AuthorizationPolicy — only the proxy's service account reaches backends&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security.istio.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AuthorizationPolicy&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backends-gateway-only&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agentgateway-system&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcp-backend&lt;/span&gt;     &lt;span class="c1"&gt;# applied to all backend pods via this label&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ALLOW&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;principals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cluster.local/ns/agentgateway-system/sa/agentgateway-proxy"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every backend pod must carry &lt;code&gt;role: mcp-backend&lt;/code&gt; in its template labels for this selector to pick it up.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4 — Deploy MCP backends (ClusterIP only)
&lt;/h2&gt;

&lt;p&gt;Deploy each MCP server with a ClusterIP Service — no &lt;code&gt;type: LoadBalancer&lt;/code&gt;, no Ingress, no external hostname. The backend literally cannot be reached from outside the cluster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;poc-utility-mcp&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agentgateway-system&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;poc-utility-mcp&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
  &lt;span class="c1"&gt;# no type: LoadBalancer — ClusterIP is the default and what we want&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register all MCP targets in a &lt;strong&gt;single&lt;/strong&gt; &lt;code&gt;AgentgatewayBackend&lt;/code&gt; under &lt;code&gt;spec.mcp.targets[]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agentgateway.dev/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AgentgatewayBackend&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;all-mcp&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agentgateway-system&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mcp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;utility&lt;/span&gt;
        &lt;span class="na"&gt;static&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;poc-utility-mcp.agentgateway-system.svc.cluster.local&lt;/span&gt;
          &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
          &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;StreamableHTTP&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;time&lt;/span&gt;
        &lt;span class="na"&gt;static&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;poc-time-mcp.agentgateway-system.svc.cluster.local&lt;/span&gt;
          &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
          &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;StreamableHTTP&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Critical:&lt;/strong&gt; all targets must be in one &lt;code&gt;AgentgatewayBackend&lt;/code&gt;, not separate ones. If you create two &lt;code&gt;AgentgatewayBackend&lt;/code&gt; resources with two &lt;code&gt;HTTPRoute&lt;/code&gt; resources both matching &lt;code&gt;/mcp&lt;/code&gt;, the proxy routes all traffic to only one — the other backend's tools never appear in &lt;code&gt;tools/list&lt;/code&gt;. The same problem occurs with multiple &lt;code&gt;backendRefs&lt;/code&gt; in a single &lt;code&gt;HTTPRoute&lt;/code&gt; rule. One backend CRD, one HTTPRoute. Tool names are automatically prefixed with the target name (&lt;code&gt;utility_uuid_generate&lt;/code&gt;, &lt;code&gt;time_time_current&lt;/code&gt;) to avoid collisions.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 5 — XSUAA JWT authentication
&lt;/h2&gt;

&lt;p&gt;Create the XSUAA service instance and binding via BTP Operator. The instance definition needs to grant the custom scope to the app's own OAuth client — otherwise a &lt;code&gt;client_credentials&lt;/code&gt; token carries only &lt;code&gt;uaa.resource&lt;/code&gt; and every tool gets stripped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;services.cloud.sap.com/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ServiceInstance&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;poc-agentgateway-xsuaa&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agentgateway-system&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;serviceOfferingName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;xsuaa&lt;/span&gt;
  &lt;span class="na"&gt;servicePlanName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application&lt;/span&gt;
  &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;xsappname&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;poc-agentgateway&lt;/span&gt;
    &lt;span class="na"&gt;tenant-mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dedicated&lt;/span&gt;
    &lt;span class="na"&gt;scopes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$XSAPPNAME.mcp.utility"&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Access&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;utility&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;MCP&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tools"&lt;/span&gt;
    &lt;span class="na"&gt;authorities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$XSAPPNAME.mcp.utility"&lt;/span&gt;   &lt;span class="c1"&gt;# grant to own OAuth client — required&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the binding is &lt;code&gt;Ready&lt;/code&gt;, extract the values for the auth policy:&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="nv"&gt;XSUAA_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl get secret poc-agentgateway-xsuaa &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.data.url}'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;kubectl get secret poc-agentgateway-xsuaa &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.data.clientid}'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configure JWT authentication on the Gateway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;traffic&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;jwtAuthentication&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Strict&lt;/span&gt;
    &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;XSUAA_URL&amp;gt;/oauth/token"&lt;/span&gt;   &lt;span class="c1"&gt;# note: /oauth/token suffix required&lt;/span&gt;
        &lt;span class="na"&gt;audiences&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;CLIENT_ID&amp;gt;"&lt;/span&gt;                    &lt;span class="c1"&gt;# the sb-poc-agentgateway!t… value&lt;/span&gt;
        &lt;span class="na"&gt;jwks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;remote&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;XSUAA_URL&amp;gt;/token_keys"&lt;/span&gt;   &lt;span class="c1"&gt;# explicit https:// — use url, not backendRef&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;mode: Permissive&lt;/code&gt; initially (returns tools even without a token) to verify the backend wiring, then switch to &lt;code&gt;Strict&lt;/code&gt; once routing is confirmed working.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6 — CEL tool authorization
&lt;/h2&gt;

&lt;p&gt;Apply an &lt;code&gt;AgentgatewayPolicy&lt;/code&gt; targeting your &lt;code&gt;AgentgatewayBackend&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agentgateway.dev/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AgentgatewayPolicy&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcp-tool-authz&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agentgateway-system&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;targetRefs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agentgateway.dev&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AgentgatewayBackend&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;all-mcp&lt;/span&gt;
  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;mcp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;authorization&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
        &lt;span class="na"&gt;policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;matchExpressions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;jwt.scope.exists(s,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;s.endsWith(".mcp.utility"))'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;endsWith&lt;/code&gt; pattern is deliberate: XSUAA prefixes scopes with the full application ID including a subaccount tenant index — e.g. &lt;code&gt;poc-agentgateway!t123456.mcp.utility&lt;/code&gt; rather than the bare &lt;code&gt;poc-agentgateway.mcp.utility&lt;/code&gt;. Matching literally on the full name will break across subaccounts. Matching by suffix works everywhere.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7 — Access logs and Jaeger tracing
&lt;/h2&gt;

&lt;p&gt;Enable structured access logging for MCP calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;targetRefs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gateway.networking.k8s.io&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gateway&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agentgateway-proxy&lt;/span&gt;
  &lt;span class="na"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;accessLog&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Only log the interesting lines: actual tool calls, plus non-MCP requests.&lt;/span&gt;
      &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mcp.methodName&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"tools/call"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;||&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;!has(mcp.methodName)'&lt;/span&gt;
      &lt;span class="na"&gt;attributes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tool_args&lt;/span&gt;
            &lt;span class="na"&gt;expression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcp.tool.arguments&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tool_result&lt;/span&gt;
            &lt;span class="na"&gt;expression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcp.tool.result&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tool_error&lt;/span&gt;
            &lt;span class="na"&gt;expression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcp.tool.error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Jaeger, deploy it as a single Deployment with a multi-port ClusterIP Service (ports 4317 for OTLP gRPC, 16686 for the query UI). Then configure the tracing policy using &lt;code&gt;url&lt;/code&gt; — &lt;strong&gt;not&lt;/strong&gt; &lt;code&gt;backendRef&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;targetRefs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gateway.networking.k8s.io&lt;/span&gt;
      &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gateway&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agentgateway-proxy&lt;/span&gt;
  &lt;span class="na"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tracing&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://jaeger-collector.agentgateway-system.svc.cluster.local:4317"&lt;/span&gt;
      &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GRPC&lt;/span&gt;
      &lt;span class="na"&gt;randomSampling&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason &lt;code&gt;url&lt;/code&gt; is required is explained in the lessons section. Point the Jaeger UI to an &lt;code&gt;HTTPRoute&lt;/code&gt; at a path like &lt;code&gt;/jaeger&lt;/code&gt; with a URL-rewrite filter.&lt;/p&gt;

&lt;p&gt;![Jaeger UI showing 5 traces for the agentgateway service — tools/call, tools/list, initialize, and GET /mcp/* — each with span count, duration, and timestamp.]&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw5wmil55991rk8foyqda.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw5wmil55991rk8foyqda.png" alt=" " width="800" height="304"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;AgentGateway emits a named span per MCP method. Every &lt;code&gt;tools/call&lt;/code&gt;, &lt;code&gt;tools/list&lt;/code&gt;, and &lt;code&gt;initialize&lt;/code&gt; appears as a distinct trace. Click into any trace to see the full span waterfall with `gen_ai.&lt;/em&gt;` attributes.*&lt;/p&gt;


&lt;h2&gt;
  
  
  Live backend registration
&lt;/h2&gt;

&lt;p&gt;Once everything is running, adding a new MCP server is two &lt;code&gt;kubectl apply&lt;/code&gt; 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;# Apply the new server's Deployment + ClusterIP Service&lt;/span&gt;
kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; k8s/70-time-mcp.yaml

&lt;span class="c"&gt;# Apply the updated all-mcp backend with the new target added&lt;/span&gt;
kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; k8s/71-time-backend.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The control plane sees the updated &lt;code&gt;AgentgatewayBackend&lt;/code&gt; CRD and pushes the new xDS configuration to the proxy over the 9978 channel. The new server's tools appear in &lt;code&gt;tools/list&lt;/code&gt; within seconds. Nothing restarts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Isolation proof
&lt;/h2&gt;

&lt;p&gt;The single most compelling moment in the demo is confirming the back door genuinely does not exist. From inside the cluster, with the wrong identity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl run probe &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;curlimages/curl &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; agentgateway-system &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://poc-utility-mcp.agentgateway-system.svc.cluster.local:8080/mcp
&lt;span class="c"&gt;# → RBAC: access denied&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;![Terminal output showing RBAC: access denied when curling the backend ClusterIP directly from inside the cluster with the wrong workload identity.]&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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ngczil7s1csd45sl4l9.png" 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%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ngczil7s1csd45sl4l9.png" alt=" " width="800" height="64"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Correct in-cluster DNS name, correct port — still &lt;code&gt;RBAC: access denied&lt;/code&gt;. The AuthorizationPolicy rejects any caller that isn't the proxy's SPIFFE identity.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;From outside the cluster there is no &lt;code&gt;poc-utility-mcp.&amp;lt;domain&amp;gt;&lt;/code&gt; to hit. That's the structural guarantee CF couldn't provide.&lt;/p&gt;


&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;p&gt;These are the issues that cost real time during the initial setup. Each one has a non-obvious root cause. Reading this section before you start will save you hours.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. The proxy won't go Ready — xDS TLS fails against its own control plane
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; the proxy pod starts but never reaches the Ready state. Logs show &lt;code&gt;UnknownIssuer&lt;/code&gt; or &lt;code&gt;certificate not valid for name&lt;/code&gt; on the xDS gRPC channel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; Istio injected a sidecar into the AgentGateway control plane (the namespace is &lt;code&gt;istio-injection=enabled&lt;/code&gt;, so it injects into everything). That sidecar intercepts port 9978 and presents an Istio SPIFFE cert — URI SAN only, no DNS SAN. The proxy's xDS TLS client uses standard DNS hostname verification. The cert fails verification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; annotate the control plane deployment's pod template &lt;code&gt;sidecar.istio.io/inject: "false"&lt;/code&gt;, and exclude port 9978 from the proxy's outbound interception (&lt;code&gt;traffic.sidecar.istio.io/excludeOutboundPorts: "9978"&lt;/code&gt;, set via the proxy's &lt;code&gt;AgentgatewayParameters&lt;/code&gt;). The proxy then reaches the control plane's own DNS-SAN cert and the handshake passes.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Every MCP call fails with &lt;code&gt;filter_chain_not_found&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; proxy is Ready, &lt;code&gt;initialize&lt;/code&gt; returns &lt;code&gt;Connection reset by peer&lt;/code&gt;, the backend's Envoy sidecar logs &lt;code&gt;NR&lt;/code&gt; (no route) and &lt;code&gt;filter_chain_not_found&lt;/code&gt;. The backend process never sees the request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; the cluster has a STRICT &lt;code&gt;PeerAuthentication&lt;/code&gt; (common on Kyma — the mesh policy may pre-date your deployment). The proxy was sending plain HTTP; STRICT means the backend's inbound listener has no plaintext filter chain. Connection refused before it reaches the app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dead end to avoid:&lt;/strong&gt; the obvious instinct is to configure the proxy's built-in mesh integration (&lt;code&gt;spec.istio.enabled: true&lt;/code&gt;) so it originates mTLS natively. On a sidecar-mode cluster that only gives a plaintext egress, and the SPIFFE-based backend TLS requires a Workload API the sidecar-free proxy doesn't have. Don't go down this path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; give the proxy an Envoy sidecar. The sidecar transparently upgrades the proxy's outbound connections to Istio mTLS — no backend TLS config needed. The proxy's inbound (from the LoadBalancer, plain HTTP) needs a workload-level &lt;code&gt;PeerAuthentication: PERMISSIVE&lt;/code&gt; so STRICT doesn't reject it. Backends stay STRICT. The sidecar split in Step 2 above encodes this directly.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. JWKS fetch fails — AgentGateway sends plain HTTP to a TLS port
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; proxy is ready and auth is enabled, but every token is rejected because the JWKS keyset never loads. Logs show a connection error fetching the JWKS endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; &lt;code&gt;jwks.remote.backendRef&lt;/code&gt; pointing at an &lt;code&gt;ExternalName&lt;/code&gt; K8s Service causes AgentGateway to construct &lt;code&gt;http://&amp;lt;host&amp;gt;:443/token_keys&lt;/code&gt; — plain HTTP to a TLS port. XSUAA only speaks HTTPS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; use &lt;code&gt;jwks.remote.url&lt;/code&gt; with an explicit &lt;code&gt;https://&lt;/code&gt; scheme. When the scheme is &lt;code&gt;https&lt;/code&gt;, the proxy originates backend TLS automatically. &lt;code&gt;backendRef&lt;/code&gt; + &lt;code&gt;ExternalName&lt;/code&gt; is the wrong tool for this.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. &lt;code&gt;Error(InvalidIssuer)&lt;/code&gt; — the token's &lt;code&gt;iss&lt;/code&gt; doesn't match
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; JWKS loads, signature verifies, but every request still returns &lt;code&gt;401 authentication failure … Error(InvalidIssuer)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; the ServiceBinding &lt;code&gt;url&lt;/code&gt; field is &lt;code&gt;https://&amp;lt;host&amp;gt;&lt;/code&gt;. But an XSUAA token's &lt;code&gt;iss&lt;/code&gt; claim is &lt;code&gt;https://&amp;lt;host&amp;gt;/oauth/token&lt;/code&gt;. The strings must match exactly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; &lt;code&gt;issuer: &amp;lt;xsuaa-url&amp;gt;/oauth/token&lt;/code&gt;. Note: reaching this error is progress — it proves the JWKS fetch and signature verification are already working.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. &lt;code&gt;tools/list&lt;/code&gt; returns empty even with a valid token
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; auth passes (200 response), but &lt;code&gt;tools/list&lt;/code&gt; returns &lt;code&gt;[]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two root causes can stack here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scope not in token:&lt;/strong&gt; &lt;code&gt;client_credentials&lt;/code&gt; tokens carry only what's explicitly granted to the OAuth client. If you define the scope in the ServiceInstance but omit &lt;code&gt;authorities&lt;/code&gt;, the token carries only &lt;code&gt;uaa.resource&lt;/code&gt;. &lt;strong&gt;Fix:&lt;/strong&gt; add &lt;code&gt;authorities: ["$XSAPPNAME.mcp.utility"]&lt;/code&gt; to the ServiceInstance parameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CEL rule matching the wrong string:&lt;/strong&gt; XSUAA prefixes scopes with the full application ID including a tenant index: &lt;code&gt;poc-agentgateway!t123456.mcp.utility&lt;/code&gt;. A literal match on &lt;code&gt;poc-agentgateway.mcp.utility&lt;/code&gt; fails. &lt;strong&gt;Fix:&lt;/strong&gt; &lt;code&gt;jwt.scope.exists(s, s.endsWith(".mcp.utility"))&lt;/code&gt; — matches by suffix, works across subaccounts.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  6. Multi-backend routing conflict — only one server's tools appear
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; &lt;code&gt;tools/list&lt;/code&gt; returns tools from one MCP server but not the other, even after both are deployed and registered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; two &lt;code&gt;AgentgatewayBackend&lt;/code&gt; resources each with their own &lt;code&gt;HTTPRoute&lt;/code&gt; both matching &lt;code&gt;PathPrefix: /mcp&lt;/code&gt;. Gateway API tie-breaking sends all requests to one backend. The same problem occurs with multiple &lt;code&gt;backendRefs&lt;/code&gt; in a single &lt;code&gt;HTTPRoute&lt;/code&gt; rule — the proxy still routes to only the first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; put all MCP targets in a single &lt;code&gt;AgentgatewayBackend&lt;/code&gt; under &lt;code&gt;spec.mcp.targets[]&lt;/code&gt;, with one HTTPRoute pointing to it. AgentGateway aggregates &lt;code&gt;tools/list&lt;/code&gt; across all targets in the same backend. Tool names get prefixed with the target name to prevent collisions (&lt;code&gt;utility_uuid_generate&lt;/code&gt;, &lt;code&gt;time_time_current&lt;/code&gt;).&lt;/p&gt;
&lt;h3&gt;
  
  
  7. Jaeger OTLP export fails — connection reset
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; traces never appear in Jaeger. Proxy Envoy logs show &lt;code&gt;upstream_cluster: "PassthroughCluster"&lt;/code&gt; and &lt;code&gt;response_flags: "UR"&lt;/code&gt; (upstream reset) on port 4317.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; &lt;code&gt;AgentgatewayPolicy.spec.frontend.tracing.backendRef&lt;/code&gt; causes AgentGateway to resolve the K8s Service to &lt;strong&gt;pod IPs&lt;/strong&gt; via the Endpoints API (standard Gateway API load-balancing behavior). When Envoy intercepts an outbound connection to a pod IP — not a ClusterIP — it has no named service cluster to match it against. It falls back to &lt;code&gt;PassthroughCluster&lt;/code&gt; (plain TCP, no mTLS). Under STRICT PeerAuthentication, Jaeger's inbound Envoy has no plaintext filter chain: &lt;code&gt;filter_chain_not_found&lt;/code&gt;, connection reset.&lt;/p&gt;

&lt;p&gt;MCP backends work correctly because their &lt;code&gt;AgentgatewayBackend.spec.mcp.targets[].static.host&lt;/code&gt; is set to the DNS hostname. AgentGateway connects by hostname → Envoy resolves to ClusterIP → matches the &lt;code&gt;outbound|8080||...&lt;/code&gt; mTLS cluster → TLS handshake succeeds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; use &lt;code&gt;url&lt;/code&gt; instead of &lt;code&gt;backendRef&lt;/code&gt; in the tracing policy. &lt;code&gt;url: "http://jaeger-collector.agentgateway-system.svc.cluster.local:4317"&lt;/code&gt; makes AgentGateway pass the DNS name to the TCP connection. Envoy resolves it to the ClusterIP and routes through the proper &lt;code&gt;outbound|4317||jaeger-collector...&lt;/code&gt; mTLS cluster. &lt;code&gt;response_code&lt;/code&gt; changes from reset to &lt;code&gt;200&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  8. Jaeger UI returns 503 via the gateway HTTPRoute
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; &lt;code&gt;GET /jaeger/&lt;/code&gt; returns &lt;code&gt;503&lt;/code&gt; from the proxy with &lt;code&gt;Connection reset by peer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; same PassthroughCluster mechanism as lesson 7, but for port 16686. The &lt;code&gt;HTTPRoute&lt;/code&gt;'s &lt;code&gt;backendRef&lt;/code&gt; resolves to the Jaeger pod IP, Envoy uses PassthroughCluster, Jaeger's inbound Envoy finds no plaintext filter chain under STRICT. Unlike the OTLP case, &lt;code&gt;HTTPRoute&lt;/code&gt; has no &lt;code&gt;url&lt;/code&gt; alternative in the Gateway API spec — you can't avoid the &lt;code&gt;backendRef&lt;/code&gt; here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; add a workload-scoped &lt;code&gt;PeerAuthentication: PERMISSIVE&lt;/code&gt; for Jaeger, overriding the namespace-wide STRICT only for the Jaeger pod:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security.istio.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PeerAuthentication&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jaeger-permissive&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agentgateway-system&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jaeger&lt;/span&gt;
  &lt;span class="na"&gt;mtls&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PERMISSIVE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This adds a plaintext filter chain alongside the mTLS one on Jaeger's inbound. The proxy's PassthroughCluster connection is accepted. All other workloads remain STRICT. The OTLP export (now using the correct mTLS path via &lt;code&gt;url&lt;/code&gt;) is unaffected.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the result demonstrates
&lt;/h2&gt;

&lt;p&gt;Once everything above is in place, you can walk through these in order to show the system working:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Baseline&lt;/strong&gt; — &lt;code&gt;tools/list&lt;/code&gt; without a token returns all tools (auth in Permissive mode)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolation&lt;/strong&gt; — &lt;code&gt;curl&lt;/code&gt; from a random pod inside the cluster to a backend's ClusterIP gets &lt;code&gt;RBAC: access denied&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live add&lt;/strong&gt; — deploy a second MCP server with two &lt;code&gt;kubectl apply&lt;/code&gt;s; new tools appear in &lt;code&gt;tools/list&lt;/code&gt; within seconds, nothing restarts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth on&lt;/strong&gt; — apply the Strict auth policy; requests without a token return &lt;code&gt;401&lt;/code&gt;, requests with a valid token return tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope authz&lt;/strong&gt; — with the CEL rule applied, a token carrying &lt;code&gt;*.mcp.utility&lt;/code&gt; sees the utility tools; a token without that scope gets &lt;code&gt;[]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traces&lt;/strong&gt; — tool calls appear as spans in Jaeger's UI with operation names like &lt;code&gt;tools/call utility_uuid_generate&lt;/code&gt;; access logs on the proxy carry &lt;code&gt;tool_args&lt;/code&gt;, &lt;code&gt;tool_result&lt;/code&gt;, and &lt;code&gt;tool_error&lt;/code&gt; for each &lt;code&gt;tools/call&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Key things to remember
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The sidecar split is load-bearing.&lt;/strong&gt; Control plane sidecar-free, proxy with sidecar + PERMISSIVE inbound, backends with sidecar + STRICT. Getting this wrong is the most common source of connectivity failures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One &lt;code&gt;AgentgatewayBackend&lt;/code&gt;, all targets.&lt;/strong&gt; Separate backends with separate HTTPRoutes at the same path means only one backend wins. Consolidate under &lt;code&gt;spec.mcp.targets[]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XSUAA scopes have a tenant suffix.&lt;/strong&gt; &lt;code&gt;$XSAPPNAME.mcp.utility&lt;/code&gt; becomes &lt;code&gt;poc-agentgateway!t123456.mcp.utility&lt;/code&gt; in a real token. Match with &lt;code&gt;endsWith&lt;/code&gt;, not literal equality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;url&lt;/code&gt;, not &lt;code&gt;backendRef&lt;/code&gt;, for OTLP tracing.&lt;/strong&gt; &lt;code&gt;backendRef&lt;/code&gt; resolves to pod IPs, which Envoy routes through PassthroughCluster (no mTLS). &lt;code&gt;url&lt;/code&gt; with a DNS hostname goes through the correct mTLS outbound cluster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Admin UI is read-only.&lt;/strong&gt; It's a debug view of the xDS-pushed state. Changes are &lt;code&gt;kubectl apply&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kubernetes</category>
      <category>mcp</category>
      <category>sap</category>
      <category>istio</category>
    </item>
    <item>
      <title>Principal Propagation with an MCP Server on SAP BTP — End to End</title>
      <dc:creator>Anirbaan Chowdhury</dc:creator>
      <pubDate>Sun, 06 Sep 2026 15:57:20 +0000</pubDate>
      <link>https://dev.to/anirbaan_chowdhury_58a600/principal-propagation-with-an-mcp-server-on-sap-btp-end-to-end-ob9</link>
      <guid>https://dev.to/anirbaan_chowdhury_58a600/principal-propagation-with-an-mcp-server-on-sap-btp-end-to-end-ob9</guid>
      <description>&lt;p&gt;If you have been building AI agents on top of SAP BTP, you have probably hit this wall: your agent calls a backend REST API and every request lands in the ABAP system under a single shared service account. Audit logs record the service account, not the person behind the request, and ABAP cannot enforce per-user authorization when every call arrives under the same identity.&lt;/p&gt;

&lt;p&gt;The fix is &lt;strong&gt;principal propagation&lt;/strong&gt; — a chain of trust that flows the end user's identity from their browser session all the way through BTP Cloud Foundry, the Cloud Connector, and into the ABAP ICM layer, where a short-lived X.509 certificate establishes who is actually making the call.&lt;/p&gt;

&lt;p&gt;This post walks through how to build exactly that, with an MCP server (a Model Context Protocol server — a lightweight HTTP service that exposes typed tools an LLM agent can call) as the entry point so your LLM agent can call ABAP REST APIs as the authenticated user.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not Just Use a Service Account?
&lt;/h2&gt;

&lt;p&gt;You could skip all of this and use a single ABAP technical user. It is simpler to set up. But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ABAP authorization objects are designed around individual users. A shared account means every call runs under the same permissions — you cannot enforce different rights for different people.&lt;/li&gt;
&lt;li&gt;Audit logs (SM20, SAL) record the technical user, not the person who made the request. This makes incident investigation and compliance reporting much harder.&lt;/li&gt;
&lt;li&gt;If an agent misbehaves or gets compromised, the blast radius is the full permissions of that service account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Principal propagation gives you per-user authorization enforcement and full auditability at the ABAP layer, without requiring the cloud application to manage any ABAP credentials.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We Are Building
&lt;/h2&gt;

&lt;p&gt;The scope of this post is the &lt;strong&gt;MCP server and its principal propagation chain&lt;/strong&gt; — everything from the MCP server down to ABAP. The browser login through the approuter to a user JWT (the top block below) is assumed to already be in place.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User browser
   │  authorization_code login
   ▼
Approuter (SAP BTP CF)   ──►  XSUAA  ──►  issues user JWT   ← assumed (not covered here)
   │
   │  user JWT (Bearer token)
   ▼
MCP Server (SAP BTP CF)                                     ← this post
   │  reads Connectivity service bindings from VCAP_SERVICES
   │  sends:  SAP-Connectivity-Authentication: Bearer &amp;lt;user JWT&amp;gt;
   │          Proxy-Authorization: Bearer &amp;lt;connectivity client_credentials token&amp;gt;
   ▼
BTP Connectivity Proxy
   │  tunnels over established WebSocket channel
   ▼
Cloud Connector
   │  verifies JWT signature (XSUAA public key, synced from BTP trust config)
   │  extracts user_uuid → generates short-lived X.509 cert  CN=&amp;lt;user&amp;gt;
   │  opens mTLS connection to ABAP; forwards cert in SSL_CLIENT_CERT header
   ▼
ABAP ICM  (trusts the Cloud Connector as reverse proxy)
   │  validates SSL_CLIENT_CERT → looks up CN=&amp;lt;user&amp;gt; in EXTID_DN
   ▼
ABAP business logic  ←─ executes as the real end user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every hop in this chain preserves the end user's identity. Only the user JWT crosses the cloud/on-premise boundary; the two &lt;code&gt;client_credentials&lt;/code&gt; tokens are consumed entirely within BTP.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BTP subaccount (CF-enabled)&lt;/td&gt;
&lt;td&gt;Hosts the apps and service instances&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XSUAA service instance&lt;/td&gt;
&lt;td&gt;Issues user JWTs via &lt;code&gt;authorization_code&lt;/code&gt; flow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connectivity service instance&lt;/td&gt;
&lt;td&gt;Manages the Cloud Connector tunnel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Connector&lt;/td&gt;
&lt;td&gt;On-premise agent that bridges BTP to the ABAP system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SAP ABAP system with REST APIs&lt;/td&gt;
&lt;td&gt;Your backend&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Step 1 — Set Up the BTP Services
&lt;/h2&gt;

&lt;p&gt;You need one service instance bound to the MCP server. The approuter has its own XSUAA binding (not covered here). In your &lt;code&gt;manifest.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# manifest.yml&lt;/span&gt;
&lt;span class="na"&gt;applications&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-abap-mcp-server&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512M&lt;/span&gt;
    &lt;span class="na"&gt;buildpacks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;python_buildpack&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python mcp_server.py&lt;/span&gt;
    &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;my-connectivity-service&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The XSUAA instance (&lt;code&gt;my-xsuaa&lt;/code&gt;) is bound to the &lt;strong&gt;approuter&lt;/strong&gt;, not this app — the approuter handles login and issues the user JWT that the MCP server consumes. The MCP server never verifies JWT signatures itself; it delegates that responsibility to the Cloud Connector.&lt;/p&gt;

&lt;p&gt;Create the instances via the BTP cockpit or CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cf create-service connectivity lite my-connectivity-service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2 — Configure the Cloud Connector
&lt;/h2&gt;

&lt;p&gt;In your Cloud Connector administration UI:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2a. Register your BTP subaccount&lt;/strong&gt;&lt;br&gt;
Under Cloud To On-Premise → Add Subaccount. You will need your subaccount's XSUAA URL and your CF API endpoint. After registration the CC establishes a persistent WebSocket tunnel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2b. Add the ABAP system as an accessible resource&lt;/strong&gt;&lt;br&gt;
Under Cloud To On-Premise → Access Control:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Back-end type: ABAP&lt;/li&gt;
&lt;li&gt;Protocol: HTTP&lt;/li&gt;
&lt;li&gt;Internal host/port: your real ABAP hostname and port&lt;/li&gt;
&lt;li&gt;Virtual host: &lt;code&gt;abap-backend-virtual&lt;/code&gt; (must match the destination URL)&lt;/li&gt;
&lt;li&gt;Principal type: &lt;strong&gt;X.509 Certificate (General Usage)&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2c. Configure the subject pattern&lt;/strong&gt;&lt;br&gt;
Under the system's "Principal Propagation" settings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Subject pattern: &lt;code&gt;CN=${user_uuid}&lt;/code&gt; (maps the &lt;code&gt;user_uuid&lt;/code&gt; JWT claim to the cert CN)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;user_uuid&lt;/code&gt; claim in a standard XSUAA JWT is a UUID (e.g. &lt;code&gt;8f3a1b2c-4d5e-6789-abcd-ef0123456789&lt;/code&gt;). Inspect a decoded token from your own environment to confirm the exact value — this is what gets embedded as the cert CN and what you must map in &lt;code&gt;EXTID_DN&lt;/code&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 3 — The MCP Server
&lt;/h2&gt;

&lt;p&gt;This is the heart of the setup: a Python MCP server deployed to BTP CF that binds the Connectivity service, checks the caller's JWT zone ID, and routes each tool invocation through the connectivity proxy with the user's identity attached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why HTTP transport?&lt;/strong&gt; MCP's stdio transport cannot carry per-request user identity — a single process handles all calls and there is no request-level header. The &lt;code&gt;streamable-http&lt;/code&gt; transport gives each tool invocation its own HTTP request, so the caller can set &lt;code&gt;Authorization: Bearer &amp;lt;user_jwt&amp;gt;&lt;/code&gt; and the server can validate and thread it through to the ABAP call.&lt;/p&gt;

&lt;p&gt;The server uses Starlette middleware to check the JWT zone ID before any tool handler runs, storing the token in a &lt;code&gt;contextvar&lt;/code&gt; so tool functions stay signature-clean — the LLM never sees a &lt;code&gt;user_token&lt;/code&gt; parameter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="c"&gt;# requirements.txt
&lt;/span&gt;&lt;span class="err"&gt;mcp[cli]==1.9.4&lt;/span&gt;
&lt;span class="py"&gt;cfenv&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=0.5.3&lt;/span&gt;
&lt;span class="py"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=2.32.3&lt;/span&gt;
&lt;span class="py"&gt;uvicorn&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=0.34.3&lt;/span&gt;
&lt;span class="py"&gt;starlette&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=0.46.2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# mcp_server.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;contextvars&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cfenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AppEnv&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.server.fastmcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;starlette.middleware.base&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseHTTPMiddleware&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;starlette.requests&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;starlette.responses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;JSONResponse&lt;/span&gt;

&lt;span class="c1"&gt;# ── BTP service bindings (injected by CF via VCAP_SERVICES) ──────────────────
&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AppEnv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;_conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;my-connectivity-service&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;XSUAA_ZONE_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;your-btp-subaccount-zone-id&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;   &lt;span class="c1"&gt;# BTP cockpit → subaccount → Overview → ID
&lt;/span&gt;&lt;span class="n"&gt;ABAP_BASE&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://abap-backend-virtual:&amp;lt;port&amp;gt;/sap/rest&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;# ── per-request user JWT, threaded via contextvar ─────────────────────────────
&lt;/span&gt;&lt;span class="n"&gt;_user_jwt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;contextvars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContextVar&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;contextvars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ContextVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user_jwt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="c1"&gt;# ── token helper ──────────────────────────────────────────────────────────────
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_connectivity_token&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;credentials&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;token_service_url&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/oauth/token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;grant_type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;client_credentials&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;client_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;clientid&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;client_secret&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;clientsecret&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;access_token&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="c1"&gt;# ── JWT validation ────────────────────────────────────────────────────────────
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_decode_jwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;segment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;segment&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlsafe_b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_validate_jwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Lightweight zone-ID check. Cryptographic signature verification is done by the CC.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_decode_jwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;zid&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;XSUAA_ZONE_ID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Token zone &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;zid&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt; does not match this subaccount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="c1"&gt;# ── ABAP call via BTP connectivity proxy ─────────────────────────────────────
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_abap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;user_jwt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_user_jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;credentials&lt;/span&gt;
    &lt;span class="n"&gt;prx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;onpremise_proxy_host&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;onpremise_proxy_port&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ABAP_BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;hdrs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;SAP-Connectivity-Authentication&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_jwt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;# CC extracts identity from this JWT
&lt;/span&gt;        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Proxy-Authorization&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;_connectivity_token&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# app auth to the connectivity proxy
&lt;/span&gt;        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Accept&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;hdrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                         &lt;span class="n"&gt;proxies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prx&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;


&lt;span class="c1"&gt;# ── middleware: validate JWT before every MCP request ─────────────────────────
&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JWTMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseHTTPMiddleware&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;authorization&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Authorization: Bearer header required&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;_validate_jwt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;tok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_user_jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;_user_jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="c1"&gt;# ── MCP tool definitions ───────────────────────────────────────────────────────
&lt;/span&gt;
&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;abap-tools&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stateless_http&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_orders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return open orders for a customer. Runs in ABAP under the calling user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s identity.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_abap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;orders&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;customerId&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return details and line items for a single order.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_abap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;orders/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;safe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update_order_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Update an order&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s processing status. The ABAP system enforces the calling user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s auth objects.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_abap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;orders/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;safe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="c1"&gt;# ── entry point ───────────────────────────────────────────────────────────────
&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JWTMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;streamable_http_app&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uvicorn&lt;/span&gt;
    &lt;span class="n"&gt;uvicorn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;PORT&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth calling out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two tokens, two purposes.&lt;/strong&gt; The user JWT carries identity and is the only token that crosses the cloud/on-premise boundary. The connectivity &lt;code&gt;client_credentials&lt;/code&gt; token authenticates the CF app to the connectivity proxy (it never leaves BTP). No Destination service is involved — the virtual hostname and port are configured directly in the Cloud Connector and hardcoded in &lt;code&gt;ABAP_BASE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;SAP-Connectivity-Authentication&lt;/code&gt; is the magic header.&lt;/strong&gt; The BTP connectivity proxy reads this header and passes it through the WebSocket tunnel to the Cloud Connector. The Cloud Connector's JWT validator decodes it, verifies the RS256 signature against the XSUAA public key, extracts &lt;code&gt;user_uuid&lt;/code&gt;, and generates the per-user X.509 cert from that value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The MCP server connects to the Connectivity Proxy over plain HTTP.&lt;/strong&gt; The &lt;code&gt;ABAP_BASE&lt;/code&gt; URL and proxy address both use &lt;code&gt;http://&lt;/code&gt; — there is no TLS at this hop. TLS and the mTLS handshake with the ABAP ICM are handled entirely by the Cloud Connector and Connectivity Proxy; the Python process is not part of those connections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zone ID validation is lightweight on purpose.&lt;/strong&gt; The MCP server does not verify JWT signatures — that is the Cloud Connector's job and it does it cryptographically. The &lt;code&gt;zid&lt;/code&gt; check is a fast guard against tokens from a completely different XSUAA tenant hitting this endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token lifetime.&lt;/strong&gt; XSUAA &lt;code&gt;authorization_code&lt;/code&gt; JWTs are typically valid for 12 hours. Long agentic loops may outlive a token. The middleware does not check &lt;code&gt;exp&lt;/code&gt; — expiry is enforced by the Cloud Connector when it verifies the JWT signature, causing the ABAP call to fail. Design your agent framework to handle auth failures and trigger a token refresh rather than retrying blindly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connecting an agent
&lt;/h3&gt;

&lt;p&gt;Any MCP client that speaks streamable HTTP can consume this server — point it at the deployed route and pass the user's JWT as a bearer token. With LangChain's MCP adapter, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_mcp_adapters.client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MultiServerMCPClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MultiServerMCPClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;abap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transport&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;streamable_http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://my-abap-mcp-server.cfapps.&amp;lt;region&amp;gt;.hana.ondemand.com/mcp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_jwt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_tools&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# hand these to your agent / LLM
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key point: the &lt;code&gt;Authorization&lt;/code&gt; header carries the &lt;em&gt;end user's&lt;/em&gt; JWT, not a service credential. Every tool call the agent makes then executes in ABAP under that user's identity — which is the whole reason for the principal propagation chain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4 — ABAP Side Configuration
&lt;/h2&gt;

&lt;p&gt;This is where most setups stall. Four things must all be in place.&lt;/p&gt;

&lt;h3&gt;
  
  
  4a. STRUST — Import the Cloud Connector's CA certificates
&lt;/h3&gt;

&lt;p&gt;The ABAP system needs to trust two certificate authorities:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The CA that issued the Cloud Connector's System Certificate&lt;/strong&gt; (e.g. &lt;code&gt;SAPNetCA_G2&lt;/code&gt;). This is used for the mTLS handshake between the CC and the ABAP ICM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Cloud Connector's Local CA&lt;/strong&gt; (&lt;code&gt;CN=SCC-Local-CA&lt;/code&gt;). This is used to validate the short-lived per-user X.509 certs that the CC generates. Download this from your CC admin UI: Configuration → On Premise → CA Certificate → Download DER.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Import both into the &lt;strong&gt;SSL Server Standard PSE&lt;/strong&gt; in transaction &lt;code&gt;STRUST&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4b. RZ10 — Trust the Cloud Connector as a reverse proxy
&lt;/h3&gt;

&lt;p&gt;By default, ABAP ICM strips any &lt;code&gt;SSL_CLIENT_CERT&lt;/code&gt; header from incoming requests (a security measure). You must explicitly list the Cloud Connector as a trusted reverse proxy so ICM accepts the forwarded user cert.&lt;/p&gt;

&lt;p&gt;In transaction &lt;code&gt;RZ10&lt;/code&gt;, open the DEFAULT profile, Extended Maintenance, and add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;icm/&lt;/span&gt;&lt;span class="py"&gt;trusted_reverse_proxy_0&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;SUBJECT="CN=&amp;lt;your-cloud-connector-cn&amp;gt;, O=&amp;lt;org&amp;gt;, C=&amp;lt;country&amp;gt;", ISSUER="CN=&amp;lt;issuer-cn&amp;gt;, O=SAP, ..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use the exact Subject and Issuer from the CC's System Certificate. After saving, restart ICM via SMICM → Administration → ICM → Exit Hard → Global.&lt;/p&gt;

&lt;p&gt;If you skip this step, the CC will generate the user cert correctly, forward it in &lt;code&gt;SSL_CLIENT_CERT&lt;/code&gt;, and ABAP will silently discard it. The request will still reach ABAP but as an anonymous/default user — a subtle failure that is easy to miss.&lt;/p&gt;

&lt;h3&gt;
  
  
  4c. EXTID_DN — Map certificate subjects to SAP users
&lt;/h3&gt;

&lt;p&gt;In transaction &lt;code&gt;EXTID_DN&lt;/code&gt; (or via program &lt;code&gt;RSUSREXT&lt;/code&gt;), add a mapping entry:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;External ID Type&lt;/td&gt;
&lt;td&gt;DN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External ID&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CN=&amp;lt;user_uuid value&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User&lt;/td&gt;
&lt;td&gt;SAP user account&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For example, if your &lt;code&gt;user_uuid&lt;/code&gt; is &lt;code&gt;8f3a1b2c-4d5e-6789-abcd-ef0123456789&lt;/code&gt;, the entry would be &lt;code&gt;CN=8f3a1b2c-4d5e-6789-abcd-ef0123456789&lt;/code&gt; → SAP user &lt;code&gt;JDOE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For production with many users, use &lt;code&gt;CERTRULE&lt;/code&gt; to set up a pattern-based mapping rule (e.g. extract CN and map to the SAP user with the same ID) rather than maintaining individual entries.&lt;/p&gt;

&lt;h3&gt;
  
  
  4d. SICF — Enable certificate logon on the service
&lt;/h3&gt;

&lt;p&gt;In transaction &lt;code&gt;SICF&lt;/code&gt;, navigate to your REST API's ICF service node, open its properties, and verify that "Logon Through SSL Certificate" is enabled as an allowed logon method.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying end to end
&lt;/h3&gt;

&lt;p&gt;Call one of your MCP tools with a valid user JWT and then check &lt;code&gt;SM20&lt;/code&gt; (Security Audit Log) in the ABAP system. If the session shows the actual user — not a technical account — principal propagation is working. If it shows the wrong user or is anonymous, work through the debugging checklist below.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Six Trust Boundaries in One Diagram
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[BTP Subaccount]
     │
     │  1. WebSocket tunnel (registered CC)
     ▼
[Cloud Connector]
     │
     │  2. XSUAA public key (synced from BTP trust config)
     │     → verifies JWT signature
     │
     │  3. mTLS to ABAP ICM
     │     (CC presents System Cert; ABAP trusts its CA via STRUST)
     ▼
[ABAP ICM]
     │
     │  4. Trusted reverse proxy param (RZ10)
     │     → accepts SSL_CLIENT_CERT header from CC
     │
     │  5. Local CA in STRUST
     │     → validates the forwarded short-lived user cert
     │
     │  6. EXTID_DN / CERTRULE
     │     → maps CN=&amp;lt;user_uuid&amp;gt; to a SAP user account
     ▼
[ABAP business logic — executing as the real user]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All six must be in place. A failure at any boundary usually manifests as the request completing but the ABAP session belonging to the wrong user or failing authorization.&lt;/p&gt;




&lt;h2&gt;
  
  
  Debugging Checklist
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TLS handshake fails between CC and ABAP&lt;/td&gt;
&lt;td&gt;CC's System Cert CA not in STRUST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Request reaches ABAP as anonymous user&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;trusted_reverse_proxy&lt;/code&gt; param missing, or ICM not restarted after RZ10 change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ABAP returns "unknown CA" on client cert&lt;/td&gt;
&lt;td&gt;Cloud Connector's Local CA not imported in STRUST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SM20 shows "external ID not found"&lt;/td&gt;
&lt;td&gt;No EXTID_DN entry for &lt;code&gt;CN=&amp;lt;user_uuid&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;403 after user appears to be logged in&lt;/td&gt;
&lt;td&gt;Missing ABAP authorization objects for the propagated user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Connector log: "JWT cannot be verified"&lt;/td&gt;
&lt;td&gt;XSUAA signing key not synced from BTP trust configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Connector log: "JWT cannot be verified" (intermittent)&lt;/td&gt;
&lt;td&gt;CC key cache stale — trigger a manual trust sync in the CC admin UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;401&lt;/code&gt; from the MCP server&lt;/td&gt;
&lt;td&gt;User JWT &lt;code&gt;zid&lt;/code&gt; does not match the configured zone ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;502&lt;/code&gt; from the MCP server&lt;/td&gt;
&lt;td&gt;Connectivity service credentials missing or proxy unreachable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The core flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User logs in via XSUAA &lt;code&gt;authorization_code&lt;/code&gt; flow → gets a JWT with &lt;code&gt;user_uuid&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;LLM agent calls an MCP tool, forwarding the user JWT&lt;/li&gt;
&lt;li&gt;MCP server on BTP CF routes the call through the connectivity proxy, setting &lt;code&gt;SAP-Connectivity-Authentication: Bearer &amp;lt;user_jwt&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Cloud Connector verifies the JWT, extracts &lt;code&gt;user_uuid&lt;/code&gt;, generates a short-lived X.509 cert &lt;code&gt;CN=&amp;lt;user_uuid&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;ABAP ICM receives the request with the user cert forwarded in &lt;code&gt;SSL_CLIENT_CERT&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;ABAP maps the cert subject to a SAP user via EXTID_DN and runs the business logic as that user&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key insight is that identity is carried as a signed JWT token across the cloud layer, then converted to an X.509 certificate at the network boundary — a format ABAP's ICM already understands natively. The MCP server is just the latest entry point into an existing protocol.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post is intended as guidance to illustrate how principal propagation works end to end. The code is simplified to keep the focus on the identity flow — a production deployment will need additional hardening (signature validation at the MCP layer, error handling, secrets management, logging, and so on). Treat it as a starting point, not a drop-in solution.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sap</category>
      <category>mcp</category>
      <category>ai</category>
      <category>cloudfoundry</category>
    </item>
    <item>
      <title>Teaching an LLM to pull MCP Resources and Prompts on demand (instead of drowning it in context)</title>
      <dc:creator>Anirbaan Chowdhury</dc:creator>
      <pubDate>Sun, 30 Aug 2026 05:19:03 +0000</pubDate>
      <link>https://dev.to/anirbaan_chowdhury_58a600/teaching-an-llm-to-pull-mcp-resources-and-prompts-on-demand-instead-of-drowning-it-in-context-591l</link>
      <guid>https://dev.to/anirbaan_chowdhury_58a600/teaching-an-llm-to-pull-mcp-resources-and-prompts-on-demand-instead-of-drowning-it-in-context-591l</guid>
      <description>&lt;h1&gt;
  
  
  Teaching an LLM to &lt;em&gt;pull&lt;/em&gt; MCP Resources and Prompts on demand (instead of drowning it in context)
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;How we wired the Model Context Protocol's "application-controlled" primitives into a model-controlled tool-calling loop — and why that small shift changes everything about context hygiene.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;The Model Context Protocol (MCP) gives a server three ways to expose capability: &lt;strong&gt;tools&lt;/strong&gt;, &lt;strong&gt;resources&lt;/strong&gt;, and &lt;strong&gt;prompts&lt;/strong&gt;. Tools drop straight into an LLM's function-calling loop. Resources and prompts don't — they're &lt;em&gt;application-controlled&lt;/em&gt;, so most integrations just &lt;strong&gt;dump every resource's content into the system prompt&lt;/strong&gt; and hope for the best.&lt;/p&gt;

&lt;p&gt;That approach bloats context, truncates large documents, breaks on binary files, and gives the model zero say in what it actually needs.&lt;/p&gt;

&lt;p&gt;Our fix: &lt;strong&gt;promote resources and prompts into synthetic, auto-approved LLM tools&lt;/strong&gt; — &lt;code&gt;read_resource(uri)&lt;/code&gt; and &lt;code&gt;invoke_prompt(name)&lt;/code&gt;. The system prompt now carries only a lightweight &lt;em&gt;catalog&lt;/em&gt; (URIs + descriptions). The model reads a resource &lt;strong&gt;only when it decides it needs one&lt;/strong&gt;, through the exact same tool-calling machinery it already uses. On-demand, selective, full-fidelity.&lt;/p&gt;




&lt;h2&gt;
  
  
  The three MCP primitives — and the control model nobody talks about
&lt;/h2&gt;

&lt;p&gt;MCP defines three server capabilities, but the interesting part is &lt;em&gt;who's in control&lt;/em&gt; of each:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Primitive&lt;/th&gt;
&lt;th&gt;Who decides when it's used&lt;/th&gt;
&lt;th&gt;Natural fit for tool-calling?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tools&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The &lt;strong&gt;model&lt;/strong&gt; (it calls them)&lt;/td&gt;
&lt;td&gt;✅ Yes — this is what function calling &lt;em&gt;is&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Resources&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The &lt;strong&gt;application / user&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;❌ No native hook in the loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prompts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The &lt;strong&gt;user&lt;/strong&gt; (usually a slash-command)&lt;/td&gt;
&lt;td&gt;❌ No native hook in the loop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Tool calling is &lt;em&gt;model-controlled&lt;/em&gt; by design: the LLM emits a &lt;code&gt;tool_use&lt;/code&gt; block, you execute it, you feed the result back. Beautiful.&lt;/p&gt;

&lt;p&gt;Resources and prompts are &lt;em&gt;application-controlled&lt;/em&gt;. The spec's mental model is a human clicking "attach this file" or "/use this prompt template." There is no obvious place for them inside an autonomous agent's reasoning loop. So what do most integrations do?&lt;/p&gt;




&lt;h2&gt;
  
  
  The naive approach (and why it hurts)
&lt;/h2&gt;

&lt;p&gt;The path of least resistance is to fetch &lt;strong&gt;every&lt;/strong&gt; resource at startup and paste it into the system prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Available Resources
### Resource: SUM ABAP Test Matrix
URI: sap-btp://sum-abap-v1
Content:
&amp;lt;... 11,000 characters of markdown ...&amp;gt;
### Resource: API Docs
URI: sap-btp://api-docs
Content:
&amp;lt;... more ...&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four problems show up fast:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Context bloat.&lt;/strong&gt; Every request pays for every resource, whether or not it's relevant. Ten resources × a few thousand tokens each = a system prompt that dwarfs the actual conversation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Truncation.&lt;/strong&gt; To keep bloat sane, you cap content (&lt;code&gt;content[:2000]&lt;/code&gt;) — and now large documents are silently chopped. In our case the SUM ABAP matrix lost its entire product list and output-format section below the 2,000-char line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binary breaks.&lt;/strong&gt; A PDF or PNG resource has no meaningful text form. Naive extractors try &lt;code&gt;blob.as_string()&lt;/code&gt;, hit a &lt;code&gt;UnicodeDecodeError&lt;/code&gt;, and quietly emit &lt;code&gt;"[No content available]"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No agency.&lt;/strong&gt; The model can't say "I don't need any of these right now" or "give me &lt;em&gt;that&lt;/em&gt; one, in full." It's force-fed.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The insight: make resources and prompts look like tools
&lt;/h2&gt;

&lt;p&gt;Here's the shift. The LLM already has a clean, well-understood way to ask for something on demand: &lt;strong&gt;it calls a tool.&lt;/strong&gt; So instead of fighting the control model, we &lt;em&gt;translate&lt;/em&gt; it.&lt;/p&gt;

&lt;p&gt;We register two &lt;strong&gt;DARA-internal&lt;/strong&gt; tools that don't exist on any MCP server — they're synthesized client-side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;read_resource(uri)&lt;/code&gt;&lt;/strong&gt; → fetches one resource's content when the model asks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;invoke_prompt(name)&lt;/code&gt;&lt;/strong&gt; → injects a named prompt template into the conversation when the model asks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The system prompt now advertises only a &lt;strong&gt;catalog&lt;/strong&gt; — names, URIs, and descriptions, &lt;em&gt;no content&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Available Resources
The following resources can be read on demand. To read one, call the
`read_resource` tool with its exact URI. Do not assume a resource's
contents until you have read it.

### Resource: SUM ABAP Test Matrix
URI: sap-btp://sum-abap-v1
Description: SUM (Software Update Manager) test matrix specification for ABAP products
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model sees what exists, then reaches for exactly what it needs — through the tool loop it already speaks fluently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Implementation walkthrough
&lt;/h2&gt;

&lt;p&gt;We built this on top of LangGraph + &lt;code&gt;langchain-mcp-adapters&lt;/code&gt;, but the pattern is framework-agnostic.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Synthesize the tool with a schema
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;read_resource&lt;/code&gt; is a &lt;code&gt;StructuredTool&lt;/code&gt; with a one-field schema. Its &lt;code&gt;func&lt;/code&gt; is a no-op lambda — we never actually &lt;em&gt;run&lt;/em&gt; it as a function; we intercept it in the graph (see step 3).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_core.tools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StructuredTool&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_ReadResourceArgs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The exact URI of the MCP resource to read, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                                 &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;e.g. &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sap-btp://sum-abap-v1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;read_resource_tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;StructuredTool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                      &lt;span class="c1"&gt;# placeholder — handled in the graph
&lt;/span&gt;    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read_resource&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Read the full contents of an MCP resource by its URI. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Call this when the user asks to read, open, or summarize a resource, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;or when you need a resource&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s contents to answer. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Only resources listed under &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Available Resources&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; can be read.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;args_schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_ReadResourceArgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;read_resource_tool&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;allowed_tools_without_review&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read_resource&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# auto-approve, no human gate
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things matter here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The description is the UX.&lt;/strong&gt; It's the only instruction the model gets on &lt;em&gt;when&lt;/em&gt; to call this. Write it like a prompt, because it is one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-approval.&lt;/strong&gt; In an agent with human-in-the-loop review, reading a read-only resource shouldn't require a click. We add it to the "no review" allow-list so the graph routes straight to execution.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Build a content map once, keyed by URI
&lt;/h3&gt;

&lt;p&gt;We fetch resources once (the adapter already returns their content as &lt;code&gt;Blob&lt;/code&gt; objects) and index them by URI so the on-demand read is an O(1) lookup — no second network round-trip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;resources_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
    &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_extract_resource_fields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# URIs can arrive as pydantic AnyUrl objects — normalize to str so the
&lt;/span&gt;    &lt;span class="c1"&gt;# plain-string URI the LLM passes actually matches the dict key. (Gotcha!)
&lt;/span&gt;    &lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;resources_content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Intercept the tool call in the graph
&lt;/h3&gt;

&lt;p&gt;When the model emits a &lt;code&gt;read_resource&lt;/code&gt; call, we don't invoke a function — we look up the content and hand it back as a &lt;strong&gt;tool message&lt;/strong&gt;. Because a tool result flows naturally back into the model's context, the content lands &lt;em&gt;only when requested&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read_resource&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;args&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uri&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resources_content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;new_messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read_resource&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;          &lt;span class="c1"&gt;# full content, no truncation
&lt;/span&gt;            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_call_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;available&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resources_content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;new_messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read_resource&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Resource &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; not found. Available URIs: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;available&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_call_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;continue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mirror image for prompts: &lt;code&gt;invoke_prompt&lt;/code&gt; injects the template's messages as real &lt;code&gt;Human&lt;/code&gt;/&lt;code&gt;AI&lt;/code&gt; turns, which is exactly how the MCP spec intends prompts to be surfaced.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Handle binary honestly (don't decode bytes as text)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;langchain-mcp-adapters&lt;/code&gt; collapses every resource into a &lt;code&gt;Blob&lt;/code&gt;: text lands in &lt;code&gt;.data&lt;/code&gt; as a &lt;code&gt;str&lt;/code&gt;, binary as raw &lt;code&gt;bytes&lt;/code&gt;, with the media type on the &lt;strong&gt;&lt;code&gt;.mimetype&lt;/code&gt; attribute&lt;/strong&gt; (not in metadata — a common trip-up). So we branch on the mime type instead of blindly calling &lt;code&gt;.as_string()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_is_text_mime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;mime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mime&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+xml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+yaml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;mime&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/xml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/yaml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;# inside the extractor, for a Blob:
&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;_is_text_mime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mime&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Binary (PDF, PNG, ...) → an honest descriptor, NOT raw bytes/base64
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[Binary resource: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mime&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;application/octet-stream&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;_human_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. This is not text and cannot be inlined; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;open it with a client that handles its media type.]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is aligned with the MCP spec itself: binary payloads belong in typed media content blocks or are referenced by URI — never stuffed into a text field. A model reading &lt;code&gt;[Binary resource: application/pdf, 240.0 KB]&lt;/code&gt; knows exactly what it's looking at and can decide what to do, instead of choking on garbage or getting a misleading "no content."&lt;/p&gt;




&lt;h2&gt;
  
  
  The flow, end to end
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   ┌─────────────────┐
   │  User message   │
   └────────┬────────┘
            │
            ▼
   ┌──────────────────────────────────────────┐
   │ System prompt = resource CATALOG only     │
   │ (URIs + descriptions, NO content)         │
   └────────┬─────────────────────────────────┘
            │
            ▼
      ┌───────────────┐
      │  LLM decides  │
      └──┬─────────┬──┘
         │         │
 needs a │         │ doesn't need one
 resource│         └──────────────► Answer directly
         ▼
 ┌──────────────────────────┐
 │ tool_use: read_resource  │
 │        (uri)             │
 └────────────┬─────────────┘
              ▼
 ┌──────────────────────────────┐
 │ Graph intercepts the call     │
 │ (auto-approved, no HITL gate) │
 └────────────┬─────────────────┘
              ▼
 ┌──────────────────────────────┐
 │ Look up URI in content map    │
 └───────┬───────────────┬──────┘
         │ text          │ binary
         ▼               ▼
 ┌────────────────┐  ┌──────────────────────┐
 │ Full content   │  │ Descriptor:          │
 │ as tool message│  │ mime type + size     │
 └───────┬────────┘  └───────────┬──────────┘
         │                       │
         └───────────┬───────────┘
                     ▼
              (back to LLM ──► answer)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only the &lt;em&gt;"needs a resource"&lt;/em&gt; branch ever pays the content cost — and it pays the &lt;strong&gt;full&lt;/strong&gt; cost, untruncated, for &lt;strong&gt;just&lt;/strong&gt; that resource.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this is nice
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Token-efficient.&lt;/strong&gt; The system prompt holds a catalog (tens of tokens per resource), not a library. Content enters context only on read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selective.&lt;/strong&gt; The model — or the user, phrasing a request — decides what to load. Ten irrelevant resources cost almost nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full fidelity.&lt;/strong&gt; No truncation cap needed, because you're no longer defending against ten simultaneous dumps. The one resource you asked for arrives whole.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binary-safe.&lt;/strong&gt; Mime-driven handling means PDFs and images degrade to a clear descriptor instead of a crash or a lie.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spec-aligned.&lt;/strong&gt; Application-controlled primitives stay application-controlled — we just expose an &lt;em&gt;affordance&lt;/em&gt; for the model to request them, rather than pre-deciding on its behalf.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uniform mental model.&lt;/strong&gt; Resources, prompts, and real tools all flow through one loop. No special-case rendering paths, no bespoke context-stuffing logic.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Gotchas worth stealing
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AnyUrl&lt;/code&gt; vs &lt;code&gt;str&lt;/code&gt;.&lt;/strong&gt; MCP resource URIs often surface as pydantic &lt;code&gt;AnyUrl&lt;/code&gt; objects. If your content map is keyed by &lt;code&gt;AnyUrl&lt;/code&gt; and the LLM passes a plain string, &lt;code&gt;.get()&lt;/code&gt; silently misses. Normalize to &lt;code&gt;str&lt;/code&gt; on &lt;strong&gt;both&lt;/strong&gt; sides.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mime lives on &lt;code&gt;.mimetype&lt;/code&gt;, not metadata.&lt;/strong&gt; For &lt;code&gt;langchain-mcp-adapters&lt;/code&gt; &lt;code&gt;Blob&lt;/code&gt;s, the media type is an attribute; metadata only carries the &lt;code&gt;uri&lt;/code&gt;. Read the right field or every binary looks like &lt;code&gt;application/octet-stream&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tool description &lt;em&gt;is&lt;/em&gt; the routing logic.&lt;/strong&gt; There's no separate policy telling the model when to read a resource — only the tool's &lt;code&gt;description&lt;/code&gt;. Invest in it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-approve read-only reads.&lt;/strong&gt; If your agent has human review gates, forcing a click to read a read-only resource kills the UX. Allow-list it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A no-op &lt;code&gt;func&lt;/code&gt; is fine.&lt;/strong&gt; The synthesized tool never executes as a function; the graph intercepts it. The lambda is just there to satisfy the schema.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Where it goes next
&lt;/h2&gt;

&lt;p&gt;The binary branch is the single hook for richer handling: extract PDF text server-side, or emit an &lt;code&gt;ImageContent&lt;/code&gt; block to a vision-capable model for images. Because everything already funnels through one &lt;code&gt;read_resource&lt;/code&gt; path, adding a modality is a localized change — not a re-architecture.&lt;/p&gt;

&lt;p&gt;The bigger takeaway: when a protocol primitive doesn't fit your execution model, don't force the model to swallow it up front. Give the model an &lt;strong&gt;affordance to ask&lt;/strong&gt;, and let the loop it already understands do the rest.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built on the Model Context Protocol (2025-03-26), LangGraph, and &lt;code&gt;langchain-mcp-adapters&lt;/code&gt;. The pattern is framework-agnostic — anywhere you have tool-calling and MCP, you can do this.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>llm</category>
      <category>python</category>
    </item>
  </channel>
</rss>
