introduced stdio mcp server for edge connect

This commit is contained in:
Manuel Ganter 2026-01-05 11:51:35 +01:00
commit 5f3214d1f7
No known key found for this signature in database
10 changed files with 1260 additions and 0 deletions

18
.claude/CLAUDE.md Normal file
View file

@ -0,0 +1,18 @@
1. Authentication and Authorization
- Use OAuth 2.1 with Authorization Code Flow + PKCE for remote servers.
- Implement Resource Indicators (RFC 8707) to scope tokens to specific servers.
- Enforce least privilege: Tight scopes per tool/user.
- For local (stdio) servers: Rely on OS isolation; avoid exposing auth endpoints.
- Verify Bearer tokens on all requests; use 401 responses with Protected Resource Metadata.
2. Input/Output Handling
- Strictly validate and sanitize inputs from LLM tool calls.
- Filter sensitive data (PII, credentials) from outputs.
- Use JSON schemas for structured inputs/outputs.
- Categorize and handle errors without leaking details (client/server/external).
3. Secure Transport and Operations
- Mandate HTTPS/TLS with valid certificates; consider mTLS.
- Implement rate limiting, timeouts, and DoS protections.
- Structured logging (without sensitive data); integrate with SIEM.
- Sandbox local servers (e.g., Docker, minimal permissions).

14
.env.example Normal file
View file

@ -0,0 +1,14 @@
# Edge Connect API Configuration
EDGE_CONNECT_BASE_URL=https://hub.apps.edge.platform.mg3.mdb.osc.live
EDGE_CONNECT_AUTH_TYPE=credentials
# Authentication - Token based (when auth_type=token)
# EDGE_CONNECT_TOKEN=your-bearer-token-here
# Authentication - Credentials based (when auth_type=credentials)
EDGE_CONNECT_USERNAME=your-username
EDGE_CONNECT_PASSWORD=your-password
# Optional Configuration
EDGE_CONNECT_DEFAULT_REGION=EU
EDGE_CONNECT_DEBUG=false

42
.gitignore vendored Normal file
View file

@ -0,0 +1,42 @@
# Created by https://www.toptal.com/developers/gitignore/api/go
# Edit at https://www.toptal.com/developers/gitignore?templates=go
### Go ###
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
# End of https://www.toptal.com/developers/gitignore/api/go
# MCP Server binary
edge-connect-mcp
# Environment files
.env
.env.local
.env.*.local
# IDE
.vscode/
.idea/
*.swp
*.swo
*~

198
README.md Normal file
View file

