11using ModelContextProtocol . AspNetCore . Stateless ;
22using ModelContextProtocol . Protocol ;
33using ModelContextProtocol . Server ;
4+ using System . Diagnostics ;
45using System . Security . Claims ;
6+ using System . Threading ;
57
68namespace ModelContextProtocol . AspNetCore ;
79
810internal sealed class HttpMcpSession < TTransport > (
911 string sessionId ,
1012 TTransport transport ,
1113 UserIdClaim ? userId ,
12- TimeProvider timeProvider ) : IAsyncDisposable
14+ TimeProvider timeProvider ,
15+ SemaphoreSlim ? idleSessionSemaphore = null ) : IAsyncDisposable
1316 where TTransport : ITransport
1417{
1518 private int _referenceCount ;
1619 private int _getRequestStarted ;
17- private CancellationTokenSource _disposeCts = new ( ) ;
20+ private bool _isDisposed ;
21+
22+ private readonly SemaphoreSlim ? _idleSessionSemaphore = idleSessionSemaphore ;
23+ private readonly CancellationTokenSource _disposeCts = new ( ) ;
24+ private readonly object _referenceCountLock = new ( ) ;
1825
1926 public string Id { get ; } = sessionId ;
2027 public TTransport Transport { get ; } = transport ;
@@ -30,16 +37,39 @@ internal sealed class HttpMcpSession<TTransport>(
3037 public IMcpServer ? Server { get ; set ; }
3138 public Task ? ServerRunTask { get ; set ; }
3239
33- public IDisposable AcquireReference ( )
40+ public IAsyncDisposable AcquireReference ( )
3441 {
35- Interlocked . Increment ( ref _referenceCount ) ;
42+ Debug . Assert ( _idleSessionSemaphore is not null , "Only StreamableHttpHandler should call AcquireReference." ) ;
43+
44+ lock ( _referenceCountLock )
45+ {
46+ if ( ! _isDisposed && ++ _referenceCount == 1 )
47+ {
48+ // Non-idle sessions should not prevent session creation.
49+ _idleSessionSemaphore . Release ( ) ;
50+ }
51+ }
52+
3653 return new UnreferenceDisposable ( this ) ;
3754 }
3855
3956 public bool TryStartGetRequest ( ) => Interlocked . Exchange ( ref _getRequestStarted , 1 ) == 0 ;
4057
4158 public async ValueTask DisposeAsync ( )
4259 {
60+ bool shouldReleaseIdleSessionSemaphore ;
61+
62+ lock ( _referenceCountLock )
63+ {
64+ if ( _isDisposed )
65+ {
66+ return ;
67+ }
68+
69+ _isDisposed = true ;
70+ shouldReleaseIdleSessionSemaphore = _referenceCount == 0 ;
71+ }
72+
4373 try
4474 {
4575 await _disposeCts . CancelAsync ( ) ;
@@ -65,20 +95,39 @@ public async ValueTask DisposeAsync()
6595 {
6696 await Transport . DisposeAsync ( ) ;
6797 _disposeCts . Dispose ( ) ;
98+
99+ // If the session was disposed while it was inactive, we need to release the semaphore
100+ // to allow new sessions to be created.
101+ if ( _idleSessionSemaphore is not null && shouldReleaseIdleSessionSemaphore )
102+ {
103+ _idleSessionSemaphore . Release ( ) ;
104+ }
68105 }
69106 }
70107 }
71108
72- public bool HasSameUserId ( ClaimsPrincipal user )
73- => UserIdClaim == StreamableHttpHandler . GetUserIdClaim ( user ) ;
109+ public bool HasSameUserId ( ClaimsPrincipal user ) => UserIdClaim == StreamableHttpHandler . GetUserIdClaim ( user ) ;
74110
75- private sealed class UnreferenceDisposable ( HttpMcpSession < TTransport > session ) : IDisposable
111+ private sealed class UnreferenceDisposable ( HttpMcpSession < TTransport > session ) : IAsyncDisposable
76112 {
77- public void Dispose ( )
113+ public async ValueTask DisposeAsync ( )
78114 {
79- if ( Interlocked . Decrement ( ref session . _referenceCount ) == 0 )
115+ Debug . Assert ( session . _idleSessionSemaphore is not null , "Only StreamableHttpHandler should call AcquireReference." ) ;
116+
117+ bool shouldMarkSessionIdle ;
118+
119+ lock ( session . _referenceCountLock )
120+ {
121+ shouldMarkSessionIdle = ! session . _isDisposed && -- session . _referenceCount == 0 ;
122+ }
123+
124+ if ( shouldMarkSessionIdle )
80125 {
81126 session . LastActivityTicks = session . TimeProvider . GetTimestamp ( ) ;
127+
128+ // Acquire semaphore when session becomes inactive (reference count goes to 0) to slow
129+ // down session creation until idle sessions are disposed by the background service.
130+ await session . _idleSessionSemaphore . WaitAsync ( ) ;
82131 }
83132 }
84133 }
0 commit comments