Skip to content

Commit 0d72f29

Browse files
committed
Integration tests for exposing resources as tools
1 parent 439c972 commit 0d72f29

File tree

7 files changed

+291
-0
lines changed

7 files changed

+291
-0
lines changed

integration-tests/mcp/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@
4444
<artifactId>quarkus-devtools-testing</artifactId>
4545
<scope>test</scope>
4646
</dependency>
47+
<dependency>
48+
<groupId>io.quarkiverse.langchain4j</groupId>
49+
<artifactId>quarkus-langchain4j-openai</artifactId>
50+
<version>${quarkus-langchain4j.version}</version>
51+
<scope>test</scope>
52+
</dependency>
4753

4854
<!-- Make sure the deployment artifact is built before executing this module -->
4955
<dependency>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package io.quarkiverse.langchain4j.mcp.test;
2+
3+
import static io.quarkiverse.langchain4j.mcp.test.McpServerHelper.skipTestsIfJbangNotAvailable;
4+
import static io.quarkiverse.langchain4j.mcp.test.McpServerHelper.startServerHttp;
5+
6+
import org.jboss.shrinkwrap.api.ShrinkWrap;
7+
import org.jboss.shrinkwrap.api.spec.JavaArchive;
8+
import org.junit.jupiter.api.AfterAll;
9+
import org.junit.jupiter.api.BeforeAll;
10+
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
11+
import org.junit.jupiter.api.extension.RegisterExtension;
12+
13+
import io.quarkus.test.QuarkusUnitTest;
14+
15+
@EnabledIfEnvironmentVariable(named = "QUARKUS_LANGCHAIN4J_OPENAI_API_KEY", matches = ".+")
16+
class McpResourcesAsToolsHttpTransportTest extends McpResourcesAsToolsTestBase {
17+
18+
private static Process processAlice;
19+
private static Process processBob;
20+
21+
@RegisterExtension
22+
static QuarkusUnitTest unitTest = new QuarkusUnitTest()
23+
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
24+
.addClasses(McpServerHelper.class))
25+
.overrideConfigKey("quarkus.langchain4j.mcp.alice.transport-type", "http")
26+
.overrideConfigKey("quarkus.langchain4j.mcp.alice.url", "http://localhost:8180/mcp/sse")
27+
.overrideConfigKey("quarkus.langchain4j.mcp.alice.log-requests", "true")
28+
.overrideConfigKey("quarkus.langchain4j.mcp.alice.log-responses", "true")
29+
.overrideConfigKey("quarkus.langchain4j.mcp.bob.transport-type", "http")
30+
.overrideConfigKey("quarkus.langchain4j.mcp.bob.url", "http://localhost:8181/mcp/sse")
31+
.overrideConfigKey("quarkus.langchain4j.mcp.bob.log-requests", "true")
32+
.overrideConfigKey("quarkus.langchain4j.mcp.bob.log-responses", "true")
33+
.overrideConfigKey("quarkus.langchain4j.mcp.expose-resources-as-tools", "true")
34+
.overrideConfigKey("quarkus.log.category.\"io.quarkiverse\".level", "DEBUG");
35+
36+
@BeforeAll
37+
static void setup() throws Exception {
38+
skipTestsIfJbangNotAvailable();
39+
processAlice = startServerHttp("resources_alice_mcp_server.java", 8180);
40+
processBob = startServerHttp("resources_bob_mcp_server.java", 8181);
41+
}
42+
43+
@AfterAll
44+
static void teardown() throws Exception {
45+
if (processAlice != null && processAlice.isAlive()) {
46+
processAlice.destroyForcibly();
47+
}
48+
if (processBob != null && processBob.isAlive()) {
49+
processBob.destroyForcibly();
50+
}
51+
}
52+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package io.quarkiverse.langchain4j.mcp.test;
2+
3+
import static io.quarkiverse.langchain4j.mcp.test.McpServerHelper.skipTestsIfJbangNotAvailable;
4+
5+
import org.jboss.shrinkwrap.api.ShrinkWrap;
6+
import org.jboss.shrinkwrap.api.spec.JavaArchive;
7+
import org.junit.jupiter.api.BeforeAll;
8+
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
9+
import org.junit.jupiter.api.extension.RegisterExtension;
10+
11+
import io.quarkus.test.QuarkusUnitTest;
12+
13+
@EnabledIfEnvironmentVariable(named = "QUARKUS_LANGCHAIN4J_OPENAI_API_KEY", matches = ".+")
14+
class McpResourcesAsToolsStdioTransportTest extends McpResourcesAsToolsTestBase {
15+
16+
@RegisterExtension
17+
static QuarkusUnitTest unitTest = new QuarkusUnitTest()
18+
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
19+
.addClasses(McpServerHelper.class))
20+
.overrideConfigKey("quarkus.langchain4j.mcp.alice.transport-type", "stdio")
21+
.overrideConfigKey("quarkus.langchain4j.mcp.alice.command",
22+
"jbang,--quiet,--fresh,run,-Dquarkus.http.port=7777,src/test/resources/resources_alice_mcp_server.java")
23+
.overrideConfigKey("quarkus.langchain4j.mcp.alice.log-requests", "true")
24+
.overrideConfigKey("quarkus.langchain4j.mcp.alice.log-responses", "true")
25+
.overrideConfigKey("quarkus.langchain4j.mcp.bob.transport-type", "stdio")
26+
.overrideConfigKey("quarkus.langchain4j.mcp.bob.command",
27+
"jbang,--quiet,--fresh,run,-Dquarkus.http.port=7778,src/test/resources/resources_bob_mcp_server.java")
28+
.overrideConfigKey("quarkus.langchain4j.mcp.bob.log-requests", "true")
29+
.overrideConfigKey("quarkus.langchain4j.mcp.bob.log-responses", "true")
30+
.overrideConfigKey("quarkus.langchain4j.mcp.expose-resources-as-tools", "true")
31+
.overrideConfigKey("quarkus.log.category.\"io.quarkiverse\".level", "DEBUG");
32+
33+
@BeforeAll
34+
static void setup() {
35+
skipTestsIfJbangNotAvailable();
36+
}
37+
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package io.quarkiverse.langchain4j.mcp.test;
2+
3+
import static io.quarkiverse.langchain4j.mcp.test.McpServerHelper.skipTestsIfJbangNotAvailable;
4+
import static io.quarkiverse.langchain4j.mcp.test.McpServerHelper.startServerHttp;
5+
6+
import org.jboss.shrinkwrap.api.ShrinkWrap;
7+
import org.jboss.shrinkwrap.api.spec.JavaArchive;
8+
import org.junit.jupiter.api.AfterAll;
9+
import org.junit.jupiter.api.BeforeAll;
10+
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
11+
import org.junit.jupiter.api.extension.RegisterExtension;
12+
13+
import io.quarkus.test.QuarkusUnitTest;
14+
15+
@EnabledIfEnvironmentVariable(named = "QUARKUS_LANGCHAIN4J_OPENAI_API_KEY", matches = ".+")
16+
class McpResourcesAsToolsStreamableHttpTransportTest extends McpResourcesAsToolsTestBase {
17+
18+
@RegisterExtension
19+
static QuarkusUnitTest unitTest = new QuarkusUnitTest()
20+
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
21+
.addClasses(McpServerHelper.class))
22+
.overrideConfigKey("quarkus.langchain4j.mcp.alice.transport-type", "streamable-http")
23+
.overrideConfigKey("quarkus.langchain4j.mcp.alice.url",
24+
"http://localhost:8180/mcp")
25+
.overrideConfigKey("quarkus.langchain4j.mcp.alice.log-requests", "true")
26+
.overrideConfigKey("quarkus.langchain4j.mcp.alice.log-responses", "true")
27+
.overrideConfigKey("quarkus.langchain4j.mcp.bob.transport-type", "streamable-http")
28+
.overrideConfigKey("quarkus.langchain4j.mcp.bob.url",
29+
"http://localhost:8181/mcp")
30+
.overrideConfigKey("quarkus.langchain4j.mcp.bob.log-requests", "true")
31+
.overrideConfigKey("quarkus.langchain4j.mcp.bob.log-responses", "true")
32+
.overrideConfigKey("quarkus.langchain4j.mcp.expose-resources-as-tools", "true")
33+
.overrideConfigKey("quarkus.log.category.\"io.quarkiverse\".level", "DEBUG");
34+
35+
private static Process processAlice;
36+
private static Process processBob;
37+
38+
@BeforeAll
39+
static void setup() throws Exception {
40+
skipTestsIfJbangNotAvailable();
41+
processAlice = startServerHttp("resources_alice_mcp_server.java", 8180);
42+
processBob = startServerHttp("resources_bob_mcp_server.java", 8181);
43+
}
44+
45+
@AfterAll
46+
static void teardown() throws Exception {
47+
if (processAlice != null && processAlice.isAlive()) {
48+
processAlice.destroyForcibly();
49+
}
50+
if (processBob != null && processBob.isAlive()) {
51+
processBob.destroyForcibly();
52+
}
53+
}
54+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package io.quarkiverse.langchain4j.mcp.test;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.util.List;
6+
import java.util.stream.Collectors;
7+
8+
import jakarta.enterprise.context.ApplicationScoped;
9+
import jakarta.inject.Inject;
10+
11+
import org.junit.jupiter.api.Test;
12+
13+
import com.fasterxml.jackson.databind.node.ArrayNode;
14+
15+
import dev.langchain4j.agent.tool.ToolExecutionRequest;
16+
import dev.langchain4j.agent.tool.ToolSpecification;
17+
import dev.langchain4j.internal.Json;
18+
import dev.langchain4j.mcp.McpToolProvider;
19+
import dev.langchain4j.mcp.client.McpClient;
20+
import dev.langchain4j.mcp.resourcesastools.DefaultMcpResourcesAsToolsPresenter;
21+
import dev.langchain4j.model.chat.ChatModel;
22+
import dev.langchain4j.service.SystemMessage;
23+
import dev.langchain4j.service.tool.ToolProvider;
24+
import dev.langchain4j.service.tool.ToolProviderResult;
25+
import io.quarkiverse.langchain4j.RegisterAiService;
26+
import io.quarkiverse.langchain4j.mcp.runtime.McpClientName;
27+
28+
public abstract class McpResourcesAsToolsTestBase {
29+
30+
@Inject
31+
@McpClientName("alice")
32+
McpClient mcpClientAlice;
33+
34+
@Inject
35+
@McpClientName("bob")
36+
McpClient mcpClientBob;
37+
38+
@Inject
39+
ChatModel chatModel;
40+
41+
@Inject
42+
ChatService service;
43+
44+
@Test
45+
public void listResourcesAndThenGet() {
46+
ToolProvider toolProvider = McpToolProvider.builder()
47+
.mcpClients(mcpClientAlice)
48+
.resourcesAsToolsPresenter(
49+
DefaultMcpResourcesAsToolsPresenter.builder().build())
50+
.build();
51+
52+
// check that the tool provider has two tools: list_resources and get_resource
53+
ToolProviderResult toolProviderResult = toolProvider.provideTools(null);
54+
assertThat(toolProviderResult.tools()).hasSize(2);
55+
List<String> toolNames = toolProviderResult.tools().keySet().stream()
56+
.map(ToolSpecification::name)
57+
.collect(Collectors.toList());
58+
assertThat(toolNames).containsExactlyInAnyOrder("list_resources", "get_resource");
59+
60+
// call the list_resources tool and verify the output
61+
String listResourcesResult = toolProviderResult.toolExecutorByName("list_resources").execute(null, null);
62+
ArrayNode resources = Json.fromJson(listResourcesResult, ArrayNode.class);
63+
assertThat(resources.size()).isEqualTo(1);
64+
assertThat(resources.get(0).get("mcpServer").asText()).isEqualTo("alice");
65+
assertThat(resources.get(0).get("uri").asText()).isEqualTo("file:///info");
66+
assertThat(resources.get(0).get("uriTemplate").isNull()).isTrue();
67+
assertThat(resources.get(0).get("name").asText()).isEqualTo("basicInfo");
68+
assertThat(resources.get(0).get("description").asText()).isEqualTo("Basic information about Alice");
69+
assertThat(resources.get(0).get("mimeType").asText()).isEqualTo("text/plain");
70+
71+
// call the get_resource tool
72+
ToolExecutionRequest request = ToolExecutionRequest.builder()
73+
.name("get_resource")
74+
.arguments("{\"mcpServer\": \"alice\", \"uri\": \"file:///info\"}")
75+
.build();
76+
String getBasicInfoResult = toolProviderResult.toolExecutorByName("get_resource").execute(request, null);
77+
assertThat(getBasicInfoResult).isEqualTo("Alice was born in 1962 and lives in Manchester.");
78+
}
79+
80+
@Test
81+
public void integrationTestWithAiService() {
82+
String aliceResponse = service.chat("When was Alice born?");
83+
assertThat(aliceResponse).contains("1962");
84+
85+
String bobResponse = service.chat("When was Bob born?");
86+
assertThat(bobResponse).contains("1956");
87+
}
88+
89+
@RegisterAiService
90+
@ApplicationScoped
91+
interface ChatService {
92+
@SystemMessage("Use list_resources and get_resource tools to answer user's questions")
93+
String chat(String prompt);
94+
}
95+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
///usr/bin/env jbang "$0" "$@" ; exit $?
2+
//DEPS io.quarkus:quarkus-bom:${quarkus.version:3.25.0}@pom
3+
//DEPS io.quarkiverse.mcp:quarkus-mcp-server-stdio:1.4.0
4+
//DEPS io.quarkiverse.mcp:quarkus-mcp-server-sse:1.4.0
5+
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
import java.util.concurrent.TimeUnit;
9+
10+
import io.quarkiverse.mcp.server.BlobResourceContents;
11+
import io.quarkiverse.mcp.server.RequestUri;
12+
import io.quarkiverse.mcp.server.Resource;
13+
import io.quarkiverse.mcp.server.ResourceTemplate;
14+
import io.quarkiverse.mcp.server.TextResourceContents;
15+
16+
public class resources_alice_mcp_server {
17+
18+
@Resource(uri = "file:///info", description = "Basic information about Alice", mimeType = "text/plain")
19+
TextResourceContents basicInfo() {
20+
return TextResourceContents.create("file:///info", "Alice was born in 1962 and lives in Manchester.");
21+
}
22+
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
///usr/bin/env jbang "$0" "$@" ; exit $?
2+
//DEPS io.quarkus:quarkus-bom:${quarkus.version:3.25.0}@pom
3+
//DEPS io.quarkiverse.mcp:quarkus-mcp-server-stdio:1.4.0
4+
//DEPS io.quarkiverse.mcp:quarkus-mcp-server-sse:1.4.0
5+
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
import java.util.concurrent.TimeUnit;
9+
10+
import io.quarkiverse.mcp.server.BlobResourceContents;
11+
import io.quarkiverse.mcp.server.RequestUri;
12+
import io.quarkiverse.mcp.server.Resource;
13+
import io.quarkiverse.mcp.server.ResourceTemplate;
14+
import io.quarkiverse.mcp.server.TextResourceContents;
15+
16+
public class resources_bob_mcp_server {
17+
18+
@Resource(uri = "file:///info", description = "Basic information about Bob", mimeType = "text/plain")
19+
TextResourceContents basicInfo() {
20+
return TextResourceContents.create("file:///info", "Bob was born in 1956 and lives in London.");
21+
}
22+
23+
}

0 commit comments

Comments
 (0)