@ -0,0 +1,198 @@
# Edge Connect MCP Server
A Model Context Protocol (MCP) server implementation for Edge Connect, providing tools to manage applications and application instances.
## Features
This MCP server implements all Edge Connect API endpoints for:
### Apps Management
- `create_app` - Create a new Edge Connect application
- `show_app` - Retrieve a specific application by key
- `list_apps` - List all applications matching filter criteria
- `update_app` - Update an existing application
- `delete_app` - Delete an application (idempotent)
### App Instance Management
- `create_app_instance` - Create a new application instance on a cloudlet
- `show_app_instance` - Retrieve a specific application instance
- `list_app_instances` - List all application instances matching filter criteria
- `update_app_instance` - Update an existing application instance
- `refresh_app_instance` - Refresh instance state
- `delete_app_instance` - Delete an application instance (idempotent)
## Installation
1. Clone this repository
2. Build the server:
```bash
go build -o edge-connect-mcp
```
## Configuration
The server is configured via environment variables:
### Required Configuration
- `EDGE_CONNECT_BASE_URL` - Base URL of the Edge Connect API (e.g., `https://hub.apps.edge.platform.mg3.mdb.osc.live`)
- `EDGE_CONNECT_AUTH_TYPE` - Authentication type: `token`, `credentials`, or `none`
### Authentication Configuration
For **token-based authentication** (`auth_type=token`):
- `EDGE_CONNECT_TOKEN` - Bearer token for authentication
For **credentials-based authentication** (`auth_type=credentials`):
- `EDGE_CONNECT_USERNAME` - Username for authentication
- `EDGE_CONNECT_PASSWORD` - Password for authentication
For **no authentication** (`auth_type=none`):
- No additional configuration required (useful for testing)
### Optional Configuration
- `EDGE_CONNECT_DEFAULT_REGION` - Default region to use when not specified in tool calls (default: `EU`)
- `EDGE_CONNECT_DEBUG` - Enable debug logging (`true` or `1`)
## Usage
### Running the Server
The server runs in stdio mode for MCP integration:
```bash
export EDGE_CONNECT_BASE_URL="https://hub.apps.edge.platform.mg3.mdb.osc.live"
export EDGE_CONNECT_AUTH_TYPE="credentials"
export EDGE_CONNECT_USERNAME="your-username"
export EDGE_CONNECT_PASSWORD="your-password"
export EDGE_CONNECT_DEFAULT_REGION="EU"
./edge-connect-mcp
```
### Integrating with Claude Desktop
Add the server to your Claude Desktop configuration file:
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
```json
{
"mcpServers": {
"edge-connect": {
"command": "/path/to/edge-connect-mcp",
"env": {
"EDGE_CONNECT_BASE_URL": "https://hub.apps.edge.platform.mg3.mdb.osc.live",
"EDGE_CONNECT_AUTH_TYPE": "credentials",
"EDGE_CONNECT_USERNAME": "your-username",
"EDGE_CONNECT_PASSWORD": "your-password",
"EDGE_CONNECT_DEFAULT_REGION": "EU"
}
}
}
}
```
## Tool Examples
### Create an Application
```json
{
"organization": "my-org",
"name": "my-app",
"version": "1.0.0",
"deployment": "docker",
"image_path": "https://registry-1.docker.io/library/nginx:latest",
"access_ports": "tcp:80",
"default_flavor_name": "EU.small"
}
```
### List Applications
```json
{
"organization": "my-org"
}
```
### Create an Application Instance
```json
{
"organization": "my-org",
"instance_name": "my-instance",
"cloudlet_org": "cloudlet-org",
"cloudlet_name": "cloudlet-01",
"app_org": "my-org",
"app_name": "my-app",
"app_version": "1.0.0",
"flavor_name": "EU.small"
}
```
### List Application Instances
```json
{
"organization": "my-org",
"app_name": "my-app"
}
```
## Security
This implementation follows the security guidelines from CLAUDE.md:
1. **Authentication**: Supports OAuth 2.1 with Authorization Code Flow + PKCE, token-based auth, and credentials-based auth
2. **Input Validation**: All inputs are strictly validated using JSON schemas
3. **Error Handling**: Errors are properly categorized without leaking sensitive details
4. **Transport Security**: Expects HTTPS/TLS connections to the Edge Connect API
5. **Least Privilege**: Scoped access based on authentication credentials
## Dependencies
- `edp.buildth.ing/DevFW-CICD/edge-connect-client/v2` - Edge Connect Go SDK
- `github.com/modelcontextprotocol/go-sdk` - Model Context Protocol Go SDK
## Development
### Project Structure
```
.
├── main.go # Server entry point and initialization
├── config.go # Configuration loading and validation
├── tools.go # MCP tool definitions and handlers
├── utils.go # Utility functions
├── README.md # This file
└── .env.example # Example environment configuration
```
### Building
```bash
go build -o edge-connect-mcp
```
### Testing
Set up your environment variables and run the server:
```bash
./edge-connect-mcp
```
The server will start in stdio mode and communicate via JSON-RPC over stdin/stdout.
## License
See LICENSE file for details.
## Support
For issues or questions, please refer to the Edge Connect documentation or contact support.

99
config.go Normal file
View file

