Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/auth0-ai-mcp/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# AI Agent Guidelines for auth0-ai-mcp

@./CLAUDE.md for all coding guidelines, commands, project structure, code style, testing conventions, and boundaries.
104 changes: 104 additions & 0 deletions packages/auth0-ai-mcp/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# AI Agent Guidelines for auth0-ai-mcp

## Your Role

You are a Python SDK engineer on auth0-ai-mcp, the async library MCP servers use to verify Auth0 access tokens, enforce scopes, and exchange the caller's token for downstream API tokens. It is built on auth0-api-python and owns no MCP framework.

---

## Working Principles

- **Think before coding.** State assumptions. When a request is ambiguous, surface the interpretations and ask before building.
- **Simplicity first.** Write the minimum code that solves the stated problem. No speculative options, single-use abstractions, or error handling for impossible states.
- **Surgical changes.** Every changed line traces to the request. Clean up imports your change orphaned, leave pre-existing dead code alone.
- **Verify before done.** Turn the request into a checkable criterion (e.g. "reject X" becomes a failing test for X, then a fix) and run it before claiming success.

---

## Project Structure

```
src/auth0_ai_mcp/
__init__.py # Public API: everything in __all__ is the supported surface
client.py # McpServerClient (verify, OBO, client credentials, Token Vault) and create_mcp_client
middleware.py # Auth0AsgiMiddleware and build_protected_resource_metadata. Not in __all__, import from auth0_ai_mcp.middleware
principal.py # Principal dataclass and build_principal (normalizes verified claims)
request_context.py # ContextVar request state and the get_caller_* accessors
request_scoped_client.py # RequestScopedMcpClient: the client bound to one request's verified token
scopes.py # require_scopes and the with_required_scopes decorator
errors.py # Errors owned by this package, all extend BaseAuthError
utils/_internal/ # Private helpers by topic (exchange, scope_input, urls, headers, mcp_session). A public helper is a plain-named file in utils/
tests/
test_client.py, test_middleware.py, test_principal.py, test_request_context.py, test_scopes.py, test_docs.py
docs/ # Hand-written guides, linked from README (see references/docs-update.md)
examples/b2b_ecommerce/ # Runnable server (server.py, tools.py, token_store.py) with its own README
README.md, EXAMPLES.md
pyproject.toml # Poetry manifest and source of truth for dependencies
.version, .shiprc # Release version source (.shiprc keeps .version and pyproject.toml in sync)
.ruff.toml # Lint config (CI-enforced)
```

Token verification, the OBO exchange, caching, and token stores live in auth0-api-python. This package wraps them and does not reimplement them. Before changing a flow, read its row in [references/flow-map.md](references/flow-map.md) for entry points, supporting helpers, tests, and docs.

---

## Boundaries

### Always Do

- Run `poetry run pytest` and `poetry run ruff check .` before committing (both run in CI, see the Python matrix in `.github/workflows/test.yml`).
- Make surgical changes: touch only what the request requires. Do not refactor or reformat adjacent code.
- Follow existing style and naming. Private helpers go in `utils/_internal/` and public modules call them through the module, never by imported name. Never reorder existing declarations.
- Add unit tests for new functionality. Async tests need no marker because `asyncio_mode = "auto"` is set. Stand in for the core client with a `Stub...` class, as `tests/test_client.py` does (no real network calls).
- Raise typed errors from `errors.py` (subclasses of `BaseAuthError` with `get_status_code()` / `get_error_code()`). Let the core SDK's own errors propagate unchanged. Never return `None` or a bare `dict` to signal failure.
- Export any new public symbol through `__init__.py` `__all__`.
- Update `README.md`, `EXAMPLES.md`, and the matching `docs/*.md` in the same PR when changing the public API or supported integration patterns (see [references/docs-update.md](references/docs-update.md)).
- Keep code compatible with Python 3.10 (`target-version = "py310"`).
- Put imports at the top of the module. The `PLC0415` lint rule rejects imports inside functions, and `TYPE_CHECKING` blocks are the place for typing-only imports.

### Ask First

