mcp-authkit 0.1.0__tar.gz

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.
Files changed (34) hide show
  1. mcp_authkit-0.1.0/LICENSE +21 -0
  2. mcp_authkit-0.1.0/PKG-INFO +297 -0
  3. mcp_authkit-0.1.0/README.md +271 -0
  4. mcp_authkit-0.1.0/mcp_authkit.egg-info/PKG-INFO +297 -0
  5. mcp_authkit-0.1.0/mcp_authkit.egg-info/SOURCES.txt +32 -0
  6. mcp_authkit-0.1.0/mcp_authkit.egg-info/dependency_links.txt +1 -0
  7. mcp_authkit-0.1.0/mcp_authkit.egg-info/requires.txt +14 -0
  8. mcp_authkit-0.1.0/mcp_authkit.egg-info/top_level.txt +1 -0
  9. mcp_authkit-0.1.0/mcpauthkit/__init__.py +81 -0
  10. mcp_authkit-0.1.0/mcpauthkit/auth_middleware.py +176 -0
  11. mcp_authkit-0.1.0/mcpauthkit/auth_routes.py +130 -0
  12. mcp_authkit-0.1.0/mcpauthkit/jwt_validator.py +154 -0
  13. mcp_authkit-0.1.0/mcpauthkit/providers/__init__.py +4 -0
  14. mcp_authkit-0.1.0/mcpauthkit/providers/credentials_provider.py +522 -0
  15. mcp_authkit-0.1.0/mcpauthkit/providers/oauth_provider.py +690 -0
  16. mcp_authkit-0.1.0/mcpauthkit/providers/templates/base.html +109 -0
  17. mcp_authkit-0.1.0/mcpauthkit/providers/templates/credentials_entry.html +150 -0
  18. mcp_authkit-0.1.0/mcpauthkit/providers/templates/credentials_error.html +8 -0
  19. mcp_authkit-0.1.0/mcpauthkit/providers/templates/credentials_success.html +10 -0
  20. mcp_authkit-0.1.0/mcpauthkit/providers/templates/oauth_error.html +9 -0
  21. mcp_authkit-0.1.0/mcpauthkit/providers/templates/oauth_success.html +11 -0
  22. mcp_authkit-0.1.0/mcpauthkit/py.typed +0 -0
  23. mcp_authkit-0.1.0/mcpauthkit/store/__init__.py +36 -0
  24. mcp_authkit-0.1.0/mcpauthkit/store/base.py +84 -0
  25. mcp_authkit-0.1.0/mcpauthkit/store/encryption.py +88 -0
  26. mcp_authkit-0.1.0/mcpauthkit/store/factory.py +120 -0
  27. mcp_authkit-0.1.0/mcpauthkit/store/file_store.py +174 -0
  28. mcp_authkit-0.1.0/mcpauthkit/store/memory.py +110 -0
  29. mcp_authkit-0.1.0/mcpauthkit/store/redis_store.py +165 -0
  30. mcp_authkit-0.1.0/pyproject.toml +98 -0
  31. mcp_authkit-0.1.0/setup.cfg +4 -0
  32. mcp_authkit-0.1.0/tests/test_auth_middleware.py +147 -0
  33. mcp_authkit-0.1.0/tests/test_auth_routes.py +146 -0
  34. mcp_authkit-0.1.0/tests/test_jwt_validator.py +228 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 masterela
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,297 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-authkit
3
+ Version: 0.1.0
4
+ Summary: Pluggable OAuth 2.0 + credentials elicitation library for FastMCP servers
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/masterela/mcp-authkit
7
+ Project-URL: Documentation, https://masterela.github.io/mcp-authkit/
8
+ Project-URL: Repository, https://github.com/masterela/mcp-authkit
9
+ Project-URL: Changelog, https://github.com/masterela/mcp-authkit/blob/main/CHANGELOG.md
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: fastapi>=0.115
14
+ Requires-Dist: httpx>=0.27
15
+ Requires-Dist: python-jose[cryptography]>=3.3
16
+ Requires-Dist: mcp>=1.6
17
+ Requires-Dist: jinja2>=3.1
18
+ Requires-Dist: cryptography>=42
19
+ Provides-Extra: redis
20
+ Requires-Dist: redis>=5; extra == "redis"
21
+ Provides-Extra: experimental
22
+ Requires-Dist: uvicorn[standard]>=0.30; extra == "experimental"
23
+ Requires-Dist: pydantic-settings>=2.6; extra == "experimental"
24
+ Requires-Dist: python-dotenv>=1.0; extra == "experimental"
25
+ Dynamic: license-file
26
+
27
+ # mcp-authkit
28
+
29
+ [![CI](https://github.com/masterela/mcp-authkit/actions/workflows/ci.yml/badge.svg)](https://github.com/masterela/mcp-authkit/actions/workflows/ci.yml)
30
+ [![Coverage](https://codecov.io/gh/masterela/mcp-authkit/branch/main/graph/badge.svg)](https://codecov.io/gh/masterela/mcp-authkit)
31
+ [![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-blue)](https://masterela.github.io/mcp-authkit/)
32
+ [![PyPI version](https://img.shields.io/pypi/v/mcp-authkit)](https://pypi.org/project/mcp-authkit/)
33
+ [![Python](https://img.shields.io/pypi/pyversions/mcp-authkit)](https://pypi.org/project/mcp-authkit/)
34
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
35
+
36
+ Pluggable authentication library for [FastMCP](https://github.com/modelcontextprotocol/python-sdk) servers built on FastAPI / Starlette.
37
+
38
+ Supports two independent auth legs:
39
+
40
+ 1. **Primary leg** — every MCP session is gated behind a standard OIDC provider (Keycloak, Okta, Entra ID, Duende, Auth0, …) using JWT bearer tokens. The MCP client (e.g. VS Code Copilot) handles the PKCE flow automatically.
41
+ 2. **Secondary leg** — individual MCP tools can additionally require a third-party OAuth token or a PAT / API key, collected on demand via MCP elicitation.
42
+
43
+ ---
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install mcp-authkit
49
+
50
+ # Optional Redis storage backend
51
+ pip install "mcp-authkit[redis]"
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Package layout
57
+
58
+ ```
59
+ mcpauthkit/
60
+ ├── __init__.py # Public exports: OAuthProvider, CredentialsProvider, …
61
+ ├── auth_middleware.py # JwtAuthMiddleware (BaseHTTPMiddleware)
62
+ ├── auth_routes.py # oauth_meta_router() — well-known + DCR façade
63
+ ├── jwt_validator.py # OIDC JWKS-based JWT validation (provider-agnostic)
64
+ ├── providers/
65
+ │ ├── oauth_provider.py # OAuthProvider — third-party OAuth 2.0 leg
66
+ │ ├── credentials_provider.py # CredentialsProvider — PAT / API-key form
67
+ │ └── templates/ # Jinja2 HTML templates (no external CDN)
68
+ └── store/
69
+ ├── base.py # Abstract store interfaces
70
+ ├── memory.py # In-process store (dev / testing)
71
+ ├── file_store.py # Fernet-encrypted file store
72
+ ├── redis_store.py # Async Redis store (requires redis extra)
73
+ ├── encryption.py # Fernet key derivation helpers
74
+ └── factory.py # create_stores() — env-driven backend selection
75
+ ```
76
+
77
+ The repository also contains `server.py` (a complete example server using GitHub OAuth and Confluence credentials) and a `docker-compose.yml` / `keycloak-realm.json` for running a local Keycloak instance. See the [quickstart repo](https://github.com/masterela/mcp-authkit-quickstart) for a guided walkthrough.
78
+
79
+ ---
80
+
81
+ ## Primary auth leg — OIDC JWT validation
82
+
83
+ Every request to the MCP server must carry a valid `Authorization: Bearer <token>` issued by the configured OIDC provider. The middleware performs JWKS discovery automatically and caches keys for 10 minutes.
84
+
85
+ ```python
86
+ from mcpauthkit.auth_middleware import JwtAuthMiddleware
87
+ from mcpauthkit.auth_routes import oauth_meta_router
88
+
89
+ ISSUER_URL = "https://sso.example.com/realms/my-realm"
90
+ SERVER_URL = "https://my-mcp-server.example.com"
91
+ CLIENT_ID = "my-mcp-public-client" # pre-registered public client
92
+
93
+ # Publish RFC 8414 / MCP-spec well-known endpoints + DCR façade
94
+ app.include_router(oauth_meta_router(
95
+ server_base_url=SERVER_URL,
96
+ issuer_url=ISSUER_URL,
97
+ client_id=CLIENT_ID,
98
+ ))
99
+
100
+ # Validate JWT on every request; populate current_user ContextVar
101
+ app.add_middleware(
102
+ JwtAuthMiddleware,
103
+ issuer_url=ISSUER_URL,
104
+ current_user=current_user,
105
+ server_base_url=SERVER_URL,
106
+ open_paths=(
107
+ "/.well-known", "/health", "/register",
108
+ github_oauth.callback_path,
109
+ *confluence_creds.open_paths,
110
+ ),
111
+ )
112
+ ```
113
+
114
+ ### `oauth_meta_router`
115
+
116
+ Returns a FastAPI `APIRouter` with:
117
+
118
+ | Route | Purpose |
119
+ |---|---|
120
+ | `GET /.well-known/oauth-protected-resource` | RFC 9728 resource metadata |
121
+ | `GET /.well-known/oauth-protected-resource/{path}` | Wildcard variant (some clients append the resource path) |
122
+ | `GET /.well-known/oauth-authorization-server` | RFC 8414 authorization server metadata (proxied from the real OIDC provider) |
123
+ | `POST /register` | Dynamic Client Registration façade — always returns the pre-registered public client ID |
124
+
125
+ ### `JwtAuthMiddleware`
126
+
127
+ `BaseHTTPMiddleware` subclass. Parameters passed via `app.add_middleware(...)`:
128
+
129
+ | Parameter | Type | Description |
130
+ |---|---|---|
131
+ | `issuer_url` | `str` | OIDC issuer base URL (e.g. Keycloak realm URL) |
132
+ | `current_user` | `ContextVar` | Populated with verified claims on each authenticated request |
133
+ | `server_base_url` | `str` | Used in `WWW-Authenticate` realm / resource-metadata URIs |
134
+ | `open_paths` | `tuple[str, ...]` | Path prefixes that bypass authentication |
135
+
136
+ Returns `401` with a standards-compliant `WWW-Authenticate: Bearer …` header when authentication fails, triggering the PKCE flow in the MCP client automatically.
137
+
138
+ ### `jwt_validator`
139
+
140
+ Provider-agnostic OIDC JWT validation. Supports RS256/384/512, PS256/384/512, ES256/384/512, EdDSA. Discovers `jwks_uri` automatically via `{issuer_url}/.well-known/openid-configuration`. OIDC config and JWKS are cached for 10 minutes.
141
+
142
+ ---
143
+
144
+ ## Secondary auth leg — tool-level credential acquisition
145
+
146
+ Individual tools can be gated behind additional credentials collected on demand via [MCP elicitation](https://spec.modelcontextprotocol.io/specification/2025-11-25/client/elicitation/).
147
+
148
+ ### `OAuthProvider` — third-party OAuth 2.0
149
+
150
+ Gates a tool behind a full Authorization Code + PKCE flow with a third-party provider. The MCP client opens the provider's login page; the tool call suspends until the callback fires.
151
+
152
+ ```python
153
+ from mcpauthkit import OAuthProvider
154
+
155
+ github_oauth = OAuthProvider.from_standard_oauth2(
156
+ name="github",
157
+ authorization_url="https://github.com/login/oauth/authorize",
158
+ token_url="https://github.com/login/oauth/access_token",
159
+ client_id=os.environ["GITHUB_CLIENT_ID"],
160
+ client_secret=os.environ["GITHUB_CLIENT_SECRET"],
161
+ scope="read:user repo",
162
+ redirect_uri=f"{SERVER_URL}/github/callback",
163
+ user_context=current_user,
164
+ )
165
+ github_oauth.register(app) # registers GET /github/callback on the FastAPI app
166
+
167
+ @mcp.tool(description="List open PRs")
168
+ @github_oauth.require_token()
169
+ async def list_prs(ctx: Context, repo: str) -> str:
170
+ token = github_oauth.get_token() # guaranteed non-None inside the decorator
171
+ ...
172
+ ```
173
+
174
+ Key methods:
175
+
176
+ | Method | Description |
177
+ |---|---|
178
+ | `from_standard_oauth2(...)` | Factory for any standard OAuth 2.0 provider |
179
+ | `register(app)` | Register the callback route on the FastAPI app |
180
+ | `require_token(*, fail_fast=False)` | Decorator — elicits token if not cached, or raises immediately |
181
+ | `get_token()` | Return the cached access token for the current user (or `None`) |
182
+ | `invalidate_token(sub)` | Evict a user's cached token |
183
+ | `.callback_path` | The redirect URI path (add to `open_paths`) |
184
+
185
+ ### `CredentialsProvider` — PAT / API key form
186
+
187
+ Serves a self-hosted HTML form where the user enters credentials (PATs, API keys, etc.). Values are stored server-side, keyed by the primary OIDC `sub`. The form optionally renders a Markdown how-to guide (client-side via [marked.js](https://marked.js.org/)).
188
+
189
+ ```python
190
+ from mcpauthkit import CredentialsProvider
191
+
192
+ confluence_creds = CredentialsProvider(
193
+ name="confluence",
194
+ variables={
195
+ "pat": {
196
+ "label": "Personal Access Token",
197
+ "type": "password",
198
+ "placeholder": "Your Confluence PAT",
199
+ },
200
+ },
201
+ user_context=current_user,
202
+ server_base_url=SERVER_URL,
203
+ doc="docs/confluence_token_how.md", # optional — rendered above the form
204
+ )
205
+ confluence_creds.register(app)
206
+
207
+ @mcp.tool(description="List Confluence pages")
208
+ @confluence_creds.require_credentials()
209
+ async def list_pages(ctx: Context, space: str) -> str:
210
+ creds = confluence_creds.get_credentials() # {"pat": "..."}
211
+ ...
212
+ ```
213
+
214
+ Key methods / properties:
215
+
216
+ | Member | Description |
217
+ |---|---|
218
+ | `register(app)` | Register entry + submit routes on the FastAPI app |
219
+ | `require_credentials(*, fail_fast=False)` | Decorator — elicits credentials if not cached |
220
+ | `get_credentials()` | Return the cached credentials dict for the current user |
221
+ | `invalidate_credentials(sub)` | Evict a user's cached credentials |
222
+ | `.open_paths` | Tuple of paths to add to `JwtAuthMiddleware` `open_paths` |
223
+
224
+ ---
225
+
226
+ ## MCP mount
227
+
228
+ The FastMCP sub-app must be mounted at `/` **after** all routes are registered. Its internal Starlette router exposes the MCP endpoint at `/mcp`:
229
+
230
+ ```python
231
+ # All routes (include_router, register, @app.get) must come before this line
232
+ app.mount("/", app=mcp.streamable_http_app())
233
+ ```
234
+
235
+ The MCP client connects to `http://<host>:<port>/mcp`.
236
+
237
+ ---
238
+
239
+ ## HTML templates
240
+
241
+ All browser-facing pages use Jinja2 templates in `mcpauthkit/providers/templates/`. Every page extends `base.html` which provides:
242
+
243
+ - A blue "MCP Authentication 🔒" top bar
244
+ - A centered card layout
245
+ - Minimal inline CSS (no external CDN dependencies except `marked.js` on the credentials entry page)
246
+
247
+ ---
248
+
249
+ ## Getting started
250
+
251
+ A full working example (Docker Compose with Redis + Keycloak, a GitHub OAuth tool, and a Confluence credentials tool) lives in the companion repo:
252
+
253
+ **[mcp-authkit-quickstart](https://github.com/masterela/mcp-authkit-quickstart)**
254
+
255
+ ---
256
+
257
+ ## Storage backends
258
+
259
+ | Mode | Class | Notes |
260
+ |---|---|---|
261
+ | `memory` (default) | `MemoryTokenStore` / `MemoryPendingStore` | In-process. Tokens lost on restart. Suitable for development. |
262
+ | `file` | `FileTokenStore` / `FilePendingStore` | Fernet-encrypted JSON files. Good for single-instance deployments. |
263
+ | `redis` | `RedisTokenStore` / `RedisPendingStore` | Async Redis. Requires `pip install "mcp-authkit[redis]"`. Use for multi-replica deployments. |
264
+
265
+ Select a backend via the `TOKEN_STORAGE_MODE` environment variable (`memory` / `file` / `redis`), or call `create_stores()` directly.
266
+
267
+ ---
268
+
269
+ ## Dependencies
270
+
271
+ | Package | Purpose |
272
+ |---|---|
273
+ | `fastapi` | HTTP framework |
274
+ | `mcp>=1.6` | MCP server SDK (FastMCP) |
275
+ | `starlette` | ASGI primitives, `BaseHTTPMiddleware` |
276
+ | `python-jose[cryptography]` | JWT decoding and JWKS validation |
277
+ | `httpx` | Async HTTP client (token exchange, OIDC discovery) |
278
+ | `jinja2` | HTML template rendering |
279
+ | `cryptography` | Fernet encryption for file and Redis stores |
280
+
281
+ ---
282
+
283
+ ## Contributing
284
+
285
+ ```bash
286
+ # Install dev dependencies
287
+ uv sync --group dev
288
+
289
+ # Lint + type-check
290
+ uv run ruff check mcpauthkit/ tests/
291
+ uv run mypy mcpauthkit/
292
+
293
+ # Tests with coverage
294
+ uv run pytest --cov=mcpauthkit --cov-report=term-missing -q
295
+ ```
296
+
297
+ See [CHANGELOG.md](CHANGELOG.md) for release history.
@@ -0,0 +1,271 @@
1
+ # mcp-authkit
2
+
3
+ [![CI](https://github.com/masterela/mcp-authkit/actions/workflows/ci.yml/badge.svg)](https://github.com/masterela/mcp-authkit/actions/workflows/ci.yml)
4
+ [![Coverage](https://codecov.io/gh/masterela/mcp-authkit/branch/main/graph/badge.svg)](https://codecov.io/gh/masterela/mcp-authkit)
5
+ [![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-blue)](https://masterela.github.io/mcp-authkit/)
6
+ [![PyPI version](https://img.shields.io/pypi/v/mcp-authkit)](https://pypi.org/project/mcp-authkit/)
7
+ [![Python](https://img.shields.io/pypi/pyversions/mcp-authkit)](https://pypi.org/project/mcp-authkit/)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
9
+
10
+ Pluggable authentication library for [FastMCP](https://github.com/modelcontextprotocol/python-sdk) servers built on FastAPI / Starlette.
11
+
12
+ Supports two independent auth legs:
13
+
14
+ 1. **Primary leg** — every MCP session is gated behind a standard OIDC provider (Keycloak, Okta, Entra ID, Duende, Auth0, …) using JWT bearer tokens. The MCP client (e.g. VS Code Copilot) handles the PKCE flow automatically.
15
+ 2. **Secondary leg** — individual MCP tools can additionally require a third-party OAuth token or a PAT / API key, collected on demand via MCP elicitation.
16
+
17
+ ---
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install mcp-authkit
23
+
24
+ # Optional Redis storage backend
25
+ pip install "mcp-authkit[redis]"
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Package layout
31
+
32
+ ```
33
+ mcpauthkit/
34
+ ├── __init__.py # Public exports: OAuthProvider, CredentialsProvider, …
35
+ ├── auth_middleware.py # JwtAuthMiddleware (BaseHTTPMiddleware)
36
+ ├── auth_routes.py # oauth_meta_router() — well-known + DCR façade
37
+ ├── jwt_validator.py # OIDC JWKS-based JWT validation (provider-agnostic)
38
+ ├── providers/
39
+ │ ├── oauth_provider.py # OAuthProvider — third-party OAuth 2.0 leg
40
+ │ ├── credentials_provider.py # CredentialsProvider — PAT / API-key form
41
+ │ └── templates/ # Jinja2 HTML templates (no external CDN)
42
+ └── store/
43
+ ├── base.py # Abstract store interfaces
44
+ ├── memory.py # In-process store (dev / testing)
45
+ ├── file_store.py # Fernet-encrypted file store
46
+ ├── redis_store.py # Async Redis store (requires redis extra)
47
+ ├── encryption.py # Fernet key derivation helpers
48
+ └── factory.py # create_stores() — env-driven backend selection
49
+ ```
50
+
51
+ The repository also contains `server.py` (a complete example server using GitHub OAuth and Confluence credentials) and a `docker-compose.yml` / `keycloak-realm.json` for running a local Keycloak instance. See the [quickstart repo](https://github.com/masterela/mcp-authkit-quickstart) for a guided walkthrough.
52
+
53
+ ---
54
+
55
+ ## Primary auth leg — OIDC JWT validation
56
+
57
+ Every request to the MCP server must carry a valid `Authorization: Bearer <token>` issued by the configured OIDC provider. The middleware performs JWKS discovery automatically and caches keys for 10 minutes.
58
+
59
+ ```python
60
+ from mcpauthkit.auth_middleware import JwtAuthMiddleware
61
+ from mcpauthkit.auth_routes import oauth_meta_router
62
+
63
+ ISSUER_URL = "https://sso.example.com/realms/my-realm"
64
+ SERVER_URL = "https://my-mcp-server.example.com"
65
+ CLIENT_ID = "my-mcp-public-client" # pre-registered public client
66
+
67
+ # Publish RFC 8414 / MCP-spec well-known endpoints + DCR façade
68
+ app.include_router(oauth_meta_router(
69
+ server_base_url=SERVER_URL,
70
+ issuer_url=ISSUER_URL,
71
+ client_id=CLIENT_ID,
72
+ ))
73
+
74
+ # Validate JWT on every request; populate current_user ContextVar
75
+ app.add_middleware(
76
+ JwtAuthMiddleware,
77
+ issuer_url=ISSUER_URL,
78
+ current_user=current_user,
79
+ server_base_url=SERVER_URL,
80
+ open_paths=(
81
+ "/.well-known", "/health", "/register",
82
+ github_oauth.callback_path,
83
+ *confluence_creds.open_paths,
84
+ ),
85
+ )
86
+ ```
87
+
88
+ ### `oauth_meta_router`
89
+
90
+ Returns a FastAPI `APIRouter` with:
91
+
92
+ | Route | Purpose |
93
+ |---|---|
94
+ | `GET /.well-known/oauth-protected-resource` | RFC 9728 resource metadata |
95
+ | `GET /.well-known/oauth-protected-resource/{path}` | Wildcard variant (some clients append the resource path) |
96
+ | `GET /.well-known/oauth-authorization-server` | RFC 8414 authorization server metadata (proxied from the real OIDC provider) |
97
+ | `POST /register` | Dynamic Client Registration façade — always returns the pre-registered public client ID |
98
+
99
+ ### `JwtAuthMiddleware`
100
+
101
+ `BaseHTTPMiddleware` subclass. Parameters passed via `app.add_middleware(...)`:
102
+
103
+ | Parameter | Type | Description |
104
+ |---|---|---|
105
+ | `issuer_url` | `str` | OIDC issuer base URL (e.g. Keycloak realm URL) |
106
+ | `current_user` | `ContextVar` | Populated with verified claims on each authenticated request |
107
+ | `server_base_url` | `str` | Used in `WWW-Authenticate` realm / resource-metadata URIs |
108
+ | `open_paths` | `tuple[str, ...]` | Path prefixes that bypass authentication |
109
+
110
+ Returns `401` with a standards-compliant `WWW-Authenticate: Bearer …` header when authentication fails, triggering the PKCE flow in the MCP client automatically.
111
+
112
+ ### `jwt_validator`
113
+
114
+ Provider-agnostic OIDC JWT validation. Supports RS256/384/512, PS256/384/512, ES256/384/512, EdDSA. Discovers `jwks_uri` automatically via `{issuer_url}/.well-known/openid-configuration`. OIDC config and JWKS are cached for 10 minutes.
115
+
116
+ ---
117
+
118
+ ## Secondary auth leg — tool-level credential acquisition
119
+
120
+ Individual tools can be gated behind additional credentials collected on demand via [MCP elicitation](https://spec.modelcontextprotocol.io/specification/2025-11-25/client/elicitation/).
121
+
122
+ ### `OAuthProvider` — third-party OAuth 2.0
123
+
124
+ Gates a tool behind a full Authorization Code + PKCE flow with a third-party provider. The MCP client opens the provider's login page; the tool call suspends until the callback fires.
125
+
126
+ ```python
127
+ from mcpauthkit import OAuthProvider
128
+
129
+ github_oauth = OAuthProvider.from_standard_oauth2(
130
+ name="github",
131
+ authorization_url="https://github.com/login/oauth/authorize",
132
+ token_url="https://github.com/login/oauth/access_token",
133
+ client_id=os.environ["GITHUB_CLIENT_ID"],
134
+ client_secret=os.environ["GITHUB_CLIENT_SECRET"],
135
+ scope="read:user repo",
136
+ redirect_uri=f"{SERVER_URL}/github/callback",
137
+ user_context=current_user,
138
+ )
139
+ github_oauth.register(app) # registers GET /github/callback on the FastAPI app
140
+
141
+ @mcp.tool(description="List open PRs")
142
+ @github_oauth.require_token()
143
+ async def list_prs(ctx: Context, repo: str) -> str:
144
+ token = github_oauth.get_token() # guaranteed non-None inside the decorator
145
+ ...
146
+ ```
147
+
148
+ Key methods:
149
+
150
+ | Method | Description |
151
+ |---|---|
152
+ | `from_standard_oauth2(...)` | Factory for any standard OAuth 2.0 provider |
153
+ | `register(app)` | Register the callback route on the FastAPI app |
154
+ | `require_token(*, fail_fast=False)` | Decorator — elicits token if not cached, or raises immediately |
155
+ | `get_token()` | Return the cached access token for the current user (or `None`) |
156
+ | `invalidate_token(sub)` | Evict a user's cached token |
157
+ | `.callback_path` | The redirect URI path (add to `open_paths`) |
158
+
159
+ ### `CredentialsProvider` — PAT / API key form
160
+
161
+ Serves a self-hosted HTML form where the user enters credentials (PATs, API keys, etc.). Values are stored server-side, keyed by the primary OIDC `sub`. The form optionally renders a Markdown how-to guide (client-side via [marked.js](https://marked.js.org/)).
162
+
163
+ ```python
164
+ from mcpauthkit import CredentialsProvider
165
+
166
+ confluence_creds = CredentialsProvider(
167
+ name="confluence",
168
+ variables={
169
+ "pat": {
170
+ "label": "Personal Access Token",
171
+ "type": "password",
172
+ "placeholder": "Your Confluence PAT",
173
+ },
174
+ },
175
+ user_context=current_user,
176
+ server_base_url=SERVER_URL,
177
+ doc="docs/confluence_token_how.md", # optional — rendered above the form
178
+ )
179
+ confluence_creds.register(app)
180
+
181
+ @mcp.tool(description="List Confluence pages")
182
+ @confluence_creds.require_credentials()
183
+ async def list_pages(ctx: Context, space: str) -> str:
184
+ creds = confluence_creds.get_credentials() # {"pat": "..."}
185
+ ...
186
+ ```
187
+
188
+ Key methods / properties:
189
+
190
+ | Member | Description |
191
+ |---|---|
192
+ | `register(app)` | Register entry + submit routes on the FastAPI app |
193
+ | `require_credentials(*, fail_fast=False)` | Decorator — elicits credentials if not cached |
194
+ | `get_credentials()` | Return the cached credentials dict for the current user |
195
+ | `invalidate_credentials(sub)` | Evict a user's cached credentials |
196
+ | `.open_paths` | Tuple of paths to add to `JwtAuthMiddleware` `open_paths` |
197
+
198
+ ---
199
+
200
+ ## MCP mount
201
+
202
+ The FastMCP sub-app must be mounted at `/` **after** all routes are registered. Its internal Starlette router exposes the MCP endpoint at `/mcp`:
203
+
204
+ ```python
205
+ # All routes (include_router, register, @app.get) must come before this line
206
+ app.mount("/", app=mcp.streamable_http_app())
207
+ ```
208
+
209
+ The MCP client connects to `http://<host>:<port>/mcp`.
210
+
211
+ ---
212
+
213
+ ## HTML templates
214
+
215
+ All browser-facing pages use Jinja2 templates in `mcpauthkit/providers/templates/`. Every page extends `base.html` which provides:
216
+
217
+ - A blue "MCP Authentication 🔒" top bar
218
+ - A centered card layout
219
+ - Minimal inline CSS (no external CDN dependencies except `marked.js` on the credentials entry page)
220
+
221
+ ---
222
+
223
+ ## Getting started
224
+
225
+ A full working example (Docker Compose with Redis + Keycloak, a GitHub OAuth tool, and a Confluence credentials tool) lives in the companion repo:
226
+
227
+ **[mcp-authkit-quickstart](https://github.com/masterela/mcp-authkit-quickstart)**
228
+
229
+ ---
230
+
231
+ ## Storage backends
232
+
233
+ | Mode | Class | Notes |
234
+ |---|---|---|
235
+ | `memory` (default) | `MemoryTokenStore` / `MemoryPendingStore` | In-process. Tokens lost on restart. Suitable for development. |
236
+ | `file` | `FileTokenStore` / `FilePendingStore` | Fernet-encrypted JSON files. Good for single-instance deployments. |
237
+ | `redis` | `RedisTokenStore` / `RedisPendingStore` | Async Redis. Requires `pip install "mcp-authkit[redis]"`. Use for multi-replica deployments. |
238
+
239
+ Select a backend via the `TOKEN_STORAGE_MODE` environment variable (`memory` / `file` / `redis`), or call `create_stores()` directly.
240
+
241
+ ---
242
+
243
+ ## Dependencies
244
+
245
+ | Package | Purpose |
246
+ |---|---|
247
+ | `fastapi` | HTTP framework |
248
+ | `mcp>=1.6` | MCP server SDK (FastMCP) |
249
+ | `starlette` | ASGI primitives, `BaseHTTPMiddleware` |
250
+ | `python-jose[cryptography]` | JWT decoding and JWKS validation |
251
+ | `httpx` | Async HTTP client (token exchange, OIDC discovery) |
252
+ | `jinja2` | HTML template rendering |
253
+ | `cryptography` | Fernet encryption for file and Redis stores |
254
+
255
+ ---
256
+
257
+ ## Contributing
258
+
259
+ ```bash
260
+ # Install dev dependencies
261
+ uv sync --group dev
262
+
263
+ # Lint + type-check
264
+ uv run ruff check mcpauthkit/ tests/
265
+ uv run mypy mcpauthkit/
266
+
267
+ # Tests with coverage
268
+ uv run pytest --cov=mcpauthkit --cov-report=term-missing -q
269
+ ```
270
+
271
+ See [CHANGELOG.md](CHANGELOG.md) for release history.