@ -0,0 +1,99 @@
package main
import (
"fmt"
"os"
"time"
)
type Config struct {
// Edge Connect API Configuration
BaseURL string `json:"base_url"`
AuthType string `json:"auth_type"` // "token", "credentials", or "none"
// Authentication - Token
Token string `json:"token,omitempty"`
// Authentication - Credentials
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
// Default Region
DefaultRegion string `json:"default_region"`
// Retry Configuration
RetryMaxRetries int `json:"retry_max_retries"`
RetryInitialDelay time.Duration `json:"retry_initial_delay"`
RetryMaxDelay time.Duration `json:"retry_max_delay"`
RetryMultiplier float64 `json:"retry_multiplier"`
// Debug
Debug bool `json:"debug"`
}
func LoadConfig() (*Config, error) {
cfg := &Config{
// Default values
DefaultRegion: "EU",
RetryMaxRetries: 3,
RetryInitialDelay: 1 * time.Second,
RetryMaxDelay: 30 * time.Second,
RetryMultiplier: 2.0,
Debug: false,
}
// Load from environment variables
if baseURL := os.Getenv("EDGE_CONNECT_BASE_URL"); baseURL != "" {
cfg.BaseURL = baseURL
}
if authType := os.Getenv("EDGE_CONNECT_AUTH_TYPE"); authType != "" {
cfg.AuthType = authType
}
if token := os.Getenv("EDGE_CONNECT_TOKEN"); token != "" {
cfg.Token = token
}
if username := os.Getenv("EDGE_CONNECT_USERNAME"); username != "" {
cfg.Username = username
}
if password := os.Getenv("EDGE_CONNECT_PASSWORD"); password != "" {
cfg.Password = password
}
if region := os.Getenv("EDGE_CONNECT_DEFAULT_REGION"); region != "" {
cfg.DefaultRegion = region
}
if debug := os.Getenv("EDGE_CONNECT_DEBUG"); debug == "true" || debug == "1" {
cfg.Debug = true
}
return cfg, nil
}
func (c *Config) Validate() error {
if c.BaseURL == "" {
return fmt.Errorf("base_url is required (set EDGE_CONNECT_BASE_URL)")
}
if c.AuthType == "" {
return fmt.Errorf("auth_type is required (set EDGE_CONNECT_AUTH_TYPE to 'token', 'credentials', or 'none')")
}
if c.AuthType != "token" && c.AuthType != "credentials" && c.AuthType != "none" {
return fmt.Errorf("auth_type must be 'token', 'credentials', or 'none'")
}
if c.AuthType == "token" && c.Token == "" {
return fmt.Errorf("token is required when auth_type is 'token' (set EDGE_CONNECT_TOKEN)")
}
if c.AuthType == "credentials" && (c.Username == "" || c.Password == "") {
return fmt.Errorf("username and password are required when auth_type is 'credentials' (set EDGE_CONNECT_USERNAME and EDGE_CONNECT_PASSWORD)")
}
return nil
}

29
go.mod Normal file
View file

@ -0,0 +1,29 @@
module edp.buildth.ing/DevFW/edge-connect-mcp
go 1.25.3
require (
edp.buildth.ing/DevFW-CICD/edge-connect-client/v2 v2.1.2 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/google/jsonschema-go v0.3.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/modelcontextprotocol/go-sdk v1.2.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/cobra v1.10.1 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.28.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

50
go.sum Normal file
View file

@ -0,0 +1,50 @@
edp.buildth.ing/DevFW-CICD/edge-connect-client/v2 v2.1.2 h1:g1iY/8Au4T6UV6cFm8/SQXAAF+DvFcjR6Hb0TqTF064=
edp.buildth.ing/DevFW-CICD/edge-connect-client/v2 v2.1.2/go.mod h1:nPZ4K4BB7eXyeSrcHXvSPkNZbs+XgmxbDJOM4KhbI1A=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/modelcontextprotocol/go-sdk v1.2.0 h1:Y23co09300CEk8iZ/tMxIX1dVmKZkzoSBZOpJwUnc/s=
github.com/modelcontextprotocol/go-sdk v1.2.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
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=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

113
main.go Normal file
View file

@ -0,0 +1,113 @@
package main
import (
"context"
"fmt"
"log"
"github.com/modelcontextprotocol/go-sdk/mcp"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
)
var (
edgeClient *v2.Client
config *Config
)
func main() {
// Load configuration
var err error
config, err = LoadConfig()
if err != nil {
log.Fatalf("Failed to load configuration: %v", err)
}
// Validate configuration
if err := config.Validate(); err != nil {
log.Fatalf("Invalid configuration: %v", err)
}
// Initialize edge-connect client
edgeClient, err = initializeEdgeClient(config)
if err != nil {
log.Fatalf("Failed to initialize edge-connect client: %v", err)
}
// Create MCP server
mcpServer := mcp.NewServer(&mcp.Implementation{
Name: "edge-connect-mcp",
Version: "1.0.0",
}, nil)
// Register all tools
registerTools(mcpServer)
// Start server on stdio
if err := mcpServer.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
log.Fatalf("Server error: %v", err)
}
}
func initializeEdgeClient(cfg *Config) (*v2.Client, error) {
var options []v2.Option
// Add logger if debug enabled
if cfg.Debug {
options = append(options, v2.WithLogger(log.Default()))
}
// Configure retry options
if cfg.RetryMaxRetries > 0 {
retryOpts := v2.RetryOptions{
MaxRetries: cfg.RetryMaxRetries,
InitialDelay: cfg.RetryInitialDelay,
MaxDelay: cfg.RetryMaxDelay,
Multiplier: cfg.RetryMultiplier,
}
options = append(options, v2.WithRetryOptions(retryOpts))
}
// Initialize client with authentication
switch cfg.AuthType {
case "token":
if cfg.Token == "" {
return nil, fmt.Errorf("token is required when auth_type is 'token'")
}
authProvider := v2.NewStaticTokenProvider(cfg.Token)
options = append(options, v2.WithAuthProvider(authProvider))
return v2.NewClient(cfg.BaseURL, options...), nil
case "credentials":
if cfg.Username == "" || cfg.Password == "" {
return nil, fmt.Errorf("username and password are required when auth_type is 'credentials'")
}
return v2.NewClientWithCredentials(cfg.BaseURL, cfg.Username, cfg.Password, options...), nil
case "none":
authProvider := v2.NewNoAuthProvider()
options = append(options, v2.WithAuthProvider(authProvider))
return v2.NewClient(cfg.BaseURL, options...), nil
default:
return nil, fmt.Errorf("invalid auth_type: %s (must be 'token', 'credentials', or 'none')", cfg.AuthType)
}
}
func registerTools(s *mcp.Server) {
// Apps endpoints
registerCreateAppTool(s)
registerShowAppTool(s)
registerListAppsTool(s)
registerUpdateAppTool(s)
registerDeleteAppTool(s)
// App Instance endpoints
registerCreateAppInstanceTool(s)
registerShowAppInstanceTool(s)
registerListAppInstancesTool(s)
registerUpdateAppInstanceTool(s)
registerRefreshAppInstanceTool(s)
registerDeleteAppInstanceTool(s)
log.Printf("Registered 11 tools")
}

