Repository navigation
Support Perplexity custom connectors (MCP Streamable HTTP) — POST /api/mcp rejects valid API keys with 401 / clients see HTTP 419 #288
Description
What happened?
Attempting to add Pascal as a custom MCP connector in Perplexity fails with:
[API_CLIENTS_ERROR] MCP server returned HTTP 419
Investigation shows Pascal's /api/mcp endpoint accepts the API key on GET (returns the service descriptor with HTTP 200) but rejects the same key on the POST initialize handshake that Perplexity actually performs, returning:
HTTP 401 — {"error":"unauthorized","message":"Invalid or revoked API key."}
The 419 surfaced in Perplexity appears to be Pascal's middleware (Laravel-style "Page Expired" / CSRF) mapping the unauthenticated POST. Net effect: Perplexity cannot connect to Pascal at all, while Codex / Claude Code / Cursor / OpenClaw work fine with the same account.
Steps to reproduce
- In Pascal → Settings → API keys, create a new key (e.g. "Perplexity") and copy it via the Copy key button.
- In Perplexity → Settings → Connectors → Add custom connector, enter:
- MCP server URL:
https://editor.pascal.app/api/mcp - Authentication: API Key
- Key: the freshly copied
sk_live_… - Transport: Streamable HTTP
- MCP server URL:
- Click Add.
→ Perplexity returns:[API_CLIENTS_ERROR] MCP server returned HTTP 419.
Reproduce server-side with curl using the same key:
GET works:
curl -i -H "Authorization: Bearer sk_live_…" https://editor.pascal.app/api/mcp
# → HTTP/2 200, service descriptor JSON, version 0.6.1POST initialize fails:
curl -i -X POST https://editor.pascal.app/api/mcp \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Protocol-Version: 2025-06-18" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"0.0.1"}}}'
# → HTTP/2 401
# → {"error":"unauthorized","message":"Invalid or revoked API key."}Expected behavior
POST /api/mcpwith a validAuthorization: Bearer sk_live_…header should accept the MCPinitializeJSON-RPC call and respond with HTTP 200 plus anMcp-Session-Idheader.- The same key that authenticates a
GET /api/mcprequest must authenticate aPOST /api/mcprequest. - Perplexity custom connectors should connect successfully, comparably to Codex / Claude Code / Cursor / OpenClaw.
- No 419 / CSRF-style rejection on stateless Bearer-authenticated MCP requests.
Browser & OS
- OS: macOS (Apple Silicon) - Browser: Perplexity Comet (latest) — also reproducible in Perplexity web on Chrome/Safari - Pascal MCP server version: 0.6.1 (from
GET /api/mcpservice descriptor) - Date observed: 2026-05-02
Screenshots or screen recordings
- Pascal Settings → API keys panel — shows a freshly created "Perplexity" key (
sk_live_…) created May 2, 2026, withCopy key/Copy setupbuttons. Key was copied via this button (not selected manually) to ensure full length.
- Perplexity → Add custom connector dialog — fields filled exactly as documented:
- MCP server URL:
https://editor.pascal.app/api/mcp - Authentication: API Key + the
sk_live_…value - Transport: Streamable HTTP
- Risk acknowledgement checkbox ticked
- Footer shows the failure:
[API_CLIENTS_ERROR] MCP server returned HTTP 419
- MCP server URL:
Additional context
- Pascal's own service descriptor advertises
transport: "streamable-http"andauth.methods: ["api-key"]withapiKeyHeader: "Authorization: Bearer sk_live_... or sk_test_..."— exactly what Perplexity sends, so the Perplexity-side configuration matches Pascal's documented expectations. - CORS allow-list on the endpoint includes
X-Agent-Client,Mcp-Protocol-Version,Mcp-Session-Id— suggesting session-based MCP. If Perplexity must sendX-Agent-Clientor some other header to be accepted, please document. - The descriptor notes: "For browser-visible co-editing, use an API key created in Settings by the same logged-in user… Agent self-registration creates a separate owner account." If Perplexity must use the
/api/auth/agent/registerflow instead of Settings keys, please clarify in docs. - HTTP 419 is non-standard; widely used by Laravel for "Page Expired" / CSRF token mismatch. Suggests the POST handler may be sitting behind web-session middleware that doesn't belong on a stateless MCP API.
- Tested clients that DO work with the same Pascal account: Codex CLI, Claude Code, Cursor. Only Perplexity fails — but the curl POST repro shows the failure is server-side, not Perplexity-specific.
- Happy to run additional curl variants (different protocol versions, with/without
X-Agent-Client, agent-registered keys, SSE transport) if it helps narrow it down. - Perplexity custom-connector support would be highly valuable for users combining research workflows with 3D scene authoring — a natural fit for Pascal.
Activity
link-webcreations commentedon Jul 6, 2026
Hello,
I have the same error, but using VS-Code / Github Copilot:
With this configuration:
{ "inputs": [ { "id": "pascal_api_key", "type": "promptString", "description": "Pascal Editor MCP API Key", "password": true } ], "servers": { "pascal": { "url": "https://editor.pascal.app/api/mcp", "headers": { "Authorization": "Bearer ${input:pascal_api_key}" } } } }Aymericr commentedon Jul 19, 2026
Current triage: the May report and the July comment expose two distinct hosted-endpoint failures. The original initialize request returns 401 before a session exists; the later client discovers tools and then gets the explicit 419 session-expired response, which means initialization succeeded but a subsequent request reached an instance that did not own the in-memory session. Neither is implemented in the public @pascal-app/mcp package itself; both are hosted editor.pascal.app transport/auth behavior. Keeping this open, but please include a fresh initialize curl response and response headers when retesting so the auth failure and cross-instance session-affinity failure are not conflated.
Aymericr commentedon Aug 4, 2026
I traced both reports through the hosted endpoint. They are two different failures, and neither lives in the open-source
@pascal-app/mcppackage — both are in the hosted/api/mcproute on editor.pascal.app. Writing up the mechanisms so the fix is reviewable, and so anyone hitting this has a workaround today.@link-webcreations' 419 (
Discovered 46 tools, thenError 419) — statelessness, not expiry.The route keeps sessions in a module-level
Map(const sessions = new Map<string, McpSession>()), created oninitializeviaonsessioninitializedand looked up by themcp-session-idheader on every subsequent request. On a serverless platform each request can land on a different instance, and that Map isn't shared between them. So the sequence in your log is exactly:initializelands on instance A and registers the session →tools/listalso hits A (hence "Discovered 46 tools") → the next request routes to instance B, wheresessions.get(sessionId)misses,sessionIdis truthy, and the handler returns our explicit419with the "session expired or moved to another server instance" message. TheSESSION_TTL_MSof 2h is a red herring; this fires seconds after a successful handshake. It's also why Claude Code/Cursor/Codex appear to "work fine" — they do, until a request happens to land elsewhere, so it presents as intermittent rather than broken.The real fix is to stop holding transport state in process memory: rehydrate the session from durable storage per request (the API key already identifies the user, and
get_project_statusalready re-binds the scene). That's a hosted-side change I'm scoping now; the 419 message itself is honest and the client-side recovery it names does work.The original 401 on
POST initialize— the GET/POST asymmetry is a false signal.Worth correcting the premise, because it points somewhere misleading: the
GET /api/mcpthat returns the 200 descriptor never validates your key at all. When the request isn't an MCP stream request, the handler returns the static discovery JSON before any auth runs. So "the key works on GET and fails on POST" isn't a POST-specific auth bug — GET simply never checked. Both methods go through the sameresolveAuthonce you're on the MCP path.The 401 body you saw (
{"error":"unauthorized","message":"Invalid or revoked API key."}) is emitted from exactly one branch: the token started withsk_live_/sk_test_andverifyApiKeyreturned null. That happens when the SHA-hash lookup misses, or the record isrevoked, orexpiresAthas passed, or — the one I'd bet on for a connector — the key is org-scoped and the owner's membership is gone, which returns null even though the key itself is valid and unexpired. If the token doesn't match either prefix you'd get the other 401 message ("Provide a Bearer API key…"), so we can rule out truncation or a mangledAuthorizationheader from your report.Two things worth checking, @fieldmanneasi:
Bearerparsing is case-insensitive and takes everything after the first whitespace run, so a trailing newline from a copy-paste would be included in the hash and cause exactly this 401. Re-creating the key and pasting without a trailing newline is the cheapest thing to try.Also relevant to Perplexity specifically: there's an
Origin-based guard that 403s any request whoseOriginheader isn't same-origin, loopback, or inPASCAL_SCENE_API_ORIGINS, and a 120 req/min per-IP rate limit that returns 429. Neither produces a 401, so they're not your failure — but a server-to-server connector calling from shared infrastructure could plausibly trip the rate limit, and I'm adding both to the docs since today they're invisible to clients.Status. Keeping this open and treating the stateless-session fix as the primary work, since it's the one that makes every Streamable HTTP client reliable rather than intermittently working. I'll report back here when it ships. Thanks both for the precise logs — the "discovered 46 tools then 419" line is what made the instance-affinity cause unambiguous.