Skip to content

Commit a00ab19

Browse files
authored
Merge pull request #1 from dgageot/gateway
Add the gateway code
2 parents ae18ed1 + c850be5 commit a00ab19

File tree

3,989 files changed

+1047096
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

3,989 files changed

+1047096
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Requirements:
44

55
- A recent compose version (build from main)
6-
- Yhe gateway (https://github.com/docker/dgagateway)
6+
- The gateway (./dgagateway)
77
- A recent Docker Desktop
88

99
Then you can run:

gateway/.dockerignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
*
2+
!/cmd
3+
!/vendor
4+
!/go.mod
5+
!/go.sum

gateway/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/docker-mcpgateway

gateway/Dockerfile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# syntax=docker/dockerfile:1
2+
3+
FROM golang:1.24-alpine3.21@sha256:ef18ee7117463ac1055f5a370ed18b8750f01589f13ea0b48642f5792b234044 AS build
4+
WORKDIR /app
5+
RUN --mount=type=cache,target=/root/.cache/go-build \
6+
--mount=source=.,target=. \
7+
go build -o / ./cmd/agent/ ./cmd/agents_gateway/
8+
9+
FROM scratch AS agent
10+
ENTRYPOINT ["/agent"]
11+
COPY --from=build /agent /
12+
13+
FROM scratch AS agents_gateway
14+
ENTRYPOINT ["/agents_gateway"]
15+
COPY --from=build /agents_gateway /

gateway/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
## How to get it running?
2+
3+
Build the `docker/mcpgateway` image:
4+
5+
```console
6+
task build-gateway-image
7+
```
8+
9+
Build the `docker-mcp` compose provider and install it as a cli plugin.
10+
11+
```console
12+
task build-compose-provider
13+
```
14+
15+
Manually build the latest version of [docker-compose](https://github.com/docker/compose)
16+
and install it as a cli plugin.
17+
18+
Run the whole stack:
19+
20+
```console
21+
docker compose up --build
22+
docker compose down --remove-orphans
23+
```
24+
25+
## Do it with a single command
26+
27+
```console
28+
task
29+
```

gateway/Taskfile.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
version: "3"
2+
3+
tasks:
4+
default:
5+
desc: Build and run the Docker image
6+
cmds:
7+
- task: build
8+
- task: down
9+
- task: up
10+
11+
up:
12+
desc: Start the demo Compose Stack
13+
cmd: docker compose up --build
14+
15+
down:
16+
desc: Stop the demo Compose Stack
17+
cmd: docker compose down --remove-orphans
18+
19+
build-gateway-image:
20+
desc: Build the gateway docker image
21+
cmd: docker build -t docker/agents_gateway --target=agents_gateway .
22+
23+
build-compose-provider:
24+
desc: Build the compose provider
25+
cmds:
26+
- go build -o docker-mcpgateway ./cmd/gateway_light_provider
27+
- /bin/ln -sf "{{.TASKFILE_DIR}}/docker-mcpgateway" "{{.HOME}}/.docker/cli-plugins/docker-mcpgateway"
28+
29+
build:
30+
desc: Build everything
31+
cmds:
32+
- task: build-gateway-image
33+
- task: build-compose-provider

gateway/cmd/agent/main.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"log"
8+
"os"
9+
"os/signal"
10+
"strings"
11+
"syscall"
12+
"time"
13+
14+
"github.com/mark3labs/mcp-go/client"
15+
"github.com/mark3labs/mcp-go/mcp"
16+
)
17+
18+
func main() {
19+
args := os.Args[1:]
20+
if len(args) == 0 {
21+
usage()
22+
return
23+
}
24+
25+
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
26+
defer cancel()
27+
28+
switch args[0] {
29+
case "list":
30+
verbose := true
31+
if err := list(ctx, verbose); err != nil {
32+
log.Fatal(err)
33+
}
34+
case "count":
35+
verbose := false
36+
if err := list(ctx, verbose); err != nil {
37+
log.Fatal(err)
38+
}
39+
case "call":
40+
if err := call(ctx, args[1:]); err != nil {
41+
log.Fatal(err)
42+
}
43+
default:
44+
usage()
45+
}
46+
}
47+
48+
func usage() {
49+
fmt.Println("Usage: client COMMAND [ARGS]")
50+
fmt.Println()
51+
fmt.Println("A command-line debug client for the MCP.")
52+
fmt.Println()
53+
fmt.Println("Commands:")
54+
fmt.Println(" list List all available tools")
55+
fmt.Println(" count Count all available tools")
56+
fmt.Println(" call Call a specific tool with arguments")
57+
}
58+
59+
func list(ctx context.Context, verbose bool) error {
60+
c, err := start(ctx)
61+
if err != nil {
62+
return fmt.Errorf("starting client: %w", err)
63+
}
64+
defer c.Close()
65+
66+
response, err := c.ListTools(ctx, mcp.ListToolsRequest{})
67+
if err != nil {
68+
return fmt.Errorf("listing tools: %w", err)
69+
}
70+
71+
buf, err := json.MarshalIndent(response.Tools, "", " ")
72+
if err != nil {
73+
return fmt.Errorf("marshalling tools: %w", err)
74+
}
75+
76+
if verbose {
77+
fmt.Println(len(response.Tools), "tools:")
78+
fmt.Println(string(buf))
79+
} else {
80+
fmt.Println(len(response.Tools), "tools")
81+
}
82+
83+
return nil
84+
}
85+
86+
func call(ctx context.Context, args []string) error {
87+
if len(args) == 0 {
88+
return fmt.Errorf("no tool name provided")
89+
}
90+
toolName := args[0]
91+
92+
c, err := start(ctx)
93+
if err != nil {
94+
return fmt.Errorf("starting client: %w", err)
95+
}
96+
defer c.Close()
97+
98+
request := mcp.CallToolRequest{}
99+
request.Params.Name = toolName
100+
request.Params.Arguments = parseArgs(args[1:])
101+
102+
start := time.Now()
103+
response, err := c.CallTool(ctx, request)
104+
if err != nil {
105+
return fmt.Errorf("calling tool: %w", err)
106+
}
107+
fmt.Println("Tool call took:", time.Since(start))
108+
109+
if response.IsError {
110+
return fmt.Errorf("error calling tool: %s", toolName)
111+
}
112+
113+
for _, content := range response.Content {
114+
if textContent, ok := content.(mcp.TextContent); ok {
115+
fmt.Println(textContent.Text)
116+
} else {
117+
fmt.Println(content)
118+
}
119+
}
120+
121+
return nil
122+
}
123+
124+
func start(ctx context.Context) (*client.Client, error) {
125+
host := os.Getenv("MCPGATEWAY_ENDPOINT")
126+
c, err := client.NewSSEMCPClient("http://" + host + "/sse")
127+
if err != nil {
128+
return nil, err
129+
}
130+
131+
if err := c.Start(ctx); err != nil {
132+
return nil, err
133+
}
134+
135+
initRequest := mcp.InitializeRequest{}
136+
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
137+
initRequest.Params.ClientInfo = mcp.Implementation{
138+
Name: "docker",
139+
Version: "1.0.0",
140+
}
141+
142+
ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
143+
defer cancel()
144+
145+
if _, err := c.Initialize(ctx, initRequest); err != nil {
146+
return nil, fmt.Errorf("initializing: %w", err)
147+
}
148+
149+
return c, nil
150+
}
151+
152+
func parseArgs(args []string) map[string]any {
153+
parsed := map[string]any{}
154+
155+
for _, arg := range args {
156+
parts := strings.SplitN(arg, "=", 2)
157+
if len(parts) == 2 {
158+
parsed[parts[0]] = parts[1]
159+
} else {
160+
parsed[arg] = nil
161+
}
162+
}
163+
164+
// MCP servers return an error if the args are empty so we make sure
165+
// there is at least one argument
166+
if len(parsed) == 0 {
167+
parsed["args"] = "..."
168+
}
169+
170+
return parsed
171+
}

0 commit comments

Comments
 (0)