Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Requirements:

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

Then you can run:
Expand Down
5 changes: 5 additions & 0 deletions gateway/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
*
!/cmd
!/vendor
!/go.mod
!/go.sum
1 change: 1 addition & 0 deletions gateway/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/docker-mcpgateway
15 changes: 15 additions & 0 deletions gateway/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# syntax=docker/dockerfile:1

FROM golang:1.24-alpine3.21@sha256:ef18ee7117463ac1055f5a370ed18b8750f01589f13ea0b48642f5792b234044 AS build
WORKDIR /app
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=source=.,target=. \
go build -o / ./cmd/agent/ ./cmd/agents_gateway/

FROM scratch AS agent
ENTRYPOINT ["/agent"]
COPY --from=build /agent /

FROM scratch AS agents_gateway
ENTRYPOINT ["/agents_gateway"]
COPY --from=build /agents_gateway /
29 changes: 29 additions & 0 deletions gateway/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## How to get it running?

Build the `docker/mcpgateway` image:

```console
task build-gateway-image
```

Build the `docker-mcp` compose provider and install it as a cli plugin.

```console
task build-compose-provider
```

Manually build the latest version of [docker-compose](https://github.com/docker/compose)
and install it as a cli plugin.

Run the whole stack:

```console
docker compose up --build
docker compose down --remove-orphans
```

## Do it with a single command

```console
task
```
33 changes: 33 additions & 0 deletions gateway/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
version: "3"

tasks:
default:
desc: Build and run the Docker image
cmds:
- task: build
- task: down
- task: up

up:
desc: Start the demo Compose Stack
cmd: docker compose up --build

down:
desc: Stop the demo Compose Stack
cmd: docker compose down --remove-orphans

build-gateway-image:
desc: Build the gateway docker image
cmd: docker build -t docker/agents_gateway --target=agents_gateway .

build-compose-provider:
desc: Build the compose provider
cmds:
- go build -o docker-mcpgateway ./cmd/gateway_light_provider
- /bin/ln -sf "{{.TASKFILE_DIR}}/docker-mcpgateway" "{{.HOME}}/.docker/cli-plugins/docker-mcpgateway"

build:
desc: Build everything
cmds:
- task: build-gateway-image
- task: build-compose-provider
171 changes: 171 additions & 0 deletions gateway/cmd/agent/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package main

import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"os/signal"
"strings"
"syscall"
"time"

"github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/mcp"
)

func main() {
args := os.Args[1:]
if len(args) == 0 {
usage()
return
}

ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()

switch args[0] {
case "list":
verbose := true
if err := list(ctx, verbose); err != nil {
log.Fatal(err)
}
case "count":
verbose := false
if err := list(ctx, verbose); err != nil {
log.Fatal(err)
}
case "call":
if err := call(ctx, args[1:]); err != nil {
log.Fatal(err)
}
default:
usage()
}
}

func usage() {
fmt.Println("Usage: client COMMAND [ARGS]")
fmt.Println()
fmt.Println("A command-line debug client for the MCP.")
fmt.Println()
fmt.Println("Commands:")
fmt.Println(" list List all available tools")
fmt.Println(" count Count all available tools")
fmt.Println(" call Call a specific tool with arguments")
}

func list(ctx context.Context, verbose bool) error {
c, err := start(ctx)
if err != nil {
return fmt.Errorf("starting client: %w", err)
}
defer c.Close()

response, err := c.ListTools(ctx, mcp.ListToolsRequest{})
if err != nil {
return fmt.Errorf("listing tools: %w", err)
}

buf, err := json.MarshalIndent(response.Tools, "", " ")
if err != nil {
return fmt.Errorf("marshalling tools: %w", err)
}

if verbose {
fmt.Println(len(response.Tools), "tools:")
fmt.Println(string(buf))
} else {
fmt.Println(len(response.Tools), "tools")
}

return nil
}

func call(ctx context.Context, args []string) error {
if len(args) == 0 {
return fmt.Errorf("no tool name provided")
}
toolName := args[0]

c, err := start(ctx)
if err != nil {
return fmt.Errorf("starting client: %w", err)
}
defer c.Close()

request := mcp.CallToolRequest{}
request.Params.Name = toolName
request.Params.Arguments = parseArgs(args[1:])

start := time.Now()
response, err := c.CallTool(ctx, request)
if err != nil {
return fmt.Errorf("calling tool: %w", err)
}
fmt.Println("Tool call took:", time.Since(start))

if response.IsError {
return fmt.Errorf("error calling tool: %s", toolName)
}

for _, content := range response.Content {
if textContent, ok := content.(mcp.TextContent); ok {
fmt.Println(textContent.Text)
} else {
fmt.Println(content)
}
}

return nil
}

func start(ctx context.Context) (*client.Client, error) {
host := os.Getenv("MCPGATEWAY_ENDPOINT")
c, err := client.NewSSEMCPClient("http://" + host + "/sse")
if err != nil {
return nil, err
}

if err := c.Start(ctx); err != nil {
return nil, err
}

initRequest := mcp.InitializeRequest{}
initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
initRequest.Params.ClientInfo = mcp.Implementation{
Name: "docker",
Version: "1.0.0",
}

ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()

if _, err := c.Initialize(ctx, initRequest); err != nil {
return nil, fmt.Errorf("initializing: %w", err)
}

return c, nil
}

func parseArgs(args []string) map[string]any {
parsed := map[string]any{}

for _, arg := range args {
parts := strings.SplitN(arg, "=", 2)
if len(parts) == 2 {
parsed[parts[0]] = parts[1]
} else {
parsed[arg] = nil
}
}

// MCP servers return an error if the args are empty so we make sure
// there is at least one argument
if len(parsed) == 0 {
parsed["args"] = "..."
}

return parsed
}
Loading
Loading