Skip to content

Commit 2e0b5c7

Browse files
committed
feat(mcp): wire up oauth2 token verification for streamable-http transport
the mcp sdk (v1.26.0+) has built-in oauth 2.1 support via AuthSettings and TokenVerifier, but the agent-memory-server never wired it up for the streamable-http transport. without it, unauthenticated requests to /mcp return 406 instead of 401 + WWW-Authenticate header, which prevents mcp clients (like claude) from discovering the oauth flow. changes: - add JWTTokenVerifier that implements the sdk's TokenVerifier protocol by wrapping the existing verify_jwt() function - conditionally pass AuthSettings + token_verifier to FastMCP when AUTH_MODE=oauth2 and OAUTH2_RESOURCE_HOST are set - add /.well-known/oauth-authorization-server endpoint (RFC 9728) to the REST API for protected resource metadata discovery - add OAUTH2_RESOURCE_HOST config setting for the server's public hostname when enabled, the mcp transport now: - returns 401 with WWW-Authenticate: Bearer resource_metadata="..." header - serves /.well-known/oauth-protected-resource automatically (sdk built-in) - validates jwt tokens on all mcp requests via jwks when AUTH_MODE != oauth2 or OAUTH2_RESOURCE_HOST is unset, behavior is unchanged — no auth kwargs are passed to FastMCP. tested with ory hydra as the oidc provider and claude as the mcp client.
1 parent cc3a832 commit 2e0b5c7

3 files changed

Lines changed: 53 additions & 0 deletions

File tree

agent_memory_server/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,7 @@ class Settings(BaseSettings):
427427
oauth2_audience: str | None = None
428428
oauth2_jwks_url: str | None = None
429429
oauth2_algorithms: list[str] = ["RS256"]
430+
oauth2_resource_host: str | None = None
430431

431432
# Token Authentication settings
432433
token_auth_enabled: bool = False

agent_memory_server/main.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,17 @@ async def lifespan(app: FastAPI):
130130
app.include_router(memory_router)
131131

132132

133+
@app.get("/.well-known/oauth-authorization-server")
134+
async def oauth_authorization_server_metadata():
135+
"""RFC 9728 Protected Resource Metadata for MCP OIDC discovery."""
136+
if not settings.oauth2_issuer_url:
137+
return {"error": "OIDC not configured"}
138+
return {
139+
"resource": f"https://{settings.oauth2_resource_host or 'localhost'}",
140+
"authorization_servers": [settings.oauth2_issuer_url],
141+
}
142+
143+
133144
def on_start_logger(port: int):
134145
"""Log startup information"""
135146
print("\n-----------------------------------")

agent_memory_server/mcp.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from typing import Any
44

55
import ulid
6+
from mcp.server.auth.provider import AccessToken, TokenVerifier
7+
from mcp.server.auth.settings import AuthSettings
68
from mcp.server.fastmcp import FastMCP as _FastMCPBase
79

810
from agent_memory_server import working_memory as working_memory_core
@@ -255,12 +257,51 @@ async def run_stdio_async(self):
255257
"""
256258

257259

260+
class JWTTokenVerifier(TokenVerifier):
261+
"""Verify JWT tokens from Hydra using the existing auth module."""
262+
263+
async def verify_token(self, token: str) -> AccessToken | None:
264+
from agent_memory_server.auth import verify_jwt
265+
266+
try:
267+
user_info = verify_jwt(token)
268+
scopes = user_info.scope.split() if user_info.scope else []
269+
return AccessToken(
270+
token=token,
271+
client_id=user_info.sub,
272+
scopes=scopes,
273+
expires_at=user_info.exp,
274+
)
275+
except Exception:
276+
return None
277+
278+
279+
def _build_mcp_auth_kwargs() -> dict[str, Any]:
280+
"""Build auth kwargs for FastMCP if OAuth2 is configured."""
281+
if (
282+
settings.auth_mode != "oauth2"
283+
or not settings.oauth2_issuer_url
284+
or not settings.oauth2_resource_host
285+
):
286+
return {}
287+
288+
resource_url = f"https://{settings.oauth2_resource_host}"
289+
return {
290+
"auth": AuthSettings(
291+
issuer_url=settings.oauth2_issuer_url,
292+
resource_server_url=resource_url,
293+
),
294+
"token_verifier": JWTTokenVerifier(),
295+
}
296+
297+
258298
mcp_app = FastMCP(
259299
"Redis Agent Memory Server",
260300
host=settings.mcp_host,
261301
port=settings.mcp_port,
262302
instructions=INSTRUCTIONS,
263303
default_namespace=settings.default_mcp_namespace,
304+
**_build_mcp_auth_kwargs(),
264305
)
265306

266307

0 commit comments

Comments
 (0)