-
Notifications
You must be signed in to change notification settings - Fork 689
Expand file tree
/
Copy pathHttpMcpSession.cs
More file actions
143 lines (118 loc) · 4.4 KB
/
HttpMcpSession.cs
File metadata and controls
143 lines (118 loc) · 4.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
using ModelContextProtocol.AspNetCore.Stateless;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.Diagnostics;
using System.Security.Claims;
using System.Threading;
namespace ModelContextProtocol.AspNetCore;
internal sealed class HttpMcpSession<TTransport>(
string sessionId,
TTransport transport,
UserIdClaim? userId,
TimeProvider timeProvider,
SemaphoreSlim? idleSessionSemaphore = null) : IAsyncDisposable
where TTransport : ITransport
{
private int _referenceCount;
private int _getRequestStarted;
private bool _isDisposed;
private readonly SemaphoreSlim? _idleSessionSemaphore = idleSessionSemaphore;
private readonly CancellationTokenSource _disposeCts = new();
private readonly object _referenceCountLock = new();
public string Id { get; } = sessionId;
public TTransport Transport { get; } = transport;
public UserIdClaim? UserIdClaim { get; } = userId;
public CancellationToken SessionClosed => _disposeCts.Token;
public bool IsActive => !SessionClosed.IsCancellationRequested && _referenceCount > 0;
public long LastActivityTicks { get; private set; } = timeProvider.GetTimestamp();
private TimeProvider TimeProvider => timeProvider;
public IMcpServer? Server { get; set; }
public Task? ServerRunTask { get; set; }
public IAsyncDisposable AcquireReference()
{
// We don't do idle tracking for stateless sessions, so we don't need to acquire a reference.
if (_idleSessionSemaphore is null)
{
return new NoopDisposable();
}
lock (_referenceCountLock)
{
if (!_isDisposed && ++_referenceCount == 1)
{
// Non-idle sessions should not prevent session creation.
_idleSessionSemaphore.Release();
}
}
return new UnreferenceDisposable(this);
}
public bool TryStartGetRequest() => Interlocked.Exchange(ref _getRequestStarted, 1) == 0;
public async ValueTask DisposeAsync()
{
bool shouldReleaseIdleSessionSemaphore;
lock (_referenceCountLock)
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
shouldReleaseIdleSessionSemaphore = _referenceCount == 0;
}
try
{
await _disposeCts.CancelAsync();
if (ServerRunTask is not null)
{
await ServerRunTask;
}
}
catch (OperationCanceledException)
{
}
finally
{
try
{
if (Server is not null)
{
await Server.DisposeAsync();
}
}
finally
{
await Transport.DisposeAsync();
_disposeCts.Dispose();
// If the session was disposed while it was inactive, we need to release the semaphore
// to allow new sessions to be created.
if (_idleSessionSemaphore is not null && shouldReleaseIdleSessionSemaphore)
{
_idleSessionSemaphore.Release();
}
}
}
}
public bool HasSameUserId(ClaimsPrincipal user) => UserIdClaim == StreamableHttpHandler.GetUserIdClaim(user);
private sealed class UnreferenceDisposable(HttpMcpSession<TTransport> session) : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
Debug.Assert(session._idleSessionSemaphore is not null, "Only StreamableHttpHandler should call AcquireReference.");
bool shouldMarkSessionIdle;
lock (session._referenceCountLock)
{
shouldMarkSessionIdle = !session._isDisposed && --session._referenceCount == 0;
}
if (shouldMarkSessionIdle)
{
session.LastActivityTicks = session.TimeProvider.GetTimestamp();
// Acquire semaphore when session becomes inactive (reference count goes to 0) to slow
// down session creation until idle sessions are disposed by the background service.
await session._idleSessionSemaphore.WaitAsync();
}
}
}
private sealed class NoopDisposable : IAsyncDisposable
{
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
}