Skip to content

Commit 0d53045

Browse files
committed
TLS integration for MCP clients
1 parent 32b88e4 commit 0d53045

File tree

13 files changed

+425
-15
lines changed

13 files changed

+425
-15
lines changed

docs/modules/ROOT/pages/includes/quarkus-langchain4j-mcp.adoc

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,27 @@ endif::add-copy-button-to-env-var[]
7272
|boolean
7373
|`true`
7474

75+
a| [[quarkus-langchain4j-mcp_quarkus-langchain4j-mcp-expose-resources-as-tools]] [.property-path]##link:#quarkus-langchain4j-mcp_quarkus-langchain4j-mcp-expose-resources-as-tools[`quarkus.langchain4j.mcp.expose-resources-as-tools`]##
76+
ifdef::add-copy-button-to-config-props[]
77+
config_property_copy_button:+++quarkus.langchain4j.mcp.expose-resources-as-tools+++[]
78+
endif::add-copy-button-to-config-props[]
79+
80+
81+
[.description]
82+
--
83+
Whether resources should be exposed as MCP tools.
84+
85+
86+
ifdef::add-copy-button-to-env-var[]
87+
Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_MCP_EXPOSE_RESOURCES_AS_TOOLS+++[]
88+
endif::add-copy-button-to-env-var[]
89+
ifndef::add-copy-button-to-env-var[]
90+
Environment variable: `+++QUARKUS_LANGCHAIN4J_MCP_EXPOSE_RESOURCES_AS_TOOLS+++`
91+
endif::add-copy-button-to-env-var[]
92+
--
93+
|boolean
94+
|`false`
95+
7596
h|[[quarkus-langchain4j-mcp_section_quarkus-langchain4j-mcp]] [.section-name.section-level0]##link:#quarkus-langchain4j-mcp_section_quarkus-langchain4j-mcp[Configured MCP clients]##
7697
h|Type
7798
h|Default
@@ -265,6 +286,48 @@ endif::add-copy-button-to-env-var[]
265286
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-quarkus-langchain4j-mcp_quarkus-langchain4j[icon:question-circle[title=More information about the Duration format]]
266287
|`10S`
267288

289+
a| [[quarkus-langchain4j-mcp_quarkus-langchain4j-mcp-client-name-roots]] [.property-path]##link:#quarkus-langchain4j-mcp_quarkus-langchain4j-mcp-client-name-roots[`quarkus.langchain4j.mcp."client-name".roots`]##
290+
ifdef::add-copy-button-to-config-props[]
291+
config_property_copy_button:+++quarkus.langchain4j.mcp."client-name".roots+++[]
292+
endif::add-copy-button-to-config-props[]
293+
294+
295+
[.description]
296+
--
297+
The initial list of MCP roots that the client can present to the server. The list can be later updated programmatically during runtime. The list is formatted as key-value pairs separated by commas. For example: workspace1=/path/to/workspace1,workspace2=/path/to/workspace2
298+
299+
300+
ifdef::add-copy-button-to-env-var[]
301+
Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_MCP__CLIENT_NAME__ROOTS+++[]
302+
endif::add-copy-button-to-env-var[]
303+
ifndef::add-copy-button-to-env-var[]
304+
Environment variable: `+++QUARKUS_LANGCHAIN4J_MCP__CLIENT_NAME__ROOTS+++`
305+
endif::add-copy-button-to-env-var[]
306+
--
307+
|list of string
308+
|
309+
310+
a| [[quarkus-langchain4j-mcp_quarkus-langchain4j-mcp-client-name-tls-configuration-name]] [.property-path]##link:#quarkus-langchain4j-mcp_quarkus-langchain4j-mcp-client-name-tls-configuration-name[`quarkus.langchain4j.mcp."client-name".tls-configuration-name`]##
311+
ifdef::add-copy-button-to-config-props[]
312+
config_property_copy_button:+++quarkus.langchain4j.mcp."client-name".tls-configuration-name+++[]
313+
endif::add-copy-button-to-config-props[]
314+
315+
316+
[.description]
317+
--
318+
The name of the TLS configuration (bucket) used for client authentication in the TLS registry. This does not have any effect when the stdio transport is used.
319+
320+
321+
ifdef::add-copy-button-to-env-var[]
322+
Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_MCP__CLIENT_NAME__TLS_CONFIGURATION_NAME+++[]
323+
endif::add-copy-button-to-env-var[]
324+
ifndef::add-copy-button-to-env-var[]
325+
Environment variable: `+++QUARKUS_LANGCHAIN4J_MCP__CLIENT_NAME__TLS_CONFIGURATION_NAME+++`
326+
endif::add-copy-button-to-env-var[]
327+
--
328+
|string
329+
|
330+
268331

