From 8de5b3704100f43123d0ae98381174789b917eef Mon Sep 17 00:00:00 2001 From: JM Huibonhoa Date: Mon, 28 Jul 2025 21:27:26 -0400 Subject: [PATCH 01/12] add kmcp run local Signed-off-by: JM Huibonhoa --- cmd/kmcp/cmd/init.go | 13 +- cmd/kmcp/cmd/run.go | 165 ++++++++++++++++++ .../python/templates/run_server.sh.tmpl | 4 - 3 files changed, 167 insertions(+), 15 deletions(-) create mode 100644 cmd/kmcp/cmd/run.go delete mode 100644 pkg/frameworks/python/templates/run_server.sh.tmpl diff --git a/cmd/kmcp/cmd/init.go b/cmd/kmcp/cmd/init.go index a744474..4e2ddda 100644 --- a/cmd/kmcp/cmd/init.go +++ b/cmd/kmcp/cmd/init.go @@ -172,17 +172,8 @@ func runInit(_ *cobra.Command, args []string) error { fmt.Printf(" Tools will be automatically discovered from the src/tools/ directory.\n") fmt.Printf(" Use 'kmcp add-tool ' to add new tools after project creation.\n") fmt.Printf("\n") - fmt.Printf(" To connect to the server using the inspector:\n") - fmt.Printf(" run npx @modelcontextprotocol/inspector\n") - fmt.Printf(" open the inspector on localhost:6274 and set transport type to STDIO\n") - fmt.Printf(" copy the `MCP_PROXY_AUTH_TOKEN` into the Proxy Session Token input under configuration\n") - fmt.Printf(" paste the following command into the inspector to connect to the server using the inspector\n") - fmt.Printf(" %s\n", filepath.Join(absProjectPath, "run_server.sh")) - fmt.Printf("\n") - fmt.Printf(" alternatively, run the following commands to start the server\n") - fmt.Printf(" cd %s\n", projectName) - fmt.Printf(" uv sync\n") - fmt.Printf(" uv run python src/main.py\n") + fmt.Printf(" To run the server locally:\n") + fmt.Printf(" kmcp run local --project-dir %s\n", absProjectPath) case frameworkMCPGo: fmt.Printf(" go mod tidy\n") fmt.Printf(" go run main.go\n") diff --git a/cmd/kmcp/cmd/run.go b/cmd/kmcp/cmd/run.go new file mode 100644 index 0000000..5532cbf --- /dev/null +++ b/cmd/kmcp/cmd/run.go @@ -0,0 +1,165 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/spf13/cobra" + "kagent.dev/kmcp/pkg/manifest" +) + +var runCmd = &cobra.Command{ + Use: "run", + Short: "Run MCP server", + Long: `Run an MCP server using the Model Context Protocol inspector. + +This command provides subcommands for different deployment scenarios.`, +} + +var localCmd = &cobra.Command{ + Use: "local", + Short: "Run MCP server locally", + Long: `Run an MCP server locally using the Model Context Protocol inspector. + +This command will: +1. Load the kmcp.yaml configuration from the project directory +2. Determine the framework type and create appropriate configuration +3. Run the MCP server using the Model Context Protocol inspector + +Examples: + kmcp run local --project-dir ./my-project # Run from specific directory`, + RunE: executeLocal, +} + +var ( + localProjectDir string +) + +func init() { + rootCmd.AddCommand(runCmd) + runCmd.AddCommand(localCmd) + + localCmd.Flags().StringVarP(&localProjectDir, "project-dir", "d", "", "Project directory to use (required)") + localCmd.MarkFlagRequired("project-dir") +} + +func executeLocal(_ *cobra.Command, _ []string) error { + // Determine project directory + projectDir := localProjectDir + if projectDir == "" { + return fmt.Errorf("--project-dir is required") + } + + // Convert relative path to absolute path + if !filepath.IsAbs(projectDir) { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current directory: %w", err) + } + projectDir = filepath.Join(cwd, projectDir) + } + + if verbose { + fmt.Printf("Using project directory: %s\n", projectDir) + } + + // Check if kmcp.yaml exists + manager := manifest.NewManager(projectDir) + if !manager.Exists() { + return fmt.Errorf("this directory is not an mcp-server directory: kmcp.yaml not found") + } + + // Load the manifest + manifest, err := manager.Load() + if err != nil { + return fmt.Errorf("failed to load kmcp.yaml: %w", err) + } + + // Check if npx is installed + if err := checkNpxInstalled(); err != nil { + return err + } + + // Determine framework and create configuration + switch manifest.Framework { + case "fastmcp-python": + return runFastMCPPython(projectDir, manifest) + default: + return fmt.Errorf("unsupported framework: %s", manifest.Framework) + } +} + +func checkNpxInstalled() error { + cmd := exec.Command("npx", "--version") + if err := cmd.Run(); err != nil { + return fmt.Errorf("npx is required to run the modelcontextinstaller. Please install Node.js and npm to get npx") + } + return nil +} + +func runFastMCPPython(projectDir string, manifest *manifest.ProjectManifest) error { + // Check if uv is available + if _, err := exec.LookPath("uv"); err != nil { + return fmt.Errorf("uv is required for this commandto run fastmcp-python projects locally. Please install uv: https://docs.astral.sh/uv/getting-started/installation/") + } + + // Run uv sync first + if verbose { + fmt.Printf("Running uv sync in: %s\n", projectDir) + } + syncCmd := exec.Command("uv", "sync") + syncCmd.Dir = projectDir + syncCmd.Stdout = os.Stdout + syncCmd.Stderr = os.Stderr + if err := syncCmd.Run(); err != nil { + return fmt.Errorf("failed to run uv sync: %w", err) + } + + // Create mcp-server-config.json + var serverConfig map[string]interface{} + serverConfig = map[string]interface{}{ + "command": "uv", + "args": []string{"run", "python", "src/main.py"}, + } + + config := map[string]interface{}{ + "mcpServers": map[string]interface{}{ + manifest.Name: serverConfig, + }, + } + + configPath := filepath.Join(projectDir, "mcp-server-config.json") + configData, err := json.MarshalIndent(config, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + if err := os.WriteFile(configPath, configData, 0644); err != nil { + return fmt.Errorf("failed to write mcp-server-config.json: %w", err) + } + + if verbose { + fmt.Printf("Created mcp-server-config.json: %s\n", configPath) + } + + // Run the inspector + args := []string{ + "@modelcontextprotocol/inspector", + "--config", configPath, + "--server", manifest.Name, + } + + if verbose { + fmt.Printf("Running: npx %s\n", args) + } + + cmd := exec.Command("npx", args...) + cmd.Dir = projectDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() +} diff --git a/pkg/frameworks/python/templates/run_server.sh.tmpl b/pkg/frameworks/python/templates/run_server.sh.tmpl deleted file mode 100644 index 40e96d4..0000000 --- a/pkg/frameworks/python/templates/run_server.sh.tmpl +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -cd "$(dirname "$0")" -source .venv/bin/activate -python src/main.py \ No newline at end of file From ab5c7fbf8bb5d5faf36a24599ee22ffee7747385 Mon Sep 17 00:00:00 2001 From: JM Huibonhoa Date: Mon, 28 Jul 2025 23:03:52 -0400 Subject: [PATCH 02/12] fix issue where an extra character was cut after refs/tags/ Signed-off-by: JM Huibonhoa --- .github/workflows/tag.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tag.yaml b/.github/workflows/tag.yaml index def00ec..bce35e5 100644 --- a/.github/workflows/tag.yaml +++ b/.github/workflows/tag.yaml @@ -47,7 +47,7 @@ jobs: if [ -n "${{ github.event.inputs.version }}" ]; then export VERSION=${{ github.event.inputs.version }} else - export VERSION=$(echo "$GITHUB_REF" | cut -c12-) + export VERSION=$(echo "$GITHUB_REF" | cut -c11-) fi echo "Building Docker image with version: ${VERSION}" make docker-build VERSION=${VERSION} @@ -75,7 +75,7 @@ jobs: if [ -n "${{ github.event.inputs.version }}" ]; then export VERSION=${{ github.event.inputs.version }} else - export VERSION=$(echo "$GITHUB_REF" | cut -c12-) + export VERSION=$(echo "$GITHUB_REF" | cut -c11-) fi echo "Publishing Helm chart with version: ${VERSION}" make helm-publish VERSION=${VERSION} @@ -97,7 +97,7 @@ jobs: if [ -n "${{ github.event.inputs.version }}" ]; then export VERSION=${{ github.event.inputs.version }} else - export VERSION=$(echo "$GITHUB_REF" | cut -c12-) + export VERSION=$(echo "$GITHUB_REF" | cut -c11-) fi echo "Building release artifacts with version: ${VERSION}" make build-cli VERSION=${VERSION} From acdcb5d13d563b30bbdeac6864107e8f98d98235 Mon Sep 17 00:00:00 2001 From: JM Huibonhoa Date: Tue, 29 Jul 2025 00:05:33 -0400 Subject: [PATCH 03/12] working kmcp run kind Signed-off-by: JM Huibonhoa --- cmd/kmcp/cmd/build.go | 6 +- cmd/kmcp/cmd/run.go | 413 +++++++++++++++++++++++++++++++++++----- cmd/kmcp/cmd/secrets.go | 4 +- test/e2e/e2e_test.go | 4 +- 4 files changed, 370 insertions(+), 57 deletions(-) diff --git a/cmd/kmcp/cmd/build.go b/cmd/kmcp/cmd/build.go index a7d5bbd..75def91 100644 --- a/cmd/kmcp/cmd/build.go +++ b/cmd/kmcp/cmd/build.go @@ -19,8 +19,8 @@ MCP server package or Docker image. Examples: kmcp build # Build from current directory - kmcp build --dir /path/to/project # Build from specific directory - kmcp build --docker --dir ./my-project # Build Docker image from specific directory`, + kmcp build --project-dir /path/to/project # Build from specific directory + kmcp build --docker --project-dir ./my-project # Build Docker image from specific directory`, RunE: runBuild, } @@ -39,7 +39,7 @@ func init() { buildCmd.Flags().StringVarP(&buildOutput, "output", "o", "", "Output directory or image name") buildCmd.Flags().StringVarP(&buildTag, "tag", "t", "", "Docker image tag") buildCmd.Flags().StringVar(&buildPlatform, "platform", "", "Target platform (e.g., linux/amd64,linux/arm64)") - buildCmd.Flags().StringVarP(&buildDir, "dir", "d", "", "Build directory (default: current directory)") + buildCmd.Flags().StringVarP(&buildDir, "project-dir", "d", "", "Build directory (default: current directory)") } func runBuild(_ *cobra.Command, _ []string) error { diff --git a/cmd/kmcp/cmd/run.go b/cmd/kmcp/cmd/run.go index 5532cbf..9bd7961 100644 --- a/cmd/kmcp/cmd/run.go +++ b/cmd/kmcp/cmd/run.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" "github.com/spf13/cobra" "kagent.dev/kmcp/pkg/manifest" @@ -34,48 +35,54 @@ Examples: RunE: executeLocal, } +var kindCmd = &cobra.Command{ + Use: "kind", + Short: "Run MCP server in kind cluster", + Long: `Run an MCP server in a kind cluster by building and deploying. + +This command will: +1. Check if kind is available and create a cluster if needed +2. Deploy the KMCP controller (includes CRDs) +3. Build the MCP server Docker image +4. Deploy the MCP server to the kind cluster in the specified namespace + +Examples: + kmcp run kind --project-dir ./my-project # Run from specific directory + kmcp run kind --namespace my-namespace # Deploy to specific namespace + kmcp run kind --registry-config ~/.docker/config.json # Use custom registry config + kmcp run kind --version v0.0.1 # Deploy specific controller version`, + RunE: executeKind, +} + var ( - localProjectDir string + localProjectDir string + kindNamespace string + kindRegistryConfig string + kindVersion string ) func init() { rootCmd.AddCommand(runCmd) runCmd.AddCommand(localCmd) + runCmd.AddCommand(kindCmd) - localCmd.Flags().StringVarP(&localProjectDir, "project-dir", "d", "", "Project directory to use (required)") - localCmd.MarkFlagRequired("project-dir") + localCmd.Flags().StringVarP(&localProjectDir, "project-dir", "d", "", "Project directory to use (default: current directory)") + kindCmd.Flags().StringVarP(&localProjectDir, "project-dir", "d", "", "Project directory to use (default: current directory)") + kindCmd.Flags().StringVarP(&kindNamespace, "namespace", "n", "default", "Namespace to deploy to (default: default)") + // TODO: registry-config flag can be removed once the helm chart is in a publiclly accessible registry + kindCmd.Flags().StringVar(&kindRegistryConfig, "registry-config", "", "Path to docker registry config file (required for controller deployment)") + kindCmd.Flags().StringVar(&kindVersion, "version", "", "Version of the controller to deploy (defaults to kmcp version)") } func executeLocal(_ *cobra.Command, _ []string) error { - // Determine project directory - projectDir := localProjectDir - if projectDir == "" { - return fmt.Errorf("--project-dir is required") - } - - // Convert relative path to absolute path - if !filepath.IsAbs(projectDir) { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get current directory: %w", err) - } - projectDir = filepath.Join(cwd, projectDir) - } - - if verbose { - fmt.Printf("Using project directory: %s\n", projectDir) - } - - // Check if kmcp.yaml exists - manager := manifest.NewManager(projectDir) - if !manager.Exists() { - return fmt.Errorf("this directory is not an mcp-server directory: kmcp.yaml not found") + projectDir, err := getProjectDir() + if err != nil { + return err } - // Load the manifest - manifest, err := manager.Load() + manifest, err := getProjectManifest(projectDir) if err != nil { - return fmt.Errorf("failed to load kmcp.yaml: %w", err) + return err } // Check if npx is installed @@ -100,6 +107,53 @@ func checkNpxInstalled() error { return nil } +// createMCPInspectorConfig creates an MCP inspector configuration file +func createMCPInspectorConfig(serverName string, serverConfig map[string]interface{}, configPath string) error { + config := map[string]interface{}{ + "mcpServers": map[string]interface{}{ + serverName: serverConfig, + }, + } + + configData, err := json.MarshalIndent(config, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal config: %w", err) + } + + if err := os.WriteFile(configPath, configData, 0644); err != nil { + return fmt.Errorf("failed to write mcp-server-config.json: %w", err) + } + + if verbose { + fmt.Printf("Created mcp-server-config.json: %s\n", configPath) + } + + return nil +} + +// runMCPInspector runs the MCP inspector with the given configuration +func runMCPInspector(configPath, serverName string, workingDir string) error { + args := []string{ + "@modelcontextprotocol/inspector", + "--config", configPath, + "--server", serverName, + } + + if verbose { + fmt.Printf("Running: npx %s\n", args) + } + + cmd := exec.Command("npx", args...) + if workingDir != "" { + cmd.Dir = workingDir + } + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + // Run synchronously + return cmd.Run() +} + func runFastMCPPython(projectDir string, manifest *manifest.ProjectManifest) error { // Check if uv is available if _, err := exec.LookPath("uv"); err != nil { @@ -118,48 +172,307 @@ func runFastMCPPython(projectDir string, manifest *manifest.ProjectManifest) err return fmt.Errorf("failed to run uv sync: %w", err) } - // Create mcp-server-config.json - var serverConfig map[string]interface{} - serverConfig = map[string]interface{}{ + // Create server configuration for local execution + serverConfig := map[string]interface{}{ "command": "uv", "args": []string{"run", "python", "src/main.py"}, } - config := map[string]interface{}{ - "mcpServers": map[string]interface{}{ - manifest.Name: serverConfig, - }, + // Create MCP inspector config + configPath := filepath.Join(projectDir, "mcp-server-config.json") + if err := createMCPInspectorConfig(manifest.Name, serverConfig, configPath); err != nil { + return err } - configPath := filepath.Join(projectDir, "mcp-server-config.json") - configData, err := json.MarshalIndent(config, "", " ") + // Run the inspector + return runMCPInspector(configPath, manifest.Name, projectDir) +} + +func getProjectDir() (string, error) { + // Determine project directory + projectDir := localProjectDir + if projectDir == "" { + // Use current working directory + var err error + projectDir, err = os.Getwd() + if err != nil { + return "", fmt.Errorf("failed to get current directory: %w", err) + } + } else { + // Convert relative path to absolute path + if !filepath.IsAbs(projectDir) { + cwd, err := os.Getwd() + if err != nil { + return "", fmt.Errorf("failed to get current directory: %w", err) + } + projectDir = filepath.Join(cwd, projectDir) + } + } + + if verbose { + fmt.Printf("Using project directory: %s\n", projectDir) + } + + return projectDir, nil +} + +func getProjectManifest(projectDir string) (*manifest.ProjectManifest, error) { + // Check if kmcp.yaml exists + manager := manifest.NewManager(projectDir) + if !manager.Exists() { + return nil, fmt.Errorf("this directory is not an mcp-server directory: kmcp.yaml not found") + } + + // Load the manifest + manifest, err := manager.Load() if err != nil { - return fmt.Errorf("failed to marshal config: %w", err) + return nil, fmt.Errorf("failed to load kmcp.yaml: %w", err) } - if err := os.WriteFile(configPath, configData, 0644); err != nil { - return fmt.Errorf("failed to write mcp-server-config.json: %w", err) + return manifest, nil +} + +func executeKind(_ *cobra.Command, _ []string) error { + projectDir, err := getProjectDir() + if err != nil { + return err + } + + manifest, err := getProjectManifest(projectDir) + if err != nil { + return err + } + + // Check if kind is available + if err := checkKindAvailable(); err != nil { + return err + } + + // Ensure kind cluster exists + if err := ensureKindCluster(); err != nil { + return err + } + + // Deploy controller + if err := deployController(kindNamespace, kindRegistryConfig, kindVersion); err != nil { + return err + } + + // Build the Docker image + if err := buildDockerImage(projectDir); err != nil { + return err + } + + // Deploy to kind cluster + if err := deployToKind(projectDir, manifest, kindNamespace); err != nil { + return err + } + + fmt.Printf("✅ MCP server successfully deployed to kind cluster\n") + fmt.Printf("💡 Check status with: kubectl get mcpserver %s -n %s\n", manifest.Name, kindNamespace) + fmt.Printf("💡 View logs with: kubectl logs -l app.kubernetes.io/name=%s -n %s\n", manifest.Name, kindNamespace) + fmt.Printf("🔌 Port forward the MCP server: kubectl port-forward deployment/%s 3000:3000 -n %s\n", manifest.Name, kindNamespace) + fmt.Printf("\nPress Enter to start the MCP inspector...") + fmt.Scanln() // Wait for user input + + // Create MCP inspector config and start inspector + if err := startInspector(projectDir, manifest.Name); err != nil { + return err + } + + return nil +} + +func checkKindAvailable() error { + cmd := exec.Command("kind", "version") + if err := cmd.Run(); err != nil { + return fmt.Errorf("kind is required but not found. Please install kind: https://kind.sigs.k8s.io/docs/user/quick-start/#installation") + } + return nil +} + +func ensureKindCluster() error { + // Check if kind cluster exists + cmd := exec.Command("kind", "get", "clusters") + output, err := cmd.Output() + if err != nil { + return fmt.Errorf("failed to check kind clusters: %w", err) + } + + clusters := strings.TrimSpace(string(output)) + if !strings.Contains(clusters, "kind") { + if verbose { + fmt.Printf("Creating kind cluster...\n") + } + + // Create kind cluster + createCmd := exec.Command("kind", "create", "cluster", "--name", "kind") + createCmd.Stdout = os.Stdout + createCmd.Stderr = os.Stderr + if err := createCmd.Run(); err != nil { + return fmt.Errorf("failed to create kind cluster: %w", err) + } + } else if verbose { + fmt.Printf("Kind cluster already exists\n") } + return nil +} + +func buildDockerImage(projectDir string) error { if verbose { - fmt.Printf("Created mcp-server-config.json: %s\n", configPath) + fmt.Printf("Building Docker image...\n") } - // Run the inspector + // Run kmcp build --docker + buildCmd := exec.Command("kmcp", "build", "--docker", "--project-dir", projectDir) + buildCmd.Stdout = os.Stdout + buildCmd.Stderr = os.Stderr + if err := buildCmd.Run(); err != nil { + return fmt.Errorf("failed to build Docker image: %w", err) + } + + // Load the built image into kind cluster + if verbose { + fmt.Printf("Loading MCP server image into kind cluster\n") + } + + // Get the project name from the manifest to determine the image name + manifest, err := getProjectManifest(projectDir) + if err != nil { + return fmt.Errorf("failed to get project manifest: %w", err) + } + + // Use the project name as the image name (this is the default behavior of kmcp build) + imageName := fmt.Sprintf("%s:latest", strings.ToLower(strings.ReplaceAll(manifest.Name, "_", "-"))) + + loadCmd := exec.Command("kind", "load", "docker-image", imageName) + loadCmd.Stdout = os.Stdout + loadCmd.Stderr = os.Stderr + if err := loadCmd.Run(); err != nil { + return fmt.Errorf("failed to load MCP server image into kind cluster: %w", err) + } + + return nil +} + +func deployToKind(projectDir string, manifest *manifest.ProjectManifest, namespace string) error { + if verbose { + fmt.Printf("Deploying to kind cluster in namespace: %s\n", namespace) + } + + // Run kmcp deploy mcp with namespace + deployCmd := exec.Command("kmcp", "deploy", "mcp", manifest.Name, "--file", fmt.Sprintf("%s/kmcp.yaml", projectDir), "--namespace", namespace) + deployCmd.Stdout = os.Stdout + deployCmd.Stderr = os.Stderr + if err := deployCmd.Run(); err != nil { + return fmt.Errorf("failed to deploy to kind cluster: %w", err) + } + + return nil +} + +func installCRDs() error { + if verbose { + fmt.Printf("Installing CRDs...\n") + } + + // Apply CRDs from the config directory + cmd := exec.Command("kubectl", "apply", "-f", "config/crd/bases/") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to install CRDs: %w", err) + } + + return nil +} + +func deployController(namespace string, registryConfig string, version string) error { + if verbose { + fmt.Printf("Deploying controller to namespace: %s\n", namespace) + if version != "" { + fmt.Printf("Using specified version: %s\n", version) + } + } + + // Pull and load the controller image into kind cluster + if version != "" { + imageName := fmt.Sprintf("ghcr.io/kagent-dev/kmcp/controller:%s", version) + if verbose { + fmt.Printf("Pulling controller image: %s\n", imageName) + } + + // Pull the image + pullCmd := exec.Command("docker", "pull", imageName) + pullCmd.Stdout = os.Stdout + pullCmd.Stderr = os.Stderr + if err := pullCmd.Run(); err != nil { + return fmt.Errorf("failed to pull controller image: %w", err) + } + + // Load the image into kind cluster + if verbose { + fmt.Printf("Loading controller image into kind cluster\n") + } + loadCmd := exec.Command("kind", "load", "docker-image", imageName) + loadCmd.Stdout = os.Stdout + loadCmd.Stderr = os.Stderr + if err := loadCmd.Run(); err != nil { + return fmt.Errorf("failed to load controller image into kind cluster: %w", err) + } + } + + // Build helm install command directly args := []string{ - "@modelcontextprotocol/inspector", - "--config", configPath, - "--server", manifest.Name, + "install", "kmcp", "oci://ghcr.io/kagent-dev/kmcp/helm/kmcp", + "--version", version, + "--namespace", namespace, + "--create-namespace", + } + + // Add registry config if found + if registryConfig != "" { + args = append(args, "--registry-config", registryConfig) + } + + // Override the image tag to match the version + if version != "" { + args = append(args, "--set", fmt.Sprintf("image.tag=%s", version)) } if verbose { - fmt.Printf("Running: npx %s\n", args) + fmt.Printf("Executing: helm %s\n", strings.Join(args, " ")) } - cmd := exec.Command("npx", args...) - cmd.Dir = projectDir + cmd := exec.Command("helm", args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - return cmd.Run() + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to deploy controller: %w", err) + } + + return nil +} + +func startInspector(projectDir string, serverName string) error { + if verbose { + fmt.Printf("Starting MCP inspector...\n") + } + + // Create server configuration for kind deployment + serverConfig := map[string]interface{}{ + "type": "streamable-http", + "url": "http://localhost:3000/mcp", + } + + // Create MCP inspector config + configPath := filepath.Join(projectDir, "mcp-server-config.json") + if err := createMCPInspectorConfig(serverName, serverConfig, configPath); err != nil { + return err + } + + // Run the inspector in background + return runMCPInspector(configPath, serverName, projectDir) } diff --git a/cmd/kmcp/cmd/secrets.go b/cmd/kmcp/cmd/secrets.go index 5bf4e00..a9d73c2 100644 --- a/cmd/kmcp/cmd/secrets.go +++ b/cmd/kmcp/cmd/secrets.go @@ -50,7 +50,7 @@ Examples: kmcp secrets sync staging --from-file .env.staging # Sync secrets from a specific project directory - kmcp secrets sync staging --dir ./my-project + kmcp secrets sync staging --project-dir ./my-project # Perform a dry run to see the generated secret without applying it kmcp secrets sync production --dry-run @@ -68,7 +68,7 @@ func init() { // create-k8s-secret-from-env flags syncCmd.Flags().StringVar(&secretSourceFile, "from-file", ".env", "Source .env file to sync from") syncCmd.Flags().BoolVar(&secretDryRun, "dry-run", false, "Output the generated secret YAML instead of applying it") - syncCmd.Flags().StringVarP(&secretDir, "dir", "d", "", "Project directory (default: current directory)") + syncCmd.Flags().StringVarP(&secretDir, "project-dir", "d", "", "Project directory (default: current directory)") } func runSync(cmd *cobra.Command, args []string) error { diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 367a955..bbd1f4f 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -202,12 +202,12 @@ var _ = ginkgo.Describe("Manager", ginkgo.Ordered, func() { ginkgo.By("creating Kubernetes secret from existing .env.local file") envFilePath := fmt.Sprintf("%s/.env.local", projectDir) - cmd = exec.Command("dist/kmcp", "secrets", "sync", "local", "--from-file", envFilePath, "--dir", projectDir) + cmd = exec.Command("dist/kmcp", "secrets", "sync", "local", "--from-file", envFilePath, "--project-dir", projectDir) _, err = utils.Run(cmd) gomega.Expect(err).NotTo(gomega.HaveOccurred(), "Failed to create secret from .env.local file") ginkgo.By("building the Docker image for the knowledge-assistant project") - cmd = exec.Command("dist/kmcp", "build", "--docker", "--verbose", "--dir", projectDir) + cmd = exec.Command("dist/kmcp", "build", "--docker", "--verbose", "--project-dir", projectDir) _, err = utils.Run(cmd) gomega.Expect(err).NotTo(gomega.HaveOccurred(), "Failed to build Docker image") From 72ab220eae128813ca56aa28d8b7fbd4ca61b3f3 Mon Sep 17 00:00:00 2001 From: JM Huibonhoa Date: Tue, 29 Jul 2025 10:38:27 -0400 Subject: [PATCH 04/12] fix lint issues Signed-off-by: JM Huibonhoa --- cmd/kmcp/cmd/run.go | 78 +++++++++++++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 27 deletions(-) diff --git a/cmd/kmcp/cmd/run.go b/cmd/kmcp/cmd/run.go index 9bd7961..dab2847 100644 --- a/cmd/kmcp/cmd/run.go +++ b/cmd/kmcp/cmd/run.go @@ -66,12 +66,34 @@ func init() { runCmd.AddCommand(localCmd) runCmd.AddCommand(kindCmd) - localCmd.Flags().StringVarP(&localProjectDir, "project-dir", "d", "", "Project directory to use (default: current directory)") - kindCmd.Flags().StringVarP(&localProjectDir, "project-dir", "d", "", "Project directory to use (default: current directory)") + localCmd.Flags().StringVarP( + &localProjectDir, + "project-dir", + "d", + "", + "Project directory to use (default: current directory)", + ) + kindCmd.Flags().StringVarP( + &localProjectDir, + "project-dir", + "d", + "", + "Project directory to use (default: current directory)", + ) kindCmd.Flags().StringVarP(&kindNamespace, "namespace", "n", "default", "Namespace to deploy to (default: default)") - // TODO: registry-config flag can be removed once the helm chart is in a publiclly accessible registry - kindCmd.Flags().StringVar(&kindRegistryConfig, "registry-config", "", "Path to docker registry config file (required for controller deployment)") - kindCmd.Flags().StringVar(&kindVersion, "version", "", "Version of the controller to deploy (defaults to kmcp version)") + // TODO: registry-config flag can be removed once the helm chart is in a publicly accessible registry + kindCmd.Flags().StringVar( + &kindRegistryConfig, + "registry-config", + "", + "Path to docker registry config file (required for controller deployment)", + ) + kindCmd.Flags().StringVar( + &kindVersion, + "version", + "", + "Version of the controller to deploy (defaults to kmcp version)", + ) } func executeLocal(_ *cobra.Command, _ []string) error { @@ -157,7 +179,8 @@ func runMCPInspector(configPath, serverName string, workingDir string) error { func runFastMCPPython(projectDir string, manifest *manifest.ProjectManifest) error { // Check if uv is available if _, err := exec.LookPath("uv"); err != nil { - return fmt.Errorf("uv is required for this commandto run fastmcp-python projects locally. Please install uv: https://docs.astral.sh/uv/getting-started/installation/") + return fmt.Errorf("uv is required for this command to run fastmcp-python projects locally. " + + "Please install uv: https://docs.astral.sh/uv/getting-started/installation/") } // Run uv sync first @@ -270,10 +293,18 @@ func executeKind(_ *cobra.Command, _ []string) error { fmt.Printf("✅ MCP server successfully deployed to kind cluster\n") fmt.Printf("💡 Check status with: kubectl get mcpserver %s -n %s\n", manifest.Name, kindNamespace) - fmt.Printf("💡 View logs with: kubectl logs -l app.kubernetes.io/name=%s -n %s\n", manifest.Name, kindNamespace) - fmt.Printf("🔌 Port forward the MCP server: kubectl port-forward deployment/%s 3000:3000 -n %s\n", manifest.Name, kindNamespace) - fmt.Printf("\nPress Enter to start the MCP inspector...") - fmt.Scanln() // Wait for user input + fmt.Printf( + "💡 View logs with: kubectl logs -l app.kubernetes.io/name=%s -n %s\n", + manifest.Name, + kindNamespace, + ) + fmt.Printf( + "🔌 Port forward the MCP server: kubectl port-forward deployment/%s 3000:3000 -n %s\n", + manifest.Name, + kindNamespace, + ) + fmt.Printf("\nPress Enter to start the MCP inspector after the MCP server is ready...") + _, _ = fmt.Scanln() // Wait for user input // Create MCP inspector config and start inspector if err := startInspector(projectDir, manifest.Name); err != nil { @@ -362,7 +393,16 @@ func deployToKind(projectDir string, manifest *manifest.ProjectManifest, namespa } // Run kmcp deploy mcp with namespace - deployCmd := exec.Command("kmcp", "deploy", "mcp", manifest.Name, "--file", fmt.Sprintf("%s/kmcp.yaml", projectDir), "--namespace", namespace) + deployCmd := exec.Command( + "kmcp", + "deploy", + "mcp", + manifest.Name, + "--file", + fmt.Sprintf("%s/kmcp.yaml", projectDir), + "--namespace", + namespace, + ) deployCmd.Stdout = os.Stdout deployCmd.Stderr = os.Stderr if err := deployCmd.Run(); err != nil { @@ -372,22 +412,6 @@ func deployToKind(projectDir string, manifest *manifest.ProjectManifest, namespa return nil } -func installCRDs() error { - if verbose { - fmt.Printf("Installing CRDs...\n") - } - - // Apply CRDs from the config directory - cmd := exec.Command("kubectl", "apply", "-f", "config/crd/bases/") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to install CRDs: %w", err) - } - - return nil -} - func deployController(namespace string, registryConfig string, version string) error { if verbose { fmt.Printf("Deploying controller to namespace: %s\n", namespace) From 5221d589624f3ac9bdc2a5f574d8ef291c6b9106 Mon Sep 17 00:00:00 2001 From: JM Huibonhoa Date: Tue, 29 Jul 2025 11:03:30 -0400 Subject: [PATCH 05/12] fix lint issues Signed-off-by: JM Huibonhoa --- cmd/kmcp/cmd/run.go | 9 ++++++--- cmd/kmcp/cmd/secrets.go | 3 +-- pkg/frameworks/golang/generator.go | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/cmd/kmcp/cmd/run.go b/cmd/kmcp/cmd/run.go index dab2847..b0b3b16 100644 --- a/cmd/kmcp/cmd/run.go +++ b/cmd/kmcp/cmd/run.go @@ -179,8 +179,10 @@ func runMCPInspector(configPath, serverName string, workingDir string) error { func runFastMCPPython(projectDir string, manifest *manifest.ProjectManifest) error { // Check if uv is available if _, err := exec.LookPath("uv"); err != nil { - return fmt.Errorf("uv is required for this command to run fastmcp-python projects locally. " + - "Please install uv: https://docs.astral.sh/uv/getting-started/installation/") + uvInstallURL := "https://docs.astral.sh/uv/getting-started/installation/" + return fmt.Errorf( + "uv is required for this command to run fastmcp-python projects locally. Please install uv: %s", uvInstallURL, + ) } // Run uv sync first @@ -317,7 +319,8 @@ func executeKind(_ *cobra.Command, _ []string) error { func checkKindAvailable() error { cmd := exec.Command("kind", "version") if err := cmd.Run(); err != nil { - return fmt.Errorf("kind is required but not found. Please install kind: https://kind.sigs.k8s.io/docs/user/quick-start/#installation") + kindInstallURL := "https://kind.sigs.k8s.io/docs/user/quick-start/#installation" + return fmt.Errorf("kind is required but not found. Please install kind: %s", kindInstallURL) } return nil } diff --git a/cmd/kmcp/cmd/secrets.go b/cmd/kmcp/cmd/secrets.go index a9d73c2..536e252 100644 --- a/cmd/kmcp/cmd/secrets.go +++ b/cmd/kmcp/cmd/secrets.go @@ -12,7 +12,6 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" - _ "k8s.io/client-go/plugin/pkg/client/auth" "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/yaml" ) @@ -71,7 +70,7 @@ func init() { syncCmd.Flags().StringVarP(&secretDir, "project-dir", "d", "", "Project directory (default: current directory)") } -func runSync(cmd *cobra.Command, args []string) error { +func runSync(_ *cobra.Command, args []string) error { environment := args[0] // Determine project root diff --git a/pkg/frameworks/golang/generator.go b/pkg/frameworks/golang/generator.go index 0a996ff..50d4e8a 100644 --- a/pkg/frameworks/golang/generator.go +++ b/pkg/frameworks/golang/generator.go @@ -15,11 +15,11 @@ func NewGenerator() *Generator { } // GenerateProject generates a new Go project. -func (g *Generator) GenerateProject(config templates.ProjectConfig) error { +func (g *Generator) GenerateProject(_ templates.ProjectConfig) error { return fmt.Errorf("go project generation not yet implemented") } // GenerateTool generates a new tool for a Go project. -func (g *Generator) GenerateTool(projectPath string, toolName string, config map[string]interface{}) error { +func (g *Generator) GenerateTool(_ string, _ string, _ map[string]interface{}) error { return fmt.Errorf("go tool generation not yet implemented") } From 597852505b1fe38d09b49529680d09fbca156ac6 Mon Sep 17 00:00:00 2001 From: JM Huibonhoa Date: Tue, 29 Jul 2025 11:58:07 -0400 Subject: [PATCH 06/12] remove kind subcommand Signed-off-by: JM Huibonhoa --- cmd/kmcp/cmd/run.go | 294 +------------------------------------------- 1 file changed, 1 insertion(+), 293 deletions(-) diff --git a/cmd/kmcp/cmd/run.go b/cmd/kmcp/cmd/run.go index b0b3b16..fe06030 100644 --- a/cmd/kmcp/cmd/run.go +++ b/cmd/kmcp/cmd/run.go @@ -6,7 +6,6 @@ import ( "os" "os/exec" "path/filepath" - "strings" "github.com/spf13/cobra" "kagent.dev/kmcp/pkg/manifest" @@ -35,36 +34,13 @@ Examples: RunE: executeLocal, } -var kindCmd = &cobra.Command{ - Use: "kind", - Short: "Run MCP server in kind cluster", - Long: `Run an MCP server in a kind cluster by building and deploying. - -This command will: -1. Check if kind is available and create a cluster if needed -2. Deploy the KMCP controller (includes CRDs) -3. Build the MCP server Docker image -4. Deploy the MCP server to the kind cluster in the specified namespace - -Examples: - kmcp run kind --project-dir ./my-project # Run from specific directory - kmcp run kind --namespace my-namespace # Deploy to specific namespace - kmcp run kind --registry-config ~/.docker/config.json # Use custom registry config - kmcp run kind --version v0.0.1 # Deploy specific controller version`, - RunE: executeKind, -} - var ( - localProjectDir string - kindNamespace string - kindRegistryConfig string - kindVersion string + localProjectDir string ) func init() { rootCmd.AddCommand(runCmd) runCmd.AddCommand(localCmd) - runCmd.AddCommand(kindCmd) localCmd.Flags().StringVarP( &localProjectDir, @@ -73,27 +49,6 @@ func init() { "", "Project directory to use (default: current directory)", ) - kindCmd.Flags().StringVarP( - &localProjectDir, - "project-dir", - "d", - "", - "Project directory to use (default: current directory)", - ) - kindCmd.Flags().StringVarP(&kindNamespace, "namespace", "n", "default", "Namespace to deploy to (default: default)") - // TODO: registry-config flag can be removed once the helm chart is in a publicly accessible registry - kindCmd.Flags().StringVar( - &kindRegistryConfig, - "registry-config", - "", - "Path to docker registry config file (required for controller deployment)", - ) - kindCmd.Flags().StringVar( - &kindVersion, - "version", - "", - "Version of the controller to deploy (defaults to kmcp version)", - ) } func executeLocal(_ *cobra.Command, _ []string) error { @@ -256,250 +211,3 @@ func getProjectManifest(projectDir string) (*manifest.ProjectManifest, error) { return manifest, nil } - -func executeKind(_ *cobra.Command, _ []string) error { - projectDir, err := getProjectDir() - if err != nil { - return err - } - - manifest, err := getProjectManifest(projectDir) - if err != nil { - return err - } - - // Check if kind is available - if err := checkKindAvailable(); err != nil { - return err - } - - // Ensure kind cluster exists - if err := ensureKindCluster(); err != nil { - return err - } - - // Deploy controller - if err := deployController(kindNamespace, kindRegistryConfig, kindVersion); err != nil { - return err - } - - // Build the Docker image - if err := buildDockerImage(projectDir); err != nil { - return err - } - - // Deploy to kind cluster - if err := deployToKind(projectDir, manifest, kindNamespace); err != nil { - return err - } - - fmt.Printf("✅ MCP server successfully deployed to kind cluster\n") - fmt.Printf("💡 Check status with: kubectl get mcpserver %s -n %s\n", manifest.Name, kindNamespace) - fmt.Printf( - "💡 View logs with: kubectl logs -l app.kubernetes.io/name=%s -n %s\n", - manifest.Name, - kindNamespace, - ) - fmt.Printf( - "🔌 Port forward the MCP server: kubectl port-forward deployment/%s 3000:3000 -n %s\n", - manifest.Name, - kindNamespace, - ) - fmt.Printf("\nPress Enter to start the MCP inspector after the MCP server is ready...") - _, _ = fmt.Scanln() // Wait for user input - - // Create MCP inspector config and start inspector - if err := startInspector(projectDir, manifest.Name); err != nil { - return err - } - - return nil -} - -func checkKindAvailable() error { - cmd := exec.Command("kind", "version") - if err := cmd.Run(); err != nil { - kindInstallURL := "https://kind.sigs.k8s.io/docs/user/quick-start/#installation" - return fmt.Errorf("kind is required but not found. Please install kind: %s", kindInstallURL) - } - return nil -} - -func ensureKindCluster() error { - // Check if kind cluster exists - cmd := exec.Command("kind", "get", "clusters") - output, err := cmd.Output() - if err != nil { - return fmt.Errorf("failed to check kind clusters: %w", err) - } - - clusters := strings.TrimSpace(string(output)) - if !strings.Contains(clusters, "kind") { - if verbose { - fmt.Printf("Creating kind cluster...\n") - } - - // Create kind cluster - createCmd := exec.Command("kind", "create", "cluster", "--name", "kind") - createCmd.Stdout = os.Stdout - createCmd.Stderr = os.Stderr - if err := createCmd.Run(); err != nil { - return fmt.Errorf("failed to create kind cluster: %w", err) - } - } else if verbose { - fmt.Printf("Kind cluster already exists\n") - } - - return nil -} - -func buildDockerImage(projectDir string) error { - if verbose { - fmt.Printf("Building Docker image...\n") - } - - // Run kmcp build --docker - buildCmd := exec.Command("kmcp", "build", "--docker", "--project-dir", projectDir) - buildCmd.Stdout = os.Stdout - buildCmd.Stderr = os.Stderr - if err := buildCmd.Run(); err != nil { - return fmt.Errorf("failed to build Docker image: %w", err) - } - - // Load the built image into kind cluster - if verbose { - fmt.Printf("Loading MCP server image into kind cluster\n") - } - - // Get the project name from the manifest to determine the image name - manifest, err := getProjectManifest(projectDir) - if err != nil { - return fmt.Errorf("failed to get project manifest: %w", err) - } - - // Use the project name as the image name (this is the default behavior of kmcp build) - imageName := fmt.Sprintf("%s:latest", strings.ToLower(strings.ReplaceAll(manifest.Name, "_", "-"))) - - loadCmd := exec.Command("kind", "load", "docker-image", imageName) - loadCmd.Stdout = os.Stdout - loadCmd.Stderr = os.Stderr - if err := loadCmd.Run(); err != nil { - return fmt.Errorf("failed to load MCP server image into kind cluster: %w", err) - } - - return nil -} - -func deployToKind(projectDir string, manifest *manifest.ProjectManifest, namespace string) error { - if verbose { - fmt.Printf("Deploying to kind cluster in namespace: %s\n", namespace) - } - - // Run kmcp deploy mcp with namespace - deployCmd := exec.Command( - "kmcp", - "deploy", - "mcp", - manifest.Name, - "--file", - fmt.Sprintf("%s/kmcp.yaml", projectDir), - "--namespace", - namespace, - ) - deployCmd.Stdout = os.Stdout - deployCmd.Stderr = os.Stderr - if err := deployCmd.Run(); err != nil { - return fmt.Errorf("failed to deploy to kind cluster: %w", err) - } - - return nil -} - -func deployController(namespace string, registryConfig string, version string) error { - if verbose { - fmt.Printf("Deploying controller to namespace: %s\n", namespace) - if version != "" { - fmt.Printf("Using specified version: %s\n", version) - } - } - - // Pull and load the controller image into kind cluster - if version != "" { - imageName := fmt.Sprintf("ghcr.io/kagent-dev/kmcp/controller:%s", version) - if verbose { - fmt.Printf("Pulling controller image: %s\n", imageName) - } - - // Pull the image - pullCmd := exec.Command("docker", "pull", imageName) - pullCmd.Stdout = os.Stdout - pullCmd.Stderr = os.Stderr - if err := pullCmd.Run(); err != nil { - return fmt.Errorf("failed to pull controller image: %w", err) - } - - // Load the image into kind cluster - if verbose { - fmt.Printf("Loading controller image into kind cluster\n") - } - loadCmd := exec.Command("kind", "load", "docker-image", imageName) - loadCmd.Stdout = os.Stdout - loadCmd.Stderr = os.Stderr - if err := loadCmd.Run(); err != nil { - return fmt.Errorf("failed to load controller image into kind cluster: %w", err) - } - } - - // Build helm install command directly - args := []string{ - "install", "kmcp", "oci://ghcr.io/kagent-dev/kmcp/helm/kmcp", - "--version", version, - "--namespace", namespace, - "--create-namespace", - } - - // Add registry config if found - if registryConfig != "" { - args = append(args, "--registry-config", registryConfig) - } - - // Override the image tag to match the version - if version != "" { - args = append(args, "--set", fmt.Sprintf("image.tag=%s", version)) - } - - if verbose { - fmt.Printf("Executing: helm %s\n", strings.Join(args, " ")) - } - - cmd := exec.Command("helm", args...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to deploy controller: %w", err) - } - - return nil -} - -func startInspector(projectDir string, serverName string) error { - if verbose { - fmt.Printf("Starting MCP inspector...\n") - } - - // Create server configuration for kind deployment - serverConfig := map[string]interface{}{ - "type": "streamable-http", - "url": "http://localhost:3000/mcp", - } - - // Create MCP inspector config - configPath := filepath.Join(projectDir, "mcp-server-config.json") - if err := createMCPInspectorConfig(serverName, serverConfig, configPath); err != nil { - return err - } - - // Run the inspector in background - return runMCPInspector(configPath, serverName, projectDir) -} From d8dd7e0cab607f976a631761c01d1c2a725ac82a Mon Sep 17 00:00:00 2001 From: JM Huibonhoa Date: Tue, 29 Jul 2025 12:30:21 -0400 Subject: [PATCH 07/12] update gh workflow to build cli for multiple platforms Signed-off-by: JM Huibonhoa --- .github/workflows/tag.yaml | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tag.yaml b/.github/workflows/tag.yaml index bce35e5..b15d8d5 100644 --- a/.github/workflows/tag.yaml +++ b/.github/workflows/tag.yaml @@ -91,7 +91,11 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 - - name: Build + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.21' + - name: Build for multiple platforms run: | # if workflow_dispatch is used, use the version input if [ -n "${{ github.event.inputs.version }}" ]; then @@ -100,10 +104,28 @@ jobs: export VERSION=$(echo "$GITHUB_REF" | cut -c11-) fi echo "Building release artifacts with version: ${VERSION}" - make build-cli VERSION=${VERSION} + + # Build for Linux amd64 + GOOS=linux GOARCH=amd64 make build-cli VERSION=${VERSION} + mv dist/kmcp dist/kmcp-linux-amd64 + + # Build for Linux arm64 + GOOS=linux GOARCH=arm64 make build-cli VERSION=${VERSION} + mv dist/kmcp dist/kmcp-linux-arm64 + + # Build for macOS amd64 + GOOS=darwin GOARCH=amd64 make build-cli VERSION=${VERSION} + mv dist/kmcp dist/kmcp-darwin-amd64 + + # Build for macOS arm64 + GOOS=darwin GOARCH=arm64 make build-cli VERSION=${VERSION} + mv dist/kmcp dist/kmcp-darwin-arm64 - name: Release uses: softprops/action-gh-release@v2 if: startsWith(github.ref, 'refs/tags/') with: files: | - dist/kmcp + dist/kmcp-linux-amd64 + dist/kmcp-linux-arm64 + dist/kmcp-darwin-amd64 + dist/kmcp-darwin-arm64 From b568178d2beaf9843488a98d2b829a1501e78991 Mon Sep 17 00:00:00 2001 From: JM Huibonhoa Date: Tue, 29 Jul 2025 15:52:08 -0400 Subject: [PATCH 08/12] remove local subcommand Signed-off-by: JM Huibonhoa --- cmd/kmcp/cmd/run.go | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/cmd/kmcp/cmd/run.go b/cmd/kmcp/cmd/run.go index fe06030..9d36b1a 100644 --- a/cmd/kmcp/cmd/run.go +++ b/cmd/kmcp/cmd/run.go @@ -13,14 +13,6 @@ import ( var runCmd = &cobra.Command{ Use: "run", - Short: "Run MCP server", - Long: `Run an MCP server using the Model Context Protocol inspector. - -This command provides subcommands for different deployment scenarios.`, -} - -var localCmd = &cobra.Command{ - Use: "local", Short: "Run MCP server locally", Long: `Run an MCP server locally using the Model Context Protocol inspector. @@ -30,20 +22,19 @@ This command will: 3. Run the MCP server using the Model Context Protocol inspector Examples: - kmcp run local --project-dir ./my-project # Run from specific directory`, - RunE: executeLocal, + kmcp run --project-dir ./my-project # Run from specific directory`, + RunE: executeRun, } var ( - localProjectDir string + projectDir string ) func init() { rootCmd.AddCommand(runCmd) - runCmd.AddCommand(localCmd) - localCmd.Flags().StringVarP( - &localProjectDir, + runCmd.Flags().StringVarP( + &projectDir, "project-dir", "d", "", @@ -51,7 +42,7 @@ func init() { ) } -func executeLocal(_ *cobra.Command, _ []string) error { +func executeRun(_ *cobra.Command, _ []string) error { projectDir, err := getProjectDir() if err != nil { return err @@ -170,30 +161,30 @@ func runFastMCPPython(projectDir string, manifest *manifest.ProjectManifest) err func getProjectDir() (string, error) { // Determine project directory - projectDir := localProjectDir - if projectDir == "" { + dir := projectDir + if dir == "" { // Use current working directory var err error - projectDir, err = os.Getwd() + dir, err = os.Getwd() if err != nil { return "", fmt.Errorf("failed to get current directory: %w", err) } } else { // Convert relative path to absolute path - if !filepath.IsAbs(projectDir) { + if !filepath.IsAbs(dir) { cwd, err := os.Getwd() if err != nil { return "", fmt.Errorf("failed to get current directory: %w", err) } - projectDir = filepath.Join(cwd, projectDir) + dir = filepath.Join(cwd, dir) } } if verbose { - fmt.Printf("Using project directory: %s\n", projectDir) + fmt.Printf("Using project directory: %s\n", dir) } - return projectDir, nil + return dir, nil } func getProjectManifest(projectDir string) (*manifest.ProjectManifest, error) { From 6e0f59947fea4e80cec17c6d72b2f46a3708dfe4 Mon Sep 17 00:00:00 2001 From: JM Huibonhoa Date: Tue, 29 Jul 2025 15:59:58 -0400 Subject: [PATCH 09/12] fix tests Signed-off-by: JM Huibonhoa --- .../python/templates/src/core/server.py.tmpl | 33 +++++++++++++++---- .../python/templates/tests/test_tools.py.tmpl | 18 +++++++--- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/pkg/frameworks/python/templates/src/core/server.py.tmpl b/pkg/frameworks/python/templates/src/core/server.py.tmpl index 91338e9..70c2da4 100644 --- a/pkg/frameworks/python/templates/src/core/server.py.tmpl +++ b/pkg/frameworks/python/templates/src/core/server.py.tmpl @@ -69,23 +69,37 @@ class DynamicMCPServer: return loaded_count = 0 + has_errors = False for tool_file in tool_files: try: + # Get the number of tools before importing + tools_before = len(self.mcp._tool_manager._tools) + # Simply import the module - tools auto-register via @mcp.tool() # decorator tool_name = tool_file.stem if self._import_tool_module(tool_file, tool_name): - self.loaded_tools.append(tool_name) - loaded_count += 1 - logging.info(f"Loaded tool module: {tool_name}") + # Check if any tools were actually registered + tools_after = len(self.mcp._tool_manager._tools) + if tools_after > tools_before: + self.loaded_tools.append(tool_name) + loaded_count += 1 + logging.info(f"Loaded tool module: {tool_name}") + else: + logging.error(f"Tool file {tool_name} did not register any tools") + has_errors = True else: - logging.warning(f"Failed to load tool module: {tool_name}") + logging.error(f"Failed to load tool module: {tool_name}") + has_errors = True except Exception as e: - logging.warning(f"Error loading tool {tool_file.name}: {e}") - # Fail fast - if any tool fails to load, stop the server - sys.exit(1) + logging.error(f"Error loading tool {tool_file.name}: {e}") + has_errors = True + + # Fail fast - if any tool fails to load, stop the server + if has_errors: + sys.exit(1) logging.info(f"📦 Successfully loaded {loaded_count} tools") @@ -122,6 +136,11 @@ class DynamicMCPServer: print(f"Error importing {tool_file}: {e}") return False + def get_tools_sync(self): + """Get tools synchronously for testing purposes.""" + # This is a simplified version for testing - in real usage, use get_tools() async + return self.mcp._tool_manager._tools + def run(self) -> None: """Run the FastMCP server.""" if not self.loaded_tools: diff --git a/pkg/frameworks/python/templates/tests/test_tools.py.tmpl b/pkg/frameworks/python/templates/tests/test_tools.py.tmpl index 9eae4b6..dd5eea8 100644 --- a/pkg/frameworks/python/templates/tests/test_tools.py.tmpl +++ b/pkg/frameworks/python/templates/tests/test_tools.py.tmpl @@ -49,7 +49,9 @@ class TestToolLoading: server = DynamicMCPServer(name="Test Server", tools_dir="src/tools") server.load_tools() - for tool_name, tool in server.mcp.tools.items(): + tools = server.get_tools_sync() + for tool_name, tool in tools.items(): + assert hasattr(tool, 'fn'), f"Tool {tool_name} has no fn attribute" assert callable(tool.fn), f"Tool {tool_name} is not callable" @@ -65,9 +67,15 @@ class TestEchoTool: def test_echo_tool_function(self): """Test that the echo tool function works.""" - # Import the echo function directly - from tools.echo import echo - - result = echo("Hello, World!") + # Get the tool from the server + server = DynamicMCPServer(name="Test Server", tools_dir="src/tools") + server.load_tools() + + tools = server.get_tools_sync() + assert "echo" in tools + + # Call the tool function + echo_tool = tools["echo"] + result = echo_tool.fn("Hello, World!") assert isinstance(result, str) assert "Hello, World!" in result From 69e561a697d212affc97cac8c760e2ca492abb93 Mon Sep 17 00:00:00 2001 From: JM Huibonhoa Date: Tue, 29 Jul 2025 18:18:12 -0400 Subject: [PATCH 10/12] use mcp content instead of unstructured content in go mcp server tmpl Signed-off-by: JM Huibonhoa --- cmd/kmcp/cmd/run.go | 40 +++++++++++++++++++ .../golang/templates/tools/echo.go.tmpl | 12 ++++-- .../golang/templates/tools/tool.go.tmpl | 7 +++- test/e2e/e2e_test.go | 4 +- 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/cmd/kmcp/cmd/run.go b/cmd/kmcp/cmd/run.go index 9d36b1a..dfe1f33 100644 --- a/cmd/kmcp/cmd/run.go +++ b/cmd/kmcp/cmd/run.go @@ -21,6 +21,10 @@ This command will: 2. Determine the framework type and create appropriate configuration 3. Run the MCP server using the Model Context Protocol inspector +Supported frameworks: +- fastmcp-python: Requires uv to be installed +- mcp-go: Requires Go to be installed + Examples: kmcp run --project-dir ./my-project # Run from specific directory`, RunE: executeRun, @@ -62,6 +66,8 @@ func executeRun(_ *cobra.Command, _ []string) error { switch manifest.Framework { case "fastmcp-python": return runFastMCPPython(projectDir, manifest) + case "mcp-go": + return runMCPGo(projectDir, manifest) default: return fmt.Errorf("unsupported framework: %s", manifest.Framework) } @@ -159,6 +165,40 @@ func runFastMCPPython(projectDir string, manifest *manifest.ProjectManifest) err return runMCPInspector(configPath, manifest.Name, projectDir) } +func runMCPGo(projectDir string, manifest *manifest.ProjectManifest) error { + // Check if go is available + if _, err := exec.LookPath("go"); err != nil { + return fmt.Errorf("go is required for this command to run mcp-go projects locally. Please install Go: https://golang.org/doc/install") + } + + // Run go mod tidy first to ensure dependencies are up to date + if verbose { + fmt.Printf("Running go mod tidy in: %s\n", projectDir) + } + tidyCmd := exec.Command("go", "mod", "tidy") + tidyCmd.Dir = projectDir + tidyCmd.Stdout = os.Stdout + tidyCmd.Stderr = os.Stderr + if err := tidyCmd.Run(); err != nil { + return fmt.Errorf("failed to run go mod tidy: %w", err) + } + + // Create server configuration for local execution + serverConfig := map[string]interface{}{ + "command": "go", + "args": []string{"run", "main.go"}, + } + + // Create MCP inspector config + configPath := filepath.Join(projectDir, "mcp-server-config.json") + if err := createMCPInspectorConfig(manifest.Name, serverConfig, configPath); err != nil { + return err + } + + // Run the inspector + return runMCPInspector(configPath, manifest.Name, projectDir) +} + func getProjectDir() (string, error) { // Determine project directory dir := projectDir diff --git a/pkg/frameworks/golang/templates/tools/echo.go.tmpl b/pkg/frameworks/golang/templates/tools/echo.go.tmpl index 914c36b..29daca9 100644 --- a/pkg/frameworks/golang/templates/tools/echo.go.tmpl +++ b/pkg/frameworks/golang/templates/tools/echo.go.tmpl @@ -22,11 +22,15 @@ func Echo() MCPTool[EchoParams, EchoResult] { Name: "echo", Description: "Echoes a message back to the user.", Handler: func(ctx context.Context, cc *mcp.ServerSession, params *mcp.CallToolParamsFor[EchoParams]) (*mcp.CallToolResultFor[EchoResult], error) { - return &mcp.CallToolResultFor[EchoResult]{ - StructuredContent: EchoResult{ - Echo: "Echo: " + params.Arguments.Message, + echoMessage := "Echo: " + params.Arguments.Message + result := &mcp.CallToolResultFor[EchoResult]{ + Content: []mcp.Content{ + &mcp.TextContent{ + Text: echoMessage, + }, }, - }, nil + } + return result, nil }, } } diff --git a/pkg/frameworks/golang/templates/tools/tool.go.tmpl b/pkg/frameworks/golang/templates/tools/tool.go.tmpl index ee4dd6d..0227b80 100644 --- a/pkg/frameworks/golang/templates/tools/tool.go.tmpl +++ b/pkg/frameworks/golang/templates/tools/tool.go.tmpl @@ -10,8 +10,13 @@ func init() { Name: "{{.ToolName}}", Description: "{{.Description}}", Handler: func(ctx context.Context, cc *mcp.ServerSession, params *mcp.CallToolParamsFor[{{.ClassName}}Params]) (*mcp.CallToolResultFor[{{.ClassName}}Result], error) { + result := run{{.ClassName}}(params.Arguments) return &mcp.CallToolResultFor[{{.ClassName}}Result]{ - StructuredContent: run{{.ClassName}}(params.Arguments), + Content: []mcp.Content{ + &mcp.TextContent{ + Text: result.Result, + }, + }, }, nil }, }) diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 537d729..2c81cc6 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -215,12 +215,12 @@ var _ = ginkgo.Describe("Manager", ginkgo.Ordered, func() { ginkgo.By("creating Kubernetes secret from existing .env.local file") - cmd = exec.Command("dist/kmcp", "secrets", "sync", "staging", "--from-file", envFilePath, "--dir", projectDir) + cmd = exec.Command("dist/kmcp", "secrets", "sync", "staging", "--from-file", envFilePath, "--project-dir", projectDir) _, err = utils.Run(cmd) gomega.Expect(err).NotTo(gomega.HaveOccurred(), "Failed to create secret from .env.local file") ginkgo.By("building the Docker image for the knowledge-assistant project") - cmd = exec.Command("dist/kmcp", "build", "--docker", "--verbose", "--dir", projectDir) + cmd = exec.Command("dist/kmcp", "build", "--docker", "--verbose", "--project-dir", projectDir) _, err = utils.Run(cmd) gomega.Expect(err).NotTo(gomega.HaveOccurred(), "Failed to build Docker image") From aa2bed7045d9dbe6e65ceff4036688aec9989503 Mon Sep 17 00:00:00 2001 From: JM Huibonhoa Date: Tue, 29 Jul 2025 18:22:08 -0400 Subject: [PATCH 11/12] fix lint issues and update .env.local to .env.staging Signed-off-by: JM Huibonhoa --- cmd/kmcp/cmd/run.go | 3 ++- test/e2e/e2e_test.go | 19 ++++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/cmd/kmcp/cmd/run.go b/cmd/kmcp/cmd/run.go index dfe1f33..a7d3f83 100644 --- a/cmd/kmcp/cmd/run.go +++ b/cmd/kmcp/cmd/run.go @@ -168,7 +168,8 @@ func runFastMCPPython(projectDir string, manifest *manifest.ProjectManifest) err func runMCPGo(projectDir string, manifest *manifest.ProjectManifest) error { // Check if go is available if _, err := exec.LookPath("go"); err != nil { - return fmt.Errorf("go is required for this command to run mcp-go projects locally. Please install Go: https://golang.org/doc/install") + goInstallURL := "https://golang.org/doc/install" + return fmt.Errorf("go is required for this command to run mcp-go projects locally. Please install Go: %s", goInstallURL) } // Run go mod tidy first to ensure dependencies are up to date diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 2c81cc6..48e63ba 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -205,17 +205,26 @@ var _ = ginkgo.Describe("Manager", ginkgo.Ordered, func() { cmd = exec.Command("rm", "-f", fmt.Sprintf("%s/kmcp.yaml.bak", projectDir)) _, _ = utils.Run(cmd) - ginkgo.By("creating a dummy .env.local file for testing secrets") - envFilePath := fmt.Sprintf("%s/.env.local", projectDir) + ginkgo.By("creating a dummy .env.staging file for testing secrets") + envFilePath := fmt.Sprintf("%s/.env.staging", projectDir) envContent := []byte("DATABASE_URL=postgres://user:pass@host:port/db\n" + "OPENAI_API_KEY=dummy-key\n" + "WEATHER_API_KEY=dummy-key\n") err = os.WriteFile(envFilePath, envContent, 0644) - gomega.Expect(err).NotTo(gomega.HaveOccurred(), "Failed to create dummy .env.local file") + gomega.Expect(err).NotTo(gomega.HaveOccurred(), "Failed to create dummy .env.staging file") - ginkgo.By("creating Kubernetes secret from existing .env.local file") + ginkgo.By("creating Kubernetes secret from existing .env.staging file") - cmd = exec.Command("dist/kmcp", "secrets", "sync", "staging", "--from-file", envFilePath, "--project-dir", projectDir) + cmd = exec.Command( + "dist/kmcp", + "secrets", + "sync", + "staging", + "--from-file", + envFilePath, + "--project-dir", + projectDir, + ) _, err = utils.Run(cmd) gomega.Expect(err).NotTo(gomega.HaveOccurred(), "Failed to create secret from .env.local file") From bba70f0042b50dcdf11c567451b4f990f98c7e99 Mon Sep 17 00:00:00 2001 From: JM Huibonhoa Date: Tue, 29 Jul 2025 18:24:33 -0400 Subject: [PATCH 12/12] shorten error msg Signed-off-by: JM Huibonhoa --- cmd/kmcp/cmd/run.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/kmcp/cmd/run.go b/cmd/kmcp/cmd/run.go index a7d3f83..bbfd107 100644 --- a/cmd/kmcp/cmd/run.go +++ b/cmd/kmcp/cmd/run.go @@ -169,7 +169,7 @@ func runMCPGo(projectDir string, manifest *manifest.ProjectManifest) error { // Check if go is available if _, err := exec.LookPath("go"); err != nil { goInstallURL := "https://golang.org/doc/install" - return fmt.Errorf("go is required for this command to run mcp-go projects locally. Please install Go: %s", goInstallURL) + return fmt.Errorf("go is required to run mcp-go projects locally. Please install Go: %s", goInstallURL) } // Run go mod tidy first to ensure dependencies are up to date