Skip to content

Commit fa41613

Browse files
ilackarmsjmhbh
andauthored
address ux feedback (#25)
addresses the feedback in #21 i have also gone to the extent to remove all but the docker build from `build` command so the `--docker` flag is no longer required --------- Signed-off-by: JM Huibonhoa <jm.huibonhoa@solo.io> Co-authored-by: JM Huibonhoa <jm.huibonhoa@solo.io>
1 parent b665361 commit fa41613

17 files changed

Lines changed: 475 additions & 351 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ kmcp init python my-mcp-server
7373

7474
# Build and test your server
7575
cd my-mcp-server
76-
kmcp build --docker --tag my-mcp-server:latest
76+
kmcp build
7777

7878
# Run locally for testing
7979
kmcp run
@@ -349,7 +349,7 @@ kmcp init go my-go-server \
349349

350350
```bash
351351
# Build a Docker container image for your MCP server project
352-
kmcp build --docker --tag my-server:latest
352+
kmcp build --tag my-server:latest
353353
354354
# Deploy to Kubernetes
355355
kmcp deploy my-server --namespace production --environment production

cmd/kmcp/cmd/build.go

Lines changed: 94 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -3,46 +3,52 @@ package cmd
33
import (
44
"fmt"
55
"os"
6-
"path/filepath"
6+
"os/exec"
7+
"strings"
8+
9+
"github.com/kagent-dev/kmcp/pkg/manifest"
10+
"github.com/stoewer/go-strcase"
711

812
"github.com/kagent-dev/kmcp/pkg/build"
913
"github.com/spf13/cobra"
1014
)
1115

1216
var buildCmd = &cobra.Command{
1317
Use: "build",
14-
Short: "Build MCP server",
18+
Short: "Build MCP server as a Docker image",
1519
Long: `Build an MCP server from the current project.
1620
1721
This command will detect the project type and build the appropriate
18-
MCP server package or Docker image.
22+
MCP server Docker image.
1923
2024
Examples:
21-
kmcp build # Build from current directory
22-
kmcp build --project-dir /path/to/project # Build from specific directory
23-
kmcp build --docker --project-dir ./my-project # Build Docker image from specific directory`,
25+
kmcp build # Build Docker image from current directory
26+
kmcp build --project-dir ./my-project # Build Docker image from specific directory`,
2427
RunE: runBuild,
2528
}
2629

2730
var (
28-
buildDocker bool
29-
buildOutput string
30-
buildTag string
31-
buildPlatform string
32-
buildDir string
31+
buildTag string
32+
buildPush bool
33+
buildKindLoad bool
34+
buildDir string
35+
buildPlatform string
36+
buildKindLoadCluster string
3337
)
3438

3539
func init() {
3640
rootCmd.AddCommand(buildCmd)
3741

38-
buildCmd.Flags().BoolVar(&buildDocker, "docker", false, "Build Docker image")
39-
buildCmd.Flags().StringVarP(&buildOutput, "output", "o", "", "Output directory or image name")
40-
buildCmd.Flags().StringVarP(&buildTag, "tag", "t", "", "Docker image tag")
41-
buildCmd.Flags().StringVar(&buildPlatform, "platform", "", "Target platform (e.g., linux/amd64,linux/arm64)")
42+
buildCmd.Flags().StringVarP(&buildTag, "tag", "t", "", "Docker image tag (alias for --output)")
43+
buildCmd.Flags().BoolVar(&buildPush, "push", false, "Push Docker image to registry")
44+
buildCmd.Flags().BoolVar(&buildKindLoad, "kind-load", false, "Load image into kind cluster (requires kind)")
45+
buildCmd.Flags().StringVar(&buildKindLoadCluster, "kind-load-cluster", "",
46+
"Name of the kind cluster to load image into (default: current cluster)")
4247
buildCmd.Flags().StringVarP(&buildDir, "project-dir", "d", "", "Build directory (default: current directory)")
48+
buildCmd.Flags().StringVar(&buildPlatform, "platform", "", "Target platform (e.g., linux/amd64,linux/arm64)")
4349
}
4450