269332
|===
270333

docs/modules/ROOT/pages/includes/quarkus-langchain4j-mcp_quarkus.langchain4j.adoc

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,27 @@ endif::add-copy-button-to-env-var[]
7272
|boolean
7373
|`true`
7474

75+
a| [[quarkus-langchain4j-mcp_quarkus-langchain4j-mcp-expose-resources-as-tools]] [.property-path]##link:#quarkus-langchain4j-mcp_quarkus-langchain4j-mcp-expose-resources-as-tools[`quarkus.langchain4j.mcp.expose-resources-as-tools`]##
76+
ifdef::add-copy-button-to-config-props[]
77+
config_property_copy_button:+++quarkus.langchain4j.mcp.expose-resources-as-tools+++[]
78+
endif::add-copy-button-to-config-props[]
79+
80+
81+
[.description]
82+
--
83+
Whether resources should be exposed as MCP tools.
84+
85+
86+
ifdef::add-copy-button-to-env-var[]
87+
Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_MCP_EXPOSE_RESOURCES_AS_TOOLS+++[]
88+
endif::add-copy-button-to-env-var[]
89+
ifndef::add-copy-button-to-env-var[]
90+
Environment variable: `+++QUARKUS_LANGCHAIN4J_MCP_EXPOSE_RESOURCES_AS_TOOLS+++`
91+
endif::add-copy-button-to-env-var[]
92+
--
93+
|boolean
94+
|`false`
95+
7596
h|[[quarkus-langchain4j-mcp_section_quarkus-langchain4j-mcp]] [.section-name.section-level0]##link:#quarkus-langchain4j-mcp_section_quarkus-langchain4j-mcp[Configured MCP clients]##
7697
h|Type
7798
h|Default
@@ -265,6 +286,48 @@ endif::add-copy-button-to-env-var[]
265286
|link:https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/Duration.html[Duration] link:#duration-note-anchor-quarkus-langchain4j-mcp_quarkus-langchain4j[icon:question-circle[title=More information about the Duration format]]
266287
|`10S`
267288

289+
a| [[quarkus-langchain4j-mcp_quarkus-langchain4j-mcp-client-name-roots]] [.property-path]##link:#quarkus-langchain4j-mcp_quarkus-langchain4j-mcp-client-name-roots[`quarkus.langchain4j.mcp."client-name".roots`]##
290+
ifdef::add-copy-button-to-config-props[]
291+
config_property_copy_button:+++quarkus.langchain4j.mcp."client-name".roots+++[]
292+
endif::add-copy-button-to-config-props[]
293+
294+
295+
[.description]
296+
--
297+
The initial list of MCP roots that the client can present to the server. The list can be later updated programmatically during runtime. The list is formatted as key-value pairs separated by commas. For example: workspace1=/path/to/workspace1,workspace2=/path/to/workspace2
298+
299+
300+
ifdef::add-copy-button-to-env-var[]
301+
Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_MCP__CLIENT_NAME__ROOTS+++[]
302+
endif::add-copy-button-to-env-var[]
303+
ifndef::add-copy-button-to-env-var[]
304+
Environment variable: `+++QUARKUS_LANGCHAIN4J_MCP__CLIENT_NAME__ROOTS+++`
305+
endif::add-copy-button-to-env-var[]
306+
--
307+
|list of string
308+
|
309+
310+
a| [[quarkus-langchain4j-mcp_quarkus-langchain4j-mcp-client-name-tls-configuration-name]] [.property-path]##link:#quarkus-langchain4j-mcp_quarkus-langchain4j-mcp-client-name-tls-configuration-name[`quarkus.langchain4j.mcp."client-name".tls-configuration-name`]##
311+
ifdef::add-copy-button-to-config-props[]
312+
config_property_copy_button:+++quarkus.langchain4j.mcp."client-name".tls-configuration-name+++[]
313+
endif::add-copy-button-to-config-props[]
314+
315+
316+
[.description]
317+
--
318+
The name of the TLS configuration (bucket) used for client authentication in the TLS registry. This does not have any effect when the stdio transport is used.
319+
320+
321+
ifdef::add-copy-button-to-env-var[]
322+
Environment variable: env_var_with_copy_button:+++QUARKUS_LANGCHAIN4J_MCP__CLIENT_NAME__TLS_CONFIGURATION_NAME+++[]
323+
endif::add-copy-button-to-env-var[]
324+
ifndef::add-copy-button-to-env-var[]
325+
Environment variable: `+++QUARKUS_LANGCHAIN4J_MCP__CLIENT_NAME__TLS_CONFIGURATION_NAME+++`
326+
endif::add-copy-button-to-env-var[]
327+
--
328+
|string
329+
|
330+
268331

