- Public signup no longer requires a caller bearer (POST /auth/signup) - create_user remains admin-only (POST /auth/users) - Optional VERAE_MCP_EDGE_URL bills global stamps via zap.verae.ai - Clearer public auth errors and instructions for Grok/clients
211 lines
5.6 KiB
Go
211 lines
5.6 KiB
Go
package server
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.georgelambert.org/marchon/verae-mcp-timestamping-server-go/internal/edgebill"
|
|
"git.georgelambert.org/marchon/verae-mcp-timestamping-server-go/internal/ledger"
|
|
"git.georgelambert.org/marchon/verae-mcp-timestamping-server-go/internal/tools"
|
|
"git.georgelambert.org/marchon/verae-mcp-timestamping-server-go/internal/veraetime"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
// Options configures the MCP HTTP server.
|
|
type Options struct {
|
|
Ledger *ledger.FileLedger
|
|
Global *veraetime.Client
|
|
Keeper *veraetime.Keeper
|
|
Edge *edgebill.Client
|
|
AuthPath string
|
|
GlobalURL string
|
|
RequireBearer bool
|
|
JSONResponse bool
|
|
Stateless bool
|
|
// Public (DEC-654): writes need the caller's own credential; anonymous verify/hash_report only, rate limited.
|
|
Public bool
|
|
}
|
|
|
|
// Server wraps the MCP Streamable HTTP handler mounted at /mcp.
|
|
type Server struct {
|
|
mcp *mcp.Server
|
|
handler http.Handler
|
|
deps *tools.Deps
|
|
}
|
|
|
|
// New builds an MCP server and Streamable HTTP handler.
|
|
func New(opts Options) (*Server, error) {
|
|
globalURL := strings.TrimSpace(opts.GlobalURL)
|
|
if globalURL == "" {
|
|
globalURL = veraetime.DefaultBaseURL
|
|
}
|
|
authPath := strings.TrimSpace(opts.AuthPath)
|
|
if authPath == "" {
|
|
authPath = veraetime.DefaultAuthPath()
|
|
}
|
|
|
|
global := opts.Global
|
|
if global == nil {
|
|
global = veraetime.NewClient(globalURL, veraetime.WithAuthPath(authPath))
|
|
}
|
|
keeper := opts.Keeper
|
|
if keeper == nil {
|
|
keeper = veraetime.NewKeeper(global, authPath)
|
|
}
|
|
led := opts.Ledger
|
|
if led == nil {
|
|
var err error
|
|
led, err = ledger.Open(ledger.DefaultPath())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open ledger: %w", err)
|
|
}
|
|
}
|
|
|
|
edge := opts.Edge
|
|
if edge == nil {
|
|
edge = edgebill.FromEnv()
|
|
}
|
|
deps := &tools.Deps{
|
|
Ledger: led,
|
|
Global: global,
|
|
Keeper: keeper,
|
|
Edge: edge,
|
|
RequireBearer: opts.RequireBearer,
|
|
}
|
|
if opts.Public {
|
|
deps.Public, deps.Checker = true, global
|
|
}
|
|
mcpServer := tools.NewServer(deps)
|
|
// Behind Caddy (or any reverse proxy) the bind is loopback while Host is
|
|
// the public name (mcp.verae.ai). Disable the SDK's localhost Host check.
|
|
stream := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server {
|
|
return mcpServer
|
|
}, &mcp.StreamableHTTPOptions{
|
|
JSONResponse: opts.JSONResponse,
|
|
Stateless: opts.Stateless,
|
|
DisableLocalhostProtection: true,
|
|
})
|
|
|
|
mux := http.NewServeMux()
|
|
mux.Handle("/mcp", stream)
|
|
mux.Handle("/mcp/", stream)
|
|
|
|
return &Server{mcp: mcpServer, handler: mux, deps: deps}, nil
|
|
}
|
|
|
|
// Handler returns the HTTP handler (routes /mcp).
|
|
func (s *Server) Handler() http.Handler {
|
|
if s == nil {
|
|
return http.NotFoundHandler()
|
|
}
|
|
return s.handler
|
|
}
|
|
|
|
// MCP returns the underlying MCP server.
|
|
func (s *Server) MCP() *mcp.Server {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
return s.mcp
|
|
}
|
|
|
|
// Config is runtime configuration from flags and env.
|
|
type Config struct {
|
|
Transport string
|
|
Host string
|
|
Port int
|
|
RequireBearer bool
|
|
GlobalURL string
|
|
AuthPath string
|
|
LedgerPath string
|
|
NATSListen string
|
|
CORS bool
|
|
}
|
|
|
|
// LoadConfig merges flag defaults with environment overrides.
|
|
// Env wins when set: VERAE_MCP_TRANSPORT, VERAE_MCP_HOST, VERAE_MCP_PORT,
|
|
// VERAE_MCP_REQUIRE_BEARER, VERAE_GLOBAL_URL, VERAE_AUTH_PROPERTIES, VERAE_LEDGER_PATH,
|
|
// VERAE_NATS_LISTEN, VERAE_MCP_CORS.
|
|
// VERAE_NATS_LISTEN defaults to 127.0.0.1:4223; set empty to disable embedded NATS.
|
|
func LoadConfig(transport, host string, port int) Config {
|
|
cfg := Config{
|
|
Transport: transport,
|
|
Host: host,
|
|
Port: port,
|
|
GlobalURL: veraetime.DefaultBaseURL,
|
|
AuthPath: veraetime.DefaultAuthPath(),
|
|
LedgerPath: ledger.DefaultPath(),
|
|
NATSListen: "127.0.0.1:4223",
|
|
}
|
|
if v := strings.TrimSpace(os.Getenv("VERAE_MCP_TRANSPORT")); v != "" {
|
|
cfg.Transport = v
|
|
}
|
|
if v := strings.TrimSpace(os.Getenv("VERAE_MCP_HOST")); v != "" {
|
|
cfg.Host = v
|
|
}
|
|
if v := strings.TrimSpace(os.Getenv("VERAE_MCP_PORT")); v != "" {
|
|
if n, err := strconv.Atoi(v); err == nil && n > 0 {
|
|
cfg.Port = n
|
|
}
|
|
}
|
|
if v := strings.TrimSpace(os.Getenv("VERAE_GLOBAL_URL")); v != "" {
|
|
cfg.GlobalURL = v
|
|
}
|
|
if v := strings.TrimSpace(os.Getenv("VERAE_AUTH_PROPERTIES")); v != "" {
|
|
cfg.AuthPath = v
|
|
}
|
|
if v := strings.TrimSpace(os.Getenv("VERAE_LEDGER_PATH")); v != "" {
|
|
cfg.LedgerPath = v
|
|
}
|
|
if _, set := os.LookupEnv("VERAE_NATS_LISTEN"); set {
|
|
cfg.NATSListen = strings.TrimSpace(os.Getenv("VERAE_NATS_LISTEN"))
|
|
}
|
|
cfg.RequireBearer = envTruthy("VERAE_MCP_REQUIRE_BEARER")
|
|
cfg.CORS = envTruthy("VERAE_MCP_CORS")
|
|
if cfg.Transport == "" {
|
|
cfg.Transport = "streamable-http"
|
|
}
|
|
if cfg.Host == "" {
|
|
cfg.Host = "127.0.0.1"
|
|
}
|
|
if cfg.Port == 0 {
|
|
cfg.Port = 8000
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
func envTruthy(key string) bool {
|
|
v := strings.TrimSpace(os.Getenv(key))
|
|
switch strings.ToLower(v) {
|
|
case "1", "true", "yes", "on":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Addr returns host:port.
|
|
func (c Config) Addr() string {
|
|
return fmt.Sprintf("%s:%d", c.Host, c.Port)
|
|
}
|
|
|
|
// WithCORS wraps h with permissive CORS when enabled (Python VERAE_MCP_CORS=1).
|
|
func WithCORS(h http.Handler, enabled bool) http.Handler {
|
|
if !enabled {
|
|
return h
|
|
}
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "*")
|
|
w.Header().Set("Access-Control-Expose-Headers", "mcp-session-id, Mcp-Session-Id")
|
|
if r.Method == http.MethodOptions {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
h.ServeHTTP(w, r)
|
|
})
|
|
}
|