From 0838bcbab2b1da8a4437985fabe1fe9ade53b4d5 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Fri, 27 Mar 2026 01:07:52 -0700 Subject: [PATCH 1/2] Bump mcdsl to f94c4b1 for $PORT env var support Update mcdsl from v1.0.0 to the port-env-support branch tip, which adds automatic $PORT environment variable support to the config package. Adapt grpcserver.New call to the updated signature that now accepts an *Options parameter (pass nil for default behavior). Co-Authored-By: Claude Opus 4.6 (1M context) --- go.mod | 2 +- go.sum | 4 +- internal/grpcserver/server.go | 2 +- .../kyle/mcdsl/config/config.go | 66 +++++++++++++++++++ .../kyle/mcdsl/grpcserver/server.go | 38 +++++++++-- vendor/modules.txt | 2 +- 6 files changed, 102 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index f80ed15..a6fb222 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module git.wntrmute.dev/kyle/mcr go 1.25.7 require ( - git.wntrmute.dev/kyle/mcdsl v1.0.0 + git.wntrmute.dev/kyle/mcdsl v1.0.2-0.20260327074919-f94c4b1abf9c github.com/go-chi/chi/v5 v5.2.5 github.com/google/uuid v1.6.0 github.com/spf13/cobra v1.10.2 diff --git a/go.sum b/go.sum index ae50143..d688e2a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.wntrmute.dev/kyle/mcdsl v1.0.0 h1:YB7dx4gdNYKKcVySpL6UkwHqdCJ9Nl1yS0+eHk0hNtk= -git.wntrmute.dev/kyle/mcdsl v1.0.0/go.mod h1:wo0tGfUAxci3XnOe4/rFmR0RjUElKdYUazc+Np986sg= +git.wntrmute.dev/kyle/mcdsl v1.0.2-0.20260327074919-f94c4b1abf9c h1:DoSa9jdpMhFgxQlQSqe9Bgg8G4vQzH/CQO1KJR39abs= +git.wntrmute.dev/kyle/mcdsl v1.0.2-0.20260327074919-f94c4b1abf9c/go.mod h1:wo0tGfUAxci3XnOe4/rFmR0RjUElKdYUazc+Np986sg= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= diff --git a/internal/grpcserver/server.go b/internal/grpcserver/server.go index ee729bc..4b7562d 100644 --- a/internal/grpcserver/server.go +++ b/internal/grpcserver/server.go @@ -65,7 +65,7 @@ type Server struct { // // If certFile or keyFile is empty, TLS is skipped (for testing only). func New(certFile, keyFile string, deps Deps, logger *slog.Logger) (*Server, error) { - srv, err := mcdslgrpc.New(certFile, keyFile, deps.Authenticator, methodMap(), logger) + srv, err := mcdslgrpc.New(certFile, keyFile, deps.Authenticator, methodMap(), logger, nil) if err != nil { return nil, err } diff --git a/vendor/git.wntrmute.dev/kyle/mcdsl/config/config.go b/vendor/git.wntrmute.dev/kyle/mcdsl/config/config.go index b28ff58..14772de 100644 --- a/vendor/git.wntrmute.dev/kyle/mcdsl/config/config.go +++ b/vendor/git.wntrmute.dev/kyle/mcdsl/config/config.go @@ -144,6 +144,8 @@ func Load[T any](path string, envPrefix string) (*T, error) { applyEnvToStruct(reflect.ValueOf(&cfg).Elem(), envPrefix) } + applyPortEnv(&cfg) + applyBaseDefaults(&cfg) if err := validateBase(&cfg); err != nil { @@ -239,6 +241,70 @@ func findBase(cfg any) *Base { return nil } +// applyPortEnv overrides ServerConfig.ListenAddr and ServerConfig.GRPCAddr +// from $PORT and $PORT_GRPC respectively. These environment variables are +// set by the MCP agent to assign authoritative port bindings, so they take +// precedence over both TOML values and generic env overrides. +func applyPortEnv(cfg any) { + sc := findServerConfig(cfg) + if sc == nil { + return + } + + if port, ok := os.LookupEnv("PORT"); ok { + sc.ListenAddr = ":" + port + } + if port, ok := os.LookupEnv("PORT_GRPC"); ok { + sc.GRPCAddr = ":" + port + } +} + +// findServerConfig returns a pointer to the ServerConfig in the config +// struct. It first checks for an embedded Base (which contains Server), +// then walks the struct tree via reflection to find any ServerConfig field +// directly (e.g., the Metacrypt pattern where ServerConfig is embedded +// without Base). +func findServerConfig(cfg any) *ServerConfig { + if base := findBase(cfg); base != nil { + return &base.Server + } + + return findServerConfigReflect(reflect.ValueOf(cfg)) +} + +// findServerConfigReflect walks the struct tree to find a ServerConfig field. +func findServerConfigReflect(v reflect.Value) *ServerConfig { + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + if v.Kind() != reflect.Struct { + return nil + } + + scType := reflect.TypeOf(ServerConfig{}) + t := v.Type() + for i := range t.NumField() { + field := t.Field(i) + fv := v.Field(i) + + if field.Type == scType { + sc, ok := fv.Addr().Interface().(*ServerConfig) + if ok { + return sc + } + } + + // Recurse into embedded or nested structs. + if fv.Kind() == reflect.Struct && field.Type != scType { + if sc := findServerConfigReflect(fv); sc != nil { + return sc + } + } + } + + return nil +} + // applyEnvToStruct recursively walks a struct and overrides field values // from environment variables. The env variable name is built from the // prefix and the toml tag: PREFIX_SECTION_FIELD (uppercased). diff --git a/vendor/git.wntrmute.dev/kyle/mcdsl/grpcserver/server.go b/vendor/git.wntrmute.dev/kyle/mcdsl/grpcserver/server.go index 2848cae..1fba879 100644 --- a/vendor/git.wntrmute.dev/kyle/mcdsl/grpcserver/server.go +++ b/vendor/git.wntrmute.dev/kyle/mcdsl/grpcserver/server.go @@ -48,21 +48,45 @@ type Server struct { listener net.Listener } +// Options configures optional behavior for the gRPC server. +type Options struct { + // PreInterceptors run before the logging and auth interceptors. + // Use for lifecycle gates like seal checks that should reject + // requests before any auth validation occurs. + PreInterceptors []grpc.UnaryServerInterceptor + + // PostInterceptors run after auth but before the handler. + // Use for audit logging, rate limiting, or other cross-cutting + // concerns that need access to the authenticated identity. + PostInterceptors []grpc.UnaryServerInterceptor +} + // New creates a gRPC server with TLS (if certFile and keyFile are -// non-empty) and an interceptor chain: logging → auth → handler. +// non-empty) and an interceptor chain: +// +// [pre-interceptors] → logging → auth → [post-interceptors] → handler // // The auth interceptor uses methods to determine the access level for // each RPC. Methods not in any map are denied by default. // // If certFile and keyFile are empty, TLS is skipped (for testing). -func New(certFile, keyFile string, authenticator *auth.Authenticator, methods MethodMap, logger *slog.Logger) (*Server, error) { - chain := grpc.ChainUnaryInterceptor( +// opts is optional; pass nil for the default chain (logging + auth only). +func New(certFile, keyFile string, authenticator *auth.Authenticator, methods MethodMap, logger *slog.Logger, opts *Options) (*Server, error) { + var interceptors []grpc.UnaryServerInterceptor + if opts != nil { + interceptors = append(interceptors, opts.PreInterceptors...) + } + interceptors = append(interceptors, loggingInterceptor(logger), authInterceptor(authenticator, methods), ) + if opts != nil { + interceptors = append(interceptors, opts.PostInterceptors...) + } + chain := grpc.ChainUnaryInterceptor(interceptors...) - var opts []grpc.ServerOption - opts = append(opts, chain) + var serverOpts []grpc.ServerOption + serverOpts = append(serverOpts, chain) if certFile != "" && keyFile != "" { cert, err := tls.LoadX509KeyPair(certFile, keyFile) @@ -73,11 +97,11 @@ func New(certFile, keyFile string, authenticator *auth.Authenticator, methods Me Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS13, } - opts = append(opts, grpc.Creds(credentials.NewTLS(tlsCfg))) + serverOpts = append(serverOpts, grpc.Creds(credentials.NewTLS(tlsCfg))) } return &Server{ - GRPCServer: grpc.NewServer(opts...), + GRPCServer: grpc.NewServer(serverOpts...), Logger: logger, }, nil } diff --git a/vendor/modules.txt b/vendor/modules.txt index 73fcece..d4e6c79 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,4 +1,4 @@ -# git.wntrmute.dev/kyle/mcdsl v1.0.0 +# git.wntrmute.dev/kyle/mcdsl v1.0.2-0.20260327074919-f94c4b1abf9c ## explicit; go 1.25.7 git.wntrmute.dev/kyle/mcdsl/auth git.wntrmute.dev/kyle/mcdsl/config -- 2.49.1 From e81903dd88bb544b94cde334150e6e15811bbf84 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Fri, 27 Mar 2026 01:14:52 -0700 Subject: [PATCH 2/2] Update mcdsl to v1.1.0 (tagged release) Replace pseudo-version with the tagged v1.1.0 release. Co-Authored-By: Claude Opus 4.6 (1M context) --- go.mod | 2 +- go.sum | 4 ++-- vendor/modules.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index a6fb222..d6a4b06 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module git.wntrmute.dev/kyle/mcr go 1.25.7 require ( - git.wntrmute.dev/kyle/mcdsl v1.0.2-0.20260327074919-f94c4b1abf9c + git.wntrmute.dev/kyle/mcdsl v1.1.0 github.com/go-chi/chi/v5 v5.2.5 github.com/google/uuid v1.6.0 github.com/spf13/cobra v1.10.2 diff --git a/go.sum b/go.sum index d688e2a..3d2127d 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.wntrmute.dev/kyle/mcdsl v1.0.2-0.20260327074919-f94c4b1abf9c h1:DoSa9jdpMhFgxQlQSqe9Bgg8G4vQzH/CQO1KJR39abs= -git.wntrmute.dev/kyle/mcdsl v1.0.2-0.20260327074919-f94c4b1abf9c/go.mod h1:wo0tGfUAxci3XnOe4/rFmR0RjUElKdYUazc+Np986sg= +git.wntrmute.dev/kyle/mcdsl v1.1.0 h1:NXfEXRtaCRPNjCbqqgU7L2SgDAZkQn9kd40xJDgxnns= +git.wntrmute.dev/kyle/mcdsl v1.1.0/go.mod h1:wo0tGfUAxci3XnOe4/rFmR0RjUElKdYUazc+Np986sg= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= diff --git a/vendor/modules.txt b/vendor/modules.txt index d4e6c79..ebf539e 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,4 +1,4 @@ -# git.wntrmute.dev/kyle/mcdsl v1.0.2-0.20260327074919-f94c4b1abf9c +# git.wntrmute.dev/kyle/mcdsl v1.1.0 ## explicit; go 1.25.7 git.wntrmute.dev/kyle/mcdsl/auth git.wntrmute.dev/kyle/mcdsl/config -- 2.49.1