What a 401 Means to an MCP Client
DEV Community

What a 401 Means to an MCP Client

Twice in this series, a client refused to connect to a working MCP server, and both refusals looked like bugs until they turned out to be the specification doing its job. The first was in MCP vs API Is the Wrong Question. The Functions-hosted MCP server was protected by its mcp_extension system key, and Claude Code, VS Code, and MCP Inspector all connected happily with the key in an x-functions-key header. Then I tried adding the same server as a connector in the Claude desktop app, and it failed with "Couldn't register with the sign-in service." The second was in the APIM setup in the repo: MCP Inspector, pointed at the gateway without its subscription key attached, reported "Dynamic Client Registration rejected (HTTP 404)." Different clients, different servers, the same wall. This post explains what that wall is and how to take it down properly with Microsoft Entra ID. A 401 is not a request for a header To a developer, a 401 means "add credentials and retry." To a client that implements the MCP authorization specification, a 401 means something more specific: start OAuth. The client fetches the server's Protected Resource Metadata to learn which authorization server protects it, registers itself with that authorization server (dynamic client registration), and walks the user through a sign-in and consent flow. No step in that sequence involves asking a human to paste a key into a header field. That is why both failures happened. My key-protected servers returned 401 without any Protected Resource Metadata behind them, the clients went looking for an OAuth story that did not exist, and the attempt died at registration with a 404. The clients were not broken. They were refusing to work around a server that spoke half a protocol. Which splits the MCP client world cleanly in two. Developer tools like Claude Code, VS Code, and MCP Inspector let you attach arbitrary headers, so a shared key works, the way it always has for developers who read documentation. End-user clients like the Claude desktop app deliberately offer no header escape hatch: the spec flow is the only flow. Keys are for tools; OAuth is for people. Turning on the real thing The Functions MCP extension supports built-in authentication with Entra ID as the identity provider, and the setup has become almost anticlimactic. In the Azure portal, the function app now has an AI (preview) tab with an Authentication section and a "Turn on MCP authentication" button. Give the Entra app registration a name, save, and it creates the registration, wires up App Service authentication, adds the required settings, and disables key-based access to the MCP endpoint in one step. The manual route shows what that button actually does, and it is worth seeing once. Key access is turned off by setting the webhook authorization level to anonymous, because the platform's authentication layer now stands in front of it: az functionapp config appsettings set --name --resource-group --settings "AzureFunctionsJobHost__extensions__mcp__system__webhookAuthorizationLevel=Anonymous" Then App Service authentication (the feature long known as Easy Auth) is configured with Microsoft as the identity provider, a new app registration, unauthenticated requests answered with 401, and the token store enabled. Finally, the server advertises its Protected Resource Metadata so that spec-compliant clients can discover the authorization server: az functionapp config appsettings set --name --resource-group --settings "WEBSITE_AUTH_PRM_DEFAULT_WITH_SCOPES= " That last setting is the difference between a dead end and a doorway. With it in place, the 401 comes with directions. The second wall: invalid_target Almost anticlimactic, I said. Then I connected VS Code and hit a second wall the tutorial does not mention. Discovery worked, the client found the metadata and the authorization server, and Entra refused the token request with AADSTS9010010 invalid_target: "The resource parameter provided in the request doesn't match with the requested scopes." The cause is visible in the metadata itself. A spec-compliant client sends the RFC 8707 resource parameter, taken straight from the Protected Resource Metadata, and Entra now enforces that this resource matches the audience of the requested scope. The button-made setup fails that check twice over. The metadata advertised resource: https://.azurewebsites.net but a scope of api:///user_impersonation, two different names for the same app. And the document clients actually read is the path-specific one at /.well-known/oauth-protected-resource/runtime/webhooks/mcp, whose resource includes the full endpoint path. The fix is three alignments, so that resource and scope audience are character-for-character identical: On the app registration, under Expose an API, change the Application ID URI from the default api:// to the endpoint URL: https://.azurewebsites.net/runtime/webhooks/mcp. Update the advertised scope to match: set WEBSITE_AUTH_PRM_DEFAULT_WITH_SCOPES to https://.azurewebsites.net/runtime/webhooks/mcp/user_impersonation. In App Service authentication, add the same URL to the allowed token audiences. Then fetch the path-specific metadata document and check that resource and the prefix of scopes_supported are the same string. When they were, VS Code connected on the next start: 401, discovery, sign-in, token, and "Discovered 3 tools" in the output, with no key anywhere in the configuration. This is not a quirk of one sample. The same error is currently open against the Azure DevOps MCP server and VS Code, and there is a growing catalog of Entra and MCP authorization mismatches. The pattern is the post's thesis repeating one level down: a strict client, a server speaking half a dialect, and the client refusing to guess. The client connection, before and after Before, the client configuration carried a secret: { "servers": { "restaurant-directory": { "type": "http", "url": "https:// .azurewebsites.net/runtime/webhooks/mcp", "headers": { "x-functions-key": " " } } } } After, it carries nothing: { "servers": { "restaurant-directory": { "type": "http", "url": "https:// .azurewebsites.net/runtime/webhooks/mcp" } } } On first connect, the client receives the 401 plus metadata, discovers Entra ID, and opens a browser window. You sign in as yourself, consent once, and the client holds a token that refreshes automatically. The credential in the picture is now an identity, revocable per person, subject to Conditional Access, visible in sign-in logs. Everything the system key was not. The Entra wrinkle: known clients only There is one honest caveat, and it is where the story gets interesting for enterprise architects. Entra ID does not do open dynamic client registration; it wants to know its clients. The tutorial flow handles this by preauthorizing specific client applications on the app registration: VS Code, for example, is client id aebc6443-996d-45c2-90f0-388ff96faa56, added under "Expose an API" so users are not prompted for admin consent. For clients outside the preauthorized list, you register a client application yourself and hand its id to the client. The Claude desktop connector anticipates exactly this: the error message from the first post already offered a field to "add an OAuth Client ID in the connector settings." What read as a workaround then is the designed path now: create the client registration in your tenant, preauthorize it against the server's app registration, give the connector the client id, and the sign-in flow completes inside your own identity perimeter. Seen from a governance angle, this is a feature wearing a limitation's clothes. Anonymous dynamic registration means any client anywhere can start a flow against your server. A preauthorized client list means your identity team decides which MCP clients exist in your estate. For a regulated organization, that is not friction; that is the control you would have had to build anyway. What the token does and does not do The token asserts who the caller is, which tenant they belong to, and that your authorization rules let them reach the endpoint. It does not decide what they may do once inside. Tool-level authorization, the difference between a user who may search restaurants and one who may place orders, remains your code's job, and the claims in the token are the input to it. Authentication moved to the platform; authorization stayed in the kitchen, exactly where the first post's caveats said it lives. Verdict Keys for demos and developer tooling, where a header field is available and rotation is cheap. Entra ID with built-in MCP authentication for anything a human signs into, and for any server that should exist inside an organization's identity perimeter rather than beside it. The clients already enforce this split; the only choice you get is whether to be surprised by it. The sample repo has both doors and the APIM patterns this builds on; the Functions MCP tutorial has the full authentication walkthrough. Top comments (0)

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.