Skip to content

Commit 8ba4b44

Browse files
authored
Merge pull request #206 from docker/bootstrap_server
Add command to bootstrap an MCP Server with a template
2 parents 11775d8 + d8e2cf4 commit 8ba4b44

File tree

19 files changed

+768
-0
lines changed

19 files changed

+768
-0
lines changed

cmd/docker-mcp/commands/server.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package commands
33
import (
44
"encoding/json"
55
"fmt"
6+
"path/filepath"
67
"strings"
78

89
"github.com/docker/cli/cli/command"
@@ -117,5 +118,31 @@ func serverCommand(docker docker.Client, dockerCli command.Cli) *cobra.Command {
117118
},
118119
})
119120

121+
var language string
122+
var templateName string
123+
initCommand := &cobra.Command{
124+
Use: "init <directory>",
125+
Short: "Initialize a new MCP server project",
126+
Long: "Initialize a new MCP server project in the specified directory with boilerplate code, Dockerfile, and compose.yaml",
127+
Args: cobra.ExactArgs(1),
128+
RunE: func(cmd *cobra.Command, args []string) error {
129+
dir := args[0]
130+
if err := server.Init(cmd.Context(), dir, language, templateName); err != nil {
131+
return err
132+
}
133+
serverName := filepath.Base(dir)
134+
fmt.Fprintf(cmd.OutOrStdout(), "Successfully initialized MCP server project in %s (template: %s)\n", dir, templateName)
135+
fmt.Fprintf(cmd.OutOrStdout(), "Next steps:\n")
136+
fmt.Fprintf(cmd.OutOrStdout(), " cd %s\n", dir)
137+
fmt.Fprintf(cmd.OutOrStdout(), " docker build -t %s:latest .\n", serverName)
138+
fmt.Fprintf(cmd.OutOrStdout(), " docker compose up\n")
139+
return nil
140+
},
141+
}
142+
initCommand.Flags().StringVar(&language, "language", "go", "Programming language for the server (currently only 'go' is supported)")
143+
initCommand.Flags().StringVar(&templateName, "template", "basic", "Template to use (basic, chatgpt-app-basic)")
144+
_ = initCommand.MarkFlagRequired("template")
145+
cmd.AddCommand(initCommand)
146+
120147
return cmd
121148
}

