Skip to main content
This document describes how an MCP client can implement the civic:rest-auth capability to intercept auth-required responses, handle user authentication out-of-band, and automatically resume the tool call — all before the auth message ever reaches the LLM.

Overview

Without civic:rest-auth, auth-required responses are returned directly to the LLM as text content. The LLM sees the authorization URL, renders it for the user, and waits for the user to manually tell it to continue. This works but is clunky. With civic:rest-auth, the client can:
  1. Declare the capability during MCP initialization
  2. Intercept auth-required tool responses before they reach the LLM
  3. Retrieve the authorization URL from the polling endpoint
  4. Open the authorization URL for the user (browser redirect, popup, etc.)
  5. Poll for completion
  6. Automatically call continue_job to resume the original tool call
  7. Return the final result to the LLM as if auth was never needed
If polling times out, the client falls back to passing the auth message to the LLM with the clickable browser link (obtained from the polling endpoint) and an instruction to retry the action after the user completes authorization.

Step 1: Declare the capability

During MCP initialization, include civic:rest-auth in your client’s experimental capabilities:
Civic advertises the same capability in its server capabilities. When both sides declare it, Civic switches to the rest-auth strategy for this session.

Step 2: Detect the auth-required response

When civic:rest-auth is active, the auth-required tool response contains 3 content items — a text message, a continue_job_id, and an auth_polling_endpoint. Notably, it does not include the authorization_url — the client retrieves that from the polling endpoint instead.
The presence of auth_polling_endpoint (and absence of authorization_url) signals that the client should handle auth programmatically.

Detection and parsing

SDK integrations: When integrating with AI SDKs (e.g. Vercel AI SDK’s ToolSet), the tool execute return type may be unknown rather than a typed CallToolResult. In that case, add defensive checks before accessing nested properties — guard against null, verify typeof for each level, and check that content is an array before calling .find().

Step 3: Get the auth URL and redirect the user

Once you detect an auth-required response, intercept it before it reaches the LLM. Make an initial GET request to the polling endpoint to retrieve the authorization URL:
Then open the authUrl for the user (popup, redirect, embedded webview — whatever fits your client).
Already authorized? If the user has previously authorized (e.g. from a prior session), the job may already be APPROVED by the time your first GET returns. Check data.status — if it’s already APPROVED, skip straight to step 5 (continue_job). There is no need to open a browser or poll.

Step 4: Poll for completion

Polling endpoint

GET /user/jobs/:id/status returns the job status without ever exposing the token: A HEAD request to the same endpoint returns only status codes (no body), useful for bandwidth-efficient polling after you already have the authUrl. The status codes mirror the GET response (200 = approved, 202 = pending/auth-required, 422 = error).
The polling endpoint requires the same JWT or API key used for MCP calls. Pass it as a Bearer token in the Authorization header.

Polling implementation

Timeout guidance: The timeout controls how long the client silently intercepts auth before falling back to the LLM. A shorter value (e.g. 15s) keeps the UX responsive — if auth doesn’t complete quickly, the user sees the link in the chat and can act on it manually. A longer value (e.g. 120s) is appropriate when the client has its own UI for showing auth progress (loading spinner, status bar). Choose based on how visible the auth flow is to the user in your client.

Step 5: Resume or fall back

When polling succeeds (status is APPROVED), call continue_job to resume the original tool call:
The continue_job response is the output of the original tool call — return it to the LLM as if auth was never needed. Important: The continue_job result may itself be another auth-required response (for multi-step auth flows). Check it with parseCivicAuthResponse again and repeat steps 3–5 if needed.

Timeout fallback

If polling times out, fall back to passing the auth message to the LLM so the user can click the link manually. The message should instruct the LLM to retry the action after the user completes authorization:

Complete reference implementation

Full auth flow

Security notes

  • The polling endpoint never returns the actual token. It only returns job status (PENDING, AUTH_REQUIRED, APPROVED, ERROR) and the authUrl when applicable. The token is only exchanged server-side within Civic when continue_job is called.
  • The polling endpoint requires the same authentication (JWT/API key) used for MCP calls. Pass it as a Bearer token in the Authorization header.
  • The authUrl points to Civic, which handles the OAuth flow securely (consent screen, PKCE, token exchange).
  • The MCP auth response intentionally omits the authUrl — it is only available via the polling endpoint, keeping the MCP message minimal and preventing the LLM from seeing it directly.