Kizuna's MCP server supports three authentication modes for inbound requests from MCP clients (Claude Desktop, Cursor, Opencode, etc.):
| Mode | Transport | Use Case |
|---|---|---|
| OAuth 2.1 | Streamable HTTP | Production, multi-user, auto-discovery |
| Static token | stdio / HTTP | Self-hosted, single-user, development |
| None | localhost HTTP only | Local development |
How It Works
MCP clients authenticate automatically using the OAuth 2.1 discovery flow (RFC 9728):
- Client sends a request without a token
- Server returns
401withWWW-Authenticateheader pointing to discovery metadata - Client fetches
/.well-known/oauth-protected-resourceto discover the authorization server - Client performs OAuth Authorization Code + PKCE flow via browser
- Client receives a JWT and uses it for all subsequent requests
Zero configuration required on the client side — the discovery protocol handles everything.
Setup with WorkOS AuthKit
WorkOS AuthKit is the recommended OAuth provider. It handles user authentication, SSO, and token issuance.
Prerequisites
- Create a WorkOS account and project
- Note your AuthKit domain (e.g.,
https://your-project.authkit.app) - Enable Client ID Metadata Documents (CIMD) in Dashboard > Connect > Configuration
- Optionally enable Dynamic Client Registration (DCR) for older MCP clients
Configure Kizuna MCP Server
# ~/.config/kizuna-mcp/config.toml
kizuna_url = "https://kizuna.example.com" # your Kizuna instance
api_token = "kzn_service_..." # Your Kizuna backend API token
[http]
bind = "0.0.0.0" # Allow remote connections
port = 3100
[auth]
mode = "oauth"
resource_uri = "https://mcp.your-domain.com"
[auth.oauth]
issuer = "https://your-project.authkit.app"
audience = "https://mcp.your-domain.com"
strict_audience = false # AuthKit doesn't support RFC 8707 audience bindingOr via environment variables:
export KIZUNA_AUTH_MODE=oauth
export KIZUNA_AUTH_ISSUER=https://your-project.authkit.app
export KIZUNA_AUTH_AUDIENCE=https://mcp.your-domain.com
export KIZUNA_AUTH_RESOURCE_URI=https://mcp.your-domain.comStart the Server
kizuna-mcp serve-http --bind 0.0.0.0 --port 3100Connect from MCP Clients
MCP clients auto-discover authentication. Just point them at your server:
Claude Desktop / Claude Code:
{
"mcpServers": {
"kizuna": {
"url": "https://mcp.your-domain.com/mcp"
}
}
}Cursor: Add the server URL in Settings > MCP Servers.
The client will handle the OAuth flow automatically — a browser window opens for consent on first use.
WorkOS Client Credentials (CI/CD, Agents)
For machine-to-machine authentication (CI/CD pipelines, automated agents) without a browser flow:
Prerequisites
- In WorkOS Dashboard, create an API key with client credentials grant
- Note the
client_idandclient_secret
Token Request
curl -X POST https://your-project.authkit.app/oauth2/token \
-d grant_type=client_credentials \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET \
-d scope="kizuna:repos:read kizuna:issues:read"The returned JWT will be classified as a ServiceAccount in Kizuna's audit logs (distinct from human OAuth tokens).
Scopes
Request only the scopes your automation needs:
| Scope | Access |
|---|---|
kizuna:repos:read | List and view repositories |
kizuna:repos:write | Create repositories |
kizuna:issues:read | List and view issues |
kizuna:issues:write | Create and close issues |
kizuna:prs:read | List and view pull requests |
kizuna:prs:write | Open, close, and merge PRs |
kizuna:agents:read | List and view agents |
kizuna:agents:write | Spawn and assign agents |
kizuna:admin | Administrative operations |
Insufficient scopes return 403 Forbidden with a WWW-Authenticate header indicating what's needed:
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope", scope="kizuna:repos:write"Generic OAuth 2.1 AS Setup
Any OAuth 2.1 authorization server that supports RS256 JWT signing works. Requirements:
- Serves
/.well-known/oauth-authorization-servermetadata (RFC 8414) - Includes
jwks_uriin metadata pointing to a JWKS endpoint - Issues RS256-signed JWTs with
kidheader - Supports Authorization Code + PKCE grant
Configuration
[auth]
mode = "oauth"
resource_uri = "https://mcp.your-domain.com"
[auth.oauth]
issuer = "https://your-auth-server.com"
audience = "https://mcp.your-domain.com"
strict_audience = true # Enable if your AS supports RFC 8707
max_token_lifetime_secs = 300 # Reject tokens valid for longer than 5 minutes
clock_skew_tolerance_secs = 60Token Claims
Kizuna classifies tokens based on these JWT claims:
| Claim | Present | Identity Type |
|---|---|---|
sub | Yes | OAuth (human user) |
client_id (no sub) | Yes | ServiceAccount (M2M) |
| Neither | -- | Rejected |
Additional claims used: email, scope (space-delimited), exp, iat, nbf, iss, aud.
Static Token Mode
For stdio transport (Claude Desktop local servers) or simple self-hosted deployments:
[auth]
mode = "static"
[auth.static_token]
token = "your-secret-token-here"Or via environment:
export KIZUNA_MCP_TOKEN=your-secret-token-hereStatic tokens bypass scope enforcement — any authenticated request can access all tools.
Migration from Static Tokens to OAuth
- Keep static tokens working — set
mode = "static"initially. Existing clients continue to work. - Deploy with HTTP transport — run
serve-httpalongside stdio. OAuth clients connect via HTTP, static clients via stdio. - Configure OAuth — add
[auth.oauth]section. Setmode = "oauth". - Test — verify MCP clients auto-discover and authenticate.
- Deprecate static tokens — once all clients support OAuth, remove
[auth.static_token]section.
Audit Logging
All authentication events and tool invocations are logged as structured JSON to stderr:
{
"timestamp": "2026-04-01T12:34:56.789Z",
"event": "tool_call",
"identity": "user:usr_123 (alice@example.com)",
"auth_mode": "oauth",
"tool": "kizuna/list_repositories",
"arguments": {"org": "acme"},
"result": "success",
"duration_ms": 42
}Event types: auth_success, auth_failure, tool_call, scope_denied, token_lifetime_exceeded
Sensitive arguments (token, password, secret, api_key) are automatically redacted.
Security Considerations
- HTTPS required in production. The server rejects
http://resource URIs unless explicitly allowed. - Origin validation — configure
allowed_originsto prevent DNS rebinding attacks. - Token lifetime —
max_token_lifetime_secs(default 300s) rejects long-lived tokens for revocation protection. - No token forwarding — client OAuth tokens are never forwarded to the Kizuna backend API.
- Static tokens hashed — raw tokens are never stored or logged, only SHA-256 hashes.
Configuration Reference
Environment Variables
| Variable | Maps to | Default |
|---|---|---|
KIZUNA_AUTH_MODE | auth.mode | static |
KIZUNA_AUTH_ISSUER | auth.oauth.issuer | -- |
KIZUNA_AUTH_AUDIENCE | auth.oauth.audience | -- |
KIZUNA_AUTH_RESOURCE_URI | auth.resource_uri | -- |
KIZUNA_MCP_TOKEN | auth.static_token.token | -- |
KIZUNA_AUTH_STRICT_AUDIENCE | auth.oauth.strict_audience | false |
KIZUNA_AUTH_ALLOWED_ORIGINS | auth.allowed_origins | -- |
KIZUNA_AUTH_CLOCK_SKEW_SECS | auth.oauth.clock_skew_tolerance_secs | 60 |
KIZUNA_AUTH_MAX_TOKEN_LIFETIME | auth.oauth.max_token_lifetime_secs | 300 |
Discovery Endpoint
GET /.well-known/oauth-protected-resourceReturns:
{
"resource": "https://mcp.your-domain.com",
"authorization_servers": ["https://your-project.authkit.app"],
"scopes_supported": ["openid", "profile", "email", "kizuna:repos:read", "..."],
"bearer_methods_supported": ["header"]
}