The day our architecture repository finally became useful was the day we stopped asking anyone to log into it. Welcome to the Copilot Studio, Azure, LeanIX MCP server party.
One more ingredient sits behind the agent: a knowledge base of internal architecture practice documents, agreements, and definitions. That is what makes the answers feel organizational rather than generic. There is a fifth layer on the roadmap as well: autonomous agents in Microsoft Foundry that would feed vendor and internal system updates back into the repository automatically. On the tape it is work in progress, so treat it as a direction of travel, not a shipped capability.
Two claims from the tape will matter later, and they pull against each other. Sukachev told the audience that, at that point, the LeanIX MCP server’s toolset “does not include any write functions” and described it as read-only. SAP’s own current documentation and community threads describe update and create tools behind role-based permissions, surfaced to an agent only when the connecting account holds the matching rights. Both statements can be true at once, and the reason is the most useful technical fact in this story. Hold that thought.
The instinct, when you first wire an agent to a repository, is to point it straight at the vendor’s endpoint. The demo works the same afternoon: the LeanIX MCP server accepts a technical user’s API token, Copilot Studio’s MCP onboarding wizard takes a URL and a credential, and the answers start flowing. I have watched three organizations do exactly this, and I have watched all three hit the same wall within two quarters, in different orders.
The first wall is sprawl. Team A connects its agent, and the token lands in a config field with no expiry discipline. Team B builds a second agent against the raw endpoint, then a third. Six months later the security review asks a simple question that has no good answer: who can call this repository, with what credential, from where, and can you prove what they read? The raw endpoint gives you nothing to audit with. The MCP protocol’s trust model is one client, one server, and a handshake; authentication, throttling, and audit are your problem, not the protocol’s. Every one of those gaps has to live somewhere, and the somewhere is either a gateway or your incident channel.
The second wall is sovereignty. A European insurer cannot paste a technical user’s token into a SaaS agent configuration and call the result governed.
The compliance review comes with questions: where does the token live, which region processes the query text, whose tenant issued the identity, and what does the model provider see. Microsoft’s own model documentation carries a warning worth reading twice: experimental models in Copilot Studio may process data outside your organization’s geographic boundaries. Put Azure API Management in the middle, in your region, and the story changes shape. The agent talks to an endpoint you own. Tokens get validated against your Entra ID tenant at the gateway. The one backend credential to LeanIX lives in a single named value on the gateway, never in an agent config. The audit trail becomes a property of the gateway, so “prove what was read” turns into a Log Analytics query instead of an archaeology project.
The third wall is money, and it arrives as a surprise invoice rather than a denial of service. Copilot Studio bills agents in Copilot Credits: pay-as-you-go meters at a published $0.01 per credit against an Azure subscription, prepaid packs listed at $200 for 25,000 credits, and the per-user Microsoft 365 Copilot license at $30 per month carrying its own terms (simple answers for licensed users inside Teams count zero against the meter, per Microsoft’s billing documentation). The published rates are the easy part. The hard part is that a chatty agent inside a busy Teams channel multiplies queries in ways seat licenses never did, and every tool-carrying turn burns credits proportional to task complexity. The tape shows the cost anti-pattern in the wild: asked for technologies near end of life, the agent “needs to fetch, first of all, the list of all technologies,” then filters client-side. A full-catalog fetch on every question is the conversational equivalent of SELECT with no WHERE clause, and it bills like one. The fixes belong at the architecture level: push filters into tool arguments, cap volume at the gateway, and arm a budget alert before the finance team arms one for you.
Latency is the argument people raise against the gateway, and it deserves a straight answer. A governed hop through APIM adds milliseconds, since the certificate check, the JWT validation, and the policy match are cheap operations against a regional endpoint. What eats the seconds in this stack is model turns and tool fan-out: every reasoning step costs a , every full-catalog fetch drags a fat payload across the wire, and a stalled stream with no attached timeout turns a four-second answer into the four-minute silence from the podcast. A gateway changes none of that by simply existing. The policies do the work. With retry, timeout, and correlation-ID policies configured, the same middle layer that was supposedly the latency tax becomes the single place in the chain that can tell you where the time went. That last claim matters more than the latency debate itself. The episode’s honest admission, “sometimes it works, sometimes not,” is what living without per-hop observability sounds like.
The counter-argument gets its day in court: why not buy seats for everyone and skip the middle layer entirely. Seats for five thousand people at enterprise EA tooling prices would fund several gateway teams for a decade, and most of those five thousand people need ten answers a year, not a query workbench. Practitioners still need the full LeanIX surface, and the guest was explicit on the show that the assistant does not retire the EA practice; the professionals still curate, review, and govern. What the conversational layer changes is who gets to ask, and how often the answers get consulted before decisions get made. In his phrasing, the repository stops being a mystery. The architecture review board gets its questions answered before the meeting, the underwriter gets the aging-system risk on a core policy without filing a ticket, and the keeper of the catalog stops being a queue.
The team’s published sequence diagram walks one question through the whole stack, and it is the right question to walk: a reinsurance governance lead typing “which core actuarial systems face retirement risk this year” into Teams. Follow the actors. Teams hands the text to the Copilot Studio agent. The agent, running its reasoning model, decides it needs lifecycle data and issues a tool call over MCP. The call hits APIM, which validates the Entra ID token, checks the rate limit, applies tool policy, and forwards to the LeanIX MCP server. The server queries the workspace, the portfolio and lifecycle dates come back through the gateway, and the agent synthesizes the result against the internal governance rules in its knowledge base before rendering a structured report with mitigation steps into the chat window.
Nine hops in the published diagram, counting both directions. Every hop can fail, every hop can add latency, every hop can hold a secret you forgot to rotate, and not one of them will say a word until you instrument the path on day one.
Strip the product names and the mechanics fit in your head. MCP is JSON-RPC 2.0 over an HTTP POST, usually at a path ending in /mcp, with responses that arrive either as plain JSON or as a server-sent-events stream. A client introduces itself with an initialize call, asks what the server can do with tools/list, then invokes a specific tool with tools/call. That is the whole conversation. The spec revisions in the wild are 2025-06-18 and 2025-11-25 (Streamable HTTP), and a 2026-07-28 revision adds a stateless protocol core and an extensions mechanism; the handshake negotiates the version, so watch the echo in the response.
APIM_HOST="apim-ea-prod.azure-api.net"MCP_URL="https://$APIM_HOST/leanix-mcp/mcp"curl-sS -X POST "$MCP_URL" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "Authorization: Bearer $ENTRA_TOKEN" \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": { "name": "ea-ops-check", "version": "0.1.0" } } }'
A healthy chain answers with the negotiated protocol version, the declared capabilities, and a serverInfo block. It should look close to this, with names and versions that belong to your chain:
{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-06-18", "capabilities": { "tools": { "listChanged": true } }, "serverInfo": { "name": "leanix-mcp-via-apim", "version": "1.0.0" } }}
Read that response like a diagnostician. The protocolVersion tells you what the gateway negotiated with the backend. The capabilities block tells you whether the thing behind APIM is genuinely an MCP server rather than a REST API someone pointed you at. The moment this handshake returns anything other than a JSON-RPC result, every downstream complaint about the agent being slow is premature. The transport is the patient.
The catalogue call comes next, and its result is the most consequential JSON in this architecture:
curl-sS -X POST "$MCP_URL" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "Authorization: Bearer $ENTRA_TOKEN" \ -d '{ "jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {} }'
The shape that comes back matters more than the specific entries. One tool name has surfaced publicly in SAP’s community exchange, and it exposes the naming convention:
{ "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "LeanIX-MCP-Server-get_architecture_decisions", "description": "Read architecture decisions from the connected SAP LeanIX workspace", "inputSchema": { "type": "object", "properties": { "filter": { "type": "string", "description": "Server-side filter applied before results return" } }, "required": [] } } ] }}
SAP’s tool naming follows the LeanIX-MCP-Server- convention, and the story behind the entry above is instructive: a developer shared his agent's catalogue in a May 2026 community thread and asked why every entry he saw was a read. The answer from SAP's community manager is the sentence this whole article could have been built around: the tools surfaced to your agent reflect the permissions of the user, or technical user, whose credential made the connection. Read-class tools appear to read-only accounts. Create and update tools appear to accounts holding create and update permissions. The catalogue is a mirror of the connecting identity. That single fact reconciles the podcast's read-only claim with SAP's documentation of update and create tools: his connection ran on a read-class credential, yours will be whatever you provision, and that sentence should frighten anyone whose governance model is "we gave the agent a service account last year and moved on."
Invocation looks like this, and the interesting part is where the filtering happens:
curl-sS -X POST "$MCP_URL" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "Authorization: Bearer $ENTRA_TOKEN" \ -d '{ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "LeanIX-MCP-Server-get_architecture_decisions", "arguments": { "filter": "status: accepted, year: 2026" } } }'
The arguments object has to match the inputSchema the server published, so read your own tools/list before scripting anything; the values above are illustrative. The structural lesson is bigger than any single tool: filters belong in the arguments, applied server-side, so the payload crossing the gateway stays thin. The agent that fetches an entire catalogue and filters client-side pays in latency and tokens on every question, and the podcast demo showed exactly that behavior in the wild.
The agent itself is the least glamorous part, which is fine. In Copilot Studio it is a declarative agent with four moving pieces: an instruction block, a set of knowledge sources, the MCP tool connection, and a model selection. The demo question set from the episode (“list all APIs,” “technologies close to end of life,” “what applications have obsolescence risk”) resolves through the same tool connection, with the instructions and knowledge base doing the work of making answers organizational rather than generic. The internal agreements and definitions live in the knowledge sources, and the guest’s point on the show was that the results mix repository data with those internal definitions, which is why the answers read like they came from inside the company rather than from a search engine. On the model point, the caption track flags the model name as unclear, and it does not need the flag: Claude Opus 4.6 shipped on February 5, 2026, listed at $5 per million input tokens and $25 per million output, and Copilot Studio’s picker carries multi-vendor models, with Microsoft’s own Copilot Studio blog naming GPT-5, GPT-5.1, and Claude models arriving same-day as their public releases. A heavyweight reasoning model is a defensible pick for read-heavy portfolio questions with synthesis attached; the cost-conscious split would put a cheaper model on the small talk and the serious one on the report questions. The picker tags regional availability alongside model names, and for shops with residency constraints that table earns a read before the first agent ships.
Between the agent and the MCP server sits the piece that earns its keep. In the portal, APIM has a dedicated MCP Servers section, separate from the standard APIs blade, and registering the LeanIX endpoint is an External MCP server registration: the transport (HTTP or SSE, with SSE deprecated since the 2024–11–05 protocol revision) is detected and proxied for you, and the agent is pointed at the APIM-exposed URL instead of the vendor’s. Two facts about the current implementation need their own sentences. The standard APIM policy engine runs on MCP traffic, which means JSON-RPC tool calls are inspectable, filterable, and rate-limited with the same policy XML you already know. And the managed MCP surface covers tools: MCP resources and prompts, the other two interface types in the spec, sit outside what APIM manages today, which for this use case is survivable, since the assistant’s value flows through tools, but it belongs in your design notes rather than your assumptions. The MCP features run on the Classic Developer, Basic, Standard, and Premium tiers plus the v2 tiers, so the lab and the production shape differ mainly in networking and SLA, with capability carrying across both.
The metadata story closes the loop in a direction most catalogs never manage: the repository learns about new systems without a human typing them in. The pattern on the tape runs APIM first, Azure API Center second, LeanIX third, so the people maintaining the catalog inherit the inventory instead of chasing it. In the guest’s words, “we don’t need to go and create them again; we just get them in LeanIX.” The plumbing for the middle of that chain is documented and scriptable, and the runbook at the end of this article wires it. The direction is the point. When the registry feeds the repository, the catalog stops aging by default.
Breakage in this stack rarely looks like an error message. It looks like silence, or confidence, or a line item.
The silence came first on the tape: minutes of dead air, a guess about connectivity, a joke about candy, and no evidence anywhere in the conversation about which hop failed. The plausible culprits read like a distributed-systems bingo card: a tool call dragging a full catalogue across a cold connection, a stream stalled with no heartbeat, a backend wait with no timeout attached, a rate limit tripped somewhere upstream with nobody watching, a model turn eating the agent’s step budget mid-fan-out. Every one of those is diagnosable within seconds when the request path carries correlation IDs and per-hop timestamps. None of them is diagnosable from the chat window. The episode’s “sometimes it works, sometimes not” is the sound of a system that has not been given the instrumentation to say which. That admission is standard for agent stacks; they all arrive in that condition, and a gateway fixes it in an afternoon.
Confidence breaks worse than silence. A stale repository wrapped in fluent prose is a screenshot waiting to happen in a steering deck. The guest said the quiet part out loud: the answers can only be as good as the data underneath them, professional review stays required, and the assistant removes nobody from the loop. That candor should be standard practice, since the failure mode here is subtler than hallucination. The agent faithfully reports a repository nobody has corrected since the last reorganization, at machine speed, to everyone in the company at once, where the old broken version only misled the forty specialists who happened to log in. Conversational access multiplies the audience for your data hygiene. That cuts both ways, and most organizations are not ready for the cut.
The third break is the one this article has been circling: the tool catalogue is a function of the connecting identity’s permissions, so it can change under an agent without anyone touching the agent. The podcast recorded a read-only toolset. SAP’s community manager confirmed months before air that create-capable accounts see create tools, and offered permission-checking as the first diagnostic when a catalogue looks thin. A vendor’s surface can grow overnight with a release note nobody read. An agent pointed at the raw endpoint inherits every tool the vendor ships, the day it ships. A young EA assistant connected with a generously provisioned technical user sits one provisioning mistake away from writing to the system of record, and nobody signed up for that in a design review. The correction is an allowlist at the gateway, which turns your exposed tool surface into a decision instead of an inheritance.
The sprawl failure compounds the tool-surface one. A February 2026 answer on Microsoft’s own Q&A site documents that Copilot Studio could not connect directly to MCP servers hosted inside Azure private networks, and that its supported connection auth models are API key and Entra ID. Verify current support before you design around either sentence, since this surface moves monthly. The gateway pattern is the answer that was hiding inside the limitation: one public, governed, APIM-fronted endpoint beats a private endpoint your agent platform cannot reach, and every agent in the company points at the same front door. When that front door validates Entra ID tokens and logs every tool call, the second and third agent cost you configuration work instead of a security review.
The money failure arrives politely. A pilot with three users behaves. The same agent published into a Teams channel with five hundred members inherits the channel’s curiosity, and Copilot Credit consumption scales with task complexity across every tool-carrying turn. Nothing in the stack yells at you when a well-intentioned team wires the assistant into a Friday status channel. The controls that work are unglamorous: burst limits at the gateway, a monthly volume ceiling, a budget with alerts at 80 and 100 percent, and instruction text that keeps the agent from fetching catalogues for small talk. The alternative is the finance team finding it first, with your name on the cost center.
Under all four breaks sits a human one the guest named from experience: architecture information living in multiple places, un-integrated, assembled by hand whenever anyone needs the aggregate view. Every integration decision in this article reduces how often a human has to be that aggregation layer. The pattern is worth more than any individual tool in it.
Everything above collapses into a build order a small platform team can execute in a week, most of which is waiting on approvals. The sequence assumes a LeanIX workspace with an API-capable technical user, a Copilot Studio environment with the agent scaffolded, and an APIM instance in the region your compliance team prefers. Where a vendor surface shifts, and in this space it shifts monthly, the validation commands at the end are the contract: they fail loudly when reality and this article disagree.
Decide the permission shape before provisioning anything, since the tool catalogue your agent sees is a direct consequence of it. The safest default I know: one technical user in LeanIX with read-class permissions, its API token stored as a named value at the gateway and nowhere else, so the agent physically cannot see write tools no matter what the vendor ships. That choice converts the entire write-surface question from a governance debate into a non-event. The autonomous update agents on the team’s Foundry roadmap make the point sharper, since autonomous anything inherits exactly the identity you gave it. Provision the frontend identity with Entra ID:
az ad app create \ --display-name "api-ea-leanix-gateway" \ --identifier-uris "api://ea-leanix-gateway" \ --sign-in-audience "AzureADMyOrg"
Expose a scope on that registration (portal path: App registration, Expose an API, Add a scope) so tokens minted for the agent’s connection carry an audience the gateway can require. When the first test call arrives, capture the JWT, read the aud and appid claims out of it, and pin both in the gateway policy. Pinning what you observed, rather than what a blog post guessed, is the practice that survives audits.
Register the LeanIX MCP endpoint as an External MCP server under APIM’s MCP Servers section, then open the policy editor and make the tool surface a decision. The policy below does five things in order: reject any caller without a valid Entra ID token for your audience, stamp a correlation ID, rate-limit per credential, answer tools/list from a pinned allowlist instead of forwarding it, and refuse any tools/call naming a tool that is not on the list. It is long, and it is complete:
<policies> <inbound> <validate-jwt header-name="Authorization" require-scheme="bearer" failed-validation-httpcode="401" failed-validation-error-message="Missing or invalid Entra ID token"> <openid-config url="https://login.microsoftonline.com/TENANT_ID/.well-known/openid-configuration" /> <audiences> <audience>api://ea-leanix-gateway</audience> </audiences> <required-claims> <claim name="appid" match="any"> <value>THE_APP_ID_YOU_OBSERVED_IN_THE_FIRST_TOKEN</value> </claim> </required-claims> </validate-jwt> <set-header name="X-Correlation-Id" exists-action="override"> <value>@(context.RequestId.ToString())</value> </set-header> <rate-limit-by-key calls="60" renewal-period="60" counter-key="@(context.Request.Headers.GetValueOrDefault("Authorization","anon").GetHash())" /> <choose> <when condition="@{ var body = context.Request.Body.As<Newtonsoft.Json.Linq.JObject>(preserveContent: true); if (body == null) { return false; } var rpcMethod = (string)body["method"]; return string.Equals(rpcMethod, "tools/list", System.StringComparison.OrdinalIgnoreCase); }"> <return-response> <set-status code="200" reason="OK" /> <set-header name="Content-Type" exists-action="override"> <value>application/json</value> </set-header> <set-body>@{ var req = context.Request.Body.As<Newtonsoft.Json.Linq.JObject>(preserveContent: true); var id = req != null ? req["id"] : Newtonsoft.Json.Linq.JValue.CreateNull(); var tools = new Newtonsoft.Json.Linq.JArray( new Newtonsoft.Json.Linq.JObject( new Newtonsoft.Json.Linq.JProperty("name", "LeanIX-MCP-Server-get_architecture_decisions"), new Newtonsoft.Json.Linq.JProperty("description", "Read architecture decisions from the connected SAP LeanIX workspace"), new Newtonsoft.Json.Linq.JProperty("inputSchema", new Newtonsoft.Json.Linq.JObject( new Newtonsoft.Json.Linq.JProperty("type", "object"), new Newtonsoft.Json.Linq.JProperty("properties", new Newtonsoft.Json.Linq.JObject()) )) ) ); var response = new Newtonsoft.Json.Linq.JObject( new Newtonsoft.Json.Linq.JProperty("jsonrpc", "2.0"), new Newtonsoft.Json.Linq.JProperty("id", id), new Newtonsoft.Json.Linq.JProperty("result", new Newtonsoft.Json.Linq.JObject( new Newtonsoft.Json.Linq.JProperty("tools", tools) )) ); return response.ToString(Newtonsoft.Json.Formatting.None); }</set-body> </return-response> </when> <when condition="@{ var body = context.Request.Body.As<Newtonsoft.Json.Linq.JObject>(preserveContent: true); if (body == null) { return false; } var rpcMethod = (string)body["method"]; var toolName = (string)body["params"]?["name"]; if (!string.Equals(rpcMethod, "tools/call", System.StringComparison.OrdinalIgnoreCase)) { return false; } string[] exposed = { "LeanIX-MCP-Server-get_architecture_decisions" }; foreach (var t in exposed) { if (string.Equals(t, toolName, System.StringComparison.OrdinalIgnoreCase)) { return false; } } return true; }"> <return-response> <set-status code="200" reason="OK" /> <set-header name="Content-Type" exists-action="override"> <value>application/json</value> </set-header> <set-body>@{ var req = context.Request.Body.As<Newtonsoft.Json.Linq.JObject>(preserveContent: true); var id = req != null ? req["id"] : Newtonsoft.Json.Linq.JValue.CreateNull(); var error = new Newtonsoft.Json.Linq.JObject( new Newtonsoft.Json.Linq.JProperty("jsonrpc", "2.0"), new Newtonsoft.Json.Linq.JProperty("id", id), new Newtonsoft.Json.Linq.JProperty("error", new Newtonsoft.Json.Linq.JObject( new Newtonsoft.Json.Linq.JProperty("code", -32601), new Newtonsoft.Json.Linq.JProperty("message", "Tool disabled by API Management policy") )) ); return error.ToString(Newtonsoft.Json.Formatting.None); }</set-body> </return-response> </when> </choose> </inbound> <backend> <retry condition="@(context.Response.StatusCode >= 500 || context.Response.StatusCode == 429)" count="2" interval="3" first-fast-retry="false"> <forward-request timeout="45" /> </retry> </backend> <outbound /> <on-error /></policies>
The order of operations is the security posture. validate-jwt runs before anything else touches the body, so an unauthenticated caller costs the gateway a 401 and nothing more. The set-header block stamps APIM's request ID onto every call as X-Correlation-Id, the thread you later pull in Application Insights when someone asks why an answer took forty seconds. rate-limit-by-key hashes the Authorization header as the counter key, so limits land per credential rather than per IP, which matters when five hundred people share one Teams channel and one egress address.
The two choose branches do the actual deciding. The tools/list branch never forwards the request: it answers from the pinned catalogue, reading the request's id so the JSON-RPC correlation stays honest, which means nothing the vendor ships tonight can appear in your users' menus until you edit the policy. The tools/call branch is the backstop for dishonest clients, the ones guessing at tool names or caching an old catalogue in a config file: a name off the exposed list receives the spec's method-not-found error, -32601, with a message naming the policy as the refusing party. Both branches read the request body with preserveContent: true, a detail that bites everyone exactly once, since without it the read consumes the body and the rest of the pipeline receives nothing. One practice note from Microsoft's MCP guidance: response-body rewriting can interfere with streaming, which is the argument for this pinned-allowlist style over the deny-list style that rewrites tools/list on the way out. The allowlist never reads a response body at all. And if the policy editor looks thin compared to the API-level editor, that is normal; the MCP Server policy editor omits the elements and works fine without them.
The backend section is the podcast stall getting its treatment. forward-request timeout="45" bounds any single upstream wait, and the retry wrapper gives a transient 429 or 5xx a second chance after a , converting silent multi-minute hangs into bounded, logged events. Forty-five seconds is a number to argue about rather than a law; set it inside the patience of your Teams users and above the p99 of your repository queries.
In Copilot Studio, start the MCP onboarding wizard from the agent’s tools pane, point it at the APIM-exposed URL rather than the vendor’s, and pick Entra ID (OAuth 2.0) as the authentication model, with API key as the documented alternative. Point the knowledge sources at the governance documents, the agreements, and the definitions, and leave the channel set to the default: Teams ships as a distribution channel from Copilot Studio with no extra plumbing. The manifest below is the exportable shape of such an agent under the declarative agent schema, version 1.8 as of this writing, with the instruction block doing more work than any single setting:
{ "version": "v1.8", "id": "leanix-assistant", "name": "LeanIX Assistant", "description": "Answers portfolio, technology lifecycle, and architecture governance questions from SAP LeanIX and internal EA practice sources.", "instructions": "You answer enterprise architecture questions using SAP LeanIX data retrieved through your MCP tools and the internal governance documents in your knowledge sources. Push every filter into tool arguments instead of fetching full catalogues. Name the fact sheets your answers come from. If the tools return no match, say so plainly and point the user to the EA practice. You have no write access; route data-change requests to the governed process. Prefer internal definitions over generic industry vocabulary. Summarize large result sets by risk and offer the full list on request.", "conversation_starters": [ { "text": "Which technologies are close to end of life?" }, { "text": "List all APIs in the portfolio." }, { "text": "Which applications carry obsolescence risk?" } ]}
Read the instruction block as three safety mechanisms wearing a prompt’s clothes. The filter-push sentence is the latency and cost control. The no-match sentence is the anti-hallucination control, since an agent instructed to say “nothing found” has less incentive to decorate an empty result. The no-write sentence is the last line of defense behind the gateway allowlist, and the two controls together are why this configuration survives a vendor shipping write tools overnight: the tool is not in the pinned catalogue, and even a misconfiguration on one side still leaves the other. The model picker is a dropdown, covered earlier; the regional availability tags deserve the same attention as the model names themselves.
The loop starts with API Center watching APIM. Give the API Center instance a system-assigned identity, teach it to read from the APIM instance, then pull the inventory across:
az apic service update \ --resource-group "rg-ea-governance" \ --service-name "ea-api-center" \ --identity '{ "type": "SystemAssigned" }'APIC_PRINCIPAL_ID=$(az apic service show \ --resource-group "rg-ea-governance" \ --service-name "ea-api-center" \ --query "identity.principalId" --output tsv)APIM_RESOURCE_ID=$(az apim show \ --resource-group "rg-ea-governance" \ --name "apim-ea-prod" \ --query "id" --output tsv)az role assignment create \ --role "API Management Service Reader Role" \ --assignee-object-id "$APIC_PRINCIPAL_ID" \ --assignee-principal-type ServicePrincipal \ --scope "$APIM_RESOURCE_ID"az apic service import-from-apim \ --resource-group "rg-ea-governance" \ --service-name "ea-api-center" \ --source-resource-ids "$APIM_RESOURCE_ID/apis/*"
The import-from-apim call is the sentence that makes the transcript's claim real: what lands in APIM shows up in the Center's inventory without a registration form. For the MCP server as a first-class asset, the portal walk is short: Inventory, then Assets, then Register an asset, then MCP server, with the runtime URL pointed at the APIM-exposed endpoint and an environment attached. Your agents and developer tooling can then query the registry at an endpoint shaped like https://.data..azure-apicenter.ms/workspaces/default/v0.1/servers, and the built-in test console in the API Center portal lets a human try a tool before an agent ever does.
The last mile into LeanIX rides the repository’s own import surfaces. SAP’s GraphQL API exposes an updateFactSheet mutation with patch operations for exactly this class of synchronization, so whether the bridge runs on a schedule or off an event is your call. Run it. The direction of the feed is what keeps the catalog from aging, and the podcast transcript is the reference implementation of the mindset: the inventory arrives, the fact sheets appear, and the architect's week goes to judgment calls instead of data entry.
Two meters matter and they answer to different people. The Copilot Credit meter answers to the platform team; the Azure meter for APIM answers to whoever owns the subscription. Put the hard ceiling where the traffic happens, which is the gateway: the rate-limit-by-key block from the policy handles bursts, and swapping it for a monthly quota-by-key adds a volume ceiling when the agent graduates from pilot to company-wide. Then put the alarm where the money lands, the same week the agent ships to a broad channel, months before the invoice can arrive:
az consumption budget create-with-rg \ --amount 500 \ --budget-name "ea-assistant-monthly" \ --resource-group "rg-ea-governance" \ --category Cost \ --time-grain Monthly \ --time-period '{"start-date":"2026-11-01","end-date":"2027-10-31"}' \ --notifications "{\"at-eighty\":{\"enabled\":\"true\",\"operator\":\"GreaterThanOrEqualTo\",\"contact-emails\":[\"ea-ops@yourcompany.example\"],\"threshold\":80.0},\"at-hundred\":{\"enabled\":\"true\",\"operator\":\"GreaterThanOrEqualTo\",\"contact-emails\":[\"ea-ops@yourcompany.example\"],\"threshold\":100.0}}"
The command group carries a preview label, and the amount and dates are placeholders for your ceiling. The sequencing holds the lesson: create the alert when the agent ships to a broad channel, and let the 80 percent notification exist so the 100 percent one never fires.
Validation starts where every hop starts, with name resolution and reachability, and the expected outputs matter as much as the commands:
dig +short apim-ea-prod.azure-api.net
A healthy answer is a CNAME into the regional cloudapp zone followed by an A record. Yours will differ from the illustration below. What matters is the two-part shape:
apim-ea-prod.northeurope.cloudapp.azure.com.52.142.17.203
The Windows-side equivalent, for the machines your EA practice actually runs on:
Resolve-DnsName -Name "apim-ea-prod.azure-api.net" -Type CNAMETest-NetConnection -ComputerName "apim-ea-prod.azure-api.net" -Port 443
The output you want is TcpTestSucceeded : True. There is no UDP surface to test in this stack, since every hop rides TCP 443, which simplifies the firewall conversation and the test matrix in equal measure. A TCP failure here ends the investigation before it starts, so fix reachability before blaming the agent, the vendor, or the model.
The identity gate gets proven with response codes, since the codes are the posture:
curl-sS -o /dev/null -w "no-token: %{http_code}\n" -X POST "$MCP_URL" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{ "jsonrpc": "2.0", "id": 9, "method": "ping" }'
curl-sS -o /dev/null -w "bad-token: %{http_code}\n" -X POST "$MCP_URL" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "Authorization: Bearer this-is-not-a-token" \ -d '{ "jsonrpc": "2.0", "id": 10, "method": "ping" }'
Both lines should read 401. A 200 on either line means the JWT policy is absent or mis-scoped, and the finding is a conversation with your security team. A 403 means the policy fired but a claim mismatched, which points at the aud or appid pinning rather than the policy itself. Learn that difference and you can read the gateway's posture from three characters of output.
The allowlist deserves the same determinism: five shots, timed, with the tool count asserted on every run:
for i in 1 2 3 4 5; do start=$(date +%s%N) code=$(curl -sS -o "run_$i.json" -w "%{http_code}" -X POST "$MCP_URL" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "Authorization: Bearer $ENTRA_TOKEN" \ -d '{ "jsonrpc": "2.0", "id": 3, "method": "tools/list", "params": {} }') end=$(date +%s%N) ms=$(( (end - start) / 1000000 )) tools=$(jq '.result.tools | length' "run_$i.json" 2>/dev/null || echo "parse-failed") echo "attempt $i: http=$code tools=$tools latency=${ms}ms"done
The output that ends the argument looks like this:
attempt 1: http=200 tools=1 latency=412msattempt 2: http=200 tools=1 latency=388msattempt 3: http=200 tools=1 latency=401msattempt 4: http=200 tools=1 latency=397msattempt 5: http=200 tools=1 latency=380ms
The same tool count on all five attempts is the pinned allowlist behaving. The latency spread is your transport budget with the noise included. A parse failure means the body came back as an SSE stream rather than JSON, which some clients need to consume differently, and knowing which mode your chain answers in is worth the five-shot loop by itself.
The backstop branch gets its own proof, using a tool name that is deliberately off-list:
curl-sS -X POST "$MCP_URL" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "Authorization: Bearer $ENTRA_TOKEN" \ -d '{ "jsonrpc": "2.0", "id": 5, "method": "tools/call", "params": { "name": "a-write-tool-you-never-exposed", "arguments": {} } }'
The answer names the policy, not the tool, since the policy is the refusing party:
{ "jsonrpc": "2.0", "id": 5, "error": { "code": -32601, "message": "Tool disabled by API Management policy" }}
The limiter gets the final proof, since a rate limit you have never watched trip is a rate limit you configured on faith:
hit429=0for i in $(seq 1 70); do code=$(curl -sS -o /dev/null -w "%{http_code}" -X POST "$MCP_URL" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -H "Authorization: Bearer $ENTRA_TOKEN" \ -d '{ "jsonrpc": "2.0", "id": 4, "method": "ping" }') if [ "$code" = "429" ]; then hit429=$((hit429+1)); fidoneecho "rejected with 429: $hit429"
Against the 60-calls-per-minute policy from the gateway section, seventy rapid pings should print rejected with 429: 10 and the neighboring ten attempts in the run. Those 429s are the sound of the budget surviving a runaway agent loop, and the Teams-side smoke test comes after: ask the end-of-life question in the channel, confirm the answer names real fact sheets with real lifecycle dates, and pull the same request by its correlation ID in Application Insights to see the per-hop timings that the podcast's dead air was missing.
This stack is done when you can paste the evidence into an audit. The five-shot loop returns http=200 five times with the same tool count on every attempt and latency inside your experience budget. The unauthenticated POST gets 401, the off-list tool call gets -32601 with the policy named as the refusing party, and the burst loop collects its 429s right at the limit you set. The end-of-life question in Teams comes back naming real fact sheets with real lifecycle dates, sourced through a chain whose per-hop timings you can pull by correlation ID. API Center shows the MCP server as a registered asset with its runtime URL and environment, the budget alert at 80 percent is armed and quiet, and the pinned catalogue still contains exactly the tools you decided to expose.
When the vendor ships a new tool class tomorrow, your users will not see it until you decide they should. That is the observable difference between an agent project and an agent platform, and it is the state worth shipping.
More actionable advice over at https://mohammed-brueckner.com/publications — and discover the playbook for surviving the compression in the agentic era with my book at https://platformeconomies.com — out now and ready to buy in both Kindle and hardcover. Direct Amazon links:
LeanIX & Enterprise Architecture in action using integrated data flows. was originally published in Dev Genius on Medium, where people are continuing the conversation by highlighting and responding to this story.