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
18 changes: 9 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ on:
push:
branches: [main]

env:
CARGO_TERM_COLOR: always

jobs:
check:
name: Check & Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo check -p devpod-mcp-core -p devpod-mcp
- run: cargo test -p devpod-mcp-core -p devpod-mcp
- run: cargo clippy -p devpod-mcp-core -p devpod-mcp -- -D warnings
- run: cargo fmt --all -- --check
- name: Build and test in devcontainer
uses: devcontainers/ci@v0.3
with:
push: never
runCmd: |
cargo fmt --all -- --check
cargo check -p devpod-mcp-core -p devpod-mcp
cargo test -p devpod-mcp-core -p devpod-mcp
cargo clippy -p devpod-mcp-core -p devpod-mcp -- -D warnings
63 changes: 41 additions & 22 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,60 @@ on:
release:
types: [created]

env:
CARGO_TERM_COLOR: always

permissions:
contents: write

jobs:
build:
name: Build release binaries
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Build all targets in devcontainer
uses: devcontainers/ci@v0.3
with:
push: never
runCmd: |
set -e

# Install cross-compilation toolchain for linux-arm64
sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu
rustup target add aarch64-unknown-linux-gnu x86_64-unknown-linux-gnu
mkdir -p ~/.cargo
echo '[target.aarch64-unknown-linux-gnu]' >> ~/.cargo/config.toml
echo 'linker = "aarch64-linux-gnu-gcc"' >> ~/.cargo/config.toml

# Build all Linux targets
cargo build --release --target x86_64-unknown-linux-gnu -p devpod-mcp
cargo build --release --target aarch64-unknown-linux-gnu -p devpod-mcp

