\"\"\", statu","inLanguage":"en-US","datePublished":"2026-03-11T11:15:51Z","dateModified":"2026-03-11T11:15:51Z","mainEntityOfPage":{"@type":"WebPage","@id":"https://www.stepcodex.com/en/issue/bug-support-cursor-mcp-callback-redirect"},"author":{"@type":"Person","name":"janario","url":"https://github.com/janario","image":"https://github.com/janario"},"publisher":{"@type":"Organization","name":"StepCodex","url":"https://www.stepcodex.com"},"articleSection":"litellm","about":[{"@type":"Thing","name":"litellm","url":"https://www.stepcodex.com/en/category/litellm"}],"keywords":"[Bug]: Support cursor:// MCP callback redirect schema, litellm, how to fix, fix, troubleshooting, root cause, solution, StepCodex","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/LikeAction","userInteractionCount":4}},{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://www.stepcodex.com/en/issue"},{"@type":"ListItem","position":2,"name":"litellm","item":"https://www.stepcodex.com/en/category/litellm"},{"@type":"ListItem","position":3,"name":"[Bug]: Support cursor:// MCP callback redirect schema","item":"https://www.stepcodex.com/en/issue/bug-support-cursor-mcp-callback-redirect"}]}]

litellm - ✅(Solved) Fix [Bug]: Support cursor:// MCP callback redirect schema [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
BerriAI/litellm#23339Fetched 2026-04-08 00:37:24
View on GitHub
Comments
0
Participants
1
Timeline
3
Reactions
0
Author
Participants
Timeline (top)
labeled ×2cross-referenced ×1

Fix Action

Fix / Workaround

Workaround

The workaround we have done internally is to have an extra container to intercept the /callback and when litellm sends 302 to cursor we actually render a 200 and make js manage the redirect.

I wonder if this could be done by litellm and reduce our internal workaround/fix.

PR fix notes

PR #23355: fix(mcp): support cursor:// and custom URI scheme redirects in OAuth #23339

Description (problem / solution / changelog)

Problem

When using Cursor IDE with an MCP server (e.g. GitLab) via LiteLLM, the OAuth callback flow breaks with:

Sending form data to '...' violates the following Content Security Policy
directive: "form-action 'self' https: http:"

This happens because:

  1. LiteLLM registers its own https:// URL as the redirect_uri with the upstream provider
  2. The upstream provider (e.g. GitLab) builds its CSP form-action header based on that URL, only trusting https: and http:
  3. LiteLLM then issues a 302 → cursor://... to hand control back to Cursor
  4. Chrome enforces the upstream CSP on that redirect and blocks it

Fix

In the /callback endpoint, detect when the final redirect target uses a non-http(s) scheme (e.g. cursor://, vscode://). Instead of returning a 302, return an HTTP 200 HTML page that uses window.location.replace() to perform the redirect via JavaScript.

JS navigation is not subject to form-action CSP restrictions, so the browser follows the deep-link without issue — no sidecar container needed.

Security

  • Scheme is validated: only alphanumeric + hyphen characters allowed
  • javascript: and data: URIs are rejected with HTTP 400
  • Quote characters and backslashes are percent-encoded before embedding in HTML/JS

Testing

Tested manually with Cursor + GitLab MCP via LiteLLM proxy.

Fixes #23339

Changed files

  • litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py (modified, +20/-0)
  • ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx (modified, +2/-1)

Code Example

form-action 'self' https: http: cursor:

---

form-action 'self' https: http:

---

Sending form data to 'https://gitlab.com/oauth/authorize' violates the following Content Security Policy directive: "form-action 'self' https: http: https:". The request has been blocked.

---

from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse, JSONResponse
import httpx
import uvicorn

app = FastAPI()
# This is the internal address of your LiteLLM container
LITELLM_URL = "http://localhost:4000"

@app.get("/")
async def health_check():
    return JSONResponse(content={"status": "healthy", "proxy_to": LITELLM_URL}, status_code=200)

@app.get("/callback")
async def proxy_callback(code: str, state: str, request: Request):
    async with httpx.AsyncClient() as client:
        # 1. Forward the exact request to LiteLLM
        # We use follow_redirects=False so WE catch the 302, not the client
        resp = await client.get(
            f"{LITELLM_URL}/callback",
            params={"code": code, "state": state},
            headers={"User-Agent": request.headers.get("user-agent", "")},
            follow_redirects=False
        )

    location = resp.headers.get("location", "")

    # 2. The Logic: If it's Cursor, break the CSP chain
    if resp.status_code == 302 and location.startswith("cursor://"):
        return HTMLResponse(
            content=f"""
            <html>
                <body style="background: #1e1e1e; color: white; font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh;">
                    <div style="text-align: center;">
                        <p>Success! Redirecting to Cursor...</p>
                        <script>window.location.replace("{location}");</script>
                        <noscript><a href="{location}" style="color: #3794ff;">Click here if not redirected</a></noscript>
                    </div>
                </body>
            </html>
            """,
            status_code=200
        )

    # 3. Else: Return the original response as-is
    return Response(
        content=resp.content,
        status_code=resp.status_code,
        headers=dict(resp.headers)
    )

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8080)

---
RAW_BUFFERClick to expand / collapse

Check for existing issues

  • I have searched the existing issues and checked that my issue is not a duplicate.

What happened?

Problem

When using MCP direct with cursor and in the example GitLab MCP.

It goes in the register, authorize, callback flow.

Thing is that cursor sends a redirect_uri as cursor:// to open the client and when GitLab receives redirect_uri it builds the CSP header based on the redirect_uri.

With pure cursor

form-action 'self' https: http: cursor:

With LiteLLM as the redirect_uri is the https gateway.

form-action 'self' https: http:

The problem is that from gitlab it gives a 302 to the litellm, and from it another 302 to cursor://. As the initial GitLab didn't trust on cursor schema chrome blocks it and the auth flow is not completed.

Sending form data to 'https://gitlab.com/oauth/authorize' violates the following Content Security Policy directive: "form-action 'self' https: http: https:". The request has been blocked.

Workaround

The workaround we have done internally is to have an extra container to intercept the /callback and when litellm sends 302 to cursor we actually render a 200 and make js manage the redirect.

I wonder if this could be done by litellm and reduce our internal workaround/fix.

Here our callback app:

from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse, JSONResponse
import httpx
import uvicorn

app = FastAPI()
# This is the internal address of your LiteLLM container
LITELLM_URL = "http://localhost:4000"

@app.get("/")
async def health_check():
    return JSONResponse(content={"status": "healthy", "proxy_to": LITELLM_URL}, status_code=200)

@app.get("/callback")
async def proxy_callback(code: str, state: str, request: Request):
    async with httpx.AsyncClient() as client:
        # 1. Forward the exact request to LiteLLM
        # We use follow_redirects=False so WE catch the 302, not the client
        resp = await client.get(
            f"{LITELLM_URL}/callback",
            params={"code": code, "state": state},
            headers={"User-Agent": request.headers.get("user-agent", "")},
            follow_redirects=False
        )

    location = resp.headers.get("location", "")

    # 2. The Logic: If it's Cursor, break the CSP chain
    if resp.status_code == 302 and location.startswith("cursor://"):
        return HTMLResponse(
            content=f"""
            <html>
                <body style="background: #1e1e1e; color: white; font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh;">
                    <div style="text-align: center;">
                        <p>Success! Redirecting to Cursor...</p>
                        <script>window.location.replace("{location}");</script>
                        <noscript><a href="{location}" style="color: #3794ff;">Click here if not redirected</a></noscript>
                    </div>
                </body>
            </html>
            """,
            status_code=200
        )

    # 3. Else: Return the original response as-is
    return Response(
        content=resp.content,
        status_code=resp.status_code,
        headers=dict(resp.headers)
    )

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8080)

