Skip to content

Move SDK hooks and two-phase session creation under Serve - #5471

Merged
tgrunnagle merged 3 commits into
mainfrom
vmcp-core-p2-2_issue_5440
Jun 10, 2026
Merged

tgrunnagle merged 3 commits into
mainfrom
vmcp-core-p2-2_issue_5440

Conversation

@tgrunnagle

@tgrunnagle tgrunnagle commented Jun 9, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This is step P2.2 of the vMCP New/Serve split (Phase 2). The mcp-go SDK hooks and the
two-phase session-creation wiring are SDK-lifecycle concerns that belong in the transport
layer of the New/Serve split, not in the core VMCP domain object — but they still lived in
server.New. This PR relocates them under Serve so they execute identically when Serve is
exercised directly, keeping the mcp-go two-phase dance (anti-pattern #5) isolated behind the
transport boundary and out of the core interface.

  • Why: Finish moving the SDK session layer behind Serve so the core VMCP object stays
    free of mcp-go session concepts (hooks, two-phase creation, cross-pod Redis re-hydration).
  • What: Move the three SDK hooks (OnRegisterSession, OnBeforeListTools,
    OnBeforeCallTool) and the session-creation wiring (transport session manager, session data
    storage, vMCP session manager) from server.New into Serve in pkg/vmcp/server/serve.go.
  • Scope: Purely additive. server.New keeps its own copy until Phase 3, so its signature
    and observable behavior are unchanged; the relocated callbacks and collaborators keep
    their existing shapes.

Closes #5440

Type of change

  • Refactoring (no behavior change)

Test plan

  • Unit tests (task test) — pkg/vmcp/server passes with -race
  • Linting — golangci-lint run ./pkg/vmcp/server/... reports 0 issues
  • Manual testing (describe below)

New Serve-level tests drive the relocated hooks and two-phase session creation through the
SDK lifecycle directly (mounting the Streamable HTTP server on the relocated mcpServer +
vmcpSessionMgr, bypassing the not-yet-relocated discovery middleware that #5441/#5442 own):

  • TestServeRegistersSessionHooks — an MCP initialize fires OnRegisterSession, which runs
    two-phase creation (MakeSessionWithID) and injects the per-session tools so a subsequent
    tools/list advertises them; the OnBeforeListTools hook runs and no-ops on the pod-local
    path.
  • TestServeLazyInjectsToolsForRehydratedSession — covers the cross-pod re-injection branch:
    with a session registered in the vMCP session manager, a fresh SDK ClientSession whose
    per-session tool store is empty (as on a second pod, where OnRegisterSession never fired)
    gets its tools re-injected by lazyInjectSessionTools; a populated store is left untouched.
  • TestServeReturnsErrorWhenSessionManagerConstructionFails — asserts Serve surfaces a
    sessionmanager.New failure that occurs after the session data storage is built (the path
    the closeStorageOnErr guard protects). It confirms the guarded path is reached; it does not
    directly observe Close() (the storage is built internally).
  • TestBuildSessionDataStorage / TestBuildSessionDataStorageRedis — provider selection:
    nil/empty/memory (case-insensitive) yields in-process storage; redis takes the Redis path
    reading THV_SESSION_REDIS_PASSWORD; unknown providers are rejected.

The ~1.2k-line HTTP session-management integration suite stays on server.New (unchanged) as
the behavioral-parity gate.

Note on repo-wide checks: task lint-fix has a pre-existing, unrelated gosec G115 failure
in cmd/thv/app/upgrade.go, and task test has a pre-existing, unrelated
Docker-dependent failure in pkg/mcp/server (TestBuildServerConfig "no available runtime
found"). Neither is caused by this change; the affected packages are untouched here.

Changes

File Change
pkg/vmcp/server/serve.go Relocate the three SDK hooks and two-phase session-creation wiring (transport session manager, buildSessionDataStorage with the closeStorageOnErr guard, vMCP session manager) into Serve; rework ServerConfig (see below).
pkg/vmcp/server/serve_session_test.go New: Serve-level coverage of the relocated hooks + two-phase creation driven through the SDK lifecycle, plus buildSessionDataStorage provider selection.
pkg/vmcp/server/serve_test.go Update existing Serve tests for the new ServerConfig shape (SessionManagerConfig + BackendRegistry); add nil-BackendRegistry validation case; register t.Cleanup teardown.

Implementation details

  • ServerConfig reshaped: added a pre-built *sessionmanager.FactoryConfig slot
    (SessionManagerConfig) and a shared BackendRegistry. The now-redundant standalone
    SessionFactory / OptimizerFactory / OptimizerConfig slots were folded into the
    FactoryConfig. Both new collaborators are validated and fail loudly with
    vmcp.ErrInvalidConfig when nil — BackendRegistry in particular, because the
    OnRegisterSession hook enumerates it inside a goroutine where the error would otherwise be
    swallowed.
  • Mechanical relocation: the hook callbacks and the *Server receiver methods
    (handleSessionRegistration, lazyInjectSessionTools) are unchanged — they already operate
    against s.vmcpSessionMgr and the SDK server.ClientSession. The hooks object is built up
    front (so it can be passed to NewMCPServer) but its callbacks are registered after srv is
    assembled, since they close over srv.
  • Resource-leak guard preserved: buildSessionDataStorage keeps the closeStorageOnErr
    defer so the storage's background cleanup goroutine is released on any error path out of
    Serve (pairs acquisition with release).
  • Boundary held: no mcp-go types cross the VMCP interface; the "fixed at initialize"
    capability set, identity binding, and the cross-pod Redis re-hydration path stay in
    Serve/session.

Does this introduce a user-facing change?

No. This relocates internal transport wiring within pkg/vmcp/server; server.New's signature
and observable behavior are unchanged.

Special notes for reviewers

@github-actions github-actions Bot added the size/M Medium PR: 300-599 lines changed label Jun 9, 2026
Base automatically changed from vmcp-core-p2-1_issue_5439 to main June 9, 2026 18:58
@codecov

codecov Bot commented Jun 9, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.17949% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.29%. Comparing base (c311dc0) to head (5c5103d).

Files with missing lines Patch % Lines
pkg/vmcp/server/serve.go 87.17% 3 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5471      +/-   ##
==========================================
+ Coverage   69.27%   69.29%   +0.02%     
==========================================
  Files         638      638              
  Lines       64904    64931      +27     
==========================================
+ Hits        44960    44992      +32     
+ Misses      16622    16612      -10     
- Partials     3322     3327       +5     

☔ 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 and others added 2 commits June 9, 2026 11:59
Step P2.2 of the vMCP New/Serve split: the three mcp-go SDK hooks and the
two-phase session-creation wiring are SDK-lifecycle concerns that belong in
the transport layer, not the core VMCP domain object. This relocates them
into Serve so they execute identically when Serve is exercised directly.
server.New keeps its own copy until Phase 3, so its signature and observable
behavior are unchanged.

Implements changes for issue #5440:
- Register OnRegisterSession + OnBeforeListTools/OnBeforeCallTool hooks in
  Serve, delegating to the unchanged *Server receiver methods
- Build the transport session manager, session data storage (memory/Redis,
  THV_SESSION_REDIS_PASSWORD) with a close-on-error leak guard, and the vMCP
  session manager inside Serve
- Add ServerConfig.SessionManagerConfig (pre-built *sessionmanager.FactoryConfig)
  and the shared BackendRegistry; fold the now-redundant SessionFactory/
  OptimizerFactory/OptimizerConfig slots into FactoryConfig
- Add Serve-level tests driving the relocated hooks + two-phase creation via
  the SDK lifecycle, plus buildSessionDataStorage coverage; the HTTP suite
  stays on server.New as its parity gate

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixed issues from code review:
- HIGH: Split the t.Setenv redis case into its own non-parallel test and made
  TestBuildSessionDataStorage parallel, resolving the tparallel lint failure
- MEDIUM: Validate BackendRegistry in Serve (fail loudly with ErrInvalidConfig
  instead of nil-panicking inside the OnRegisterSession hook); add a
  nil-registry validation case and a testMinimalServeConfig helper
- MEDIUM: Document on SessionManagerConfig that the composition root must
  validate workflow definitions before assembling FactoryConfig, since Serve
  no longer runs validateWorkflows

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tgrunnagle
tgrunnagle force-pushed the vmcp-core-p2-2_issue_5440 branch from 7b8d982 to 22452a6 Compare June 9, 2026 19:00
@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 9, 2026

@tgrunnagle tgrunnagle left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Multi-agent review — Move SDK hooks + two-phase session creation under Serve

Reviewed by 4 specialist agents (Go correctness, vMCP architecture, test coverage, general quality). Codex cross-review skipped (CLI not installed).

This is a faithful, behavior-preserving relocation. The block moved into Serve is a near-byte-for-byte copy of server.New (server.go:338–522), differing only by a safe ctx→hookCtx rename. The SDK boundary holds — zero mcp-go imports leak onto core.VMCP. ServerConfig reshape reduces config surface by reusing the existing FactoryConfig. All acceptance criteria in #5440 are met and scope is well under the 400 LOC / 10 file budget. On a normal PR this would be an APPROVE; left as a COMMENT because the PR is a draft (and GitHub disallows self-approval).

Findings

# Severity Finding Location
M1 MEDIUM validateWorkflows obligation moved to composition root with no enforcement seam serve.go:117
M2 MEDIUM Cross-pod re-injection path untested; PR description's "covered by integration suite" claim is inaccurate serve_session_test.go:103
L1 LOW sessionManager cleanup goroutine leaks on the Serve error path (asymmetric with the new storage guard) serve.go:206
L2 LOW toolSessionState.lastSession written under mutex but never read (dead state) serve_session_test.go:45
L3 LOW TestServeClosesStorageOnSessionManagerError name overstates — does not observe Close() serve_session_test.go:169
L4 LOW Redis test assertion not bound to the Redis branch serve_session_test.go:245

0 HIGH — no blocking issues. The two MEDIUM items are worth resolving (or consciously deferring) before this leaves draft: M2 is the only one really worth acting on in this PR — either add the missing test or correct the description. M1 and L1 are reasonable to track for Phase 3 (#5444) when the two wiring copies collapse. L2–L4 are minor test-quality nits. Details are in the inline comments.

Dropped below threshold (no action): a wording nit on the discovery.Middleware nil-deref comment (the named mechanism was independently verified accurate, just incomplete); a doc clarification that the telemetry provider is intentionally set in two places; and an optional .Times(1) hardening of the MakeSessionWithID mock.

Comment thread pkg/vmcp/server/serve.go
Comment thread pkg/vmcp/server/serve_session_test.go
Comment thread pkg/vmcp/server/serve.go Outdated
Comment thread pkg/vmcp/server/serve_session_test.go Outdated
Comment thread pkg/vmcp/server/serve_session_test.go Outdated
Comment thread pkg/vmcp/server/serve_session_test.go
Addresses #5471 review comments:
- LOW serve.go (3383233591): build the transport sessionManager after the
  fallible buildSessionDataStorage/sessionmanager.New calls so its cleanup
  goroutine cannot leak on a Serve error path (symmetry with the storage guard)
- MEDIUM serve_session_test.go (3383233579): add a Serve-level test covering
  the cross-pod empty-store inject branch of lazyInjectSessionTools (and the
  pod-local no-op branch), the path the prior tests did not reach
- LOW serve_session_test.go (3383233598): remove dead mu/lastSession test state
- LOW serve_session_test.go (3383233608): rename the storage-error test to
  match what it asserts (error returned when session manager construction fails)
- LOW serve_session_test.go (3383233616): bind the redis test assertion to the
  connection-failure wrap so an unrelated error can't satisfy it

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 size/L Large PR: 600-999 lines changed and removed size/M Medium PR: 300-599 lines changed labels Jun 9, 2026
@tgrunnagle
tgrunnagle marked this pull request as ready for review June 9, 2026 19:40
@github-actions github-actions Bot added size/L Large PR: 600-999 lines changed and removed size/L Large PR: 600-999 lines changed labels Jun 9, 2026
@tgrunnagle
tgrunnagle merged commit 1101e04 into main Jun 10, 2026
46 checks passed
@tgrunnagle
tgrunnagle deleted the vmcp-core-p2-2_issue_5440 branch June 10, 2026 15:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L Large PR: 600-999 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

P2.2 Move SDK hooks + two-phase session creation under Serve

2 participants