verae-mcp-timestamping-serv.../internal/server/server.go
George Lambert e04fe6e2c4 feat: embedded NATS with VeraeTime handlers and stdio MCP
Start in-process nats-server on VERAE_NATS_LISTEN (default 127.0.0.1:4223;
empty disables). Queue-subscribe verae.ts.request|verify|hash.report with
ledger/veraetime routing and authctx.Resolve from authorization/access_token.
Publish verae.ts.completed on core NATS only (no JetStream). Wire embed into
main alongside Streamable HTTP; stdio MCP via go-sdk StdioTransport.
2026-09-29 12:11:47 -04:00

196 lines
5 KiB
Go

package server
import (
"fmt"
"net/http"
"os"
"strconv"
"strings"
"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
AuthPath string
GlobalURL string
RequireBearer bool
JSONResponse bool
Stateless 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)
}
}
deps := &tools.Deps{
Ledger: led,
Global: global,
Keeper: keeper,
RequireBearer: opts.RequireBearer,
}
mcpServer := tools.NewServer(deps)
stream := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server {
return mcpServer
}, &mcp.StreamableHTTPOptions{
JSONResponse: opts.JSONResponse,
Stateless: opts.Stateless,
})
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)
})
}