The MCP Server Was the Easy Part
Getting an MCP agent published into Microsoft Teams meant two identity systems sharing one login page, a license that lets you build but not publish, and about US$200 a month at the exit. Notes from a weekend in the Microsoft admin gauntlet.
Over the weekend I published a Teams agent for Ridgeline, the food-costing system I've been building for Outward Bound Hong Kong. They're a Hong Kong SME and a long-time Microsoft shop, so a Teams agent is the natural front door: ask "what's the food cost for Journey Water at 12 people" in a chat window and get the rollup back. The client hasn't seen any of this yet: I piloted it dogfood-first, in my own tenant with my own Teams, which is how the weekend also turned into a guided tour of my own Microsoft account sprawl. The engineering half was quick. Ridgeline already had an MCP server, and Copilot Studio speaks MCP natively now - you point a wizard at your endpoint, fill in the OAuth fields, and the connection works. That part took an afternoon.
The rest of the weekend was spent on things that were not engineering: figuring out which of my Microsoft accounts was allowed to do what, why the license that let me build the agent refused to publish it, and what the standing cost of the publish button would be. I kept a running symptom-to-fix table as I went, because every one of these walls cost real time once and I never want to re-derive any of it.1
Two identity worlds sharing one login page
Microsoft has two separate identity systems that happen to share a sign-in page: personal accounts (the outlook.com lineage, "MSA" internally) and work accounts (Entra ID, owned by an organization's tenant). An email address can exist in both worlds at once, and the sign-in flow guesses which one you mean. It often guesses wrong.
Both of my long-standing Microsoft emails turned out to be personal accounts. Years of Azure sign-ups had made each of them silently spawn a bare organization ("Default Directory") in which my own email participates only as an external guest - the usernames have a literal #EXT# in them. A guest can own Azure subscriptions and even hold Global Admin, which is why I never noticed. But Microsoft 365 products treat guests as second-class, and Copilot Studio spent an evening throwing access errors at me before that picture came together. One symptom, as a taste: a tenant sign-in page telling me my own domain "isn't in our system," because a guest's domain isn't verified in the tenant they're a guest of, so you cannot type it into that prompt at all. You have to arrive by clicking an account tile in an already-signed-in session.
The fix was to create my first native work account - a real member of the tenant, no personal-account twin - after something like a decade of using Azure. All the Microsoft 365 licensing and the pilot now hang off that one account, and I keep a small identity map in the repo of who is who, because with three accounts across two tenants I could no longer hold it in my head.
Licensed to build, but not to publish
Copilot Studio let me build the whole agent on a free trial: instructions pasted in, MCP tools connected, test pane answering costing questions correctly. Then I hit Publish and got "You do not have a user license that allows you to publish."
Before that, Copilot Studio wouldn't load at all: an infinite spinner, because a brand-new tenant's default environment has no Dataverse database, which is a sentence I could not have parsed a week ago. The session log from that stretch has me typing "all of this jargon is annoying as fuck," and I stand by it.
It turns out the free trial (the SKU is literally named CCIBOTS_PRIVPREV_VIRAL) allows authoring only. Publishing - which is the entire point, since Teams is a publish channel - needs the tenant-level Copilot Studio license. You start that trial from the admin center, where it shows up under the product's previous name, Power_Virtual_Agents.2 It cannot be assigned to individual users, which looks broken and is normal. And after it activates, the Publish button still fails, because the license check is cached in your session. You have to fully sign out of Copilot Studio and back in. Nothing tells you this. That one took a while.
Then there's the price. After the trial, the full Copilot Studio license runs about HK$1,500 a month (roughly US$200) per maker. My reaction in the session log, verbatim: "copilot studio is 1500 HKD a month, ridiculous." The softeners are real: only the maker (the person who authors and publishes the agent) needs it, end users in Teams don't, and end-user message consumption can be billed pay-as-you-go against an Azure subscription instead. The client side has its own layer too: every end user needs a Teams-bearing Microsoft 365 seat before they can even open the chat window (though not a Copilot license). But the plain reading stands. US$200 a month is the standing cost of the publish button, for a chat window into software that already runs and already has an open protocol for exactly this kind of access. The MCP endpoint costs me nothing to expose. The distribution channel is where the toll booth is.
Two surprises
The first surprise came at agent creation: the default model in Copilot Studio was Claude Sonnet 4.6. Microsoft's own agent builder, and the model pre-selected in the dropdown is Anthropic's, not OpenAI's. I was suspicious enough that I typed, into the Claude Code session that was helping me: "interesting that the default model is sonnet 4.6 (and not openai). did you need to do some integration?" I was asking Claude whether it had somehow installed itself. It had not. But the loop is real: Ridgeline was built with Claude Code driving these same MCP tools during development, the agent's instructions were ported from the Claude-side skill files, and now a Claude model inside Microsoft's builder drives the same tools in production.
The second surprise is stranger and still open. The pilot's smoke-test question is "what's the food cost for Journey Water at 12 people." In Copilot Studio's test pane, the agent answers the way I want it to: a short, summarized rollup. Ask the identical agent the identical question from Teams and it produces the full per-line breakdown, verbose to the point of being unfriendly. Same instructions (they're versioned in the repo and pasted in), same tools, same model. Only the channel changed. I haven't found the knob that explains the difference yet, and the honest current answer is that the same published agent behaves differently depending on which Microsoft surface you talk to it through.
One more papercut worth naming for anyone piloting this: Copilot Studio's manually-configured OAuth connections re-prompt users to sign in every 60 to 90 minutes. Requesting offline_access in the scopes softens it (Entra issues refresh tokens), but softened is not fixed, and pilot users should hear about it before they hit it.
The integrator was Claude Code
The third surprise is one I probably should have seen coming. The whole integration started as a question typed into a Claude Code session on Saturday: does Microsoft Copilot work with Ridgeline now? The client is a Microsoft shop, and I was wondering what an ordinary staff member would do to reach Ridgeline over MCP. An hour later there was a plan. When the identity walls started, I asked whether there was an Azure MCP server so Claude could help drive the integration directly. The answer turned out to be simpler than that: Claude Code driving the plain az CLI. Every portal step ended up performed as a command instead of a click, and the repo now carries a runbook of the actual commands with a note I stand behind: scripted operations are deterministic, auditable, and they can't forget manifest edits.
Some examples of what that meant in practice. My very first az login died with AADSTS50076 (an MFA policy triggering mid-login); Claude re-issued it pinned to the tenant with --use-device-code --allow-no-subscriptions and we were in. The app registration's manifest invariants (v2 access tokens, the exposed scope, the optional claims) went in as a single Graph PATCH, which is how requestedAccessTokenVersion: 2 got set correctly on the first try. The troubleshooting table calls that field the number-one setup mistake, and I never made it, because Claude wrote the patch with the field pinned rather than trusting me to find the checkbox. When token minting failed with AADSTS65001, Claude recognized that the Azure CLI is itself a client app that needed consent for my custom scope, granted its well-known service principal directly, then sat in a retry loop until the consent propagated. When Copilot Studio said I needed "additional access" to the new Dataverse environment, the portal's obvious-looking fix (a page called "Application users") turned out to be a red herring for service principals; the real fix was a force-sync POST against the Power Platform admin API, which I would not have found by clicking. And before Copilot Studio entered the picture at all, Claude verified the whole auth chain at the token level: mint an access token with az, decode it, curl the MCP endpoint with it, and read a 401 as the expected "valid token, unbound user" rather than a failure.
The division of labor was clean. Microsoft won't let you script a license trial (purchases go through the admin center UI, full stop), so I did the clicking where clicking was mandatory, pasting each error back into the session as I hit it. Everything else became a command in a runbook that the next tenant onboarding can replay.
Worth it, with a sigh
The thing that makes this worth the weekend is per-user attribution, end to end. Each staff member adds the agent in Teams, consents once, and from then on every MCP call carries their own token. When someone applies a supplier price update from a chat window, the audit row records that human, with no shared credential anywhere in the chain. Whatever device policy and Conditional Access their tenant enforces apply automatically, because identity lives where it already lived. For a client in Microsoft's world, that's the product.
And the gauntlet is now documented: runbooks for the identity setup and the agent setup, the symptom-to-fix table, an identity map. Onboarding the next client tenant should be an admin consent and a config change rather than a weekend. The US$200-a-month question I get to defer until the trials lapse in mid-August, when I'll decide between the full license and the pay-as-you-go route.
What stays with me is the asymmetry. The open-protocol half of this integration (an MCP server, an OAuth flow) was an afternoon of work I'd happily do again. The proprietary half was two days of guessing which account, which license, and which cached session stood between me and a button. I understand why enterprises pay integrators for this. Mine turned out to be Claude Code with an az login, and I'm less sure it has to be this way.
Footnotes
-
The table lives in the repo as a troubleshooting index: every wall from the two days, written as symptom, actual cause, fix. My favorite entry is the redirect-URI one, because every new Copilot Studio connection mints its own callback URL, and patching the app registration replaces the whole array, so forgetting to include the existing URIs breaks the connections that were working. ↩
-
Copilot Studio has been renamed at least once (it was Power Virtual Agents), and the SKU names never caught up. So the admin center shows a product name that no longer exists for a license that cannot be assigned to anyone, and both of those are working as intended. ↩