Steps to Reproduce

  1. Use GitLab MCP via LiteLLM with Cursor IDE

Relevant log output

What part of LiteLLM is this about?

Proxy

What LiteLLM version are you on ?

v1.81.16

Twitter / LinkedIn details

No response

extent analysis

Fix Plan

To resolve the Content Security Policy (CSP) issue, we need to modify the LiteLLM proxy to handle the redirect to cursor:// schema. We can achieve this by implementing a similar logic to the provided callback app.

Step-by-Step Solution

  • Modify the LiteLLM proxy to catch the 302 redirect response from the GitLab authorization server.
  • Check if the redirect location starts with cursor://. If it does, return an HTML response with a JavaScript redirect to the cursor:// location.
  • If the redirect location does not start with cursor://, return the original response as-is.

Example Code

from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse, JSONResponse
import httpx

app = FastAPI()

# This is the internal address of your GitLab authorization server
GITLAB_URL = "https://gitlab.com/oauth/authorize"

@app.get("/callback")
async def proxy_callback(code: str, state: str, request: Request):
    async with httpx.AsyncClient() as client:
        # 1. Forward the exact request to GitLab
        # We use follow_redirects=False so WE catch the 302, not the client
        resp = await client.get(
            f"{GITLAB_URL}",
            params={"code": code, "state": state},
            headers={"User-Agent": request.headers.get("user-agent", "")},
            follow_redirects=False
        )

    location = resp.headers.get("location", "")

    # 2. The Logic: If it's Cursor, break the CSP chain
    if resp.status_code == 302 and location.startswith("cursor://"):
        return HTMLResponse(
            content=f"""
            <html>
                <body style="background: #1e1e1e; color: white; font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh;">
                    <div style="text-align: center;">
                        <p>Success! Redirecting to Cursor...</p>
                        <script>window.location.replace("{location}");</script>
                        <noscript><a href="{location}" style="color: #3794ff;">Click here if not redirected</a></noscript>
                    </div>
                </body>
            </html>
            """,
            status_code=200
        )

    # 3. Else: Return the original response as-is
    return Response(
        content=resp.content,
        status_code=resp.status_code,
        headers=dict(resp.headers)
    )

Verification

To verify that the fix worked, test the authorization flow with GitLab MCP via LiteLLM with Cursor IDE. The redirect to cursor:// should now be successful, and the authorization flow should complete without any CSP errors.

Extra Tips

  • Make sure to update the

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

litellm - ✅(Solved) Fix [Bug]: Support cursor:// MCP callback redirect schema [1 pull requests, 1 participants]