mcp-authflow 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.
@@ -0,0 +1,43 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ lint:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: astral-sh/setup-uv@v6
15
+ with:
16
+ enable-cache: true
17
+ - run: uv sync --frozen
18
+ - run: uv run ruff check .
19
+ - run: uv run ruff format --check .
20
+
21
+ typecheck:
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ - uses: astral-sh/setup-uv@v6
26
+ with:
27
+ enable-cache: true
28
+ - run: uv sync --frozen
29
+ - run: uv run pyright
30
+
31
+ test:
32
+ runs-on: ubuntu-latest
33
+ strategy:
34
+ matrix:
35
+ python-version: ["3.11", "3.12", "3.13"]
36
+ steps:
37
+ - uses: actions/checkout@v4
38
+ - uses: astral-sh/setup-uv@v6
39
+ with:
40
+ enable-cache: true
41
+ - run: uv python install ${{ matrix.python-version }}
42
+ - run: uv sync --frozen --python ${{ matrix.python-version }}
43
+ - run: uv run pytest --tb=short
@@ -0,0 +1,21 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ id-token: write
10
+
11
+ jobs:
12
+ publish:
13
+ runs-on: ubuntu-latest
14
+ environment: pypi
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v6
18
+ with:
19
+ enable-cache: true
20
+ - run: uv build
21
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .eggs/
8
+ *.egg
9
+ .venv/
10
+ venv/
11
+ .ruff_cache/
12
+ .pyright/
13
+ .pytest_cache/
14
+ .coverage
15
+ htmlcov/
@@ -0,0 +1 @@
1
+ 3.11
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 brooksmcmillin
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,306 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-authflow
3
+ Version: 0.1.0
4
+ Summary: OAuth 2.0 Authorization Server framework for MCP (Model Context Protocol) servers
5
+ Project-URL: Homepage, https://github.com/brooksmcmillin/mcpauth
6
+ Project-URL: Documentation, https://github.com/brooksmcmillin/mcpauth#readme
7
+ Project-URL: Repository, https://github.com/brooksmcmillin/mcpauth
8
+ Project-URL: Issues, https://github.com/brooksmcmillin/mcpauth/issues
9
+ Author: Brooks McMillin
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Framework :: AsyncIO
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Internet :: WWW/HTTP
21
+ Classifier: Topic :: Security
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: aiohttp>=3.13.0
25
+ Requires-Dist: mcp>=1.24.0
26
+ Requires-Dist: pydantic-settings>=2.0.0
27
+ Requires-Dist: pydantic>=2.0.0
28
+ Requires-Dist: starlette>=0.40.0
29
+ Provides-Extra: postgres
30
+ Requires-Dist: asyncpg>=0.31.0; extra == 'postgres'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # mcp-authflow
34
+
35
+ OAuth 2.0 Authorization Server framework for [MCP](https://modelcontextprotocol.io/) servers. Issue and manage tokens that protect MCP tool access.
36
+
37
+ Pair with [mcp-authflow-resource](https://github.com/brooksmcmillin/mcpauth-resource) on the resource server side.
38
+
39
+ ## Features
40
+
41
+ - **Token storage** with PostgreSQL and in-memory backends
42
+ - **RFC 6749** standardized OAuth error responses
43
+ - **Sliding-window rate limiting** for token endpoints
44
+ - **Input validation** for client IDs, scopes, and PKCE parameters
45
+ - **CORS helpers** with origin allowlisting
46
+ - **Async-first** design, built on Starlette
47
+
48
+ ## Installation
49
+
50
+ ```bash
51
+ pip install mcp-authflow
52
+
53
+ # With PostgreSQL token storage (production)
54
+ pip install mcp-authflow[postgres]
55
+ ```
56
+
57
+ ## Quick Start
58
+
59
+ Build an OAuth authorization server that issues tokens for MCP clients:
60
+
61
+ ```python
62
+ import secrets
63
+ import time
64
+ from contextlib import asynccontextmanager
65
+
66
+ from starlette.applications import Starlette
67
+ from starlette.requests import Request
68
+ from starlette.responses import JSONResponse
69
+ from starlette.routing import Route
70
+
71
+ from mcp_auth_framework.rate_limiting import SlidingWindowRateLimiter
72
+ from mcp_auth_framework.responses import invalid_request, rate_limit_exceeded
73
+ from mcp_auth_framework.storage import MemoryTokenStorage
74
+ from mcp_auth_framework.validation import parse_scope_field, validate_client_id
75
+
76
+ # --- Setup ---
77
+
78
+ storage = MemoryTokenStorage() # Use PostgresTokenStorage for production
79
+ limiter = SlidingWindowRateLimiter(requests_per_window=60, window_seconds=3600)
80
+
81
+
82
+ # --- Token endpoint ---
83
+
84
+ async def token_endpoint(request: Request) -> JSONResponse:
85
+ form = await request.form()
86
+ client_id = str(form.get("client_id", ""))
87
+
88
+ # Rate limit per client
89
+ if not limiter.is_allowed(client_id):
90
+ return rate_limit_exceeded(
91
+ "Too many requests",
92
+ retry_after=limiter.get_retry_after(client_id),
93
+ )
94
+
95
+ # Validate client
96
+ if not validate_client_id(client_id):
97
+ return invalid_request("Invalid client_id format")
98
+
99
+ # Issue token
100
+ token = secrets.token_urlsafe(32)
101
+ scopes = parse_scope_field(form.get("scope"))
102
+ expires_at = int(time.time()) + 3600
103
+
104
+ await storage.store_token(
105
+ token=token,
106
+ client_id=client_id,
107
+ scopes=scopes.split(),
108
+ expires_at=expires_at,
109
+ resource=str(form.get("resource", "")),
110
+ )
111
+
112
+ return JSONResponse({
113
+ "access_token": token,
114
+ "token_type": "bearer",
115
+ "expires_in": 3600,
116
+ "scope": scopes,
117
+ })
118
+
119
+
120
+ # --- Introspection endpoint (called by resource servers) ---
121
+
122
+ async def introspect_endpoint(request: Request) -> JSONResponse:
123
+ form = await request.form()
124
+ token = str(form.get("token", ""))
125
+
126
+ token_data = await storage.load_token(token)
127
+ if not token_data or token_data["expires_at"] < time.time():
128
+ return JSONResponse({"active": False})
129
+
130
+ return JSONResponse({
131
+ "active": True,
132
+ "client_id": token_data["client_id"],
133
+ "scope": " ".join(token_data["scopes"]),
134
+ "exp": token_data["expires_at"],
135
+ "aud": token_data.get("resource", ""),
136
+ })
137
+
138
+
139
+ @asynccontextmanager
140
+ async def lifespan(app):
141
+ await storage.initialize()
142
+ yield
143
+ await storage.close()
144
+
145
+
146
+ app = Starlette(
147
+ routes=[
148
+ Route("/token", token_endpoint, methods=["POST"]),
149
+ Route("/introspect", introspect_endpoint, methods=["POST"]),
150
+ ],
151
+ lifespan=lifespan,
152
+ )
153
+ ```
154
+
155
+ Run with: `uvicorn myapp:app --port 8000`
156
+
157
+ ## Architecture
158
+
159
+ ```
160
+ MCP Client (Claude, etc.)
161
+ |
162
+ 1. Authorization request
163
+ |
164
+ v
165
+ +---------------------+
166
+ | Auth Server | <-- this package
167
+ | (mcp-authflow) |
168
+ | |
169
+ | /token | 2. Issues access token
170
+ | /introspect | 4. Validates token
171
+ +---------------------+
172
+ ^
173
+ |
174
+ 4. Token introspection (RFC 7662)
175
+ |
176
+ +---------------------+
177
+ | Resource Server | <-- mcp-authflow-resource
178
+ | (MCP tools) |
179
+ | |
180
+ | 3. Client calls |
181
+ | MCP tools with |
182
+ | Bearer token |
183
+ +---------------------+
184
+ ```
185
+
186
+ 1. MCP client authenticates with the auth server
187
+ 2. Auth server issues an access token (stored in PostgreSQL or memory)
188
+ 3. Client calls MCP tools on the resource server with the Bearer token
189
+ 4. Resource server validates the token by calling the auth server's `/introspect` endpoint
190
+
191
+ ## API Reference
192
+
193
+ ### Token Storage
194
+
195
+ Abstract base class with two implementations:
196
+
197
+ ```python
198
+ from mcp_auth_framework.storage import MemoryTokenStorage, PostgresTokenStorage
199
+
200
+ # In-memory (development/testing)
201
+ storage = MemoryTokenStorage()
202
+
203
+ # PostgreSQL (production) -- requires `postgres` extra
204
+ storage = PostgresTokenStorage(database_url="postgresql://user:pass@host/db")
205
+ # Or reads DATABASE_URL env var if no argument provided
206
+ storage = PostgresTokenStorage()
207
+
208
+ await storage.initialize() # Create tables / prepare connections
209
+ ```
210
+
211
+ **Storage interface:**
212
+
213
+ | Method | Description |
214
+ |--------|-------------|
215
+ | `store_token(token, client_id, scopes, expires_at, resource?, user_id?)` | Store an access token |
216
+ | `load_token(token) -> dict \| None` | Look up a token |
217
+ | `delete_token(token)` | Revoke a token |
218
+ | `cleanup_expired_tokens() -> int` | Purge expired tokens, returns count |
219
+ | `get_token_count() -> int` | Count active tokens |
220
+ | `store_refresh_token(...)` | Store a refresh token (same interface) |
221
+ | `load_refresh_token(token) -> dict \| None` | Look up a refresh token |
222
+ | `delete_refresh_token(token)` | Revoke a refresh token |
223
+ | `cleanup_expired_refresh_tokens() -> int` | Purge expired refresh tokens, returns count |
224
+
225
+ Token data returned by `load_token()`:
226
+
227
+ ```python
228
+ {
229
+ "token": str,
230
+ "client_id": str,
231
+ "scopes": list[str],
232
+ "resource": str | None, # RFC 8707 resource binding
233
+ "expires_at": int, # Unix timestamp
234
+ "created_at": int, # Unix timestamp
235
+ "user_id": int | None,
236
+ }
237
+ ```
238
+
239
+ ### OAuth Error Responses
240
+
241
+ Standardized error helpers following RFC 6749:
242
+
243
+ ```python
244
+ from mcp_auth_framework.responses import (
245
+ invalid_request, # 400 - Missing/invalid parameters
246
+ invalid_client, # 401 - Authentication failure
247
+ invalid_grant, # 400 - Expired/invalid code or token
248
+ invalid_scope, # 400 - Scope violation
249
+ slow_down, # 400 - Device flow rate limiting
250
+ rate_limit_exceeded, # 429 - Too many requests
251
+ server_error, # 500 - Internal error
252
+ backend_timeout, # 504 - Upstream timeout
253
+ )
254
+ ```
255
+
256
+ Each returns a Starlette `JSONResponse` with the appropriate status code and `Cache-Control: no-store` header.
257
+
258
+ ### Rate Limiting
259
+
260
+ ```python
261
+ from mcp_auth_framework.rate_limiting import SlidingWindowRateLimiter
262
+
263
+ limiter = SlidingWindowRateLimiter(
264
+ requests_per_window=60, # Max requests per window
265
+ window_seconds=3600, # Window duration (1 hour)
266
+ )
267
+
268
+ if not limiter.is_allowed(client_id):
269
+ retry_after = limiter.get_retry_after(client_id) # Seconds until next allowed request
270
+ ```
271
+
272
+ ### Input Validation
273
+
274
+ ```python
275
+ from mcp_auth_framework.validation import validate_client_id, parse_scope_field
276
+
277
+ validate_client_id("my-client-123") # True (alphanumeric + hyphens/underscores)
278
+ validate_client_id("") # False
279
+
280
+ parse_scope_field("read write") # "read write"
281
+ parse_scope_field(["read", "write"]) # "read write"
282
+ parse_scope_field(None) # "read" (default)
283
+ ```
284
+
285
+ ### CORS
286
+
287
+ ```python
288
+ from mcp_auth_framework.cors import parse_allowed_origins, build_cors_headers
289
+
290
+ # Reads ALLOWED_MCP_ORIGINS env var (comma-separated)
291
+ origins = parse_allowed_origins()
292
+
293
+ # Returns CORS headers if request origin is in allowlist
294
+ headers = build_cors_headers(request, origins)
295
+ ```
296
+
297
+ ## Configuration
298
+
299
+ | Env Variable | Description | Default |
300
+ |-------------|-------------|---------|
301
+ | `DATABASE_URL` | PostgreSQL connection string (for `PostgresTokenStorage`) | Required for postgres |
302
+ | `ALLOWED_MCP_ORIGINS` | Comma-separated allowed CORS origins | Empty (no CORS) |
303
+
304
+ ## License
305
+
306
+ MIT
@@ -0,0 +1,274 @@
1
+ # mcp-authflow
2
+
3
+ OAuth 2.0 Authorization Server framework for [MCP](https://modelcontextprotocol.io/) servers. Issue and manage tokens that protect MCP tool access.
4
+
5
+ Pair with [mcp-authflow-resource](https://github.com/brooksmcmillin/mcpauth-resource) on the resource server side.
6
+
7
+ ## Features
8
+
9
+ - **Token storage** with PostgreSQL and in-memory backends
10
+ - **RFC 6749** standardized OAuth error responses
11
+ - **Sliding-window rate limiting** for token endpoints
12
+ - **Input validation** for client IDs, scopes, and PKCE parameters
13
+ - **CORS helpers** with origin allowlisting
14
+ - **Async-first** design, built on Starlette
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pip install mcp-authflow
20
+
21
+ # With PostgreSQL token storage (production)
22
+ pip install mcp-authflow[postgres]
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ Build an OAuth authorization server that issues tokens for MCP clients:
28
+
29
+ ```python
30
+ import secrets
31
+ import time
32
+ from contextlib import asynccontextmanager
33
+
34
+ from starlette.applications import Starlette
35
+ from starlette.requests import Request
36
+ from starlette.responses import JSONResponse
37
+ from starlette.routing import Route
38
+
39
+ from mcp_auth_framework.rate_limiting import SlidingWindowRateLimiter
40
+ from mcp_auth_framework.responses import invalid_request, rate_limit_exceeded
41
+ from mcp_auth_framework.storage import MemoryTokenStorage
42
+ from mcp_auth_framework.validation import parse_scope_field, validate_client_id
43
+
44
+ # --- Setup ---
45
+
46
+ storage = MemoryTokenStorage() # Use PostgresTokenStorage for production
47
+ limiter = SlidingWindowRateLimiter(requests_per_window=60, window_seconds=3600)
48
+
49
+
50
+ # --- Token endpoint ---
51
+
52
+ async def token_endpoint(request: Request) -> JSONResponse:
53
+ form = await request.form()
54
+ client_id = str(form.get("client_id", ""))
55
+
56
+ # Rate limit per client
57
+ if not limiter.is_allowed(client_id):
58
+ return rate_limit_exceeded(
59
+ "Too many requests",
60
+ retry_after=limiter.get_retry_after(client_id),
61
+ )
62
+
63
+ # Validate client
64
+ if not validate_client_id(client_id):
65
+ return invalid_request("Invalid client_id format")
66
+
67
+ # Issue token
68
+ token = secrets.token_urlsafe(32)
69
+ scopes = parse_scope_field(form.get("scope"))
70
+ expires_at = int(time.time()) + 3600
71
+
72
+ await storage.store_token(
73
+ token=token,
74
+ client_id=client_id,
75
+ scopes=scopes.split(),
76
+ expires_at=expires_at,
77
+ resource=str(form.get("resource", "")),
78
+ )
79
+
80
+ return JSONResponse({
81
+ "access_token": token,
82
+ "token_type": "bearer",
83
+ "expires_in": 3600,
84
+ "scope": scopes,
85
+ })
86
+
87
+
88
+ # --- Introspection endpoint (called by resource servers) ---
89
+
90
+ async def introspect_endpoint(request: Request) -> JSONResponse:
91
+ form = await request.form()
92
+ token = str(form.get("token", ""))
93
+
94
+ token_data = await storage.load_token(token)
95
+ if not token_data or token_data["expires_at"] < time.time():
96
+ return JSONResponse({"active": False})
97
+
98
+ return JSONResponse({
99
+ "active": True,
100
+ "client_id": token_data["client_id"],
101
+ "scope": " ".join(token_data["scopes"]),
102
+ "exp": token_data["expires_at"],
103
+ "aud": token_data.get("resource", ""),
104
+ })
105
+
106
+
107
+ @asynccontextmanager
108
+ async def lifespan(app):
109
+ await storage.initialize()
110
+ yield
111
+ await storage.close()
112
+
113
+
114
+ app = Starlette(
115
+ routes=[
116
+ Route("/token", token_endpoint, methods=["POST"]),
117
+ Route("/introspect", introspect_endpoint, methods=["POST"]),
118
+ ],
119
+ lifespan=lifespan,
120
+ )
121
+ ```
122
+
123
+ Run with: `uvicorn myapp:app --port 8000`
124
+
125
+ ## Architecture
126
+
127
+ ```
128
+ MCP Client (Claude, etc.)
129
+ |
130
+ 1. Authorization request
131
+ |
132
+ v
133
+ +---------------------+
134
+ | Auth Server | <-- this package
135
+ | (mcp-authflow) |
136
+ | |
137
+ | /token | 2. Issues access token
138
+ | /introspect | 4. Validates token
139
+ +---------------------+
140
+ ^
141
+ |
142
+ 4. Token introspection (RFC 7662)
143
+ |
144
+ +---------------------+
145
+ | Resource Server | <-- mcp-authflow-resource
146
+ | (MCP tools) |
147
+ | |
148
+ | 3. Client calls |
149
+ | MCP tools with |
150
+ | Bearer token |
151
+ +---------------------+
152
+ ```
153
+
154
+ 1. MCP client authenticates with the auth server
155
+ 2. Auth server issues an access token (stored in PostgreSQL or memory)
156
+ 3. Client calls MCP tools on the resource server with the Bearer token
157
+ 4. Resource server validates the token by calling the auth server's `/introspect` endpoint
158
+
159
+ ## API Reference
160
+
161
+ ### Token Storage
162
+
163
+ Abstract base class with two implementations:
164
+
165
+ ```python
166
+ from mcp_auth_framework.storage import MemoryTokenStorage, PostgresTokenStorage
167
+
168
+ # In-memory (development/testing)
169
+ storage = MemoryTokenStorage()
170
+
171
+ # PostgreSQL (production) -- requires `postgres` extra
172
+ storage = PostgresTokenStorage(database_url="postgresql://user:pass@host/db")
173
+ # Or reads DATABASE_URL env var if no argument provided
174
+ storage = PostgresTokenStorage()
175
+
176
+ await storage.initialize() # Create tables / prepare connections
177
+ ```
178
+
179
+ **Storage interface:**
180
+
181
+ | Method | Description |
182
+ |--------|-------------|
183
+ | `store_token(token, client_id, scopes, expires_at, resource?, user_id?)` | Store an access token |
184
+ | `load_token(token) -> dict \| None` | Look up a token |
185
+ | `delete_token(token)` | Revoke a token |
186
+ | `cleanup_expired_tokens() -> int` | Purge expired tokens, returns count |
187
+ | `get_token_count() -> int` | Count active tokens |
188
+ | `store_refresh_token(...)` | Store a refresh token (same interface) |
189
+ | `load_refresh_token(token) -> dict \| None` | Look up a refresh token |
190
+ | `delete_refresh_token(token)` | Revoke a refresh token |
191
+ | `cleanup_expired_refresh_tokens() -> int` | Purge expired refresh tokens, returns count |
192
+
193
+ Token data returned by `load_token()`:
194
+
195
+ ```python
196
+ {
197
+ "token": str,
198
+ "client_id": str,
199
+ "scopes": list[str],
200
+ "resource": str | None, # RFC 8707 resource binding
201
+ "expires_at": int, # Unix timestamp
202
+ "created_at": int, # Unix timestamp
203
+ "user_id": int | None,
204
+ }
205
+ ```
206
+
207
+ ### OAuth Error Responses
208
+
209
+ Standardized error helpers following RFC 6749:
210
+
211
+ ```python
212
+ from mcp_auth_framework.responses import (
213
+ invalid_request, # 400 - Missing/invalid parameters
214
+ invalid_client, # 401 - Authentication failure
215
+ invalid_grant, # 400 - Expired/invalid code or token
216
+ invalid_scope, # 400 - Scope violation
217
+ slow_down, # 400 - Device flow rate limiting
218
+ rate_limit_exceeded, # 429 - Too many requests
219
+ server_error, # 500 - Internal error
220
+ backend_timeout, # 504 - Upstream timeout
221
+ )
222
+ ```
223
+
224
+ Each returns a Starlette `JSONResponse` with the appropriate status code and `Cache-Control: no-store` header.
225
+
226
+ ### Rate Limiting
227
+
228
+ ```python
229
+ from mcp_auth_framework.rate_limiting import SlidingWindowRateLimiter
230
+
231
+ limiter = SlidingWindowRateLimiter(
232
+ requests_per_window=60, # Max requests per window
233
+ window_seconds=3600, # Window duration (1 hour)
234
+ )
235
+
236
+ if not limiter.is_allowed(client_id):
237
+ retry_after = limiter.get_retry_after(client_id) # Seconds until next allowed request
238
+ ```
239
+
240
+ ### Input Validation
241
+
242
+ ```python
243
+ from mcp_auth_framework.validation import validate_client_id, parse_scope_field
244
+
245
+ validate_client_id("my-client-123") # True (alphanumeric + hyphens/underscores)
246
+ validate_client_id("") # False
247
+
248
+ parse_scope_field("read write") # "read write"
249
+ parse_scope_field(["read", "write"]) # "read write"
250
+ parse_scope_field(None) # "read" (default)
251
+ ```
252
+
253
+ ### CORS
254
+
255
+ ```python
256
+ from mcp_auth_framework.cors import parse_allowed_origins, build_cors_headers
257
+
258
+ # Reads ALLOWED_MCP_ORIGINS env var (comma-separated)
259
+ origins = parse_allowed_origins()
260
+
261
+ # Returns CORS headers if request origin is in allowlist
262
+ headers = build_cors_headers(request, origins)
263
+ ```
264
+
265
+ ## Configuration
266
+
267
+ | Env Variable | Description | Default |
268
+ |-------------|-------------|---------|
269
+ | `DATABASE_URL` | PostgreSQL connection string (for `PostgresTokenStorage`) | Required for postgres |
270
+ | `ALLOWED_MCP_ORIGINS` | Comma-separated allowed CORS origins | Empty (no CORS) |
271
+
272
+ ## License
273
+
274
+ MIT