use super::super::{
get_thread_working_directory, set_thread_working_directory, McpFunction, McpToolCall,
McpToolResult,
};
use anyhow::Result;
use serde_json::{json, Value};
pub fn get_workdir_function() -> McpFunction {
McpFunction {
name: "workdir".to_string(),
description: "Get or set the working directory for file and shell operations.
This tool manages the runtime working directory used by all MCP tools (shell, text_editor, etc.).
Parameters:
- `path`: Optional path to set as new working directory. If not provided, returns current working directory.
- `reset`: If true, resets to the original project directory (ignores `path` parameter).
Use Cases:
- Parallel execution: Each actor can work in its own isolated git worktree
- Testing: Switch to a test directory before running tests
- Multi-project workflows: Work across multiple related projects
Examples:
- Get current: `{\"path\": null}` or `{}`
- Set new: `{\"path\": \"/path/to/worktree\"}`
- Reset: `{\"reset\": true}`
Note: This changes the working directory for the current thread only.
File paths in subsequent tool calls will be relative to this directory.".to_string(),
parameters: json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Optional path to set as new working directory. Can be absolute or relative to current working directory."
},
"reset": {
"type": "boolean",
"default": false,
"description": "If true, reset to original project directory (ignores 'path' parameter)"
}
}
}),
}
}
pub async fn execute_workdir_command(call: &McpToolCall) -> Result<McpToolResult> {
let reset = call
.parameters
.get("reset")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if reset {
let original_dir =
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
set_thread_working_directory(Some(original_dir.clone()));
return Ok(McpToolResult::success(
"workdir".to_string(),
call.tool_id.clone(),
json!({
"success": true,
"action": "reset",
"working_directory": original_dir.to_string_lossy(),
"message": format!("Working directory reset to: {}", original_dir.display())
})
.to_string(),
));
}
match call.parameters.get("path") {
Some(Value::String(path_str)) if !path_str.trim().is_empty() => {
let path_str = path_str.trim();
let new_path = if std::path::Path::new(path_str).is_absolute() {
std::path::PathBuf::from(path_str)
} else {
let current = get_thread_working_directory();
current.join(path_str)
};
let canonical_path = match new_path.canonicalize() {
Ok(p) => p,
Err(e) => {
return Ok(McpToolResult::error(
"workdir".to_string(),
call.tool_id.clone(),
format!(
"Path does not exist or is not accessible: {} (error: {})",
new_path.display(),
e
),
));
}
};
if !canonical_path.is_dir() {
return Ok(McpToolResult::error(
"workdir".to_string(),
call.tool_id.clone(),
format!("Path is not a directory: {}", canonical_path.display()),
));
}
let old_dir = get_thread_working_directory();
set_thread_working_directory(Some(canonical_path.clone()));
Ok(McpToolResult::success(
"workdir".to_string(),
call.tool_id.clone(),
json!({
"success": true,
"action": "set",
"previous_directory": old_dir.to_string_lossy(),
"working_directory": canonical_path.to_string_lossy(),
"message": format!("Working directory changed from {} to {}", old_dir.display(), canonical_path.display())
}).to_string(),
))
}
Some(_) => Ok(McpToolResult::error(
"workdir".to_string(),
call.tool_id.clone(),
"Parameter 'path' must be a non-empty string".to_string(),
)),
None => {
let current_dir = get_thread_working_directory();
Ok(McpToolResult::success(
"workdir".to_string(),
call.tool_id.clone(),
json!({
"success": true,
"action": "get",
"working_directory": current_dir.to_string_lossy(),
"message": format!("Current working directory: {}", current_dir.display())
})
.to_string(),
))
}
}
}