Skip to content

Commit f05d806

Browse files
committed
feature improvements
1 parent 495ac16 commit f05d806

17 files changed

+409
-45
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,5 @@ build/
3333

3434
### Mac OS ###
3535
.DS_Store
36+
37+
*.log

pom.xml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
</dependency>
3636
<dependency>
3737
<groupId>org.springframework.boot</groupId>
38-
<artifactId>spring-boot-starter-webflux</artifactId>
38+
<artifactId>spring-boot-starter-web</artifactId>
3939
</dependency>
4040
<dependency>
4141
<groupId>org.springframework.ai</groupId>
@@ -45,10 +45,6 @@
4545
<groupId>org.springframework.ai</groupId>
4646
<artifactId>spring-ai-mcp</artifactId>
4747
</dependency>
48-
<dependency>
49-
<groupId>org.springframework.ai</groupId>
50-
<artifactId>spring-ai-autoconfigure-model-tool</artifactId>
51-
</dependency>
5248
<dependency>
5349
<groupId>com.javaaidev.chatagentui</groupId>
5450
<artifactId>chat-agent-ui</artifactId>

sample-config/sample-config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"args": [
2121
"-y",
2222
"@modelcontextprotocol/server-filesystem",
23-
"/path"
23+
"/tmp"
2424
]
2525
}
2626
}

src/main/java/com/javaaidev/easymcpclient/AppConfiguration.java

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,31 @@
44
import com.fasterxml.jackson.databind.ObjectMapper;
55
import com.fasterxml.jackson.databind.json.JsonMapper;
66
import com.javaaidev.easymcpclient.chatmodel.ChatModelService;
7+
import com.javaaidev.easymcpclient.client.CloseableMcpSyncClients;
78
import com.javaaidev.easymcpclient.client.McpClientService;
9+
import com.javaaidev.easymcpclient.client.McpToolCallbackResolver;
10+
import com.javaaidev.easymcpclient.client.NamedMcpSyncClient;
11+
import com.javaaidev.easymcpclient.client.SamplingService;
812
import com.javaaidev.easymcpclient.config.McpClientConfig;
9-
import io.modelcontextprotocol.client.McpSyncClient;
13+
import com.javaaidev.easymcpclient.config.mcp.NamedMcpServer;
1014
import java.io.IOException;
1115
import java.nio.file.Path;
1216
import java.util.List;
1317
import org.springframework.ai.chat.model.ChatModel;
14-
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
15-
import org.springframework.ai.tool.ToolCallbackProvider;
18+
import org.springframework.ai.model.tool.ToolCallingManager;
19+
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
20+
import org.springframework.ai.tool.execution.ToolExecutionExceptionProcessor;
21+
import org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver;
22+
import org.springframework.ai.tool.resolution.SpringBeanToolCallbackResolver;
23+
import org.springframework.ai.tool.resolution.ToolCallbackResolver;
1624
import org.springframework.ai.util.JacksonUtils;
17-
import org.springframework.beans.factory.ObjectProvider;
25+
import org.springframework.beans.factory.annotation.Qualifier;
1826
import org.springframework.boot.ApplicationArguments;
27+
import org.springframework.context.ApplicationEventPublisher;
1928
import org.springframework.context.annotation.Bean;
2029
import org.springframework.context.annotation.Configuration;
30+
import org.springframework.context.annotation.Lazy;
31+
import org.springframework.context.support.GenericApplicationContext;
2132

