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.
|
# authentication (AGENT.md §0). Only bind to a non-local interface deliberately.
|
||||||
LISTEN_ADDR=127.0.0.1:8080
|
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).
|
# Roots to scan for Git repositories, comma-separated (absolute paths).
|
||||||
# Inside Docker these must be the *container* paths that the host roots are
|
# Inside Docker these must be the *container* paths that the host roots are
|
||||||
# mounted to (see docker-compose.yml). Example: /repos,/work/other
|
# mounted to (see docker-compose.yml). Example: /repos,/work/other
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ gitmanager.exe
|
|||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# Local TLS certificates (generated with mkcert; never commit)
|
||||||
|
/certs/
|
||||||
|
|
||||||
# Editor / OS
|
# Editor / OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
@@ -345,6 +345,13 @@ obeys the safety rules (§1.4).
|
|||||||
`github.com/modelcontextprotocol/go-sdk`. Claude Desktop connects to it as a
|
`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
|
**custom connector**. Like the rest of the app it is **localhost‑bound and
|
||||||
unauthenticated** (Section 0) — do not expose it off‑host without asking.
|
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
|
- 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):
|
the tool handlers (§1.7). Expected tools (grow as features land):
|
||||||
- Read: `list_repos`, `get_repo`, `get_active_project`, `get_activity`,
|
- 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
|
- ✅ **RESOLVED 2026-09-19:** **Project handoff is cooperative** — user requests a
|
||||||
switch, Claude finishes to a safe checkpoint, switches, and the user is notified;
|
switch, Claude finishes to a safe checkpoint, switches, and the user is notified;
|
||||||
pull‑first, not autonomous (Section 8.3).
|
pull‑first, not autonomous (Section 8.3).
|
||||||
- **MCP connector setup:** confirm this Claude Desktop build supports adding a
|
- ✅ **RESOLVED 2026-09-20:** Claude Desktop's custom connector **requires
|
||||||
local Streamable‑HTTP custom connector (`http://127.0.0.1:8080/mcp`); if not,
|
`https://`**. Resolved with **local TLS via mkcert** — connect to
|
||||||
fall back to a stdio shim.
|
`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.
|
- **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,
|
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.
|
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
|
- **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
|
air's watch-based reload silently never fired. Fixed by enabling air polling
|
||||||
(`poll = true`, `poll_interval = 500` in `.air.toml`).
|
(`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)
|
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)
|
quit := make(chan os.Signal, 1)
|
||||||
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
||||||
<-quit
|
<-quit
|
||||||
|
|||||||
@@ -12,10 +12,17 @@ services:
|
|||||||
# Bind all interfaces INSIDE the container so the published port reaches
|
# Bind all interfaces INSIDE the container so the published port reaches
|
||||||
# it; the `ports` mapping below still keeps it localhost-only on the HOST.
|
# it; the `ports` mapping below still keeps it localhost-only on the HOST.
|
||||||
- LISTEN_ADDR=0.0.0.0:8080
|
- 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.
|
# The scanner looks here; matches the volume mount below.
|
||||||
- GIT_REPO_ROOTS=/repos
|
- GIT_REPO_ROOTS=/repos
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8080:8080"
|
- "127.0.0.1:8080:8080"
|
||||||
|
- "127.0.0.1:8443:8443"
|
||||||
volumes:
|
volumes:
|
||||||
# Source, for hot reload.
|
# Source, for hot reload.
|
||||||
- .:/app
|
- .:/app
|
||||||
|
|||||||
@@ -14,7 +14,14 @@ import (
|
|||||||
|
|
||||||
// Config is the typed application configuration.
|
// Config is the typed application configuration.
|
||||||
type Config struct {
|
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
|
RepoRoots []string // roots to scan for git repositories
|
||||||
GitBin string // path to the git binary
|
GitBin string // path to the git binary
|
||||||
@@ -38,6 +45,9 @@ func Load() (Config, error) {
|
|||||||
|
|
||||||
c := Config{
|
c := Config{
|
||||||
ListenAddr: env("LISTEN_ADDR", "127.0.0.1:8080"),
|
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", "")),
|
RepoRoots: splitList(env("GIT_REPO_ROOTS", "")),
|
||||||
GitBin: env("GIT_BIN", "git"),
|
GitBin: env("GIT_BIN", "git"),
|
||||||
ScanMaxDepth: envInt("SCAN_MAX_DEPTH", 4),
|
ScanMaxDepth: envInt("SCAN_MAX_DEPTH", 4),
|
||||||
|
|||||||
Reference in New Issue
Block a user