Skip to content
GitmoapdocsBeta
Guide· How do I…?Draft

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:read to list them and token:manage to 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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. You allow some or all of what it asked for — never more — or decline. Either way your browser goes back to the app.

  6. 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 tokenA gmp_ personal access token with the scopes you allowed and the binding you chose. It lives for one hour
A refresh tokenA 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

  1. Add Gitmoap to the app by its MCP address, https://api.gitmoap.com/mcp. Add /readonly to the address — https://api.gitmoap.com/mcp/readonly — if the app should never change anything, whatever you allow it.

  2. When the app opens your browser, sign in to Gitmoap if asked.

  3. On the consent screen, check that the return address beside the app's name is one you expect.

  4. 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.

  5. 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.

  6. 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.

Terminalbash
gitmoap oauth list-oauth-grants
curlbash
curl -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.

Terminalbash
gitmoap oauth revoke-oauth-grant --oauth-grant-id <id>
curlbash
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:

json
{
"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_id must be the exact URL the document is served from.
  • Every redirect is https, or a loopback http address 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-Control header 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.

Terminalbash
gitmoap oauth oauth-protected-resource-metadata
curlbash
curl -X GET https://api.gitmoap.example/.well-known/oauth-protected-resource/mcp

Open 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.

Terminalbash
gitmoap oauth oauth-authorization-server-metadata
curlbash
curl -X GET https://api.gitmoap.example/.well-known/oauth-authorization-server/oauth

Send 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=af0ifjsldkj

Leave 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.

Terminalbash
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>
curlbash
curl -X GET https://api.gitmoap.example/oauth/authorize

Exchange the code for a token

Within a minute of receiving the code, post it with your verifier to the token address as a form:

shellscript
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
json
{
"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.

Terminalbash
gitmoap oauth oauth-token
curlbash
curl -X POST https://api.gitmoap.example/oauth/token

Renew 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:

shellscript
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/mcp

Note

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.

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.

Terminalbash
gitmoap oauth create-oauth-authorization --query <query>
curlbash
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.

Terminalbash
gitmoap oauth get-oauth-authorization --authorization-id <id>
curlbash
curl -X GET https://api.gitmoap.example/oauth/authorizations/{authorization_id} \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Choose Allow on the consent screen.

Terminalbash
gitmoap oauth approve-oauth-authorization \
--authorization-id <id> \
--scopes '<json>'
curlbash
curl -X POST https://api.gitmoap.example/oauth/authorizations/{authorization_id}/approve \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Choose Deny on the consent screen.

Terminalbash
gitmoap oauth deny-oauth-authorization --authorization-id <id>
curlbash
curl -X POST https://api.gitmoap.example/oauth/authorizations/{authorization_id}/deny \
-H "Authorization: Bearer $GITMOAP_TOKEN"

Reference

10 endpoints

GET/.well-known/oauth-authorization-server/oauthpublic

Metadata for this deployment's OAuth authorization server

oauth_authorization_server_metadataGET /.well-known/oauth-authorization-server/oauthOpen in the API reference

Token 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

  • authorization_endpointstring
  • authorization_response_iss_parameter_supportedboolean
  • client_id_metadata_document_supportedboolean
  • issuerstring
  • service_documentationobjectoptional
  • token_endpointstring
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 reference

Token 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
GET/oauth/authorizepublic

Start connecting an app: checks the request and sends the browser to the consent page

oauth_authorizeGET /oauth/authorizeOpen in the API reference

Token scope

None — this one is public

Send

Query
  • response_typestringAlways code
  • client_idstringThe app's identifier — a URL for an app that publishes a metadata document
  • redirect_uristringWhere the app receives the result; must be one it registered
  • code_challengestringThe PKCE S256 challenge
  • code_challenge_methodstringAlways S256
  • resourcestringThe service the access is for: this deployment's MCP address
  • scopestringoptionalSpace-separated scopes or bundles; the agent read bundle when omitted
  • statestringoptionalReturned to the app unchanged

Refuses

  • 302To the consent page, to the error page when the app or its redirect is in doubt, or back to the app with a refusal
  • 501The authorization server is not configured on this deployment

Receive

No body.

POST/oauth/tokenpublic

Exchange an authorization code, or a refresh token, for an access token

oauth_tokenPOST /oauth/tokenOpen in the API reference

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 malformed
  • 429The person has connected as many apps as they may
  • 501The 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.
POST/oauth/authorizationstoken:manage

Open a consent request from the consent page, for the signed-in person

create_oauth_authorizationPOST /oauth/authorizationsOpen in the API reference

Token scope

token:manage

Counts against

oauth_authorizations_per_user

Send

Body
  • querystringThe authorization request's query string, with or without its leading ?.

Refuses

  • 400The app's request is not valid
  • 401Not signed in
  • 403Consent needs a signed-in browser session, not an access token
  • 409The request conflicts with the resource's current state; `reason` says how
  • 429Too many consent requests are waiting for an answer
  • 501The authorization server is not configured on this deployment

Receive

  • authorization_iduuid
  • clientobject5 fields
    • client_idstring
    • kindfirst_partymetadata_documentWhere a client's registration comes from.
    • logo_uriobjectoptional
    • namestring
    • uriobjectoptional
  • expires_atstring
  • redirect_hoststringThe host the authorization code is sent to.
  • requested[]object5 fields
    • accessreadwriteWhether a scope reads or changes things.
    • bundleobjectoptionalThe bundle that includes it, when one does.
    • descriptionstringWhat the scope allows, in a sentence.
    • named_onlybooleanNever included in a bundle: it is granted only when chosen on its own.
    • scopestring
  • resourcestringThe service the access is for.
GET/oauth/authorizations/{authorization_id}token:read

A consent request waiting for the signed-in person's answer

get_oauth_authorizationGET /oauth/authorizations/{authorization_id}Open in the API reference

Token scope

token:read

Send

Path
  • authorization_iduuidConsent request ID

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401Not signed in
  • 403Consent needs a signed-in browser session, not an access token
  • 404Not found, already answered or expired

Receive

The same shape create_oauth_authorization returns — 6 fields. Go to it

Show it here
  • authorization_iduuid
  • clientobject5 fields
    • client_idstring
    • kindfirst_partymetadata_documentWhere a client's registration comes from.
    • logo_uriobjectoptional
    • namestring
    • uriobjectoptional
  • expires_atstring
  • redirect_hoststringThe host the authorization code is sent to.
  • requested[]object5 fields
    • accessreadwriteWhether a scope reads or changes things.
    • bundleobjectoptionalThe bundle that includes it, when one does.
    • descriptionstringWhat the scope allows, in a sentence.
    • named_onlybooleanNever included in a bundle: it is granted only when chosen on its own.
    • scopestring
  • resourcestringThe service the access is for.
POST/oauth/authorizations/{authorization_id}/approvetoken:manage

Allow an app the scopes the person chose, and send the browser back to it

approve_oauth_authorizationPOST /oauth/authorizations/{authorization_id}/approveOpen in the API reference

Token scope

token:manage

Counts against

oauth_grants_per_user

Send

Path
  • authorization_iduuidConsent request ID
Body
  • resourceobjectoptional

Refuses

  • 400A scope the app did not ask for, or a binding to something you cannot see
  • 401Not signed in
  • 403Consent needs a signed-in browser session, not an access token
  • 404Not found, already answered or expired
  • 409The request conflicts with the resource's current state; `reason` says how
  • 429You have connected as many apps as you may
  • 501The authorization server is not configured on this deployment

Receive

  • redirect_tostring
POST/oauth/authorizations/{authorization_id}/denytoken:manage

Refuse to connect an app, and send the browser back to it

deny_oauth_authorizationPOST /oauth/authorizations/{authorization_id}/denyOpen in the API reference

Token scope

token:manage

Send

Path
  • authorization_iduuidConsent request ID

Refuses

  • 400Bad request — a path parameter is not a well-formed value of its type
  • 401Not signed in
  • 403Consent needs a signed-in browser session, not an access token
  • 404Not found, already answered or expired
  • 409The request conflicts with the resource's current state; `reason` says how
  • 501The authorization server is not configured on this deployment

Receive

The same shape approve_oauth_authorization returns — 1 field. Go to it

Show it here
  • redirect_tostring
GET/user/oauth-grantstoken:read

The apps the authenticated user has connected through sign-in

list_oauth_grantsGET /user/oauth-grantsOpen in the API reference

Token scope

token:read

Send

Query
  • limitint32optionalPage size (1-100)
  • next_tokenstringoptionalPagination cursor

Refuses

  • 401Unauthorized
  • 403Forbidden — 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

revoke_oauth_grantDELETE /user/oauth-grants/{oauth_grant_id}Open in the API reference

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 type
  • 401Unauthorized
  • 403Forbidden — the caller's access level or token scope is insufficient
  • 404Not found

Receive

No body.

Limits

WhatLimit
Connected apps per person50
Sign-in requests waiting for an answer, per person20 — a request you never answer stops counting after ten minutes
Time to answer the consent screen10 minutes
Time to exchange the sign-in result1 minute, once
Access token lifetime1 hour
A connection nobody renewsends after 90 days
An app's metadata document16 KiB
state1,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_uri and resource you 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 scope out 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