Documentation
¶
Overview ¶
Package sessionmanager provides session lifecycle management.
This package implements the two-phase session creation pattern that bridges the MCP SDK's session management with the vMCP server's backend lifecycle:
- Phase 1 (Generate): Creates a placeholder session with no context
- Phase 2 (CreateSession): Replaces placeholder with fully-initialized MultiSession
The Manager type implements the server.SessionManager interface and is used by the server package.
Index ¶
- Constants
- Variables
- type FactoryConfig
- type Manager
- func (sm *Manager) CreateSession(ctx context.Context, sessionID string) (vmcpsession.MultiSession, error)
- func (sm *Manager) DecorateSession(sessionID string, fn func(sessiontypes.MultiSession) sessiontypes.MultiSession) error
- func (sm *Manager) Generate() string
- func (sm *Manager) GetAdaptedPrompts(sessionID string) ([]mcpserver.ServerPrompt, error)
- func (sm *Manager) GetAdaptedResources(sessionID string) ([]mcpserver.ServerResource, error)
- func (sm *Manager) GetAdaptedTools(sessionID string) ([]mcpserver.ServerTool, error)
- func (sm *Manager) GetMultiSession(sessionID string) (vmcpsession.MultiSession, bool)
- func (sm *Manager) Terminate(sessionID string) (isNotAllowed bool, err error)
- func (sm *Manager) Validate(sessionID string) (isTerminated bool, err error)
- type RestorableCache
Constants ¶
const ( // MetadataKeyTerminated is the session metadata key that marks a placeholder // session as explicitly terminated by the client. MetadataKeyTerminated = "terminated" // MetadataValTrue is the string value stored under MetadataKeyTerminated // when a session has been terminated. MetadataValTrue = "true" )
Variables ¶
var ErrExpired = errors.New("cache entry expired")
ErrExpired is returned by the check function passed to newRestorableCache to signal that a cached entry has definitively expired and should be evicted.
Functions ¶
This section is empty.
Types ¶
type FactoryConfig ¶ added in v0.12.5
type FactoryConfig struct {
// Base is the underlying session factory. Required.
Base vmcpsession.MultiSessionFactory
// WorkflowDefs are the composite tool workflow definitions.
// If empty, composite tool decoration is skipped.
WorkflowDefs map[string]*composer.WorkflowDefinition
// ComposerFactory builds a per-session composer bound to the session's
// routing table and tool list.
ComposerFactory func(rt *vmcp.RoutingTable, tools []vmcp.Tool) composer.Composer
// OptimizerConfig is optional optimizer configuration.
// When non-nil and OptimizerFactory is nil, New() creates the optimizer
// factory from this config and returns a cleanup function.
OptimizerConfig *optimizer.Config
// OptimizerFactory is an optional pre-built optimizer factory.
// If set, takes precedence over OptimizerConfig.
// If nil and OptimizerConfig is also nil, the optimizer is disabled.
OptimizerFactory func(context.Context, []mcpserver.ServerTool) (optimizer.Optimizer, error)
// TelemetryProvider is the optional telemetry provider.
// If non-nil, the optimizer factory (whether derived from OptimizerConfig or
// supplied via OptimizerFactory) and workflow executors are wrapped with telemetry.
TelemetryProvider *telemetry.Provider
}
FactoryConfig holds the session factory construction parameters that the session manager needs to build its decorating factory. It is separate from server.Config to avoid a circular import between the server and sessionmanager packages.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager bridges the domain session lifecycle (MultiSession / MultiSessionFactory) to the mark3labs SDK's SessionIdManager interface.
It implements a two-phase session-creation pattern:
- Generate(): called by SDK during initialize without context; stores an empty placeholder via storage.
- CreateSession(): called from OnRegisterSession hook once context is available; calls factory.MakeSessionWithID(), then persists the session metadata to storage.
Storage split ¶
MultiSession holds live in-process state (backend HTTP connections, routing table) that cannot be serialized or recovered across processes. A separate in-process multiSessions map holds the authoritative MultiSession reference for this pod. The pluggable SessionDataStorage (LocalSessionDataStorage or RedisSessionDataStorage) carries only the lightweight, serialisable session metadata required for TTL management, Validate(), and cross-pod visibility.
Because MultiSession objects are node-local, horizontal scaling requires sticky routing when session-affinity is desired. When Redis is used as the session-storage backend the metadata is durable across pod restarts, and the live MultiSession can be re-created via factory.RestoreSession() on a cache miss.
func New ¶
func New( storage transportsession.DataStorage, cfg *FactoryConfig, backendRegistry vmcp.BackendRegistry, ) (*Manager, func(context.Context) error, error)
New creates a Manager backed by the given SessionDataStorage and backend registry. It builds the decorating session factory from cfg, wiring the optimizer and composite tool layers internally.
The returned cleanup function releases any resources allocated during construction (e.g. the optimizer's SQLite store). Callers must invoke it on shutdown. If no cleanup is needed, a no-op function is returned.
func (*Manager) CreateSession ¶
func (sm *Manager) CreateSession( ctx context.Context, sessionID string, ) (vmcpsession.MultiSession, error)
CreateSession is Phase 2 of the two-phase creation pattern.
It is called from the OnRegisterSession hook once the request context is available. It:
- Resolves the caller identity from the context.
- Lists available backends from the registry.
- Calls MultiSessionFactory.MakeSessionWithID() to build a fully-formed MultiSession (which opens real HTTP connections to each backend).
- Persists session metadata to storage and caches the live MultiSession in the node-local map.
The returned MultiSession can be retrieved later via GetMultiSession().
func (*Manager) DecorateSession ¶ added in v0.12.3
func (sm *Manager) DecorateSession(sessionID string, fn func(sessiontypes.MultiSession) sessiontypes.MultiSession) error
DecorateSession retrieves the MultiSession for sessionID, applies fn to it, and stores the result back. Returns an error if the session is not found or has not yet been upgraded from placeholder to MultiSession.
A re-check is performed immediately before storing to guard against a race with Terminate(): if the session is deleted between GetMultiSession and the store, the store would silently resurrect a terminated session. The re-check catches that window. A narrow TOCTOU gap remains between the re-check and the store, but its consequence is bounded: Terminate() already called Close() on the underlying MultiSession before deleting it, so any resurrected decorator wraps an already-closed session and will fail on first use rather than leaking backend connections.
func (*Manager) Generate ¶
Generate implements the SDK's SessionIdManager.Generate().
Phase 1 of the two-phase creation pattern: creates a unique session ID, stores an empty placeholder via storage, and returns the ID to the SDK. No context is available at this point.
The placeholder is replaced by CreateSession() in Phase 2 once context is available via the OnRegisterSession hook.
func (*Manager) GetAdaptedPrompts ¶ added in v0.15.0
func (sm *Manager) GetAdaptedPrompts(sessionID string) ([]mcpserver.ServerPrompt, error)
GetAdaptedPrompts returns SDK-format prompts for the given session, with handlers that delegate prompt requests directly to the session's GetPrompt() method.
func (*Manager) GetAdaptedResources ¶ added in v0.12.2
func (sm *Manager) GetAdaptedResources(sessionID string) ([]mcpserver.ServerResource, error)
GetAdaptedResources returns SDK-format resources for the given session, with handlers that delegate read requests directly to the session's ReadResource() method.
func (*Manager) GetAdaptedTools ¶
func (sm *Manager) GetAdaptedTools(sessionID string) ([]mcpserver.ServerTool, error)
GetAdaptedTools returns SDK-format tools for the given session, with handlers that delegate tool invocations directly to the session's CallTool() method.
When the session factory is configured with an aggregator (WithAggregator), tools are in their final resolved form — overrides and conflict resolution applied via ProcessPreQueriedCapabilities. Each handler passes the resolved tool name to CallTool, which translates it back to the original backend name via GetBackendCapabilityName.
Without an aggregator, raw backend tool names are used as-is (no overrides or conflict resolution applied).
func (*Manager) GetMultiSession ¶
func (sm *Manager) GetMultiSession(sessionID string) (vmcpsession.MultiSession, bool)
GetMultiSession retrieves the fully-formed MultiSession for a given SDK session ID. Returns (nil, false) if the session does not exist or has not yet been upgraded from placeholder to MultiSession.
On a cache hit, liveness is confirmed via storage.Load (which also refreshes the Redis TTL). On a cache miss, the session is restored from storage via factory.RestoreSession, enabling cross-pod session recovery when Redis is used as the storage backend.
Known limitation: GetMultiSession's signature is fixed by the MultiSessionGetter interface and carries no context. Both the liveness check and the restore path use context.Background() with per-operation timeouts (restoreStorageTimeout / restoreSessionTimeout), so they are bounded independently of any caller deadline. The caller's HTTP request cancellation cannot propagate here. TODO: add context propagation through MultiSessionGetter so the caller's deadline can further bound these operations.
func (*Manager) Terminate ¶
Terminate implements the SDK's SessionIdManager.Terminate().
The two session types are handled asymmetrically to prevent a race condition where client termination during the Phase 1→Phase 2 window could resurrect sessions with open backend connections:
MultiSession (Phase 2): Close() releases backend connections, then the session is deleted from storage immediately. After deletion Validate() returns (false, error) — the same response as "never existed". This is intentional: a terminated MultiSession has no resources to preserve, so immediate removal is cleaner than marking and waiting for TTL.
Placeholder (Phase 1): the session is marked terminated=true and left for TTL cleanup. This prevents CreateSession() from opening backend connections for an already-terminated session (see fast-fail check in CreateSession). The terminated flag also lets Validate() return (isTerminated=true, nil) during the window between termination and TTL expiry, allowing the SDK to distinguish "actively terminated" from "never existed".
Returns (isNotAllowed=false, nil) on success; client termination is always permitted.
func (*Manager) Validate ¶
Validate implements the SDK's SessionIdManager.Validate().
Returns (isTerminated=true, nil) for explicitly terminated sessions. Returns (false, error) for unknown sessions — per the SDK interface contract, a lookup failure is signalled via err, not via isTerminated. Returns (false, nil) for valid, active sessions.
type RestorableCache ¶ added in v0.15.0
type RestorableCache[K comparable, V any] struct { // contains filtered or unexported fields }
RestorableCache is a node-local write-through cache backed by a sync.Map, with singleflight-deduplicated restore on cache miss and lazy liveness validation on cache hit.
Type parameter K is the key type (must be comparable). Type parameter V is the cached value type.
Values are stored internally as any, which allows callers to place sentinel markers alongside V entries (e.g. a tombstone during teardown). Get performs a type assertion to V and treats non-V entries as "not found". Peek and Store expose raw any access for sentinel use.
func (*RestorableCache[K, V]) CompareAndSwap ¶ added in v0.15.0
func (c *RestorableCache[K, V]) CompareAndSwap(key K, old, replacement any) bool
CompareAndSwap atomically replaces the value stored under key from old to new. Both old and new may be any type, including sentinels.
func (*RestorableCache[K, V]) Delete ¶ added in v0.15.0
func (c *RestorableCache[K, V]) Delete(key K)
Delete removes key from the cache.
func (*RestorableCache[K, V]) Get ¶ added in v0.15.0
func (c *RestorableCache[K, V]) Get(key K) (V, bool)
Get returns the cached V value for key.
On a cache hit, check is run first: ErrExpired evicts the entry and returns (zero, false); transient errors return the cached value unchanged. Non-V values stored via Store (e.g. sentinels) return (zero, false) without triggering a restore.
On a cache miss, load is called under a singleflight group so at most one restore runs concurrently per key.
func (*RestorableCache[K, V]) Peek ¶ added in v0.15.0
func (c *RestorableCache[K, V]) Peek(key K) (any, bool)
Peek returns the raw value stored under key without type assertion, liveness check, or restore. Used for sentinel inspection.
func (*RestorableCache[K, V]) Store ¶ added in v0.15.0
func (c *RestorableCache[K, V]) Store(key K, value any)
Store sets key to value. value may be any type, including sentinel markers.