Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 41 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ SPDX-License-Identifier: Apache-2.0

# openstack-mcp-server

MCP (Model Context Protocol) server for OpenStack and SAP Converged Cloud. Provides AI coding agents with typed, structured tools for managing infrastructure — 86 tools across 18 services (72 read + 14 write).
MCP (Model Context Protocol) server for OpenStack and SAP Converged Cloud. Provides AI coding agents with typed, structured tools for managing infrastructure — 119 tools across 18 services (91 read + 16 write + 12 admin).

## Quick Start

Expand Down Expand Up @@ -37,17 +37,17 @@ claude mcp add openstack openstack-mcp-server \
### Standard OpenStack
| Service | Tools | Description |
|---------|-------|-------------|
| **Nova** (Compute) | `nova_list_servers`, `nova_get_server`, `nova_list_flavors`, `nova_list_keypairs`, `nova_list_availability_zones`, `nova_get_quotas`, `nova_server_action`*, `nova_create_server`* | Servers, flavors, keypairs, AZs, quotas, actions |
| **Neutron** (Networking) | `neutron_list_networks`, `neutron_list_subnets`, `neutron_list_ports`, `neutron_list_security_groups`, `neutron_list_routers`, `neutron_list_floating_ips`, `neutron_create_security_group_rule`*, `neutron_delete_security_group_rule`* | Networks, subnets, ports, security groups, routers, floating IPs |
| **Cinder** (Block Storage) | `cinder_list_volumes`, `cinder_get_volume`, `cinder_list_snapshots`, `cinder_get_snapshot`, `cinder_list_volume_types`, `cinder_get_quotas`, `cinder_create_volume`*, `cinder_delete_volume`* | Volumes, snapshots, types, quotas |
| **Keystone** (Identity) | `keystone_list_projects`, `keystone_token_info`, `keystone_list_application_credentials`, `keystone_list_domains`, `keystone_list_users`, `keystone_list_roles`, `keystone_create_application_credential`*, `keystone_delete_application_credential`* | Projects, auth info, domains, users, roles, app credentials |
| **Designate** (DNS) | `designate_list_zones`, `designate_get_zone`, `designate_list_recordsets`, `designate_create_recordset`*, `designate_delete_recordset`* | DNS zones and records |
| **Barbican** (Key Manager) | `barbican_list_secrets`, `barbican_get_secret` | Secrets metadata (no payloads) |
| **Swift** (Object Storage) | `swift_list_containers`, `swift_list_objects`, `swift_get_object_metadata`, `swift_upload_object`*, `swift_delete_object`* | Containers and objects |
| **Manila** (Shared Filesystems) | `manila_list_shares`, `manila_get_share`, `manila_list_access_rules`, `manila_list_share_networks` | File shares, access rules, share networks |
| **Octavia** (Load Balancer) | `octavia_list_loadbalancers`, `octavia_get_loadbalancer`, `octavia_list_listeners`, `octavia_list_pools`, `octavia_list_members`, `octavia_list_healthmonitors`, `octavia_list_l7policies`, `octavia_create_loadbalancer`*, `octavia_delete_loadbalancer`* | Load balancers, listeners, pools, members, health monitors, L7 policies |
| **Glance** (Image) | `glance_list_images`, `glance_get_image` | Images |
| **Ironic** (Bare Metal) | `ironic_list_nodes`, `ironic_get_node`, `ironic_list_node_ports`, `ironic_list_allocations` | Baremetal nodes, ports, allocations |
| **Nova** (Compute) | `nova_list_servers`, `nova_get_server`, `nova_list_flavors`, `nova_list_keypairs`, `nova_list_availability_zones`, `nova_get_quotas`, `nova_list_instance_actions`, `nova_list_server_groups`, `nova_list_volume_attachments`, `nova_server_action`\*, `nova_create_server`\*, `nova_list_hypervisors`†, `nova_get_hypervisor`†, `nova_list_services`†, `nova_list_aggregates`† | Servers, flavors, keypairs, AZs, quotas, actions, hypervisors, aggregates |
| **Neutron** (Networking) | `neutron_list_networks`, `neutron_list_subnets`, `neutron_list_ports`, `neutron_list_security_groups`, `neutron_list_routers`, `neutron_list_floating_ips`, `neutron_list_trunks`, `neutron_list_network_ip_availabilities`, `neutron_list_bgpvpn_interconnections`, `neutron_create_security_group_rule`\*, `neutron_delete_security_group_rule`\*, `neutron_create_floating_ip`\*, `neutron_delete_floating_ip`\*, `neutron_list_agents`† | Networks, subnets, ports, SGs, routers, FIPs, trunks, agents |
| **Cinder** (Block Storage) | `cinder_list_volumes`, `cinder_get_volume`, `cinder_list_snapshots`, `cinder_get_snapshot`, `cinder_list_volume_types`, `cinder_get_quotas`, `cinder_list_backups`, `cinder_list_transfers`, `cinder_create_volume`\*, `cinder_delete_volume`\*, `cinder_list_services`† | Volumes, snapshots, types, quotas, backups, transfers |
| **Keystone** (Identity) | `keystone_list_projects`, `keystone_token_info`, `keystone_list_application_credentials`, `keystone_list_domains`, `keystone_list_users`, `keystone_list_roles`, `keystone_create_application_credential`\*, `keystone_delete_application_credential`\*, `keystone_list_role_assignments`†, `keystone_list_groups`† | Projects, auth, domains, users, roles, app credentials, groups |
| **Designate** (DNS) | `designate_list_zones`, `designate_get_zone`, `designate_list_recordsets`, `designate_list_zone_transfer_requests`, `designate_list_zone_transfer_accepts`, `designate_create_recordset`\*, `designate_delete_recordset`\* | DNS zones, records, zone transfers |
| **Barbican** (Key Manager) | `barbican_list_secrets`, `barbican_get_secret`, `barbican_list_containers`, `barbican_get_container`, `barbican_list_orders` | Secrets, containers, orders (metadata only) |
| **Swift** (Object Storage) | `swift_list_containers`, `swift_list_objects`, `swift_get_object_metadata`, `swift_upload_object`\*, `swift_delete_object`\* | Containers and objects |
| **Manila** (Shared Filesystems) | `manila_list_shares`, `manila_get_share`, `manila_list_access_rules`, `manila_list_share_networks`, `manila_list_snapshots`, `manila_list_security_services`, `manila_list_share_types` | Shares, access rules, networks, snapshots, security services |
| **Octavia** (Load Balancer) | `octavia_list_loadbalancers`, `octavia_get_loadbalancer`, `octavia_list_listeners`, `octavia_list_pools`, `octavia_list_members`, `octavia_list_healthmonitors`, `octavia_list_l7policies`, `octavia_list_l7rules`, `octavia_create_loadbalancer`\*, `octavia_delete_loadbalancer`\*, `octavia_list_amphorae`† | Load balancers, listeners, pools, members, L7, amphorae |
| **Glance** (Image) | `glance_list_images`, `glance_get_image`, `glance_list_image_members`, `glance_list_tasks`† | Images, members, import tasks |
| **Ironic** (Bare Metal) | `ironic_list_nodes`, `ironic_get_node`, `ironic_list_node_ports`, `ironic_list_allocations`, `ironic_list_portgroups`, `ironic_list_chassis`†, `ironic_node_power_state`†\* | Baremetal nodes, ports, allocations, portgroups, chassis |

