Files
sanspie 5eb1832907
ci-platforms / darwin amd64 smoke (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 15m28s
ci / release (push) Skipped
ci-platforms / windows amd64 smoke (push) Skipped
ci-platforms / darwin arm64 smoke (push) Skipped
ci / checks (push) Successful in 7m47s
Complete Stage 12 for v0.12.0
2026-11-05 11:46:53 +03:00

101 lines
2.4 KiB
Go

package mcp
import (
"crypto/rand"
"crypto/subtle"
"encoding/hex"
"encoding/json"
"io"
"net"
"net/http"
"sync/atomic"
"time"
)
const maxBody = 8 << 20
// HTTP serves MCP as JSON-RPC over POST on loopback, for an agent the editor runs (D341).
type HTTP struct {
URL string
token atomic.Pointer[string]
srv *http.Server
s *Server
}
// ListenHTTP serves on 127.0.0.1 at a free port with a fresh bearer token.
func ListenHTTP(opt Options) (*HTTP, error) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, err
}
h := &HTTP{URL: "http://" + ln.Addr().String() + "/mcp", s: newServer(opt)}
h.RotateToken()
h.s.ready.Store(true)
mux := http.NewServeMux()
mux.HandleFunc("/mcp", h.serve)
h.srv = &http.Server{Handler: mux, ReadHeaderTimeout: 10 * time.Second}
go h.srv.Serve(ln)
return h, nil
}
func (h *HTTP) Token() string { return *h.token.Load() }
func (h *HTTP) RotateToken() {
var key [32]byte
rand.Read(key[:])
token := hex.EncodeToString(key[:])
h.token.Store(&token)
}
func (h *HTTP) authorized(r *http.Request) bool {
got := r.Header.Get("Authorization")
return subtle.ConstantTimeCompare([]byte(got), []byte("Bearer "+h.Token())) == 1
}
// local refuses a request whose Host is not loopback, so a web page cannot reach the server by DNS rebinding.
func local(r *http.Request) bool {
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
host = r.Host
}
return host == "127.0.0.1" || host == "localhost"
}
func (h *HTTP) serve(w http.ResponseWriter, r *http.Request) {
switch {
case !local(r):
http.Error(w, "forbidden", http.StatusForbidden)
return
case !h.authorized(r):
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
case r.Method != http.MethodPost:
w.Header().Set("Allow", http.MethodPost)
http.Error(w, "use POST", http.StatusMethodNotAllowed)
return
}
raw, err := io.ReadAll(io.LimitReader(r.Body, maxBody))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
rep, ok := h.s.respond(raw)
if !ok {
w.WriteHeader(http.StatusAccepted)
return
}
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
enc.SetEscapeHTML(false)
enc.Encode(rep)
}
func (h *HTTP) Close() error {
return h.srv.Close()
}
// Definitions is what the model reads about this server before the first message.
func (h *HTTP) Definitions() string {
return h.s.Definitions()
}