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
9 changes: 2 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/devcontainer-mcp-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ tracing = { workspace = true }
futures-util = "0.3"
async-trait = "0.1"
base64 = "0.22"
shell-escape = "0.1"
shlex = "1"
14 changes: 8 additions & 6 deletions crates/devcontainer-mcp-core/src/file_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@
//! formatting, and helpers to build shell commands for reading/writing files
//! through any backend (DevPod SSH, devcontainer exec, Codespaces SSH).

use std::borrow::Cow;

use base64::{engine::general_purpose::STANDARD, Engine};
use shell_escape::escape;

use crate::error::{Error, Result};

Expand Down Expand Up @@ -57,7 +54,9 @@ pub fn apply_edit(content: &str, old_str: &str, new_str: &str) -> Result<String>

/// Shell-escape a string for safe embedding in a shell command.
fn quote(s: &str) -> String {
escape(Cow::Borrowed(s)).into_owned()
shlex::try_quote(s)
.unwrap_or(std::borrow::Cow::Borrowed(s))
.into_owned()
}

/// Build a shell command that reads a file via `cat`.
Expand Down Expand Up @@ -133,8 +132,11 @@ mod tests {
#[test]
fn test_quote_path_with_single_quote() {
let result = quote("it's");
// Should not break when used in a shell command
assert!(!result.contains("it's") || result.contains("\\'"));
// Result should be shell-safe: either escaped or wrapped in quotes
assert!(
result != "it's",
"single quote must be escaped or quoted, got: {result}"
);
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions crates/devcontainer-mcp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ rmcp = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
schemars = "1"
shlex = "1"
7 changes: 4 additions & 3 deletions crates/devcontainer-mcp/src/tools/devcontainer/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ impl DevContainerMcp {
&self,
Parameters(params): Parameters<DevcontainerBuildParams>,
) -> String {
let extra: Vec<&str> = params
let extra: Vec<String> = params
.extra_args
.as_deref()
.map(|a| a.split_whitespace().collect())
.and_then(shlex::split)
.unwrap_or_default();
match devcontainer::build(&params.workspace_folder, &extra).await {
let extra_refs: Vec<&str> = extra.iter().map(|s| s.as_str()).collect();
match devcontainer::build(&params.workspace_folder, &extra_refs).await {
Ok(output) => format_output(&output),
Err(e) => format!("Error: {e}"),
}
Expand Down
11 changes: 5 additions & 6 deletions crates/devcontainer-mcp/src/tools/devcontainer/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,11 @@ impl DevContainerMcp {
&self,
Parameters(params): Parameters<DevcontainerExecParams>,
) -> String {
let cmd_args: Vec<&str> = params
.args
.as_deref()
.map(|a| a.split_whitespace().collect())
.unwrap_or_default();
match devcontainer::exec(&params.workspace_folder, &params.command, &cmd_args).await {
let full_cmd = match &params.args {
Some(a) => format!("{} {}", params.command, a),
None => params.command,
};
match devcontainer::exec(&params.workspace_folder, "sh", &["-c", &full_cmd]).await {
Ok(output) => format_output(&output),
Err(e) => format!("Error: {e}"),
}
Expand Down
13 changes: 10 additions & 3 deletions crates/devcontainer-mcp/src/tools/devcontainer/up.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,19 @@ impl DevContainerMcp {
&self,
Parameters(params): Parameters<DevcontainerUpParams>,
) -> String {
let extra: Vec<&str> = params
let extra: Vec<String> = params
.extra_args
.as_deref()
.map(|a| a.split_whitespace().collect())
.and_then(shlex::split)
.unwrap_or_default();
match devcontainer::up(&params.workspace_folder, params.config.as_deref(), &extra).await {
let extra_refs: Vec<&str> = extra.iter().map(|s| s.as_str()).collect();
match devcontainer::up(
&params.workspace_folder,
params.config.as_deref(),
&extra_refs,
)
.await
{
Ok(output) => format_output(&output),
Err(e) => format!("Error: {e}"),
}
Expand Down
6 changes: 4 additions & 2 deletions crates/devcontainer-mcp/src/tools/devpod/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ impl DevContainerMcp {
description = "Build a DevPod workspace image without starting it."
)]
async fn devpod_build(&self, Parameters(params): Parameters<DevpodBuildParams>) -> String {
let parts: Vec<&str> = params.args.split_whitespace().collect();
match devpod::build(&parts).await {
let parts: Vec<String> = shlex::split(&params.args)
.unwrap_or_else(|| params.args.split_whitespace().map(String::from).collect());
let part_refs: Vec<&str> = parts.iter().map(|s| s.as_str()).collect();
match devpod::build(&part_refs).await {
Ok(output) => format_output(&output),
Err(e) => format!("Error: {e}"),
}
Expand Down
7 changes: 4 additions & 3 deletions crates/devcontainer-mcp/src/tools/devpod/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,13 @@ impl DevContainerMcp {
&self,
Parameters(params): Parameters<DevpodProviderAddParams>,
) -> String {
let opt_parts: Vec<&str> = params
let opt_parts: Vec<String> = params
.options
.as_deref()
.map(|o| o.split_whitespace().collect())
.and_then(shlex::split)
.unwrap_or_default();
match devpod::provider_add(&params.provider, &opt_parts).await {
let opt_refs: Vec<&str> = opt_parts.iter().map(|s| s.as_str()).collect();
match devpod::provider_add(&params.provider, &opt_refs).await {
Ok(output) => format_output(&output),
Err(e) => format!("Error: {e}"),
}
Expand Down
6 changes: 4 additions & 2 deletions crates/devcontainer-mcp/src/tools/devpod/up.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ impl DevContainerMcp {
description = "Create and start a DevPod workspace. Pass the source (git URL, local path, or image) and any flags as space-separated args. Returns full build output for self-healing."
)]
async fn devpod_up(&self, Parameters(params): Parameters<DevpodUpParams>) -> String {
let parts: Vec<&str> = params.args.split_whitespace().collect();
match devpod::up(&parts).await {
let parts: Vec<String> = shlex::split(&params.args)
.unwrap_or_else(|| params.args.split_whitespace().map(String::from).collect());
let part_refs: Vec<&str> = parts.iter().map(|s| s.as_str()).collect();
match devpod::up(&part_refs).await {
Ok(output) => format_output(&output),
Err(e) => format!("Error: {e}"),
}
Expand Down