Troubleshooting an MCP connection that won't work

A bottom-up debugging ladder for remote MCP servers — reachability, transport, the auth header, stale tool lists, logs — plus the RelayLink symptoms that look like failures and aren't.

7 min read Updated

The error you see is almost never generated at the layer that broke. A client that cannot resolve your hostname, one that got a 401, and one that connected perfectly but is showing a tool list from last Tuesday all surface the same way — the tools aren't there, or a call fails, and the message blames something vague.

So stop reading the message and work up from the bottom. Five rungs, in order, and don't skip one for looking too obvious to be wrong.

Rung 1 — is anything listening at that URL?

Take the client out of it:

curl -i https://your-server.example/mcp

A DNS or connection-refused error means the host is wrong or nothing is deployed. A TLS error means you are talking to something, but not to what you think. A 404 means the host is right and the path is wrong — a missing or extra path segment is the most common version. A bare GET may return a protocol-level complaint rather than a page — not a fault, just something MCP-shaped answering.

A 401 is good news. You reached the server and its authentication code ran.

For RelayLink, a credential-less request to /mcp returns a 401 with an empty body. One curl proves host, TLS, path, and that the authentication code ran. Read the response headers rather than the body: if OAuth is available you will also get a WWW-Authenticate: Bearer resource_metadata="…" challenge, and that URL is the machine-readable answer to "where do I get a token".

Rung 2 — does your client speak remote at all?

MCP servers come in two shapes: a local process the client launches and talks to over standard input and output, or a remote HTTP endpoint you point a URL at. They are not interchangeable, and a client configured for one and given the other fails confusingly rather than loudly.

Confirm your client supports remote servers, and that you filled the field it expects — a URL where it wants a URL, not a URL where it wants a command. If you are wiring this from code, connecting an agent framework covers the same ground with the config in front of you, and what streamable HTTP is explains the transport a remote server uses.

There is a second version of this rung that looks identical and isn't: a client that speaks remote perfectly well but won't let you name a server. ChatGPT is the one you'll meet. Adding an arbitrary MCP server URL requires developer mode — a toggle in its settings, currently beta and on paid web plans — and with it off there is no field to fill in, which reads as "this app doesn't do remote servers" when the truth is that it does and hasn't been permitted to here. Check for a place to put the URL before debugging the URL. Claude has no equivalent gate, so a setup that works in one app and appears unsupported in the other is usually this and not your server.

The general form is worth carrying past this example: capability and permission fail the same way from the outside. Before concluding a client can't do something, establish that it has been allowed to.

RelayLink is remote-only, over HTTP, in stateless mode. It identifies itself as relaylink version 0.1.0 — useful confirmation you are talking to what you meant to.

Rung 3 — the header, character by character

Most "it won't connect" is one wrong string.

HTTP header names are case-insensitive. Values are where you have to be exact, and for RelayLink "exact" now means exact.

A leading space, a trailing space, a newline, a tab, a smart quote from a pasted document, or the wrong case will all fail. RelayLink compares a hash of exactly the bytes you send, so there is no forgiveness in any direction.

This is worth calling out because it used to be untrue, and the change is invisible from the outside. RelayLink previously matched the key inside SQL Server, under a collation that ignores case and trailing spaces — so KEY and key both authenticated as key. Storing a hash instead removed that, along with the uncomfortable implication that the effective keyspace was smaller than the key length suggested. If a key that worked for months stopped working after an upgrade, this is the first thing to check: compare what your client sends against what was issued, character by character, and look at the case and the end of the string.

The forgiving half is the one to watch. A key that works despite being mistyped is one you will re-enter differently somewhere else and lose an afternoon to — and it means the string you configured is not the string being compared. Open it in a plain text editor and check both ends.

Three traps:

  • A prefix you did not add. Some clients assume OAuth and prepend Bearer to whatever you type. RelayLink's key is a bare value in a header named X-RelayLink-Key — not an Authorization header, no prefix.
  • A field that never saved. If the client insists it is sending a header and the server says nothing arrived, either the field didn't persist or a proxy stripped it.
  • The wrong half of the problem, and no help from the server. An absent header and a wrong key both return a bare 401. RelayLink used to distinguish them in the response body and no longer can: once a request may authenticate either by key or by OAuth token, every authentication scheme gets asked to explain itself on the same response, and the first one to write a body prevents the others from replying at all. So the server says less than it did. Distinguish the two yourself by sending a deliberately wrong key: if that also 401s, your header is arriving and the key is the problem; if the client cannot make the header arrive at all, nothing about the key matters yet.

Rung 4 — the client is showing you a stale tool list

Clients fetch a server's tool list when the connection is established, then work from that copy. Deploy a new tool, or fix a broken header, and the session you are in may never notice.

Reconnect the server and start a new conversation before concluding a tool is missing. Where that lives in the UI moves as these apps evolve — the constant is that a fresh connection re-reads the list and an old session does not.

Rung 5 — read the logs on the correct side

Client-side connector logs tell you whether a request left. Server-side logs tell you whether it arrived. Only one is answered where you are looking.

If you own the server, log the request path and whether the credential was present at the auth boundary. That single line separates "never arrived" from "arrived and was rejected" — the fork this ladder is hunting for. If you don't own it, the closest equivalent is the status you can reproduce with curl, so rule out the boring layer before rewriting your connector setup.

Some of what gets reported as a broken connection is correct behaviour.

  • list_contacts comes back empty. Expected on a new account. It lists standing accepted contacts only, and nothing in the assistant's surface creates one. Writing to an address with no RelayLink account needs no pair — that path is ordinary email, with its own limits.
  • You said "send it" and nothing arrived. A draft is not a send. draft_package returns a draft id and the words "NOT sent yet." Check list_drafts, then confirm. Pending drafts expire after 24 hours; confirming an expired one cancels it and asks for a fresh draft.
  • check_inbox is empty and you know something arrived. It returns unread packages by default, and a package counts as read once it has been pulled, opened on the web, or replied to. Ask for read items too. It also shows at most 20 and reports the true total.
  • The transcript contains ERROR: text. Domain refusals come back as real errors with actionable wording — an invalid response shape, a consent refusal, a malformed id. That is the server working.
  • A 429 from /mcp itself. It has its own ceiling, separate from the per-IP limit on the public token pages and the health endpoint: 300 calls a minute, partitioned by the presented credential rather than by address. It exists to catch a runaway tool loop or a stolen key, not to throttle ordinary use, so seeing one means checking for a loop before assuming the server is at fault.

If you are on the other side of this — writing a server rather than connecting to one — the same ladder in reverse is how to build an MCP server that's safe to connect. And if you got here because it now works, give it something real to carry.

Frequently asked questions

Why does my MCP server connect but show no tools?
Usually one of two things. The client is holding a tool list it fetched in an earlier session, so reconnect the server and start a fresh conversation. Or the client was configured for a local server and handed a remote URL, which tends to fail in ways that never name the real problem.
What does a 401 from an MCP endpoint actually tell me?
More than it feels like. A 401 means you reached the right host and the right path and the server's authentication ran and rejected you. A wrong URL usually produces a connection error or a 404 instead. From there the fault is the header name or the header value, including whitespace picked up from copy and paste.
Is a draft the same as a sent message?
On RelayLink, no. Drafting and sending are separate tool calls, and a draft sits on the server unsent until it is explicitly confirmed from the same account. Pending drafts expire after 24 hours. If you expected something to arrive and it did not, list the drafts before assuming the connection broke.