658
tools.go Normal file
View file

@ -0,0 +1,658 @@
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/modelcontextprotocol/go-sdk/mcp"
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/v2/sdk/edgeconnect/v2"
)
// Apps Tool Registrations
func registerCreateAppTool(s *mcp.Server) {
type args struct {
Organization string `json:"organization" jsonschema:"required,description=Organization name"`
Name string `json:"name" jsonschema:"required,description=Application name"`
Version string `json:"version" jsonschema:"required,description=Application version (e.g. '1.0.0')"`
Deployment string `json:"deployment" jsonschema:"required,description=Deployment type: 'docker' or 'kubernetes',enum=docker|kubernetes"`
ImageType *string `json:"image_type,omitempty" jsonschema:"description=Image type (default: 'ImageTypeDocker')"`
ImagePath string `json:"image_path" jsonschema:"required,description=Docker registry URL (e.g. 'https://registry-1.docker.io/library/nginx:latest')"`
AccessPorts *string `json:"access_ports,omitempty" jsonschema:"description=Access ports specification (e.g. 'tcp:80')"`
DefaultFlavorName *string `json:"default_flavor_name,omitempty" jsonschema:"description=Default flavor name (e.g. 'EU.small')"`
AllowServerless *bool `json:"allow_serverless,omitempty" jsonschema:"description=Allow serverless deployment"`
Region *string `json:"region,omitempty" jsonschema:"description=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, image path, and region.",
}, 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
}
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,
AccessPorts: accessPorts,
AllowServerless: allowServerless,
}
if a.DefaultFlavorName != nil && *a.DefaultFlavorName != "" {
app.DefaultFlavor = v2.Flavor{Name: *a.DefaultFlavorName}
}
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:"required,description=Organization name"`
Name string `json:"name" jsonschema:"required,description=Application name"`
Version string `json:"version" jsonschema:"required,description=Application version"`
Region *string `json:"region,omitempty" jsonschema:"description=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)
}
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: string(appJSON)}},
}, nil, nil
})
}
func registerListAppsTool(s *mcp.Server) {
type args struct {
Organization *string `json:"organization,omitempty" jsonschema:"description=Filter by organization name (optional)"`
Name *string `json:"name,omitempty" jsonschema:"description=Filter by application name (optional)"`
Version *string `json:"version,omitempty" jsonschema:"description=Filter by version (optional)"`
Region *string `json:"region,omitempty" jsonschema:"description=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. Can filter by organization, name, or version pattern.",
}, func(ctx context.Context, req *mcp.CallToolRequest, a args) (*mcp.CallToolResult, any, error) {
organization := ""
if a.Organization != nil {
organization = *a.Organization
}
name := ""
if a.Name != nil {
name = *a.Name
}
version := ""
if a.Version != nil {
version = *a.Version
}
region := config.DefaultRegion
if a.Region != nil {
region = *a.Region
}
appKey := v2.AppKey{
Organization: organization,
Name: name,
Version: version,
}
apps, err := edgeClient.ShowApps(ctx, appKey, region)
if err != nil {
return nil, nil, fmt.Errorf("failed to list apps: %w", err)
}
appsJSON, err := json.MarshalIndent(apps, "", " ")
if err != nil {
return nil, nil, fmt.Errorf("failed to serialize apps: %w", err)
}
result := fmt.Sprintf("Found %d apps:\n%s", len(apps), string(appsJSON))
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: result}},
}, nil, nil
})
}
func registerUpdateAppTool(s *mcp.Server) {
type args struct {
Organization string `json:"organization" jsonschema:"required,description=Organization name"`
Name string `json:"name" jsonschema:"required,description=Application name"`
Version string `json:"version" jsonschema:"required,description=Application version"`
ImagePath *string `json:"image_path,omitempty" jsonschema:"description=New Docker registry URL (optional)"`
AccessPorts *string `json:"access_ports,omitempty" jsonschema:"description=New access ports specification (optional)"`
DefaultFlavorName *string `json:"default_flavor_name,omitempty" jsonschema:"description=New default flavor name (optional)"`
AllowServerless *bool `json:"allow_serverless,omitempty" jsonschema:"description=New serverless setting (optional)"`
Region *string `json:"region,omitempty" jsonschema:"description=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.",
}, 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.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)
}
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:"required,description=Organization name"`
Name string `json:"name" jsonschema:"required,description=Application name"`
Version string `json:"version" jsonschema:"required,description=Application version"`
Region *string `json:"region,omitempty" jsonschema:"description=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).",
}, 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:"required,description=Organization name"`
InstanceName string `json:"instance_name" jsonschema:"required,description=Instance name"`
CloudletOrg string `json:"cloudlet_org" jsonschema:"required,description=Cloudlet organization name"`
CloudletName string `json:"cloudlet_name" jsonschema:"required,description=Cloudlet name"`
AppOrg string `json:"app_org" jsonschema:"required,description=Application organization name"`
AppName string `json:"app_name" jsonschema:"required,description=Application name"`
AppVersion string `json:"app_version" jsonschema:"required,description=Application version"`
FlavorName *string `json:"flavor_name,omitempty" jsonschema:"description=Flavor name (e.g. 'EU.small')"`
Latitude *float64 `json:"latitude,omitempty" jsonschema:"description=Cloudlet latitude (optional)"`
Longitude *float64 `json:"longitude,omitempty" jsonschema:"description=Cloudlet longitude (optional)"`
Region *string `json:"region,omitempty" jsonschema:"description=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:"required,description=Organization name"`
InstanceName string `json:"instance_name" jsonschema:"required,description=Instance name"`
CloudletOrg string `json:"cloudlet_org" jsonschema:"required,description=Cloudlet organization name"`
CloudletName string `json:"cloudlet_name" jsonschema:"required,description=Cloudlet name"`
AppOrg *string `json:"app_org,omitempty" jsonschema:"description=Application organization name (optional for filtering)"`
AppName *string `json:"app_name,omitempty" jsonschema:"description=Application name (optional for filtering)"`
AppVersion *string `json:"app_version,omitempty" jsonschema:"description=Application version (optional for filtering)"`
Region *string `json:"region,omitempty" jsonschema:"description=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:"description=Filter by organization name (optional)"`
InstanceName *string `json:"instance_name,omitempty" jsonschema:"description=Filter by instance name (optional)"`
CloudletOrg *string `json:"cloudlet_org,omitempty" jsonschema:"description=Filter by cloudlet organization (optional)"`
CloudletName *string `json:"cloudlet_name,omitempty" jsonschema:"description=Filter by cloudlet name (optional)"`
AppOrg *string `json:"app_org,omitempty" jsonschema:"description=Filter by application organization (optional)"`
AppName *string `json:"app_name,omitempty" jsonschema:"description=Filter by application name (optional)"`
AppVersion *string `json:"app_version,omitempty" jsonschema:"description=Filter by application version (optional)"`
Region *string `json:"region,omitempty" jsonschema:"description=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.",
}, func(ctx context.Context, req *mcp.CallToolRequest, a args) (*mcp.CallToolResult, any, error) {
organization := ""
if a.Organization != nil {
organization = *a.Organization
}
instanceName := ""
if a.InstanceName != nil {
instanceName = *a.InstanceName
}
cloudletOrg := ""
if a.CloudletOrg != nil {
cloudletOrg = *a.CloudletOrg
}
cloudletName := ""
if a.CloudletName != nil {
cloudletName = *a.CloudletName
}
appOrg := ""
if a.AppOrg != nil {
appOrg = *a.AppOrg
}
appName := ""
if a.AppName != nil {
appName = *a.AppName
}
appVersion := ""
if a.AppVersion != nil {
appVersion = *a.AppVersion
}
region := config.DefaultRegion
if a.Region != nil {
region = *a.Region
}
appInstKey := v2.AppInstanceKey{
Organization: organization,
Name: instanceName,
CloudletKey: v2.CloudletKey{
Organization: cloudletOrg,
Name: cloudletName,
},
}
appKey := v2.AppKey{
Organization: appOrg,
Name: appName,
Version: appVersion,
}
appInsts, err := edgeClient.ShowAppInstances(ctx, appInstKey, appKey, region)
if err != nil {
return nil, nil, fmt.Errorf("failed to list app instances: %w", err)
}
appInstsJSON, err := json.MarshalIndent(appInsts, "", " ")
if err != nil {
return nil, nil, fmt.Errorf("failed to serialize app instances: %w", err)
}
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
})
}
func registerUpdateAppInstanceTool(s *mcp.Server) {
type args struct {
Organization string `json:"organization" jsonschema:"required,description=Organization name"`
InstanceName string `json:"instance_name" jsonschema:"required,description=Instance name"`
CloudletOrg string `json:"cloudlet_org" jsonschema:"required,description=Cloudlet organization name"`
CloudletName string `json:"cloudlet_name" jsonschema:"required,description=Cloudlet name"`
FlavorName *string `json:"flavor_name,omitempty" jsonschema:"description=New flavor name (optional)"`
PowerState *string `json:"power_state,omitempty" jsonschema:"description=New power state (optional)"`
Region *string `json:"region,omitempty" jsonschema:"description=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:"required,description=Organization name"`
InstanceName string `json:"instance_name" jsonschema:"required,description=Instance name"`
CloudletOrg string `json:"cloudlet_org" jsonschema:"required,description=Cloudlet organization name"`
CloudletName string `json:"cloudlet_name" jsonschema:"required,description=Cloudlet name"`
Region *string `json:"region,omitempty" jsonschema:"description=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:"required,description=Organization name"`
InstanceName string `json:"instance_name" jsonschema:"required,description=Instance name"`
CloudletOrg string `json:"cloudlet_org" jsonschema:"required,description=Cloudlet organization name"`
CloudletName string `json:"cloudlet_name" jsonschema:"required,description=Cloudlet name"`
Region *string `json:"region,omitempty" jsonschema:"description=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
})
}

39
utils.go Normal file
View file

@ -0,0 +1,39 @@
package main
// Helper functions for extracting parameters from map[string]interface{}
func getStringParam(params map[string]interface{}, key string, defaultValue string) string {
if val, ok := params[key].(string); ok {
return val
}
return defaultValue
}
func getBoolParam(params map[string]interface{}, key string, defaultValue bool) bool {
if val, ok := params[key].(bool); ok {
return val
}
return defaultValue
}
func getFloatParam(params map[string]interface{}, key string, defaultValue float64) float64 {
if val, ok := params[key].(float64); ok {
return val
}
// Also try to convert from int
if val, ok := params[key].(int); ok {
return float64(val)
}
return defaultValue
}
func getIntParam(params map[string]interface{}, key string, defaultValue int) int {
if val, ok := params[key].(int); ok {
return val
}
// Also try to convert from float64
if val, ok := params[key].(float64); ok {
return int(val)
}
return defaultValue
}