Skip to content

docs: add auth0-ai-mcp guides - #173

Open
kishore7snehil wants to merge 5 commits into
feat/auth0-ai-mcp-examplefrom
docs/auth0-ai-mcp-guides
Open

kishore7snehil wants to merge 5 commits into
feat/auth0-ai-mcp-examplefrom
docs/auth0-ai-mcp-guides

Conversation

@kishore7snehil

@kishore7snehil kishore7snehil commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📋 Changes

Adds the guides for auth0-ai-mcp. The package README now links to them, and the links to these guides in the package and example READMEs from the earlier PRs resolve once this lands.

📖 Documentation

  • docs/ has one guide each for configuration, token verification, request context, scope enforcement, On-Behalf-Of, Token Vault, token caching, organization policy and writing an adapter.
  • TokenVerification.md covers the whitespace bearer check and no-store on 401 and 503.
  • EXAMPLES.md points to the runnable b2b_ecommerce server.
  • README.md gets a Documentation section and links from each feature to its guide.
  • CLAUDE.md, AGENTS.md and references/ give coding agents the commands, code style, testing and git workflow for the package.

🧪 Testing

Documentation, plus tests/test_docs.py, which checks that the Python snippets in the README, EXAMPLES and guides parse and that their imports resolve. All relative links resolve.

  • This change adds unit test coverage
  • This change adds integration test coverage
  • This change has been tested on the latest version of the platform/language or why not

Contributor Checklist

@kishore7snehil
kishore7snehil marked this pull request as ready for review October 30, 2026 08:17
@kishore7snehil
kishore7snehil requested a review from a team as a code owner October 30, 2026 08:17

### Passing a prebuilt client

`ApiClient` comes from [`auth0-api-python`](https://github.com/auth0/auth0-api-python), the library `auth0-ai-mcp` is built on. Build one yourself when you need an option `create_mcp_client` does not expose, such as multi-domain mode or DPoP settings.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried ApiClientOptions(dpop_required=True) passed through api_client. The middleware still accepts a plain Bearer token, and also a token that has cnf.jkt sent as Bearer, and that token can be exchanged. The middleware calls verify_access_token, so the DPoP options have no effect.

Can we remove 'or DPoP settings' here, and add a warning in TokenVerification.md that DPoP is not enforced? I have raised the code side in the first PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A token with a cnf claim is now rejected with a 401, even when it is sent as Bearer. TokenVerification.md says so and warns that DPoP is not supported, and the DPoP wording is gone from Configuration.md.

Fixed in dff43fb.

```python
from auth0_ai_mcp import with_required_scopes

@with_required_scopes(["read:orders", "read:payments"])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This snippet shows only the scope decorator. I tried both orders with @mcp.tool(). With @mcp.tool() on top, a token without the scope gets a tool error. With it below, the registered function is the unguarded one, so the body runs and the check is silently skipped.

Can we show @mcp.tool() as the first line here, in EXAMPLES.md and in the README, with one sentence about the order?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dff43fb.

- `InsufficientScopeError` - caller is authenticated but is missing one or more required scopes.
- `ConfigurationError` - the `scopes` argument is empty or malformed.

These errors are raised inside the tool, so the MCP framework reports them as a tool error and the HTTP response is still successful. Each scope denial is logged at INFO.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add what the caller and the logs see? With MCP SDK 2.x the client gets only Error executing tool X at HTTP 200, so the model cannot tell it is a scope problem, and there is no insufficient_scope response to step up on. Each denial also writes an ERROR traceback in the server log.

One or two sentences here will save debugging time. I have asked about raising ToolError in the first PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scope errors now extend the SDK ToolError, so the client sees Error executing tool X: Missing one or more required scopes. and the model can tell it is a scope problem. The server log gets an INFO line. There is still no insufficient_scope response to step up on, and the page now says that. I only checked this on mcp 2.3.

Fixed in dff43fb.


## Tenant-side setup: scopes must be in the client-grant

`with_required_scopes` checks `principal.scopes`, which comes from the incoming token. Scope checks read only the `scope` claim and never the RBAC `permissions` claim. Auth0 only includes a scope in a token if two things are true: the scope is registered on the API (resource server), and the MCP client's client-grant explicitly lists it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can see there is a third condition. For a third party client the grant is only a ceiling, so the scope must also be requested at login. If the authorize request has no scope, the token has no API scopes and nothing is reported as an error.

The README quick start passes no scopes_supported, so a server built from it will see exactly this. Can we list 'requested' as the third condition here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dff43fb.


## Decorator form

`with_required_scopes(scopes)` is a decorator for async tool handlers. It reads the verified caller from the request context and checks the scopes before the tool body runs. `scopes` is one scope as a string or several as a list.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The matching here is exact: a token with tool:admin does not satisfy tool:read. The 2026 revision of the MCP authorization spec says servers must account for scope hierarchies, where a broader scope implies narrower ones.

It fails closed, so I do not think any code is needed. Please say here that matching is exact, and that a hierarchy needs both scopes in the token or a check inside the tool.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dff43fb.

```

> [!WARNING]
> The MCP SDK turns on its Host and Origin checks only when the app is bound to localhost, so keep `127.0.0.1` for local use. Bind another interface only behind a trusted proxy or after configuring the allowed hosts.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rule is a bit different. The MCP SDK turns these checks on when streamable_http_app(host=...) is 127.0.0.1, localhost or ::1, and that is also the default. I tried the default with Host: your-mcp-server and got 421 'Invalid Host header'. With host="0.0.0.0" the checks are off.

Can we state this rule and show transport_security=TransportSecuritySettings(allowed_hosts=[...]) for a public URL? EXAMPLES.md uses the default call, so a pointer there also helps.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dff43fb.

)
```

See [Writing an Adapter](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/docs/WritingAnAdapter.md) for other frameworks and for the native FastMCP adapter package `auth0-ai-fastmcp`. For a complete runnable server, see [`examples/b2b_ecommerce`](https://github.com/auth0/auth0-ai-python/tree/main/packages/auth0-ai-mcp/examples/b2b_ecommerce).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could not find auth0-ai-fastmcp on PyPI or in this repo, and I did not see a PR for it. This line and WritingAnAdapter.md line 25 tell FastMCP users to use it, so they have nothing to install today.

Can we remove the sentence until the package is released, or say clearly that it is planned?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would like to keep this mention. auth0-ai-fastmcp is the FastMCP adapter for this package and it is planned to join this repo, so these two lines will not need to change when it does.

Until then it cannot be installed, so we can say clearly that it is planned if you prefer. WDYT?

)
```

