introduced stdio mcp server for edge connect
This commit is contained in:
commit
5f3214d1f7
10 changed files with 1260 additions and 0 deletions
18
.claude/CLAUDE.md
Normal file
18
.claude/CLAUDE.md
Normal 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
14
.env.example
Normal 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
42
.gitignore
vendored
Normal 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
198
README.md
Normal 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
99
config.go
Normal 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
29
go.mod
Normal 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
50
go.sum
Normal 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
113
main.go
Normal 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
658
tools.go
Normal 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
39
utils.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue