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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 13aa3a020b35fdc5882599c7e62c9507a776ee61bcacf0e503b5112c7983cbab
4
- data.tar.gz: b43e7dd8b18b6872c389211ec44a290e12b110d3e4b5bbe9292387bb581e8b69
3
+ metadata.gz: efb62c57572aa0d7838b21fb503cc82d4d29b1b7a4b67a56a73403abf95569cd
4
+ data.tar.gz: 659cee10a2d4bb77b2a4dabe5acf47c63415e17e67b34f3046eb5bbb149ca9a1
5
5
  SHA512:
6
- metadata.gz: 34ded89e697e7f03614625faf3f0edf4489454385ee707cce29465a739976661f157504bb2a71f497c1abcb6f6fdd9776dab703518a0f5f94f0a30326452ae0b
7
- data.tar.gz: cd6830efe07d41492b349cb15ac11a3ffdbf085de1639e502eef23db62773c4127fa8e8966225bbb4b5eabeb7e25e40fb94471d14490166b14530746eb057eda
6
+ metadata.gz: f24eb8a997aa33b2a4389170fd3d938d9ac0e00fec6f437074497ddfc260d81a8390b61b883caf35901d19f34af271b1d8d32f3b5ff24ca51dea9c885cb9e7c7
7
+ data.tar.gz: 68672e9b0c1432fa2575aea0034d4d4d74e872d83da97456d2fc0cc698249fd643d9325cfc9ed25223f3832b980926212fc98d228d135a10180c7cdd8ea8e0d5
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Aaron F Stanton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md CHANGED
@@ -1,35 +1,271 @@
1
- # DockerMcp
1
+ # DockerMCP
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ A Model Context Protocol (MCP) server that provides comprehensive Docker management capabilities through a standardized interface. This tool enables AI assistants and other MCP clients to interact with Docker containers, images, networks, and volumes programmatically.
4
4
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/docker_mcp`. To experiment with that code, run `bin/console` for an interactive prompt.
5
+ ## ⚠️ Security Warning
6
6
 
7
- ## Installation
7
+ **This tool is inherently unsafe and should be used with extreme caution.**
8
+
9
+ - **Arbitrary Code Execution**: The `exec_container` tool allows execution of arbitrary commands inside Docker containers
10
+ - **File System Access**: The `copy_to_container` tool can copy files from the host system into containers
11
+ - **Container Management**: Full container lifecycle management including creation, modification, and deletion
12
+ - **Network & Volume Control**: Complete control over Docker networks and volumes
8
13
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
14
+ **Recommendations:**
15
+ - Only use in trusted environments
16
+ - Ensure proper Docker daemon security configuration
17
+ - Consider running with restricted Docker permissions
18
+ - Monitor and audit all container operations
19
+ - Be cautious when exposing this tool to external or untrusted MCP clients
20
+
21
+ ## Installation
10
22
 
11
23
  Install the gem and add to the application's Gemfile by executing:
12
24
 
13
25
  ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
26
+ bundle add docker_mcp
15
27
  ```
16
28
 
17
29
  If bundler is not being used to manage dependencies, install the gem by executing:
18
30
 
19
31
  ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
