ai
1382 TopicsNew Microsoft Certified: Multi-Agent AI Solutions Expert Certification
The future of AI isn’t a single model, it’s intelligent systems of agents working together. Are you ready to build them? As organizations move from standalone AI features to complex, multi-agent systems, the skills required are evolving fast. Organizations are no longer just experimenting, they’re deploying production-scale agent ecosystems that must be orchestrated, governed, and optimized. Introducing the Microsoft Certified: Multi-Agent AI Solutions Expert Certification, that validates your ability to design, build, and operate scalable, production-ready multi-agent AI solutions. To earn it, you’ll need to pass Exam AI-500: Designing and Implementing Multi-Agent AI Solutions (currently in beta). Is this the right Certification for you? This Certification focuses on the practical skills needed to architect and develop multi-agent AI solutions by using Microsoft Foundry and Azure. It validates the ability to: Design logical architecture for multi-agent solutions. Build and integrate tool ecosystems. Implement multi-agent orchestration. Evaluate, optimize, and monitor multi-agent solutions. Secure, govern, and deploy multi-agent solutions. Candidates for this Certification are expert-level practitioners with subject matter expertise in designing, building, and optimizing scalable, production-ready, multi-agent AI solutions and workflows. They lead the end-to-end lifecycle of AI solution development from architecture and design through deployment and optimization and are prepared to progress from building individual components to owning complete AI systems architecture and delivery. These professionals collaborate closely with developers, machine learning engineers, platform engineers, data scientists, and business stakeholders to translate complex business and technical requirements into production-ready, multi-agent solutions They should have experience developing AI and machine learning based solutions, deploying agentic systems in production environments, and orchestrating agent logic by using Microsoft Foundry. They should also be proficient in Python. Additionally, they need experience developing solutions that include Azure compute, network, storage, and data services. They should be familiar with open-source frameworks and standards, including Microsoft Agent Framework, Model Context Protocol (MCP), retrieval-augmented generation (RAG), and LangGraph. Certification requirement: To earn the Microsoft Certified: Multi-Agent AI Solutions Expert (AI-500) certification, candidates must also earn the Microsoft Certified: Azure AI Apps and Agents Developer Associate (Exam AI -103) certification. AI-103 provides the foundational Azure AI Foundry and agent development skills that AI-500 extends through advanced multi-agent solution design, orchestration, governance, and optimization. Ready to prove your skills? Take advantage of the discounted beta exam offer. The first 300 people who take Exam AI-500 (beta) on or before August 5, 2026, can get 80% off. To receive the discount, when you register for the exam and are prompted for payment, use code AI500Wabash. This is not a private access code. The seats are offered on a first-come, first-served basis. As noted, you must take the exam on or before August 5, 2026. Please note that this discount is not available in Turkey, Pakistan, India, or China. How to prepare Get ready to take Exam AI-500 (beta): Review the Exam AI-500 (beta) page for training resources, exam registration, and other details. The Exam AI-500 study guide explores key topics covered in the exam. Connect with Microsoft Training Services Partners in your area for in-person offerings. Need other preparation ideas? Check out Just How Does One Prepare for Beta Exams? Ready to get started? You can take Certification exams online, from your home or office. Get the details in Online proctored exams: What to expect and how to prepare. Remember, only the first 300 candidates can get 80% off Exam AI-500 (beta) with code AI500Wabash on or before 8/5/2026. Beta exam rescoring begins when the exam goes live, with final results released approximately 10 days later. For more details, read Creating high-quality exams: The path from beta to live. Stay tuned for general availability of this Certification in October 2026. Additional information For more Certification updates, read our recent blog post, Microsoft Credentials roundup: June 2026. Follow our credentials news on The Skills Hub Blog as we roll out additional new Certifications in July, August, and September 2026. Join our Microsoft Worldwide Learning SME Group for Credentials on LinkedIn for beta exam alerts and opportunities to help shape future Microsoft learning and assessments. Explore Microsoft Credentials on AI Skills Navigator.6.6KViews5likes31CommentsHow partners can lead Frontier Transformation in FY27
As organizations move from AI experimentation to business-wide transformation, Microsoft partners are uniquely positioned to help customers innovate, operate, and grow. In the FY27 MCAPS Start for Partners keynote, Nicole Dezen, Chief Partner Officer and CVP, Global Channel Partner Sales, shares how Microsoft is investing in partner capability, go-to-market acceleration, co-sell engagement, and Microsoft Marketplace opportunities to help partners deliver greater customer value. From new AI-focused skilling and specializations to expanded incentives and Marketplace investments, the FY27 updates provide practical resources to help partners build differentiated offerings, drive adoption, and scale growth in the agentic AI era. Read the full blog and explore the FY27 partner priorities: ➡️ MCAPS Start for Partners FY27 blog Help amplify this important announcement across your networks: Nicole Dezen on LinkedIn Microsoft AI Cloud Partner Program on LinkedIn Microsoft Tech Community post Microsoft Partner on X Microsoft Partner on Facebook15Views0likes0Comments📢 Announcing Built-In Knowledge for Azure Logic Apps
Now in Public Preview Turn your documents into a ready-to-use knowledge base without custom RAG pipelines. Today at Microsoft Build 2026, we are announcing the Public Preview of built-in Knowledge for Azure Logic Apps. It is a managed knowledge layer that transforms your documents into a ready-to-use knowledge base, removing the need to build custom Retrieval-Augmented Generation (RAG) pipeline, operate a vector store, or maintain retrieval logic. The result is grounded, accurate answers for the agents and workflows you are building today. Most organizations hold a significant amount of institutional knowledge such as HR policies, product manuals, support runbooks, contracts, and specifications distributed across documents, spreadsheets, and internal systems. The challenge has rarely been the availability of content. It has been making that content reliably and accurately retrievable by AI agents and workflows. Until now, addressing this challenge required building a RAG pipeline in-house. As any team that has implemented one can attest, a production-grade RAG pipeline involves substantial engineering effort and ongoing operational overhead. The complexity of building RAG in-house A production-grade RAG pipeline is not a single component. It is a set of interdependent systems that must be designed, integrated, and maintained: Ingestion: parsing multiple file formats, chunking content appropriately, summarizing, and generating embeddings. Storage: provisioning a vector database, defining indexing policies, and tuning for cost and performance. Retrieval: rewriting queries, vectorizing them, executing semantic search, and returning the most relevant chunks to the model. Operations: monitoring upload status, handling failures, managing credentials, and maintaining security. Each component represents a meaningful engineering investment. Together, they constitute a platform — one that diverts engineering capacity away from the business problems teams set out to solve. Introducing built-in Knowledge capability Built-in Knowledge in Logic Apps is a managed knowledge layer built into Azure Logic Apps that turns your documents into a ready-to-use knowledge base, without requiring you to build or operate a RAG pipeline. You provide the documents, and the platform manages the remainder of the process, both ingestion and retrieval, end to end. Built directly into Logic Apps, KBaaS provides an abstraction over the underlying vector store and AI models, enabling your workflows to consume structured, semantically searchable knowledge through a single connection. A knowledge base is a logical container that organizes related sources for a given domain. For example, an "HR Policies" knowledge base might hold all relevant HR documents. You create the knowledge base, upload your files, and attach it as a tool that your agent can call. How it works Managed Knowledge experience is built around two managed pipelines. Ingestion pipeline. When you upload a knowledge source, the service automatically parses, chunks, summarizes, and vectorizes the content, then stores the results, with no manual preprocessing required. The current preview supports a broad range of formats out of the box: DOC, DOCX, HTML, MD, PDF, PPT, PPTX, TXT, XLS, and XLSX. Each upload provides a progress status and a clear Completed or Failed result. Retrieval pipeline. When your agent queries the knowledge base, the service rewrites the query where beneficial, generates a vector representation, executes a semantic search, and returns the most relevant chunks to the language model for response generation. Query planning, vector search, and ranking are all handled by the service. The outcome is that your agents receive accurate, context-rich answers grounded in your own content, without requiring you to author retrieval logic. Built for agentic workflows Knowledge is available in Azure Logic Apps Standard, where it integrates directly with agentic workflows. Once a knowledge base has been created, it appears as a capability that can be attached to an agent loop. From there, the agent automatically queries the knowledge base to retrieve semantically relevant information from your uploaded documents at the point it is needed, as part of completing a task. Getting started involves three steps: Create the knowledge base connection - associate your vector store and your completions and embeddings models. Add knowledge sources - upload files into a knowledge base, optionally organized into groups. Add the knowledge base as a context - select it from the agent node so your agent can begin retrieving. The platform provisions and manages the required databases, containers, and indexing policies on your behalf, removing the burden of operating the underlying storage and search infrastructure. Two SKUs to consume it - Standard or Automation This feature is available across Logic Apps SKUs, with some differences in how you setup and manage them. Logic Apps Standard — bring your own resources. On Standard SKU, the model operates on your own Cosmos DB vector store and AI models, KBaaS integrates with them directly. You connect your existing resources, and the platform manages the complete ingestion and retrieval pipeline on top of them. This approach retains full control over your data and models while removing the need to build and maintain the RAG pipeline. Logic Apps Automation SKU — bring only your documents. On the Automation SKU, this feature operates on a hosted-on-behalf-of model, in which the platform provisions and manages both the underlying vector store and the AI models. There is no Cosmos DB to provision, no embeddings or completions model to deploy, and no connections to configure. You upload your documents and attach the knowledge base to your agent, and the entire knowledge layer, including the supporting infrastructure is fully managed for you. This delivers the same managed knowledge experience with the maximum degree of abstraction, providing the most direct path from source documents to a working, agent-ready knowledge base. Secure by design KBaaS supports authentication through Microsoft Entra ID using either a managed identity or an API key. We recommend managed identity wherever possible. It is the most secure option and eliminates the need to manually provision and rotate credentials, secrets, or access keys. Available today in Public Preview This initial release focuses on the most common starting point: uploading unstructured documents. Additional capabilities are planned, including support for more knowledge sources, richer ingestion (such as image parsing, semantic chunking, and multimodal embeddings), configurable retrieval settings, access checks during retrieval, and more. Built-in Knowledge in Logic Apps is available now in Public Preview. Provide your documents and give your agents a knowledge base that is ready to use, without building or operating a RAG pipeline. Read the MS Learn docs to get started Check out the demo belowUnlock AI agents without sacrificing security
AI agents are reaching into mailboxes, files, line-of-business apps, and the open web on behalf of your users—and the business wants more of them, faster. To scale agents safely, your security teams need to be able to verify each agent, govern what it can access, and enforce clear boundaries across every interaction. Learn how Microsoft Entra helps you discover shadow AI agents, govern agent permissions, keep BYOD and endpoint-based agents in scope, and apply Conditional Access to AI prompts and responses. Then see how Microsoft Purview provides visibility into agent activity, strengthens runtime data protection, helps detect agentic risk, and supports auditability across local agents developed on GitHub Copilot CLI, Claude Code, OpenAI Codex, and OpenClaw. Walk away with practical ways to unlock AI agents while keeping access and data protection aligned with your enterprise security needs. How do I participate? Select Add to Calendar to save the date, then click the Attend button to save your spot, receive event reminders, and participate in the Q&A. Not able to attend live? This session will be recorded and available on demand shortly after airing. Don't see Attend or Add to Calendar? Sign in to the Tech Community to join the conversation. If you are unable to watch the session here due to your organizational policies, you can also tune in on LinkedIn. This session is part of Securing data and access in the era of AI with Microsoft Entra and Microsoft Purview. View the full agenda for more insights to help you move from experimenting with AI to deploying it at scale, securing sensitive data, access, and AI usage.731Views0likes2CommentsSecuring data and access in the era of AI with Microsoft Entra and Microsoft Purview
As organizations move from experimenting with AI to deploying it at scale, securing sensitive data, access, and AI usage has become mission critical. In this series, Microsoft experts will show how Microsoft Entra and Microsoft Purview help you: Protect sensitive data across networks, apps, and AI interactions Govern access for users, applications, and AI agents Reduce risk while enabling innovation at scale Whether you're shaping your security strategy or implementing controls, you’ll walk away with the guidance you need to secure data and access to AI as one unified strategy. DATE TIME (PDT) TOPIC July 21 9:00 AM Secure the age of AI: Redefining trust, data and access July 22 9:00 AM Data and identity controls for the browser and network July 23 9:00 AM Unlock AI agents without sacrificing security How do I participate? Select the sessions you are interested in, then select Add to Calendar to save the date and/or the Attend button to save your spot, receive event reminders, and participate in the Q&A. Not able to attend live? This session will be recorded and available on demand shortly after airing. Don't see Attend or Add to Calendar? Sign in to the Tech Community to join the conversation. If your organizational policies prevent you from signing in to Tech Community with your organization account, you can use a personal account or tune in and participate on LinkedIn. (Links to the LinkedIn stream can be found on the session pages.)2.7KViews2likes0CommentsUnderstanding Microsoft 365 Copilot Risk Surface and Mitigations
Microsoft 365 Copilot acts on the access users already have. It does not grant new permissions — it is a force multiplier that makes existing access faster and easier to discover, aggregate, and act on across Microsoft 365. That speed and accessibility can also amplify existing security and governance gaps. In environments with overshared content, excessive permissions, or inconsistent governance, users may gain faster access to sensitive information than organizations realize. It’s a shift that carries important security implications organizations should understand before scaling deployment. Customers with Microsoft 365 E5 licensing may already have access to many of the tools that can help identify and reduce Copilot-related risk, but licensing alone does not reduce exposure. What’s often missing during early Copilot deployments is a clear understanding of the risk surface itself: what data is exposed, who can access it, and through which vectors.Governance Is the New Bottleneck: What Agent 365 Means for Admins ?
Hi all , following up on my last post about token limits, I wanted to write about something that's been on my mind a lot lately: the sheer number of agents quietly showing up across our tenant. Not just the ones we built deliberately in Copilot Studio, but ones people spun up in Power Platform, ones connected through Teams, and a few I genuinely couldn't trace back to an owner when I went looking. That's the moment this topic stopped being theoretical for me. For the last couple of years, the Microsoft AI conversation was mostly about capability -what can Copilot do, which model is better, how do I write a good prompt. That conversation hasn't gone away, but a second one has caught up to it fast: who's actually watching all of this. Microsoft's own 2026 Work Trend Index makes the shift explicit this isn't about saving a few minutes in Outlook anymore, it's about organizations redesigning how work gets divided between people and agents. And the moment agents start acting semi-independently across your tenant, "how many do we have, and what are they allowed to touch" becomes a real operational question, not a hypothetical one. That's exactly the gap Microsoft Agent 365 is built to close. It went generally available on May 1, 2026, alongside Microsoft 365 E7, and I think it's worth understanding properly especially if you're the one who ends up fielding the "wait, there's an agent doing what?" conversation. What Agent 365 Actually Is (and Isn't) The first thing worth clearing up: Agent 365 doesn't build agents. That's still Copilot Studio's job, or Foundry, or whatever platform your team is using. Agent 365 is the layer that sits on top of all of that it's a control plane, not a construction tool. Think of it less like "another AI product" and more like the admin and security backbone that was honestly missing from the picture until now. Microsoft frames it around three pillars: observe, govern, and secure. In practice, that means every agent in your tenant whether it was built in Copilot Studio, imported from AWS or Google Cloud, or even running locally on someone's Windows machine gets registered, gets its own identity through Microsoft Entra, and becomes something you can actually see and act on instead of just hoping it's behaving. That identity piece is the part I think gets underrated. Each agent gets its own Entra Agent ID, the same way a human user would. That's a meaningful shift it means conditional access policies, auditing, and compliance tooling that already exist for people can now extend to agents instead of treating them as some invisible background process. Why This Matters Right Now Here's the honest version of what's been happening across a lot of organizations, including bits of what I've seen firsthand: agent creation has gotten easy. Almost too easy. Between Copilot Studio, Power Platform, and now agentic mode built directly into Word, Excel, and PowerPoint, it doesn't take much for someone in a business unit to spin up something that's technically an AI agent with access to real data without IT or security ever being looped in. Microsoft has been fairly direct about this risk themselves, which I appreciated seeing in writing rather than just implied: the speed of agent development shows real value, but without guardrails, that pace turns into blind spots, lower ROI, and genuine security exposure. That's not vendor fear-mongering, that's just what happens when adoption outpaces oversight in any technology, and agents are no exception. What makes this particular moment different from past "shadow IT" waves is that agents don't just store or move data they can act on it. An agent with the wrong scope isn't just a compliance footnote, it's something that could send an email, modify a file, or trigger a workflow on its own. That's a different risk category than an unsanctioned spreadsheet sitting in someone's OneDrive. What You Actually Get With Agent 365 A few capabilities stood out to me as genuinely useful rather than just checkbox governance: The overview dashboard gives you a real-time view of your entire agent fleet total registered agents, active users, connected platforms, runtime hours, and risk signals, all in one place. Before this, getting even a rough headcount of "how many agents exist in our tenant" was a manual, frustrating exercise. Registry sync extends that visibility beyond Microsoft's own tools. It can pull in agents built on AWS Bedrock and Google Cloud, so you're not stuck with three different governance stories depending on where an agent happens to live. For organizations that are realistically never going to be 100% single-vendor, that matters. Lifecycle actions install, publish, block, unblock, delete, reassign ownership are now available directly from the registry. That's a big deal operationally. Before, tracking down who owned a rogue or abandoned agent could turn into an actual investigation. Now it's a few clicks. Local agent controls through Defender and Intune are rolling in too, extending management down to agents running on individual Windows endpoints, not just cloud-hosted ones. Given how much agent activity is starting to happen at the device level, this closes a gap that would've otherwise been a blind spot. Conditional access for agents, through Entra, means you can apply the same kind of dynamic, granular access policies to agents that you'd apply to a human user which is really the whole philosophical shift Agent 365 represents: agents aren't a separate, ungoverned category anymore, they're first-class identities in your tenant. What This Means for Admins, Practically If you're managing a tenant with any real Copilot or agent activity, here's where I'd actually start: Don't wait for "full autonomy" to engage. It's tempting to think governance can wait until agents are doing something more dramatic than they are today. Microsoft's own guidance pushes against that the advice is to establish visibility and guardrails early, while adoption is still accelerating, not after. Get a real inventory first. Before writing new policies, it's worth just knowing what already exists. I'd genuinely bet most tenants have more agents running than the admin team could name off the top of their head. The overview dashboard is the fastest way to close that gap. Loop in more than just IT. Agent 365 licensing and controls touch the M365 admin center, Entra, Defender, Purview, and Intune which means this isn't a single-team rollout. Security, compliance, and helpdesk all need to understand what's changing, especially the distinction between Frontier (preview, no production SLA) and GA (production-ready, supported). Understand the licensing model before you scope a rollout. Agent 365 is licensed per human user — the person who manages, sponsors, or is served by an agent rather than per agent. It's available standalone at $15 per user per month, or bundled into Microsoft 365 E7. Worth mapping that against your actual agent-using population rather than assuming it's a flat cost per bot. Treat this as incremental, not a one-time setup. Microsoft has said plainly that Agent 365's capabilities will keep evolving as adoption patterns and governance models mature. This isn't a project you finish and close out it's closer to how you'd think about identity and access management generally: ongoing, not a one-time rollout. The Bigger Shift Underneath All This What I find genuinely interesting about Agent 365 is what it signals about where Microsoft thinks this is all heading. They're not just selling a better Copilot anymore they're positioning Microsoft 365 as the place where AI-driven work gets governed, regardless of which vendor's model or platform an agent actually runs on. Whether that's the right long-term answer for every organization is a fair thing to debate. But the underlying problem it's solving that agents were multiplying faster than anyone's ability to see or control them is real, and I don't think it's specific to Microsoft shops. If your organization is building agents in Copilot Studio, experimenting with Foundry, or even just watching Copilot's agentic mode quietly take on more autonomous work in Word and Excel, this is worth getting ahead of. The teams that treat agent governance as a foundational layer now are going to have a much easier time scaling adoption later than the ones who bolt it on after something goes wrong. Curious whether others are already rolling out Agent 365, or still in the "let's figure out how many agents we actually have" phase I suspect a lot of us are somewhere in between. Would love to hear how your organization is approaching this. Cheers, and happy reading. Surya Vennapusa-MCT103Views0likes0CommentsSecure the age of AI: Redefining trust, data and access
There is no question that AI is transforming the enterprise: changing how data moves, how decisions are made, and how risk takes shape. As agents access, interpret, and act on sensitive data, unmanaged AI use expands and traditional boundaries blur. Kicking off our series on Securing Data and Access in the Era of AI, Microsoft Entra VP of Product Sinead O’Donovan and Microsoft Purview GM of Product Maithili Dandige explain why legacy security models fall short in the age of AI—and why you need a strategy that brings together identity, access, and data protection. Want to adopt and enable AI innovation with greater control and confidence? Join us to learn how leading organizations are securing access, protecting data, and establishing trust for the next generation of AI-powered work. How do I participate? Select Add to Calendar to save the date, then click the Attend button to save your spot, receive event reminders, and participate in the Q&A. Not able to attend live? This session will be recorded and available on demand shortly after airing. Don't see Attend or Add to Calendar? Sign in to the Tech Community to join the conversation. This session is part of Securing data and access in the era of AI with Microsoft Entra and Microsoft Purview. View the full agenda for more insights to help you move from experimenting with AI to deploying it at scale, securing sensitive data, access, and AI usage.1.7KViews1like8CommentsSet Up Plaud Note Pro with Microsoft Foundry
Prerequisites Riffado, up and running: follow the setup guide in the official Riffado repository to get it going with Docker Compose. A Microsoft Foundry (formerly Azure AI Foundry) resource, with the models you want deployed; in my case, whisper for transcription and o3-mini for summaries. A Plaud device, or any audio recordings you can import into Riffado. Once Riffado is up, head to the Settings page > Providers > Add Provider, and select Custom. This is where the Azure details will go. Why "OpenAI-compatible" isn’t one thing on Microsoft Foundry Azure AI Foundry exposes two different API surfaces on the same resource, and which one serves your model depends on the model: Surface Path shape Serves OpenAI-compatible? v1 route /openai/v1/… gpt-4o-transcribe, gpt-4o-mini-transcribe, chat models, embeddings Yes: Bearer auth, model in the body, no api-version needed Classic route /openai/deployments/{name}/… Whisper (and other legacy audio) No: deployment name lives in the URL, and ?api-version= is mandatory A generic OpenAI client (Riffado's included) can only speak the first dialect. It has nowhere to put a deployment name in the path and no way to append a query parameter. That single fact drives everything below. Part 1 - Transcription Whisper and the DeploymentNotFound mystery Symptom My very first transcription attempt in Riffado failed with 404 Resource not found. Off to a flying start. Configured provider: base URL https://<resource>.services.ai.azure.com, model whisper. Dead end #1: the missing path The first bug was mine: the base URL had no path. Riffado's OpenAI client appends /audio/transcriptions to whatever you give it, so requests were hitting https://<resource>…/audio/transcriptions, a path that doesn't exist on the resource at all. Fixing the base URL to end in /openai/v1 got us to a more interesting error: POST /openai/v1/audio/transcriptions · model=whisper {"error":{"code":"DeploymentNotFound","message":"The API deployment for this resource does not exist. If you created the deployment within the last 5 minutes, please wait a moment and try again."}} Dead end #2: catalog ≠ deployment Worth checking before anything else: selecting a model in the Foundry catalog is not deploying it. GET /openai/v1/models lists everything you could deploy; only Deployments → Deploy model creates an endpoint that answers. If you get DeploymentNotFound, first confirm a deployment actually exists (the listing below requires only the API key): enumerate real deployments (classic control-plane, key auth) curl -s -H "api-key: $KEY" \ "https://<resource>.openai.azure.com/openai/deployments?api-version=2023-03-15-preview" # → {"data":[{"id":"whisper","model":"whisper","status":"succeeded",…}]} The actual cause Here is the part that nearly drove me mad: the deployment existed and was succeeded, yet the v1 route still said DeploymentNotFound. Because Whisper deployments are not served on the v1 route at all. They only answer on the classic path. Verified side by side with the same tiny WAV file: Request Result POST /openai/v1/audio/transcriptions · model=whisper · Bearer 404 DeploymentNotFound POST /openai/deployments/whisper/audio/transcriptions?api-version=2024-06-01 · Bearer 200 {"text":"you"} Same classic path, without ?api-version= 404 Resource not found Three constraints, then: Whisper needs the classic path; the classic path needs api-version; Riffado can send neither. One piece of good news hiding in the table: the classic route accepts Authorization: Bearer, not just Azure's api-key header, so the shim doesn't have to touch auth at all. The fix: a Caddy shim Drop a stock caddy:2-alpine container into the Compose network. Riffado points at it as if it were OpenAI; the shim rewrites the path, injects api-version, and proxies to Azure. The Bearer header passes through untouched. azure-shim.Caddyfile { admin off auto_https off } :80 { @transcribe path /v1/audio/transcriptions /audio/transcriptions handle @transcribe { rewrite * /openai/deployments/whisper/audio/transcriptions?api-version=2024-06-01 reverse_proxy https://<resource>.services.ai.azure.com { header_up Host <resource>.services.ai.azure.com } } handle { respond "azure-shim ok" 200 } } docker-compose.yml (added service) azure-shim: image: caddy:2-alpine restart: unless-stopped volumes: - ./azure-shim.Caddyfile:/etc/caddy/Caddyfile:ro Riffado's provider settings become: Field Value Base URL http://azure-shim/v1 Model whisper (must equal the deployment name) API key the Azure resource key (forwarded as Bearer) Verified From inside the Riffado container: POST http://azure-shim/v1/audio/transcriptions → 200 {"text":"…"}. Transcription works end-to-end in the UI. Part 2 · Summaries & titles o3-mini and the empty answer Symptom The summary button showed "An unexpected error occurred." The container logs were more honest: riffado-app logs Error generating title: TypeError: undefined is not an object (evaluating 'C.choices[0]') Riffado calls chat/completions and reads choices[0] without checking whether the response was an error. So anything the API refuses becomes "an unexpected error." What was it refusing? Cause 1: reasoning models reject the classic knobs o3-mini belongs to Azure/OpenAI's o-series reasoning models, which hard-reject parameters every classic chat client sends. Riffado sends temperature: 0.7 and max_tokens: 50 for titles (0.5 / 2000 for summaries), and o3-mini answers: POST /openai/v1/chat/completions · model=o3-mini HTTP 400 {"error":{"message":"Unsupported parameter: 'max_tokens' is not supported with this model. Use 'max_completion_tokens' instead.", …}} # and with max_tokens fixed: HTTP 400 {"error":{"message":"Unsupported parameter: 'temperature' is not supported with this model.", …}} Cause 2: reasoning tokens starve the output Stripping the bad params gets you to 200, and then comes a subtler failure, my personal favourite of this whole saga. Reasoning models spend completion tokens on internal "thinking" before emitting a single visible character. Riffado's 50-token title budget is consumed entirely by reasoning, and the reply comes back syntactically valid and empty: max_completion_tokens reasoning_effort finish_reason content 50 not set length "" (all 50 spent reasoning) 2000 not set stop "Q3 Budget Planning Strategy Meeting" 2000 low stop same, less reasoning overhead The fix: a Node shim that rewrites the request body Caddy can rewrite paths but not JSON bodies, so this shim is ~60 lines of dependency-free Node on node:20-alpine. Per request it: converts max_tokens → max_completion_tokens, strips temperature / top_p / penalties, floors the token budget at 4000, sets reasoning_effort: "low", maps /v1/* → /openai/v1/*, and forwards to the Azure resource. o3-shim.js const http = require('http'); const https = require('https'); const UPSTREAM_HOST = '<resource>.services.ai.azure.com'; // Params o-series reasoning models reject on chat/completions. const STRIP = ['temperature','top_p','presence_penalty', 'frequency_penalty','logprobs','top_logprobs']; const server = http.createServer((req, res) => { const chunks = []; req.on('data', c => chunks.push(c)); req.on('end', () => { let body = Buffer.concat(chunks); // Riffado's base_url is http://o3-shim/v1 → map to Azure's /openai/v1 let path = req.url; if (path.startsWith('/v1/')) path = '/openai' + path; const ct = (req.headers['content-type'] || '').toLowerCase(); if (ct.includes('application/json') && body.length) { try { const j = JSON.parse(body.toString('utf8')); if (j && typeof j === 'object' && !Array.isArray(j)) { if ('max_tokens' in j) { if (!('max_completion_tokens' in j)) j.max_completion_tokens = j.max_tokens; delete j.max_tokens; } // Reasoning spends tokens before any visible output; small // budgets (Riffado sends 50 for titles) return empty strings. if (Array.isArray(j.messages)) { j.max_completion_tokens = Math.max(Number(j.max_completion_tokens) || 0, 4000); if (!('reasoning_effort' in j)) j.reasoning_effort = 'low'; } for (const k of STRIP) delete j[k]; body = Buffer.from(JSON.stringify(j)); } } catch (_) { /* not JSON - forward untouched */ } } const headers = { ...req.headers, host: UPSTREAM_HOST, 'content-length': Buffer.byteLength(body) }; const up = https.request( { host: UPSTREAM_HOST, port: 443, method: req.method, path, headers }, upRes => { res.writeHead(upRes.statusCode, upRes.headers); upRes.pipe(res); } ); up.on('error', e => { res.writeHead(502, {'content-type':'application/json'}); res.end(JSON.stringify({error:{message:'o3-shim upstream error: '+e.message}})); }); up.end(body); }); }); server.listen(80, () => console.log('o3-shim listening on :80')); docker-compose.yml (added service) o3-shim: image: node:20-alpine restart: unless-stopped working_dir: /app command: ["node", "/app/o3-shim.js"] volumes: - ./o3-shim.js:/app/o3-shim.js:ro Add a second provider in Riffado (base URL http://o3-shim/v1, model o3-mini, the resource's API key) and set it as the default enhancement provider (summaries/titles), keeping the Whisper one as default for transcription. Riffado's exact title request (temperature: 0.7, max_tokens: 50) through the shim → 200, finish_reason: stop, real title text. A full meeting-transcript summary returns structured key points and action items. The final shape Reading it left to right: Riffado never talks to Azure directly. Transcription requests pass through azure-shim, a stock Caddy container that rewrites each request onto Whisper's classic deployment path and injects the mandatory api-version parameter. Summary and title requests pass through o3-shim, a tiny Node server that rewrites the request body into the shape o3-mini accepts and floors the token budget so the model's internal reasoning cannot starve the actual answer. As far as Riffado is concerned, it is simply talking to two ordinary OpenAI providers. Both shims live on the Compose network only; nothing is exposed publicly. Riffado is unmodified. Verification checklist Each layer, testable in isolation. Run these before blaming the app: smoke tests # 1. Key + resource alive? (v1 models listing, Bearer auth) curl -s -H "Authorization: Bearer $KEY" \ https://<resource>.services.ai.azure.com/openai/v1/models | head -c 200 # 2. Whisper answers on the classic path? curl -s -H "Authorization: Bearer $KEY" -F file=@test.wav \ "https://<resource>.services.ai.azure.com/openai/deployments/whisper/audio/transcriptions?api-version=2024-06-01" # 3. Shim translates correctly? (from inside the compose network) docker exec riffado-app node -e "fetch('http://azure-shim/') .then(r=>r.text()).then(console.log)" # 4. o3-mini via shim, sending the params Riffado sends? # (temperature + max_tokens:50; the shim must absorb both) If you'd rather not run shims Both shims exist because of the specific models chosen. Pick models that live natively on the v1 route and Riffado connects directly, with base URL https://<resource>.services.ai.azure.com/openai/v1 and zero extra containers: Transcription: deploy gpt-4o-mini-transcribe (or gpt-4o-transcribe) instead of Whisper. Summaries: deploy a non-reasoning chat model such as gpt-4o-mini, which happily accepts temperature and max_tokens. The shim approach earns its keep when you're standardized on specific models (Whisper's transcription quality, o3-mini's reasoning), or when you want a control point to add logging, retries, or budget caps later. For reference, this is what the finished setup looks like on Riffado's side. Each shim is registered as a plain Custom provider. Here is the whisper provider pointing at azure-shim, with Use for transcription ticked: And once both are saved, they sit side by side in the providers list, whisper tagged for transcription and o3-mini tagged for enhancement: A quick look at the Foundry portal In the Microsoft Foundry portal, head over to Models > AI Services and you will find a pleasant surprise: fifteen AI service models already deployed and ready to use, covering the Azure Speech family (including Voice Live and Speech to Text), Azure Translator, Azure Language, and Content Understanding: You can of course deploy another model for this, but the pre-deployed ones are a handy cost-saving option. Click on the Azure Speech – Voice Live radio button and you will be shown the Base URL and API Key, which you can then paste into the provider settings on Riffado's Settings page. A quick note on cost: these services are not free. They are billed pay-as-you-go based on usage. Azure Speech transcription is charged per audio hour, and Voice Live pricing is tiered by the model you choose. The free tier does include a monthly allowance, though. Check the Azure Speech pricing page before committing. And if you would rather deploy a dedicated transcription model such as whisper, Foundry gives you the flexibility to do just that. Open the model page in the catalogue, click Deploy, and go with Default settings unless you need custom quotas or guardrails: Let's test the setup On your Plaud device, just tap to start recording. The little LED bars light up to show it is listening: Or skip the device entirely and upload an audio file straight into Riffado using the Upload Audio button. Either way, the recording lands on the Recordings page; hit Transcribe and let the spinner do its thing: As you can see below, whisper, the transcription model we deployed earlier, even managed to transcribe a recording in Malay without a hitch. My 3:32 test clip came back as 186 words of clean Malay, with the language correctly detected and tagged: I have also set o3-mini as the enhancement provider, and it enhanced the transcription with a proper summary, key points, and title as well! The Meeting Notes-style summary came straight out of o3-mini through the shim, with zero manual prompting. Wrapping up What started as a TikTok-fuelled impulse buy nearly killed off by subscription pricing ended up as a fully self-hosted pipeline: Plaud for recording, Riffado as the interface, and Microsoft Foundry serving whisper and o3-mini behind two tiny shims. The total extra infrastructure came to two containers and roughly sixty lines of code, and not a single monthly subscription in sight. If you try this setup and run into a failure mode I have not covered here, do share it in the comments. Half the fun is in the debugging.48Views0likes0CommentsMCP Server Authorization with Azure API Management: From Simple to Advanced
Why put API Management in front of your MCP servers The Model Context Protocol (MCP) has quickly become the standard way for AI agents, such as GitHub Copilot in VS Code, to reach external tools and data. As soon as an MCP server does anything meaningful, the same questions that govern any API resurface: who is allowed to call it, what are they allowed to do, and how do you enforce that consistently across many servers without rewriting each one. Azure API Management (APIM) answers those questions for MCP. It sits between the MCP client and the tool backend and applies the controls you already trust for REST APIs: identity validation, OAuth, rate limiting, IP filtering, and observability. Crucially, APIM speaks the MCP authorization specification, which is built on OAuth 2.1 and Protected Resource Metadata (PRM, RFC 9728). That means APIM can do more than block bad requests. It can actively drive an interactive sign-in from the IDE, so the user logs in with their own identity and the agent acts on their behalf. This article walks through a progression of authorization scenarios, each one building on the last: The simple case: validate a token and block everything else. Triggering an interactive sign-in from VS Code for an MCP server that APIM hosts from your own APIs. Going beyond "is this a tenant user" to "does this user have the right attribute" with Entra app roles. Fronting an existing external MCP server and letting it drive its own OAuth flow (GitHub as the example). Governing which tools of an existing MCP server an agent is actually allowed to invoke. APIM MCP capabilities and the basic authorization options API Management exposes MCP servers in two distinct ways, and the authorization story differs slightly for each. Expose a REST API as an MCP server. APIM takes an API it already manages and projects selected operations as MCP tools. You own the operations, so you choose exactly which ones become tools at configuration time. This is the right mode when the capability you want to expose is an API you control. Expose an existing MCP server (passthrough). APIM fronts a remote MCP-compatible server (LangChain, an Azure Function, GitHub's remote MCP server, your own container) and relays the MCP protocol to it. APIM governs access, but the upstream server still owns its tool catalog. On top of either mode, you have a spectrum of authorization options: Subscription keys for simple, machine-to-machine access where a shared secret in a header is acceptable. Token validation with Microsoft Entra ID, where APIM acts as the protected resource and verifies a bearer token on every call. Interactive OAuth 2.1 sign-in, where APIM advertises Protected Resource Metadata so an MCP client can discover the authorization server, log the user in, and retry with a user token. Authorization passthrough, where an external MCP server presents its own authorization challenge and APIM relays it faithfully so the client authenticates directly against the upstream's identity provider. The rest of the article works through these options in increasing order of capability. The example setup The walkthroughs in the first three scenarios all use the same backend so you can reproduce them without standing up anything of your own: the publicly available Star Wars API at Star Wars API. It is a simple, read-friendly REST API (characters, films, planets, starships, and so on) imported into API Management as a normal API and then projected as an MCP server. The reason this single API is enough to illustrate the whole progression is that, in API Management, one underlying API can back several independent MCP servers, each exposing a different slice of its operations. For example, you can create: A read-only MCP server that exposes only the GET operations, for agents that should be able to query data but never change it. A write-capable MCP server that exposes the POST, PUT, or DELETE operations, for trusted automation that is allowed to mutate state. Same backend API, two MCP servers, two different tool surfaces. Each of these servers is an independent resource in APIM, so each one can carry its own authorization. Both can require an authenticated user (Scenarios 1 and 2), and you can go further by protecting only the sensitive one: gate the write-capable server behind an Entra app role so that, even among authenticated users, only those who carry a specific claim can reach the mutating tools. That app-role mechanism is the subject of Scenario 3, and it composes naturally with the multi-server split described here. Registering the MCP API in Microsoft Entra ID Before any of the policies below can validate a token, you need an application registration in Microsoft Entra ID that represents the MCP API. This registration is what defines the audience and scope that tokens are issued for, and it is the source of the mcp-audience, mcp-scope, and (indirectly) mcp-client-id values that the policies reference. Create it once and reuse it across all the MCP servers in this article. In the Azure portal, open Microsoft Entra ID, then App registrations, then New registration. Name it (for example, star-wars-mcp-api), choose single-tenant, and register. Record the Application (client) ID and the Directory (tenant) ID. Open Expose an API and add an Application ID URI. Accept the default api://<app-id>. This URI is your token audience. Still under Expose an API, add a delegated scope named MCP.Access, set its consent display name and description, set the state to Enabled, and save. Authorize the client that will request the scope. Under Expose an API, select Add a client application and enter the client ID of the MCP client. For VS Code, this is the built-in Microsoft authentication client aebc6443-996d-45c2-90f0-388ff96faa56. Check the MCP.Access scope and save. These steps produce the four constants the validation policy needs: Named value Comes from Example entra-tenant-id The Directory (tenant) ID from step 1 11111111-1111-1111-1111-111111111111 mcp-audience The Application ID URI from step 2 api://22222222-2222-2222-2222-222222222222 mcp-scope The scope name from step 3 MCP.Access mcp-client-id The client ID of the calling app from step 4 aebc6443-996d-45c2-90f0-388ff96faa56 [!NOTE] mcp-client-id is the identity of the application calling the MCP server, not the MCP API itself. For VS Code it is the built-in Microsoft authentication client, and its value lands in the token's appid claim, which is why the validation policy lists it under client-application-ids. If your tenant blocks the first-party VS Code client, register your own public client application and use its client ID instead. [!TIP] For the privileged-access feature in Scenario 3, you will also declare an app role on this same registration. You do not need it yet, but it is convenient to know that all identity configuration for these servers lives on this one app registration. With that backend and structure in mind, the scenarios below build up the authorization model one capability at a time. Scenario 1: The simple case, validate the token and block unauthorized access The most basic protection is to require a valid Entra ID token on every MCP request and reject anything that fails validation. No interactive flow, no roles, just a gate. APIM does this with the validate-azure-ad-token policy. The policy checks the issuing tenant, the audience (your MCP API), the calling client application, and the required scope. Anything that does not satisfy all four is rejected with a 401. <policies> <inbound> <base /> <validate-azure-ad-token tenant-id="{{entra-tenant-id}}" header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid."> <client-application-ids> <application-id>{{mcp-client-id}}</application-id> </client-application-ids> <audiences> <audience>{{mcp-audience}}</audience> </audiences> <required-claims> <claim name="scp" match="any"> <value>{{mcp-scope}}</value> </claim> </required-claims> </validate-azure-ad-token> </inbound> <backend> <base /> </backend> <outbound> <base /> </outbound> <on-error> <base /> </on-error> </policies> The values in double braces are APIM named values: centralized constants, defined once and shared by every MCP server. They map directly to the four values produced by the Entra app registration in the example setup (entra-tenant-id, mcp-audience, mcp-scope, and mcp-client-id). Storing them as named values keeps the policy free of hardcoded identifiers and lets every server reuse the same configuration. This gets you a server that nobody can call without a properly minted token. What it does not do is help a fresh client obtain that token in the first place. That is the next scenario. Scenario 2: Driving an interactive sign-in from VS Code for an APIM-hosted MCP server When you expose one of your own APIs as an MCP server, you usually want a developer to open VS Code, connect to the server, and be prompted to sign in with their Microsoft account. No pre-shared key, no manual token handling. APIM achieves this by behaving as a well-mannered OAuth 2.1 protected resource. Using the Star Wars MCP server from the example setup, each selected operation becomes a tool the agent can call, so an agent can answer "which films featured the character named Leia" by calling the underlying API through APIM. How the sign-in flow works The protocol choreography is what turns a plain 401 into an interactive login: Two ingredients make this work: a 401 challenge that points to a metadata document, and the metadata document itself. The challenge: a 401 that points the client to its metadata Instead of a bare 401, APIM returns a WWW-Authenticate header carrying the URL of the server's Protected Resource Metadata. This is what tells the client "you need a token, and here is where to learn how to get one." Keeping this logic in a shared policy fragment means every MCP server reuses it. Notice the mcpResourceMetadataUrl reference in the fragment below. It is not hardcoded; it is a context variable that each MCP server sets in its own server-level policy before including this fragment (you will see that wiring in the per-server policy later in this scenario). The fragment simply reads whatever value the calling server provided. This indirection is what keeps the fragment pluggable: the same shared challenge-and-validate logic serves every MCP server, while each server supplies its own PRM URL. In most deployments the PRM endpoint is a single, dynamic one (built in the next section) that derives the resource from the request path, so the variable just carries that server's path. But because the URL is configurable per server rather than baked into the fragment, you retain flexibility for the cases that need it. <fragment> <!-- No token: challenge with the per-server PRM URL set by the caller --> <choose> <when condition="@(!context.Request.Headers.ContainsKey("Authorization"))"> <return-response> <set-status code="401" reason="Unauthorized" /> <set-header name="WWW-Authenticate" exists-action="override"> <value>@("Bearer resource_metadata=\"" + (string)context.Variables.GetValueOrDefault("mcpResourceMetadataUrl", "") + "\"")</value> </set-header> </return-response> </when> </choose> <!-- Token present: validate against shared named values --> <validate-azure-ad-token tenant-id="{{entra-tenant-id}}" header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid."> <client-application-ids> <application-id>{{mcp-client-id}}</application-id> </client-application-ids> <audiences> <audience>{{mcp-audience}}</audience> </audiences> <required-claims> <claim name="scp" match="any"> <value>{{mcp-scope}}</value> </claim> </required-claims> </validate-azure-ad-token> </fragment> Creating the /.well-known PRM endpoint in APIM with a policy This is the part that often surprises people: APIM itself serves the metadata document. There is no separate identity service to stand up. You publish one small anonymous API at the service root that answers GET /.well-known/oauth-protected-resource/*, derives the resource value from the requested path, and returns a JSON document pointing at Microsoft Entra ID as the authorization server. Create a blank HTTP API named well-known with an empty API URL suffix so it resolves at the service root, add a GET operation with the template /.well-known/oauth-protected-resource/*, clear the subscription requirement so it is reachable anonymously, and apply this policy: <policies> <inbound> <base /> <!-- Build the resource URL from the requested PRM sub-path --> <set-variable name="resourceUrl" value="@{ var prefix = "/.well-known/oauth-protected-resource"; var path = context.Request.OriginalUrl.Path; var resourcePath = path.Length > prefix.Length ? path.Substring(prefix.Length) : ""; return "https://" + context.Request.OriginalUrl.Host + resourcePath; }" /> <return-response> <set-status code="200" reason="OK" /> <set-header name="Content-Type" exists-action="override"> <value>application/json</value> </set-header> <set-body>@{ return new JObject( new JProperty("resource", (string)context.Variables["resourceUrl"]), new JProperty("authorization_servers", new JArray( "https://login.microsoftonline.com/{{entra-tenant-id}}/v2.0")), new JProperty("scopes_supported", new JArray("{{mcp-prm-scope}}")), new JProperty("bearer_methods_supported", new JArray("header")) ).ToString(); }</set-body> </return-response> </inbound> <backend> <base /> </backend> <outbound> <base /> </outbound> <on-error> <base /> </on-error> </policies> The {{mcp-prm-scope}} named value populates the scopes_supported array of the metadata document. It tells the client which delegated scope to request when it goes to the authorization server, so it must be the fully qualified scope value: the token audience (the Application ID URI from the app registration) followed by the scope name. With the example values that is api://22222222-2222-2222-2222-222222222222/MCP.Access. In other words, it is the combination of the mcp-audience and mcp-scope values defined in the example setup. Named value Value to set Example mcp-prm-scope <mcp-audience>/<mcp-scope> api://22222222-2222-2222-2222-222222222222/MCP.Access [!NOTE] Keep mcp-prm-scope in sync with the scope the validation fragment requires. The PRM document advertises this scope so the client requests it, and validate-azure-ad-token then checks for it in the scp claim. A mismatch means the client obtains a token without the scope APIM expects, and validation fails. Because the policy builds the resource value from the request path, this single endpoint serves metadata for every MCP server you ever add. The Star Wars server, a future inventory server, and anything else all share it. Wiring it onto the MCP server Each MCP server only needs to declare its own metadata URL and include the shared fragment: <policies> <inbound> <base /> <set-variable name="mcpResourceMetadataUrl" value="https://apim-contoso-mcp.azure-api.net/.well-known/oauth-protected-resource/star-wars-mcp/mcp" /> <include-fragment fragment-id="mcp-entra-auth" /> </inbound> <backend> <base /> </backend> <outbound> <base /> </outbound> <on-error> <base /> <include-fragment fragment-id="mcp-auth-challenge-onerror" /> </on-error> </policies> On the VS Code side, the configuration is deliberately plain. With no subscription-key header present, the client falls straight into the OAuth flow: { "servers": { "star-wars-mcp": { "url": "https://apim-contoso-mcp.azure-api.net/star-wars-mcp/mcp", "type": "http" } } } Restart the server in VS Code, and it detects the 401, reads the metadata, opens a browser sign-in, requests consent on first use, and then loads the tools using the user's token. [!CAUTION] Do not read the response body with context.Response.Body inside MCP server policies. It forces response buffering and breaks the MCP streaming transport. If global diagnostic logging is enabled, set the Frontend Response payload bytes to log to 0 at the All APIs scope. Scenario 3: Beyond tenant membership, authorize on a user attribute with app roles Validating a token confirms the caller is a signed-in user in your tenant with the right scope. That is often not enough. Some MCP servers expose sensitive tools that only a subset of users should reach. You want to express "this user is not only part of the tenant, but has a specific attribute that permits this server." Microsoft Entra app roles are the optimal mechanism for this. You declare a role on the MCP API app registration, assign it to specific users or to a security group, and Entra ID emits a roles claim in the access token whenever your API is the audience. APIM then authorizes on that claim. App roles beat the groups claim here because they avoid the group overage problem, they are scoped to the application, and they travel with the app. Declaring and assigning the role On the MCP API app registration, under App roles, create a role: Setting Value Display name Privileged Access Allowed member types Users/Groups Value Privileged.Access Description Access to privileged MCP servers Then, on the matching enterprise application, under Users and groups, assign the users (or, better, a security group) to the Privileged Access role. The Value field is the exact string that lands in the token roles claim, so it cannot contain spaces. [!TIP] Keep User assignment required set to No on the enterprise application. Unassigned users still obtain a valid token with the MCP.Access scope and keep access to the non-privileged servers. They simply do not carry the roles claim, so the privileged servers reject them. Enforcing the claim in the per-server policy The shared mcp-entra-auth fragment is used by every server, so the role requirement must not live there. Place the check in the privileged server's own policy, right after the fragment include. The token is already validated at that point, so this step is pure authorization. Because the caller is authenticated but not authorized, return 403, not 401, and do not emit a challenge: re-authenticating will not grant a role the user does not have. <policies> <inbound> <base /> <set-variable name="mcpResourceMetadataUrl" value="https://apim-contoso-mcp.azure-api.net/.well-known/oauth-protected-resource/star-wars-mcp/mcp" /> <include-fragment fragment-id="mcp-entra-auth" /> <!-- Privileged guardrail: require the Privileged.Access app role --> <choose> <when condition="@(!context.Request.Headers.GetValueOrDefault("Authorization","").Replace("Bearer ","").AsJwt().Claims.GetValueOrDefault("roles", new string[0]).Contains("Privileged.Access"))"> <return-response> <set-status code="403" reason="Forbidden" /> <set-header name="Content-Type" exists-action="override"> <value>application/json</value> </set-header> <set-body>{"error":"forbidden","message":"You lack the Privileged.Access role required for this MCP server."}</set-body> </return-response> </when> </choose> </inbound> <backend> <base /> </backend> <outbound> <base /> </outbound> <on-error> <base /> <include-fragment fragment-id="mcp-auth-challenge-onerror" /> </on-error> </policies> One operational detail worth calling out: app-role assignments only appear in newly issued tokens. A user who is granted the role after they signed in must obtain a fresh token. In VS Code, run MCP: Reset Cached Tokens (or sign out of the Microsoft account from the Accounts menu), then restart the server and sign in again. You can confirm the result by pasting the access token into https://jwt.ms and checking for "roles": ["Privileged.Access"]. Scenario 4: Fronting an existing external MCP server that drives its own sign-in So far APIM has been the authorization resource. But many valuable MCP servers already exist and run their own identity. GitHub publishes a remote MCP server with dozens of tools, and it authenticates users against GitHub's own OAuth authorization server. You do not want to re-implement that. You want APIM to govern access (rate limits, IP rules, logging, a single managed endpoint) while letting the upstream own the login. This is the "expose an existing MCP server" passthrough mode. When you register GitHub's remote MCP server behind APIM, the gateway relays the upstream's own authorization challenge. The client never authenticates against Entra here. It authenticates directly against GitHub. The flow, confirmed by probing the gateway: A call to the APIM endpoint with no token returns GitHub's own 401 with a WWW-Authenticate header, relayed through APIM. The Protected Resource Metadata that GitHub serves advertises authorization_servers: ["https://github.com/login/oauth"], so the client knows to log in at GitHub. The PRM resource reflects the APIM host, because GitHub builds it from the forwarded Host header. The client trusts the APIM endpoint while still logging in at GitHub. VS Code completes the GitHub sign-in and the full tool catalog loads. In the proof of concept this surfaced all 47 GitHub tools through the single APIM endpoint. The client configuration is again just a URL pointing at APIM: { "servers": { "github-via-apim": { "url": "https://apim-contoso-mcp.azure-api.net/github-mcp/mcp", "type": "http" } } } The key insight is that APIM transparently relays the backend's authentication challenge. GitHub remains the authorization server, GitHub tolerates being fronted by APIM, and you get a governed, centrally managed entry point without owning the identity flow. [!NOTE] Passthrough only relays what the upstream advertises. If the backend's PRM resource value and the actual MCP transport endpoint differ by a path segment, some clients fall back to deriving the metadata location from the server URL and can miss it. When you onboard a custom self-authenticating server, verify that the resource it advertises matches the exact URL the client connects to. Scenario 5: Restricting which tools of an existing MCP server an agent may call Passthrough raises a governance question that token validation alone cannot answer. A developer may legitimately have permission to merge a pull request through GitHub, but you may not want their AI agent to perform that action autonomously. You want to allow the read and discovery tools while blocking the destructive write tools, at the gateway, regardless of what the client tries. What is and is not possible for an external server It is important to be precise here, because the capability differs from the REST-as-MCP mode: For a REST-API-exposed-as-MCP server, you pick which operations become tools at creation time. That is native tool selection and the cleanest possible filter. For an existing/external MCP server, APIM does not enumerate the upstream's tools. The portal Tools blade explicitly states that tools are not visible for external MCP servers, and there is no allow-list property for them. APIM also cannot safely rewrite the tools/list response, because reading the response body breaks the streaming transport and the list may arrive as text/event-stream. What APIM can do reliably, and server-agnostically, is block the invocation. Every tool call arrives as a JSON-RPC tools/call request in the request body, which APIM can inspect safely. The deny-listed tools remain visible in the catalog, but any attempt to invoke one is intercepted at the gateway and returned a JSON-RPC error before it ever reaches the upstream. The reusable deny-list fragment The block is driven by a per-server named value (a comma-separated list of tool names), so the same fragment governs every external server. Only the named value changes. <!-- Fragment: mcp-tool-filter (include after the auth fragment) --> <fragment> <choose> <when condition="@(context.Request.Body != null)"> <set-variable name="mcpMethod" value="@{ try { var body = context.Request.Body.As<JObject>(preserveContent: true); return (string)body?["method"] ?? string.Empty; } catch { return string.Empty; } }" /> <choose> <when condition="@(((string)context.Variables["mcpMethod"]).Equals("tools/call", StringComparison.OrdinalIgnoreCase))"> <set-variable name="mcpToolName" value="@{ var body = context.Request.Body.As<JObject>(preserveContent: true); return (string)body?["params"]?["name"] ?? string.Empty; }" /> <!-- mcpBlockedTools is a comma-separated deny-list set by the per-server policy before this include --> <set-variable name="mcpBlocked" value="@{ var tool = ((string)context.Variables["mcpToolName"]).Trim().ToLowerInvariant(); var deny = ((string)context.Variables.GetValueOrDefault("mcpBlockedTools", "")).ToLowerInvariant().Split(',').Select(t => t.Trim()); return deny.Contains(tool); }" /> <choose> <when condition="@((bool)context.Variables["mcpBlocked"])"> <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 id = "null"; try { var body = context.Request.Body.As<JObject>(preserveContent: true); id = body?["id"]?.ToString(Newtonsoft.Json.Formatting.None) ?? "null"; } catch {} return "{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"error\":{\"code\":-32602,\"message\":\"Unknown tool: " + ((string)context.Variables["mcpToolName"]) + "\"}}"; }</set-body> </return-response> </when> </choose> </when> </choose> </when> </choose> </fragment> The deny-list itself lives in a named value, one per server: APIM named value. Comma-separated, case-insensitive. mcp-blocked-tools-github = merge_pull_request,create_repository,delete_repository,push_files,create_or_update_file,issue_write,label_write # <policies> <inbound> <base /> <set-variable name="mcpResourceMetadataUrl" value="https://apim-contoso-mcp.azure-api.net/.well-known/oauth-protected-resource/github-mcp/mcp" /> <include-fragment fragment-id="mcp-entra-auth" /> <set-variable name="mcpBlockedTools" value="{{mcp-blocked-tools-github}}" /> <include-fragment fragment-id="mcp-tool-filter" /> </inbound> <backend> <base /> </backend> <outbound> <base /> </outbound> <on-error> <base /> <include-fragment fragment-id="mcp-auth-challenge-onerror" /> </on-error> </policies> Generic per-server pattern: mcp-blocked-tools-<server> = <comma,separated,tool,names> Wiring it onto the GitHub passthrough server <policies> <inbound> <base /> <set-variable name="mcpResourceMetadataUrl" value="https://apim-contoso-mcp.azure-api.net/.well-known/oauth-protected-resource/github-mcp/mcp" /> <include-fragment fragment-id="mcp-entra-auth" /> <set-variable name="mcpBlockedTools" value="{{mcp-blocked-tools-github}}" /> <include-fragment fragment-id="mcp-tool-filter" /> </inbound> <backend> <base /> </backend> <outbound> <base /> </outbound> <on-error> <base /> <include-fragment fragment-id="mcp-auth-challenge-onerror" /> </on-error> </policies> Now when the agent tries to merge a pull request, the gateway returns a clean -32602 Unknown tool error and the upstream is never touched. Read and discovery tools continue to work. The tool still appears in the client's catalog. Adding governance for another external server is just one more named value plus the same fragment include. No new policy logic. Key takeaways API Management turns MCP servers into governed resources, applying the same identity, traffic, and observability controls you already use for APIs. Start simple with validate-azure-ad-token to gate access, then graduate to a full interactive sign-in by serving Protected Resource Metadata from a single APIM policy. You can publish multiple MCP servers from one underlying API, for example a read-only server and a read-write server, by selecting different operations. App roles let you authorize on a user attribute, not just tenant membership, and the check belongs in the per-server policy so shared logic stays clean. For existing external servers, APIM relays the upstream's own OAuth flow, so a server like GitHub keeps owning its identity while you keep central governance. When an external server's full tool surface is too broad, APIM can block specific tool invocations at the gateway with a reusable, named-value-driven policy, so a user's agent cannot perform actions the user could perform manually. References About MCP servers in Azure API Management Secure access to MCP servers in API Management Expose REST API in API Management as an MCP server Expose and govern an existing MCP server validate-azure-ad-token policy reference Policy fragments in API Management RFC 9728: OAuth 2.0 Protected Resource Metadata MCP authorization specification Star Wars API (example backend) MCP for Beginners