Skip to content

Commit a2dcfad

Browse files
authored
Merge pull request #97 from tadata-org/fix/bearer-token-usage
Fix/bearer token usage
2 parents 83c9180 + 06c9a84 commit a2dcfad

8 files changed

+185
-12
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.3.2]
9+
10+
### Fixed
11+
- 🐛 Fix a bug preventing simple setup of [basic token passthrough](docs/03_authentication_and_authorization.md#basic-token-passthrough)
12+
813
## [0.3.1]
914

1015
🚀 FastApiMCP now supports MCP Authorization!

docs/03_authentication_and_authorization.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ mcp = FastApiMCP(
4949
mcp.mount()
5050
```
5151

52+
For a complete working example of authorization header, check out the [auth_example_token_passthrough.py](/examples/08_auth_example_token_passthrough.py) in the examples folder.
53+
5254
## OAuth Flow
5355

5456
FastAPI-MCP supports the full OAuth 2 flow, compliant with [MCP Spec 2025-03-26](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization).
@@ -138,7 +140,7 @@ For this to work, you have to make sure mcp-remote is running [on a fixed port](
138140

139141
## Working Example with Auth0
140142

141-
For a complete working example of OAuth integration with Auth0, check out the [auth_example_auth0.py](/examples/08_auth_example_auth0.py) in the examples folder. This example demonstrates the simple case of using Auth0 as an OAuth provider, with a working example of the OAuth flow.
143+
For a complete working example of OAuth integration with Auth0, check out the [auth_example_auth0.py](/examples/09_auth_example_auth0.py) in the examples folder. This example demonstrates the simple case of using Auth0 as an OAuth provider, with a working example of the OAuth flow.
142144

143145
For it to work, you need an .env file in the root of the project with the following variables:
144146

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"""
2+
This example shows how to reject any request without a valid token passed in the Authorization header.
3+
4+
In order to configure the auth header, the config file for the MCP server should looks like this:
5+
```json
6+
{
7+
"mcpServers": {
8+
"remote-example": {
9+
"command": "npx",
10+
"args": [
11+
"mcp-remote",
12+
"http://localhost:8000/mcp",
13+
"--header",
14+
"Authorization:${AUTH_HEADER}"
15+
]
16+
},
17+
"env": {
18+
"AUTH_HEADER": "Bearer <your-token>"
19+
}
20+
}
21+
}
22+
```
23+
"""
24+
from examples.shared.apps.items import app # The FastAPI app
25+
from examples.shared.setup import setup_logging
26+
27+
from fastapi import Depends
28+
from fastapi.security import HTTPBearer
29+
30+
from fastapi_mcp import FastApiMCP, AuthConfig
31+
32+
setup_logging()
33+
34+
# Scheme for the Authorization header
35+
token_auth_scheme = HTTPBearer()
36+
37+
# Create a private endpoint
38+
@app.get("/private")
39+
async def private(token = Depends(token_auth_scheme)):
40+
return token.credentials
41+
42+
# Create the MCP server with the token auth scheme
43+
mcp = FastApiMCP(
44+
app,
45+
name="Protected MCP",
46+
auth_config=AuthConfig(
47+
dependencies=[Depends(token_auth_scheme)],
48+
),
49+
)
50+
51+
# Mount the MCP server
52+
mcp.mount()
53+
54+
55+
if __name__ == "__main__":
56+
import uvicorn
57+
58+
uvicorn.run(app, host="0.0.0.0", port=8000)
File renamed without changes.

fastapi_mcp/types.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,13 @@ class OAuthMetadata(BaseType):
4141
]
4242

4343
authorization_endpoint: Annotated[
44-
StrHttpUrl,
44+
Optional[StrHttpUrl],
4545
Doc(
4646
"""
4747
URL of the authorization server's authorization endpoint.
4848
"""
4949
),
50-
]
50+
] = None
5151

5252
token_endpoint: Annotated[
5353
StrHttpUrl,
@@ -353,18 +353,15 @@ async def authenticate_request(request: Request, token: str = Depends(oauth2_sch
353353

354354
@model_validator(mode="after")
355355
def validate_required_fields(self):
356-
if self.custom_oauth_metadata is None and self.issuer is None:
357-
raise ValueError("'issuer' is required when 'custom_oauth_metadata' is not provided")
356+
if self.custom_oauth_metadata is None and self.issuer is None and not self.dependencies:
357+
raise ValueError("at least one of 'issuer', 'custom_oauth_metadata' or 'dependencies' is required")
358358

359359
if self.setup_proxies:
360360
if self.client_id is None:
361361
raise ValueError("'client_id' is required when 'setup_proxies' is True")
362362

363-
if self.setup_fake_dynamic_registration and not self.client_secret:
364-
raise ValueError("'client_secret' is required when 'setup_fake_dynamic_registration' is True")
365-
366-
if self.setup_fake_dynamic_registration and not self.setup_proxies:
367-
raise ValueError("'setup_fake_dynamic_registration' can only be True when 'setup_proxies' is True")
363+
if self.setup_fake_dynamic_registration and not self.client_secret:
364+
raise ValueError("'client_secret' is required when 'setup_fake_dynamic_registration' is True")
368365

369366
return self
370367

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "fastapi-mcp"
7-
version = "0.3.1"
7+
version = "0.3.2"
88
description = "Automatic MCP server generator for FastAPI applications - converts FastAPI endpoints to MCP tools for LLM integration"
99
readme = "README.md"
1010
requires-python = ">=3.10"

tests/test_types_validation.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import pytest
2+
from pydantic import ValidationError
3+
from fastapi import Depends
4+
5+
from fastapi_mcp.types import (
6+
OAuthMetadata,
7+
AuthConfig,
8+
)
9+
10+
11+
class TestOAuthMetadata:
12+
def test_non_empty_lists_validation(self):
13+
for field in [
14+
"scopes_supported",
15+
"response_types_supported",
16+
"grant_types_supported",
17+
"token_endpoint_auth_methods_supported",
18+
"code_challenge_methods_supported",
19+
]:
20+
with pytest.raises(ValidationError, match=f"{field} cannot be empty"):
21+
OAuthMetadata(
22+
issuer="https://example.com",
23+
authorization_endpoint="https://example.com/auth",
24+
token_endpoint="https://example.com/token",
25+
**{field: []},
26+
)
27+
28+
def test_authorization_endpoint_required_for_authorization_code(self):
29+
with pytest.raises(ValidationError) as exc_info:
30+
OAuthMetadata(
31+
issuer="https://example.com",
32+
token_endpoint="https://example.com/token",
33+
grant_types_supported=["authorization_code", "client_credentials"],
34+
)
35+
assert "authorization_endpoint is required when authorization_code grant type is supported" in str(
36+
exc_info.value
37+
)
38+
39+
OAuthMetadata(
40+
issuer="https://example.com",
41+
token_endpoint="https://example.com/token",
42+
authorization_endpoint="https://example.com/auth",
43+
grant_types_supported=["client_credentials"],
44+
)
45+
46+
def test_model_dump_excludes_none(self):
47+
metadata = OAuthMetadata(
48+
issuer="https://example.com",
49+
authorization_endpoint="https://example.com/auth",
50+
token_endpoint="https://example.com/token",
51+
)
52+
53+
dumped = metadata.model_dump()
54+
55+
assert "registration_endpoint" not in dumped
56+
57+
58+
class TestAuthConfig:
59+
def test_required_fields_validation(self):
60+
with pytest.raises(
61+
ValidationError, match="at least one of 'issuer', 'custom_oauth_metadata' or 'dependencies' is required"
62+
):
63+
AuthConfig()
64+
65+
AuthConfig(issuer="https://example.com")
66+
67+
AuthConfig(
68+
custom_oauth_metadata={
69+
"issuer": "https://example.com",
70+
"authorization_endpoint": "https://example.com/auth",
71+
"token_endpoint": "https://example.com/token",
72+
},
73+
)
74+
75+
def dummy_dependency():
76+
pass
77+
78+
AuthConfig(dependencies=[Depends(dummy_dependency)])
79+
80+
def test_client_id_required_for_setup_proxies(self):
81+
with pytest.raises(ValidationError, match="'client_id' is required when 'setup_proxies' is True"):
82+
AuthConfig(
83+
issuer="https://example.com",
84+
setup_proxies=True,
85+
)
86+
87+
AuthConfig(
88+
issuer="https://example.com",
89+
setup_proxies=True,
90+
client_id="test-client-id",
91+
client_secret="test-client-secret",
92+
)
93+
94+
def test_client_secret_required_for_fake_registration(self):
95+
with pytest.raises(
96+
ValidationError, match="'client_secret' is required when 'setup_fake_dynamic_registration' is True"
97+
):
98+
AuthConfig(
99+
issuer="https://example.com",
100+
setup_proxies=True,
101+
client_id="test-client-id",
102+
setup_fake_dynamic_registration=True,
103+
)
104+
105+
AuthConfig(
106+
issuer="https://example.com",
107+
setup_proxies=True,
108+
client_id="test-client-id",
109+
client_secret="test-client-secret",
110+
setup_fake_dynamic_registration=True,
111+
)

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)