Zuplo
On this page
MCP error

No token issued for an unknown resource

The string your client printed
resource indicator is missing, or unknown

Your authorization server refused to bind a token to your MCP server — the resource value it was handed names something it does not recognize, or the client sent none and this server requires one — so no token is minted and nothing ever reaches your MCP server.

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 prove nothing on their own: 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. A provider that lists no resource in its documented parameters and ignores yours also lands here: RFC 6749 requires an authorization server to ignore unrecognized request parameters, so silence is a legitimate outcome and the audience check still has to happen at your MCP server.
Then this isn't 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 teach it that identifier, or switch to the parameter it actually reads, which for Microsoft Entra ID, Keycloak, and Auth0 is scope or audience rather than resource. If the parameter is missing instead, the client dropped it — both reference SDKs omit resource when protected resource metadata was not discovered, which is a narrower rule than the specification's.

Have your agent fix this

Copy a prompt that sends your coding agent to this page to 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, which 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 Tell the two error codes apart, because they are different problems. 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 dialed.
  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 Check the client build before you keep digging: 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 Expect Auth0 to read audience, not resource, by default. 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 specification 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 specification 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 Learn 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 specification required.
  5. 5 Plan around one boundary: 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 Read this as 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 specification requires the parameter regardless of support, and Keycloak ignoring it is the correct OAuth behavior for an unrecognized 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 Expect this to fail silently rather than with invalid_target, because Keycloak never rejects the indicator: the flow completes and the token's aud is wrong. Your MCP server is the only component that can catch it, and the specification makes that its job: servers MUST validate that access tokens were issued specifically for them as the intended audience.
  5. 5 Rule out the realm path before you touch mappers. A realm issuer always carries a path (/realms/{realm-name}), which is a separate source of failure one step earlier in the flow.
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 Check whether the SDK is sending the parameter at all, because its rule is narrower than the specification'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 serialized 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, expect the metadata's resource to win over the URL you dialed, after checkResourceAllowed confirms the dialed URL is same-origin with it and under its path. 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 behaviors: 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 are supposed to serve.
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 Read the gate, OAuthContext.should_include_resource_param. 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 Treat this error as a symptom more often than a cause. If your MCP server's protected resource metadata isn't 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 Change the identifier in your metadata, not in the client. The value comes from get_resource_url, which starts at the canonical server URL and switches to the metadata's resource when the dialed 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 Read the gate in mcp/client/auth/oauth2.py rather than the docs before you rely on the override. validate_resource_url on OAuthClientProvider replaces the default RFC 8707 mismatch check — it is called with (server_url, prm_resource) and raises to reject — but it doesn't change whether the parameter is sent, and the SDK's client OAuth guide barely mentions it.
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 Start from the mechanism an open report describes 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 can't patch the client, so change what it normalizes. Publish a resource that already has a path — https://mcp.example.com/mcp rather than https://mcp.example.com — and URL serialization 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 doesn't implement resource indicators and you'd rather not have every MCP client negotiate that

  1. 1 Let the gateway honor the indicator. For its MCP routes it is both the OAuth 2.1 resource server and the authorization server: 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 That 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 Expect per-route binding, which 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 Check one caveat before you rely on it: the gateway derives the canonical resource URI from the incoming request origin. Front it with a proxy that rewrites the host and it publishes and validates against an identifier that is not the one clients dialed — the same rejection, one layer further along.
Zuplo MCP Gateway: authorization

Something else

There are only two strings here, 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. Compare the two strings character by character: a trailing slash, an api:// prefix where an https:// one was published, or a path dropped from an endpoint URL are each enough. Don't 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 specification 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 stops a token minted for one MCP server from working at another. The 2026-07-28 specification requires 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 is looser. An authorization server that cannot parse the value, or does not accept the resource, rejects the request with invalid_target ("The requested resource is invalid, missing, unknown, or malformed"), and it MAY require the parameter and fail requests that omit it with the same code. So one error code covers three mistakes.

The value is wrong, usually by one character. RFC 8707 comparison is exact and URL serialization adds a trailing slash to an origin-only resource: the MCP TypeScript SDK sends resource.href, and new URL('https://mcp.example.com').href is https://mcp.example.com/. An open Claude Code report against Entra ID names that as its root cause.

The value is right but the provider reads a different parameter. Entra ID's v2.0 protocol reference lists no resource parameter, and reports 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 tells you which case you are in. As the open specification issue asking to relax the requirement points out, neither RFC 8414 nor OpenID Connect Discovery defines a metadata field advertising RFC 8707 support, so a client cannot learn from .well-known whether to send the parameter.

The exchange

  • 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 that metadata document omits the parameter entirely.

    Inside MCP client

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

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

    400 Bad Request (this step fails) 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 normalizes 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 recognize 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

A resource indicator settles which server a token is for, and nothing about who is holding it.

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

mcp-capability-filter-inbound

Just the ones on the route's allowlist — anything else is refused with MethodNotFound before the request reaches your server.

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

require-user-claims-inbound

Two jobs, both driven by claims on the validated token. This policy decides whether the caller reaches the route at all, answering 403; the capability filter, in rolesAndGroups mode, decides which tools they see once they are through it.

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

mcp-token-exchange-inbound

Not the client's token — the specification 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. Your MCP server keeps the code and the authentication it has today.

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.