title: "TICKET_054: Add OAuth 2.1 to Bus MCP Server for Custom Connectors"
type: ticket
subtype: execution
purpose: "Implement MCP-spec OAuth 2.1 on the Bus MCP Server so claude.ai Custom Connectors (B1-B5 web tabs) can authenticate and connect."
Claude.ai Custom Connectors do NOT support raw bearer tokens. They only support:
Our Bus MCP Server currently only has bearer token auth. Web brains (B1-B5) cannot connect via Custom Connectors without OAuth.
Claude Code CLI (CM0/CC0) already works with bearer tokens via claude mcp add. This ticket adds OAuth for web surfaces only.
Implement the MCP OAuth 2.1 authorization spec on the Bus MCP Server. Claude will act as the OAuth client using our registered Client ID.
https://claude.ai/api/mcp/auth_callbackhttps://claude.com/api/mcp/auth_callbackGET /.well-known/oauth-authorization-server
{
"issuer": "https://bus.struxio.ai",
"authorization_endpoint": "https://bus.struxio.ai/oauth/authorize",
"token_endpoint": "https://bus.struxio.ai/oauth/token",
"registration_endpoint": "https://bus.struxio.ai/oauth/register",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"],
"code_challenge_methods_supported": ["S256"],
"scopes_supported": ["bus:read", "bus:write", "paperclip:read", "paperclip:write"]
}
GET /.well-known/oauth-protected-resource
{
"resource": "https://bus.struxio.ai/mcp",
"authorization_servers": ["https://bus.struxio.ai"]
}
POST /oauth/register — Dynamic Client Registration
client_name, redirect_uris, grant_types, response_typesclient_id, client_secretoauth_clients tableGET /oauth/authorize — Authorization endpoint
client_id, redirect_uri, response_type=code, code_challenge, code_challenge_method=S256, state, scoperedirect_uri with code and statePOST /oauth/token — Token exchange
authorization_code: exchange code for access_token + refresh_tokenrefresh_token: issue new access_token from refresh_tokenCREATE TABLE oauth_clients (
client_id TEXT PRIMARY KEY,
client_secret_hash BYTEA NOT NULL,
client_name TEXT NOT NULL,
redirect_uris TEXT[] NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE oauth_codes (
code TEXT PRIMARY KEY,
client_id TEXT NOT NULL REFERENCES oauth_clients(client_id),
principal_id UUID REFERENCES principals(principal_id),
redirect_uri TEXT NOT NULL,
code_challenge TEXT,
code_challenge_method TEXT,
scope TEXT,
expires_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ
);
CREATE TABLE oauth_tokens (
token_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
access_token_hash BYTEA NOT NULL UNIQUE,
refresh_token_hash BYTEA UNIQUE,
client_id TEXT NOT NULL REFERENCES oauth_clients(client_id),
principal_id UUID REFERENCES principals(principal_id),
scope TEXT,
access_expires_at TIMESTAMPTZ NOT NULL,
refresh_expires_at TIMESTAMPTZ,
revoked_at TIMESTAMPTZ
);
The /mcp endpoint should accept BOTH:
Auth flow:
Authorization: Bearer <token> headerapi_tokens table)oauth_tokens table)WWW-Authenticate: Bearer + PRM pointerSince all STRUXIO brains are trusted, the authorize endpoint should auto-approve without showing a consent screen:
redirect_uri matches Claude's callback (claude.ai or claude.com): auto-approveNew files:
src/auth/oauth.ts — OAuth endpoints (register, authorize, token)src/auth/oauth_store.ts — DB queries for oauth_clients, oauth_codes, oauth_tokenssrc/store/migrations/002_oauth.sql — New tablesModified files:
src/server.ts — Mount OAuth routessrc/auth/tokens.ts — Support both bearer + OAuth token validationAdd OAuth routes to Caddyfile:
handle /oauth/* {
reverse_proxy bus:8088
}
handle /.well-known/* {
reverse_proxy bus:8088
}
https://bus.struxio.ai/mcp/.well-known/oauth-authorization-server returns valid metadata/.well-known/oauth-protected-resource returns resource pointerbus.poll via the connectorRemove OAuth routes. Bearer token auth continues to work for CLI agents.
STRUXIO.ai // Confidential & Proprietary // © 2026