965 lines
34 KiB
Go
965 lines
34 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
|
|
mcpuiserver "github.com/MCP-UI-Org/mcp-ui/sdks/go/server"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
// Lightweight DTOs for list responses to reduce token usage
|
|
|
|
// AppListItem represents a lightweight app for list responses
|
|
type AppListItem struct {
|
|
Key struct {
|
|
Organization string `json:"organization"`
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
} `json:"key"`
|
|
Deployment string `json:"deployment"`
|
|
ImagePath string `json:"image_path"`
|
|
AccessPorts string `json:"access_ports"`
|
|
AllowServerless bool `json:"allow_serverless"`
|
|
}
|
|
|
|
// AppInstanceListItem represents a lightweight app instance for list responses
|
|
type AppInstanceListItem struct {
|
|
Key struct {
|
|
Organization string `json:"organization"`
|
|
Name string `json:"name"`
|
|
CloudletKey struct {
|
|
Organization string `json:"organization"`
|
|
Name string `json:"name"`
|
|
} `json:"cloudlet_key"`
|
|
} `json:"key"`
|
|
AppKey struct {
|
|
Organization string `json:"organization"`
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
} `json:"app_key"`
|
|
PowerState string `json:"power_state"`
|
|
Flavor struct {
|
|
Name string `json:"name"`
|
|
} `json:"flavor"`
|
|
}
|
|
|
|
// convertToAppListItem converts a full App to a lightweight AppListItem
|
|
func convertToAppListItem(app v2.App) AppListItem {
|
|
item := AppListItem{
|
|
Deployment: app.Deployment,
|
|
ImagePath: app.ImagePath,
|
|
AccessPorts: app.AccessPorts,
|
|
AllowServerless: app.AllowServerless,
|
|
}
|
|
item.Key.Organization = app.Key.Organization
|
|
item.Key.Name = app.Key.Name
|
|
item.Key.Version = app.Key.Version
|
|
return item
|
|
}
|
|
|
|
// convertToAppListItems converts a slice of Apps to AppListItems
|
|
func convertToAppListItems(apps []v2.App) []AppListItem {
|
|
items := make([]AppListItem, len(apps))
|
|
for i, app := range apps {
|
|
items[i] = convertToAppListItem(app)
|
|
}
|
|
return items
|
|
}
|
|
|
|
// convertToAppInstanceListItem converts a full AppInstance to a lightweight AppInstanceListItem
|
|
func convertToAppInstanceListItem(inst v2.AppInstance) AppInstanceListItem {
|
|
item := AppInstanceListItem{
|
|
PowerState: inst.PowerState,
|
|
}
|
|
item.Key.Organization = inst.Key.Organization
|
|
item.Key.Name = inst.Key.Name
|
|
item.Key.CloudletKey.Organization = inst.Key.CloudletKey.Organization
|
|
item.Key.CloudletKey.Name = inst.Key.CloudletKey.Name
|
|
item.AppKey.Organization = inst.AppKey.Organization
|
|
item.AppKey.Name = inst.AppKey.Name
|
|
item.AppKey.Version = inst.AppKey.Version
|
|
item.Flavor.Name = inst.Flavor.Name
|
|
return item
|
|
}
|
|
|
|
// convertToAppInstanceListItems converts a slice of AppInstances to AppInstanceListItems
|
|
func convertToAppInstanceListItems(instances []v2.AppInstance) []AppInstanceListItem {
|
|
items := make([]AppInstanceListItem, len(instances))
|
|
for i, inst := range instances {
|
|
items[i] = convertToAppInstanceListItem(inst)
|
|
}
|
|
return items
|
|
}
|
|
|
|
// getProtocolFromRequest extracts the protocol from the MCP initialize request
|
|
func getProtocolFromRequest(req *mcp.CallToolRequest) mcpuiserver.ProtocolType {
|
|
// Get initialize params from session
|
|
initParams := req.Session.InitializeParams()
|
|
if initParams == nil {
|
|
return "" // Return empty if not available
|
|
}
|
|
|
|
// Convert InitializeParams to map for ParseProtocolFromInitialize
|
|
paramsMap := make(map[string]any)
|
|
paramsJSON, err := json.Marshal(initParams)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
if err := json.Unmarshal(paramsJSON, ¶msMap); err != nil {
|
|
return ""
|
|
}
|
|
|
|
// Parse protocol from initialize params
|
|
return mcpuiserver.ParseProtocolFromInitialize(paramsMap)
|
|
}
|
|
|
|
// filterAppInstances performs client-side partial matching on app instances
|
|
func filterAppInstances(instances []v2.AppInstance, organization, instanceName, cloudletOrg, cloudletName, appOrg, appName, appVersion *string) []v2.AppInstance {
|
|
if organization == nil && instanceName == nil &&
|
|
cloudletOrg == nil && cloudletName == nil &&
|
|
appOrg == nil && appName == nil &&
|
|
appVersion == nil {
|
|
return instances // No filters, return all
|
|
}
|
|
|
|
var filtered []v2.AppInstance
|
|
for _, inst := range instances {
|
|
match := true
|
|
|
|
if organization != nil && *organization != "" {
|
|
if !strings.Contains(strings.ToLower(inst.Key.Organization), strings.ToLower(*organization)) {
|
|
match = false
|
|
}
|
|
}
|
|
|
|
if match && instanceName != nil && *instanceName != "" {
|
|
if !strings.Contains(strings.ToLower(inst.Key.Name), strings.ToLower(*instanceName)) {
|
|
match = false
|
|
}
|
|
}
|
|
|
|
if match && cloudletOrg != nil && *cloudletOrg != "" {
|
|
if !strings.Contains(strings.ToLower(inst.Key.CloudletKey.Organization), strings.ToLower(*cloudletOrg)) {
|
|
match = false
|
|
}
|
|
}
|
|
|
|
if match && cloudletName != nil && *cloudletName != "" {
|
|
if !strings.Contains(strings.ToLower(inst.Key.CloudletKey.Name), strings.ToLower(*cloudletName)) {
|
|
match = false
|
|
}
|
|
}
|
|
|
|
if match && appOrg != nil && *appOrg != "" {
|
|
if !strings.Contains(strings.ToLower(inst.AppKey.Organization), strings.ToLower(*appOrg)) {
|
|
match = false
|
|
}
|
|
}
|
|
|
|
if match && appName != nil && *appName != "" {
|
|
if !strings.Contains(strings.ToLower(inst.AppKey.Name), strings.ToLower(*appName)) {
|
|
match = false
|
|
}
|
|
}
|
|
|
|
if match && appVersion != nil && *appVersion != "" {
|
|
if !strings.Contains(strings.ToLower(inst.AppKey.Version), strings.ToLower(*appVersion)) {
|
|
match = false
|
|
}
|
|
}
|
|
|
|
if match {
|
|
filtered = append(filtered, inst)
|
|
}
|
|
}
|
|
|
|
return filtered
|
|
}
|
|
|
|
// filterApps performs client-side partial matching on apps
|
|
func filterApps(apps []v2.App, organization, name, version *string) []v2.App {
|
|
if organization == nil && name == nil && version == nil {
|
|
return apps // No filters, return all
|
|
}
|
|
|
|
var filtered []v2.App
|
|
for _, app := range apps {
|
|
match := true
|
|
|
|
if organization != nil && *organization != "" {
|
|
if !strings.Contains(strings.ToLower(app.Key.Organization), strings.ToLower(*organization)) {
|
|
match = false
|
|
}
|
|
}
|
|
|
|
if match && name != nil && *name != "" {
|
|
if !strings.Contains(strings.ToLower(app.Key.Name), strings.ToLower(*name)) {
|
|
match = false
|
|
}
|
|
}
|
|
|
|
if match && version != nil && *version != "" {
|
|
if !strings.Contains(strings.ToLower(app.Key.Version), strings.ToLower(*version)) {
|
|
match = false
|
|
}
|
|
}
|
|
|
|
if match {
|
|
filtered = append(filtered, app)
|
|
}
|
|
}
|
|
|
|
return filtered
|
|
}
|
|
|
|
// Apps Tool Registrations
|
|
|
|
func registerCreateAppTool(s *mcp.Server) {
|
|
type args struct {
|
|
Organization string `json:"organization" jsonschema:"Organization name"`
|
|
Name string `json:"name" jsonschema:"Application name"`
|
|
Version string `json:"version" jsonschema:"Application version (e.g. '1.0.0')"`
|
|
Deployment string `json:"deployment" jsonschema:"Deployment type: 'docker' or 'kubernetes'"`
|
|
ImageType *string `json:"image_type,omitempty" jsonschema:"Image type (default: 'ImageTypeDocker')"`
|
|
ImagePath string `json:"image_path" jsonschema:"Docker registry URL (e.g. 'https://registry-1.docker.io/library/nginx:latest')"`
|
|
DeploymentManifest *string `json:"deployment_manifest,omitempty" jsonschema:"Kubernetes manifest (YAML) or Docker Compose file content (optional)"`
|
|
AccessPorts *string `json:"access_ports,omitempty" jsonschema:"Access ports specification (e.g. 'tcp:80')"`
|
|
DefaultFlavorName *string `json:"default_flavor_name,omitempty" jsonschema:"Default flavor name (e.g. 'EU.small')"`
|
|
AllowServerless *bool `json:"allow_serverless,omitempty" jsonschema:"Allow serverless deployment"`
|
|
ServerlessMinReplicas *int `json:"serverless_min_replicas,omitempty" jsonschema:"Serverless minimum replicas (optional, e.g. 1)"`
|
|
ServerlessRAM *int `json:"serverless_ram,omitempty" jsonschema:"Serverless RAM in MB (optional, e.g. 512)"`
|
|
ServerlessVCPUs *int `json:"serverless_vcpus,omitempty" jsonschema:"Serverless vCPUs (optional, e.g. 2)"`
|
|
Region *string `json:"region,omitempty" jsonschema:"Region (e.g. 'EU' 'US'). If not specified uses default from config."`
|
|
}
|
|
|
|
mcp.AddTool(s, &mcp.Tool{
|
|
Name: "create_app",
|
|
Description: "Create a new Edge Connect application. Requires organization, name, version, deployment type, and image path. Optionally accepts deployment manifest for Kubernetes or Docker Compose configurations. Either default_flavor_name or serverless configuration (serverless_min_replicas, serverless_ram, serverless_vcpus) must be specified.",
|
|
}, func(ctx context.Context, req *mcp.CallToolRequest, a args) (*mcp.CallToolResult, any, error) {
|
|
imageType := "ImageTypeDocker"
|
|
if a.ImageType != nil {
|
|
imageType = *a.ImageType
|
|
}
|
|
|
|
accessPorts := ""
|
|
if a.AccessPorts != nil {
|
|
accessPorts = *a.AccessPorts
|
|
}
|
|
|
|
allowServerless := false
|
|
if a.AllowServerless != nil {
|
|
allowServerless = *a.AllowServerless
|
|
}
|
|
|
|
deploymentManifest := ""
|
|
if a.DeploymentManifest != nil {
|
|
deploymentManifest = *a.DeploymentManifest
|
|
}
|
|
|
|
region := config.DefaultRegion
|
|
if a.Region != nil {
|
|
region = *a.Region
|
|
}
|
|
|
|
app := v2.App{
|
|
Key: v2.AppKey{
|
|
Organization: a.Organization,
|
|
Name: a.Name,
|
|
Version: a.Version,
|
|
},
|
|
Deployment: a.Deployment,
|
|
ImageType: imageType,
|
|
ImagePath: a.ImagePath,
|
|
DeploymentManifest: deploymentManifest,
|
|
AccessPorts: accessPorts,
|
|
AllowServerless: allowServerless,
|
|
}
|
|
|
|
if a.DefaultFlavorName != nil && *a.DefaultFlavorName != "" {
|
|
app.DefaultFlavor = v2.Flavor{Name: *a.DefaultFlavorName}
|
|
}
|
|
|
|
// Build serverless config if any serverless parameters are provided
|
|
if a.ServerlessMinReplicas != nil || a.ServerlessRAM != nil || a.ServerlessVCPUs != nil {
|
|
serverlessConfig := v2.ServerlessConfig{}
|
|
if a.ServerlessMinReplicas != nil {
|
|
serverlessConfig.MinReplicas = *a.ServerlessMinReplicas
|
|
}
|
|
if a.ServerlessRAM != nil {
|
|
serverlessConfig.RAM = *a.ServerlessRAM
|
|
}
|
|
if a.ServerlessVCPUs != nil {
|
|
serverlessConfig.VCPUs = *a.ServerlessVCPUs
|
|
}
|
|
app.ServerlessConfig = serverlessConfig
|
|
}
|
|
|
|
input := &v2.NewAppInput{
|
|
Region: region,
|
|
App: app,
|
|
}
|
|
|
|
if err := edgeClient.CreateApp(ctx, input); err != nil {
|
|
return nil, nil, fmt.Errorf("failed to create app: %w", err)
|
|
}
|
|
|
|
result := fmt.Sprintf("Successfully created app %s/%s:%s in region %s", a.Organization, a.Name, a.Version, region)
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{&mcp.TextContent{Text: result}},
|
|
}, nil, nil
|
|
})
|
|
}
|
|
|
|
func registerShowAppTool(s *mcp.Server) {
|
|
type args struct {
|
|
Organization string `json:"organization" jsonschema:"Organization name"`
|
|
Name string `json:"name" jsonschema:"Application name"`
|
|
Version string `json:"version" jsonschema:"Application version"`
|
|
Region *string `json:"region,omitempty" jsonschema:"Region (e.g. 'EU' 'US'). If not specified uses default from config."`
|
|
}
|
|
|
|
mcp.AddTool(s, &mcp.Tool{
|
|
Name: "show_app",
|
|
Description: "Retrieve a specific Edge Connect application by its key.",
|
|
}, func(ctx context.Context, req *mcp.CallToolRequest, a args) (*mcp.CallToolResult, any, error) {
|
|
region := config.DefaultRegion
|
|
if a.Region != nil {
|
|
region = *a.Region
|
|
}
|
|
|
|
appKey := v2.AppKey{
|
|
Organization: a.Organization,
|
|
Name: a.Name,
|
|
Version: a.Version,
|
|
}
|
|
|
|
app, err := edgeClient.ShowApp(ctx, appKey, region)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to show app: %w", err)
|
|
}
|
|
|
|
appJSON, err := json.MarshalIndent(app, "", " ")
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to serialize app: %w", err)
|
|
}
|
|
|
|
// Parse protocol from MCP initialize request
|
|
protocol := getProtocolFromRequest(req)
|
|
|
|
// Create UI resource for app detail
|
|
uiResource, err := createAppDetailUI(app, region, protocol)
|
|
if err != nil {
|
|
// If UI creation fails, just return text content
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{&mcp.TextContent{Text: string(appJSON)}},
|
|
}, nil, nil
|
|
}
|
|
|
|
// Convert UIResource to MCP EmbeddedResource
|
|
resourceContents := &mcp.ResourceContents{
|
|
URI: uiResource.Resource.URI,
|
|
MIMEType: uiResource.Resource.MimeType,
|
|
Text: uiResource.Resource.Text,
|
|
Meta: uiResource.Resource.Meta,
|
|
}
|
|
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{
|
|
&mcp.TextContent{Text: string(appJSON)},
|
|
&mcp.EmbeddedResource{
|
|
Resource: resourceContents,
|
|
},
|
|
},
|
|
}, nil, nil
|
|
})
|
|
}
|
|
|
|
func registerListAppsTool(s *mcp.Server) {
|
|
type args struct {
|
|
Organization *string `json:"organization,omitempty" jsonschema:"Filter by organization name (optional)"`
|
|
Name *string `json:"name,omitempty" jsonschema:"Filter by application name (optional)"`
|
|
Version *string `json:"version,omitempty" jsonschema:"Filter by version (optional)"`
|
|
Region *string `json:"region,omitempty" jsonschema:"Region (e.g. 'EU' 'US'). If not specified uses default from config."`
|
|
}
|
|
|
|
mcp.AddTool(s, &mcp.Tool{
|
|
Name: "list_apps",
|
|
Description: "List all Edge Connect applications matching the specified filter. Supports partial (substring) matching for all filter fields.",
|
|
}, func(ctx context.Context, req *mcp.CallToolRequest, a args) (*mcp.CallToolResult, any, error) {
|
|
region := config.DefaultRegion
|
|
if a.Region != nil {
|
|
region = *a.Region
|
|
}
|
|
|
|
// Fetch all apps (empty filters) to enable client-side partial matching
|
|
appKey := v2.AppKey{
|
|
Organization: "",
|
|
Name: "",
|
|
Version: "",
|
|
}
|
|
|
|
allApps, err := edgeClient.ShowApps(ctx, appKey, region)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to list apps: %w", err)
|
|
}
|
|
|
|
// Apply client-side partial matching filters
|
|
apps := filterApps(allApps, a.Organization, a.Name, a.Version)
|
|
|
|
// Convert to lightweight DTOs for reduced token usage
|
|
appListItems := convertToAppListItems(apps)
|
|
appsJSON, err := json.MarshalIndent(appListItems, "", " ")
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to serialize apps: %w", err)
|
|
}
|
|
|
|
// Parse protocol from MCP initialize request
|
|
protocol := getProtocolFromRequest(req)
|
|
|
|
// Create UI resource for apps list (uses full apps)
|
|
uiResource, err := createAppListUI(apps, region, protocol)
|
|
if err != nil {
|
|
// If UI creation fails, just return text content
|
|
result := fmt.Sprintf("Found %d apps:\n%s", len(apps), string(appsJSON))
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{&mcp.TextContent{Text: result}},
|
|
}, nil, nil
|
|
}
|
|
|
|
result := fmt.Sprintf("Found %d apps:\n%s", len(apps), string(appsJSON))
|
|
|
|
// Convert UIResource to MCP EmbeddedResource
|
|
resourceContents := &mcp.ResourceContents{
|
|
URI: uiResource.Resource.URI,
|
|
MIMEType: uiResource.Resource.MimeType,
|
|
Text: uiResource.Resource.Text,
|
|
Meta: uiResource.Resource.Meta,
|
|
}
|
|
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{
|
|
&mcp.TextContent{Text: result},
|
|
&mcp.EmbeddedResource{
|
|
Resource: resourceContents,
|
|
},
|
|
},
|
|
}, nil, nil
|
|
})
|
|
}
|
|
|
|
func registerUpdateAppTool(s *mcp.Server) {
|
|
type args struct {
|
|
Organization string `json:"organization" jsonschema:"Organization name"`
|
|
Name string `json:"name" jsonschema:"Application name"`
|
|
Version string `json:"version" jsonschema:"Application version"`
|
|
ImagePath *string `json:"image_path,omitempty" jsonschema:"New Docker registry URL (optional)"`
|
|
DeploymentManifest *string `json:"deployment_manifest,omitempty" jsonschema:"New Kubernetes manifest (YAML) or Docker Compose file content (optional)"`
|
|
AccessPorts *string `json:"access_ports,omitempty" jsonschema:"New access ports specification (optional)"`
|
|
DefaultFlavorName *string `json:"default_flavor_name,omitempty" jsonschema:"New default flavor name (optional)"`
|
|
AllowServerless *bool `json:"allow_serverless,omitempty" jsonschema:"New serverless setting (optional)"`
|
|
ServerlessMinReplicas *int `json:"serverless_min_replicas,omitempty" jsonschema:"New serverless minimum replicas (optional)"`
|
|
ServerlessRAM *int `json:"serverless_ram,omitempty" jsonschema:"New serverless RAM in MB (optional)"`
|
|
ServerlessVCPUs *int `json:"serverless_vcpus,omitempty" jsonschema:"New serverless vCPUs (optional)"`
|
|
Region *string `json:"region,omitempty" jsonschema:"Region (e.g. 'EU' 'US'). If not specified uses default from config."`
|
|
}
|
|
|
|
mcp.AddTool(s, &mcp.Tool{
|
|
Name: "update_app",
|
|
Description: "Update an existing Edge Connect application. Only specified fields will be updated. Supports updating deployment manifest, image path, access ports, flavor, serverless settings, and serverless configuration (min_replicas, ram, vcpus).",
|
|
}, func(ctx context.Context, req *mcp.CallToolRequest, a args) (*mcp.CallToolResult, any, error) {
|
|
region := config.DefaultRegion
|
|
if a.Region != nil {
|
|
region = *a.Region
|
|
}
|
|
|
|
appKey := v2.AppKey{
|
|
Organization: a.Organization,
|
|
Name: a.Name,
|
|
Version: a.Version,
|
|
}
|
|
|
|
currentApp, err := edgeClient.ShowApp(ctx, appKey, region)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to fetch current app: %w", err)
|
|
}
|
|
|
|
var fields []string
|
|
|
|
if a.ImagePath != nil && *a.ImagePath != "" {
|
|
currentApp.ImagePath = *a.ImagePath
|
|
fields = append(fields, v2.AppFieldImagePath)
|
|
}
|
|
|
|
if a.DeploymentManifest != nil && *a.DeploymentManifest != "" {
|
|
currentApp.DeploymentManifest = *a.DeploymentManifest
|
|
fields = append(fields, v2.AppFieldDeploymentManifest)
|
|
}
|
|
|
|
if a.AccessPorts != nil && *a.AccessPorts != "" {
|
|
currentApp.AccessPorts = *a.AccessPorts
|
|
fields = append(fields, v2.AppFieldAccessPorts)
|
|
}
|
|
|
|
if a.DefaultFlavorName != nil && *a.DefaultFlavorName != "" {
|
|
currentApp.DefaultFlavor = v2.Flavor{Name: *a.DefaultFlavorName}
|
|
fields = append(fields, v2.AppFieldDefaultFlavor)
|
|
}
|
|
|
|
if a.AllowServerless != nil {
|
|
currentApp.AllowServerless = *a.AllowServerless
|
|
fields = append(fields, v2.AppFieldAllowServerless)
|
|
}
|
|
|
|
// Update serverless config if any serverless parameters are provided
|
|
if a.ServerlessMinReplicas != nil || a.ServerlessRAM != nil || a.ServerlessVCPUs != nil {
|
|
// Get existing serverless config or create new one
|
|
serverlessConfig := v2.ServerlessConfig{}
|
|
|
|
// Try to preserve existing config by converting through JSON
|
|
if configBytes, err := json.Marshal(currentApp.ServerlessConfig); err == nil {
|
|
_ = json.Unmarshal(configBytes, &serverlessConfig)
|
|
}
|
|
|
|
// Update provided fields
|
|
if a.ServerlessMinReplicas != nil {
|
|
serverlessConfig.MinReplicas = *a.ServerlessMinReplicas
|
|
}
|
|
if a.ServerlessRAM != nil {
|
|
serverlessConfig.RAM = *a.ServerlessRAM
|
|
}
|
|
if a.ServerlessVCPUs != nil {
|
|
serverlessConfig.VCPUs = *a.ServerlessVCPUs
|
|
}
|
|
|
|
currentApp.ServerlessConfig = serverlessConfig
|
|
fields = append(fields, v2.AppFieldServerlessConfig)
|
|
}
|
|
|
|
if len(fields) == 0 {
|
|
return nil, nil, fmt.Errorf("no fields to update")
|
|
}
|
|
|
|
currentApp.Fields = fields
|
|
|
|
input := &v2.UpdateAppInput{
|
|
Region: region,
|
|
App: currentApp,
|
|
}
|
|
|
|
if err := edgeClient.UpdateApp(ctx, input); err != nil {
|
|
return nil, nil, fmt.Errorf("failed to update app: %w", err)
|
|
}
|
|
|
|
result := fmt.Sprintf("Successfully updated app %s/%s:%s in region %s (updated fields: %v)",
|
|
a.Organization, a.Name, a.Version, region, fields)
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{&mcp.TextContent{Text: result}},
|
|
}, nil, nil
|
|
})
|
|
}
|
|
|
|
func registerDeleteAppTool(s *mcp.Server) {
|
|
type args struct {
|
|
Organization string `json:"organization" jsonschema:"Organization name"`
|
|
Name string `json:"name" jsonschema:"Application name"`
|
|
Version string `json:"version" jsonschema:"Application version"`
|
|
Region *string `json:"region,omitempty" jsonschema:"Region (e.g. 'EU' 'US'). If not specified uses default from config."`
|
|
}
|
|
|
|
mcp.AddTool(s, &mcp.Tool{
|
|
Name: "delete_app",
|
|
Description: "Delete an Edge Connect application. This operation is idempotent (safe to call multiple times). Apps can only be deleted if all app instances referencing the app has been deleted.",
|
|
}, func(ctx context.Context, req *mcp.CallToolRequest, a args) (*mcp.CallToolResult, any, error) {
|
|
region := config.DefaultRegion
|
|
if a.Region != nil {
|
|
region = *a.Region
|
|
}
|
|
|
|
appKey := v2.AppKey{
|
|
Organization: a.Organization,
|
|
Name: a.Name,
|
|
Version: a.Version,
|
|
}
|
|
|
|
if err := edgeClient.DeleteApp(ctx, appKey, region); err != nil {
|
|
return nil, nil, fmt.Errorf("failed to delete app: %w", err)
|
|
}
|
|
|
|
result := fmt.Sprintf("Successfully deleted app %s/%s:%s from region %s", a.Organization, a.Name, a.Version, region)
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{&mcp.TextContent{Text: result}},
|
|
}, nil, nil
|
|
})
|
|
}
|
|
|
|
// App Instance Tool Registrations
|
|
|
|
func registerCreateAppInstanceTool(s *mcp.Server) {
|
|
type args struct {
|
|
Organization string `json:"organization" jsonschema:"Organization name"`
|
|
InstanceName string `json:"instance_name" jsonschema:"Instance name"`
|
|
CloudletOrg string `json:"cloudlet_org" jsonschema:"Cloudlet organization name"`
|
|
CloudletName string `json:"cloudlet_name" jsonschema:"Cloudlet name"`
|
|
AppOrg string `json:"app_org" jsonschema:"Application organization name"`
|
|
AppName string `json:"app_name" jsonschema:"Application name"`
|
|
AppVersion string `json:"app_version" jsonschema:"Application version"`
|
|
FlavorName *string `json:"flavor_name,omitempty" jsonschema:"Flavor name (e.g. 'EU.small')"`
|
|
Latitude *float64 `json:"latitude,omitempty" jsonschema:"Cloudlet latitude (optional)"`
|
|
Longitude *float64 `json:"longitude,omitempty" jsonschema:"Cloudlet longitude (optional)"`
|
|
Region *string `json:"region,omitempty" jsonschema:"Region (e.g. 'EU' 'US'). If not specified uses default from config."`
|
|
}
|
|
|
|
mcp.AddTool(s, &mcp.Tool{
|
|
Name: "create_app_instance",
|
|
Description: "Create a new Edge Connect application instance on a specific cloudlet.",
|
|
}, func(ctx context.Context, req *mcp.CallToolRequest, a args) (*mcp.CallToolResult, any, error) {
|
|
region := config.DefaultRegion
|
|
if a.Region != nil {
|
|
region = *a.Region
|
|
}
|
|
|
|
appInst := v2.AppInstance{
|
|
Key: v2.AppInstanceKey{
|
|
Organization: a.Organization,
|
|
Name: a.InstanceName,
|
|
CloudletKey: v2.CloudletKey{
|
|
Organization: a.CloudletOrg,
|
|
Name: a.CloudletName,
|
|
},
|
|
},
|
|
AppKey: v2.AppKey{
|
|
Organization: a.AppOrg,
|
|
Name: a.AppName,
|
|
Version: a.AppVersion,
|
|
},
|
|
}
|
|
|
|
if a.FlavorName != nil && *a.FlavorName != "" {
|
|
appInst.Flavor = v2.Flavor{Name: *a.FlavorName}
|
|
}
|
|
|
|
if (a.Latitude != nil && *a.Latitude != 0) || (a.Longitude != nil && *a.Longitude != 0) {
|
|
lat := 0.0
|
|
lon := 0.0
|
|
if a.Latitude != nil {
|
|
lat = *a.Latitude
|
|
}
|
|
if a.Longitude != nil {
|
|
lon = *a.Longitude
|
|
}
|
|
appInst.CloudletLoc = v2.CloudletLoc{
|
|
Latitude: lat,
|
|
Longitude: lon,
|
|
}
|
|
}
|
|
|
|
input := &v2.NewAppInstanceInput{
|
|
Region: region,
|
|
AppInst: appInst,
|
|
}
|
|
|
|
if err := edgeClient.CreateAppInstance(ctx, input); err != nil {
|
|
return nil, nil, fmt.Errorf("failed to create app instance: %w", err)
|
|
}
|
|
|
|
result := fmt.Sprintf("Successfully created app instance %s/%s on cloudlet %s/%s in region %s",
|
|
a.Organization, a.InstanceName, a.CloudletOrg, a.CloudletName, region)
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{&mcp.TextContent{Text: result}},
|
|
}, nil, nil
|
|
})
|
|
}
|
|
|
|
func registerShowAppInstanceTool(s *mcp.Server) {
|
|
type args struct {
|
|
Organization string `json:"organization" jsonschema:"Organization name"`
|
|
InstanceName string `json:"instance_name" jsonschema:"Instance name"`
|
|
CloudletOrg string `json:"cloudlet_org" jsonschema:"Cloudlet organization name"`
|
|
CloudletName string `json:"cloudlet_name" jsonschema:"Cloudlet name"`
|
|
AppOrg *string `json:"app_org,omitempty" jsonschema:"Application organization name (optional for filtering)"`
|
|
AppName *string `json:"app_name,omitempty" jsonschema:"Application name (optional for filtering)"`
|
|
AppVersion *string `json:"app_version,omitempty" jsonschema:"Application version (optional for filtering)"`
|
|
Region *string `json:"region,omitempty" jsonschema:"Region (e.g. 'EU' 'US'). If not specified uses default from config."`
|
|
}
|
|
|
|
mcp.AddTool(s, &mcp.Tool{
|
|
Name: "show_app_instance",
|
|
Description: "Retrieve a specific Edge Connect application instance.",
|
|
}, func(ctx context.Context, req *mcp.CallToolRequest, a args) (*mcp.CallToolResult, any, error) {
|
|
region := config.DefaultRegion
|
|
if a.Region != nil {
|
|
region = *a.Region
|
|
}
|
|
|
|
appInstKey := v2.AppInstanceKey{
|
|
Organization: a.Organization,
|
|
Name: a.InstanceName,
|
|
CloudletKey: v2.CloudletKey{
|
|
Organization: a.CloudletOrg,
|
|
Name: a.CloudletName,
|
|
},
|
|
}
|
|
|
|
appKey := v2.AppKey{}
|
|
if a.AppOrg != nil {
|
|
appKey.Organization = *a.AppOrg
|
|
}
|
|
if a.AppName != nil {
|
|
appKey.Name = *a.AppName
|
|
}
|
|
if a.AppVersion != nil {
|
|
appKey.Version = *a.AppVersion
|
|
}
|
|
|
|
appInst, err := edgeClient.ShowAppInstance(ctx, appInstKey, appKey, region)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to show app instance: %w", err)
|
|
}
|
|
|
|
appInstJSON, err := json.MarshalIndent(appInst, "", " ")
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to serialize app instance: %w", err)
|
|
}
|
|
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{&mcp.TextContent{Text: string(appInstJSON)}},
|
|
}, nil, nil
|
|
})
|
|
}
|
|
|
|
func registerListAppInstancesTool(s *mcp.Server) {
|
|
type args struct {
|
|
Organization *string `json:"organization,omitempty" jsonschema:"Filter by organization name (optional)"`
|
|
InstanceName *string `json:"instance_name,omitempty" jsonschema:"Filter by instance name (optional)"`
|
|
CloudletOrg *string `json:"cloudlet_org,omitempty" jsonschema:"Filter by cloudlet organization (optional)"`
|
|
CloudletName *string `json:"cloudlet_name,omitempty" jsonschema:"Filter by cloudlet name (optional)"`
|
|
AppOrg *string `json:"app_org,omitempty" jsonschema:"Filter by application organization (optional)"`
|
|
AppName *string `json:"app_name,omitempty" jsonschema:"Filter by application name (optional)"`
|
|
AppVersion *string `json:"app_version,omitempty" jsonschema:"Filter by application version (optional)"`
|
|
Region *string `json:"region,omitempty" jsonschema:"Region (e.g. 'EU' 'US'). If not specified uses default from config."`
|
|
}
|
|
|
|
mcp.AddTool(s, &mcp.Tool{
|
|
Name: "list_app_instances",
|
|
Description: "List all Edge Connect application instances matching the specified filter. Supports partial (substring) matching for all filter fields.",
|
|
}, func(ctx context.Context, req *mcp.CallToolRequest, a args) (*mcp.CallToolResult, any, error) {
|
|
region := config.DefaultRegion
|
|
if a.Region != nil {
|
|
region = *a.Region
|
|
}
|
|
|
|
// Fetch all instances (empty filters) to enable client-side partial matching
|
|
appInstKey := v2.AppInstanceKey{
|
|
Organization: "",
|
|
Name: "",
|
|
CloudletKey: v2.CloudletKey{
|
|
Organization: "",
|
|
Name: "",
|
|
},
|
|
}
|
|
|
|
appKey := v2.AppKey{
|
|
Organization: "",
|
|
Name: "",
|
|
Version: "",
|
|
}
|
|
|
|
allAppInsts, err := edgeClient.ShowAppInstances(ctx, appInstKey, appKey, region)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to list app instances: %w", err)
|
|
}
|
|
|
|
// Apply client-side partial matching filters
|
|
appInsts := filterAppInstances(allAppInsts, a.Organization, a.InstanceName, a.CloudletOrg, a.CloudletName, a.AppOrg, a.AppName, a.AppVersion)
|
|
|
|
// Convert to lightweight DTOs for reduced token usage
|
|
appInstListItems := convertToAppInstanceListItems(appInsts)
|
|
appInstsJSON, err := json.MarshalIndent(appInstListItems, "", " ")
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to serialize app instances: %w", err)
|
|
}
|
|
|
|
// Parse protocol from MCP initialize request
|
|
protocol := getProtocolFromRequest(req)
|
|
|
|
// Create UI resource for app instances list (uses full instances)
|
|
uiResource, err := createAppInstanceListUI(appInsts, region, protocol)
|
|
if err != nil {
|
|
// If UI creation fails, just return text content
|
|
result := fmt.Sprintf("Found %d app instances:\n%s", len(appInsts), string(appInstsJSON))
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{&mcp.TextContent{Text: result}},
|
|
}, nil, nil
|
|
}
|
|
|
|
result := fmt.Sprintf("Found %d app instances:\n%s", len(appInsts), string(appInstsJSON))
|
|
|
|
// Convert UIResource to MCP EmbeddedResource
|
|
resourceContents := &mcp.ResourceContents{
|
|
URI: uiResource.Resource.URI,
|
|
MIMEType: uiResource.Resource.MimeType,
|
|
Text: uiResource.Resource.Text,
|
|
Meta: uiResource.Resource.Meta,
|
|
}
|
|
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{
|
|
&mcp.TextContent{Text: result},
|
|
&mcp.EmbeddedResource{
|
|
Resource: resourceContents,
|
|
},
|
|
},
|
|
}, nil, nil
|
|
})
|
|
}
|
|
|
|
func registerUpdateAppInstanceTool(s *mcp.Server) {
|
|
type args struct {
|
|
Organization string `json:"organization" jsonschema:"Organization name"`
|
|
InstanceName string `json:"instance_name" jsonschema:"Instance name"`
|
|
CloudletOrg string `json:"cloudlet_org" jsonschema:"Cloudlet organization name"`
|
|
CloudletName string `json:"cloudlet_name" jsonschema:"Cloudlet name"`
|
|
FlavorName *string `json:"flavor_name,omitempty" jsonschema:"New flavor name (optional)"`
|
|
PowerState *string `json:"power_state,omitempty" jsonschema:"New power state (optional)"`
|
|
Region *string `json:"region,omitempty" jsonschema:"Region (e.g. 'EU' 'US'). If not specified uses default from config."`
|
|
}
|
|
|
|
mcp.AddTool(s, &mcp.Tool{
|
|
Name: "update_app_instance",
|
|
Description: "Update an existing Edge Connect application instance. Only specified fields will be updated.",
|
|
}, func(ctx context.Context, req *mcp.CallToolRequest, a args) (*mcp.CallToolResult, any, error) {
|
|
region := config.DefaultRegion
|
|
if a.Region != nil {
|
|
region = *a.Region
|
|
}
|
|
|
|
appInstKey := v2.AppInstanceKey{
|
|
Organization: a.Organization,
|
|
Name: a.InstanceName,
|
|
CloudletKey: v2.CloudletKey{
|
|
Organization: a.CloudletOrg,
|
|
Name: a.CloudletName,
|
|
},
|
|
}
|
|
|
|
currentAppInst, err := edgeClient.ShowAppInstance(ctx, appInstKey, v2.AppKey{}, region)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to fetch current app instance: %w", err)
|
|
}
|
|
|
|
var fields []string
|
|
|
|
if a.FlavorName != nil && *a.FlavorName != "" {
|
|
currentAppInst.Flavor = v2.Flavor{Name: *a.FlavorName}
|
|
fields = append(fields, v2.AppInstFieldFlavor)
|
|
}
|
|
|
|
if a.PowerState != nil && *a.PowerState != "" {
|
|
currentAppInst.PowerState = *a.PowerState
|
|
fields = append(fields, v2.AppInstFieldPowerState)
|
|
}
|
|
|
|
if len(fields) == 0 {
|
|
return nil, nil, fmt.Errorf("no fields to update")
|
|
}
|
|
|
|
currentAppInst.Fields = fields
|
|
|
|
input := &v2.UpdateAppInstanceInput{
|
|
Region: region,
|
|
AppInst: currentAppInst,
|
|
}
|
|
|
|
if err := edgeClient.UpdateAppInstance(ctx, input); err != nil {
|
|
return nil, nil, fmt.Errorf("failed to update app instance: %w", err)
|
|
}
|
|
|
|
result := fmt.Sprintf("Successfully updated app instance %s/%s on cloudlet %s/%s in region %s (updated fields: %v)",
|
|
a.Organization, a.InstanceName, a.CloudletOrg, a.CloudletName, region, fields)
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{&mcp.TextContent{Text: result}},
|
|
}, nil, nil
|
|
})
|
|
}
|
|
|
|
func registerRefreshAppInstanceTool(s *mcp.Server) {
|
|
type args struct {
|
|
Organization string `json:"organization" jsonschema:"Organization name"`
|
|
InstanceName string `json:"instance_name" jsonschema:"Instance name"`
|
|
CloudletOrg string `json:"cloudlet_org" jsonschema:"Cloudlet organization name"`
|
|
CloudletName string `json:"cloudlet_name" jsonschema:"Cloudlet name"`
|
|
Region *string `json:"region,omitempty" jsonschema:"Region (e.g. 'EU' 'US'). If not specified uses default from config."`
|
|
}
|
|
|
|
mcp.AddTool(s, &mcp.Tool{
|
|
Name: "refresh_app_instance",
|
|
Description: "Refresh an Edge Connect application instance to update its state.",
|
|
}, func(ctx context.Context, req *mcp.CallToolRequest, a args) (*mcp.CallToolResult, any, error) {
|
|
region := config.DefaultRegion
|
|
if a.Region != nil {
|
|
region = *a.Region
|
|
}
|
|
|
|
appInstKey := v2.AppInstanceKey{
|
|
Organization: a.Organization,
|
|
Name: a.InstanceName,
|
|
CloudletKey: v2.CloudletKey{
|
|
Organization: a.CloudletOrg,
|
|
Name: a.CloudletName,
|
|
},
|
|
}
|
|
|
|
if err := edgeClient.RefreshAppInstance(ctx, appInstKey, region); err != nil {
|
|
return nil, nil, fmt.Errorf("failed to refresh app instance: %w", err)
|
|
}
|
|
|
|
result := fmt.Sprintf("Successfully refreshed app instance %s/%s on cloudlet %s/%s in region %s",
|
|
a.Organization, a.InstanceName, a.CloudletOrg, a.CloudletName, region)
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{&mcp.TextContent{Text: result}},
|
|
}, nil, nil
|
|
})
|
|
}
|
|
|
|
func registerDeleteAppInstanceTool(s *mcp.Server) {
|
|
type args struct {
|
|
Organization string `json:"organization" jsonschema:"Organization name"`
|
|
InstanceName string `json:"instance_name" jsonschema:"Instance name"`
|
|
CloudletOrg string `json:"cloudlet_org" jsonschema:"Cloudlet organization name"`
|
|
CloudletName string `json:"cloudlet_name" jsonschema:"Cloudlet name"`
|
|
Region *string `json:"region,omitempty" jsonschema:"Region (e.g. 'EU' 'US'). If not specified uses default from config."`
|
|
}
|
|
|
|
mcp.AddTool(s, &mcp.Tool{
|
|
Name: "delete_app_instance",
|
|
Description: "Delete an Edge Connect application instance. This operation is idempotent (safe to call multiple times).",
|
|
}, func(ctx context.Context, req *mcp.CallToolRequest, a args) (*mcp.CallToolResult, any, error) {
|
|
region := config.DefaultRegion
|
|
if a.Region != nil {
|
|
region = *a.Region
|
|
}
|
|
|
|
appInstKey := v2.AppInstanceKey{
|
|
Organization: a.Organization,
|
|
Name: a.InstanceName,
|
|
CloudletKey: v2.CloudletKey{
|
|
Organization: a.CloudletOrg,
|
|
Name: a.CloudletName,
|
|
},
|
|
}
|
|
|
|
if err := edgeClient.DeleteAppInstance(ctx, appInstKey, region); err != nil {
|
|
return nil, nil, fmt.Errorf("failed to delete app instance: %w", err)
|
|
}
|
|
|
|
result := fmt.Sprintf("Successfully deleted app instance %s/%s from cloudlet %s/%s in region %s",
|
|
a.Organization, a.InstanceName, a.CloudletOrg, a.CloudletName, region)
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{&mcp.TextContent{Text: result}},
|
|
}, nil, nil
|
|
})
|
|
}
|