45-
func runBuild(_ *cobra.Command, _ []string) error {
51+
func runBuild(cmd *cobra.Command, args []string) error {
4652
// Determine build directory
4753
buildDirectory := buildDir
4854
if buildDirectory == "" {
@@ -51,59 +57,93 @@ func runBuild(_ *cobra.Command, _ []string) error {
5157
if err != nil {
5258
return fmt.Errorf("failed to get current directory: %w", err)
5359
}
54-
} else {
55-
// Convert relative path to absolute path
56-
if !filepath.IsAbs(buildDirectory) {
57-
cwd, err := os.Getwd()
58-
if err != nil {
59-
return fmt.Errorf("failed to get current directory: %w", err)
60-
}
61-
buildDirectory = filepath.Join(cwd, buildDirectory)
62-
}
6360
}
6461

65-
if verbose {
66-
fmt.Printf("Building MCP server in: %s\n", buildDirectory)
67-
}
62+
imageName := buildTag
63+
if imageName == "" {
64+
// Load project manifest
65+
manifestManager := manifest.NewManager(buildDirectory)
66+
if !manifestManager.Exists() {
67+
return fmt.Errorf(
68+
"kmcp.yaml not found in %s. Run 'kmcp init' first or specify a valid path with --project-dir",
69+
buildDirectory,
70+
)
71+
}
6872

69-
// Check if this is a valid MCP project
70-
if err := validateMCPProject(buildDirectory); err != nil {
71-
return fmt.Errorf("invalid MCP project: %w", err)
73+
projectManifest, err := manifestManager.Load()
74+
if err != nil {
75+
return fmt.Errorf("failed to load project manifest: %w", err)
76+
}
77+
78+
version := projectManifest.Version
79+
if version == "" {
80+
version = "latest"
81+
}
82+
imageName = fmt.Sprintf("%s:%s", strcase.KebabCase(projectManifest.Name), version)
7283
}
7384

74-
// Create build options
85+
// Execute build
86+
builder := build.New()
7587
opts := build.Options{
7688
ProjectDir: buildDirectory,
77-
Docker: buildDocker,
78-
Output: buildOutput,
79-
Tag: buildTag,
89+
Tag: imageName,
8090
Platform: buildPlatform,
8191
Verbose: verbose,
8292
}
8393

84-
// Execute build
85-
builder := build.New()
86-
return builder.Build(opts)
87-
}
88-
89-
// validateMCPProject checks if the current directory contains a valid MCP project
90-
func validateMCPProject(dir string) error {
91-
// Check for common MCP project indicators
92-
indicators := []string{
93-
"pyproject.toml", // Python projects
94-
"package.json", // Node.js projects
95-
"go.mod", // Go projects
96-
".mcpbuilder.yaml", // Our project config
94+
if err := builder.Build(opts); err != nil {
95+
return fmt.Errorf("build failed: %w", err)
9796
}
9897

99-
for _, indicator := range indicators {
100-
if _, err := os.Stat(filepath.Join(dir, indicator)); err == nil {
101-
if verbose {
102-
fmt.Printf("Detected project file: %s\n", indicator)
98+
if buildPush {
99+
fmt.Printf("Pushing Docker image %s...\n", imageName)
100+
if err := runDocker("push", imageName); err != nil {
101+
return fmt.Errorf("docker push failed: %w", err)
102+
}
103+
fmt.Printf("✅ Docker image pushed successfully\n")
104+
}
105+
if buildKindLoad || buildKindLoadCluster != "" {
106+
fmt.Printf("Loading Docker image %s into kind cluster...\n", imageName)
107+
kindArgs := []string{"load", "docker-image", imageName}
108+
clusterName := buildKindLoadCluster
109+
if clusterName == "" {
110+
var err error
111+
clusterName, err = getCurrentKindClusterName()
112+
if err != nil {
113+
if verbose {
114+
fmt.Printf("could not detect kind cluster name: %v, using default\n", err)
115+
}
116+
clusterName = "kind" // default to kind cluster
103117
}
104-
return nil
105118
}
119+
120+
kindArgs = append(kindArgs, "--name", clusterName)
121+
122+
if err := runKind(kindArgs...); err != nil {
123+
return fmt.Errorf("kind load failed: %w", err)
124+
}
125+
fmt.Printf("✅ Docker image loaded into kind cluster %s\n", clusterName)
106126
}
107127

108-
return fmt.Errorf("no MCP project detected. Expected one of: %v", indicators)
128+
return nil
129+
}
130+
131+
func runDocker(args ...string) error {
132+
if verbose {
133+
fmt.Printf("Running: docker %s\n", strings.Join(args, " "))
134+
}
135+
cmd := exec.Command("docker", args...)
136+
cmd.Stdout = os.Stdout
137+
cmd.Stderr = os.Stderr
138+
return cmd.Run()
139+
}
140+
141+
func runKind(args ...string) error {
142+
if verbose {
143+
fmt.Printf("Running: kind %s\n", strings.Join(args, " "))
144+
}
145+
cmd := exec.Command("kind", args...)
146+
cmd.Stdout = os.Stdout
147+
cmd.Stderr = os.Stderr
148+
return cmd.Run()
109149
}

0 commit comments

Comments
 (0)