story-kit: merge 347_story_mcp_tool_for_shell_command_execution

This commit is contained in:
Dave
2026-03-20 09:51:53 +00:00
parent 31085e8c9f
commit 287c64faf1
4 changed files with 672 additions and 6 deletions

View File

@@ -12,6 +12,7 @@ pub mod agent_tools;
pub mod diagnostics;
pub mod merge_tools;
pub mod qa_tools;
pub mod shell_tools;
pub mod story_tools;
/// Returns true when the Accept header includes text/event-stream.
@@ -33,7 +34,7 @@ struct JsonRpcRequest {
}
#[derive(Serialize)]
struct JsonRpcResponse {
pub(super) struct JsonRpcResponse {
jsonrpc: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<Value>,
@@ -52,7 +53,7 @@ struct JsonRpcError {
}
impl JsonRpcResponse {
fn success(id: Option<Value>, result: Value) -> Self {
pub(super) fn success(id: Option<Value>, result: Value) -> Self {
Self {
jsonrpc: "2.0",
id,
@@ -61,7 +62,7 @@ impl JsonRpcResponse {
}
}
fn error(id: Option<Value>, code: i64, message: String) -> Self {
pub(super) fn error(id: Option<Value>, code: i64, message: String) -> Self {
Self {
jsonrpc: "2.0",
id,
@@ -132,6 +133,9 @@ pub async fn mcp_post_handler(req: &Request, body: Body, ctx: Data<&Arc<AppConte
if tool_name == "get_agent_output" {
return handle_agent_output_sse(rpc.id, &rpc.params, &ctx);
}
if tool_name == "run_command" {
return shell_tools::handle_run_command_sse(rpc.id, &rpc.params, &ctx);
}
}
let resp = match rpc.method.as_str() {
@@ -160,7 +164,7 @@ fn to_json_response(resp: JsonRpcResponse) -> Response {
.body(Body::from(body))
}
fn to_sse_response(resp: JsonRpcResponse) -> Response {
pub(super) fn to_sse_response(resp: JsonRpcResponse) -> Response {
let json = serde_json::to_string(&resp).unwrap_or_default();
let sse_body = format!("data: {json}\n\n");
Response::builder()
@@ -999,6 +1003,28 @@ fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
},
"required": ["story_id", "target_stage"]
}
},
{
"name": "run_command",
"description": "Execute a shell command in an agent's worktree directory. The working_dir must be inside .story_kit/worktrees/. Returns stdout, stderr, exit_code, and timed_out. Supports SSE streaming (send Accept: text/event-stream) for long-running commands. Dangerous commands (rm -rf /, sudo, etc.) are blocked.",
"inputSchema": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The bash command to execute (passed to bash -c)"
},
"working_dir": {
"type": "string",
"description": "Absolute path to the worktree directory to run the command in. Must be inside .story_kit/worktrees/."
},
"timeout": {
"type": "integer",
"description": "Timeout in seconds (default: 120, max: 600)"
}
},
"required": ["command", "working_dir"]
}
}
]
}),
@@ -1079,6 +1105,8 @@ async fn handle_tools_call(
"delete_story" => story_tools::tool_delete_story(&args, ctx).await,
// Arbitrary pipeline movement
"move_story" => diagnostics::tool_move_story(&args, ctx),
// Shell command execution
"run_command" => shell_tools::tool_run_command(&args, ctx).await,
_ => Err(format!("Unknown tool: {tool_name}")),
};
@@ -1188,7 +1216,8 @@ mod tests {
assert!(names.contains(&"get_token_usage"));
assert!(names.contains(&"move_story"));
assert!(names.contains(&"delete_story"));
assert_eq!(tools.len(), 42);
assert!(names.contains(&"run_command"));
assert_eq!(tools.len(), 43);
}
#[test]