forked from modelcontextprotocol/java-sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMcpServerSession.java
More file actions
398 lines (344 loc) · 14.2 KB
/
McpServerSession.java
File metadata and controls
398 lines (344 loc) · 14.2 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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
package io.modelcontextprotocol.spec;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import com.fasterxml.jackson.core.type.TypeReference;
import io.modelcontextprotocol.server.McpAsyncServerExchange;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoSink;
import reactor.core.publisher.Sinks;
/**
* Represents a Model Control Protocol (MCP) session on the server side. It manages
* bidirectional JSON-RPC communication with the client.
*/
public class McpServerSession implements McpSession {
private static final Logger logger = LoggerFactory.getLogger(McpServerSession.class);
private final ConcurrentHashMap<Object, MonoSink<McpSchema.JSONRPCResponse>> pendingResponses = new ConcurrentHashMap<>();
private final String id;
private final AtomicLong requestCounter = new AtomicLong(0);
private final InitRequestHandler initRequestHandler;
private final InitNotificationHandler initNotificationHandler;
private final Map<String, RequestHandler<?>> requestHandlers;
private final Map<String, NotificationHandler> notificationHandlers;
private final McpServerTransport transport;
private final Sinks.One<McpAsyncServerExchange> exchangeSink = Sinks.one();
private final AtomicReference<McpSchema.ClientCapabilities> clientCapabilities = new AtomicReference<>();
private final AtomicReference<McpSchema.Implementation> clientInfo = new AtomicReference<>();
private static final int STATE_UNINITIALIZED = 0;
private static final int STATE_INITIALIZING = 1;
private static final int STATE_INITIALIZED = 2;
private final AtomicInteger state = new AtomicInteger(STATE_UNINITIALIZED);
/**
* keyed by request ID, value is true if the request is being cancelled.
*/
private final Map<Object, Boolean> requestCancellation = new ConcurrentHashMap<>();
/**
* Creates a new server session with the given parameters and the transport to use.
* @param id session id
* @param transport the transport to use
* @param initHandler called when a
* {@link io.modelcontextprotocol.spec.McpSchema.InitializeRequest} is received by the
* server
* @param initNotificationHandler called when a
* {@link McpSchema.METHOD_NOTIFICATION_INITIALIZED} is received.
* @param requestHandlers map of request handlers to use
* @param notificationHandlers map of notification handlers to use
*/
public McpServerSession(String id, McpServerTransport transport, InitRequestHandler initHandler,
InitNotificationHandler initNotificationHandler, Map<String, RequestHandler<?>> requestHandlers,
Map<String, NotificationHandler> notificationHandlers) {
this.id = id;
this.transport = transport;
this.initRequestHandler = initHandler;
this.initNotificationHandler = initNotificationHandler;
this.requestHandlers = requestHandlers;
this.notificationHandlers = notificationHandlers;
}
/**
* Retrieve the session id.
* @return session id
*/
public String getId() {
return this.id;
}
/**
* Called upon successful initialization sequence between the client and the server
* with the client capabilities and information.
*
* <a href=
* "https://github.com/modelcontextprotocol/specification/blob/main/docs/specification/basic/lifecycle.md#initialization">Initialization
* Spec</a>
* @param clientCapabilities the capabilities the connected client provides
* @param clientInfo the information about the connected client
*/
public void init(McpSchema.ClientCapabilities clientCapabilities, McpSchema.Implementation clientInfo) {
this.clientCapabilities.lazySet(clientCapabilities);
this.clientInfo.lazySet(clientInfo);
}
private String generateRequestId() {
return this.id + "-" + this.requestCounter.getAndIncrement();
}
@Override
public <T> Mono<T> sendRequest(String method, Object requestParams, TypeReference<T> typeRef) {
String requestId = this.generateRequestId();
return Mono.<McpSchema.JSONRPCResponse>create(sink -> {
this.pendingResponses.put(requestId, sink);
McpSchema.JSONRPCRequest jsonrpcRequest = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, method,
requestId, requestParams);
this.transport.sendMessage(jsonrpcRequest).subscribe(v -> {
}, error -> {
this.pendingResponses.remove(requestId);
sink.error(error);
});
}).timeout(Duration.ofSeconds(10)).handle((jsonRpcResponse, sink) -> {
if (jsonRpcResponse.error() != null) {
sink.error(new McpError(jsonRpcResponse.error()));
}
else {
if (typeRef.getType().equals(Void.class)) {
sink.complete();
}
else {
sink.next(this.transport.unmarshalFrom(jsonRpcResponse.result(), typeRef));
}
}
});
}
@Override
public Mono<Void> sendNotification(String method, Object params) {
McpSchema.JSONRPCNotification jsonrpcNotification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION,
method, params);
return this.transport.sendMessage(jsonrpcNotification);
}
/**
* Called by the {@link McpServerTransportProvider} once the session is determined.
* The purpose of this method is to dispatch the message to an appropriate handler as
* specified by the MCP server implementation
* ({@link io.modelcontextprotocol.server.McpAsyncServer} or
* {@link io.modelcontextprotocol.server.McpSyncServer}) via
* {@link McpServerSession.Factory} that the server creates.
* @param message the incoming JSON-RPC message
* @return a Mono that completes when the message is processed
*/
public Mono<Void> handle(McpSchema.JSONRPCMessage message) {
return Mono.defer(() -> {
// TODO handle errors for communication to without initialization happening
// first
if (message instanceof McpSchema.JSONRPCResponse response) {
logger.debug("Received Response: {}", response);
var sink = pendingResponses.remove(response.id());
if (sink == null) {
logger.warn("Unexpected response for unknown id {}", response.id());
}
else {
sink.success(response);
}
return Mono.empty();
}
else if (message instanceof McpSchema.JSONRPCRequest request) {
logger.debug("Received request: {}", request);
requestCancellation.put(request.id(), false);
return handleIncomingRequest(request).onErrorResume(error -> {
var errorResponse = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null,
new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR,
error.getMessage(), null));
// TODO: Should the error go to SSE or back as POST return?
return this.transport.sendMessage(errorResponse)
.doFinally(signal -> requestCancellation.remove(request.id()))
.then(Mono.empty());
})
.flatMap(response -> this.transport.sendMessage(response)
.doFinally(signal -> requestCancellation.remove(request.id())));
}
else if (message instanceof McpSchema.JSONRPCNotification notification) {
// TODO handle errors for communication to without initialization
// happening first
logger.debug("Received notification: {}", notification);
// TODO: in case of error, should the POST request be signalled?
return handleIncomingNotification(notification)
.doOnError(error -> logger.error("Error handling notification: {}", error.getMessage()));
}
else {
logger.warn("Received unknown message type: {}", message);
return Mono.empty();
}
});
}
/**
* Handles an incoming JSON-RPC request by routing it to the appropriate handler.
* @param request The incoming JSON-RPC request
* @return A Mono containing the JSON-RPC response
*/
private Mono<McpSchema.JSONRPCResponse> handleIncomingRequest(McpSchema.JSONRPCRequest request) {
return Mono.defer(() -> {
Mono<?> resultMono;
if (McpSchema.METHOD_INITIALIZE.equals(request.method())) {
// TODO handle situation where already initialized!
McpSchema.InitializeRequest initializeRequest = transport.unmarshalFrom(request.params(),
new TypeReference<McpSchema.InitializeRequest>() {
});
this.state.lazySet(STATE_INITIALIZING);
this.init(initializeRequest.capabilities(), initializeRequest.clientInfo());
resultMono = this.initRequestHandler.handle(initializeRequest);
}
else {
// cancellation request
if (requestCancellation.get(request.id())) {
requestCancellation.remove(request.id());
return Mono.empty();
}
// TODO handle errors for communication to this session without
// initialization happening first
var handler = this.requestHandlers.get(request.method());
if (handler == null) {
MethodNotFoundError error = getMethodNotFoundError(request.method());
return Mono.just(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null,
new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.METHOD_NOT_FOUND,
error.message(), error.data())));
}
resultMono = this.exchangeSink.asMono()
.flatMap(exchange -> handler.handle(exchange, request.params()).flatMap(result -> {
if (requestCancellation.get(request.id())) {
requestCancellation.remove(request.id());
return Mono.empty();
}
else {
return Mono.just(result);
}
}).doOnCancel(() -> requestCancellation.remove(request.id())));
}
return resultMono
.map(result -> new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), result, null))
.onErrorResume(error -> {
if (requestCancellation.get(request.id())) {
requestCancellation.remove(request.id());
return Mono.empty();
}
else {
return Mono.just(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null,
new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR,
error.getMessage(), null)));
}
}); // TODO: add error message
// through the data field
});
}
/**
* Handles an incoming JSON-RPC notification by routing it to the appropriate handler.
* @param notification The incoming JSON-RPC notification
* @return A Mono that completes when the notification is processed
*/
private Mono<Void> handleIncomingNotification(McpSchema.JSONRPCNotification notification) {
return Mono.defer(() -> {
if (McpSchema.METHOD_NOTIFICATION_INITIALIZED.equals(notification.method())) {
this.state.lazySet(STATE_INITIALIZED);
exchangeSink.tryEmitValue(new McpAsyncServerExchange(this, clientCapabilities.get(), clientInfo.get()));
return this.initNotificationHandler.handle();
}
else if (McpSchema.METHOD_NOTIFICATION_CANCELLED.equals(notification.method())) {
McpSchema.CancellationMessageNotification cancellationMessageNotification = transport
.unmarshalFrom(notification.params(), new TypeReference<>() {
});
if (requestCancellation.containsKey(cancellationMessageNotification.requestId())) {
logger.warn("Received cancellation notification for request {}, cancellation reason is {}",
cancellationMessageNotification.requestId(), cancellationMessageNotification.reason());
requestCancellation.put(cancellationMessageNotification.requestId(), true);
}
return Mono.empty();
}
var handler = notificationHandlers.get(notification.method());
if (handler == null) {
logger.error("No handler registered for notification method: {}", notification.method());
return Mono.empty();
}
return this.exchangeSink.asMono().flatMap(exchange -> handler.handle(exchange, notification.params()));
});
}
record MethodNotFoundError(String method, String message, Object data) {
}
static MethodNotFoundError getMethodNotFoundError(String method) {
switch (method) {
case McpSchema.METHOD_ROOTS_LIST:
return new MethodNotFoundError(method, "Roots not supported",
Map.of("reason", "Client does not have roots capability"));
default:
return new MethodNotFoundError(method, "Method not found: " + method, null);
}
}
@Override
public Mono<Void> closeGracefully() {
return this.transport.closeGracefully();
}
@Override
public void close() {
this.transport.close();
}
/**
* Request handler for the initialization request.
*/
public interface InitRequestHandler {
/**
* Handles the initialization request.
* @param initializeRequest the initialization request by the client
* @return a Mono that will emit the result of the initialization
*/
Mono<McpSchema.InitializeResult> handle(McpSchema.InitializeRequest initializeRequest);
}
/**
* Notification handler for the initialization notification from the client.
*/
public interface InitNotificationHandler {
/**
* Specifies an action to take upon successful initialization.
* @return a Mono that will complete when the initialization is acted upon.
*/
Mono<Void> handle();
}
/**
* A handler for client-initiated notifications.
*/
public interface NotificationHandler {
/**
* Handles a notification from the client.
* @param exchange the exchange associated with the client that allows calling
* back to the connected client or inspecting its capabilities.
* @param params the parameters of the notification.
* @return a Mono that completes once the notification is handled.
*/
Mono<Void> handle(McpAsyncServerExchange exchange, Object params);
}
/**
* A handler for client-initiated requests.
*
* @param <T> the type of the response that is expected as a result of handling the
* request.
*/
public interface RequestHandler<T> {
/**
* Handles a request from the client.
* @param exchange the exchange associated with the client that allows calling
* back to the connected client or inspecting its capabilities.
* @param params the parameters of the request.
* @return a Mono that will emit the response to the request.
*/
Mono<T> handle(McpAsyncServerExchange exchange, Object params);
}
/**
* Factory for creating server sessions which delegate to a provided 1:1 transport
* with a connected client.
*/
@FunctionalInterface
public interface Factory {
/**
* Creates a new 1:1 representation of the client-server interaction.
* @param sessionTransport the transport to use for communication with the client.
* @return a new server session.
*/
McpServerSession create(McpServerTransport sessionTransport);
}
}