Skip to content

Route server.New through core.New and Serve#5556

Merged
tgrunnagle merged 6 commits into
mainfrom
vmcp-core-p3-2_issue_5445
Jun 23, 2026
Merged

Route server.New through core.New and Serve#5556
tgrunnagle merged 6 commits into
mainfrom
vmcp-core-p3-2_issue_5445

Conversation

@tgrunnagle

@tgrunnagle tgrunnagle commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

server.New was the vMCP transport god-object: a ~210-line constructor that inlined the
mcp-go server, SDK hooks, two-phase session creation, the workflow composer, telemetry
decoration, and the health monitor (anti-pattern #3). Phases 1–2 (#5430/#5431) extracted a
stateless domain core (core.New) and a transport entry point (Serve) behind a stable
seam, and #5444 added the deriveCoreConfig/deriveServerConfig projections — but
server.New still ran the legacy inline path, so the behavioral-parity suite only ever
exercised the dead-end legacy chain rather than the live core path.

This PR reduces server.New's body to the thin wrapper the RFC (THV-0076) specifies:
Serve(ctx, core.New(deriveCoreConfig(...)), deriveServerConfig(...)). The New/Serve
split becomes the single live path for every caller, so the parity suite now genuinely
gates the core path. The 7-param server.New signature is byte-for-byte unchanged.

  • Route New through the core. server.New builds the backend health monitor at the
    composition root (A2), assembles the domain core via core.New(deriveCoreConfig(...)),
    and hands it to Serve(deriveServerConfig(...)). The health monitor is threaded both as
    a health.StatusProvider into the core (capability filtering) and as the lifecycle owner
    into Serve (Start/Stop, P2.5 Move AS runner, status reporter, optimizer, health monitor under Serve #5443).
  • Feed the core under the frozen signature. Added Aggregator and Authz fields to
    server.Config — the only channel to supply the core without changing the signature.
    cli/serve.go populates both. A new authfactory.BuildAuthzConfig surfaces the
    *authz.Config the core admission seam consumes, reusing the exact config the existing
    Cedar HTTP middleware builder produces (DRY, allow-all parity preserved for the
    no-policies case).
  • Resolve the elicitation construction-order inversion. The SDK elicitation adapter
    wraps the mcp-go server that Serve builds after core.New, but the core requires a
    non-nil ElicitationRequester at construction. A late-bound requester
    (elicitation_latebound.go) is handed to the core and bound to the real adapter after
    Serve returns, before serving begins.
  • Make the core the single aggregator. FactoryConfig.AdvertiseFromCore is set so the
    session factory sources tools from the core and the Serve-layer optimizer (Wire the tool optimizer onto the Serve path #5543)
    advertises find_tool/call_tool, rather than the factory decorator aggregating in
    parallel.
  • Remove now-dead constructor cruft. Dropped New's //nolint:gocyclo, the dead
    validateWorkflows, and four orphaned Server fields
    (router/backendClient/handlerFactory/capabilityAdapter) the legacy inline body
    held.
  • Rework the parity suite to exercise the core path. Server tests now source tools from
    a stub or real Aggregator and route calls through core.CallTool → backend client,
    traversing identity binding — instead of the legacy chain.

Review feedback added two changes the reroute activates:

  • Cache backend aggregation per identity. The core re-derives the advertised view on
    every call, so the reroute would otherwise sweep every backend's tools/list on every
    tool call. aggregator.NewCachingAggregator is a transparent, identity-keyed, TTL-bounded
    decorator wrapping the Aggregator below the core (the cache key includes the subject and
    forwarded credentials, so no caller's capability view leaks to another).
  • Move backend health-monitor ownership into the core. core.New builds, starts, and
    (via Close) stops the monitor and filters with it, exposing it through a BackendHealth()
    reporter; the server keeps only the /health + /status reporting routes. This gives the
    monitor one owner and removes the server-side buildHealthMonitor, the
    healthMonitor/healthMonitorMu fields, ServerConfig.HealthMonitor, and the typed-nil
    dance. (Reverses the P2.5 Move AS runner, status reporter, optimizer, health monitor under Serve #5443 split that homed the monitor's Start/Stop in Serve.)

Part of #5445.

Rebased onto #5561

This branch is rebased onto #5561, which landed the two Serve-path prerequisites this
reroute would otherwise have had to carry, so they are intentionally not in this diff:

  • Composite-tool workflow telemetry (toolhive_vmcp_workflow_*) now lives in the core
    (core_telemetry.go's telemetryComposer), reproducing the metrics the now-bypassed
    session-layer composite decorator emitted.
  • Passthrough-header forwarding on the Serve path is resolved as per-request (Restore session-stable passthrough-header forwarding on the Serve path #5560):
    the shared backend client merges the headers captured by headerforward.CaptureMiddleware,
    and TestVMCPServer_PassthroughHeaders asserts the per-request contract. Forward passthrough headers on the Serve path #5561 also
    switched the integration harness (NewVMCPServer) to build the core via core.New + Serve
    directly, so both it and this rerouted server.New exercise the same production path.

Large PR Justification

This PR is one atomic refactor whose line count is dominated by tests, not production code:

  • The production change stays modest, and server.go shrinks. 13 non-test files change
    (+555 / −510), and server.go itself is a net reduction (+154 / −312, ≈158 lines
    removed) — the goal is shrinking the server.New god-object to a thin wrapper. The growth
    beyond the bare reroute is the two review-driven changes above (the caching aggregator
    decorator and moving the health monitor into the core), both small per-file deltas.
  • The bulk of the diff is tests (+1025 / −636). The behavioral-parity suite had to be
    reworked to exercise the live core.New + Serve path (it previously exercised the dead
    legacy chain), and review feedback added regression tests (Cedar-authz integration,
    late-bound elicitation, nil-aggregator, the construction-guard tests, the caching-decorator
    tests, and the relocated core health-monitor lifecycle tests). CLAUDE.md explicitly allows
    large PRs for test-heavy changes.
  • It is indivisible. The reroute flips server.New from the legacy inline path to
    core.New + Serve; a partial application would leave the parity suite red, so the complete
    switch is the smallest reviewable unit.
  • It is already split. Per P3.2 Reduce server.New body to the wrapper #5445's recommendation this is PR 1 of 2 (302a); the
    stacked follow-up (302b) removes the remaining now-dead discovery middleware and the factory
    aggregator. Folding 302b in here would make this PR larger, not smaller.

Type of change

  • Refactoring (no behavior change)

Test plan

  • Unit tests (task test) — full repo suite passes (exit 0)
  • Linting (task lint) — clean except a pre-existing, unrelated G115 in cmd/thv/app/upgrade.go (not touched by this PR)
  • Integration tests (go test ./test/integration/vmcp/...)
  • Race detector + no-cache (go test -race -count=1) across pkg/vmcp/server, pkg/vmcp/core, test/integration/vmcp — no data races

Regression verification

This is the highest-integration-risk PR in the epic: it retires the legacy path and routes
every caller through core.New + Serve, so the acceptance gate is behavioral parity.
The reroute decomposes into four links — wiring (derive*), core domain logic, Serve
transport, and end-to-end — each independently covered. Verification performed for this PR:

  • The parity suite now genuinely gates the live path. The pre-existing TestServe* /
    TestIntegration_* server-package suites and the test/integration/vmcp end-to-end suite
    previously exercised the dead-end legacy chain; after the reroute they traverse
    server.New → core.New + Serve for real — tools/list, tools/call, resources, prompts,
    composite workflows, optimizer mode, session lifecycle/termination, token binding, audit
    logging, telemetry, and the real-backend path.
  • The moved authorization boundary is covered end-to-end. Authz moved off the HTTP
    AuthzMiddleware onto the core admission seam (Config.Authz). Coverage spans the wiring
    (TestDeriveCoreConfigMapsAllFields asserts the exact *authz.Config reaches the core,
    plus the raw-server-name and nil→allow-all cases), the enforcement (core/admission_test.go
    with a real Cedar authorizer and annotation-gated policies), and the handler mapping
    (TestServeCoreToolHandler genericizes the denial and asserts the authorizer detail never
    leaks; TestServeToolCallTerminatesOnBindingFailure proves the fail-closed binding check).
    New TestIntegration_RealBackend_CedarAuthzGatesToolCall closes the integration gap:
    it boots server.New with a real Cedar policy over HTTP and proves both core gates — the
    list filter (an unpermitted tool is filtered out, unadvertised, and uncallable) and the
    call gate (an advertised tool's arg-gated call is denied with the generic message and no
    detail leak).
  • A coverage gap in the new code was found and closed. A function-coverage pass on the
    reroute showed New and buildHealthMonitor at 100% but
    lateBoundElicitationRequester.RequestElicitation (the new elicitation indirection every
    composite elicitation flows through) at 0%. New elicitation_latebound_test.go covers
    its not-bound guard, verbatim delegation, and concurrent bind/read under -race → 100%.
  • Blast radius is one in-tree caller (cli/serve.go, whose tests pass). The external
    brood-box embedder is the only out-of-tree caller — see the user-facing-change note below.

Does this introduce a user-facing change?

The Go signature of server.New is unchanged, but its runtime contract changed for
out-of-tree embedders
(notably the external brood-box embedder):

  • Config.Aggregator is now requiredcore.New fails without it, because the core is
    the single source of the advertised capability set.
  • Config.AuthzMiddleware is now vestigial on the New/Serve path: Serve never
    applies it. Authorization is enforced by the core admission seam from Config.Authz. An
    embedder that previously enforced Cedar authz only via AuthzMiddleware must set
    Config.Authz instead. Setting AuthzMiddleware without Authz is now a construction
    error
    (server.New returns vmcp.ErrInvalidConfig) rather than a logged warning, so the
    silent allow-all fails fast instead of degrading quietly.
  • Config.Authz combined with Config.OptimizerConfig is now rejected at construction
    (vmcp.ErrInvalidConfig): the optimizer meta-tools (find_tool/call_tool) have no Cedar
    representation, so the combination is mutually exclusive per the core admission-seam contract.

In-tree (cli/serve.go) both fields are set and the optimizer is not combined with authz, so
no behavior changes for the CLI or operator. The new construction errors require coordination
with the brood-box maintainers.

Special notes for reviewers

This is the highest-integration-risk PR in the epic: it is the moment the legacy path is
retired and every request flows through the core.New + Serve split for real. The full
behavioral-parity suite is the acceptance gate.

PR 1 of a 2-PR split of #5445. The issue itself recommends the split because the full
change exceeds the 400 LOC / 10 file limit. This PR (302a) reroutes server.New and reworks
the parity suite. The stacked follow-up PR (302b) performs the now-safe dead-code removal —
nothing in 302b is reachable after this PR:

  • Physical deletion of the now-dead discovery HTTP middleware and its context seam (the
    authz and annotation-enrichment blocks were already deleted in this PR per review; discovery
    is entangled with the context-coupled router, so it stays for 302b).
  • Removing session.WithAggregator and the session factory's aggregator field
    (making the AC2 "factory must not aggregate" contract structural rather than a comment).
  • Removing the now-orphaned Aggregator.ProcessPreQueriedCapabilities.
  • Dropping cli/serve.go's WithAggregator(agg) factory wiring.

server.Config.AuthzMiddleware is intentionally kept as a vestigial input: the
cli/serve.go assignment stays (set alongside Config.Authz), the HTTP blocks that read it
were removed in this PR, and deriveServerConfig omits it so the Serve path never applies
it. Setting it without Config.Authz is now a construction error (see the user-facing change).

@github-actions github-actions Bot added the size/XL Extra large PR: 1000+ lines changed label Jun 17, 2026

@github-actions github-actions Bot 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.

Large PR Detected

This PR exceeds 1000 lines of changes and requires justification before it can be reviewed.

How to unblock this PR:

Add a section to your PR description with the following format:

## Large PR Justification

[Explain why this PR must be large, such as:]
- Generated code that cannot be split
- Large refactoring that must be atomic
- Multiple related changes that would break if separated
- Migration or data transformation

Alternative:

Consider splitting this PR into smaller, focused changes (< 1000 lines each) for easier review and reduced risk.

See our Contributing Guidelines for more details.


This review will be automatically dismissed once you add the justification section.

@tgrunnagle tgrunnagle force-pushed the vmcp-core-p3-2_issue_5445 branch from 15f7968 to ae70c0c Compare June 18, 2026 16:37
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Jun 18, 2026
@codecov

codecov Bot commented Jun 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.22148% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.92%. Comparing base (cbaa26b) to head (1fec9e4).
⚠️ Report is 13 commits behind head on main.

Files with missing lines Patch % Lines
pkg/vmcp/auth/factory/incoming.go 38.46% 6 Missing and 2 partials ⚠️
pkg/vmcp/cli/serve.go 0.00% 5 Missing ⚠️
pkg/vmcp/core/core_vmcp.go 84.37% 3 Missing and 2 partials ⚠️
pkg/vmcp/server/status_reporting.go 0.00% 1 Missing and 3 partials ⚠️
pkg/vmcp/aggregator/caching_aggregator.go 95.00% 1 Missing and 1 partial ⚠️
pkg/vmcp/server/server.go 97.29% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5556      +/-   ##
==========================================
- Coverage   69.97%   69.92%   -0.05%     
==========================================
  Files         653      660       +7     
  Lines       66489    66805     +316     
==========================================
+ Hits        46524    46713     +189     
- Misses      16586    16724     +138     
+ Partials     3379     3368      -11     

☔ 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 force-pushed the vmcp-core-p3-2_issue_5445 branch from ae70c0c to 497126e Compare June 22, 2026 16:14
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Jun 22, 2026
@tgrunnagle tgrunnagle force-pushed the vmcp-core-p3-2_issue_5445 branch from 497126e to 45bc869 Compare June 22, 2026 16:36
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Jun 22, 2026
server.New was the vMCP transport god-object: a ~210-line constructor that
inlined the mcp-go server, SDK hooks, two-phase session creation, the workflow
composer, telemetry decoration, and the health monitor. Phases 1-2 extracted a
stateless domain core (core.New) and a transport entry point (Serve) behind a
stable seam, but server.New still ran the legacy inline path. This reduces
server.New to the thin wrapper the RFC specifies, making New/Serve the single
live path for every caller.

What changed:
- server.New now builds the health monitor at the composition root (A2),
  assembles the core via core.New(deriveCoreConfig(...)) and hands it to
  Serve(deriveServerConfig(...)). The 7-param signature is unchanged.
- server.Config gains Aggregator and Authz fields (the only channel to feed the
  core under the frozen signature); cli/serve.go sets them, and a new
  authfactory.BuildAuthzConfig surfaces the *authz.Config the HTTP middleware is
  built from so the core admission seam enforces the same policy.
- Elicitation's construction-order inversion (the SDK adapter wraps the mcp-go
  server Serve builds) is resolved with a late-bound requester bound after Serve.
- The session factory config sets AdvertiseFromCore so the core is the single
  aggregator and the Serve-layer optimizer (not the factory decorator) advertises
  find_tool/call_tool.
- Removed New's //nolint:gocyclo, the dead validateWorkflows, and the now-orphaned
  router/backendClient/handlerFactory/capabilityAdapter Server fields.
- Reworked the server parity suite to exercise the core path: tools are sourced
  from a stub/real Aggregator and calls route through core.CallTool.

Rebased onto #5561, which landed the prerequisites this reroute depends on:
composite-tool workflow telemetry now lives in the core (core_telemetry.go's
telemetryComposer), and passthrough-header forwarding on the Serve path is
resolved as per-request (the shared backend client merges the captured headers).
The integration harness it ships builds the core via core.New + Serve directly,
so both it and this wrapper exercise the same path.

The now-dead authz/annotation/discovery middleware and the factory aggregator are
left in place (unreachable) and removed in a stacked follow-up PR.

Part of #5445.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tgrunnagle tgrunnagle force-pushed the vmcp-core-p3-2_issue_5445 branch from 45bc869 to 2b7a7fe Compare June 22, 2026 16:52
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Jun 22, 2026

@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 Consensus Review

Agents consulted: architecture-design, security, concurrency-performance, test-coverage, general-code-quality

Consensus Summary

# Finding Score Severity Action
F1 //nolint:gocyclo on Start() not removed per 302a AC 8/10 MEDIUM Fix
F2 AuthzMiddleware-without-Authz should be startup error, not WARN 7/10 MEDIUM Fix
F3 Goroutine leak if core.New succeeds but Serve fails 7/10 MEDIUM Fix
F5 Optimizer + Config.Authz combination has no guard 7/10 MEDIUM Fix
F6 No TestNew_NilAggregator_ReturnsError for newly required field 7/10 MEDIUM Fix

Overall

This PR correctly achieves its primary goal: server.New's body is now a thin composition root over core.New + Serve, and the behavioral-parity suite genuinely gates the live path for the first time. The late-bound elicitation requester is a clean, well-tested solution to the construction-order inversion. The Cedar authz DRY refactor and the new authz_integration_test.go are high-quality additions.

Five MEDIUM findings warrant attention before merge. Two carry security risk: the AuthzMiddleware-without-Authz guard degrades to allow-all with only a log warning (F2), and the Optimizer + Config.Authz combination lacks a construction-time mutual-exclusion check that the admission layer docs require (F5). One is a resource-management correctness issue in an error path: core.New's background state-store goroutine leaks if Serve subsequently fails (F3). One is a missed 302a acceptance-criteria item: the //nolint:gocyclo on (*Server).Start() was listed as a 302a deliverable in the split note but was not removed (F1). One is a missing test for the new required Config.Aggregator field (F6).

The intentionally-deferred 302b items (A1 dead-code cleanup, session.WithAggregator removal, ProcessPreQueriedCapabilities removal) are correctly scoped out. The AdvertiseFromCore: true guard for double-aggregation is the right interim approach, but should be explicitly verified before 302b lands.

F1: //nolint:gocyclo on Start() not removed

The split note for TASK-302a explicitly lists "remove the two //nolint:gocyclo" as a 302a deliverable. The directive on New was correctly removed. The one on (*Server).Start() at server.go:688 was not removed. Run task lint after removing it: if lint passes (the complexity moved to Serve), remove it; if it fires, the AC claim needs updating.


Generated with Claude Code

Comment thread pkg/vmcp/server/server.go
Comment thread pkg/vmcp/server/server.go Outdated
Comment thread pkg/vmcp/server/server.go
Comment thread pkg/vmcp/server/server_test.go
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Jun 22, 2026
Addresses #5556 review comments:
- MEDIUM pkg/vmcp/server/server.go (3455015667): F2 — return an error instead
  of WARN when AuthzMiddleware is set without Config.Authz, so the vestigial
  middleware can never silently degrade to allow-all on the New/Serve path.
- MEDIUM pkg/vmcp/server/server.go (3455015695): F5 — reject Config.Authz
  combined with Config.OptimizerConfig (ErrInvalidConfig) per the admission
  seam contract; optimizer meta-tools have no Cedar policy representation.
- MEDIUM pkg/vmcp/server/server.go (3455015686): F3 — close the core on a Serve
  failure so its state-store cleanup goroutine cannot leak (mirrors Serve's
  closeStorageOnErr guard).
- MEDIUM pkg/vmcp/server/server.go (F1, review body): remove the now-unneeded
  //nolint:gocyclo on (*Server).Start(); lint passes without it.
- MEDIUM pkg/vmcp/server/server_test.go (3455015705): F6 — add
  TestNew_NilAggregator_ReturnsError for the now-required Config.Aggregator.

Also adds error tests for the F2/F5 guards and updates
TestNewIgnoresVestigialAuthzMiddleware to set Config.Authz (now required
alongside AuthzMiddleware).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tgrunnagle

Copy link
Copy Markdown
Contributor Author

F1 addressed in f5cfa7b — removed the //nolint:gocyclo on (*Server).Start(). Confirmed task lint passes without it (the complexity moved to Serve).

@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Jun 22, 2026
@tgrunnagle tgrunnagle marked this pull request as ready for review June 22, 2026 20:25
@github-actions github-actions Bot dismissed their stale review June 22, 2026 20:25

Large PR justification has been provided. Thank you!

@github-actions

Copy link
Copy Markdown
Contributor

✅ Large PR justification has been provided. The size review has been dismissed and this PR can now proceed with normal review.

@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Jun 22, 2026

@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.

Two blockers (the per-call aggregation below, plus the health-ownership one inline on the monitor wiring), with inline suggestions, a nitpick, and a question. The reroute itself is sound — these are mostly about what flipping to the single live path activates.

blocker: per-call backend re-aggregation on the Serve path

First — apologies we didn't flag this earlier; it's hard to see until the path comes together, since the per-call aggregation only becomes live once server.New routes through the core.

This PR makes core.New + Serve the single live path, changing backend discovery from once-per-session to once-per-call: coreToolHandler (serve_handlers.go:196) → core.CallTool (core_calls.go:40) → aggregatedViewAggregateCapabilitiesQueryAllCapabilities, a full tools/list sweep of every backend on every tools/call (and resources/read). The legacy path swept once at initialize and routed calls through the session's cached routing table, so a session with M calls goes from ~1 sweep to ~M+1. The result-only parity suite won't surface this.

Possible solution: a transparent caching decorator wrapping the aggregator.Aggregator, applied at composition below the core — AggregateCapabilities serves a cached view on hit and only sweeps backends on miss/TTL-expiry. Core and Serve stay unchanged; no loopback to the server, and it needn't be session-scoped. Backend enumeration is identity-dependent, so the cache key MUST include identity — at the granularity that actually drives enumeration (subject + forwarded scopes/credential, not just the backend set). A backend-set-only key would serve one user's capability view to another. Keyed that way it is shared across a single user's sessions without leaking across users; a TTL bounds staleness, matching legacy's once-per-session freshness. (This memoizes the per-identity enumeration the call path already does correctly — same behavior, without the M+1 backend cost.)

nitpick: prefer deleting now-dead code in the same PR

Now that this PR routes every caller through Serve, the authz/annotation blocks in (*Server).Handler (~server.go:592) are dead — deriveServerConfig drops AuthzMiddleware, so the guard is always false. I would generally find it clearer to delete code in the same PR where it becomes unused, rather than leaving an always-false guard on main that reads as load-bearing until 302b (you already did this for New's own orphaned fields / validateWorkflows). Pure deletions of provably-dead code are cheap to review, so folding it in carries near-zero risk. Keeping 302a behavioral and 302b mechanical-deletion is a defensible call too — flagging the preference for next time, not asking to re-cut the split.

Comment thread pkg/vmcp/server/server.go Outdated
Comment thread pkg/vmcp/server/server.go
Comment thread pkg/vmcp/server/server.go
Comment thread pkg/vmcp/server/server.go Outdated
tgrunnagle and others added 3 commits June 23, 2026 08:31
Addresses #5556 review comments:
- pkg/vmcp/server/server.go (3456622160): A — fix stale New doc comment (it said a
  WARN is logged; the guard now returns ErrInvalidConfig).
- pkg/vmcp/server/server.go (3456622170): C — add a construction-root guard rejecting
  Config.Authz with an empty Config.Name, for a clearer error than core.New's deeper
  admission-seam message (which already enforces it). +TestNew_AuthzWithoutName_ReturnsError.
- pkg/vmcp/server/server.go (F, review body): delete the now-dead authz and
  annotation-enrichment blocks in (*Server).Handler (always false on the Serve path) and
  the orphaned annotation_enrichment.go + test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses #5556 review (body blocker — per-call re-aggregation):
- The reroute makes core.New + Serve the live path, where the core re-derives the
  advertised view on every call (it holds no per-session cache). Without caching, each
  tools/call or resources/read triggers a full tools/list sweep of every backend, turning
  a session's M calls into ~M+1 sweeps.
- Add aggregator.NewCachingAggregator: a transparent decorator wrapping the Aggregator
  below the core, memoizing AggregateCapabilities for a bounded TTL. The cache key is a
  SHA-256 of (subject, forwarded headers, backend IDs) so a caller's capability view is
  never served to another; errors are not cached; expired entries are evicted on miss; a
  nil/zero-TTL config disables it rather than masking the nil-aggregator check.
- server.New wraps Config.Aggregator with it (capabilityCacheTTL = 30s); core and Serve
  are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses #5556 review (comment 3456622182, blocker):
- The backend health monitor was owned by three places: server.New built it, Serve
  Start/Stopped it, and it was injected into both the core and Serve. Connecting to and
  tracking downstream backends is a domain concern the core already neighbors (registry,
  client, aggregation, and the capability filtering that already consumes health).
- core.New now builds and starts the monitor from cfg.HealthMonitorConfig, filters
  capabilities with it (as before), stops it in Close, and exposes it via a new
  BackendHealth() health.Reporter accessor.
- server.New/Serve no longer build, start, stop, or inject the monitor: buildHealthMonitor,
  the Server.healthMonitor/healthMonitorMu fields, ServerConfig.HealthMonitor, the typed-nil
  StatusProvider dance, and the deriveCoreConfig/deriveServerConfig health params are removed.
  The /status, /api/backends/health, and periodic status reporter read the monitor back
  through core.BackendHealth() via a thin (*Server).backendHealth() helper.
- This reverses the #5443 split that homed the monitor's Start/Stop in Serve, giving the
  monitor one owner whose lifecycle is the core's (Close, already driven by Server.Stop).
- Adds health.Reporter; relocates the Serve-path monitor-lifecycle tests to core-level tests
  (TestNew_HealthMonitorOwnedByCore / _DisabledWhenNil) and reworks the derive/health/status
  tests for the new wiring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tgrunnagle

Copy link
Copy Markdown
Contributor Author

Per-call re-aggregation (body blocker) addressed in b8dcc8c — added aggregator.NewCachingAggregator, a transparent decorator wrapping the Aggregator below the core. It memoizes AggregateCapabilities for a TTL (30s), keyed on a SHA-256 of (subject, forwarded headers, backend IDs) so one caller's capability view is never served to another; errors aren't cached; expired entries evict on miss. server.New wraps Config.Aggregator with it. Core and Serve unchanged.

@tgrunnagle

Copy link
Copy Markdown
Contributor Author

Dead authz/annotation Handler blocks (nitpick) folded into 1591269 — deleted both AuthzMiddleware != nil blocks in (*Server).Handler and the now-orphaned annotation_enrichment.go. (Backend-enrichment + discovery remain for 302b: the former is a guarded no-op rather than always-false dead code, and discovery is entangled with the context-coupled router.)

@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Jun 23, 2026
jerm-dro
jerm-dro previously approved these changes Jun 23, 2026

@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.

Approving. Non-blocking follow-up: the entire s.core == nil legacy path is now unreachable dead codeServe is the sole *Server constructor and always sets the core — so 302b should delete all of it (discovery middleware, backend_enrichment, GetAdaptedTools, and the session-factory aggregation/composite/telemetry mirrors), not just the annotation_enrichment removed in this PR.

The caching aggregator from the prior commit hand-rolled its own map + mutex + TTL
eviction. The repo already depends directly on github.com/hashicorp/golang-lru/v2 (used by
pkg/cache), so leverage it instead of reinventing the cache mechanics.

- Replace the hand-rolled map/mutex/write-time-sweep with lru.Cache, which provides
  thread-safety, LRU eviction, and a hard size bound (capabilityCacheMaxEntries) — the
  size cap the previous implementation lacked.
- Use the base (non-expirable) LRU plus a lazy TTL check on read, deliberately avoiding the
  expirable variant: it runs a perpetual background cleanup goroutine with no Stop, which a
  per-server cache would leak (the test suite runs under goleak).
- Behavior, the identity-keyed cache key, and the public NewCachingAggregator surface are
  unchanged; the existing decorator tests pass as-is.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added size/XL Extra large PR: 1000+ lines changed and removed size/XL Extra large PR: 1000+ lines changed labels Jun 23, 2026
@tgrunnagle tgrunnagle linked an issue Jun 23, 2026 that may be closed by this pull request
28 tasks
@tgrunnagle tgrunnagle merged commit ee29dff into main Jun 23, 2026
75 of 76 checks passed
@tgrunnagle tgrunnagle deleted the vmcp-core-p3-2_issue_5445 branch June 23, 2026 21:23
@github-actions github-actions Bot mentioned this pull request Jun 23, 2026
2 tasks
tgrunnagle added a commit that referenced this pull request Jun 24, 2026
Now that #5445 (PR #5556) merged, Serve is the sole *Server constructor and always sets the
core, so the legacy `s.core == nil` session path is unreachable dead code. Stage 1 of the
dead-code removal tracked in #5621:

- handleSessionRegistrationImpl: drop the `if s.core != nil` conditional and the legacy
  GetAdaptedTools/GetAdaptedResources registration tail — registration is now unconditionally
  injectCoreSessionCapabilities.
- lazyInjectSessionTools: re-derive via serveSessionTools unconditionally (drop the
  GetAdaptedTools fallback).
- Delete backend_enrichment.go (+test): withBackendEnrichment was guarded to s.core == nil,
  a no-op on the Serve path.
- Delete the now-orphaned Manager.GetAdaptedTools/GetAdaptedResources/GetAdaptedPrompts and
  the two SessionManager interface methods. GetAdaptedPrompts was never wired (prompts
  unsupported), so it was already dead.

The discovery middleware + context seam, the session-factory aggregation mirrors, and the
server.New signature change are deferred to the follow-up stages in #5621.

Part of #5621.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Extra large PR: 1000+ lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

P3.2 Reduce server.New body to the wrapper

2 participants