auth0-diagnostics-mcp 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.4
2
+ Name: auth0-diagnostics-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server exposing Auth0 developer tools for Claude
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: mcp[cli]>=1.0
8
+ Requires-Dist: starlette>=0.40
9
+ Requires-Dist: uvicorn[standard]>=0.30
10
+ Requires-Dist: httpx>=0.27
11
+ Requires-Dist: PyJWT>=2.8
12
+ Requires-Dist: cryptography>=41.0
13
+
14
+ # auth0-mcp-server
15
+
16
+ Auth0 developer tools for Claude Code and any MCP-compatible AI assistant.
17
+
18
+ Decode JWTs, diagnose OAuth errors, validate tokens, and look up the right Auth0 flow — all without leaving your chat.
19
+
20
+ ## Installation
21
+
22
+ Add this to `~/.claude/settings.json`:
23
+
24
+ ```json
25
+ {
26
+ "mcpServers": {
27
+ "auth0-local": {
28
+ "command": "uvx",
29
+ "args": ["auth0-mcp-server"]
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ Requires [uv](https://docs.astral.sh/uv/getting-started/installation/). No other setup needed — `uvx` fetches and runs the server automatically.
36
+
37
+ ## Tools
38
+
39
+ ### `decode_token`
40
+ Decode and explain a JWT. Shows the header, all claims, expiry status, and what each standard claim means. Does not verify the signature.
41
+
42
+ > "Decode this token: eyJhbGci..."
43
+
44
+ ### `validate_token`
45
+ Verify a JWT's signature against an Auth0 tenant's public JWKS endpoint.
46
+
47
+ > "Validate this token against my-app.us.auth0.com"
48
+
49
+ ### `diagnose_auth_error`
50
+ Explain an Auth0 or OAuth error code — what it means, why it happens, and how to fix it.
51
+
52
+ > "What does invalid_grant mean?"
53
+
54
+ ### `explain_flow`
55
+ Recommend the right Auth0 / OAuth 2.0 flow for your app type and use case.
56
+
57
+ > "What flow should I use for a React SPA that calls an API?"
58
+
59
+ ### `get_tenant_config`
60
+ Fetch and summarize an Auth0 tenant's OIDC discovery document — issuer, JWKS URI, supported scopes, grant types, and more.
61
+
62
+ > "Show me the config for my-app.us.auth0.com"
63
+
64
+ ## Usage
65
+
66
+ Just describe what you need in plain English — Claude picks the right tool automatically:
67
+
68
+ - "Decode this JWT: eyJhbGci..."
69
+ - "Why am I getting server_error on my callback URL?"
70
+ - "What Auth0 flow should I use for a mobile app?"
71
+ - "Validate this token against compass-os.us.auth0.com with audience https://my-api"
72
+
73
+ ## Requirements
74
+
75
+ - Python 3.10+
76
+ - [uv](https://docs.astral.sh/uv/getting-started/installation/)
@@ -0,0 +1,6 @@
1
+ server.py,sha256=NJkrQh2iNv7pvsMtBmeU96DfB15WHb8Y7CyoLZSiPxg,23211
2
+ auth0_diagnostics_mcp-0.1.0.dist-info/METADATA,sha256=LchjHV0OEkM9ii_t80jYBIwqEn3krV5HcYRBcx7hGd8,2210
3
+ auth0_diagnostics_mcp-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
4
+ auth0_diagnostics_mcp-0.1.0.dist-info/entry_points.txt,sha256=9bvHE9L7YFOGSoK51ZKYXiox9CfMvI75mNG-oDgicpk,54
5
+ auth0_diagnostics_mcp-0.1.0.dist-info/top_level.txt,sha256=StKOSmRhvWS5IPcvhsDRbtxUTEofJgYFGOu5AAJdSWo,7
6
+ auth0_diagnostics_mcp-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ auth0-diagnostics-mcp = server:main
@@ -0,0 +1 @@
1
+ server
server.py ADDED
@@ -0,0 +1,491 @@
1
+ """Auth0 MCP Server — developer tools for Auth0 integrations."""
2
+
3
+ import base64
4
+ import json
5
+ import textwrap
6
+ from typing import Any
7
+
8
+ import httpx
9
+ from mcp.server import MCPServer
10
+ from starlette.requests import Request
11
+ from starlette.responses import JSONResponse
12
+
13
+ mcp = MCPServer("auth0-diagnostics-mcp")
14
+
15
+
16
+ # ── JWT helpers ────────────────────────────────────────────────────────────────
17
+
18
+ def _b64_decode(segment: str) -> dict[str, Any]:
19
+ padded = segment + "=" * (-len(segment) % 4)
20
+ return json.loads(base64.urlsafe_b64decode(padded))
21
+
22
+
23
+ def _split_token(token: str) -> tuple[dict[str, Any], dict[str, Any], str]:
24
+ parts = token.strip().split(".")
25
+ if len(parts) != 3:
26
+ raise ValueError("Not a valid JWT — expected 3 dot-separated parts.")
27
+ return _b64_decode(parts[0]), _b64_decode(parts[1]), parts[2]
28
+
29
+
30
+ # ── Static knowledge bases ─────────────────────────────────────────────────────
31
+
32
+ AUTH0_ERRORS: dict[str, dict[str, str]] = {
33
+ "invalid_grant": {
34
+ "summary": "The grant (auth code, refresh token, or device code) is invalid, expired, or already used.",
35
+ "common_causes": "Authorization code used more than once; refresh token revoked or expired; clock skew between client and server.",
36
+ "fix": "Restart the auth flow to get a fresh code. For refresh tokens, re-authenticate the user. Ensure system clocks are synced (NTP).",
37
+ },
38
+ "unauthorized_client": {
39
+ "summary": "The client application is not authorized to use this grant type.",
40
+ "common_causes": "Grant type not enabled for the application in the Auth0 dashboard; wrong client ID.",
41
+ "fix": "In Auth0 Dashboard → Applications → your app → scroll to 'Grant Types' and enable the required flow.",
42
+ },
43
+ "access_denied": {
44
+ "summary": "The user or authorization server denied the request.",
45
+ "common_causes": "User clicked 'Cancel' on the consent screen; a Rule/Action blocked the login; MFA failed.",
46
+ "fix": "Check your Auth0 Actions/Rules logs. If a custom action is blocking, review its logic in Dashboard → Auth Pipeline.",
47
+ },
48
+ "invalid_client": {
49
+ "summary": "Client authentication failed — wrong client ID or secret.",
50
+ "common_causes": "Mismatched client_id/client_secret; rotating a secret without updating the app config.",
51
+ "fix": "Verify your client_id and client_secret in Auth0 Dashboard → Applications → your app → Settings.",
52
+ },
53
+ "invalid_request": {
54
+ "summary": "The request is missing a required parameter or has an invalid value.",
55
+ "common_causes": "Missing redirect_uri, scope, or response_type; redirect_uri not whitelisted.",
56
+ "fix": "Add your redirect_uri to 'Allowed Callback URLs' in the Auth0 Dashboard. Check all required OAuth params are present.",
57
+ },
58
+ "unsupported_grant_type": {
59
+ "summary": "The authorization server does not support the requested grant type.",
60
+ "common_causes": "Typo in grant_type value; grant type not enabled for the application.",
61
+ "fix": "Check the grant_type value in your request. Enable it in Auth0 Dashboard → Applications → Grant Types.",
62
+ },
63
+ "invalid_scope": {
64
+ "summary": "The requested scope is invalid, unknown, or malformed.",
65
+ "common_causes": "Requesting a custom scope not defined in Auth0; typo in scope name.",
66
+ "fix": "Define custom scopes in Auth0 Dashboard → APIs → your API → Permissions. Ensure scopes are space-separated.",
67
+ },
68
+ "login_required": {
69
+ "summary": "The authorization server requires end-user authentication.",
70
+ "common_causes": "prompt=none used but the user has no active session.",
71
+ "fix": "Remove prompt=none or handle this error by redirecting the user to login.",
72
+ },
73
+ "consent_required": {
74
+ "summary": "The user must explicitly consent to the requested scopes.",
75
+ "common_causes": "prompt=none used but consent hasn't been granted yet.",
76
+ "fix": "Remove prompt=none for first-time users, or mark the application as first-party to skip consent.",
77
+ },
78
+ "mfa_required": {
79
+ "summary": "Multi-factor authentication is required for this user.",
80
+ "common_causes": "MFA policy enforced via Auth0 Actions or the user's profile.",
81
+ "fix": "Your app must handle the MFA challenge. Use Universal Login or the MFA API to complete the MFA step.",
82
+ },
83
+ "too_many_attempts": {
84
+ "summary": "The account has been blocked due to too many failed login attempts.",
85
+ "common_causes": "Brute force protection triggered.",
86
+ "fix": "User must wait for the lockout to expire, or an admin can unblock the account in Auth0 Dashboard → User Management → Users.",
87
+ },
88
+ "expired_token": {
89
+ "summary": "The provided token has expired.",
90
+ "common_causes": "Using an access token past its expiry; not refreshing tokens proactively.",
91
+ "fix": "Check the exp claim in the token. Use a refresh token to get a new access token, or re-authenticate the user.",
92
+ },
93
+ "invalid_token": {
94
+ "summary": "The token is malformed or its signature is invalid.",
95
+ "common_causes": "Token tampered with; wrong signing key; audience or issuer mismatch.",
96
+ "fix": "Verify the token using the validate_token tool. Ensure issuer matches https://{domain}/ and audience matches your API identifier.",
97
+ },
98
+ "PasswordStrengthError": {
99
+ "summary": "The password does not meet the tenant's strength requirements.",
100
+ "common_causes": "Password policy set to Fair, Good, or Excellent in Auth0.",
101
+ "fix": "Check the password policy in Auth0 Dashboard → Authentication → Database → your connection → Password Policy.",
102
+ },
103
+ "user_cancelled": {
104
+ "summary": "The user cancelled the authentication flow.",
105
+ "common_causes": "User clicked 'Cancel' or closed the login window.",
106
+ "fix": "Handle this gracefully — redirect to a landing page or show a re-login prompt.",
107
+ },
108
+ "server_error": {
109
+ "summary": "An unexpected error occurred on the Auth0 server.",
110
+ "common_causes": "Transient Auth0 service issue; misconfigured custom domain; an Action throwing an unhandled exception.",
111
+ "fix": "Check https://status.auth0.com for incidents. Review Auth0 Logs in Dashboard → Monitoring → Logs for stack traces.",
112
+ },
113
+ }
114
+
115
+ FLOWS_CONTENT = textwrap.dedent("""\
116
+ # Auth0 OAuth / OIDC Flows
117
+
118
+ ## Authorization Code Flow
119
+ **Best for:** Server-side web apps (Node, Python, Rails, etc.)
120
+ **How it works:** App redirects user → Auth0 login → Auth0 redirects back with a code →
121
+ App exchanges code + client_secret for tokens on the backend.
122
+ **Key property:** Client secret never exposed to the browser.
123
+ **Auth0 guide:** https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow
124
+
125
+ ---
126
+
127
+ ## Authorization Code Flow with PKCE
128
+ **Best for:** SPAs, mobile/native apps, CLI tools — any public client without a secure secret store.
129
+ **How it works:** Same as Authorization Code but replaces the client_secret with a
130
+ PKCE code_verifier / code_challenge pair generated by the client.
131
+ **Key property:** Secure even without a client secret.
132
+ **Auth0 guide:** https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow-with-pkce
133
+
134
+ ---
135
+
136
+ ## Client Credentials Flow
137
+ **Best for:** Machine-to-machine (M2M) — backend services, daemons, cron jobs acting as themselves.
138
+ **How it works:** App sends client_id + client_secret directly to /oauth/token and gets an access token.
139
+ **Key property:** No user involved.
140
+ **Auth0 guide:** https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-credentials-flow
141
+
142
+ ---
143
+
144
+ ## Device Authorization Flow
145
+ **Best for:** Input-constrained devices — smart TVs, IoT devices, CLI tools without a browser.
146
+ **How it works:** Device requests a user_code → user visits a URL on another device to authorize → device polls for token.
147
+ **Key property:** Decouples authentication from the device.
148
+ **Auth0 guide:** https://auth0.com/docs/get-started/authentication-and-authorization-flow/device-authorization-flow
149
+
150
+ ---
151
+
152
+ ## Resource Owner Password Flow (Legacy)
153
+ **Best for:** Legacy systems only — highly trusted first-party apps where redirect-based flows are impossible.
154
+ **Key property:** Auth0 recommends AGAINST this for new projects — use PKCE instead.
155
+ **Auth0 guide:** https://auth0.com/docs/get-started/authentication-and-authorization-flow/resource-owner-password-flow
156
+
157
+ ---
158
+
159
+ ## Implicit Flow (Deprecated)
160
+ **Status:** Deprecated — do not use for new applications.
161
+ **Replacement:** Authorization Code Flow with PKCE.
162
+ """)
163
+
164
+ FLOW_DETAILS: dict[str, dict[str, str]] = {
165
+ "authorization_code": {
166
+ "name": "Authorization Code Flow",
167
+ "when": "Server-side web applications with a secure backend.",
168
+ "why": "The client_secret stays on the server and is never exposed to the browser.",
169
+ "guide": "https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow",
170
+ },
171
+ "pkce": {
172
+ "name": "Authorization Code Flow with PKCE",
173
+ "when": "SPAs, mobile/native apps, CLI tools — any public client.",
174
+ "why": "PKCE replaces the client_secret with a cryptographic challenge, making it safe for clients that can't store secrets securely.",
175
+ "guide": "https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow-with-pkce",
176
+ },
177
+ "client_credentials": {
178
+ "name": "Client Credentials Flow",
179
+ "when": "Machine-to-machine communication — no user involved.",
180
+ "why": "The application authenticates as itself, not on behalf of a user.",
181
+ "guide": "https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-credentials-flow",
182
+ },
183
+ "device": {
184
+ "name": "Device Authorization Flow",
185
+ "when": "Input-constrained devices or headless CLIs without a browser.",
186
+ "why": "Decouples authentication from the device — the user authorizes on their phone or laptop.",
187
+ "guide": "https://auth0.com/docs/get-started/authentication-and-authorization-flow/device-authorization-flow",
188
+ },
189
+ }
190
+
191
+
192
+ def _recommend_flow(app_type: str, use_case: str) -> str:
193
+ text = f"{app_type} {use_case}".lower()
194
+ if any(w in text for w in ["m2m", "machine", "service", "daemon", "cron", "backend to backend", "api to api", "server to server"]):
195
+ return "client_credentials"
196
+ if any(w in text for w in ["tv", "iot", "device", "limited input", "no browser", "headless", "smart "]):
197
+ return "device"
198
+ if any(w in text for w in ["spa", "single page", "react", "vue", "angular", "browser app", "mobile", "ios", "android", "native", "public client", "cli"]):
199
+ return "pkce"
200
+ if any(w in text for w in ["server", "backend", "rails", "django", "flask", "fastapi", "express", "next", "nuxt", "web app"]):
201
+ return "authorization_code"
202
+ return "pkce"
203
+
204
+
205
+ # ── Tools ──────────────────────────────────────────────────────────────────────
206
+
207
+ @mcp.tool()
208
+ async def decode_token(token: str) -> str:
209
+ """Decode and explain a JWT — shows the header, all claims, expiry status,
210
+ and what each standard claim means. Does NOT verify the signature.
211
+ Strips a 'Bearer ' prefix automatically."""
212
+ from datetime import datetime, timezone
213
+ import time
214
+
215
+ token = token.removeprefix("Bearer ").strip()
216
+ try:
217
+ header, payload, _ = _split_token(token)
218
+ except ValueError as e:
219
+ return f"Invalid JWT: {e}"
220
+
221
+ now = int(time.time())
222
+ lines: list[str] = ["## JWT Decoded\n", "### Header"]
223
+ lines.append(f"```json\n{json.dumps(header, indent=2)}\n```")
224
+ lines.append("\n### Payload")
225
+ lines.append(f"```json\n{json.dumps(payload, indent=2)}\n```")
226
+ lines.append("\n### Key Claims Explained")
227
+
228
+ if "sub" in payload:
229
+ lines.append(f"- **sub** (Subject): `{payload['sub']}` — unique identifier for the user or entity")
230
+ if "iss" in payload:
231
+ lines.append(f"- **iss** (Issuer): `{payload['iss']}` — the Auth0 tenant that issued this token")
232
+ if "aud" in payload:
233
+ lines.append(f"- **aud** (Audience): `{payload['aud']}` — intended recipient(s); must match your API identifier")
234
+ if exp := payload.get("exp"):
235
+ exp_dt = datetime.fromtimestamp(exp, tz=timezone.utc).isoformat()
236
+ status = "**EXPIRED**" if now > exp else f"valid for {exp - now}s more"
237
+ lines.append(f"- **exp** (Expiry): `{exp_dt}` — {status}")
238
+ if iat := payload.get("iat"):
239
+ iat_dt = datetime.fromtimestamp(iat, tz=timezone.utc).isoformat()
240
+ lines.append(f"- **iat** (Issued At): `{iat_dt}`")
241
+ if nbf := payload.get("nbf"):
242
+ nbf_dt = datetime.fromtimestamp(nbf, tz=timezone.utc).isoformat()
243
+ not_yet = now < nbf
244
+ lines.append(f"- **nbf** (Not Before): `{nbf_dt}`{' — **not yet valid**' if not_yet else ''}")
245
+ if "scope" in payload:
246
+ lines.append(f"- **scope**: `{payload['scope']}` — granted permissions")
247
+ if "azp" in payload:
248
+ lines.append(f"- **azp** (Authorized Party): `{payload['azp']}` — client application that requested this token")
249
+
250
+ lines.append("\n> Signature not verified — use `validate_token` to cryptographically verify against the issuer's JWKS.")
251
+ return "\n".join(lines)
252
+
253
+
254
+ @mcp.tool()
255
+ async def validate_token(token: str, domain: str, audience: str = "") -> str:
256
+ """Verify a JWT's signature and standard claims against an Auth0 tenant's
257
+ public JWKS endpoint. Returns a pass/fail result and decoded claims on success.
258
+
259
+ Args:
260
+ token: The JWT to validate.
261
+ domain: Auth0 domain, e.g. myapp.us.auth0.com
262
+ audience: Expected audience (your API identifier). Optional but recommended.
263
+ """
264
+ import jwt as pyjwt
265
+ from jwt.algorithms import RSAAlgorithm
266
+
267
+ token = token.removeprefix("Bearer ").strip()
268
+ domain = domain.removeprefix("https://").rstrip("/")
269
+
270
+ try:
271
+ header, _, _ = _split_token(token)
272
+ except ValueError as e:
273
+ return f"Invalid JWT format: {e}"
274
+
275
+ kid = header.get("kid")
276
+ alg = header.get("alg", "RS256")
277
+
278
+ try:
279
+ async with httpx.AsyncClient(timeout=10) as client:
280
+ jwks_resp = await client.get(f"https://{domain}/.well-known/jwks.json")
281
+ jwks_resp.raise_for_status()
282
+ jwks = jwks_resp.json()
283
+ except httpx.HTTPError as e:
284
+ return f"Failed to fetch JWKS from `{domain}`: {e}"
285
+
286
+ matching = [k for k in jwks.get("keys", []) if k.get("kid") == kid]
287
+ if not matching:
288
+ available = [k.get("kid") for k in jwks.get("keys", [])]
289
+ return f"No JWKS key found matching `kid={kid}`.\nAvailable kids: {available}"
290
+
291
+ try:
292
+ public_key = RSAAlgorithm.from_jwk(json.dumps(matching[0]))
293
+ decode_kwargs: dict[str, Any] = {
294
+ "algorithms": [alg],
295
+ "issuer": f"https://{domain}/",
296
+ }
297
+ if audience:
298
+ decode_kwargs["audience"] = audience
299
+ else:
300
+ decode_kwargs["options"] = {"verify_aud": False}
301
+
302
+ claims = pyjwt.decode(token, public_key, **decode_kwargs)
303
+ lines = [
304
+ "## Token Validation: PASSED ✓\n",
305
+ "Signature is valid and claims verified.\n",
306
+ f"```json\n{json.dumps(claims, indent=2)}\n```",
307
+ ]
308
+ if not audience:
309
+ lines.append("\n> Tip: pass an `audience` value to also verify the `aud` claim.")
310
+ return "\n".join(lines)
311
+
312
+ except pyjwt.ExpiredSignatureError:
313
+ return "## Token Validation: FAILED ✗\n\n**Reason:** Token has expired. Check the `exp` claim with `decode_token`."
314
+ except pyjwt.InvalidIssuerError:
315
+ return f"## Token Validation: FAILED ✗\n\n**Reason:** Issuer mismatch. Token issuer does not match `https://{domain}/`."
316
+ except pyjwt.InvalidAudienceError:
317
+ return f"## Token Validation: FAILED ✗\n\n**Reason:** Audience mismatch. Token `aud` does not match `{audience}`."
318
+ except Exception as e:
319
+ return f"## Token Validation: FAILED ✗\n\n**Reason:** {e}"
320
+
321
+
322
+ @mcp.tool()
323
+ async def get_tenant_config(domain: str) -> str:
324
+ """Fetch and summarize an Auth0 tenant's public OIDC discovery document —
325
+ issuer, JWKS URI, supported scopes, grant types, and more.
326
+
327
+ Args:
328
+ domain: Auth0 domain, e.g. myapp.us.auth0.com
329
+ """
330
+ domain = domain.removeprefix("https://").rstrip("/")
331
+ url = f"https://{domain}/.well-known/openid-configuration"
332
+
333
+ try:
334
+ async with httpx.AsyncClient(timeout=10) as client:
335
+ resp = await client.get(url)
336
+ resp.raise_for_status()
337
+ config: dict[str, Any] = resp.json()
338
+ except httpx.HTTPError as e:
339
+ return f"Failed to fetch OIDC config from `{domain}`: {e}"
340
+
341
+ lines = [
342
+ f"## Auth0 Tenant: `{domain}`\n",
343
+ f"**Issuer:** `{config.get('issuer', 'N/A')}`",
344
+ f"**JWKS URI:** `{config.get('jwks_uri', 'N/A')}`",
345
+ f"**Authorization endpoint:** `{config.get('authorization_endpoint', 'N/A')}`",
346
+ f"**Token endpoint:** `{config.get('token_endpoint', 'N/A')}`",
347
+ f"**Userinfo endpoint:** `{config.get('userinfo_endpoint', 'N/A')}`",
348
+ ]
349
+ if scopes := config.get("scopes_supported"):
350
+ lines.append(f"\n**Supported scopes:** {', '.join(f'`{s}`' for s in scopes)}")
351
+ if algs := config.get("id_token_signing_alg_values_supported"):
352
+ lines.append(f"**ID token signing algorithms:** {', '.join(algs)}")
353
+ if grants := config.get("grant_types_supported"):
354
+ lines.append(f"**Grant types:** {', '.join(f'`{g}`' for g in grants)}")
355
+ if response_types := config.get("response_types_supported"):
356
+ lines.append(f"**Response types:** {', '.join(response_types)}")
357
+
358
+ lines.append(f"\n<details><summary>Full OIDC config</summary>\n\n```json\n{json.dumps(config, indent=2)}\n```\n</details>")
359
+ return "\n".join(lines)
360
+
361
+
362
+ @mcp.tool()
363
+ def diagnose_auth_error(error: str) -> str:
364
+ """Explain an Auth0 or OAuth error code — returns what it means,
365
+ common reasons it happens, and how to fix it.
366
+
367
+ Args:
368
+ error: The OAuth/Auth0 error code, e.g. invalid_grant
369
+ """
370
+ info = AUTH0_ERRORS.get(error.strip())
371
+
372
+ if not info:
373
+ known = ", ".join(f"`{k}`" for k in AUTH0_ERRORS)
374
+ return (
375
+ f"Unknown error code: `{error}`.\n\n"
376
+ f"Known codes: {known}\n\n"
377
+ "For unlisted errors, check: https://auth0.com/docs/libraries/error-messages"
378
+ )
379
+
380
+ return "\n".join([
381
+ f"## Auth0 Error: `{error}`\n",
382
+ f"**What it means:** {info['summary']}\n",
383
+ f"**Common causes:** {info['common_causes']}\n",
384
+ f"**How to fix:** {info['fix']}",
385
+ ])
386
+
387
+
388
+ @mcp.tool()
389
+ def explain_flow(app_type: str, use_case: str) -> str:
390
+ """Given an application type and use case, recommend the right
391
+ Auth0 / OAuth 2.0 flow with an explanation of why it fits.
392
+
393
+ Args:
394
+ app_type: e.g. SPA, mobile app, CLI, backend service, IoT device
395
+ use_case: Brief description of what the app needs to do with auth
396
+ """
397
+ flow_key = _recommend_flow(app_type, use_case)
398
+ flow = FLOW_DETAILS[flow_key]
399
+
400
+ return "\n".join([
401
+ f"## Recommended Flow: **{flow['name']}**\n",
402
+ f"**When to use:** {flow['when']}",
403
+ f"**Why it fits your case:** {flow['why']}\n",
404
+ f"**Auth0 guide:** {flow['guide']}\n",
405
+ "---",
406
+ "\nRead the `auth0://flows` resource for a comparison of all Auth0 flows.",
407
+ ])
408
+
409
+
410
+ # ── Resources ──────────────────────────────────────────────────────────────────
411
+
412
+ @mcp.resource(
413
+ "auth0://flows",
414
+ name="Auth0 OAuth / OIDC Flows",
415
+ description="Reference guide for all Auth0 authentication flows — when to use each and why.",
416
+ mime_type="text/markdown",
417
+ )
418
+ def flows_resource() -> str:
419
+ return FLOWS_CONTENT
420
+
421
+
422
+ # ── OAuth discovery stubs (no auth required) ──────────────────────────────────
423
+ # Claude Code's MCP client hits these RFC 9728/8414 endpoints and fails hard if
424
+ # the response body isn't valid JSON — even on 404. It also probes path-based
425
+ # discovery variants (RFC 9728 §3.1) and the SSE-prefixed paths, so all
426
+ # variants need JSON responses.
427
+
428
+ def _no_auth_json() -> JSONResponse:
429
+ return JSONResponse(
430
+ {"error": "not_found", "error_description": "This server does not require OAuth authentication."},
431
+ status_code=404,
432
+ )
433
+
434
+
435
+ @mcp.custom_route("/.well-known/oauth-protected-resource", methods=["GET"])
436
+ async def _oauth_protected_resource(request: Request) -> JSONResponse:
437
+ return _no_auth_json()
438
+
439
+
440
+ @mcp.custom_route("/.well-known/oauth-protected-resource/sse", methods=["GET"])
441
+ async def _oauth_protected_resource_sse(request: Request) -> JSONResponse:
442
+ return _no_auth_json()
443
+
444
+
445
+ @mcp.custom_route("/.well-known/oauth-authorization-server", methods=["GET"])
446
+ async def _oauth_authorization_server(request: Request) -> JSONResponse:
447
+ return _no_auth_json()
448
+
449
+
450
+ @mcp.custom_route("/.well-known/oauth-authorization-server/sse", methods=["GET"])
451
+ async def _oauth_authorization_server_sse(request: Request) -> JSONResponse:
452
+ return _no_auth_json()
453
+
454
+
455
+ @mcp.custom_route("/.well-known/openid-configuration", methods=["GET"])
456
+ async def _openid_configuration(request: Request) -> JSONResponse:
457
+ return _no_auth_json()
458
+
459
+
460
+ @mcp.custom_route("/.well-known/openid-configuration/sse", methods=["GET"])
461
+ async def _openid_configuration_sse(request: Request) -> JSONResponse:
462
+ return _no_auth_json()
463
+
464
+
465
+ @mcp.custom_route("/sse/.well-known/openid-configuration", methods=["GET"])
466
+ async def _sse_openid_configuration(request: Request) -> JSONResponse:
467
+ return _no_auth_json()
468
+
469
+
470
+ @mcp.custom_route("/register", methods=["POST"])
471
+ async def _register(request: Request) -> JSONResponse:
472
+ return JSONResponse(
473
+ {"error": "not_supported", "error_description": "Dynamic client registration is not supported."},
474
+ status_code=400,
475
+ )
476
+
477
+
478
+ # ── Entry point ────────────────────────────────────────────────────────────────
479
+
480
+ def main() -> None:
481
+ import sys
482
+ transport = "stdio"
483
+ if "--transport" in sys.argv:
484
+ idx = sys.argv.index("--transport")
485
+ if idx + 1 < len(sys.argv):
486
+ transport = sys.argv[idx + 1]
487
+ mcp.run(transport=transport)
488
+
489
+
490
+ if __name__ == "__main__":
491
+ main()