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-fastmcp/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# AI Agent Guidelines for auth0-ai-fastmcp

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

## Your Role

You are a Python SDK engineer on auth0-ai-fastmcp, the adapter that secures a Prefect FastMCP server with Auth0. It verifies Auth0 access tokens through FastMCP's own auth provider and makes the verified caller available to tools, resources, and prompts. It is built on auth0-ai-mcp and owns no verification, exchange, or caching logic.

---

## 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_fastmcp/
__init__.py # Public API: everything in __all__ is the supported surface
config.py # Auth0FastMCPOptions and Auth0FastMCP (builds the client, provider, and middleware)
verifier.py # Auth0TokenVerifier: FastMCP TokenVerifier backed by McpServerClient
middleware.py # Auth0ContextMiddleware: binds the verified caller for every request and shortens failed token exchange errors
scopes.py # require_scopes and with_required_scopes: auth0-ai-mcp checks whose denials are FastMCP ToolErrors
utils/_internal/ # Private helpers by topic (validation, telemetry, outage)
tests/
test_config.py, test_middleware.py, test_outage.py, test_scopes.py, test_verifier.py, test_docs.py
docs/ # Hand-written guides, linked from README (see references/docs-update.md)
examples/b2b_ecommerce/ # Runnable FastMCP server 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)
```

Verification, scope checks, token exchange, caching, and token stores live in auth0-ai-mcp and auth0-api-python. This package wires them into FastMCP and does not reimplement them. Before changing a flow, read its row in [references/flow-map.md](references/flow-map.md).

---

## Boundaries

### Always Do

- Run `poetry run pytest` and `poetry run ruff check .` before committing.
- 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.
- Add unit tests for new functionality. Async tests need no marker because `asyncio_mode = "auto"` is set. Stand in for `McpServerClient` with a `Stub...` class, as `tests/test_verifier.py` does (no real network calls).
- Use the public names auth0-ai-mcp exports (`build_principal`, `bind_request_context`, and so on). Do not import from its private modules.
- Export any new public symbol through `__init__.py` `__all__`.
- Update `README.md`, `EXAMPLES.md`, 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.

### Ask First

- Any breaking change to a version that has already shipped, such as a renamed or removed public symbol, a changed signature or default, or a raised Python floor.
- Adding or bumping dependencies (`pyproject.toml`, `poetry.lock`), including the `fastmcp` range and the auth0-ai-mcp reference.
- Changes to how a token is verified, how the caller is built, or which requests need a token.
- Changes to CI/CD configuration, `.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 and `.env` stays untracked.
- 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 failure or a missing token must reject the request, never fall through to a permissive default.
- Hand the raw incoming token to tool code. `RequestScopedMcpClient` closes over it on purpose, so tools read the caller with `get_caller_principal`. FastMCP's `get_access_token()` still holds the raw token because the verifier returns it, so do not point tool authors at it.
- 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.
- Modify `.venv*/`, `dist/`, `__pycache__/`, `.coverage`, `coverage.xml`.
- Log, print, or include access tokens, exchanged tokens, or client secrets in error messages.

---

## Security Considerations

- **Verification is delegated:** every token goes through `McpServerClient.verify_access_token`. Never decode a token yourself.
- **Fail closed in the verifier:** `Auth0TokenVerifier.verify_token` returns `None` for a blank token, any token that fails verification, or one that `build_principal` rejects (missing `sub`, `exp`, or client id, malformed `org_id`, or a `permissions` claim that is not a list of strings), and FastMCP answers `401`. If Auth0 cannot be reached, `AuthServerUnavailableError` is re-raised and the provider from `Auth0FastMCP` answers `503` (JSON-RPC code `-32003`) with `Retry-After` and no challenge, so never turn it into `None`. Keep both for any new rejection path.
- **Fail closed in the middleware:** `Auth0ContextMiddleware.on_request` raises `UnauthenticatedError` when a request that needs a caller has no verified token. Only `initialize`, `ping` and `server/discover` are exempt. It rebuilds the principal from the claims the verifier already checked, so it is not an independent verification.
- **Required scopes live on the verifier:** FastMCP reads `required_scopes` from the token verifier, so the verifier must pass them to `TokenVerifier.__init__`. Passing them only to the provider has no effect once a custom verifier is set.
- **Request context is per request:** the context is bound with `bind_request_context`, which resets on exit even when the handler raises. Never set it without that reset, otherwise one caller's identity can leak into the next request.
- **Audience is derived from the advertised resource:** FastMCP appends the MCP path to `resource_url`, so `audience` defaults to `resource_url` plus `mcp_path` and an explicit value that differs raises `ConfigurationError`.
- **Dependency scanning:** Snyk, CodeQL, OpenSSF Scorecard, 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 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)
79 changes: 79 additions & 0 deletions packages/auth0-ai-fastmcp/EXAMPLES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Examples

This page walks through building a complete Auth0-secured FastMCP server using `Auth0FastMCP`.

## Full server example

The example below shows every piece you need: configuring `Auth0FastMCP`, registering the provider and middleware, advertising tool scopes, and gating a tool that mints a downstream token on behalf of the caller.

