Skip to content

Commit 6bb7fbd

Browse files
committed
refactor(client): improve validation and remove server methods
Add client initialization and capability validation checks - New isInitialized() method to check client state - Validate server capabilities before tool/resource operations - Add clear error messages for common failure cases - Remove server-side notification methods from client: sendResourcesListChanged(), promptListChangedNotification() - Improve protocol version handling - Testing improvements and new initialization tests Resolves #13 Signed-off-by: Christian Tzolov <[email protected]>
1 parent c69cc18 commit 6bb7fbd

File tree

9 files changed

+148
-48
lines changed

9 files changed

+148
-48
lines changed

mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,6 @@ void testNotificationHandlers() {
274274

275275
assertThatCode(() -> {
276276
client.initialize().block();
277-
// Trigger notifications
278-
client.sendResourcesListChanged().block();
279-
client.promptListChangedNotification().block();
280277
client.closeGracefully().block();
281278
}).doesNotThrowAnyException();
282279
}

mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,9 +258,6 @@ void testNotificationHandlers() {
258258

259259
assertThatCode(() -> {
260260
client.initialize();
261-
// Trigger notifications
262-
client.sendResourcesListChanged();
263-
client.promptListChangedNotification();
264261
client.close();
265262
}).doesNotThrowAnyException();
266263
}

mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,14 @@ public McpSchema.Implementation getServerInfo() {
296296
return this.serverInfo;
297297
}
298298

299+
/**
300+
* Check if the client-server connection is initialized.
301+
* @return true if the client-server connection is initialized
302+
*/
303+
public boolean isInitialized() {
304+
return this.serverCapabilities != null;
305+
}
306+
299307
/**
300308
* Get the client capabilities that define the supported features and functionality.
301309
* @return The client capabilities
@@ -456,6 +464,12 @@ private RequestHandler<CreateMessageResult> samplingCreateMessageHandler() {
456464
* (false/absent)
457465
*/
458466
public Mono<McpSchema.CallToolResult> callTool(McpSchema.CallToolRequest callToolRequest) {
467+
if (!this.isInitialized()) {
468+
return Mono.error(new McpError("Client must be initialized before calling tools"));
469+
}
470+
if (this.serverCapabilities.tools() == null) {
471+
return Mono.error(new McpError("Server does not provide tools capability"));
472+
}
459473
return this.mcpSession.sendRequest(McpSchema.METHOD_TOOLS_CALL, callToolRequest, CALL_TOOL_RESULT_TYPE_REF);
460474
}
461475

