Skip to content

Add vMCP core/server config derivation helpers#5513

Open
tgrunnagle wants to merge 1 commit into
mainfrom
vmcp-core-p3-1_issue_5444
Open

Add vMCP core/server config derivation helpers#5513
tgrunnagle wants to merge 1 commit into
mainfrom
vmcp-core-p3-1_issue_5444

Conversation

@tgrunnagle

@tgrunnagle tgrunnagle commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

The single in-memory server.Config is the last god-config coupling the vMCP domain core
to the transport. Phase 1 (RFC THV-0076) extracted a stateless core.Config, and Phase 2
re-homed every transport concern under a ServerConfig consumed by Serve — but
server.New still threads one monolithic config through both layers. This PR (Phase 3.1)
adds the two derivation helpers that map server.Config onto that core/transport pair,
setting up the #5445 wrapper that will reduce server.New to
Serve(ctx, core.New(deriveCoreConfig(cfg, …)), deriveServerConfig(cfg, …)).

The change is purely additive — two new unexported helpers plus their tests:

  • deriveServerConfig projects 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 the
    same cmp.Or transport defaults server.New applies today.
  • deriveCoreConfig assembles the core.Config from the cross-cutting fields plus the
    collaborators (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 an
in-memory-only refactor with no observable behavior change.

Closes #5444

Type of change

  • Refactoring (no behavior change)

Test plan

  • Unit tests (task test)
  • Linting (task lint-fix)

pkg/vmcp/server and pkg/vmcp/core pass cleanly, including under -race. The new
derive_test.go adds 8 tests covering: verbatim transport-field projection, transport
defaulting via cmp.Or, nil cross-cutting propagation (disabled subsystems stay nil),
collaborator pass-through by identity for the core config, raw-ServerName use with nil
admission/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 test run are pre-existing, environment-only
failed to create container runtime: no available runtime found errors in pkg/api and
pkg/mcp/server (no Docker/Podman in this environment), unrelated to this change.
task lint-fix is clean for the new files; the only flag is a pre-existing gosec G115 in
cmd/thv/app/upgrade.go, also unrelated.

Changes

File Change
pkg/vmcp/server/derive.go New deriveServerConfig and deriveCoreConfig helpers
pkg/vmcp/server/derive_test.go Unit tests for both helpers, including drift guards

Does 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:

  • deriveCoreConfig drops the issue's discoveryMgr param. core.Config has no
    discovery field — the core replaces discovery's aggregation role — so the helper instead
    populates Aggregator/Authz/ServerName/Elicitation/HealthStatusProvider.
    ServerName uses the raw cfg.Name (not the transport default) so authorization keys on
    the real VirtualMCPServer name rather than the synthetic toolhive-vmcp fallback.
    Authz/ServerName validation is deferred to core.New, not defaulted here.

  • The legacy SessionFactory/OptimizerFactory/OptimizerConfig fields fold into the
    injected *sessionmanager.FactoryConfig
    (per P2.2 Move SDK hooks + two-phase session creation under Serve #5440). deriveServerConfig takes that,
    the pre-built *health.Monitor (A2), and the shared BackendRegistry as parameters
    because server.Config does not carry them; only the composition root can build the
    factory config (its ComposerFactory closes over the core-owned router/backend client).

  • AuthzMiddleware is intentionally not projected. ServerConfig has no such field —
    authorization moved to the core admission seam (P1.5 Core admission seam (bounded rewrite) #5438), which derives its authorizer from
    the Authz config. The vestigial server.Config.AuthzMiddleware field is kept on purpose
    because cli/serve.go still sets it and must stay unchanged; the dead HTTP authz blocks
    that read it are removed later in P3.2 Reduce server.New body to the wrapper #5445.

Both helpers treat cfg as read-only — unlike server.New, they do no defaulting
write-back (asserted by TestDeriveConfigsTreatInputAsReadOnly). The reflection-based
*MapsAllFields tests guard against silent drift: if a future field is added to either
output 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.New reduction).
Parent story #5432; epic #5419.


🤖 Generated with Claude Code

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>
@github-actions github-actions Bot added size/M Medium PR: 300-599 lines changed and removed size/M Medium PR: 300-599 lines changed labels Jun 12, 2026
@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 69.71%. Comparing base (2ddb9c8) to head (dcbd4db).
⚠️ Report is 1 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tgrunnagle tgrunnagle left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread pkg/vmcp/server/derive_test.go
Comment thread pkg/vmcp/server/derive_test.go
Comment thread pkg/vmcp/server/derive.go
@tgrunnagle tgrunnagle marked this pull request as ready for review June 12, 2026 19:07

@jerm-dro jerm-dro left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Medium PR: 300-599 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

P3.1 deriveCoreConfig/deriveServerConfig config split

2 participants