---
title: "Resource Indicator Is Missing, or Unknown (invalid_target)"
description: "The authorization server returns invalid_target because the RFC 8707 resource parameter is missing, unknown or mismatched. The fix per IdP, SDK and client."
canonicalUrl: "https://zuplo.com/learn/mcp/errors/resource-indicator-missing-or-unknown"
pageType: "mcp-error"
errorString: "resource indicator is missing, or unknown"
lastVerified: "2026-07-30"
specVersion: "2026-07-28"
---

# `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.

```bash
# 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.

**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. If this is what you see, this is not your problem.

**Clients also report this as:**

- `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

**In short.** 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".

The symptom is identical across platforms; the cause is not.

### Microsoft Entra ID

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

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. 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. 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. 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. 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. 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.

```json
{
  "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](https://learn.microsoft.com/en-us/entra/identity-platform/scopes-oidc)

### Auth0

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

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. 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. 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. 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. 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](https://auth0.com/ai/docs/mcp/guides/resource-param-compatibility-profile)

### Keycloak

If the authorization server is a Keycloak realm

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. 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. 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. 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. 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](https://www.keycloak.org/securing-apps/mcp-authz-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. 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. 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. 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. `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.

```ts
// @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](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/clients/oauth.md)

### MCP Python SDK (client)

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

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. 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. 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. `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](https://github.com/modelcontextprotocol/python-sdk/blob/main/docs/client/oauth-clients.md)

### Claude Code

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

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. 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. 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. 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.

```json
{
  "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](https://github.com/anthropics/claude-code/issues/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. 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. 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. 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. 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](/docs/mcp-gateway/auth/overview)

### 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

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.

**Participants:** MCP client, MCP server (resource server), Authorization server

1. **MCP client → MCP server** — `GET …/oauth-protected-resource/mcp` — `the URL the 401 challenge named`
2. **MCP server → MCP client** — `200 OK` — `"resource": "https://mcp.example.com"` _(response)_
3. **MCP client** (internal step) — Normalise it into a resource indicator — `resource=https://mcp.example.com/`
4. **MCP client → Authorization server** — `POST /token` — `code=…&resource=https%3A%2F%2Fmcp.example.com%2F` _(carries the credential)_
5. **Authorization server → MCP client** — `400 Bad Request` — `error="invalid_target"` _(response; this step fails — the flow stops here)_

Notes:

- **Steps 1–2** — 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.
- **Steps 3–5** — 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.

## 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. See [Zuplo MCP Gateway](/mcp-gateway).

## Keep reading

- **Prevent it** — [OAuth 2.1 Authentication for MCP Servers](/learn/mcp/authentication/oauth-2-1) — Servers that must be reachable by MCP clients you do not control, or listed in a connector directory
- **Similar error** — [`Authorization with the MCP server failed`](/learn/mcp/errors/oauth-completes-but-no-token-sent) — A token was issued and the client says it is connected, but the credential never reaches your server intact: either the client does not attach it, or it carries an audience your server is right to reject.
- **Similar error** — [`Failed to fetch authorization server metadata from all attempted URLs`](/learn/mcp/errors/failed-to-fetch-authorization-server-metadata) — The client knows which authorization server it is supposed to use, but none of the well-known URLs it is required to try returned a metadata document — so it never learns where `/authorize` and `/token` are, and the flow stops one step short of a login.
- **Similar error** — [`Resource server does not implement OAuth 2.0 Protected Resource Metadata.`](/learn/mcp/errors/401-without-www-authenticate) — Your server answers 401 correctly, but the response carries no pointer to protected resource metadata — so the client has nothing to discover the authorization server with, and the OAuth flow never starts.
- **Error reference** — [every documented MCP error](/learn/mcp/errors)
