@@ -22,6 +22,11 @@ export interface McpIssuedCredential {
2222
2323export interface McpSessionRegistryShape {
2424 readonly issue : ( request : McpCredentialRequest ) => Effect . Effect < McpIssuedCredential > ;
25+ /** Atomically replace one thread credential only while its provider generation is current. */
26+ readonly issueIfCurrent : (
27+ request : McpCredentialRequest ,
28+ isCurrent : Effect . Effect < boolean > ,
29+ ) => Effect . Effect < McpIssuedCredential | undefined > ;
2530 readonly resolve : (
2631 rawToken : string ,
2732 ) => Effect . Effect < McpInvocationContext . McpInvocationScope | undefined > ;
@@ -117,26 +122,26 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (
117122 return next . size === records . size ? records : next ;
118123 } ;
119124
120- const issue : McpSessionRegistryShape [ "issue" ] = Effect . fn ( "McpSessionRegistry.issue" ) (
121- function * ( request ) {
122- const issuedAt = yield * currentTimeMillis ;
123- const providerSessionId = yield * crypto . randomUUIDv4 . pipe ( Effect . orDie ) ;
124- const rawToken = yield * crypto . randomBytes ( 32 ) . pipe ( Effect . map ( tokenFromBytes ) , Effect . orDie ) ;
125- const tokenHash = yield * hashToken ( rawToken ) ;
126- const scope : McpInvocationContext . McpInvocationScope = {
127- environmentId ,
128- threadId : ThreadId . make ( request . threadId ) ,
129- providerSessionId ,
130- providerInstanceId : ProviderInstanceId . make ( request . providerInstanceId ) ,
131- capabilities : new Set ( [ "preview" ] ) ,
132- issuedAt ,
133- } ;
134- yield * SynchronizedRef . update ( state , ( { records } ) => {
135- const next = new Map ( pruneDead ( records , issuedAt ) ) ;
136- next . set ( tokenHash , { tokenHash , scope , lastAliveAt : issuedAt } ) ;
137- return { records : next } ;
138- } ) ;
139- return {
125+ const prepareCredential = Effect . fn ( "McpSessionRegistry.prepareCredential" ) ( function * (
126+ request : McpCredentialRequest ,
127+ ) {
128+ const issuedAt = yield * currentTimeMillis ;
129+ const providerSessionId = yield * crypto . randomUUIDv4 . pipe ( Effect . orDie ) ;
130+ const rawToken = yield * crypto . randomBytes ( 32 ) . pipe ( Effect . map ( tokenFromBytes ) , Effect . orDie ) ;
131+ const tokenHash = yield * hashToken ( rawToken ) ;
132+ const scope : McpInvocationContext . McpInvocationScope = {
133+ environmentId ,
134+ threadId : ThreadId . make ( request . threadId ) ,
135+ providerSessionId ,
136+ providerInstanceId : ProviderInstanceId . make ( request . providerInstanceId ) ,
137+ capabilities : new Set ( [ "preview" ] ) ,
138+ issuedAt ,
139+ } ;
140+ return {
141+ issuedAt,
142+ tokenHash ,
143+ scope ,
144+ credential : {
140145 config : {
141146 environmentId,
142147 threadId : scope . threadId ,
@@ -145,10 +150,50 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (
145150 endpoint,
146151 authorizationHeader : `Bearer ${ rawToken } ` ,
147152 } ,
148- } ;
153+ } satisfies McpIssuedCredential ,
154+ } ;
155+ } ) ;
156+
157+ const issue : McpSessionRegistryShape [ "issue" ] = Effect . fn ( "McpSessionRegistry.issue" ) (
158+ function * ( request ) {
159+ const prepared = yield * prepareCredential ( request ) ;
160+ yield * SynchronizedRef . update ( state , ( { records } ) => {
161+ const next = new Map ( pruneDead ( records , prepared . issuedAt ) ) ;
162+ next . set ( prepared . tokenHash , {
163+ tokenHash : prepared . tokenHash ,
164+ scope : prepared . scope ,
165+ lastAliveAt : prepared . issuedAt ,
166+ } ) ;
167+ return { records : next } ;
168+ } ) ;
169+ return prepared . credential ;
149170 } ,
150171 ) ;
151172
173+ const issueIfCurrent : McpSessionRegistryShape [ "issueIfCurrent" ] = Effect . fn (
174+ "McpSessionRegistry.issueIfCurrent" ,
175+ ) ( function * ( request , isCurrent ) {
176+ const prepared = yield * prepareCredential ( request ) ;
177+ return yield * SynchronizedRef . modifyEffect ( state , ( { records } ) =>
178+ Effect . gen ( function * ( ) {
179+ // The generation check and replacement share the registry's single mutation permit.
180+ if ( ! ( yield * isCurrent ) ) return [ undefined , { records } ] as const ;
181+ const current = pruneDead ( records , prepared . issuedAt ) ;
182+ const next = new Map (
183+ Array . from ( current ) . filter (
184+ ( [ , record ] ) => record . scope . threadId !== prepared . scope . threadId ,
185+ ) ,
186+ ) ;
187+ next . set ( prepared . tokenHash , {
188+ tokenHash : prepared . tokenHash ,
189+ scope : prepared . scope ,
190+ lastAliveAt : prepared . issuedAt ,
191+ } ) ;
192+ return [ prepared . credential , { records : next } ] as const ;
193+ } ) ,
194+ ) ;
195+ } ) ;
196+
152197 const resolve : McpSessionRegistryShape [ "resolve" ] = Effect . fn ( "McpSessionRegistry.resolve" ) (
153198 function * ( rawToken ) {
154199 if ( rawToken . length === 0 ) return undefined ;
@@ -188,6 +233,7 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (
188233
189234 return McpSessionRegistry . of ( {
190235 issue,
236+ issueIfCurrent,
191237 resolve,
192238 touch,
193239 revokeProviderSession : Effect . fn ( "McpSessionRegistry.revokeProviderSession" ) (
@@ -224,13 +270,17 @@ export const layer = Layer.effect(McpSessionRegistry, make);
224270
225271export const issueActiveMcpCredential = (
226272 request : McpCredentialRequest ,
273+ isCurrent : Effect . Effect < boolean > = Effect . succeed ( true ) ,
227274) : Effect . Effect < McpIssuedCredential | undefined > =>
228275 activeMcpSessionRegistry
229- ? activeMcpSessionRegistry
230- . revokeThread ( request . threadId )
231- . pipe ( Effect . andThen ( activeMcpSessionRegistry . issue ( request ) ) )
276+ ? activeMcpSessionRegistry . issueIfCurrent ( request , isCurrent )
232277 : Effect . sync ( ( ) : McpIssuedCredential | undefined => undefined ) ;
233278
279+ export const revokeActiveMcpProviderSession = ( providerSessionId : string ) : Effect . Effect < void > =>
280+ activeMcpSessionRegistry
281+ ? activeMcpSessionRegistry . revokeProviderSession ( providerSessionId )
282+ : Effect . void ;
283+
234284/**
235285 * Refreshes the liveness of a thread's MCP credential. Called on every provider
236286 * turn so an active session is never mistaken for an abandoned one.
0 commit comments