diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 575ed69..591c40e 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -22,7 +22,7 @@ package v1alpha1 import ( corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) diff --git a/cmd/kmcp/cmd/add_tool.go b/cmd/kmcp/cmd/add_tool.go index a1e2563..c494d50 100644 --- a/cmd/kmcp/cmd/add_tool.go +++ b/cmd/kmcp/cmd/add_tool.go @@ -2,9 +2,12 @@ package cmd import ( "fmt" + "os" "path/filepath" "strings" + "kagent.dev/kmcp/pkg/templates" + "github.com/spf13/cobra" "kagent.dev/kmcp/pkg/frameworks" "kagent.dev/kmcp/pkg/manifest" @@ -34,6 +37,7 @@ var ( addToolDescription string addToolForce bool addToolInteractive bool + addToolDir string ) func init() { @@ -42,6 +46,7 @@ func init() { addToolCmd.Flags().StringVarP(&addToolDescription, "description", "d", "", "Tool description") addToolCmd.Flags().BoolVarP(&addToolForce, "force", "f", false, "Overwrite existing tool file") addToolCmd.Flags().BoolVarP(&addToolInteractive, "interactive", "i", false, "Interactive tool creation") + addToolCmd.Flags().StringVar(&addToolDir, "project-dir", "", "Project directory (default: current directory)") } func runAddTool(_ *cobra.Command, args []string) error { @@ -52,12 +57,26 @@ func runAddTool(_ *cobra.Command, args []string) error { return fmt.Errorf("invalid tool name: %w", err) } - // Get project root and framework - projectRoot, err := findProjectRoot() - if err != nil { - return err + // Determine project directory + projectDirectory := addToolDir + if projectDirectory == "" { + var err error + projectDirectory, 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(projectDirectory) { + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current directory: %w", err) + } + projectDirectory = filepath.Join(cwd, projectDirectory) + } } - manifestManager := manifest.NewManager(projectRoot) + + manifestManager := manifest.NewManager(projectDirectory) projectManifest, err := manifestManager.Load() if err != nil { return fmt.Errorf("failed to load project manifest: %w", err) @@ -78,10 +97,10 @@ func runAddTool(_ *cobra.Command, args []string) error { } if addToolInteractive { - return createToolInteractive(toolName, projectRoot, framework) + return createToolInteractive(toolName, projectDirectory, framework) } - return createTool(toolName, projectRoot, framework) + return createTool(toolName, projectDirectory, framework) } func validateToolName(name string) error { @@ -168,23 +187,14 @@ func generateTool(toolName, projectRoot, framework string) error { return err } - config := map[string]interface{}{ - "description": addToolDescription, + config := templates.ToolConfig{ + ToolName: toolName, + Description: addToolDescription, } - if err := generator.GenerateTool(projectRoot, toolName, config); err != nil { + if err := generator.GenerateTool(projectRoot, config); err != nil { return fmt.Errorf("failed to generate tool file: %w", err) } - fmt.Printf("āœ… Successfully created tool: %s\n", toolName) - fmt.Printf("šŸ“ Generated file: src/tools/%s.py\n", toolName) - fmt.Printf("šŸ”„ Updated tools/__init__.py with new tool import\n") - - fmt.Printf("\nNext steps:\n") - fmt.Printf("1. Edit src/tools/%s.py to implement your tool logic\n", toolName) - fmt.Printf("2. Configure any required environment variables in kmcp.yaml\n") - fmt.Printf("3. Run 'uv run python src/main.py' to start the server\n") - fmt.Printf("4. Run 'uv run pytest tests/' to test your tool\n") - return nil } diff --git a/cmd/kmcp/cmd/deploy.go b/cmd/kmcp/cmd/deploy.go index 22b283d..07815a4 100644 --- a/cmd/kmcp/cmd/deploy.go +++ b/cmd/kmcp/cmd/deploy.go @@ -23,16 +23,6 @@ const ( var deployCmd = &cobra.Command{ Use: "deploy", - Short: "Deploy to Kubernetes", - Long: `Deploy components to Kubernetes clusters. - -This command provides functionality to deploy MCP servers and the KMCP controller -to Kubernetes clusters.`, -} - -// deployMCPCmd deploys an MCP server to Kubernetes -var deployMCPCmd = &cobra.Command{ - Use: "mcp [name]", Short: "Deploy MCP server to Kubernetes", Long: `Deploy an MCP server to Kubernetes by generating MCPServer CRDs. @@ -53,15 +43,15 @@ Secret namespace will be overridden with the deployment namespace to avoid the n to enable cross-namespace references. Examples: - kmcp deploy mcp # Deploy with project name to cluster - kmcp deploy mcp my-server # Deploy with custom name - kmcp deploy mcp --namespace staging # Deploy to staging namespace - kmcp deploy mcp --dry-run # Generate manifest without applying to cluster - kmcp deploy mcp --image custom:tag # Use custom image - kmcp deploy mcp --transport http # Use HTTP transport - kmcp deploy mcp --output deploy.yaml # Save to file - kmcp deploy mcp --file /path/to/kmcp.yaml # Use custom kmcp.yaml file - kmcp deploy mcp --environment staging # Target environment for deployment (e.g., staging, production)`, + kmcp deploy # Deploy with project name to cluster + kmcp deploy my-server # Deploy with custom name + kmcp deploy --namespace staging # Deploy to staging namespace + kmcp deploy --dry-run # Generate manifest without applying to cluster + kmcp deploy --image custom:tag # Use custom image + kmcp deploy --transport http # Use HTTP transport + kmcp deploy --output deploy.yaml # Save to file + kmcp deploy --file /path/to/kmcp.yaml # Use custom kmcp.yaml file + kmcp deploy --environment staging # Target environment for deployment (e.g., staging, production)`, Args: cobra.MaximumNArgs(1), RunE: runDeployMCP, } @@ -86,23 +76,20 @@ var ( func init() { rootCmd.AddCommand(deployCmd) - // Add subcommands - deployCmd.AddCommand(deployMCPCmd) - // MCP deployment flags - deployMCPCmd.Flags().StringVarP(&deployNamespace, "namespace", "n", "default", "Kubernetes namespace") - deployMCPCmd.Flags().BoolVar(&deployDryRun, "dry-run", false, "Generate manifest without applying to cluster") - deployMCPCmd.Flags().StringVarP(&deployOutput, "output", "o", "", "Output file for the generated YAML") - deployMCPCmd.Flags().StringVar(&deployImage, "image", "", "Docker image to deploy (overrides build image)") - deployMCPCmd.Flags().StringVar(&deployTransport, "transport", "", "Transport type (stdio, http)") - deployMCPCmd.Flags().IntVar(&deployPort, "port", 0, "Container port (default: from project config)") - deployMCPCmd.Flags().IntVar(&deployTargetPort, "target-port", 0, "Target port for HTTP transport") - deployMCPCmd.Flags().StringVar(&deployCommand, "command", "", "Command to run (overrides project config)") - deployMCPCmd.Flags().StringSliceVar(&deployArgs, "args", []string{}, "Command arguments") - deployMCPCmd.Flags().StringSliceVar(&deployEnv, "env", []string{}, "Environment variables (KEY=VALUE)") - deployMCPCmd.Flags().BoolVar(&deployForce, "force", false, "Force deployment even if validation fails") - deployMCPCmd.Flags().StringVarP(&deployFile, "file", "f", "", "Path to kmcp.yaml file (default: current directory)") - deployMCPCmd.Flags().StringVar( + deployCmd.Flags().StringVarP(&deployNamespace, "namespace", "n", "default", "Kubernetes namespace") + deployCmd.Flags().BoolVar(&deployDryRun, "dry-run", false, "Generate manifest without applying to cluster") + deployCmd.Flags().StringVarP(&deployOutput, "output", "o", "", "Output file for the generated YAML") + deployCmd.Flags().StringVar(&deployImage, "image", "", "Docker image to deploy (overrides build image)") + deployCmd.Flags().StringVar(&deployTransport, "transport", "", "Transport type (stdio, http)") + deployCmd.Flags().IntVar(&deployPort, "port", 0, "Container port (default: from project config)") + deployCmd.Flags().IntVar(&deployTargetPort, "target-port", 0, "Target port for HTTP transport") + deployCmd.Flags().StringVar(&deployCommand, "command", "", "Command to run (overrides project config)") + deployCmd.Flags().StringSliceVar(&deployArgs, "args", []string{}, "Command arguments") + deployCmd.Flags().StringSliceVar(&deployEnv, "env", []string{}, "Environment variables (KEY=VALUE)") + deployCmd.Flags().BoolVar(&deployForce, "force", false, "Force deployment even if validation fails") + deployCmd.Flags().StringVarP(&deployFile, "file", "f", "", "Path to kmcp.yaml file (default: current directory)") + deployCmd.Flags().StringVar( &deployEnvironment, "environment", "staging", @@ -167,7 +154,7 @@ func runDeployMCP(_ *cobra.Command, args []string) error { // Add YAML document separator and standard header yamlContent := fmt.Sprintf( - "---\n# MCPServer deployment generated by kmcp deploy mcp\n# Project: %s\n# Framework: %s\n%s", + "---\n# MCPServer deployment generated by kmcp deploy\n# Project: %s\n# Framework: %s\n%s", projectManifest.Name, projectManifest.Framework, string(yamlData), @@ -376,6 +363,8 @@ func sanitizeLabelValue(value string) string { func getDefaultCommand(framework string) string { switch framework { + case manifest.FrameworkMCPGo: + return "./server" default: return "python" } @@ -385,6 +374,8 @@ func getDefaultArgs(framework string) []string { switch framework { case manifest.FrameworkFastMCPPython: return []string{"src/main.py"} + case manifest.FrameworkMCPGo: + return []string{} default: return []string{"src/main.py"} } diff --git a/cmd/kmcp/cmd/init.go b/cmd/kmcp/cmd/init.go index a744474..488749f 100644 --- a/cmd/kmcp/cmd/init.go +++ b/cmd/kmcp/cmd/init.go @@ -2,14 +2,14 @@ package cmd import ( "fmt" - "os" "path/filepath" "strings" - "github.com/spf13/cobra" "kagent.dev/kmcp/pkg/frameworks" "kagent.dev/kmcp/pkg/manifest" "kagent.dev/kmcp/pkg/templates" + + "github.com/spf13/cobra" ) const ( @@ -23,20 +23,12 @@ var initCmd = &cobra.Command{ Short: "Initialize a new MCP server project", Long: `Initialize a new MCP server project with dynamic tool loading. -This command creates a new MCP server project using one of the supported frameworks: -- FastMCP Python (recommended) - Dynamic tool loading with FastMCP - -The recommended approach is FastMCP Python which provides: -- Automatic tool discovery and loading -- One file per tool with clear structure -- Built-in configuration management -- Comprehensive testing framework`, - Args: cobra.MaximumNArgs(1), +This command provides subcommands to initialize a new MCP server project +using one of the supported frameworks.`, RunE: runInit, } var ( - initFramework string initForce bool initNoGit bool initAuthor string @@ -49,84 +41,56 @@ var ( func init() { rootCmd.AddCommand(initCmd) - initCmd.Flags().StringVarP(&initFramework, "framework", "f", "", "Framework to use (fastmcp-python)") - initCmd.Flags().BoolVar(&initForce, "force", false, "Overwrite existing directory") - initCmd.Flags().BoolVar(&initNoGit, "no-git", false, "Skip git initialization") - initCmd.Flags().StringVar(&initAuthor, "author", "", "Author name for the project") - initCmd.Flags().StringVar(&initEmail, "email", "", "Author email for the project") - initCmd.Flags().StringVar(&initDescription, "description", "", "Description for the project") - initCmd.Flags().BoolVar(&initNonInteractive, "non-interactive", false, "Run in non-interactive mode") - initCmd.Flags().StringVar(&initNamespace, "namespace", "default", "Default namespace for project resources") + initCmd.PersistentFlags().BoolVar(&initForce, "force", false, "Overwrite existing directory") + initCmd.PersistentFlags().BoolVar(&initNoGit, "no-git", false, "Skip git initialization") + initCmd.PersistentFlags().StringVar(&initAuthor, "author", "", "Author name for the project") + initCmd.PersistentFlags().StringVar(&initEmail, "email", "", "Author email for the project") + initCmd.PersistentFlags().StringVar(&initDescription, "description", "", "Description for the project") + initCmd.PersistentFlags().BoolVar(&initNonInteractive, "non-interactive", false, "Run in non-interactive mode") + initCmd.PersistentFlags().StringVar(&initNamespace, "namespace", "default", "Default namespace for project resources") } -func runInit(_ *cobra.Command, args []string) error { - var projectName string - - // Get project name from args or prompt - if len(args) > 0 { - projectName = args[0] - } else if !initNonInteractive { - name, err := promptForProjectName() - if err != nil { - return fmt.Errorf("failed to get project name: %w", err) - } - projectName = name - } else { - return fmt.Errorf("project name is required in non-interactive mode") +func runInit(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return cmd.Help() } + return nil +} + +func runInitFramework( + projectName, framework string, + customizeProjectConfig func(*templates.ProjectConfig) error, +) error { // Validate project name if err := validateProjectName(projectName); err != nil { return fmt.Errorf("invalid project name: %w", err) } - // Check if directory exists - projectPath := filepath.Join(".", projectName) - if _, err := os.Stat(projectPath); err == nil && !initForce { - return fmt.Errorf("directory '%s' already exists. Use --force to overwrite", projectName) - } - - // Get framework selection - framework := initFramework - if framework == "" && !initNonInteractive { - framework = promptForFramework() - } else if framework == "" { - framework = frameworkFastMCPPython // Default framework - } - - // Get author information - author := initAuthor - email := initEmail - description := initDescription if !initNonInteractive { - if author == "" { - author = promptForAuthor() + if initDescription == "" { + initDescription = promptForDescription() } - if email == "" { - email = promptForEmail() + if initAuthor == "" { + initAuthor = promptForAuthor() } - if description == "" { - description = promptForDescription() + if initEmail == "" { + initEmail = promptForEmail() } } - if verbose { - fmt.Printf("Creating %s project using %s framework\n", projectName, framework) - fmt.Printf("Project directory: %s\n", projectPath) - } + // Create project manifest + projectManifest := manifest.GetDefault(projectName, framework, initDescription, initAuthor, initEmail, initNamespace) - // Create project directory first - if err := os.MkdirAll(projectPath, 0755); err != nil { - return fmt.Errorf("failed to create project directory: %w", err) + // Check if directory exists + projectPath, err := filepath.Abs(projectName) + if err != nil { + return fmt.Errorf("failed to get absolute path for project: %w", err) } - // Create the project manifest object - projectManifest := manifest.GetDefault(projectName, framework, description, author, email, initNamespace) - // Create project configuration projectConfig := templates.ProjectConfig{ ProjectName: projectManifest.Name, - Framework: projectManifest.Framework, Version: projectManifest.Version, Description: projectManifest.Description, Author: projectManifest.Author, @@ -139,71 +103,23 @@ func runInit(_ *cobra.Command, args []string) error { Verbose: verbose, } - // Use the generator to create the project files - fmt.Println("āœ… Creating project structure...") - generator, err := frameworks.GetGenerator(framework) - if err != nil { - return fmt.Errorf("failed to get generator: %w", err) - } - - if err := generator.GenerateProject(projectConfig); err != nil { - return fmt.Errorf("failed to generate project files: %w", err) - } - - // Create kmcp.yaml - manifestManager := manifest.NewManager(projectPath) - if err := manifestManager.Save(projectManifest); err != nil { - return fmt.Errorf("failed to save project manifest: %w", err) + // Customize project config for the specific framework + if customizeProjectConfig != nil { + if err := customizeProjectConfig(&projectConfig); err != nil { + return fmt.Errorf("failed to customize project config: %w", err) + } } - // Get absolute path for output - absProjectPath, err := filepath.Abs(projectPath) + // Generate project files + generator, err := frameworks.GetGenerator(framework) if err != nil { - absProjectPath = projectPath // Fallback to relative path if absolute fails + return err } - - // Success message - fmt.Printf("\nāœ“ Successfully created MCP server project: %s\n", projectName) - fmt.Printf("āœ“ Generated project manifest: kmcp.yaml\n") - fmt.Printf("\nNext steps for local development:\n") - - switch framework { - case frameworkFastMCPPython: - 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") - case frameworkMCPGo: - fmt.Printf(" go mod tidy\n") - fmt.Printf(" go run main.go\n") + if err := generator.GenerateProject(projectConfig); err != nil { + return fmt.Errorf("failed to generate project: %w", err) } - fmt.Printf("\nTo build a Docker image:\n") - fmt.Printf(" kmcp build --docker\n") - - fmt.Printf("\nTo build a Docker image in a specific directory:\n") - fmt.Printf(" kmcp build --docker --dir ./my-project\n") - - fmt.Printf("\nTo develop using Docker only (no local Python/uv required):\n") - fmt.Printf(" kmcp build --docker --verbose # Build and test\n") - fmt.Printf(" kmcp deploy mcp --apply # Deploy MCP server to Kubernetes\n") - - fmt.Printf("\nTo manage secrets:\n") - fmt.Printf(" kmcp secrets add-secret API_KEY --environment local\n") - fmt.Printf(" kmcp secrets generate-k8s-secrets --environment staging\n") - fmt.Printf("\nNote: Default namespace for secrets and deployments is '%s'\n", initNamespace) - - return nil + return manifest.NewManager(projectPath).Save(projectManifest) } func validateProjectName(name string) error { @@ -226,36 +142,6 @@ func validateProjectName(name string) error { // Prompts for user input -func promptForProjectName() (string, error) { - fmt.Print("Enter project name: ") - var name string - if _, err := fmt.Scanln(&name); err != nil { - return "", err - } - return strings.TrimSpace(name), nil -} - -func promptForFramework() string { - fmt.Println("\nSelect a framework:") - fmt.Println("1. FastMCP Python (recommended) - Dynamic tool loading with FastMCP") - fmt.Print("Enter choice [1]: ") - - var choice string - if _, err := fmt.Scanln(&choice); err != nil { - // Default to FastMCP Python on any scan error (e.g., empty input) - return frameworkFastMCPPython - } - - switch strings.TrimSpace(choice) { - case "1", "": - return frameworkFastMCPPython - case "2": - return frameworkMCPGo - default: - return frameworkFastMCPPython // Default to recommended - } -} - func promptForAuthor() string { fmt.Print("Enter author name (optional): ") var author string diff --git a/cmd/kmcp/cmd/init_go.go b/cmd/kmcp/cmd/init_go.go new file mode 100644 index 0000000..383dd06 --- /dev/null +++ b/cmd/kmcp/cmd/init_go.go @@ -0,0 +1,64 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + "kagent.dev/kmcp/pkg/templates" +) + +var ( + goModuleName string +) + +var initGoCmd = &cobra.Command{ + Use: "go [project-name]", + Short: "Initialize a new Go MCP server project", + Long: `Initialize a new MCP server project using the mcp-go framework. + +This command will create a new directory with a basic mcp-go project structure, +including a go.mod file, a main.go file, and an example tool. + +You must provide a valid Go module name for the project.`, + Args: cobra.ExactArgs(1), + RunE: runInitGo, +} + +func init() { + initCmd.AddCommand(initGoCmd) + initGoCmd.Flags().StringVar( + &goModuleName, + "go-module-name", + "", + "The Go module name for the project (e.g., github.com/my-org/my-project)", + ) +} + +func runInitGo(_ *cobra.Command, args []string) error { + projectName := args[0] + framework := frameworkMCPGo + + customize := func(p *templates.ProjectConfig) error { + // Interactively get module name if not provided + if goModuleName == "" && !initNonInteractive { + var err error + goModuleName, err = promptForInput("Enter Go module name (e.g., github.com/my-org/my-project): ") + if err != nil { + return fmt.Errorf("failed to read module name: %w", err) + } + } + if goModuleName == "" { + return fmt.Errorf("--module-name is required") + } + p.GoModuleName = goModuleName + return nil + } + + if err := runInitFramework(projectName, framework, customize); err != nil { + return err + } + + fmt.Printf("āœ“ Successfully created Go MCP server project: %s\n", projectName) + // Add next steps here + return nil +} diff --git a/cmd/kmcp/cmd/init_python.go b/cmd/kmcp/cmd/init_python.go new file mode 100644 index 0000000..b003b3b --- /dev/null +++ b/cmd/kmcp/cmd/init_python.go @@ -0,0 +1,35 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +var initPythonCmd = &cobra.Command{ + Use: "python [project-name]", + Short: "Initialize a new Python MCP server project", + Long: `Initialize a new MCP server project using the fastmcp-python framework. + +This command will create a new directory with a basic fastmcp-python project structure, +including a pyproject.toml file, a main.py file, and an example tool.`, + Args: cobra.ExactArgs(1), + RunE: runInitPython, +} + +func init() { + initCmd.AddCommand(initPythonCmd) +} + +func runInitPython(_ *cobra.Command, args []string) error { + projectName := args[0] + framework := frameworkFastMCPPython + + if err := runInitFramework(projectName, framework, nil); err != nil { + return err + } + + fmt.Printf("āœ“ Successfully created Python MCP server project: %s\n", projectName) + // Add next steps here + return nil +} diff --git a/cmd/kmcp/cmd/utils.go b/cmd/kmcp/cmd/utils.go index 8c2e2c0..a5a1d28 100644 --- a/cmd/kmcp/cmd/utils.go +++ b/cmd/kmcp/cmd/utils.go @@ -3,32 +3,23 @@ package cmd import ( "fmt" "os" - "path/filepath" + "strings" ) -// findProjectRoot finds the root of the KMCP project by looking for kmcp.yaml. -func findProjectRoot() (string, error) { - dir, err := os.Getwd() - if err != nil { - return "", err - } - for { - if _, err := os.Stat(filepath.Join(dir, "kmcp.yaml")); err == nil { - return dir, nil - } - if _, err := os.Stat(filepath.Join(dir, "kmcp.yml")); err == nil { - return dir, nil - } - parent := filepath.Dir(dir) - if parent == dir { - return "", fmt.Errorf("not in a KMCP project directory") - } - dir = parent - } -} - // fileExists checks if a file exists at the given path. func fileExists(path string) bool { _, err := os.Stat(path) return err == nil } + +func promptForInput(promptText string) (string, error) { + fmt.Print(promptText) + var input string + if _, err := fmt.Scanln(&input); err != nil { + if err.Error() == "unexpected newline" { + return "", nil + } + return "", err + } + return strings.TrimSpace(input), nil +} diff --git a/examples/fastmcp-python-basic/.gitignore b/examples/fastmcp-python-basic/.gitignore deleted file mode 100644 index 359a664..0000000 --- a/examples/fastmcp-python-basic/.gitignore +++ /dev/null @@ -1,129 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ \ No newline at end of file diff --git a/examples/fastmcp-python-basic/Dockerfile b/examples/fastmcp-python-basic/Dockerfile deleted file mode 100644 index 96097b2..0000000 --- a/examples/fastmcp-python-basic/Dockerfile +++ /dev/null @@ -1,67 +0,0 @@ -# Multi-stage build for FastMCP Python server -FROM python:3.11-slim as builder - -# Set working directory -WORKDIR /app - -# Install system dependencies -RUN apt-get update && apt-get install -y \ - build-essential \ - curl \ - && rm -rf /var/lib/apt/lists/* - -# Copy requirements first for layer caching -COPY pyproject.toml README.md ./ - -# Install build tools -RUN pip install --no-cache-dir --upgrade pip setuptools wheel - -# Copy source code -COPY fastmcp_python_basic/ ./fastmcp_python_basic/ - -# Install the application -RUN pip install --no-cache-dir -e . - -# Production stage -FROM python:3.11-slim - -# Create non-root user -RUN groupadd -r mcpuser && useradd -r -g mcpuser mcpuser - -# Set working directory -WORKDIR /app - -# Install runtime dependencies only -RUN apt-get update && apt-get install -y \ - ca-certificates \ - && rm -rf /var/lib/apt/lists/* - -# Copy built application from builder stage -COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages -COPY --from=builder /usr/local/bin /usr/local/bin - -# Copy application code -COPY . . - -# Install the application -RUN pip install --no-cache-dir --no-deps -e . - -# Change ownership to non-root user -RUN chown -R mcpuser:mcpuser /app - -# Switch to non-root user -USER mcpuser - -# Expose port (if needed for HTTP transport) -EXPOSE 8080 - -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD python -c "import sys; sys.exit(0)" - -# Set environment variables -ENV PYTHONPATH=/app -ENV PYTHONUNBUFFERED=1 - -# Default command -CMD ["python", "-m", "fastmcp_python_basic.server"] \ No newline at end of file diff --git a/examples/fastmcp-python-basic/README.md b/examples/fastmcp-python-basic/README.md deleted file mode 100644 index f1e2c32..0000000 --- a/examples/fastmcp-python-basic/README.md +++ /dev/null @@ -1,219 +0,0 @@ -# FastMCP Python Basic Example - -This is a basic Model Context Protocol (MCP) server implementation using the FastMCP framework. It demonstrates common MCP patterns and provides several example tools that can be used by AI assistants. - -## Overview - -This MCP server provides four basic tools: -- **calculator**: Perform arithmetic operations (add, subtract, multiply, divide) -- **list_files**: List files in a directory with optional pattern matching -- **system_info**: Get system information and environment details -- **echo**: Echo messages back to the client (useful for testing) - -## Features - -- **FastMCP Framework**: Uses the high-level FastMCP library for simplified development -- **Type Safety**: Pydantic models for request/response validation -- **Error Handling**: Comprehensive error handling with informative messages -- **Docker Support**: Multi-stage Dockerfile for containerized deployment -- **Security**: Non-root user execution and minimal attack surface - -## Installation - -### Local Development - -1. **Clone or navigate to this directory**: - ```bash - cd examples/fastmcp-python-basic - ``` - -2. **Install dependencies**: - ```bash - pip install -e . - ``` - -3. **Run the server**: - ```bash - python -m fastmcp_python_basic.server - ``` - -### Docker Deployment - -1. **Build the Docker image**: - ```bash - docker build -t fastmcp-python-basic . - ``` - -2. **Run the container**: - ```bash - docker run -i fastmcp-python-basic - ``` - -## Usage Examples - -### Calculator Tool - -Perform basic arithmetic operations: - -```json -{ - "operation": "add", - "a": 10, - "b": 5 -} -``` - -Response: -```json -{ - "result": 15, - "operation": "add", - "inputs": {"a": 10, "b": 5} -} -``` - -### List Files Tool - -List files in a directory: - -```json -{ - "directory": ".", - "pattern": "*.py" -} -``` - -Response: -```json -{ - "directory": ".", - "pattern": "*.py", - "files": [ - { - "name": "server.py", - "path": "./server.py", - "size": 1024, - "modified": "2024-01-01T12:00:00", - "is_file": true - } - ], - "count": 1 -} -``` - -### System Info Tool - -Get system information: - -```json -{} -``` - -Response: -```json -{ - "timestamp": "2024-01-01T12:00:00", - "python_version": "3.11.0", - "platform": "Darwin-21.6.0-x86_64-i386-64bit", - "working_directory": "/app", - "environment_variables": { - "PATH": "/usr/local/bin:/usr/bin:/bin", - "HOME": "/home/mcpuser" - } -} -``` - -### Echo Tool - -Echo a message: - -```json -{ - "message": "Hello, MCP!" -} -``` - -Response: -```json -{ - "message": "Hello, MCP!", - "timestamp": "2024-01-01T12:00:00", - "length": 11 -} -``` - -## Development - -### Running Tests - -```bash -pip install -e ".[dev]" -pytest -``` - -### Code Formatting - -```bash -black . -ruff check . -``` - -### Type Checking - -```bash -mypy . -``` - -## Project Structure - -``` -fastmcp-python-basic/ -ā”œā”€ā”€ fastmcp_python_basic/ -│ ā”œā”€ā”€ __init__.py # Package initialization -│ └── server.py # Main server implementation -ā”œā”€ā”€ Dockerfile # Multi-stage Docker build -ā”œā”€ā”€ pyproject.toml # Python project configuration -└── README.md # This file -``` - -## MCP Protocol Details - -This server implements the Model Context Protocol (MCP) specification: - -- **Transport**: STDIO (standard input/output) -- **Protocol**: JSON-RPC 2.0 -- **Capabilities**: Tools (functions that can be called by AI assistants) - -## Integration - -This server can be integrated with various MCP clients including: - -- **Claude Desktop**: Add to your MCP configuration -- **Cursor**: Use with MCP extension -- **VS Code**: MCP protocol support -- **Custom Applications**: Any application supporting MCP - -### Example MCP Client Configuration - -```json -{ - "mcpServers": { - "basic-server": { - "command": "python", - "args": ["-m", "fastmcp_python_basic.server"], - "cwd": "/path/to/project" - } - } -} -``` - -## Security Considerations - -- Server runs as non-root user in Docker -- Limited environment variable exposure -- Input validation through Pydantic models -- No network access from tools (local operation only) - -## License - -This example is part of the KMCP project and follows the same license terms. \ No newline at end of file diff --git a/examples/fastmcp-python-basic/fastmcp_python_basic/__init__.py b/examples/fastmcp-python-basic/fastmcp_python_basic/__init__.py deleted file mode 100644 index 47928f8..0000000 --- a/examples/fastmcp-python-basic/fastmcp_python_basic/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""FastMCP Python Basic Example Server. - -A basic MCP server implementation using FastMCP framework. -""" - -__version__ = "0.1.0" \ No newline at end of file diff --git a/examples/fastmcp-python-basic/fastmcp_python_basic/server.py b/examples/fastmcp-python-basic/fastmcp_python_basic/server.py deleted file mode 100644 index 584422a..0000000 --- a/examples/fastmcp-python-basic/fastmcp_python_basic/server.py +++ /dev/null @@ -1,191 +0,0 @@ -"""Main FastMCP server implementation with example tools.""" - -import asyncio -import json -import os -import sys -from datetime import datetime -from pathlib import Path -from typing import Any, Dict, List, Optional - -from fastmcp import FastMCP -from pydantic import BaseModel, Field - - -class CalculationRequest(BaseModel): - """Request model for calculation operations.""" - operation: str = Field(..., description="The operation to perform: add, subtract, multiply, divide") - a: float = Field(..., description="First number") - b: float = Field(..., description="Second number") - - -class FileListRequest(BaseModel): - """Request model for listing files.""" - directory: str = Field(".", description="Directory to list files from") - pattern: str = Field("*", description="File pattern to match") - - -class SystemInfoResponse(BaseModel): - """Response model for system information.""" - timestamp: str - python_version: str - platform: str - working_directory: str - environment_variables: Dict[str, str] - - -# Initialize FastMCP server -mcp = FastMCP("Basic MCP Server") - - -@mcp.tool() -def calculator(request: CalculationRequest) -> Dict[str, Any]: - """Perform basic arithmetic calculations. - - This tool can perform addition, subtraction, multiplication, and division - operations on two numbers. - """ - try: - result = 0.0 - - if request.operation == "add": - result = request.a + request.b - elif request.operation == "subtract": - result = request.a - request.b - elif request.operation == "multiply": - result = request.a * request.b - elif request.operation == "divide": - if request.b == 0: - return { - "error": "Division by zero is not allowed", - "operation": request.operation, - "inputs": {"a": request.a, "b": request.b} - } - result = request.a / request.b - else: - return { - "error": f"Unknown operation: {request.operation}", - "supported_operations": ["add", "subtract", "multiply", "divide"] - } - - return { - "result": result, - "operation": request.operation, - "inputs": {"a": request.a, "b": request.b} - } - except Exception as e: - return { - "error": f"Calculation error: {str(e)}", - "operation": request.operation, - "inputs": {"a": request.a, "b": request.b} - } - - -@mcp.tool() -def list_files(request: FileListRequest) -> Dict[str, Any]: - """List files in a directory with optional pattern matching. - - This tool lists files in the specified directory and can filter - results using glob patterns. - """ - try: - directory = Path(request.directory) - - if not directory.exists(): - return { - "error": f"Directory does not exist: {request.directory}", - "directory": request.directory - } - - if not directory.is_dir(): - return { - "error": f"Path is not a directory: {request.directory}", - "directory": request.directory - } - - # List files matching pattern - files = [] - for file_path in directory.glob(request.pattern): - if file_path.is_file(): - stat = file_path.stat() - files.append({ - "name": file_path.name, - "path": str(file_path), - "size": stat.st_size, - "modified": datetime.fromtimestamp(stat.st_mtime).isoformat(), - "is_file": True - }) - elif file_path.is_dir(): - files.append({ - "name": file_path.name, - "path": str(file_path), - "is_file": False - }) - - return { - "directory": request.directory, - "pattern": request.pattern, - "files": files, - "count": len(files) - } - except Exception as e: - return { - "error": f"File listing error: {str(e)}", - "directory": request.directory, - "pattern": request.pattern - } - - -@mcp.tool() -def system_info() -> SystemInfoResponse: - """Get system information and environment details. - - This tool provides information about the current system environment, - Python version, and other runtime details. - """ - import platform - - # Get a subset of environment variables (excluding sensitive ones) - env_vars = {} - safe_vars = ["PATH", "HOME", "USER", "SHELL", "LANG", "PWD"] - for var in safe_vars: - if var in os.environ: - env_vars[var] = os.environ[var] - - return SystemInfoResponse( - timestamp=datetime.now().isoformat(), - python_version=sys.version, - platform=platform.platform(), - working_directory=os.getcwd(), - environment_variables=env_vars - ) - - -@mcp.tool() -def echo(message: str) -> Dict[str, Any]: - """Echo a message back to the client. - - This is a simple tool that returns the input message along with - a timestamp, useful for testing connectivity and basic functionality. - """ - return { - "message": message, - "timestamp": datetime.now().isoformat(), - "length": len(message) - } - - -def main() -> None: - """Main entry point for the MCP server.""" - try: - # Run the FastMCP server - mcp.run() - except KeyboardInterrupt: - print("\nShutting down server...") - except Exception as e: - print(f"Server error: {e}", file=sys.stderr) - sys.exit(1) - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/examples/fastmcp-python-basic/pyproject.toml b/examples/fastmcp-python-basic/pyproject.toml deleted file mode 100644 index f6d1bb0..0000000 --- a/examples/fastmcp-python-basic/pyproject.toml +++ /dev/null @@ -1,43 +0,0 @@ -[project] -name = "fastmcp-python-basic" -version = "0.1.0" -description = "A basic FastMCP Python server example" -authors = [ - {name = "KMCP CLI", email = "noreply@kagent.dev"} -] -readme = "README.md" -requires-python = ">=3.9" -dependencies = [ - "mcp>=1.0.0", - "fastmcp>=0.1.0", -] - -[project.optional-dependencies] -dev = [ - "pytest>=7.0.0", - "black>=22.0.0", - "mypy>=1.0.0", - "ruff>=0.1.0", -] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[project.scripts] -basic-server = "fastmcp_python_basic.server:main" - -[tool.black] -line-length = 88 -target-version = ['py39'] - -[tool.ruff] -line-length = 88 -target-version = "py39" -select = ["E", "F", "I", "N", "W", "UP"] - -[tool.mypy] -python_version = "3.9" -warn_return_any = true -warn_unused_configs = true -disallow_untyped_defs = true \ No newline at end of file diff --git a/helm/kmcp/values.yaml b/helm/kmcp/values.yaml index 4efca53..edfc45f 100644 --- a/helm/kmcp/values.yaml +++ b/helm/kmcp/values.yaml @@ -7,7 +7,7 @@ image: repository: ghcr.io/kagent-dev/kmcp/controller pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. - tag: "v0.0.1-test" + tag: "v0.1.0" # Image pull secrets for private registries imagePullSecrets: [] diff --git a/pkg/build/build.go b/pkg/build/build.go index 9492e20..2ce8ed8 100644 --- a/pkg/build/build.go +++ b/pkg/build/build.go @@ -119,16 +119,43 @@ func (b *Builder) buildNode(opts Options) error { // buildGo handles building Go MCP servers func (b *Builder) buildGo(opts Options) error { - fmt.Println("Building Go MCP server...") - if opts.Docker { return b.buildDockerImage(opts, "go") } - // For now, just validate that we can build - fmt.Println("āœ“ Go project validation passed") - fmt.Println("Note: Native Go builds will be implemented in future iterations") + fmt.Println("Building Go MCP server...") + + // Get output path + outputPath := opts.Output + if outputPath == "" { + outputPath = filepath.Join(opts.ProjectDir, "server") + } + + // Prepare build command + args := []string{"build", "-o", outputPath, "."} + + if opts.Verbose { + fmt.Printf("Running: go %s\n", strings.Join(args, " ")) + } + + // Create go build command + cmd := exec.Command("go", args...) + cmd.Dir = opts.ProjectDir + + // Set environment variables for cross-compilation if needed + if opts.Platform != "" { + parts := strings.Split(opts.Platform, "/") + if len(parts) == 2 { + cmd.Env = append(os.Environ(), "GOOS="+parts[0], "GOARCH="+parts[1]) + } + } + + // Run the command + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("go build failed: %w\n%s", err, string(output)) + } + fmt.Printf("āœ“ Successfully built Go binary: %s\n", outputPath) return nil } diff --git a/pkg/frameworks/common/base_generator.go b/pkg/frameworks/common/base_generator.go new file mode 100644 index 0000000..db0ad9d --- /dev/null +++ b/pkg/frameworks/common/base_generator.go @@ -0,0 +1,201 @@ +package common + +import ( + "fmt" + "io/fs" + "os" + "os/exec" + "path/filepath" + "strings" + "text/template" + + "github.com/stoewer/go-strcase" + + "golang.org/x/text/cases" + "golang.org/x/text/language" + "kagent.dev/kmcp/pkg/templates" +) + +// Base Generator for MCP projects +type BaseGenerator struct { + TemplateFiles fs.FS + ToolTemplateName string +} + +// GenerateProject generates a new project +func (g *BaseGenerator) GenerateProject(config templates.ProjectConfig) error { + + templateRoot, err := fs.Sub(g.TemplateFiles, "templates") + if err != nil { + return fmt.Errorf("failed to get templates subdirectory: %w", err) + } + + err = fs.WalkDir(templateRoot, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + // Skip tool.*.tmpl during project generation - it's for individual tool generation + if path == g.ToolTemplateName { + return nil + } + + destPath := filepath.Join(config.Directory, strings.TrimSuffix(path, ".tmpl")) + + if d.IsDir() { + // Create the directory if it doesn't exist + if err := os.MkdirAll(destPath, 0755); err != nil { + return fmt.Errorf("failed to create directory %s: %w", destPath, err) + } + return nil + } + + // Read template file + templateContent, err := fs.ReadFile(templateRoot, path) + if err != nil { + return fmt.Errorf("failed to read template file %s: %w", path, err) + } + + // Render template content + renderedContent, err := renderTemplate(string(templateContent), config) + if err != nil { + return fmt.Errorf("failed to render template for %s: %w", path, err) + } + + // Create file + if err := os.WriteFile(destPath, []byte(renderedContent), 0644); err != nil { + return fmt.Errorf("failed to write file %s: %w", destPath, err) + } + return nil + }) + + if err != nil { + return fmt.Errorf("failed to walk templates: %w", err) + } + + // Initialize git repository + if !config.NoGit { + if err := g.initGitRepo(config.Directory, config.Verbose); err != nil { + // Don't fail the whole operation if git init fails + if config.Verbose { + fmt.Printf("Warning: failed to initialize git repository: %v\n", err) + } + } + } + + return nil +} + +// GenerateTool generates a new tool for a project. +func (g *BaseGenerator) GenerateTool(projectRoot string, config templates.ToolConfig) error { + + templateRoot, err := fs.Sub(g.TemplateFiles, "templates") + if err != nil { + return fmt.Errorf("failed to get templates subdirectory: %w", err) + } + + return fs.WalkDir(templateRoot, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + // Only generate tool.*.tmpl during tool generation + if path != g.ToolTemplateName { + return nil + } + + toolNameSnakeCase := strcase.SnakeCase(config.ToolName) + + destPath := filepath.Join( + projectRoot, + filepath.Dir(path), + toolNameSnakeCase+filepath.Ext(strings.TrimSuffix(path, ".tmpl")), + ) + + if d.IsDir() { + // Create the directory if it doesn't exist + if err := os.MkdirAll(destPath, 0755); err != nil { + return fmt.Errorf("failed to create directory %s: %w", destPath, err) + } + return nil + } + + return g.generateToolFile(destPath, config) + }) +} + +// GenerateToolFile generates a new tool file from the unified template +func (g *BaseGenerator) generateToolFile(filePath string, config templates.ToolConfig) error { + // Prepare template data + toolName := config.ToolName + data := map[string]interface{}{ + "ToolName": toolName, + "ToolNameTitle": cases.Title(language.English).String(toolName), + "ToolNameUpper": strings.ToUpper(toolName), + "ToolNameLower": strings.ToLower(toolName), + "ClassName": cases.Title(language.English).String(toolName) + "Tool", + "Description": config.Description, + } + + // Create the directory if it doesn't exist + if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + + // Parse and execute the template + templateContent, err := fs.ReadFile(g.TemplateFiles, filepath.Join("templates", g.ToolTemplateName)) + if err != nil { + return fmt.Errorf("failed to read tool template: %w", err) + } + + tmpl, err := template.New("tool").Parse(string(templateContent)) + if err != nil { + return fmt.Errorf("failed to parse template: %w", err) + } + + // Create the output file + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + + // Execute the template + err = tmpl.Execute(file, data) + + // Close the file and check for errors + if closeErr := file.Close(); err == nil { + err = closeErr + } + return err +} + +// initGitRepo initializes a git repository in the specified directory +func (g *BaseGenerator) initGitRepo(dir string, verbose bool) error { + cmd := exec.Command("git", "init") + cmd.Dir = dir + + if verbose { + fmt.Printf(" Initializing git repository...\n") + } + + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to run git init: %w", err) + } + + return nil +} + +// renderTemplate renders a template string with the provided data +func renderTemplate(tmplContent string, data interface{}) (string, error) { + tmpl, err := template.New("template").Parse(tmplContent) + if err != nil { + return "", fmt.Errorf("failed to parse template: %w", err) + } + + var result strings.Builder + if err := tmpl.Execute(&result, data); err != nil { + return "", fmt.Errorf("failed to execute template: %w", err) + } + + return result.String(), nil +} diff --git a/pkg/frameworks/frameworks.go b/pkg/frameworks/frameworks.go index 70aa971..3e501d2 100644 --- a/pkg/frameworks/frameworks.go +++ b/pkg/frameworks/frameworks.go @@ -11,7 +11,7 @@ import ( // Generator defines the interface for a framework-specific generator. type Generator interface { GenerateProject(config templates.ProjectConfig) error - GenerateTool(projectPath string, toolName string, config map[string]interface{}) error + GenerateTool(projectRoot string, config templates.ToolConfig) error } // GetGenerator returns a generator for the specified framework. diff --git a/pkg/frameworks/golang/generator.go b/pkg/frameworks/golang/generator.go index 0a996ff..302692e 100644 --- a/pkg/frameworks/golang/generator.go +++ b/pkg/frameworks/golang/generator.go @@ -1,25 +1,89 @@ package golang import ( + "embed" "fmt" + "os/exec" + "github.com/stoewer/go-strcase" + "kagent.dev/kmcp/pkg/frameworks/common" "kagent.dev/kmcp/pkg/templates" ) +//go:embed all:templates +var templateFiles embed.FS + // Generator is the Go-specific generator. -type Generator struct{} +type Generator struct { + common.BaseGenerator +} // NewGenerator creates a new Go generator. func NewGenerator() *Generator { - return &Generator{} + return &Generator{ + BaseGenerator: common.BaseGenerator{ + TemplateFiles: templateFiles, + ToolTemplateName: "tools/tool.go.tmpl", + }, + } } // GenerateProject generates a new Go project. func (g *Generator) GenerateProject(config templates.ProjectConfig) error { - return fmt.Errorf("go project generation not yet implemented") + + if config.Verbose { + fmt.Println("Generating Golang MCP project...") + } + + if err := g.BaseGenerator.GenerateProject(config); err != nil { + return fmt.Errorf("failed to generate project: %w", err) + } + + // Tidy dependencies to create go.sum + if err := g.tidyGoMod(config.Directory, config.Verbose); err != nil { + return fmt.Errorf("failed to finalize Go project: %w", err) + } + + return nil } -// GenerateTool generates a new tool for a Go project. -func (g *Generator) GenerateTool(projectPath string, toolName string, config map[string]interface{}) error { - return fmt.Errorf("go tool generation not yet implemented") +// GenerateTool generates a new tool for a Python project. +func (g *Generator) GenerateTool(projectroot string, config templates.ToolConfig) error { + if err := g.BaseGenerator.GenerateTool(projectroot, config); err != nil { + return fmt.Errorf("failed to generate tool: %w", err) + } + + toolNameSnakeCase := strcase.SnakeCase(config.ToolName) + + fmt.Printf("āœ… Successfully created tool: %s\n", config.ToolName) + fmt.Printf("šŸ“ Generated file: tools/%s.go\n", toolNameSnakeCase) + + fmt.Printf("\nNext steps:\n") + fmt.Printf("1. Edit tools/%s.go to implement your tool logic\n", toolNameSnakeCase) + fmt.Printf("2. Configure any required environment variables in kmcp.yaml\n") + fmt.Printf("3. Run 'go run main.go' to start the server\n") + + return nil +} + +func (g *Generator) tidyGoMod(dir string, verbose bool) error { + if verbose { + fmt.Println("Tidying Go module dependencies...") + } + cmd := exec.Command("go", "mod", "tidy") + cmd.Dir = dir + + output, err := cmd.CombinedOutput() + if verbose && len(output) > 0 { + fmt.Println(string(output)) + } + + if err != nil { + return fmt.Errorf("`go mod tidy` failed: %w\n%s", err, string(output)) + } + + if verbose { + fmt.Println("āœ… Go module dependencies tidied successfully.") + } + return nil } diff --git a/pkg/frameworks/golang/templates/.gitignore.tmpl b/pkg/frameworks/golang/templates/.gitignore.tmpl new file mode 100644 index 0000000..8101d88 --- /dev/null +++ b/pkg/frameworks/golang/templates/.gitignore.tmpl @@ -0,0 +1,35 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Go workspace file +go.work +go.work.sum + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go extra files +{{.ProjectName}} +/server +/{{.ProjectName}} +*.log + +# Docker +.dockerignore +docker-compose.yml + +# VSCode +.vscode/ + +# Intellij +.idea/ \ No newline at end of file diff --git a/pkg/frameworks/golang/templates/Dockerfile.tmpl b/pkg/frameworks/golang/templates/Dockerfile.tmpl new file mode 100644 index 0000000..215dafb --- /dev/null +++ b/pkg/frameworks/golang/templates/Dockerfile.tmpl @@ -0,0 +1,22 @@ +# Build stage +FROM golang:1.23.0-alpine AS builder + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server . + +# Final stage +FROM alpine + +WORKDIR /app + +COPY --from=builder /app/server /app/server + +EXPOSE 8080 + +ENTRYPOINT ["/app/server"] \ No newline at end of file diff --git a/pkg/frameworks/golang/templates/README.md.tmpl b/pkg/frameworks/golang/templates/README.md.tmpl new file mode 100644 index 0000000..dab3bbe --- /dev/null +++ b/pkg/frameworks/golang/templates/README.md.tmpl @@ -0,0 +1,64 @@ +# {{.ProjectName}} + +{{.Description}} + +## šŸš€ Getting Started + +This project was generated with [`kmcp`](https://github.com/kagent-dev/kmcp). + +### Prerequisites + +- [Go](https://golang.org/doc/install) (1.23 or later) +- [Docker](https://docs.docker.com/get-docker/) + +### Local Development + +1. **Tidy dependencies:** + ```bash + go mod tidy + ``` + +2. **Run the server:** + ```bash + go run main.go + ``` + +3. **Connect with the MCP Inspector:** + - Run `npx @modelcontextprotocol/inspector` + - Open the inspector on `localhost:6274` + - Set transport type to `STDIO` + - Copy the `MCP_PROXY_AUTH_TOKEN` into the Proxy Session Token input under configuration + - Paste the following command into the inspector to connect: + ```bash + {{.Directory}}/run_server.sh + ``` + +### Building the Docker Image + +To build a Docker image for this project, run: + +```bash +kmcp build --docker +``` + +This will create an image named `{{.ProjectName}}:latest`. + +### Deploying to Kubernetes + +To deploy the MCP server to Kubernetes, first ensure you have a running cluster and `kubectl` is configured. Then, run: + +```bash +kmcp deploy mcp +``` + +This will create an `MCPServer` custom resource in the `default` namespace. + +## šŸ› ļø Adding a New Tool + +To add a new tool to your project, use the `kmcp add-tool` command: + +```bash +kmcp add-tool +``` + +This will generate a new Go file in the `tools/` directory with a template for your new tool. You will need to add the new tool to the `main.go` file. \ No newline at end of file diff --git a/pkg/frameworks/golang/templates/go.mod.tmpl b/pkg/frameworks/golang/templates/go.mod.tmpl new file mode 100644 index 0000000..6762922 --- /dev/null +++ b/pkg/frameworks/golang/templates/go.mod.tmpl @@ -0,0 +1,7 @@ +module {{.GoModuleName}} + +go 1.23.0 + +require github.com/modelcontextprotocol/go-sdk v0.2.0 + +require github.com/yosida95/uritemplate/v3 v3.0.2 // indirect diff --git a/pkg/frameworks/golang/templates/main.go.tmpl b/pkg/frameworks/golang/templates/main.go.tmpl new file mode 100644 index 0000000..24a4474 --- /dev/null +++ b/pkg/frameworks/golang/templates/main.go.tmpl @@ -0,0 +1,28 @@ +package main + +import ( + "context" + "fmt" + "os" + + "{{.GoModuleName}}/tools" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +func main() { + if err := run(); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } +} + +func run() error { + // Create the MCP server + server := mcp.NewServer(&mcp.Implementation{Name: "{{.ProjectName}}", Version: "{{.Version}}"}, nil) + + // Register tools + tools.AddToolsToServer(server) + + // Run the server over stdin/stdout, until the client disconnects + return server.Run(context.Background(), mcp.NewStdioTransport()) +} diff --git a/pkg/frameworks/golang/templates/tools/all_tools.go.tmpl b/pkg/frameworks/golang/templates/tools/all_tools.go.tmpl new file mode 100644 index 0000000..94e5b67 --- /dev/null +++ b/pkg/frameworks/golang/templates/tools/all_tools.go.tmpl @@ -0,0 +1,26 @@ +package tools + +import ( + "context" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +func AddToolsToServer(server *mcp.Server) { + for _, addToolFunc := range toolsToAdd { + addToolFunc(server) + } +} + +var toolsToAdd []func(server *mcp.Server) + +func registerTool[I, O any](tool MCPTool[I, O]) { + toolsToAdd = append(toolsToAdd, func(server *mcp.Server) { + mcp.AddTool(server, &mcp.Tool{Name: tool.Name, Description: tool.Description}, tool.Handler) + }) +} + +type MCPTool[I, O any] struct { + Name string + Description string + Handler func(ctx context.Context, cc *mcp.ServerSession, params *mcp.CallToolParamsFor[I]) (*mcp.CallToolResultFor[O], error) +} diff --git a/pkg/frameworks/golang/templates/tools/echo.go.tmpl b/pkg/frameworks/golang/templates/tools/echo.go.tmpl new file mode 100644 index 0000000..914c36b --- /dev/null +++ b/pkg/frameworks/golang/templates/tools/echo.go.tmpl @@ -0,0 +1,32 @@ +package tools + +import ( + "context" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +func init() { + registerTool(Echo()) +} + +type EchoParams struct { + Message string `json:"message" description:"The message to echo."` +} + +type EchoResult struct { + Echo string `json:"echo" description:"The echoed message."` +} + +func Echo() MCPTool[EchoParams, EchoResult] { + return 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, + }, + }, nil + }, + } +} diff --git a/pkg/frameworks/golang/templates/tools/tool.go.tmpl b/pkg/frameworks/golang/templates/tools/tool.go.tmpl new file mode 100644 index 0000000..ee4dd6d --- /dev/null +++ b/pkg/frameworks/golang/templates/tools/tool.go.tmpl @@ -0,0 +1,35 @@ +package tools + +import ( + "context" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +func init() { + registerTool(MCPTool[{{.ClassName}}Params, {{.ClassName}}Result]{ + Name: "{{.ToolName}}", + Description: "{{.Description}}", + Handler: func(ctx context.Context, cc *mcp.ServerSession, params *mcp.CallToolParamsFor[{{.ClassName}}Params]) (*mcp.CallToolResultFor[{{.ClassName}}Result], error) { + return &mcp.CallToolResultFor[{{.ClassName}}Result]{ + StructuredContent: run{{.ClassName}}(params.Arguments), + }, nil + }, + }) +} + +// define your input/output schemas here +type {{.ClassName}}Params struct { + Message string `json:"message" description:"The message to input to call {{.ClassName}}."` +} + +type {{.ClassName}}Result struct { + Result string `json:"result" description:"The result of calling {{.ClassName}}."` +} + +// your logic goes here +func run{{.ClassName}}(args {{.ClassName}}Params) {{.ClassName}}Result { + // Implement your logic here + return {{.ClassName}}Result{ + Result: "{{.ClassName}}: " + args.Message, + } +} diff --git a/pkg/frameworks/python/generator.go b/pkg/frameworks/python/generator.go index b6589d5..2c50eb0 100644 --- a/pkg/frameworks/python/generator.go +++ b/pkg/frameworks/python/generator.go @@ -3,15 +3,12 @@ package python import ( "embed" "fmt" - "io/fs" "os" - "os/exec" "path/filepath" "strings" - "text/template" - "golang.org/x/text/cases" - "golang.org/x/text/language" + "github.com/stoewer/go-strcase" + "kagent.dev/kmcp/pkg/frameworks/common" "kagent.dev/kmcp/pkg/templates" ) @@ -19,156 +16,64 @@ import ( var templateFiles embed.FS // Generator for Python projects -type Generator struct{} +type Generator struct { + common.BaseGenerator +} // NewGenerator creates a new Python generator func NewGenerator() *Generator { - return &Generator{} + return &Generator{ + BaseGenerator: common.BaseGenerator{ + TemplateFiles: templateFiles, + ToolTemplateName: "src/tools/tool.py.tmpl", + }, + } } // GenerateProject generates a new Python project func (g *Generator) GenerateProject(config templates.ProjectConfig) error { - if config.Framework == "fastmcp-python" { - // Generate project from embedded templates - return g.generateFastMCPPython(config) - } - return fmt.Errorf("unsupported python framework: %s", config.Framework) -} - -// GenerateTool generates a new tool for a Python project. -func (g *Generator) GenerateTool(projectPath string, toolName string, config map[string]interface{}) error { - toolPath := filepath.Join(projectPath, "src", "tools", toolName+".py") - if err := g.GenerateToolFile(toolPath, toolName, config); err != nil { - return fmt.Errorf("failed to generate tool file: %w", err) - } - - // After generating the tool file, regenerate the __init__.py file - toolsDir := filepath.Dir(toolPath) - if err := g.RegenerateToolsInit(toolsDir); err != nil { - return fmt.Errorf("failed to regenerate __init__.py: %w", err) - } - return nil -} - -func (g *Generator) generateFastMCPPython(config templates.ProjectConfig) error { if config.Verbose { fmt.Println("Generating FastMCP Python project...") } - templateRoot, err := fs.Sub(templateFiles, "templates") - if err != nil { - return fmt.Errorf("failed to get templates subdirectory: %w", err) - } - - err = fs.WalkDir(templateRoot, ".", func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - - // Skip tool.py.tmpl during project generation - it's for individual tool generation - if path == "tool.py.tmpl" { - return nil - } - - destPath := filepath.Join(config.Directory, strings.TrimSuffix(path, ".tmpl")) - - if d.IsDir() { - // Create the directory if it doesn't exist - if err := os.MkdirAll(destPath, 0755); err != nil { - return fmt.Errorf("failed to create directory %s: %w", destPath, err) - } - return nil - } - - // Read template file - templateContent, err := fs.ReadFile(templateRoot, path) - if err != nil { - return fmt.Errorf("failed to read template file %s: %w", path, err) - } - - // Render template content - renderedContent, err := g.renderTemplate(string(templateContent), config) - if err != nil { - return fmt.Errorf("failed to render template for %s: %w", path, err) - } - - // Create file - if err := os.WriteFile(destPath, []byte(renderedContent), 0644); err != nil { - return fmt.Errorf("failed to write file %s: %w", destPath, err) - } - return nil - }) - - if err != nil { - return fmt.Errorf("failed to walk templates: %w", err) - } - - // Initialize git repository - if !config.NoGit { - if err := g.initGitRepo(config.Directory, config.Verbose); err != nil { - // Don't fail the whole operation if git init fails - if config.Verbose { - fmt.Printf("Warning: failed to initialize git repository: %v\n", err) - } - } + if err := g.BaseGenerator.GenerateProject(config); err != nil { + return fmt.Errorf("failed to generate project: %w", err) } return nil } -// GenerateToolFile generates a new Python tool file from the unified template -func (g *Generator) GenerateToolFile(filePath, toolName string, config map[string]interface{}) error { - // Prepare template data - data := map[string]interface{}{ - "ToolName": toolName, - "ToolNameTitle": cases.Title(language.English).String(toolName), - "ToolNameUpper": strings.ToUpper(toolName), - "ToolNameLower": strings.ToLower(toolName), - "ClassName": cases.Title(language.English).String(toolName) + "Tool", - "Config": config, - } - - // Add config values to template data - for key, value := range config { - data[key] = value - } - - // Create the directory if it doesn't exist - if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { - return fmt.Errorf("failed to create directory: %w", err) +// GenerateTool generates a new tool for a Python project. +func (g *Generator) GenerateTool(projectroot string, config templates.ToolConfig) error { + if err := g.BaseGenerator.GenerateTool(projectroot, config); err != nil { + return fmt.Errorf("failed to generate tool: %w", err) } - // Parse and execute the template - templateContent, err := fs.ReadFile(templateFiles, "templates/tool.py.tmpl") - if err != nil { - return fmt.Errorf("failed to read tool template: %w", err) + // After generating the tool file, regenerate the __init__.py file + toolsDir := filepath.Join(projectroot, "src", "tools") + if err := g.regenerateToolsInit(toolsDir); err != nil { + return fmt.Errorf("failed to regenerate __init__.py: %w", err) } - tmpl, err := template.New("tool").Parse(string(templateContent)) - if err != nil { - return fmt.Errorf("failed to parse template: %w", err) - } + toolNameSnakeCase := strcase.SnakeCase(config.ToolName) - // Create the output file - file, err := os.Create(filePath) - if err != nil { - return fmt.Errorf("failed to create file: %w", err) - } + fmt.Printf("āœ… Successfully created tool: %s\n", config.ToolName) + fmt.Printf("šŸ“ Generated file: src/tools/%s.py\n", toolNameSnakeCase) + fmt.Printf("šŸ”„ Updated tools/__init__.py with new tool import\n") - // Execute the template - err = tmpl.Execute(file, data) + fmt.Printf("\nNext steps:\n") + fmt.Printf("1. Edit src/tools/%s.py to implement your tool logic\n", toolNameSnakeCase) + fmt.Printf("2. Configure any required environment variables in kmcp.yaml\n") + fmt.Printf("3. Run 'uv run python src/main.py' to start the server\n") + fmt.Printf("4. Run 'uv run pytest tests/' to test your tool\n") - // Close the file and check for errors - if closeErr := file.Close(); err == nil { - err = closeErr - } - return err + return nil } -// RegenerateToolsInit regenerates the __init__.py file in the tools directory -func (g *Generator) RegenerateToolsInit(toolsDir string) error { +// regenerateToolsInit regenerates the __init__.py file in the tools directory +func (g *Generator) regenerateToolsInit(toolsDir string) error { // Scan the tools directory for Python files - tools, err := g.ScanToolsDirectory(toolsDir) + tools, err := g.scanToolsDirectory(toolsDir) if err != nil { return fmt.Errorf("failed to scan tools directory: %w", err) } @@ -181,8 +86,8 @@ func (g *Generator) RegenerateToolsInit(toolsDir string) error { return os.WriteFile(initPath, []byte(content), 0644) } -// ScanToolsDirectory scans the tools directory and returns a list of tool names -func (g *Generator) ScanToolsDirectory(toolsDir string) ([]string, error) { +// scanToolsDirectory scans the tools directory and returns a list of tool names +func (g *Generator) scanToolsDirectory(toolsDir string) ([]string, error) { var tools []string // Read the directory @@ -241,34 +146,3 @@ Do not edit manually - it will be overwritten when tools are loaded. return content.String() } - -// renderTemplate renders a template string with the provided data -func (g *Generator) renderTemplate(tmplContent string, data interface{}) (string, error) { - tmpl, err := template.New("template").Parse(tmplContent) - if err != nil { - return "", fmt.Errorf("failed to parse template: %w", err) - } - - var result strings.Builder - if err := tmpl.Execute(&result, data); err != nil { - return "", fmt.Errorf("failed to execute template: %w", err) - } - - return result.String(), nil -} - -// initGitRepo initializes a git repository in the specified directory -func (g *Generator) initGitRepo(dir string, verbose bool) error { - cmd := exec.Command("git", "init") - cmd.Dir = dir - - if verbose { - fmt.Printf(" Initializing git repository...\n") - } - - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to run git init: %w", err) - } - - return nil -} diff --git a/pkg/frameworks/python/templates/.env.local.tmpl b/pkg/frameworks/python/templates/.env.local.tmpl deleted file mode 100644 index 4492bd7..0000000 --- a/pkg/frameworks/python/templates/.env.local.tmpl +++ /dev/null @@ -1,17 +0,0 @@ -# {{.ProjectName}} Environment Variables - -# Example API keys (configure these in kmcp.yaml under tools) -WEATHER_API_KEY=your-weather-api-key-here -DATABASE_URL=postgresql://user:password@localhost:5432/database -OPENAI_API_KEY=your-openai-api-key-here - -# Server configuration -MCP_SERVER_HOST=127.0.0.1 -MCP_SERVER_PORT=8080 -MCP_LOG_LEVEL=INFO -MCP_DEBUG=false - -# Tool-specific configuration -WEATHER_TIMEOUT=30 -DB_MAX_CONNECTIONS=10 -FILE_MAX_SIZE=10485760 # 10MB in bytes \ No newline at end of file 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 diff --git a/pkg/frameworks/python/templates/tool.py.tmpl b/pkg/frameworks/python/templates/src/tools/tool.py.tmpl similarity index 100% rename from pkg/frameworks/python/templates/tool.py.tmpl rename to pkg/frameworks/python/templates/src/tools/tool.py.tmpl diff --git a/pkg/manifest/manager.go b/pkg/manifest/manager.go index 2070e0c..cb81894 100644 --- a/pkg/manifest/manager.go +++ b/pkg/manifest/manager.go @@ -94,10 +94,9 @@ func GetDefault(name, framework, description, author, email, namespace string) * Tools: make(map[string]ToolConfig), Secrets: SecretsConfig{ "local": { - Enabled: false, - Provider: SecretProviderKubernetes, - Namespace: namespace, - SecretName: fmt.Sprintf("%s-secrets-local", strings.ReplaceAll(name, "_", "-")), + Enabled: false, + Provider: SecretProviderEnv, + File: ".env.local", }, "staging": { Enabled: false, @@ -211,6 +210,7 @@ func (m *Manager) validateSecrets(secrets SecretsConfig) error { func isValidFramework(framework string) bool { validFrameworks := []string{ FrameworkFastMCPPython, + FrameworkMCPGo, } for _, valid := range validFrameworks { diff --git a/pkg/manifest/types.go b/pkg/manifest/types.go index a83657f..9351525 100644 --- a/pkg/manifest/types.go +++ b/pkg/manifest/types.go @@ -82,6 +82,7 @@ type DockerConfig struct { // Supported frameworks const ( FrameworkFastMCPPython = "fastmcp-python" + FrameworkMCPGo = "mcp-go" ) // Supported secret providers diff --git a/pkg/templates/generator.go b/pkg/templates/generator.go index 058ef0c..44474c8 100644 --- a/pkg/templates/generator.go +++ b/pkg/templates/generator.go @@ -2,18 +2,24 @@ package templates import "kagent.dev/kmcp/pkg/manifest" -// ProjectConfig contains configuration for generating a new project +// ProjectConfig contains all the information needed to generate a project type ProjectConfig struct { - ProjectName string - Framework string - Author string - Email string - Directory string - NoGit bool - Verbose bool - Version string + ProjectName string + Framework string + Version string + Description string + Author string + Email string + Tools map[string]manifest.ToolConfig + Secrets manifest.SecretsConfig + Build manifest.BuildConfig + Directory string + NoGit bool + Verbose bool + GoModuleName string +} + +type ToolConfig struct { + ToolName string Description string - Tools map[string]manifest.ToolConfig - Secrets manifest.SecretsConfig - Build manifest.BuildConfig } diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 367a955..537d729 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -21,6 +21,7 @@ import ( "encoding/json" "fmt" "net/http" + "os" "os/exec" "strings" "time" @@ -181,9 +182,10 @@ var _ = ginkgo.Describe("Manager", ginkgo.Ordered, func() { ginkgo.By("creating a knowledge-assistant project using kmcp CLI") cmd = exec.Command( "dist/kmcp", - "init", projectDir, - "--framework", - "fastmcp-python", + "init", + "python", + projectDir, + "--non-interactive", "--force", "--namespace", namespace, @@ -191,18 +193,29 @@ var _ = ginkgo.Describe("Manager", ginkgo.Ordered, func() { _, err = utils.Run(cmd) gomega.Expect(err).NotTo(gomega.HaveOccurred(), "Failed to create knowledge-assistant project") - ginkgo.By("updating kmcp.yaml to enable all secrets") - cmd = exec.Command("sed", "-i.bak", "s/enabled: false/enabled: true/", fmt.Sprintf("%s/kmcp.yaml", projectDir)) + ginkgo.By("updating kmcp.yaml to enable staging secrets") + cmd = exec.Command("sed", + "-i.bak", + "/staging:/,/enabled:/ s/enabled: false/enabled: true/", + fmt.Sprintf("%s/kmcp.yaml", projectDir)) _, err = utils.Run(cmd) - gomega.Expect(err).NotTo(gomega.HaveOccurred(), "Failed to update kmcp.yaml to enable local secrets") + gomega.Expect(err).NotTo(gomega.HaveOccurred(), "Failed to update kmcp.yaml to enable staging secrets") // clean up kmcp yaml backup file cmd = exec.Command("rm", "-f", fmt.Sprintf("%s/kmcp.yaml.bak", projectDir)) _, _ = utils.Run(cmd) - ginkgo.By("creating Kubernetes secret from existing .env.local file") + ginkgo.By("creating a dummy .env.local file for testing secrets") envFilePath := fmt.Sprintf("%s/.env.local", projectDir) - cmd = exec.Command("dist/kmcp", "secrets", "sync", "local", "--from-file", envFilePath, "--dir", 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") + + ginkgo.By("creating Kubernetes secret from existing .env.local file") + + cmd = exec.Command("dist/kmcp", "secrets", "sync", "staging", "--from-file", envFilePath, "--dir", projectDir) _, err = utils.Run(cmd) gomega.Expect(err).NotTo(gomega.HaveOccurred(), "Failed to create secret from .env.local file") @@ -220,13 +233,12 @@ var _ = ginkgo.Describe("Manager", ginkgo.Ordered, func() { cmd = exec.Command( "dist/kmcp", "deploy", - "mcp", "-f", fmt.Sprintf("%s/kmcp.yaml", projectDir), "-n", namespace, "--environment", - "local", + "staging", ) _, err = utils.Run(cmd) gomega.Expect(err).NotTo(gomega.HaveOccurred(), "Failed to deploy knowledge-assistant MCP server") diff --git a/test/utils/utils.go b/test/utils/utils.go index fbc7a1f..021965e 100644 --- a/test/utils/utils.go +++ b/test/utils/utils.go @@ -56,6 +56,7 @@ func Run(cmd *exec.Cmd) (string, error) { if err != nil { return string(output), fmt.Errorf("%s failed with error: (%v) %s", command, err, string(output)) } + _, _ = fmt.Fprintf(GinkgoWriter, "%s\n", output) return string(output), nil }