Skip to content

add orgs and apps... #4359

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 7, 2025
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
63 changes: 58 additions & 5 deletions internal/command/mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os/exec"
"slices"
"strconv"
"strings"

mcpGo "github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
Expand All @@ -17,7 +18,10 @@ import (
)

var COMMANDS = slices.Concat(
mcpServer.AppCommands,
mcpServer.LogCommands,
mcpServer.MachineCommands,
mcpServer.OrgCommands,
mcpServer.PlatformCommands,
mcpServer.StatusCommands,
mcpServer.VolumeCommands,
Expand Down Expand Up @@ -85,25 +89,51 @@ func runServer(ctx context.Context) error {
return nil, fmt.Errorf("argument %s is required", argName)
}

if description.Type == "string" {
switch description.Type {
case "string":
if strValue, ok := argValue.(string); ok {
args[argName] = strValue
} else {
return nil, fmt.Errorf("argument %s must be a string", argName)
}
} else if description.Type == "number" {
case "enum":
if strValue, ok := argValue.(string); ok {
if !slices.Contains(description.Enum, strValue) {
return nil, fmt.Errorf("argument %s must be one of %v", argName, description.Enum)
}
args[argName] = strValue
} else {
return nil, fmt.Errorf("argument %s must be a string", argName)
}
case "array":
if arrValue, ok := argValue.([]any); ok {
if len(arrValue) > 0 {
strArr := make([]string, len(arrValue))
for i, v := range arrValue {
if str, ok := v.(string); ok {
strArr[i] = str
} else {
return nil, fmt.Errorf("argument %s must be an array of strings", argName)
}
}
args[argName] = strings.Join(strArr, ",")
}
} else {
return nil, fmt.Errorf("argument %s must be an array of strings", argName)
}
case "number":
if numValue, ok := argValue.(float64); ok {
args[argName] = strconv.FormatFloat(numValue, 'f', -1, 64)
} else {
return nil, fmt.Errorf("argument %s must be a number", argName)
}
} else if description.Type == "boolean" {
case "boolean":
if boolValue, ok := argValue.(bool); ok {
args[argName] = strconv.FormatBool(boolValue)
} else {
return nil, fmt.Errorf("argument %s must be a boolean", argName)
}
} else {
default:
return nil, fmt.Errorf("unsupported argument type %s for argument %s", description.Type, argName)
}
}
Expand Down Expand Up @@ -162,6 +192,29 @@ func runServer(ctx context.Context) error {

toolOptions = append(toolOptions, mcpGo.WithString(argName, options...))

case "enum":
if arg.Default != "" {
if slices.Contains(arg.Enum, arg.Default) {
options = append(options, mcpGo.DefaultString(arg.Default))
} else {
return fmt.Errorf("invalid default value for argument %s: %s is not in enum %v", argName, arg.Default, arg.Enum)
}
}

if len(arg.Enum) > 0 {
options = append(options, mcpGo.Enum(arg.Enum...))
} else {
return fmt.Errorf("enum argument %s must have at least one value", argName)
}

toolOptions = append(toolOptions, mcpGo.WithString(argName, options...))

case "array":
schema := map[string]any{"type": "string"}
options = append(options, mcpGo.Items(schema))

toolOptions = append(toolOptions, mcpGo.WithArray(argName, options...))

case "number":
if arg.Default != "" {
if defaultValue, err := strconv.ParseFloat(arg.Default, 64); err == nil {
Expand Down Expand Up @@ -197,7 +250,7 @@ func runServer(ctx context.Context) error {

fmt.Fprintf(os.Stderr, "Starting MCP server...\n")
if err := server.ServeStdio(srv); err != nil {
return fmt.Errorf("Server error: %v\n", err)
return fmt.Errorf("Server error: %v", err)
}

return nil
Expand Down
220 changes: 220 additions & 0 deletions internal/command/mcp/server/apps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package mcpServer

import (
"fmt"
"strconv"
)

var AppCommands = []FlyCommand{
{
ToolName: "fly-apps-create",
ToolDescription: "Create a new Fly.io app. If you don't specify a name, one will be generated for you.",
ToolArgs: map[string]FlyArg{
"name": {
Description: "Name of the app",
Required: false,
Type: "string",
},
"org": {
Description: "Slug of the organization to create the app in",
Required: true,
Type: "string",
},
"network": {
Description: "Custom network id",
Required: false,
Type: "string",
},
},

Builder: func(args map[string]string) ([]string, error) {
cmdArgs := []string{"apps", "create"}

if name, ok := args["name"]; ok {
cmdArgs = append(cmdArgs, name)
} else {
cmdArgs = append(cmdArgs, "--generate-name")
}

if org, ok := args["org"]; ok {
cmdArgs = append(cmdArgs, "--org", org)
}

if network, ok := args["network"]; ok {
cmdArgs = append(cmdArgs, "--network", network)
}

cmdArgs = append(cmdArgs, "--json")

return cmdArgs, nil
},
},

{
ToolName: "fly-apps-destroy",
ToolDescription: "Destroy a Fly.io app. All machines and volumes will be destroyed.",
ToolArgs: map[string]FlyArg{
"name": {
Description: "Name of the app",
Required: true,
Type: "string",
},
},

Builder: func(args map[string]string) ([]string, error) {
cmdArgs := []string{"apps", "destroy"}

if name, ok := args["name"]; ok {
cmdArgs = append(cmdArgs, name)
} else {
return nil, fmt.Errorf("missing required argument: name")
}

cmdArgs = append(cmdArgs, "--yes")

return cmdArgs, nil
},
},

{
ToolName: "fly-apps-list",
ToolDescription: "List all Fly.io apps in the organization",
ToolArgs: map[string]FlyArg{
"org": {
Description: "Slug of the organization to list apps for",
Required: false,
Type: "string",
},
},

Builder: func(args map[string]string) ([]string, error) {
cmdArgs := []string{"apps", "list"}

if org, ok := args["org"]; ok {
cmdArgs = append(cmdArgs, "--org", org)
}

cmdArgs = append(cmdArgs, "--json")

return cmdArgs, nil
},
},

{
ToolName: "fly-apps-move",
ToolDescription: "Move a Fly.io app to a different organization",
ToolArgs: map[string]FlyArg{
"name": {
Description: "Name of the app",
Required: true,
Type: "string",
},
"org": {
Description: "Slug of the organization to move the app to",
Required: true,
Type: "string",
},
"skip-health-checks": {
Description: "Skip health checks during the move",
Required: false,
Type: "boolean",
},
},

Builder: func(args map[string]string) ([]string, error) {
cmdArgs := []string{"apps", "move"}

if name, ok := args["name"]; ok {
cmdArgs = append(cmdArgs, name)
} else {
return nil, fmt.Errorf("missing required argument: name")
}

if org, ok := args["org"]; ok {
cmdArgs = append(cmdArgs, "--org", org)
} else {
return nil, fmt.Errorf("missing required argument: org")
}

if skipHealthChecks, ok := args["skip-health-checks"]; ok {
if value, err := strconv.ParseBool(skipHealthChecks); err == nil && value {
cmdArgs = append(cmdArgs, "--skip-health-checks")
}
}

return cmdArgs, nil
},
},

{
ToolName: "fly-apps-releases",
ToolDescription: "List all releases for a Fly.io app, including type, when, success/fail and which user triggered the release.",
ToolArgs: map[string]FlyArg{
"name": {
Description: "Name of the app",
Required: true,
Type: "string",
},
},

Builder: func(args map[string]string) ([]string, error) {
cmdArgs := []string{"apps", "releases"}

if name, ok := args["name"]; ok {
cmdArgs = append(cmdArgs, name)
} else {
return nil, fmt.Errorf("missing required argument: name")
}

cmdArgs = append(cmdArgs, "--json")

return cmdArgs, nil
},
},

{
ToolName: "fly-apps-restart",
ToolDescription: "Restart a Fly.io app. Perform a rolling restart against all running Machines.",
ToolArgs: map[string]FlyArg{
"name": {
Description: "Name of the app",
Required: true,
Type: "string",
},
"force-stop": {
Description: "Force stop the app before restarting",
Required: false,
Type: "boolean",
},
"skip-health-checks": {
Description: "Skip health checks during the restart",
Required: false,
Type: "boolean",
},
},

Builder: func(args map[string]string) ([]string, error) {
cmdArgs := []string{"apps", "restart"}

if name, ok := args["name"]; ok {
cmdArgs = append(cmdArgs, name)
} else {
return nil, fmt.Errorf("missing required argument: name")
}

if forceStop, ok := args["force-stop"]; ok {
if value, err := strconv.ParseBool(forceStop); err == nil && value {
cmdArgs = append(cmdArgs, "--force-stop")
}
}

if skipHealthChecks, ok := args["skip-health-checks"]; ok {
if value, err := strconv.ParseBool(skipHealthChecks); err == nil && value {
cmdArgs = append(cmdArgs, "--skip-health-checks")
}
}

return cmdArgs, nil
},
},
}
9 changes: 9 additions & 0 deletions internal/command/mcp/server/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ var LogCommands = []FlyCommand{
Required: false,
Type: "string",
},
"region": {
Description: "Region to get logs from",
Required: false,
Type: "string",
},
},
Builder: func(args map[string]string) ([]string, error) {
cmdArgs := []string{"logs", "--no-tail"}
Expand All @@ -27,6 +32,10 @@ var LogCommands = []FlyCommand{
cmdArgs = append(cmdArgs, "--machine", machine)
}

if region, ok := args["region"]; ok {
cmdArgs = append(cmdArgs, "--region", region)
}

return cmdArgs, nil
},
},
Expand Down
Loading
Loading