### SAP Converged Cloud
| Service | Tools | Description |
Expand All @@ -60,7 +60,9 @@ claude mcp add openstack openstack-mcp-server \
| **Castellum** (Autoscaling) | `castellum_get_project_resources`, `castellum_list_pending_operations`, `castellum_list_recently_failed_operations` | Resource autoscaling |
| **Cronus** (Email) | `cronus_get_usage`, `cronus_list_templates` | Email service usage and templates |

*\* Mutating tools (14 total) — disabled in read-only mode (default). Set `MCP_READ_ONLY=false` to enable.*
*\* Mutating tools (16 total) — disabled in read-only mode (default). Set `MCP_READ_ONLY=false` to enable.*

*† Admin tools (12 total) — hidden by default. Set `MCP_ADMIN_TOOLS=true` to enable. Requires cloud admin role.*

## Configuration

Expand Down Expand Up @@ -123,6 +125,7 @@ Add to your MCP client's configuration file (e.g., `.cursor/mcp.json`):
| `OS_APPCRED_SECRET_CMD` | Shell command to retrieve app credential secret | — |
| `MCP_TRANSPORT` | Transport: `stdio` or `sse` | `stdio` |
| `MCP_READ_ONLY` | Set to `false` to enable mutating tools | `true` |
| `MCP_ADMIN_TOOLS` | Set to `true` to enable admin-only tools (requires cloud admin role) | `false` |
| `MCP_DEBUG` | Enable debug logging | `false` |
| `SAPCC_*_ENDPOINT` | Override SAP CC service endpoints | — |

