OAuth for custom MCP clients
If you're building your own MCP client (CLI, SDK, IDE plugin), this page describes our OAuth implementation: standards we support, endpoints, redirect_uri rules, and how to debug common errors. Claude.ai and ChatGPT users don't need this page — use the Quickstart instead.
Standards we implement
mcp-telegram.com is fully compliant with the OAuth specifications that the MCP ecosystem relies on — no proprietary extensions, no shared secrets baked into clients.
- RFC 6749 — OAuth 2.0 Authorization Framework — base authorization code grant with refresh tokens
- RFC 7591 — Dynamic Client Registration — any client can register itself at /oauth/register without prior coordination
- RFC 7636 — PKCE (Proof Key for Code Exchange) — S256 challenge required for public clients
- RFC 8252 — OAuth 2.0 for Native Apps — loopback IP redirect URIs with ephemeral port flexibility
- RFC 8414 — OAuth 2.0 Authorization Server Metadata — discovery document at /.well-known/oauth-authorization-server
- RFC 9728 — OAuth 2.0 Protected Resource Metadata — discovery of the MCP resource boundary
Endpoints
Authorization code flow with PKCE
Standard RFC 6749 §4.1 + RFC 7636 flow. No surprises — if your OAuth library handles "authorization code + PKCE", it will work.
Register your client
POST /oauth/register with { redirect_uris: ["http://127.0.0.1:<port>/callback"], client_name: "your-client-name" }. The server returns client_id + client_secret (the secret is optional for public PKCE clients but harmless to receive).
Generate PKCE pair
Generate a random 43-128 char code_verifier, then compute code_challenge = BASE64URL(SHA256(code_verifier)). Store the verifier locally for step 4.
Open /oauth/authorize in a browser
Redirect the user to /oauth/authorize?response_type=code&client_id=...&redirect_uri=...&state=...&code_challenge=...&code_challenge_method=S256. The server displays a QR code; the user scans it in the Telegram app to authenticate.
Receive the authorization code
After QR scan, the server redirects to your redirect_uri with ?code=...&state=.... Verify the state parameter matches what you sent.
Exchange code for token
POST /oauth/token with grant_type=authorization_code, code, redirect_uri, code_verifier, client_id. Receive an access_token (10-year TTL — see Token Lifetime below) and a refresh_token. Use the access_token as Authorization: Bearer ... on /mcp requests.
redirect_uri matching rules
We follow RFC 8252 §7.3 and §8.4 strictly. The matching algorithm depends on whether the registered URI is a loopback IP literal.
Loopback (http://127.0.0.1 or http://[::1])
If you registered a loopback URI, the authorize-time redirect_uri must match scheme, host, path, and query exactly — but the port may differ. This is required by RFC 8252 §7.3 because native clients bind an OS-assigned ephemeral port that may change between runs.
- Registered http://127.0.0.1:36201/callback matches authorize-time http://127.0.0.1:50000/callback ✅
- Registered http://127.0.0.1:36201/callback does NOT match http://127.0.0.1:50000/different-path — path must be exact
- Registered http://[::1]:36201/cb does NOT match http://127.0.0.1:36201/cb — IPv4 and IPv6 loopback are separate identifiers
HTTPS (https://your-domain.example/...)
Exact byte-for-byte match required, including path, query, and port (RFC 6749 §3.1.2). No flexibility.
localhost (NOT recommended)
We treat http://localhost as a regular hostname — exact match required, no port flexibility. RFC 8252 §8.3 recommends using IP literals (127.0.0.1 / [::1]) instead of localhost because DNS resolution is implementation-dependent.
PKCE is mandatory
code_challenge_method=S256 is required on every authorize request. Plain method is rejected. If you skip code_challenge entirely, the /token exchange will fail at code_verifier validation.
Token lifetime
access_token TTL is intentionally long (10 years) because some MCP clients do not persist refresh_token reliably. A refresh_token is still issued and rotates on every refresh (RFC 6819 §5.2.2.3). The user can revoke all tokens any time at /my/sessions or by logging out of Telegram. The user can also explicitly POST to /oauth/revoke (RFC 7009) to terminate a session.
Common errors
HTTP 400 "Invalid redirect_uri" at /oauth/authorize
The redirect_uri query parameter doesn't match any URI registered for this client_id. For loopback clients: check that scheme + host + path + query match exactly (only the port is flexible). For HTTPS clients: full byte-equality is required — watch for trailing slashes and URL-encoding differences.
HTTP 400 "Unknown client"
The client_id doesn't exist in our database. Either you skipped the /oauth/register step, the registration response was lost before you saved the client_id, or the client was revoked by the user via /my/clients.
Client surfaces "Needs Auth" repeatedly
If your client persists access_token but not refresh_token, you'll see this when the access_token expires. Our access tokens are 10-year TTL specifically to mitigate this, but if your client expires tokens client-side after some shorter window, you'll need to either persist refresh_token or call /oauth/revoke + re-auth on demand.
Tested clients
Confirmed to interoperate with our server. If you build something new and it works, send us a PR to add it here.
- Claude.ai web — via built-in MCP connector
- ChatGPT Apps — via built-in MCP connector
- Hermes Agent — open-source CLI MCP client (RFC 8252 loopback flow)
- Cursor MCP — IDE plugin (RFC 8252 loopback flow)
- Any RFC 6749 + RFC 7591 + RFC 7636 + RFC 8252 compliant client should work without code changes on our end.
Multiple Telegram accounts on one OAuth connection
Since v2.32.0 a single OAuth connection can hold several Telegram identities and switch between them with one tool call — no Disconnect/Connect cycle, no new client registration.
List what's attached
Call telegram-accounts-list. You will see your primary (OAuth-bound) account marked with ⭐ if it's active, plus any secondaries you've added.
Attach another account
Call telegram-accounts-add with an optional label (e.g. "testing"). The tool returns a one-time URL (TTL 10 minutes) — open it on any device and scan the QR with the Telegram account you want to add.
Switch active account
Call telegram-accounts-switch with identifier=label, @username, account_id, or 'primary'. The next telegram-* tool call uses that account immediately. No reconnect.
Detach a secondary
Call telegram-accounts-remove identifier=… on a secondary. The Telegram account itself is NOT logged out — only the binding here. The primary cannot be removed this way; use your client's Disconnect flow to fully sign out.
Each OAuth connection is isolated: accounts you attach via Hermes are invisible to Claude.ai or ChatGPT, even if it's the same Telegram identity. To connect different accounts to different MCP clients, register each client separately and use telegram-accounts-add inside its own session.