@@ -477,6 +491,12 @@ public Mono<McpSchema.ListToolsResult> listTools() {
477491
* Optional cursor for pagination if more tools are available
478492
*/
479493
public Mono<McpSchema.ListToolsResult> listTools(String cursor) {
494+
if (!this.isInitialized()) {
495+
return Mono.error(new McpError("Client must be initialized before calling tools"));
496+
}
497+
if (this.serverCapabilities.tools() == null) {
498+
return Mono.error(new McpError("Server does not provide tools capability"));
499+
}
480500
return this.mcpSession.sendRequest(McpSchema.METHOD_TOOLS_LIST, new McpSchema.PaginatedRequest(cursor),
481501
LIST_TOOLS_RESULT_TYPE_REF);
482502
}
@@ -532,6 +552,12 @@ public Mono<McpSchema.ListResourcesResult> listResources() {
532552
* @return A Mono that completes with the list of resources result
533553
*/
534554
public Mono<McpSchema.ListResourcesResult> listResources(String cursor) {
555+
if (!this.isInitialized()) {
556+
return Mono.error(new McpError("Client must be initialized before calling tools"));
557+
}
558+
if (this.serverCapabilities.resources() == null) {
559+
return Mono.error(new McpError("Server does not provide the resources capability"));
560+
}
535561
return this.mcpSession.sendRequest(McpSchema.METHOD_RESOURCES_LIST, new McpSchema.PaginatedRequest(cursor),
536562
LIST_RESOURCES_RESULT_TYPE_REF);
537563
}
@@ -551,6 +577,12 @@ public Mono<McpSchema.ReadResourceResult> readResource(McpSchema.Resource resour
551577
* @return A Mono that completes with the resource content
552578
*/
553579
public Mono<McpSchema.ReadResourceResult> readResource(McpSchema.ReadResourceRequest readResourceRequest) {
580+
if (!this.isInitialized()) {
581+
return Mono.error(new McpError("Client must be initialized before calling tools"));
582+
}
583+
if (this.serverCapabilities.resources() == null) {
584+
return Mono.error(new McpError("Server does not provide the resources capability"));
585+
}
554586
return this.mcpSession.sendRequest(McpSchema.METHOD_RESOURCES_READ, readResourceRequest,
555587
READ_RESOURCE_RESULT_TYPE_REF);
556588
}
@@ -575,19 +607,16 @@ public Mono<McpSchema.ListResourceTemplatesResult> listResourceTemplates() {
575607
* @return A Mono that completes with the list of resource templates result
576608
*/
577609
public Mono<McpSchema.ListResourceTemplatesResult> listResourceTemplates(String cursor) {
610+
if (!this.isInitialized()) {
611+
return Mono.error(new McpError("Client must be initialized before calling tools"));
612+
}
613+
if (this.serverCapabilities.resources() == null) {
614+
return Mono.error(new McpError("Server does not provide the resources capability"));
615+
}
578616
return this.mcpSession.sendRequest(McpSchema.METHOD_RESOURCES_TEMPLATES_LIST,
579617
new McpSchema.PaginatedRequest(cursor), LIST_RESOURCE_TEMPLATES_RESULT_TYPE_REF);
580618
}
581619

582-
/**
583-
* List Changed Notification. When the list of available resources changes, servers
584-
* that declared the listChanged capability SHOULD send a notification.
585-
* @return A Mono that completes when the notification is sent
586-
*/
587-
public Mono<Void> sendResourcesListChanged() {
588-
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_RESOURCES_LIST_CHANGED);
589-
}
590-
591620
/**
592621
* Subscriptions. The protocol supports optional subscriptions to resource changes.
593622
* Clients can subscribe to specific resources and receive notifications when they
@@ -660,16 +689,6 @@ public Mono<GetPromptResult> getPrompt(GetPromptRequest getPromptRequest) {
660689
return this.mcpSession.sendRequest(McpSchema.METHOD_PROMPT_GET, getPromptRequest, GET_PROMPT_RESULT_TYPE_REF);
661690
}
662691

663-
/**
664-
* (Server) An optional notification from the server to the client, informing it that
665-
* the list of prompts it offers has changed. This may be issued by servers without
666-
* any previous subscription from the client.
667-
* @return A Mono that completes when the notification is sent
668-
*/
669-
public Mono<Void> promptListChangedNotification() {
670-
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED);
671-
}
672-
673692
private NotificationHandler asyncPromptsChangeNotificationHandler(
674693
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers) {
675694
return params -> listPrompts().flatMap(listPromptsResult -> Flux.fromIterable(promptsChangeConsumers)

mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -284,14 +284,6 @@ public McpSchema.ListResourceTemplatesResult listResourceTemplates() {
284284
return this.delegate.listResourceTemplates().block();
285285
}
286286

287-
/**
288-
* List Changed Notification. When the list of available resources changes, servers
289-
* that declared the listChanged capability SHOULD send a notification:
290-
*/
291-
public void sendResourcesListChanged() {
292-
this.delegate.sendResourcesListChanged().block();
293-
}
294-
295287
/**
296288
* Subscriptions. The protocol supports optional subscriptions to resource changes.
297289
* Clients can subscribe to specific resources and receive notifications when they
@@ -329,15 +321,6 @@ public GetPromptResult getPrompt(GetPromptRequest getPromptRequest) {
329321
return this.delegate.getPrompt(getPromptRequest).block();
330322
}
331323

332-
/**
333-
* (Server) An optional notification from the server to the client, informing it that
334-
* the list of prompts it offers has changed. This may be issued by servers without
335-
* any previous subscription from the client.
336-
*/
337-
public void promptListChangedNotification() {
338-
this.delegate.promptListChangedNotification().block();
339-
}
340-
341324
/**
342325
* Client can set the minimum logging level it wants to receive from the server.
343326
* @param loggingLevel the min logging level

mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,13 @@ private DefaultMcpSession.RequestHandler<McpSchema.InitializeResult> asyncInitia
192192
initializeRequest.protocolVersion(), initializeRequest.capabilities(),
193193
initializeRequest.clientInfo());
194194

195+
// The server MUST respond with the highest protocol version it supports if
196+
// it does not support the requested (e.g. Client) version.
195197
String serverProtocolVersion = this.protocolVersions.get(this.protocolVersions.size() - 1);
196198

197199
if (this.protocolVersions.contains(initializeRequest.protocolVersion())) {
200+
// If the server supports the requested protocol version, it MUST respond
201+
// with the same version.
198202
serverProtocolVersion = initializeRequest.protocolVersion();
199203
}
200204
else {

mcp/src/test/java/io/modelcontextprotocol/MockMcpTransport.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ public void simulateIncomingMessage(McpSchema.JSONRPCMessage message) {
3838
throw new RuntimeException("Failed to emit message " + message);
3939
}
4040
inboundMessageCount.incrementAndGet();
41+
42+
try {
43+
Thread.sleep(200);
44+
}
45+
catch (InterruptedException e) {
46+
e.printStackTrace();
47+
}
4148
}
4249

4350
@Override

mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -275,9 +275,6 @@ void testNotificationHandlers() {
275275

276276
assertThatCode(() -> {
277277
client.initialize().block();
278-
// Trigger notifications
279-
client.sendResourcesListChanged().block();
280-
client.promptListChangedNotification().block();
281278
client.closeGracefully().block();
282279
}).doesNotThrowAnyException();
283280
}

mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,6 @@ void testNotificationHandlers() {
259259

260260
assertThatCode(() -> {
261261
client.initialize();
262-
// Trigger notifications
263-
client.sendResourcesListChanged();
264-
client.promptListChangedNotification();
265262
client.close();
266263
}).doesNotThrowAnyException();
267264
}

mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
import io.modelcontextprotocol.spec.McpError;
1818
import io.modelcontextprotocol.spec.McpSchema;
1919
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
20+
import io.modelcontextprotocol.spec.McpSchema.InitializeResult;
2021
import io.modelcontextprotocol.spec.McpSchema.Root;
22+
import org.junit.jupiter.api.Disabled;
2123
import org.junit.jupiter.api.Test;
2224
import reactor.core.publisher.Mono;
2325

@@ -27,6 +29,91 @@
2729

2830
class McpAsyncClientResponseHandlerTests {
2931

32+
private InitializeResult initialization(McpAsyncClient asyncMcpClient, MockMcpTransport transport) {
33+
34+
// Create mock server response
35+
McpSchema.ServerCapabilities mockServerCapabilities = McpSchema.ServerCapabilities.builder()
36+
.tools(true)
37+
.resources(true, true) // Enable both resources and resource templates
38+
.build();
39+
McpSchema.Implementation mockServerInfo = new McpSchema.Implementation("test-server", "1.0.0");
40+
McpSchema.InitializeResult mockInitResult = new McpSchema.InitializeResult(McpSchema.LATEST_PROTOCOL_VERSION,
41+
mockServerCapabilities, mockServerInfo, "Test instructions");
42+
43+
Mono<McpSchema.InitializeResult> initMono = asyncMcpClient.initialize();
44+
45+
new Thread(new Runnable() {
46+
@Override
47+
public void run() {
48+
McpSchema.JSONRPCRequest initRequest = transport.getLastSentMessageAsRequest();
49+
assertThat(initRequest.method()).isEqualTo(McpSchema.METHOD_INITIALIZE);
50+
51+
// Send mock server response
52+
McpSchema.JSONRPCResponse initResponse = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION,
53+
initRequest.id(), mockInitResult, null);
54+
transport.simulateIncomingMessage(initResponse);
55+
}
56+
}).start();
57+
58+
return initMono.block();
59+
}
60+
61+
@Test
62+
void testSuccessfulInitialization() {
63+
MockMcpTransport transport = new MockMcpTransport();
64+
McpAsyncClient asyncMcpClient = McpClient.async(transport).build();
65+
66+
// Verify client is not initialized initially
67+
assertThat(asyncMcpClient.isInitialized()).isFalse();
68+
69+
// Create mock server response
70+
McpSchema.ServerCapabilities mockServerCapabilities = McpSchema.ServerCapabilities.builder()
71+
.tools(true)
72+
.resources(true, true) // Enable both resources and resource templates
73+
.build();
74+
McpSchema.Implementation mockServerInfo = new McpSchema.Implementation("test-server", "1.0.0");
75+
McpSchema.InitializeResult mockInitResult = new McpSchema.InitializeResult(McpSchema.LATEST_PROTOCOL_VERSION,
76+
mockServerCapabilities, mockServerInfo, "Test instructions");
77+
78+
// Start initialization
79+
Mono<McpSchema.InitializeResult> initMono = asyncMcpClient.initialize();
80+
81+
new Thread(new Runnable() {
82+
@Override
83+
public void run() {
84+
McpSchema.JSONRPCRequest initRequest = transport.getLastSentMessageAsRequest();
85+
assertThat(initRequest.method()).isEqualTo(McpSchema.METHOD_INITIALIZE);
86+
87+
// Send mock server response
88+
McpSchema.JSONRPCResponse initResponse = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION,
89+
initRequest.id(), mockInitResult, null);
90+
transport.simulateIncomingMessage(initResponse);
91+
}
92+
}).start();
93+
94+
InitializeResult result = initMono.block();
95+
96+
// Verify initialized notification was sent
97+
McpSchema.JSONRPCMessage notificationMessage = transport.getLastSentMessage();
98+
assertThat(notificationMessage).isInstanceOf(McpSchema.JSONRPCNotification.class);
99+
McpSchema.JSONRPCNotification notification = (McpSchema.JSONRPCNotification) notificationMessage;
100+
assertThat(notification.method()).isEqualTo(McpSchema.METHOD_NOTIFICATION_INITIALIZED);
101+
102+
// Verify initialization result
103+
assertThat(result).isNotNull();
104+
assertThat(result.protocolVersion()).isEqualTo(McpSchema.LATEST_PROTOCOL_VERSION);
105+
assertThat(result.capabilities()).isEqualTo(mockServerCapabilities);
106+
assertThat(result.serverInfo()).isEqualTo(mockServerInfo);
107+
assertThat(result.instructions()).isEqualTo("Test instructions");
108+
109+
// Verify client state after initialization
110+
assertThat(asyncMcpClient.isInitialized()).isTrue();
111+
assertThat(asyncMcpClient.getServerCapabilities()).isEqualTo(mockServerCapabilities);
112+
assertThat(asyncMcpClient.getServerInfo()).isEqualTo(mockServerInfo);
113+
114+
asyncMcpClient.closeGracefully();
115+
}
116+
30117
@Test
31118
void testToolsChangeNotificationHandling() throws JsonProcessingException {
32119
MockMcpTransport transport = new MockMcpTransport();
@@ -41,6 +128,8 @@ void testToolsChangeNotificationHandling() throws JsonProcessingException {
41128
// Create client with tools change consumer
42129
McpAsyncClient asyncMcpClient = McpClient.async(transport).toolsChangeConsumer(toolsChangeConsumer).build();
43130

131+
assertThat(initialization(asyncMcpClient, transport)).isNotNull();
132+
44133
// Create a mock tools list that the server will return
45134
Map<String, Object> inputSchema = Map.of("type", "object", "properties", Map.of(), "required", List.of());
46135
McpSchema.Tool mockTool = new McpSchema.Tool("test-tool", "Test Tool Description",
@@ -78,6 +167,8 @@ void testRootsListRequestHandling() {
78167
.roots(new Root("file:///test/path", "test-root"))
79168
.build();
80169

170+
assertThat(initialization(asyncMcpClient, transport)).isNotNull();
171+
81172
// Simulate incoming request
82173
McpSchema.JSONRPCRequest request = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION,
83174
McpSchema.METHOD_ROOTS_LIST, "test-id", null);
@@ -112,6 +203,8 @@ void testResourcesChangeNotificationHandling() {
112203
.resourcesChangeConsumer(resourcesChangeConsumer)
113204
.build();
114205

206+
assertThat(initialization(asyncMcpClient, transport)).isNotNull();
207+
115208
// Create a mock resources list that the server will return
116209
McpSchema.Resource mockResource = new McpSchema.Resource("test://resource", "Test Resource", "A test resource",
117210
"text/plain", null);
@@ -156,6 +249,8 @@ void testPromptsChangeNotificationHandling() {
156249
// Create client with prompts change consumer
157250
McpAsyncClient asyncMcpClient = McpClient.async(transport).promptsChangeConsumer(promptsChangeConsumer).build();
158251

252+
assertThat(initialization(asyncMcpClient, transport)).isNotNull();
253+
159254
// Create a mock prompts list that the server will return
160255
McpSchema.Prompt mockPrompt = new McpSchema.Prompt("test-prompt", "Test Prompt Description",
161256
List.of(new McpSchema.PromptArgument("arg1", "Test argument", true)));
@@ -203,6 +298,8 @@ void testSamplingCreateMessageRequestHandling() {
203298
.sampling(samplingHandler)
204299
.build();
205300

301+
assertThat(initialization(asyncMcpClient, transport)).isNotNull();
302+
206303
// Create a mock create message request
207304
var messageRequest = new McpSchema.CreateMessageRequest(
208305
List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Test message"))),
@@ -247,6 +344,8 @@ void testSamplingCreateMessageRequestHandlingWithoutCapability() {
247344
.capabilities(ClientCapabilities.builder().build()) // No sampling capability
248345
.build();
249346

347+
assertThat(initialization(asyncMcpClient, transport)).isNotNull();
348+
250349
// Create a mock create message request
251350
var messageRequest = new McpSchema.CreateMessageRequest(
252351
List.of(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Test message"))),

0 commit comments

Comments
 (0)