2233
@Configuration
2334
public class AppConfiguration {
@@ -46,18 +57,68 @@ public McpClientConfig mcpClientConfig(ApplicationArguments arguments,
4657
}
4758

4859
@Bean
49-
public ChatModel chatModel(McpClientConfig mcpClientConfig) {
50-
return new ChatModelService().create(mcpClientConfig.chatModel());
60+
public ChatModel chatModel(McpClientConfig mcpClientConfig,
61+
ToolCallingManager toolCallingManager) {
62+
return new ChatModelService().create(mcpClientConfig.chatModel(), toolCallingManager);
5163
}
5264

5365
@Bean
54-
public List<McpSyncClient> mcpSyncClients(McpClientConfig mcpClientConfig) {
55-
return new McpClientService().connect(mcpClientConfig.mcpServers().values());
66+
public SamplingService samplingService(ChatModel chatModel) {
67+
return new SamplingService(chatModel);
5668
}
5769

5870
@Bean
59-
public ToolCallbackProvider mcpToolCallbacks(ObjectProvider<List<McpSyncClient>> syncMcpClients) {
60-
List<McpSyncClient> mcpClients = syncMcpClients.stream().flatMap(List::stream).toList();
61-
return new SyncMcpToolCallbackProvider(mcpClients);
71+
public McpClientService mcpClientService(@Lazy SamplingService samplingService,
72+
ApplicationEventPublisher applicationEventPublisher) {
73+
return new McpClientService(samplingService, applicationEventPublisher);
74+
}
75+
76+
@Bean
77+
public List<NamedMcpSyncClient> mcpSyncClients(McpClientService mcpClientService,
78+
McpClientConfig mcpClientConfig) {
79+
return mcpClientService.connect(mcpClientConfig.mcpServers().entrySet().stream()
80+
.map(entry -> new NamedMcpServer(entry.getKey(), entry.getValue())).toList());
81+
}
82+
83+
@Bean
84+
public McpToolCallbackResolver mcpToolCallbackResolver(List<NamedMcpSyncClient> syncMcpClients) {
85+
return new McpToolCallbackResolver(syncMcpClients);
86+
}
87+
88+
@Bean
89+
public CloseableMcpSyncClients closeableMcpSyncClients(List<NamedMcpSyncClient> mcpSyncClients) {
90+
return new CloseableMcpSyncClients(mcpSyncClients);
91+
}
92+
93+
@Bean
94+
public AppListener appListener() {
95+
return new AppListener();
96+
}
97+
98+
@Bean
99+
public ToolExecutionExceptionProcessor toolExecutionExceptionProcessor() {
100+
return new DefaultToolExecutionExceptionProcessor(false);
101+
}
102+
103+
@Bean
104+
public ToolCallingManager toolCallingManager(
105+
@Qualifier("mainToolCallbackResolver") ToolCallbackResolver toolCallbackResolver,
106+
ToolExecutionExceptionProcessor toolExecutionExceptionProcessor) {
107+
return ToolCallingManager.builder()
108+
.toolCallbackResolver(toolCallbackResolver)
109+
.toolExecutionExceptionProcessor(toolExecutionExceptionProcessor)
110+
.build();
111+
}
112+
113+
@Bean
114+
@Qualifier("mainToolCallbackResolver")
115+
public ToolCallbackResolver toolCallbackResolver(McpToolCallbackResolver mcpToolCallbackResolver,
116+
GenericApplicationContext applicationContext) {
117+
var springBeanToolCallbackResolver = SpringBeanToolCallbackResolver.builder()
118+
.applicationContext(applicationContext)
119+
.build();
120+
121+
return new DelegatingToolCallbackResolver(
122+
List.of(mcpToolCallbackResolver, springBeanToolCallbackResolver));
62123
}
63124
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.javaaidev.easymcpclient;
2+
3+
import java.io.IOException;
4+
import org.slf4j.Logger;
5+
import org.slf4j.LoggerFactory;
6+
import org.springframework.boot.web.context.WebServerInitializedEvent;
7+
import org.springframework.context.ApplicationListener;
8+
9+
public class AppListener implements
10+
ApplicationListener<WebServerInitializedEvent> {
11+
12+
private static final Logger LOGGER = LoggerFactory.getLogger(AppListener.class);
13+
14+
@Override
15+
public void onApplicationEvent(WebServerInitializedEvent event) {
16+
int port = event.getWebServer().getPort();
17+
LOGGER.info("Server running on port {}", port);
18+
try {
19+
BrowserOpener.openUrl("http://localhost:%s/webjars/chat-agent-ui/index.html".formatted(port));
20+
} catch (IOException e) {
21+
LOGGER.error("Failed to open url", e);
22+
}
23+
}
24+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.javaaidev.easymcpclient;
2+
3+
import java.io.IOException;
4+
import java.util.List;
5+
6+
public class BrowserOpener {
7+
8+
public static void openUrl(String url) throws IOException {
9+
var os = System.getProperty("os.name").toLowerCase();
10+
String[] commands;
11+
if (os.contains("win")) {
12+
commands = new String[]{"rundll32", "url.dll,FileProtocolHandler", url};
13+
} else if (os.contains("mac")) {
14+
commands = new String[]{"open", url};
15+
} else {
16+
var browsers = List.of("google-chrome", "firefox", "mozilla", "epiphany", "konqueror",
17+
"netscape", "opera", "links", "lynx");
18+
var command = String.join(" || ",
19+
browsers.stream().map(browser -> "%s \"%s\"".formatted(browser, url)).toList());
20+
commands = new String[]{"sh", "-c", command};
21+
}
22+
Runtime.getRuntime().exec(commands);
23+
}
24+
}

src/main/java/com/javaaidev/easymcpclient/chatmodel/ChatModelService.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.util.function.Supplier;
88
import org.apache.commons.lang3.StringUtils;
99
import org.springframework.ai.chat.model.ChatModel;
10+
import org.springframework.ai.model.tool.ToolCallingManager;
1011
import org.springframework.ai.openai.OpenAiChatModel;
1112
import org.springframework.ai.openai.OpenAiChatOptions;
1213
import org.springframework.ai.openai.api.OpenAiApi;
@@ -15,14 +16,14 @@ public class ChatModelService {
1516

1617
private final Dotenv dotenv = Dotenv.configure().ignoreIfMissing().ignoreIfMalformed().load();
1718

18-
public ChatModel create(ChatModelConfig config) {
19+
public ChatModel create(ChatModelConfig config, ToolCallingManager toolCallingManager) {
1920
if (config instanceof OpenAIChatModelConfig openAIChatModelConfig) {
20-
return create(openAIChatModelConfig);
21+
return create(openAIChatModelConfig, toolCallingManager);
2122
}
2223
throw new IllegalArgumentException("Invalid chat model config");
2324
}
2425

25-
private ChatModel create(OpenAIChatModelConfig config) {
26+
private ChatModel create(OpenAIChatModelConfig config, ToolCallingManager toolCallingManager) {
2627
String apiKey = "";
2728
if (StringUtils.isNotEmpty(config.apiKey())) {
2829
apiKey = config.apiKey();
@@ -43,6 +44,7 @@ private ChatModel create(OpenAIChatModelConfig config) {
4344
return OpenAiChatModel.builder()
4445
.openAiApi(openAiApiBuilder.build())
4546
.defaultOptions(chatOptionsBuilder.build())
47+
.toolCallingManager(toolCallingManager)
4648
.build();
4749
}
4850

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.javaaidev.easymcpclient.client;
2+
3+
import java.util.List;
4+
5+
public record CloseableMcpSyncClients(List<NamedMcpSyncClient> clients) implements AutoCloseable {
6+
7+
@Override
8+
public void close() {
9+
this.clients.forEach(client -> client.mcpSyncClient().close());
10+
}
11+
}
Lines changed: 47 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
package com.javaaidev.easymcpclient.client;
22

3-
import com.javaaidev.easymcpclient.config.mcp.McpServer;
3+
import com.javaaidev.easymcpclient.config.mcp.NamedMcpServer;
44
import com.javaaidev.easymcpclient.config.mcp.SseServer;
55
import com.javaaidev.easymcpclient.config.mcp.StdioServer;
66
import io.modelcontextprotocol.client.McpClient;
7-
import io.modelcontextprotocol.client.McpSyncClient;
87
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
98
import io.modelcontextprotocol.client.transport.ServerParameters;
109
import io.modelcontextprotocol.client.transport.StdioClientTransport;
@@ -14,41 +13,67 @@
1413
import java.time.Duration;
1514
import java.util.Collection;
1615
import java.util.List;
16+
import java.util.Optional;
17+
import org.slf4j.Logger;
18+
import org.slf4j.LoggerFactory;
19+
import org.springframework.context.ApplicationEventPublisher;
1720

1821
public class McpClientService {
1922

20-
public List<McpSyncClient> connect(Collection<McpServer> servers) {
21-
return servers.stream().map(server -> {
23+
private static final Logger LOGGER = LoggerFactory.getLogger(McpClientService.class);
24+
25+
private final SamplingService samplingService;
26+
private final ApplicationEventPublisher applicationEventPublisher;
27+
28+
public McpClientService(SamplingService samplingService,
29+
ApplicationEventPublisher applicationEventPublisher) {
30+
this.samplingService = samplingService;
31+
this.applicationEventPublisher = applicationEventPublisher;
32+
}
33+
34+
public List<NamedMcpSyncClient> connect(Collection<NamedMcpServer> servers) {
35+
return servers.stream().map(namedMcpServer -> {
36+
var name = namedMcpServer.name();
37+
var server = namedMcpServer.mcpServer();
2238
if (server instanceof StdioServer stdioServer) {
23-
return connect(stdioServer);
39+
return connect(name, stdioServer);
2440
} else if (server instanceof SseServer sseServer) {
25-
return connect(sseServer);
41+
return connect(name, sseServer);
2642
}
27-
throw new IllegalArgumentException("Invalid MCP server");
28-
}).toList();
43+
return Optional.<NamedMcpSyncClient>empty();
44+
}).flatMap(Optional::stream).toList();
2945
}
3046

31-
private McpSyncClient connect(SseServer server) {
32-
return doConnect(HttpClientSseClientTransport.builder(server.url()).build());
47+
private Optional<NamedMcpSyncClient> connect(String name, SseServer server) {
48+
return doConnect(name, HttpClientSseClientTransport.builder(server.url()).build());
3349
}
3450

35-
private McpSyncClient connect(StdioServer server) {
36-
return doConnect(new StdioClientTransport(
51+
private Optional<NamedMcpSyncClient> connect(String name, StdioServer server) {
52+
return doConnect(name, new StdioClientTransport(
3753
ServerParameters.builder(server.command())
3854
.args(server.args())
3955
.env(server.env())
4056
.build()));
4157
}
4258

43-
private McpSyncClient doConnect(McpClientTransport clientTransport) {
44-
var client = McpClient.sync(clientTransport)
45-
.clientInfo(new Implementation("easy-mcp-client", "0.1.0"))
46-
.requestTimeout(Duration.ofSeconds(30))
47-
.capabilities(ClientCapabilities.builder()
48-
.sampling()
49-
.build())
50-
.build();
51-
client.initialize();
52-
return client;
59+
private Optional<NamedMcpSyncClient> doConnect(String name, McpClientTransport clientTransport) {
60+
try {
61+
var client = McpClient.sync(clientTransport)
62+
.clientInfo(new Implementation("easy-mcp-client", "0.1.0"))
63+
.requestTimeout(Duration.ofSeconds(30))
64+
.initializationTimeout(Duration.ofSeconds(30))
65+
.capabilities(ClientCapabilities.builder()
66+
.sampling()
67+
.build())
68+
.sampling(samplingService)
69+
.toolsChangeConsumer(
70+
tools -> applicationEventPublisher.publishEvent(new ToolsChangedEvent(name)))
71+
.build();
72+
client.initialize();
73+
return Optional.of(new NamedMcpSyncClient(name, client));
74+
} catch (Exception e) {
75+
LOGGER.error("Failed to connect to MCP server {}", name, e);
76+
}
77+
return Optional.empty();
5378
}
5479
}

0 commit comments

Comments
 (0)