feat: add Portainer Edge MCP server
- Implement comprehensive edge computing functionality - Add edge environment management (list, get, status, generate keys) - Add edge stack operations (list, get, create, update, delete) - Add edge group management (list, get, create, update, delete) - Add edge job scheduling (list, get, create, delete) - Add edge settings configuration (get, update) - Create test scripts for edge API validation - Add comprehensive README documentation for edge server - Include nginx stack creation script from earlier testing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d7055a912e
commit
7a1abbe243
312
README_EDGE.md
Normal file
312
README_EDGE.md
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
# Portainer Edge MCP Server
|
||||||
|
|
||||||
|
This MCP server provides edge computing functionality through Portainer's API, managing edge environments, edge stacks, edge groups, and edge jobs.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Edge Environment Management**: List and monitor edge environments
|
||||||
|
- **Edge Stack Deployment**: Deploy and manage stacks across edge devices
|
||||||
|
- **Edge Group Organization**: Create and manage groups of edge endpoints
|
||||||
|
- **Edge Job Scheduling**: Schedule and run jobs on edge devices
|
||||||
|
- **Edge Settings Configuration**: Configure global edge settings
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Ensure you have the Portainer MCP servers repository:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/yourusername/portainer-mcp.git
|
||||||
|
cd portainer-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Configure environment variables:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env with your Portainer URL and API key
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Make the server executable:
|
||||||
|
```bash
|
||||||
|
chmod +x portainer_edge_server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Add to your Claude Desktop configuration:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"portainer-edge": {
|
||||||
|
"command": "python",
|
||||||
|
"args": ["/path/to/portainer-mcp/portainer_edge_server.py"],
|
||||||
|
"env": {
|
||||||
|
"PORTAINER_URL": "https://your-portainer-instance.com",
|
||||||
|
"PORTAINER_API_KEY": "your-api-key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
|
||||||
|
### Edge Environment Management
|
||||||
|
|
||||||
|
#### list_edge_environments
|
||||||
|
List all edge environments.
|
||||||
|
- **Parameters**:
|
||||||
|
- `status` (optional): Filter by status - "connected" or "disconnected"
|
||||||
|
|
||||||
|
#### get_edge_environment
|
||||||
|
Get details of a specific edge environment.
|
||||||
|
- **Parameters**:
|
||||||
|
- `environment_id` (required): Environment ID
|
||||||
|
|
||||||
|
#### get_edge_status
|
||||||
|
Get edge environment status and check-in information.
|
||||||
|
- **Parameters**:
|
||||||
|
- `environment_id` (required): Environment ID
|
||||||
|
|
||||||
|
#### generate_edge_key
|
||||||
|
Generate an edge key for adding new edge agents.
|
||||||
|
- **Parameters**:
|
||||||
|
- `name` (required): Environment name
|
||||||
|
- `group_ids` (optional): List of edge group IDs
|
||||||
|
|
||||||
|
### Edge Stack Management
|
||||||
|
|
||||||
|
#### list_edge_stacks
|
||||||
|
List all edge stacks.
|
||||||
|
|
||||||
|
#### get_edge_stack
|
||||||
|
Get details of a specific edge stack.
|
||||||
|
- **Parameters**:
|
||||||
|
- `edge_stack_id` (required): Edge Stack ID
|
||||||
|
|
||||||
|
#### create_edge_stack
|
||||||
|
Create a new edge stack.
|
||||||
|
- **Parameters**:
|
||||||
|
- `name` (required): Stack name
|
||||||
|
- `stack_content` (required): Stack file content (Docker Compose)
|
||||||
|
- `edge_groups` (required): List of edge group IDs
|
||||||
|
- `deploy_type` (optional): Deployment type (0: compose, 1: kubernetes)
|
||||||
|
- `edge_id_list` (optional): Specific edge IDs for deployment
|
||||||
|
- `registries` (optional): List of registry IDs
|
||||||
|
|
||||||
|
#### update_edge_stack
|
||||||
|
Update an existing edge stack.
|
||||||
|
- **Parameters**:
|
||||||
|
- `edge_stack_id` (required): Edge Stack ID
|
||||||
|
- `stack_content` (optional): Updated stack content
|
||||||
|
- `edge_groups` (optional): Updated edge group IDs
|
||||||
|
- `deploy_type` (optional): Updated deployment type
|
||||||
|
|
||||||
|
#### delete_edge_stack
|
||||||
|
Delete an edge stack.
|
||||||
|
- **Parameters**:
|
||||||
|
- `edge_stack_id` (required): Edge Stack ID
|
||||||
|
|
||||||
|
### Edge Group Management
|
||||||
|
|
||||||
|
#### list_edge_groups
|
||||||
|
List all edge groups.
|
||||||
|
|
||||||
|
#### get_edge_group
|
||||||
|
Get details of a specific edge group.
|
||||||
|
- **Parameters**:
|
||||||
|
- `edge_group_id` (required): Edge Group ID
|
||||||
|
|
||||||
|
#### create_edge_group
|
||||||
|
Create a new edge group.
|
||||||
|
- **Parameters**:
|
||||||
|
- `name` (required): Group name
|
||||||
|
- `dynamic` (optional): Enable dynamic membership (default: false)
|
||||||
|
- `tag_ids` (optional): Tag IDs for dynamic groups
|
||||||
|
- `endpoints` (optional): Endpoint IDs for static groups
|
||||||
|
|
||||||
|
#### update_edge_group
|
||||||
|
Update an existing edge group.
|
||||||
|
- **Parameters**:
|
||||||
|
- `edge_group_id` (required): Edge Group ID
|
||||||
|
- `name` (optional): Updated name
|
||||||
|
- `dynamic` (optional): Update dynamic membership
|
||||||
|
- `tag_ids` (optional): Updated tag IDs
|
||||||
|
- `endpoints` (optional): Updated endpoint IDs
|
||||||
|
|
||||||
|
#### delete_edge_group
|
||||||
|
Delete an edge group.
|
||||||
|
- **Parameters**:
|
||||||
|
- `edge_group_id` (required): Edge Group ID
|
||||||
|
|
||||||
|
### Edge Job Management
|
||||||
|
|
||||||
|
#### list_edge_jobs
|
||||||
|
List all edge jobs.
|
||||||
|
|
||||||
|
#### get_edge_job
|
||||||
|
Get details of a specific edge job.
|
||||||
|
- **Parameters**:
|
||||||
|
- `edge_job_id` (required): Edge Job ID
|
||||||
|
|
||||||
|
#### create_edge_job
|
||||||
|
Create a new edge job.
|
||||||
|
- **Parameters**:
|
||||||
|
- `name` (required): Job name
|
||||||
|
- `edge_groups` (required): Target edge group IDs
|
||||||
|
- `script_content` (required): Script content to execute
|
||||||
|
- `recurring` (optional): Enable recurring execution
|
||||||
|
- `cron_expression` (optional): Cron expression for scheduling
|
||||||
|
|
||||||
|
#### delete_edge_job
|
||||||
|
Delete an edge job.
|
||||||
|
- **Parameters**:
|
||||||
|
- `edge_job_id` (required): Edge Job ID
|
||||||
|
|
||||||
|
### Edge Settings
|
||||||
|
|
||||||
|
#### get_edge_settings
|
||||||
|
Get global edge settings.
|
||||||
|
|
||||||
|
#### update_edge_settings
|
||||||
|
Update global edge settings.
|
||||||
|
- **Parameters**:
|
||||||
|
- `check_in_interval` (optional): Check-in interval in seconds
|
||||||
|
- `command_interval` (optional): Command interval in seconds
|
||||||
|
- `ping_interval` (optional): Ping interval in seconds
|
||||||
|
- `snapshot_interval` (optional): Snapshot interval in seconds
|
||||||
|
- `tunnel_server_address` (optional): Tunnel server address
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### List Edge Environments
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await use_mcp_tool("portainer-edge", "list_edge_environments", {
|
||||||
|
status: "connected"
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create Edge Stack
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await use_mcp_tool("portainer-edge", "create_edge_stack", {
|
||||||
|
name: "nginx-edge",
|
||||||
|
stack_content: `version: '3'
|
||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
ports:
|
||||||
|
- "80:80"`,
|
||||||
|
edge_groups: ["1", "2"]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create Edge Group
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Static group with specific endpoints
|
||||||
|
await use_mcp_tool("portainer-edge", "create_edge_group", {
|
||||||
|
name: "production-edge",
|
||||||
|
dynamic: false,
|
||||||
|
endpoints: ["10", "11", "12"]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dynamic group based on tags
|
||||||
|
await use_mcp_tool("portainer-edge", "create_edge_group", {
|
||||||
|
name: "tagged-devices",
|
||||||
|
dynamic: true,
|
||||||
|
tag_ids: ["1", "3"]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schedule Edge Job
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await use_mcp_tool("portainer-edge", "create_edge_job", {
|
||||||
|
name: "system-update",
|
||||||
|
edge_groups: ["1"],
|
||||||
|
script_content: "apt update && apt upgrade -y",
|
||||||
|
recurring: true,
|
||||||
|
cron_expression: "0 2 * * *" // Daily at 2 AM
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Edge Computing Concepts
|
||||||
|
|
||||||
|
### Edge Environments
|
||||||
|
Edge environments are remote Docker or Kubernetes environments that connect to Portainer via the Edge Agent. They can be:
|
||||||
|
- **Connected**: Currently connected and checking in
|
||||||
|
- **Disconnected**: Not currently reachable
|
||||||
|
|
||||||
|
### Edge Stacks
|
||||||
|
Edge stacks are Docker Compose or Kubernetes deployments that can be deployed to multiple edge environments simultaneously through edge groups.
|
||||||
|
|
||||||
|
### Edge Groups
|
||||||
|
Edge groups organize edge environments for bulk operations:
|
||||||
|
- **Static Groups**: Manually selected endpoints
|
||||||
|
- **Dynamic Groups**: Automatically populated based on tags
|
||||||
|
|
||||||
|
### Edge Jobs
|
||||||
|
Edge jobs execute scripts on edge devices:
|
||||||
|
- **One-time Jobs**: Execute once immediately
|
||||||
|
- **Recurring Jobs**: Execute on a schedule (cron)
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Use the provided test script to verify edge functionality:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python test_edge_server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This will test:
|
||||||
|
- Listing edge environments
|
||||||
|
- Creating and managing edge groups
|
||||||
|
- Listing edge stacks
|
||||||
|
- Viewing edge jobs
|
||||||
|
- Checking edge settings
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Group Organization**: Use edge groups to organize devices by location, purpose, or capability
|
||||||
|
2. **Stack Templates**: Create reusable stack templates for common deployments
|
||||||
|
3. **Job Scheduling**: Use recurring jobs for maintenance tasks
|
||||||
|
4. **Monitoring**: Regularly check edge environment status
|
||||||
|
5. **Security**: Use proper authentication for edge agents
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- Edge agents use unique keys for authentication
|
||||||
|
- Communication is encrypted between edge agents and Portainer
|
||||||
|
- Edge jobs execute with the permissions of the edge agent
|
||||||
|
- Limit edge job permissions appropriately
|
||||||
|
- Regularly rotate edge keys
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Edge Environment Not Connecting
|
||||||
|
- Check network connectivity from edge device
|
||||||
|
- Verify edge key is correct
|
||||||
|
- Check firewall rules
|
||||||
|
- Review edge agent logs
|
||||||
|
|
||||||
|
### Stack Deployment Failures
|
||||||
|
- Verify stack syntax
|
||||||
|
- Check image availability on edge devices
|
||||||
|
- Review resource constraints
|
||||||
|
- Check edge agent permissions
|
||||||
|
|
||||||
|
### Edge Group Issues
|
||||||
|
- Verify tag configuration for dynamic groups
|
||||||
|
- Check endpoint assignments for static groups
|
||||||
|
- Review group membership rules
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.8+
|
||||||
|
- Portainer Business Edition 2.19+ (for full edge features)
|
||||||
|
- Valid Portainer API token
|
||||||
|
- Edge agents deployed on target devices
|
131
create_nginx_stack.py
Normal file
131
create_nginx_stack.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Create nginx02 stack from Git repository"""
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
PORTAINER_URL = "https://partner.portainer.live"
|
||||||
|
PORTAINER_API_KEY = "ptr_uMqreULEo44qvuszgG8oZWdjkDx3K9HBXSmjd+F/vDE="
|
||||||
|
|
||||||
|
# Stack configuration
|
||||||
|
STACK_NAME = "nginx02"
|
||||||
|
ENVIRONMENT_ID = 6 # docker03
|
||||||
|
REPOSITORY_URL = "https://git.oe74.net/adelorenzo/portainer-yaml"
|
||||||
|
REPOSITORY_REF = "main" # or master
|
||||||
|
COMPOSE_PATH = "nginx-cmpose.yaml"
|
||||||
|
GIT_USERNAME = "adelorenzo"
|
||||||
|
GIT_PASSWORD = "dimi2014"
|
||||||
|
|
||||||
|
def create_stack_from_git():
|
||||||
|
"""Create a stack from Git repository"""
|
||||||
|
|
||||||
|
# Build request data
|
||||||
|
data = {
|
||||||
|
"Name": STACK_NAME,
|
||||||
|
"EndpointId": ENVIRONMENT_ID,
|
||||||
|
"GitConfig": {
|
||||||
|
"URL": REPOSITORY_URL,
|
||||||
|
"ReferenceName": REPOSITORY_REF,
|
||||||
|
"ComposeFilePathInRepository": COMPOSE_PATH,
|
||||||
|
"Authentication": {
|
||||||
|
"Username": GIT_USERNAME,
|
||||||
|
"Password": GIT_PASSWORD
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Headers
|
||||||
|
headers = {
|
||||||
|
"X-API-Key": PORTAINER_API_KEY,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
# API endpoint
|
||||||
|
url = f"{PORTAINER_URL}/api/stacks"
|
||||||
|
|
||||||
|
print(f"Creating stack '{STACK_NAME}' from Git repository...")
|
||||||
|
print(f"Repository: {REPOSITORY_URL}")
|
||||||
|
print(f"Compose file: {COMPOSE_PATH}")
|
||||||
|
print(f"Environment ID: {ENVIRONMENT_ID}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(url, json=data, headers=headers)
|
||||||
|
|
||||||
|
if response.status_code == 200 or response.status_code == 201:
|
||||||
|
result = response.json()
|
||||||
|
print(f"\n✅ Stack created successfully!")
|
||||||
|
print(f"Stack ID: {result['Id']}")
|
||||||
|
print(f"Stack Name: {result['Name']}")
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
print(f"\n❌ Error creating stack: {response.status_code}")
|
||||||
|
print(f"Response: {response.text}")
|
||||||
|
|
||||||
|
# Try to parse error message
|
||||||
|
try:
|
||||||
|
error_data = response.json()
|
||||||
|
if "message" in error_data:
|
||||||
|
print(f"Error message: {error_data['message']}")
|
||||||
|
elif "details" in error_data:
|
||||||
|
print(f"Error details: {error_data['details']}")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ Exception occurred: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def list_existing_stacks():
|
||||||
|
"""List existing stacks to check for references"""
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"X-API-Key": PORTAINER_API_KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
url = f"{PORTAINER_URL}/api/stacks"
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
if response.status_code == 200:
|
||||||
|
stacks = response.json()
|
||||||
|
print("\n📚 Existing stacks:")
|
||||||
|
for stack in stacks:
|
||||||
|
if stack.get("EndpointId") == ENVIRONMENT_ID:
|
||||||
|
print(f" - {stack['Name']} (ID: {stack['Id']})")
|
||||||
|
if stack.get("GitConfig"):
|
||||||
|
print(f" Git: {stack['GitConfig']['URL']}")
|
||||||
|
print(f" Path: {stack['GitConfig'].get('ComposeFilePathInRepository', 'N/A')}")
|
||||||
|
return stacks
|
||||||
|
else:
|
||||||
|
print(f"Error listing stacks: {response.status_code}")
|
||||||
|
return []
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Exception listing stacks: {str(e)}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# First, list existing stacks
|
||||||
|
print("Checking existing stacks on environment docker03...")
|
||||||
|
existing_stacks = list_existing_stacks()
|
||||||
|
|
||||||
|
# Check if stack already exists
|
||||||
|
stack_exists = any(s['Name'] == STACK_NAME and s.get('EndpointId') == ENVIRONMENT_ID for s in existing_stacks)
|
||||||
|
|
||||||
|
if stack_exists:
|
||||||
|
print(f"\n⚠️ Stack '{STACK_NAME}' already exists on this environment!")
|
||||||
|
response = input("Do you want to continue anyway? (y/n): ")
|
||||||
|
if response.lower() != 'y':
|
||||||
|
print("Aborting...")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# Create the stack
|
||||||
|
print("\n" + "="*50)
|
||||||
|
result = create_stack_from_git()
|
||||||
|
|
||||||
|
if result:
|
||||||
|
print("\n🎉 Stack deployment completed!")
|
||||||
|
else:
|
||||||
|
print("\n😞 Stack deployment failed!")
|
1019
portainer_edge_server.py
Normal file
1019
portainer_edge_server.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -2,12 +2,12 @@ Metadata-Version: 2.4
|
|||||||
Name: portainer-core-mcp
|
Name: portainer-core-mcp
|
||||||
Version: 0.1.0
|
Version: 0.1.0
|
||||||
Summary: Portainer Core MCP Server - Authentication and User Management
|
Summary: Portainer Core MCP Server - Authentication and User Management
|
||||||
Author-email: Your Name <your.email@example.com>
|
Author-email: Portainer MCP Team <support@portainer.io>
|
||||||
License: MIT
|
License: MIT
|
||||||
Project-URL: Homepage, https://github.com/yourusername/portainer-core-mcp
|
Project-URL: Homepage, https://github.com/portainer/portainer-mcp-core
|
||||||
Project-URL: Documentation, https://github.com/yourusername/portainer-core-mcp#readme
|
Project-URL: Documentation, https://github.com/portainer/portainer-mcp-core#readme
|
||||||
Project-URL: Repository, https://github.com/yourusername/portainer-core-mcp
|
Project-URL: Repository, https://github.com/portainer/portainer-mcp-core
|
||||||
Project-URL: Issues, https://github.com/yourusername/portainer-core-mcp/issues
|
Project-URL: Issues, https://github.com/portainer/portainer-mcp-core/issues
|
||||||
Classifier: Development Status :: 3 - Alpha
|
Classifier: Development Status :: 3 - Alpha
|
||||||
Classifier: Intended Audience :: Developers
|
Classifier: Intended Audience :: Developers
|
||||||
Classifier: License :: OSI Approved :: MIT License
|
Classifier: License :: OSI Approved :: MIT License
|
||||||
@ -41,62 +41,145 @@ Requires-Dist: pre-commit>=3.0.0; extra == "dev"
|
|||||||
|
|
||||||
# Portainer Core MCP Server
|
# Portainer Core MCP Server
|
||||||
|
|
||||||
A Model Context Protocol (MCP) server for Portainer Business Edition authentication and user management.
|
A Model Context Protocol (MCP) server that provides authentication and user management functionality for Portainer Business Edition.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Authentication & Session Management**: JWT token handling and user authentication
|
- **Authentication**: JWT token-based authentication with Portainer API
|
||||||
- **User Management**: Create, read, update, and delete users
|
- **User Management**: Complete CRUD operations for users
|
||||||
- **Settings Management**: Retrieve and update Portainer settings
|
- **Settings Management**: Portainer instance configuration
|
||||||
- **Secure Token Handling**: Automatic token refresh and secure storage
|
- **Health Monitoring**: Server and service health checks
|
||||||
- **Error Handling**: Comprehensive error handling with retry logic
|
- **Fault Tolerance**: Circuit breaker pattern with automatic recovery
|
||||||
- **Circuit Breaker**: Fault tolerance for external API calls
|
- **Structured Logging**: JSON-formatted logs with correlation IDs
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.8+
|
||||||
|
- Portainer Business Edition instance
|
||||||
|
- Valid Portainer API key
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
### Using pip
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install portainer-core-mcp
|
pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using uv (recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using uvx (run without installing)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# No installation needed - runs directly
|
||||||
|
uvx --from . portainer-core-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using npm/npx
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g portainer-core-mcp
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Set the following environment variables:
|
### Environment Variables
|
||||||
|
|
||||||
|
Create a `.env` file or set environment variables:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Required
|
||||||
PORTAINER_URL=https://your-portainer-instance.com
|
PORTAINER_URL=https://your-portainer-instance.com
|
||||||
PORTAINER_API_KEY=your-api-token # Optional, for API key authentication
|
PORTAINER_API_KEY=your-api-key-here
|
||||||
PORTAINER_USERNAME=admin # For username/password authentication
|
|
||||||
PORTAINER_PASSWORD=your-password # For username/password authentication
|
# Optional
|
||||||
|
HTTP_TIMEOUT=30
|
||||||
|
MAX_RETRIES=3
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
DEBUG=false
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Generate API Key
|
||||||
|
|
||||||
|
1. Log in to your Portainer instance
|
||||||
|
2. Go to **User Settings** > **API Tokens**
|
||||||
|
3. Click **Add API Token**
|
||||||
|
4. Copy the generated token
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### As MCP Server
|
### Start the Server
|
||||||
|
|
||||||
|
#### Using Python
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
portainer-core-mcp
|
python run_server.py
|
||||||
```
|
```
|
||||||
|
|
||||||
### Programmatic Usage
|
#### Using uv
|
||||||
|
|
||||||
```python
|
```bash
|
||||||
from portainer_core.server import PortainerCoreMCPServer
|
uv run python run_server.py
|
||||||
|
|
||||||
server = PortainerCoreMCPServer()
|
|
||||||
# Use server instance
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Available MCP Tools
|
#### Using uvx
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvx --from . portainer-core-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Using npm/npx
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx portainer-core-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy example environment file
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Edit configuration
|
||||||
|
nano .env
|
||||||
|
|
||||||
|
# Start server (choose your preferred method)
|
||||||
|
python run_server.py
|
||||||
|
# OR
|
||||||
|
uvx --from . portainer-core-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Tools
|
||||||
|
|
||||||
|
The MCP server provides the following tools:
|
||||||
|
|
||||||
|
### Authentication
|
||||||
- `authenticate` - Login with username/password
|
- `authenticate` - Login with username/password
|
||||||
- `generate_token` - Generate API token
|
- `generate_token` - Generate API tokens
|
||||||
- `get_current_user` - Get authenticated user info
|
- `get_current_user` - Get current user info
|
||||||
|
|
||||||
|
### User Management
|
||||||
- `list_users` - List all users
|
- `list_users` - List all users
|
||||||
- `create_user` - Create new user
|
- `create_user` - Create new user
|
||||||
- `update_user` - Update user details
|
- `update_user` - Update user details
|
||||||
- `delete_user` - Delete user
|
- `delete_user` - Delete user
|
||||||
|
|
||||||
|
### Settings
|
||||||
- `get_settings` - Get Portainer settings
|
- `get_settings` - Get Portainer settings
|
||||||
- `update_settings` - Update settings
|
- `update_settings` - Update configuration
|
||||||
|
|
||||||
|
### Health
|
||||||
|
- `health_check` - Server health status
|
||||||
|
|
||||||
|
## Available Resources
|
||||||
|
|
||||||
|
- `portainer://users` - User management data
|
||||||
|
- `portainer://settings` - Configuration settings
|
||||||
|
- `portainer://health` - Server health status
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
@ -4,11 +4,18 @@ src/portainer_core/__init__.py
|
|||||||
src/portainer_core/config.py
|
src/portainer_core/config.py
|
||||||
src/portainer_core/server.py
|
src/portainer_core/server.py
|
||||||
src/portainer_core/models/__init__.py
|
src/portainer_core/models/__init__.py
|
||||||
|
src/portainer_core/models/auth.py
|
||||||
|
src/portainer_core/models/settings.py
|
||||||
|
src/portainer_core/models/users.py
|
||||||
src/portainer_core/services/__init__.py
|
src/portainer_core/services/__init__.py
|
||||||
|
src/portainer_core/services/auth.py
|
||||||
src/portainer_core/services/base.py
|
src/portainer_core/services/base.py
|
||||||
|
src/portainer_core/services/settings.py
|
||||||
|
src/portainer_core/services/users.py
|
||||||
src/portainer_core/utils/__init__.py
|
src/portainer_core/utils/__init__.py
|
||||||
src/portainer_core/utils/errors.py
|
src/portainer_core/utils/errors.py
|
||||||
src/portainer_core/utils/logging.py
|
src/portainer_core/utils/logging.py
|
||||||
|
src/portainer_core/utils/tokens.py
|
||||||
src/portainer_core_mcp.egg-info/PKG-INFO
|
src/portainer_core_mcp.egg-info/PKG-INFO
|
||||||
src/portainer_core_mcp.egg-info/SOURCES.txt
|
src/portainer_core_mcp.egg-info/SOURCES.txt
|
||||||
src/portainer_core_mcp.egg-info/dependency_links.txt
|
src/portainer_core_mcp.egg-info/dependency_links.txt
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
[console_scripts]
|
[console_scripts]
|
||||||
portainer-core-mcp = portainer_core.server:main
|
portainer-core-mcp = portainer_core.server:main_sync
|
||||||
|
44
test_edge_api.sh
Executable file
44
test_edge_api.sh
Executable file
@ -0,0 +1,44 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Test Portainer Edge API endpoints
|
||||||
|
|
||||||
|
echo "🚀 Testing Portainer Edge API"
|
||||||
|
echo "URL: $PORTAINER_URL"
|
||||||
|
echo "API Key: ***${PORTAINER_API_KEY: -4}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test edge environments
|
||||||
|
echo "🌐 Testing Edge Environments..."
|
||||||
|
curl -s -H "X-API-Key: $PORTAINER_API_KEY" \
|
||||||
|
"$PORTAINER_URL/api/endpoints?types=4" | jq '.[] | select(.Type == 4) | {name: .Name, id: .Id, status: .Status}' 2>/dev/null || echo "No edge environments found or jq not installed"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test edge groups
|
||||||
|
echo "👥 Testing Edge Groups..."
|
||||||
|
curl -s -H "X-API-Key: $PORTAINER_API_KEY" \
|
||||||
|
"$PORTAINER_URL/api/edge_groups" | jq '.[0:3] | .[] | {name: .Name, id: .Id, dynamic: .Dynamic}' 2>/dev/null || echo "No edge groups found or jq not installed"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test edge stacks
|
||||||
|
echo "📚 Testing Edge Stacks..."
|
||||||
|
curl -s -H "X-API-Key: $PORTAINER_API_KEY" \
|
||||||
|
"$PORTAINER_URL/api/edge_stacks" | jq '.[0:3] | .[] | {name: .Name, id: .Id, groups: .EdgeGroups}' 2>/dev/null || echo "No edge stacks found or jq not installed"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test edge jobs
|
||||||
|
echo "💼 Testing Edge Jobs..."
|
||||||
|
curl -s -H "X-API-Key: $PORTAINER_API_KEY" \
|
||||||
|
"$PORTAINER_URL/api/edge_jobs" | jq '.[0:3] | .[] | {name: .Name, id: .Id, recurring: .Recurring}' 2>/dev/null || echo "No edge jobs found or jq not installed"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test edge settings
|
||||||
|
echo "⚙️ Testing Edge Settings..."
|
||||||
|
curl -s -H "X-API-Key: $PORTAINER_API_KEY" \
|
||||||
|
"$PORTAINER_URL/api/settings" | jq '.Edge | {checkin: .CheckinInterval, command: .CommandInterval, ping: .PingInterval}' 2>/dev/null || echo "Failed to get edge settings or jq not installed"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Edge API test completed!"
|
163
test_edge_server.py
Executable file
163
test_edge_server.py
Executable file
@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for Portainer Edge MCP Server
|
||||||
|
Tests edge environments, stacks, groups, and jobs functionality
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
PORTAINER_URL = os.getenv("PORTAINER_URL", "").rstrip("/")
|
||||||
|
PORTAINER_API_KEY = os.getenv("PORTAINER_API_KEY", "")
|
||||||
|
|
||||||
|
async def make_request(method: str, endpoint: str, json_data=None, params=None):
|
||||||
|
"""Make a request to Portainer API"""
|
||||||
|
url = f"{PORTAINER_URL}{endpoint}"
|
||||||
|
headers = {"X-API-Key": PORTAINER_API_KEY}
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.request(
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
json=json_data,
|
||||||
|
params=params,
|
||||||
|
headers=headers
|
||||||
|
) as response:
|
||||||
|
text = await response.text()
|
||||||
|
if response.status >= 400:
|
||||||
|
print(f"❌ Error {response.status}: {text}")
|
||||||
|
return None
|
||||||
|
return json.loads(text) if text else {}
|
||||||
|
|
||||||
|
async def test_edge_environments():
|
||||||
|
"""Test edge environment operations"""
|
||||||
|
print("\n🌐 Testing Edge Environments...")
|
||||||
|
|
||||||
|
# List all endpoints/environments
|
||||||
|
endpoints = await make_request("GET", "/api/endpoints")
|
||||||
|
if endpoints:
|
||||||
|
edge_envs = [e for e in endpoints if e.get("Type") == 4] # Type 4 is Edge
|
||||||
|
print(f"✅ Found {len(edge_envs)} edge environments")
|
||||||
|
|
||||||
|
if edge_envs:
|
||||||
|
# Get details of first edge environment
|
||||||
|
env = edge_envs[0]
|
||||||
|
print(f" • {env['Name']} (ID: {env['Id']})")
|
||||||
|
print(f" Status: {env.get('Status', 'Unknown')}")
|
||||||
|
print(f" Edge ID: {env.get('EdgeID', 'N/A')}")
|
||||||
|
|
||||||
|
# Get edge status
|
||||||
|
status = await make_request("GET", f"/api/endpoints/{env['Id']}/edge/status")
|
||||||
|
if status:
|
||||||
|
print(f" Check-in: {status.get('CheckinTime', 'Never')}")
|
||||||
|
else:
|
||||||
|
print("❌ Failed to list environments")
|
||||||
|
|
||||||
|
async def test_edge_groups():
|
||||||
|
"""Test edge group operations"""
|
||||||
|
print("\n👥 Testing Edge Groups...")
|
||||||
|
|
||||||
|
# List edge groups
|
||||||
|
groups = await make_request("GET", "/api/edge_groups")
|
||||||
|
if groups:
|
||||||
|
print(f"✅ Found {len(groups)} edge groups")
|
||||||
|
for group in groups[:3]: # Show first 3
|
||||||
|
print(f" • {group['Name']} (ID: {group['Id']})")
|
||||||
|
print(f" Dynamic: {'Yes' if group.get('Dynamic') else 'No'}")
|
||||||
|
print(f" Endpoints: {len(group.get('Endpoints', []))}")
|
||||||
|
else:
|
||||||
|
print("❌ Failed to list edge groups")
|
||||||
|
|
||||||
|
# Try to create a test edge group
|
||||||
|
print("\n📝 Creating test edge group...")
|
||||||
|
test_group_data = {
|
||||||
|
"Name": "test-edge-group",
|
||||||
|
"Dynamic": False,
|
||||||
|
"TagIds": []
|
||||||
|
}
|
||||||
|
|
||||||
|
new_group = await make_request("POST", "/api/edge_groups", json_data=test_group_data)
|
||||||
|
if new_group:
|
||||||
|
print(f"✅ Created edge group: {new_group['Name']} (ID: {new_group['Id']})")
|
||||||
|
|
||||||
|
# Clean up - delete the test group
|
||||||
|
await make_request("DELETE", f"/api/edge_groups/{new_group['Id']}")
|
||||||
|
print("🗑️ Cleaned up test edge group")
|
||||||
|
else:
|
||||||
|
print("❌ Failed to create edge group")
|
||||||
|
|
||||||
|
async def test_edge_stacks():
|
||||||
|
"""Test edge stack operations"""
|
||||||
|
print("\n📚 Testing Edge Stacks...")
|
||||||
|
|
||||||
|
# List edge stacks
|
||||||
|
stacks = await make_request("GET", "/api/edge_stacks")
|
||||||
|
if stacks:
|
||||||
|
print(f"✅ Found {len(stacks)} edge stacks")
|
||||||
|
for stack in stacks[:3]: # Show first 3
|
||||||
|
print(f" • {stack['Name']} (ID: {stack['Id']})")
|
||||||
|
print(f" Type: {stack.get('StackType', 'Unknown')}")
|
||||||
|
print(f" Groups: {len(stack.get('EdgeGroups', []))}")
|
||||||
|
|
||||||
|
# Check if it has GitOps
|
||||||
|
if stack.get("GitConfig") and stack.get("AutoUpdate"):
|
||||||
|
print(f" GitOps: Enabled ({stack['AutoUpdate'].get('Interval', 'N/A')})")
|
||||||
|
else:
|
||||||
|
print("❌ Failed to list edge stacks")
|
||||||
|
|
||||||
|
async def test_edge_jobs():
|
||||||
|
"""Test edge job operations"""
|
||||||
|
print("\n💼 Testing Edge Jobs...")
|
||||||
|
|
||||||
|
# List edge jobs
|
||||||
|
jobs = await make_request("GET", "/api/edge_jobs")
|
||||||
|
if jobs:
|
||||||
|
print(f"✅ Found {len(jobs)} edge jobs")
|
||||||
|
for job in jobs[:3]: # Show first 3
|
||||||
|
print(f" • {job['Name']} (ID: {job['Id']})")
|
||||||
|
print(f" Recurring: {'Yes' if job.get('Recurring') else 'No'}")
|
||||||
|
if job.get('CronExpression'):
|
||||||
|
print(f" Schedule: {job['CronExpression']}")
|
||||||
|
print(f" Target Groups: {len(job.get('EdgeGroups', []))}")
|
||||||
|
else:
|
||||||
|
print("❌ Failed to list edge jobs")
|
||||||
|
|
||||||
|
async def test_edge_settings():
|
||||||
|
"""Test edge settings"""
|
||||||
|
print("\n⚙️ Testing Edge Settings...")
|
||||||
|
|
||||||
|
# Get settings
|
||||||
|
settings = await make_request("GET", "/api/settings")
|
||||||
|
if settings and settings.get("Edge"):
|
||||||
|
edge_settings = settings["Edge"]
|
||||||
|
print("✅ Edge Settings:")
|
||||||
|
print(f" • Check-in Interval: {edge_settings.get('CheckinInterval', 'N/A')} seconds")
|
||||||
|
print(f" • Command Interval: {edge_settings.get('CommandInterval', 'N/A')} seconds")
|
||||||
|
print(f" • Ping Interval: {edge_settings.get('PingInterval', 'N/A')} seconds")
|
||||||
|
print(f" • Tunnel Server: {edge_settings.get('TunnelServerAddress', 'Not configured')}")
|
||||||
|
else:
|
||||||
|
print("❌ Failed to get edge settings")
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Run all edge tests"""
|
||||||
|
print("🚀 Portainer Edge API Tests")
|
||||||
|
print(f"URL: {PORTAINER_URL}")
|
||||||
|
print(f"API Key: {'***' + PORTAINER_API_KEY[-4:] if PORTAINER_API_KEY else 'Not set'}")
|
||||||
|
|
||||||
|
if not PORTAINER_URL or not PORTAINER_API_KEY:
|
||||||
|
print("\n❌ Please set PORTAINER_URL and PORTAINER_API_KEY environment variables")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
await test_edge_environments()
|
||||||
|
await test_edge_groups()
|
||||||
|
await test_edge_stacks()
|
||||||
|
await test_edge_jobs()
|
||||||
|
await test_edge_settings()
|
||||||
|
|
||||||
|
print("\n✅ Edge API tests completed!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
Loading…
Reference in New Issue
Block a user