Expand All @@ -133,13 +136,14 @@ Add to your MCP client's configuration file (e.g., `.cursor/mcp.json`):
| Layer | Mechanism | Effect |
|-------|-----------|--------|
| **1. Read-Only Mode** | `MCP_READ_ONLY=true` (default) | Mutating tools are not registered — invisible to the LLM |
| **2. Confirmed Pattern** | Two-call execution | First call returns a preview of what will happen; second call with `confirmed=true` executes |
| **3. Semantic Guardrails** | Domain-specific validation | Rejects dangerous operations outright (e.g., 0.0.0.0/0 on SSH, deleting in-use volumes) |
| **4. Credential Isolation** | Secrets held in server memory only | Auth tokens and passwords never reach the LLM |
| **2. Admin Gating** | `MCP_ADMIN_TOOLS=false` (default) | Admin tools are not registered unless explicitly enabled |
| **3. Confirmed Pattern** | Two-call execution | First call returns a preview of what will happen; second call with `confirmed=true` executes |
| **4. Semantic Guardrails** | Domain-specific validation | Rejects dangerous operations outright (e.g., 0.0.0.0/0 on SSH, deleting in-use volumes) |
| **5. Credential Isolation** | Secrets held in server memory only | Auth tokens and passwords never reach the LLM |

### Write Safety: The Confirmed Pattern

All 14 write tools implement a two-call safety pattern:
All 16 write tools implement a two-call safety pattern:

