diff --git a/core/src/main/java/org/testcontainers/containers/DockerMcpGatewayContainer.java b/core/src/main/java/org/testcontainers/containers/DockerMcpGatewayContainer.java new file mode 100644 index 00000000000..592b02cb0ad --- /dev/null +++ b/core/src/main/java/org/testcontainers/containers/DockerMcpGatewayContainer.java @@ -0,0 +1,105 @@ +package org.testcontainers.containers; + +import org.testcontainers.DockerClientFactory; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.images.builder.Transferable; +import org.testcontainers.utility.DockerImageName; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Testcontainers implementation of the Docker MCP Gateway container. + *

+ * Supported images: {@code docker/agents_gateway} + *

+ * Exposed ports: 8811 + */ +public class DockerMcpGatewayContainer extends GenericContainer { + + private static final String DOCKER_AGENT_GATEWAY_IMAGE = "docker/agents_gateway"; + + private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(DOCKER_AGENT_GATEWAY_IMAGE); + + private static final int DEFAULT_PORT = 8811; + + private static final String SECRETS_PATH = "/testcontainers/app/secrets"; + + private final List servers = new ArrayList<>(); + + private final List tools = new ArrayList<>(); + + private final Map secrets = new HashMap<>(); + + public DockerMcpGatewayContainer(String dockerImageName) { + this(DockerImageName.parse(dockerImageName)); + } + + public DockerMcpGatewayContainer(DockerImageName dockerImageName) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + withExposedPorts(DEFAULT_PORT); + withFileSystemBind(DockerClientFactory.instance().getRemoteDockerUnixSocketPath(), "/var/run/docker.sock"); + waitingFor(Wait.forLogMessage(".*Start sse server on port.*", 1)); + } + + @Override + protected void configure() { + List command = new ArrayList<>(); + command.add("--transport=sse"); + for (String server : this.servers) { + if (!server.isEmpty()) { + command.add("--servers=" + server); + } + } + for (String tool : this.tools) { + if (!tool.isEmpty()) { + command.add("--tools=" + tool); + } + } + if (this.secrets != null && !this.secrets.isEmpty()) { + command.add("--secrets=" + SECRETS_PATH); + } + withCommand(String.join(" ", command)); + } + + @Override + protected void containerIsCreated(String containerId) { + if (this.secrets != null && !this.secrets.isEmpty()) { + StringBuilder secretsFile = new StringBuilder(); + for (Map.Entry entry : this.secrets.entrySet()) { + secretsFile.append(entry.getKey()).append("=").append(entry.getValue()).append("\n"); + } + copyFileToContainer(Transferable.of(secretsFile.toString()), SECRETS_PATH); + } + } + + public DockerMcpGatewayContainer withServer(String server, List tools) { + this.servers.add(server); + this.tools.addAll(tools); + return this; + } + + public DockerMcpGatewayContainer withServer(String server, String... tools) { + this.servers.add(server); + this.tools.addAll(Arrays.asList(tools)); + return this; + } + + public DockerMcpGatewayContainer withSecrets(Map secrets) { + this.secrets.putAll(secrets); + return this; + } + + public DockerMcpGatewayContainer withSecret(String secretKey, String secretValue) { + this.secrets.put(secretKey, secretValue); + return this; + } + + public String getEndpoint() { + return "http://" + getHost() + ":" + getMappedPort(DEFAULT_PORT); + } +} diff --git a/core/src/test/java/org/testcontainers/containers/DockerMcpGatewayContainerTest.java b/core/src/test/java/org/testcontainers/containers/DockerMcpGatewayContainerTest.java new file mode 100644 index 00000000000..ab9305cdcfd --- /dev/null +++ b/core/src/test/java/org/testcontainers/containers/DockerMcpGatewayContainerTest.java @@ -0,0 +1,37 @@ +package org.testcontainers.containers; + +import org.junit.Test; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DockerMcpGatewayContainerTest { + + @Test + public void serviceSuccessfullyStarts() { + try (DockerMcpGatewayContainer gateway = new DockerMcpGatewayContainer("docker/agents_gateway:v2")) { + gateway.start(); + + assertThat(gateway.isRunning()).isTrue(); + } + } + + @Test + public void gatewayStartsWithServers() { + try ( + // container { + DockerMcpGatewayContainer gateway = new DockerMcpGatewayContainer("docker/agents_gateway:v2") + .withServer("curl", "curl") + .withServer("brave", "brave_local_search", "brave_web_search") + .withServer("github-official", Collections.singletonList("add_issue_comment")) + .withSecret("brave.api_key", "test_key") + .withSecrets(Collections.singletonMap("github.personal_access_token", "test_token")) + // } + ) { + gateway.start(); + + assertThat(gateway.getLogs()).contains("4 tools listed"); + } + } +} diff --git a/docs/modules/docker_mcp_gateway.md b/docs/modules/docker_mcp_gateway.md new file mode 100644 index 00000000000..da0b2e55c09 --- /dev/null +++ b/docs/modules/docker_mcp_gateway.md @@ -0,0 +1,32 @@ +# Docker MCP Gateway + +Testcontainers module for [Docker MCP Gateway](https://hub.docker.com/r/docker/agents_gateway). + +## DockerMcpGatewayContainer's usage examples + +You can start a Docker MCP Gateway container instance from any Java application by using: + + +[Create a DockerMcpGatewayContainer](../../core/src/test/java/org/testcontainers/containers/DockerMcpGatewayContainerTest.java) inside_block:container + + +## Adding this module to your project dependencies + +*Docker MCP Gateway support is part of the core Testcontainers library.* + +Add the following dependency to your `pom.xml`/`build.gradle` file: + +=== "Gradle" + ```groovy + testImplementation "org.testcontainers:testcontainers:{{latest_version}}" + ``` +=== "Maven" + ```xml + + org.testcontainers + testcontainers + {{latest_version}} + test + + ``` + diff --git a/mkdocs.yml b/mkdocs.yml index 65235b7964f..c0e5054706e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -82,6 +82,7 @@ nav: - modules/chromadb.md - modules/consul.md - modules/docker_compose.md + - modules/docker_mcp_gateway.md - modules/docker_model_runner.md - modules/elasticsearch.md - modules/gcloud.md