Signed-in MCP and OAuth

Every docs site serves an MCP endpoint for signed-in readers. An agent that connects to it reads the docs as the reader who approved it. A member reads what their role allows, and a guest reads only the published pages in the guest's scope. The endpoint uses OAuth 2.1, as the MCP authorization spec describes, so the agent needs no agent key.

https://{your-docs-host}/mcp/signed-in

Use this endpoint when a person connects their own agent to docs that are not public. Use https://{your-docs-host}/mcp for public pages, and an agent key for changes and analytics.

Connect a client

claude mcp add --transport http acme-docs https://docs.acme.com/mcp/signed-in

The first call returns 401. The client then runs the OAuth flow. It opens a ContextOwl page in the browser, the reader signs in and allows the client, and the client gets its tokens. Claude Code, Cursor, VS Code, Claude, and ChatGPT do these steps without more setup.

Find the authorization server

A call without a token returns 401 with a challenge that names the resource metadata:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://docs.acme.com/.well-known/oauth-protected-resource/mcp/signed-in", scope="docs:read"

The Protected Resource Metadata of RFC 9728 names the authorization server. It is the docs host itself:

curl https://docs.acme.com/.well-known/oauth-protected-resource/mcp/signed-in
{
  "resource": "https://docs.acme.com/mcp/signed-in",
  "authorization_servers": ["https://docs.acme.com"],
  "scopes_supported": ["docs:read"],
  "bearer_methods_supported": ["header"],
  "resource_name": "Acme docs"
}

The authorization server metadata of RFC 8414 lists the endpoints:

curl https://docs.acme.com/.well-known/oauth-authorization-server
Field Value
issuer https://docs.acme.com
authorization_endpoint https://docs.acme.com/oauth/authorize
token_endpoint https://docs.acme.com/oauth/token
registration_endpoint https://docs.acme.com/oauth/register
revocation_endpoint https://docs.acme.com/oauth/revoke
code_challenge_methods_supported S256
client_id_metadata_document_supported true
authorization_response_iss_parameter_supported true

Identify the client

A client identifies itself in one of two ways:

curl -X POST https://docs.acme.com/oauth/register \
  -H 'Content-Type: application/json' \
  -d '{"client_name":"My agent","redirect_uris":["http://127.0.0.1:8976/callback"]}'

Every client is a public client. The token endpoint takes no client secret, and PKCE proves the client. A redirect URI must use https, http on a loopback address, or the URI scheme of a native app. A loopback redirect URI matches with any port.

Get a code

Send the reader to the authorization endpoint with a PKCE challenge of the S256 method, and with the endpoint as the resource:

https://docs.acme.com/oauth/authorize?response_type=code
  &client_id=CLIENT_ID
  &redirect_uri=http%3A%2F%2F127.0.0.1%3A8976%2Fcallback
  &code_challenge=CHALLENGE
  &code_challenge_method=S256
  &state=STATE
  &resource=https%3A%2F%2Fdocs.acme.com%2Fmcp%2Fsigned-in

The reader signs in, checks the client and the host of its redirect URI, and selects Allow or Deny. The redirect carries code, state, and iss. Compare iss with the issuer before you use the code, as RFC 9207 describes. A code expires after 2 minutes and works once.

Exchange the code

curl -X POST https://docs.acme.com/oauth/token \
  -d grant_type=authorization_code \
  -d code=CODE \
  -d redirect_uri=http://127.0.0.1:8976/callback \
  -d client_id=CLIENT_ID \
  -d code_verifier=VERIFIER \
  -d resource=https://docs.acme.com/mcp/signed-in
{
  "access_token": "cowl_oat_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "cowl_ort_...",
  "scope": "docs:read"
}

Send the access token on every MCP request:

curl -X POST https://docs.acme.com/mcp/signed-in \
  -H 'Authorization: Bearer cowl_oat_...' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Tokens

What the agent can do

The token has the docs:read scope. The agent gets the read tools of the public endpoint: search_docs, semantic_search, list_articles, get_article, list_changelog, get_changelog, list_workspaces, whoami, and report_content_gap. It reaches only the workspaces that allow AI agents. A guest reads only the published pages in the guest's scope, and whoami reports the role guest.

Errors

Response What to do
401 with error="invalid_token" The token expired, was revoked, or came from another host. Refresh it, or run the flow again.
400 invalid_grant The code or refresh token is not valid, expired, or was used. Run the flow again.
400 invalid_target The resource is not the signed-in endpoint of this host. Send the URL from the resource metadata.
429 The client sent too many requests. Wait for the Retry-After seconds, then retry.