Skip to main content

Build an MCP client

This guide is for developers who connect their own app, agent or service to the Auto MCP server. Most MCP SDKs do all of this for you when you give them the server URL. Read on if you write the client yourself, or if you need to debug one.

The flow​

Client Auto
| POST /api/mcp/v1 (no token) |
|----------------------------->| 401 + WWW-Authenticate: resource_metadata=...
| GET protected resource doc |
|----------------------------->| names the authorization server
| GET auth server metadata |
|----------------------------->| endpoints, PKCE S256, scopes
| POST /api/oauth/register |
|----------------------------->| client_id
| browser: /api/oauth/authorize|
|----------------------------->| user signs in, clicks Allow, code
| POST /api/oauth/token |
|----------------------------->| access_token (1 h) + refresh_token (30 d)
| POST /api/mcp/v1 + Bearer |
|----------------------------->| tool results

The rest of this page walks through each step.

1. Find the authorization server​

Call the server with no token. Auto answers 401 and points to its metadata:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer scope="mcp:images", resource_metadata="https://app.auto.inc/.well-known/oauth-protected-resource/api/mcp/v1"

Fetch that document (RFC 9728):

curl https://app.auto.inc/.well-known/oauth-protected-resource/api/mcp/v1
{
"resource": "https://app.auto.inc/api/mcp/v1",
"authorization_servers": ["https://app.auto.inc/api"],
"scopes_supported": ["mcp:images"],
"bearer_methods_supported": ["header"]
}

Then fetch the authorization server metadata (RFC 8414). The issuer has a path, so the path goes after the well-known part:

curl https://app.auto.inc/.well-known/oauth-authorization-server/api
{
"issuer": "https://app.auto.inc/api",
"authorization_endpoint": "https://app.auto.inc/api/oauth/authorize",
"token_endpoint": "https://app.auto.inc/api/oauth/token",
"registration_endpoint": "https://app.auto.inc/api/oauth/register",
"revocation_endpoint": "https://app.auto.inc/api/oauth/revoke",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none", "client_secret_basic", "client_secret_post"],
"scopes_supported": ["mcp:photos", "mcp:images"]
}

2. Register your client​

Auto supports dynamic client registration (RFC 7591). You don't need to ask Auto for a client ID.

curl https://app.auto.inc/api/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My Photo Agent",
"redirect_uris": ["http://127.0.0.1:8976/callback"],
"scope": "mcp:images"
}'
{
"client_id": "Hn2kq8Zr4Lw1Tc9Vb3Xa",
"client_name": "My Photo Agent",
"redirect_uris": ["http://127.0.0.1:8976/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "mcp:images",
"token_endpoint_auth_method": "none",
"client_id_issued_at": 1792000000
}

Rules for redirect URIs:

  • Any https: URI works.
  • http: works only for localhost, 127.0.0.1 and [::1].
  • Custom schemes, such as myapp://callback, work.
  • file:, data: and javascript: are refused.

The default is a public client (token_endpoint_auth_method: "none"). A server-side client can ask for client_secret_basic or client_secret_post. Then the answer also has a client_secret. Auto shows it only once.

If you register the same public client again, Auto gives you back the same client_id.

3. Send the user to sign in​

Make a PKCE verifier and challenge. Only S256 is supported:

import crypto from 'node:crypto';

const verifier = crypto.randomBytes(32).toString('base64url');
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');

Open this URL in the user's browser:

https://app.auto.inc/api/oauth/authorize
?response_type=code
&client_id=$CLIENT_ID
&redirect_uri=http://127.0.0.1:8976/callback
&code_challenge=<challenge>
&code_challenge_method=S256
&scope=mcp:images
&resource=https://app.auto.inc/api/mcp/v1
&state=<random>

The user signs in to Auto if they need to. Then Auto shows a consent page with your client_name. When the user clicks Allow, Auto sends the browser to your redirect URI with code and state. When they click Deny, you get error=access_denied.

Always send resource. Auto issues tokens for one MCP URL at a time, and a token for one URL does not work at another.

4. Get tokens​

Trade the code for tokens within 10 minutes. A code works only once.

curl https://app.auto.inc/api/oauth/token \
-d grant_type=authorization_code \
-d client_id=$CLIENT_ID \
-d code=$CODE \
-d redirect_uri=http://127.0.0.1:8976/callback \
-d code_verifier=$VERIFIER
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "mF4wT0p...y2Qk",
"scope": "mcp:images"
}