- Any breaking change to a version that has already shipped. Stop and ask the maintainer before writing a renamed or removed public symbol, a changed signature or default, a new required option, or a raised Python floor.
- Changing an existing error's `get_error_code()` string or `get_status_code()`. Integrators branch on these, so treat them as public API.
- Adding or bumping dependencies (`pyproject.toml`, `poetry.lock`), including the `auth0-api-python` pin.
- Changes to token verification, the principal built from claims, scope matching, or the downscope check.
- Changes to CI/CD configuration (`.github/workflows/`, `.github/actions/`), `.ruff.toml` rule selection, or `.snyk` suppressions.

### Never Do

- Commit secrets, client secrets, real tokens, or private keys. Examples read tenant values from environment variables.
- Hand-edit `poetry.lock` or `CHANGELOG.md`, or edit `.version` / the `pyproject.toml` version (the release flow owns them via `.shiprc`).
- Remove or skip a failing test instead of fixing it, or weaken an assertion to make it pass.
- Fail open. A verification, scope, exchange, or missing-context failure must raise a typed error or return a `401` challenge, never fall through to a permissive default.
- Hand the raw incoming token to tool code. `RequestScopedMcpClient` closes over it on purpose.
- Add a cache or token store to this package. Caching belongs to the core SDK and this package only passes the `token_store` through.
- Run `ruff format .` repo-wide. It is not a CI gate and the tree is not format-clean (see [references/pitfalls.md](references/pitfalls.md)).
- Modify `.venv*/`, `dist/`, `__pycache__/`, `.coverage`, `coverage.xml`.
- Log, print, or include access tokens, exchanged tokens, client secrets, or token endpoint response bodies in error messages.
- Break backward compatibility without asking first and getting explicit approval.

---

## Security Considerations

- **Verification is delegated:** every token goes through `McpServerClient.verify_access_token`, which calls the core SDK. Never decode a token yourself, and only pass already-verified claims to `build_principal`.
- **Fail closed in the middleware:** a missing, blank or non-Bearer token gets a `401` with a bare challenge, and a token that fails verification (including a claim set `build_principal` rejects) or has whitespace inside it gets a `401` with `invalid_token`. Both challenges carry `scope` when `scopes_supported` is set, and every `401` and `503` has `Cache-Control: no-store`. If Auth0 cannot be reached (`AuthServerUnavailableError`) the answer is a `503` with no challenge. WebSocket connections are closed and `OPTIONS` passes through without a token. The tool never runs, so keep it that way for any new rejection path.
- **Scope checks need every scope:** `require_scopes` raises `InsufficientScopeError` if any required scope is missing and `UnauthenticatedError` when there is no principal. `with_required_scopes` reads the principal from the request context, so a call outside a request is rejected.
- **Downscope detection:** `exchange.reject_if_downscoped` raises `DownscopedTokenError` when the exchange grants fewer scopes than were requested, for both On-Behalf-Of and client credentials but not Token Vault. An absent `scope` field in the response means the grant matched the request, so only a present field is compared.
- **Expired results are rejected:** after every exchange `exchange.reject_if_expired` raises the flow's own error when `expires_at` is missing or not in the future, so tool code never gets an already-expired token. Any leeway for clock skew belongs in the core SDK.
- **Generic error messages:** `InsufficientScopeError` and `DownscopedTokenError` deliberately do not name the scopes, and tests assert it. The invalid-token challenge uses the fixed description "The access token is not valid" and the real reason only goes to the log, so keep verification messages free of token contents.
- **Token stores:** a store is the integrator's `AbstractTokenStore`, and the base class takes a secret of at least 32 characters and provides `encrypt` and `decrypt` helpers that the store has to call itself in `set` and `get`. Core treats a failing store read as a cache miss and a failed write as non-fatal, so a failing store must not turn into an authorization decision.
- **Request context is per request:** `RequestContext` is a `ContextVar` set and reset around each request in `Auth0AsgiMiddleware`. Always reset it in a `finally`, otherwise one caller's identity can leak into the next request.
- **Dependency scanning:** Snyk (`sca_scan.yml`), CodeQL (`codeql.yml`), OpenSSF Scorecard (`scorecard.yml`), and ruff `S` (bandit) rules in CI.

---

## Reference Files

