How-to › Use AI to do the integration

How to connect a remote MCP server to Claude Desktop#

Get a remote MCP server answering tool calls in Claude Desktop, as a custom connector or through a stdio bridge, and probe its transport before blaming the config.

Audience
API consumer
Level
beginner
Topic
Connect MCP clients to servers
Verified

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

ApproachWhen it fitsWhat it costs youWhen to pick something else
A locally installed stdio serverThe server ships as a package and works on data on your machineA runtime to install, a full restart after every config edit, and updates you apply by hand on each machineThe tools live behind a URL someone else runs
Claude Desktop custom connectorA public Streamable HTTP URL, with OAuth or with no auth at allThe connection starts in Anthropic’s cloud, so a private-network URL never connects, and on Team and Enterprise plans only an Owner can add oneThe server is on a VPN or on localhost, or your plan’s Owner will not add it
mcp-remote stdio bridgeAny stdio-only client, and a URL on any network you can reach from your machineOne more Node process per server, a token cache under ~/.mcp-auth, and a bridge with flags of its own to debugThe 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.

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

  1. 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

  2. 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

  3. 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

Read this page as markdown · All how-to guides

Generate the client instead of writing it#

Retries, timeouts, pagination and auth are the same problems in every client. Voxgig generates them from your OpenAPI description, in 23 languages, from one model.

Get the Voxgig dispatch

Short notes on building SDKs, CLIs, REPLs, and MCPs for API-first teams, plus the occasional Fireside episode pick.

By signing up you agree to our Terms and Conditions.