pymcp-kit ships optional authentication and authorization hooks. They are off by default and are activated through create_app(...) or MiddlewareConfig(...).
When auth is enabled, the HTTP middleware authenticates the Bearer token before the JSON-RPC dispatcher runs. Authorization then evaluates the resolved MCP method, tool name, resource URI, prompt name, or task ID before the handler is called.
sequenceDiagram
autonumber
participant Client
participant HTTP as HTTP Transport
participant Authn as Authenticator
participant Authz as Authorizer
participant Dispatcher
participant Handler
alt no Bearer token and require_authn=True
Client->>HTTP: POST /mcp without Authorization
HTTP-->>Client: 401 Bearer challenge<br/>resource_metadata URL
Client->>HTTP: GET /.well-known/oauth-protected-resource
HTTP-->>Client: protected resource metadata<br/>authorization_servers and scopes
else Bearer token supplied
Client->>HTTP: POST /mcp<br/>Authorization: Bearer token
HTTP->>Authn: authenticate(request)
alt TokenMapAuthenticator
Authn-->>HTTP: Principal from local token map
else TokenIntrospectionAuthenticator
Authn->>Authn: POST token to introspection endpoint
Authn-->>HTTP: Principal from active token claims
end
HTTP->>Dispatcher: dispatch with principal
Dispatcher->>Authz: authorize(principal, AuthzRequest)
alt request allowed
Authz-->>Dispatcher: allow
Dispatcher->>Handler: run MCP method handler
Handler-->>Client: JSON-RPC result
else request denied
Authz-->>Dispatcher: AuthorizationError
Dispatcher-->>Client: JSON-RPC error or 403/insufficient_scope challenge
end
end
TokenMapAuthenticator maps static Bearer tokens to Principal objects:
from pymcp import create_app
from pymcp.security import TokenMapAuthenticator
app = create_app(
authn=TokenMapAuthenticator(
{
"secret-token": {
"subject": "alice",
"display_name": "Alice",
"roles": ["ops"],
"scopes": ["tools:*", "tasks:*"],
"claims": {"tenant": "acme"},
}
}
),
require_authn=True,
)With require_authn=True, unauthenticated requests receive 401.
By default, the WWW-Authenticate challenge derives resource_metadata from the inbound request. If your deployment needs a public URL that differs from the request URL, set resource_metadata_url on OAuthProtectedResourceConfig.
TokenIntrospectionAuthenticator validates each Bearer token against a remote introspection endpoint and builds the request principal from the returned claims:
from pymcp import create_app
from pymcp.security import TokenIntrospectionAuthenticator
app = create_app(
authn=TokenIntrospectionAuthenticator(
"https://auth.example.com/oauth2/introspect",
client_id="pymcp-server",
client_secret="super-secret",
expected_audience="mcp-api",
expected_resource="https://mcp.example.com/test-mcp",
expected_issuer="https://auth.example.com",
),
require_authn=True,
)Use this when your deployment validates opaque access tokens through an authorization server or gateway instead of local JWT verification.
Set expected_audience and/or expected_resource to the public MCP resource URI or audience your authorization server issues tokens for.
Access tokens in the URI query string are rejected by the HTTP auth middleware.
Configure OAuthProtectedResourceConfig when clients should discover the MCP
resource server metadata described by RFC 9728:
from pymcp import create_app
from pymcp.security import OAuthProtectedResourceConfig, TokenMapAuthenticator
app = create_app(
authn=TokenMapAuthenticator({"secret-token": {"subject": "alice"}}),
require_authn=True,
oauth=OAuthProtectedResourceConfig(
authorization_servers=["https://auth.example.com"],
resource="https://mcp.example.com/mcp",
scopes_supported=["tools:read", "tools:call"],
bearer_methods_supported=["header"],
),
)The HTTP transport serves metadata at:
/.well-known/oauth-protected-resource/.well-known/oauth-protected-resource/mcp
Authentication failures include a Bearer WWW-Authenticate challenge with a
resource_metadata URL. Set resource_metadata_url when the public URL differs
from the inbound request URL behind a proxy or gateway.
RuleBasedAuthorizer evaluates ordered allow/deny rules:
from pymcp import create_app
from pymcp.security import RuleBasedAuthorizer, TokenMapAuthenticator
authn = TokenMapAuthenticator(
{
"secret-token": {
"subject": "alice",
"roles": ["ops"],
"scopes": ["tools:*", "tasks:*"],
}
}
)
authz = RuleBasedAuthorizer(
{
"default_effect": "deny",
"hide_unauthorized_capabilities": True,
"hide_unauthorized_tools": True,
"rules": [
{
"methods": ["initialize", "ping", "tools/list", "tasks/list", "tasks/get", "tasks/result"],
"allow_subjects": ["alice"],
"effect": "allow",
},
{
"methods": ["tools/call"],
"tool": "deploy_*",
"allow_roles": ["ops"],
"effect": "allow",
},
],
}
)
app = create_app(
authn=authn,
authz=authz,
require_authn=True,
)from pymcp.security import RuleBasedAuthorizer, load_json_config
authz = RuleBasedAuthorizer(load_json_config("authz_policy.json"))hide_unauthorized_capabilities: remove disallowed capability fragments frominitializehide_unauthorized_tools: filtertools/listoutput to only visible toolsauth_exempt_paths: skip auth middleware on selected HTTP routes
When authentication is enabled, task ownership follows the authenticated principal rather than the raw session ID. That matters for tasks/list, tasks/get, tasks/cancel, and tasks/result.
- keep token maps or authn setup in
config.py - keep JSON authz policies in a separate file
- pass
authn,authz, andrequire_authnintocreate_app() - use capability and tool filtering if clients should not even discover restricted functionality
pymcp-kit provides HTTP authentication hooks, authorization hooks, and OAuth protected-resource metadata discovery for MCP resource servers.
It does not implement a full authorization server, a full OAuth client, or token issuance flows. Those belong in your deployment architecture, not in this server toolkit.