You paste a server URL into Claude Desktop, but no tools appear. Or you add an entry to
claude_desktop_config.json, restart, and find the server list unchanged. Nothing says whether
the URL is wrong, the transport is wrong, the server wants a login, or the app never read the
file. From the chat window, all four failures look the same.
What you get
You will end up with a remote server’s tools listed in Claude Desktop, one read-only call answered, and a probe that names the transport a URL speaks. This is for you if you run Claude Desktop and someone has handed you an MCP URL.
Short answer
Add the server’s Streamable HTTP URL as a custom connector in Claude Desktop’s settings, or,
where a connector cannot reach it, put an mcp-remote entry in claude_desktop_config.json and
restart the app. Probe the URL first: a server still on the deprecated HTTP+SSE transport
answers a Streamable HTTP client with a 405 and nothing more. Make the first target a public,
no-auth endpoint, so a transport fault is not mistaken for an auth fault.
You will need
Claude Desktop, a URL for a remote MCP server, and Node 22 or later if you take the mcp-remote
route. Verified 2026-09-25 against Node 22.22.2, @modelcontextprotocol/sdk 1.30.1,
mcp-remote 0.14.3, and zod 4.4.2. The first target here is a public endpoint with no
login: voxgig.com/mcp speaks Streamable HTTP, issues no
session id, and answers tools/list to anyone. A failure against it is a transport or config
failure, never an authentication one. The captured output below comes from a stand-in with
the same shape, because the check has to run offline.
Approaches compared
| Approach | When it fits | What it costs you | When to pick something else |
|---|---|---|---|
| A locally installed stdio server | The server ships as a package and works on data on your machine | A runtime to install, a full restart after every config edit, and updates you apply by hand on each machine | The tools live behind a URL someone else runs |
| Claude Desktop custom connector | A public Streamable HTTP URL, with OAuth or with no auth at all | The connection starts in Anthropic’s cloud, so a private-network URL never connects, and on Team and Enterprise plans only an Owner can add one | The server is on a VPN or on localhost, or your plan’s Owner will not add it |
mcp-remote stdio bridge | Any stdio-only client, and a URL on any network you can reach from your machine | One more Node process per server, a token cache under ~/.mcp-auth, and a bridge with flags of its own to debug | The client can connect to the URL itself |
Voxgig maintains the site at voxgig.com, whose /mcp endpoint is the first target on this
page. This page compares a custom connector with the mcp-remote bridge and a local stdio
server.
A custom connector is one URL typed into a settings screen. The connection it makes starts
in Anthropic’s cloud rather than on your laptop, so localhost and VPN addresses are out of
its reach. mcp-remote runs where you sit and reaches whatever you can, at the price of
a process and a token cache to debug. A local stdio server needs neither, and needs
installing on every machine.
Probe the URL before you touch a config file
The Streamable HTTP specification
tells a client how to find out which transport an unknown URL speaks. POST an initialize
request with an Accept header naming both application/json and text/event-stream. A
2xx with an initialize result is Streamable HTTP. A 400, 404, or 405 whose body is not a
JSON-RPC error means try a GET. If a stream opens and its first event is endpoint, the
server is on the HTTP+SSE transport of 2024-11-05, deprecated since 2025-03-26.1 The probe
does exactly that.
// A body cut off half way reads as no body: the status alone still decides the fallback.
const body = await res.text().catch(() => '')
const err = jsonRpcError(body)
if (err) return { transport: 'streamable-http', status: res.status, detail: `initialize refused: ${err.message}` }
if ([400, 404, 405].includes(res.status)) {
let get
try {
get = await fetchImpl(url, { headers: { accept: 'text/event-stream' }, signal: AbortSignal.timeout(timeoutMs) })
} catch (e) {
return { transport: 'not-mcp', status: res.status, detail: e.cause?.message ?? e.message }
}
if (get.ok && (get.headers.get('content-type') || '').startsWith('text/event-stream')) {
let first
try {
first = await firstEvent(get)
} catch (e) {
// A stream that stays silent until the timeout, or breaks, is reported like any other failure.
return { transport: 'not-mcp', status: res.status, detail: `the GET stream sent no event: ${e.cause?.message ?? e.message}` }
}
if (first?.event === 'endpoint') return { transport: 'http+sse (deprecated)', status: res.status, endpoint: first.data }
} else {
await get.body?.cancel().catch(() => {})
}
}
return { transport: 'not-mcp', status: res.status }
The jsonRpcError check before the GET matters. A server on revision 2026-07-28 answers a
version it does not support with a 400 and a JSON-RPC error. The specification says a client
must read that body before falling back, or a modern server reads as a web page.
The demo starts three servers on 127.0.0.1: the stand-in for the remote server, the same tools on the legacy transport, and a page of HTML. Then it connects to the stand-in both ways Claude Desktop can.
node demo.mjs
transport probe
remote server transport streamable-http POST 200 protocol 2025-11-25 server acme-docs 2.1.0 no session id
legacy server transport http+sse (deprecated) POST 405 endpoint event /messages?sessionId=14103df5-523b-4f1f-8095-f3591d08b06c
a web page transport not-mcp POST 200 unexpected content-type text/html
through mcp-remote, as Claude Desktop would start it
command npx -y mcp-remote http://127.0.0.1:45101/mcp
server acme-docs 2.1.0
tools search_docs (read-only), read_doc (read-only)
search_docs "api key" -> authentication Authentication
straight over Streamable HTTP, as a custom connector does
server acme-docs 2.1.0
tools search_docs (read-only), read_doc (read-only)
search_docs "api key" -> authentication Authentication
identical true
a Streamable HTTP client pointed at the legacy server
Streamable HTTP error: Error POSTing to endpoint: method not allowed
mcp-remote told the transport, pointed at the legacy server
tools search_docs (read-only), read_doc (read-only)
The legacy line is the pitfall in one row. The POST gets a 405 and no body a client can use,
which is what Claude Desktop’s log shows as a failed connection with no further detail. Only
the GET, and the endpoint event on the stream it opens, says why. The last block shows the
way through for a server that cannot move yet: mcp-remote with --transport sse-only
speaks the old transport on the client’s behalf.2
Run the probe against the real target before the config file exists. It prints one line and exits 0 only for Streamable HTTP:
node probe.mjs https://voxgig.com/mcp
Bridge the URL through a stdio process
Claude Desktop reads its servers from claude_desktop_config.json, at
~/Library/Application Support/Claude/ on macOS and %APPDATA%\Claude\ on Windows, and the
local servers guide
documents one entry shape: a command, its args, and an optional env. There is no
documented key for a URL. mcp-remote turns a URL into that shape by running as the command
and speaking stdio to the app while it speaks HTTP to the server.
export function bridgeEntry(url, { transport } = {}) {
const args = ['-y', 'mcp-remote', url]
// http-first is the default: try Streamable HTTP, fall back to SSE on a 404. A server
// known to be on the deprecated transport is told so, which saves the failed attempt.
if (transport) args.push('--transport', transport)
return { command: 'npx', args }
}
The file that results, for a server at docs.example.com:
{
"mcpServers": {
"acme-docs": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://docs.example.com/mcp"]
}
}
}
Save it, quit Claude Desktop completely, and start it again. The app reads the file at launch and never again, so an edit while it runs changes nothing until the next start. Then open the attachment menu in the conversation input, go to Connectors, and open the server to see its tools. The config checker in the same file reads a config the way the app does and names what the app would do with it:
node demo.mjs
claude_desktop_config.json, as Claude Desktop reads it
bridge entry ok
url entry acme-docs: url is not a documented key here, so nothing runs; add the URL as a custom connector, or bridge it with mcp-remote
servers key no mcpServers object: Claude Desktop finds no servers and reports nothing
The second and third lines are the two edits that produce an unchanged server list and no
error message. A url key is the shape other clients accept for a remote server, and this
file does not. A servers key is the shape VS Code writes, and this file reads mcpServers.
Add it as a custom connector
The custom connector is the route with no process to run. In Claude Desktop open Settings, choose Connectors, click Add and then Add custom connector, and paste the server URL. The connector guide says the feature is on Free, Pro, Max, Team, and Enterprise plans, and that a Free account gets one custom connector. On Team and Enterprise plans only an Owner can add one.3 A server that needs OAuth opens its login in the browser at this point, and a server with no auth connects at once.
One property of this route decides whether it fits at all. The connection to your server is
made from Anthropic’s infrastructure, not from your machine, on every client including
Desktop. A URL that resolves only on your VPN, or a server bound to localhost, never
connects this way. mcp-remote is the answer for those, because it runs on your side of the
firewall.
Check it worked
Whichever route you took, the check is the same: the tool list shows the server’s tools, and
one read-only call returns data. Against the stand-in that is search_docs (read-only) in
the list and authentication Authentication from the call, and the test suite asserts both
routes give the same answers.
node --test probe.test.mjs
1..14
# tests 14
# suites 0
# pass 14
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 2041.973409
Three of the fourteen guard the diagnosis. One holds the probe to the first message on a
stream rather than to its end, because a stateful server may keep the stream open after
the result. One holds the probe to the endpoint event, so a 405 from a page that is not MCP at all
is reported as not MCP. The third sends the probe a 400 carrying a JSON-RPC error, as a
2026-07-28 server does, and asserts that no GET follows. Two more cover the probe’s failure
paths: a body cut off, a silent stream, and a directory with a space in its path.
When it goes wrong
The server never connects and mcp-server-<name>.log shows a failed POST with a 405. The
server is on the HTTP+SSE transport. Run the probe to confirm, then add --transport sse-only
to the mcp-remote entry, or ask the server’s maintainer for a Streamable HTTP endpoint.
The file was edited and the app shows the old list. Claude Desktop reads the config at
launch. Quit it from the menu, not by closing the window, and start it again. The MCP logs,
mcp.log and one mcp-server-<name>.log per server, sit under ~/Library/Logs/Claude on
macOS and %APPDATA%\Claude\logs on Windows, and the
debugging guide covers
what each one records.
The log shows ENOENT for npx. The app could not find it on the path it was started with.
Put the absolute path to npx in command; the local servers guide has the Windows-specific
fix for %APPDATA% in env.
A custom connector to a server on your own network times out. The connection comes from
Anthropic’s IP ranges, which the connector guide lists a link to, so either allow those
ranges at the firewall or use mcp-remote from your machine instead.
When not to do this
Do not bridge a server that a custom connector can reach. Every mcp-remote entry is a Node
process the app starts, a ~/.mcp-auth directory to clear when a login goes stale, and one
more place a connection can fail. The bridge exists for the case the connector cannot cover,
and it is the wrong default for the case it can.
Do not make a server that needs OAuth your first target. When the login fails you cannot tell an auth fault from a transport fault, which is the whole reason this page starts from a public endpoint. The Voxgig site’s endpoint is read-only and open, which makes it a fine first target and nothing more: it is a check on your client, not a tool for your work.
Do not put a bearer token in args. Anyone on the machine can read the process list, and
the mcp-remote README documents --header-file for exactly that reason.
Do not keep a bridge entry after the app grows a route of its own to that server. A bridge nobody remembers adding is the first suspect when a tool call fails a year later.
Related how-tos
Last verified
Verified 2026-09-25 against Node 22.22.2, @modelcontextprotocol/sdk 1.30.1, mcp-remote
0.14.3, and zod 4.4.2. Every output block is what the command preceding it printed. The
remote server in the output is a stand-in on 127.0.0.1. The probe was also run against
voxgig.com/mcp, which answered as Streamable HTTP, protocol 2025-11-25, with no session id.
Claude Desktop itself was not driven by a script: the connector and config steps are the
ones the vendor’s documentation gives.
Footnotes
-
The transport has had three shapes in twenty months, and the first is on its way out with paperwork. The 2026-07-28 changelog reclassifies HTTP+SSE, deprecated since 2025-03-26, as Deprecated under a feature lifecycle policy adopted in the same revision, which sets a minimum deprecation window of twelve months. A transport deprecated for a year and a half is placed on twelve months’ notice. That is the arithmetic of a governance process that arrives after the fact. ↩︎ Back to text
-
The bridge is a package that documents its own retirement. Its README explains that most clients were stdio-only when it was written, and that as soon as your chosen client supports remote, authorized servers, you can remove it. The same README then lists more than twenty flags, four transport strategies, and a protocol-era mode. That is the usual fate of a stopgap: it stays until the gap closes, and the gap is in no hurry. ↩︎ Back to text
-
The connector guide is specific about where the connection comes from: Anthropic’s cloud infrastructure, on every client, the desktop app included. It then names the one thing that does use your local network, the servers in
claude_desktop_config.json, and adds that those are not available on the web. A desktop application whose remote connections leave from someone else’s data center, and whose local connections stay home, is two products sharing a window. ↩︎ Back to text