docker_mcp 0.0.1 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE.txt +21 -0
- data/README.md +246 -10
- data/Rakefile +3 -3
- data/examples/.keep +0 -0
- data/exe/docker_mcp +8 -0
- data/lib/docker_mcp/build_image.rb +127 -0
- data/lib/docker_mcp/copy_to_container.rb +223 -0
- data/lib/docker_mcp/create_container.rb +174 -0
- data/lib/docker_mcp/create_network.rb +152 -0
- data/lib/docker_mcp/create_volume.rb +148 -0
- data/lib/docker_mcp/exec_container.rb +219 -0
- data/lib/docker_mcp/fetch_container_logs.rb +161 -0
- data/lib/docker_mcp/list_containers.rb +66 -0
- data/lib/docker_mcp/list_images.rb +80 -0
- data/lib/docker_mcp/list_networks.rb +84 -0
- data/lib/docker_mcp/list_volumes.rb +91 -0
- data/lib/docker_mcp/pull_image.rb +145 -0
- data/lib/docker_mcp/push_image.rb +179 -0
- data/lib/docker_mcp/recreate_container.rb +168 -0
- data/lib/docker_mcp/remove_container.rb +142 -0
- data/lib/docker_mcp/remove_image.rb +164 -0
- data/lib/docker_mcp/remove_network.rb +136 -0
- data/lib/docker_mcp/remove_volume.rb +146 -0
- data/lib/docker_mcp/run_container.rb +132 -0
- data/lib/docker_mcp/server.rb +108 -0
- data/lib/docker_mcp/start_container.rb +112 -0
- data/lib/docker_mcp/stop_container.rb +126 -0
- data/lib/docker_mcp/tag_image.rb +163 -0
- data/lib/docker_mcp/version.rb +11 -2
- data/lib/docker_mcp.rb +64 -3
- data/sig/docker_mcp.rbs +1 -1
- metadata +63 -6
@@ -0,0 +1,174 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DockerMCP
|
4
|
+
# MCP tool for creating Docker containers without starting them.
|
5
|
+
#
|
6
|
+
# This tool provides the ability to create Docker containers from images with
|
7
|
+
# comprehensive configuration options. Unlike RunContainer, this tool only
|
8
|
+
# creates the container but does not start it, allowing for additional
|
9
|
+
# configuration or manual startup control.
|
10
|
+
#
|
11
|
+
# == Features
|
12
|
+
#
|
13
|
+
# - Create containers from any available Docker image
|
14
|
+
# - Full container configuration support
|
15
|
+
# - Port exposure and binding configuration
|
16
|
+
# - Volume mounting capabilities
|
17
|
+
# - Environment variable configuration
|
18
|
+
# - Custom command specification
|
19
|
+
# - Comprehensive error handling with specific error types
|
20
|
+
#
|
21
|
+
# == Security Considerations
|
22
|
+
#
|
23
|
+
# - Containers inherit Docker daemon security context
|
24
|
+
# - Host configuration can expose host resources
|
25
|
+
# - Volume mounts provide filesystem access
|
26
|
+
# - Port bindings expose services to networks
|
27
|
+
# - Environment variables may contain sensitive data
|
28
|
+
#
|
29
|
+
# Use appropriate security measures:
|
30
|
+
# - Validate image sources and integrity
|
31
|
+
# - Limit host resource exposure
|
32
|
+
# - Use read-only volumes where appropriate
|
33
|
+
# - Restrict network access through host configuration
|
34
|
+
# - Sanitize environment variables
|
35
|
+
#
|
36
|
+
# == Example Usage
|
37
|
+
#
|
38
|
+
# # Simple container creation
|
39
|
+
# CreateContainer.call(
|
40
|
+
# server_context: context,
|
41
|
+
# image: "nginx:latest",
|
42
|
+
# name: "web-server"
|
43
|
+
# )
|
44
|
+
#
|
45
|
+
# # Advanced container with configuration
|
46
|
+
# CreateContainer.call(
|
47
|
+
# server_context: context,
|
48
|
+
# image: "postgres:13",
|
49
|
+
# name: "database",
|
50
|
+
# env: "POSTGRES_PASSWORD=secret,POSTGRES_DB=myapp",
|
51
|
+
# exposed_ports: {"5432/tcp" => {}},
|
52
|
+
# host_config: {
|
53
|
+
# "PortBindings" => {"5432/tcp" => [{"HostPort" => "5432"}]},
|
54
|
+
# "Binds" => ["/host/data:/var/lib/postgresql/data:rw"]
|
55
|
+
# }
|
56
|
+
# )
|
57
|
+
#
|
58
|
+
# @see RunContainer
|
59
|
+
# @see StartContainer
|
60
|
+
# @see Docker::Container.create
|
61
|
+
# @since 0.1.0
|
62
|
+
class CreateContainer < MCP::Tool
|
63
|
+
description 'Create a Docker container'
|
64
|
+
|
65
|
+
input_schema(
|
66
|
+
properties: {
|
67
|
+
image: {
|
68
|
+
type: 'string',
|
69
|
+
description: 'Image name to use (e.g., "ubuntu:22.04")'
|
70
|
+
},
|
71
|
+
name: {
|
72
|
+
type: 'string',
|
73
|
+
description: 'Container name (optional)'
|
74
|
+
},
|
75
|
+
cmd: {
|
76
|
+
type: 'string',
|
77
|
+
description: 'Command to run as space-separated string (optional, e.g., "npm start" or "python app.py")'
|
78
|
+
},
|
79
|
+
env: {
|
80
|
+
type: 'string',
|
81
|
+
description: 'Environment variables as comma-separated KEY=VALUE pairs (optional)'
|
82
|
+
},
|
83
|
+
exposed_ports: {
|
84
|
+
type: 'object',
|
85
|
+
description: 'Exposed ports as {"port/protocol": {}} (optional)'
|
86
|
+
},
|
87
|
+
host_config: {
|
88
|
+
type: 'object',
|
89
|
+
description: 'Host configuration including port bindings, volumes, etc. (optional)'
|
90
|
+
}
|
91
|
+
},
|
92
|
+
required: ['image']
|
93
|
+
)
|
94
|
+
|
95
|
+
# Create a new Docker container from an image.
|
96
|
+
#
|
97
|
+
# This method creates a container with the specified configuration but does
|
98
|
+
# not start it. The container can be started later using StartContainer or
|
99
|
+
# other Docker commands. All configuration parameters are optional except
|
100
|
+
# for the base image.
|
101
|
+
#
|
102
|
+
# @param image [String] Docker image name with optional tag (e.g., "nginx:latest")
|
103
|
+
# @param server_context [Object] MCP server context (unused but required)
|
104
|
+
# @param name [String, nil] custom name for the container
|
105
|
+
# @param cmd [String, nil] command to execute as space-separated string
|
106
|
+
# @param env [String, nil] environment variables as comma-separated KEY=VALUE pairs
|
107
|
+
# @param exposed_ports [Hash, nil] ports to expose in {"port/protocol" => {}} format
|
108
|
+
# @param host_config [Hash, nil] Docker host configuration including bindings and volumes
|
109
|
+
#
|
110
|
+
# @return [MCP::Tool::Response] creation results with container ID and name
|
111
|
+
#
|
112
|
+
# @raise [Docker::Error::NotFoundError] if the specified image doesn't exist
|
113
|
+
# @raise [Docker::Error::ConflictError] if container name already exists
|
114
|
+
# @raise [StandardError] for other creation failures
|
115
|
+
#
|
116
|
+
# @example Create simple container
|
117
|
+
# response = CreateContainer.call(
|
118
|
+
# server_context: context,
|
119
|
+
# image: "alpine:latest",
|
120
|
+
# name: "test-container"
|
121
|
+
# )
|
122
|
+
#
|
123
|
+
# @example Create container with full configuration
|
124
|
+
# response = CreateContainer.call(
|
125
|
+
# server_context: context,
|
126
|
+
# image: "redis:7-alpine",
|
127
|
+
# name: "redis-cache",
|
128
|
+
# env: "REDIS_PASSWORD=secret,REDIS_PORT=6379",
|
129
|
+
# exposed_ports: {"6379/tcp" => {}},
|
130
|
+
# host_config: {
|
131
|
+
# "PortBindings" => {"6379/tcp" => [{"HostPort" => "6379"}]},
|
132
|
+
# "RestartPolicy" => {"Name" => "unless-stopped"}
|
133
|
+
# }
|
134
|
+
# )
|
135
|
+
#
|
136
|
+
# @see Docker::Container.create
|
137
|
+
def self.call(image:, server_context:, name: nil, cmd: nil, env: nil, exposed_ports: nil, host_config: nil)
|
138
|
+
config = { 'Image' => image }
|
139
|
+
config['name'] = name if name
|
140
|
+
|
141
|
+
# Parse cmd string into array if provided
|
142
|
+
config['Cmd'] = Shellwords.split(cmd) if cmd && !cmd.strip.empty?
|
143
|
+
|
144
|
+
# Parse env string into array if provided
|
145
|
+
config['Env'] = env.split(',').map(&:strip) if env && !env.strip.empty?
|
146
|
+
|
147
|
+
config['ExposedPorts'] = exposed_ports if exposed_ports
|
148
|
+
config['HostConfig'] = host_config if host_config
|
149
|
+
|
150
|
+
container = Docker::Container.create(config)
|
151
|
+
container_name = container.info['Names']&.first&.delete_prefix('/')
|
152
|
+
|
153
|
+
MCP::Tool::Response.new([{
|
154
|
+
type: 'text',
|
155
|
+
text: "Container created successfully. ID: #{container.id}, Name: #{container_name}"
|
156
|
+
}])
|
157
|
+
rescue Docker::Error::NotFoundError
|
158
|
+
MCP::Tool::Response.new([{
|
159
|
+
type: 'text',
|
160
|
+
text: "Image #{image} not found"
|
161
|
+
}])
|
162
|
+
rescue Docker::Error::ConflictError
|
163
|
+
MCP::Tool::Response.new([{
|
164
|
+
type: 'text',
|
165
|
+
text: "Container with name #{name} already exists"
|
166
|
+
}])
|
167
|
+
rescue StandardError => e
|
168
|
+
MCP::Tool::Response.new([{
|
169
|
+
type: 'text',
|
170
|
+
text: "Error creating container: #{e.message}"
|
171
|
+
}])
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DockerMCP
|
4
|
+
# MCP tool for creating Docker networks.
|
5
|
+
#
|
6
|
+
# This tool provides the ability to create custom Docker networks for
|
7
|
+
# container communication and isolation. Networks enable containers to
|
8
|
+
# communicate securely while providing isolation from other network
|
9
|
+
# segments.
|
10
|
+
#
|
11
|
+
# == Features
|
12
|
+
#
|
13
|
+
# - Create custom Docker networks
|
14
|
+
# - Support for different network drivers
|
15
|
+
# - Duplicate name detection
|
16
|
+
# - Comprehensive error handling
|
17
|
+
# - Flexible network configuration
|
18
|
+
#
|
19
|
+
# == Network Drivers
|
20
|
+
#
|
21
|
+
# Docker supports various network drivers:
|
22
|
+
# - **bridge**: Default isolated network for single-host networking
|
23
|
+
# - **host**: Remove network isolation, use host networking directly
|
24
|
+
# - **overlay**: Multi-host networking for swarm services
|
25
|
+
# - **macvlan**: Assign MAC addresses to containers
|
26
|
+
# - **none**: Disable networking completely
|
27
|
+
# - **Custom**: Third-party network drivers
|
28
|
+
#
|
29
|
+
# == Security Considerations
|
30
|
+
#
|
31
|
+
# Network creation affects system security:
|
32
|
+
# - **Network Isolation**: Networks provide security boundaries
|
33
|
+
# - **Traffic Control**: Custom networks enable traffic filtering
|
34
|
+
# - **Access Control**: Networks control container communication
|
35
|
+
# - **DNS Resolution**: Networks provide internal DNS services
|
36
|
+
#
|
37
|
+
# Security implications:
|
38
|
+
# - Bridge networks isolate containers from host network
|
39
|
+
# - Host networks expose containers to all host traffic
|
40
|
+
# - Custom networks should follow least-privilege principles
|
41
|
+
# - Network names may reveal infrastructure details
|
42
|
+
#
|
43
|
+
# Best practices:
|
44
|
+
# - Use descriptive but not sensitive network names
|
45
|
+
# - Implement network segmentation strategies
|
46
|
+
# - Limit container access to necessary networks only
|
47
|
+
# - Monitor network traffic and connections
|
48
|
+
# - Regular audit of network configurations
|
49
|
+
#
|
50
|
+
# == Example Usage
|
51
|
+
#
|
52
|
+
# # Create basic bridge network
|
53
|
+
# CreateNetwork.call(
|
54
|
+
# server_context: context,
|
55
|
+
# name: "app-network"
|
56
|
+
# )
|
57
|
+
#
|
58
|
+
# # Create network with specific driver
|
59
|
+
# CreateNetwork.call(
|
60
|
+
# server_context: context,
|
61
|
+
# name: "frontend-net",
|
62
|
+
# driver: "bridge"
|
63
|
+
# )
|
64
|
+
#
|
65
|
+
# # Create without duplicate checking
|
66
|
+
# CreateNetwork.call(
|
67
|
+
# server_context: context,
|
68
|
+
# name: "temp-network",
|
69
|
+
# check_duplicate: false
|
70
|
+
# )
|
71
|
+
#
|
72
|
+
# @see ListNetworks
|
73
|
+
# @see RemoveNetwork
|
74
|
+
# @see Docker::Network.create
|
75
|
+
# @since 0.1.0
|
76
|
+
class CreateNetwork < MCP::Tool
|
77
|
+
description 'Create a Docker network'
|
78
|
+
|
79
|
+
input_schema(
|
80
|
+
properties: {
|
81
|
+
name: {
|
82
|
+
type: 'string',
|
83
|
+
description: 'Name of the network'
|
84
|
+
},
|
85
|
+
driver: {
|
86
|
+
type: 'string',
|
87
|
+
description: 'Driver to use (default: bridge)'
|
88
|
+
},
|
89
|
+
check_duplicate: {
|
90
|
+
type: 'boolean',
|
91
|
+
description: 'Check for networks with duplicate names (default: true)'
|
92
|
+
}
|
93
|
+
},
|
94
|
+
required: ['name']
|
95
|
+
)
|
96
|
+
|
97
|
+
# Create a new Docker network.
|
98
|
+
#
|
99
|
+
# This method creates a custom Docker network with the specified name
|
100
|
+
# and driver. The network can then be used by containers for isolated
|
101
|
+
# communication.
|
102
|
+
#
|
103
|
+
# @param name [String] name for the new network
|
104
|
+
# @param server_context [Object] MCP server context (unused but required)
|
105
|
+
# @param driver [String] network driver to use (default: "bridge")
|
106
|
+
# @param check_duplicate [Boolean] whether to check for duplicate names (default: true)
|
107
|
+
#
|
108
|
+
# @return [MCP::Tool::Response] network creation results with network ID
|
109
|
+
#
|
110
|
+
# @raise [Docker::Error::ConflictError] if network name already exists
|
111
|
+
# @raise [StandardError] for other network creation failures
|
112
|
+
#
|
113
|
+
# @example Create application network
|
114
|
+
# response = CreateNetwork.call(
|
115
|
+
# server_context: context,
|
116
|
+
# name: "webapp-network"
|
117
|
+
# )
|
118
|
+
#
|
119
|
+
# @example Create host network
|
120
|
+
# response = CreateNetwork.call(
|
121
|
+
# server_context: context,
|
122
|
+
# name: "high-performance-net",
|
123
|
+
# driver: "host"
|
124
|
+
# )
|
125
|
+
#
|
126
|
+
# @see Docker::Network.create
|
127
|
+
def self.call(name:, server_context:, driver: 'bridge', check_duplicate: true)
|
128
|
+
options = {
|
129
|
+
'Name' => name,
|
130
|
+
'Driver' => driver,
|
131
|
+
'CheckDuplicate' => check_duplicate
|
132
|
+
}
|
133
|
+
|
134
|
+
network = Docker::Network.create(name, options)
|
135
|
+
|
136
|
+
MCP::Tool::Response.new([{
|
137
|
+
type: 'text',
|
138
|
+
text: "Network #{name} created successfully. ID: #{network.id}"
|
139
|
+
}])
|
140
|
+
rescue Docker::Error::ConflictError
|
141
|
+
MCP::Tool::Response.new([{
|
142
|
+
type: 'text',
|
143
|
+
text: "Network #{name} already exists"
|
144
|
+
}])
|
145
|
+
rescue StandardError => e
|
146
|
+
MCP::Tool::Response.new([{
|
147
|
+
type: 'text',
|
148
|
+
text: "Error creating network: #{e.message}"
|
149
|
+
}])
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DockerMCP
|
4
|
+
# MCP tool for creating Docker volumes.
|
5
|
+
#
|
6
|
+
# This tool provides the ability to create persistent Docker volumes for
|
7
|
+
# data storage that survives container lifecycle events. Volumes are the
|
8
|
+
# preferred mechanism for persisting data generated and used by Docker
|
9
|
+
# containers.
|
10
|
+
#
|
11
|
+
# == Features
|
12
|
+
#
|
13
|
+
# - Create named persistent volumes
|
14
|
+
# - Support for different volume drivers
|
15
|
+
# - Comprehensive error handling
|
16
|
+
# - Duplicate name detection
|
17
|
+
# - Flexible storage configuration
|
18
|
+
#
|
19
|
+
# == Volume Drivers
|
20
|
+
#
|
21
|
+
# Docker supports various volume drivers:
|
22
|
+
# - **local**: Default driver using host filesystem
|
23
|
+
# - **nfs**: Network File System for shared storage
|
24
|
+
# - **cifs**: Common Internet File System
|
25
|
+
# - **rexray**: External storage orchestration
|
26
|
+
# - **Custom**: Third-party volume drivers
|
27
|
+
#
|
28
|
+
# == Persistence Benefits
|
29
|
+
#
|
30
|
+
# Docker volumes provide several advantages:
|
31
|
+
# - **Data Persistence**: Survive container deletion and recreation
|
32
|
+
# - **Performance**: Better performance than bind mounts on Docker Desktop
|
33
|
+
# - **Portability**: Can be moved between containers and hosts
|
34
|
+
# - **Backup**: Easier to backup and restore than container filesystems
|
35
|
+
# - **Sharing**: Can be shared between multiple containers
|
36
|
+
# - **Management**: Managed by Docker daemon with proper lifecycle
|
37
|
+
#
|
38
|
+
# == Security Considerations
|
39
|
+
#
|
40
|
+
# Volume creation affects data security:
|
41
|
+
# - **Data Isolation**: Volumes provide isolation between containers
|
42
|
+
# - **Access Control**: Volume permissions affect data access
|
43
|
+
# - **Storage Location**: Local volumes stored on host filesystem
|
44
|
+
# - **Shared Access**: Multiple containers can access the same volume
|
45
|
+
#
|
46
|
+
# Security implications:
|
47
|
+
# - Volumes persist data beyond container lifecycle
|
48
|
+
# - Volume names may reveal application details
|
49
|
+
# - Shared volumes can leak data between containers
|
50
|
+
# - Storage driver choice affects security properties
|
51
|
+
#
|
52
|
+
# Best practices:
|
53
|
+
# - Use descriptive but not sensitive volume names
|
54
|
+
# - Implement proper volume access controls
|
55
|
+
# - Regular backup of critical volume data
|
56
|
+
# - Monitor volume usage and access patterns
|
57
|
+
# - Choose appropriate drivers for security requirements
|
58
|
+
#
|
59
|
+
# == Example Usage
|
60
|
+
#
|
61
|
+
# # Create basic local volume
|
62
|
+
# CreateVolume.call(
|
63
|
+
# server_context: context,
|
64
|
+
# name: "app-data"
|
65
|
+
# )
|
66
|
+
#
|
67
|
+
# # Create volume with specific driver
|
68
|
+
# CreateVolume.call(
|
69
|
+
# server_context: context,
|
70
|
+
# name: "shared-storage",
|
71
|
+
# driver: "nfs"
|
72
|
+
# )
|
73
|
+
#
|
74
|
+
# @see ListVolumes
|
75
|
+
# @see RemoveVolume
|
76
|
+
# @see Docker::Volume.create
|
77
|
+
# @since 0.1.0
|
78
|
+
class CreateVolume < MCP::Tool
|
79
|
+
description 'Create a Docker volume'
|
80
|
+
|
81
|
+
input_schema(
|
82
|
+
properties: {
|
83
|
+
name: {
|
84
|
+
type: 'string',
|
85
|
+
description: 'Name of the volume'
|
86
|
+
},
|
87
|
+
driver: {
|
88
|
+
type: 'string',
|
89
|
+
description: 'Driver to use (default: local)'
|
90
|
+
}
|
91
|
+
},
|
92
|
+
required: ['name']
|
93
|
+
)
|
94
|
+
|
95
|
+
# Create a new Docker volume for persistent data storage.
|
96
|
+
#
|
97
|
+
# This method creates a named Docker volume that can be used by containers
|
98
|
+
# for persistent data storage. The volume will survive container deletion
|
99
|
+
# and can be shared between multiple containers.
|
100
|
+
#
|
101
|
+
# @param name [String] name for the new volume
|
102
|
+
# @param server_context [Object] MCP server context (unused but required)
|
103
|
+
# @param driver [String] volume driver to use (default: "local")
|
104
|
+
#
|
105
|
+
# @return [MCP::Tool::Response] volume creation results
|
106
|
+
#
|
107
|
+
# @raise [Docker::Error::ConflictError] if volume name already exists
|
108
|
+
# @raise [StandardError] for other volume creation failures
|
109
|
+
#
|
110
|
+
# @example Create application data volume
|
111
|
+
# response = CreateVolume.call(
|
112
|
+
# server_context: context,
|
113
|
+
# name: "webapp-data"
|
114
|
+
# )
|
115
|
+
#
|
116
|
+
# @example Create NFS volume
|
117
|
+
# response = CreateVolume.call(
|
118
|
+
# server_context: context,
|
119
|
+
# name: "shared-files",
|
120
|
+
# driver: "nfs"
|
121
|
+
# )
|
122
|
+
#
|
123
|
+
# @see Docker::Volume.create
|
124
|
+
def self.call(name:, server_context:, driver: 'local')
|
125
|
+
options = {
|
126
|
+
'Name' => name,
|
127
|
+
'Driver' => driver
|
128
|
+
}
|
129
|
+
|
130
|
+
Docker::Volume.create(name, options)
|
131
|
+
|
132
|
+
MCP::Tool::Response.new([{
|
133
|
+
type: 'text',
|
134
|
+
text: "Volume #{name} created successfully"
|
135
|
+
}])
|
136
|
+
rescue Docker::Error::ConflictError
|
137
|
+
MCP::Tool::Response.new([{
|
138
|
+
type: 'text',
|
139
|
+
text: "Volume #{name} already exists"
|
140
|
+
}])
|
141
|
+
rescue StandardError => e
|
142
|
+
MCP::Tool::Response.new([{
|
143
|
+
type: 'text',
|
144
|
+
text: "Error creating volume: #{e.message}"
|
145
|
+
}])
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,219 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DockerMCP
|
4
|
+
# MCP tool for executing commands inside running Docker containers.
|
5
|
+
#
|
6
|
+
# This tool provides the ability to execute arbitrary commands inside running
|
7
|
+
# Docker containers, with full control over execution environment including
|
8
|
+
# working directory, user context, environment variables, and stdin input.
|
9
|
+
#
|
10
|
+
# == ⚠️ CRITICAL SECURITY WARNING ⚠️
|
11
|
+
#
|
12
|
+
# This tool is EXTREMELY DANGEROUS as it allows arbitrary command execution
|
13
|
+
# within Docker containers. This can be used to:
|
14
|
+
# - Execute malicious code inside containers
|
15
|
+
# - Access sensitive data within container filesystems
|
16
|
+
# - Escalate privileges if container is poorly configured
|
17
|
+
# - Perform lateral movement within containerized environments
|
18
|
+
# - Exfiltrate data from applications
|
19
|
+
#
|
20
|
+
# ONLY use this tool in trusted environments with proper security controls:
|
21
|
+
# - Ensure containers run with minimal privileges
|
22
|
+
# - Use read-only filesystems where possible
|
23
|
+
# - Implement proper network segmentation
|
24
|
+
# - Monitor and audit all command executions
|
25
|
+
# - Never expose this tool to untrusted clients
|
26
|
+
#
|
27
|
+
# == Features
|
28
|
+
#
|
29
|
+
# - Execute commands with custom working directory
|
30
|
+
# - Run commands as specific users
|
31
|
+
# - Set custom environment variables
|
32
|
+
# - Provide stdin input to commands
|
33
|
+
# - Configurable timeout protection
|
34
|
+
# - Comprehensive error handling
|
35
|
+
# - Separate stdout/stderr capture
|
36
|
+
#
|
37
|
+
# == Example Usage
|
38
|
+
#
|
39
|
+
# # Simple command execution
|
40
|
+
# ExecContainer.call(
|
41
|
+
# server_context: context,
|
42
|
+
# id: "my-container",
|
43
|
+
# cmd: "ls -la /app"
|
44
|
+
# )
|
45
|
+
#
|
46
|
+
# # Advanced execution with custom environment
|
47
|
+
# ExecContainer.call(
|
48
|
+
# server_context: context,
|
49
|
+
# id: "web-server",
|
50
|
+
# cmd: "python manage.py migrate",
|
51
|
+
# working_dir: "/app",
|
52
|
+
# user: "appuser",
|
53
|
+
# env: "DJANGO_ENV=production,DEBUG=false",
|
54
|
+
# timeout: 120
|
55
|
+
# )
|
56
|
+
#
|
57
|
+
# @see Docker::Container#exec
|
58
|
+
# @since 0.1.0
|
59
|
+
class ExecContainer < MCP::Tool
|
60
|
+
description 'Execute a command inside a running Docker container. ' \
|
61
|
+
'WARNING: This provides arbitrary command execution within the container. ' \
|
62
|
+
'Ensure proper security measures are in place.'
|
63
|
+
|
64
|
+
input_schema(
|
65
|
+
properties: {
|
66
|
+
id: {
|
67
|
+
type: 'string',
|
68
|
+
description: 'Container ID or name'
|
69
|
+
},
|
70
|
+
cmd: {
|
71
|
+
type: 'string',
|
72
|
+
description: 'Command to execute (e.g., "ls -la /app" or "python script.py")'
|
73
|
+
},
|
74
|
+
working_dir: {
|
75
|
+
type: 'string',
|
76
|
+
description: 'Working directory for the command (optional)'
|
77
|
+
},
|
78
|
+
user: {
|
79
|
+
type: 'string',
|
80
|
+
description: 'User to run the command as (optional, e.g., "1000" or "username")'
|
81
|
+
},
|
82
|
+
env: {
|
83
|
+
type: 'string',
|
84
|
+
description: 'Environment variables as comma-separated KEY=VALUE pairs (optional)'
|
85
|
+
},
|
86
|
+
stdin: {
|
87
|
+
type: 'string',
|
88
|
+
description: 'Input to send to the command via stdin (optional)'
|
89
|
+
},
|
90
|
+
timeout: {
|
91
|
+
type: 'integer',
|
92
|
+
description: 'Timeout in seconds (optional, default: 60)'
|
93
|
+
}
|
94
|
+
},
|
95
|
+
required: %w[id cmd]
|
96
|
+
)
|
97
|
+
|
98
|
+
# Execute a command inside a running Docker container.
|
99
|
+
#
|
100
|
+
# This method provides comprehensive command execution capabilities within
|
101
|
+
# Docker containers, including advanced features like custom user context,
|
102
|
+
# environment variables, working directory, and stdin input.
|
103
|
+
#
|
104
|
+
# The command is parsed using shell-like syntax with support for quoted
|
105
|
+
# arguments. Output is captured separately for stdout and stderr, and
|
106
|
+
# the exit code is reported.
|
107
|
+
#
|
108
|
+
# @param id [String] container ID or name to execute command in
|
109
|
+
# @param cmd [String] command to execute (shell-parsed into arguments)
|
110
|
+
# @param server_context [Object] MCP server context (unused but required)
|
111
|
+
# @param working_dir [String, nil] working directory for command execution
|
112
|
+
# @param user [String, nil] user to run command as (username or UID)
|
113
|
+
# @param env [String, nil] environment variables as comma-separated KEY=VALUE pairs
|
114
|
+
# @param stdin [String, nil] input to send to command via stdin
|
115
|
+
# @param timeout [Integer] maximum execution time in seconds (default: 60)
|
116
|
+
#
|
117
|
+
# @return [MCP::Tool::Response] execution results including stdout, stderr, and exit code
|
118
|
+
#
|
119
|
+
# @raise [Docker::Error::NotFoundError] if container doesn't exist
|
120
|
+
# @raise [Docker::Error::TimeoutError] if execution exceeds timeout
|
121
|
+
# @raise [StandardError] for other execution failures
|
122
|
+
#
|
123
|
+
# @example Basic command execution
|
124
|
+
# response = ExecContainer.call(
|
125
|
+
# server_context: context,
|
126
|
+
# id: "web-container",
|
127
|
+
# cmd: "nginx -t"
|
128
|
+
# )
|
129
|
+
#
|
130
|
+
# @example Advanced execution with environment
|
131
|
+
# response = ExecContainer.call(
|
132
|
+
# server_context: context,
|
133
|
+
# id: "app-container",
|
134
|
+
# cmd: "bundle exec rails console",
|
135
|
+
# working_dir: "/app",
|
136
|
+
# user: "rails",
|
137
|
+
# env: "RAILS_ENV=production,DEBUG=true",
|
138
|
+
# timeout: 300
|
139
|
+
# )
|
140
|
+
#
|
141
|
+
# @see Docker::Container#exec
|
142
|
+
def self.call(id:, cmd:, server_context:, working_dir: nil, user: nil,
|
143
|
+
env: nil, stdin: nil, timeout: 60)
|
144
|
+
container = Docker::Container.get(id)
|
145
|
+
|
146
|
+
# Parse command string into array
|
147
|
+
# Simple shell-like parsing: split on spaces but respect quoted strings
|
148
|
+
|
149
|
+
cmd_array = Shellwords.split(cmd)
|
150
|
+
|
151
|
+
# Parse environment variables from comma-separated string to array
|
152
|
+
env_array = nil
|
153
|
+
env_array = env.split(',').map(&:strip) if env && !env.empty?
|
154
|
+
|
155
|
+
# Build exec options
|
156
|
+
exec_options = {
|
157
|
+
'Cmd' => cmd_array,
|
158
|
+
'AttachStdout' => true,
|
159
|
+
'AttachStderr' => true
|
160
|
+
}
|
161
|
+
exec_options['WorkingDir'] = working_dir if working_dir
|
162
|
+
exec_options['User'] = user if user
|
163
|
+
exec_options['Env'] = env_array if env_array
|
164
|
+
exec_options['AttachStdin'] = true if stdin
|
165
|
+
|
166
|
+
# Execute the command
|
167
|
+
stdout_data = []
|
168
|
+
stderr_data = []
|
169
|
+
exit_code = nil
|
170
|
+
|
171
|
+
begin
|
172
|
+
# Use container.exec which returns [stdout, stderr, exit_code]
|
173
|
+
result = if stdin
|
174
|
+
container.exec(cmd_array, stdin: StringIO.new(stdin), wait: timeout)
|
175
|
+
else
|
176
|
+
container.exec(cmd_array, wait: timeout)
|
177
|
+
end
|
178
|
+
|
179
|
+
stdout_data = result[0]
|
180
|
+
stderr_data = result[1]
|
181
|
+
exit_code = result[2]
|
182
|
+
rescue Docker::Error::TimeoutError
|
183
|
+
return MCP::Tool::Response.new([{
|
184
|
+
type: 'text',
|
185
|
+
text: "Command execution timed out after #{timeout} seconds"
|
186
|
+
}])
|
187
|
+
end
|
188
|
+
|
189
|
+
# Format response
|
190
|
+
response_text = "Command executed in container #{id}\n"
|
191
|
+
response_text += "Exit code: #{exit_code}\n\n"
|
192
|
+
|
193
|
+
if stdout_data && !stdout_data.empty?
|
194
|
+
stdout_str = stdout_data.join
|
195
|
+
response_text += "STDOUT:\n#{stdout_str}\n" unless stdout_str.strip.empty?
|
196
|
+
end
|
197
|
+
|
198
|
+
if stderr_data && !stderr_data.empty?
|
199
|
+
stderr_str = stderr_data.join
|
200
|
+
response_text += "\nSTDERR:\n#{stderr_str}\n" unless stderr_str.strip.empty?
|
201
|
+
end
|
202
|
+
|
203
|
+
MCP::Tool::Response.new([{
|
204
|
+
type: 'text',
|
205
|
+
text: response_text.strip
|
206
|
+
}])
|
207
|
+
rescue Docker::Error::NotFoundError
|
208
|
+
MCP::Tool::Response.new([{
|
209
|
+
type: 'text',
|
210
|
+
text: "Container #{id} not found"
|
211
|
+
}])
|
212
|
+
rescue StandardError => e
|
213
|
+
MCP::Tool::Response.new([{
|
214
|
+
type: 'text',
|
215
|
+
text: "Error executing command: #{e.message}"
|
216
|
+
}])
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|