cd /news/ai-agents/shipping-a-remote-mcp-server-with-oa… · home › topics › ai-agents › article
[ARTICLE · art-168286] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Shipping a remote MCP server with OAuth: what the clients actually check

A developer at Prisma Reviews deployed a remote MCP server in front of the company's reviews API, letting Claude, Cursor, VS Code and other MCP clients read reviews across 84 platforms and, with user approval, publish replies where platform APIs allow. The writeup details the OAuth discovery flow clients actually follow: the server returns a 401 with a WWW-Authenticate header pointing to RFC 9728 resource metadata, exposes its tool list via a static server card at /.well-known/mcp/server-card.json rather than answering unauthenticated initialize calls with 200, and accepts both OAuth and Bearer/X-API-Key headers. The server uses stateless JSON responses with no SSE stream or session id, supports three MCP protocol versions, and marks its seven read-only tools with readOnlyHint while flagging reply_to_review as destructive.

by read5 min views2 publishedNov 12, 2026

Two weeks ago we put a remote MCP server in front of our reviews API. It lets Claude, Cursor, VS Code and other MCP clients read reviews from the 84 platforms the API covers, and publish a reply, with the user's approval, on the ones whose API allows replies. (How few of them do is the subject of an earlier post.) The tools were the easy part, because each one forwards to a REST route we already had. Most of the work went into getting clients to connect without a pasted API key.

These are the things that mattered, in the order we ran into them.

A connector UI that supports OAuth doesn't know your login page. It finds it by calling the server with no token and reading the 401 response:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://api.prismareviews.com/.well-known/oauth-protected-resource/mcp", scope="reviews"
Access-Control-Expose-Headers: WWW-Authenticate

The resource_metadata URL (RFC 9728) points to a small JSON document that names the authorization server. From there the client reads /.well-known/oauth-authorization-server and gets the authorize, token, registration and revocation endpoints. Ours advertises only authorization_code with PKCE S256 and public clients (token_endpoint_auth_methods_supported: ["none"]), because that's all an MCP client needs.

Two things bit us:

Access-Control-Expose-Headers the client never sees WWW-Authenticate and gives up.initialize and tools/list without auth, so that directories could list our tools. We didn't. A 200 on the first call is how a client decides that a server needs no login. If we had returned one, every connector would have skipped OAuth and then failed on the first real call. Instead we publish the tool list, with the input schemas, as a static server card at /.well-known/mcp/server-card.json. Directories read that; the MCP endpoint itself still answers 401. MCP clients usually register themselves (RFC 7591) before they start the OAuth flow, and there are many of them. We didn't want a table that grows by one row each time someone opens a new Cursor window. So the flow keeps no state on our side:

/v1 with a normal key, so the MCP layer doesn't need a second auth path. Validating redirect URIs took more care than we expected. Desktop and CLI clients come back to http://localhost or 127.0.0.1 on a random port, and editors use their own schemes, such as cursor:// and vscode://. We allow https, loopback http and custom schemes. We reject plain http on the internet, URLs with a fragment, and javascript:, data:, file: and similar schemes.

The consent screen asks whether to connect Live (your real accounts) or Test (sample data from every platform). People who are only trying the connector don't need to connect a platform account first. It also gave the directory review a test account with data to look at, instead of an empty one.

Claude Code, Cursor and VS Code configs take a header, and some people prefer an API key to OAuth. So the same URL also accepts Authorization: Bearer <key>. Some gateways keep Authorization for themselves, so the server also accepts X-API-Key. Claude Desktop needs mcp-remote to pass a header to a remote server, so the docs give that config in full.

Our tools are request and response: list reviews, get stats, reply. So the server answers each POST with application/json, with no SSE stream and no session id. That runs well on serverless functions, and no client has needed more. We accept the 2025-06-18, 2025-03-26 and 2024-11-05 protocol versions, because clients upgrade at different speeds.

We have ten tools. Seven only read, and they're marked readOnlyHint: true. reply_to_review is the one that matters:

annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }

The reply is public and can replace an earlier one, so we mark it destructiveHint. Clients use that to ask the user before running the tool. openWorldHint is there because the text leaves our system and appears on the review platform.

Retries were the other risk. A reader pointed it out in the comments of an earlier post: on platforms where a reply can only be created, never edited, a retry after a dropped connection could publish the same text twice. The REST route under the tool accepts an Idempotency-Key header, so a repeated request with the same key gets the stored response instead of a second reply.

We also kept the tool descriptions plain: what the tool does and what it returns. They don't tell the model what to do next. It's easier to keep descriptions honest from the start than to rewrite them for a directory review.

This one is new since the first version of the server. Some of our tools can't finish a job without the account owner, such as create_connect_link, which returns a link the owner opens to connect a platform that needs a browser sign-in. But an agent that has created or opened an account for someone can still do the paperwork.

Every new account answers three sign-up questions: where the owner heard of us, which review platforms they need, and how many locations, apps or listings they have. complete_onboarding takes those answers over MCP and writes them through PUT /v1/onboarding, the same route the dashboard form uses:

annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }

platforms and locations_count are required.origin is optional. If the agent doesn't know where the owner heard of us, it leaves origin_detail and platforms_other are required in that case, with a length limit. It's a small tool, and it is the only one that changes the account rather than reading from a platform or publishing to one. That's why it has its own annotations instead of reusing the read-only set.

com.prismareviews/reviews, and we publish a new server.json version whenever the tools or the platform list change. It's on 1.0.5 now, and the description names the platform count ("... Viator and 79 more"). The older versions stay in the registry, flagged as not the latest./.well-known/glama.json, and shows whether the server responds. Start with the 401 and the metadata documents, and test them with a real connector UI before writing any tools. If discovery doesn't work, the tools never get called. Of the whole setup, only the redirect URI rules needed real security thought.

The server is at https://api.prismareviews.com/mcp. Setup for each client, the tool list and the API route behind each tool are in the docs: https://prismareviews.com/docs/mcp (there's also a shorter overview at https://prismareviews.com/reviews-mcp). If you run a remote MCP server and found that clients behave differently from this, I'd like to hear about it in the comments.

── more in #ai-agents 4 stories · sorted by recency
── more on @prisma reviews 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/shipping-a-remote-mc…] indexed:0 read:5min 2026-11-12 · —