Zuplo
MCP error

resource indicator is missing, or unknown

The client reached your authorization server and asked for a token bound to your MCP server, and the authorization server refused the binding: the `resource` value it was handed names something it does not recognise, or the client sent no `resource` at all and this server requires one. No token is minted, so nothing ever arrives at your MCP server to be accepted or rejected.

MCP Specification
2026-07-28

Is this you?

Take the resource identifier from your own metadata rather than from your notes, then find out whether the authorization server has an opinion about it. The second half works by sending a token request that is certain to fail on the grant, and reading which error the server reaches for first.

TerminalRun this to check — it changes nothing
# 1. The resource identifier your server publishes, byte for byte, plus the
#    scopes it expects to go with it.
curl -sS https://your-server.example.com/.well-known/oauth-protected-resource/mcp \
  | jq -r '.resource, (.scopes_supported // [] | join(" "))'
# → https://mcp.example.com

# 2. A token request that cannot succeed, carrying that resource.
curl -sS -X POST https://auth.example.com/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d grant_type=authorization_code \
  -d code=deliberately-not-a-real-code \
  -d client_id=<your-client-id> \
  -d resource=https://mcp.example.com

# 3. The same request with the resource removed, for the comparison.
curl -sS -X POST https://auth.example.com/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d grant_type=authorization_code \
  -d code=deliberately-not-a-real-code \
  -d client_id=<your-client-id>
This error
Step 2 answers invalid_target while step 3 answers something about the code: the server parsed your resource, rejected it, and never looked at the grant. Read the description for which product you are arguing with — resource indicator is missing, or unknown is the default text node-oidc-provider ships for its InvalidTarget error, so a Logto or node-oidc-provider deployment is the far end; AADSTS901002 or AADSTS9010010 is Entra ID. Two results are not proof of anything and it is worth knowing which: a server that validates the grant before the resource returns the grant error in both steps even when your resource is wrong, and a server that rejects resource only at /authorize cannot be caught from the token endpoint at all. In both cases only a real authorization request settles it. If step 1 prints a resource with no path component, treat that as unhealthy on its own — it is the shape that acquires a trailing slash in transit.
Then the fix is the next section.
Working
Step 1 prints a resource identifier that is exactly the URL a client dials — same scheme, host, port and path, and the same trailing slash or absence of one — and steps 2 and 3 come back with the *same* complaint about the authorization code, typically invalid_grant. Identical errors mean the server got past resource validation and is now objecting to the fake code, which is the answer you wanted. A provider that lists no resource in its documented parameters and simply ignores yours also lands here: RFC 6749 requires an authorization server to ignore unrecognised request parameters, so silence is a legitimate outcome and the audience check still has to happen at your MCP server.
Then this is not your problem — try the error reference.
6 other strings clients print for this
  • Authorization error: InvalidTargetError: resource indicator is missing, or unknown
  • AADSTS9010010: The resource parameter provided in the request doesn't match with the requested scopes.
  • AADSTS901002: The 'resource' request parameter is not supported.
  • {"error":"invalid_target","error_description":"apps scopes require a valid MCP resource"}
  • Error getting token from server metadata: ServerError: invalid_target
  • Missing or invalid grant_type, code, redirect_uri, client_id, PKCE (code_verifier), or resource required

Fix it

Read the resource string your own protected resource metadata publishes, then make the authorization server agree with it — either by teaching it that identifier or by switching to the parameter it actually reads. Microsoft Entra ID's v2.0 endpoint documents no resource parameter at all and wants scope={resource}/.default; Keycloak's docs say plainly it "cannot recognize resource parameter" and route you to an Audience mapper on a client scope; Auth0 uses audience unless you turn on its Resource Parameter Compatibility Profile. If instead the parameter is missing, the client dropped it: both reference SDKs omit resource when protected resource metadata was not discovered, which is a narrower rule than the spec's, and the spec requires clients to send it "regardless of whether authorization servers support it".

Have your agent fix this

Copies a prompt that sends your coding agent to this page, then has it find and apply the right fix for your project.

The cause depends on your platform. Pick yours.

Microsoft Entra ID