- Commands for install, test, lint, and build: [references/commands.md](references/commands.md)
- Test layout, async mode, and stubbing the core client: [references/testing.md](references/testing.md)
- Naming, the error pattern, and the comment rules: [references/code-style.md](references/code-style.md)
- Entry points, helpers, tests, and docs for each flow: [references/flow-map.md](references/flow-map.md)
- Branches, commits, PR template, CI, and releases: [references/git-workflow.md](references/git-workflow.md)
- Traps that cost time: [references/pitfalls.md](references/pitfalls.md)
- Which docs to update for which code change: [references/docs-update.md](references/docs-update.md)
70 changes: 70 additions & 0 deletions packages/auth0-ai-mcp/EXAMPLES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Examples

This page walks through a complete Auth0-secured MCP server using the ASGI adapter shipped with `auth0-ai-mcp`. The goal is to show the full wiring from client construction to a gated tool that mints a downstream token, so you can see what the library handles and what stays in your code.

- [Building the client](#building-the-client)
- [Wiring the ASGI adapter](#wiring-the-asgi-adapter)
- [Writing a gated tool](#writing-a-gated-tool)
- [Complete example - b2b_ecommerce](#complete-example---b2b_ecommerce)

## Building the client

```python
from auth0_ai_mcp import create_mcp_client

client = create_mcp_client(
domain="your-tenant.auth0.com",
audience="https://your-mcp-server/mcp",
client_id="YOUR_CLIENT_ID",
client_secret="YOUR_CLIENT_SECRET",
)
```

Every client option is described in [Configuration](./docs/Configuration.md).

## Wiring the ASGI adapter

```python
from mcp.server.mcpserver import MCPServer
from auth0_ai_mcp.middleware import Auth0AsgiMiddleware

mcp = MCPServer(name="My Server")

secured_app = Auth0AsgiMiddleware(
mcp.streamable_http_app(),
mcp_client=client,
scopes_supported=["read:weather"],
resource_name="My Server",
resource_documentation="https://your-mcp-server/docs",
)
```

From this point on, the library verifies every incoming token, rejects bad ones with a `401` before any tool runs, and serves the protected resource metadata. For a public URL, pass `transport_security` as shown in [Writing an Adapter](./docs/WritingAnAdapter.md#ready-made-integrations), or the `mcp` SDK answers every request with a `421`. See [Token Verification](./docs/TokenVerification.md) and [Configuration](./docs/Configuration.md).

## Writing a gated tool

```python
from auth0_ai_mcp import require_caller_mcp_client, with_required_scopes

@mcp.tool()
@with_required_scopes("read:weather")
async def get_weather(location: str) -> dict:
auth0 = require_caller_mcp_client()
token = await auth0.get_token_on_behalf_of(
audience="https://weather-api/",
scope="read:weather",
)
# Use token["access_token"] in your downstream HTTP call.
...
```

Keep `@mcp.tool()` as the top decorator, see [Scope Enforcement](./docs/ScopeEnforcement.md#decorator-form). For how scope failures are reported, see [Scope Enforcement](./docs/ScopeEnforcement.md#errors).

For how `require_caller_mcp_client` and the caller's token work, see [Request Context](./docs/RequestContext.md#requestscopedmcpclient).

## Complete example - b2b_ecommerce

The [`examples/b2b_ecommerce`](./examples/b2b_ecommerce/) directory contains a runnable server whose flagship `cancel_and_refund_order` tool touches two downstream services (orders and payments) in one call, minting a separate scoped token for each via On-Behalf-Of. It also shows:

- An in-memory `token_store` passed to `create_mcp_client` for caching OBO tokens, and `scope_matching` driven by an environment variable to switch between matching modes at deploy time. See [Token Caching](./docs/TokenCaching.md) for what a production store needs and the available modes.
- Organization policy configured to accept tokens only from a specific organization. The policy is optional and defaults to `"allow"`. See [Organization Policy](./docs/OrganizationPolicy.md).
35 changes: 25 additions & 10 deletions packages/auth0-ai-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ An MCP server is just another resource server. This library ships no MCP framewo

![Release](https://img.shields.io/pypi/v/auth0-ai-mcp) ![Downloads](https://img.shields.io/pypi/dw/auth0-ai-mcp) [![License](https://img.shields.io/:license-Apache--2.0-blue.svg?style=flat)](https://opensource.org/licenses/Apache-2.0)

🚀 [Getting Started](#getting-started) - 💬 [Feedback](#feedback)
📚 [Documentation](#documentation) - 🚀 [Getting Started](#getting-started) - 💬 [Feedback](#feedback)

## How it works

1. **Verify** - every incoming bearer token is checked before the MCP handler runs.
2. **Context** - the verified caller is available to tool code.
3. **Scopes** - tools can require scopes on the caller's token.
4. **On-Behalf-Of** - exchange the caller's token for a downstream token.
5. **Token Vault** - exchange the caller's token for a federated provider token.
6. **Client Credentials** - get a token for the server itself.
1. **Verify** - every incoming bearer token is checked before the MCP handler runs. See [Token Verification](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/docs/TokenVerification.md).
2. **Context** - the verified caller is available to tool code. See [Request Context](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/docs/RequestContext.md).
3. **Scopes** - tools can require scopes on the caller's token. See [Scope Enforcement](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/docs/ScopeEnforcement.md).
4. **On-Behalf-Of** - exchange the caller's token for a downstream token. See [On-Behalf-Of](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/docs/OnBehalfOf.md).
5. **Token Vault** - exchange the caller's token for a federated provider token. See [Token Vault](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/docs/TokenVault.md).
6. **Client Credentials** - get a token for the server itself. See [On-Behalf-Of](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/docs/OnBehalfOf.md#server-identity-calls-client-credentials).

## Getting Started

Expand Down Expand Up @@ -53,7 +53,7 @@ client = create_mcp_client(
> [!IMPORTANT]
> `audience` must be the public URL of your MCP server. It is also the API identifier you register in Auth0, so a token issued for any other value is rejected with a `401`.

`client_id` and `client_secret` are only needed for token exchanges.
`client_id` and `client_secret` are only needed for token exchanges. Every option is described in [Configuration](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/docs/Configuration.md).

### 3. Wire an adapter

Expand All @@ -76,7 +76,9 @@ secured_app = Auth0AsgiMiddleware(
)
```

The `mcp` SDK only accepts requests for `127.0.0.1`, `localhost` and `::1` by default, so list your public host in `allowed_hosts`, with and without a port, or every request gets a `421`. `scopes_supported` is advertised in the metadata, so clients know to request the scopes your tools need. Without it a client may ask for no scope and every gated tool fails.
The `mcp` SDK only accepts requests for `127.0.0.1`, `localhost` and `::1` by default, and only with a port, so list your public host in `allowed_hosts`, with and without a port, or every request gets a `421`. A browser request whose `Origin` is not in `allowed_origins` gets a `403`. `scopes_supported` is advertised in the metadata, so clients know to request the scopes your tools need. Without it a client may ask for no scope and every gated tool fails.

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?


### 4. Write a gated tool

Expand All @@ -95,7 +97,20 @@ async def get_weather(location: str) -> dict:
...
```

`with_required_scopes` raises before the tool body runs if the caller's token is missing a listed scope. Keep `@mcp.tool()` as the top decorator. If it sits below, the framework registers the unguarded function and the check is skipped. `require_caller_mcp_client` reads the request-scoped client from the `contextvar` set by the adapter.
`with_required_scopes` raises before the tool body runs if the caller's token is missing a listed scope. Keep `@mcp.tool()` as the top decorator. If it sits below, the framework registers the unguarded function and the check is skipped. See [Scope Enforcement](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/docs/ScopeEnforcement.md). `require_caller_mcp_client` reads the request-scoped client from the `contextvar` set by the adapter.

## Documentation

- [Configuration](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/docs/Configuration.md) - every client and middleware option.
- [Token Verification](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/docs/TokenVerification.md) - how the incoming token is checked and the `Principal` is built.
- [Request Context](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/docs/RequestContext.md) - reading the caller inside a tool.
- [Scope Enforcement](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/docs/ScopeEnforcement.md) - gating tools on the caller's scopes.
- [On-Behalf-Of](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/docs/OnBehalfOf.md) - exchanging the caller's token for a downstream token.
- [Token Vault](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/docs/TokenVault.md) - exchanging the caller's token for a federated provider token via Token Vault.
- [Token Caching](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/docs/TokenCaching.md) - the optional OBO cache and scope-matching modes.
- [Organization Policy](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/docs/OrganizationPolicy.md) - accepting or requiring an Organization.
- [Writing an Adapter](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/docs/WritingAnAdapter.md) - wiring the core into any MCP framework.
- [Examples](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/EXAMPLES.md) - a full runnable server.

## Feedback

Expand Down
Loading
Loading