Skip to main content

claude_pool_server/
tools.rs

1//! MCP tool definitions for claude-pool.
2
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use claude_pool::PoolStore;
7use claude_pool::skill::{SkillScope, SkillSource};
8use claude_pool::types::SlotConfig;
9use schemars::JsonSchema;
10use serde::Deserialize;
11use tower_mcp::ToolBuilder;
12use tower_mcp::protocol::CallToolResult;
13use tower_mcp::tool::Tool;
14
15use crate::State;
16
17// ── Input schemas ────────────────────────────────────────────────────
18
19#[derive(Debug, Deserialize, JsonSchema)]
20pub struct RunInput {
21    /// The prompt/task to execute.
22    pub prompt: String,
23    /// Model override for this task.
24    pub model: Option<String>,
25    /// Effort override for this task (min, low, medium, high, max).
26    pub effort: Option<String>,
27    /// Additional MCP servers for this task (merged with global/slot servers).
28    /// Keys are server names, values are server config objects.
29    pub mcp_servers: Option<std::collections::HashMap<String, serde_json::Value>>,
30}
31
32#[derive(Debug, Deserialize, JsonSchema)]
33pub struct SubmitInput {
34    /// The prompt/task to execute.
35    pub prompt: String,
36    /// Model override for this task.
37    pub model: Option<String>,
38    /// Effort override for this task (min, low, medium, high, max).
39    pub effort: Option<String>,
40    /// Tags for grouping/filtering.
41    pub tags: Option<Vec<String>>,
42    /// Additional MCP servers for this task (merged with global/slot servers).
43    /// Keys are server names, values are server config objects.
44    pub mcp_servers: Option<std::collections::HashMap<String, serde_json::Value>>,
45}
46
47#[derive(Debug, Deserialize, JsonSchema)]
48pub struct TaskIdInput {
49    /// The task ID to look up.
50    pub task_id: String,
51}
52
53#[derive(Debug, Deserialize, JsonSchema)]
54pub struct FanOutInput {
55    /// List of prompts to execute in parallel.
56    pub prompts: Vec<String>,
57}
58
59#[derive(Debug, Deserialize, JsonSchema)]
60pub struct ContextSetInput {
61    /// Context key.
62    pub key: String,
63    /// Context value.
64    pub value: String,
65}
66
67#[derive(Debug, Deserialize, JsonSchema)]
68pub struct ContextKeyInput {
69    /// Context key.
70    pub key: String,
71}
72
73#[derive(Debug, Deserialize, JsonSchema)]
74pub struct ConfigureSlotInput {
75    /// Slot ID to configure (e.g. "slot-0").
76    pub slot_id: String,
77    /// Human-readable name for the slot.
78    pub name: Option<String>,
79    /// Role classification for the slot.
80    pub role: Option<String>,
81    /// Description of the slot's purpose.
82    pub description: Option<String>,
83}
84
85#[derive(Debug, Deserialize, JsonSchema)]
86pub struct InvokeWorkflowInput {
87    /// Workflow name (e.g. "issue_to_pr", "refactor_and_test", "review_and_fix").
88    pub workflow: String,
89    /// Workflow arguments as key-value pairs (e.g. {"issue_url": "https://..."}).
90    #[serde(default)]
91    pub arguments: std::collections::HashMap<String, String>,
92    /// Tags for the workflow task.
93    pub tags: Option<Vec<String>>,
94}
95
96#[derive(Debug, Deserialize, JsonSchema)]
97pub struct ScalingInput {
98    /// Number of slots to add or remove.
99    pub count: usize,
100}
101
102#[derive(Debug, Deserialize, JsonSchema)]
103pub struct SetTargetSlotsInput {
104    /// Target number of slots.
105    pub target: usize,
106}
107
108// ── Skill management input schemas ──────────────────────────────────
109
110/// Input for listing skills with optional filters.
111#[derive(Debug, Deserialize, JsonSchema)]
112pub struct SkillListInput {
113    /// Filter by scope: "task", "coordinator", "chain".
114    pub scope: Option<String>,
115    /// Filter by source: "builtin", "project", "runtime".
116    pub source: Option<String>,
117}
118
119/// Input for getting a skill by name.
120#[derive(Debug, Deserialize, JsonSchema)]
121pub struct SkillGetInput {
122    /// Skill name.
123    pub name: String,
124}
125
126/// Argument definition for a skill being added.
127#[derive(Debug, Deserialize, JsonSchema)]
128pub struct SkillArgumentInput {
129    /// Argument name (used as `{name}` placeholder in the prompt template).
130    pub name: String,
131    /// Human-readable description.
132    pub description: String,
133    /// Whether this argument is required.
134    #[serde(default)]
135    pub required: bool,
136}
137
138/// Input for adding a new skill at runtime.
139#[derive(Debug, Deserialize, JsonSchema)]
140pub struct SkillAddInput {
141    /// Unique skill name.
142    pub name: String,
143    /// Human-readable description.
144    pub description: String,
145    /// Prompt template. Use `{arg_name}` placeholders for arguments.
146    pub prompt: String,
147    /// Argument definitions.
148    #[serde(default)]
149    pub arguments: Vec<SkillArgumentInput>,
150    /// Where this skill runs: "task" (default), "coordinator", "chain".
151    pub scope: Option<String>,
152    /// Per-skill config overrides as JSON (model, effort, etc.).
153    pub config: Option<serde_json::Value>,
154}
155
156/// Input for removing a skill by name.
157#[derive(Debug, Deserialize, JsonSchema)]
158pub struct SkillRemoveInput {
159    /// Skill name to remove.
160    pub name: String,
161}
162
163/// Input for saving a skill to disk.
164#[derive(Debug, Deserialize, JsonSchema)]
165pub struct SkillSaveInput {
166    /// Skill name to save.
167    pub name: String,
168    /// Directory to save to. Defaults to the configured skills_dir.
169    pub dir: Option<String>,
170}
171
172// ── Helpers ──────────────────────────────────────────────────────────
173
174fn parse_effort(s: &str) -> Option<claude_pool::Effort> {
175    match s.to_lowercase().as_str() {
176        "min" | "low" => Some(claude_pool::Effort::Low),
177        "medium" => Some(claude_pool::Effort::Medium),
178        "high" => Some(claude_pool::Effort::High),
179        "max" => Some(claude_pool::Effort::Max),
180        _ => None,
181    }
182}
183
184fn task_config_from(
185    model: Option<String>,
186    effort: Option<String>,
187    mcp_servers: Option<std::collections::HashMap<String, serde_json::Value>>,
188) -> Option<SlotConfig> {
189    if model.is_none() && effort.is_none() && mcp_servers.is_none() {
190        return None;
191    }
192    Some(SlotConfig {
193        model,
194        effort: effort.and_then(|e| parse_effort(&e)),
195        mcp_servers,
196        ..Default::default()
197    })
198}
199
200fn parse_scope(s: &str) -> SkillScope {
201    match s {
202        "coordinator" => SkillScope::Coordinator,
203        "chain" => SkillScope::Chain,
204        _ => SkillScope::Task,
205    }
206}
207
208fn parse_isolation(s: Option<&str>) -> claude_pool::chain::ChainIsolation {
209    match s {
210        Some("none") => claude_pool::chain::ChainIsolation::None,
211        _ => claude_pool::chain::ChainIsolation::Worktree,
212    }
213}
214
215fn parse_source(s: &str) -> Option<SkillSource> {
216    match s {
217        "builtin" => Some(SkillSource::Builtin),
218        "project" => Some(SkillSource::Project),
219        "runtime" => Some(SkillSource::Runtime),
220        _ => None,
221    }
222}
223
224// ── Tool builders ────────────────────────────────────────────────────
225
226pub fn pool_status_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
227    ToolBuilder::new("pool_status")
228        .title("Pool Status")
229        .description("Get pool status: slots, tasks in flight, budget")
230        .read_only()
231        .no_params_handler(move || {
232            let state = Arc::clone(&state);
233            async move {
234                match state.pool.status().await {
235                    Ok(status) => Ok(CallToolResult::json(serde_json::to_value(&status).unwrap())),
236                    Err(e) => Ok(CallToolResult::error(e.to_string())),
237                }
238            }
239        })
240        .build()
241}
242
243pub fn pool_run_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
244    ToolBuilder::new("pool_run")
245        .title("Run Task (Sync)")
246        .description(
247            "Run a task synchronously on the next available slot. Blocks until completion.",
248        )
249        .handler(move |input: RunInput| {
250            let state = Arc::clone(&state);
251            async move {
252                let config = task_config_from(input.model, input.effort, input.mcp_servers);
253                match state.pool.run_with_config(&input.prompt, config).await {
254                    Ok(result) => Ok(CallToolResult::json(serde_json::to_value(&result).unwrap())),
255                    Err(e) => Ok(CallToolResult::error(e.to_string())),
256                }
257            }
258        })
259        .build()
260}
261
262pub fn pool_submit_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
263    ToolBuilder::new("pool_submit")
264        .title("Submit Task (Async)")
265        .description("Submit a task for async execution. Returns a task_id immediately.")
266        .handler(move |input: SubmitInput| {
267            let state = Arc::clone(&state);
268            async move {
269                let config = task_config_from(input.model, input.effort, input.mcp_servers);
270                let tags = input.tags.unwrap_or_default();
271                match state
272                    .pool
273                    .submit_with_config(&input.prompt, config, tags)
274                    .await
275                {
276                    Ok(task_id) => Ok(CallToolResult::json(
277                        serde_json::json!({ "task_id": task_id.0 }),
278                    )),
279                    Err(e) => Ok(CallToolResult::error(e.to_string())),
280                }
281            }
282        })
283        .build()
284}
285
286pub fn pool_result_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
287    ToolBuilder::new("pool_result")
288        .title("Get Task Result")
289        .description("Check/collect result for a submitted task. Returns null if still running.")
290        .read_only()
291        .handler(move |input: TaskIdInput| {
292            let state = Arc::clone(&state);
293            async move {
294                let task_id = claude_pool::TaskId(input.task_id);
295                match state.pool.result(&task_id).await {
296                    Ok(Some(r)) => Ok(CallToolResult::json(serde_json::to_value(&r).unwrap())),
297                    Ok(None) => Ok(CallToolResult::json(
298                        serde_json::json!({ "status": "running" }),
299                    )),
300                    Err(e) => Ok(CallToolResult::error(e.to_string())),
301                }
302            }
303        })
304        .build()
305}
306
307pub fn pool_cancel_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
308    ToolBuilder::new("pool_cancel")
309        .title("Cancel Task")
310        .description("Cancel a pending or running task.")
311        .handler(move |input: TaskIdInput| {
312            let state = Arc::clone(&state);
313            async move {
314                let task_id = claude_pool::TaskId(input.task_id);
315                match state.pool.cancel(&task_id).await {
316                    Ok(()) => Ok(CallToolResult::text("cancelled")),
317                    Err(e) => Ok(CallToolResult::error(e.to_string())),
318                }
319            }
320        })
321        .build()
322}
323
324pub fn pool_fan_out_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
325    ToolBuilder::new("pool_fan_out")
326        .title("Fan Out (Parallel)")
327        .description(
328            "Execute multiple tasks in parallel across available slots. Returns all results.",
329        )
330        .handler(move |input: FanOutInput| {
331            let state = Arc::clone(&state);
332            async move {
333                let prompts: Vec<&str> = input.prompts.iter().map(|s| s.as_str()).collect();
334                match state.pool.fan_out(&prompts).await {
335                    Ok(results) => Ok(CallToolResult::json(
336                        serde_json::json!({ "results": results }),
337                    )),
338                    Err(e) => Ok(CallToolResult::error(e.to_string())),
339                }
340            }
341        })
342        .build()
343}
344
345pub fn pool_drain_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
346    ToolBuilder::new("pool_drain")
347        .title("Drain Pool")
348        .description(
349            "Gracefully shut down the pool. Waits for in-flight tasks, then stops all slots.",
350        )
351        .destructive()
352        .no_params_handler(move || {
353            let state = Arc::clone(&state);
354            async move {
355                match state.pool.drain().await {
356                    Ok(summary) => Ok(CallToolResult::json(
357                        serde_json::to_value(&summary).unwrap(),
358                    )),
359                    Err(e) => Ok(CallToolResult::error(e.to_string())),
360                }
361            }
362        })
363        .build()
364}
365
366pub fn context_set_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
367    ToolBuilder::new("context_set")
368        .title("Set Context")
369        .description("Set a shared context value. Context is injected into slot system prompts.")
370        .handler(move |input: ContextSetInput| {
371            let state = Arc::clone(&state);
372            async move {
373                state.pool.set_context(input.key, input.value);
374                Ok(CallToolResult::text("ok"))
375            }
376        })
377        .build()
378}
379
380pub fn context_get_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
381    ToolBuilder::new("context_get")
382        .title("Get Context")
383        .description("Get a shared context value by key.")
384        .read_only()
385        .handler(move |input: ContextKeyInput| {
386            let state = Arc::clone(&state);
387            async move {
388                match state.pool.get_context(&input.key) {
389                    Some(value) => Ok(CallToolResult::text(value)),
390                    None => Ok(CallToolResult::error(format!(
391                        "key not found: {}",
392                        input.key
393                    ))),
394                }
395            }
396        })
397        .build()
398}
399
400pub fn context_delete_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
401    ToolBuilder::new("context_delete")
402        .title("Delete Context")
403        .description("Delete a shared context value by key.")
404        .handler(move |input: ContextKeyInput| {
405            let state = Arc::clone(&state);
406            async move {
407                state.pool.delete_context(&input.key);
408                Ok(CallToolResult::text("ok"))
409            }
410        })
411        .build()
412}
413
414pub fn context_list_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
415    ToolBuilder::new("context_list")
416        .title("List Context")
417        .description("List all shared context keys and values.")
418        .read_only()
419        .no_params_handler(move || {
420            let state = Arc::clone(&state);
421            async move {
422                let entries = state.pool.list_context();
423                let map: serde_json::Map<String, serde_json::Value> = entries
424                    .into_iter()
425                    .map(|(k, v)| (k, serde_json::Value::String(v)))
426                    .collect();
427                Ok(CallToolResult::json(serde_json::Value::Object(map)))
428            }
429        })
430        .build()
431}
432
433pub fn pool_configure_slot_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
434    ToolBuilder::new("pool_configure_slot")
435        .title("Configure Slot")
436        .description("Set name/role/description for a slot to give it persistent identity")
437        .handler(move |input: ConfigureSlotInput| {
438            let state = Arc::clone(&state);
439            async move {
440                let slot_id = claude_pool::SlotId(input.slot_id.clone());
441
442                match state.pool.store().get_slot(&slot_id).await {
443                    Ok(Some(mut slot)) => {
444                        // Update identity fields
445                        if let Some(name) = input.name {
446                            slot.config.name = Some(name);
447                        }
448                        if let Some(role) = input.role {
449                            slot.config.role = Some(role);
450                        }
451                        if let Some(description) = input.description {
452                            slot.config.description = Some(description);
453                        }
454
455                        // Persist updated slot
456                        match state.pool.store().put_slot(slot.clone()).await {
457                            Ok(_) => {
458                                let response = serde_json::json!({
459                                    "slot_id": slot_id.0,
460                                    "name": slot.config.name,
461                                    "role": slot.config.role,
462                                    "description": slot.config.description,
463                                });
464                                Ok(CallToolResult::json(response))
465                            }
466                            Err(e) => {
467                                Ok(CallToolResult::error(format!("failed to update slot: {e}")))
468                            }
469                        }
470                    }
471                    Ok(None) => Ok(CallToolResult::error(format!(
472                        "slot not found: {}",
473                        input.slot_id
474                    ))),
475                    Err(e) => Ok(CallToolResult::error(format!("failed to fetch slot: {e}"))),
476                }
477            }
478        })
479        .build()
480}
481
482// ── Skill + chain tools ──────────────────────────────────────────────
483
484#[derive(Debug, Deserialize, JsonSchema)]
485pub struct SkillRunInput {
486    /// Name of the skill to run.
487    pub skill: String,
488    /// Skill arguments as key-value pairs.
489    pub arguments: std::collections::HashMap<String, String>,
490    /// Model override.
491    pub model: Option<String>,
492    /// Effort override.
493    pub effort: Option<String>,
494}
495
496#[derive(Debug, Deserialize, JsonSchema)]
497pub struct ChainInput {
498    /// Ordered list of chain steps.
499    pub steps: Vec<ChainStepInput>,
500}
501
502#[derive(Debug, Deserialize, JsonSchema)]
503pub struct SubmitChainInput {
504    /// Ordered list of chain steps.
505    pub steps: Vec<ChainStepInput>,
506    /// Tags for grouping/filtering.
507    pub tags: Option<Vec<String>>,
508    /// Isolation mode: "worktree" for per-chain git worktree, or omit for default (none).
509    pub isolation: Option<String>,
510}
511
512#[derive(Debug, Deserialize, JsonSchema)]
513pub struct FanOutChainsInput {
514    /// List of chains, each a list of steps.
515    pub chains: Vec<Vec<ChainStepInput>>,
516    /// Tags for grouping/filtering.
517    pub tags: Option<Vec<String>>,
518    /// Isolation mode: "worktree" for per-chain git worktree, or omit for default (none).
519    pub isolation: Option<String>,
520}
521
522#[derive(Debug, Deserialize, JsonSchema)]
523pub struct ChainStepInput {
524    /// Step name.
525    pub name: String,
526    /// Step type: "prompt" or "skill".
527    #[serde(rename = "type")]
528    pub step_type: String,
529    /// For prompt steps: the prompt text. For skill steps: the skill name.
530    pub value: String,
531    /// For skill steps: arguments as key-value pairs.
532    pub arguments: Option<std::collections::HashMap<String, String>>,
533    /// Model override for this step.
534    pub model: Option<String>,
535    /// Effort override for this step.
536    pub effort: Option<String>,
537    /// Number of retries on failure (default: 0).
538    pub retries: Option<u32>,
539    /// Recovery prompt template on exhausted retries. {error} and {previous_output} are substituted.
540    pub recovery_prompt: Option<String>,
541    /// Extract named values from this step's JSON output for use in later steps.
542    /// Key = variable name, Value = dot-path (e.g. "files_changed", "result.summary", ".").
543    /// Reference in later prompts as: {steps.STEP_NAME.VAR_NAME}
544    pub output_vars: Option<std::collections::HashMap<String, String>>,
545}
546
547fn convert_chain_steps(steps: Vec<ChainStepInput>) -> Vec<claude_pool::ChainStep> {
548    steps
549        .into_iter()
550        .map(|s| {
551            let action = match s.step_type.as_str() {
552                "skill" => claude_pool::StepAction::Skill {
553                    skill: s.value,
554                    arguments: s.arguments.unwrap_or_default(),
555                },
556                _ => claude_pool::StepAction::Prompt { prompt: s.value },
557            };
558            let config = task_config_from(s.model, s.effort, None);
559            let failure_policy = claude_pool::StepFailurePolicy {
560                retries: s.retries.unwrap_or(0),
561                recovery_prompt: s.recovery_prompt,
562            };
563            claude_pool::ChainStep {
564                name: s.name,
565                action,
566                config,
567                failure_policy,
568                output_vars: s.output_vars.unwrap_or_default(),
569            }
570        })
571        .collect()
572}
573
574pub fn pool_skill_run_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
575    ToolBuilder::new("pool_skill_run")
576        .title("Run Skill")
577        .description("Run a registered skill by name with arguments. Blocks until completion.")
578        .handler(move |input: SkillRunInput| {
579            let state = Arc::clone(&state);
580            async move {
581                let registry = state.skills.read().await;
582                let skill = match registry.get(&input.skill) {
583                    Some(s) => s.clone(),
584                    None => {
585                        return Ok(CallToolResult::error(format!(
586                            "skill not found: {}",
587                            input.skill
588                        )));
589                    }
590                };
591                drop(registry);
592
593                let prompt = match skill.render(&input.arguments) {
594                    Ok(p) => p,
595                    Err(e) => return Ok(CallToolResult::error(e.to_string())),
596                };
597
598                // Merge skill config with per-call overrides.
599                let mut config = skill.config.unwrap_or_default();
600                if let Some(model) = input.model {
601                    config.model = Some(model);
602                }
603                if let Some(effort) = input.effort {
604                    config.effort = parse_effort(&effort);
605                }
606
607                match state.pool.run_with_config(&prompt, Some(config)).await {
608                    Ok(result) => Ok(CallToolResult::json(serde_json::to_value(&result).unwrap())),
609                    Err(e) => Ok(CallToolResult::error(e.to_string())),
610                }
611            }
612        })
613        .build()
614}
615
616pub fn pool_chain_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
617    ToolBuilder::new("pool_chain")
618        .title("Run Chain (Sync)")
619        .description(
620            "Execute a sequential pipeline of steps synchronously. Each step's output feeds \
621             the next. Steps can be inline prompts or skill references. Blocks until all \
622             steps complete. For long chains, use pool_submit_chain instead.",
623        )
624        .handler(move |input: ChainInput| {
625            let state = Arc::clone(&state);
626            async move {
627                let steps = convert_chain_steps(input.steps);
628                let skills = state.skills.read().await;
629                match claude_pool::execute_chain(&state.pool, &skills, &steps).await {
630                    Ok(result) => Ok(CallToolResult::json(serde_json::to_value(&result).unwrap())),
631                    Err(e) => Ok(CallToolResult::error(e.to_string())),
632                }
633            }
634        })
635        .build()
636}
637
638pub fn pool_submit_chain_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
639    ToolBuilder::new("pool_submit_chain")
640        .title("Submit Chain (Async)")
641        .description(
642            "Submit a sequential pipeline for async execution. Returns a task_id immediately. \
643             Poll with pool_chain_result for per-step progress, or pool_result for final output.",
644        )
645        .handler(move |input: SubmitChainInput| {
646            let state = Arc::clone(&state);
647            async move {
648                let steps = convert_chain_steps(input.steps);
649                let isolation = parse_isolation(input.isolation.as_deref());
650                let options = claude_pool::ChainOptions {
651                    tags: input.tags.unwrap_or_default(),
652                    isolation,
653                };
654                let skills = state.skills.read().await;
655                match state.pool.submit_chain(steps, &skills, options).await {
656                    Ok(task_id) => Ok(CallToolResult::json(
657                        serde_json::json!({ "task_id": task_id.0 }),
658                    )),
659                    Err(e) => Ok(CallToolResult::error(e.to_string())),
660                }
661            }
662        })
663        .build()
664}
665
666pub fn pool_fan_out_chains_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
667    ToolBuilder::new("pool_fan_out_chains")
668        .title("Fan Out Chains (Parallel Pipelines)")
669        .description(
670            "Submit multiple sequential chains to run in parallel, each on its own slot. \
671             Returns all task IDs for individual progress tracking via pool_chain_result.",
672        )
673        .handler(move |input: FanOutChainsInput| {
674            let state = Arc::clone(&state);
675            async move {
676                let chains = input.chains.into_iter().map(convert_chain_steps).collect();
677                let isolation = parse_isolation(input.isolation.as_deref());
678                let options = claude_pool::ChainOptions {
679                    tags: input.tags.unwrap_or_default(),
680                    isolation,
681                };
682                let skills = state.skills.read().await;
683                match state.pool.fan_out_chains(chains, &skills, options).await {
684                    Ok(task_ids) => Ok(CallToolResult::json(serde_json::json!({
685                        "task_ids": task_ids.iter().map(|id| &id.0).collect::<Vec<_>>()
686                    }))),
687                    Err(e) => Ok(CallToolResult::error(e.to_string())),
688                }
689            }
690        })
691        .build()
692}
693
694pub fn pool_chain_result_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
695    ToolBuilder::new("pool_chain_result")
696        .title("Get Chain Progress")
697        .description(
698            "Get per-step progress of an async chain. Shows which step is running, \
699             completed steps, and overall status.",
700        )
701        .read_only()
702        .handler(move |input: TaskIdInput| {
703            let state = Arc::clone(&state);
704            async move {
705                let task_id = claude_pool::TaskId(input.task_id.clone());
706                match state.pool.chain_progress(&task_id) {
707                    Some(progress) => Ok(CallToolResult::json(
708                        serde_json::to_value(&progress).unwrap(),
709                    )),
710                    None => {
711                        // Fall back to checking if the task exists at all.
712                        match state.pool.result(&task_id).await {
713                            Ok(Some(r)) => {
714                                Ok(CallToolResult::json(serde_json::to_value(&r).unwrap()))
715                            }
716                            Ok(None) => Ok(CallToolResult::error(format!(
717                                "no chain found for task_id: {}",
718                                input.task_id,
719                            ))),
720                            Err(e) => Ok(CallToolResult::error(e.to_string())),
721                        }
722                    }
723                }
724            }
725        })
726        .build()
727}
728
729/// Cancel a running chain, skipping remaining steps.
730pub fn pool_cancel_chain_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
731    ToolBuilder::new("pool_cancel_chain")
732        .title("Cancel Chain")
733        .description(
734            "Cancel a running chain submitted with pool_submit_chain or pool_fan_out_chains. \
735             The current step finishes before cancellation takes effect. Remaining steps are \
736             skipped (marked skipped=true). Use pool_chain_result to confirm, then pool_result \
737             to retrieve partial output.",
738        )
739        .handler(move |input: TaskIdInput| {
740            let state = Arc::clone(&state);
741            async move {
742                let task_id = claude_pool::TaskId(input.task_id.clone());
743                match state.pool.cancel_chain(&task_id).await {
744                    Ok(()) => Ok(CallToolResult::json(serde_json::json!({
745                        "status": "cancellation_requested",
746                        "task_id": input.task_id,
747                    }))),
748                    Err(e) => Ok(CallToolResult::error(e.to_string())),
749                }
750            }
751        })
752        .build()
753}
754
755pub fn pool_invoke_workflow_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
756    ToolBuilder::new("pool_invoke_workflow")
757        .title("Invoke Workflow")
758        .description(
759            "Submit a named workflow template with arguments. Returns a task_id immediately. \
760             Example workflows: 'issue_to_pr', 'refactor_and_test', 'review_and_fix'.",
761        )
762        .handler(move |input: InvokeWorkflowInput| {
763            let state = Arc::clone(&state);
764            async move {
765                let skills = state.skills.read().await;
766                match state
767                    .pool
768                    .submit_workflow(
769                        &input.workflow,
770                        input.arguments,
771                        &skills,
772                        &state.workflows,
773                        input.tags.unwrap_or_default(),
774                    )
775                    .await
776                {
777                    Ok(task_id) => Ok(CallToolResult::json(serde_json::json!({
778                        "task_id": task_id.0,
779                        "workflow": input.workflow,
780                    }))),
781                    Err(e) => Ok(CallToolResult::error(e.to_string())),
782                }
783            }
784        })
785        .build()
786}
787
788/// Build all pool tools.
789pub fn pool_scale_up_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
790    ToolBuilder::new("pool_scale_up")
791        .title("Scale Up Slots")
792        .description("Add N new slots to the pool. Returns the new total slot count.")
793        .handler(move |input: ScalingInput| {
794            let state = Arc::clone(&state);
795            async move {
796                match state.pool.scale_up(input.count).await {
797                    Ok(new_count) => Ok(CallToolResult::json(serde_json::json!({
798                        "success": true,
799                        "new_slot_count": new_count,
800                        "details": format!("Scaled up by {} slots", input.count),
801                    }))),
802                    Err(e) => Ok(CallToolResult::error(e.to_string())),
803                }
804            }
805        })
806        .build()
807}
808
809pub fn pool_scale_down_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
810    ToolBuilder::new("pool_scale_down")
811        .title("Scale Down Slots")
812        .description(
813            "Remove N slots from the pool. Removes idle slots first, \
814             then waits for busy slots to complete. Returns the new total slot count.",
815        )
816        .handler(move |input: ScalingInput| {
817            let state = Arc::clone(&state);
818            async move {
819                match state.pool.scale_down(input.count).await {
820                    Ok(new_count) => Ok(CallToolResult::json(serde_json::json!({
821                        "success": true,
822                        "new_slot_count": new_count,
823                        "details": format!("Scaled down by {} slots", input.count),
824                    }))),
825                    Err(e) => Ok(CallToolResult::error(e.to_string())),
826                }
827            }
828        })
829        .build()
830}
831
832pub fn pool_set_target_slots_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
833    ToolBuilder::new("pool_set_target_slots")
834        .title("Set Target Slot Count")
835        .description("Set the pool to a specific number of slots, scaling up or down as needed.")
836        .handler(move |input: SetTargetSlotsInput| {
837            let state = Arc::clone(&state);
838            async move {
839                match state.pool.set_target_slots(input.target).await {
840                    Ok(new_count) => Ok(CallToolResult::json(serde_json::json!({
841                        "success": true,
842                        "new_slot_count": new_count,
843                        "target": input.target,
844                    }))),
845                    Err(e) => Ok(CallToolResult::error(e.to_string())),
846                }
847            }
848        })
849        .build()
850}
851
852// ── Skill management tools ──────────────────────────────────────────
853
854/// List registered skills with optional scope/source filters.
855pub fn pool_skill_list_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
856    ToolBuilder::new("pool_skill_list")
857        .title("List Skills")
858        .description("List registered skills with optional scope/source filters.")
859        .read_only()
860        .handler(move |input: SkillListInput| {
861            let state = Arc::clone(&state);
862            async move {
863                let registry = state.skills.read().await;
864                let scope_filter = input.scope.as_deref().map(parse_scope);
865                let source_filter = input.source.as_deref().and_then(parse_source);
866
867                let mut results: Vec<_> = registry
868                    .list_registered()
869                    .into_iter()
870                    .filter(|rs| {
871                        if let Some(scope) = scope_filter
872                            && rs.skill.scope != scope
873                        {
874                            return false;
875                        }
876                        if let Some(source) = source_filter
877                            && rs.source != source
878                        {
879                            return false;
880                        }
881                        true
882                    })
883                    .map(|rs| {
884                        serde_json::json!({
885                            "name": rs.skill.name,
886                            "description": rs.skill.description,
887                            "scope": rs.skill.scope.to_string(),
888                            "source": rs.source.to_string(),
889                        })
890                    })
891                    .collect();
892                results.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str()));
893                Ok(CallToolResult::json(serde_json::json!(results)))
894            }
895        })
896        .build()
897}
898
899/// Get full details of a skill by name, including prompt template.
900pub fn pool_skill_get_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
901    ToolBuilder::new("pool_skill_get")
902        .title("Get Skill Details")
903        .description("Get full details of a skill by name, including prompt template.")
904        .read_only()
905        .handler(move |input: SkillGetInput| {
906            let state = Arc::clone(&state);
907            async move {
908                let registry = state.skills.read().await;
909                match registry.get_registered(&input.name) {
910                    Some(rs) => {
911                        let response = serde_json::json!({
912                            "name": rs.skill.name,
913                            "description": rs.skill.description,
914                            "prompt": rs.skill.prompt,
915                            "arguments": rs.skill.arguments.iter().map(|a| serde_json::json!({
916                                "name": a.name,
917                                "description": a.description,
918                                "required": a.required,
919                            })).collect::<Vec<_>>(),
920                            "scope": rs.skill.scope.to_string(),
921                            "source": rs.source.to_string(),
922                            "config": rs.skill.config,
923                        });
924                        Ok(CallToolResult::json(response))
925                    }
926                    None => Ok(CallToolResult::error(format!(
927                        "skill not found: {}",
928                        input.name
929                    ))),
930                }
931            }
932        })
933        .build()
934}
935
936/// Register a skill at runtime. Ephemeral unless saved with pool_skill_save.
937pub fn pool_skill_add_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
938    ToolBuilder::new("pool_skill_add")
939        .title("Add Skill")
940        .description(
941            "Register a skill at runtime. Ephemeral (lost on restart) unless saved \
942             with pool_skill_save. Overwrites any existing skill with the same name.",
943        )
944        .handler(move |input: SkillAddInput| {
945            let state = Arc::clone(&state);
946            async move {
947                let scope = input.scope.as_deref().map(parse_scope).unwrap_or_default();
948                let arguments = input
949                    .arguments
950                    .into_iter()
951                    .map(|a| claude_pool::SkillArgument {
952                        name: a.name,
953                        description: a.description,
954                        required: a.required,
955                    })
956                    .collect();
957                let config: Option<SlotConfig> =
958                    input.config.and_then(|v| serde_json::from_value(v).ok());
959                let skill = claude_pool::Skill {
960                    name: input.name.clone(),
961                    description: input.description,
962                    prompt: input.prompt,
963                    arguments,
964                    config,
965                    scope,
966                };
967                let mut registry = state.skills.write().await;
968                let overwritten = registry.get(&input.name).is_some();
969                registry.register(skill, SkillSource::Runtime);
970                Ok(CallToolResult::json(serde_json::json!({
971                    "name": input.name,
972                    "overwritten": overwritten,
973                    "source": "runtime",
974                })))
975            }
976        })
977        .build()
978}
979
980/// Remove a skill by name. Runtime-only, does not delete files.
981pub fn pool_skill_remove_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
982    ToolBuilder::new("pool_skill_remove")
983        .title("Remove Skill")
984        .description("Remove a skill by name. Runtime-only, does not delete files on disk.")
985        .handler(move |input: SkillRemoveInput| {
986            let state = Arc::clone(&state);
987            async move {
988                let mut registry = state.skills.write().await;
989                match registry.remove(&input.name) {
990                    Some(_) => Ok(CallToolResult::json(serde_json::json!({
991                        "removed": input.name,
992                    }))),
993                    None => Ok(CallToolResult::error(format!(
994                        "skill not found: {}",
995                        input.name
996                    ))),
997                }
998            }
999        })
1000        .build()
1001}
1002
1003/// Persist a skill to the project skills directory as JSON.
1004pub fn pool_skill_save_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
1005    ToolBuilder::new("pool_skill_save")
1006        .title("Save Skill to Disk")
1007        .description(
1008            "Persist a skill to the project skills directory as JSON. \
1009             Creates/overwrites {dir}/{name}.json.",
1010        )
1011        .handler(move |input: SkillSaveInput| {
1012            let state = Arc::clone(&state);
1013            async move {
1014                let skill = {
1015                    let registry = state.skills.read().await;
1016                    match registry.get(&input.name) {
1017                        Some(s) => s.clone(),
1018                        None => {
1019                            return Ok(CallToolResult::error(format!(
1020                                "skill not found: {}",
1021                                input.name
1022                            )));
1023                        }
1024                    }
1025                };
1026
1027                let dir = input
1028                    .dir
1029                    .map(PathBuf::from)
1030                    .unwrap_or_else(|| state.skills_dir.clone());
1031
1032                if let Err(e) = std::fs::create_dir_all(&dir) {
1033                    return Ok(CallToolResult::error(format!(
1034                        "failed to create directory {}: {e}",
1035                        dir.display()
1036                    )));
1037                }
1038
1039                let path = dir.join(format!("{}.json", input.name));
1040                let json = match serde_json::to_string_pretty(&skill) {
1041                    Ok(j) => j,
1042                    Err(e) => return Ok(CallToolResult::error(format!("serialize error: {e}"))),
1043                };
1044
1045                if let Err(e) = std::fs::write(&path, &json) {
1046                    return Ok(CallToolResult::error(format!(
1047                        "failed to write {}: {e}",
1048                        path.display()
1049                    )));
1050                }
1051
1052                // Update source to Project since it's now persisted.
1053                {
1054                    let mut registry = state.skills.write().await;
1055                    if let Some(existing) = registry.get(&input.name).cloned() {
1056                        registry.register(existing, SkillSource::Project);
1057                    }
1058                }
1059
1060                Ok(CallToolResult::json(serde_json::json!({
1061                    "saved": input.name,
1062                    "path": path.display().to_string(),
1063                })))
1064            }
1065        })
1066        .build()
1067}
1068
1069pub fn all_tools<S: PoolStore + 'static>(state: &Arc<State<S>>) -> Vec<Tool> {
1070    vec![
1071        pool_status_tool(Arc::clone(state)),
1072        pool_run_tool(Arc::clone(state)),
1073        pool_submit_tool(Arc::clone(state)),
1074        pool_result_tool(Arc::clone(state)),
1075        pool_cancel_tool(Arc::clone(state)),
1076        pool_fan_out_tool(Arc::clone(state)),
1077        pool_drain_tool(Arc::clone(state)),
1078        pool_skill_run_tool(Arc::clone(state)),
1079        pool_chain_tool(Arc::clone(state)),
1080        pool_submit_chain_tool(Arc::clone(state)),
1081        pool_fan_out_chains_tool(Arc::clone(state)),
1082        pool_chain_result_tool(Arc::clone(state)),
1083        pool_cancel_chain_tool(Arc::clone(state)),
1084        pool_invoke_workflow_tool(Arc::clone(state)),
1085        pool_scale_up_tool(Arc::clone(state)),
1086        pool_scale_down_tool(Arc::clone(state)),
1087        pool_set_target_slots_tool(Arc::clone(state)),
1088        context_set_tool(Arc::clone(state)),
1089        context_get_tool(Arc::clone(state)),
1090        context_delete_tool(Arc::clone(state)),
1091        context_list_tool(Arc::clone(state)),
1092        pool_configure_slot_tool(Arc::clone(state)),
1093        pool_skill_list_tool(Arc::clone(state)),
1094        pool_skill_get_tool(Arc::clone(state)),
1095        pool_skill_add_tool(Arc::clone(state)),
1096        pool_skill_remove_tool(Arc::clone(state)),
1097        pool_skill_save_tool(Arc::clone(state)),
1098    ]
1099}