Connecting Claude, Cursor and Codex to a remote MCP server
Remote MCP over Streamable HTTP removed the local bridge process, and with it most of the reasons MCP setups used to fail. What is left is a smaller set of confusions: which transport your client actually opened, what the OAuth handshake is exchanging, and why the tool list sometimes comes back shorter than the documentation promised.
Two kinds of MCP server, and why it matters
Nearly every MCP integration you have installed is a local server: a package your client launches as a child process, talking JSON-RPC over stdin and stdout. It holds your API key, translates tool calls into HTTP requests against some vendor's API, and dies when your editor closes.
A remote server skips the middle layer. The vendor's service speaks MCP itself, over HTTPS, and your client is just an HTTP client. Nothing is installed, nothing runs locally, and there is no npm package sitting between your credentials and the API.
The practical differences show up fast. A local server needs a runtime you have to keep current, and a key pasted into a config file in plaintext. A remote server needs a URL, and authenticates through OAuth so the token lives in your client's credential store and can be revoked from the vendor's dashboard. When a local server breaks you debug a subprocess; when a remote one breaks you read an HTTP status code.
Streamable HTTP in one paragraph
Streamable HTTP replaced the older HTTP+SSE transport. Your client POSTs a single JSON-RPC message to one endpoint URL and reads the response. The server may answer with plain JSON, or upgrade to an SSE stream if it needs to send several messages for one request. A server is free to implement only the JSON half, and many do, because most tool calls are a simple request and response.
That freedom is the source of the most common setup failure. A client configured for the old SSE transport opens a long-lived GET against the endpoint expecting an event stream. A JSON-only server has nothing to serve on GET and answers 405 Method Not Allowed. The client reports that the server is unreachable, which is misleading: the server is fine, the verb is wrong.
What the handshake actually exchanges
Before debugging any client, it helps to have seen the raw conversation. It is three messages, and you can drive all of them with curl.
# 1. initialize — agree on a protocol version curl -s -X POST https://c1k.me/mcp/org_EXAMPLE \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "jsonrpc":"2.0","id":1,"method":"initialize", "params":{ "protocolVersion":"2025-11-25", "capabilities":{}, "clientInfo":{"name":"curl","version":"1.0.0"} } }' # 2. notifications/initialized — a notification, so 202 and no body curl -s -i -X POST https://c1k.me/mcp/org_EXAMPLE \ -H "Authorization: Bearer $TOKEN" \ -H "MCP-Protocol-Version: 2025-11-25" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' # 3. tools/list — what this token is actually allowed to see curl -s -X POST https://c1k.me/mcp/org_EXAMPLE \ -H "Authorization: Bearer $TOKEN" \ -H "MCP-Protocol-Version: 2025-11-25" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
Two details trip people up. First, after initialization every request must carry MCP-Protocol-Version; omitting it is a protocol error, not a warning, and servers are entitled to answer 400. Second, a notification has no id and therefore no response — a 202 with an empty body is success, not a failure.
If those three calls work with curl and your client still cannot connect, the problem is in the client configuration and not in the server. That single fact saves a lot of time.
The OAuth part, without the spec language
When you connect without a token, a well-behaved remote server answers 401 with a WWW-Authenticate header pointing at its protected-resource metadata. Your client fetches that, discovers the authorization and token endpoints, opens a browser, and you approve a consent screen naming the scopes. The client exchanges the resulting code for an access token using PKCE, so the code is useless to anyone who intercepts it.
Three things about that flow cause real-world failures.
Many clients expect to register themselves on the fly. A server that deliberately does not advertise dynamic registration — a reasonable choice for a small service that wants a known list of clients — will reject that attempt. The fix is not on your side: an administrator has to preregister the client and its exact redirect URI.
Fifteen minutes is typical. Your client refreshes silently, so you never notice — until a client with a buggy refresh implementation starts failing an hour into a session. If a connection works and then dies reproducibly after a fixed interval, suspect refresh rather than the network.
Each refresh issues a new token and invalidates the old one. Replaying an old one is treated as theft and revokes the whole family. This is why copying a client config between two machines quietly breaks both: they fight over the same rotating token. Authenticate each machine separately.
Client by client
Every configuration below points at the same URL. Where a client supports both OAuth and a static bearer token, prefer OAuth for anything interactive and reserve tokens for CI and headless agents.
Claude Code
The cleanest of the set, because scope is explicit. --scope project writes to a committed .mcp.json, --scope user keeps it to your machine.
claude mcp add --transport http c1k https://c1k.me/mcp/org_EXAMPLE # then, inside a session, authenticate /mcp # headless alternative claude mcp add --transport http c1k https://c1k.me/mcp/org_EXAMPLE \ --header "Authorization: Bearer $C1K_MCP_TOKEN" claude mcp list claude mcp get c1k
Committing .mcp.json to a shared repository is genuinely useful — everyone gets the server, each person authenticates individually. Never commit the header variant.
Claude Desktop
Add a custom connector in settings and approve consent in the browser, or edit claude_desktop_config.json and restart. On macOS it lives in ~/Library/Application Support/Claude/, on Windows in %APPDATA%\Claude\.
{
"mcpServers": {
"c1k": {
"type": "http",
"url": "https://c1k.me/mcp/org_EXAMPLE"
}
}
}
The restart is not optional; the config is read at launch. If your edit appears to have no effect, that is almost always why.
Codex CLI
Configuration is TOML in ~/.codex/config.toml. Reference an environment variable rather than pasting a literal token.
[mcp_servers.c1k]
url = "https://c1k.me/mcp/org_EXAMPLE"
[mcp_servers.c1k.http_headers]
Authorization = "Bearer ${C1K_MCP_TOKEN}"
"MCP-Protocol-Version" = "2025-11-25"
Cursor
Project config in .cursor/mcp.json, global in ~/.cursor/mcp.json. Omit headers entirely to use the browser OAuth flow.
{
"mcpServers": {
"c1k": {
"url": "https://c1k.me/mcp/org_EXAMPLE",
"headers": {
"Authorization": "Bearer ${env:C1K_MCP_TOKEN}",
"MCP-Protocol-Version": "2025-11-25"
}
}
}
}
Check Settings → MCP after saving. A connected server shows its tool list; a server that connected but lists no tools is a scope problem, covered below.
VS Code with Copilot agent mode
VS Code reads .vscode/mcp.json. Its input mechanism is the best answer to the “never commit a token” problem of any client here: declare the secret as a prompted input and the editor asks at runtime.
{
"inputs": [
{
"id": "c1k-token",
"type": "promptString",
"description": "c1k.me MCP service token",
"password": true
}
],
"servers": {
"c1k": {
"type": "http",
"url": "https://c1k.me/mcp/org_EXAMPLE",
"headers": { "Authorization": "Bearer ${input:c1k-token}" }
}
}
}
JetBrains IDEs
Settings → Tools → AI Assistant → Model Context Protocol. Add a server, choose the remote HTTP option, paste the URL. The dialog also accepts a pasted JSON block in the same shape as Cursor's. Keep tokens in the IDE password safe rather than in a run configuration that syncs.
ChatGPT
Added as a custom connector in settings, authenticated through OAuth in the browser. This is the client most likely to hit the dynamic-registration wall, because it expects to register itself. Against a server with a preregistered client list, someone has to add ChatGPT's redirect URI first; until then the authorization request is refused and the error message will not explain why.
Why your tool list is shorter than the docs
A well-built remote server filters tools/list by the scopes your token actually holds. If you granted read access, the write and delete tools are not merely rejected when called — they are never advertised. The model cannot attempt them, cannot hallucinate arguments for them, and cannot tell you it “tried but was blocked”.
This is the correct behaviour and it looks like a bug the first time. Before filing one, call the server's capability tool — on c1k.me that is get_capabilities — and compare the effective scope set with what you expected to grant.
While you are there, resist granting more than you need. An assistant that reports on links needs read scopes and nothing else. Destructive scopes for a model should be the exception you can justify out loud, not the default you clicked through.
A debugging table
Two habits worth forming
The first is scope discipline. Grant the narrowest set that completes the task, and create a separate connection when an agent genuinely needs more. Revoking one narrow connection is easy; auditing one over-scoped connection after the fact is not.
The second is remembering that tool output is untrusted input. Titles, descriptions and URLs coming back from a remote server originate with people, and sometimes with strangers. A model reading them should treat them as data rather than as instructions. Servers can help — by never embedding credentials in returned URLs, and by shaping output so it does not read as a command — but the boundary is worth holding on your side too.
Pick the HTTP transport rather than SSE. Send the protocol version header on every request after initialization. Expect the first 401. Do not share one authenticated config between machines. If the tool list looks short, check your scopes before blaming the server. And verify with curl before debugging a client — three commands will tell you which side the problem is on.