Skip to content

lighttpd: bare /mcp (no trailing slash) returns 404 — breaks MCP clients that strip the trailing slash #618

@noahsprent

Description

@noahsprent

Summary

The lighttpd rewrite rule for the MCP endpoint only matches /mcp/... (with a trailing slash), so a request to the bare path /mcp returns 404 instead of reaching the FastCGI MCP app. This breaks remote MCP clients that normalize away the trailing slash (e.g. Claude's web connector), since they connect to https://<host>/mcp.

Location

packaging/linux-leader/files/lighttpd/50-pioreactorui.conf

url.rewrite-once = (
  "^(/static($|/.*))$"   => "$1",
  "^(/api/.*)$"          => "/api.fcgi$1",
  "^(/unit_api/.*)$"     => "/api.fcgi$1",
  "^(/mcp/.*)$"          => "/api.fcgi$1",   # <-- requires trailing slash
)

Why bare /mcp 404s

A request to /mcp (no trailing slash):

  1. does not match "^(/mcp/.*)$", so it is never rewritten to /api.fcgi, and
  2. is excluded from the SPA fallback by the negative match block:
    $HTTP["url"] !~ "^(/api($|/)|/api\.fcgi($|/)|/unit_api($|/)|/mcp($|/)|/static($|/)|/exports($|/))" { ... }
    
    (/mcp matches /mcp($|/), so the SPA rewrite is skipped too)

With no rewrite from either branch, lighttpd tries to serve /mcp as a static file and returns 404.

Reproduce

On a leader exposing the UI:

curl -i -X POST http://127.0.0.1/mcp  -H 'Content-Type: application/json' -d '{}'   # -> 404
curl -i -X POST http://127.0.0.1/mcp/ -H 'Content-Type: application/json' -d '{}'   # -> reaches MCP app

The MCP backend itself handles both paths fine — the discrepancy is purely in this rewrite rule. The MCP spec doesn't require clients to preserve a trailing slash on the server URL, and several do strip it, so the bare-path case needs to work.

Suggested fix

Widen the pattern to also match the empty (bare) case:

-  "^(/mcp/.*)$"          => "/api.fcgi$1",
+  "^(/mcp($|/.*))$"      => "/api.fcgi$1",

This is low-risk: it only adds the bare /mcp path; existing /mcp/... behaviour is unchanged. I've been running this patch on a leader (pioreactor 26.5.2) and /mcp now reaches the MCP app — verified end-to-end through a Cloudflare Tunnel + Cloudflare Access, where the bare path now returns the expected MCP OAuth 401 challenge instead of a 404.

(Note: /api and /unit_api carry the same trailing-slash-only pattern, but no client connects to those bare roots, so this report is scoped to /mcp.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions