Developer reference
Raw OAuth endpoints, well-known URLs, curl-level examples. Useful if you're debugging, writing your own MCP client, or auditing the connector.
Spec compliance
The connector implements the MCP Authorization specification (2025-06-18):
- OAuth 2.1 (RFC 9700 — BCP on OAuth 2.0 security)
- PKCE, S256 only (mandatory)
- RFC 7591 — Dynamic Client Registration
- RFC 8414 — Authorization Server Metadata
- RFC 8707 — Resource Indicators (audience binding)
- RFC 9207 —
issparameter in the authorization response - RFC 9728 — OAuth 2.0 Protected Resource Metadata
Canonical URIs
| Purpose | URL |
|---|---|
| MCP endpoint | https://mcp.abm.dev/mcp |
| Resource URI (audience) | https://mcp.abm.dev (exact, no trailing slash) |
| Issuer | https://mcp.abm.dev |
| Railway fallback | https://mcp-server-production-883e.up.railway.app |
| Sign-in redirect | https://abm.dev/sign-in |
Discovery endpoints
GET /.well-known/oauth-protected-resourceRFC 9728Declares this MCP endpoint as a protected resource and points to its authorization servers. Claude fetches this first.
GET /.well-known/oauth-authorization-serverRFC 8414Authorization server metadata: authorization_endpoint, token_endpoint, registration_endpoint, jwks_uri, supported scopes, grant types, PKCE methods.
GET /.well-known/jwks.jsonJWKSPublic key(s) for verifying access token signatures. RS256.
curl https://mcp.abm.dev/.well-known/oauth-protected-resource | jq
curl https://mcp.abm.dev/.well-known/oauth-authorization-server | jq
curl https://mcp.abm.dev/.well-known/jwks.json | jqOAuth endpoints
POST /registerDynamic Client Registration. Open to anyone — no pre-registration required. Returns a client_id; client_secret is only issued when token_endpoint_auth_method isn't none.
GET /authorizeConsent + auth code issuance. Required query params: response_type=code, client_id, redirect_uri, code_challenge, code_challenge_method=S256, resource=https://mcp.abm.dev. Optional: scope, state.
POST /tokenCode/refresh exchange. Supports grant_type=authorization_code (with code_verifier) and grant_type=refresh_token. Returns an RS256 JWT access token (1h) + opaque refresh token (30d).
POST /revokeRevoke a refresh token. Always returns 200 (even for unknown tokens).
Full OAuth walk-through (curl)
1. Verify 401 behaviour
curl -i -X POST https://mcp.abm.dev/mcp \
-H 'Content-Type: application/json' \
-d '{}'
# HTTP/1.1 401 Unauthorized
# WWW-Authenticate: Bearer realm="https://mcp.abm.dev",
# error="invalid_token",
# resource_metadata="https://mcp.abm.dev/.well-known/oauth-protected-resource"2. Register a test client
curl -X POST https://mcp.abm.dev/register \
-H 'Content-Type: application/json' \
-d '{
"client_name": "Test Client",
"redirect_uris": ["https://claude.ai/api/mcp/auth_callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}'
# -> { "client_id": "abm-mcp-...", ... }3. Generate PKCE pair
CODE_VERIFIER=$(openssl rand -base64 48 | tr -d '=\n' | tr '/+' '_-')
CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" \
| openssl dgst -sha256 -binary \
| base64 | tr -d '=\n' | tr '/+' '_-')
echo "verifier=$CODE_VERIFIER"
echo "challenge=$CODE_CHALLENGE"4. Send the user to /authorize
Open this URL in a browser. Sign in on abm.dev, approve the consent screen, and you'll land on your redirect_uri with a ?code=... query param.
https://mcp.abm.dev/authorize?\
response_type=code&\
client_id=<client_id>&\
redirect_uri=https://claude.ai/api/mcp/auth_callback&\
code_challenge=$CODE_CHALLENGE&\
code_challenge_method=S256&\
resource=https://mcp.abm.dev&\
scope=abm:read abm:write abm:linkedin abm:enrich abm:generate&\
state=<random>5. Exchange code for token
curl -X POST https://mcp.abm.dev/token \
-d "grant_type=authorization_code" \
-d "code=<code-from-step-4>" \
-d "code_verifier=$CODE_VERIFIER" \
-d "client_id=<client_id>" \
-d "redirect_uri=https://claude.ai/api/mcp/auth_callback" \
-d "resource=https://mcp.abm.dev"
# -> {
# "access_token": "eyJ...",
# "token_type": "Bearer",
# "expires_in": 3600,
# "refresh_token": "<opaque>",
# "scope": "abm:read abm:write abm:linkedin abm:enrich abm:generate"
# }6. Call the MCP endpoint
curl -X POST https://mcp.abm.dev/mcp \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'What this service never does
- Never passes Claude's bearer token upstream to
api.abm.dev. The MCP service holds its own service credentials and derives org context from the JWT'sorg_idclaim. - Never issues tokens with a mismatched audience. RFC 8707
resourceis validated on both/authorizeand/token. - Never accepts plain
code_challenge_method. S256 only. - Never persists Clerk session cookies or LinkedIn cookies on Claude's side. All secrets stay server-side and encrypted at rest.
Architecture
Claude.ai mcp.abm.dev api.abm.dev
───────── ──────────── ────────────
discover ──▶ /.well-known/*
register ──▶ POST /register
authorize ─▶ GET /authorize ────▶ abm.dev/sign-in (Clerk) ─▶ consent
POST /authorize ◀── user clicks Allow
exchange ──▶ POST /token (PKCE)
call ──▶ POST /mcp (Bearer JWT)
│
│ validate aud=mcp.abm.dev
│ load org_id from JWT
│
▼
AbmApiClient
│
│ X-Internal-Key + x-org-id
│
▼
┌─── halo.* ──┐
│ │
│ Postgres │
│ │
└─────────────┘Source & deployment
- Source:
mcp-server/in theabm.dev-platformmonorepo. - Runtime: Node 20, Hono HTTP framework. Deployed as a Railway service in the
abm-productionproject. - Dockerfile:
mcp-server/Dockerfile.http. - CI sync:
npm run check-routeskeeps MCP tool schemas aligned with the ABM.dev Gateway controllers.