cmd/docker-mcp/server/init.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package server
2+
3+
import (
4+
"bytes"
5+
"context"
6+
_ "embed"
7+
"fmt"
8+
"os"
9+
"path/filepath"
10+
"text/template"
11+
)
12+
13+
//go:embed templates/basic/main.go
14+
var basicMainGoTemplate string
15+
16+
//go:embed templates/basic/Dockerfile
17+
var basicDockerfileTemplate string
18+
19+
//go:embed templates/basic/compose.yaml
20+
var basicComposeTemplate string
21+
22+
//go:embed templates/basic/catalog.yaml
23+
var basicCatalogTemplate string
24+
25+
//go:embed templates/basic/go.mod.template
26+
var basicGoModTemplate string
27+
28+
//go:embed templates/basic/README.md
29+
var basicReadmeTemplate string
30+
31+
//go:embed templates/chatgpt-app-basic/main.go
32+
var chatgptAppMainGoTemplate string
33+
34+
//go:embed templates/chatgpt-app-basic/Dockerfile
35+
var chatgptAppDockerfileTemplate string
36+
37+
//go:embed templates/chatgpt-app-basic/compose.yaml
38+
var chatgptAppComposeTemplate string
39+
40+
//go:embed templates/chatgpt-app-basic/catalog.yaml
41+
var chatgptAppCatalogTemplate string
42+
43+
//go:embed templates/chatgpt-app-basic/go.mod.template
44+
var chatgptAppGoModTemplate string
45+
46+
//go:embed templates/chatgpt-app-basic/README.md
47+
var chatgptAppReadmeTemplate string
48+
49+
//go:embed templates/chatgpt-app-basic/ui.html
50+
var chatgptAppUITemplate string
51+
52+
type templateData struct {
53+
ServerName string
54+
}
55+
56+
type templateSet struct {
57+
mainGo string
58+
dockerfile string
59+
compose string
60+
catalog string
61+
goMod string
62+
readme string
63+
uiHTML string // optional, only for chatgpt-app-basic
64+
}
65+
66+
func getTemplateSet(templateName string) (*templateSet, error) {
67+
switch templateName {
68+
case "basic":
69+
return &templateSet{
70+
mainGo: basicMainGoTemplate,
71+
dockerfile: basicDockerfileTemplate,
72+
compose: basicComposeTemplate,
73+
catalog: basicCatalogTemplate,
74+
goMod: basicGoModTemplate,
75+
readme: basicReadmeTemplate,
76+
}, nil
77+
case "chatgpt-app-basic":
78+
return &templateSet{
79+
mainGo: chatgptAppMainGoTemplate,
80+
dockerfile: chatgptAppDockerfileTemplate,
81+
compose: chatgptAppComposeTemplate,
82+
catalog: chatgptAppCatalogTemplate,
83+
goMod: chatgptAppGoModTemplate,
84+
readme: chatgptAppReadmeTemplate,
85+
uiHTML: chatgptAppUITemplate,
86+
}, nil
87+
default:
88+
return nil, fmt.Errorf("unknown template: %s (available: basic, chatgpt-app-basic)", templateName)
89+
}
90+
}
91+
92+
// Init initializes a new MCP server project in the specified directory
93+
func Init(_ context.Context, dir string, language string, templateName string) error {
94+
if language != "go" {
95+
return fmt.Errorf("unsupported language: %s (currently only 'go' is supported)", language)
96+
}
97+
98+
// Get the template set
99+
templates, err := getTemplateSet(templateName)
100+
if err != nil {
101+
return err
102+
}
103+
104+
// Create directory if it doesn't exist
105+
if err := os.MkdirAll(dir, 0o755); err != nil {
106+
return fmt.Errorf("creating directory: %w", err)
107+
}
108+
109+
// Check if directory is empty
110+
entries, err := os.ReadDir(dir)
111+
if err != nil {
112+
return fmt.Errorf("reading directory: %w", err)
113+
}
114+
if len(entries) > 0 {
115+
return fmt.Errorf("directory %s is not empty", dir)
116+
}
117+
118+
// Extract server name from directory path
119+
serverName := filepath.Base(dir)
120+
data := templateData{ServerName: serverName}
121+
122+
// Generate files from templates
123+
files := map[string]string{
124+
"main.go": templates.mainGo,
125+
"Dockerfile": templates.dockerfile,
126+
"compose.yaml": templates.compose,
127+
"catalog.yaml": templates.catalog,
128+
"go.mod": templates.goMod,
129+
"README.md": templates.readme,
130+
}
131+
132+
// Add ui.html for chatgpt-app-basic template
133+
if templates.uiHTML != "" {
134+
files["ui.html"] = templates.uiHTML
135+
}
136+
137+
for filename, tmplContent := range files {
138+
// Parse and execute template
139+
tmpl, err := template.New(filename).Parse(tmplContent)
140+
if err != nil {
141+
return fmt.Errorf("parsing template %s: %w", filename, err)
142+
}
143+
144+
var buf bytes.Buffer
145+
if err := tmpl.Execute(&buf, data); err != nil {
146+
return fmt.Errorf("executing template %s: %w", filename, err)
147+
}
148+
149+
// Write file
150+
path := filepath.Join(dir, filename)
151+
if err := os.WriteFile(path, buf.Bytes(), 0o644); err != nil {
152+
return fmt.Errorf("writing %s: %w", filename, err)
153+
}
154+
}
155+
156+
return nil
157+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
FROM golang:1.24-alpine AS builder
2+
3+
WORKDIR /app
4+
5+
# Install git (required for go mod download)
6+
RUN apk add --no-cache git
7+
8+
# Copy source code
9+
COPY . .
10+
11+
# Download dependencies and build the application
12+
RUN go mod tidy && \
13+
CGO_ENABLED=0 GOOS=linux go build -o /mcp-server .
14+
15+
# Final stage
16+
FROM alpine:latest
17+
18+
RUN apk --no-cache add ca-certificates
19+
20+
WORKDIR /root/
21+
22+
# Copy the binary from builder
23+
COPY --from=builder /mcp-server .
24+
25+
# Run the server
26+
ENTRYPOINT ["./mcp-server"]
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# {{.ServerName}} MCP Server
2+
3+
A simple Model Context Protocol (MCP) server written in Go that provides a greeting tool.
4+
5+
## Building
6+
7+
Build the Docker image:
8+
9+
```bash
10+
docker build -t {{.ServerName}}:latest .
11+
```
12+
13+
## Running with Docker Compose
14+
15+
Start the gateway with the {{.ServerName}} server in streaming mode:
16+
17+
```bash
18+
docker compose up
19+
```
20+
21+
The gateway will start in streaming mode on port 8811 and connect to the {{.ServerName}} server. You can then interact with it through MCP clients using HTTP streaming at http://localhost:8811.
22+
23+
## Tools
24+
25+
- **greet**: Says hi to a specified person
26+
- Input: `name` (string) - the person to greet
27+
- Output: A greeting message
28+
29+
## Development
30+
31+
To modify the server, edit `main.go` and rebuild the Docker image.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
registry:
2+
{{.ServerName}}:
3+
description: A simple MCP server that greets users
4+
title: {{.ServerName}}
5+
type: server
6+
image: {{.ServerName}}:latest
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
services:
2+
gateway:
3+
image: docker/mcp-gateway
4+
command:
5+
- --servers={{.ServerName}}
6+
- --catalog=/mcp/catalog.yaml
7+
- --transport=streaming
8+
- --port=8811
9+
environment:
10+
- DOCKER_MCP_IN_CONTAINER=1
11+
volumes:
12+
- /var/run/docker.sock:/var/run/docker.sock
13+
- ./catalog.yaml:/mcp/catalog.yaml
14+
ports:
15+
- "8811:8811"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module {{.ServerName}}
2+
3+
go 1.24
4+
5+
require github.com/modelcontextprotocol/go-sdk v1.0.0
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"log"
6+
7+
"github.com/modelcontextprotocol/go-sdk/mcp"
8+
)
9+
10+
func main() {
11+
// Create a server with a single tool that says "Hi".
12+
server := mcp.NewServer(&mcp.Implementation{Name: "greeter"}, nil)
13+
14+
// Using the generic AddTool automatically populates the input and output
15+
// schema of the tool.
16+
//
17+
// The schema considers 'json' and 'jsonschema' struct tags to get argument
18+
// names and descriptions.
19+
type args struct {
20+
Name string `json:"name" jsonschema:"the person to greet"`
21+
}
22+
mcp.AddTool(server, &mcp.Tool{
23+
Name: "greet",
24+
Description: "say hi",
25+
}, func(_ context.Context, _ *mcp.CallToolRequest, args args) (*mcp.CallToolResult, any, error) {
26+
return &mcp.CallToolResult{
27+
Content: []mcp.Content{
28+
&mcp.TextContent{Text: "Hi " + args.Name},
29+
},
30+
}, nil, nil
31+
})
32+
33+
// server.Run runs the server on the given transport.
34+
//
35+
// In this case, the server communicates over stdin/stdout.
36+
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
37+
log.Printf("Server failed: %v", err)
38+
}
39+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
FROM golang:1.24-alpine AS builder
2+
3+
WORKDIR /app
4+
5+
# Install git (required for go mod download)
6+
RUN apk add --no-cache git
7+
8+
# Copy source code
9+
COPY . .
10+
11+
# Download dependencies and build the application
12+
RUN go mod tidy && \
13+
CGO_ENABLED=0 GOOS=linux go build -o /mcp-server .
14+
15+
# Final stage
16+
FROM alpine:latest
17+
18+
RUN apk --no-cache add ca-certificates
19+
20+
WORKDIR /root/
21+
22+
# Copy the binary from builder
23+
COPY --from=builder /mcp-server .
24+
25+
# Run the server
26+
ENTRYPOINT ["./mcp-server"]

0 commit comments

Comments
 (0)