1/**
2 * Stateful lifecycle storage for MCP revisions before v2026-07-28.
3 *
4 * @internal
5 */
6import * as Effect from "../../Effect.ts"
7import type * as Headers from "../../http/Headers.ts"
8import * as LogLevel from "../../LogLevel.ts"
9import type * as PublicMcpProtocol from "../McpProtocol.ts"
10import * as PublicMcpSchema from "../McpSchema.ts"
11import type * as McpCore from "./mcpCore.ts"
12import * as McpProtocol from "./mcpProtocol.ts"
13
14const MCP_SESSION_ID_HEADER = "mcp-session-id"
15
16type SessionLogLevel =
17 | { readonly _tag: "Effect"; readonly level: LogLevel.LogLevel }
18 | { readonly _tag: "Mcp"; readonly level: PublicMcpSchema.LoggingLevel }
19
20/**
21 * @internal
22 */
23export interface Binding {
24 readonly initializePayload: typeof PublicMcpSchema.Initialize.payloadSchema.Type
25 readonly negotiatedProfile: McpCore.NegotiatedProtocolProfile
26 readonly protocol: PublicMcpProtocol.ProtocolAdapter
27}
28
29interface Session extends Binding {
30 readonly resourceSubscriptions: Set<string> | undefined
31 logLevel: SessionLogLevel
32}
33
34/**
35 * @internal
36 */
37export interface Registration extends Binding {
38 readonly supportsResourceSubscriptions: boolean
39 readonly logLevel: LogLevel.LogLevel
40}
41
42/**
43 * @internal
44 */
45export interface StatefulRuntime {
46 readonly registerHttp: (sessionId: string, registration: Registration) => Binding
47 readonly registerConnection: (clientId: number, registration: Registration) => Binding
48 readonly resolve: (clientId: number, headers: Headers.Headers) => Binding | undefined
49 readonly resolveSessionId: (sessionId: string) => Binding | undefined
50 readonly setLogLevel: (
51 level: PublicMcpSchema.LoggingLevel,
52 clientId: number,
53 headers: Headers.Headers
54 ) => Effect.Effect<void>
55 readonly subscribe: (
56 uri: string,
57 clientId: number,
58 headers: Headers.Headers
59 ) => Effect.Effect<void, McpProtocol.ProtocolError>
60 readonly unsubscribe: (
61 uri: string,
62 clientId: number,
63 headers: Headers.Headers
64 ) => Effect.Effect<void, McpProtocol.ProtocolError>
65 readonly canDeliver: (
66 clientId: number,
67 headers: Headers.Headers,
68 notification: McpCore.ServerNotification,
69 fallbackLogLevel: LogLevel.LogLevel
70 ) => boolean
71 readonly effectLogLevel: (
72 clientId: number,
73 headers: Headers.Headers,
74 fallback: LogLevel.LogLevel
75 ) => LogLevel.LogLevel
76 readonly markInitialized: (clientId: number) => void
77 readonly initializedClientIds: () => Iterable<number>
78 readonly disconnect: (clientId: number) => void
79}
80
81const makeSession = (registration: Registration): Session => ({
82 initializePayload: registration.initializePayload,
83 negotiatedProfile: registration.negotiatedProfile,
84 protocol: registration.protocol,
85 resourceSubscriptions: registration.supportsResourceSubscriptions ? new Set() : undefined,
86 logLevel: { _tag: "Effect", level: registration.logLevel }
87})
88
89/**
90 * @internal
91 */
92export const make = (): StatefulRuntime => {
93 const bySessionId = new Map<string, Session>()
94 const byClientId = new Map<number, Session>()
95 const initializedClientIds = new Set<number>()
96
97 const resolveSession = (clientId: number, headers: Headers.Headers): Session | undefined => {
98 const sessionId = headers[MCP_SESSION_ID_HEADER]
99 return sessionId === undefined ? byClientId.get(clientId) : bySessionId.get(sessionId)
100 }
101
102 const effectLogLevel = (
103 clientId: number,
104 headers: Headers.Headers,
105 fallback: LogLevel.LogLevel
106 ): LogLevel.LogLevel => {
107 const session = resolveSession(clientId, headers)
108 return session?.logLevel._tag === "Mcp"
109 ? McpProtocol.mcpLogLevels[session.logLevel.level].effect
110 : session?.logLevel.level ?? fallback
111 }
112
113 return {
114 registerHttp: (sessionId, registration) => {
115 const session = makeSession(registration)
116 bySessionId.set(sessionId, session)
117 return session
118 },
119 registerConnection: (clientId, registration) => {
120 const session = makeSession(registration)
121 byClientId.set(clientId, session)
122 return session
123 },
124 resolve: resolveSession,
125 resolveSessionId: (sessionId) => bySessionId.get(sessionId),
126 setLogLevel: (level, clientId, headers) =>
127 Effect.sync(() => {
128 const session = resolveSession(clientId, headers)
129 if (session !== undefined) {
130 session.logLevel = { _tag: "Mcp", level }
131 }
132 }),
133 subscribe: (uri, clientId, headers) => {
134 const subscriptions = resolveSession(clientId, headers)?.resourceSubscriptions
135 if (subscriptions === undefined) {
136 return Effect.fail(
137 new McpProtocol.ProtocolError({
138 code: PublicMcpSchema.METHOD_NOT_FOUND_ERROR_CODE,
139 message: "Resource subscriptions are not supported"
140 })
141 )
142 }
143 return Effect.sync(() => subscriptions.add(uri)).pipe(Effect.asVoid)
144 },
145 unsubscribe: (uri, clientId, headers) => {
146 const subscriptions = resolveSession(clientId, headers)?.resourceSubscriptions
147 if (subscriptions === undefined) {
148 return Effect.fail(
149 new McpProtocol.ProtocolError({
150 code: PublicMcpSchema.METHOD_NOT_FOUND_ERROR_CODE,
151 message: "Resource subscriptions are not supported"
152 })
153 )
154 }
155 return Effect.sync(() => subscriptions.delete(uri)).pipe(Effect.asVoid)
156 },
157 effectLogLevel,
158 canDeliver: (clientId, headers, notification, fallbackLogLevel) => {
159 const session = resolveSession(clientId, headers)
160 if (notification._tag === "LoggingMessage") {
161 const minimum = session?.logLevel
162 return minimum?._tag === "Mcp"
163 ? McpProtocol.mcpLogLevels[notification.level].order >= McpProtocol.mcpLogLevels[minimum.level].order
164 : LogLevel.isGreaterThanOrEqualTo(
165 McpProtocol.mcpLogLevels[notification.level].effect,
166 minimum?.level ?? fallbackLogLevel
167 )
168 }
169 return notification._tag !== "ResourceUpdated" || session?.resourceSubscriptions?.has(notification.uri) === true
170 },
171 markInitialized: (clientId) => {
172 initializedClientIds.add(clientId)
173 },
174 initializedClientIds: () => initializedClientIds.values(),
175 disconnect: (clientId) => {
176 byClientId.delete(clientId)
177 initializedClientIds.delete(clientId)
178 }
179 }
180}