ai
1380 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 Engineer Associate (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.3.3KViews5likes20CommentsGovernance 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-MCT46Views0likes0CommentsSecure 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.6KViews1like8CommentsSecuring 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.6KViews2likes0CommentsSet 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.36Views0likes0CommentsUnlock 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.610Views0likes0CommentsMCP 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 BeginnersOperationalizing AI powered medical imaging pipeline for cohort building
Authors: Jared Erwin, Senior Software Engineer, HLS Nursing AI and Data Platform, Faculty UW School of Medicine Manoj Kumar, Director, HLS - Data & AI HLS Frontiers AI Alberto Santamaria-Pang, Principal Applied Data Scientist, HLS Frontiers AI and Adjunct Faculty, Johns Hopkins Medicine Overview In Part 1, of this series, we showed how natural language could be used to define medical imaging cohorts and retrieve relevant studies in seconds instead of months. That proof-of-concept demonstrated the value of the idea — but not how to make it repeatable, or production-ready. This post focuses on how we turned that prototype into a production-oriented Azure Machine Learning pipeline — to scale execution and produce clear, versioned artifacts that could drive an interactive cohort exploration UI. If you're building ML pipelines for medical imaging, or any domain where data is large, messy, and locked behind access controls, we hope our experience saves you time. From scripts to a pipeline: Why Azure ML components? The original hackathon implementation consisted of notebooks and scripts that required careful manual execution. To make the system repeatable and auditable, we standardized it using Azure ML pipelines. Azure ML pipelines gave us: Componentized execution — each processing step is a self-contained unit with defined inputs, outputs, and dependencies Parallel branches — steps that don't depend on each other run concurrently Reproducibility — every run is versioned and logged with full lineage Compute flexibility — run on CPU for metadata extraction, GPU for model inference, without manual orchestration The pipeline architecture The pipeline consists of 5 python components arranged in a DAG with two parallel branches: [0]scans a DICOM directory and extracts metadata from headers — study/series UIDs, modality, body part, slice counts. [1]classifies each series by anatomy and orientation using a multi-tier strategy (more on this below). [2] and [3] form the search pipeline: anatomy labels are converted to natural language text templates, then encoded with BiomedCLIP into a FAISS vector index. [4]generates 2D UMAP coordinates from the embeddings for the interactive scatter plot visualization in the UI. The image depicts a flowchart detailing the process of DICOM metadata extraction, anatomy classification, visualization enrichment, and text template generation, followed by the creation of a FAISS vector index. Components 2 and 4 run in parallel after component 1 completes, saving roughly 10-15% of total execution time. It's a modest gain for a single run, but it adds up when iterating on pipeline parameters. [1] Anatomy classification, integrating MedImageInsight The Anatomy classification component in the pipeline relies on MedImageInsight (MI2). MedImageInsight is Microsoft's foundation model for medical image understanding, available through the Azure AI Foundry model catalog. Unlike generative models, MedImageInsight is an embedding model — it maps medical images and text into a shared 1024-dimensional vector space, enabling tasks like classification and similarity search by comparing image embeddings against text label embeddings. Given a DICOM image, we compare its embedding against candidate labels (e.g., "Brain", "Chest", "Abdomen") to determine the body part, scan orientation, and other imaging characteristics through zero-shot classification. We also may get directly annotated anatomy from component 0, the DICOM metadata extractor component. We can combine both data points to build our final search index. [2] [3] FAISS index construction As an input to the FAISS index, we first run component 2, the text template generator. This component takes the metadata and anatomy information from components 0 and 1 and feeds them into 5 different agents with different instructions on how to describe the DICOM study. This results in textual descriptions which some variation, referred to as text templates, which can be indexed in the next component The FAISS index builder (component 3) uses BiomedCLIP to encode all text templates into 512-dimensional vectors: MODEL_NAME = "hf-hub:microsoft/BiomedCLIP-PubMedBERT_256-vit_base_patch16_224" @torch.no_grad() def encode(self, texts: List[str], batch_size: int = 256) -> np.ndarray: embeddings = [] for i in range(0, len(texts), batch_size): batch = texts[i:i+batch_size] tokens = self.tokenizer(batch).to(self.device) batch_embeddings = self.model.encode_text(tokens) batch_embeddings = F.normalize(batch_embeddings, dim=-1) # L2 normalize embeddings.append(batch_embeddings.cpu().numpy()) return np.vstack(embeddings) We L2-normalize all vectors and use faiss.IndexFlatIP (inner product), which is equivalent to cosine similarity on normalized vectors. For our current dataset sizes (thousands of series), flat indexing is fast enough. For hospital-scale datasets with millions of images, we might switch to IndexIVFFlat or IndexHNSW for approximate nearest neighbor search. In the cohort explorer app, a user will enter a natural language query, which is then converted to embeddings using the same BiomedCLIP model. This allows a search using the FAISS index to find relevant DICOM studies. [4] Visualization: making embeddings explorable The scatter plot in the UI is often the first thing users interact with. It needs to show meaningful clusters without requiring users to understand dimensionality reduction. Component 4 takes the embeddings from component 1 and projects them to 2D with UMAP: umap = UMAP( n_components=2, n_neighbors=10, # Balances local vs. global structure min_dist=0.5, # Prevents over-clustering metric='cosine', # Matches our embedding similarity metric random_state=42 # Reproducible layouts ) coordinates_2d = umap.fit_transform(features) Each point in the scatter plot corresponds to a single DICOM series produced by the pipeline, with color, grouping, and hover metadata derived directly from the JSON artifacts emitted by components 1 and 4. Each pipeline run produces a small set of well-defined artifacts — metadata tables, embedding vectors, UMAP coordinates, and the FAISS index — which are consumed directly by the cohort exploration UI. The cohort explorer application can reload or switch between datasets. The diagram is a screen capture of an Azure ML pipeline. It includes 5 pipeline components along with connecting arrows showing incoming and outgoing data, including the final outputs of the pipeline. Pipeline execution: time, cost, and what we learned Here's what a typical pipeline run looks like for a dataset of ~4,500 DICOM series: Component Task Approximate Time (CPU) Approximate Time (GPU) 0 - DICOM Metadata Extractor Scan files, extract headers 5-10 min 5-10 min 1 - Anatomy Classification Classify anatomy/orientation 90-120 min 5-10 min 2 - Text Template Generator Generate 5 templates per series 5-10 min 5-10 min 3 - FAISS Index Builder BiomedCLIP encoding + FAISS build 60-90 min 10-15 min 4 - Visualization Enrichment UMAP + color assignment 20-40 min 5-10 min Azure ML overhead Compute provisioning, env setup 5-10 min 5-10 min Total ~200-300 min ~30-50 min Key observations: Azure ML overhead is significant when doing quick iteration and testing. Compute provisioning, conda environment builds, and data mounting add several minutes before any component code runs. We first built each component as python code to run locally and debug before our first Azure ML run. This way we quickly iterated and avoided cost until we were ready. BiomedCLIP encoding dominates on CPU. Component 3 is the bottleneck. Moving to GPU compute for this component cuts encoding time roughly in half, but GPU clusters cost more. For a pipeline you run occasionally, CPU is fine. For frequent re-indexing, GPU pays for itself. Batch size tuning matters. The default BiomedCLIP batch size of 256 balances memory and throughput. On GPU, you can push to 512. On CPU with limited RAM, drop to 128. At Scale: 120,000 Images, CPU vs. GPU We ran the full pipeline against a larger dataset of ~120,000 images to understand how compute choice affects end-to-end time and cost: CPU Pipeline GPU Pipeline Pipeline compute time 4 days, 12 hours (108 hrs) 15 hours Pipeline compute cost ~$0.25/hr × 108 hrs = ~$27 ~$3.00/hr × 15 hrs = ~$45 MedImageInsight endpoint (MaaP on Standard_NC4as_T4_v3) ~$151 ~$21 Total estimated cost ~$178 ~$66 Both pipeline runs make the same ~120,000 classification calls to the MedImageInsight endpoint, but those calls are spread out over different time periods depending on how quickly and efficiently the pipeline can make the calls to MedImageInsight. The hourly cost for MedImageInsight on a Standard_NC4as_T4_v3 VM is ~$1.40/hr. Resulting in the estimated costs for MedImageInsight in the table above. GPU compute was roughly 7× faster at about 0.37× the total cost when endpoint costs are included. This was a key learning and clearly indicates the benefits of the more powerful compute resources. MedImageInsight can be deployed in two ways, depending on dataset size and operational needs. For smaller or infrequently processed datasets, we deploy MedImageInsight as a managed Azure ML online endpoint and invoke it from the pipeline. This keeps the pipeline simpler and avoids managing the MedImageInsight compute directly, while offering comparable performance at modest scale. For larger batch workloads, an alternative approach is to load MedImageInsight directly on the Azure ML pipeline’s GPU-backed compute. In this model, the pipeline handles both model loading and classification, eliminating per-request network round trips and the fixed cost of hosting a persistent endpoint. While this approach requires slightly longer pipeline run time, it becomes more cost‑effective at scale by avoiding endpoint overhead and improving throughput during bulk processing. Possible future enhancements Additional modalities: Extending the pipeline and classification to CT, X-ray, and ultrasound imaging, and build on the pattern for pathology images Image embeddings fusion: Combining MedImageInsight image embeddings with text embeddings for hybrid search Condition-aware search: Enabling queries about findings and conditions, not just imaging parameters The gap between a hackathon demo and a production system is where the real engineering happens. We hope sharing our journey helps others building similar systems. If you’re interested in partnering with us to work toward this goal or need access to the GitHub repo with the pipeline and UI code, contact authors through your Microsoft account team or reach out to Microsoft HLS AI frontier team The healthcare AI models in Microsoft Foundry are intended for research and model development exploration. The models are not designed or intended to be deployed in clinical settings as-is nor for use in the diagnosis or treatment of any health or medical condition, and the individual models' performances for such purposes have not been established. You bear sole responsibility and liability for any use of the healthcare AI models, including verification of outputs and incorporation into any product or service intended for a medical purpose or to inform clinical decision-making, compliance with applicable healthcare laws and regulations, and obtaining any necessary clearances or approvals.Sizing Copilot Credits for Cowork? Let Your Users Use Cowork Investment Advisor Agent.
Right, let's talk about a headache. If you're helping a customer allocate Copilot Credits for M365 Copilot Cowork, you have to size it all up first — and doing that by hand is a fiddly, time-consuming job. Here's the problem, and a much easier way to solve it. Start with the Cowork Estimator Here's the good news: you don't have to start from scratch. Microsoft has published the Customer Cowork Estimator — a handy tool that turns personas, prompt complexity and expected usage into an estimated credit number. It's the perfect place to begin, and it does the core maths for you. To get the most out of it, you just need to feed it good inputs — and that's where a little groundwork comes in. For each part of the business, you'll want to: Work out who's who. There are four types of user — corporate knowledge workers, management and senior leaders, customer-facing folks, and technical staff. Count how many of each. Quick for a small team, a bit more involved for a big one. Pin down what they'd genuinely use Cowork for. The real multi-step workflows that hop across apps and actually do things — not just a quick chat or a summary. Judge how heavy each workflow is. Light, Medium or Heavy — since each level uses a different number of credits per run. Estimate how often it runs. Daily adds up to a lot of runs a month; weekly is far fewer. The estimator handles the sums beautifully once those inputs are in. Gathering the inputs themselves — persona by persona, workflow by workflow, across a few thousand people — is simply the part that takes time. And that's exactly where this agent lends a hand: it builds on the estimator by automating the groundwork that feeds it. How this agent helps This agent takes on that groundwork for you. Instead of sizing everyone from the outside, the admin simply switches the agent on for every user. Each person then sizes their own needs — and the agent does the clever bits for them, ready to drop into the estimator. Here's what it does behind the scenes: Spots the right persona. It works out which of the four personas each user fits. Finds the real workflows. It looks at the top Cowork scenarios the user would actually run. Keeps it honest. It checks each one is a genuine Cowork job — several steps, more than one app, real actions and a bit of decision-making — not something a Scheduled Prompt or plain Copilot Chat could do just as well. Grounds it in evidence. It reads the signals from recent work — emails, meetings, documents and Teams chats — so the estimate is based on what people actually do, not thin air. Does the sums. It maps each workflow's complexity to credits per run and totals it up — giving you numbers that line up neatly with the estimator. Shows the value. It gives a view of pay-as-you-go versus a pre-purchase plan, and a sense of the return on the spend. Stays transparent. It states its assumptions, flags how confident it is, and sticks to permitted data and the usual privacy and compliance rules. And here's the kind of report it hands back — persona, the top workflows, complexity, credits, cost and a clear recommendation, all in one place: Why this approach works better So why hand it to the users? A few good reasons: More accurate. The numbers come from each person's real workload, not a top-down guess. Consistent by design. Everyone follows the same method — same personas, same complexity bands, same maths — so the results line up and roll into one clean figure. No over-buying. Because it weeds out the workflows that don't really need Cowork, your customer only pays for credits they'll genuinely use. A proper business case. You get cost clarity and a feel for the return, so you're handing over more than just a number. It scales. Ten users or ten thousand — the effort on your side stays much the same. It saves you hours. You swap manual sizing for gathering and rolling up. Your time goes on advising, not tallying. Not ready to roll it out? Other ways in If an admin isn't quite ready to switch the agent on for everyone, that's OK— there's no need to. This agent is an M365 Copilot agent, and it has free access to Work IQ — the same engine that will eventually power Cowork. Because that access is free, the agent can read how people really work and take the guesswork right out of sizing. And there's more than one way to get at it: Build it with Agent Builder. Ready-made instructions, a description and a starter prompt are all sitting in the GitHub repo. Anyone with access to M365 Copilot can pop them into Agent Builder and stand the agent up in minutes. Prefer not to build an agent? Just use the prompt. There's a comprehensive prompt you can drop straight into the M365 Copilot Chat experience. Same sizing, same evidence — no agent to create. Ready to give it a go? Whichever route suits you best, getting started is dead simple. If you're rolling the agent out to your users: Have a quick word with your customer's admin about switching the agent on for their users. Let each person size their own Cowork needs. Gather it all up and roll it into a single estimate. Prefer to keep it in your own hands? Build the agent from the GitHub repo with Agent Builder, or drop the comprehensive prompt into M365 Copilot Chat — you'll get to the same place with even less setup. THE BOTTOM LINE That's the heavy lifting done for you. You'll save yourself hours, spare yourself the guesswork, and hand your customer a credit plan they can genuinely trust — with the numbers and the business case sitting right behind it. Give it a spin on your next engagement and see how much quicker it gets you there. Resources Everything you need is in one place: Customer Cowork Estimator — https://aka.ms/CustomerCoworkEstimator GitHub repo — agent instructions, description, starter prompt and the full Copilot Chat prompt: Cowork Investment Assessment490Views1like0CommentsGoodstack-Validated Nonprofit Rejected After Forced Resubmission — No Escalation Path
I am posting this because Microsoft Elevate frontline support told me there are no further escalation paths available, and I believe this may affect other nonprofits. Timeline of facts: - June 19, 2026: Goodstack, Microsoft's authorized validation partner, confirmed our nonprofit (Opera Verace Foundation, EIN 33-2305878, California 501(c)(3)) was approved for Microsoft nonprofit grants and discounts. - June 23, 2026: Microsoft Elevate support informed me the original application was in an unrecoverable system error state due to an unrecoverable error in their system, and they instructed me to resubmit under a different email address. I only had one email directed to my business email and asked if I could use a Yahoo email. - June 30, 2026: The same Elevate representative confirmed in email to me that using a Yahoo email address as a temporary workaround was fully supported and "the correct and supported path forward." - July 2026: A different Microsoft Validation Team (not the one I had worked with originally) rejected the resubmission (likely due to use of a yahoo.com email as my business email address) and demanded documents not listed in the published eligibility requirements, including a bank statement. They stated the original Goodstack approval cannot be reinstated. - When I pointed out that Goodstack's validation was never rescinded, that my organization's nonprofit status can be confirmed on propublica online, and that the resubmission existed solely due to Microsoft's own system error and instructions, I was told this was a determination of another team and no further escalation paths exist. The core issue: Goodstack validated our organization on Microsoft's behalf. Microsoft's own system error forced resubmission. Microsoft's own representative authorized the (incorrect) workaround in writing. The Validation Team is now treating the workaround submission as a brand new independent submission requiring extraordinary measures for re-validation, ignoring the Goodstack validation, ignoring the request to check propublica, and ignoring the fact that thier reuest for additional documents is nowhere to be found in Microsoft's own document requirements in Microsoft's published nonprofit eligibility policies. Ticket number: 2606210040000788 Is anyone at Microsoft able to escalate this to someone with authority over the Validation Team? Has anyone else encountered this after being directed to use a personal email for resubmission?26Views0likes0Comments