Skip to content

feat: add auth0-ai-fastmcp package - #174

Open
kishore7snehil wants to merge 9 commits into
docs/auth0-ai-mcp-guidesfrom
feat/auth0-ai-fastmcp-package
Open

kishore7snehil wants to merge 9 commits into
docs/auth0-ai-mcp-guidesfrom
feat/auth0-ai-fastmcp-package

Conversation

@kishore7snehil

Copy link
Copy Markdown
Contributor

📋 Changes

Adds auth0-ai-fastmcp, the adapter that secures Prefect FastMCP servers with Auth0. It is built on auth0-ai-mcp and wires token verification, the caller's identity and token exchange into a FastMCP server from one options object. The example and guides follow in separate PRs. This PR is stacked on #173.

✨ Features

  • Auth0FastMCP takes an Auth0FastMCPOptions and exposes a provider for FastMCP(auth=...) and a middleware for add_middleware(...).
  • Auth0TokenVerifier verifies each bearer token through auth0-ai-mcp, so a token that fails verification gets a 401 and the tool never runs. required_scopes are passed to FastMCP, which answers 403 for a token missing one.
  • Auth0ContextMiddleware makes the verified caller and a request-scoped client available to tools, resources and prompts, using the same accessors as auth0-ai-mcp.
  • with_required_scopes and require_scopes gate tools, resources and prompts on the caller's scopes.
  • domain is checked at startup, and a failed token exchange reaches the client as an error code only.
  • test.yml, dependabot.yml and publish.yml get an entry for the package.

🔧 API Changes

New public API exported from auth0_ai_fastmcp. Nothing existing changes.

📖 Documentation

Adds a short README.md and CONTRIBUTING.md. The guides and example are added in follow-up PRs.

🧪 Testing

The package has 117 unit tests that stub the core client, so they need no Auth0 tenant. pytest and ruff check . pass on Python 3.10, 3.13 and 3.14, and poetry build passes.

auth0-ai-mcp is a path dependency until it is released, and its auth0-api-python dependency is pinned to a commit SHA. This PR stays a draft until both are replaced with released versions.

  • 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

Comment thread packages/auth0-ai-fastmcp/poetry.lock Resolved
@kishore7snehil kishore7snehil reopened this Nov 4, 2026
@kishore7snehil
kishore7snehil marked this pull request as ready for review November 4, 2026 12:32
@kishore7snehil
kishore7snehil requested a review from a team as a code owner November 4, 2026 12:32
"""Return the error code only version of `exc` if FastMCP wrapped a failed token exchange, else None."""
cause = exc.__cause__
# FastMCP's own wrapper message starts with this prefix, a handler's own error does not.
if isinstance(cause, ApiError | DownscopedTokenError) and str(exc).startswith(wrapper_prefix):

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 works only when the error text starts with the exact name from the request. With mcp.mount(child, namespace="orders"), the child wraps the error with its own tool name, so the prefix does not match and Auth0's error_description reaches the client. I saw the same with mask_error_details=True on the parent, and with FastMCP's ErrorHandlingMiddleware added after auth0.middleware.

