Connect an AI app by signing in
Overview
An AI app that speaks the Model Context Protocol — claude.ai, Claude Code, Cursor, VS Code — can connect to Gitmoap's MCP server by sending you to sign in, instead of asking you to paste an access token. You sign in, Gitmoap shows you what the app is asking for, you choose what to allow and, if you like, the one repository or group it may touch, and the app receives a credential carrying exactly that and nothing more.
What the app receives is an ordinary personal access token. It appears in your token list labelled with the app's name, it is refused outside its scopes and its binding like any other token, and everything it does is in the audit log. Unlike a token you mint yourself, it lasts an hour and the app renews it quietly — until you disconnect the app, or stop using it for 90 days.
The second half of this page is for people building an app: how to register one, and the two requests that turn your sign-in into a token.
Before you start
- A Gitmoap account.Connecting acts as you, so it needs you signed in to the web app in the browser the app opens.
- An app that supports signing in to MCP servers.Give it the MCP address,
https://api.gitmoap.com/mcp. An app that only accepts a pasted token works too — see AI agents and the MCP server. - To manage connected apps over the API or CLI, a credential carrying
token:readto list them andtoken:manageto disconnect one. Your browser session carries both. - The consent screen accepts only your browser session.A token — even one with
token:manage— cannot approve a connection, so no app can ever grant itself, or another app, more access.
How it works
What happens when you connect
You add Gitmoap to the app by its MCP address. The app asks the MCP server for something, is told it must sign in, and learns from the server where to send you.
The app opens your browser at Gitmoap with its request: who it is, where to send you back, and what it would like to do.
Gitmoap checks the app and its return address. If either is in doubt, you see an error page and the app hears nothing — Gitmoap never sends a browser to an address it cannot vouch for.
You sign in if you are not already, and the consent screen shows the app's name next to the address it will return you to, the access it asked for, and a choice of what to limit it to.
You allow some or all of what it asked for — never more — or decline. Either way your browser goes back to the app.
The app exchanges what it received for a token and starts working. The whole round trip has to finish within ten minutes, and the final step within a minute.
The app's name is the app's own claim. The address it returns you to is the part Gitmoap checks, which is why the consent screen shows both side by side: an app calling itself "Claude" that returns you to an address you do not recognise is not one to allow.
What the app receives
| An access token | A gmp_ personal access token with the scopes you allowed and the binding you chose. It lives for one hour |
| A refresh token | A gmr_ secret the app exchanges for a fresh access token when the hour is up. Each exchange replaces it, so it is only ever good for one use |
When the app renews its access, the token keeps its id and its place in your token list — only the secret changes, and the old secret stops working at that moment. A refresh never widens anything: it renews exactly the access you allowed.
How a connection ends
A connection ends, and the app's token stops working on its very next request, when:
- You disconnect the app from your connected apps, or revoke its token from your token list. Either one ends both.
- Nobody uses it for 90 days. Every renewal resets the clock, so an app you use keeps working indefinitely.
- A refresh token comes back after it was replaced. Only the app and Gitmoap ever hold that secret, so a second use means a copy exists somewhere else. Gitmoap ends the connection rather than guess which copy is genuine; the app sends you to sign in again.
- The same sign-in result is exchanged twice, for the same reason.
How to
Connect an app
Add Gitmoap to the app by its MCP address,
https://api.gitmoap.com/mcp. Add/readonlyto the address —https://api.gitmoap.com/mcp/readonly— if the app should never change anything, whatever you allow it.When the app opens your browser, sign in to Gitmoap if asked.
On the consent screen, check that the return address beside the app's name is one you expect.
Choose what to allow. The agent read access is selected by default; anything the app asked for beyond it is listed for you to switch on.
Choose what to limit the app to. One repository, or one group, is what most agents need, and a connection limited that way cannot read anything outside it whatever it is asked.
Choose Allow. Your browser returns to the app, which is connected.
Note
The consent screen lists only what the app asked for. To give an app more later, disconnect it and connect again — a connection cannot be widened in place.
Review your connected apps
Each connected app is listed with its name, the address it returns to, what you allowed, what it is limited to, when you connected it, when it was last used, and when it lapses if unused.
The web app has no screen for this; use the CLI or the API. Each app's token also appears in your token list, labelled with the app's name.
gitmoap oauth list-oauth-grantscurl -X GET https://api.gitmoap.example/user/oauth-grants \ -H "Authorization: Bearer $GITMOAP_TOKEN"Disconnect an app
Warning
Disconnecting is immediate and cannot be undone. The app's next request fails, including work it is in the middle of, and to use it again you connect it again from the start.
The web app has no screen for this; use the CLI or the API — or revoke the app's token from your token list, which disconnects it in the same way.
gitmoap oauth revoke-oauth-grant --oauth-grant-id <id>curl -X DELETE https://api.gitmoap.example/user/oauth-grants/{oauth_grant_id} \ -H "Authorization: Bearer $GITMOAP_TOKEN"Register an app
You do not register in advance. Your app's client_id is an https URL where it publishes a small JSON document describing itself, and Gitmoap fetches that document when someone connects:
{ "client_id": "https://app.example.com/oauth/client.json", "client_name": "Example Agent", "client_uri": "https://app.example.com", "logo_uri": "https://app.example.com/logo.png", "redirect_uris": ["https://app.example.com/oauth/callback", "http://127.0.0.1/callback"], "token_endpoint_auth_method": "none"}client_idmust be the exact URL the document is served from.- Every redirect is
https, or a loopbackhttpaddress for a desktop app. A loopback redirect matches on any port, so a desktop app can pick a free port at run time; any other redirect must match exactly. - The app is a public client: it holds no client secret, and proves itself with PKCE instead.
- Gitmoap keeps the document for as long as its
Cache-Controlheader says, between one minute and one hour.
To find everything else an app needs, read the server's own description of itself — the address the MCP server names in its sign-in challenge:
Open https://api.gitmoap.com/.well-known/oauth-protected-resource/mcp. It names the authorization server and the scopes the MCP service understands.
gitmoap oauth oauth-protected-resource-metadatacurl -X GET https://api.gitmoap.example/.well-known/oauth-protected-resource/mcpOpen https://api.gitmoap.com/.well-known/oauth-authorization-server/oauth. It lists the sign-in and token addresses, and what the server supports: the authorization code flow, PKCE with S256, refresh tokens, and client metadata documents.
gitmoap oauth oauth-authorization-server-metadatacurl -X GET https://api.gitmoap.example/.well-known/oauth-authorization-server/oauthSend someone to sign in
Open the browser at the authorization address with your request. code_challenge is the S256 hash of a random verifier your app keeps, and resource is the MCP address the access is for:
https://api.gitmoap.com/oauth/authorize?response_type=code &client_id=https%3A%2F%2Fapp.example.com%2Foauth%2Fclient.json &redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM &code_challenge_method=S256 &resource=https%3A%2F%2Fapi.gitmoap.com%2Fmcp &scope=mcp_agent &state=af0ifjsldkjLeave scope out and the request asks for mcp_agent, the agent read access. Your redirect receives code and your state back, or an error with an error_description.
The browser does this; there is nothing to do in the web app.
gitmoap oauth oauth-authorize \ --client-id <client id> \ --code-challenge <code challenge> \ --code-challenge-method <code challenge method> \ --redirect-uri <redirect uri> \ --resource <resource> \ --response-type <response type>curl -X GET https://api.gitmoap.example/oauth/authorizeExchange the code for a token
Within a minute of receiving the code, post it with your verifier to the token address as a form:
curl -X POST https://api.gitmoap.com/oauth/token \ -d grant_type=authorization_code \ -d code=… \ -d code_verifier=dBjftJeZ4CVP-mA92Q5uAkdgmLb5uqLkC9DrfnJfOEXk \ -d client_id=https://app.example.com/oauth/client.json \ -d redirect_uri=https://app.example.com/oauth/callback \ -d resource=https://api.gitmoap.com/mcp{ "access_token": "gmp_…", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "gmr_…", "scope": "issue:read repo:read …"}scope is what the person allowed, which may be less than you asked for. Send the access token to the MCP server as Authorization: Bearer gmp_….
The app does this; there is nothing to do in the web app.
gitmoap oauth oauth-tokencurl -X POST https://api.gitmoap.example/oauth/tokenRenew a token
Before the hour is up, or when a request answers 401, post the refresh token. Store the new one at once — the one you sent no longer works:
curl -X POST https://api.gitmoap.com/oauth/token \ -d grant_type=refresh_token \ -d refresh_token=gmr_… \ -d client_id=https://app.example.com/oauth/client.json \ -d resource=https://api.gitmoap.com/mcpNote
Two copies of your app renewing with the same refresh token look exactly like a stolen one: the second renewal ends the connection. Renew in one place, and share the result.
Answer the consent screen
The web app's consent screen opens the request, shows it, and records the answer. These calls accept only a signed-in browser session.
The consent screen opens the request for you when the app sends you to sign in.
gitmoap oauth create-oauth-authorization --query <query>curl -X POST https://api.gitmoap.example/oauth/authorizations \ -H "Authorization: Bearer $GITMOAP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "query": "<string>" }'The consent screen reads the request back if you reload it.
gitmoap oauth get-oauth-authorization --authorization-id <id>curl -X GET https://api.gitmoap.example/oauth/authorizations/{authorization_id} \ -H "Authorization: Bearer $GITMOAP_TOKEN"Choose Allow on the consent screen.
gitmoap oauth approve-oauth-authorization \ --authorization-id <id> \ --scopes '<json>'curl -X POST https://api.gitmoap.example/oauth/authorizations/{authorization_id}/approve \ -H "Authorization: Bearer $GITMOAP_TOKEN"Choose Deny on the consent screen.
gitmoap oauth deny-oauth-authorization --authorization-id <id>curl -X POST https://api.gitmoap.example/oauth/authorizations/{authorization_id}/deny \ -H "Authorization: Bearer $GITMOAP_TOKEN"Reference
10 endpoints
GET/.well-known/oauth-protected-resource/mcppublic
Which authorization server protects the MCP service, and what it can grant
oauth_protected_resource_metadataGET /.well-known/oauth-protected-resource/mcpOpen in the API referenceToken scope
None — this one is public
Send
Nothing — the address is the whole request.
Refuses
501The authorization server is not configured on this deployment
Receive
- resourcestring
- resource_documentationobjectoptional
- resource_namestring
POST/oauth/tokenpublic
Exchange an authorization code, or a refresh token, for an access token
Token scope
None — this one is public
Counts against
oauth_grants_per_user
Send
Nothing — the address is the whole request.
Refuses
400The code or refresh token was refused, or the request was malformed429The person has connected as many apps as they may501The authorization server is not configured on this deployment
Receive
- access_tokenstring
- expires_inint64Seconds until the access token expires.
- refresh_tokenstring
- scopestringThe scopes the access token carries, space-separated.
- token_typestringAlways
Bearer.
GET/user/oauth-grantstoken:read
The apps the authenticated user has connected through sign-in
Token scope
token:read
Send
Query
- limitint32optionalPage size (1-100)
- next_tokenstringoptionalPagination cursor
Refuses
401Unauthorized403Forbidden — the caller's access level or token scope is insufficient
Receive
items[]object10 fields
- client_idstring
- client_namestring
- client_uriobjectoptional
- created_atstring
- expires_atstringWhen the connection lapses unless the app uses it.
- grant_iduuid
- last_used_atobjectoptional
- logo_uriobjectoptional
- redirect_hoststringThe host the app's sign-in returns to.
- resource_scope_idobjectoptionalThe group or repository the access is limited to, when it is.
- next_tokenobjectoptional
DELETE/user/oauth-grants/{oauth_grant_id}token:manage
Disconnect an app: its access ends at once
Token scope
token:manage
Send
Path
- oauth_grant_iduuidConnected app ID
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthorized403Forbidden — the caller's access level or token scope is insufficient404Not found
Receive
No body.
Limits
| What | Limit |
|---|---|
| Connected apps per person | 50 |
| Sign-in requests waiting for an answer, per person | 20 — a request you never answer stops counting after ten minutes |
| Time to answer the consent screen | 10 minutes |
| Time to exchange the sign-in result | 1 minute, once |
| Access token lifetime | 1 hour |
| A connection nobody renews | ends after 90 days |
| An app's metadata document | 16 KiB |
state | 1,024 bytes |
The limits are per person and the same on every plan. An app's token does not count against the 100 tokens you may mint yourself, because each connection holds exactly one.
At 50 connected apps, choosing Allow is refused with 429 TOO_MANY_REQUESTS and nothing is connected; disconnect an app you no longer use and try again. At 20 waiting requests, opening the consent screen is refused the same way until one expires.
Troubleshooting
The browser lands on a Gitmoap error page instead of the consent screen
- Cause
- The app could not be identified — its metadata document could not be fetched or is not valid — or it asked to return to an address it did not register. Gitmoap does not send you back to an address it cannot vouch for
- What to do
- Tell the app's developer what the page says. If you are the developer, check the document against Register an app
This deployment accepts only apps it has registered in advance.
- Cause
- This Gitmoap environment does not fetch app metadata documents, and the app is not on its list
- What to do
- Connect with a pasted token instead, or ask the environment's administrator to register the app
The app reports invalid_target
- Cause
- The request named something other than the MCP service as its
resource - What to do
- Send
resource=https://api.gitmoap.com/mcp. Gitmoap issues access to its MCP service only
The app reports invalid_request — A PKCE code_challenge with code_challenge_method=S256 is required.
- Cause
- The request carried no PKCE challenge, or used
plain - What to do
- Send an S256 challenge. Every app, including one with a backend, uses PKCE
The app reports invalid_scope
- Cause
- A requested scope or bundle does not exist
- What to do
- Fix the one the description names. Scopes are
domain:verb; the bundles are listed in Access tokens
403with SESSION_REQUIRED when opening or answering a consent request
- Cause
- The call carried an access token. Consent accepts only a signed-in browser session
- What to do
- Answer from the web app. No token can approve a connection, by design
404on the consent screen
- Cause
- The request expired after ten minutes, was already answered, or belongs to someone else
- What to do
- Start again from the app
invalid_grant — The code is not valid.
- Cause
- The code expired after a minute, or was never issued
- What to do
- Send the person to sign in again, and exchange the code as soon as it arrives
invalid_grant — The code does not match this client, redirect, verifier or resource.
- Cause
- One of the four differs from the original request. The code is spent either way
- What to do
- Send exactly the
client_id,redirect_uriandresourceyou sent to sign in, and the verifier whose hash you sent
invalid_grant — The code has already been used.
- Cause
- The code was exchanged twice. The connection it created has been ended
- What to do
- Exchange each code once. The person signs in again
invalid_grant — The refresh token was already used; the connection is revoked.
- Cause
- A refresh token came back after it was replaced — two copies of the app renewing, a retry that resent the old token, or a stolen copy
- What to do
- Store each new refresh token before using the access token, and renew from one place. The person signs in again
invalid_grant — The refresh token is not valid.
- Cause
- The connection was disconnected, lapsed after 90 days unused, or the value was mangled
- What to do
- Send the person to sign in again
invalid_scope — A refresh renews the access the person granted; it cannot change it.
- Cause
- A refresh asked for scopes beyond what was allowed
- What to do
- Leave
scopeout of a refresh. To ask for more, send the person to sign in again
401from the MCP server an hour after connecting
- Cause
- The access token expired
- What to do
- Renew it with the refresh token
An app's name on the consent screen is not the app you started
- Cause
- The name is the app's own claim, and anyone can publish a document calling itself anything
- What to do
- Decline, and check the return address shown beside the name. Connect apps only from links you started yourself
501from every call on this page
- Cause
- The Gitmoap environment you are using does not offer sign-in for apps
- What to do
- Connect with a pasted access token instead
Related
- AI agents and the MCP serverwhat an agent can do once it is connected
- Access tokensscopes, bundles and bindings, and connecting with a pasted token instead
- Audit logwhat a connected app did, with the app's name on each record