If the authorization server is Microsoft Entra ID and the request fails with AADSTS901002 or AADSTS9010010

  1. 1 Start from what Entra documents, because it is less than people expect. The v2.0 authorization code reference lists the parameters /authorize and /token accept, and resource is not among them; the documented replacement is the .default suffix, and Microsoft states the mapping outright — "Using scope={resource-identifier}/.default is functionally the same as resource={resource-identifier} on the v1.0 endpoint".
  2. 2 The two error codes are not the same problem. AADSTS901002 — "The 'resource' request parameter isn't supported", which is in Microsoft's own error-code reference — is the parameter being refused outright. AADSTS9010010 — "The resource parameter provided in the request doesn't match with the requested scopes" — is Entra having parsed it and found it inconsistent with scope, and that code appears nowhere in the reference. Reports of it cluster on Microsoft's own remote MCP servers: Azure DevOps, Power BI, Fabric.
  3. 3 Where you can influence both sides, make them share one URI namespace. Microsoft's own App Service walkthrough has you publish api://<app-registration-app-id>/user_impersonation as the scope via the WEBSITE_AUTH_PRM_DEFAULT_WITH_SCOPES app setting — and the protected resource metadata resource is then your app's HTTPS URL, which shares no prefix with api://…. A filed VS Code case reports that replacing the api:// Application ID URI with the server's own HTTPS URL, so the scope becomes https://<host>/mcp/user_impersonation against a resource of https://<host>/mcp, made AADSTS9010010 stop.
  4. 4 Add the MCP server URL as an Application ID URI on the registration that represents the API (identifierUris in the manifest), path included, no trailing slash. Entra compares strings; the default api://<client-id> can never match a URL a client dialled.
  5. 5 Watch the trailing slash on both sides. Microsoft documents the trap for .default explicitly: a resource URI that ends in a slash needs the slash kept, so https://management.azure.com/ is requested as https://management.azure.com//.default — "notice the double slash!". The same character is what breaks the resource comparison from the client side.
  6. 6 Not every failure here is yours to fix. VS Code closed its own AADSTS901002 issue with "We don't include resource for Entra flows", and a VS Code maintainer has suggested "microsoft-authentication.implementation": "msal-no-broker" on an open AADSTS9010010 report — so which client build a user is on changes what goes on the wire.
JSONjson
{
  "resource": "https://mcp.contoso.com/mcp",
  "scopes_supported": ["https://mcp.contoso.com/mcp/user_impersonation"],
  "authorization_servers": [
    "https://login.microsoftonline.com/<tenant-id>/v2.0"
  ]
}
Microsoft Entra ID: scopes, permissions and .default

Auth0

If the authorization server is Auth0 and the token comes back for the wrong audience, or resource appears to be ignored

  1. 1 By default Auth0 reads audience, not resource. Its own guidance for the compatibility profile says so: when the profile is disabled Auth0 "will keep the experience as is and only use the audience parameter". An MCP client that obeys the spec sends resource and no audience, so the resulting token is not bound to your MCP server at all — which surfaces later as a rejected token rather than as invalid_target.
  2. 2 Turn on the Resource Parameter Compatibility Profile: Dashboard → Settings → Advanced → Settings, where two toggles are listed as required — "Resource Parameter Compatibility Profile" and "Include Issuer in Authorization Responses", the second being the RFC 9207 iss parameter the 2026-07-28 spec asks authorization servers to emit.
  3. 3 Then fix the identifier. Auth0's note is that "RFC 8707 requires the resource parameter to be an absolute URI; we recommend defining your resource server identifiers in URI format" — so the API's identifier has to be the MCP server's canonical URI, not a bare name.
  4. 4 Know the precedence before you debug a mixed setup: "If both the resource and audience are available, the audience will still be used." A client library that helpfully adds audience will quietly win over the resource the spec required.
  5. 5 One boundary worth planning around: with the profile on, Auth0 "will not forward the resource to upstream Identity Providers (IdPs)". If Auth0 federates to another provider that does its own audience binding, that provider will not see the indicator.
Auth0: Resource Parameter Compatibility Profile

Keycloak

If the authorization server is a Keycloak realm

  1. 1 This is documented, not broken. Keycloak's own MCP authorization server guide states it: "Keycloak cannot recognize resource parameter." A conformant MCP client will still send it, because the spec requires the parameter regardless of support, and Keycloak ignoring it is the correct OAuth behaviour for an unrecognised parameter.
  2. 2 Bind the audience through the channel Keycloak does read. The guide directs you to "use OAuth 2.0's scope parameter instead of the resource parameter", with a client scope carrying an Audience mapper whose "Included Custom Audience" is the MCP server's URL.
  3. 3 The two strings have to be identical, and the guide says which two: "the client scope's Included Custom Audience field needs to be the same as the authorization request's resource parameter value and the MCP server's URL." Publish that same string as resource in your protected resource metadata and you have one identifier in three places instead of three near-misses.
  4. 4 Because Keycloak never rejects the indicator, this configuration fails silently rather than with invalid_target — the flow completes and the token's aud is wrong. Your MCP server is the only component that can catch it, and the spec makes that its job: servers **MUST** validate that access tokens were issued specifically for them as the intended audience.
  5. 5 A realm issuer always carries a path (/realms/<realm>), which is a separate source of failure one step earlier in the flow, and worth ruling out before you touch mappers.