32
+ gem install docker_mcp
21
33
  ```
22
34
 
35
+ ## Prerequisites
36
+
37
+ - Docker Engine installed and running
38
+ - Ruby 3.2+
39
+ - Docker permissions for the user running the MCP server
40
+
23
41
  ## Usage
24
42
 
25
- TODO: Write usage instructions here
43
+ ### MCP Client Configuration
44
+
45
+ Add this to your MCP client configuration after installing the gem:
46
+
47
+ ```json
48
+ {
49
+ "docker_mcp": {
50
+ "command": "bash",
51
+ "args": [
52
+ "-l",
53
+ "-c",
54
+ "docker_mcp"
55
+ ]
56
+ }
57
+ }
58
+ ```
59
+
60
+ ### Example Usage
61
+
62
+ Once configured, you can use the tools through your MCP client:
63
+
64
+ ```
65
+ # List all containers
66
+ list_containers
67
+
68
+ # Create and run a new container
69
+ run_container image="nginx:latest" name="my-web-server"
70
+
71
+ # Execute commands in a container
72
+ exec_container id="my-web-server" cmd="nginx -v"
73
+
74
+ # Copy files to a container
75
+ copy_to_container id="my-web-server" source_path="/local/file.txt" destination_path="/var/www/html/"
76
+
77
+ # View container logs
78
+ fetch_container_logs id="my-web-server"
79
+ ```
80
+
81
+ ## 🔨 Tools
82
+
83
+ This MCP server provides 22 comprehensive Docker management tools organized by functionality:
84
+
85
+ ### Container Management
86
+
87
+ - **`list_containers`** - List all Docker containers (running and stopped) with detailed information
88
+ - **`create_container`** - Create a new container from an image without starting it
89
+ - **`run_container`** - Create and immediately start a container from an image
90
+ - **`start_container`** - Start an existing stopped container
91
+ - **`stop_container`** - Stop a running container gracefully
92
+ - **`remove_container`** - Delete a container (must be stopped first unless forced)
93
+ - **`recreate_container`** - Stop, remove, and recreate a container with the same configuration
94
+ - **`exec_container`** ⚠️ - Execute arbitrary commands inside a running container
95
+ - **`fetch_container_logs`** - Retrieve stdout/stderr logs from a container
96
+ - **`copy_to_container`** ⚠️ - Copy files or directories from host to container
97
+
98
+ ### Image Management
99
+
100
+ - **`list_images`** - List all Docker images available locally
101
+ - **`pull_image`** - Download an image from a Docker registry
102
+ - **`push_image`** - Upload an image to a Docker registry
103
+ - **`build_image`** - Build a new image from a Dockerfile
104
+ - **`tag_image`** - Create a new tag for an existing image
105
+ - **`remove_image`** - Delete an image from local storage
106
+
107
+ ### Network Management
108
+
109
+ - **`list_networks`** - List all Docker networks
110
+ - **`create_network`** - Create a new Docker network
111
+ - **`remove_network`** - Delete a Docker network
112
+
113
+ ### Volume Management
114
+
115
+ - **`list_volumes`** - List all Docker volumes
116
+ - **`create_volume`** - Create a new Docker volume for persistent data
117
+ - **`remove_volume`** - Delete a Docker volume
118
+
119
+ ### Tool Parameters
120
+
121
+ Most tools accept standard Docker parameters:
122
+ - **Container ID/Name**: Can use either the full container ID, short ID, or container name
123
+ - **Image**: Specify images using `name:tag` format (e.g., `nginx:latest`, `ubuntu:22.04`)
124
+ - **Ports**: Use Docker port mapping syntax (e.g., `"8080:80"`)
125
+ - **Volumes**: Use Docker volume mount syntax (e.g., `"/host/path:/container/path"`)
126
+ - **Environment**: Set environment variables as `KEY=VALUE` pairs
127
+
128
+ ## Common Use Cases
129
+
130
+ ### Development Environment Setup
131
+ ```bash
132
+ # Pull development image
133
+ pull_image from_image="node:18-alpine"
134
+
135
+ # Create development container with volume mounts
136
+ run_container image="node:18-alpine" name="dev-env" \
137
+ host_config='{"PortBindings":{"3000/tcp":[{"HostPort":"3000"}]},"Binds":["/local/project:/app"]}'
138
+
139
+ # Execute development commands
140
+ exec_container id="dev-env" cmd="npm install"
141
+ exec_container id="dev-env" cmd="npm start"
142
+ ```
143
+
144
+ ### Container Debugging
145
+ ```bash
146
+ # Check container status
147
+ list_containers
148
+
149
+ # View container logs
150
+ fetch_container_logs id="problematic-container"
151
+
152
+ # Execute diagnostic commands
153
+ exec_container id="problematic-container" cmd="ps aux"
154
+ exec_container id="problematic-container" cmd="df -h"
155
+ exec_container id="problematic-container" cmd="netstat -tlnp"
156
+ ```
157
+
158
+ ### File Management
159
+ ```bash
160
+ # Copy configuration files to container
161
+ copy_to_container id="web-server" \
162
+ source_path="/local/nginx.conf" \
163
+ destination_path="/etc/nginx/"
164
+
165
+ # Copy application code
166
+ copy_to_container id="app-container" \
167
+ source_path="/local/src" \
168
+ destination_path="/app/"
169
+ ```
170
+
171
+ ## Error Handling
172
+
173
+ The server provides detailed error messages for common issues:
174
+
175
+ - **Container Not Found**: When referencing non-existent containers
176
+ - **Image Not Available**: When trying to use images that aren't pulled locally
177
+ - **Permission Denied**: When Docker daemon access is restricted
178
+ - **Network Conflicts**: When creating networks with conflicting configurations
179
+ - **Volume Mount Issues**: When specified paths don't exist or lack permissions
180
+
181
+ All errors include descriptive messages to help diagnose and resolve issues.
182
+
183
+ ## Troubleshooting
184
+
185
+ ### Docker Daemon Connection Issues
186
+ ```bash
187
+ # Check if Docker daemon is running
188
+ docker info
189
+
190
+ # Verify Docker permissions
191
+ docker ps
192
+
193
+ # Check MCP server logs for connection errors
194
+ ```
195
+
196
+ ### Container Operation Failures
197
+ - Ensure container IDs/names are correct (use `list_containers` to verify)
198
+ - Check if containers are in the expected state (running/stopped)
199
+ - Verify image availability with `list_images`
200
+
201
+ ### Permission Issues
202
+ - Ensure the user running the MCP server has Docker permissions
203
+ - Consider adding user to the `docker` group: `sudo usermod -aG docker $USER`
204
+ - Verify Docker socket permissions: `ls -la /var/run/docker.sock`
205
+
206
+ ## Limitations
207
+
208
+ - **Platform Specific**: Some container operations may behave differently across operating systems
209
+ - **Docker API Version**: Requires compatible Docker Engine API version
210
+ - **Resource Limits**: Large file copies and image operations may timeout
211
+ - **Concurrent Operations**: Heavy concurrent usage may impact performance
212
+
213
+ ## Contributing
214
+
215
+ We welcome contributions! Areas for improvement:
216
+
217
+ - **Enhanced Security**: Additional safety checks and permission validation
218
+ - **Better Error Handling**: More specific error messages and recovery suggestions
219
+ - **Performance Optimization**: Streaming for large file operations
220
+ - **Extended Functionality**: Support for Docker Compose, Swarm, etc.
221
+ - **Testing**: Comprehensive test coverage for all tools
26
222
 
27
223
  ## Development
28
224
 
29
225
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
226
 
227
+ ### Running Tests
228
+ ```bash
229
+ # Install dependencies
230
+ bundle install
231
+
232
+ # Run the test suite
233
+ bundle exec rake spec
234
+
235
+ # Run tests with coverage
236
+ bundle exec rake spec COVERAGE=true
237
+ ```
238
+
239
+ ### Local Development Setup
240
+ ```bash
241
+ # Clone the repository
242
+ git clone https://github.com/afstanton/docker_mcp.git
243
+ cd docker_mcp
244
+
245
+ # Install dependencies
246
+ bin/setup
247
+
248
+ # Start development console
249
+ bin/console
250
+
251
+ # Build the gem locally
252
+ bundle exec rake build
253
+
254
+ # Install locally built gem
255
+ bundle exec rake install
256
+ ```
257
+
258
+ ### Testing with MCP Client
259
+ ```bash
260
+ # Start the MCP server locally
261
+ bundle exec exe/docker_mcp
262
+
263
+ # Configure your MCP client to use local development server
264
+ # Use file path instead of installed gem command
265
+ ```
266
+
31
267
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
268
 
33
- ## Contributing
269
+ ## License
34
270
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/docker_mcp.
271
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
- require "rubocop/rake_task"
8
+ require 'rubocop/rake_task'
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
data/examples/.keep ADDED
File without changes
data/exe/docker_mcp ADDED
@@ -0,0 +1,8 @@
1
+ require 'docker_mcp'
2
+
3
+ # Set up the server
4
+ server = DockerMCP::Server.new.server
5
+
6
+ # Create and start the transport
7
+ transport = MCP::Server::Transports::StdioTransport.new(server)
8
+ transport.open
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerMCP
4
+ # MCP tool for building Docker images from Dockerfile content.
5
+ #
6
+ # This tool provides the ability to build Docker images by providing Dockerfile
7
+ # content as a string. It supports optional tagging of the resulting image for
8
+ # easy identification and reuse.
9
+ #
10
+ # == Security Considerations
11
+ #
12
+ # Building Docker images can be potentially dangerous:
13
+ # - Dockerfile commands execute with Docker daemon privileges
14
+ # - Images can contain malicious software or backdoors
15
+ # - Build process can access host resources (network, files)
16
+ # - Base images may contain vulnerabilities
17
+ # - Build context may expose sensitive information
18
+ #
19
+ # Security recommendations:
20
+ # - Review Dockerfile content carefully before building
21
+ # - Use trusted base images from official repositories
22
+ # - Scan built images for vulnerabilities
23
+ # - Limit network access during builds
24
+ # - Avoid including secrets in Dockerfile instructions
25
+ # - Use multi-stage builds to minimize final image size
26
+ #
27
+ # == Features
28
+ #
29
+ # - Build images from Dockerfile content strings
30
+ # - Optional image tagging during build
31
+ # - Comprehensive error handling
32
+ # - Support for all standard Dockerfile instructions
33
+ # - Returns image ID and build status
34
+ #
35
+ # == Example Usage
36
+ #
37
+ # # Simple image build
38
+ # BuildImage.call(
39
+ # server_context: context,
40
+ # dockerfile: "FROM alpine:latest\nRUN apk add --no-cache curl"
41
+ # )
42
+ #
43
+ # # Build with custom tag
44
+ # BuildImage.call(
45
+ # server_context: context,
46
+ # dockerfile: dockerfile_content,
47
+ # tag: "myapp:v1.0"
48
+ # )
49
+ #
50
+ # @see Docker::Image.build
51
+ # @see TagImage
52
+ # @since 0.1.0
53
+ class BuildImage < MCP::Tool
54
+ description 'Build a Docker image'
55
+
56
+ input_schema(
57
+ properties: {
58
+ dockerfile: {
59
+ type: 'string',
60
+ description: 'Dockerfile content as a string'
61
+ },
62
+ tag: {
63
+ type: 'string',
64
+ description: 'Tag for the built image (e.g., "myimage:latest")'
65
+ }
66
+ },
67
+ required: ['dockerfile']
68
+ )
69
+
70
+ # Build a Docker image from Dockerfile content.
71
+ #
72
+ # This method creates a Docker image by building from the provided Dockerfile
73
+ # content string. The Dockerfile is processed by the Docker daemon and can
74
+ # include any valid Dockerfile instructions. Optionally, the resulting image
75
+ # can be tagged with a custom name for easy reference.
76
+ #
77
+ # @param dockerfile [String] the complete Dockerfile content as a string
78
+ # @param server_context [Object] MCP server context (unused but required)
79
+ # @param tag [String, nil] optional tag to apply to the built image
80
+ #
81
+ # @return [MCP::Tool::Response] build results including image ID and tag info
82
+ #
83
+ # @raise [Docker::Error] for Docker daemon communication errors
84
+ # @raise [StandardError] for build failures or other errors
85
+ #
86
+ # @example Build simple image
87
+ # dockerfile = <<~DOCKERFILE
88
+ # FROM alpine:latest
89
+ # RUN apk add --no-cache nginx
90
+ # EXPOSE 80
91
+ # CMD ["nginx", "-g", "daemon off;"]
92
+ # DOCKERFILE
93
+ #
94
+ # response = BuildImage.call(
95
+ # server_context: context,
96
+ # dockerfile: dockerfile,
97
+ # tag: "my-nginx:latest"
98
+ # )
99
+ #
100
+ # @see Docker::Image.build
101
+ def self.call(dockerfile:, server_context:, tag: nil)
102
+ # Build the image
103
+ image = Docker::Image.build(dockerfile)
104
+
105
+ # If a tag was specified, tag the image
106
+ if tag
107
+ # Split tag into repo and tag parts
108
+ repo, image_tag = tag.split(':', 2)
109
+ image_tag ||= 'latest'
110
+ image.tag('repo' => repo, 'tag' => image_tag, 'force' => true)
111
+ end
112
+
113
+ response_text = "Image built successfully. ID: #{image.id}"
114
+ response_text += ", Tag: #{tag}" if tag
115
+
116
+ MCP::Tool::Response.new([{
117
+ type: 'text',
118
+ text: response_text
119
+ }])
120
+ rescue StandardError => e
121
+ MCP::Tool::Response.new([{
122
+ type: 'text',
123
+ text: "Error building image: #{e.message}"
124
+ }])
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerMCP
4
+ # MCP tool for copying files and directories from host to Docker containers.
5
+ #
6
+ # This tool provides the ability to copy files or entire directory trees from
7
+ # the host filesystem into running Docker containers. It uses Docker's archive
8
+ # streaming API to efficiently transfer files while preserving permissions and
9
+ # directory structure.
10
+ #
11
+ # == ⚠️ SECURITY WARNING ⚠️
12
+ #
13
+ # This tool can be dangerous as it allows:
14
+ # - Reading arbitrary files from the host filesystem
15
+ # - Writing files into container filesystems
16
+ # - Potentially overwriting critical container files
17
+ # - Escalating privileges if used with setuid/setgid files
18
+ # - Exposing sensitive host data to containers
19
+ #
20
+ # Security recommendations:
21
+ # - Validate source paths to prevent directory traversal
22
+ # - Ensure containers run with minimal privileges
23
+ # - Monitor file copy operations for sensitive paths
24
+ # - Use read-only filesystems where possible
25
+ # - Implement proper access controls on source files
26
+ #
27
+ # == Features
28
+ #
29
+ # - Copy individual files or entire directories
30
+ # - Preserve file permissions and directory structure
31
+ # - Optional ownership changes after copy
32
+ # - Comprehensive error handling
33
+ # - Support for both absolute and relative paths
34
+ #
35
+ # == Example Usage
36
+ #
37
+ # # Copy a configuration file
38
+ # CopyToContainer.call(
39
+ # server_context: context,
40
+ # id: "web-server",
41
+ # source_path: "/host/config/nginx.conf",
42
+ # destination_path: "/etc/nginx/"
43
+ # )
44
+ #
45
+ # # Copy directory with ownership change
46
+ # CopyToContainer.call(
47
+ # server_context: context,
48
+ # id: "app-container",
49
+ # source_path: "/host/app/src",
50
+ # destination_path: "/app/",
51
+ # owner: "appuser:appgroup"
52
+ # )
53
+ #
54
+ # @see Docker::Container#archive_in_stream
55
+ # @since 0.1.0
56
+ class CopyToContainer < MCP::Tool
57
+ description 'Copy a file or directory from the local filesystem into a running Docker container. ' \
58
+ 'The source path is on the local machine, and the destination path is inside the container.'
59
+
60
+ input_schema(
61
+ properties: {
62
+ id: {
63
+ type: 'string',
64
+ description: 'Container ID or name'
65
+ },
66
+ source_path: {
67
+ type: 'string',
68
+ description: 'Path to the file or directory on the local filesystem to copy'
69
+ },
70
+ destination_path: {
71
+ type: 'string',
72
+ description: 'Path inside the container where the file/directory should be copied'
73
+ },
74
+ owner: {
75
+ type: 'string',
76
+ description: 'Owner for the copied files (optional, e.g., "1000:1000" or "username:group")'
77
+ }
78
+ },
79
+ required: %w[id source_path destination_path]
80
+ )
81
+
82
+ # Copy files or directories from host filesystem to a Docker container.
83
+ #
84
+ # This method creates a tar archive of the source path and streams it into
85
+ # the specified container using Docker's archive API. The operation preserves
86
+ # file permissions and directory structure. Optionally, ownership can be
87
+ # changed after the copy operation completes.
88
+ #
89
+ # The source path must exist on the host filesystem and be readable by the
90
+ # process running the MCP server. The destination path must be a valid path
91
+ # within the container.
92
+ #
93
+ # @param id [String] container ID or name to copy files into
94
+ # @param source_path [String] path to file/directory on host filesystem
95
+ # @param destination_path [String] destination path inside container
96
+ # @param server_context [Object] MCP server context (unused but required)
97
+ # @param owner [String, nil] ownership specification (e.g., "user:group", "1000:1000")
98
+ #
99
+ # @return [MCP::Tool::Response] success/failure message with operation details
100
+ #
101
+ # @raise [Docker::Error::NotFoundError] if container doesn't exist
102
+ # @raise [StandardError] for file system or Docker API errors
103
+ #
104
+ # @example Copy configuration file
105
+ # response = CopyToContainer.call(
106
+ # server_context: context,
107
+ # id: "nginx-container",
108
+ # source_path: "/etc/nginx/sites-available/default",
109
+ # destination_path: "/etc/nginx/sites-enabled/"
110
+ # )
111
+ #
112
+ # @example Copy directory with ownership
113
+ # response = CopyToContainer.call(
114
+ # server_context: context,
115
+ # id: "app-container",
116
+ # source_path: "/local/project",
117
+ # destination_path: "/app/",
118
+ # owner: "www-data:www-data"
119
+ # )
120
+ #
121
+ # @see Docker::Container#archive_in_stream
122
+ # @see #add_to_tar
123
+ def self.call(id:, source_path:, destination_path:, server_context:, owner: nil)
124
+ container = Docker::Container.get(id)
125
+
126
+ # Verify source path exists
127
+ unless File.exist?(source_path)
128
+ return MCP::Tool::Response.new([{
129
+ type: 'text',
130
+ text: "Source path not found: #{source_path}"
131
+ }])
132
+ end
133
+
134
+ # Create a tar archive of the source
135
+ tar_io = StringIO.new
136
+ tar_io.set_encoding('ASCII-8BIT')
137
+
138
+ Gem::Package::TarWriter.new(tar_io) do |tar|
139
+ add_to_tar(tar, source_path, File.basename(source_path))
140
+ end
141
+
142
+ tar_io.rewind
143
+
144
+ # Copy to container
145
+ container.archive_in_stream(destination_path) do
146
+ tar_io.read
147
+ end
148
+
149
+ # Optionally change ownership
150
+ if owner
151
+ chown_path = File.join(destination_path, File.basename(source_path))
152
+ container.exec(['chown', '-R', owner, chown_path])
153
+ end
154
+
155
+ file_type = File.directory?(source_path) ? 'directory' : 'file'
156
+ response_text = "Successfully copied #{file_type} from #{source_path} to #{id}:#{destination_path}"
157
+ response_text += "\nOwnership changed to #{owner}" if owner
158
+
159
+ MCP::Tool::Response.new([{
160
+ type: 'text',
161
+ text: response_text
162
+ }])
163
+ rescue Docker::Error::NotFoundError
164
+ MCP::Tool::Response.new([{
165
+ type: 'text',
166
+ text: "Container #{id} not found"
167
+ }])
168
+ rescue StandardError => e
169
+ MCP::Tool::Response.new([{
170
+ type: 'text',
171
+ text: "Error copying to container: #{e.message}"
172
+ }])
173
+ end
174
+
175
+ # Recursively add files and directories to a tar archive.
176
+ #
177
+ # This helper method builds a tar archive by recursively traversing
178
+ # the filesystem starting from the given path. It preserves file
179
+ # permissions and handles both files and directories appropriately.
180
+ #
181
+ # For directories, it creates directory entries in the tar and then
182
+ # recursively processes all contained files and subdirectories.
183
+ # For files, it reads the content and adds it to the tar with
184
+ # preserved permissions.
185
+ #
186
+ # @param tar [Gem::Package::TarWriter] the tar writer instance
187
+ # @param path [String] the filesystem path to add to the archive
188
+ # @param archive_path [String] the path within the tar archive
189
+ #
190
+ # @return [void]
191
+ #
192
+ # @example Add single file
193
+ # add_to_tar(tar_writer, "/host/file.txt", "file.txt")
194
+ #
195
+ # @example Add directory tree
196
+ # add_to_tar(tar_writer, "/host/mydir", "mydir")
197
+ #
198
+ # @see Gem::Package::TarWriter#mkdir
199
+ # @see Gem::Package::TarWriter#add_file_simple
200
+ def self.add_to_tar(tar, path, archive_path)
201
+ if File.directory?(path)
202
+ # Add directory entry
203
+ tar.mkdir(archive_path, File.stat(path).mode)
204
+
205
+ # Add directory contents
206
+ Dir.entries(path).each do |entry|
207
+ next if ['.', '..'].include?(entry)
208
+
209
+ full_path = File.join(path, entry)
210
+ archive_entry_path = File.join(archive_path, entry)
211
+ add_to_tar(tar, full_path, archive_entry_path)
212
+ end
213
+ else
214
+ # Add file
215
+ File.open(path, 'rb') do |file|
216
+ tar.add_file_simple(archive_path, File.stat(path).mode, file.size) do |tar_file|
217
+ IO.copy_stream(file, tar_file)
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end