# Copy binaries to output dir
mkdir -p /tmp/release
cp target/x86_64-unknown-linux-gnu/release/devpod-mcp /tmp/release/devpod-mcp-linux-x64
cp target/aarch64-unknown-linux-gnu/release/devpod-mcp /tmp/release/devpod-mcp-linux-arm64
chmod +x /tmp/release/*

- name: Upload linux-x64
uses: softprops/action-gh-release@v2
with:
files: /tmp/release/devpod-mcp-linux-x64

- name: Upload linux-arm64
uses: softprops/action-gh-release@v2
with:
files: /tmp/release/devpod-mcp-linux-arm64

build-macos:
name: Build ${{ matrix.artifact }}
runs-on: ${{ matrix.os }}
runs-on: macos-latest
strategy:
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
artifact: devpod-mcp-linux-x64
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
artifact: devpod-mcp-linux-arm64
cross: true
- target: x86_64-apple-darwin
os: macos-latest
artifact: devpod-mcp-darwin-x64
- target: aarch64-apple-darwin
os: macos-latest
artifact: devpod-mcp-darwin-arm64

steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
Expand All @@ -40,14 +67,6 @@ jobs:
with:
key: ${{ matrix.target }}

- name: Install cross-compilation deps
if: matrix.cross
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
echo '[target.aarch64-unknown-linux-gnu]' >> ~/.cargo/config.toml
echo 'linker = "aarch64-linux-gnu-gcc"' >> ~/.cargo/config.toml

- name: Build
run: cargo build --release --target ${{ matrix.target }} -p devpod-mcp

Expand Down
7 changes: 6 additions & 1 deletion crates/devpod-mcp-core/src/devpod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,12 @@ pub async fn list() -> Result<DevPodOutput> {
// ---------------------------------------------------------------------------

/// `devpod ssh --command` — execute a command in a workspace.
pub async fn ssh_exec(workspace: &str, command: &str, user: Option<&str>, workdir: Option<&str>) -> Result<DevPodOutput> {
pub async fn ssh_exec(
workspace: &str,
command: &str,
user: Option<&str>,
workdir: Option<&str>,
) -> Result<DevPodOutput> {
let mut args = vec!["ssh", workspace, "--command", command];
if let Some(u) = user {
args.push("--user");
Expand Down
7 changes: 1 addition & 6 deletions crates/devpod-mcp-core/src/docker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,7 @@ pub async fn inspect_container(docker: &Docker, name_or_id: &str) -> Result<Cont

/// Stream container logs, returning them as a single string.
/// `tail` limits to the last N lines (0 = all).
pub async fn container_logs(
docker: &Docker,
container_id: &str,
tail: usize,
) -> Result<String> {
pub async fn container_logs(docker: &Docker, container_id: &str, tail: usize) -> Result<String> {
let options = LogsOptions::<String> {
stdout: true,
stderr: true,
Expand All @@ -123,4 +119,3 @@ pub async fn container_logs(

Ok(output)
}

5 changes: 1 addition & 4 deletions crates/devpod-mcp-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ pub enum Error {
DevPodNotFound,

#[error("DevPod command failed (exit code {exit_code}): {stderr}")]
DevPodCommand {
exit_code: i32,
stderr: String,
},
DevPodCommand { exit_code: i32, stderr: String },

#[error("IO error: {0}")]
Io(#[from] std::io::Error),
Expand Down
73 changes: 58 additions & 15 deletions crates/devpod-mcp/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,16 @@ impl DevContainerMcp {
// Workspace lifecycle
// -----------------------------------------------------------------------

#[tool(name = "devpod_up", 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.")]
#[tool(
name = "devpod_up",
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 up(
&self,
#[tool(param)]
#[schemars(description = "All arguments for 'devpod up', e.g. 'https://github.com/org/repo --provider docker --id my-ws'")]
#[schemars(
description = "All arguments for 'devpod up', e.g. 'https://github.com/org/repo --provider docker --id my-ws'"
)]
args: String,
) -> String {
let parts: Vec<&str> = args.split_whitespace().collect();
Expand All @@ -56,7 +61,10 @@ impl DevContainerMcp {
}
}

#[tool(name = "devpod_delete", description = "Delete a DevPod workspace. Stops and removes all associated resources.")]
#[tool(
name = "devpod_delete",
description = "Delete a DevPod workspace. Stops and removes all associated resources."
)]
async fn delete(
&self,
#[tool(param)]
Expand All @@ -72,11 +80,16 @@ impl DevContainerMcp {
}
}

#[tool(name = "devpod_build", description = "Build a DevPod workspace image without starting it.")]
#[tool(
name = "devpod_build",
description = "Build a DevPod workspace image without starting it."
)]
async fn build(
&self,
#[tool(param)]
#[schemars(description = "All arguments for 'devpod build', e.g. 'my-workspace --provider docker'")]
#[schemars(
description = "All arguments for 'devpod build', e.g. 'my-workspace --provider docker'"
)]
args: String,
) -> String {
let parts: Vec<&str> = args.split_whitespace().collect();
Expand All @@ -90,7 +103,10 @@ impl DevContainerMcp {
// Workspace queries
// -----------------------------------------------------------------------

#[tool(name = "devpod_status", description = "Get the status of a DevPod workspace. Returns structured JSON with state (Running, Stopped, Busy, NotFound).")]
#[tool(
name = "devpod_status",
description = "Get the status of a DevPod workspace. Returns structured JSON with state (Running, Stopped, Busy, NotFound)."
)]
async fn status(
&self,
#[tool(param)]
Expand All @@ -106,7 +122,10 @@ impl DevContainerMcp {
}
}

#[tool(name = "devpod_list", description = "List all DevPod workspaces. Returns JSON array with workspace IDs, sources, providers, and status.")]
#[tool(
name = "devpod_list",
description = "List all DevPod workspaces. Returns JSON array with workspace IDs, sources, providers, and status."
)]
async fn list(&self) -> String {
match devpod::list().await {
Ok(output) => format_output(&output),
Expand All @@ -118,7 +137,10 @@ impl DevContainerMcp {
// Command execution
// -----------------------------------------------------------------------

#[tool(name = "devpod_ssh", description = "Execute a command inside a DevPod workspace via SSH. Returns stdout, stderr, and exit code.")]
#[tool(
name = "devpod_ssh",
description = "Execute a command inside a DevPod workspace via SSH. Returns stdout, stderr, and exit code."
)]
async fn ssh(
&self,
#[tool(param)]
Expand All @@ -144,7 +166,10 @@ impl DevContainerMcp {
// Logs
// -----------------------------------------------------------------------

#[tool(name = "devpod_logs", description = "Get logs from a DevPod workspace.")]
#[tool(
name = "devpod_logs",
description = "Get logs from a DevPod workspace."
)]
async fn logs(
&self,
#[tool(param)]
Expand All @@ -161,7 +186,10 @@ impl DevContainerMcp {
// Provider management
// -----------------------------------------------------------------------

#[tool(name = "devpod_provider_list", description = "List all configured DevPod providers.")]
#[tool(
name = "devpod_provider_list",
description = "List all configured DevPod providers."
)]
async fn provider_list(&self) -> String {
match devpod::provider_list().await {
Ok(output) => format_output(&output),
Expand Down Expand Up @@ -189,7 +217,10 @@ impl DevContainerMcp {
}
}

#[tool(name = "devpod_provider_delete", description = "Delete a DevPod provider.")]
#[tool(
name = "devpod_provider_delete",
description = "Delete a DevPod provider."
)]
async fn provider_delete(
&self,
#[tool(param)]
Expand All @@ -206,15 +237,21 @@ impl DevContainerMcp {
// Context management
// -----------------------------------------------------------------------

#[tool(name = "devpod_context_list", description = "List all DevPod contexts.")]
#[tool(
name = "devpod_context_list",
description = "List all DevPod contexts."
)]
async fn context_list(&self) -> String {
match devpod::context_list().await {
Ok(output) => format_output(&output),
Err(e) => format!("Error: {e}"),
}
}

#[tool(name = "devpod_context_use", description = "Switch to a different DevPod context.")]
#[tool(
name = "devpod_context_use",
description = "Switch to a different DevPod context."
)]
async fn context_use(
&self,
#[tool(param)]
Expand All @@ -231,7 +268,10 @@ impl DevContainerMcp {
// Direct Docker (via bollard)
// -----------------------------------------------------------------------

#[tool(name = "devpod_container_inspect", description = "Inspect a Docker container directly — returns labels, ports, mounts, and state. Useful for details DevPod CLI doesn't expose.")]
#[tool(
name = "devpod_container_inspect",
description = "Inspect a Docker container directly — returns labels, ports, mounts, and state. Useful for details DevPod CLI doesn't expose."
)]
async fn container_inspect(
&self,
#[tool(param)]
Expand All @@ -248,7 +288,10 @@ impl DevContainerMcp {
}
}

#[tool(name = "devpod_container_logs", description = "Get Docker container logs directly via the Docker API. Supports tail parameter for last N lines.")]
#[tool(
name = "devpod_container_logs",
description = "Get Docker container logs directly via the Docker API. Supports tail parameter for last N lines."
)]
async fn container_logs(
&self,
#[tool(param)]
Expand Down
Loading