Troubleshooting
Most failures are one of five things: the first 401, which is expected; a token that stopped working; an asset the credential cannot see; a result that is empty because nothing was measured; or a client holding a stale configuration. Every message below is quoted from the code that sends it.
The first 401 is expected
A request to https://app.seoaio.ai/api/mcp without a token answers 401 with a JSON-RPC error (code -32001) whose message is:
Unauthorized. Present a SEOAIO API key or an OAuth access token as a Bearer token. MCP clients: start OAuth from the resource_metadata URL in the WWW-Authenticate header.
The response carries a WWW-Authenticate header whose resource_metadata points at https://app.seoaio.ai/.well-known/oauth-protected-resource. That is how an MCP client finds the authorization server (https://app.seoaio.ai/.well-known/oauth-authorization-server) and opens a browser for consent. Nothing is wrong yet. If the client never opens a browser:
- Remove any
Authorizationkey from the client configuration. The config carries only the URL:
{
"mcpServers": {
"seoaio": {
"type": "http",
"url": "https://app.seoaio.ai/api/mcp"
}
}
}- Reload the client, then connect or authorize from its MCP settings.
- A plain
curlwithout a Bearer token will always see this 401. That is the server working.
The token worked, then 401 again
An access token expires, a client refreshes it at https://app.seoaio.ai/api/oauth/token, and a refresh that fails answers with one of these:
| error | error_description | What it means |
|---|---|---|
| invalid_grant | Refresh token is invalid or expired. | The refresh token expired, was revoked, or was already used once. A reused refresh token revokes its whole family on purpose. Reconnect. |
| invalid_grant | Authorization code is invalid or expired. | The authorization code was used already or took too long. Start the consent flow again. |
| invalid_grant | Client mismatch. | The token belongs to a different client registration than the one presenting it. Reconnect from the client that will use it. |
| invalid_grant | redirect_uri mismatch. | The redirect_uri differs from the one the code was issued to. |
| invalid_grant | PKCE verification failed. | The code_verifier does not match the challenge. The client, not the server, holds the verifier. |
A token is minted for the resource https://app.seoaio.ai/api/mcp and no other host. Revoke a client under Connected apps at https://app.seoaio.ai/dashboard/settings/api; after that, the next call answers 401 until you connect again.
Asset not found
Every tool that takes an assetId refuses one that is not yours with a message naming the sites you can see, so an agent that guessed can correct itself. An id from another organization and an id that does not exist read the same, on purpose. A credential pinned to one site is told only that site.
| When | Message | On the wire |
|---|---|---|
| The assetId is not one of yours, or does not exist. Both cases read the same, and the message names your own sites. | Asset not found. Call list_assets and use one of its ids. Your sites: example.com. | tools/call result with isError: true |
| A tool name the server does not have. The list from tools/list is complete. | Unknown tool: get_history | JSON-RPC error -32602 |
| A JSON-RPC method the server does not implement, such as a subscribe. | Method not found: resources/subscribe | JSON-RPC error -32601 |
| A required argument is missing. | Error: assetId (string) is required | tools/call result with isError: true |
Empty is not broken
A connected client with a valid token can still get nothing back, because nothing has been measured yet. That is a result, not a failure, and the tools say so:
get_measurement_status.analysis: collectedAt and ageHours of the latest analysis snapshot, or null when none exists.get_measurement_status.citations: collectedAt and ageHours of the latest citation sweep, or null when none exists.- A share tool with no citation sweep returns
aggregate: nulland an emptyengineslist, with a note. Quote the note, not a zero. - A held composite returns
held: trueand aheldExplanation. Quote the explanation and the measured pillars, not a score.
Do not reconnect, and do not read the marketing site instead. Call get_measurement_status: it says whether a run is queued or running and how old the latest measurement is.
Reconnect checklist
- Revoke the old connection under Connected apps at
https://app.seoaio.ai/dashboard/settings/api. - Put only the URL in the client configuration:
https://app.seoaio.ai/api/mcp. - Reload the client and connect or authorize from its MCP settings.
- Sign in with the account that owns the workspace and approve.
- Call
list_assets, thenget_measurement_status.
The full call order and the rules for quoting a number are in the skill at https://seoaio.ai/SKILL.md.
