(
opts: { ttlMs?: number } = {},
)
| 63 | * written, keeping the map bounded to threads active within the TTL window. |
| 64 | */ |
| 65 | export function inMemoryMcpSessionStore( |
| 66 | opts: { ttlMs?: number } = {}, |
| 67 | ): McpSessionStore { |
| 68 | const map = new Map< |
| 69 | string, |
| 70 | { at: number; servers: Record<string, McpServerDescriptor> } |
| 71 | >() |
| 72 | const ttl = opts.ttlMs ?? 30 * 60_000 |
| 73 | |
| 74 | return { |
| 75 | async set(threadId, servers) { |
| 76 | // Opportunistic sweep: reclaim every expired entry, not just this thread, |
| 77 | // so set-but-never-read threads can't accumulate unbounded. |
| 78 | const now = Date.now() |
| 79 | for (const [id, e] of map) { |
| 80 | if (now - e.at > ttl) map.delete(id) |
| 81 | } |
| 82 | map.set(threadId, { at: now, servers }) |
| 83 | }, |
| 84 | async get(threadId, serverId) { |
| 85 | const e = map.get(threadId) |
| 86 | if (!e || Date.now() - e.at > ttl) { |
| 87 | map.delete(threadId) |
| 88 | return null |
| 89 | } |
| 90 | // Sliding TTL: refresh on a successful hit so an actively-used thread |
| 91 | // doesn't expire by absolute time mid-session. |
| 92 | e.at = Date.now() |
| 93 | // serverId omitted (single-server setups): default to the sole server. |
| 94 | if (serverId === undefined) { |
| 95 | const entries = Object.entries(e.servers) |
| 96 | return entries.length === 1 ? (entries[0]?.[1] ?? null) : null |
| 97 | } |
| 98 | return e.servers[serverId] ?? null |
| 99 | }, |
| 100 | } |
| 101 | } |
no outgoing calls
no test coverage detected