text
# Realm: https://keycloak.example.com/realms/mcp
#
# Client scope "mcp-server"
#   Mappers → Add mapper → By configuration → Audience
#     Included Custom Audience: https://mcp.example.com/mcp
#
# What the client sends (resource is ignored, and that is fine):
#   GET /realms/mcp/protocol/openid-connect/auth
#     ?scope=openid+mcp-server
#     &resource=https%3A%2F%2Fmcp.example.com%2Fmcp
#
# What your protected resource metadata must publish:
#   "resource": "https://mcp.example.com/mcp"
#
# Resulting token:
#   "aud": ["https://mcp.example.com/mcp"]
Keycloak: Keycloak as an MCP authorization server

MCP TypeScript SDK (client)

If the client is built on the MCP TypeScript SDK and resource is absent, or arrives with a trailing slash you did not put there

  1. 1 The SDK decides for you whether to send the parameter at all, and its rule is narrower than the spec's. selectResourceURL returns undefined when no protected resource metadata was discovered — the comment in the source is exactly that, "Only include resource parameter when Protected Resource Metadata is present" — and startAuthorization and the token exchange each set resource only when it is defined. So a discovery failure upstream reappears here as a missing indicator, and an authorization server that requires one answers invalid_target.
  2. 2 The value is serialised as resource.href on both requests, and that is where the trailing slash comes from: new URL('https://mcp.example.com').href is https://mcp.example.com/. Publish a resource with a path (https://mcp.example.com/mcp) and href is a no-op; publish an origin, and every client on this code path sends a string that differs from your metadata by one character. An open Claude Code report against Entra ID names this as the root cause of its AADSTS9010010.
  3. 3 When metadata *is* present, the SDK prefers the metadata's resource over the URL you dialled, after checking the dialled URL is same-origin with it and under its path — checkResourceAllowed. A published resource that is not a prefix of the endpoint throws Protected resource … does not match expected … rather than reaching the authorization server, which is a different error with a different fix.
  4. 4 validateResourceURL on the provider is the one hook that overrides both behaviours: implement it and whatever URL you return is used, skipping the "omit when there is no metadata" default and the prefix check. Use it to pin an exact string for a provider that is fussy about one, not to paper over metadata you should be serving.
TypeScriptts
// @modelcontextprotocol/client is the 2.x package. On stable
// @modelcontextprotocol/sdk (1.x) the same hook is on OAuthClientProvider
// under the /client/auth path.
import type { OAuthClientProvider } from '@modelcontextprotocol/client';

class PinnedResourceProvider implements OAuthClientProvider {
  // Returning a URL here replaces BOTH defaults: the "omit the parameter
  // when there is no protected resource metadata" rule, and the
  // same-origin-plus-path-prefix check against the published resource.
  // The value is sent as `.href`, so give it a path — an origin-only URL
  // gains a trailing slash on the way out.
  async validateResourceURL(_serverUrl: string | URL, _resource?: string) {
    return new URL('https://mcp.example.com/mcp');
  }

  // …redirectUrl, clientMetadata, tokens(), saveTokens(), etc.
}
MCP TypeScript SDK: OAuth clients

MCP Python SDK (client)

If the client is built on the MCP Python SDK and the authorization server says the indicator is missing

  1. 1 The gate is OAuthContext.should_include_resource_param, and it returns True only when protected resource metadata was discovered, or when an MCP-Protocol-Version of 2025-06-18 or later is available to compare against. With neither, resource is dropped from the authorization request, the token request and the refresh — three places, one condition.
  2. 2 Which makes this error a symptom more often than a cause. If your MCP server's protected resource metadata is not being found, fix that first: with the document in hand the SDK includes the parameter unconditionally and this failure goes away without you touching the client.
  3. 3 The value comes from get_resource_url, which starts at the canonical server URL and switches to the metadata's resource when the dialled URL passes check_resource_allowed against it. So the identifier on the wire is the one you published, and getting it right in the metadata is what changes it.
  4. 4 validate_resource_url on OAuthClientProvider replaces the default RFC 8707 mismatch check — it is called with (server_url, prm_resource) and raises to reject. It does not change whether the parameter is sent, and the SDK's client OAuth guide barely mentions it, so read the gate in mcp/client/auth/oauth2.py rather than the docs before you rely on either.
