OAuth2 & MCP
Reference for the OAuth2 authorization-code flow — discovery, authorization, token exchange, and dynamic client registration — and for the Model Context Protocol (MCP) endpoint that exposes read-only repository tools to AI assistants.
Resource overview
This page covers two related surfaces.
OAuth2 authorization server. A third-party application obtains an access token on a user's behalf through the authorization-code flow with PKCE. The flow spans four endpoint groups:
- Discovery metadata at
/.well-known/oauth-authorization-server(RFC 8414) and/.well-known/oauth-protected-resource/api/v1/mcp/{owner}/{repo}(RFC 9728), which let a client bootstrap with no out-of-band configuration. - The authorization endpoint
/api/v1/oauth2/authorize, which backs the browser consent screen. - The token endpoint
/api/v1/oauth2/token, plus tokenrevoke(RFC 7009) andintrospect(RFC 7662). - Dynamic client registration at
/api/v1/oauth2/register(RFC 7591).
MCP endpoint. /api/v1/mcp/{owner}/{repo} is a per-repository JSON-RPC
endpoint that exposes a fixed set of read-only tools (repository browsing, issue
reading, and search). /api/v1/repos/{owner}/{repo}/mcp/info returns the
descriptor a UI uses to display the connection URL and toolset.
The OAuth2 endpoints and the MCP audience are advertised as absolute URLs built
from the instance's configured external base URL, so the discovery documents
always name the hosts that deployment actually serves. The token, revocation,
introspection, and registration endpoints, and the MCP audience, resolve to the
REST API host; the authorization_endpoint resolves to the browser-facing
consent page on the web UI host (see the host table in
conventions.md). On a single-origin deployment every URL
shares one host.
Interactive registration and management of OAuth2 applications — creating an app, rotating its secret, and reviewing or revoking the applications a user has authorized — are done through the web console; dynamic client registration below is the programmatic equivalent for clients that self-register.
Auth & scopes
See conventions.md for the accepted credential types, the
OAuth2 access-token format (vetrix_oat_ / vetrix_ort_), the full scope
catalog, and how scopes are enforced. Scope enforcement is additive: an OAuth2
caller must hold both the granted scope and the underlying ACL permission.
The flow endpoints are part of the authentication bootstrap rather than scope-gated resources:
- Discovery documents are public.
/authorizerequires a signed-in user session (the consent screen acts as that user)./token,/revoke, and/introspectauthenticate the client, not a scope./register(dynamic client registration) is gated by an instance admin setting and is disabled by default.
The MCP endpoint is the scope-gated resource on this page:
| Operation | OAuth2 scope | PAT scope |
|---|---|---|
| Connect to the MCP endpoint | mcp:read |
mcp:read |
A caller must additionally have read access to the target repository; tools are gated against that access per call (see The MCP endpoint).
Endpoints
| Method | Path | Summary |
|---|---|---|
GET |
/.well-known/oauth-authorization-server |
Authorization-server metadata (RFC 8414) |
GET |
/.well-known/oauth-protected-resource/api/v1/mcp/{owner}/{repo} |
Protected-resource metadata (RFC 9728) |
GET |
/api/v1/oauth2/authorize |
Fetch consent metadata for an authorization request |
POST |
/api/v1/oauth2/authorize |
Submit the consent decision and issue a code |
POST |
/api/v1/oauth2/token |
Exchange a code or refresh token for tokens |
POST |
/api/v1/oauth2/revoke |
Revoke an access or refresh token |
POST |
/api/v1/oauth2/introspect |
Introspect a token |
POST |
/api/v1/oauth2/register |
Register a client dynamically |
POST |
/api/v1/mcp/{owner}/{repo} |
MCP JSON-RPC wire endpoint |
GET |
/api/v1/repos/{owner}/{repo}/mcp/info |
Read a repository's MCP descriptor |
PUT |
/api/v1/repos/{owner}/{repo}/mcp/settings |
Enable or disable MCP for a repository |
GET /.well-known/oauth-authorization-server
Authorization-server metadata (RFC 8414). A client fetches this first to discover the authorization, token, and registration endpoints and the server's capabilities. Public.
Response
{
"issuer": "https://api.gitvetrix.com",
"authorization_endpoint": "https://www.gitvetrix.com/oauth2/authorize",
"token_endpoint": "https://api.gitvetrix.com/api/v1/oauth2/token",
"revocation_endpoint": "https://api.gitvetrix.com/api/v1/oauth2/revoke",
"introspection_endpoint": "https://api.gitvetrix.com/api/v1/oauth2/introspect",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"scopes_supported": [
"read:user", "read:repo", "write:repo", "admin:repo",
"read:issue", "write:issue", "read:pipeline", "write:pipeline",
"read:package", "write:package", "read:audit",
"openid", "profile", "email", "mcp:read"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"none"
]
}
| Field | Description |
|---|---|
authorization_endpoint |
Browser consent page on the web UI host. |
token_endpoint / revocation_endpoint / introspection_endpoint |
API-host endpoints below. |
code_challenge_methods_supported |
Only S256; PKCE is required (see Token). |
token_endpoint_auth_methods_supported |
none is for public clients (PKCE only); client_secret_basic / client_secret_post are for confidential clients. |
registration_endpoint |
Present only when dynamic client registration is enabled; otherwise omitted. |
The response carries Cache-Control: no-store because the document varies with
the registration setting.
Status codes
| Status | When |
|---|---|
200 OK |
The authorization server is enabled. |
503 Service Unavailable |
The authorization server is disabled instance-wide. |
Example
curl https://api.gitvetrix.com/.well-known/oauth-authorization-server
GET /.well-known/oauth-protected-resource/api/v1/mcp/{owner}/{repo}
Protected-resource metadata (RFC 9728) for one repository's MCP endpoint. A
client that receives a 401 from the MCP endpoint follows the
WWW-Authenticate challenge to this document to learn which authorization
server protects the resource. Public.
Path parameters
| Name | Type | Description |
|---|---|---|
{owner} |
string | Repository owner (user or organization). |
{repo} |
string | Repository name. |
Response
{
"resource": "https://api.gitvetrix.com/api/v1/mcp/{owner}/{repo}",
"authorization_servers": ["https://api.gitvetrix.com"],
"bearer_methods_supported": ["header"],
"scopes_supported": ["mcp:read"]
}
resource is the canonical MCP audience an OAuth2 token must target (see the
resource parameter on /authorize).
authorization_servers names where to fetch the RFC 8414 metadata above.
Status codes
| Status | When |
|---|---|
200 OK |
The authorization server is enabled. |
503 Service Unavailable |
The authorization server is disabled instance-wide. |
GET /api/v1/oauth2/authorize
Returns the consent metadata the browser consent screen renders before a user approves an application. Requires a signed-in user session.
Query parameters
| Name | Type | Description |
|---|---|---|
client_id |
string | The application's client ID. Required. |
redirect_uri |
string | Must exactly match a URI registered for the client. Required. |
response_type |
string | Must be code. Required. |
scope |
string | Space-separated scopes being requested. |
state |
string | Opaque value echoed back on the callback; use it for CSRF defense. |
code_challenge |
string | PKCE challenge: the base64url-encoded SHA-256 of the verifier. Required. |
code_challenge_method |
string | Must be S256. Required. |
resource |
string | Optional RFC 8707 audience. When present it must equal the MCP resource from the protected-resource metadata; any other value is rejected with invalid_target. |
Response
{
"decision": "consent_required",
"app_name": "Example Assistant",
"description": "...",
"homepage_url": "https://example.com",
"logo_url": "https://example.com/logo.png",
"scopes_requested": ["read:repo", "mcp:read"],
"grant_exists": false,
"remembered_scopes": []
}
When the user has already granted a superset of the requested scopes, the
response instead carries an auto_approve_redirect_url the client navigates to
directly — no second prompt.
Status codes
| Status | When |
|---|---|
200 OK |
Consent metadata returned (or an auto-approve redirect). |
400 Bad Request |
response_type is not code, or PKCE S256 is missing. |
401 Unauthorized |
No signed-in user session. |
302 Found |
A redirect back to the registered redirect_uri carrying an error parameter (RFC 6749 §4.1.2.1) — for example invalid_scope when a public client requests an admin-only scope, or invalid_target for a bad resource. Becomes a 400 Bad Request carrying the same error code when no usable redirect_uri is available. |
POST /api/v1/oauth2/authorize
Submits the user's consent decision and, on approval, issues a short-lived
authorization code. The body is form-encoded. Requires a signed-in user session
and the same query parameters as the GET form.
Request body
| Field | Description |
|---|---|
decision |
cancel to deny; any other value approves. |
remember |
true to persist the grant for silent re-approval; false to forget any prior grant. |
Response
On approval, and on cancel, the response is a JSON object naming the URL to
navigate to. The approval URL carries the authorization code and the echoed
state; the cancel URL carries error=access_denied.
{ "redirect_url": "https://example.com/callback?code=<code>&state=<state>" }
The authorization code is single-use and expires ten minutes after issuance.
POST /api/v1/oauth2/token
Exchanges an authorization code (or a refresh token) for an access token. The
body is application/x-www-form-urlencoded. A confidential client authenticates
with HTTP Basic (client_secret_basic) or by sending client_id and
client_secret in the body (client_secret_post); a public client sends
client_id and relies on PKCE.
Request body — authorization code
| Field | Description |
|---|---|
grant_type |
authorization_code. Required. |
code |
The authorization code from /authorize. Required. |
redirect_uri |
Must match the redirect_uri used at /authorize. Required. |
client_id |
The application's client ID (or supplied via HTTP Basic). |
code_verifier |
The PKCE verifier whose SHA-256 matches the code_challenge. Required. |
Request body — refresh token
| Field | Description |
|---|---|
grant_type |
refresh_token. Required. |
refresh_token |
A vetrix_ort_ refresh token. Required. |
client_id |
The application's client ID (or supplied via HTTP Basic). |
Response
{
"access_token": "vetrix_oat_...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read:repo mcp:read",
"refresh_token": "vetrix_ort_..."
}
| Field | Description |
|---|---|
access_token |
The OAuth2 access token, prefixed vetrix_oat_. |
expires_in |
Seconds until the access token expires (one hour by default). |
scope |
Space-separated scopes actually granted. |
refresh_token |
A new vetrix_ort_ refresh token; present when refresh is configured. |
id_token |
A signed OpenID Connect ID token; present only when the openid scope was granted. |
The response carries Cache-Control: no-store.
Refresh-token rotation. Each refresh exchange invalidates the presented
refresh token and returns a new one in the same rotation family. Replaying a
refresh token that has already been rotated is treated as a compromise: the
entire family is revoked and the request fails with 400 Bad Request,
invalid_grant, "refresh token reuse detected". Always store and use only the
most recently returned refresh token.
Status codes
| Status | When |
|---|---|
200 OK |
Tokens issued. |
400 Bad Request |
Unsupported grant_type, PKCE verification failed, redirect_uri mismatch, or refresh-token reuse / invalidity. |
401 Unauthorized |
Client authentication failed. |
Example
curl -X POST https://api.gitvetrix.com/api/v1/oauth2/token \
-d grant_type=authorization_code \
-d code=<code> \
-d redirect_uri=https://example.com/callback \
-d client_id=<client_id> \
-d code_verifier=<verifier>
POST /api/v1/oauth2/revoke
Revokes an access or refresh token (RFC 7009). The body is form-encoded with a
token field and an optional token_type_hint. The client authenticates as on
the token endpoint. The response is 200 OK whether or not the token existed,
per the specification.
POST /api/v1/oauth2/introspect
Reports whether a token is currently active (RFC 7662). The body is form-encoded
with a token field. Confidential-client authentication is required; public
clients are refused. The response is the RFC 7662 introspection object with an
active boolean.
POST /api/v1/oauth2/register
Registers an OAuth2 client dynamically (RFC 7591). Disabled by default; an
instance admin enables it. While it is disabled an anonymous caller receives
404 Not Found (the endpoint is indistinguishable from one that is not mounted)
and an authenticated admin receives 403 Forbidden with access_denied.
Two caller classes are served when registration is enabled:
- An authenticated instance admin (holding the
admin:oauth2scope) may pre-register confidential clients. - An anonymous caller (no bearer) may self-register a zero-configuration MCP
connector. Anonymous registrations are restricted to the single
mcp:readscope and are subject to a per-IP throttle and an instance-wide client cap.
Request body
{
"redirect_uris": ["https://example.com/callback"],
"token_endpoint_auth_method": "none",
"client_name": "Example Assistant",
"scope": "mcp:read"
}
| Field | Description |
|---|---|
redirect_uris |
One or more callback URIs. Each must be https, or http on a loopback address (RFC 8252). Required. |
token_endpoint_auth_method |
none (or omitted) registers a public client; client_secret_basic / client_secret_post registers a confidential client and mints a secret. |
client_name |
Human-readable application name. |
scope |
Requested scopes. Anonymous registrations may request only mcp:read. |
Response
201 Created with the client-information response. client_secret and
client_secret_expires_at are present only for a confidential client.
{
"client_id": "...",
"client_id_issued_at": 1700000000,
"client_secret": "...",
"client_secret_expires_at": null,
"redirect_uris": ["https://example.com/callback"],
"token_endpoint_auth_method": "none",
"client_name": "Example Assistant",
"scope": "mcp:read"
}
Status codes
| Status | When |
|---|---|
201 Created |
Client registered. |
400 Bad Request |
invalid_client_metadata, invalid_redirect_uri, or invalid_scope. |
403 Forbidden |
Registration is disabled (authenticated admin), or the caller lacks admin:oauth2. |
404 Not Found |
Registration is disabled (anonymous caller). |
503 Service Unavailable |
Registration is enabled but its backing store is not wired. |
The MCP endpoint
POST /api/v1/mcp/{owner}/{repo} is a JSON-RPC endpoint spoken over Streamable
HTTP (initialize, then tools/list and tools/call). It exposes read-only
tools scoped to a single repository.
Authentication. Send either an OAuth2 access token (vetrix_oat_) carrying
the mcp:read scope, or a personal access token with the mcp:read scope, as a
bearer credential. An OAuth2 token must also have been minted for this
endpoint's audience (the resource from the protected-resource metadata).
Gate order. The endpoint applies these checks in order, and each rejection is a protocol-level error rather than a tool result:
| Status | Code | When |
|---|---|---|
401 Unauthorized |
unauthorized |
No bearer token. The response carries a WWW-Authenticate: Bearer challenge whose resource_metadata points at the protected-resource document. |
503 Service Unavailable |
— | MCP is disabled instance-wide. |
403 Forbidden |
insufficient_scope |
The token lacks mcp:read. The challenge again points at the protected-resource document. |
403 Forbidden |
invalid_audience |
An OAuth2 token's audience does not match this endpoint. |
404 Not Found |
not_found |
The repository does not exist, or the caller may not read it (private repositories return 404, never 403 — see errors.md). |
503 Service Unavailable |
mcp_disabled_for_repo |
MCP is turned off for this specific repository. |
Tools. tools/list returns a fixed set of read-only tools. Every tool runs
with the caller's own read access — a tool only ever returns repositories,
issues, comments, and content the caller is already permitted to see — and
none of them mutate state.
Repository tools:
| Tool | Returns |
|---|---|
repo_info |
Repository metadata: name, default branch, description, visibility, size. |
list_branches |
Branches with default/protected flags (cursor-paginated). |
list_tags |
Tags with the first line of the annotation (cursor-paginated). |
list_tree |
Files and directories at a (ref, path) with last-commit metadata. |
read_file |
File text at a (ref, path), optionally a line range. |
list_commits |
Commits on a ref, optionally filtered by path (cursor-paginated). |
show_commit |
A commit's envelope and per-file add/delete counts (no patch). |
get_file_at_two_refs_diff |
The unified diff of one file between two refs. |
Issue tools:
| Tool | Returns |
|---|---|
list_issues |
Repository issues, optionally filtered by state and label (cursor-paginated). |
read_issue |
One issue by number (comments excluded). |
list_comments |
Comments on an issue, with restricted comments filtered out (cursor-paginated). |
search_issues |
Full-text issue search ranked by relevance. |
Search tools:
| Tool | Returns |
|---|---|
search_code |
Code search across the repositories the caller can read. |
find_symbol |
Symbol definitions by name from the symbol index. |
Example
curl -X POST https://api.gitvetrix.com/api/v1/mcp/{owner}/{repo} \
-H "Authorization: Bearer vetrix_oat_..." \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
GET /api/v1/repos/{owner}/{repo}/mcp/info
Returns the MCP descriptor for a repository, used to display the connection URL
and toolset. Session-authenticated; gated by repository read access (a caller
who cannot read the repository receives 404).
Response
{
"server_url": "https://api.gitvetrix.com/api/v1/mcp/{owner}/{repo}",
"capabilities": ["repo", "issues", "search"],
"enabled": true,
"instance_enabled": true
}
| Field | Description |
|---|---|
server_url |
The wire endpoint a client connects to. |
capabilities |
The toolset families the server exposes. |
enabled |
The per-repository MCP toggle. |
instance_enabled |
The instance-wide MCP kill switch. The endpoint is usable only when both are true. |
PUT /api/v1/repos/{owner}/{repo}/mcp/settings
Enables or disables MCP for one repository. Requires repository-admin
permission; a caller who cannot administer the repository receives 404 (the
same opaque response a non-existent repository returns). The body is
{ "enabled": <bool> }, and the response is the same descriptor as /mcp/info.
Errors
These endpoints use the shared error envelope and status-code conventions in
errors.md, including the 404-not-403 rule for private
repositories. The OAuth2 endpoints additionally return the flow-specific error
codes defined by their specifications, as a JSON error (with an optional
error_description):
| Code | Where | Meaning |
|---|---|---|
invalid_request |
authorize, token | A required parameter is missing or malformed (including a missing PKCE S256 challenge). |
invalid_client |
authorize, token | Unknown, suspended, or unauthenticated client. |
invalid_grant |
token | Bad code, redirect_uri mismatch, failed PKCE, or refresh-token reuse / invalidity. |
invalid_scope |
authorize, register | An unknown scope, an admin-only scope for a public client, or a non-mcp:read scope on an anonymous registration. |
invalid_target |
authorize | The resource parameter is not this instance's MCP audience. |
unsupported_response_type |
authorize | response_type is not code. |
unsupported_grant_type |
token | grant_type is neither authorization_code nor refresh_token. |
access_denied |
authorize, register | The user denied consent, or registration is disabled. |
invalid_client_metadata / invalid_redirect_uri |
register | Malformed registration request or an unacceptable redirect URI. |
temporarily_unavailable |
discovery | The authorization server is disabled. |
server_error |
any | An unexpected server-side failure. |
The MCP endpoint reports its gate rejections with the codes listed in The MCP endpoint.
Rate limits
These endpoints are metered under the standard scopes described in
rate-limits.md: the discovery and GET /authorize
endpoints count as api.read, and the POST OAuth2 endpoints count as
api.write. MCP wire calls are metered under the dedicated mcp.tool_call
scope so AI-assistant traffic cannot starve ordinary REST traffic.