Skip to content
This repository was archived by the owner on Feb 14, 2025. It is now read-only.

Commit 1982c39

Browse files
committed
break into multiple modules. The mcp module is the MCP implementation with minimal dependecies
1 parent 6a90fdc commit 1982c39

32 files changed

+200
-2858
lines changed

mcp/pom.xml

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,7 @@
33
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
44
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
55
<modelVersion>4.0.0</modelVersion>
6-
<!-- <parent>
7-
<groupId>org.springframework.boot</groupId>
8-
<artifactId>spring-boot-starter-parent</artifactId>
9-
<version>3.4.0</version>
10-
<relativePath />
11-
</parent> -->
12-
<groupId>spring.ai.experiment</groupId>
6+
<groupId>spring.ai.experimental</groupId>
137
<artifactId>mcp</artifactId>
148
<version>0.0.1-SNAPSHOT</version>
159
<name>mcp</name>
@@ -43,6 +37,25 @@
4337
<artifactId>reactor-core</artifactId>
4438
</dependency>
4539

40+
<dependency>
41+
<groupId>org.assertj</groupId>
42+
<artifactId>assertj-core</artifactId>
43+
<version>3.26.3</version>
44+
<scope>test</scope>
45+
</dependency>
46+
<dependency>
47+
<groupId>org.junit.jupiter</groupId>
48+
<artifactId>junit-jupiter-api</artifactId>
49+
<version>5.11.3</version>
50+
<scope>test</scope>
51+
</dependency>
52+
<dependency>
53+
<groupId>org.mockito</groupId>
54+
<artifactId>mockito-core</artifactId>
55+
<version>5.11.0</version>
56+
<scope>test</scope>
57+
</dependency>
58+
4659
<!-- <dependency>
4760
<groupId>org.springframework.ai</groupId>
4861
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>

mcp/src/main/java/spring/ai/mcp/client/McpAsyncClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public McpAsyncClient(McpAsyncTransport transport) {
1414
this(transport, Duration.ofSeconds(10), new ObjectMapper());
1515
}
1616

