Skip to content

Commit

Permalink
Merge pull request #1286 from kdubois/main
Browse files Browse the repository at this point in the history
MCP Client - Server sample using SSE transport protocol
  • Loading branch information
geoand authored Feb 14, 2025
2 parents 3e63c73 + 6a51077 commit cbeee42
Show file tree
Hide file tree
Showing 16 changed files with 802 additions and 0 deletions.
19 changes: 19 additions & 0 deletions samples/mcp-sse-client-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# MCP-based client-server example using the SSE transport protocol

This sample showcases how to use a Quarkus MCP server to
provide tools to an LLM. In this case, we use the [SSE transport protocol](https://modelcontextprotocol.io/docs/concepts/transports#server-sent-events-sse) giving the LLM a set of tools to interact with for
weather forecast services.

# Running the sample

Run the sample by starting the mcp server component in the `mcp-server` directory using `mvn quarkus:dev`.
This will start the server on port 8081.

Then start the client component in the `mcp-client` directory using `mvn quarkus:dev`.

# Testing the service

Go to `http://localhost:8080` and interact with the chatbot (click the icon in the bottom left corner to open the chat
window).

You can also use the /alerts endpoint to get weather alerts for a specific state without interacting with a chatbot. For example, you can send a GET request to `/alerts?state=New York` to get alerts for the state of New York.
Empty file.
163 changes: 163 additions & 0 deletions samples/mcp-sse-client-server/mcp-client/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>

<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-sample-mcp-client</artifactId>
<name>Quarkus LangChain4j - Sample - Model Context Protocol Client</name>
<version>1.0-SNAPSHOT</version>

<properties>
<compiler-plugin.version>3.13.0</compiler-plugin.version>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.version>3.18.3</quarkus.platform.version>

<quarkus-langchain4j.version>0.24.0</quarkus-langchain4j.version>

<skipITs>true</skipITs>
<surefire-plugin.version>3.5.0</surefire-plugin.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.quarkus.platform</groupId>
<artifactId>quarkus-bom</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-bom</artifactId>
<version>${quarkus-langchain4j.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-mcp</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-openai</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-websockets-next</artifactId>
</dependency>

<!-- UI -->
<dependency>
<groupId>io.mvnpm</groupId>
<artifactId>importmap</artifactId>
<version>1.0.11</version>
</dependency>
<dependency>
<groupId>org.mvnpm</groupId>
<artifactId>lit</artifactId>
<version>3.2.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mvnpm</groupId>
<artifactId>wc-chatbot</artifactId>
<version>0.2.0</version>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>

</dependencies>
<build>
<plugins>
<plugin>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.platform.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>build</goal>
<goal>generate-code</goal>
<goal>generate-code-tests</goal>
<goal>native-image-agent</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
<configuration>
<systemPropertyVariables>
<native.image.path>
${project.build.directory}/${project.build.finalName}-runner</native.image.path>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>

<profiles>
<profile>
<id>native</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<properties>
<skipITs>false</skipITs>
<quarkus.native.enabled>true</quarkus.native.enabled>
</properties>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.quarkiverse.langchain4j.sample.chatbot;

import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import io.quarkiverse.langchain4j.RegisterAiService;

@RegisterAiService( )
public interface AiWeatherService {

@SystemMessage("You are a weather expert")
@UserMessage("""
Get the most recent weather alerts for a given state with state code {state}
""")
String getWeatherAlerts(String state);

@SystemMessage("You are a weather expert. The user will give you a location, and you should first" +
"get the coordinates for that location, and then based on the coordinates," +
"get the weather for that specific location. " +
"If you can get the US State, then you should also return any weather alerts for the location.")
String getWeather(String message);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.quarkiverse.langchain4j.sample.chatbot;

import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
import io.smallrye.common.annotation.Blocking;

@WebSocket(path = "/chatbot")
public class ChatBotWebSocket {

private final AiWeatherService aiWeatherService;

public ChatBotWebSocket(AiWeatherService aiWeatherService) {
this.aiWeatherService = aiWeatherService;
}

@OnOpen
public String onOpen() {
return "Hello, I am a weather service bot, how can I help?";
}

@OnTextMessage
@Blocking
public String onMessage(String message) {
return aiWeatherService.getWeather(message);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package io.quarkiverse.langchain4j.sample.chatbot;

import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;

import io.mvnpm.importmap.Aggregator;

/**
* Dynamically create the import map
*/
@ApplicationScoped
@Path("/_importmap")
public class ImportmapResource {
private String importmap;

// See https://github.com/WICG/import-maps/issues/235
// This does not seem to be supported by browsers yet...
@GET
@Path("/dynamic.importmap")
@Produces("application/importmap+json")
public String importMap() {
return this.importmap;
}

@GET
@Path("/dynamic-importmap.js")
@Produces("application/javascript")
public String importMapJson() {
return JAVASCRIPT_CODE.formatted(this.importmap);
}

@PostConstruct
void init() {
Aggregator aggregator = new Aggregator();
// Add our own mappings
aggregator.addMapping("icons/", "/icons/");
aggregator.addMapping("components/", "/components/");
aggregator.addMapping("fonts/", "/fonts/");
this.importmap = aggregator.aggregateAsJson();
}

private static final String JAVASCRIPT_CODE = """
const im = document.createElement('script');
im.type = 'importmap';
im.textContent = JSON.stringify(%s);
document.currentScript.after(im);
""";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.quarkiverse.langchain4j.sample.chatbot;

import io.quarkus.logging.Log;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;

@Path("/alerts")
public class MyWeatherResource {

@Inject
AiWeatherService aiWeatherService;

@GET
@Produces(MediaType.TEXT_HTML)
public String getWeatherAlertsForUtah(@QueryParam ("state") String state) {
if (state == null || state.isEmpty()) {
throw new IllegalArgumentException("State parameter is required");
}

String weather = aiWeatherService.getWeatherAlerts(state);
Log.info(weather);
return weather;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {css, LitElement} from 'lit';

export class DemoChat extends LitElement {

_stripHtml(html) {
const div = document.createElement("div");
div.innerHTML = html;
return div.textContent || div.innerText || "";
}

connectedCallback() {
const chatBot = document.getElementsByTagName("chat-bot")[0];

const protocol = (window.location.protocol === 'https:') ? 'wss' : 'ws';
const socket = new WebSocket(protocol + '://' + window.location.host + '/chatbot');

const that = this;
socket.onmessage = function (event) {
chatBot.hideLastLoading();
// LLM response
let lastMessage;
if (chatBot.messages.length > 0) {
lastMessage = chatBot.messages[chatBot.messages.length - 1];
}
if (lastMessage && lastMessage.sender.name === "Bot" && ! lastMessage.loading) {
if (! lastMessage.msg) {
lastMessage.msg = "";
}
lastMessage.msg += event.data;
let bubbles = chatBot.shadowRoot.querySelectorAll("chat-bubble");
let bubble = bubbles.item(bubbles.length - 1);
if (lastMessage.message) {
bubble.innerHTML = that._stripHtml(lastMessage.message) + lastMessage.msg;
} else {
bubble.innerHTML = lastMessage.msg;
}
chatBot.body.scrollTo({ top: chatBot.body.scrollHeight, behavior: 'smooth' })
} else {
chatBot.sendMessage(event.data, {
right: false,
sender: {
name: "Bot"
}
});
}
}

chatBot.addEventListener("sent", function (e) {
if (e.detail.message.sender.name !== "Bot") {
// User message
const msg = that._stripHtml(e.detail.message.message);
socket.send(msg);
chatBot.sendMessage("", {
right: false,
loading: true
});
}
});
}


}

customElements.define('demo-chat', DemoChat);
Loading

0 comments on commit cbeee42

Please sign in to comment.