Skip to content
Open
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
5 changes: 5 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ openlabs range destroy test-range
- `openlabs config show` - Show current configuration
- `openlabs config set <key> <value>` - Set configuration value

### MCP (Model Context Protocol)
- `openlabs mcp start` - Start MCP server for AI assistant integration
- `openlabs mcp status` - Check MCP server status and connectivity
- `openlabs mcp tools` - List available MCP tools

## Global Flags

- `--format` - Output format (table, json, yaml)
Expand Down
3 changes: 2 additions & 1 deletion cli/cmd/auth/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/spf13/cobra"

"github.com/OpenLabsHQ/OpenLabs/cli/internal/logger"
"github.com/OpenLabsHQ/OpenLabs/cli/internal/progress"
"github.com/OpenLabsHQ/OpenLabs/cli/internal/utils"
)
Expand Down Expand Up @@ -66,7 +67,7 @@ func runRegister(cmd *cobra.Command, name, email, password string) error {
}

if cmd.Flag("password").Changed {
fmt.Println("Password provided via flag - skipping confirmation")
logger.Info("Password provided via flag - skipping confirmation")
} else {
confirmPassword, err := utils.PromptPassword("Confirm password")
if err != nil {
Expand Down
26 changes: 26 additions & 0 deletions cli/cmd/mcp/mcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package mcp

import (
"github.com/spf13/cobra"

"github.com/OpenLabsHQ/OpenLabs/cli/internal/config"
)

var globalConfig *config.Config

func NewMCPCommand() *cobra.Command {
mcpCmd := &cobra.Command{
Use: "mcp",
Short: "Manage Model Context Protocol server",
}

mcpCmd.AddCommand(newStartCommand())
mcpCmd.AddCommand(newToolsCommand())
mcpCmd.AddCommand(newStatusCommand())

return mcpCmd
}

func SetGlobalConfig(cfg *config.Config) {
globalConfig = cfg
}
84 changes: 84 additions & 0 deletions cli/cmd/mcp/start.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package mcp

import (
"context"
"fmt"
"os"
"os/signal"
"syscall"

"github.com/spf13/cobra"

"github.com/OpenLabsHQ/OpenLabs/cli/internal/logger"
"github.com/OpenLabsHQ/OpenLabs/cli/internal/mcp"
)

var (
transport string
port int
debug bool
)

func newStartCommand() *cobra.Command {
startCmd := &cobra.Command{
Use: "start",
Short: "Start the MCP server",
Long: "Start the Model Context Protocol server for AI assistant integration.",
RunE: runStartCommand,
}

startCmd.Flags().StringVar(&transport, "transport", "sse", "Transport mode: stdio or sse")
startCmd.Flags().IntVar(&port, "port", 8080, "Port for SSE transport")
startCmd.Flags().BoolVar(&debug, "debug", false, "Enable debug logging")

return startCmd
}

func runStartCommand(cmd *cobra.Command, args []string) error {
if debug {
logger.SetDebug(true)
logger.Info("Debug logging enabled")
}

if transport != "stdio" && transport != "sse" {
return fmt.Errorf("invalid transport: %s (must be 'stdio' or 'sse')", transport)
}

if transport == "sse" && (port < 1 || port > 65535) {
return fmt.Errorf("invalid port: %d (must be between 1 and 65535)", port)
}

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

go func() {
<-sigChan
logger.Info("Received shutdown signal, shutting down gracefully...")
cancel()
}()

server, err := mcp.NewServer(globalConfig, debug)
if err != nil {
return fmt.Errorf("failed to create MCP server: %w", err)
}

logger.Info("Starting OpenLabs MCP server with %s transport", transport)

switch transport {
case "stdio":
if err := server.RunStdio(ctx); err != nil && err != context.Canceled {
return fmt.Errorf("failed to run stdio server: %w", err)
}
case "sse":
addr := fmt.Sprintf(":%d", port)
logger.Info("MCP server listening on %s", addr)
if err := server.RunSSE(ctx, addr); err != nil && err != context.Canceled {
return fmt.Errorf("failed to run SSE server: %w", err)
}
}

return nil
}
70 changes: 70 additions & 0 deletions cli/cmd/mcp/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package mcp

import (
"fmt"

"github.com/spf13/cobra"

"github.com/OpenLabsHQ/OpenLabs/cli/internal/client"
"github.com/OpenLabsHQ/OpenLabs/cli/internal/logger"
)

func newStatusCommand() *cobra.Command {
statusCmd := &cobra.Command{
Use: "status",
Short: "Check MCP server status and connectivity",
Long: "Check MCP server prerequisites and API connectivity.",
RunE: runStatusCommand,
}

return statusCmd
}

func runStatusCommand(cmd *cobra.Command, args []string) error {
fmt.Println("OpenLabs MCP Server Status Check")

if globalConfig == nil {
logger.Failure("No configuration loaded")
return fmt.Errorf("configuration not loaded")
}
logger.Success("Configuration loaded")

logger.Success("API URL: %s", globalConfig.APIURL)

if globalConfig.AuthToken == "" {
logger.Failure("No auth token found")
logger.Notice("Run 'openlabs auth login' to authenticate, or use the 'login' MCP tool")
return nil
}
logger.Success("Authentication token present")

apiClient := client.New(globalConfig)
if err := apiClient.Ping(); err != nil {
logger.Failure("API connectivity failed (%v)", err)
logger.Debug("API ping failed: %v", err)

logger.Notice("Troubleshooting steps:")
logger.Notice(" 1. Check your network connection")
logger.Notice(" 2. Verify API URL with 'openlabs config show'")
logger.Notice(" 3. Re-authenticate with 'openlabs auth login'")
return nil
}
logger.Success("API connectivity verified")

userInfo, err := apiClient.GetUserInfo()
if err != nil {
logger.Failure("Failed to get user info (%v)", err)
logger.Notice("Your token may be expired. Run 'openlabs auth login' to re-authenticate, or use the 'login' MCP tool")
return nil
}
logger.Success("User info: %s (%s)", userInfo.Name, userInfo.Email)

logger.Success("MCP server is ready to start!")

fmt.Println("\nQuick start commands:")
fmt.Println(" openlabs mcp start # Start with stdio transport")
fmt.Println(" openlabs mcp start --debug # Start with debug logging")
fmt.Println(" openlabs mcp tools # List available tools")

return nil
}
37 changes: 37 additions & 0 deletions cli/cmd/mcp/tools.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package mcp

import (
"fmt"

"github.com/spf13/cobra"

"github.com/OpenLabsHQ/OpenLabs/cli/internal/mcp"
"github.com/OpenLabsHQ/OpenLabs/cli/internal/output"
)

func newToolsCommand() *cobra.Command {
toolsCmd := &cobra.Command{
Use: "tools",
Short: "List available MCP tools",
Long: "List all Model Context Protocol tools available to AI assistants.",
RunE: runToolsCommand,
}

return toolsCmd
}

func runToolsCommand(cmd *cobra.Command, args []string) error {
tools := mcp.GetAllTools()

if globalConfig.OutputFormat == "table" || globalConfig.OutputFormat == "" {
if err := output.DisplayMCPTools(tools); err != nil {
return fmt.Errorf("failed to display tools: %w", err)
}
} else {
if err := output.Display(tools, globalConfig.OutputFormat); err != nil {
return fmt.Errorf("failed to display tools: %w", err)
}
}

return nil
}
3 changes: 3 additions & 0 deletions cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/OpenLabsHQ/OpenLabs/cli/cmd/auth"
"github.com/OpenLabsHQ/OpenLabs/cli/cmd/blueprints"
"github.com/OpenLabsHQ/OpenLabs/cli/cmd/config"
"github.com/OpenLabsHQ/OpenLabs/cli/cmd/mcp"
"github.com/OpenLabsHQ/OpenLabs/cli/cmd/ranges"
internalConfig "github.com/OpenLabsHQ/OpenLabs/cli/internal/config"
"github.com/OpenLabsHQ/OpenLabs/cli/internal/logger"
Expand Down Expand Up @@ -96,6 +97,7 @@ func addSubcommands() {
rootCmd.AddCommand(ranges.NewRangeCommand())
rootCmd.AddCommand(blueprints.NewBlueprintsCommand())
rootCmd.AddCommand(config.NewConfigCommand())
rootCmd.AddCommand(mcp.NewMCPCommand())
}

func initializeGlobalConfig() error {
Expand Down Expand Up @@ -133,6 +135,7 @@ func applyGlobalFlags() {
auth.SetGlobalConfig(globalConfig)
ranges.SetGlobalConfig(globalConfig)
blueprints.SetGlobalConfig(globalConfig)
mcp.SetGlobalConfig(globalConfig)
}

func loadConfigFromPath(path string) (*internalConfig.Config, error) {
Expand Down
8 changes: 6 additions & 2 deletions cli/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.24

require (
github.com/aws/aws-sdk-go-v2/config v1.29.17
github.com/mark3labs/mcp-go v0.34.0
github.com/olekukonko/tablewriter v0.0.5
github.com/spf13/cobra v1.8.1
golang.org/x/term v0.32.0
Expand All @@ -23,11 +24,14 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect
github.com/aws/smithy-go v1.22.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/sys v0.33.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)
27 changes: 23 additions & 4 deletions cli/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,14 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zg
github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw=
github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
Expand All @@ -35,20 +42,32 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mark3labs/mcp-go v0.34.0 h1:eWy7WBGvhk6EyAAyVzivTCprE52iXJwNtvHV6Cv3bR0=
github.com/mark3labs/mcp-go v0.34.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
Expand Down
19 changes: 19 additions & 0 deletions cli/internal/logger/logger.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package logger

import (
"fmt"
"log"
"os"
)
Expand Down Expand Up @@ -80,3 +81,21 @@ func Warnf(format string, args ...interface{}) {
func Errorf(format string, args ...interface{}) {
Error(format, args...)
}

// Status logging functions with consistent indicators
// These use stdout for status messages and stderr for errors

// Success logs a success message with [+] indicator
func Success(format string, args ...interface{}) {
fmt.Printf("[+] "+format+"\n", args...)
}

// Failure logs a failure message with [-] indicator
func Failure(format string, args ...interface{}) {
fmt.Printf("[-] "+format+"\n", args...)
}

// Notice logs a notice/warning message with [!] indicator
func Notice(format string, args ...interface{}) {
fmt.Printf("[!] "+format+"\n", args...)
}
Loading