Simplify authentication to require URL and API key only
Major configuration and tooling updates: Authentication Changes: - Remove username/password authentication support - Require PORTAINER_URL and PORTAINER_API_KEY (both mandatory) - Simplify PortainerConfig class and validation logic - Update all documentation to reflect API key requirement Multiple Runtime Support: - Add uvx support for running without installation - Add uv support with dedicated wrapper script - Add npx support with Node.js wrapper script - Maintain backward compatibility with direct Python execution Documentation Updates: - Comprehensive README.md with all execution methods - Detailed USAGE.md with step-by-step instructions - Updated .env.example with clear required vs optional sections - Enhanced docstrings in server.py and config.py Tooling Support: - package.json for npm/npx support with cross-platform wrapper - scripts/run-with-uv.py for uv integration - bin/portainer-core-mcp Node.js wrapper for npx - test_uvx.py for uvx functionality testing Configuration Improvements: - Clear separation of required vs optional environment variables - Better validation error messages - Simplified authentication flow - Enhanced project metadata in pyproject.toml 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
84ca8aee99
commit
be1e05c382
26
.env.example
26
.env.example
@ -1,15 +1,24 @@
|
|||||||
# Portainer Core MCP Server Configuration
|
# Portainer Core MCP Server Configuration
|
||||||
|
|
||||||
# Portainer connection settings
|
# =============================================================================
|
||||||
PORTAINER_URL=https://your-portainer-instance.com
|
# REQUIRED CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
# Authentication settings (choose one method)
|
# Portainer instance URL (required)
|
||||||
# Method 1: API Key authentication (recommended)
|
# Examples:
|
||||||
|
# - https://portainer.example.com
|
||||||
|
# - https://portainer.company.com:9443
|
||||||
|
# - http://localhost:9000
|
||||||
|
PORTAINER_URL=https://portainer.example.com
|
||||||
|
|
||||||
|
# Portainer API key for authentication (required)
|
||||||
|
# Generate this from Portainer UI: User settings > API tokens
|
||||||
|
# Example: ptr_XYZ123abc456def789
|
||||||
PORTAINER_API_KEY=your-api-key-here
|
PORTAINER_API_KEY=your-api-key-here
|
||||||
|
|
||||||
# Method 2: Username/Password authentication
|
# =============================================================================
|
||||||
# PORTAINER_USERNAME=admin
|
# OPTIONAL CONFIGURATION
|
||||||
# PORTAINER_PASSWORD=your-password-here
|
# =============================================================================
|
||||||
|
|
||||||
# HTTP client settings
|
# HTTP client settings
|
||||||
HTTP_TIMEOUT=30
|
HTTP_TIMEOUT=30
|
||||||
@ -31,6 +40,3 @@ LOG_FORMAT=json
|
|||||||
# Development settings
|
# Development settings
|
||||||
DEBUG=false
|
DEBUG=false
|
||||||
|
|
||||||
# Server settings
|
|
||||||
SERVER_HOST=localhost
|
|
||||||
SERVER_PORT=8000
|
|
131
README.md
131
README.md
@ -1,61 +1,144 @@
|
|||||||
# 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
|
||||||
|
|
||||||
|
276
USAGE.md
Normal file
276
USAGE.md
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
# Portainer Core MCP Server Usage Guide
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Set Required Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Required configuration
|
||||||
|
export PORTAINER_URL=https://your-portainer-instance.com
|
||||||
|
export PORTAINER_API_KEY=your-api-key-here
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Generate Portainer API Key
|
||||||
|
|
||||||
|
1. Log in to your Portainer instance
|
||||||
|
2. Navigate to **User Settings** > **API Tokens**
|
||||||
|
3. Click **Add API Token**
|
||||||
|
4. Give it a name (e.g., "MCP Server")
|
||||||
|
5. Copy the generated token
|
||||||
|
|
||||||
|
### 3. Start the Server
|
||||||
|
|
||||||
|
Choose your preferred method:
|
||||||
|
|
||||||
|
#### Option A: Using Python (Direct)
|
||||||
|
```bash
|
||||||
|
python run_server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option B: Using uv (Recommended)
|
||||||
|
```bash
|
||||||
|
# Install uv if not installed
|
||||||
|
pip install uv
|
||||||
|
|
||||||
|
# Run with uv
|
||||||
|
uv run python run_server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option C: Using uvx (Run without installing)
|
||||||
|
```bash
|
||||||
|
# Install uv if not installed
|
||||||
|
pip install uv
|
||||||
|
|
||||||
|
# Run directly without installation
|
||||||
|
uvx --from . portainer-core-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option D: Using npm/npx
|
||||||
|
```bash
|
||||||
|
# Install globally
|
||||||
|
npm install -g portainer-core-mcp
|
||||||
|
|
||||||
|
# Run with npx
|
||||||
|
npx portainer-core-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Option E: Using the uv wrapper script
|
||||||
|
```bash
|
||||||
|
# Make executable (if not already)
|
||||||
|
chmod +x scripts/run-with-uv.py
|
||||||
|
|
||||||
|
# Run with uv wrapper
|
||||||
|
python scripts/run-with-uv.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Configuration
|
||||||
|
|
||||||
|
### Using .env File
|
||||||
|
|
||||||
|
1. Copy the example file:
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Edit the file:
|
||||||
|
```bash
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Set your values:
|
||||||
|
```bash
|
||||||
|
# Required
|
||||||
|
PORTAINER_URL=https://your-portainer-instance.com
|
||||||
|
PORTAINER_API_KEY=your-api-key-here
|
||||||
|
|
||||||
|
# Optional (with defaults)
|
||||||
|
HTTP_TIMEOUT=30
|
||||||
|
MAX_RETRIES=3
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
DEBUG=false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Export variables
|
||||||
|
export PORTAINER_URL=https://your-portainer-instance.com
|
||||||
|
export PORTAINER_API_KEY=your-api-key-here
|
||||||
|
|
||||||
|
# Start server
|
||||||
|
python run_server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation Methods
|
||||||
|
|
||||||
|
### Development Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clone and install in development mode
|
||||||
|
git clone <repository-url>
|
||||||
|
cd portainer-mcp
|
||||||
|
pip install -e .
|
||||||
|
|
||||||
|
# Or with uv
|
||||||
|
uv pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install from PyPI (when published)
|
||||||
|
pip install portainer-core-mcp
|
||||||
|
|
||||||
|
# Or with uv
|
||||||
|
uv pip install portainer-core-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run Without Installing (uvx)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run directly from source without installation
|
||||||
|
uvx --from . portainer-core-mcp
|
||||||
|
|
||||||
|
# Or from PyPI (when published)
|
||||||
|
uvx portainer-core-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
### NPM Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install globally
|
||||||
|
npm install -g portainer-core-mcp
|
||||||
|
|
||||||
|
# Or run without installing
|
||||||
|
npx portainer-core-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Commands
|
||||||
|
|
||||||
|
### MCP Tools
|
||||||
|
|
||||||
|
Once the server is running, the following tools are available:
|
||||||
|
|
||||||
|
- **`authenticate`** - Login with username/password
|
||||||
|
- **`generate_token`** - Generate API tokens
|
||||||
|
- **`get_current_user`** - Get current user info
|
||||||
|
- **`list_users`** - List all users
|
||||||
|
- **`create_user`** - Create new user
|
||||||
|
- **`update_user`** - Update user details
|
||||||
|
- **`delete_user`** - Delete user
|
||||||
|
- **`get_settings`** - Get Portainer settings
|
||||||
|
- **`update_settings`** - Update configuration
|
||||||
|
- **`health_check`** - Server health status
|
||||||
|
|
||||||
|
### MCP Resources
|
||||||
|
|
||||||
|
- **`portainer://users`** - User management data
|
||||||
|
- **`portainer://settings`** - Configuration settings
|
||||||
|
- **`portainer://health`** - Server health status
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Run Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
python -m pytest
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
python -m pytest --cov=src
|
||||||
|
|
||||||
|
# Run specific test file
|
||||||
|
python -m pytest tests/test_basic.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test configuration loading
|
||||||
|
python -c "
|
||||||
|
import os
|
||||||
|
os.environ['PORTAINER_URL'] = 'https://test.com'
|
||||||
|
os.environ['PORTAINER_API_KEY'] = 'test-key'
|
||||||
|
from src.portainer_core.config import get_config
|
||||||
|
config = get_config()
|
||||||
|
print('✅ Configuration OK')
|
||||||
|
print(f'URL: {config.portainer_url}')
|
||||||
|
print(f'API Base: {config.api_base_url}')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
1. **Missing API Key Error**
|
||||||
|
```
|
||||||
|
ValueError: PORTAINER_API_KEY must be provided and cannot be empty
|
||||||
|
```
|
||||||
|
**Solution**: Set the `PORTAINER_API_KEY` environment variable
|
||||||
|
|
||||||
|
2. **Invalid URL Error**
|
||||||
|
```
|
||||||
|
ValueError: Invalid Portainer URL
|
||||||
|
```
|
||||||
|
**Solution**: Ensure `PORTAINER_URL` includes the protocol (http/https)
|
||||||
|
|
||||||
|
3. **Python Not Found (npx)**
|
||||||
|
```
|
||||||
|
Error: Python is not installed or not in PATH
|
||||||
|
```
|
||||||
|
**Solution**: Install Python 3.8+ and ensure it's in your PATH
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
Enable debug logging for troubleshooting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DEBUG=true LOG_LEVEL=DEBUG python run_server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
|
||||||
|
Test if the server is working:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check server health
|
||||||
|
curl -X POST http://localhost:8000/health
|
||||||
|
|
||||||
|
# Or use the health_check tool through MCP
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### Custom Configuration
|
||||||
|
|
||||||
|
```python
|
||||||
|
from portainer_core.config import PortainerConfig
|
||||||
|
|
||||||
|
# Create custom config
|
||||||
|
config = PortainerConfig(
|
||||||
|
portainer_url="https://custom.portainer.com",
|
||||||
|
portainer_api_key="custom-key",
|
||||||
|
http_timeout=60,
|
||||||
|
log_level="DEBUG"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Programmatic Usage
|
||||||
|
|
||||||
|
```python
|
||||||
|
from portainer_core.server import create_server
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
server = create_server()
|
||||||
|
await server.run()
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- **Documentation**: See README.md and code comments
|
||||||
|
- **Issues**: Report via GitHub Issues
|
||||||
|
- **Configuration**: Check .env.example for all options
|
115
bin/portainer-core-mcp
Executable file
115
bin/portainer-core-mcp
Executable file
@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const { spawn } = require('child_process');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Get the directory where this script is located
|
||||||
|
const scriptDir = path.dirname(__filename);
|
||||||
|
const runServerPath = path.join(scriptDir, '..', 'run_server.py');
|
||||||
|
|
||||||
|
// Check if Python is available
|
||||||
|
function checkPython() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const pythonCmd = process.platform === 'win32' ? 'python' : 'python3';
|
||||||
|
const proc = spawn(pythonCmd, ['--version'], { stdio: 'pipe' });
|
||||||
|
|
||||||
|
proc.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve(pythonCmd);
|
||||||
|
} else {
|
||||||
|
// Try alternative python command
|
||||||
|
const altPythonCmd = 'python';
|
||||||
|
const altProc = spawn(altPythonCmd, ['--version'], { stdio: 'pipe' });
|
||||||
|
|
||||||
|
altProc.on('close', (altCode) => {
|
||||||
|
if (altCode === 0) {
|
||||||
|
resolve(altPythonCmd);
|
||||||
|
} else {
|
||||||
|
reject(new Error('Python is not installed or not in PATH'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the Python server
|
||||||
|
async function runServer() {
|
||||||
|
try {
|
||||||
|
const pythonCmd = await checkPython();
|
||||||
|
console.log('🚀 Starting Portainer Core MCP Server via npx...');
|
||||||
|
|
||||||
|
const proc = spawn(pythonCmd, [runServerPath], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
cwd: path.dirname(runServerPath)
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on('error', (err) => {
|
||||||
|
console.error('❌ Failed to start server:', err.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on('close', (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
console.error(`❌ Server exited with code ${code}`);
|
||||||
|
process.exit(code);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle signals
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('\n👋 Stopping server...');
|
||||||
|
proc.kill('SIGINT');
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => {
|
||||||
|
console.log('\n👋 Stopping server...');
|
||||||
|
proc.kill('SIGTERM');
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error:', error.message);
|
||||||
|
console.error('💡 Please ensure Python 3.8+ is installed and in your PATH');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show help
|
||||||
|
function showHelp() {
|
||||||
|
console.log(`
|
||||||
|
Portainer Core MCP Server
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
npx portainer-core-mcp [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help Show this help message
|
||||||
|
-v, --version Show version information
|
||||||
|
|
||||||
|
Environment Variables:
|
||||||
|
PORTAINER_URL Portainer instance URL (required)
|
||||||
|
PORTAINER_API_KEY Portainer API key (required)
|
||||||
|
LOG_LEVEL Logging level (default: INFO)
|
||||||
|
DEBUG Enable debug mode (default: false)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
npx portainer-core-mcp
|
||||||
|
PORTAINER_URL=https://portainer.example.com PORTAINER_API_KEY=your-key npx portainer-core-mcp
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.includes('-h') || args.includes('--help')) {
|
||||||
|
showHelp();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.includes('-v') || args.includes('--version')) {
|
||||||
|
const packageJson = require('../package.json');
|
||||||
|
console.log(`portainer-core-mcp v${packageJson.version}`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
runServer().catch(console.error);
|
47
package.json
Normal file
47
package.json
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "portainer-core-mcp",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "MCP server for Portainer Business Edition authentication and user management",
|
||||||
|
"main": "run_server.py",
|
||||||
|
"scripts": {
|
||||||
|
"start": "python run_server.py",
|
||||||
|
"dev": "python run_server.py",
|
||||||
|
"test": "python -m pytest tests/",
|
||||||
|
"install": "pip install -e .",
|
||||||
|
"install-dev": "pip install -e .[dev]"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"portainer-core-mcp": "bin/portainer-core-mcp"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"mcp",
|
||||||
|
"portainer",
|
||||||
|
"docker",
|
||||||
|
"kubernetes",
|
||||||
|
"model-context-protocol",
|
||||||
|
"claude"
|
||||||
|
],
|
||||||
|
"author": "Portainer MCP Team",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/portainer/portainer-mcp-core"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/portainer/portainer-mcp-core/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/portainer/portainer-mcp-core#readme",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0.0",
|
||||||
|
"python": ">=3.8"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/",
|
||||||
|
"bin/",
|
||||||
|
"scripts/",
|
||||||
|
"run_server.py",
|
||||||
|
"pyproject.toml",
|
||||||
|
"README.md",
|
||||||
|
".env.example"
|
||||||
|
]
|
||||||
|
}
|
@ -7,7 +7,7 @@ name = "portainer-core-mcp"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Portainer Core MCP Server - Authentication and User Management"
|
description = "Portainer Core MCP Server - Authentication and User Management"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Your Name", email = "your.email@example.com"}
|
{name = "Portainer MCP Team", email = "support@portainer.io"}
|
||||||
]
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
@ -49,10 +49,10 @@ dev = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://github.com/yourusername/portainer-core-mcp"
|
Homepage = "https://github.com/portainer/portainer-mcp-core"
|
||||||
Documentation = "https://github.com/yourusername/portainer-core-mcp#readme"
|
Documentation = "https://github.com/portainer/portainer-mcp-core#readme"
|
||||||
Repository = "https://github.com/yourusername/portainer-core-mcp"
|
Repository = "https://github.com/portainer/portainer-mcp-core"
|
||||||
Issues = "https://github.com/yourusername/portainer-core-mcp/issues"
|
Issues = "https://github.com/portainer/portainer-mcp-core/issues"
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
portainer-core-mcp = "portainer_core.server:main"
|
portainer-core-mcp = "portainer_core.server:main"
|
||||||
|
@ -11,9 +11,7 @@ Usage:
|
|||||||
|
|
||||||
Environment Variables:
|
Environment Variables:
|
||||||
PORTAINER_URL: The base URL of your Portainer instance (required)
|
PORTAINER_URL: The base URL of your Portainer instance (required)
|
||||||
PORTAINER_API_KEY: API key for authentication (option 1)
|
PORTAINER_API_KEY: API key for authentication (required)
|
||||||
PORTAINER_USERNAME: Username for authentication (option 2)
|
|
||||||
PORTAINER_PASSWORD: Password for authentication (option 2)
|
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
export PORTAINER_URL=https://portainer.example.com
|
export PORTAINER_URL=https://portainer.example.com
|
||||||
@ -44,7 +42,6 @@ def setup_environment():
|
|||||||
Environment Variables Handled:
|
Environment Variables Handled:
|
||||||
PORTAINER_URL: Sets default demo URL if not provided
|
PORTAINER_URL: Sets default demo URL if not provided
|
||||||
PORTAINER_API_KEY: Sets placeholder if no authentication is configured
|
PORTAINER_API_KEY: Sets placeholder if no authentication is configured
|
||||||
PORTAINER_USERNAME/PORTAINER_PASSWORD: Alternative authentication method
|
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
None: This function doesn't raise exceptions but prints warnings for
|
None: This function doesn't raise exceptions but prints warnings for
|
||||||
@ -64,13 +61,11 @@ def setup_environment():
|
|||||||
os.environ['PORTAINER_URL'] = 'https://demo.portainer.io'
|
os.environ['PORTAINER_URL'] = 'https://demo.portainer.io'
|
||||||
print("🔧 Using demo Portainer URL: https://demo.portainer.io")
|
print("🔧 Using demo Portainer URL: https://demo.portainer.io")
|
||||||
|
|
||||||
# Configure authentication - requires either API key or username/password
|
# Configure authentication - requires API key
|
||||||
has_api_key = os.environ.get('PORTAINER_API_KEY')
|
has_api_key = os.environ.get('PORTAINER_API_KEY')
|
||||||
has_credentials = (os.environ.get('PORTAINER_USERNAME') and
|
|
||||||
os.environ.get('PORTAINER_PASSWORD'))
|
|
||||||
|
|
||||||
if not has_api_key and not has_credentials:
|
if not has_api_key:
|
||||||
print("⚠️ No authentication configured. Set PORTAINER_API_KEY or PORTAINER_USERNAME/PORTAINER_PASSWORD")
|
print("⚠️ No API key configured. Set PORTAINER_API_KEY")
|
||||||
print(" For demo purposes, using placeholder API key")
|
print(" For demo purposes, using placeholder API key")
|
||||||
os.environ['PORTAINER_API_KEY'] = 'demo-api-key'
|
os.environ['PORTAINER_API_KEY'] = 'demo-api-key'
|
||||||
|
|
||||||
@ -96,7 +91,7 @@ if __name__ == "__main__":
|
|||||||
"""
|
"""
|
||||||
print("🚀 Starting Portainer Core MCP Server...")
|
print("🚀 Starting Portainer Core MCP Server...")
|
||||||
print(" Configuration will be loaded from environment variables")
|
print(" Configuration will be loaded from environment variables")
|
||||||
print(" Set PORTAINER_URL and PORTAINER_API_KEY (or username/password) before running")
|
print(" Set PORTAINER_URL and PORTAINER_API_KEY before running")
|
||||||
print("")
|
print("")
|
||||||
|
|
||||||
# Initialize environment with fallback values for demo/testing
|
# Initialize environment with fallback values for demo/testing
|
||||||
|
64
scripts/run-with-uv.py
Executable file
64
scripts/run-with-uv.py
Executable file
@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
UV runner script for Portainer Core MCP Server.
|
||||||
|
|
||||||
|
This script provides a way to run the MCP server using uv for dependency management.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def find_project_root():
|
||||||
|
"""Find the project root directory."""
|
||||||
|
current = Path(__file__).parent
|
||||||
|
while current != current.parent:
|
||||||
|
if (current / "pyproject.toml").exists():
|
||||||
|
return current
|
||||||
|
current = current.parent
|
||||||
|
return Path(__file__).parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def check_uv():
|
||||||
|
"""Check if uv is installed."""
|
||||||
|
try:
|
||||||
|
subprocess.run(["uv", "--version"], capture_output=True, check=True)
|
||||||
|
return True
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def run_server():
|
||||||
|
"""Run the server using uv."""
|
||||||
|
project_root = find_project_root()
|
||||||
|
|
||||||
|
# Check if uv is available
|
||||||
|
if not check_uv():
|
||||||
|
print("❌ uv is not installed or not in PATH")
|
||||||
|
print("💡 Install uv: pip install uv")
|
||||||
|
print("💡 Or use: python run_server.py")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("🚀 Starting Portainer Core MCP Server with uv...")
|
||||||
|
|
||||||
|
# Change to project root
|
||||||
|
os.chdir(project_root)
|
||||||
|
|
||||||
|
# Run the server with uv
|
||||||
|
try:
|
||||||
|
subprocess.run([
|
||||||
|
"uv", "run",
|
||||||
|
"python", "run_server.py"
|
||||||
|
], check=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f"❌ Server failed with exit code {e.returncode}")
|
||||||
|
sys.exit(e.returncode)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n👋 Server stopped by user")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_server()
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,7 +1,41 @@
|
|||||||
"""
|
"""
|
||||||
Configuration management for Portainer Core MCP Server.
|
Configuration management for Portainer Core MCP Server.
|
||||||
|
|
||||||
This module handles environment variable validation and configuration settings.
|
This module provides comprehensive configuration management for the Portainer Core MCP Server,
|
||||||
|
including environment variable validation, authentication setup, and runtime settings.
|
||||||
|
|
||||||
|
The configuration system supports:
|
||||||
|
- Environment variable loading with validation
|
||||||
|
- Authentication via API key (required)
|
||||||
|
- HTTP client configuration with retry logic
|
||||||
|
- Circuit breaker settings for fault tolerance
|
||||||
|
- Token management and caching
|
||||||
|
- Logging configuration
|
||||||
|
- Development and debug settings
|
||||||
|
|
||||||
|
Key Components:
|
||||||
|
- PortainerConfig: Main configuration class with validation
|
||||||
|
- ServerConfig: Server-specific settings (host, port, name)
|
||||||
|
- Global configuration instances with lazy initialization
|
||||||
|
- Factory functions for configuration creation
|
||||||
|
- Validation methods for authentication and settings
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
```python
|
||||||
|
from portainer_core.config import get_global_config
|
||||||
|
|
||||||
|
config = get_global_config()
|
||||||
|
print(config.portainer_url)
|
||||||
|
print(config.api_base_url)
|
||||||
|
```
|
||||||
|
|
||||||
|
Environment Variables:
|
||||||
|
- PORTAINER_URL: Base URL of Portainer instance (required)
|
||||||
|
- PORTAINER_API_KEY: API key for authentication (required)
|
||||||
|
- HTTP_TIMEOUT: HTTP request timeout in seconds (default: 30)
|
||||||
|
- MAX_RETRIES: Maximum retry attempts (default: 3)
|
||||||
|
- LOG_LEVEL: Logging level (default: INFO)
|
||||||
|
- DEBUG: Enable debug mode (default: false)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@ -13,7 +47,44 @@ from pydantic_settings import BaseSettings
|
|||||||
|
|
||||||
|
|
||||||
class PortainerConfig(BaseSettings):
|
class PortainerConfig(BaseSettings):
|
||||||
"""Configuration settings for Portainer Core MCP Server."""
|
"""
|
||||||
|
Configuration settings for Portainer Core MCP Server.
|
||||||
|
|
||||||
|
This class defines all configuration parameters for the MCP server,
|
||||||
|
including connection settings, authentication, HTTP client behavior,
|
||||||
|
circuit breaker configuration, and logging settings.
|
||||||
|
|
||||||
|
The configuration is loaded from environment variables with validation
|
||||||
|
and provides computed properties for common operations.
|
||||||
|
|
||||||
|
Configuration Categories:
|
||||||
|
- Connection: Portainer URL and API endpoints
|
||||||
|
- Authentication: API key (required)
|
||||||
|
- HTTP Client: Timeouts, retries, and connection settings
|
||||||
|
- Circuit Breaker: Fault tolerance and recovery settings
|
||||||
|
- Token Management: Caching and refresh policies
|
||||||
|
- Logging: Level, format, and debug settings
|
||||||
|
|
||||||
|
Validation:
|
||||||
|
- URL format validation with scheme checking
|
||||||
|
- API key presence validation (required field)
|
||||||
|
- Log level and format validation
|
||||||
|
- Numeric range validation for timeouts and thresholds
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
- api_base_url: Computed API base URL
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```python
|
||||||
|
# Environment variables:
|
||||||
|
# PORTAINER_URL=https://portainer.example.com
|
||||||
|
# PORTAINER_API_KEY=your-api-key
|
||||||
|
|
||||||
|
config = PortainerConfig()
|
||||||
|
print(config.api_base_url) # https://portainer.example.com/api
|
||||||
|
print(config.portainer_api_key) # your-api-key
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
# Portainer connection settings
|
# Portainer connection settings
|
||||||
portainer_url: str = Field(
|
portainer_url: str = Field(
|
||||||
@ -21,22 +92,12 @@ class PortainerConfig(BaseSettings):
|
|||||||
description="Base URL of the Portainer instance"
|
description="Base URL of the Portainer instance"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Authentication settings (either API key or username/password)
|
# Authentication settings (API key required)
|
||||||
portainer_api_key: Optional[str] = Field(
|
portainer_api_key: str = Field(
|
||||||
default=None,
|
...,
|
||||||
description="Portainer API key for authentication"
|
description="Portainer API key for authentication"
|
||||||
)
|
)
|
||||||
|
|
||||||
portainer_username: Optional[str] = Field(
|
|
||||||
default=None,
|
|
||||||
description="Portainer username for authentication"
|
|
||||||
)
|
|
||||||
|
|
||||||
portainer_password: Optional[str] = Field(
|
|
||||||
default=None,
|
|
||||||
description="Portainer password for authentication"
|
|
||||||
)
|
|
||||||
|
|
||||||
# HTTP client settings
|
# HTTP client settings
|
||||||
http_timeout: int = Field(
|
http_timeout: int = Field(
|
||||||
default=30,
|
default=30,
|
||||||
@ -101,7 +162,32 @@ class PortainerConfig(BaseSettings):
|
|||||||
@field_validator("portainer_url")
|
@field_validator("portainer_url")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_portainer_url(cls, v):
|
def validate_portainer_url(cls, v):
|
||||||
"""Validate Portainer URL format."""
|
"""
|
||||||
|
Validate Portainer URL format and structure.
|
||||||
|
|
||||||
|
Ensures the provided URL is properly formatted, uses a valid scheme,
|
||||||
|
and normalizes the URL by removing trailing slashes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
v: URL string to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Validated and normalized URL (trailing slash removed)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If URL is empty, malformed, or uses invalid scheme
|
||||||
|
|
||||||
|
Validation Rules:
|
||||||
|
- URL must not be empty
|
||||||
|
- Must have valid scheme (http or https)
|
||||||
|
- Must have network location (hostname)
|
||||||
|
- Trailing slashes are removed for consistency
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- https://portainer.example.com/ -> https://portainer.example.com
|
||||||
|
- http://localhost:9000 -> http://localhost:9000
|
||||||
|
- invalid-url -> raises ValueError
|
||||||
|
"""
|
||||||
if not v:
|
if not v:
|
||||||
raise ValueError("Portainer URL is required")
|
raise ValueError("Portainer URL is required")
|
||||||
|
|
||||||
@ -121,7 +207,33 @@ class PortainerConfig(BaseSettings):
|
|||||||
@field_validator("log_level")
|
@field_validator("log_level")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_log_level(cls, v):
|
def validate_log_level(cls, v):
|
||||||
"""Validate logging level."""
|
"""
|
||||||
|
Validate logging level against allowed values.
|
||||||
|
|
||||||
|
Ensures the logging level is one of the standard Python logging levels
|
||||||
|
and normalizes it to uppercase.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
v: Log level string to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Validated log level in uppercase
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If log level is not in allowed values
|
||||||
|
|
||||||
|
Valid Levels:
|
||||||
|
- DEBUG: Detailed diagnostic information
|
||||||
|
- INFO: General information about program execution
|
||||||
|
- WARNING: Warning messages for potential issues
|
||||||
|
- ERROR: Error messages for serious problems
|
||||||
|
- CRITICAL: Critical error messages for fatal problems
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- "info" -> "INFO"
|
||||||
|
- "Debug" -> "DEBUG"
|
||||||
|
- "invalid" -> raises ValueError
|
||||||
|
"""
|
||||||
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
||||||
if v.upper() not in valid_levels:
|
if v.upper() not in valid_levels:
|
||||||
raise ValueError(f"Log level must be one of {valid_levels}")
|
raise ValueError(f"Log level must be one of {valid_levels}")
|
||||||
@ -130,52 +242,103 @@ class PortainerConfig(BaseSettings):
|
|||||||
@field_validator("log_format")
|
@field_validator("log_format")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_log_format(cls, v):
|
def validate_log_format(cls, v):
|
||||||
"""Validate logging format."""
|
"""
|
||||||
|
Validate logging format against allowed values.
|
||||||
|
|
||||||
|
Ensures the logging format is one of the supported formats
|
||||||
|
and normalizes it to lowercase.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
v: Log format string to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Validated log format in lowercase
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If log format is not in allowed values
|
||||||
|
|
||||||
|
Valid Formats:
|
||||||
|
- json: Structured JSON logging for machine parsing
|
||||||
|
- text: Human-readable text logging for development
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- "JSON" -> "json"
|
||||||
|
- "Text" -> "text"
|
||||||
|
- "invalid" -> raises ValueError
|
||||||
|
"""
|
||||||
valid_formats = ["json", "text"]
|
valid_formats = ["json", "text"]
|
||||||
if v.lower() not in valid_formats:
|
if v.lower() not in valid_formats:
|
||||||
raise ValueError(f"Log format must be one of {valid_formats}")
|
raise ValueError(f"Log format must be one of {valid_formats}")
|
||||||
return v.lower()
|
return v.lower()
|
||||||
|
|
||||||
def validate_auth_config(self) -> None:
|
def validate_auth_config(self) -> None:
|
||||||
"""Validate authentication configuration."""
|
"""
|
||||||
has_api_key = self.portainer_api_key is not None
|
Validate authentication configuration.
|
||||||
has_credentials = (
|
|
||||||
self.portainer_username is not None
|
|
||||||
and self.portainer_password is not None
|
|
||||||
)
|
|
||||||
|
|
||||||
if not has_api_key and not has_credentials:
|
Ensures that the required API key is configured properly.
|
||||||
|
|
||||||
|
Authentication Method:
|
||||||
|
- API Key: Direct API key authentication (required)
|
||||||
|
|
||||||
|
Validation Rules:
|
||||||
|
- API key must be provided and non-empty
|
||||||
|
- API key should not contain only whitespace
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If API key is missing or invalid
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This method is called automatically during configuration validation
|
||||||
|
to ensure authentication is properly configured.
|
||||||
|
"""
|
||||||
|
if not self.portainer_api_key or not self.portainer_api_key.strip():
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Either PORTAINER_API_KEY or both PORTAINER_USERNAME and "
|
"PORTAINER_API_KEY must be provided and cannot be empty"
|
||||||
"PORTAINER_PASSWORD must be provided"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if has_api_key and has_credentials:
|
|
||||||
# Prefer API key if both are provided
|
|
||||||
self.portainer_username = None
|
|
||||||
self.portainer_password = None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def api_base_url(self) -> str:
|
def api_base_url(self) -> str:
|
||||||
"""Get the base API URL."""
|
"""
|
||||||
|
Get the base API URL for Portainer REST API.
|
||||||
|
|
||||||
|
Constructs the full API base URL by appending '/api' to the
|
||||||
|
configured Portainer URL.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Complete API base URL for making requests
|
||||||
|
|
||||||
|
Example:
|
||||||
|
If portainer_url is 'https://portainer.example.com',
|
||||||
|
returns 'https://portainer.example.com/api'
|
||||||
|
"""
|
||||||
return f"{self.portainer_url}/api"
|
return f"{self.portainer_url}/api"
|
||||||
|
|
||||||
@property
|
|
||||||
def use_api_key_auth(self) -> bool:
|
|
||||||
"""Check if API key authentication should be used."""
|
|
||||||
return self.portainer_api_key is not None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def use_credentials_auth(self) -> bool:
|
|
||||||
"""Check if username/password authentication should be used."""
|
|
||||||
return (
|
|
||||||
self.portainer_username is not None
|
|
||||||
and self.portainer_password is not None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ServerConfig(BaseModel):
|
class ServerConfig(BaseModel):
|
||||||
"""Server-specific configuration settings."""
|
"""
|
||||||
|
Server-specific configuration settings.
|
||||||
|
|
||||||
|
This class defines configuration parameters specific to the MCP server
|
||||||
|
instance, including network settings and server identification.
|
||||||
|
|
||||||
|
Configuration Parameters:
|
||||||
|
- host: Server host address (default: localhost)
|
||||||
|
- port: Server port number (default: 8000)
|
||||||
|
- server_name: Server identifier for MCP protocol (default: portainer-core-mcp)
|
||||||
|
- version: Server version string (default: 0.1.0)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
```python
|
||||||
|
config = ServerConfig()
|
||||||
|
print(f"Server: {config.server_name} v{config.version}")
|
||||||
|
print(f"Listening on: {config.host}:{config.port}")
|
||||||
|
```
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This configuration is separate from PortainerConfig to allow
|
||||||
|
independent management of server vs. Portainer connection settings.
|
||||||
|
"""
|
||||||
|
|
||||||
host: str = Field(
|
host: str = Field(
|
||||||
default="localhost",
|
default="localhost",
|
||||||
@ -199,14 +362,46 @@ class ServerConfig(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
def get_config() -> PortainerConfig:
|
def get_config() -> PortainerConfig:
|
||||||
"""Get validated configuration instance."""
|
"""
|
||||||
|
Get validated configuration instance.
|
||||||
|
|
||||||
|
Creates a new PortainerConfig instance from environment variables
|
||||||
|
and validates the authentication configuration.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PortainerConfig: Validated configuration instance
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If configuration is invalid or authentication is missing
|
||||||
|
ValidationError: If environment variables have invalid values
|
||||||
|
|
||||||
|
Process:
|
||||||
|
1. Create PortainerConfig instance from environment variables
|
||||||
|
2. Validate authentication configuration
|
||||||
|
3. Return validated configuration
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This function creates a new instance each time it's called.
|
||||||
|
For singleton behavior, use get_global_config() instead.
|
||||||
|
"""
|
||||||
config = PortainerConfig()
|
config = PortainerConfig()
|
||||||
config.validate_auth_config()
|
config.validate_auth_config()
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def get_server_config() -> ServerConfig:
|
def get_server_config() -> ServerConfig:
|
||||||
"""Get server configuration instance."""
|
"""
|
||||||
|
Get server configuration instance.
|
||||||
|
|
||||||
|
Creates a new ServerConfig instance with default values.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ServerConfig: Server configuration instance
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This function creates a new instance each time it's called.
|
||||||
|
For singleton behavior, use get_global_server_config() instead.
|
||||||
|
"""
|
||||||
return ServerConfig()
|
return ServerConfig()
|
||||||
|
|
||||||
|
|
||||||
@ -215,14 +410,66 @@ _config = None
|
|||||||
_server_config = None
|
_server_config = None
|
||||||
|
|
||||||
def get_global_config() -> PortainerConfig:
|
def get_global_config() -> PortainerConfig:
|
||||||
"""Get global configuration instance with lazy initialization."""
|
"""
|
||||||
|
Get global configuration instance with lazy initialization.
|
||||||
|
|
||||||
|
Implements singleton pattern for configuration management. The configuration
|
||||||
|
is loaded and validated only once, then cached for subsequent calls.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PortainerConfig: Global configuration instance
|
||||||
|
|
||||||
|
Thread Safety:
|
||||||
|
This function is not thread-safe. In multi-threaded environments,
|
||||||
|
ensure it's called during initialization before spawning threads.
|
||||||
|
|
||||||
|
Lazy Initialization:
|
||||||
|
The configuration is only loaded when first accessed, which allows
|
||||||
|
for environment variable changes during testing.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
```python
|
||||||
|
# First call loads and validates configuration
|
||||||
|
config = get_global_config()
|
||||||
|
|
||||||
|
# Subsequent calls return cached instance
|
||||||
|
same_config = get_global_config()
|
||||||
|
assert config is same_config # True
|
||||||
|
```
|
||||||
|
"""
|
||||||
global _config
|
global _config
|
||||||
if _config is None:
|
if _config is None:
|
||||||
_config = get_config()
|
_config = get_config()
|
||||||
return _config
|
return _config
|
||||||
|
|
||||||
def get_global_server_config() -> ServerConfig:
|
def get_global_server_config() -> ServerConfig:
|
||||||
"""Get global server configuration instance with lazy initialization."""
|
"""
|
||||||
|
Get global server configuration instance with lazy initialization.
|
||||||
|
|
||||||
|
Implements singleton pattern for server configuration management.
|
||||||
|
The configuration is created only once, then cached for subsequent calls.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ServerConfig: Global server configuration instance
|
||||||
|
|
||||||
|
Thread Safety:
|
||||||
|
This function is not thread-safe. In multi-threaded environments,
|
||||||
|
ensure it's called during initialization before spawning threads.
|
||||||
|
|
||||||
|
Lazy Initialization:
|
||||||
|
The configuration is only created when first accessed, which reduces
|
||||||
|
startup overhead when server configuration isn't needed.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
```python
|
||||||
|
# First call creates configuration
|
||||||
|
config = get_global_server_config()
|
||||||
|
|
||||||
|
# Subsequent calls return cached instance
|
||||||
|
same_config = get_global_server_config()
|
||||||
|
assert config is same_config # True
|
||||||
|
```
|
||||||
|
"""
|
||||||
global _server_config
|
global _server_config
|
||||||
if _server_config is None:
|
if _server_config is None:
|
||||||
_server_config = get_server_config()
|
_server_config = get_server_config()
|
||||||
|
@ -34,9 +34,64 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class PortainerCoreMCPServer:
|
class PortainerCoreMCPServer:
|
||||||
"""Main MCP server class for Portainer Core functionality."""
|
"""
|
||||||
|
Main MCP server class for Portainer Core functionality.
|
||||||
|
|
||||||
|
This class implements the Model Context Protocol (MCP) server that provides
|
||||||
|
authentication and user management functionality for Portainer Business Edition.
|
||||||
|
It handles MCP protocol communication, resource management, and tool execution.
|
||||||
|
|
||||||
|
The server provides the following capabilities:
|
||||||
|
- User authentication with JWT token management
|
||||||
|
- User management operations (CRUD)
|
||||||
|
- Settings configuration management
|
||||||
|
- Health monitoring and status reporting
|
||||||
|
- Resource and tool discovery through MCP protocol
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
server: The underlying MCP server instance
|
||||||
|
auth_service: Authentication service for login/token operations
|
||||||
|
user_service: User management service for CRUD operations
|
||||||
|
settings_service: Settings management service for configuration
|
||||||
|
|
||||||
|
Architecture:
|
||||||
|
The server follows a service-oriented architecture with:
|
||||||
|
- Service Layer: Business logic separation (auth, users, settings)
|
||||||
|
- Error Handling: Comprehensive error mapping and logging
|
||||||
|
- Circuit Breaker: Fault tolerance with automatic recovery
|
||||||
|
- Correlation IDs: Request tracing and debugging support
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
```python
|
||||||
|
server = PortainerCoreMCPServer()
|
||||||
|
await server.run()
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
"""
|
||||||
|
Initialize the Portainer Core MCP Server.
|
||||||
|
|
||||||
|
Sets up the MCP server instance with configuration from environment variables,
|
||||||
|
initializes the handler setup, and prepares service instances for lazy loading.
|
||||||
|
|
||||||
|
The initialization process:
|
||||||
|
1. Loads global configuration from environment variables
|
||||||
|
2. Creates the underlying MCP server instance
|
||||||
|
3. Sets up MCP protocol handlers for resources and tools
|
||||||
|
4. Prepares service instances for lazy initialization
|
||||||
|
5. Logs initialization success with configuration details
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PortainerConfigurationError: If required configuration is missing
|
||||||
|
PortainerError: If server initialization fails
|
||||||
|
|
||||||
|
Side Effects:
|
||||||
|
- Creates MCP server instance
|
||||||
|
- Registers protocol handlers
|
||||||
|
- Initializes logging context
|
||||||
|
- Prepares service instances (not yet initialized)
|
||||||
|
"""
|
||||||
config = get_global_config()
|
config = get_global_config()
|
||||||
server_config = get_global_server_config()
|
server_config = get_global_server_config()
|
||||||
|
|
||||||
@ -56,11 +111,57 @@ class PortainerCoreMCPServer:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def setup_handlers(self) -> None:
|
def setup_handlers(self) -> None:
|
||||||
"""Set up MCP server handlers."""
|
"""
|
||||||
|
Set up MCP server handlers for resources and tools.
|
||||||
|
|
||||||
|
Registers handlers for the MCP protocol operations:
|
||||||
|
- Resource handlers: For listing and reading available resources
|
||||||
|
- Tool handlers: For executing available tools and operations
|
||||||
|
|
||||||
|
The handlers are registered using decorators on the server instance:
|
||||||
|
- @server.list_resources(): Lists available resources (users, settings, health)
|
||||||
|
- @server.read_resource(): Reads specific resource content
|
||||||
|
- @server.list_tools(): Lists available tools with their schemas
|
||||||
|
- @server.call_tool(): Executes specific tools with arguments
|
||||||
|
|
||||||
|
Resource Types:
|
||||||
|
- portainer://users: User management resource
|
||||||
|
- portainer://settings: Settings configuration resource
|
||||||
|
- portainer://health: Server health status resource
|
||||||
|
|
||||||
|
Tool Types:
|
||||||
|
- Authentication: authenticate, generate_token, get_current_user
|
||||||
|
- User Management: list_users, create_user, update_user, delete_user
|
||||||
|
- Settings: get_settings, update_settings
|
||||||
|
- Health: health_check
|
||||||
|
|
||||||
|
Complexity: O(1) - Handler registration is constant time
|
||||||
|
|
||||||
|
Side Effects:
|
||||||
|
- Registers MCP protocol handlers on server instance
|
||||||
|
- Creates handler closures with access to server state
|
||||||
|
"""
|
||||||
|
|
||||||
@self.server.list_resources()
|
@self.server.list_resources()
|
||||||
async def handle_list_resources() -> List[Resource]:
|
async def handle_list_resources() -> List[Resource]:
|
||||||
"""List available resources."""
|
"""
|
||||||
|
List available MCP resources.
|
||||||
|
|
||||||
|
Returns a list of all available resources that can be accessed through
|
||||||
|
the MCP protocol. Each resource represents a collection of data that
|
||||||
|
can be read or queried.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Resource]: List of available resources with metadata:
|
||||||
|
- portainer://users: User management data
|
||||||
|
- portainer://settings: Configuration settings
|
||||||
|
- portainer://health: Server health status
|
||||||
|
|
||||||
|
Complexity: O(1) - Returns static resource list
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This is a discovery endpoint - no authentication required
|
||||||
|
"""
|
||||||
return [
|
return [
|
||||||
Resource(
|
Resource(
|
||||||
uri="portainer://users",
|
uri="portainer://users",
|
||||||
@ -84,7 +185,38 @@ class PortainerCoreMCPServer:
|
|||||||
|
|
||||||
@self.server.read_resource()
|
@self.server.read_resource()
|
||||||
async def handle_read_resource(uri: str) -> str:
|
async def handle_read_resource(uri: str) -> str:
|
||||||
"""Read a specific resource."""
|
"""
|
||||||
|
Read a specific MCP resource by URI.
|
||||||
|
|
||||||
|
Retrieves the content of a specific resource identified by its URI.
|
||||||
|
Different resource types return different data formats and may have
|
||||||
|
different authentication requirements.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
uri: Resource URI to read (e.g., 'portainer://users')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Resource content as string (usually JSON formatted)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PortainerError: If resource URI is unknown or invalid
|
||||||
|
PortainerAuthenticationError: If authentication is required but missing
|
||||||
|
PortainerNetworkError: If communication with Portainer fails
|
||||||
|
|
||||||
|
Supported URIs:
|
||||||
|
- portainer://health: Server health status (no auth required)
|
||||||
|
- portainer://users: User list (requires authentication)
|
||||||
|
- portainer://settings: Settings data (requires authentication)
|
||||||
|
|
||||||
|
Complexity: O(n) where n is the size of the resource data
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. Log resource access attempt
|
||||||
|
2. Route to appropriate resource handler
|
||||||
|
3. Ensure required services are initialized
|
||||||
|
4. Fetch and return resource data
|
||||||
|
5. Handle errors with appropriate error types
|
||||||
|
"""
|
||||||
with LogContext():
|
with LogContext():
|
||||||
logger.info("Reading resource", uri=uri)
|
logger.info("Reading resource", uri=uri)
|
||||||
|
|
||||||
@ -104,7 +236,37 @@ class PortainerCoreMCPServer:
|
|||||||
|
|
||||||
@self.server.list_tools()
|
@self.server.list_tools()
|
||||||
async def handle_list_tools() -> List[Tool]:
|
async def handle_list_tools() -> List[Tool]:
|
||||||
"""List available tools."""
|
"""
|
||||||
|
List available MCP tools.
|
||||||
|
|
||||||
|
Returns a comprehensive list of all available tools that can be executed
|
||||||
|
through the MCP protocol. Each tool includes its schema definition,
|
||||||
|
parameter requirements, and description.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[Tool]: List of available tools with complete schemas:
|
||||||
|
- Authentication tools: authenticate, generate_token, get_current_user
|
||||||
|
- User management tools: list_users, create_user, update_user, delete_user
|
||||||
|
- Settings tools: get_settings, update_settings
|
||||||
|
- Health tools: health_check
|
||||||
|
|
||||||
|
Tool Categories:
|
||||||
|
1. Authentication (3 tools): Login, token generation, current user
|
||||||
|
2. User Management (4 tools): Full CRUD operations for users
|
||||||
|
3. Settings (2 tools): Get and update configuration
|
||||||
|
4. Health (1 tool): Server health monitoring
|
||||||
|
|
||||||
|
Schema Features:
|
||||||
|
- JSON Schema validation for all parameters
|
||||||
|
- Required vs optional field definitions
|
||||||
|
- Type validation and constraints
|
||||||
|
- Human-readable descriptions
|
||||||
|
|
||||||
|
Complexity: O(1) - Returns static tool list
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This is a discovery endpoint - no authentication required
|
||||||
|
"""
|
||||||
return [
|
return [
|
||||||
Tool(
|
Tool(
|
||||||
name="authenticate",
|
name="authenticate",
|
||||||
@ -258,7 +420,46 @@ class PortainerCoreMCPServer:
|
|||||||
|
|
||||||
@self.server.call_tool()
|
@self.server.call_tool()
|
||||||
async def handle_call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
|
async def handle_call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]:
|
||||||
"""Handle tool calls."""
|
"""
|
||||||
|
Handle MCP tool execution requests.
|
||||||
|
|
||||||
|
Executes a specific tool with provided arguments and returns the result.
|
||||||
|
Each tool call is tracked with correlation IDs for debugging and logging.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Tool name to execute (e.g., 'authenticate', 'list_users')
|
||||||
|
arguments: Tool arguments as dictionary (validated against tool schema)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[TextContent]: Tool execution results wrapped in TextContent
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PortainerError: If tool name is unknown
|
||||||
|
PortainerValidationError: If arguments are invalid
|
||||||
|
PortainerAuthenticationError: If authentication is required but missing
|
||||||
|
PortainerNetworkError: If communication with Portainer fails
|
||||||
|
|
||||||
|
Tool Execution Flow:
|
||||||
|
1. Generate correlation ID for request tracing
|
||||||
|
2. Log tool execution attempt with arguments
|
||||||
|
3. Route to appropriate tool handler method
|
||||||
|
4. Ensure required services are initialized
|
||||||
|
5. Execute tool with argument validation
|
||||||
|
6. Return formatted result or error message
|
||||||
|
|
||||||
|
Error Handling:
|
||||||
|
- All exceptions are caught and logged
|
||||||
|
- Error messages are returned as TextContent
|
||||||
|
- Correlation IDs are preserved for debugging
|
||||||
|
- Service failures are gracefully handled
|
||||||
|
|
||||||
|
Complexity: O(f) where f is the complexity of the specific tool function
|
||||||
|
|
||||||
|
Security:
|
||||||
|
- Arguments are validated against tool schemas
|
||||||
|
- Authentication is enforced per tool requirements
|
||||||
|
- Sensitive data is not logged in arguments
|
||||||
|
"""
|
||||||
correlation_id = set_correlation_id()
|
correlation_id = set_correlation_id()
|
||||||
|
|
||||||
with LogContext(correlation_id):
|
with LogContext(correlation_id):
|
||||||
@ -297,7 +498,39 @@ class PortainerCoreMCPServer:
|
|||||||
return [TextContent(type="text", text=error_message)]
|
return [TextContent(type="text", text=error_message)]
|
||||||
|
|
||||||
async def _get_health_status(self) -> str:
|
async def _get_health_status(self) -> str:
|
||||||
"""Get server health status."""
|
"""
|
||||||
|
Get comprehensive server health status.
|
||||||
|
|
||||||
|
Performs health checks on all services and returns a detailed status report.
|
||||||
|
This method is used by both the health resource and health_check tool.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: JSON-formatted health status containing:
|
||||||
|
- Overall server status (healthy/degraded)
|
||||||
|
- Individual service statuses
|
||||||
|
- Server configuration details
|
||||||
|
- Portainer connection status
|
||||||
|
|
||||||
|
Health Status Levels:
|
||||||
|
- healthy: All services operational
|
||||||
|
- degraded: Some services failing
|
||||||
|
- unhealthy: Critical services failing
|
||||||
|
- not_initialized: Services not yet initialized
|
||||||
|
|
||||||
|
Complexity: O(s) where s is the number of services to check
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. Ensure all services are initialized
|
||||||
|
2. Check health of each service individually
|
||||||
|
3. Aggregate results into overall status
|
||||||
|
4. Include configuration and connection details
|
||||||
|
5. Return formatted status report
|
||||||
|
|
||||||
|
Error Handling:
|
||||||
|
- Service failures are caught and reported
|
||||||
|
- Partial failures don't prevent status reporting
|
||||||
|
- Connection errors are logged but don't crash health check
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
config = get_global_config()
|
config = get_global_config()
|
||||||
server_config = get_global_server_config()
|
server_config = get_global_server_config()
|
||||||
@ -340,7 +573,41 @@ class PortainerCoreMCPServer:
|
|||||||
return f"Health check failed: {str(e)}"
|
return f"Health check failed: {str(e)}"
|
||||||
|
|
||||||
async def _ensure_services_initialized(self) -> None:
|
async def _ensure_services_initialized(self) -> None:
|
||||||
"""Ensure all services are initialized."""
|
"""
|
||||||
|
Ensure all required services are properly initialized.
|
||||||
|
|
||||||
|
Implements lazy initialization pattern for services - they are only
|
||||||
|
initialized when first needed. This reduces startup time and resource
|
||||||
|
usage when services aren't required.
|
||||||
|
|
||||||
|
Services Initialized:
|
||||||
|
- AuthService: JWT authentication and token management
|
||||||
|
- UserService: User CRUD operations and management
|
||||||
|
- SettingsService: Configuration management
|
||||||
|
|
||||||
|
Initialization Process:
|
||||||
|
1. Check if service instance exists (None check)
|
||||||
|
2. Create service instance if needed
|
||||||
|
3. Call service.initialize() for setup
|
||||||
|
4. Service handles its own configuration and HTTP client setup
|
||||||
|
|
||||||
|
Complexity: O(1) per service - constant time initialization
|
||||||
|
|
||||||
|
Thread Safety:
|
||||||
|
- Safe for concurrent access (async/await pattern)
|
||||||
|
- Services handle their own initialization state
|
||||||
|
- No shared mutable state during initialization
|
||||||
|
|
||||||
|
Error Handling:
|
||||||
|
- Service initialization failures are propagated
|
||||||
|
- Partial initialization leaves other services unaffected
|
||||||
|
- Subsequent calls will retry failed initializations
|
||||||
|
|
||||||
|
Side Effects:
|
||||||
|
- Creates service instances
|
||||||
|
- Establishes HTTP client connections
|
||||||
|
- Validates service configurations
|
||||||
|
"""
|
||||||
if self.auth_service is None:
|
if self.auth_service is None:
|
||||||
self.auth_service = AuthService()
|
self.auth_service = AuthService()
|
||||||
await self.auth_service.initialize()
|
await self.auth_service.initialize()
|
||||||
@ -354,7 +621,31 @@ class PortainerCoreMCPServer:
|
|||||||
await self.settings_service.initialize()
|
await self.settings_service.initialize()
|
||||||
|
|
||||||
async def _get_users_resource(self) -> str:
|
async def _get_users_resource(self) -> str:
|
||||||
"""Get users resource."""
|
"""
|
||||||
|
Get users resource data.
|
||||||
|
|
||||||
|
Retrieves the complete list of users from Portainer for the users resource.
|
||||||
|
This method is called when the portainer://users resource is accessed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: JSON-formatted user list or error message
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PortainerAuthenticationError: If authentication is required but missing
|
||||||
|
PortainerNetworkError: If communication with Portainer fails
|
||||||
|
|
||||||
|
Complexity: O(n) where n is the number of users in the system
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. Ensure UserService is initialized
|
||||||
|
2. Call user_service.list_users() to fetch data
|
||||||
|
3. Convert result to string format
|
||||||
|
4. Handle errors gracefully with logging
|
||||||
|
|
||||||
|
Authentication:
|
||||||
|
- Requires valid authentication token
|
||||||
|
- Respects user permissions and RBAC
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
await self._ensure_services_initialized()
|
await self._ensure_services_initialized()
|
||||||
users = await self.user_service.list_users()
|
users = await self.user_service.list_users()
|
||||||
@ -364,7 +655,31 @@ class PortainerCoreMCPServer:
|
|||||||
return f"Failed to get users: {str(e)}"
|
return f"Failed to get users: {str(e)}"
|
||||||
|
|
||||||
async def _get_settings_resource(self) -> str:
|
async def _get_settings_resource(self) -> str:
|
||||||
"""Get settings resource."""
|
"""
|
||||||
|
Get settings resource data.
|
||||||
|
|
||||||
|
Retrieves the current Portainer settings for the settings resource.
|
||||||
|
This method is called when the portainer://settings resource is accessed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: JSON-formatted settings data or error message
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PortainerAuthenticationError: If authentication is required but missing
|
||||||
|
PortainerNetworkError: If communication with Portainer fails
|
||||||
|
|
||||||
|
Complexity: O(1) - Settings data is typically small and constant
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. Ensure SettingsService is initialized
|
||||||
|
2. Call settings_service.get_settings() to fetch data
|
||||||
|
3. Convert result to string format
|
||||||
|
4. Handle errors gracefully with logging
|
||||||
|
|
||||||
|
Authentication:
|
||||||
|
- Requires valid authentication token
|
||||||
|
- Admin privileges may be required for some settings
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
await self._ensure_services_initialized()
|
await self._ensure_services_initialized()
|
||||||
settings = await self.settings_service.get_settings()
|
settings = await self.settings_service.get_settings()
|
||||||
@ -374,11 +689,57 @@ class PortainerCoreMCPServer:
|
|||||||
return f"Failed to get settings: {str(e)}"
|
return f"Failed to get settings: {str(e)}"
|
||||||
|
|
||||||
async def _handle_health_check(self) -> str:
|
async def _handle_health_check(self) -> str:
|
||||||
"""Handle health check tool call."""
|
"""
|
||||||
|
Handle health check tool execution.
|
||||||
|
|
||||||
|
Wrapper method that delegates to _get_health_status() for consistency
|
||||||
|
between the health resource and health_check tool.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: JSON-formatted health status report
|
||||||
|
|
||||||
|
Complexity: O(s) where s is the number of services to check
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This is a convenience method to maintain consistency between
|
||||||
|
resource access and tool execution for health monitoring.
|
||||||
|
"""
|
||||||
return await self._get_health_status()
|
return await self._get_health_status()
|
||||||
|
|
||||||
async def _handle_authenticate(self, arguments: Dict[str, Any]) -> str:
|
async def _handle_authenticate(self, arguments: Dict[str, Any]) -> str:
|
||||||
"""Handle authentication tool call."""
|
"""
|
||||||
|
Handle user authentication tool execution.
|
||||||
|
|
||||||
|
Authenticates a user with username and password, returning a JWT token
|
||||||
|
for subsequent API calls.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
arguments: Dictionary containing:
|
||||||
|
- username (str): Username for authentication
|
||||||
|
- password (str): Password for authentication
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: JSON-formatted authentication result with JWT token
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PortainerValidationError: If username/password are missing
|
||||||
|
PortainerAuthenticationError: If credentials are invalid
|
||||||
|
PortainerNetworkError: If communication with Portainer fails
|
||||||
|
|
||||||
|
Complexity: O(1) - Authentication is a constant time operation
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. Ensure AuthService is initialized
|
||||||
|
2. Extract username and password from arguments
|
||||||
|
3. Call auth_service.login() with credentials
|
||||||
|
4. Log successful authentication (username only)
|
||||||
|
5. Return formatted authentication result
|
||||||
|
|
||||||
|
Security:
|
||||||
|
- Passwords are not logged
|
||||||
|
- Failed attempts are logged for security monitoring
|
||||||
|
- JWT tokens are returned securely
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
await self._ensure_services_initialized()
|
await self._ensure_services_initialized()
|
||||||
username = arguments.get("username")
|
username = arguments.get("username")
|
||||||
@ -392,7 +753,39 @@ class PortainerCoreMCPServer:
|
|||||||
return f"Authentication failed: {str(e)}"
|
return f"Authentication failed: {str(e)}"
|
||||||
|
|
||||||
async def _handle_generate_token(self, arguments: Dict[str, Any]) -> str:
|
async def _handle_generate_token(self, arguments: Dict[str, Any]) -> str:
|
||||||
"""Handle token generation tool call."""
|
"""
|
||||||
|
Handle API token generation tool execution.
|
||||||
|
|
||||||
|
Generates a new API token for a specified user that can be used for
|
||||||
|
programmatic access to the Portainer API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
arguments: Dictionary containing:
|
||||||
|
- user_id (int): User ID to generate token for
|
||||||
|
- description (str, optional): Token description (default: 'MCP Server Token')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: JSON-formatted token generation result with API token
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PortainerValidationError: If user_id is missing or invalid
|
||||||
|
PortainerAuthenticationError: If not authorized to generate tokens
|
||||||
|
PortainerNetworkError: If communication with Portainer fails
|
||||||
|
|
||||||
|
Complexity: O(1) - Token generation is a constant time operation
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. Ensure AuthService is initialized
|
||||||
|
2. Extract user_id and optional description from arguments
|
||||||
|
3. Call auth_service.generate_api_token() with parameters
|
||||||
|
4. Log successful token generation (user_id only)
|
||||||
|
5. Return formatted token result
|
||||||
|
|
||||||
|
Security:
|
||||||
|
- Tokens are generated with appropriate permissions
|
||||||
|
- Token creation is logged for audit purposes
|
||||||
|
- Only authorized users can generate tokens
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
await self._ensure_services_initialized()
|
await self._ensure_services_initialized()
|
||||||
user_id = arguments.get("user_id")
|
user_id = arguments.get("user_id")
|
||||||
@ -406,7 +799,33 @@ class PortainerCoreMCPServer:
|
|||||||
return f"Token generation failed: {str(e)}"
|
return f"Token generation failed: {str(e)}"
|
||||||
|
|
||||||
async def _handle_get_current_user(self, arguments: Dict[str, Any]) -> str:
|
async def _handle_get_current_user(self, arguments: Dict[str, Any]) -> str:
|
||||||
"""Handle get current user tool call."""
|
"""
|
||||||
|
Handle get current user tool execution.
|
||||||
|
|
||||||
|
Retrieves information about the currently authenticated user based on
|
||||||
|
the authentication token in the request context.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
arguments: Dictionary (empty - no arguments required)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: JSON-formatted current user information
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PortainerAuthenticationError: If authentication token is missing/invalid
|
||||||
|
PortainerNetworkError: If communication with Portainer fails
|
||||||
|
|
||||||
|
Complexity: O(1) - User lookup is a constant time operation
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. Ensure AuthService is initialized
|
||||||
|
2. Call auth_service.get_current_user() with current token
|
||||||
|
3. Return formatted user information
|
||||||
|
|
||||||
|
Authentication:
|
||||||
|
- Requires valid authentication token
|
||||||
|
- Returns user data based on token context
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
await self._ensure_services_initialized()
|
await self._ensure_services_initialized()
|
||||||
result = await self.auth_service.get_current_user()
|
result = await self.auth_service.get_current_user()
|
||||||
@ -416,7 +835,35 @@ class PortainerCoreMCPServer:
|
|||||||
return f"Get current user failed: {str(e)}"
|
return f"Get current user failed: {str(e)}"
|
||||||
|
|
||||||
async def _handle_list_users(self, arguments: Dict[str, Any]) -> str:
|
async def _handle_list_users(self, arguments: Dict[str, Any]) -> str:
|
||||||
"""Handle list users tool call."""
|
"""
|
||||||
|
Handle list users tool execution.
|
||||||
|
|
||||||
|
Retrieves a list of all users in the Portainer instance. The results
|
||||||
|
are filtered based on the current user's permissions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
arguments: Dictionary (empty - no arguments required)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: JSON-formatted list of users
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PortainerAuthenticationError: If authentication is required but missing
|
||||||
|
PortainerAuthorizationError: If user lacks permission to list users
|
||||||
|
PortainerNetworkError: If communication with Portainer fails
|
||||||
|
|
||||||
|
Complexity: O(n) where n is the number of users in the system
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. Ensure UserService is initialized
|
||||||
|
2. Call user_service.list_users() to fetch all users
|
||||||
|
3. Return formatted user list
|
||||||
|
|
||||||
|
Authorization:
|
||||||
|
- Requires authentication
|
||||||
|
- May require admin privileges depending on configuration
|
||||||
|
- Results filtered by user permissions
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
await self._ensure_services_initialized()
|
await self._ensure_services_initialized()
|
||||||
result = await self.user_service.list_users()
|
result = await self.user_service.list_users()
|
||||||
@ -426,7 +873,41 @@ class PortainerCoreMCPServer:
|
|||||||
return f"List users failed: {str(e)}"
|
return f"List users failed: {str(e)}"
|
||||||
|
|
||||||
async def _handle_create_user(self, arguments: Dict[str, Any]) -> str:
|
async def _handle_create_user(self, arguments: Dict[str, Any]) -> str:
|
||||||
"""Handle create user tool call."""
|
"""
|
||||||
|
Handle create user tool execution.
|
||||||
|
|
||||||
|
Creates a new user in the Portainer instance with the specified
|
||||||
|
username, password, and role.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
arguments: Dictionary containing:
|
||||||
|
- username (str): Username for the new user
|
||||||
|
- password (str): Password for the new user
|
||||||
|
- role (int): Role ID (1=Admin, 2=User)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: JSON-formatted created user information
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PortainerValidationError: If required fields are missing or invalid
|
||||||
|
PortainerAuthenticationError: If authentication is required but missing
|
||||||
|
PortainerAuthorizationError: If user lacks permission to create users
|
||||||
|
PortainerNetworkError: If communication with Portainer fails
|
||||||
|
|
||||||
|
Complexity: O(1) - User creation is a constant time operation
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. Ensure UserService is initialized
|
||||||
|
2. Extract username, password, and role from arguments
|
||||||
|
3. Call user_service.create_user() with parameters
|
||||||
|
4. Log successful user creation (username only)
|
||||||
|
5. Return formatted user information
|
||||||
|
|
||||||
|
Security:
|
||||||
|
- Passwords are validated but not logged
|
||||||
|
- User creation is logged for audit purposes
|
||||||
|
- Role assignments are validated
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
await self._ensure_services_initialized()
|
await self._ensure_services_initialized()
|
||||||
username = arguments.get("username")
|
username = arguments.get("username")
|
||||||
@ -441,7 +922,42 @@ class PortainerCoreMCPServer:
|
|||||||
return f"User creation failed: {str(e)}"
|
return f"User creation failed: {str(e)}"
|
||||||
|
|
||||||
async def _handle_update_user(self, arguments: Dict[str, Any]) -> str:
|
async def _handle_update_user(self, arguments: Dict[str, Any]) -> str:
|
||||||
"""Handle update user tool call."""
|
"""
|
||||||
|
Handle update user tool execution.
|
||||||
|
|
||||||
|
Updates an existing user's information including username, password,
|
||||||
|
and role. Only provided fields are updated (partial updates supported).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
arguments: Dictionary containing:
|
||||||
|
- user_id (int): User ID to update (required)
|
||||||
|
- username (str, optional): New username
|
||||||
|
- password (str, optional): New password
|
||||||
|
- role (int, optional): New role ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: JSON-formatted updated user information
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PortainerValidationError: If user_id is missing or invalid
|
||||||
|
PortainerAuthenticationError: If authentication is required but missing
|
||||||
|
PortainerAuthorizationError: If user lacks permission to update users
|
||||||
|
PortainerNetworkError: If communication with Portainer fails
|
||||||
|
|
||||||
|
Complexity: O(1) - User update is a constant time operation
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. Ensure UserService is initialized
|
||||||
|
2. Extract user_id and optional fields from arguments
|
||||||
|
3. Call user_service.update_user() with parameters
|
||||||
|
4. Log successful user update (user_id only)
|
||||||
|
5. Return formatted user information
|
||||||
|
|
||||||
|
Security:
|
||||||
|
- Passwords are validated but not logged
|
||||||
|
- User updates are logged for audit purposes
|
||||||
|
- Role changes are validated and logged
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
await self._ensure_services_initialized()
|
await self._ensure_services_initialized()
|
||||||
user_id = arguments.get("user_id")
|
user_id = arguments.get("user_id")
|
||||||
@ -457,7 +973,39 @@ class PortainerCoreMCPServer:
|
|||||||
return f"User update failed: {str(e)}"
|
return f"User update failed: {str(e)}"
|
||||||
|
|
||||||
async def _handle_delete_user(self, arguments: Dict[str, Any]) -> str:
|
async def _handle_delete_user(self, arguments: Dict[str, Any]) -> str:
|
||||||
"""Handle delete user tool call."""
|
"""
|
||||||
|
Handle delete user tool execution.
|
||||||
|
|
||||||
|
Deletes a user from the Portainer instance. This operation is
|
||||||
|
irreversible and removes all user data and permissions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
arguments: Dictionary containing:
|
||||||
|
- user_id (int): User ID to delete
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Success message confirming user deletion
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PortainerValidationError: If user_id is missing or invalid
|
||||||
|
PortainerAuthenticationError: If authentication is required but missing
|
||||||
|
PortainerAuthorizationError: If user lacks permission to delete users
|
||||||
|
PortainerNetworkError: If communication with Portainer fails
|
||||||
|
|
||||||
|
Complexity: O(1) - User deletion is a constant time operation
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. Ensure UserService is initialized
|
||||||
|
2. Extract user_id from arguments
|
||||||
|
3. Call user_service.delete_user() with user_id
|
||||||
|
4. Log successful user deletion
|
||||||
|
5. Return success confirmation message
|
||||||
|
|
||||||
|
Security:
|
||||||
|
- User deletion is logged for audit purposes
|
||||||
|
- Prevents self-deletion in service layer
|
||||||
|
- Requires appropriate permissions
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
await self._ensure_services_initialized()
|
await self._ensure_services_initialized()
|
||||||
user_id = arguments.get("user_id")
|
user_id = arguments.get("user_id")
|
||||||
@ -470,7 +1018,34 @@ class PortainerCoreMCPServer:
|
|||||||
return f"User deletion failed: {str(e)}"
|
return f"User deletion failed: {str(e)}"
|
||||||
|
|
||||||
async def _handle_get_settings(self, arguments: Dict[str, Any]) -> str:
|
async def _handle_get_settings(self, arguments: Dict[str, Any]) -> str:
|
||||||
"""Handle get settings tool call."""
|
"""
|
||||||
|
Handle get settings tool execution.
|
||||||
|
|
||||||
|
Retrieves the current Portainer instance settings including
|
||||||
|
authentication, templates, and other configuration options.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
arguments: Dictionary (empty - no arguments required)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: JSON-formatted settings data
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PortainerAuthenticationError: If authentication is required but missing
|
||||||
|
PortainerAuthorizationError: If user lacks permission to view settings
|
||||||
|
PortainerNetworkError: If communication with Portainer fails
|
||||||
|
|
||||||
|
Complexity: O(1) - Settings retrieval is a constant time operation
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. Ensure SettingsService is initialized
|
||||||
|
2. Call settings_service.get_settings() to fetch current settings
|
||||||
|
3. Return formatted settings data
|
||||||
|
|
||||||
|
Authorization:
|
||||||
|
- Requires authentication
|
||||||
|
- May require admin privileges for sensitive settings
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
await self._ensure_services_initialized()
|
await self._ensure_services_initialized()
|
||||||
result = await self.settings_service.get_settings()
|
result = await self.settings_service.get_settings()
|
||||||
@ -480,7 +1055,39 @@ class PortainerCoreMCPServer:
|
|||||||
return f"Get settings failed: {str(e)}"
|
return f"Get settings failed: {str(e)}"
|
||||||
|
|
||||||
async def _handle_update_settings(self, arguments: Dict[str, Any]) -> str:
|
async def _handle_update_settings(self, arguments: Dict[str, Any]) -> str:
|
||||||
"""Handle update settings tool call."""
|
"""
|
||||||
|
Handle update settings tool execution.
|
||||||
|
|
||||||
|
Updates Portainer instance settings with new configuration values.
|
||||||
|
Only provided settings are updated (partial updates supported).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
arguments: Dictionary containing:
|
||||||
|
- settings (dict): Settings to update as key-value pairs
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: JSON-formatted updated settings data
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PortainerValidationError: If settings format is invalid
|
||||||
|
PortainerAuthenticationError: If authentication is required but missing
|
||||||
|
PortainerAuthorizationError: If user lacks permission to update settings
|
||||||
|
PortainerNetworkError: If communication with Portainer fails
|
||||||
|
|
||||||
|
Complexity: O(1) - Settings update is a constant time operation
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. Ensure SettingsService is initialized
|
||||||
|
2. Extract settings dictionary from arguments
|
||||||
|
3. Call settings_service.update_settings() with new values
|
||||||
|
4. Log successful settings update
|
||||||
|
5. Return formatted updated settings
|
||||||
|
|
||||||
|
Security:
|
||||||
|
- Settings updates are logged for audit purposes
|
||||||
|
- Sensitive settings may require additional validation
|
||||||
|
- Only authorized users can modify settings
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
await self._ensure_services_initialized()
|
await self._ensure_services_initialized()
|
||||||
settings = arguments.get("settings")
|
settings = arguments.get("settings")
|
||||||
@ -493,7 +1100,39 @@ class PortainerCoreMCPServer:
|
|||||||
return f"Settings update failed: {str(e)}"
|
return f"Settings update failed: {str(e)}"
|
||||||
|
|
||||||
async def run(self) -> None:
|
async def run(self) -> None:
|
||||||
"""Run the MCP server."""
|
"""
|
||||||
|
Run the MCP server with full lifecycle management.
|
||||||
|
|
||||||
|
Starts the MCP server and handles the complete lifecycle including
|
||||||
|
service initialization, MCP protocol communication, and graceful shutdown.
|
||||||
|
|
||||||
|
The server runs until interrupted by user or system signal.
|
||||||
|
|
||||||
|
Lifecycle Flow:
|
||||||
|
1. Initialize all required services
|
||||||
|
2. Set up MCP protocol communication streams
|
||||||
|
3. Start server with initialization options
|
||||||
|
4. Handle requests until shutdown
|
||||||
|
5. Clean up resources and close connections
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PortainerConfigurationError: If configuration is invalid
|
||||||
|
PortainerError: If server fails to start
|
||||||
|
|
||||||
|
Complexity: O(∞) - Server runs indefinitely until shutdown
|
||||||
|
|
||||||
|
Error Handling:
|
||||||
|
- Service initialization failures prevent startup
|
||||||
|
- Communication errors are logged and handled
|
||||||
|
- Graceful shutdown on exceptions
|
||||||
|
- Resource cleanup is guaranteed
|
||||||
|
|
||||||
|
Side Effects:
|
||||||
|
- Initializes all services
|
||||||
|
- Establishes MCP protocol streams
|
||||||
|
- Starts background request processing
|
||||||
|
- Maintains active connections
|
||||||
|
"""
|
||||||
logger.info("Starting Portainer Core MCP Server")
|
logger.info("Starting Portainer Core MCP Server")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -519,7 +1158,31 @@ class PortainerCoreMCPServer:
|
|||||||
await self._cleanup_services()
|
await self._cleanup_services()
|
||||||
|
|
||||||
async def _cleanup_services(self) -> None:
|
async def _cleanup_services(self) -> None:
|
||||||
"""Clean up service resources."""
|
"""
|
||||||
|
Clean up service resources during shutdown.
|
||||||
|
|
||||||
|
Properly shuts down all services and releases their resources including
|
||||||
|
HTTP connections, authentication tokens, and any background tasks.
|
||||||
|
|
||||||
|
Cleanup Process:
|
||||||
|
1. Call cleanup() on AuthService if initialized
|
||||||
|
2. Call cleanup() on UserService if initialized
|
||||||
|
3. Call cleanup() on SettingsService if initialized
|
||||||
|
4. Each service handles its own resource cleanup
|
||||||
|
|
||||||
|
Complexity: O(s) where s is the number of initialized services
|
||||||
|
|
||||||
|
Error Handling:
|
||||||
|
- Service cleanup errors are logged but don't prevent other cleanups
|
||||||
|
- Cleanup is attempted for all services regardless of individual failures
|
||||||
|
- Resources are released even if cleanup methods fail
|
||||||
|
|
||||||
|
Side Effects:
|
||||||
|
- Closes HTTP client connections
|
||||||
|
- Invalidates authentication tokens
|
||||||
|
- Stops background tasks
|
||||||
|
- Releases system resources
|
||||||
|
"""
|
||||||
if self.auth_service:
|
if self.auth_service:
|
||||||
await self.auth_service.cleanup()
|
await self.auth_service.cleanup()
|
||||||
if self.user_service:
|
if self.user_service:
|
||||||
@ -529,12 +1192,64 @@ class PortainerCoreMCPServer:
|
|||||||
|
|
||||||
|
|
||||||
def create_server() -> PortainerCoreMCPServer:
|
def create_server() -> PortainerCoreMCPServer:
|
||||||
"""Create and return a configured MCP server instance."""
|
"""
|
||||||
|
Create and return a configured MCP server instance.
|
||||||
|
|
||||||
|
Factory function that creates a new PortainerCoreMCPServer instance with
|
||||||
|
default configuration from environment variables.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PortainerCoreMCPServer: Configured server instance ready to run
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PortainerConfigurationError: If required environment variables are missing
|
||||||
|
|
||||||
|
Complexity: O(1) - Server creation is constant time
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
```python
|
||||||
|
server = create_server()
|
||||||
|
await server.run()
|
||||||
|
```
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This is the preferred way to create server instances as it ensures
|
||||||
|
proper configuration and initialization order.
|
||||||
|
"""
|
||||||
return PortainerCoreMCPServer()
|
return PortainerCoreMCPServer()
|
||||||
|
|
||||||
|
|
||||||
async def main() -> None:
|
async def main() -> None:
|
||||||
"""Main entry point for the MCP server."""
|
"""
|
||||||
|
Main entry point for the MCP server.
|
||||||
|
|
||||||
|
Async main function that handles server creation, execution, and shutdown.
|
||||||
|
This function is called by the run_server.py script or when the module
|
||||||
|
is run directly.
|
||||||
|
|
||||||
|
Lifecycle:
|
||||||
|
1. Create server instance using factory function
|
||||||
|
2. Run server until completion or interruption
|
||||||
|
3. Handle graceful shutdown on KeyboardInterrupt
|
||||||
|
4. Log errors and re-raise for proper exit codes
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
PortainerError: If server fails to start or run
|
||||||
|
KeyboardInterrupt: Re-raised after logging for proper shutdown
|
||||||
|
|
||||||
|
Complexity: O(∞) - Runs until shutdown
|
||||||
|
|
||||||
|
Error Handling:
|
||||||
|
- KeyboardInterrupt: Logged as user-initiated shutdown
|
||||||
|
- Other exceptions: Logged as server failures and re-raised
|
||||||
|
- Ensures proper exit codes for process management
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
asyncio.run(main())
|
||||||
|
```
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
server = create_server()
|
server = create_server()
|
||||||
await server.run()
|
await server.run()
|
||||||
|
56
test_uvx.py
Normal file
56
test_uvx.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script to verify uvx functionality.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
def test_uvx():
|
||||||
|
"""Test that uvx can run the application."""
|
||||||
|
print("🧪 Testing uvx functionality...")
|
||||||
|
|
||||||
|
# Set test environment variables
|
||||||
|
env = os.environ.copy()
|
||||||
|
env['PORTAINER_URL'] = 'https://demo.portainer.io'
|
||||||
|
env['PORTAINER_API_KEY'] = 'demo-key'
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Test uvx --help first
|
||||||
|
result = subprocess.run([
|
||||||
|
'uvx', '--help'
|
||||||
|
], capture_output=True, text=True, timeout=10)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
print("❌ uvx not available")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("✅ uvx is available")
|
||||||
|
|
||||||
|
# Test that our package can be found
|
||||||
|
print("📦 Testing package discovery...")
|
||||||
|
result = subprocess.run([
|
||||||
|
'uvx', '--from', '.', 'portainer-core-mcp', '--help'
|
||||||
|
], capture_output=True, text=True, timeout=10, env=env)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"❌ Package test failed: {result.stderr}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("✅ Package can be discovered by uvx")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
print("❌ uvx test timed out")
|
||||||
|
return False
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("❌ uvx not found - install with: pip install uv")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ uvx test failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = test_uvx()
|
||||||
|
sys.exit(0 if success else 1)
|
Loading…
Reference in New Issue
Block a user