Skip to content

Commit ccb679f

Browse files
authored
feat: 🧰 MCP server and client (#102)
1 parent 0917ef2 commit ccb679f

File tree

9 files changed

+347
-0
lines changed

9 files changed

+347
-0
lines changed

ai/ai-endpoints/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Don't hesitate to use the source code and give us feedback.
1515

1616
### ☕️ Java demos ☕️
1717

18+
- [MCP server / client](./mcp-quarkus-langchain4j)
1819
- [Function calling with LangChain4J](./function-calling-langchain4j)
1920
- [Simple Structured Output](./structured-output-langchain4j/)
2021
- [Natural Language Processing](./java-nlp)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# MCP server and client with LangChain4j, Quarkus and AI Endpoints
2+
3+
### 🧰 Pre requisites 🧰
4+
5+
- Java 24+ installed (with preview mode enabled)
6+
- AI Endpoints API token
7+
- model to use: any of the LLM instruct models
8+
- have the following environment variables created:
9+
- OVH_AI_ENDPOINTS_ACCESS_TOKEN: the API token, see [documentation](https://help.ovhcloud.com/csm/en-gb-public-cloud-ai-endpoints-getting-started?id=kb_article_view&sysparm_article=KB0065401#generating-your-first-api-access-key) to know how to generate it
10+
- OVH_AI_ENDPOINTS_MODEL_URL: URL of the model, see [AI Endpoints website](https://endpoints.ai.cloud.ovh.net/) to know how to get it.
11+
- OVH_AI_ENDPOINTS_MODEL_NAME: model name, see [AI Endpoints website](https://endpoints.ai.cloud.ovh.net/) to know how to get it.
12+
- MCP_SERVER_URL: Quarkus MCP server URL (for example `http://127.0.0.1:8080/mcp/sse`)
13+
- [JBang](https://www.jbang.dev/documentation/guide/latest/index.html) installed
14+
15+
## ⚡️ How to use the project ⚡️
16+
17+
- run the MCP server in [ovh-mcp-server](./ovh-mcp-server) folder: `quarkus dev`
18+
- run `jbang SimpleMCPClient.java` command in [mcp-client](./mcp-client) folder
19+
20+
## 🗺️ Architecture 🗺️
21+
22+
```mermaid
23+
graph RL
24+
subgraph User app
25+
A[Chatbot]
26+
end
27+
subgraph MCP Server
28+
C[MCP Server]
29+
end
30+
subgraph AI Endpoints
31+
B[LLM Model]
32+
end
33+
A[Chatbot] -->| 1-USer prompt in natural language + MCP Server available| B[LLM Model]
34+
B -->| 2-Tool name from MCP server to use | A
35+
A -->| 3-Call the tool | C
36+
C -->| 4-Response from MCP server | A
37+
A-->| 5-Prompt + data from tool | B
38+
B-->| 6-Answer to user | A
39+
```
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
///usr/bin/env jbang "$0" "$@" ; exit $?
2+
//JAVA 24+
3+
//PREVIEW
4+
//DEPS dev.langchain4j:langchain4j-mcp:1.0.1-beta6 dev.langchain4j:langchain4j:1.0.1 dev.langchain4j:langchain4j-mistral-ai:1.0.1-beta6
5+
6+
7+
import dev.langchain4j.mcp.McpToolProvider;
8+
import dev.langchain4j.mcp.client.DefaultMcpClient;
9+
import dev.langchain4j.mcp.client.McpClient;
10+
import dev.langchain4j.mcp.client.transport.McpTransport;
11+
import dev.langchain4j.mcp.client.transport.http.HttpMcpTransport;
12+
import dev.langchain4j.model.chat.ChatModel;
13+
import dev.langchain4j.model.mistralai.MistralAiChatModel;
14+
import dev.langchain4j.service.AiServices;
15+
16+
// Simple chatbot definition with AI Services from LangChain4J
17+
public interface Bot {
18+
String chat(String prompt);
19+
}
20+
21+
void main() {
22+
// Mistral model from OVHcloud AI Endpoints
23+
ChatModel chatModel = MistralAiChatModel.builder()
24+
.apiKey(System.getenv("OVH_AI_ENDPOINTS_ACCESS_TOKEN"))
25+
.baseUrl(System.getenv("OVH_AI_ENDPOINTS_MODEL_URL"))
26+
.modelName(System.getenv("OVH_AI_ENDPOINTS_MODEL_NAME"))
27+
.logRequests(false)
28+
.logResponses(false)
29+
.build();
30+
31+
// Configure the MCP server to use
32+
McpTransport transport = new HttpMcpTransport.Builder()
33+
// https://xxxx/mcp/sse
34+
.sseUrl(System.getenv("MCP_SERVER_URL"))
35+
.logRequests(false)
36+
.logResponses(false)
37+
.build();
38+
39+
// Create the MCP client for the given MCP server
40+
McpClient mcpClient = new DefaultMcpClient.Builder()
41+
.transport(transport)
42+
.build();
43+
44+
// Configure the tools list for the LLM
45+
McpToolProvider toolProvider = McpToolProvider.builder()
46+
.mcpClients(mcpClient)
47+
.build();
48+
49+
// Create the chatbot with the given LLM and tools list
50+
Bot bot = AiServices.builder(Bot.class)
51+
.chatModel(chatModel)
52+
.toolProvider(toolProvider)
53+
.build();
54+
55+
// Play with the chatbot 🤖
56+
String response = bot.chat("Can I have some details about my OVHcloud account?");
57+
System.out.println("RESPONSE: " + response);
58+
59+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
3+
<modelVersion>4.0.0</modelVersion>
4+
<groupId>com.ovhcloud.ai.mcp</groupId>
5+
<artifactId>ovh-mcp</artifactId>
6+
<version>1.0.0-SNAPSHOT</version>
7+
8+
<properties>
9+
<compiler-plugin.version>3.14.0</compiler-plugin.version>
10+
<maven.compiler.release>21</maven.compiler.release>
11+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
12+
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
13+
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
14+
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
15+
<quarkus.platform.version>3.23.0</quarkus.platform.version>
16+
<skipITs>true</skipITs>
17+
<surefire-plugin.version>3.5.2</surefire-plugin.version>
18+
</properties>
19+
20+
<dependencyManagement>
21+
<dependencies>
22+
<dependency>
23+
<groupId>${quarkus.platform.group-id}</groupId>
24+
<artifactId>${quarkus.platform.artifact-id}</artifactId>
25+
<version>${quarkus.platform.version}</version>
26+
<type>pom</type>
27+
<scope>import</scope>
28+
</dependency>
29+
</dependencies>
30+
</dependencyManagement>
31+
32+
<dependencies>
33+
<dependency>
34+
<groupId>io.quarkus</groupId>
35+
<artifactId>quarkus-rest-client-jackson</artifactId>
36+
</dependency>
37+
<dependency>
38+
<groupId>io.quarkiverse.mcp</groupId>
39+
<artifactId>quarkus-mcp-server-sse</artifactId>
40+
<version>1.2.1</version>
41+
</dependency>
42+
<dependency>
43+
<groupId>io.quarkus</groupId>
44+
<artifactId>quarkus-smallrye-openapi</artifactId>
45+
</dependency>
46+
<dependency>
47+
<groupId>io.quarkus</groupId>
48+
<artifactId>quarkus-swagger-ui</artifactId>
49+
</dependency>
50+
<dependency>
51+
<groupId>io.quarkus</groupId>
52+
<artifactId>quarkus-arc</artifactId>
53+
</dependency>
54+
<dependency>
55+
<groupId>io.quarkus</groupId>
56+
<artifactId>quarkus-junit5</artifactId>
57+
<scope>test</scope>
58+
</dependency>
59+
<dependency>
60+
<groupId>io.rest-assured</groupId>
61+
<artifactId>rest-assured</artifactId>
62+
<scope>test</scope>
63+
</dependency>
64+
</dependencies>
65+
66+
<build>
67+
<plugins>
68+
<plugin>
69+
<groupId>${quarkus.platform.group-id}</groupId>
70+
<artifactId>quarkus-maven-plugin</artifactId>
71+
<version>${quarkus.platform.version}</version>
72+
<extensions>true</extensions>
73+
<executions>
74+
<execution>
75+
<goals>
76+
<goal>build</goal>
77+
<goal>generate-code</goal>
78+
<goal>generate-code-tests</goal>
79+
<goal>native-image-agent</goal>
80+
</goals>
81+
</execution>
82+
</executions>
83+
</plugin>
84+
<plugin>
85+
<artifactId>maven-compiler-plugin</artifactId>
86+
<version>${compiler-plugin.version}</version>
87+
<configuration>
88+
<parameters>true</parameters>
89+
</configuration>
90+
</plugin>
91+
<plugin>
92+
<artifactId>maven-surefire-plugin</artifactId>
93+
<version>${surefire-plugin.version}</version>
94+
<configuration>
95+
<systemPropertyVariables>
96+
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
97+
<maven.home>${maven.home}</maven.home>
98+
</systemPropertyVariables>
99+
</configuration>
100+
</plugin>
101+
<plugin>
102+
<artifactId>maven-failsafe-plugin</artifactId>
103+
<version>${surefire-plugin.version}</version>
104+
<executions>
105+
<execution>
106+
<goals>
107+
<goal>integration-test</goal>
108+
<goal>verify</goal>
109+
</goals>
110+
</execution>
111+
</executions>
112+
<configuration>
113+
<systemPropertyVariables>
114+
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
115+
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
116+
<maven.home>${maven.home}</maven.home>
117+
</systemPropertyVariables>
118+
</configuration>
119+
</plugin>
120+
</plugins>
121+
</build>
122+
123+
<profiles>
124+
<profile>
125+
<id>native</id>
126+
<activation>
127+
<property>
128+
<name>native</name>
129+
</property>
130+
</activation>
131+
<properties>
132+
<skipITs>false</skipITs>
133+
<quarkus.native.enabled>true</quarkus.native.enabled>
134+
</properties>
135+
</profile>
136+
</profiles>
137+
</project>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.ovhcloud.ai.mcp;
2+
3+
import com.ovhcloud.sdk.OVHcloudSignatureHelper;
4+
import com.ovhcloud.sdk.service.OVHcloudMe;
5+
import io.quarkiverse.mcp.server.TextContent;
6+
import io.quarkiverse.mcp.server.Tool;
7+
import io.quarkiverse.mcp.server.ToolResponse;
8+
import org.eclipse.microprofile.rest.client.inject.RestClient;
9+
10+
public class PublicCloudUserTool {
11+
12+
@RestClient
13+
OVHcloudMe ovhcloudMe;
14+
15+
@Tool(description = "Tool to manage the OVHcloud public cloud user.")
16+
ToolResponse getUserDetails() {
17+
Long ovhTimestamp = System.currentTimeMillis() / 1000;
18+
return ToolResponse.success(
19+
new TextContent(ovhcloudMe.getMe(OVHcloudSignatureHelper.signature("me", ovhTimestamp),
20+
Long.toString(ovhTimestamp)).toString()));
21+
}
22+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.ovhcloud.sdk;
2+
3+
import java.io.UnsupportedEncodingException;
4+
import java.security.MessageDigest;
5+
import java.security.NoSuchAlgorithmException;
6+
7+
public class OVHcloudSignatureHelper {
8+
public static String signature(String endPoint, Long timestamp) {
9+
// build signature
10+
String toSign = new StringBuilder(System.getenv("OVH_APPLICATION_SECRET"))
11+
.append("+")
12+
.append(System.getenv("OVH_CONSUMER_KEY"))
13+
.append("+")
14+
.append("GET")
15+
.append("+")
16+
.append("https://eu.api.ovh.com/v1/" + endPoint)
17+
.append("+")
18+
.append("")
19+
.append("+")
20+
.append(timestamp)
21+
.toString();
22+
try {
23+
return new StringBuilder("$1$").append(hashSHA1(toSign)).toString();
24+
} catch (Exception e) {
25+
e.printStackTrace();
26+
}
27+
return "";
28+
}
29+
30+
private static String hashSHA1(String text)
31+
throws NoSuchAlgorithmException, UnsupportedEncodingException {
32+
MessageDigest md;
33+
md = MessageDigest.getInstance("SHA-1");
34+
byte[] sha1hash = new byte[40];
35+
md.update(text.getBytes("iso-8859-1"), 0, text.length());
36+
sha1hash = md.digest();
37+
StringBuffer sb = new StringBuffer();
38+
for (int i = 0; i < sha1hash.length; i++) {
39+
sb.append(Integer.toString((sha1hash[i] & 0xff) + 0x100, 16).substring(1));
40+
}
41+
return sb.toString();
42+
}
43+
44+
45+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.ovhcloud.sdk.repository;
2+
3+
public record OVHcloudUser(String firstname, String name, String city, String country, String language) {
4+
5+
public String toString() {
6+
return """
7+
First name: %s
8+
Last name: %s
9+
City: %s
10+
Country: %s
11+
Language: %s
12+
""".formatted(firstname, name, city, country, language);
13+
}
14+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.ovhcloud.sdk.service;
2+
3+
4+
import com.ovhcloud.sdk.repository.OVHcloudUser;
5+
import jakarta.ws.rs.GET;
6+
import jakarta.ws.rs.HeaderParam;
7+
import jakarta.ws.rs.Path;
8+
import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam;
9+
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
10+
11+
@Path("/v1")
12+
@RegisterRestClient
13+
@ClientHeaderParam(name = "X-Ovh-Consumer", value = "${ovhcloud.consumer}")
14+
@ClientHeaderParam(name = "X-Ovh-Application", value = "${ovhcloud.application}")
15+
@ClientHeaderParam(name = "Content-Type", value = "application/json")
16+
public interface OVHcloudMe {
17+
18+
@GET
19+
@Path("/me")
20+
OVHcloudUser getMe(@HeaderParam("X-Ovh-Signature") String signature,
21+
@HeaderParam("X-Ovh-Timestamp") String ovhTimestamp);
22+
}
23+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# OVHcloud parameter
2+
ovhcloud.consumer=${OVH_CONSUMER_KEY}
3+
ovhcloud.application=${OVH_APPLICATION_KEY}
4+
ovhcloud.projectId=${OVH_CLOUD_PROJECT_SERVICE}
5+
6+
# RestClient parameter
7+
quarkus.rest-client."com.ovhcloud.sdk.service.OVHcloudMe".url=https://eu.api.ovh.com/

0 commit comments

Comments
 (0)