Can we match only the generic start (Error calling tool , Error reading resource , Error rendering prompt ) and mention the middleware order in the docs? Lines 78, 91 and 104 have the same issue.

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 e04dc69. The note on middleware order goes in the docs PR (#176).

config_url=f"https://{options.domain}/.well-known/openid-configuration",
base_url=options.resource_url,
resource_base_url=options.resource_url,
scopes_supported=list(dict.fromkeys([*options.tool_scopes, *(options.required_scopes or [])])),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With required_scopes set, the metadata lists tool_scopes and required_scopes, but the 401 WWW-Authenticate carries only required_scopes in scope. A client that follows the MCP spec uses the challenge scope first, so it logs in with mcp:access only and never asks for tool_scopes. Then every gated tool returns a tool error with HTTP 200.

Can we override get_challenge_scopes in _Auth0Provider to return scopes_supported? Otherwise the docstring at line 25 and the guides in #176 should say this.

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

from fastmcp.server.dependencies import get_access_token
from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext

_METHODS_WITHOUT_CALLER = frozenset({"initialize", "ping"})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

server/discover is missing from this set. With Client(server) in memory, which is how most people unit test a FastMCP server, the connect fails with connection is serving the 2026-07-28 protocol; the initialize handshake is not accepted. In legacy mode it connects, but every call fails with No verified access token on this request. Over HTTP it is fine.

Can we add server/discover here, as it carries no caller? Calls will still need a token, so a short note in the docs on how to test tool code would help too.

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 e04dc69. The note on testing tool code goes in the docs PR (#176).


def __init__(self, options: Auth0FastMCPOptions) -> None:
self._options = options
validation.require_bare_domain(options.domain)

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 scope lists are not checked. required_scopes=["read write"] gives 403 for every request, as no token can hold a scope with a space. A plain string like required_scopes="read" is split into r e a d, and tool_scopes=None fails with a bare TypeError.

require_scopes in the lower package already raises ConfigurationError for a blank or whitespace entry. Can we run the same check on tool_scopes and required_scopes 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 65e8a05.

assert auth0.provider.required_scopes == ["read:a"]


class StubVerifyingClient:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All verify tests use this stub, so the cnf, aud and JWKS handling of the real core never run in this package. I removed the cnf check from McpServerClient.verify_access_token and all 117 tests still passed.

Can we add a few tests with a real McpServerClient and a token from generate_token? A valid token runs the tool, a cnf token gets 401, a wrong aud gets 401, a JWKS outage gets 503. I wrote four such tests, and they pass on this head.

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


### 1. Install the SDK

`auth0-ai-fastmcp` requires Python 3.10 or later and FastMCP 4.0.10 or later. It is tested with FastMCP 4.0.10 to 4.1.6.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

CI installs only the locked FastMCP 4.0.10. I ran the suite on 4.1.6 and it passes, but nothing keeps it so. Also 'or later' is wider than ^4.0.10, which stops before 5.0.

The adapter uses FastMCP internals, like the error wrapper text and the provider routes. Can we add one CI job that installs the latest 4.x and runs the tests?

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 a6875a8. The range is now >=4.0.10 and a new CI job installs the newest FastMCP and runs the tests. The README line stays as the tested range.

tool_scopes: Scopes advertised in the metadata so the client requests them at login. `required_scopes` are advertised too.
client_id: Needed for the on-behalf-of exchange, client credentials tokens and Token Vault. Token verification works without it.
client_secret: Needed alongside `client_id` for the same flows.
token_store: Backing store for cached OBO tokens. Nothing is cached without one.

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 token_store also receives Token Vault tokens and client credentials tokens, not only OBO tokens. In my run it got writes for all three kinds.

Can we say 'cached exchange tokens' here, so people who write their own store or set a TTL know what will come in?

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

_BODY = json.dumps(
{
"jsonrpc": "2.0",
"error": {"code": -32002, "message": "Service unavailable: the token could not be verified"},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

FastMCP's own ErrorHandlingMiddleware uses -32002 for 'Resource not found', and the MCP types list it as reserved and never to be reused. Here it means 'service unavailable', so a client can read the same code in two ways.

Can we use a free code from the implementation range, such as -32003, or the standard -32603?

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 65e8a05. The auth0-ai-mcp package uses the same code now, in 1d77893.

python = ">=3.10,<4.0"
fastmcp = "^4.0.10"
# Path dependency while reviewed with auth0-ai-mcp, becomes a version range once it is released.
auth0-ai-mcp = { path = "../auth0-ai-mcp", develop = true }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

With a path dependency, the built wheel gets Requires-Dist: auth0-ai-mcp @ file:///... in its METADATA, and PyPI rejects an upload with such a direct reference. So this can merge, but it cannot be released until this line is a version range.

The description says the PR stays a draft until this and the core pin are replaced, but it is open for review. I assume the merge waits for that.

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.

This is a path dependency while auth0-ai-mcp is unreleased. It becomes a version range once auth0-ai-mcp is published, and this PR will not merge before that.

Comment thread packages/auth0-ai-fastmcp/LICENSE Outdated

4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:

You must give any other recipients of the Work or Derivative Works a copy of this License; and

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Section 4 lost its (a) to (d) markers here, and the clauses (b) and (c) are joined in one line. The words match the root LICENSE, but the structure does not. auth0-ai-mcp/LICENSE has the same copy.

Can we copy the root LICENSE file as it is? License scanners read this 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 43e7f5a for this package and in 6fd63c5 for auth0-ai-mcp.

@nandan-bhat nandan-bhat mentioned this pull request Nov 4, 2026
4 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-fastmcp-package branch from 1cdf42a to 43e7f5a 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
@kishore7snehil
kishore7snehil force-pushed the feat/auth0-ai-fastmcp-package branch from 43e7f5a to aaacf22 Compare November 5, 2026 15:02
@kishore7snehil
kishore7snehil force-pushed the docs/auth0-ai-mcp-guides branch from 73155b4 to de6ffca Compare November 5, 2026 15:48
@kishore7snehil
kishore7snehil force-pushed the feat/auth0-ai-fastmcp-package branch from aaacf22 to afbcbf5 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.

3 participants