claude.ai's connector dialog takes a name and a URL. Sending a bearer token needs a "Request headers" beta most accounts lack, and OAuth is not built yet, so with MCP_PATH_SECRET set the endpoint is also served at /<secret>/mcp without the bearer token — a trial until OAuth replaces it. The path is the credential there. It is compared in constant time, and a wrong one answers 404 like any unknown path. The config refuses fewer than 32 URL-safe characters and never echoes the value, nothing in the server logs request paths, and the Caddy snippet rewrites the segment before an access log entry is written (verified against Caddy 2.11). Claude Code and the CLI keep the bearer token; DEPLOYMENT.md says what the path trades away. 178 tests. Smoke 76/76 and 74/74 on the local instance. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
51 lines
1.7 KiB
Caddyfile
51 lines
1.7 KiB
Caddyfile
# Add this to the Pi's existing Caddyfile.
|
|
#
|
|
# Caddy obtains and renews the certificate automatically, provided the VPS
|
|
# forwards ports 80 and 443 through to this Caddy and the DNS name resolves to
|
|
# the VPS's public address.
|
|
#
|
|
# The bearer-token check lives in the application, not here: Caddy would have
|
|
# to be reloaded to rotate the token, whereas the app reads it from the
|
|
# environment. Caddy's job is TLS, timeouts and keeping the container off the
|
|
# public interface.
|
|
|
|
mcp.example.org {
|
|
encode zstd gzip
|
|
|
|
# `schulcloud-mcp` is the Compose service name; Docker's embedded DNS
|
|
# resolves it on the shared network. No host port is published. One proxy
|
|
# serves both surfaces: /mcp for Claude and /api for the CLI.
|
|
reverse_proxy schulcloud-mcp:8080 {
|
|
# MCP's Streamable HTTP transport keeps a server-sent-events channel
|
|
# open for server-initiated messages. Without flush_interval -1 Caddy
|
|
# buffers those, and the connector appears to hang.
|
|
flush_interval -1
|
|
|
|
# Long enough for a full re-crawl (~270 upstream requests) and for the
|
|
# CLI streaming large files out of the mirror.
|
|
transport http {
|
|
read_timeout 600s
|
|
write_timeout 600s
|
|
}
|
|
}
|
|
|
|
header {
|
|
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
|
X-Content-Type-Options "nosniff"
|
|
Referrer-Policy "no-referrer"
|
|
-Server
|
|
}
|
|
|
|
log {
|
|
output file /var/log/caddy/schulcloud-mcp.log
|
|
# With MCP_PATH_SECRET set, a request path *is* a credential: claude.ai
|
|
# reaches the server at /<secret>/mcp. The filter rewrites that segment
|
|
# before the entry is written. The Authorization header must never reach
|
|
# disk either; Caddy does not log headers by default — do not add them.
|
|
format filter {
|
|
request>uri regexp ^/[A-Za-z0-9_-]{32,}/mcp /<secret>/mcp
|
|
wrap json
|
|
}
|
|
}
|
|
}
|