Skip to content

Commit ec2c2f4

Browse files
authored
feat(mcpgateway): add MCP gateway module (#3232)
1 parent 07ccfdc commit ec2c2f4

File tree

12 files changed

+906
-0
lines changed

12 files changed

+906
-0
lines changed

.github/dependabot.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ updates:
2929
- /modules/couchbase
3030
- /modules/databend
3131
- /modules/dind
32+
- /modules/dockermcpgateway
3233
- /modules/dockermodelrunner
3334
- /modules/dolt
3435
- /modules/dynamodb

.vscode/.testcontainers-go.code-workspace

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@
6565
"name": "module / dind",
6666
"path": "../modules/dind"
6767
},
68+
{
69+
"name": "module / dockermcpgateway",
70+
"path": "../modules/dockermcpgateway"
71+
},
6872
{
6973
"name": "module / dockermodelrunner",
7074
"path": "../modules/dockermodelrunner"

docs/modules/dockermcpgateway.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Docker MCP Gateway
2+
3+
Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>
4+
5+
## Introduction
6+
7+
The Testcontainers module for the Docker MCP Gateway.
8+
9+
## Adding this module to your project dependencies
10+
11+
Please run the following command to add the Docker MCP Gateway module to your Go dependencies:
12+
13+
```
14+
go get github.com/testcontainers/testcontainers-go/modules/dockermcpgateway
15+
```
16+
17+
## Usage example
18+
19+
<!--codeinclude-->
20+
[Creating a DockerMCPGateway container](../../modules/dockermcpgateway/examples_test.go) inside_block:run_mcp_gateway
21+
<!--/codeinclude-->
22+
23+
## Module Reference
24+
25+
### Run function
26+
27+
- Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>
28+
29+
The DockerMCPGateway module exposes one entrypoint function to create the DockerMCPGateway container, and this function receives three parameters:
30+
31+
```golang
32+
func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error)
33+
```
34+
35+
- `context.Context`, the Go context.
36+
- `string`, the Docker image to use.
37+
- `testcontainers.ContainerCustomizer`, a variadic argument for passing options.
38+
39+
#### Image
40+
41+
Use the second argument in the `Run` function to set a valid Docker image.
42+
In example: `Run(context.Background(), "docker/mcp-gateway:latest")`.
43+
44+
### Container Options
45+
46+
When starting the DockerMCPGateway container, you can pass options in a variadic way to configure it.
47+
48+
{% include "../features/common_functional_options_list.md" %}
49+
50+
#### WithTools
51+
52+
- Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>
53+
54+
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.
55+
56+
```golang
57+
dockermcpgateway.WithTools("brave", []string{"brave_local_search", "brave_web_search"})
58+
```
59+
60+
#### WithSecrets
61+
62+
- Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>
63+
64+
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.
65+
66+
```golang
67+
dockermcpgateway.WithSecret("github_token", "test_value")
68+
dockermcpgateway.WithSecrets(map[string]{
69+
"github_token": "test_value",
70+
"foo": "bar",
71+
})
72+
```
73+
74+
### Container Methods
75+
76+
The DockerMCPGateway container exposes the following methods:
77+
78+
#### Tools
79+
80+
- Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>
81+
82+
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.
83+
84+
```golang
85+
tools := ctr.Tools()
86+
```
87+
88+
#### GatewayEndpoint
89+
90+
- Not available until the next release <a href="https://github.com/testcontainers/testcontainers-go"><span class="tc-version">:material-tag: main</span></a>
91+
92+
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).
93+
94+
```golang
95+
endpoint := ctr.GatewayEndpoint()
96+
```
97+
### Examples
98+
99+
#### Connecting to the MCP Gateway using an MCP client
100+
101+
This example shows the usage of the MCP Gateway module to connect with an [MCP client](https://github.com/modelcontextprotocol/go-sdk).
102+
103+
<!--codeinclude-->
104+
[Run the MCP Gateway](../../modules/dockermcpgateway/examples_test.go) inside_block:run_mcp_gateway
105+
[Get MCP Gateway's endpoint](../../modules/dockermcpgateway/examples_test.go) inside_block:get_gateway
106+
[Connect with an MCP client](../../modules/dockermcpgateway/examples_test.go) inside_block:connect_mcp_client
107+
[List tools](../../modules/dockermcpgateway/examples_test.go) inside_block:list_tools
108+
<!--/codeinclude-->

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ nav:
8080
- modules/couchbase.md
8181
- modules/databend.md
8282
- modules/dind.md
83+
- modules/dockermcpgateway.md
8384
- modules/dockermodelrunner.md
8485
- modules/dolt.md
8586
- modules/dynamodb.md

modules/dockermcpgateway/Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
include ../../commons-test.mk
2+
3+
.PHONY: test
4+
test:
5+
$(MAKE) test-dockermcpgateway
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package dockermcpgateway
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/docker/docker/api/types/container"
9+
10+
"github.com/testcontainers/testcontainers-go"
11+
"github.com/testcontainers/testcontainers-go/internal/core"
12+
"github.com/testcontainers/testcontainers-go/wait"
13+
)
14+
15+
const (
16+
defaultPort = "8811/tcp"
17+
secretsPath = "/testcontainers/app/secrets"
18+
)
19+
20+
// Container represents the DockerMCPGateway container type used in the module
21+
type Container struct {
22+
testcontainers.Container
23+
tools map[string][]string
24+
}
25+
26+
// Run creates an instance of the DockerMCPGateway container type
27+
func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error) {
28+
dockerHostMount := core.MustExtractDockerSocket(ctx)
29+
30+
moduleOpts := []testcontainers.ContainerCustomizer{
31+
testcontainers.WithExposedPorts(defaultPort),
32+
testcontainers.WithHostConfigModifier(func(hc *container.HostConfig) {
33+
hc.Binds = []string{
34+
dockerHostMount + ":/var/run/docker.sock",
35+
}
36+
}),
37+
testcontainers.WithWaitStrategy(wait.ForAll(
38+
wait.ForListeningPort(defaultPort),
39+
wait.ForLog(".*Start sse server on port.*").AsRegexp(),
40+
)),
41+
}
42+
43+
settings := defaultOptions()
44+
for _, opt := range opts {
45+
if apply, ok := opt.(Option); ok {
46+
if err := apply(&settings); err != nil {
47+
return nil, err
48+
}
49+
}
50+
}
51+
52+
cmds := []string{"--transport=sse"}
53+
for server, tools := range settings.tools {
54+
cmds = append(cmds, "--servers="+server)
55+
for _, tool := range tools {
56+
cmds = append(cmds, "--tools="+tool)
57+
}
58+
}
59+
if len(settings.secrets) > 0 {
60+
cmds = append(cmds, "--secrets="+secretsPath)
61+
62+
secretsContent := ""
63+
for key, value := range settings.secrets {
64+
secretsContent += key + "=" + value + "\n"
65+
}
66+
67+
moduleOpts = append(moduleOpts, testcontainers.WithFiles(testcontainers.ContainerFile{
68+
Reader: strings.NewReader(secretsContent),
69+
ContainerFilePath: secretsPath,
70+
FileMode: 0o644,
71+
}))
72+
}
73+
74+
moduleOpts = append(moduleOpts, testcontainers.WithCmd(cmds...))
75+
76+
// append user-defined options
77+
moduleOpts = append(moduleOpts, opts...)
78+
79+
container, err := testcontainers.Run(ctx, img, moduleOpts...)
80+
var c *Container
81+
if container != nil {
82+
c = &Container{Container: container, tools: settings.tools}
83+
}
84+
85+
if err != nil {
86+
return c, fmt.Errorf("generic container: %w", err)
87+
}
88+
89+
return c, nil
90+
}
91+
92+
// GatewayEndpoint returns the endpoint for the DockerMCPGateway container.
93+
// It uses the mapped port for the default port (8811/tcp) and the "http" protocol.
94+
func (c *Container) GatewayEndpoint(ctx context.Context) (string, error) {
95+
endpoint, err := c.PortEndpoint(ctx, defaultPort, "http")
96+
if err != nil {
97+
return "", fmt.Errorf("port endpoint: %w", err)
98+
}
99+
100+
return endpoint, nil
101+
}
102+
103+
// Tools returns the tools configured for the DockerMCPGateway container,
104+
// indexed by server name.
105+
// The keys are the server names and the values are slices of tool names.
106+
func (c *Container) Tools() map[string][]string {
107+
return c.tools
108+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package dockermcpgateway_test
2+
3+
import (
4+
"context"
5+
"io"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/testcontainers/testcontainers-go"
11+
dmcpg "github.com/testcontainers/testcontainers-go/modules/dockermcpgateway"
12+
)
13+
14+
func TestDockerMCPGateway(t *testing.T) {
15+
ctx := context.Background()
16+
17+
ctr, err := dmcpg.Run(ctx, "docker/mcp-gateway:latest")
18+
testcontainers.CleanupContainer(t, ctr)
19+
require.NoError(t, err)
20+
21+
require.Empty(t, ctr.Tools())
22+
}
23+
24+
func TestDockerMCPGateway_withServerAndTools(t *testing.T) {
25+
ctx := context.Background()
26+
27+
ctr, err := dmcpg.Run(
28+
ctx, "docker/mcp-gateway:latest",
29+
dmcpg.WithTools("curl", []string{"curl"}),
30+
dmcpg.WithTools("brave", []string{"brave_local_search", "brave_web_search"}),
31+
dmcpg.WithTools("github-official", []string{"add_issue_comment"}),
32+
)
33+
testcontainers.CleanupContainer(t, ctr)
34+
require.NoError(t, err)
35+
36+
require.Len(t, ctr.Tools(), 3)
37+
38+
for server, tools := range ctr.Tools() {
39+
switch server {
40+
case "curl":
41+
require.Equal(t, []string{"curl"}, tools)
42+
case "brave":
43+
require.ElementsMatch(t, []string{"brave_local_search", "brave_web_search"}, tools)
44+
case "github-official":
45+
require.Equal(t, []string{"add_issue_comment"}, tools)
46+
default:
47+
t.Errorf("unexpected server: %s", server)
48+
}
49+
}
50+
}
51+
52+
func TestDockerMCPGateway_withSecret(t *testing.T) {
53+
ctx := context.Background()
54+
55+
ctr, err := dmcpg.Run(
56+
ctx, "docker/mcp-gateway:latest",
57+
dmcpg.WithSecret("github.personal_access_token", "test_token"),
58+
)
59+
testcontainers.CleanupContainer(t, ctr)
60+
require.NoError(t, err)
61+
62+
r, err := ctr.CopyFileFromContainer(ctx, "/testcontainers/app/secrets")
63+
require.NoError(t, err)
64+
65+
bytes, err := io.ReadAll(r)
66+
require.NoError(t, err)
67+
require.Equal(t, "github.personal_access_token=test_token\n", string(bytes))
68+
}
69+
70+
func TestDockerMCPGateway_withSecrets(t *testing.T) {
71+
ctx := context.Background()
72+
73+
ctr, err := dmcpg.Run(
74+
ctx, "docker/mcp-gateway:latest",
75+
dmcpg.WithSecrets(map[string]string{
76+
"github.personal_access_token": "test_token",
77+
"another.secret": "another_value",
78+
}),
79+
)
80+
testcontainers.CleanupContainer(t, ctr)
81+
require.NoError(t, err)
82+
83+
r, err := ctr.CopyFileFromContainer(ctx, "/testcontainers/app/secrets")
84+
require.NoError(t, err)
85+
86+
bytes, err := io.ReadAll(r)
87+
require.NoError(t, err)
88+
require.Equal(t, "github.personal_access_token=test_token\nanother.secret=another_value\n", string(bytes))
89+
}

0 commit comments

Comments
 (0)