diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 6c660ea045..bce1c60fdf 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -29,6 +29,7 @@ updates:
- /modules/couchbase
- /modules/databend
- /modules/dind
+ - /modules/dockermcpgateway
- /modules/dockermodelrunner
- /modules/dolt
- /modules/dynamodb
diff --git a/.vscode/.testcontainers-go.code-workspace b/.vscode/.testcontainers-go.code-workspace
index 08ce2a5476..35a073bba7 100644
--- a/.vscode/.testcontainers-go.code-workspace
+++ b/.vscode/.testcontainers-go.code-workspace
@@ -65,6 +65,10 @@
"name": "module / dind",
"path": "../modules/dind"
},
+ {
+ "name": "module / dockermcpgateway",
+ "path": "../modules/dockermcpgateway"
+ },
{
"name": "module / dockermodelrunner",
"path": "../modules/dockermodelrunner"
diff --git a/docs/modules/dockermcpgateway.md b/docs/modules/dockermcpgateway.md
new file mode 100644
index 0000000000..29f1ef00fb
--- /dev/null
+++ b/docs/modules/dockermcpgateway.md
@@ -0,0 +1,108 @@
+# Docker MCP Gateway
+
+Not available until the next release :material-tag: main
+
+## Introduction
+
+The Testcontainers module for the Docker MCP Gateway.
+
+## Adding this module to your project dependencies
+
+Please run the following command to add the Docker MCP Gateway module to your Go dependencies:
+
+```
+go get github.com/testcontainers/testcontainers-go/modules/dockermcpgateway
+```
+
+## Usage example
+
+
+[Creating a DockerMCPGateway container](../../modules/dockermcpgateway/examples_test.go) inside_block:run_mcp_gateway
+
+
+## Module Reference
+
+### Run function
+
+- Not available until the next release :material-tag: main
+
+The DockerMCPGateway module exposes one entrypoint function to create the DockerMCPGateway container, and this function receives three parameters:
+
+```golang
+func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error)
+```
+
+- `context.Context`, the Go context.
+- `string`, the Docker image to use.
+- `testcontainers.ContainerCustomizer`, a variadic argument for passing options.
+
+#### Image
+
+Use the second argument in the `Run` function to set a valid Docker image.
+In example: `Run(context.Background(), "docker/mcp-gateway:latest")`.
+
+### Container Options
+
+When starting the DockerMCPGateway container, you can pass options in a variadic way to configure it.
+
+{% include "../features/common_functional_options_list.md" %}
+
+#### WithTools
+
+- Not available until the next release :material-tag: main
+
+Use the `WithTools` option to set the tools from a server to be available in the MCP Gateway container. Adding multiple tools for the same server will append to the existing tools for that server, and no duplicate tools will be added for the same server.
+
+```golang
+dockermcpgateway.WithTools("brave", []string{"brave_local_search", "brave_web_search"})
+```
+
+#### WithSecrets
+
+- Not available until the next release :material-tag: main
+
+Use the `WithSecrets` option to set the tools from a server to be available in the MCP Gateway container. Empty keys are not allowed, although empty values are allowed for a key.
+
+```golang
+dockermcpgateway.WithSecret("github_token", "test_value")
+dockermcpgateway.WithSecrets(map[string]{
+ "github_token": "test_value",
+ "foo": "bar",
+})
+```
+
+### Container Methods
+
+The DockerMCPGateway container exposes the following methods:
+
+#### Tools
+
+- Not available until the next release :material-tag: main
+
+Returns a map of tools available in the MCP Gateway container, where the key is the server name and the value is a slice of tool names.
+
+```golang
+tools := ctr.Tools()
+```
+
+#### GatewayEndpoint
+
+- Not available until the next release :material-tag: main
+
+Returns the endpoint of the MCP Gateway container, which is a string containing the host and mapped port for the default MCP Gateway port (8811/tcp).
+
+```golang
+endpoint := ctr.GatewayEndpoint()
+```
+### Examples
+
+#### Connecting to the MCP Gateway using an MCP client
+
+This example shows the usage of the MCP Gateway module to connect with an [MCP client](https://github.com/modelcontextprotocol/go-sdk).
+
+
+[Run the MCP Gateway](../../modules/dockermcpgateway/examples_test.go) inside_block:run_mcp_gateway
+[Get MCP Gateway's endpoint](../../modules/dockermcpgateway/examples_test.go) inside_block:get_gateway
+[Connect with an MCP client](../../modules/dockermcpgateway/examples_test.go) inside_block:connect_mcp_client
+[List tools](../../modules/dockermcpgateway/examples_test.go) inside_block:list_tools
+
diff --git a/mkdocs.yml b/mkdocs.yml
index 60fa87ca64..af0621ab2a 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -80,6 +80,7 @@ nav:
- modules/couchbase.md
- modules/databend.md
- modules/dind.md
+ - modules/dockermcpgateway.md
- modules/dockermodelrunner.md
- modules/dolt.md
- modules/dynamodb.md
diff --git a/modules/dockermcpgateway/Makefile b/modules/dockermcpgateway/Makefile
new file mode 100644
index 0000000000..f5c6b60f0d
--- /dev/null
+++ b/modules/dockermcpgateway/Makefile
@@ -0,0 +1,5 @@
+include ../../commons-test.mk
+
+.PHONY: test
+test:
+ $(MAKE) test-dockermcpgateway
diff --git a/modules/dockermcpgateway/dockermcpgateway.go b/modules/dockermcpgateway/dockermcpgateway.go
new file mode 100644
index 0000000000..be4dabcbaf
--- /dev/null
+++ b/modules/dockermcpgateway/dockermcpgateway.go
@@ -0,0 +1,108 @@
+package dockermcpgateway
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "github.com/docker/docker/api/types/container"
+
+ "github.com/testcontainers/testcontainers-go"
+ "github.com/testcontainers/testcontainers-go/internal/core"
+ "github.com/testcontainers/testcontainers-go/wait"
+)
+
+const (
+ defaultPort = "8811/tcp"
+ secretsPath = "/testcontainers/app/secrets"
+)
+
+// Container represents the DockerMCPGateway container type used in the module
+type Container struct {
+ testcontainers.Container
+ tools map[string][]string
+}
+
+// Run creates an instance of the DockerMCPGateway container type
+func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error) {
+ dockerHostMount := core.MustExtractDockerSocket(ctx)
+
+ moduleOpts := []testcontainers.ContainerCustomizer{
+ testcontainers.WithExposedPorts(defaultPort),
+ testcontainers.WithHostConfigModifier(func(hc *container.HostConfig) {
+ hc.Binds = []string{
+ dockerHostMount + ":/var/run/docker.sock",
+ }
+ }),
+ testcontainers.WithWaitStrategy(wait.ForAll(
+ wait.ForListeningPort(defaultPort),
+ wait.ForLog(".*Start sse server on port.*").AsRegexp(),
+ )),
+ }
+
+ settings := defaultOptions()
+ for _, opt := range opts {
+ if apply, ok := opt.(Option); ok {
+ if err := apply(&settings); err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ cmds := []string{"--transport=sse"}
+ for server, tools := range settings.tools {
+ cmds = append(cmds, "--servers="+server)
+ for _, tool := range tools {
+ cmds = append(cmds, "--tools="+tool)
+ }
+ }
+ if len(settings.secrets) > 0 {
+ cmds = append(cmds, "--secrets="+secretsPath)
+
+ secretsContent := ""
+ for key, value := range settings.secrets {
+ secretsContent += key + "=" + value + "\n"
+ }
+
+ moduleOpts = append(moduleOpts, testcontainers.WithFiles(testcontainers.ContainerFile{
+ Reader: strings.NewReader(secretsContent),
+ ContainerFilePath: secretsPath,
+ FileMode: 0o644,
+ }))
+ }
+
+ moduleOpts = append(moduleOpts, testcontainers.WithCmd(cmds...))
+
+ // append user-defined options
+ moduleOpts = append(moduleOpts, opts...)
+
+ container, err := testcontainers.Run(ctx, img, moduleOpts...)
+ var c *Container
+ if container != nil {
+ c = &Container{Container: container, tools: settings.tools}
+ }
+
+ if err != nil {
+ return c, fmt.Errorf("generic container: %w", err)
+ }
+
+ return c, nil
+}
+
+// GatewayEndpoint returns the endpoint for the DockerMCPGateway container.
+// It uses the mapped port for the default port (8811/tcp) and the "http" protocol.
+func (c *Container) GatewayEndpoint(ctx context.Context) (string, error) {
+ endpoint, err := c.PortEndpoint(ctx, defaultPort, "http")
+ if err != nil {
+ return "", fmt.Errorf("port endpoint: %w", err)
+ }
+
+ return endpoint, nil
+}
+
+// Tools returns the tools configured for the DockerMCPGateway container,
+// indexed by server name.
+// The keys are the server names and the values are slices of tool names.
+func (c *Container) Tools() map[string][]string {
+ return c.tools
+}
diff --git a/modules/dockermcpgateway/dockermcpgateway_test.go b/modules/dockermcpgateway/dockermcpgateway_test.go
new file mode 100644
index 0000000000..e78fa5479b
--- /dev/null
+++ b/modules/dockermcpgateway/dockermcpgateway_test.go
@@ -0,0 +1,89 @@
+package dockermcpgateway_test
+
+import (
+ "context"
+ "io"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/testcontainers/testcontainers-go"
+ dmcpg "github.com/testcontainers/testcontainers-go/modules/dockermcpgateway"
+)
+
+func TestDockerMCPGateway(t *testing.T) {
+ ctx := context.Background()
+
+ ctr, err := dmcpg.Run(ctx, "docker/mcp-gateway:latest")
+ testcontainers.CleanupContainer(t, ctr)
+ require.NoError(t, err)
+
+ require.Empty(t, ctr.Tools())
+}
+
+func TestDockerMCPGateway_withServerAndTools(t *testing.T) {
+ ctx := context.Background()
+
+ ctr, err := dmcpg.Run(
+ ctx, "docker/mcp-gateway:latest",
+ dmcpg.WithTools("curl", []string{"curl"}),
+ dmcpg.WithTools("brave", []string{"brave_local_search", "brave_web_search"}),
+ dmcpg.WithTools("github-official", []string{"add_issue_comment"}),
+ )
+ testcontainers.CleanupContainer(t, ctr)
+ require.NoError(t, err)
+
+ require.Len(t, ctr.Tools(), 3)
+
+ for server, tools := range ctr.Tools() {
+ switch server {
+ case "curl":
+ require.Equal(t, []string{"curl"}, tools)
+ case "brave":
+ require.ElementsMatch(t, []string{"brave_local_search", "brave_web_search"}, tools)
+ case "github-official":
+ require.Equal(t, []string{"add_issue_comment"}, tools)
+ default:
+ t.Errorf("unexpected server: %s", server)
+ }
+ }
+}
+
+func TestDockerMCPGateway_withSecret(t *testing.T) {
+ ctx := context.Background()
+
+ ctr, err := dmcpg.Run(
+ ctx, "docker/mcp-gateway:latest",
+ dmcpg.WithSecret("github.personal_access_token", "test_token"),
+ )
+ testcontainers.CleanupContainer(t, ctr)
+ require.NoError(t, err)
+
+ r, err := ctr.CopyFileFromContainer(ctx, "/testcontainers/app/secrets")
+ require.NoError(t, err)
+
+ bytes, err := io.ReadAll(r)
+ require.NoError(t, err)
+ require.Equal(t, "github.personal_access_token=test_token\n", string(bytes))
+}
+
+func TestDockerMCPGateway_withSecrets(t *testing.T) {
+ ctx := context.Background()
+
+ ctr, err := dmcpg.Run(
+ ctx, "docker/mcp-gateway:latest",
+ dmcpg.WithSecrets(map[string]string{
+ "github.personal_access_token": "test_token",
+ "another.secret": "another_value",
+ }),
+ )
+ testcontainers.CleanupContainer(t, ctr)
+ require.NoError(t, err)
+
+ r, err := ctr.CopyFileFromContainer(ctx, "/testcontainers/app/secrets")
+ require.NoError(t, err)
+
+ bytes, err := io.ReadAll(r)
+ require.NoError(t, err)
+ require.Equal(t, "github.personal_access_token=test_token\nanother.secret=another_value\n", string(bytes))
+}
diff --git a/modules/dockermcpgateway/examples_test.go b/modules/dockermcpgateway/examples_test.go
new file mode 100644
index 0000000000..a0a5b88337
--- /dev/null
+++ b/modules/dockermcpgateway/examples_test.go
@@ -0,0 +1,102 @@
+package dockermcpgateway_test
+
+import (
+ "context"
+ "fmt"
+ "log"
+
+ "github.com/modelcontextprotocol/go-sdk/mcp"
+
+ "github.com/testcontainers/testcontainers-go"
+ dmcpg "github.com/testcontainers/testcontainers-go/modules/dockermcpgateway"
+)
+
+func ExampleRun() {
+ ctx := context.Background()
+
+ ctr, err := dmcpg.Run(
+ ctx, "docker/mcp-gateway:latest",
+ dmcpg.WithTools("curl", []string{"curl"}),
+ dmcpg.WithTools("brave", []string{"brave_local_search", "brave_web_search"}),
+ dmcpg.WithTools("github-official", []string{"add_issue_comment"}),
+ )
+ defer func() {
+ if err := testcontainers.TerminateContainer(ctr); err != nil {
+ log.Printf("failed to terminate container: %s", err)
+ }
+ }()
+ if err != nil {
+ log.Printf("failed to start container: %s", err)
+ return
+ }
+
+ state, err := ctr.State(ctx)
+ if err != nil {
+ log.Printf("failed to get container state: %s", err)
+ return
+ }
+
+ fmt.Println(state.Running)
+ fmt.Println(len(ctr.Tools()))
+
+ // Output:
+ // true
+ // 3
+}
+
+func ExampleRun_connectMCPClient() {
+ // run_mcp_gateway {
+ ctx := context.Background()
+
+ ctr, err := dmcpg.Run(
+ ctx, "docker/mcp-gateway:latest",
+ dmcpg.WithTools("curl", []string{"curl"}),
+ dmcpg.WithTools("brave", []string{"brave_local_search", "brave_web_search"}),
+ dmcpg.WithTools("github-official", []string{"add_issue_comment"}),
+ )
+ defer func() {
+ if err := testcontainers.TerminateContainer(ctr); err != nil {
+ log.Printf("failed to terminate container: %s", err)
+ }
+ }()
+ if err != nil {
+ log.Printf("failed to start container: %s", err)
+ return
+ }
+ // }
+
+ // get_gateway {
+ gatewayEndpoint, err := ctr.GatewayEndpoint(ctx)
+ if err != nil {
+ log.Printf("failed to get gateway endpoint: %s", err)
+ return
+ }
+ // }
+
+ // connect_mcp_client {
+ transport := mcp.NewSSEClientTransport(gatewayEndpoint, nil)
+
+ client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)
+
+ cs, err := client.Connect(context.Background(), transport)
+ if err != nil {
+ log.Printf("Failed to connect to MCP gateway: %v", err)
+ return
+ }
+ // }
+
+ // list_tools {
+ mcpTools, err := cs.ListTools(context.Background(), &mcp.ListToolsParams{})
+ if err != nil {
+ log.Printf("Failed to list tools: %v", err)
+ return
+ }
+ // }
+
+ fmt.Println(len(mcpTools.Tools))
+ fmt.Println(len(ctr.Tools()))
+
+ // Output:
+ // 4
+ // 3
+}
diff --git a/modules/dockermcpgateway/go.mod b/modules/dockermcpgateway/go.mod
new file mode 100644
index 0000000000..0e34b4c5e4
--- /dev/null
+++ b/modules/dockermcpgateway/go.mod
@@ -0,0 +1,69 @@
+module github.com/testcontainers/testcontainers-go/modules/dockermcpgateway
+
+go 1.23.0
+
+require (
+ github.com/docker/docker v28.2.2+incompatible
+ github.com/modelcontextprotocol/go-sdk v0.2.0
+ github.com/stretchr/testify v1.10.0
+ github.com/testcontainers/testcontainers-go v0.38.0
+)
+
+require (
+ dario.cat/mergo v1.0.1 // indirect
+ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
+ github.com/Microsoft/go-winio v0.6.2 // indirect
+ github.com/cenkalti/backoff/v4 v4.2.1 // indirect
+ github.com/containerd/errdefs v1.0.0 // indirect
+ github.com/containerd/errdefs/pkg v0.3.0 // indirect
+ github.com/containerd/log v0.1.0 // indirect
+ github.com/containerd/platforms v0.2.1 // indirect
+ github.com/cpuguy83/dockercfg v0.3.2 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/distribution/reference v0.6.0 // indirect
+ github.com/docker/go-connections v0.5.0 // indirect
+ github.com/docker/go-units v0.5.0 // indirect
+ github.com/ebitengine/purego v0.8.4 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/go-logr/logr v1.4.3 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-ole/go-ole v1.2.6 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/klauspost/compress v1.18.0 // indirect
+ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
+ github.com/magiconair/properties v1.8.10 // indirect
+ github.com/moby/docker-image-spec v1.3.1 // indirect
+ github.com/moby/go-archive v0.1.0 // indirect
+ github.com/moby/patternmatcher v0.6.0 // indirect
+ github.com/moby/sys/sequential v0.6.0 // indirect
+ github.com/moby/sys/user v0.4.0 // indirect
+ github.com/moby/sys/userns v0.1.0 // indirect
+ github.com/moby/term v0.5.0 // indirect
+ github.com/morikuni/aec v1.0.0 // indirect
+ github.com/opencontainers/go-digest v1.0.0 // indirect
+ github.com/opencontainers/image-spec v1.1.1 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
+ github.com/shirou/gopsutil/v4 v4.25.5 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/tklauser/go-sysconf v0.3.12 // indirect
+ github.com/tklauser/numcpus v0.6.1 // indirect
+ github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
+ github.com/yusufpapurcu/wmi v1.2.4 // indirect
+ go.opentelemetry.io/auto/sdk v1.1.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
+ go.opentelemetry.io/otel v1.37.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
+ go.opentelemetry.io/otel/metric v1.37.0 // indirect
+ go.opentelemetry.io/otel/sdk v1.37.0 // indirect
+ go.opentelemetry.io/otel/trace v1.37.0 // indirect
+ go.opentelemetry.io/proto/otlp v1.7.0 // indirect
+ golang.org/x/crypto v0.37.0 // indirect
+ golang.org/x/sys v0.33.0 // indirect
+ google.golang.org/protobuf v1.36.6 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
+
+replace github.com/testcontainers/testcontainers-go => ../..
diff --git a/modules/dockermcpgateway/go.sum b/modules/dockermcpgateway/go.sum
new file mode 100644
index 0000000000..3aa578ca86
--- /dev/null
+++ b/modules/dockermcpgateway/go.sum
@@ -0,0 +1,196 @@
+dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
+dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
+github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
+github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
+github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
+github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
+github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
+github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
+github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
+github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
+github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
+github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
+github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
+github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
+github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
+github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
+github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
+github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
+github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
+github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
+github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
+github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
+github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
+github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
+github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
+github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
+github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
+github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
+github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
+github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
+github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
+github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
+github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
+github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
+github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
+github.com/modelcontextprotocol/go-sdk v0.2.0 h1:PESNYOmyM1c369tRkzXLY5hHrazj8x9CY1Xu0fLCryM=
+github.com/modelcontextprotocol/go-sdk v0.2.0/go.mod h1:0sL9zUKKs2FTTkeCCVnKqbLJTw5TScefPAzojjU459E=
+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
+github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
+github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
+github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
+github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
+github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
+github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
+github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
+github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
+github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
+go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
+go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
+golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
+golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
+golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
+golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
+golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44=
+golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
+golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ=
+google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
+google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
+gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
diff --git a/modules/dockermcpgateway/options.go b/modules/dockermcpgateway/options.go
new file mode 100644
index 0000000000..19b87918ab
--- /dev/null
+++ b/modules/dockermcpgateway/options.go
@@ -0,0 +1,93 @@
+package dockermcpgateway
+
+import (
+ "errors"
+ "maps"
+ "slices"
+
+ "github.com/testcontainers/testcontainers-go"
+)
+
+type options struct {
+ tools map[string][]string
+ secrets map[string]string
+}
+
+func defaultOptions() options {
+ return options{
+ tools: map[string][]string{},
+ secrets: map[string]string{},
+ }
+}
+
+// Compiler check to ensure that Option implements the testcontainers.ContainerCustomizer interface.
+var _ testcontainers.ContainerCustomizer = (Option)(nil)
+
+// Option is an option for the Redpanda container.
+type Option func(*options) error
+
+// Customize is a NOOP. It's defined to satisfy the testcontainers.ContainerCustomizer interface.
+func (o Option) Customize(*testcontainers.GenericContainerRequest) error {
+ // NOOP to satisfy interface.
+ return nil
+}
+
+// WithTools sets a server's tools to use in the DockerMCPGateway container.
+// Multiple calls to this function with the same server will append to the existing tools for that server.
+// No duplicate tools will be added for the same server.
+func WithTools(server string, tools []string) Option {
+ return func(o *options) error {
+ if server == "" {
+ return errors.New("server cannot be empty")
+ }
+ if len(tools) == 0 {
+ return errors.New("tools cannot be empty")
+ }
+
+ if slices.Contains(tools, "") {
+ return errors.New("tool cannot be empty")
+ }
+
+ currentTools, exists := o.tools[server]
+ if exists {
+ // Append only unique tools to avoid duplicates
+ for _, tool := range tools {
+ if !slices.Contains(currentTools, tool) {
+ currentTools = append(currentTools, tool)
+ }
+ }
+ o.tools[server] = currentTools
+ } else {
+ // If the server does not exist, create a new entry
+ o.tools[server] = tools
+ }
+
+ return nil
+ }
+}
+
+// WithServers sets the servers to use in the DockerMCPGateway container.
+// Multiple calls to this function will append to the existing values.
+func WithSecret(key, value string) Option {
+ return func(o *options) error {
+ if key == "" {
+ return errors.New("secret key cannot be empty")
+ }
+
+ o.secrets[key] = value
+ return nil
+ }
+}
+
+// WithSecrets sets the secrets to use in the DockerMCPGateway container.
+// Multiple calls to this function will merge the secrets into the existing map.
+func WithSecrets(secrets map[string]string) Option {
+ return func(o *options) error {
+ if len(secrets) == 0 {
+ return errors.New("secrets cannot be empty")
+ }
+
+ maps.Copy(o.secrets, secrets)
+ return nil
+ }
+}
diff --git a/modules/dockermcpgateway/options_test.go b/modules/dockermcpgateway/options_test.go
new file mode 100644
index 0000000000..a7b8fe50d3
--- /dev/null
+++ b/modules/dockermcpgateway/options_test.go
@@ -0,0 +1,130 @@
+package dockermcpgateway
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestWithTools(t *testing.T) {
+ t.Run("one-server", func(t *testing.T) {
+ t.Run("single-tool", func(t *testing.T) {
+ settings := defaultOptions()
+
+ err := WithTools("server", []string{"tool1"})(&settings)
+ require.NoError(t, err)
+ require.Len(t, settings.tools, 1)
+ require.Equal(t, []string{"tool1"}, settings.tools["server"])
+ })
+
+ t.Run("multiple-tools", func(t *testing.T) {
+ settings := defaultOptions()
+
+ err := WithTools("server", []string{"tool1", "tool2"})(&settings)
+ require.NoError(t, err)
+ require.Len(t, settings.tools, 1)
+ require.Contains(t, settings.tools["server"], "tool1")
+ require.Contains(t, settings.tools["server"], "tool2")
+ })
+ })
+
+ t.Run("empty-server", func(t *testing.T) {
+ settings := defaultOptions()
+
+ err := WithTools("", []string{"tool"})(&settings)
+ require.ErrorContains(t, err, "server cannot be empty")
+ })
+
+ t.Run("empty-tools", func(t *testing.T) {
+ settings := defaultOptions()
+
+ err := WithTools("server", nil)(&settings)
+ require.ErrorContains(t, err, "tools cannot be empty")
+ })
+
+ t.Run("empty-tool-in-slice", func(t *testing.T) {
+ settings := defaultOptions()
+
+ err := WithTools("server", []string{"tool1", "", "tool3"})(&settings)
+ require.ErrorContains(t, err, "tool cannot be empty")
+ })
+
+ t.Run("duplicated-tools", func(t *testing.T) {
+ settings := defaultOptions()
+
+ err := WithTools("server", []string{"tool1"})(&settings)
+ require.NoError(t, err)
+
+ err = WithTools("server", []string{"tool1"})(&settings)
+ require.NoError(t, err)
+
+ require.Len(t, settings.tools, 1)
+ require.Equal(t, []string{"tool1"}, settings.tools["server"])
+ })
+
+ t.Run("duplicated-server", func(t *testing.T) {
+ settings := defaultOptions()
+
+ err := WithTools("server", []string{"tool1"})(&settings)
+ require.NoError(t, err)
+
+ err = WithTools("server", []string{"tool2"})(&settings)
+ require.NoError(t, err)
+
+ require.Len(t, settings.tools, 1)
+ require.Equal(t, []string{"tool1", "tool2"}, settings.tools["server"])
+ })
+
+ t.Run("multiple-servers", func(t *testing.T) {
+ settings := defaultOptions()
+
+ err := WithTools("server1", []string{"tool1.1"})(&settings)
+ require.NoError(t, err)
+ err = WithTools("server2", []string{"tool2.1", "tool2.2"})(&settings)
+ require.NoError(t, err)
+
+ require.Len(t, settings.tools, 2)
+ require.Contains(t, settings.tools["server1"], "tool1.1")
+ require.Contains(t, settings.tools["server2"], "tool2.1")
+ require.Contains(t, settings.tools["server2"], "tool2.2")
+ })
+}
+
+func TestWithSecrets(t *testing.T) {
+ t.Run("single-secret", func(t *testing.T) {
+ settings := defaultOptions()
+
+ err := WithSecret("key", "value")(&settings)
+ require.NoError(t, err)
+ require.Contains(t, settings.secrets, "key")
+ require.Equal(t, "value", settings.secrets["key"])
+ })
+
+ t.Run("multiple-secrets", func(t *testing.T) {
+ settings := defaultOptions()
+
+ err := WithSecrets(map[string]string{
+ "key1": "value1",
+ "key2": "value2",
+ })(&settings)
+ require.NoError(t, err)
+ require.Len(t, settings.secrets, 2)
+ require.Equal(t, "value1", settings.secrets["key1"])
+ require.Equal(t, "value2", settings.secrets["key2"])
+ })
+
+ t.Run("empty-key", func(t *testing.T) {
+ settings := defaultOptions()
+
+ err := WithSecret("", "value")(&settings)
+ require.ErrorContains(t, err, "secret key cannot be empty")
+ })
+
+ t.Run("empty-value", func(t *testing.T) {
+ settings := defaultOptions()
+
+ err := WithSecret("key", "")(&settings)
+ require.NoError(t, err)
+ require.Empty(t, settings.secrets["key"])
+ })
+}