```python
from fastmcp import FastMCP
from auth0_ai_fastmcp import (
Auth0FastMCP,
Auth0FastMCPOptions,
require_scopes,
with_required_scopes,
)
from auth0_ai_mcp import get_caller_principal, require_caller_mcp_client

# The provider fetches the tenant's OIDC configuration at startup.
auth0 = Auth0FastMCP(Auth0FastMCPOptions(
domain="your-tenant.auth0.com",
resource_url="https://your-api.example.com",
tool_scopes=["tool:read_orders", "tool:place_order", "resource:recent_orders", "prompt:order_summary"],
client_id="YOUR_CLIENT_ID",
client_secret="YOUR_CLIENT_SECRET",
))

mcp = FastMCP(name="My Store Server", auth=auth0.provider)
mcp.add_middleware(auth0.middleware)


@mcp.tool
@with_required_scopes("tool:read_orders")
async def read_orders() -> list[dict]:
"""Return the caller's orders from the downstream API."""
client = require_caller_mcp_client()
token = await client.get_token_on_behalf_of(
audience="https://store-api/",
scope="read:orders",
)
# Use token["access_token"] to call the downstream API.
...


@mcp.tool
@with_required_scopes("tool:place_order")
async def place_order(item_id: str, quantity: int) -> dict:
"""Place an order on behalf of the caller."""
client = require_caller_mcp_client()
token = await client.get_token_on_behalf_of(
audience="https://store-api/",
scope="write:orders",
)
# Use token["access_token"] to call the downstream API.
...
```

For what each `Auth0FastMCPOptions` field does, see [Configuration](./docs/Configuration.md#auth0fastmcpoptions-fields).

## Resources and prompts

The middleware sets the caller for resources and prompts too. Check the scope with `require_scopes` inside the handler, or put `with_required_scopes` on the handler as it is on a tool. The scopes used here are in `tool_scopes` in the full server above, so the MCP client requests them at login.

```python
@mcp.resource("orders://recent")
async def recent_orders() -> str:
require_scopes(get_caller_principal(), "resource:recent_orders")

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 server above advertises only the two tool:* scopes in tool_scopes. A client that follows the MCP spec asks for the advertised scopes, so it never gets resource:recent_orders or prompt:order_summary, and both handlers deny everyone. In my run the resource gave Missing one or more required scopes.

Can we add both scopes to tool_scopes in the sample above, and say in ToolScopes.md that tool_scopes should list every scope clients need, for tools, resources and prompts?

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

return "..."


@mcp.prompt
async def order_summary() -> str:
require_scopes(get_caller_principal(), "prompt:order_summary")
return "Summarize the caller's recent orders."
```

## Runnable version

A complete runnable server using this pattern lives in [`examples/b2b_ecommerce`](./examples/b2b_ecommerce/).
24 changes: 21 additions & 3 deletions packages/auth0-ai-fastmcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ It is the FastMCP adapter for [`auth0-ai-mcp`](https://github.com/auth0/auth0-ai

![Release](https://img.shields.io/pypi/v/auth0-ai-fastmcp) ![Downloads](https://img.shields.io/pypi/dw/auth0-ai-fastmcp) [![License](https://img.shields.io/:license-Apache%202.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)

## Getting Started

Expand Down Expand Up @@ -43,11 +43,13 @@ mcp.add_middleware(auth0.middleware)
> [!IMPORTANT]
> Both `auth=auth0.provider` and `mcp.add_middleware(auth0.middleware)` are required. Without the middleware, tools cannot read the caller.

The provider checks every incoming token and the middleware makes the verified caller available to your tools, resources and prompts.
The provider checks every incoming token and the middleware makes the verified caller available to your tools, resources and prompts. See [What the middleware does](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-fastmcp/docs/Configuration.md#what-the-middleware-does).

> [!WARNING]
> Do not add `/mcp` to the end of `resource_url`. FastMCP appends the MCP path itself.

The tenant also needs the `compatibility` resource parameter profile, the scopes added to the API as permissions, and a client grant for the MCP client. See [What you need on the tenant](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-fastmcp/examples/b2b_ecommerce/README.md#what-you-need-on-the-tenant) for the commands.

### 3. Gate a tool

```python
Expand All @@ -65,7 +67,23 @@ async def do_thing() -> str:

### 4. Gate a resource or prompt

Resources and prompts are gated the same way.
Resources and prompts are gated the same way. See [Gating resources and prompts](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-fastmcp/docs/ToolScopes.md#gating-resources-and-prompts).

## Documentation

- [Configuration](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-fastmcp/docs/Configuration.md) - all `Auth0FastMCPOptions` fields.
- [Tool Scopes](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-fastmcp/docs/ToolScopes.md) - how scopes are advertised at login and how tools, resources and prompts are gated.
- [Examples](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-fastmcp/EXAMPLES.md) - code samples for a full server. The runnable version is [`examples/b2b_ecommerce`](https://github.com/auth0/auth0-ai-python/tree/main/packages/auth0-ai-fastmcp/examples/b2b_ecommerce).

### Core guides

These guides cover the behavior this package builds on. In FastMCP code, import the scope helpers from `auth0_ai_fastmcp` and not from `auth0_ai_mcp`. The `auth0_ai_mcp` versions still deny the call, but with `mask_error_details=True` the client sees only `Error calling tool 'x'` and the server logs an ERROR traceback.

- [On-Behalf-Of](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/docs/OnBehalfOf.md) - get a token for a downstream API for the caller.
- [Token Vault](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/docs/TokenVault.md) - get a token for a third-party provider such as Google.
- [Token Caching](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/docs/TokenCaching.md) - reuse exchanged tokens with a token store.
- [Request Context](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/docs/RequestContext.md) - read the caller inside a tool.
- [Scope Enforcement](https://github.com/auth0/auth0-ai-python/blob/main/packages/auth0-ai-mcp/docs/ScopeEnforcement.md) - check the caller's scopes before a tool runs.

## Feedback

Expand Down
Loading
Loading