Serve MCP over HTTPS for the Claude Desktop connector
Claude Desktop's custom connector only accepts https URLs. Added an optional TLS listener (HTTPS_ADDR + TLS_CERT_FILE/TLS_KEY_FILE) alongside HTTP; docker-compose publishes 127.0.0.1:8443 and mounts a local mkcert cert from certs/ (git-ignored). Best-effort: a missing cert logs a warning and stays HTTP-only. Verified the Windows store trusts the mkcert cert and MCP initialize succeeds over https://127.0.0.1:8443/mcp. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,20 @@
|
||||
# authentication (AGENT.md §0). Only bind to a non-local interface deliberately.
|
||||
LISTEN_ADDR=127.0.0.1:8080
|
||||
|
||||
# HTTPS (optional, but REQUIRED for the MCP connector — Claude Desktop only
|
||||
# accepts https:// URLs). When HTTPS_ADDR and both cert/key are set, an HTTPS
|
||||
# listener starts alongside HTTP. docker-compose sets these to the mounted certs.
|
||||
# Generate a locally-trusted cert on the HOST with mkcert (installs a local CA
|
||||
# your OS — and Claude Desktop — will trust), from the project root:
|
||||
# winget install FiloSottile.mkcert
|
||||
# mkcert -install # trust step (adds the local CA)
|
||||
# mkdir certs
|
||||
# mkcert -cert-file certs/localhost.pem -key-file certs/localhost-key.pem localhost 127.0.0.1 ::1
|
||||
# Then connect Claude Desktop to https://localhost:8443/mcp
|
||||
HTTPS_ADDR=
|
||||
TLS_CERT_FILE=
|
||||
TLS_KEY_FILE=
|
||||
|
||||
# Roots to scan for Git repositories, comma-separated (absolute paths).
|
||||
# Inside Docker these must be the *container* paths that the host roots are
|
||||
# mounted to (see docker-compose.yml). Example: /repos,/work/other
|
||||
|
||||
@@ -10,6 +10,9 @@ gitmanager.exe
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Local TLS certificates (generated with mkcert; never commit)
|
||||
/certs/
|
||||
|
||||
# Editor / OS
|
||||
.DS_Store
|
||||
.idea/
|
||||
|
||||
@@ -345,6 +345,13 @@ obeys the safety rules (§1.4).
|
||||
`github.com/modelcontextprotocol/go-sdk`. Claude Desktop connects to it as a
|
||||
**custom connector**. Like the rest of the app it is **localhost‑bound and
|
||||
unauthenticated** (Section 0) — do not expose it off‑host without asking.
|
||||
- **The connector requires `https://`** (Claude Desktop rejects plain `http`).
|
||||
So `/mcp` is also served over **TLS on localhost** with a **locally‑trusted
|
||||
cert** (mkcert‑generated; the app just reads the cert/key files). Installing the
|
||||
local CA into the OS trust store is a **host setup step the user performs**, not
|
||||
something the app or Claude does (it is a security‑settings change). Do **not**
|
||||
reach for a public tunnel to get HTTPS — that would expose an unauthenticated
|
||||
repo‑management app to the internet.
|
||||
- MCP **tools are thin adapters** over the service layer — no Git/forge logic in
|
||||
the tool handlers (§1.7). Expected tools (grow as features land):
|
||||
- Read: `list_repos`, `get_repo`, `get_active_project`, `get_activity`,
|
||||
@@ -507,9 +514,11 @@ silently guess.)*
|
||||
- ✅ **RESOLVED 2026-09-19:** **Project handoff is cooperative** — user requests a
|
||||
switch, Claude finishes to a safe checkpoint, switches, and the user is notified;
|
||||
pull‑first, not autonomous (Section 8.3).
|
||||
- **MCP connector setup:** confirm this Claude Desktop build supports adding a
|
||||
local Streamable‑HTTP custom connector (`http://127.0.0.1:8080/mcp`); if not,
|
||||
fall back to a stdio shim.
|
||||
- ✅ **RESOLVED 2026-09-20:** Claude Desktop's custom connector **requires
|
||||
`https://`**. Resolved with **local TLS via mkcert** — connect to
|
||||
`https://localhost:8443/mcp` (HTTP dashboard stays on `:8080`). The user runs
|
||||
`mkcert -install` + cert generation on the host (§8.1); the stdio‑shim fallback
|
||||
is no longer needed.
|
||||
- **Idle‑trigger for handoff:** the pull model syncs at Claude's turn boundaries.
|
||||
If Claude is idle when the user switches, decide the nudge (user's next message,
|
||||
a heartbeat/poll, or a host push if available) — do not assume instant wake.
|
||||
|
||||
@@ -69,3 +69,18 @@ Append-only running history of all changes (AGENT.md §9.1). Newest last.
|
||||
- **Gotcha:** Docker-on-Windows bind mounts do NOT deliver filesystem events, so
|
||||
air's watch-based reload silently never fired. Fixed by enabling air polling
|
||||
(`poll = true`, `poll_interval = 500` in `.air.toml`).
|
||||
|
||||
## 2026-09-20 — HTTPS for the MCP connector (local TLS via mkcert)
|
||||
- **What:** Added an optional HTTPS listener alongside HTTP. New config
|
||||
`HTTPS_ADDR`, `TLS_CERT_FILE`, `TLS_KEY_FILE`; when set, `cmd/server` starts
|
||||
`e.StartTLS` on the same Echo app (best-effort — a missing cert logs a warning
|
||||
and stays HTTP-only). docker-compose publishes `127.0.0.1:8443` and points the
|
||||
TLS vars at `certs/localhost.pem` (mounted via the existing source mount).
|
||||
`.gitignore` ignores `/certs/`; `.env.example` documents the mkcert steps.
|
||||
- **Why:** Claude Desktop's custom MCP connector only accepts `https://` URLs.
|
||||
Local TLS with an mkcert-trusted cert lets `https://localhost:8443/mcp` work
|
||||
without exposing the unauthenticated app via a public tunnel (AGENT.md §8.1).
|
||||
- **Affects:** `internal/config`, `cmd/server/main.go`, `docker-compose.yml`,
|
||||
`.gitignore`, `.env.example`, `AGENT.md` (§8.1, §11).
|
||||
- **Host setup (user-run):** the local CA install (`mkcert -install`) is a
|
||||
security-settings change performed by the user, not the app.
|
||||
|
||||
@@ -110,6 +110,22 @@ func main() {
|
||||
}()
|
||||
log.Info("listening", "addr", cfg.ListenAddr)
|
||||
|
||||
// Optional HTTPS listener (same Echo app). Required for the MCP connector,
|
||||
// which only accepts https:// URLs (§8.1). Best-effort: a missing/unreadable
|
||||
// cert logs a warning and leaves the app running over HTTP.
|
||||
if cfg.HTTPSAddr != "" && cfg.TLSCertFile != "" && cfg.TLSKeyFile != "" {
|
||||
if _, err := os.Stat(cfg.TLSCertFile); err != nil {
|
||||
log.Warn("HTTPS requested but cert not readable — serving HTTP only", "cert", cfg.TLSCertFile, "err", err)
|
||||
} else {
|
||||
go func() {
|
||||
if err := e.StartTLS(cfg.HTTPSAddr, cfg.TLSCertFile, cfg.TLSKeyFile); err != nil && err != http.ErrServerClosed {
|
||||
log.Error("TLS server error", "err", err)
|
||||
}
|
||||
}()
|
||||
log.Info("listening (https)", "addr", cfg.HTTPSAddr)
|
||||
}
|
||||
}
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
@@ -12,10 +12,17 @@ services:
|
||||
# Bind all interfaces INSIDE the container so the published port reaches
|
||||
# it; the `ports` mapping below still keeps it localhost-only on the HOST.
|
||||
- LISTEN_ADDR=0.0.0.0:8080
|
||||
# HTTPS for the MCP connector (Claude Desktop only accepts https URLs).
|
||||
# Certs are generated on the host with mkcert (see README/.env.example)
|
||||
# and mounted read-only below.
|
||||
- HTTPS_ADDR=0.0.0.0:8443
|
||||
- TLS_CERT_FILE=/app/certs/localhost.pem
|
||||
- TLS_KEY_FILE=/app/certs/localhost-key.pem
|
||||
# The scanner looks here; matches the volume mount below.
|
||||
- GIT_REPO_ROOTS=/repos
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
- "127.0.0.1:8443:8443"
|
||||
volumes:
|
||||
# Source, for hot reload.
|
||||
- .:/app
|
||||
|
||||
@@ -14,7 +14,14 @@ import (
|
||||
|
||||
// Config is the typed application configuration.
|
||||
type Config struct {
|
||||
ListenAddr string // address the HTTP server binds to
|
||||
ListenAddr string // address the plain HTTP server binds to
|
||||
|
||||
// TLS: when HTTPSAddr and both cert/key files are set, an HTTPS listener is
|
||||
// started in addition to the HTTP one. Claude Desktop's MCP connector only
|
||||
// accepts https:// URLs, so /mcp must be reachable over TLS (AGENT.md §8.1).
|
||||
HTTPSAddr string // address the HTTPS server binds to ("" disables TLS)
|
||||
TLSCertFile string // PEM cert (e.g. an mkcert leaf trusted by the OS store)
|
||||
TLSKeyFile string // PEM private key
|
||||
|
||||
RepoRoots []string // roots to scan for git repositories
|
||||
GitBin string // path to the git binary
|
||||
@@ -38,6 +45,9 @@ func Load() (Config, error) {
|
||||
|
||||
c := Config{
|
||||
ListenAddr: env("LISTEN_ADDR", "127.0.0.1:8080"),
|
||||
HTTPSAddr: env("HTTPS_ADDR", ""),
|
||||
TLSCertFile: env("TLS_CERT_FILE", ""),
|
||||
TLSKeyFile: env("TLS_KEY_FILE", ""),
|
||||
RepoRoots: splitList(env("GIT_REPO_ROOTS", "")),
|
||||
GitBin: env("GIT_BIN", "git"),
|
||||
ScanMaxDepth: envInt("SCAN_MAX_DEPTH", 4),
|
||||
|
||||
Reference in New Issue
Block a user