Add vMCP core/server config derivation helpers#5513
Conversation
Phase 3.1 of the RFC THV-0076 config decomposition. The single in-memory server.Config is the last god-config that couples the domain core to the transport. Introduce two unexported helpers that map it onto the already- existing core.Config (Phase 1) and transport ServerConfig (Phase 2): - deriveServerConfig projects the transport-only fields plus the cross-cutting TelemetryProvider/AuditConfig (R3); the built health monitor (A2), shared backend registry, and assembled session-manager FactoryConfig are threaded in by the composition root since server.Config does not carry them. AuthzMiddleware is not projected — ServerConfig has no such field; authz moved to the core admission seam. - deriveCoreConfig assembles core.Config from cfg's cross-cutting fields (ServerName uses the raw cfg.Name for authz parity) plus the collaborators passed through rather than reached out of cfg. Both treat cfg as read-only (no defaulting write-back like server.New). This only adds the helpers and their tests; #5445 rewires server.New to call them through Serve. server.New and all serialized formats are unchanged. Closes #5444 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #5513 +/- ##
==========================================
+ Coverage 69.68% 69.71% +0.03%
==========================================
Files 645 646 +1
Lines 65590 65633 +43
==========================================
+ Hits 45708 45758 +50
+ Misses 16535 16528 -7
Partials 3347 3347 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
tgrunnagle
left a comment
There was a problem hiding this comment.
Multi-agent review summary
Recommendation: COMMENT — clean refactor, 0 blocking issues. All findings are LOW/INFO test-quality polish or forward-looking notes for #5445. (Posted as COMMENT rather than APPROVE because GitHub does not permit approving one's own PR.)
Phase 3.1 of the vMCP config decomposition (RFC THV-0076). Two unexported helpers — deriveServerConfig and deriveCoreConfig — map the legacy monolithic server.Config onto the new core.Config (domain) + ServerConfig (transport) pair, with 8 unit tests including reflection-based drift guards. Purely additive and in-memory-only: the helpers are not yet wired into server.New (#5445 will do that), so there is no observable behavior change.
Four specialist reviewers (Go conventions, architecture/anti-patterns, test coverage, security/authz) independently verified the mappings are complete — all 20 ServerConfig fields and all 11 core.Config fields are populated — that the cmp.Or defaulting matches server.New exactly (and intentionally leaves Port undefaulted), that cfg is treated as read-only (unlike server.New, which writes defaults back), and that Cedar authz parity is preserved by keying core.Config.ServerName on the raw VirtualMCPServer name rather than the synthetic toolhive-vmcp default. No correctness or security defects were found.
Consensus findings
| # | Severity | Category | Finding |
|---|---|---|---|
| 1 | LOW | Test | AuthMiddleware/RateLimitMiddleware are only NotNil-checked; a swap between the two same-typed fields would not be caught |
| 2 | LOW | Test | *MapsAllFields drift guard overstates its guarantee for cmp.Or-defaulted fields; skip-set / zero-valid Port handling under-documented; an idempotence parity test would help |
| 3 | INFO | Security | Forward-looking note for #5445: pass the raw cfg.Name to deriveCoreConfig (server.New mutates it in place) and preserve nil-Authz allow-all parity |
No HIGH or MEDIUM findings. The change satisfies every acceptance criterion in #5444.
Codex cross-review: skipped (codex CLI not installed).
🤖 Multi-agent review via /dev:pr-review
jerm-dro
left a comment
There was a problem hiding this comment.
suggestion: These are strictly options — the helpers are correct and the *MapsAllFields reflection guards are a solid safety net against silent drift. A couple of directions that could reduce the moving parts, take or leave:
Option 1 — pull defaulting strictly upstream. Right now the cmp.Or transport defaults live in three places: server.New, buildServeConfig, and deriveServerConfig. If the config is fully resolved once at the edge (cli/serve.go, where flags/CRD/YAML become config), then every projection downstream is pure pass-through — no cmp.Or, no duplicated "which fields get defaulted" list, no idempotent re-defaulting. The config types would then hold resolved values, which is easier to reason about than "defaulted depending on which constructor you came through."
Option 2 — reconsider whether the split needs separate types + derivation code. core.New and Serve are already separate functions, so a caller naturally only supplies what each needs — the boundary is enforced by what you pass each function. That raises the question of whether two config types (plus deriveCoreConfig/deriveServerConfig to convert between them, plus the server.Config → ServerConfig → Config round-trip) are buying enough over one shared config that both reference. Worth noting core.Config is ~8/11 injected collaborators and only 3 actual config values, so it's close to a collaborator bag core.New could take directly. If the split is still wanted, expressing it on the existing config (e.g. embedded sub-structs) would get the same separation without new conversion code or parallel transport configs that can drift.
Both are bigger than this diff and partly touch the earlier-phase types, so deferring the split entirely is also a reasonable call. I'll leave this up to you.
Summary
The single in-memory
server.Configis the last god-config coupling the vMCP domain coreto the transport. Phase 1 (RFC THV-0076) extracted a stateless
core.Config, and Phase 2re-homed every transport concern under a
ServerConfigconsumed byServe— butserver.Newstill threads one monolithic config through both layers. This PR (Phase 3.1)adds the two derivation helpers that map
server.Configonto that core/transport pair,setting up the #5445 wrapper that will reduce
server.NewtoServe(ctx, core.New(deriveCoreConfig(cfg, …)), deriveServerConfig(cfg, …)).The change is purely additive — two new unexported helpers plus their tests:
deriveServerConfigprojects the transport-only fields (Host,Port,EndpointPath,SessionTTL, the auth/rate-limit middleware handlers,AuthServer,status reporting, watcher, session storage) onto a fresh
ServerConfig, applying thesame
cmp.Ortransport defaultsserver.Newapplies today.deriveCoreConfigassembles thecore.Configfrom the cross-cutting fields plus thecollaborators (aggregator, router, backend client/registry, workflow defs, authz config,
elicitation requester, health status provider) the composition root builds.
server.New,cli/serve.go, and all serialized/CRD/YAML formats are unchanged; this is anin-memory-only refactor with no observable behavior change.
Closes #5444
Type of change
Test plan
task test)task lint-fix)pkg/vmcp/serverandpkg/vmcp/corepass cleanly, including under-race. The newderive_test.goadds 8 tests covering: verbatim transport-field projection, transportdefaulting via
cmp.Or, nil cross-cutting propagation (disabled subsystems stay nil),collaborator pass-through by identity for the core config, raw-
ServerNameuse with niladmission/health defaults, read-only treatment of the input
cfg(no defaulting write-back),and reflection-based drift guards asserting every field of both output configs is populated.
The only failures in the full
task testrun are pre-existing, environment-onlyfailed to create container runtime: no available runtime founderrors inpkg/apiandpkg/mcp/server(no Docker/Podman in this environment), unrelated to this change.task lint-fixis clean for the new files; the only flag is a pre-existing gosec G115 incmd/thv/app/upgrade.go, also unrelated.Changes
pkg/vmcp/server/derive.goderiveServerConfigandderiveCoreConfighelperspkg/vmcp/server/derive_test.goDoes this introduce a user-facing change?
No.
Special notes for reviewers
The issue's illustrative code predates the final Phase 1/2 types, so the helpers follow the
actual code rather than the issue's snippet. Three intentional divergences worth surfacing:
deriveCoreConfigdrops the issue'sdiscoveryMgrparam.core.Confighas nodiscovery field — the core replaces discovery's aggregation role — so the helper instead
populates
Aggregator/Authz/ServerName/Elicitation/HealthStatusProvider.ServerNameuses the rawcfg.Name(not the transport default) so authorization keys onthe real
VirtualMCPServername rather than the synthetictoolhive-vmcpfallback.Authz/ServerNamevalidation is deferred tocore.New, not defaulted here.The legacy
SessionFactory/OptimizerFactory/OptimizerConfigfields fold into theinjected
*sessionmanager.FactoryConfig(per P2.2 Move SDK hooks + two-phase session creation under Serve #5440).deriveServerConfigtakes that,the pre-built
*health.Monitor(A2), and the sharedBackendRegistryas parametersbecause
server.Configdoes not carry them; only the composition root can build thefactory config (its
ComposerFactorycloses over the core-owned router/backend client).AuthzMiddlewareis intentionally not projected.ServerConfighas no such field —authorization moved to the core admission seam (P1.5 Core admission seam (bounded rewrite) #5438), which derives its authorizer from
the
Authzconfig. The vestigialserver.Config.AuthzMiddlewarefield is kept on purposebecause
cli/serve.gostill sets it and must stay unchanged; the dead HTTP authz blocksthat read it are removed later in P3.2 Reduce server.New body to the wrapper #5445.
Both helpers treat
cfgas read-only — unlikeserver.New, they do no defaultingwrite-back (asserted by
TestDeriveConfigsTreatInputAsReadOnly). The reflection-based*MapsAllFieldstests guard against silent drift: if a future field is added to eitheroutput config and a helper forgets to populate it, the test fails.
This is the first half of Phase 3; it blocks #5445 (P3.2, the
server.Newreduction).Parent story #5432; epic #5419.
🤖 Generated with Claude Code