Add vMCP Serve skeleton and ServerConfig#5467
Conversation
Introduce the transport-side entry point of the vMCP New/Serve split. Serve wraps an already-constructed core VMCP and returns the existing *Server, building the mcp-go server and applying the transport defaults that server.New applies today. The route mux and HTTP lifecycle are provided by the carried-forward Handler/Start/Stop, so observable behavior is unchanged and server.New is not yet routed through Serve. Implements changes for issue #5439: - Add ServerConfig (transport-only fields plus the cross-cutting TelemetryProvider/AuditConfig) and Serve in pkg/vmcp/server/serve.go - Validate nil cfg / nil core / nil SessionFactory via ErrInvalidConfig - Wire the core's idempotent Close into the server shutdown sequence - Guard the /status handler against a nil backend registry, which the transport does not own yet in this phase Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Clarify the Serve doc comment: it builds only the mcp-go server and shutdown wiring; the route mux and HTTP lifecycle stay in the carried-forward Handler/Start until later phases relocate them - Document the Config fields buildServeConfig deliberately leaves unmapped (AuthzMiddleware, HealthMonitorConfig) and why - Add a reflection drift guard asserting every other Config field is populated by buildServeConfig, so a future field can't be dropped silently 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 @@
## vmcp-core-p1-5_issue_5438 #5467 +/- ##
=============================================================
+ Coverage 69.23% 69.25% +0.02%
=============================================================
Files 637 638 +1
Lines 64859 64909 +50
=============================================================
+ Hits 44907 44955 +48
- Misses 16629 16633 +4
+ Partials 3323 3321 -2 ☔ 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 — Serve skeleton + ServerConfig
Sound, behavior-preserving additive skeleton. The deferral of the route mux/lifecycle to the carried-forward Handler/Start/Stop (rather than rebuilding it in Serve) is the correct design call and faithful to the epic's "relocate, don't rewrite" principle — not a scope gap. Tests pass; clean boundary (no mcp-go types cross core.VMCP); buildServeConfig copies-before-mutating (better than New).
No HIGH findings — nothing blocks merge. The MEDIUM items are about tightening the parity-drift protection this staged refactor leans on, plus a couple of test/comment-precision gaps.
| Severity | Finding |
|---|---|
| MEDIUM | Default literals triplicated (New / buildServeConfig / test) + drift guard checks presence, not value/inverse |
| MEDIUM | Dead Config.StatusReporter mapping — set directly on the struct, mapped only to satisfy the drift test |
| MEDIUM | .well-known/ route in the AC but untested; "auth-chain absent" comment overstates reality (Handler still builds the chain) |
| LOW-MED | Serve-built *Server is not startable (nil session/discovery/registry); document the nil-field contract |
| LOW | nil-registry guard may silently mask a real misconfig on the server.New path |
Minor (non-blocking, no inline comment): no positive /metrics-registered assertion; Close idempotency and a combined nil-cfg+nil-vmcp ordering case aren't asserted.
Reviewed by 4 specialist agents (correctness, architecture, test, security). Codex cross-review skipped (CLI not installed).
🤖 Generated with Claude Code
Addresses #5467 review comments: - MEDIUM serve.go (3381738359): extract shared default consts (defaultServerName/Version/Host/EndpointPath) referenced by New and buildServeConfig, and assert against them in tests, killing the triplicated literals - MEDIUM serve.go (3381738361): drop the dead Config.StatusReporter mapping (set directly on Server like HealthMonitor) and add it to the drift guard's intentionallyUnmapped set - LOW-MEDIUM serve.go (3381738364): document the returned *Server's nil-field contract (must not be Started/served on "/" until later phases wire session/discovery/registry) - MEDIUM serve_test.go (3381738372): probe /.well-known/ for the clean JSON 404; soften the overstated "auth-chain absent" comment - MEDIUM serve_test.go (3381738374): note the drift guard checks presence only; cover SessionTTL/StatusReportingInterval value correctness in TestServePreservesExplicitConfig - LOW status.go (3381738382): slog.Debug in the nil-registry branch so an unexpectedly-nil registry on the New path is observable Also from the review body: add a positive /metrics-registered test and a combined nil-cfg+nil-vmcp validation case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Re: the non-blocking minor items in the review body (addressed in 3d5e331):
|
Summary
This is the transport-side entry point of the New/Serve split that the rest of
the vMCP Phase 2 refactor (epic #5419) builds on. Today
server.Newis agod-object that constructs both the identity-parameterized core domain logic and
the HTTP/SDK transport in one call. This PR lands the additive
Serveskeleton solater tasks can relocate the remaining transport concerns onto it without churning
server.New.Serve(ctx, core.VMCP, *ServerConfig) (*Server, error)in the newpkg/vmcp/server/serve.go.Servewraps an already-constructed coreVMCPandreturns the existing
*Serverstruct, so all carried-forward accessors(
Handler/Start/Stop/Address/MCPServer/Ready) keep working unchanged.ServerConfigstruct — the subset of the legacyserver.Configthat configures the HTTP/SDK runtime, plus the cross-cuttingTelemetryProvider/AuditConfigconsumed by both the core and the transport.Serveapplies the same transport defaultsserver.Newapplies today(
Host→127.0.0.1,EndpointPath→/mcp,Name/Version/SessionTTLdefaults;
Port == 0stays "OS-assigned"), builds the mcp-go server, and wiresthe core's
Closeinto the server's shutdown sequence./statushandler against a nil backend registry, since the transportdoes not yet own the registry at this phase.
server.Newis intentionally not routed throughServein this PR, so thechange is purely additive and observable behavior is unchanged. This keeps the PR
small and the parity surface flat; Phase 3 (#5444/#5445) reduces
server.Newto awrapper over
Serve.Closes #5439
Type of change
Test plan
task test)task lint-fix)Added
pkg/vmcp/server/serve_test.gocovering:Serveapplies transport defaults identically toserver.Newand leaves thecaller's
ServerConfigunmutated andPort == 0OS-assigned.Servepreserves explicitly set config values.*Serverexposes a usableMCPServer(), andHandler(ctx)registers the unauthenticated routes (
/health,/ping,/readyz,/status,/api/backends/health);/metricsis absent without a telemetry provider.Stopruns the shutdown funcs and closes the injected core.vmcp.ErrInvalidConfig-wrapped error for nilcfg, nilcore, and nil
SessionFactory.Configfield is populated bybuildServeConfigexcept the documented intentionally-unmapped set(
AuthzMiddleware,HealthMonitorConfig).The existing
server.New-driven behavioral-parity suite stays green;server.Newis untouched and not routed through
Serve. Repo-wide lint is clean apart from apre-existing, unrelated gosec G115 finding in
cmd/thv/app/upgrade.go.Changes
pkg/vmcp/server/serve.goServeentry point and transport-onlyServerConfig;buildServeConfigmaps config and applies defaultspkg/vmcp/server/serve_test.gopkg/vmcp/server/status.gobuildStatusResponseso the Serve skeleton returns an empty backend view instead of panickingDoes this introduce a user-facing change?
No.
Special notes for reviewers
Servereturns the same*Serverstruct ratherthan a new type, and mirrors the defaults and mcp-go construction that
server.Newperforms today. No mcp-go types cross theVMCPboundary.ServerConfigexists. These fields configure the HTTP/SDK runtime andare meaningless to the core (R3). The stutter
server.ServerConfigisintentional and
nolint-annotated: the split mandates the name and the package'sexisting
Configis the legacy type being decomposed.authenticated middleware chain and direct
VMCPrequest path (P2.3 Move middleware chain under Serve; remove authz + annotation mw #5441), and the ASrunner / status reporter / optimizer / health monitor lifecycle (P2.5 Move AS runner, status reporter, optimizer, health monitor under Serve #5443) are moved
under
Serveby later Phase 2 tasks. ThebuildServeConfigdrift-guard testdocuments the two fields deliberately unmapped until then.