MCP server
How sign-in works
The MCP server uses OAuth 2.1 with PKCE. What a client does to sign in, what the discovery documents say, how long tokens last, and how to disconnect.
When a client such as Claude or Codex connects to https://api.signwithsigil.com/mcp without a credential, the server answers 401 with a WWW-Authenticate header that points at a small JSON document. From that document the client finds Sigil's authorization server, sends you to a Sigil page to log in and allow the connection, and receives its own tokens. This is the MCP authorization specification, which is OAuth 2.1 with PKCE. You never paste a key; every connected app can be disconnected on its own.
This page is for people writing an MCP client or CLI of their own, and for anyone who wants to know what the browser round trip is doing.
The discovery documents
Protected resource metadata (RFC 9728), at https://api.signwithsigil.com/mcp's host under /.well-known/oauth-protected-resource/mcp (and without the /mcp), names the resource and its authorization server:
{
"resource": "https://api.signwithsigil.com/mcp",
"authorization_servers": ["https://www.signwithsigil.com"],
"scopes_supported": ["sigil"],
"bearer_methods_supported": ["header"],
"resource_name": "Sigil",
"resource_documentation": "https://docs.signwithsigil.com/mcp"
}Authorization server metadata (RFC 8414), at https://www.signwithsigil.com/.well-known/oauth-authorization-server, lists the endpoints and what they accept:
{
"issuer": "https://www.signwithsigil.com",
"authorization_endpoint": "https://www.signwithsigil.com/oauth/authorize",
"token_endpoint": "https://www.signwithsigil.com/oauth/token",
"registration_endpoint": "https://www.signwithsigil.com/oauth/register",
"revocation_endpoint": "https://www.signwithsigil.com/oauth/revoke",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"],
"client_id_metadata_document_supported": true,
"scopes_supported": ["sigil"],
"authorization_response_iss_parameter_supported": true
}Who the client is
Every client is a public client: there is no client secret, and PKCE with S256 is required on every authorization. A client identifies itself in one of two ways.
A client ID metadata document (CIMD). The client_id is an https:// URL of a JSON document the client's maker hosts, which lists the client's name and the redirect URIs it may use. Claude and ChatGPT identify themselves this way, and so does the Sigil CLI with the document at https://www.signwithsigil.com/oauth/clients/cli.json. Sigil fetches the document, checks that its client_id is the URL it came from, and caches it for a day. The consent page names the document's host, not the name inside it.
Dynamic client registration (RFC 7591). A client without a hosted document registers once:
curl https://www.signwithsigil.com/oauth/register \
-H "Content-Type: application/json" \
-d '{"client_name":"My agent","redirect_uris":["http://127.0.0.1/callback"],"token_endpoint_auth_method":"none"}'{
"client_id": "sgc_…",
"client_name": "My agent",
"redirect_uris": ["http://127.0.0.1/callback"],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "sigil"
}Redirect URIs must be https://, or http:// on localhost, 127.0.0.1 or [::1]. A loopback redirect is matched with the port ignored (RFC 8252 §7.3), because a native app binds whatever port is free; the path and host must still match. A registration unused for 90 days is deleted.
The round trip
- The client sends the browser to
https://www.signwithsigil.com/oauth/authorizewithresponse_type=code, itsclient_id, a registeredredirect_uri,code_challengeandcode_challenge_method=S256, astate, and optionallyscope=sigilandresource=https://api.signwithsigil.com/mcp. - You log in if you are not, and the consent page shows the app's name, where the browser will be sent back to, and what the app may do. A loopback redirect is pointed out, since it means an app on your own computer.
- Allow sends the browser to the
redirect_uriwith a one-usecode, thestate, andiss=https://www.signwithsigil.com(RFC 9207). Deny sendserror=access_denied. A request Sigil cannot send back safely, such as an unknown client or an unregistered redirect, is shown as an error page and never redirected. - The client posts the code to the token endpoint, form-encoded:
curl https://www.signwithsigil.com/oauth/token \
-d grant_type=authorization_code \
-d code=sga_… \
-d code_verifier=… \
-d redirect_uri=http://127.0.0.1:3118/callback \
-d client_id=…{
"access_token": "sgt_…",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "sgr_…",
"scope": "sigil"
}The client then sends Authorization: Bearer sgt_… on every call to https://api.signwithsigil.com/mcp, exactly as it would send an API key. The same token works on the REST API, so the download links a tool returns can be fetched with it.
Tokens and their lives
| Lasts | Then | |
|---|---|---|
Access token sgt_… |
1 hour | Refresh it. The server answers 401 with the same WWW-Authenticate header, and clients refresh on that or shortly before expiry. |
Refresh token sgr_… |
90 days, extended on each use | Sign in again. |
Authorization code sga_… |
10 minutes, one use | A second use revokes the connection it opened. |
Refreshing rotates both tokens:
curl https://www.signwithsigil.com/oauth/token \
-d grant_type=refresh_token \
-d refresh_token=sgr_… \
-d client_id=…The old refresh token stops working at once. If it is presented again later, every token of that connection is revoked, because two parties holding one refresh token means it leaked; the real client simply signs in again.
There is one scope, sigil, which is everything the account can do through the API: read and create templates and pacts, send, resend and void, download signed documents. It cannot change the password, billing or domains.
Disconnect
Each connection is listed on the API page with when it was made and last used. Disconnect revokes it; the app gets 401 on its next call and has to sign in again. A client can also revoke itself (RFC 7009):
curl https://www.signwithsigil.com/oauth/revoke -d token=sgr_…Logging out everywhere and closing the account revoke every connection, as they revoke every API key.
Errors
The token endpoint answers the standard codes (RFC 6749 §5.2): invalid_request, invalid_grant (unknown, expired, used or mismatched code or refresh token), unsupported_grant_type, with an error_description that says which. Only application/x-www-form-urlencoded bodies are read; JSON answers 415. An address that fails many requests in a row is held off for a while.
A 403 from the MCP server after a successful sign-in is the plan, not the token: the account needs Pro or Business.