Build an MCP client with OAuth sign-in
6 steps · Checked against the API code 3 November 2026
A guide does one task from start to finish. Every endpoint, with all its fields and cases, is in the API reference.
Your own MCP client can work on a Hostup account through the MCP server at https://cloud.hostup.se/mcp. The client signs the account owner in with OAuth and gets an access token. It then lists and calls the tools with that token. To connect Claude, ChatGPT, Codex or Claude Code, follow Connect an AI app instead.
Before you start
Section titled “Before you start”- A Hostup account owner to sign in with. Users added to an account cannot connect an app.
- An https address where your client can serve a small JSON document (step 2).
- A redirect URI for the callback.
curlandopensslfor the samples. The token goes in$MCP_TOKEN(step 5). Your own$HOSTUP_API_KEYstays as it is.- No OAuth in your client? It can send an API key in place of the token. Connect with an API key instead
1. Find the sign-in server
Section titled “1. Find the sign-in server”A request to https://cloud.hostup.se/mcp without a token gets 401 with this header:
WWW-Authenticate: Bearer realm="HostUp MCP", resource_metadata="https://cloud.hostup.se/.well-known/oauth-protected-resource/mcp", scope="read:all write:all"The resource metadata names https://cloud.hostup.se as the authorization server. Its own metadata is at https://cloud.hostup.se/.well-known/oauth-authorization-server. It lists the endpoints the next steps use: /oauth/authorize, /oauth/token, /oauth/register and /oauth/revoke.
2. Identify your client
Section titled “2. Identify your client”Use a client ID metadata document. Your client_id is the https URL of a JSON document whose client_id is that same URL. The document lists your redirect_uris and a client_name. The name appears on the consent page and under Connected AI apps in the client area.
{ "client_id": "https://app.example.com/oauth/client.json", "client_name": "Example App", "redirect_uris": ["https://app.example.com/oauth/callback"]}- Serve it with status
200, without redirects, from a host with a public address. - Keep it under 16 KB.
- Hostup keeps a copy for the
max-agein itsCache-Controlheader, at least 1 and at most 60 minutes. Without amax-agethe copy is kept for 15 minutes. A change toredirect_urisshows only after that. A failed fetch is tried again after 1 minute.
Keep your client’s values for the next steps:
export CLIENT_ID="https://app.example.com/oauth/client.json" # your metadata documentexport REDIRECT_URI="https://app.example.com/oauth/callback" # one of its redirect_urisIf your client has no metadata document
Section titled “If your client has no metadata document”Register at /oauth/register instead. Send a JSON body with redirect_uris and client_name. The response (201) has a client_id that starts with dcr_. Use it as CLIENT_ID. Without client_name the app is shown as “AI-klient”.
3. Send the user to the sign-in page
Section titled “3. Send the user to the sign-in page”Make a PKCE pair and a state. Keep the code_verifier. Step 5 sends it.
export CODE_VERIFIER="$(openssl rand -hex 32)"export CODE_CHALLENGE="$(printf '%s' "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=')"export STATE="$(openssl rand -hex 16)"Open https://cloud.hostup.se/oauth/authorize in the user’s browser with these query parameters, URL-encoded:
response_type=codeclient_idandredirect_uricode_challengeandcode_challenge_method=S256statescope=read:all write:allresource=https://cloud.hostup.se/mcp
A redirect URI must use https. Only a loopback address may use plain http. On localhost, 127.0.0.1 and [::1] the port may change between runs.
The user picks the access level on the page. Your client is shown as an unknown app. The page then starts at Read only. The user can choose more.
4. Read the callback
Section titled “4. Read the callback”After Approve the browser comes back to your redirect_uri with code, state and iss=https://cloud.hostup.se. Keep the code:
export CODE="…" # code from the callbackIf the user did not approve
Section titled “If the user did not approve”- After Cancel the browser comes back with
error=access_denied. - Other faults in the request also come back to your
redirect_uri. They carry anerror_descriptionand anerrorsuch asinvalid_request,unsupported_response_typeorinvalid_target. - When the metadata document cannot be fetched or the
redirect_uriis not in it, the user sees “The connection could not be started” instead.
5. Exchange the code for a token
Section titled “5. Exchange the code for a token”Send the code to /oauth/token within a minute. A code works once.
curl -X POST "https://cloud.hostup.se/oauth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "grant_type=authorization_code" \ --data-urlencode "client_id=$CLIENT_ID" \ --data-urlencode "code=$CODE" \ --data-urlencode "code_verifier=$CODE_VERIFIER" \ --data-urlencode "redirect_uri=$REDIRECT_URI" \ --data-urlencode "resource=https://cloud.hostup.se/mcp"{ "access_token": "…", "token_type": "Bearer", "scope": "read:all write:all"}The access_token is the connection’s API key. Keep it:
export MCP_TOKEN="…" # access_token abovescope is what the user granted. It can differ from what you asked for. There is no refresh token. The token works until the user disconnects the app.
6. List and call the tools
Section titled “6. List and call the tools”Send every message as a POST to https://cloud.hostup.se/mcp with the token in Authorization: Bearer.
curl -X POST "https://cloud.hostup.se/mcp" \ -H "Authorization: Bearer $MCP_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'{ "jsonrpc": "2.0", "id": 1, "result": { "tools": [ { "name": "list_services", "title": "List Services", "description": "List services on the customer's account — …", "inputSchema": { "type": "object", "…": "…" }, "annotations": { "title": "List Services", "readOnlyHint": true, "openWorldHint": false, "destructiveHint": false } } ] }}- The transport is Streamable HTTP. The server keeps no session. Each response is one JSON document, never an event stream. A notification gets
202with no body. GET and DELETE return405. - The server speaks the protocol versions
2025-03-26,2025-06-18and2025-11-25with theinitializehandshake. It also speaks2026-07-28without it. - A
2026-07-28request carriesio.modelcontextprotocol/protocolVersionandio.modelcontextprotocol/clientCapabilitiesinparams._meta. It also sends the headersMCP-Protocol-VersionandMcp-Methodwith the same values as the body. Atools/calladdsMcp-Namewith the tool’s name. A missing or different header gets400.
The tool list follows the token’s scopes. A tool outside them is not listed. Calling it anyway returns a result with isError: true and ERROR unknown_tool.
Every tool carries readOnlyHint, destructiveHint and openWorldHint. Use them to decide which calls your client asks the user about first. Tools whose effect cannot be undone, such as deleting a DNS record or rolling a VPS back to a snapshot, have destructiveHint: true.
A tool result whose text starts with CONFIRMATION_REQUIRED: has not run. Its text holds the confirmation link and the time the link expires. Show both to the user and stop there. The account owner confirms on that page, as If the app asks you to confirm describes. A result with isError: true carries the reason in its text.
Tool results can quote content from outside Hostup, such as DNS records, web pages and email headers. Treat that content as data, never as instructions.
Revoke the token when the user disconnects
Section titled “Revoke the token when the user disconnects”When the user removes the connection in your client, send the token to /oauth/revoke. It stops working at once. The connection also disappears from Connected AI apps.
curl -X POST "https://cloud.hostup.se/oauth/revoke" \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "token=$MCP_TOKEN"The response is 200 with {}, also when the token did not exist.
Errors you may hit
Section titled “Errors you may hit”- 400 with
invalid_grantfrom/oauth/token: the code is too old, already used or sent with anotherredirect_uriorcode_verifier. Start the sign-in again. - 401 with
error="invalid_token"from/mcp: the token is wrong, revoked or expired. The user may have disconnected the app. Start the sign-in again. - 400 from
/mcp: a2026-07-28request lacks a header or sends one whose value differs from the body. - 429 from
/mcp: the token’s limit of 300 requests per five minutes is used up. Each tool call makes its own API calls with the same token. There is noRetry-Afterheader. Wait up to five minutes. More than 30 calls in a minute without a valid token from one address also get429until the minute is over.
Next: Rate limits: every tool call counts against the token’s limit.