Compare commits
No commits in common. "main" and "feature/oauth-authentication" have entirely different histories.
main
...
feature/oa
@ -1,82 +0,0 @@
|
||||
# mcvsphere OAuth Configuration Template
|
||||
# Copy to .env and configure for your environment
|
||||
#
|
||||
# Usage:
|
||||
# cp .env.oauth.example .env
|
||||
# # Edit .env with your values
|
||||
# docker compose -f docker-compose.oauth-standalone.yml up -d
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Docker Compose
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
COMPOSE_PROJECT_NAME=mcvsphere
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# vCenter Connection (Required)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
VCENTER_HOST=vcenter.example.com
|
||||
VCENTER_USER=mcpservice@vsphere.local
|
||||
VCENTER_PASSWORD=your-secure-password
|
||||
|
||||
# Optional: Skip SSL verification (dev only - use false in production)
|
||||
VCENTER_INSECURE=false
|
||||
|
||||
# Optional: Specify defaults (auto-detected if not set)
|
||||
# VCENTER_DATACENTER=Datacenter
|
||||
# VCENTER_CLUSTER=Cluster
|
||||
# VCENTER_DATASTORE=datastore1
|
||||
# VCENTER_NETWORK=VM Network
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# MCP Transport (Required for OAuth)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
MCP_TRANSPORT=streamable-http
|
||||
MCP_HOST=0.0.0.0
|
||||
MCP_PORT=8080
|
||||
|
||||
# Your public domain (must match Caddy proxy)
|
||||
MCP_DOMAIN=mcp.example.com
|
||||
|
||||
# TLS mode: 'internal' for self-signed (dev), remove for auto HTTPS (prod)
|
||||
MCP_TLS_MODE=internal
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# OAuth / OIDC Provider (Required)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
OAUTH_ENABLED=true
|
||||
|
||||
# OIDC Discovery URL (ends with /.well-known/openid-configuration)
|
||||
# Examples:
|
||||
# Authentik: https://auth.example.com/application/o/mcvsphere/
|
||||
# Keycloak: https://keycloak.example.com/realms/myrealm
|
||||
# Auth0: https://myapp.auth0.com/
|
||||
# Okta: https://myorg.okta.com/oauth2/default
|
||||
OAUTH_ISSUER_URL=https://auth.example.com/application/o/mcvsphere/
|
||||
|
||||
# OAuth Client Credentials (from your OIDC provider)
|
||||
OAUTH_CLIENT_ID=your-client-id
|
||||
OAUTH_CLIENT_SECRET=your-client-secret
|
||||
|
||||
# Public callback URL (must be accessible from browser)
|
||||
OAUTH_BASE_URL=https://mcp.example.com
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# RBAC Permission Groups
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Create these groups in your OIDC provider and assign users:
|
||||
#
|
||||
# | Group | Access Level |
|
||||
# |------------------------|---------------------------------|
|
||||
# | vsphere-super-admins | Full control (all 94 tools) |
|
||||
# | vsphere-host-admins | Host operations + VM management |
|
||||
# | vsphere-admins | VM lifecycle management |
|
||||
# | vsphere-operators | Power ops + snapshots |
|
||||
# | vsphere-readers | Read-only |
|
||||
#
|
||||
# Users without any vsphere-* group will be denied access (default-deny).
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Optional Settings
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# LOG_LEVEL=INFO
|
||||
# MCVSPHERE_VERSION=0.2.2
|
||||
14
Dockerfile
14
Dockerfile
@ -48,11 +48,8 @@ ENV PATH="/app/.venv/bin:$PATH"
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
# Default transport - override with MCP_TRANSPORT env var
|
||||
# stdio: Direct CLI usage (default for local)
|
||||
# sse: Server-Sent Events (legacy HTTP)
|
||||
# streamable-http: HTTP with OAuth support (required for multi-user)
|
||||
ENV MCP_TRANSPORT=streamable-http
|
||||
# Default to SSE transport for Docker
|
||||
ENV MCP_TRANSPORT=sse
|
||||
ENV MCP_HOST=0.0.0.0
|
||||
ENV MCP_PORT=8080
|
||||
|
||||
@ -61,9 +58,10 @@ USER mcpuser
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
# Health check - works with both SSE and streamable-http
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/.well-known/oauth-authorization-server')" || exit 1
|
||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080')" || exit 1
|
||||
|
||||
# Run the MCP server - transport configured via environment
|
||||
# Run the MCP server
|
||||
ENTRYPOINT ["mcvsphere"]
|
||||
CMD ["--transport", "sse"]
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
# OAuth & RBAC Architecture for mcvsphere
|
||||
# OAuth Architecture for vSphere MCP Server
|
||||
|
||||
## Overview
|
||||
## The Problem
|
||||
|
||||
mcvsphere supports multi-user OAuth 2.1 authentication with Role-Based Access Control (RBAC). This enables:
|
||||
We need to add authentication to the MCP server so that:
|
||||
1. Users authenticate via **OAuth 2.1 / OIDC** (using Authentik as IdP)
|
||||
2. The MCP server knows WHO is making requests (for audit logging)
|
||||
3. vCenter permissions are respected per-user
|
||||
|
||||
1. **Single Sign-On** via any OIDC provider (Authentik, Keycloak, Auth0, etc.)
|
||||
2. **User Identity** for audit logging - know WHO made each request
|
||||
3. **Group-Based Permissions** - control what users can do based on OAuth groups
|
||||
4. **Audit Trail** - every tool invocation logged with user identity and timing
|
||||
**Challenge:** vCenter 7.0.3 doesn't support OAuth token exchange (RFC 8693), so we can't pass OAuth tokens directly to vCenter.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
@ -22,352 +22,360 @@ mcvsphere supports multi-user OAuth 2.1 authentication with Role-Based Access Co
|
||||
│ (browser opens for login)
|
||||
│
|
||||
┌────────────────────────────▼────────────────────────────────────┐
|
||||
│ OIDC Provider │
|
||||
│ (Authentik, Keycloak, Auth0, etc.) │
|
||||
│ Authentik │
|
||||
│ (Self-hosted OIDC IdP) │
|
||||
│ │
|
||||
│ - Issues JWT access tokens │
|
||||
│ - Validates user credentials │
|
||||
│ - Includes groups claim in token │
|
||||
│ - Includes user identity in token (sub, email, groups) │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
│ 2. JWT Bearer token
|
||||
│ Authorization: Bearer <jwt>
|
||||
│
|
||||
┌────────────────────────────▼────────────────────────────────────┐
|
||||
│ mcvsphere │
|
||||
│ vSphere MCP Server │
|
||||
│ (FastMCP + pyvmomi) │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ OIDCProxy (FastMCP) │ │
|
||||
│ │ - Validates JWT signature via JWKS endpoint │ │
|
||||
│ │ - Extracts user identity (preferred_username, email) │ │
|
||||
│ │ - Extracts groups from token claims │ │
|
||||
│ │ - Validates JWT signature via Authentik JWKS │ │
|
||||
│ │ - Extracts user identity (preferred_username) │ │
|
||||
│ │ - Makes user available via ctx.request_context.user │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ RBACMiddleware │ │
|
||||
│ │ - Intercepts ALL tool calls via on_call_tool() │ │
|
||||
│ │ - Maps OAuth groups → Permission levels │ │
|
||||
│ │ - Denies access if user lacks required permission │ │
|
||||
│ │ - Logs audit events with user identity │ │
|
||||
│ │ Credential Broker │ │
|
||||
│ │ - Maps OAuth user → vCenter credentials │ │
|
||||
│ │ - Caches pyvmomi connections per-user │ │
|
||||
│ │ - Retrieves passwords from Vault / env vars │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ VMware Tools (94) │ │
|
||||
│ │ - Execute vCenter/ESXi operations via pyvmomi │ │
|
||||
│ │ - Single service account connection to vCenter │ │
|
||||
│ │ Audit Logger │ │
|
||||
│ │ - Logs all tool invocations with OAuth identity │ │
|
||||
│ │ - "User ryan@example.com powered on VM web-server" │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
└────────────────────────────┬────────────────────────────────────┘
|
||||
│
|
||||
│ 3. pyvmomi (service account)
|
||||
│ 3. pyvmomi (as mapped user)
|
||||
│
|
||||
┌────────────────────────────▼────────────────────────────────────┐
|
||||
│ vCenter / ESXi │
|
||||
│ - Receives API calls as service account │
|
||||
│ - mcvsphere audit logs show real user identity │
|
||||
│ vCenter 7.0.3 │
|
||||
│ - Receives API calls as the actual user │
|
||||
│ - Native audit logs show real user identity │
|
||||
│ - vCenter permissions apply naturally │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## RBAC Permission Model
|
||||
## User Mapping Strategies
|
||||
|
||||
### Permission Levels
|
||||
Since we can't exchange OAuth tokens for vCenter tokens, we need a "credential broker":
|
||||
|
||||
mcvsphere defines 5 permission levels, from least to most privileged:
|
||||
| Strategy | How it Works | Security | Use Case |
|
||||
|----------|--------------|----------|----------|
|
||||
| **Service Account** | All requests use one vCenter admin account | Medium | Simple/dev |
|
||||
| **Per-User Mapping** | Map OAuth username → vCenter credentials from Vault | High | Production |
|
||||
| **LDAP Sync** | Same username/password in Authentik and vCenter SSO | Medium | AD environments |
|
||||
|
||||
| Level | Description | Example Tools |
|
||||
|-------|-------------|---------------|
|
||||
| `READ_ONLY` | View-only operations | `list_vms`, `get_vm_info`, `vm_screenshot` |
|
||||
| `POWER_OPS` | Power and snapshot operations | `power_on`, `create_snapshot`, `reboot_guest` |
|
||||
| `VM_LIFECYCLE` | Create/delete/modify VMs | `create_vm`, `clone_vm`, `add_disk`, `deploy_ovf` |
|
||||
| `HOST_ADMIN` | ESXi host operations | `reboot_host`, `enter_maintenance_mode` |
|
||||
| `FULL_ADMIN` | Everything including guest OS ops | `run_command_in_guest`, `restart_service` |
|
||||
|
||||
### OAuth Groups → Permissions
|
||||
|
||||
Users are granted permissions based on their OAuth group memberships:
|
||||
|
||||
| OAuth Group | Permissions Granted |
|
||||
|-------------|---------------------|
|
||||
| `vsphere-readers` | READ_ONLY |
|
||||
| `vsphere-operators` | READ_ONLY, POWER_OPS |
|
||||
| `vsphere-admins` | READ_ONLY, POWER_OPS, VM_LIFECYCLE |
|
||||
| `vsphere-host-admins` | READ_ONLY, POWER_OPS, VM_LIFECYCLE, HOST_ADMIN |
|
||||
| `vsphere-super-admins` | ALL (full access) |
|
||||
|
||||
**Security Note:** Users with NO recognized groups are denied ALL access. There is no default permission.
|
||||
|
||||
### Tool → Permission Mapping
|
||||
|
||||
All 94 tools are mapped to permission levels in `src/mcvsphere/permissions.py`:
|
||||
### Recommended: Per-User Mapping with Fallback
|
||||
|
||||
```python
|
||||
# READ_ONLY - 32 tools
|
||||
"list_vms", "get_vm_info", "list_snapshots", "get_vm_stats", ...
|
||||
class CredentialBroker:
|
||||
"""Maps OAuth users to vCenter credentials."""
|
||||
|
||||
# POWER_OPS - 14 tools
|
||||
"power_on", "power_off", "create_snapshot", "revert_to_snapshot", ...
|
||||
def __init__(self, vcenter_host: str, fallback_user: str = None, fallback_password: str = None):
|
||||
self.vcenter_host = vcenter_host
|
||||
self.fallback_user = fallback_user # Service account fallback
|
||||
self.fallback_password = fallback_password
|
||||
self._connections: dict[str, ServiceInstance] = {}
|
||||
|
||||
# VM_LIFECYCLE - 33 tools
|
||||
"create_vm", "clone_vm", "delete_vm", "add_disk", "deploy_ovf", ...
|
||||
def get_connection_for_user(self, oauth_user: dict) -> ServiceInstance:
|
||||
"""Get pyvmomi connection for this OAuth user."""
|
||||
username = oauth_user.get("preferred_username")
|
||||
|
||||
# HOST_ADMIN - 6 tools
|
||||
"enter_maintenance_mode", "reboot_host", "shutdown_host", ...
|
||||
# Try per-user credentials first
|
||||
try:
|
||||
vcenter_creds = self._lookup_credentials(username)
|
||||
return self._get_or_create_connection(
|
||||
vcenter_creds["user"],
|
||||
vcenter_creds["password"]
|
||||
)
|
||||
except KeyError:
|
||||
# Fall back to service account
|
||||
if self.fallback_user:
|
||||
return self._get_or_create_connection(
|
||||
self.fallback_user,
|
||||
self.fallback_password
|
||||
)
|
||||
raise ValueError(f"No vCenter credentials for user: {username}")
|
||||
|
||||
# FULL_ADMIN - 11 tools
|
||||
"run_command_in_guest", "write_guest_file", "restart_service", ...
|
||||
def _lookup_credentials(self, username: str) -> dict:
|
||||
"""Look up vCenter credentials for OAuth user."""
|
||||
# Option 1: Environment variable
|
||||
env_key = f"VCENTER_PASSWORD_{username.upper().replace('@', '_').replace('.', '_')}"
|
||||
if password := os.environ.get(env_key):
|
||||
return {"user": f"{username}@vsphere.local", "password": password}
|
||||
|
||||
# Option 2: HashiCorp Vault (production)
|
||||
# return vault_client.read(f"secret/vcenter/users/{username}")
|
||||
|
||||
raise KeyError(f"No credentials found for {username}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Details
|
||||
## FastMCP OAuth Integration
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/mcvsphere/auth.py` | OIDCProxy configuration |
|
||||
| `src/mcvsphere/permissions.py` | Permission levels and tool mappings |
|
||||
| `src/mcvsphere/middleware.py` | RBACMiddleware implementation |
|
||||
| `src/mcvsphere/audit.py` | Audit logging with user context |
|
||||
| `src/mcvsphere/server.py` | Server setup with OAuth + RBAC |
|
||||
|
||||
### RBACMiddleware Flow
|
||||
### 1. Add OIDCProxy to server.py
|
||||
|
||||
```python
|
||||
class RBACMiddleware(Middleware):
|
||||
"""Intercepts all tool calls to enforce permissions."""
|
||||
import os
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth import OIDCProxy
|
||||
|
||||
async def on_call_tool(self, context, call_next):
|
||||
# 1. Extract user from OAuth token
|
||||
claims = self._extract_user_from_context(context.fastmcp_context)
|
||||
username = claims.get("preferred_username", "unknown")
|
||||
groups = claims.get("groups", [])
|
||||
# Configure OAuth with Authentik
|
||||
auth = OIDCProxy(
|
||||
# Authentik OIDC Discovery URL
|
||||
config_url=os.environ["AUTHENTIK_OIDC_URL"],
|
||||
# e.g., "https://auth.example.com/application/o/vsphere-mcp/.well-known/openid-configuration"
|
||||
|
||||
# 2. Check permission
|
||||
tool_name = context.message.name
|
||||
if not check_permission(tool_name, groups):
|
||||
required = get_required_permission(tool_name)
|
||||
audit_permission_denied(tool_name, {...}, required.value)
|
||||
raise PermissionDeniedError(username, tool_name, required)
|
||||
# Application credentials from Authentik
|
||||
client_id=os.environ["AUTHENTIK_CLIENT_ID"],
|
||||
client_secret=os.environ["AUTHENTIK_CLIENT_SECRET"],
|
||||
|
||||
# 3. Execute tool with timing
|
||||
start = time.perf_counter()
|
||||
result = await call_next(context)
|
||||
duration_ms = (time.perf_counter() - start) * 1000
|
||||
# MCP Server base URL (for redirects)
|
||||
base_url=os.environ.get("MCP_BASE_URL", "http://localhost:8000"),
|
||||
|
||||
# 4. Audit log
|
||||
audit_log(tool_name, {...}, result="success", duration_ms=duration_ms)
|
||||
return result
|
||||
# Token validation
|
||||
required_scopes=["openid", "profile", "email"],
|
||||
|
||||
# Allow Claude Code localhost redirects
|
||||
allowed_client_redirect_uris=["http://localhost:*", "http://127.0.0.1:*"],
|
||||
)
|
||||
|
||||
# Create MCP server with OAuth
|
||||
mcp = FastMCP(
|
||||
"vSphere MCP Server",
|
||||
auth=auth,
|
||||
# Use Streamable HTTP transport for OAuth
|
||||
)
|
||||
```
|
||||
|
||||
### Audit Log Format
|
||||
### 2. Access User Identity in Tools
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-12-27T08:15:32.123456+00:00",
|
||||
"user": "ryan@example.com",
|
||||
"groups": ["vsphere-admins", "vsphere-operators"],
|
||||
"tool": "power_on",
|
||||
"args": {"vm_name": "web-server"},
|
||||
"duration_ms": 1234.56,
|
||||
"result": "success"
|
||||
}
|
||||
```
|
||||
```python
|
||||
from fastmcp import Context
|
||||
|
||||
Permission denied events:
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-12-27T08:15:32.123456+00:00",
|
||||
"user": "guest@example.com",
|
||||
"groups": ["vsphere-readers"],
|
||||
"tool": "delete_vm",
|
||||
"args": {"vm_name": "web-server"},
|
||||
"required_permission": "vm_lifecycle",
|
||||
"event": "PERMISSION_DENIED"
|
||||
}
|
||||
@mcp.tool()
|
||||
async def power_on_vm(ctx: Context, vm_name: str) -> str:
|
||||
"""Power on a virtual machine."""
|
||||
# Get authenticated user from OAuth token
|
||||
user = ctx.request_context.user
|
||||
username = user.get("preferred_username", user.get("sub"))
|
||||
|
||||
# Get vCenter connection for this user
|
||||
broker = get_credential_broker()
|
||||
connection = broker.get_connection_for_user(user)
|
||||
|
||||
# Execute operation
|
||||
content = connection.RetrieveContent()
|
||||
vm = find_vm(content, vm_name)
|
||||
vm.PowerOnVM_Task()
|
||||
|
||||
# Audit log
|
||||
logger.info(f"User {username} powered on VM {vm_name}")
|
||||
|
||||
return f"VM '{vm_name}' is powering on"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
## MCP Transport: Streamable HTTP
|
||||
|
||||
### Environment Variables
|
||||
OAuth requires HTTP transport (not stdio). Use Streamable HTTP:
|
||||
|
||||
```python
|
||||
# In server.py or via environment
|
||||
mcp = FastMCP(
|
||||
"vSphere MCP Server",
|
||||
auth=auth,
|
||||
)
|
||||
|
||||
def main():
|
||||
# Run with HTTP transport for OAuth support
|
||||
mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
**Transport characteristics:**
|
||||
- Single HTTP endpoint (`/mcp`)
|
||||
- POST requests with JSON-RPC body
|
||||
- Server-Sent Events (SSE) for streaming responses
|
||||
- `Authorization: Bearer <token>` header on every request
|
||||
- `Mcp-Session-Id` header for session continuity
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# OAuth Configuration
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
OAUTH_ENABLED=true
|
||||
OAUTH_ISSUER_URL=https://auth.example.com/application/o/mcvsphere/
|
||||
OAUTH_CLIENT_ID=<from-oidc-provider>
|
||||
OAUTH_CLIENT_SECRET=<from-oidc-provider>
|
||||
OAUTH_BASE_URL=https://mcp.example.com # Public URL for callbacks
|
||||
OAUTH_SCOPES='["openid", "profile", "email", "groups"]'
|
||||
# Authentik OIDC
|
||||
AUTHENTIK_OIDC_URL=https://auth.example.com/application/o/vsphere-mcp/.well-known/openid-configuration
|
||||
AUTHENTIK_CLIENT_ID=<from-authentik-application>
|
||||
AUTHENTIK_CLIENT_SECRET=<from-authentik-application>
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Transport (must be HTTP for OAuth)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# MCP Server
|
||||
MCP_BASE_URL=https://mcp.example.com # Public URL for OAuth redirects
|
||||
MCP_TRANSPORT=streamable-http
|
||||
MCP_HOST=0.0.0.0
|
||||
MCP_PORT=8080
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# vCenter Connection (service account)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# vCenter Connection (service account fallback)
|
||||
VCENTER_HOST=vcenter.example.com
|
||||
VCENTER_USER=svc-mcvsphere@vsphere.local
|
||||
VCENTER_USER=svc-mcp@vsphere.local
|
||||
VCENTER_PASSWORD=<service-account-password>
|
||||
VCENTER_INSECURE=false
|
||||
VCENTER_INSECURE=true
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Optional
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
LOG_LEVEL=INFO
|
||||
```
|
||||
# Per-user credentials (optional, for testing)
|
||||
VCENTER_PASSWORD_RYAN=<ryan's-vcenter-password>
|
||||
VCENTER_PASSWORD_ALICE=<alice's-vcenter-password>
|
||||
|
||||
### Server Startup Banner
|
||||
|
||||
When OAuth is enabled, the server shows:
|
||||
```
|
||||
mcvsphere v0.2.1
|
||||
────────────────────────────────────────
|
||||
Starting HTTP transport on 0.0.0.0:8080
|
||||
OAuth: ENABLED via https://auth.example.com/application/o/mcvsphere/
|
||||
RBAC: ENABLED - permissions enforced via groups
|
||||
────────────────────────────────────────
|
||||
# User Mapping Mode
|
||||
USER_MAPPING_MODE=service_account # or 'per_user', 'ldap_sync'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## OIDC Provider Setup
|
||||
|
||||
### Authentik (Recommended)
|
||||
## Authentik Setup (Quick Reference)
|
||||
|
||||
1. **Create OAuth2/OIDC Provider:**
|
||||
- Name: `mcvsphere`
|
||||
- Client Type: **Confidential**
|
||||
- Name: `vsphere-mcp`
|
||||
- Client Type: Confidential
|
||||
- Redirect URIs:
|
||||
- `http://localhost:*/callback` (for local dev)
|
||||
- `http://localhost:*/callback`
|
||||
- `https://mcp.example.com/auth/callback`
|
||||
- Signing Key: Select RS256 certificate
|
||||
- Scopes: `openid`, `profile`, `email`
|
||||
- Signing Key: Select or create RS256 key
|
||||
|
||||
2. **Create Application:**
|
||||
- Name: `mcvsphere`
|
||||
- Slug: `mcvsphere`
|
||||
- Provider: Select provider from step 1
|
||||
- Name: `vSphere MCP Server`
|
||||
- Slug: `vsphere-mcp`
|
||||
- Provider: Select the provider above
|
||||
- Note the **Client ID** and **Client Secret**
|
||||
|
||||
3. **Create Groups:**
|
||||
- `vsphere-readers`
|
||||
- `vsphere-operators`
|
||||
- `vsphere-admins`
|
||||
- `vsphere-host-admins`
|
||||
- `vsphere-super-admins`
|
||||
|
||||
4. **Add Scope Mapping for Groups:**
|
||||
- Ensure `groups` claim is included in tokens
|
||||
- Authentik includes this by default
|
||||
|
||||
5. **Note Credentials:**
|
||||
- Copy Client ID and Client Secret
|
||||
- Discovery URL: `https://auth.example.com/application/o/mcvsphere/.well-known/openid-configuration`
|
||||
|
||||
### Other Providers
|
||||
|
||||
The same pattern works with Keycloak, Auth0, Okta, etc. Key requirements:
|
||||
- OIDC Discovery endpoint (`.well-known/openid-configuration`)
|
||||
- JWT access tokens (not opaque)
|
||||
- `groups` claim in tokens with group names
|
||||
3. **Configure Groups (optional):**
|
||||
- `vsphere-admins` - Full access
|
||||
- `vsphere-operators` - Limited access
|
||||
- Groups are included in JWT `groups` claim
|
||||
|
||||
---
|
||||
|
||||
## OAuth Flow
|
||||
## OAuth Flow (End-to-End)
|
||||
|
||||
```
|
||||
1. Client connects to mcvsphere
|
||||
→ POST /mcp (no auth)
|
||||
→ Server returns 401 + OAuth metadata URL
|
||||
1. Claude Code connects to MCP Server
|
||||
→ GET /mcp
|
||||
→ Server returns 401 Unauthorized
|
||||
→ WWW-Authenticate header includes OAuth metadata URL
|
||||
|
||||
2. Client initiates OAuth flow
|
||||
→ Opens browser to OIDC provider
|
||||
→ User logs in
|
||||
→ Provider redirects with authorization code
|
||||
2. Claude Code fetches OAuth metadata
|
||||
→ Discovers Authentik authorization URL
|
||||
→ Discovers required scopes
|
||||
|
||||
3. Client exchanges code for tokens
|
||||
→ POST to provider token endpoint
|
||||
→ Receives JWT access token
|
||||
3. Claude Code initiates OAuth flow
|
||||
→ Opens browser to Authentik login page
|
||||
→ User enters credentials
|
||||
→ Authentik redirects back with authorization code
|
||||
|
||||
4. Client reconnects with token
|
||||
4. Claude Code exchanges code for tokens
|
||||
→ POST to Authentik token endpoint
|
||||
→ Receives JWT access token + refresh token
|
||||
|
||||
5. Claude Code reconnects with Bearer token
|
||||
→ POST /mcp with Authorization: Bearer <jwt>
|
||||
→ Server validates JWT via JWKS
|
||||
→ Server extracts user + groups
|
||||
→ RBACMiddleware checks permissions
|
||||
→ User can invoke allowed tools
|
||||
→ Server validates JWT via Authentik JWKS
|
||||
→ Server extracts user identity
|
||||
→ User can now invoke tools
|
||||
|
||||
5. Tool invocation
|
||||
→ Client: "power on web-server"
|
||||
→ Middleware: Validate user has POWER_OPS
|
||||
→ Tool: Execute pyvmomi call
|
||||
→ Audit: Log with user identity
|
||||
→ Client: Receive response
|
||||
6. Tool invocation
|
||||
→ Client: "power on web-server VM"
|
||||
→ Server: Validates token, maps user to vCenter creds
|
||||
→ Server: Executes pyvmomi call
|
||||
→ Server: Logs "User ryan@example.com powered on web-server"
|
||||
→ Client: Receives success response
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Status
|
||||
## Implementation Checklist
|
||||
|
||||
### Completed
|
||||
### Phase 1: Prepare Server
|
||||
- [ ] Add `fastmcp[auth]` to dependencies
|
||||
- [ ] Create `auth.py` with OIDCProxy configuration
|
||||
- [ ] Create `credential_broker.py` for user mapping
|
||||
- [ ] Add audit logging to all tools
|
||||
- [ ] Update server.py to use HTTP transport
|
||||
|
||||
- [x] OIDCProxy configuration (`auth.py`)
|
||||
- [x] Permission levels and tool mappings (`permissions.py`)
|
||||
- [x] RBACMiddleware with FastMCP integration (`middleware.py`)
|
||||
- [x] Audit logging with user context (`audit.py`)
|
||||
- [x] Server integration with OAuth + RBAC (`server.py`)
|
||||
- [x] Startup banner showing OAuth/RBAC status
|
||||
- [x] Security fix: deny-by-default for no groups
|
||||
- [x] Authentik setup with 5 vsphere-* groups
|
||||
### Phase 2: Deploy Authentik
|
||||
- [ ] Docker Compose for Authentik
|
||||
- [ ] Create OIDC provider and application
|
||||
- [ ] Configure redirect URIs
|
||||
- [ ] Note client credentials
|
||||
- [ ] Test OIDC flow manually with curl
|
||||
|
||||
### Future Enhancements
|
||||
### Phase 3: Integration
|
||||
- [ ] Configure environment variables
|
||||
- [ ] Test with `fastmcp dev` (OAuth mode)
|
||||
- [ ] Test with Claude Code (`claude mcp add --auth oauth`)
|
||||
- [ ] Verify audit logs show correct user identity
|
||||
|
||||
- [ ] Per-user vCenter credential mapping (Vault integration)
|
||||
- [ ] Rate limiting per user
|
||||
- [ ] Session management and token refresh
|
||||
- [ ] Admin tools for permission management
|
||||
- [ ] Prometheus metrics for RBAC decisions
|
||||
### Phase 4: Production
|
||||
- [ ] HTTPS via Caddy reverse proxy
|
||||
- [ ] Secrets in Docker secrets / Vault
|
||||
- [ ] Service account with minimal vCenter permissions
|
||||
- [ ] Log aggregation and monitoring
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
## Files to Create/Modify
|
||||
|
||||
1. **Default Deny**: Users without recognized groups get NO access
|
||||
2. **Token Validation**: JWTs validated via OIDC provider's JWKS endpoint
|
||||
3. **Audit Trail**: All operations logged with user identity
|
||||
4. **Secrets**: Client secrets should be stored securely (env vars, Docker secrets, Vault)
|
||||
5. **HTTPS**: Production deployments should use TLS (via Caddy, nginx, etc.)
|
||||
6. **Service Account**: Use minimal vCenter permissions for the service account
|
||||
```
|
||||
esxi-mcp-server/
|
||||
├── src/esxi_mcp_server/
|
||||
│ ├── auth.py # NEW: OIDCProxy configuration
|
||||
│ ├── credential_broker.py # NEW: OAuth → vCenter credential mapping
|
||||
│ ├── server.py # MODIFY: Add auth, HTTP transport
|
||||
│ └── mixins/
|
||||
│ └── *.py # MODIFY: Add ctx.request_context.user logging
|
||||
├── .env.example # MODIFY: Add OAuth variables
|
||||
└── docker-compose.yml # MODIFY: Add Authentik services
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
## Key Insight
|
||||
|
||||
### "401 Unauthorized" on all requests
|
||||
- Check `OAUTH_ISSUER_URL` points to valid OIDC discovery endpoint
|
||||
- Verify client ID and secret match provider configuration
|
||||
- Ensure token hasn't expired
|
||||
The "middleman" role of the MCP server is critical:
|
||||
|
||||
### "Permission denied" errors
|
||||
- Check user's group memberships in OIDC provider
|
||||
- Verify groups claim is included in JWT (decode at jwt.io)
|
||||
- Confirm group names match exactly (e.g., `vsphere-admins` not `vsphere_admins`)
|
||||
```
|
||||
OAuth Token (Authentik) ──┐
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ MCP Server │ ← Validates OAuth, maps to vCenter creds
|
||||
│ (Middleman) │ ← Logs audit trail with OAuth identity
|
||||
└─────────────┘
|
||||
│
|
||||
▼
|
||||
vCenter API (pyvmomi)
|
||||
```
|
||||
|
||||
### Token validation fails
|
||||
- Ensure OIDC provider issues JWTs (not opaque tokens)
|
||||
- Check signing key is configured in provider
|
||||
- Verify `OAUTH_BASE_URL` matches redirect URI in provider
|
||||
The MCP server doesn't pass OAuth tokens to vCenter. Instead, it:
|
||||
1. **Authenticates** users via OAuth (trusts Authentik)
|
||||
2. **Authorizes** by mapping OAuth identity to vCenter credentials
|
||||
3. **Audits** by logging all actions with the OAuth user identity
|
||||
4. **Executes** vCenter API calls using mapped credentials
|
||||
|
||||
### Audit logs not showing user
|
||||
- Check `groups` scope is requested
|
||||
- Verify token contains `preferred_username` or `email` claim
|
||||
This gives you SSO-like experience while working within vCenter 7.0.3's authentication limitations.
|
||||
|
||||
32
README.md
32
README.md
@ -195,41 +195,11 @@ Claude: [snapshots 12 VMs in parallel]
|
||||
| `VCENTER_CLUSTER` | Target cluster | *auto-detect* |
|
||||
| `VCENTER_DATASTORE` | Default datastore | *auto-detect* |
|
||||
| `VCENTER_NETWORK` | Default network | *auto-detect* |
|
||||
| `MCP_TRANSPORT` | `stdio` or `streamable-http` | `stdio` |
|
||||
| `MCP_TRANSPORT` | `stdio` or `sse` | `stdio` |
|
||||
| `LOG_LEVEL` | Logging verbosity | `INFO` |
|
||||
|
||||
---
|
||||
|
||||
## Multi-User / OAuth Mode
|
||||
|
||||
For shared infrastructure or production deployments, mcvsphere supports OAuth 2.1 with any OIDC provider (Authentik, Keycloak, Auth0, etc.):
|
||||
|
||||
```bash
|
||||
# Enable HTTP transport with OAuth
|
||||
export MCP_TRANSPORT=streamable-http
|
||||
export OAUTH_ENABLED=true
|
||||
export OAUTH_ISSUER_URL=https://auth.example.com/application/o/mcvsphere/
|
||||
export OAUTH_CLIENT_ID=your-client-id
|
||||
export OAUTH_CLIENT_SECRET=your-client-secret
|
||||
export OAUTH_BASE_URL=https://mcp.example.com
|
||||
|
||||
uvx mcvsphere
|
||||
```
|
||||
|
||||
Users authenticate via browser, and group memberships map to permission levels:
|
||||
|
||||
| Group | Access |
|
||||
|-------|--------|
|
||||
| `vsphere-super-admins` | Full control (all 94 tools) |
|
||||
| `vsphere-host-admins` | Host operations + VM management |
|
||||
| `vsphere-admins` | VM lifecycle management |
|
||||
| `vsphere-operators` | Power ops + snapshots |
|
||||
| `vsphere-readers` | Read-only |
|
||||
|
||||
See [OAUTH-ARCHITECTURE.md](OAUTH-ARCHITECTURE.md) for detailed setup instructions.
|
||||
|
||||
---
|
||||
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
|
||||
@ -235,89 +235,15 @@ MCP_LOG_LEVEL=DEBUG
|
||||
log_level: "DEBUG"
|
||||
```
|
||||
|
||||
## OAuth Multi-User Mode
|
||||
|
||||
For shared infrastructure or production deployments, mcvsphere supports OAuth 2.1 with any OIDC provider. This enables:
|
||||
|
||||
- **Browser-based authentication** via Authentik, Keycloak, Auth0, Okta, etc.
|
||||
- **Group-based RBAC** with 5 permission levels
|
||||
- **Audit logging** with user identity
|
||||
|
||||
### Quick Start (Existing OIDC Provider)
|
||||
|
||||
If you already have an OIDC provider:
|
||||
|
||||
```bash
|
||||
# 1. Copy and configure environment
|
||||
cp .env.oauth.example .env
|
||||
# Edit .env with your OIDC provider details
|
||||
|
||||
# 2. Start mcvsphere with OAuth
|
||||
docker compose -f docker-compose.oauth-standalone.yml up -d
|
||||
|
||||
# 3. Add to Claude Code
|
||||
claude mcp add -t http vsphere https://mcp.example.com/mcp
|
||||
```
|
||||
|
||||
### Quick Start (New Authentik Deployment)
|
||||
|
||||
To deploy mcvsphere with a complete Authentik identity provider:
|
||||
|
||||
```bash
|
||||
# 1. Generate secrets and configure
|
||||
./scripts/setup-oauth.sh
|
||||
|
||||
# 2. Start full stack (mcvsphere + Authentik + PostgreSQL + Redis)
|
||||
docker compose -f docker-compose.oauth.yml up -d
|
||||
|
||||
# 3. Configure Authentik at https://auth.yourdomain.com
|
||||
# - Create OAuth2 provider
|
||||
# - Create vsphere-* groups
|
||||
# - Assign users to groups
|
||||
```
|
||||
|
||||
### RBAC Permission Groups
|
||||
|
||||
Create these groups in your OIDC provider:
|
||||
|
||||
| Group | Access Level |
|
||||
|-------|--------------|
|
||||
| `vsphere-super-admins` | Full control (all 94 tools) |
|
||||
| `vsphere-host-admins` | Host operations + VM management |
|
||||
| `vsphere-admins` | VM lifecycle management |
|
||||
| `vsphere-operators` | Power ops + snapshots |
|
||||
| `vsphere-readers` | Read-only |
|
||||
|
||||
Users without any `vsphere-*` group are denied access (default-deny security).
|
||||
|
||||
### Required Environment Variables
|
||||
|
||||
```bash
|
||||
# Transport (must be streamable-http for OAuth)
|
||||
MCP_TRANSPORT=streamable-http
|
||||
|
||||
# OIDC Provider
|
||||
OAUTH_ENABLED=true
|
||||
OAUTH_ISSUER_URL=https://auth.example.com/application/o/mcvsphere/
|
||||
OAUTH_CLIENT_ID=your-client-id
|
||||
OAUTH_CLIENT_SECRET=your-client-secret
|
||||
OAUTH_BASE_URL=https://mcp.example.com
|
||||
```
|
||||
|
||||
See [OAUTH-ARCHITECTURE.md](OAUTH-ARCHITECTURE.md) for detailed setup and troubleshooting.
|
||||
|
||||
---
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Security Recommendations
|
||||
|
||||
1. Use a dedicated service account for vCenter access
|
||||
2. Enable OAuth authentication (not API keys)
|
||||
3. Use valid SSL certificates (set `VCENTER_INSECURE=false`)
|
||||
1. Use a dedicated user account for vCenter access
|
||||
2. Enable API key authentication
|
||||
3. Use valid SSL certificates (set `insecure: false`)
|
||||
4. Limit container resources
|
||||
5. Use Docker secrets for sensitive data
|
||||
6. Deploy behind a reverse proxy with HTTPS (Caddy recommended)
|
||||
|
||||
### High Availability
|
||||
|
||||
|
||||
@ -1,64 +0,0 @@
|
||||
# mcvsphere with OAuth - Standalone Mode
|
||||
# For users who already have an OIDC provider (Authentik, Keycloak, Auth0, Okta, etc.)
|
||||
#
|
||||
# Usage:
|
||||
# 1. Copy .env.oauth.example to .env
|
||||
# 2. Configure your OIDC provider settings
|
||||
# 3. docker compose -f docker-compose.oauth-standalone.yml up -d
|
||||
#
|
||||
# Requires:
|
||||
# - External OIDC provider with OAuth 2.1 support
|
||||
# - Caddy network (caddy-docker-proxy) for HTTPS termination
|
||||
#
|
||||
# For full Authentik deployment, use docker-compose.oauth.yml instead
|
||||
|
||||
services:
|
||||
mcvsphere:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: mcvsphere:${MCVSPHERE_VERSION:-latest}
|
||||
container_name: mcvsphere
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
# Transport - streamable-http required for OAuth
|
||||
MCP_TRANSPORT: streamable-http
|
||||
MCP_HOST: 0.0.0.0
|
||||
MCP_PORT: 8080
|
||||
# OAuth - set in .env file
|
||||
OAUTH_ENABLED: ${OAUTH_ENABLED:-true}
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
networks:
|
||||
- caddy
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/.well-known/oauth-authorization-server')"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
labels:
|
||||
# Caddy reverse proxy - configure domain in .env
|
||||
caddy: ${MCP_DOMAIN:-mcp.localhost}
|
||||
caddy.reverse_proxy: "{{upstreams 8080}}"
|
||||
# TLS - use 'internal' for local dev, remove for production (auto HTTPS)
|
||||
caddy.tls: ${MCP_TLS_MODE:-internal}
|
||||
# WebSocket/streaming support
|
||||
caddy.reverse_proxy.flush_interval: "-1"
|
||||
caddy.reverse_proxy.transport: http
|
||||
caddy.reverse_proxy.transport.read_timeout: "0"
|
||||
caddy.reverse_proxy.transport.write_timeout: "0"
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
cpus: '1.0'
|
||||
reservations:
|
||||
memory: 256M
|
||||
cpus: '0.25'
|
||||
|
||||
networks:
|
||||
caddy:
|
||||
external: true
|
||||
19
docs/.gitignore
vendored
19
docs/.gitignore
vendored
@ -1,19 +0,0 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
.astro/
|
||||
|
||||
# Editor
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
@ -1,2 +0,0 @@
|
||||
esbuild@*
|
||||
sharp@*
|
||||
@ -1,70 +0,0 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
import starlight from '@astrojs/starlight';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: 'https://docs.mcvsphere.dev',
|
||||
telemetry: false,
|
||||
devToolbar: { enabled: false },
|
||||
integrations: [
|
||||
starlight({
|
||||
title: 'mcvsphere',
|
||||
description: 'AI-driven VMware vSphere management via Model Context Protocol',
|
||||
logo: {
|
||||
src: './src/assets/logo.svg',
|
||||
replacesTitle: false,
|
||||
},
|
||||
social: {
|
||||
github: 'https://git.supported.systems/MCP/mcvsphere',
|
||||
},
|
||||
sidebar: [
|
||||
{
|
||||
label: 'Getting Started',
|
||||
items: [
|
||||
{ label: 'Introduction', slug: 'getting-started/introduction' },
|
||||
{ label: 'Installation', slug: 'getting-started/installation' },
|
||||
{ label: 'Quick Start', slug: 'getting-started/quickstart' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Configuration',
|
||||
items: [
|
||||
{ label: 'Environment Variables', slug: 'configuration/environment' },
|
||||
{ label: 'vCenter Connection', slug: 'configuration/vcenter' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Deployment',
|
||||
items: [
|
||||
{ label: 'Docker Setup', slug: 'deployment/docker' },
|
||||
{ label: 'OAuth Multi-User', slug: 'deployment/oauth' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Reference',
|
||||
items: [
|
||||
{ label: 'Tool Reference', slug: 'reference/tools' },
|
||||
{ label: 'RBAC Permissions', slug: 'reference/rbac' },
|
||||
{ label: 'Architecture', slug: 'reference/architecture' },
|
||||
],
|
||||
},
|
||||
],
|
||||
customCss: [
|
||||
'./src/styles/custom.css',
|
||||
],
|
||||
head: [
|
||||
{
|
||||
tag: 'meta',
|
||||
attrs: {
|
||||
name: 'theme-color',
|
||||
content: '#146eb4',
|
||||
},
|
||||
},
|
||||
],
|
||||
editLink: {
|
||||
baseUrl: 'https://git.supported.systems/MCP/mcvsphere/edit/main/docs/',
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "mcvsphere-docs",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/starlight": "^0.32.3",
|
||||
"astro": "^5.1.6",
|
||||
"sharp": "^0.33.5"
|
||||
}
|
||||
}
|
||||
4429
docs/pnpm-lock.yaml
generated
4429
docs/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,4 +0,0 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://docs.mcvsphere.dev/sitemap-index.xml
|
||||
@ -1,22 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- vSphere-inspired cube -->
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#3d9cdb;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#146eb4;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="grad2" x1="0%" y1="100%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#0d4f8c;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#146eb4;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<!-- Main cube shape -->
|
||||
<path d="M24 4L6 14v20l18 10 18-10V14L24 4z" fill="url(#grad1)" opacity="0.9"/>
|
||||
<path d="M24 4L6 14l18 10 18-10L24 4z" fill="url(#grad2)" opacity="0.8"/>
|
||||
<path d="M24 24v20l18-10V14L24 24z" fill="#0d4f8c" opacity="0.7"/>
|
||||
<path d="M24 24L6 14v20l18 10V24z" fill="#146eb4" opacity="0.6"/>
|
||||
<!-- VM representation lines -->
|
||||
<path d="M18 20l6-3.5 6 3.5" stroke="#fff" stroke-width="1.5" fill="none" opacity="0.8"/>
|
||||
<path d="M18 26l6-3.5 6 3.5" stroke="#fff" stroke-width="1.5" fill="none" opacity="0.6"/>
|
||||
<path d="M18 32l6-3.5 6 3.5" stroke="#fff" stroke-width="1.5" fill="none" opacity="0.4"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@ -1,6 +0,0 @@
|
||||
import { defineCollection } from 'astro:content';
|
||||
import { docsSchema } from '@astrojs/starlight/schema';
|
||||
|
||||
export const collections = {
|
||||
docs: defineCollection({ schema: docsSchema() }),
|
||||
};
|
||||
@ -1,99 +0,0 @@
|
||||
---
|
||||
title: Environment Variables
|
||||
description: Complete reference for all mcvsphere configuration options
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
## vCenter Connection
|
||||
|
||||
These are required to connect to your VMware infrastructure.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `VCENTER_HOST` | vCenter or ESXi hostname/IP | **required** |
|
||||
| `VCENTER_USER` | Username (e.g., `admin@vsphere.local`) | **required** |
|
||||
| `VCENTER_PASSWORD` | Password | **required** |
|
||||
| `VCENTER_INSECURE` | Skip SSL certificate verification | `false` |
|
||||
| `VCENTER_DATACENTER` | Target datacenter | auto-detect |
|
||||
| `VCENTER_CLUSTER` | Target cluster | auto-detect |
|
||||
| `VCENTER_DATASTORE` | Default datastore | auto-detect |
|
||||
| `VCENTER_NETWORK` | Default network | auto-detect |
|
||||
|
||||
<Aside type="caution">
|
||||
Only set `VCENTER_INSECURE=true` in development environments. Production deployments should use valid SSL certificates.
|
||||
</Aside>
|
||||
|
||||
## MCP Transport
|
||||
|
||||
Control how mcvsphere communicates with MCP clients.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `MCP_TRANSPORT` | `stdio` or `streamable-http` | `stdio` |
|
||||
| `MCP_HOST` | HTTP bind address | `0.0.0.0` |
|
||||
| `MCP_PORT` | HTTP port | `8080` |
|
||||
|
||||
### Transport Modes
|
||||
|
||||
- **stdio**: Direct subprocess communication. Used for single-user local development.
|
||||
- **streamable-http**: HTTP server with streaming support. Required for OAuth multi-user deployments.
|
||||
|
||||
## OAuth Authentication
|
||||
|
||||
Required when `OAUTH_ENABLED=true` for multi-user deployments.
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `OAUTH_ENABLED` | Enable OAuth authentication |
|
||||
| `OAUTH_ISSUER_URL` | OIDC discovery URL |
|
||||
| `OAUTH_CLIENT_ID` | OAuth client ID |
|
||||
| `OAUTH_CLIENT_SECRET` | OAuth client secret |
|
||||
| `OAUTH_BASE_URL` | Public HTTPS URL for callbacks |
|
||||
|
||||
Example OIDC discovery URLs:
|
||||
- **Authentik**: `https://auth.example.com/application/o/mcvsphere/`
|
||||
- **Keycloak**: `https://keycloak.example.com/realms/myrealm`
|
||||
- **Auth0**: `https://myapp.auth0.com/`
|
||||
- **Okta**: `https://myorg.okta.com/oauth2/default`
|
||||
|
||||
## Logging
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `LOG_LEVEL` | Logging verbosity (`DEBUG`, `INFO`, `WARNING`, `ERROR`) | `INFO` |
|
||||
|
||||
## Example Configurations
|
||||
|
||||
### Local Development
|
||||
|
||||
```bash
|
||||
export VCENTER_HOST=10.20.0.222
|
||||
export VCENTER_USER=admin@vsphere.local
|
||||
export VCENTER_PASSWORD=secret
|
||||
export VCENTER_INSECURE=true
|
||||
export LOG_LEVEL=DEBUG
|
||||
```
|
||||
|
||||
### Production (Single User)
|
||||
|
||||
```bash
|
||||
export VCENTER_HOST=vcenter.prod.example.com
|
||||
export VCENTER_USER=svc-mcvsphere@vsphere.local
|
||||
export VCENTER_PASSWORD="${VCENTER_PASSWORD}" # from secrets manager
|
||||
export VCENTER_INSECURE=false
|
||||
```
|
||||
|
||||
### Production (Multi-User OAuth)
|
||||
|
||||
```bash
|
||||
export VCENTER_HOST=vcenter.prod.example.com
|
||||
export VCENTER_USER=svc-mcvsphere@vsphere.local
|
||||
export VCENTER_PASSWORD="${VCENTER_PASSWORD}"
|
||||
export MCP_TRANSPORT=streamable-http
|
||||
export OAUTH_ENABLED=true
|
||||
export OAUTH_ISSUER_URL=https://auth.example.com/application/o/mcvsphere/
|
||||
export OAUTH_CLIENT_ID=mcvsphere-client
|
||||
export OAUTH_CLIENT_SECRET="${OAUTH_CLIENT_SECRET}"
|
||||
export OAUTH_BASE_URL=https://mcp.example.com
|
||||
```
|
||||
@ -1,126 +0,0 @@
|
||||
---
|
||||
title: vCenter Connection
|
||||
description: Configuring vCenter/ESXi access for mcvsphere
|
||||
---
|
||||
|
||||
import { Aside, Steps } from '@astrojs/starlight/components';
|
||||
|
||||
## Service Account Setup
|
||||
|
||||
For production deployments, create a dedicated service account rather than using your personal administrator credentials.
|
||||
|
||||
<Steps>
|
||||
|
||||
1. **Create the user in vCenter**
|
||||
|
||||
In the vSphere Client, go to **Administration** → **Single Sign-On** → **Users and Groups** → **Add User**
|
||||
|
||||
- Username: `svc-mcvsphere`
|
||||
- Domain: `vsphere.local`
|
||||
- Password: Generate a strong password
|
||||
|
||||
2. **Create a role with required permissions**
|
||||
|
||||
Go to **Administration** → **Access Control** → **Roles** → **Add Role**
|
||||
|
||||
Required permissions vary by which tools you need:
|
||||
|
||||
| Permission Category | Required For |
|
||||
|---------------------|--------------|
|
||||
| Virtual machine → Interaction | Power operations, console |
|
||||
| Virtual machine → Inventory | Create, delete, rename VMs |
|
||||
| Virtual machine → Configuration | CPU, memory, disk changes |
|
||||
| Virtual machine → Snapshot management | Snapshots |
|
||||
| Virtual machine → Guest operations | Commands, file transfer |
|
||||
| Datastore → Browse datastore | OVF deploy, file operations |
|
||||
| Network → Assign network | Network adapter changes |
|
||||
| Host → Configuration | Host management tools |
|
||||
|
||||
3. **Assign the role**
|
||||
|
||||
Go to your datacenter or cluster, **Permissions** tab, **Add Permission**
|
||||
|
||||
- User: `svc-mcvsphere@vsphere.local`
|
||||
- Role: The role you created
|
||||
- Propagate to children: **Yes**
|
||||
|
||||
</Steps>
|
||||
|
||||
## Connection Testing
|
||||
|
||||
Test your credentials before configuring mcvsphere:
|
||||
|
||||
```bash
|
||||
# Set credentials
|
||||
export VCENTER_HOST=vcenter.example.com
|
||||
export VCENTER_USER=svc-mcvsphere@vsphere.local
|
||||
export VCENTER_PASSWORD=your-password
|
||||
|
||||
# Quick test with Python
|
||||
python -c "
|
||||
from pyVim.connect import SmartConnect
|
||||
import ssl
|
||||
ctx = ssl._create_unverified_context()
|
||||
si = SmartConnect(host='$VCENTER_HOST', user='$VCENTER_USER', pwd='$VCENTER_PASSWORD', sslContext=ctx)
|
||||
print(f'Connected to: {si.content.about.fullName}')
|
||||
"
|
||||
```
|
||||
|
||||
## SSL Certificates
|
||||
|
||||
### Self-Signed Certificates (Development)
|
||||
|
||||
```bash
|
||||
export VCENTER_INSECURE=true
|
||||
```
|
||||
|
||||
<Aside type="caution">
|
||||
This disables SSL verification. Only use in lab environments.
|
||||
</Aside>
|
||||
|
||||
### Custom CA Certificates (Production)
|
||||
|
||||
If your vCenter uses certificates signed by an internal CA:
|
||||
|
||||
```bash
|
||||
# Add your CA to the system trust store
|
||||
sudo cp your-ca.crt /usr/local/share/ca-certificates/
|
||||
sudo update-ca-certificates
|
||||
|
||||
# Or specify the CA bundle
|
||||
export REQUESTS_CA_BUNDLE=/path/to/ca-bundle.crt
|
||||
```
|
||||
|
||||
## Auto-Detection Behavior
|
||||
|
||||
When `VCENTER_DATACENTER`, `VCENTER_CLUSTER`, `VCENTER_DATASTORE`, or `VCENTER_NETWORK` are not set, mcvsphere auto-detects defaults:
|
||||
|
||||
1. **Datacenter**: First datacenter found
|
||||
2. **Cluster**: First cluster in the datacenter
|
||||
3. **Datastore**: Datastore with most free space
|
||||
4. **Network**: Network named "VM Network", or first network found
|
||||
|
||||
To see what was auto-detected, check the startup logs:
|
||||
|
||||
```bash
|
||||
LOG_LEVEL=DEBUG uvx mcvsphere
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Cannot complete login due to an incorrect user name or password"
|
||||
|
||||
- Check username includes domain: `admin@vsphere.local` not `admin`
|
||||
- Verify password doesn't contain special characters that need escaping
|
||||
- Confirm the account isn't locked in vCenter
|
||||
|
||||
### "SSL: CERTIFICATE_VERIFY_FAILED"
|
||||
|
||||
- Use `VCENTER_INSECURE=true` for self-signed certs
|
||||
- Or install the vCenter CA certificate (see above)
|
||||
|
||||
### "Permission denied" for specific operations
|
||||
|
||||
- Check the service account has the required vCenter permissions
|
||||
- Permissions must be assigned at the right level (datacenter, cluster, or VM folder)
|
||||
- Ensure "Propagate to children" is enabled
|
||||
@ -1,186 +0,0 @@
|
||||
---
|
||||
title: Docker Deployment
|
||||
description: Run mcvsphere in Docker for production deployments
|
||||
---
|
||||
|
||||
import { Tabs, TabItem, Aside, Steps } from '@astrojs/starlight/components';
|
||||
|
||||
## Quick Start
|
||||
|
||||
<Steps>
|
||||
|
||||
1. **Create configuration**
|
||||
|
||||
```bash
|
||||
# Create environment file
|
||||
cat > .env << 'EOF'
|
||||
COMPOSE_PROJECT_NAME=mcvsphere
|
||||
|
||||
# vCenter Connection
|
||||
VCENTER_HOST=vcenter.example.com
|
||||
VCENTER_USER=svc-mcvsphere@vsphere.local
|
||||
VCENTER_PASSWORD=your-password
|
||||
VCENTER_INSECURE=false
|
||||
|
||||
# Transport
|
||||
MCP_TRANSPORT=streamable-http
|
||||
MCP_HOST=0.0.0.0
|
||||
MCP_PORT=8080
|
||||
EOF
|
||||
```
|
||||
|
||||
2. **Run the container**
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name mcvsphere \
|
||||
--env-file .env \
|
||||
-p 8080:8080 \
|
||||
ghcr.io/supportedsystems/mcvsphere:latest
|
||||
```
|
||||
|
||||
3. **Verify it's running**
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/.well-known/oauth-authorization-server
|
||||
```
|
||||
|
||||
</Steps>
|
||||
|
||||
## Docker Compose
|
||||
|
||||
For production deployments, use Docker Compose:
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
mcvsphere:
|
||||
image: ghcr.io/supportedsystems/mcvsphere:latest
|
||||
container_name: mcvsphere
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/.well-known/oauth-authorization-server')"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
cpus: '1.0'
|
||||
reservations:
|
||||
memory: 256M
|
||||
cpus: '0.25'
|
||||
```
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
docker compose logs -f
|
||||
```
|
||||
|
||||
## With Caddy Reverse Proxy
|
||||
|
||||
For HTTPS termination, use [caddy-docker-proxy](https://github.com/lucaslorentz/caddy-docker-proxy):
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
mcvsphere:
|
||||
image: ghcr.io/supportedsystems/mcvsphere:latest
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
networks:
|
||||
- caddy
|
||||
labels:
|
||||
caddy: mcp.example.com
|
||||
caddy.reverse_proxy: "{{upstreams 8080}}"
|
||||
# WebSocket/streaming support
|
||||
caddy.reverse_proxy.flush_interval: "-1"
|
||||
caddy.reverse_proxy.transport: http
|
||||
caddy.reverse_proxy.transport.read_timeout: "0"
|
||||
caddy.reverse_proxy.transport.write_timeout: "0"
|
||||
|
||||
networks:
|
||||
caddy:
|
||||
external: true
|
||||
```
|
||||
|
||||
<Aside type="tip">
|
||||
The `flush_interval` and timeout settings are important for MCP's streaming protocol. Without them, connections may drop unexpectedly.
|
||||
</Aside>
|
||||
|
||||
## Building from Source
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
git clone https://git.supported.systems/MCP/mcvsphere.git
|
||||
cd mcvsphere
|
||||
|
||||
# Build image
|
||||
docker build -t mcvsphere:local .
|
||||
|
||||
# Run
|
||||
docker run -d --env-file .env mcvsphere:local
|
||||
```
|
||||
|
||||
### Multi-Stage Build
|
||||
|
||||
The Dockerfile uses a multi-stage build for minimal image size:
|
||||
|
||||
1. **Builder stage**: Installs dependencies with `uv`
|
||||
2. **Production stage**: Copies only the virtual environment, runs as non-root user
|
||||
|
||||
## Health Checks
|
||||
|
||||
The container includes automatic health checks that verify the MCP endpoint is responding:
|
||||
|
||||
```bash
|
||||
# Check container health
|
||||
docker inspect --format='{{.State.Health.Status}}' mcvsphere
|
||||
|
||||
# View health check logs
|
||||
docker inspect --format='{{range .State.Health.Log}}{{.Output}}{{end}}' mcvsphere
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
||||
Logs are written to `/app/logs` inside the container. Mount a volume to persist them:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
```
|
||||
|
||||
View logs:
|
||||
|
||||
```bash
|
||||
# Docker logs
|
||||
docker logs mcvsphere
|
||||
|
||||
# Application logs (if volume mounted)
|
||||
tail -f ./logs/mcvsphere.log
|
||||
```
|
||||
|
||||
## Resource Limits
|
||||
|
||||
Recommended resource limits:
|
||||
|
||||
| Setting | Development | Production |
|
||||
|---------|-------------|------------|
|
||||
| Memory limit | 256M | 512M |
|
||||
| Memory reservation | 128M | 256M |
|
||||
| CPU limit | 0.5 | 1.0 |
|
||||
| CPU reservation | 0.1 | 0.25 |
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [OAuth Multi-User Mode](/deployment/oauth/) — Enable authentication for shared deployments
|
||||
- [RBAC Permissions](/reference/rbac/) — Understand permission levels
|
||||
@ -1,259 +0,0 @@
|
||||
---
|
||||
title: OAuth Multi-User Mode
|
||||
description: Enable browser-based authentication with any OIDC provider
|
||||
---
|
||||
|
||||
import { Tabs, TabItem, Aside, Steps, Badge } from '@astrojs/starlight/components';
|
||||
|
||||
For shared infrastructure or production deployments, mcvsphere supports OAuth 2.1 with any OIDC-compliant provider. This enables:
|
||||
|
||||
- **Browser-based authentication** via Authentik, Keycloak, Auth0, Okta, etc.
|
||||
- **Group-based RBAC** with 5 permission levels
|
||||
- **Audit logging** with user identity
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ MCP Client (Claude Code) │
|
||||
└─────────────────────────────┬───────────────────────────────┘
|
||||
│ 1. OAuth 2.1 + PKCE flow
|
||||
│ (browser opens for login)
|
||||
┌─────────────────────────────▼───────────────────────────────┐
|
||||
│ OIDC Provider │
|
||||
│ (Authentik, Keycloak, Auth0, etc.) │
|
||||
│ - Issues JWT access tokens │
|
||||
│ - Validates user credentials │
|
||||
│ - Includes groups claim in token │
|
||||
└─────────────────────────────┬───────────────────────────────┘
|
||||
│ 2. JWT Bearer token
|
||||
┌─────────────────────────────▼───────────────────────────────┐
|
||||
│ mcvsphere │
|
||||
│ ┌────────────────────────────────────────────────────┐ │
|
||||
│ │ OIDCProxy: Validates JWT via JWKS, extracts claims │ │
|
||||
│ └────────────────────────────────────────────────────┘ │
|
||||
│ ┌────────────────────────────────────────────────────┐ │
|
||||
│ │ RBACMiddleware: Maps groups → permissions │ │
|
||||
│ └────────────────────────────────────────────────────┘ │
|
||||
│ ┌────────────────────────────────────────────────────┐ │
|
||||
│ │ VMware Tools: Execute vCenter operations │ │
|
||||
│ └────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────┬───────────────────────────────┘
|
||||
│ 3. pyvmomi (service account)
|
||||
┌─────────────────────────────▼───────────────────────────────┐
|
||||
│ vCenter / ESXi │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Existing OIDC Provider">
|
||||
If you already have Authentik, Keycloak, Auth0, or another OIDC provider:
|
||||
|
||||
<Steps>
|
||||
|
||||
1. **Create OAuth application in your provider**
|
||||
|
||||
- Client type: Confidential
|
||||
- Redirect URI: `https://mcp.example.com/auth/callback`
|
||||
- Scopes: `openid`, `profile`, `email`, `groups`
|
||||
|
||||
2. **Configure environment**
|
||||
|
||||
```bash
|
||||
cat > .env << 'EOF'
|
||||
# vCenter
|
||||
VCENTER_HOST=vcenter.example.com
|
||||
VCENTER_USER=svc-mcvsphere@vsphere.local
|
||||
VCENTER_PASSWORD=your-password
|
||||
|
||||
# Transport (required for OAuth)
|
||||
MCP_TRANSPORT=streamable-http
|
||||
MCP_DOMAIN=mcp.example.com
|
||||
|
||||
# OAuth
|
||||
OAUTH_ENABLED=true
|
||||
OAUTH_ISSUER_URL=https://auth.example.com/application/o/mcvsphere/
|
||||
OAUTH_CLIENT_ID=your-client-id
|
||||
OAUTH_CLIENT_SECRET=your-client-secret
|
||||
OAUTH_BASE_URL=https://mcp.example.com
|
||||
EOF
|
||||
```
|
||||
|
||||
3. **Deploy with Docker**
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.oauth-standalone.yml up -d
|
||||
```
|
||||
|
||||
4. **Add to Claude Code**
|
||||
|
||||
```bash
|
||||
claude mcp add -t http vsphere https://mcp.example.com/mcp
|
||||
```
|
||||
|
||||
</Steps>
|
||||
</TabItem>
|
||||
<TabItem label="New Authentik Setup">
|
||||
To deploy mcvsphere with a fresh Authentik identity provider:
|
||||
|
||||
<Steps>
|
||||
|
||||
1. **Generate secrets**
|
||||
|
||||
```bash
|
||||
./scripts/setup-oauth.sh
|
||||
```
|
||||
|
||||
2. **Start full stack**
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.oauth.yml up -d
|
||||
```
|
||||
|
||||
This starts:
|
||||
- mcvsphere (MCP server)
|
||||
- Authentik (identity provider)
|
||||
- PostgreSQL (Authentik database)
|
||||
- Redis (Authentik cache)
|
||||
|
||||
3. **Configure Authentik**
|
||||
|
||||
Open `https://auth.yourdomain.com` and:
|
||||
- Create OAuth2 provider for mcvsphere
|
||||
- Create `vsphere-*` groups
|
||||
- Assign users to groups
|
||||
|
||||
</Steps>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## RBAC Permission Groups
|
||||
|
||||
Create these groups in your OIDC provider and assign users:
|
||||
|
||||
| Group | Access Level |
|
||||
|-------|--------------|
|
||||
| `vsphere-super-admins` | Full control (all 94 tools) |
|
||||
| `vsphere-host-admins` | Host operations + VM management |
|
||||
| `vsphere-admins` | VM lifecycle management |
|
||||
| `vsphere-operators` | Power ops + snapshots |
|
||||
| `vsphere-readers` | Read-only |
|
||||
|
||||
<Aside type="caution">
|
||||
**Default Deny**: Users without any `vsphere-*` group are denied all access. This is a security feature, not a bug.
|
||||
</Aside>
|
||||
|
||||
See [RBAC Permissions Reference](/reference/rbac/) for complete tool-to-permission mappings.
|
||||
|
||||
## Authentik Configuration
|
||||
|
||||
<Steps>
|
||||
|
||||
1. **Create OAuth2/OIDC Provider**
|
||||
|
||||
In Authentik, go to **Applications** → **Providers** → **Create**
|
||||
|
||||
- Name: `mcvsphere`
|
||||
- Authorization flow: `default-authorization-flow`
|
||||
- Client type: **Confidential**
|
||||
- Client ID: (auto-generated, copy this)
|
||||
- Client Secret: (auto-generated, copy this)
|
||||
- Redirect URIs:
|
||||
```
|
||||
http://localhost:*/callback
|
||||
https://mcp.example.com/auth/callback
|
||||
```
|
||||
- Signing Key: Select your RS256 certificate
|
||||
|
||||
2. **Create Application**
|
||||
|
||||
Go to **Applications** → **Applications** → **Create**
|
||||
|
||||
- Name: `mcvsphere`
|
||||
- Slug: `mcvsphere`
|
||||
- Provider: Select the provider from step 1
|
||||
|
||||
3. **Create Groups**
|
||||
|
||||
Go to **Directory** → **Groups** → **Create** for each:
|
||||
|
||||
- `vsphere-readers`
|
||||
- `vsphere-operators`
|
||||
- `vsphere-admins`
|
||||
- `vsphere-host-admins`
|
||||
- `vsphere-super-admins`
|
||||
|
||||
4. **Assign Users to Groups**
|
||||
|
||||
Edit each user and add them to appropriate groups.
|
||||
|
||||
</Steps>
|
||||
|
||||
## OAuth Flow
|
||||
|
||||
When a user connects with Claude Code:
|
||||
|
||||
1. **Initial connection** — Client connects without auth, server returns 401 with OAuth metadata URL
|
||||
2. **Discovery** — Client fetches OIDC configuration from provider
|
||||
3. **Authorization** — Browser opens, user logs in via OIDC provider
|
||||
4. **Token exchange** — Authorization code exchanged for JWT access token
|
||||
5. **Authenticated requests** — Client includes `Authorization: Bearer <jwt>` header
|
||||
6. **Permission check** — RBACMiddleware validates user's groups against required tool permissions
|
||||
7. **Audit logging** — Every operation logged with user identity and timing
|
||||
|
||||
## Audit Logging
|
||||
|
||||
All tool invocations are logged with user context:
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-01-16T08:15:32.123456+00:00",
|
||||
"user": "ryan@example.com",
|
||||
"groups": ["vsphere-admins", "vsphere-operators"],
|
||||
"tool": "power_on",
|
||||
"args": {"vm_name": "web-server"},
|
||||
"duration_ms": 1234.56,
|
||||
"result": "success"
|
||||
}
|
||||
```
|
||||
|
||||
Permission denied events:
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-01-16T08:15:32.123456+00:00",
|
||||
"user": "guest@example.com",
|
||||
"groups": ["vsphere-readers"],
|
||||
"tool": "delete_vm",
|
||||
"required_permission": "vm_lifecycle",
|
||||
"event": "PERMISSION_DENIED"
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "401 Unauthorized" on all requests
|
||||
|
||||
- Check `OAUTH_ISSUER_URL` points to valid OIDC discovery endpoint
|
||||
- Verify client ID and secret match provider configuration
|
||||
- Ensure token hasn't expired
|
||||
|
||||
### "Permission denied" errors
|
||||
|
||||
- Check user's group memberships in OIDC provider
|
||||
- Verify groups claim is included in JWT (decode at [jwt.io](https://jwt.io))
|
||||
- Confirm group names match exactly: `vsphere-admins` not `vsphere_admins`
|
||||
|
||||
### Token validation fails
|
||||
|
||||
- Ensure OIDC provider issues JWTs (not opaque tokens)
|
||||
- Check signing key is configured in provider
|
||||
- Verify `OAUTH_BASE_URL` matches redirect URI in provider
|
||||
|
||||
### User shows as "anonymous"
|
||||
|
||||
- Restart Claude Code session after OAuth flow completes
|
||||
- Check server logs for claims extraction errors
|
||||
- Verify `groups` scope is included in token request
|
||||
@ -1,71 +0,0 @@
|
||||
---
|
||||
title: Installation
|
||||
description: Install mcvsphere in under 30 seconds
|
||||
---
|
||||
|
||||
import { Tabs, TabItem } from '@astrojs/starlight/components';
|
||||
|
||||
## Quick Install
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="uvx (Recommended)">
|
||||
```bash
|
||||
uvx mcvsphere
|
||||
```
|
||||
|
||||
No installation needed. uvx downloads and runs in one command.
|
||||
</TabItem>
|
||||
<TabItem label="pip">
|
||||
```bash
|
||||
pip install mcvsphere
|
||||
```
|
||||
|
||||
Then run with:
|
||||
```bash
|
||||
mcvsphere
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="pipx">
|
||||
```bash
|
||||
pipx install mcvsphere
|
||||
```
|
||||
|
||||
Isolated environment, available system-wide.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Requirements
|
||||
|
||||
| Requirement | Version |
|
||||
|------------|---------|
|
||||
| Python | 3.11+ |
|
||||
| VMware vSphere | 7.0+ |
|
||||
| VMware Tools | Latest (for guest operations) |
|
||||
|
||||
## Verify Installation
|
||||
|
||||
```bash
|
||||
# Check version
|
||||
uvx mcvsphere --version
|
||||
|
||||
# Or if installed via pip
|
||||
mcvsphere --version
|
||||
```
|
||||
|
||||
You should see something like:
|
||||
|
||||
```
|
||||
mcvsphere v0.2.3
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
mcvsphere installs these automatically:
|
||||
|
||||
- **[FastMCP](https://gofastmcp.com)** — MCP server framework
|
||||
- **[pyvmomi](https://github.com/vmware/pyvmomi)** — VMware vSphere Python SDK
|
||||
- **[pydantic](https://docs.pydantic.dev)** — Configuration validation
|
||||
|
||||
## Next Steps
|
||||
|
||||
Now that mcvsphere is installed, [configure your vCenter connection](/getting-started/quickstart/).
|
||||
@ -1,71 +0,0 @@
|
||||
---
|
||||
title: Introduction
|
||||
description: What is mcvsphere and why you'd want it
|
||||
---
|
||||
|
||||
mcvsphere is a Model Context Protocol (MCP) server that connects AI assistants like Claude to your VMware vSphere infrastructure. Instead of clicking through the vSphere UI or writing scripts, you can manage your virtual machines through natural language conversation.
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Claude Code / MCP Client │
|
||||
└──────────────────────┬──────────────────────────────────────┘
|
||||
│ MCP Protocol (stdio or HTTP)
|
||||
│
|
||||
┌──────────────────────▼──────────────────────────────────────┐
|
||||
│ mcvsphere │
|
||||
│ FastMCP + pyvmomi │
|
||||
│ │
|
||||
│ 94 Tools 6 Resources │
|
||||
│ • VM lifecycle • VMs list │
|
||||
│ • Power operations • Hosts list │
|
||||
│ • Snapshots • Datastores │
|
||||
│ • Guest operations • Networks │
|
||||
│ • Disk/NIC management • Clusters │
|
||||
│ • Host management • Resource pools │
|
||||
└──────────────────────┬──────────────────────────────────────┘
|
||||
│ pyvmomi (vSphere API)
|
||||
│
|
||||
┌──────────────────────▼──────────────────────────────────────┐
|
||||
│ VMware vCenter / ESXi Host │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Two Operating Modes
|
||||
|
||||
### STDIO Mode (Single User)
|
||||
|
||||
Direct connection for local development or single-user setups. Claude Code spawns the MCP server as a subprocess.
|
||||
|
||||
```bash
|
||||
claude mcp add vsphere "uvx mcvsphere"
|
||||
```
|
||||
|
||||
### HTTP + OAuth Mode (Multi-User)
|
||||
|
||||
Browser-based authentication via any OIDC provider. Multiple users share one server, each with their own permissions based on group membership.
|
||||
|
||||
```bash
|
||||
claude mcp add -t http vsphere https://mcp.example.com/mcp
|
||||
```
|
||||
|
||||
## What Can You Do?
|
||||
|
||||
With mcvsphere connected, you can ask Claude to:
|
||||
|
||||
- **Create infrastructure**: "Spin up 3 Ubuntu VMs with 4 CPUs each"
|
||||
- **Manage power**: "Power on all VMs starting with 'prod-'"
|
||||
- **Take snapshots**: "Snapshot all production VMs before the update"
|
||||
- **Run commands**: "Check the disk usage on linux-server" (no SSH needed)
|
||||
- **Configure hardware**: "Add a 100GB disk to the database server"
|
||||
- **Deploy appliances**: "Deploy this OVA to the dev cluster"
|
||||
- **Monitor systems**: "Show me which hosts have high memory usage"
|
||||
|
||||
All 94 tools work through conversation. No scripting required.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Install mcvsphere](/getting-started/installation/)
|
||||
- [Configure vCenter connection](/getting-started/quickstart/)
|
||||
- [Learn about all 94 tools](/reference/tools/)
|
||||
@ -1,102 +0,0 @@
|
||||
---
|
||||
title: Quick Start
|
||||
description: Connect mcvsphere to vCenter and Claude Code in 2 minutes
|
||||
---
|
||||
|
||||
import { Steps, Aside } from '@astrojs/starlight/components';
|
||||
|
||||
## Single-User Setup (STDIO)
|
||||
|
||||
<Steps>
|
||||
|
||||
1. **Set your vCenter credentials**
|
||||
|
||||
```bash
|
||||
export VCENTER_HOST=vcenter.example.com
|
||||
export VCENTER_USER=administrator@vsphere.local
|
||||
export VCENTER_PASSWORD=your-password
|
||||
```
|
||||
|
||||
<Aside type="tip">
|
||||
For self-signed certificates in dev/lab environments:
|
||||
```bash
|
||||
export VCENTER_INSECURE=true
|
||||
```
|
||||
</Aside>
|
||||
|
||||
2. **Add to Claude Code**
|
||||
|
||||
```bash
|
||||
claude mcp add vsphere "uvx mcvsphere"
|
||||
```
|
||||
|
||||
3. **Test the connection**
|
||||
|
||||
Open Claude Code and ask:
|
||||
```
|
||||
List all VMs in my vCenter
|
||||
```
|
||||
|
||||
Claude should respond with your VM inventory.
|
||||
|
||||
</Steps>
|
||||
|
||||
## Multi-User Setup (HTTP + OAuth)
|
||||
|
||||
For shared infrastructure with multiple users, see [OAuth Multi-User Deployment](/deployment/oauth/).
|
||||
|
||||
## Example Conversations
|
||||
|
||||
Once connected, try these:
|
||||
|
||||
### Create a VM
|
||||
|
||||
```
|
||||
Create a VM called "test-web" with 4 CPUs, 8GB RAM, and 100GB disk
|
||||
```
|
||||
|
||||
### Manage Snapshots
|
||||
|
||||
```
|
||||
Take a snapshot of production-db called "before-upgrade"
|
||||
```
|
||||
|
||||
### Run Commands in Guests
|
||||
|
||||
```
|
||||
What's the uptime on linux-server?
|
||||
```
|
||||
|
||||
mcvsphere runs `uptime` inside the VM via VMware Tools—no SSH required.
|
||||
|
||||
### Take Console Screenshots
|
||||
|
||||
```
|
||||
Show me what's on the screen of that stuck VM
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Failed to connect to vCenter"
|
||||
|
||||
- Verify `VCENTER_HOST` is reachable: `nc -zv $VCENTER_HOST 443`
|
||||
- Check username format: `admin@vsphere.local` not just `admin`
|
||||
- Try `VCENTER_INSECURE=true` for certificate issues
|
||||
|
||||
### "VMware Tools not running"
|
||||
|
||||
Guest operations require VMware Tools installed and running in the VM:
|
||||
|
||||
```
|
||||
Check VMware Tools status on linux-server
|
||||
```
|
||||
|
||||
### "Permission denied"
|
||||
|
||||
Your vCenter user needs appropriate permissions. For testing, use an administrator account. For production, create a dedicated service account with minimal required permissions.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Environment Variables Reference](/configuration/environment/)
|
||||
- [Docker Deployment](/deployment/docker/)
|
||||
- [All 94 Tools](/reference/tools/)
|
||||
@ -1,81 +0,0 @@
|
||||
---
|
||||
title: mcvsphere
|
||||
description: 94 tools. Full VMware control. Natural language.
|
||||
template: splash
|
||||
hero:
|
||||
tagline: Stop clicking through vSphere. Start talking to it.
|
||||
image:
|
||||
file: ../../assets/logo.svg
|
||||
actions:
|
||||
- text: Get Started
|
||||
link: /getting-started/installation/
|
||||
icon: right-arrow
|
||||
- text: View on GitHub
|
||||
link: https://git.supported.systems/MCP/mcvsphere
|
||||
icon: external
|
||||
variant: minimal
|
||||
---
|
||||
|
||||
import { Card, CardGrid } from '@astrojs/starlight/components';
|
||||
|
||||
## What is mcvsphere?
|
||||
|
||||
mcvsphere is an MCP server that gives AI assistants like Claude complete control over your VMware infrastructure. Create VMs, manage snapshots, run commands inside guests, take console screenshots, configure network appliances—all through conversation.
|
||||
|
||||
**94 MCP tools.** Not a toy demo. Not "just the basics." The whole thing.
|
||||
|
||||
```
|
||||
You: "Spin up a new VM with 4 CPUs and 16GB RAM, clone it twice,
|
||||
then snapshot all three before I start testing"
|
||||
|
||||
Claude: Done. Three VMs running, all snapshotted. What's next?
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
<CardGrid stagger>
|
||||
<Card title="94 MCP Tools" icon="rocket">
|
||||
VM lifecycle, power operations, snapshots, guest operations, serial console, disk management, network adapters, OVF/OVA, host management, and more.
|
||||
</Card>
|
||||
<Card title="Guest Operations" icon="seti:shell">
|
||||
Run commands inside VMs, transfer files, browse filesystems—no SSH required. VMware Tools does the heavy lifting.
|
||||
</Card>
|
||||
<Card title="OAuth + RBAC" icon="shield">
|
||||
Multi-user authentication via any OIDC provider with 5 permission levels. Audit logging shows WHO did WHAT.
|
||||
</Card>
|
||||
<Card title="Real-time Resources" icon="list-format">
|
||||
6 MCP resources provide always-current data: VMs, hosts, datastores, networks, clusters, resource pools.
|
||||
</Card>
|
||||
</CardGrid>
|
||||
|
||||
## Quick Example
|
||||
|
||||
```bash
|
||||
# Install
|
||||
uvx mcvsphere
|
||||
|
||||
# Configure
|
||||
export VCENTER_HOST=vcenter.example.com
|
||||
export VCENTER_USER=administrator@vsphere.local
|
||||
export VCENTER_PASSWORD=your-password
|
||||
|
||||
# Add to Claude Code
|
||||
claude mcp add vsphere "uvx mcvsphere"
|
||||
```
|
||||
|
||||
That's it. You're managing vSphere with AI now.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.11+
|
||||
- VMware vSphere 7.0+ (ESXi or vCenter)
|
||||
- VMware Tools (for guest operations)
|
||||
|
||||
## Built With
|
||||
|
||||
- [FastMCP](https://github.com/jlowin/fastmcp) — MCP server framework
|
||||
- [pyVmomi](https://github.com/vmware/pyvmomi) — VMware vSphere API
|
||||
|
||||
---
|
||||
|
||||
**94 tools. Your entire VMware infrastructure. Controlled by conversation.**
|
||||
@ -1,239 +0,0 @@
|
||||
---
|
||||
title: Architecture
|
||||
description: Technical overview of mcvsphere internals
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
## System Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ MCP Client (Claude Code) │
|
||||
└──────────────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
│ MCP Protocol
|
||||
│ (stdio or streamable-http)
|
||||
│
|
||||
┌──────────────────────────▼──────────────────────────────────┐
|
||||
│ mcvsphere │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ FastMCP │ │
|
||||
│ │ - Protocol handling (JSON-RPC 2.0) │ │
|
||||
│ │ - Tool registration and dispatch │ │
|
||||
│ │ - Resource serving │ │
|
||||
│ │ - OAuth integration (OIDCProxy) │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ RBACMiddleware │ │
|
||||
│ │ - Intercepts all tool calls │ │
|
||||
│ │ - Extracts user from OAuth token │ │
|
||||
│ │ - Enforces permission checks │ │
|
||||
│ │ - Audit logging │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ Tool Implementations │ │
|
||||
│ │ - vm_lifecycle.py (7 tools) │ │
|
||||
│ │ - power_ops.py (6 tools) │ │
|
||||
│ │ - snapshots.py (5 tools) │ │
|
||||
│ │ - guest_ops.py (7 tools) │ │
|
||||
│ │ - ... (94 tools total) │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ Connection Manager │ │
|
||||
│ │ - pyvmomi ServiceInstance │ │
|
||||
│ │ - Connection pooling │ │
|
||||
│ │ - Reconnection handling │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
│ vSphere API (SOAP/REST)
|
||||
│
|
||||
┌──────────────────────────▼──────────────────────────────────┐
|
||||
│ VMware vCenter / ESXi │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Source Layout
|
||||
|
||||
```
|
||||
src/mcvsphere/
|
||||
├── __init__.py # Package entry point, version
|
||||
├── server.py # FastMCP server setup
|
||||
├── config.py # Pydantic Settings configuration
|
||||
├── connection.py # vSphere connection manager
|
||||
├── auth.py # OIDCProxy configuration
|
||||
├── middleware.py # RBAC middleware
|
||||
├── permissions.py # Permission levels and mappings
|
||||
├── audit.py # Audit logging
|
||||
└── tools/ # MCP tool implementations
|
||||
├── __init__.py
|
||||
├── vm_lifecycle.py
|
||||
├── power_ops.py
|
||||
├── snapshots.py
|
||||
├── guest_ops.py
|
||||
├── serial_console.py
|
||||
├── disk_mgmt.py
|
||||
├── nic_mgmt.py
|
||||
├── ovf_ops.py
|
||||
├── host_mgmt.py
|
||||
├── datastore_ops.py
|
||||
└── vcenter_ops.py
|
||||
```
|
||||
|
||||
## Key Components
|
||||
|
||||
### FastMCP Server (`server.py`)
|
||||
|
||||
The core MCP server built on [FastMCP](https://gofastmcp.com):
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP(
|
||||
name="mcvsphere",
|
||||
instructions="VMware vSphere management server..."
|
||||
)
|
||||
|
||||
# Tools are registered via decorators
|
||||
@mcp.tool()
|
||||
def list_vms() -> list[dict]:
|
||||
"""List all virtual machines."""
|
||||
return connection.list_vms()
|
||||
```
|
||||
|
||||
### Configuration (`config.py`)
|
||||
|
||||
Pydantic Settings model for type-safe configuration:
|
||||
|
||||
```python
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
class Settings(BaseSettings):
|
||||
vcenter_host: str
|
||||
vcenter_user: str
|
||||
vcenter_password: str
|
||||
vcenter_insecure: bool = False
|
||||
mcp_transport: Literal["stdio", "streamable-http"] = "stdio"
|
||||
oauth_enabled: bool = False
|
||||
# ...
|
||||
```
|
||||
|
||||
### Connection Manager (`connection.py`)
|
||||
|
||||
Manages the pyvmomi ServiceInstance connection:
|
||||
|
||||
```python
|
||||
class VSphereConnection:
|
||||
def __init__(self, settings: Settings):
|
||||
self.si = SmartConnect(
|
||||
host=settings.vcenter_host,
|
||||
user=settings.vcenter_user,
|
||||
pwd=settings.vcenter_password,
|
||||
sslContext=self._get_ssl_context()
|
||||
)
|
||||
|
||||
def list_vms(self) -> list[dict]:
|
||||
content = self.si.RetrieveContent()
|
||||
# ... pyvmomi operations
|
||||
```
|
||||
|
||||
### RBAC Middleware (`middleware.py`)
|
||||
|
||||
Intercepts all tool calls for permission checking:
|
||||
|
||||
```python
|
||||
from fastmcp import Middleware
|
||||
|
||||
class RBACMiddleware(Middleware):
|
||||
async def on_call_tool(self, context, call_next):
|
||||
# 1. Extract user from OAuth token
|
||||
claims = self._extract_user_from_context(context.fastmcp_context)
|
||||
groups = claims.get("groups", [])
|
||||
|
||||
# 2. Check permission
|
||||
tool_name = context.message.name
|
||||
if not check_permission(tool_name, groups):
|
||||
raise PermissionDeniedError(...)
|
||||
|
||||
# 3. Execute with audit logging
|
||||
result = await call_next(context)
|
||||
audit_log(tool_name, claims, result)
|
||||
return result
|
||||
```
|
||||
|
||||
### OAuth Configuration (`auth.py`)
|
||||
|
||||
Configures FastMCP's OIDCProxy for token validation:
|
||||
|
||||
```python
|
||||
from fastmcp.server.auth import OIDCProxy
|
||||
|
||||
def create_auth_provider(settings: Settings) -> OIDCProxy | None:
|
||||
if not settings.oauth_enabled:
|
||||
return None
|
||||
|
||||
return OIDCProxy(
|
||||
issuer_url=settings.oauth_issuer_url,
|
||||
client_id=settings.oauth_client_id,
|
||||
client_secret=settings.oauth_client_secret,
|
||||
redirect_uri=f"{settings.oauth_base_url}/auth/callback",
|
||||
required_scopes=[], # Authentik uses opaque tokens
|
||||
)
|
||||
```
|
||||
|
||||
## Data Flow
|
||||
|
||||
### Tool Invocation (STDIO mode)
|
||||
|
||||
1. Claude Code sends JSON-RPC request via stdin
|
||||
2. FastMCP parses and validates request
|
||||
3. Tool function executes
|
||||
4. pyvmomi calls vSphere API
|
||||
5. Result serialized and returned via stdout
|
||||
|
||||
### Tool Invocation (HTTP + OAuth mode)
|
||||
|
||||
1. Client sends HTTP POST with Bearer token
|
||||
2. OIDCProxy validates JWT via JWKS
|
||||
3. RBACMiddleware extracts claims, checks permissions
|
||||
4. Tool function executes
|
||||
5. Audit log written
|
||||
6. Result returned as HTTP response
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Connection Reuse
|
||||
|
||||
A single pyvmomi connection is shared across all requests. This avoids the overhead of establishing new connections for each tool call.
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
vCenter inventory is fetched on-demand rather than cached. This ensures data freshness but means each tool call queries vCenter directly.
|
||||
|
||||
### Resource Pooling
|
||||
|
||||
For high-volume deployments, consider running multiple mcvsphere instances behind a load balancer. Each instance maintains its own vCenter connection.
|
||||
|
||||
## Security Model
|
||||
|
||||
### STDIO Mode
|
||||
|
||||
- No authentication (trusts the parent process)
|
||||
- Single-user by design
|
||||
- Credentials passed via environment variables
|
||||
|
||||
### HTTP + OAuth Mode
|
||||
|
||||
- JWT validation via OIDC provider
|
||||
- Group-based RBAC
|
||||
- Audit trail with user identity
|
||||
- Service account connection to vCenter (user identity tracked in audit logs, not vCenter permissions)
|
||||
|
||||
<Aside>
|
||||
In OAuth mode, ALL users share the same vCenter service account. User identity is tracked in mcvsphere audit logs, not vCenter audit logs. Future versions may support per-user vCenter credentials via Vault integration.
|
||||
</Aside>
|
||||
@ -1,184 +0,0 @@
|
||||
---
|
||||
title: RBAC Permissions
|
||||
description: Role-based access control for multi-user deployments
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
When OAuth is enabled, mcvsphere enforces role-based access control (RBAC) based on the user's OAuth group memberships.
|
||||
|
||||
## Permission Levels
|
||||
|
||||
mcvsphere defines 5 permission levels, from least to most privileged:
|
||||
|
||||
| Level | Description | Tools |
|
||||
|-------|-------------|-------|
|
||||
| `READ_ONLY` | View-only operations | 32 tools |
|
||||
| `POWER_OPS` | Power and snapshot operations | 14 tools |
|
||||
| `VM_LIFECYCLE` | Create/delete/modify VMs | 33 tools |
|
||||
| `HOST_ADMIN` | ESXi host operations | 6 tools |
|
||||
| `FULL_ADMIN` | Everything including guest OS | 11 tools |
|
||||
|
||||
## OAuth Groups
|
||||
|
||||
Map OAuth groups to permission levels:
|
||||
|
||||
| OAuth Group | Permissions Granted |
|
||||
|-------------|---------------------|
|
||||
| `vsphere-readers` | READ_ONLY |
|
||||
| `vsphere-operators` | READ_ONLY + POWER_OPS |
|
||||
| `vsphere-admins` | READ_ONLY + POWER_OPS + VM_LIFECYCLE |
|
||||
| `vsphere-host-admins` | READ_ONLY + POWER_OPS + VM_LIFECYCLE + HOST_ADMIN |
|
||||
| `vsphere-super-admins` | ALL (full access) |
|
||||
|
||||
<Aside type="caution">
|
||||
**Default Deny Security**: Users with NO recognized `vsphere-*` groups are denied ALL access. There is no implicit "read" permission.
|
||||
</Aside>
|
||||
|
||||
## Tool Permission Mapping
|
||||
|
||||
### READ_ONLY (32 tools)
|
||||
|
||||
View and inspect operations:
|
||||
|
||||
```
|
||||
list_vms, get_vm_info, list_snapshots, get_vm_stats
|
||||
get_host_stats, vm_screenshot, get_vm_tools_status
|
||||
wait_for_vm_tools, list_disks, get_disk_info
|
||||
list_nics, get_nic_info, get_serial_port
|
||||
list_hosts, get_host_info, get_host_hardware
|
||||
get_host_networking, list_services, get_service_status
|
||||
get_ntp_config, get_datastore_info, browse_datastore
|
||||
get_vcenter_info, get_resource_pool_info, get_network_info
|
||||
list_templates, get_alarms, get_recent_events
|
||||
list_folders, list_clusters, get_cluster_info
|
||||
list_resource_pools, list_tags, get_vm_tags
|
||||
list_recent_tasks, list_recent_events
|
||||
```
|
||||
|
||||
### POWER_OPS (14 tools)
|
||||
|
||||
Power state and snapshots:
|
||||
|
||||
```
|
||||
power_on_vm, power_off_vm, shutdown_guest
|
||||
reboot_guest, suspend_vm, reset_vm
|
||||
create_snapshot, revert_to_snapshot
|
||||
delete_snapshot, delete_all_snapshots
|
||||
connect_nic, setup_serial_port
|
||||
connect_serial_port, clear_serial_port
|
||||
```
|
||||
|
||||
### VM_LIFECYCLE (33 tools)
|
||||
|
||||
Create, modify, and delete VMs:
|
||||
|
||||
```
|
||||
create_vm, clone_vm, delete_vm
|
||||
reconfigure_vm, rename_vm
|
||||
add_disk, remove_disk, resize_disk
|
||||
add_nic, remove_nic, change_nic_network
|
||||
remove_serial_port
|
||||
deploy_ovf, export_ovf, list_ovf_networks
|
||||
upload_to_datastore, download_from_datastore
|
||||
create_folder, delete_folder, move_vm_to_folder
|
||||
create_resource_pool, delete_resource_pool
|
||||
move_vm_to_resource_pool
|
||||
apply_tag_to_vm, remove_tag_from_vm
|
||||
migrate_vm, cancel_task
|
||||
```
|
||||
|
||||
### HOST_ADMIN (6 tools)
|
||||
|
||||
ESXi host management:
|
||||
|
||||
```
|
||||
start_service, stop_service, restart_service
|
||||
enter_maintenance_mode, exit_maintenance_mode
|
||||
reboot_host, shutdown_host
|
||||
```
|
||||
|
||||
### FULL_ADMIN (11 tools)
|
||||
|
||||
Guest OS operations and full control:
|
||||
|
||||
```
|
||||
run_command_in_guest, read_guest_file, write_guest_file
|
||||
list_guest_processes, list_guest_directory
|
||||
create_guest_directory, delete_guest_file
|
||||
```
|
||||
|
||||
<Aside>
|
||||
Guest operations are the most privileged because they allow arbitrary command execution inside VMs.
|
||||
</Aside>
|
||||
|
||||
## Permission Check Flow
|
||||
|
||||
When a tool is invoked:
|
||||
|
||||
1. **Extract user identity** from OAuth token claims
|
||||
2. **Get user's groups** from `groups` claim
|
||||
3. **Look up required permission** for the tool
|
||||
4. **Check if any group grants** the required permission
|
||||
5. **Allow or deny** with audit logging
|
||||
|
||||
```python
|
||||
# Pseudocode
|
||||
def check_permission(tool_name: str, user_groups: list[str]) -> bool:
|
||||
required = TOOL_PERMISSIONS[tool_name] # e.g., POWER_OPS
|
||||
|
||||
for group in user_groups:
|
||||
if group in GROUP_PERMISSIONS:
|
||||
granted = GROUP_PERMISSIONS[group] # e.g., {READ_ONLY, POWER_OPS}
|
||||
if required in granted:
|
||||
return True
|
||||
|
||||
return False # Default deny
|
||||
```
|
||||
|
||||
## Audit Logging
|
||||
|
||||
Every permission decision is logged:
|
||||
|
||||
### Successful access
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-01-16T08:15:32.123456+00:00",
|
||||
"user": "ryan@example.com",
|
||||
"groups": ["vsphere-admins"],
|
||||
"tool": "power_on_vm",
|
||||
"args": {"vm_name": "web-server"},
|
||||
"duration_ms": 1234.56,
|
||||
"result": "success"
|
||||
}
|
||||
```
|
||||
|
||||
### Permission denied
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-01-16T08:15:32.123456+00:00",
|
||||
"user": "intern@example.com",
|
||||
"groups": ["vsphere-readers"],
|
||||
"tool": "delete_vm",
|
||||
"args": {"vm_name": "production-db"},
|
||||
"required_permission": "vm_lifecycle",
|
||||
"event": "PERMISSION_DENIED"
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Permission denied" for expected access
|
||||
|
||||
1. **Check group names match exactly**: `vsphere-admins` vs `vsphere_admins`
|
||||
2. **Verify groups claim** in JWT token (decode at [jwt.io](https://jwt.io))
|
||||
3. **Check group membership** in your OIDC provider
|
||||
4. **Review audit logs** for the exact permission required
|
||||
|
||||
### User has no permissions at all
|
||||
|
||||
1. **Confirm user is in at least one** `vsphere-*` group
|
||||
2. **Check OIDC provider** includes `groups` in token claims
|
||||
3. **Verify** the groups scope is requested during OAuth flow
|
||||
@ -1,202 +0,0 @@
|
||||
---
|
||||
title: Tool Reference
|
||||
description: Complete reference for all 94 MCP tools
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
mcvsphere provides 94 MCP tools for comprehensive VMware vSphere management.
|
||||
|
||||
## VM Lifecycle
|
||||
|
||||
Create, clone, delete, and configure virtual machines.
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `create_vm` | Create a new VM from scratch |
|
||||
| `clone_vm` | Clone from template or existing VM |
|
||||
| `delete_vm` | Remove a VM permanently |
|
||||
| `reconfigure_vm` | Change CPU, memory, annotations |
|
||||
| `rename_vm` | Rename a VM |
|
||||
| `list_vms` | List all VMs |
|
||||
| `get_vm_info` | Detailed VM information |
|
||||
|
||||
## Power Operations
|
||||
|
||||
Control VM power state.
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `power_on_vm` | Start a VM |
|
||||
| `power_off_vm` | Force power off (like pulling the plug) |
|
||||
| `shutdown_guest` | Graceful OS shutdown via VMware Tools |
|
||||
| `reboot_guest` | Graceful OS reboot via VMware Tools |
|
||||
| `suspend_vm` | Suspend to memory |
|
||||
| `reset_vm` | Hard reset |
|
||||
|
||||
<Aside type="tip">
|
||||
Use `shutdown_guest` and `reboot_guest` for graceful operations. They require VMware Tools running in the guest.
|
||||
</Aside>
|
||||
|
||||
## Snapshots
|
||||
|
||||
Point-in-time VM state management.
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `create_snapshot` | Create a new snapshot |
|
||||
| `revert_to_snapshot` | Restore VM to snapshot state |
|
||||
| `delete_snapshot` | Remove a specific snapshot |
|
||||
| `delete_all_snapshots` | Remove all snapshots (consolidate) |
|
||||
| `list_snapshots` | List all snapshots |
|
||||
|
||||
## Guest Operations
|
||||
|
||||
Execute commands and transfer files inside VMs without SSH.
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `run_command_in_guest` | Execute any command in the guest OS |
|
||||
| `read_guest_file` | Download file from guest |
|
||||
| `write_guest_file` | Upload file to guest |
|
||||
| `list_guest_processes` | List running processes |
|
||||
| `list_guest_directory` | Browse guest filesystem |
|
||||
| `create_guest_directory` | Create directory in guest |
|
||||
| `delete_guest_file` | Delete file in guest |
|
||||
|
||||
<Aside type="note">
|
||||
Guest operations require VMware Tools installed and running. Use `get_vm_tools_status` to verify.
|
||||
</Aside>
|
||||
|
||||
## Console & Monitoring
|
||||
|
||||
Visual access and performance monitoring.
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `vm_screenshot` | Capture VM console as image |
|
||||
| `get_vm_stats` | CPU, memory, disk, network metrics |
|
||||
| `get_host_stats` | ESXi host performance metrics |
|
||||
| `wait_for_vm_tools` | Block until Tools are ready |
|
||||
| `get_vm_tools_status` | Check Tools installation state |
|
||||
|
||||
## Serial Console
|
||||
|
||||
For network appliances and headless systems.
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `setup_serial_port` | Configure serial port |
|
||||
| `get_serial_port` | Get serial port configuration |
|
||||
| `connect_serial_port` | Get connection URI (telnet/ssh) |
|
||||
| `clear_serial_port` | Clear serial port buffer |
|
||||
| `remove_serial_port` | Remove serial port |
|
||||
|
||||
## Disk Management
|
||||
|
||||
Virtual disk operations.
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `add_disk` | Add new virtual disk |
|
||||
| `remove_disk` | Remove virtual disk |
|
||||
| `resize_disk` | Expand disk capacity |
|
||||
| `list_disks` | List all disks |
|
||||
| `get_disk_info` | Detailed disk information |
|
||||
|
||||
## Network Adapters
|
||||
|
||||
Virtual NIC management.
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `add_nic` | Add network adapter |
|
||||
| `remove_nic` | Remove network adapter |
|
||||
| `connect_nic` | Connect/disconnect NIC |
|
||||
| `change_nic_network` | Change port group |
|
||||
| `list_nics` | List all NICs |
|
||||
| `get_nic_info` | Detailed NIC information |
|
||||
|
||||
## OVF/OVA Operations
|
||||
|
||||
Deploy and export virtual appliances.
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `deploy_ovf` | Deploy OVF/OVA package |
|
||||
| `export_ovf` | Export VM as OVF |
|
||||
| `list_ovf_networks` | List networks in OVF |
|
||||
| `upload_to_datastore` | Upload file to datastore |
|
||||
| `download_from_datastore` | Download file from datastore |
|
||||
|
||||
## Host Management
|
||||
|
||||
ESXi host operations.
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_hosts` | List all ESXi hosts |
|
||||
| `get_host_info` | Host configuration details |
|
||||
| `get_host_hardware` | Hardware inventory |
|
||||
| `get_host_networking` | Network configuration |
|
||||
| `list_services` | Host services |
|
||||
| `get_service_status` | Service state |
|
||||
| `start_service` | Start a host service |
|
||||
| `stop_service` | Stop a host service |
|
||||
| `restart_service` | Restart a host service |
|
||||
| `get_ntp_config` | NTP configuration |
|
||||
|
||||
## Datastore & Resources
|
||||
|
||||
Storage and resource inventory.
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `get_datastore_info` | Datastore capacity and usage |
|
||||
| `browse_datastore` | List datastore contents |
|
||||
| `get_vcenter_info` | vCenter version and configuration |
|
||||
| `get_resource_pool_info` | Resource pool settings |
|
||||
| `get_network_info` | Network/port group details |
|
||||
| `list_templates` | VM templates |
|
||||
| `get_alarms` | Active alarms |
|
||||
| `get_recent_events` | Recent vCenter events |
|
||||
|
||||
## vCenter Operations
|
||||
|
||||
Organization and cluster management.
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `list_folders` | VM and host folders |
|
||||
| `create_folder` | Create new folder |
|
||||
| `delete_folder` | Remove folder |
|
||||
| `move_vm_to_folder` | Organize VMs |
|
||||
| `list_clusters` | Compute clusters |
|
||||
| `get_cluster_info` | Cluster configuration |
|
||||
| `list_resource_pools` | Resource pools |
|
||||
| `create_resource_pool` | Create resource pool |
|
||||
| `delete_resource_pool` | Remove resource pool |
|
||||
| `move_vm_to_resource_pool` | Move VM to pool |
|
||||
| `list_tags` | vCenter tags |
|
||||
| `get_vm_tags` | Tags on a VM |
|
||||
| `apply_tag_to_vm` | Apply tag |
|
||||
| `remove_tag_from_vm` | Remove tag |
|
||||
| `migrate_vm` | vMotion/Storage vMotion |
|
||||
| `list_recent_tasks` | Recent vCenter tasks |
|
||||
| `list_recent_events` | Recent events |
|
||||
| `cancel_task` | Cancel running task |
|
||||
|
||||
## MCP Resources
|
||||
|
||||
In addition to tools, mcvsphere provides 6 real-time resources:
|
||||
|
||||
| Resource URI | Data |
|
||||
|--------------|------|
|
||||
| `esxi://vms` | All virtual machines |
|
||||
| `esxi://hosts` | All ESXi hosts |
|
||||
| `esxi://datastores` | All datastores |
|
||||
| `esxi://networks` | All networks |
|
||||
| `esxi://clusters` | All clusters |
|
||||
| `esxi://resource-pools` | All resource pools |
|
||||
|
||||
Resources provide always-current inventory data that Claude can reference during conversations.
|
||||
@ -1,74 +0,0 @@
|
||||
/* mcvsphere Documentation Custom Styles */
|
||||
|
||||
:root {
|
||||
/* VMware-inspired color palette */
|
||||
--sl-color-accent-low: #1a3a5c;
|
||||
--sl-color-accent: #146eb4;
|
||||
--sl-color-accent-high: #3d9cdb;
|
||||
--sl-color-white: #ffffff;
|
||||
--sl-color-gray-1: #eceef2;
|
||||
--sl-color-gray-2: #c0c2c7;
|
||||
--sl-color-gray-3: #888b96;
|
||||
--sl-color-gray-4: #545861;
|
||||
--sl-color-gray-5: #353841;
|
||||
--sl-color-gray-6: #24272f;
|
||||
--sl-color-black: #17181c;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] {
|
||||
--sl-color-accent-low: #1a3a5c;
|
||||
--sl-color-accent: #3d9cdb;
|
||||
--sl-color-accent-high: #7fbfeb;
|
||||
}
|
||||
|
||||
/* Code block styling */
|
||||
.expressive-code {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
/* Hero section enhancement */
|
||||
.hero {
|
||||
--sl-color-hero-text: var(--sl-color-white);
|
||||
}
|
||||
|
||||
/* Card styling for feature boxes */
|
||||
.card {
|
||||
border-radius: 0.5rem;
|
||||
background: var(--sl-color-gray-6);
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
/* Badge styling for permission levels */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge-reader {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.badge-operator {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.badge-admin {
|
||||
background: #fff3e0;
|
||||
color: #ef6c00;
|
||||
}
|
||||
|
||||
.badge-host-admin {
|
||||
background: #fce4ec;
|
||||
color: #c2185b;
|
||||
}
|
||||
|
||||
.badge-super-admin {
|
||||
background: #f3e5f5;
|
||||
color: #7b1fa2;
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"strictNullChecks": true
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "mcvsphere"
|
||||
version = "0.2.3"
|
||||
version = "0.2.1"
|
||||
description = "Model Control for vSphere - AI-driven VMware virtual machine management via MCP"
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
|
||||
@ -3,18 +3,12 @@
|
||||
Provides decorators and hooks for wrapping tool execution with:
|
||||
- OAuth permission validation
|
||||
- Audit logging with user identity
|
||||
- FastMCP middleware integration for RBAC
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import mcp.types as mt
|
||||
from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext
|
||||
from fastmcp.tools.tool import ToolResult
|
||||
from typing import Any
|
||||
|
||||
from mcvsphere.audit import (
|
||||
audit_log,
|
||||
@ -30,11 +24,6 @@ from mcvsphere.permissions import (
|
||||
get_required_permission,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastmcp.server.context import Context
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def with_permission_check(tool_name: str) -> Callable:
|
||||
"""Decorator factory for permission checking and audit logging.
|
||||
@ -92,26 +81,24 @@ def with_permission_check(tool_name: str) -> Callable:
|
||||
def extract_user_from_context(ctx) -> dict[str, Any] | None:
|
||||
"""Extract user information from FastMCP context.
|
||||
|
||||
Uses FastMCP's dependency injection to get the access token from
|
||||
the current request's context variable.
|
||||
|
||||
Args:
|
||||
ctx: FastMCP Context object (unused, kept for API compatibility).
|
||||
ctx: FastMCP Context object.
|
||||
|
||||
Returns:
|
||||
User info dict from OAuth token claims, or None if not authenticated.
|
||||
"""
|
||||
try:
|
||||
# FastMCP stores access token in a context variable, accessed via dependency
|
||||
from fastmcp.server.dependencies import get_access_token
|
||||
if ctx is None:
|
||||
return None
|
||||
|
||||
access_token = get_access_token()
|
||||
if access_token and hasattr(access_token, "claims"):
|
||||
return access_token.claims
|
||||
except (RuntimeError, ImportError) as e:
|
||||
# RuntimeError: No active HTTP request context
|
||||
# ImportError: FastMCP auth dependencies not available
|
||||
logger.debug("Could not get access token: %s", e)
|
||||
# Try to get access token from context
|
||||
try:
|
||||
# FastMCP stores the access token in request_context
|
||||
if hasattr(ctx, "request_context") and ctx.request_context:
|
||||
token = getattr(ctx.request_context, "access_token", None)
|
||||
if token and hasattr(token, "claims"):
|
||||
return token.claims
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
@ -192,154 +179,3 @@ def get_permission_summary() -> dict[str, list[str]]:
|
||||
summary[level].sort()
|
||||
|
||||
return summary
|
||||
|
||||
|
||||
class RBACMiddleware(Middleware):
|
||||
"""FastMCP middleware for Role-Based Access Control.
|
||||
|
||||
Integrates with FastMCP's middleware system to enforce permissions
|
||||
on every tool call based on OAuth group memberships.
|
||||
|
||||
Example:
|
||||
mcp = FastMCP("my-server", auth=oauth_provider)
|
||||
mcp.add_middleware(RBACMiddleware())
|
||||
"""
|
||||
|
||||
def _extract_user_from_context(
|
||||
self, fastmcp_ctx: "Context | None"
|
||||
) -> dict[str, Any] | None:
|
||||
"""Extract user claims from FastMCP context.
|
||||
|
||||
Uses FastMCP's dependency injection to retrieve the access token
|
||||
from the current request's context variable.
|
||||
|
||||
Args:
|
||||
fastmcp_ctx: FastMCP Context object (unused, kept for API compatibility).
|
||||
|
||||
Returns:
|
||||
User claims dict from OAuth token, or None if not authenticated.
|
||||
"""
|
||||
try:
|
||||
# FastMCP stores access token in a context variable, accessed via dependency
|
||||
from fastmcp.server.dependencies import get_access_token
|
||||
|
||||
access_token = get_access_token()
|
||||
if access_token and hasattr(access_token, "claims"):
|
||||
return access_token.claims
|
||||
except (RuntimeError, ImportError) as e:
|
||||
# RuntimeError: No active HTTP request context
|
||||
# ImportError: FastMCP auth dependencies not available
|
||||
logger.debug("Could not get access token: %s", e)
|
||||
|
||||
return None
|
||||
|
||||
def _get_username(self, claims: dict[str, Any] | None) -> str:
|
||||
"""Extract username from OAuth claims.
|
||||
|
||||
Args:
|
||||
claims: OAuth token claims dict.
|
||||
|
||||
Returns:
|
||||
Username string, or 'anonymous' if no claims.
|
||||
"""
|
||||
if not claims:
|
||||
return "anonymous"
|
||||
|
||||
for claim in ("preferred_username", "email", "sub"):
|
||||
if value := claims.get(claim):
|
||||
return str(value)
|
||||
|
||||
return "unknown"
|
||||
|
||||
def _get_groups(self, claims: dict[str, Any] | None) -> list[str]:
|
||||
"""Extract groups from OAuth claims.
|
||||
|
||||
Args:
|
||||
claims: OAuth token claims dict.
|
||||
|
||||
Returns:
|
||||
List of group names, or empty list if no claims.
|
||||
"""
|
||||
if not claims:
|
||||
return []
|
||||
|
||||
groups = claims.get("groups", [])
|
||||
if isinstance(groups, list):
|
||||
return groups
|
||||
return []
|
||||
|
||||
async def on_call_tool(
|
||||
self,
|
||||
context: MiddlewareContext[mt.CallToolRequestParams],
|
||||
call_next: CallNext[mt.CallToolRequestParams, ToolResult],
|
||||
) -> ToolResult:
|
||||
"""Intercept tool calls to enforce RBAC permissions.
|
||||
|
||||
Args:
|
||||
context: Middleware context containing tool call params.
|
||||
call_next: Next handler in the middleware chain.
|
||||
|
||||
Returns:
|
||||
Tool result if permitted.
|
||||
|
||||
Raises:
|
||||
PermissionDeniedError: If user lacks required permission.
|
||||
"""
|
||||
# Extract tool name and arguments from the request
|
||||
tool_name = context.message.name
|
||||
tool_args = context.message.arguments or {}
|
||||
|
||||
# Get user info from OAuth context
|
||||
claims = self._extract_user_from_context(context.fastmcp_context)
|
||||
username = self._get_username(claims)
|
||||
groups = self._get_groups(claims)
|
||||
|
||||
# Set up audit context for this request
|
||||
set_current_user(claims)
|
||||
|
||||
# Check permission
|
||||
if not check_permission(tool_name, groups):
|
||||
required = get_required_permission(tool_name)
|
||||
logger.warning(
|
||||
"Permission denied: user=%s groups=%s tool=%s required=%s",
|
||||
username,
|
||||
groups,
|
||||
tool_name,
|
||||
required.value,
|
||||
)
|
||||
audit_permission_denied(tool_name, tool_args, required.value)
|
||||
raise PermissionDeniedError(username, tool_name, required)
|
||||
|
||||
# Permission granted - execute tool with timing
|
||||
start_time = time.perf_counter()
|
||||
try:
|
||||
result = await call_next(context)
|
||||
duration_ms = (time.perf_counter() - start_time) * 1000
|
||||
|
||||
# Audit successful execution
|
||||
# ToolResult can be complex, just log that it succeeded
|
||||
audit_log(tool_name, tool_args, result="success", duration_ms=duration_ms)
|
||||
|
||||
logger.debug(
|
||||
"Tool executed: user=%s tool=%s duration=%.2fms",
|
||||
username,
|
||||
tool_name,
|
||||
duration_ms,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except PermissionDeniedError:
|
||||
# Re-raise without additional logging
|
||||
raise
|
||||
except Exception as e:
|
||||
duration_ms = (time.perf_counter() - start_time) * 1000
|
||||
audit_log(tool_name, tool_args, error=str(e), duration_ms=duration_ms)
|
||||
logger.error(
|
||||
"Tool failed: user=%s tool=%s error=%s duration=%.2fms",
|
||||
username,
|
||||
tool_name,
|
||||
str(e),
|
||||
duration_ms,
|
||||
)
|
||||
raise
|
||||
|
||||
@ -120,11 +120,10 @@ TOOL_PERMISSIONS: dict[str, PermissionLevel] = {
|
||||
"configure_ntp": PermissionLevel.HOST_ADMIN,
|
||||
"set_service_policy": PermissionLevel.HOST_ADMIN,
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# FULL_ADMIN - Everything including guest OS and service control (11 tools)
|
||||
# FULL_ADMIN - Everything including guest OS and service control (10 tools)
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
"start_service": PermissionLevel.FULL_ADMIN,
|
||||
"stop_service": PermissionLevel.FULL_ADMIN,
|
||||
"restart_service": PermissionLevel.FULL_ADMIN,
|
||||
# Guest OS operations (requires guest credentials, high privilege)
|
||||
"run_command_in_guest": PermissionLevel.FULL_ADMIN,
|
||||
"list_guest_processes": PermissionLevel.FULL_ADMIN,
|
||||
@ -192,10 +191,9 @@ def get_user_permissions(groups: list[str] | None) -> set[PermissionLevel]:
|
||||
|
||||
Returns:
|
||||
Set of granted permission levels (union of all group permissions).
|
||||
Returns empty set if no recognized groups (deny all access).
|
||||
"""
|
||||
if not groups:
|
||||
return set() # No groups = no permissions (enforces RBAC)
|
||||
return {PermissionLevel.READ_ONLY}
|
||||
|
||||
permissions: set[PermissionLevel] = set()
|
||||
|
||||
@ -203,7 +201,10 @@ def get_user_permissions(groups: list[str] | None) -> set[PermissionLevel]:
|
||||
if group in GROUP_PERMISSIONS:
|
||||
permissions.update(GROUP_PERMISSIONS[group])
|
||||
|
||||
# No fallback - unrecognized groups get no permissions
|
||||
# Default to read-only if no recognized groups
|
||||
if not permissions:
|
||||
permissions.add(PermissionLevel.READ_ONLY)
|
||||
|
||||
return permissions
|
||||
|
||||
|
||||
|
||||
@ -9,7 +9,6 @@ from fastmcp import FastMCP
|
||||
from mcvsphere.auth import create_auth_provider
|
||||
from mcvsphere.config import Settings, get_settings
|
||||
from mcvsphere.connection import VMwareConnection
|
||||
from mcvsphere.middleware import RBACMiddleware
|
||||
from mcvsphere.mixins import (
|
||||
ConsoleMixin,
|
||||
DiskManagementMixin,
|
||||
@ -69,11 +68,6 @@ def create_server(settings: Settings | None = None) -> FastMCP:
|
||||
auth=auth,
|
||||
)
|
||||
|
||||
# Add RBAC middleware when OAuth is enabled
|
||||
if settings.oauth_enabled:
|
||||
mcp.add_middleware(RBACMiddleware())
|
||||
logger.info("RBAC middleware enabled - permissions enforced via OAuth groups")
|
||||
|
||||
# Create shared VMware connection
|
||||
logger.info("Connecting to VMware vCenter/ESXi...")
|
||||
conn = VMwareConnection(settings)
|
||||
@ -143,9 +137,8 @@ def run_server(config_path: Path | None = None) -> None:
|
||||
)
|
||||
if settings.oauth_enabled:
|
||||
print(f"OAuth: ENABLED via {settings.oauth_issuer_url}", file=sys.stderr)
|
||||
print("RBAC: ENABLED - permissions enforced via groups", file=sys.stderr)
|
||||
else:
|
||||
print("OAuth: disabled (single-user mode)", file=sys.stderr)
|
||||
print("OAuth: disabled", file=sys.stderr)
|
||||
print("─" * 40, file=sys.stderr)
|
||||
|
||||
# Create and run server
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user