fix(asgi): redirect /mcp → /mcp/ for clients that omit the trailing slash
Starlette's Mount("/mcp", ...) only matches /mcp/* paths. A POST to bare
/mcp falls through to the catch-all Django mount and returns 404. The
fast-agent MCP client and the README example both used the no-slash URL,
so the validator was never able to initialize a session — every call
landed in django.request.
Adds a 307 redirect at /mcp so any client URL works, and points the
validator config at /mcp/ directly to skip the redirect round-trip.
Also gitignores fastagent.jsonl (a runtime log file fast-agent writes
into the working directory).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,7 @@ django.setup()
|
|||||||
|
|
||||||
from django.core.asgi import get_asgi_application # noqa: E402
|
from django.core.asgi import get_asgi_application # noqa: E402
|
||||||
from starlette.applications import Starlette # noqa: E402
|
from starlette.applications import Starlette # noqa: E402
|
||||||
from starlette.responses import JSONResponse # noqa: E402
|
from starlette.responses import JSONResponse, RedirectResponse # noqa: E402
|
||||||
from starlette.routing import Mount, Route # noqa: E402
|
from starlette.routing import Mount, Route # noqa: E402
|
||||||
|
|
||||||
from mcp_server.server import mcp # noqa: E402
|
from mcp_server.server import mcp # noqa: E402
|
||||||
@@ -35,6 +35,13 @@ async def health(request):
|
|||||||
return JSONResponse({"status": "ok"})
|
return JSONResponse({"status": "ok"})
|
||||||
|
|
||||||
|
|
||||||
|
async def mcp_redirect(request):
|
||||||
|
"""Bare /mcp → /mcp/ — MCP clients that omit the trailing slash hit
|
||||||
|
Starlette's Mount which only matches /mcp/* paths, falling through to
|
||||||
|
Django and returning 404. A 307 preserves the POST body."""
|
||||||
|
return RedirectResponse(url="/mcp/", status_code=307)
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app):
|
async def lifespan(app):
|
||||||
async with mcp_http_app.lifespan(app), mcp_sse_app.lifespan(app):
|
async with mcp_http_app.lifespan(app), mcp_sse_app.lifespan(app):
|
||||||
@@ -44,6 +51,7 @@ async def lifespan(app):
|
|||||||
app = Starlette(
|
app = Starlette(
|
||||||
routes=[
|
routes=[
|
||||||
Route("/mcp/health", health),
|
Route("/mcp/health", health),
|
||||||
|
Route("/mcp", mcp_redirect, methods=["GET", "POST"]),
|
||||||
Mount("/mcp/sse", app=mcp_sse_app),
|
Mount("/mcp/sse", app=mcp_sse_app),
|
||||||
Mount("/mcp", app=mcp_http_app),
|
Mount("/mcp", app=mcp_http_app),
|
||||||
Mount("/", app=application),
|
Mount("/", app=application),
|
||||||
|
|||||||
1
validator/.gitignore
vendored
1
validator/.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
fastagent.secrets.yaml
|
fastagent.secrets.yaml
|
||||||
|
fastagent.jsonl
|
||||||
.venv/
|
.venv/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ cd mnemosyne/
|
|||||||
uvicorn mnemosyne.asgi:app --host 0.0.0.0 --port 22091 --workers 1
|
uvicorn mnemosyne.asgi:app --host 0.0.0.0 --port 22091 --workers 1
|
||||||
```
|
```
|
||||||
|
|
||||||
By default the validator points at `http://localhost:22091/mcp`. If your Mnemosyne is on another host, override `mcp.servers.mnemosyne.url` in `fastagent.secrets.yaml`.
|
By default the validator points at `http://localhost:22091/mcp/` (note the trailing slash — Starlette's `Mount` only matches paths under the mount prefix, so `/mcp` without the slash falls through to Django). If your Mnemosyne is on another host, override `mcp.servers.mnemosyne.url` in `fastagent.secrets.yaml`.
|
||||||
|
|
||||||
## Run the validator
|
## Run the validator
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,6 @@ mcp:
|
|||||||
# override the URL in fastagent.secrets.yaml or via Ansible if remote.
|
# override the URL in fastagent.secrets.yaml or via Ansible if remote.
|
||||||
mnemosyne:
|
mnemosyne:
|
||||||
transport: http
|
transport: http
|
||||||
url: "http://localhost:22091/mcp"
|
url: "http://localhost:22091/mcp/"
|
||||||
# Bearer token in fastagent.secrets.yaml (provisioned via
|
# Bearer token in fastagent.secrets.yaml (provisioned via
|
||||||
# `python manage.py create_mcp_token <user>`).
|
# `python manage.py create_mcp_token <user>`).
|
||||||
|
|||||||
Reference in New Issue
Block a user