17-
public McpAsyncClient(McpAsyncTransport transport, Duration requestTimeout, ObjectMapper objectMapper) {
17+
public McpAsyncClient(McpAsyncTransport transport, Duration requestTimeout, ObjectMapper objectMapper) {
1818
super(requestTimeout, objectMapper, transport);
1919
}
2020

mcp/src/main/java/spring/ai/mcp/client/McpAsyncSession.java

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
import java.time.Duration;
44
import java.util.Map;
5-
import java.util.Objects;
65
import java.util.UUID;
76
import java.util.concurrent.ConcurrentHashMap;
87

98
import com.fasterxml.jackson.core.type.TypeReference;
109
import com.fasterxml.jackson.databind.ObjectMapper;
1110
import reactor.core.publisher.Mono;
1211
import reactor.core.publisher.MonoSink;
12+
import spring.ai.mcp.client.util.Assert;
1313
import spring.ai.mcp.spec.McpAsyncTransport;
1414
import spring.ai.mcp.spec.McpSchema;
1515

@@ -25,9 +25,9 @@ public class McpAsyncSession {
2525

2626
public McpAsyncSession(Duration requestTimeout, ObjectMapper objectMapper, McpAsyncTransport transport) {
2727

28-
Objects.nonNull(requestTimeout);
29-
Objects.nonNull(objectMapper);
30-
Objects.nonNull(transport);
28+
Assert.notNull(objectMapper, "The ObjectMapper can not be null");
29+
Assert.notNull(requestTimeout, "The requstTimeout can not be null");
30+
Assert.notNull(transport, "The transport can not be null");
3131

3232
this.requestTimeout = requestTimeout;
3333
this.objectMapper = objectMapper;
@@ -39,8 +39,7 @@ public McpAsyncSession(Duration requestTimeout, ObjectMapper objectMapper, McpAs
3939
var sink = pendingResponses.remove(response.id());
4040
if (sink == null) {
4141
System.out.println("Unexpected response for unkown id " + response.id());
42-
}
43-
else {
42+
} else {
4443
sink.success(response);
4544
}
4645
}
@@ -69,26 +68,23 @@ public <T> Mono<T> sendRequest(String method, Object requestParams, TypeReferenc
6968
// TODO: This is non-blocking, but it's actually a synchronous call,
7069
// perhaps there's no need to make it return Mono?
7170
this.transport.sendMessage(jsonrpcRequest)
72-
// TODO: It's most efficient to create a dedicated
73-
// Subscriber here
74-
.subscribe(v -> {
75-
}, e -> {
76-
this.pendingResponses.remove(requestId);
77-
sink.error(e);
78-
});
79-
}
80-
catch (Exception e) {
71+
// TODO: It's most efficient to create a dedicated
72+
// Subscriber here
73+
.subscribe(v -> {
74+
}, e -> {
75+
this.pendingResponses.remove(requestId);
76+
sink.error(e);
77+
});
78+
} catch (Exception e) {
8179
sink.error(e);
8280
}
8381
}).timeout(this.requestTimeout).handle((jsonRpcResponse, s) -> {
8482
if (jsonRpcResponse.error() != null) {
8583
s.error(new McpError(jsonRpcResponse.error()));
86-
}
87-
else {
84+
} else {
8885
if (typeRef.getType().getTypeName().equals("java.lang.Void")) {
8986
s.complete();
90-
}
91-
else {
87+
} else {
9288
s.next(this.objectMapper.convertValue(jsonRpcResponse.result(), typeRef));
9389
}
9490
}
@@ -113,8 +109,7 @@ public Mono<Void> sendNotification(String method, Map<String, Object> params) {
113109
try {
114110
// TODO: make it non-blocking
115111
this.transport.sendMessage(jsonrpcNotification);
116-
}
117-
catch (Exception e) {
112+
} catch (Exception e) {
118113
return Mono.error(new McpError(e));
119114
}
120115
return Mono.empty();

mcp/src/main/java/spring/ai/mcp/client/McpSyncClient.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package spring.ai.mcp.client;
22

33
import java.time.Duration;
4-
import java.util.Objects;
54

5+
import spring.ai.mcp.client.util.Assert;
66
import spring.ai.mcp.spec.McpSchema;
77

88
public class McpSyncClient implements AutoCloseable {
@@ -15,7 +15,7 @@ public class McpSyncClient implements AutoCloseable {
1515
McpAsyncClient delegate;
1616

1717
public McpSyncClient(McpAsyncClient delegate) {
18-
Objects.nonNull(delegate);
18+
Assert.notNull(delegate, "The delegate can not be null");
1919
this.delegate = delegate;
2020
}
2121

mcp/src/main/java/spring/ai/mcp/client/stdio/StdioServerParameters.java

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@
55
import java.util.HashMap;
66
import java.util.List;
77
import java.util.Map;
8-
import java.util.Objects;
98
import java.util.stream.Collectors;
109

1110
import com.fasterxml.jackson.annotation.JsonProperty;
12-
13-
import org.springframework.util.CollectionUtils;
11+
import spring.ai.mcp.client.util.Assert;
1412

1513
/**
1614
* Server parameters for stdio client.
@@ -19,11 +17,11 @@ public class StdioServerParameters {
1917

2018
// Environment variables to inherit by default
2119
private static final List<String> DEFAULT_INHERITED_ENV_VARS = System.getProperty("os.name")
22-
.toLowerCase()
23-
.contains("win")
24-
? Arrays.asList("APPDATA", "HOMEDRIVE", "HOMEPATH", "LOCALAPPDATA", "PATH",
25-
"PROCESSOR_ARCHITECTURE", "SYSTEMDRIVE", "SYSTEMROOT", "TEMP", "USERNAME", "USERPROFILE")
26-
: Arrays.asList("HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER");
20+
.toLowerCase()
21+
.contains("win")
22+
? Arrays.asList("APPDATA", "HOMEDRIVE", "HOMEPATH", "LOCALAPPDATA", "PATH",
23+
"PROCESSOR_ARCHITECTURE", "SYSTEMDRIVE", "SYSTEMROOT", "TEMP", "USERNAME", "USERPROFILE")
24+
: Arrays.asList("HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER");
2725

2826
@JsonProperty("command")
2927
private String command;
@@ -35,13 +33,13 @@ public class StdioServerParameters {
3533
private Map<String, String> env;
3634

3735
private StdioServerParameters(String command, List<String> args, Map<String, String> env) {
38-
Objects.nonNull(command);
39-
Objects.nonNull(args);
36+
Assert.notNull(command, "The command can not be null");
37+
Assert.notNull(args, "The args can not be null");
4038

4139
this.command = command;
4240
this.args = args;
4341
this.env = new HashMap<>(getDefaultEnvironment());
44-
if (!CollectionUtils.isEmpty(env)) {
42+
if (env != null && !env.isEmpty()) {
4543
this.env.putAll(env);
4644
}
4745
}
@@ -71,38 +69,38 @@ public static class Builder {
7169
private Map<String, String> env = new HashMap<>();
7270

7371
public Builder(String command) {
74-
Objects.no.hasText(command, "Command must not be empty");
72+
Assert.notNull(command, "The command can not be null");
7573
this.command = command;
7674
}
7775

7876
public Builder args(String... args) {
79-
Assert.notNull(args, "Arguments must not be null");
77+
Assert.notNull(args, "The args can not be null");
8078
this.args = Arrays.asList(args);
8179
return this;
8280
}
8381

8482
public Builder args(List<String> args) {
85-
Assert.notNull(args, "Arguments must not be null");
83+
Assert.notNull(args, "The args can not be null");
8684
this.args = new ArrayList<>(args);
8785
return this;
8886
}
8987

9088
public Builder arg(String arg) {
91-
Assert.hasText(arg, "Argument must not be empty");
89+
Assert.notNull(arg, "The arg can not be null");
9290
this.args.add(arg);
9391
return this;
9492
}
9593

9694
public Builder env(Map<String, String> env) {
97-
if (!CollectionUtils.isEmpty(env)) {
95+
if (env != null && !env.isEmpty()) {
9896
this.env.putAll(env);
9997
}
10098
return this;
10199
}
102100

103101
public Builder addEnvVar(String key, String value) {
104-
Assert.hasText(key, "Environment variable key must not be empty");
105-
Assert.notNull(value, "Environment variable value must not be null");
102+
Assert.notNull(key, "The key can not be null");
103+
Assert.notNull(value, "The value can not be null");
106104
this.env.put(key, value);
107105
return this;
108106
}
@@ -119,12 +117,12 @@ public StdioServerParameters build() {
119117
*/
120118
private static Map<String, String> getDefaultEnvironment() {
121119
return System.getenv()
122-
.entrySet()
123-
.stream()
124-
.filter(entry -> DEFAULT_INHERITED_ENV_VARS.contains(entry.getKey()))
125-
.filter(entry -> entry.getValue() != null)
126-
.filter(entry -> !entry.getValue().startsWith("()"))
127-
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
120+
.entrySet()
121+
.stream()
122+
.filter(entry -> DEFAULT_INHERITED_ENV_VARS.contains(entry.getKey()))
123+
.filter(entry -> entry.getValue() != null)
124+
.filter(entry -> !entry.getValue().startsWith("()"))
125+
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
128126
}
129127

130128
}

mcp/src/main/java/spring/ai/mcp/client/stdio/StdioServerTransport.java

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,23 @@
55
import java.io.IOException;
66
import java.time.Duration;
77
import java.util.ArrayList;
8+
import java.util.HashMap;
89
import java.util.List;
10+
import java.util.Map;
11+
import java.util.Objects;
912
import java.util.concurrent.Executors;
1013

14+
import com.fasterxml.jackson.core.type.TypeReference;
15+
import com.fasterxml.jackson.databind.ObjectMapper;
1116
import reactor.core.scheduler.Scheduler;
1217
import reactor.core.scheduler.Schedulers;
18+
import spring.ai.mcp.client.util.Assert;
1319
import spring.ai.mcp.spec.DefaultMcpTransport;
1420
import spring.ai.mcp.spec.McpSchema.JSONRPCMessage;
1521
import spring.ai.mcp.spec.McpSchema.JSONRPCNotification;
1622
import spring.ai.mcp.spec.McpSchema.JSONRPCRequest;
1723
import spring.ai.mcp.spec.McpSchema.JSONRPCResponse;
1824

19-
import org.springframework.ai.model.ModelOptionsUtils;
20-
import org.springframework.util.Assert;
21-
2225
/**
2326
* Stdio client for communicating with a server process.
2427
*/
@@ -47,7 +50,7 @@ public StdioServerTransport(StdioServerParameters params) {
4750
public StdioServerTransport(StdioServerParameters params, Duration writeTimeout) {
4851
super(writeTimeout);
4952

50-
Assert.notNull(params, "Server parameters must not be null");
53+
Assert.notNull(params, "The params can not be null");
5154

5255
this.params = params;
5356

@@ -135,19 +138,34 @@ private void startErrorProcessing() {
135138
});
136139
}
137140

141+
private static ObjectMapper OBJECT_MAPPER = new ObjectMapper();
142+
143+
private static TypeReference<HashMap<String, Object>> MAP_TYPE_REF = new TypeReference<HashMap<String, Object>>() {
144+
145+
};
146+
147+
public static Map<String, Object> jsonToMap(String json) {
148+
try {
149+
return OBJECT_MAPPER.readValue(json, MAP_TYPE_REF);
150+
}
151+
catch (Exception e) {
152+
throw new RuntimeException(e);
153+
}
154+
}
155+
138156
private JSONRPCMessage deserializeJsonRpcMessage(String jsonText) throws IOException {
139157

140-
var map = ModelOptionsUtils.jsonToMap(jsonText);
158+
var map = jsonToMap(jsonText);
141159

142160
// Determine message type based on specific JSON structure
143161
if (map.containsKey("method") && map.containsKey("id")) {
144-
return ModelOptionsUtils.OBJECT_MAPPER.convertValue(map, JSONRPCRequest.class);
162+
return OBJECT_MAPPER.convertValue(map, JSONRPCRequest.class);
145163
}
146164
else if (map.containsKey("method") && !map.containsKey("id")) {
147-
return ModelOptionsUtils.OBJECT_MAPPER.convertValue(map, JSONRPCNotification.class);
165+
return OBJECT_MAPPER.convertValue(map, JSONRPCNotification.class);
148166
}
149167
else if (map.containsKey("result") || map.containsKey("error")) {
150-
return ModelOptionsUtils.OBJECT_MAPPER.convertValue(map, JSONRPCResponse.class);
168+
return OBJECT_MAPPER.convertValue(map, JSONRPCResponse.class);
151169
}
152170

153171
throw new IllegalArgumentException("Cannot deserialize JSONRPCMessage: " + jsonText);
@@ -190,7 +208,7 @@ private void startOutboundProcessing() {
190208
.handle((message, s) -> {
191209
if (message != null) {
192210
try {
193-
this.processWriter.write(ModelOptionsUtils.toJsonString(message));
211+
this.processWriter.write(OBJECT_MAPPER.writeValueAsString(message));
194212
this.processWriter.newLine();
195213
this.processWriter.flush();
196214
s.next(message);

0 commit comments

Comments
 (0)