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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions .github/workflows/tag.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
6 changes: 3 additions & 3 deletions cmd/kmcp/cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand All @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions cmd/kmcp/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ func runInitFramework(
return fmt.Errorf("failed to generate project: %w", err)
}

fmt.Printf(" To run the server locally:\n")
fmt.Printf(" kmcp run local --project-dir %s\n", projectPath)

return manifest.NewManager(projectPath).Save(projectManifest)
}

Expand Down
245 changes: 245 additions & 0 deletions cmd/kmcp/cmd/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
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 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

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,
}

var (
projectDir string
)

func init() {
rootCmd.AddCommand(runCmd)

runCmd.Flags().StringVarP(
&projectDir,
"project-dir",
"d",
"",
"Project directory to use (default: current directory)",
)
}

func executeRun(_ *cobra.Command, _ []string) error {
projectDir, err := getProjectDir()
if err != nil {
return err
}

manifest, err := getProjectManifest(projectDir)
if err != nil {
return 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)
case "mcp-go":
return runMCPGo(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
}

// 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 {
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
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 server configuration for local execution
serverConfig := map[string]interface{}{
"command": "uv",
"args": []string{"run", "python", "src/main.py"},
}

// 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 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 to run mcp-go projects locally. Please install Go: %s", goInstallURL)
}

// 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
if dir == "" {
// Use current working directory
var err error
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(dir) {
cwd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("failed to get current directory: %w", err)
}
dir = filepath.Join(cwd, dir)
}
}

if verbose {
fmt.Printf("Using project directory: %s\n", dir)
}

return dir, 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 nil, fmt.Errorf("failed to load kmcp.yaml: %w", err)
}

return manifest, nil
}
7 changes: 3 additions & 4 deletions cmd/kmcp/cmd/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -50,7 +49,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
Expand All @@ -68,10 +67,10 @@ 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 {
func runSync(_ *cobra.Command, args []string) error {
environment := args[0]

// Determine project root
Expand Down
12 changes: 8 additions & 4 deletions pkg/frameworks/golang/templates/tools/echo.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
}
}
7 changes: 6 additions & 1 deletion pkg/frameworks/golang/templates/tools/tool.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
})
Expand Down
Loading
Loading