python
# mcp 2.x client. should_include_resource_param() is False when there is
# neither protected resource metadata nor an MCP-Protocol-Version to
# compare, and then no resource parameter is sent on /authorize, /token or
# the refresh.
from mcp.client.auth import OAuthClientProvider


async def accept_resource(server_url: str, prm_resource: str | None) -> None:
    """Replaces the default RFC 8707 mismatch check. Raise to reject."""
    if prm_resource and not prm_resource.startswith("https://mcp.example.com/"):
        raise ValueError(f"unexpected resource {prm_resource}")


provider = OAuthClientProvider(
    server_url="https://mcp.example.com/mcp",
    client_metadata=client_metadata,
    storage=storage,
    redirect_handler=redirect_handler,
    callback_handler=callback_handler,
    validate_resource_url=accept_resource,
)
MCP Python SDK: OAuth clients

Claude Code

If the client is Claude Code and Entra ID answers invalid_target on the callback

  1. 1 An open report describes the mechanism precisely: Claude Code's MCP OAuth client appends a trailing / to the resource parameter on both the /authorize redirect and the /token exchange, so a protected resource metadata resource published without one no longer matches the implicit resource of the requested scope, and Entra returns AADSTS9010010. It was filed against Microsoft's own Business Central MCP server, whose metadata publishes "resource": "https://mcp.businesscentral.dynamics.com".
  2. 2 You cannot patch the client, so change what it is normalising. Publish a resource that already has a path — https://mcp.example.com/mcp rather than https://mcp.example.com — and URL serialisation has nothing left to add.
  3. 3 Then move the scope with it, or you have traded one mismatch for another. Entra compares resource against the resource implied by scope, so scopes_supported has to sit under the same identifier: https://mcp.example.com/mcp/user_impersonation against a resource of https://mcp.example.com/mcp.
  4. 4 To confirm the client is the layer at fault rather than your metadata, capture the authorize URL it opens. The report's method works: point BROWSER at a script that logs its arguments instead of opening anything, run the connection again, and URL-decode the resource value.
JSONjson
{
  "resource": "https://mcp.example.com/mcp",
  "scopes_supported": ["https://mcp.example.com/mcp/user_impersonation"],
  "authorization_servers": [
    "https://login.microsoftonline.com/<tenant-id>/v2.0"
  ]
}
anthropics/claude-code#52871

Zuplo MCP Gateway

If your identity provider does not implement resource indicators and you would rather not have every MCP client negotiate that

  1. 1 For its MCP routes the gateway is both the OAuth 2.1 resource server and the authorization server, so it is the component that honours the indicator. It builds the canonical resource URI from the request origin and the route path, publishes it as resource in the RFC 9728 document at /.well-known/oauth-protected-resource/{routePath}, mints tokens against it, and then validates that incoming bearer tokens were minted for that route's canonical resource URI.
  2. 2 Which is what removes the negotiation. mcp-oauth-inbound delegates browser login to an OIDC provider you configure and then issues the gateway's own bearer token, so whether your provider reads resource, audience or neither is a question between the gateway and the provider, not between the provider and every MCP client.
  3. 3 Per-route binding is the point, and the docs state the consequence plainly: a token issued for /mcp/linear-v1 cannot be reused on /mcp/stripe-v1. Two routes, two audiences, without two app registrations.
  4. 4 The caveat to know before you rely on it: the canonical resource URI is derived from the incoming request origin. Front the gateway with a proxy that rewrites the host and it will publish and validate against an identifier that is not the one clients dialled — the same rejection, one layer further along.
Zuplo MCP Gateway: authorization

Something else

Whatever the components are, there are only two strings and they have to be the same one. Change what the client sends, so the indicator matches an identifier the authorization server already knows. Or change what the authorization server knows, by registering that identifier — or by binding the audience through the parameter it does read, which for most providers today is scope or audience rather than resource. Decide which with the curl above, and compare the two strings character by character rather than by eye: a trailing slash, an api:// prefix where an https:// one was published, or a path dropped from an endpoint URL are each enough. Do not treat a provider that ignores the parameter as broken — RFC 8707 makes resource optional for clients to send and optional for servers to require, and the 2026-07-28 spec puts the obligation on the client. What is never optional is the check at the other end: MCP servers **MUST** validate that access tokens were issued specifically for them as the intended audience, and no request-side arrangement substitutes for that.

