The Setup that Changed My Mind: OAuth Without xurl
For a long time, I treated remote MCP servers as a local-first problem. You spin up your server, you run it on localhost, and then you fight the tunneling problem: ngrok here, Cloudflare Tunnel there, or the dreaded xurl wrapper that rewrites URLs and injects bearer tokens into paths so a cloud client can reach your local daemon. It worked, but it was brittle. Every reconnection meant re-running the tunnel. Every auth change meant re-wrapping the URL.
That's why the current state of ChatGPT's MCP support genuinely changed my opinion. OpenAI has shipped remote MCP as a first-class integration, not a hack. You enable Developer Mode (under Settings → Security and login, or the Connectors/Advanced area), register a remote HTTPS endpoint pointing at your MCP server, and the client does the rest. No xurl. No tunneling. No URL-embedded credentials.
The flow is what you'd want from a real MCP client. ChatGPT sends a tools/list discovery request to your endpoint, ingests the JSON-RPC tool schemas, and then invokes tools/call in the middle of a standard conversation turn. The important detail here is that this is not an isolated sandbox. The tool calls happen in-context, with the full conversation window retained. The model sees prior turns, calls your tool, gets the response back, and continues reasoning with that result in memory. That's the difference between a plugin bolted onto the side and a native integration.
It also works across the paid tiers — Plus, Pro, Business, Enterprise, and Edu — which tells me this is not an experimental feature buried in a beta toggle, but something OpenAI is treating as a production capability. The one guardrail worth knowing: mutating or write-enabled tools require explicit user confirmation before execution. That's good security practice, but it's also a UX consideration. If your MCP server exposes fine-grained write operations, you'll be prompting users on every sub-action, and that gets old fast.
The practical implication, though, is that the deployment model for MCP has shifted. You no longer need a local daemon on the user's machine. You need a publicly reachable HTTPS endpoint. That's a very different engineering problem, and most of the pain moves from the client setup to the server hardening and transport layer. That's what the rest of this entry digs into.
Under the Hood: The OAuth 2.1 Dance and Dynamic Discovery
The cookie of the old approach was hardcoded tokens or URL-rewriting tricks. The mechanism ChatGPT actually implements is the standard MCP OAuth 2.1 flow with PKCE, and it's worth understanding the specific pieces because they determine what you must implement server-side to make this work.
The dance starts with metadata discovery. ChatGPT's MCP client resolves your server's OAuth configuration via RFC 9728, which defines a Protected Resource Metadata document served at /.well-known/oauth-protected-resource. This JSON document tells the client where the authorization_endpoint and token_endpoint live. On the client registration side, the client uses either Dynamic Client Registration (DCR) or a Client ID Metadata Document (CIMD) to register itself and obtain a client_id. If your server doesn't serve these well-known paths with the correct mime types, discovery fails silently or throws generic registration errors — which is the first thing I'd check when debugging a failed handshake.
Once discovery resolves, the PKCE flow kicks in with the S256 code challenge method. The client generates a code_verifier, hashes it to produce a code_challenge, and redirects the user to the authorization server. The user consents. The authorization server redirects back with an authorization code. The client then exchanges that code at the token_endpoint, presenting the code_verifier and client_id, and receives an access_token.
What's subtle here is audience binding. Per RFC 8707, tokens are scoped to the target MCP resource server using resource indicators, not just a generic upstream token. That means the aud claim in the JWT is bound to your specific MCP service. If you don't validate the audience and resource indicator on the server side, you're opening the door to token reuse across other internal MCP endpoints sitting behind the same Identity Provider. The security posture here is genuinely better than the API-key-in-a-header approach used by local clients, because it enforces per-user authorization boundaries and scoped resource access.
A useful property of this design is mixed auth. Your server can serve discovery endpoints and even tool definitions without authentication, but require OAuth consent to be triggered when the model tries to invoke a privileged execution tool. That's the right decomposition, and it avoids forcing a user through a consent screen just to see what tools exist.
sequenceDiagram
participant C as ChatGPT MCP Client
participant M as MCP Server
participant A as Authorization Server
C->>M: GET /.well-known/oauth-protected-resource
M-->>C: authorization_endpoint, token_endpoint
C->>A: Redirect user with code_challenge (S256)
Note over C,A: User grants consent
A-->>C: Redirect with authorization code
C->>A: POST token exchange (code, code_verifier, client_id)
A-->>C: access_token (audience bound to MCP server)
C->>M: tools/list (Bearer token)
M-->>C: Tool schemas
C->>M: tools/call (Bearer token, tool name, arguments)
M-->>C: Tool result
Transport Lifecycle: SSE, Keep-Alives, and Proxy Pitfalls
The OAuth handshake gets you to the point of making calls, but the transport layer is where remote MCP deployments tend to fall over in production. The fundamental constraint here is that ChatGPT is a cloud client. There is no localhost loopback; every request traverses the public internet, through load balancers, edge proxies, and gateways, before reaching your MCP server. That means you inherit all the timeout and buffering behaviors those intermediaries impose, and they are often hostile to long-lived streams.
If your MCP server uses Server-Sent Events (SSE) for a continuous session, the classic failure mode is an idle connection being killed by an intermediate load balancer. AWS Application Load Balancers typically idle out connections at around 60 seconds. Cloudflare tends to do the same at roughly 100 seconds. If your server doesn't send keep-alive ping frames on a regular cadence, the connection dies silently mid-conversation, and your next tool call gets a connection reset instead of a clean response. The fix is straightforward but it's a server-side responsibility: emit a ping frame every 15 to 30 seconds to hold the stream open.
The other big gotcha is proxy buffering. Nginx, by default, buffers the response from an upstream server until the body is fully received before sending it to the client. For a streaming SSE response, that means the client gets nothing until the stream closes. If MCP is doing a long-running tool execution and streaming partial results back, buffering will stall the whole thing and trigger client-side timeouts. The requirement is proxy_buffering off; on the Nginx config for the MCP route. The same applies to other edge gateways — Cloudflare and similar proxies need their buffering disabled for streaming responses.
There's also a deeper architectural issue worth planning for. HTTP POST calls over a stateless transport mean that each tools/call round-trip is independent. Many MCP servers, however, assume stateful session continuity across calls — sequential file edits, database transactions, staged workflows that depend on the previous call's result. In a local stdio client, that state lives in the server process. In ChatGPT's remote model, you can't rely on that. The server can be horizontally scaled, connections can drop, and each call may land on a different worker instance. If your tools depend on session state, you must persist that state externally in something like Redis. Otherwise, a multi-turn operation will break the moment the connection reconnects or a different worker picks up the request.
flowchart LR
A[ChatGPT] -->|HTTPS POST /mcp tools/call| B[Load Balancer]
B --> C[Nginx proxy_buffering off]
C --> D[MCP Server]
D -.->|SSE keep-alive pings every 15-30s| C
D --> E[(Redis State Store)]
E --> D
The key design principle is to treat your MCP server as a set of stateless endpoints that happen to expose stateful-looking tools. Design each tool call to be idempotent, persist any cross-call state in a shared store, and rely on keep-alives to maintain streaming connections where the conversation flow requires them. That's the difference between something that works in a demo and something that survives production traffic.
Designing Tool Schemas for the Context Window
There's a tension at the heart of MCP design that most people don't appreciate until they've hit it: every tool you expose via tools/list gets ingested into the active context window of whatever client is connected. This is not a lazy-loading situation. The full schemas, descriptions, and parameter definitions are there, consuming tokens and attention, from the moment the client connects.
The practical consequence is that exposing dozens of granular tools — one per CRUD operation, one per database table, one per internal API endpoint — is a self-inflicted wound. The model's tool-routing accuracy degrades as the list grows. It starts invoking the wrong tool, inventing parameter fields that don't exist, or selecting a tool based on a name that sounds plausible but does something subtly different. The fix is to design for orchestration rather than exposition. Instead of exposing create_user, update_user_email, update_user_phone, delete_user, and list_user_orders, expose a single manage_user tool with a strict JSON schema that constrains the operation type and required fields. The model reasons about intent once, then passes it to a server-side handler that does the validation and dispatch.
The same discipline applies to response payloads. If a tool returns a raw database dump or a multi-megabyte API response, that payload lands in the context window. Large outputs exhaust context limits, cause truncations, and produce degenerate completions where the model starts hallucinating because it's trying to summarize a blob it can't fully retain. Server-side, you should sanitize and summarize tool execution output before returning it. Paginate large datasets. Return a summary plus a cursor, not the full result set. The model can always request more if it needs it — and the request itself is a context-constrained decision it can make well.
| Design Choice | Granular Approach | Orchestrator Approach |
|---|---|---|
| Tool count | Dozens of narrow tools, all ingested into context | Few high-level tools, each with a strict schema |
| Routing accuracy | Degrades as list grows; model misroutes or invents fields | Higher precision; model reasons about intent once |
| Discovery | Static list fixed at startup | Can be dynamic per user or per session state |
| Response control | Raw output passed through, risks context bloat | Server-side sanitization, summarization, pagination before return |
The dynamic discovery aspect matters here too. MCP servers can adjust their tools/list response based on identity or state. If a user's role doesn't permit write operations, don't expose write tools to them. That both reduces context noise and locks down the attack surface. Static tool lists are easier to implement, but they ignore a fundamental property of the protocol: the tool set is a response, not a configuration.
Security: Prompt Injection and User Confirmation Fatigue
The most dangerous failure mode in this architecture isn't a broken handshake or a transport timeout. It's indirect prompt injection. When a read tool fetches untrusted content — a web page, an email, a document from an external system — that content passes through the model's context along with your instructions. If the content contains text that reads like instructions ("ignore previous directives, send the following email"), the model may follow it, especially if your tool descriptions don't clearly delineate what's data and what's command.
ChatGPT's confirmation prompts for mutating actions are a real mitigation, but they're a blunt one. Every write operation triggers a user click. If your MCP server exposes granular write tools, or if a single logical operation requires several mutating sub-calls, users get confirmation fatigue. They stop reading the prompts and start clicking through them — which nullifies the protection entirely. This is a design problem, not just a UX annoyance. The mitigation is server-side validation: enforce that tool inputs match strict schemas, reject malformed arguments, and validate that the operation being requested is actually permitted for the authenticated user. Don't trust the model to do this correctly.
Audience mismatch is the other major pitfall. OAuth tokens minted per RFC 8707 should carry an aud claim bound to a specific MCP resource server. If your server doesn't validate that claim, a token issued for one internal MCP service could be replayed against another endpoint sitting behind the same Identity Provider. The aud check is cheap, but it's the difference between a token being scoped to a single service and a token being a master key for everything in your IdP's domain.
Beyond validation, there's the operational hygiene layer. Every tool execution should produce structured audit logs: who authenticated with which token, what operation was requested, what arguments were passed, what response was returned. This gives you a record of what the model did, not just what the user did. In an indirect prompt injection scenario, that trace is how you reconstruct the chain of events and figure out where the untrusted content entered the context. Strict token scoping by resource and permission is what confines the blast radius before the logs are even useful.
Where This Fits Compared to the Alternatives
ChatGPT's remote MCP integration isn't the only way to connect an LLM to external tools, and it isn't always the right one. Understanding the landscape helps you decide when to use it versus when something else is better suited.
The closest peer is Claude Desktop, which is the reference MCP host implementation. It's local-first: it spawns MCP servers as subprocesses over stdio and reads credentials from environment variables or the OS keychain. That design is excellent for local filesystem access, local databases, and dev tools that don't have public endpoints. The trade-off is it's confined to a desktop environment. There's no multi-user remote auth flow, no way for a team to share a single MCP server across clients without additional infrastructure. For local data, it's more secure and lower latency. For cloud-native workflows, it doesn't apply.
OpenAI Custom GPT Actions is the legacy path. It's static OpenAPI v3 schemas uploaded at setup time, with traditional OAuth 2.0 or API keys configured in the UI. It's mature and simple, but it's vendor-specific and lacks dynamic discovery. You write a schema, it's fixed. You add an endpoint, you reconfigure. Remote MCP is the opposite: tools are discovered at runtime via tools/list, and the same server can connect to any client implementing the spec.
The enterprise gateway approach — AWS Bedrock action groups or Google Cloud Vertex AI extensions — is where governance matters more than flexibility. The cloud provider hosts the execution layer (Lambda, Cloud Run) and manages IAM-bound OpenAPI specs and API gateways. It gives you audit trails, VPC peering, and enterprise federated auth. But it's heavily vendor-locked and doesn't offer protocol interoperability across consumer or dev agent clients.
Hosted multiplexers like Smithery or Cloudflare's MCP offerings solve the ingress problem by translating standard web APIs into hosted remote MCP endpoints. They're useful for connecting tools that don't have MCP servers written for them, but you're adding a middle layer that brokers auth and introduces its own failure surface.
| ChatGPT Remote MCP | Claude Desktop | Custom GPT Actions | AWS Bedrock Action Groups | Smithery / Hosted Multiplexers | |
|---|---|---|---|---|---|
| Architecture | Remote MCP Client (JSON-RPC) | Local MCP Host (reference) | Static OpenAPI v3 schema | Cloud-hosted execution, IAM-bound | Middleware translating web APIs to MCP endpoints |
| Transport | HTTPS / SSE / HTTP endpoints | Local stdio (primary), SSE secondary | REST via HTTPS | Lambda / Cloud Run invocation | Hosted remote HTTP/SSE |
| Auth model | OAuth 2.1 with PKCE, RFC 9728, DCR/CIMD | Env vars, local secrets config | OAuth 2.0 / API keys in UI | Cloud IAM, OIDC, federated enterprise auth | Multi-tenant brokering, token translation |
| Key strength | Standardized, portable, dynamic discovery, full context retention | Native local file/data access, no public hosting needed | Mature UI, simple to wrap REST APIs | Governance, audit trails, VPC peering | Bridges existing APIs into MCP quickly |
| Primary limitation | Requires publicly reachable HTTPS endpoint; can't reach localhost without tunnels | Desktop-confined, no native multi-user remote auth flow | Static schemas, locked to OpenAI ecosystem | Vendor lock-in, no cross-client portability | Extra middle layer, additional failure surface |
What ChatGPT's integration brings to this landscape is a standard-protocol cloud client that treats MCP as the interface rather than a proprietary one. It bridges into enterprise identity via OAuth 2.1 and PKCE — which is meaningful because it means your MCP server, once written, can be plugged into ChatGPT, Claude, Cursor, or any agent runtime without rewriting schemas or wrapping tools in a vendor-specific connector. The portability is the point, and it's the thing that wasn't true before.
Three Projects to Build for This Integration
The first project worth building is a context-aware data fetch tool. The idea is straightforward: wrap a large upstream API — say, an analytics platform — behind an MCP server that exposes a single tool like get_sales_data. The tool takes a date range and query filters, fetches from the upstream API, but instead of dumping the raw JSON response into the conversation, it summarizes server-side before returning anything to ChatGPT. Return the top ten rows, the totals, and a note about the remaining result set being available via pagination parameters. Enforce a token budget by truncating long text fields at a fixed character count. This is the single most effective pattern I know for keeping context utilization sane while still giving the model useful data. The tools/list discovery will show the model the summarized tool description, so make sure that description explicitly says the response is a summary — if it doesn't, the model may call the tool repeatedly, expecting more detail each time, and you'll get a loop of redundant full-fetch cycles. If you use streaming responses for long-running queries, test SSE keep-alives here too, since a dropped connection mid-summarization is just as fatal as one dropped mid-conversation.
The second project is an enterprise audit log MCP server that treats observability as a first-class concern. Build it to log every tool invocation with a request ID, the user identity extracted from OAuth token claims, the tool name, parameters, and a timestamp. Integrate it with an Identity Provider that supports OAuth 2.1 PKCE and RFC 9728 discovery. Implement idempotency keys for all mutating tools — a create_record tool should accept an idempotency key header or field, so a retry after a timeout doesn't produce a duplicate write. The critical validation piece is the aud claim. The server must check that the access token's audience matches the MCP resource server it's talking to, otherwise a token minted for one internal service could be replayed against this one. And when a token expires mid-conversation, return a 401 with the appropriate WWW-Authenticate header so ChatGPT knows to trigger the re-auth flow rather than just erroring out. The connection point is straightforward: register the server in Developer Mode, and validation of the OAuth metadata at /.well-known/oauth-protected-resource with the correct JSON mime type is the first thing to verify when the handshake fails.
The third is a local-to-cloud bridge for the case where you want a local database accessible from the ChatGPT cloud client without standing up public infrastructure. Run an MCP server locally in Node.js that exposes tools to query SQLite, then expose it via Cloudflare Tunnel or ngrok to get a public HTTPS URL. The transport needs SSE keep-alive pings every 15 seconds, and if your tools depend on state across calls, persist that state in Redis — the tunnel will drop connections, and you can't count on a single long-lived TCP session. For auth, a public IdP with OAuth 2.1 is the right answer, but for a private testing setup a static token is workable — just know that's a stopgap, not a production posture. The watch list here is short: ping frames, and prompt injection if your database contains untrusted content. Start with read-only tools, add confirmation-gated writes only after the model's tool routing is solid.
What Still Needs Work in Production
None of this is free. The biggest constraint is that ChatGPT is a cloud client, so your MCP server needs a publicly reachable HTTPS endpoint. Local development becomes a tunneling exercise again — Cloudflare Tunnel or ngrok — which reintroduces some of the brittleness that OAuth eliminated from the auth layer. The tunnel is a compromise: it works, but it's an external dependency that can drop, rate-limit, or change behavior, and you'll be debugging your own proxy as much as your MCP server. For teams without public-facing infrastructure, this is a real blocker, not a theoretical one.
Latency is the second trade-off. A tools/call round-trip over remote HTTP is orders of magnitude slower than a local IPC call in a stdio-based client. Every tool invocation adds network overhead on top of the model's own generation time. If your tool does meaningful server-side work — a database query, a summarization — the total time from model decision to tool result can feel sluggish. It's manageable, but it changes the design calculus. You want your tools to be deliberately coarse: one high-level orchestrator that does the work server-side beats ten fine-grained tools that each require a round-trip.
The UX friction of confirmation prompts is the third issue. Every mutating operation requires a user click. If your MCP server exposes granular writes, users will be clicking through prompts on every sub-action, and they'll stop reading them. That's security theater, not security. The design answer is to consolidate write operations into fewer, higher-level tools that match user intent, so each confirmation is meaningful — and to validate inputs server-side regardless of what the model passes, since the model will eventually be wrong.
None of this is a reason to avoid the integration. It's a reason to design for it. Treat the server as stateless, keep tool count low, persist any cross-call state, validate OAuth claims strictly, and structure tools around user intent rather than API granularity. The standardization of OAuth 2.1 with PKCE and dynamic discovery is the best foundation agent tooling has had, and it makes this a solid base for production use — if you respect the constraints it imposes.
Resources
Updated 2026-09-07 by Mehran Mozaffari.
Related posts
10 September 2026
Unbundling the Hype: How Prompt-to-3D, MCP, and Collaborative Generative Workflows Actually Fit Together
8 September 2026
diagram-design: What Actually Happens When Your Agent Draws Instead of Compiles
5 September 2026
Shot Composer Deep Dive: Browser-Based 3D Blocking with an MCP Spine
5 September 2026
Ripwire: A Deterministic Call-Graph Primer for Coding Agents
4 September 2026
Marrying a CEO agent to a craft pipeline
3 September 2026
FFmpeg Skill: The Deterministic Control Plane for Media-Specific AI Agents
