stacklok / stacklok/toolhive

vMCP: Implement backend client pooling to reuse MCP connections within sessions

Open
#2,417 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

api enhancement go
Dominant language
Go
Stars
2.2k
Forks
300
Avg merge
1d 15h
Merged PRs (30d)
184

Description

Problem

vMCP creates a fresh MCP client for every backend operation:

// pkg/vmcp/client/client.go:302-320
func (h *httpBackendClient) CallTool(...) {
    c, err := h.clientFactory(ctx, target)  // New client every time
    defer c.Close()
    
    initializeClient(ctx, c)                 // Repeat handshake
    result, err := c.CallTool(...)          // Single operation
}

Impact:

  • Repeats MCP initialization handshake on every operation
  • Violates MCP spec session lifecycle best practices
  • 50-80% unnecessary overhead (new connection + capability negotiation)
  • Loses backend server-side session state

Findings

1. We Already Have Session Management
  • pkg/transport/session/Manager tracks vMCP sessions with TTL cleanup
  • pkg/vmcp/server/session_adapter.go integrates with mark3labs SDK
  • ✅ Sessions work correctly for client-facing connections
2. mark3labs SDK Provides Session Context

The SDK injects session info into every handler's context:

import "github.com/mark3labs/mcp-go/server"

func handler(ctx context.Context, request mcp.CallToolRequest) {
    session := server.ClientSessionFromContext(ctx)
    sessionID := session.SessionID()  // Available in every handler!
}
3. Backend Clients Are Always Local

MCP clients contain TCP connections and goroutines - they cannot be serialized. They must live in-memory even if session metadata is in Redis.

Proposed Solution

Create a BackendClientPool that:

  1. Maps sessionID → (backendID → *client.Client)
  2. Reuses initialized clients within a vMCP session
  3. Cleans up when vMCP session expires/terminates
  4. Lives in-memory on each vMCP instance
Architecture
vMCP Session (tracked by session.Manager)
    │
    ├─ Backend Client Pool (NEW)
    │   ├─ Backend A Client (reused)
    │   ├─ Backend B Client (reused)
    │   └─ Backend C Client (reused)
    │
    └─ Cleanup on session termination
Implementation Sketch
// pkg/vmcp/client/pool.go
type BackendClientPool struct {
    clients map[string]map[string]*client.Client  // sessionID → backendID → Client
    mu      sync.RWMutex
}

func (p *BackendClientPool) GetOrCreateClient(
    ctx context.Context,
    sessionID string,
    target *vmcp.BackendTarget,
) (*client.Client, error) {
    // Check pool first
    if client := p.getFromPool(sessionID, target.WorkloadID); client != nil {
        return client, nil  // Reuse!
    }
    
    // Create, initialize, and cache
    client := p.factory(ctx, target)
    initializeClient(ctx, client)
    p.storeInPool(sessionID, target.WorkloadID, client)
    return client, nil
}

func (p *BackendClientPool) CleanupSession(ctx context.Context, sessionID string) error {
    // Close all backend clients for this session
}
Integration Points

1. Extract session ID in handlers:

// pkg/vmcp/server/server.go
func (s *Server) createToolHandler(toolName string) func(...) {
    return func(ctx context.Context, request mcp.CallToolRequest) {
        sessionID := extractSessionID(ctx)  // From SDK context
        target, _ := s.router.RouteTool(ctx, toolName)
        
        // Use pooled client
        client, _ := s.backendClientPool.GetOrCreateClient(ctx, sessionID, target)
        result, _ := client.CallTool(...)  // No initialization needed!
    }
}

2. Cleanup on session termination:

// pkg/vmcp/server/session_adapter.go
func (a *sessionIDAdapter) Terminate(sessionID string) (bool, error) {
    // ... existing logic ...
    
    // Cleanup backend clients
    a.backendClientPool.CleanupSession(context.Background(), sessionID)
    return false, nil
}

Benefits

  • 50-80% fewer backend initialization calls
  • 20-40% latency reduction for operations within a session
  • MCP spec compliant session lifecycle
  • Preserves backend state across operations
  • Simple implementation - reuses existing session infrastructure

Files to Create/Modify

New:

  • pkg/vmcp/client/pool.go
  • pkg/vmcp/client/pool_test.go

Modified:

  • pkg/vmcp/server/server.go - Wire pool, extract session ID
  • pkg/vmcp/server/session_adapter.go - Add cleanup hook
  • pkg/vmcp/client/client.go - Use pool instead of one-shot clients

Future: Multi-Instance with Redis

When Redis session storage is added, the pool stays in-memory (connections can't be serialized):

Redis: Session metadata only {id, timestamps}
         │
    ┌────┼────┐
    ▼    ▼    ▼
 vMCP-1 vMCP-2 vMCP-3
    │    │    │
    └─ BackendClientPool (local on each instance)

If a request moves to a different instance, clients are recreated (acceptable one-time cost).

Open Questions

  1. Does session.Manager support expiration callbacks? (May need to add for pool cleanup)
  2. Should ListCapabilities use pooling? (Some backends require auth for discovery)
  3. Fallback strategy if sessionID is empty? (Create one-shot client for backwards compatibility)

Effort: 2-3 days
Priority: High (performance + MCP compliance)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with pkg/vmcp/client/client.go to trace one-shot client creation and initialization, then read pkg/vmcp/server/server.go and session_adapter.go for handler and termination integration. Use the proposed pool_test.go to cover reuse and cleanup; done means initialized backend clients are reused within a session and closed when that session terminates.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
backend
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.