Python SDK
Protect your application in five lines. The Python SDK wraps your existing FastAPI / Starlette handler so AuthSec owns authentication, scope-level authorization, and tool inventory — your code keeps doing what it already does.
Install
pip install authsec-sdk
Requires Python 3.10+. The package is on PyPI.
Drop in the wrapper
from fastapi import FastAPI
from authsec_sdk import from_env, mount_mcp
app = FastAPI()
cfg = from_env()
mount_mcp(app, "/mcp", your_existing_handler, cfg)
That's it. mount_mcp registers two routes:
/mcp— your handler, wrapped: every request has its bearer token validated andtools/callchecked against the scope policy you configured in AuthSec./.well-known/oauth-protected-resource/mcp— the RFC 9728 metadata that lets OAuth clients discover where to authenticate.
mount_mcp, Config, PolicyMode, ValidationMode, from_env, Runtime, and the error types are all importable from the top-level authsec_sdk package or from authsec_sdk.runtime (they're the same objects).
The five-step launch flow
The SDK is step 2 of a five-step product flow. The rest happens in the Applications screen of the admin UI.
1. Register the application
Open Applications → Install protection. Enter a name, your Public Base URL, and the Protected Path (defaults to /mcp). AuthSec generates a one-time introspection secret — copy it, you only see it once. The application's id becomes resource_server_id and introspection_client_id in your Config.
More: Register an application.
2. Install protection (this page)
Add the snippet above. Two values from step 1 land in Config:
resource_server_id— the application UUIDintrospection_client_secret— read from a secret store, never hard-coded
Start your app. The SDK validates the config, fetches the scope policy, and (with publish_manifest=True) pushes your tool list to AuthSec in the background.
3. Publish the tool manifest
A tool manifest is the list of tools your application exposes, with each tool's required scopes. publish_manifest=True does this for you on every startup. The SDK runs initialize → notifications/initialized → tools/list against your unwrapped handler and PUTs the result to AuthSec. Your tools show up in the admin UI ready to be mapped to scopes.
If your handler can't service a synthetic tools/list (custom auth on initialize, dynamic registration, non-HTTP transport), use the escape hatch:
from authsec_sdk import ManifestTool
def my_tools():
return [
ManifestTool(
name="create_pr",
description="Open a pull request",
input_schema={"type": "object", "properties": {"repo": {"type": "string"}}},
suggested_scopes=["github.pr:write"],
),
]
cfg = Config(
# ...
publish_manifest=True,
tool_inventory_provider=my_tools,
)
4. Review tool access
In the admin UI, map each tool to its required scopes. Mark health-check / read-only tools public. Until every tool is mapped (or explicitly public), the application is held at "Needs review."
More: Map tools to scopes.
5. Launch
Hit Activate. From this moment the SDK's scope matrix flips to enforce-mode. Requests with the right scopes get 200s; everything else gets 401 invalid_token or 403 insufficient_scope.
The Applications screen now shows Readiness, Risk, Last Signal, and Next Action for your app.
More: Launch.
Configuration
All fields are documented inline in authsec_sdk.runtime.config.Config. The required ones for a production deploy:
| Field | Source | Notes |
|---|---|---|
issuer | AuthSec base URL | e.g. https://api.authsec.dev |
authorization_server | AuthSec API origin | Defaults to issuer when omitted |
jwks_url | JWKS endpoint | Required for JWT validation modes |
introspection_url | RFC 7662 endpoint | Required for introspection validation modes |
resource_server_id | Application detail panel, field id | UUID |
introspection_client_id | Same value as resource_server_id | Username for Basic auth |
introspection_client_secret | One-time secret from registration | Store in a secret manager |
resource_uri | Application detail panel, field resource_url | Must match the aud claim of issued tokens |
publish_manifest | True recommended | Pushes tool inventory at startup |
Optional but useful:
| Field | Purpose |
|---|---|
policy_mode | REMOTE_REQUIRED (default), REMOTE_WITH_LOCAL_FALLBACK, LOCAL_ONLY, OPEN. |
validation_mode | JWT_AND_INTROSPECT (default), JWT_ONLY, INTROSPECTION_ONLY, JWT_OR_INTROSPECT. |
tool_scopes | Local fallback policy. Required if policy_mode=REMOTE_WITH_LOCAL_FALLBACK. |
tool_scope_suggestions | Per-tool scope hints sent in the manifest payload. |
scope_matrix_ttl | How long fetched policy is cached. Default 5 min. |
Environment-variable config
For container deployments, use from_env():
from authsec_sdk import from_env, mount_mcp
cfg = from_env() # reads AUTHSEC_* env vars
cfg.validate() # raises ValueError on misconfiguration
mount_mcp(app, "/mcp", your_handler, cfg)
Supported env vars (all prefixed AUTHSEC_):
AUTHSEC_ISSUER
AUTHSEC_AUTHORIZATION_SERVER
AUTHSEC_JWKS_URL
AUTHSEC_INTROSPECTION_URL
AUTHSEC_INTROSPECTION_CLIENT_ID
AUTHSEC_INTROSPECTION_CLIENT_SECRET
AUTHSEC_RESOURCE_URI
AUTHSEC_RESOURCE_NAME
AUTHSEC_RESOURCE_SERVER_ID
AUTHSEC_SUPPORTED_SCOPES # space-separated
AUTHSEC_POLICY_MODE # remote_required | remote_with_local_fallback | local_only | open
AUTHSEC_VALIDATION_MODE # jwt_and_introspect | jwt_only | introspection_only | jwt_or_introspect
AUTHSEC_PUBLISH_MANIFEST # true | false
Older aliases such as AUTHSEC_RESOURCE, AUTHSEC_JWKS_URI, AUTHSEC_INTROSPECTION_ENDPOINT, AUTHSEC_INTROSPECTION_ID, and AUTHSEC_INTROSPECTION_SECRET are accepted for compatibility, but new deploys should use the canonical names above.
Common scenarios
Protect a FastAPI MCP server
The five-line snippet at the top of this page. Full executable: sdk-authsec/packages/python-sdk/examples/protect_existing_mcp_server.py.
Run in observe-only mode
You want the SDK to validate tokens but not block anything while you map scopes. Use PolicyMode.OPEN:
from authsec_sdk import Config, PolicyMode
cfg = Config(
# ... regular fields ...
policy_mode=PolicyMode.OPEN,
)
Open mode logs every request, attaches the validated Principal to the request, but allows all tool calls regardless of scope. Switch back to REMOTE_REQUIRED once you've completed step 4.
Keep serving when AuthSec is briefly unreachable
Use REMOTE_WITH_LOCAL_FALLBACK and ship a baseline tool_scopes map:
from authsec_sdk import Config, PolicyMode
cfg = Config(
# ... regular fields ...
policy_mode=PolicyMode.REMOTE_WITH_LOCAL_FALLBACK,
tool_scopes={
"list_repos": ["github.repo:read"],
"create_issue": ["github.issue:write"],
"health_check": [], # empty list = public
},
)
The SDK prefers remote policy; the local map is used only when the AuthSec scope-matrix fetch fails.
Read the authenticated principal in a downstream handler
# Via request state (FastAPI / Starlette route)
async def my_tool(request: Request):
principal = request.state.authsec_principal
log.info("subject=%s scopes=%s", principal.subject, principal.scopes)
# Or via contextvar in deeply-nested async code
from authsec_sdk import principal_from_context
async def deep_helper():
p = principal_from_context()
if p:
...
The Principal carries subject, issuer, audience (list), scopes (list), claims (raw dict), active (bool).
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
ValueError: introspection client credentials are required | introspection_url set with no client ID/secret | Set both. They come from the application registration response. |
App refuses to start with REMOTE_REQUIRED initial scope matrix fetch failed | The SDK can't reach /authsec/resource-servers/{id}/sdk-policy and manifest publishing is not enabled | Check the AuthSec URLs and credentials. For first launch, set AUTHSEC_PUBLISH_MANIFEST=true so the SDK can start, publish tools, and deny tool calls until policy is complete. |
Every request returns 401 invalid_token with audience mismatch | Token's aud claim doesn't match cfg.resource_uri | The OAuth client must request resource=<your resource_uri>. MCP clients with proper discovery do this automatically. |
403 insufficient_scope on a tool you mapped | Scope matrix cached pre-mapping; or the tool name in tools/call doesn't match the manifest | Wait for scope_matrix_ttl to expire (default 5 min) or restart the app. |
| Manifest publish silently fails | Best-effort; failures are logged at WARNING | Check logs for authsec.manifest and confirm credentials. |
Reference
authsec_sdk.runtime.config.Config— all fields, validators, mode resolution.Runtime— low-level constructor if you can't usemount_mcp(custom transport, non-ASGI framework).- Package on PyPI:
authsec-sdk - Source on GitHub:
sdk-authsec/packages/python-sdk - Working example:
examples/protect_existing_mcp_server.py - Tests:
tests/test_runtime.py
The legacy decorator API (protected_by_AuthSec, run_mcp_server_with_oauth) still ships in authsec_sdk for backward compatibility but is in maintenance mode. New applications should use mount_mcp.