- `get_caller_principal() -> Principal | None` - returns the verified `Principal` for the current request, or `None` when called outside a request (for example in a background task or test that has not set up a context).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked this. A task started inside a request with asyncio.create_task still saw the principal 0.2 seconds after the response, and the same for asyncio.to_thread. Only a plain thread or startup code sees None.

Can we use that as the example? A line that such a task keeps the caller's token would also help, as the exchange fails once the token expires.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dff43fb.

@@ -0,0 +1,40 @@
# Commands

Run these from the `auth0-ai-mcp` directory. They match `.github/workflows/test.yml` (Poetry 2.2.1, Python 3.10 to 3.12) and `CONTRIBUTING.md`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This says Poetry 2.2.1, but test.yml installs 2.4.1. A number copied here will go stale again.

Can we drop the version, or point to the workflow file?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dff43fb.


## Conventions

- `test_client.py` and `test_principal.py` use module-level `test_<subject>_<condition>` functions. `test_scopes.py` groups tests in `TestRequireScopes` and `TestWithRequiredScopes`. Follow the style of the file you are adding to.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_scopes.py has no TestRequireScopes or TestWithRequiredScopes class. All the tests there are module level functions.

Line 31 also says tests use bind_request_context, but only test_request_context.py does. test_scopes.py uses set_request_context and reset_request_context.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in dff43fb.

@kishore7snehil
kishore7snehil force-pushed the feat/auth0-ai-mcp-example branch from 2ce0bb3 to 564707a Compare November 2, 2026 16:25
@kishore7snehil
kishore7snehil force-pushed the docs/auth0-ai-mcp-guides branch from 205e032 to dff43fb Compare November 2, 2026 17:07
@kishore7snehil
kishore7snehil force-pushed the feat/auth0-ai-mcp-example branch from 564707a to c7085b6 Compare November 4, 2026 06:30
@kishore7snehil
kishore7snehil force-pushed the docs/auth0-ai-mcp-guides branch from 5538f83 to 49aa270 Compare November 4, 2026 06:30
@kishore7snehil
kishore7snehil force-pushed the feat/auth0-ai-mcp-example branch from c7085b6 to b9c4335 Compare November 4, 2026 08:31
@kishore7snehil
kishore7snehil force-pushed the docs/auth0-ai-mcp-guides branch from 49aa270 to ce2c3b2 Compare November 4, 2026 08:32
@kishore7snehil kishore7snehil mentioned this pull request Nov 4, 2026
3 of 5 tasks
@kishore7snehil
kishore7snehil force-pushed the docs/auth0-ai-mcp-guides branch from ce2c3b2 to 109c402 Compare November 5, 2026 14:06
@kishore7snehil
kishore7snehil force-pushed the feat/auth0-ai-mcp-example branch from b9c4335 to a51a447 Compare November 5, 2026 14:07
@kishore7snehil
kishore7snehil force-pushed the docs/auth0-ai-mcp-guides branch from 109c402 to 73155b4 Compare November 5, 2026 15:02
- Document the whitespace bearer rejection and the no-store header on 401 and 503.
- Document that scopes_supported must be a list of single-value strings.
- Document the separate expiry messages for exchanged tokens.
- Add tests/test_docs.py, which checks that the Python snippets in the docs parse and their imports resolve.
@kishore7snehil
kishore7snehil force-pushed the docs/auth0-ai-mcp-guides branch from 73155b4 to de6ffca Compare November 5, 2026 15:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants