Authentication for MCP clients
#imsu.io's MCP server at https://imsu.io/mcp is protected by an OAuth 2.1 authorization server at https://imsu.io. This page is for people who build MCP clients, and for agents that want to know what their client does. If you only want to connect a client, see MCP server; MCP clients do all of this for you.
What imsu.io supports, in short:
- The MCP authorization spec (revisions 2025-06-18, 2025-11-25 and 2026-07-28): a
401withresource_metadata, Protected Resource Metadata (RFC 9728) and Authorization Server Metadata (RFC 8414). - The authorization code grant with PKCE (S256 only) and refresh tokens. Public clients only: there are no client secrets.
- Clients identify themselves with a Client ID Metadata Document (an HTTPS URL as
client_id) or register with Dynamic Client Registration (RFC 7591). - Resource indicators (RFC 8707): tokens are issued for
https://imsu.io/mcponly. issin the authorization response (RFC 9207).- No implicit flow, password grant,
plainPKCE, client secrets, JWT access tokens or OpenID Connect.
Scripts and headless agents don't need any of this: they can send an API key (REST API), which /mcp accepts too.
Metadata URLs
#| Document | URL |
|---|---|
| Protected Resource Metadata of the MCP server | https://imsu.io/.well-known/oauth-protected-resource/mcp |
| The same, for clients that look at the root | https://imsu.io/.well-known/oauth-protected-resource |
| Authorization Server Metadata | https://imsu.io/.well-known/oauth-authorization-server |
They need no credential, may be read from any origin, and are cacheable for an hour (Cache-Control: public, max-age=3600). There is no openid-configuration.
The flow, request by request
#1. Call the MCP server without a token
#POST /mcp HTTP/1.1
Host: imsu.io
Content-Type: application/json
Accept: application/json, text/event-stream
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"my-agent","version":"1.0"}}}
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://imsu.io/.well-known/oauth-protected-resource/mcp", scope="monitors:read monitors:write"
Content-Type: application/problem+json
The body is described in MCP server.
2. Read the protected resource metadata
#GET /.well-known/oauth-protected-resource/mcp HTTP/1.1
Host: imsu.io
{
"resource": "https://imsu.io/mcp",
"authorization_servers": ["https://imsu.io"],
"scopes_supported": ["monitors:read", "monitors:write"],
"bearer_methods_supported": ["header"],
"resource_name": "imsu.io",
"resource_documentation": "https://imsu.io/docs/mcp"
}
3. Read the authorization server metadata
#GET /.well-known/oauth-authorization-server HTTP/1.1
Host: imsu.io
{
"issuer": "https://imsu.io",
"authorization_endpoint": "https://imsu.io/oauth/authorize",
"token_endpoint": "https://imsu.io/oauth/token",
"registration_endpoint": "https://imsu.io/oauth/register",
"revocation_endpoint": "https://imsu.io/oauth/revoke",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"],
"revocation_endpoint_auth_methods_supported": ["none"],
"scopes_supported": ["monitors:read", "monitors:write"],
"client_id_metadata_document_supported": true,
"authorization_response_iss_parameter_supported": true,
"service_documentation": "https://imsu.io/docs/authentication"
}
4. Get a client id
#Use the URL of your client metadata document as client_id, or register. Registering a native app that listens on a loopback port:
POST /oauth/register HTTP/1.1
Host: imsu.io
Content-Type: application/json
{"client_name":"My agent","redirect_uris":["http://127.0.0.1:33418/callback"]}
HTTP/1.1 201 Created
Cache-Control: no-store
Content-Type: application/json; charset=utf-8
{
"client_id": "imsu_client_6nYmKDC0DnLQnAtPUhFKCM",
"client_id_issued_at": 1794337800,
"client_name": "My agent",
"redirect_uris": ["http://127.0.0.1:33418/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}
5. Make a PKCE pair
#The code_verifier is 43 to 128 characters of A-Z a-z 0-9 - . _ ~, e.g. 32 random bytes in base64url. The code_challenge is BASE64URL(SHA-256(code_verifier)) without padding. The pair from RFC 7636:
code_verifier = dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
code_challenge = E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
6. Send the user's browser to the authorization endpoint
#https://imsu.io/oauth/authorize?response_type=code&client_id=imsu_client_6nYmKDC0DnLQnAtPUhFKCM&redirect_uri=http%3A%2F%2F127.0.0.1%3A33418%2Fcallback&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&scope=monitors%3Aread%20monitors%3Awrite&state=af0ifjsldkj&resource=https%3A%2F%2Fimsu.io%2Fmcp
| Parameter | |
|---|---|
response_type |
Required: code |
client_id |
Required: a registered id or a metadata document URL |
redirect_uri |
Required: one of the client's redirect URIs (how they match) |
code_challenge |
Required: the S256 challenge |
code_challenge_method |
Required: S256 |
scope |
Optional: monitors:read, monitors:write or both, space-separated (scopes) |
state |
Optional; sent back unchanged. PKCE already ties the answer to the request |
resource |
Optional; if sent, https://imsu.io/mcp |
Each parameter may be sent once (resource may repeat with the same value). imsu.io checks the request and sends the browser to its consent page, inside a sealed handle that is valid for 10 minutes:
HTTP/1.1 302 Found
Location: https://imsu.io/authorize?request=CfDJ8EMIfNIR89tLlY866dBoCic4aI0Fq2yp95Dzogt9H3O…
If the request is wrong, see errors at the authorization endpoint.
7. The user signs in and allows access
#If the user isn't signed in, imsu.io asks them to sign in (or sign up) first and then brings them back. The consent page names the app, says where the browser will return to, and lists what the app may do:
- See your monitors and their status (
monitors:read, always included) - Create, change, pause and delete monitors (
monitors:write, if asked for; the user may untick it)
Apps registered with Dynamic Client Registration are marked "imsu.io hasn't verified this app", because anyone can register an app under any name. Apps known by their metadata URL show that URL's host instead. No logos are shown.
When the user clicks Allow, the browser goes back to the client with a single-use code:
http://127.0.0.1:33418/callback?code=imsu_ac_MoogF2zjfJ7JQKXxgg3YtY97erq1xcrkmUw02JEchmj&state=af0ifjsldkj&iss=https%3A%2F%2Fimsu.io
Check that iss is https://imsu.io. When the user clicks Cancel, the client gets error=access_denied with state and iss.
8. Exchange the code for tokens
#The code works once, within 60 seconds.
POST /oauth/token HTTP/1.1
Host: imsu.io
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=imsu_ac_MoogF2zjfJ7JQKXxgg3YtY97erq1xcrkmUw02JEchmj&client_id=imsu_client_6nYmKDC0DnLQnAtPUhFKCM&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk&resource=https%3A%2F%2Fimsu.io%2Fmcp
HTTP/1.1 200 OK
Cache-Control: no-store
Content-Type: application/json; charset=utf-8
{
"access_token": "imsu_at_H4zN5w3m47PM075l0Hpaj8thgf5yjNtKndxdfJtEORw",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "imsu_rt_amp3sWE4h6uIq3OZUHRrHA2CLHnLN9doHtRIbiyrCJH",
"scope": "monitors:read monitors:write"
}
redirect_uri may be left out, as OAuth 2.1 allows; if you send it, it must be the one from step 6. scope says what the user allowed, which may be less than you asked for.
9. Call the MCP server with the access token
#POST /mcp HTTP/1.1
Host: imsu.io
Authorization: Bearer imsu_at_H4zN5w3m47PM075l0Hpaj8thgf5yjNtKndxdfJtEORw
Content-Type: application/json
Accept: application/json, text/event-stream
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"my-agent","version":"1.0"}}}
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache,no-store
event: message
data: {"result":{"protocolVersion":"2025-11-25","capabilities":{"logging":{},"tools":{}},"serverInfo":{"name":"imsu.io","title":"imsu.io","version":"1.0.0"},"instructions":"imsu.io checks websites (http), TCP ports (port) and cron jobs (heartbeat) on a schedule … Ask the user before deleting."},"id":1,"jsonrpc":"2.0"}
The answer may come as an event stream of one event whose data: line holds the JSON-RPC message, or as plain JSON; accept both. There is no Mcp-Session-Id: the server keeps no session, so send the access token and MCP-Protocol-Version with every later request.
The token works only in the Authorization header and only at https://imsu.io/mcp; the REST API at /api/v1 refuses it with 401.
10. Refresh the access token
#Access tokens last 1 hour. Before or after one expires, trade the refresh token for new tokens:
POST /oauth/token HTTP/1.1
Host: imsu.io
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=imsu_rt_amp3sWE4h6uIq3OZUHRrHA2CLHnLN9doHtRIbiyrCJH&client_id=imsu_client_6nYmKDC0DnLQnAtPUhFKCM
{
"access_token": "imsu_at_IFFtxW7u0V8QwjJdVILXm7c3SCQeoYA74NpajGgEeIJ",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "imsu_rt_o1owcrDfMAIclUkzXPdP5sANxYwIa8GfxrCK3oR65fM",
"scope": "monitors:read monitors:write"
}
Keep the new refresh token and forget the old one: see refresh token rotation. Optional parameters: scope, to narrow the new access token (the refresh token keeps what was granted), and resource (https://imsu.io/mcp).
11. Revoke tokens when the user disconnects
#POST /oauth/revoke HTTP/1.1
Host: imsu.io
Content-Type: application/x-www-form-urlencoded
token=imsu_rt_o1owcrDfMAIclUkzXPdP5sANxYwIa8GfxrCK3oR65fM&client_id=imsu_client_6nYmKDC0DnLQnAtPUhFKCM&token_type_hint=refresh_token
HTTP/1.1 200 OK
Cache-Control: no-store
Revoking a refresh token ends the whole connection, with every token issued for it; revoking an access token ends only that token. Any well-formed request gets an empty 200, also for a token that is unknown, already revoked or another client's (then nothing is revoked). A request without token or client_id gets 400 invalid_request.
Client ID Metadata Documents
#A client can use an HTTPS URL as its client_id, pointing at a JSON document it publishes. imsu.io then needs no registration, and the consent page shows the URL's host, so users can tell who the app is.
{
"client_id": "https://app.example.com/oauth/client.json",
"client_name": "Example Agent",
"client_uri": "https://app.example.com",
"redirect_uris": ["https://app.example.com/oauth/callback", "http://127.0.0.1/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}
- The URL must be HTTPS with a path, without a fragment, user name, password or
./..segments, and at most 2048 characters. - imsu.io fetches it with
Accept: application/json, from public addresses only, without following redirects, within 5 seconds. The document must be a JSON object of at most 5 KB. client_idmust equal the URL exactly;redirect_urismust hold 1 to 10 valid redirect URIs;token_endpoint_auth_methodmust be absent ornone.client_namedefaults to the URL's host.- The document is cached as its
Cache-Control: max-agesays, at least 5 minutes and at most 24 hours. When the cached copy has expired and can't be fetched again, the authorization fails (client_metadata_unavailable); a stale copy is never used. - Hosts are shown in their ASCII form (
xn--…for internationalized names), so a look-alike name can't pass for another.
Dynamic Client Registration
#POST https://imsu.io/oauth/register takes a JSON body of at most 16 KB and registers a public client (example).
redirect_uris(required): 1 to 10 redirect URIs.client_name: trimmed, without control characters, at most 100 characters; "Unnamed app" if missing.client_uri: kept only if it is HTTPS.logo_uriis ignored: imsu.io shows no logos.grant_typesandresponse_typesmay only listauthorization_code,refresh_tokenandcode.token_endpoint_auth_methodis always registered asnone; no secret is issued.- Registered apps are marked unverified on the consent page. Registrations that are never used to connect are deleted after 7 days.
- Each address may register 10 clients an hour; then
429withRetry-After.
Errors are 400 with { "error", "error_description" }: invalid_redirect_uri for a redirect URI that isn't allowed, invalid_client_metadata for anything else.
{
"error": "invalid_redirect_uri",
"error_description": "An http redirect URI is only allowed on a loopback host (127.0.0.1, [::1] or localhost). Use https."
}
Redirect URIs
#A redirect URI is an absolute URI without a fragment, and one of:
- HTTPS, e.g.
https://app.example.com/oauth/callback; - HTTP on a loopback host:
http://127.0.0.1,http://[::1]orhttp://localhost; - an app's own scheme, e.g.
cursor://anysphere.cursor-mcp/oauth/callbackorcom.example.app:/callback.
Schemes such as javascript:, data:, file:, ws: and ftp: are refused. The redirect_uri of an authorization request must match a registered one exactly, except that on a loopback host any port matches, because native apps listen on a port they pick at run time. Scheme, host, path and query must still match.
PKCE
#PKCE is required, with code_challenge_method=S256; plain is refused. The token request must send the code_verifier whose S256 hash is the code_challenge of the authorization request (example).
Resource
#The only resource is https://imsu.io/mcp. resource is optional in the authorization and token requests; if sent, it must be that value, or the answer is invalid_target. Every token is issued for it, and other resources refuse it.
Scopes
#| Scope | Allows |
|---|---|
monitors:read |
List and read monitors and the plan's limits |
monitors:write |
Create, change, pause, resume and delete monitors; includes monitors:read |
Scopes imsu.io doesn't know (openid, offline_access, …) are ignored, and asking for none of these two, or leaving scope out, asks for both. The user may allow reading only. Such a connection gets a 403 insufficient_scope challenge when it calls a tool that changes monitors; ask again with both scopes to step up (details).
Token lifetimes
#| Format | Lifetime | |
|---|---|---|
| Authorization code | imsu_ac_… |
60 seconds, once |
| Access token | imsu_at_… |
1 hour (expires_in: 3600) |
| Refresh token | imsu_rt_… |
30 days, replaced on every use |
| Consent request | sealed request handle |
10 minutes to sign in and allow |
| Connection (grant) | Until the user disconnects it, the app revokes it, or a reused code or refresh token ends it |
Tokens are opaque: don't parse them. imsu.io stores only their SHA-256 hashes. Connecting the same app again for the same user and account replaces the old connection and its tokens.
Refresh token rotation
#Every refresh returns a new refresh token, and the old one stops working. If an old refresh token is presented again, imsu.io assumes it was stolen and ends the connection with all its tokens:
{
"error": "invalid_grant",
"error_description": "The refresh token was already used, so the access it gave was revoked. Connect the app again."
}
The same happens when an authorization code is used twice. Store the newest refresh token before using the new access token, and don't refresh from two places at once.
Disconnecting
#The user sees every connected app at https://imsu.io/settings, under Connected apps, with what it may do and what it changed recently, and can disconnect it there. Disconnecting, or the app revoking its refresh token, ends all of the connection's tokens at once: the next request gets 401. While the account is suspended, or once the user is no longer a member of it, its tokens stop working too.
Errors at the authorization endpoint
#While imsu.io can't trust where to send the browser (the client or its redirect URI is unknown or doesn't match), it shows its own error page instead: https://imsu.io/authorize?error=<code>.
| Code | Meaning |
|---|---|
invalid_request |
client_id or redirect_uri is missing or sent twice |
unknown_client |
No registered client has this client_id |
client_metadata_unavailable |
The client metadata document couldn't be fetched, or isn't valid |
invalid_redirect_uri |
redirect_uri isn't one of the client's |
Once client and redirect URI are known, other errors go back to a native app's redirect URI (loopback HTTP or its own scheme) with error, error_description, state and iss; an HTTPS redirect URI gets the error page instead, so imsu.io can't be used to send people to any site:
http://127.0.0.1:33418/callback?error=invalid_request&error_description=code_challenge_method%20must%20be%20S256;%20plain%20is%20not%20supported.&state=af0ifjsldkj&iss=https%3A%2F%2Fimsu.io
error |
Meaning |
|---|---|
invalid_request |
PKCE is missing or malformed, the method isn't S256, response_type is missing, or a parameter was sent twice |
unsupported_response_type |
response_type isn't code |
invalid_target |
resource isn't https://imsu.io/mcp |
access_denied |
The user clicked Cancel |
Errors at the token endpoint
#The token endpoint takes form-encoded bodies of at most 16 KB and answers errors as { "error", "error_description" } with Cache-Control: no-store:
Status and error |
Meaning |
|---|---|
400 invalid_request |
A parameter is missing or sent twice, the body isn't form-encoded or is too large, or the code_verifier is malformed |
400 unsupported_grant_type |
grant_type isn't authorization_code or refresh_token |
401 invalid_client |
client_id is unknown |
400 invalid_grant |
The code or refresh token is unknown, expired, revoked, already used, or another client's; the code_verifier doesn't match; redirect_uri differs from the authorization request's |
400 invalid_target |
resource isn't the token's resource |
400 invalid_scope |
A refresh asked for a scope the user didn't grant |
Rate limits
#Per client address: 60 requests a minute to each of /oauth/authorize, /oauth/token and /oauth/revoke, and 10 registrations an hour at /oauth/register. Over a limit, the answer is 429 with Retry-After in seconds. The MCP server's own limit is per credential (details).
Troubleshooting
#| Problem | Likely cause | Fix |
|---|---|---|
| The browser shows "imsu.io doesn't know this app" | The client_id was never registered, or the registration was deleted after 7 days unused |
Register again, or use a client metadata document |
| "This app asked to return to an address it didn't register" | redirect_uri doesn't match a registered one |
Send a registered redirect URI; on loopback only the port may differ |
| "imsu.io couldn't read this app's details" | The metadata document is unreachable, too large, slow, or invalid | Serve valid JSON of at most 5 KB from a public address, without redirects |
| "This sign-in request has expired" | More than 10 minutes passed between the authorization request and Allow | Start the authorization again |
invalid_grant when exchanging the code |
The code expired (60 seconds), was already used, or the code_verifier doesn't match |
Exchange the code right away, once, with the verifier of the same request |
invalid_grant "already used, so the access it gave was revoked" |
A refresh token or code was used twice | Connect again, and keep only the newest refresh token |
401 from /mcp with error="invalid_token" |
The access token expired or was revoked | Refresh it; if that fails, connect again |
401 "Token not accepted here" from /api/v1 |
OAuth tokens only work at /mcp |
Use an API key for the REST API |
403 insufficient_scope from /mcp |
The user allowed reading only | Ask for monitors:read monitors:write again |
429 |
Too many requests from one address or credential | Wait Retry-After seconds |