The token endpoint takes form or JSON bodies.

TokenLastsNotes
Authorization code10 minutesSingle use.
Access token1 hourSend it on every MCP request.
Refresh token30 daysEach refresh gives you a new one. The old one stops working at once.

To refresh:

curl https://app.auto.inc/api/oauth/token \
-d grant_type=refresh_token \
-d client_id=$CLIENT_ID \
-d refresh_token=$REFRESH_TOKEN

Store the new refresh token every time. If you lose it, the user has to sign in again.

5. Call tools​

The server speaks Streamable HTTP in stateless mode. Every request is a POST with one JSON-RPC message, and every answer is plain JSON. There is no session ID and no event stream. GET and DELETE answer 405.

Send Accept: application/json, text/event-stream on each request:

curl https://app.auto.inc/api/mcp/v1 \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "search_images",
"arguments": { "query": "boarding pass", "limit": 5 }
}
}'
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{ "type": "text", "text": "{\"source\":\"auto_photos\",\"results\":[...],\"pagination\":{...}}" }
]
}
}

Most tools return one text block that holds a JSON string. Parse it. The display tools also return structuredContent. See the tool reference for each shape.

Use an SDK​

With the official TypeScript SDK, the transport and the OAuth flow are built in. You supply an OAuthClientProvider that stores tokens and opens the browser.

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

const transport = new StreamableHTTPClientTransport(new URL('https://app.auto.inc/api/mcp/v1'), {
authProvider: myOAuthProvider, // your OAuthClientProvider
});

const client = new Client({ name: 'my-photo-agent', version: '1.0.0' });
await client.connect(transport);

const { tools } = await client.listTools();
const result = await client.callTool({ name: 'get_info', arguments: {} });

The Python SDK has the same pieces in mcp.client.streamable_http and mcp.client.auth.

API keys​

For a script or a server job that acts as you, you can skip OAuth. Send an API key as the bearer token:

curl https://app.auto.inc/api/mcp/v1 \
-H "Authorization: Bearer $AUTO_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

A key has no scopes, so it can call every tool. Don't build an app for other people on API keys. Use OAuth, so each user signs in for themselves and can disconnect your app.

Revoke a token​

When a user signs out of your app, revoke the grant (RFC 7009):

curl https://app.auto.inc/api/oauth/revoke -d token=$REFRESH_TOKEN

You can send the refresh token or the access token. Either one ends the whole grant. Auto checks the grant on every request, so access tokens stop working at once. The answer is always 200.

Users can also disconnect your app from Connected apps on their Account page.

Errors​

StatusHeader or bodyMeaning
401WWW-Authenticate: Bearer ... resource_metadata="..."No token, or the token expired or was revoked. Refresh, or start sign-in again.
403{"error":"insufficient_scope"}The token lacks mcp:images. Sign in again and ask for it.
405JSON-RPC error -32000You sent GET or DELETE. Use POST.

A failing tool call is a normal JSON-RPC result with isError: true and a message in a text block. See Errors.

Other OAuth details​

  • No OpenID Connect. There is no ID token and no openid-configuration.
  • A token request may include resource. Auto uses the resource from the authorization code.
  • A refresh keeps the scopes of the first grant. Sending scope on refresh does not change them.
  • For the full list of endpoints and parameters, see OAuth reference.