Why it happens

The resource parameter is what stops a token minted for one MCP server from working at another, and the 2026-07-28 spec is unusually firm about it: MCP clients **MUST** implement RFC 8707, the parameter **MUST** be included in both the authorization request and the token request, and clients **MUST** send it "regardless of whether authorization servers support it". RFC 8707 itself is looser — an authorization server that cannot parse a value, or does not consider the resource acceptable, rejects the request with invalid_target ("The requested resource is invalid, missing, unknown, or malformed"), and it **MAY** also require the parameter and fail requests that omit it with the same code. So one error code covers three different mistakes. The value is wrong: usually by a single character, because URL serialisation adds a trailing slash to an origin-only resource and RFC 8707 comparison is exact — the MCP TypeScript SDK sends resource.href, and new URL('https://mcp.example.com').href is https://mcp.example.com/, which is the root cause identified in an open Claude Code report against Entra ID. The value is right but the provider reads a different parameter: Entra ID's v2.0 protocol reference lists no resource parameter, and reports against it split between AADSTS901002 ("The 'resource' request parameter isn't supported", which is in Microsoft's own error-code reference) and AADSTS9010010 ("doesn't match with the requested scopes", which is not). Or the parameter never left the client: both reference SDKs gate it on having found protected resource metadata first, so a discovery hiccup upstream turns into invalid_target here. Nothing in the handshake warns you which case you are in, because — as the open spec issue asking for the requirement to be relaxed points out — neither RFC 8414 nor OpenID Connect Discovery defines a metadata field that advertises RFC 8707 support, so a client cannot learn from .well-known whether to send the parameter.

The exchange

Each step in the exchange, in order.

  • MCP client
  • MCP server resource server
  • Authorization server
  1. The client does not invent this identifier. It comes out of your own metadata document, which is why a value nobody at the authorization server has heard of is a configuration mismatch rather than a client bug.

    MCP client to MCP server

    GET …/oauth-protected-resource/mcp the URL the 401 challenge named
  2. MCP server to MCP client

    200 OK "resource": "https://mcp.example.com"
  3. Two things go wrong here and produce the same error code. The string can arrive altered — a trailing slash is enough, because the comparison is exact. Or it can not arrive at all, because a client that never found the metadata document above omits the parameter entirely.

    Inside MCP client

    Normalise it into a resource indicator resource=https://mcp.example.com/
  4. MCP client to Authorization server

    POST /token code=…&resource=https%3A%2F%2Fmcp.example.com%2F
  5. Authorization server to MCP client

    400 Bad Request error="invalid_target"

    Flow stops here

The MCP client fetches the server's protected resource metadata and reads the resource identifier out of it. It normalises that string into a resource indicator, gaining a trailing slash because the published value had no path, and sends the result as the resource parameter on the token request. The authorization server does not recognise that identifier and answers 400 with error invalid_target, so no access token is ever issued and the MCP server is never contacted with a credential.
Zuplo

What lands on you next

A resource indicator settles which server a token is for. It says nothing about who is holding it, or what they are allowed to do once they arrive.

The token is bound to this route. Which of our tools may it call?

mcp-capability-filter-inbound

A static per-route allow-list of capabilities. Anything not on it is refused with MethodNotFound before the request reaches your server.

Same route, two kinds of user. How do we split what each may do?

require-user-claims-inbound

Per-user decisions come from claims on the validated token. The capability filter has no per-user axis, so this is a different policy rather than a setting on that one.

The user consented. What credential reaches the API behind us?

mcp-token-exchange-inbound

Not the client's token — the spec forbids passthrough with a MUST NOT. The inbound credential is dropped and an upstream one is minted per user or per gateway.

Something is being turned away. What was it, and why?

mcp_request_rejected

Each refused request emits its own event with a named reason, so a rejection resolves to an outcome you can query rather than a 401 in an access log.

All of these attach to one MCP route's policies.inbound — the same policy engine on the way in and on the way out.

Stop debugging auth on every MCP server

A gateway in front of your MCP servers handles discovery, audience binding, and token validation once — for every server and every client.