269332
|===
270333

integration-tests/mcp/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@
5050
<version>${quarkus-langchain4j.version}</version>
5151
<scope>test</scope>
5252
</dependency>
53+
<dependency>
54+
<groupId>io.smallrye.certs</groupId>
55+
<artifactId>smallrye-certificate-generator-junit5</artifactId>
56+
<version>${smallrye-certificate-generator.version}</version>
57+
<scope>test</scope>
58+
</dependency>
5359

5460
<!-- Make sure the deployment artifact is built before executing this module -->
5561
<dependency>

integration-tests/mcp/src/test/java/io/quarkiverse/langchain4j/mcp/test/McpServerHelper.java

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import static org.junit.jupiter.api.Assumptions.assumeTrue;
44

55
import java.io.File;
6+
import java.util.ArrayList;
67
import java.util.Arrays;
8+
import java.util.List;
79
import java.util.concurrent.TimeUnit;
810
import java.util.concurrent.TimeoutException;
911

@@ -19,15 +21,26 @@ public class McpServerHelper {
1921

2022
private static final Logger log = LoggerFactory.getLogger(McpServerHelper.class);
2123

22-
static Process startServerHttp(String scriptName) throws Exception {
24+
public static Process startServerHttp(String scriptName) throws Exception {
2325
return startServerHttp(scriptName, 8082);
2426
}
2527

26-
static Process startServerHttp(String scriptName, int port) throws Exception {
28+
public static Process startServerHttp(String scriptName, int port) throws Exception {
29+
return startServerHttp(scriptName, port, port + 1000, new String[] {});
30+
}
31+
32+
public static Process startServerHttp(String scriptName, int port, int sslPort, String[] extraArgs) throws Exception {
2733
skipTestsIfJbangNotAvailable();
2834
String path = getPathToScript(scriptName);
29-
String[] command = new String[] { getJBangCommand(), "--quiet", "--fresh", "run", "-Dquarkus.http.port=" + port, path };
30-
log.info("Starting the MCP server using command: " + Arrays.toString(command));
35+
List<String> command = new ArrayList<>();
36+
command.add(getJBangCommand());
37+
command.add("--quiet");
38+
command.add("--fresh");
39+
command.addAll(Arrays.asList(extraArgs));
40+
command.add("-Dquarkus.http.ssl-port=" + sslPort);
41+
command.add("-Dquarkus.http.port=" + port);
42+
command.add(path);
43+
log.info("Starting the MCP server using command: " + command);
3144
Process process = new ProcessBuilder().command(command).inheritIO().start();
3245
waitForPort(port, 120);
3346
log.info("MCP server has started");
@@ -49,7 +62,7 @@ static String getJBangCommand() {
4962
return command;
5063
}
5164

52-
static void skipTestsIfJbangNotAvailable() {
65+
public static void skipTestsIfJbangNotAvailable() {
5366
String command = getJBangCommand();
5467
try {
5568
new ProcessBuilder().command(command, "--version").start().waitFor();
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package io.quarkiverse.langchain4j.mcp.test.tls;
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.extension.RegisterExtension;
11+
12+
import io.quarkiverse.langchain4j.mcp.test.McpServerHelper;
13+
import io.quarkus.test.QuarkusUnitTest;
14+
import io.smallrye.certs.Format;
15+
import io.smallrye.certs.junit5.Certificate;
16+
import io.smallrye.certs.junit5.Certificates;
17+
18+
@Certificates(baseDir = "target/certs", certificates = {
19+
@Certificate(name = "mcp", password = "password", formats = { Format.PKCS12 }, client = true),
20+
@Certificate(name = "mcp-bad", password = "password", formats = { Format.PKCS12 }, client = true)
21+
})
22+
class McpTlsHttpTransportTest extends McpTlsTestBase {
23+
24+
private static Process process;
25+
26+
@RegisterExtension
27+
static QuarkusUnitTest unitTest = new QuarkusUnitTest()
28+
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
29+
.addClasses(McpServerHelper.class))
30+
.overrideConfigKey("quarkus.tls.tls-client-correct.key-store.p12.path", "target/certs/mcp-client-keystore.p12")
31+
.overrideConfigKey("quarkus.tls.tls-client-correct.key-store.p12.password", "password")
32+
.overrideConfigKey("quarkus.tls.tls-client-correct.trust-store.p12.path", "target/certs/mcp-client-truststore.p12")
33+
.overrideConfigKey("quarkus.tls.tls-client-correct.trust-store.p12.password", "password")
34+
.overrideConfigKey("quarkus.langchain4j.mcp.client1.transport-type", "http")
35+
.overrideConfigKey("quarkus.langchain4j.mcp.client1.url", "https://localhost:8083/mcp/sse")
36+
.overrideConfigKey("quarkus.langchain4j.mcp.client1.log-requests", "true")
37+
.overrideConfigKey("quarkus.langchain4j.mcp.client1.log-responses", "true")
38+
.overrideConfigKey("quarkus.langchain4j.mcp.client1.tls-configuration-name", "tls-client-correct")
39+
// 'tls-client-bad' and therefore MCP client 'client2' is using a truststore that does not trust the server
40+
.overrideConfigKey("quarkus.tls.tls-client-bad.key-store.p12.path", "target/certs/mcp-client-keystore.p12")
41+
.overrideConfigKey("quarkus.tls.tls-client-bad.key-store.p12.password", "password")
42+
.overrideConfigKey("quarkus.tls.tls-client-bad.trust-store.p12.path", "target/certs/mcp-bad-client-truststore.p12")
43+
.overrideConfigKey("quarkus.tls.tls-client-bad.trust-store.p12.password", "password")
44+
.overrideConfigKey("quarkus.langchain4j.mcp.client2.transport-type", "http")
45+
.overrideConfigKey("quarkus.langchain4j.mcp.client2.url", "https://localhost:8083/mcp/sse")
46+
.overrideConfigKey("quarkus.langchain4j.mcp.client2.log-requests", "true")
47+
.overrideConfigKey("quarkus.langchain4j.mcp.client2.log-responses", "true")
48+
.overrideConfigKey("quarkus.langchain4j.mcp.client2.tool-execution-timeout", "3s")
49+
.overrideConfigKey("quarkus.langchain4j.mcp.client2.tls-configuration-name", "tls-client-bad")
50+
51+
.overrideConfigKey("quarkus.log.category.\"io.quarkiverse\".level", "DEBUG");
52+
53+
@BeforeAll
54+
static void setup() throws Exception {
55+
skipTestsIfJbangNotAvailable();
56+
String[] serverTlsConfiguration = new String[] {
57+
"-Dquarkus.tls.key-store.p12.path=target/certs/mcp-keystore.p12",
58+
"-Dquarkus.tls.key-store.p12.password=password",
59+
"-Dquarkus.tls.trust-store.p12.path=target/certs/mcp-server-truststore.p12",
60+
"-Dquarkus.tls.trust-store.p12.password=password" };
61+
process = McpServerHelper.startServerHttp("tls_mcp_server.java", 8082, 8083, serverTlsConfiguration);
62+
}
63+
64+
@AfterAll
65+
static void teardown() {
66+
if (process != null && process.isAlive()) {
67+
process.destroyForcibly();
68+
}
69+
}
70+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package io.quarkiverse.langchain4j.mcp.test.tls;
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.AfterAll;
8+
import org.junit.jupiter.api.BeforeAll;
9+
import org.junit.jupiter.api.extension.RegisterExtension;
10+
11+
import io.quarkiverse.langchain4j.mcp.test.McpServerHelper;
12+
import io.quarkus.test.QuarkusUnitTest;
13+
import io.smallrye.certs.Format;
14+
import io.smallrye.certs.junit5.Certificate;
15+
import io.smallrye.certs.junit5.Certificates;
16+
17+
@Certificates(baseDir = "target/certs", certificates = {
18+
@Certificate(name = "mcp", password = "password", formats = { Format.PKCS12 }, client = true),
19+
@Certificate(name = "mcp-bad", password = "password", formats = { Format.PKCS12 }, client = true)
20+
})
21+
class McpTlsStreamableHttpTransportTest extends McpTlsTestBase {
22+
23+
private static Process process;
24+
25+
@RegisterExtension
26+
static QuarkusUnitTest unitTest = new QuarkusUnitTest()
27+
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
28+
.addClasses(McpServerHelper.class))
29+
.overrideConfigKey("quarkus.tls.tls-client-correct.key-store.p12.path", "target/certs/mcp-client-keystore.p12")
30+
.overrideConfigKey("quarkus.tls.tls-client-correct.key-store.p12.password", "password")
31+
.overrideConfigKey("quarkus.tls.tls-client-correct.trust-store.p12.path", "target/certs/mcp-client-truststore.p12")
32+
.overrideConfigKey("quarkus.tls.tls-client-correct.trust-store.p12.password", "password")
33+
.overrideConfigKey("quarkus.langchain4j.mcp.client1.transport-type", "streamable-http")
34+
.overrideConfigKey("quarkus.langchain4j.mcp.client1.url", "https://localhost:8083/mcp")
35+
.overrideConfigKey("quarkus.langchain4j.mcp.client1.log-requests", "true")
36+
.overrideConfigKey("quarkus.langchain4j.mcp.client1.log-responses", "true")
37+
.overrideConfigKey("quarkus.langchain4j.mcp.client1.tls-configuration-name", "tls-client-correct")
38+
// 'tls-client-bad' and therefore MCP client 'client2' is using a truststore that does not trust the server
39+
.overrideConfigKey("quarkus.tls.tls-client-bad.key-store.p12.path", "target/certs/mcp-client-keystore.p12")
40+
.overrideConfigKey("quarkus.tls.tls-client-bad.key-store.p12.password", "password")
41+
.overrideConfigKey("quarkus.tls.tls-client-bad.trust-store.p12.path", "target/certs/mcp-bad-client-truststore.p12")
42+
.overrideConfigKey("quarkus.tls.tls-client-bad.trust-store.p12.password", "password")
43+
.overrideConfigKey("quarkus.langchain4j.mcp.client2.transport-type", "streamable-http")
44+
.overrideConfigKey("quarkus.langchain4j.mcp.client2.url", "https://localhost:8083/mcp")
45+
.overrideConfigKey("quarkus.langchain4j.mcp.client2.log-requests", "true")
46+
.overrideConfigKey("quarkus.langchain4j.mcp.client2.log-responses", "true")
47+
.overrideConfigKey("quarkus.langchain4j.mcp.client2.tls-configuration-name", "tls-client-bad")
48+
49+
.overrideConfigKey("quarkus.log.category.\"io.quarkiverse\".level", "DEBUG");
50+
51+
@BeforeAll
52+
static void setup() throws Exception {
53+
skipTestsIfJbangNotAvailable();
54+
String[] serverTlsConfiguration = new String[] {
55+
"-Dquarkus.tls.key-store.p12.path=target/certs/mcp-keystore.p12",
56+
"-Dquarkus.tls.key-store.p12.password=password",
57+
"-Dquarkus.tls.trust-store.p12.path=target/certs/mcp-server-truststore.p12",
58+
"-Dquarkus.tls.trust-store.p12.password=password" };
59+
process = McpServerHelper.startServerHttp("tls_mcp_server.java", 8082, 8083, serverTlsConfiguration);
60+
}
61+
62+
@AfterAll
63+
static void teardown() {
64+
if (process != null && process.isAlive()) {
65+
process.destroyForcibly();
66+
}
67+
}
68+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package io.quarkiverse.langchain4j.mcp.test.tls;
2+
3+
import jakarta.inject.Inject;
4+
5+
import org.assertj.core.api.Assertions;
6+
import org.junit.jupiter.api.Test;
7+
8+
import dev.langchain4j.agent.tool.ToolExecutionRequest;
9+
import dev.langchain4j.mcp.client.*;
10+
import io.quarkiverse.langchain4j.mcp.runtime.McpClientName;
11+
12+
public abstract class McpTlsTestBase {
13+
14+
@Inject
15+
@McpClientName("client1")
16+
McpClient mcpClient;
17+
18+
@Inject
19+
@McpClientName("client2")
20+
McpClient clientWithBadTrustStore;
21+
22+
@Test
23+
void testAuthenticationSuccessful() throws Exception {
24+
ToolExecutionRequest toolExecutionRequest = ToolExecutionRequest.builder()
25+
.name("echoString")
26+
.arguments("{\"input\": \"abc\"}")
27+
.build();
28+
Assertions.assertThat(mcpClient.executeTool(toolExecutionRequest)).isEqualTo("abc");
29+
}
30+
31+
@Test
32+
void testClientWithBadTrustStore() throws Exception {
33+
ToolExecutionRequest toolExecutionRequest = ToolExecutionRequest.builder()
34+
.name("echoString")
35+
.arguments("{\"input\": \"abc\"}")
36+
.build();
37+
Assertions.assertThatThrownBy(() -> clientWithBadTrustStore.executeTool(toolExecutionRequest))
38+
.isNotNull();
39+
}
40+
41+
}

0 commit comments

Comments
 (0)