edge-connect-mcp/tools.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, &paramsMap); 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
})
}