```
1st call (confirmed absent/false):
Expand All @@ -165,22 +169,38 @@ Write tools include domain-specific safety rules that reject dangerous operation

### Read-Only Mode (Default)

By default, all 14 mutating tools are **disabled** (`MCP_READ_ONLY=true`). Set `MCP_READ_ONLY=false` only when you explicitly need write operations. The write tools are:
By default, all 16 mutating tools are **disabled** (`MCP_READ_ONLY=true`). Set `MCP_READ_ONLY=false` only when you explicitly need write operations. The write tools are:

- `nova_server_action`, `nova_create_server`
- `neutron_create_security_group_rule`, `neutron_delete_security_group_rule`
- `neutron_create_security_group_rule`, `neutron_delete_security_group_rule`, `neutron_create_floating_ip`, `neutron_delete_floating_ip`
- `cinder_create_volume`, `cinder_delete_volume`
- `designate_create_recordset`, `designate_delete_recordset`
- `swift_upload_object`, `swift_delete_object`
- `octavia_create_loadbalancer`, `octavia_delete_loadbalancer`
- `keystone_create_application_credential`, `keystone_delete_application_credential`

Additionally, `ironic_node_power_state` requires **both** `MCP_READ_ONLY=false` and `MCP_ADMIN_TOOLS=true`.

### Admin Tools

Admin tools are hidden by default and require `MCP_ADMIN_TOOLS=true` to enable. These tools require cloud admin privileges and provide infrastructure-wide visibility:

| Service | Admin Tools |
|---------|------------|
| **Nova** | `nova_list_hypervisors`, `nova_get_hypervisor`, `nova_list_services`, `nova_list_aggregates` |
| **Neutron** | `neutron_list_agents` |
| **Cinder** | `cinder_list_services` |
| **Octavia** | `octavia_list_amphorae` |
| **Glance** | `glance_list_tasks` |
| **Ironic** | `ironic_list_chassis`, `ironic_node_power_state`\* |
| **Keystone** | `keystone_list_role_assignments`, `keystone_list_groups` |

### Tool Annotations (Human-in-the-Loop)

All tools declare their intent via [MCP tool annotations](https://modelcontextprotocol.io/specification/2025-03-26/server/tools#annotations):

- **Read-only tools** (72 tools): Annotated with `readOnlyHint: true`. Clients may auto-approve these.
- **Destructive tools** (14 tools): Annotated with `destructiveHint: true`. Clients **must prompt the user** before execution.
- **Read-only tools** (91 tools): Annotated with `readOnlyHint: true`. Clients may auto-approve these.
- **Destructive tools** (16 tools + 1 admin write): Annotated with `destructiveHint: true`. Clients **must prompt the user** before execution.

This means even when `MCP_READ_ONLY=false` enables destructive tools, the MCP client (Claude Code, Cursor, etc.) will still ask "Allow this action?" before executing. The server declares intent, the client enforces the gate.

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/gophercloud/utils/v2 v2.0.0-20260424064311-2eeed4ceb3e9
github.com/mark3labs/mcp-go v0.52.0
github.com/sapcc/go-bits v0.0.0-20260504092817-9df533508379
github.com/sapcc/gophercloud-sapcc/v2 v2.0.5
github.com/spf13/cobra v1.10.2
gopkg.in/yaml.v3 v3.0.1
)
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEV
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/sapcc/go-bits v0.0.0-20260504092817-9df533508379 h1:NJ1gqb2S4CNrdsCk90iM3TTG9QXKrrgb/tG3sxuzxfw=
github.com/sapcc/go-bits v0.0.0-20260504092817-9df533508379/go.mod h1:DxXen6GflUaHbQ9qQFwxJp4xQWzRKm3mzUgGrXbwn84=
github.com/sapcc/gophercloud-sapcc/v2 v2.0.5 h1:pWVIM0qP3YYAoZNx8jtfX+8nBSJ03ETDyrrdBmI/b6I=
github.com/sapcc/gophercloud-sapcc/v2 v2.0.5/go.mod h1:ylQgPjcEWI6Kw7GvG5e8iEfIP+p1oiFQgrAImGIGvac=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
Expand Down
10 changes: 10 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ type Config struct {
// Enabled by default for safety. Set MCP_READ_ONLY=false to allow mutations.
ReadOnly bool `yaml:"read_only"`

// AdminTools enables admin-only tools (hypervisors, services, agents, etc.).
// Disabled by default. Set MCP_ADMIN_TOOLS=true to expose admin tools.
// These tools require OpenStack admin role to function.
AdminTools bool `yaml:"admin_tools"`

// SAPCC holds SAP Converged Cloud-specific configuration.
SAPCC SAPCCConfig `yaml:"sapcc"`
}
Expand Down Expand Up @@ -91,6 +96,11 @@ func Load() (*Config, error) {
cfg.ReadOnly = false
}

// MCP_ADMIN_TOOLS defaults false; explicitly set to "true" to enable admin tools.
if os.Getenv("MCP_ADMIN_TOOLS") == "true" {
cfg.AdminTools = true
}

// SAP CC endpoint overrides
cfg.SAPCC.KeppelEndpoint = osext.GetenvOrDefault("SAPCC_KEPPEL_ENDPOINT", cfg.SAPCC.KeppelEndpoint)
cfg.SAPCC.ArcherEndpoint = osext.GetenvOrDefault("SAPCC_ARCHER_ENDPOINT", cfg.SAPCC.ArcherEndpoint)
Expand Down
23 changes: 12 additions & 11 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,18 +76,20 @@ func (s *Server) Run() error {
// registerTools registers all OpenStack service tools with the MCP server.
func (s *Server) registerTools() {
readOnly := s.cfg.ReadOnly
admin := s.cfg.AdminTools

// Standard OpenStack services
nova.Register(s.mcp, s.provider, readOnly)
neutron.Register(s.mcp, s.provider, readOnly)
cinder.Register(s.mcp, s.provider, readOnly)
keystone.Register(s.mcp, s.provider, readOnly)
designate.Register(s.mcp, s.provider, readOnly)
barbican.Register(s.mcp, s.provider)
swift.Register(s.mcp, s.provider, readOnly)
manila.Register(s.mcp, s.provider)
octavia.Register(s.mcp, s.provider, readOnly)
glance.Register(s.mcp, s.provider)
nova.Register(s.mcp, s.provider, readOnly, admin)
neutron.Register(s.mcp, s.provider, readOnly, admin)
cinder.Register(s.mcp, s.provider, readOnly, admin)
keystone.Register(s.mcp, s.provider, readOnly, admin)
designate.Register(s.mcp, s.provider, readOnly, admin)
barbican.Register(s.mcp, s.provider, admin)
swift.Register(s.mcp, s.provider, readOnly, admin)
manila.Register(s.mcp, s.provider, admin)
octavia.Register(s.mcp, s.provider, readOnly, admin)
glance.Register(s.mcp, s.provider, admin)
ironic.Register(s.mcp, s.provider, readOnly, admin)

// SAP CC-specific services
hermes.Register(s.mcp, s.provider)
Expand All @@ -97,5 +99,4 @@ func (s *Server) registerTools() {
maia.Register(s.mcp, s.provider)
castellum.Register(s.mcp, s.provider)
cronus.Register(s.mcp, s.provider)
ironic.Register(s.mcp, s.provider)
}
51 changes: 40 additions & 11 deletions internal/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,25 +37,54 @@ func TestAllModulesRegisterWithoutPanic(t *testing.T) {

// Each Register call captures the provider pointer in closures.
// Passing nil is safe because we never invoke the handlers.
nova.Register(s, nil, false)
neutron.Register(s, nil, false)
cinder.Register(s, nil, false)
keystone.Register(s, nil, false)
designate.Register(s, nil, false)
barbican.Register(s, nil)
swift.Register(s, nil, false)
manila.Register(s, nil)
octavia.Register(s, nil, false)
glance.Register(s, nil)
nova.Register(s, nil, false, false)
neutron.Register(s, nil, false, false)
cinder.Register(s, nil, false, false)
keystone.Register(s, nil, false, false)
designate.Register(s, nil, false, false)
barbican.Register(s, nil, false)
swift.Register(s, nil, false, false)
manila.Register(s, nil, false)
octavia.Register(s, nil, false, false)
glance.Register(s, nil, false)
hermes.Register(s, nil)
limes.Register(s, nil)
keppel.Register(s, nil)
archer.Register(s, nil)
maia.Register(s, nil)
castellum.Register(s, nil)
cronus.Register(s, nil)
ironic.Register(s, nil)
ironic.Register(s, nil, false, false)

// If we reach here, all 18 modules registered without panic.
t.Logf("All 18 service modules registered successfully")
}

// TestAllModulesRegisterWithAdmin exercises the admin=true and readOnly=false
// registration paths. This ensures admin tool definitions (hypervisors, agents,
// chassis, role_assignments, etc.) and admin write tools (node_power_state) are
// well-formed and do not panic during registration.
func TestAllModulesRegisterWithAdmin(t *testing.T) {
s := mcpserver.NewMCPServer("test", "0.0.1", mcpserver.WithToolCapabilities(true))

nova.Register(s, nil, false, true)
neutron.Register(s, nil, false, true)
cinder.Register(s, nil, false, true)
keystone.Register(s, nil, false, true)
designate.Register(s, nil, false, true)
barbican.Register(s, nil, true)
swift.Register(s, nil, false, true)
manila.Register(s, nil, true)
octavia.Register(s, nil, false, true)
glance.Register(s, nil, true)
hermes.Register(s, nil)
limes.Register(s, nil)
keppel.Register(s, nil)
archer.Register(s, nil)
maia.Register(s, nil)
castellum.Register(s, nil)
cronus.Register(s, nil)
ironic.Register(s, nil, false, true)

t.Logf("All 18 service modules registered with admin=true successfully")
}
Loading
Loading