tomydiaz
All posts

MCP Server Hands-On · Part 4 of 5

9 min read

Adding Login to the MCP Server: What Entra Does Not Do and What Claude Expects

Write tools need to know who is calling. The client registration gap between Claude and Entra ID, why a platform subdomain cannot be the resource, and the on-behalf-of exchange that keeps the API honest.

The first three parts of this series cover things that are built and running: the server, the API behind it, and the deploy and the connection from Claude. This one is a design. The login is not built yet, but I have spent enough time on why it has the shape it has that I want it written down before the code exists.

Why it needs a login now

Today the server has one read-only tool that returns published events, which are already public. The next tools create and edit events. For those, the events API has to know who is making each change, both to allow it and to be able to answer later who changed what. An unauthenticated URL cannot give it that.

The users are a handful of people on the team, all admins, all already in the Entra tenant the admin app signs in against. That fact ends up settling most of the decisions below.

How Claude logs in to an MCP server

Written out as steps it is easier to follow than it sounds:

  1. Claude calls the server without a token.
  2. The server answers 401 with a WWW-Authenticate header pointing at a small JSON document, the protected resource metadata.
  3. That document names the authorization server, which is Entra here.
  4. Claude registers itself with the authorization server as a client, opens the login page, and the user signs in.
  5. Claude receives a token meant for the MCP server and sends it on every request from then on.

The registration gap

I followed a Pluralsight course on MCP that builds a server protected by Entra, with token validation and all. It ends by admitting that neither Claude Desktop nor Claude Code could connect to the server it had built, and leaves the fix for a follow-up course. I finished it convinced that Claude did not support some part of OAuth.

Claude’s connector documentation says otherwise. Claude can register itself in two automatic ways, Dynamic Client Registration and Client ID Metadata Documents, and it also accepts a client ID you registered beforehand. That is the three-option list in the connector dialog I described in part three.

The missing piece is on Entra’s side, which supports neither automatic method. Every application that wants tokens from Entra has to be registered by hand in advance. When Claude arrives expecting to register itself, there is no endpoint to do it with.

For this project there are three ways across:

  • Pre-register a client ID and hand it out. Each person pastes the URL and the client ID into the connector’s advanced settings. For Claude Code the CLI has --client-id, plus --callback-port, since Claude Code receives the login on a local port that is otherwise random and Entra needs a redirect URL it knows in advance.
  • Put something in front of Entra that implements registration. At the small end, a registration endpoint that registers nothing and always answers with the pre-registered client ID, while the login and the tokens still go directly between Claude and Entra. Users would paste only the URL. It is not quite standard, and it depends on Claude not checking something it currently does not check.
  • List the server in Claude’s connector directory, where Anthropic holds the client credentials and users search for the server and click connect.

The second and third exist to spare dozens of unknown users an extra step. Here the users are five colleagues, and a client ID pasted once is not worth engineering around. Option one it is. The small registration endpoint stays filed as a possible upgrade.

The token the API sees

With a login, a naive version would take the token Claude sends to the MCP server and forward it to the events API. That does not work, and it should not. The token’s audience is the MCP server, and the API is right to reject a token that was issued for someone else.

What the server does instead is the on-behalf-of exchange. It hands Entra the user’s token and asks for a new token for the events API that still carries the user’s identity. The API then sees the real person, which is the whole point of the login.

The design turns out to be cheap in one place. The token the exchange produces is for the same API scope the admin app already uses, and part two already taught the API to validate exactly that. The API needs no changes for this.

Claude MCP server Entra events API 1 a tool call, with no token 2 401, and where to sign in the server is not in this part 3 sign-in with Claude's client ID and PKCE 4 a token for the MCP server 5 the same call, with that token 6 check the signature, the issuer, the audience and the scope 7 that token plus the server's secret 8 a token for the events API, same user 9 the call, carrying that token 10 the answer, as that person
The design end to end. Steps 1 to 5 are the sign-in, which happens between Claude and Entra; steps 7 and 8 are the on-behalf-of exchange, which is the server's own conversation with Entra.

The cost lands on the MCP server. To perform the exchange it needs a credential of its own with Entra. Where the server runs there is no managed identity, so that credential is a client secret stored as a secret on the host, and someone has to rotate it before it expires.

Why the server needs a domain of its own

This was the part that took me longest to understand, so here it is plainly.

When the MCP server is registered in Entra, its URL goes into the registration as its identifier. That identifier is what ends up inside every token, saying “this credential is for this server”. Entra only accepts identifiers on domains the tenant has proven it owns. The server’s current address is a subdomain of the hosting platform, which belongs to the platform, so Entra rejects it, with an error Claude’s documentation calls out by name: AADSTS9010010.

The fix is a subdomain of our own domain, proven to Entra with a TXT record in DNS. There is one open question I have not answered yet: the tenant is External ID, and I still need to confirm it can verify a custom domain for this purpose before building on the assumption that it can.

Staying awake for the login

Claude waits up to ten seconds for the server’s discovery, registration and token responses. A server that sleeps when idle and needs a few seconds to wake can therefore fail logins some of the time, which is the hardest kind of failure to diagnose. The server has to stay up, and it does now. The moment to worry about this is before a teammate reports that the login sometimes does not work.

The shape of the write tools

Two decisions for when the tools arrive.

The whole server goes behind the login. Mixing anonymous read tools with authenticated write tools on the same server is possible, and it is also one more thing to get wrong. Once everyone signs in, nothing needs to be anonymous.

And the first write tools are create and edit, not delete. With a signed-in user, the API will also show the agent drafts, and an agent that can delete events because a prompt was phrased badly is not something I want to meet in production. Deleting can come later, once there is a record of how the other tools get used.

What I still expect to trip over

Two things I have read about and not yet hit. Entra may demand a client secret for the browser’s redirect URL and reject the login with AADSTS7000218, which would mean registering that redirect differently. And the resource field in the metadata document has to match, exactly, the URL the user pasted, /mcp path included, or Claude refuses to continue.

I would rather have those written down now, and find out which ones were wrong, than meet all of them for the first time together.