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/// Input for ejecting a builtin skill to disk for customization.
173#[derive(Debug, Deserialize, JsonSchema)]
174pub struct SkillEjectInput {
175    /// Skill name to eject (must be a builtin skill).
176    pub name: String,
177    /// Directory to eject to. Defaults to the configured skills_dir.
178    pub dir: Option<String>,
179}
180
181// ── Messaging input schemas ────────────────────────────────────────────
182
183/// Input for sending a message between slots.
184#[derive(Debug, Deserialize, JsonSchema)]
185pub struct SendMessageInput {
186    /// Sender slot ID (e.g., "slot-0").
187    pub from: String,
188    /// Recipient slot ID (e.g., "slot-1").
189    pub to: String,
190    /// Message content.
191    pub content: String,
192}
193
194/// Input for reading messages from a slot's inbox.
195#[derive(Debug, Deserialize, JsonSchema)]
196pub struct ReadMessagesInput {
197    /// Slot ID to read messages from (e.g., "slot-0").
198    pub slot_id: String,
199}
200
201/// Input for peeking at messages in a slot's inbox.
202#[derive(Debug, Deserialize, JsonSchema)]
203pub struct PeekMessagesInput {
204    /// Slot ID to peek at messages from (e.g., "slot-0").
205    pub slot_id: String,
206}
207
208/// Input for broadcasting a message to all slots.
209#[derive(Debug, Deserialize, JsonSchema)]
210pub struct BroadcastInput {
211    /// Sender slot ID (e.g., "slot-0").
212    pub from: String,
213    /// Message content to broadcast.
214    pub content: String,
215}
216
217/// Input for finding slots by name, role, or state.
218#[derive(Debug, Deserialize, JsonSchema)]
219pub struct FindSlotsInput {
220    /// Filter by slot name (exact match).
221    #[serde(default)]
222    pub name: Option<String>,
223    /// Filter by slot role (exact match).
224    #[serde(default)]
225    pub role: Option<String>,
226    /// Filter by slot state (idle, busy, stopped, errored).
227    #[serde(default)]
228    pub state: Option<String>,
229}
230
231/// Input for claiming the next pending task.
232#[derive(Debug, Deserialize, JsonSchema)]
233pub struct ClaimInput {
234    /// Slot ID that wants to claim a task (e.g., "slot-0").
235    pub slot_id: String,
236}
237
238/// Input for submitting a task that requires review before completion.
239#[derive(Debug, Deserialize, JsonSchema)]
240pub struct SubmitWithReviewInput {
241    /// The prompt/task to execute.
242    pub prompt: String,
243    /// Model override for this task.
244    pub model: Option<String>,
245    /// Effort override for this task (min, low, medium, high, max).
246    pub effort: Option<String>,
247    /// Tags for grouping/filtering.
248    pub tags: Option<Vec<String>>,
249    /// Maximum number of rejections before failing (default: 3).
250    pub max_rejections: Option<u32>,
251    /// Additional MCP servers for this task (merged with global/slot servers).
252    /// Keys are server names, values are server config objects.
253    pub mcp_servers: Option<std::collections::HashMap<String, serde_json::Value>>,
254}
255
256/// Input for approving a task result.
257#[derive(Debug, Deserialize, JsonSchema)]
258pub struct ApproveResultInput {
259    /// The task ID to approve.
260    pub task_id: String,
261}
262
263/// Input for rejecting a task result with feedback.
264#[derive(Debug, Deserialize, JsonSchema)]
265pub struct RejectResultInput {
266    /// The task ID to reject.
267    pub task_id: String,
268    /// Feedback explaining why the result was rejected. This is appended to the
269    /// original prompt when the task is re-queued.
270    pub feedback: String,
271}
272
273// ── Helpers ──────────────────────────────────────────────────────────
274
275fn parse_effort(s: &str) -> Option<claude_pool::Effort> {
276    match s.to_lowercase().as_str() {
277        "min" | "low" => Some(claude_pool::Effort::Low),
278        "medium" => Some(claude_pool::Effort::Medium),
279        "high" => Some(claude_pool::Effort::High),
280        "max" => Some(claude_pool::Effort::Max),
281        _ => None,
282    }
283}
284
285fn task_config_from(
286    model: Option<String>,
287    effort: Option<String>,
288    mcp_servers: Option<std::collections::HashMap<String, serde_json::Value>>,
289) -> Option<SlotConfig> {
290    if model.is_none() && effort.is_none() && mcp_servers.is_none() {
291        return None;
292    }
293    Some(SlotConfig {
294        model,
295        effort: effort.and_then(|e| parse_effort(&e)),
296        mcp_servers,
297        ..Default::default()
298    })
299}
300
301fn parse_scope(s: &str) -> SkillScope {
302    match s {
303        "coordinator" => SkillScope::Coordinator,
304        "chain" => SkillScope::Chain,
305        _ => SkillScope::Task,
306    }
307}
308
309fn parse_isolation(s: Option<&str>) -> claude_pool::chain::ChainIsolation {
310    match s {
311        Some("none") => claude_pool::chain::ChainIsolation::None,
312        Some("clone") => claude_pool::chain::ChainIsolation::Clone,
313        _ => claude_pool::chain::ChainIsolation::Worktree,
314    }
315}
316
317fn parse_source(s: &str) -> Option<SkillSource> {
318    match s {
319        "builtin" => Some(SkillSource::Builtin),
320        "project" => Some(SkillSource::Project),
321        "runtime" => Some(SkillSource::Runtime),
322        _ => None,
323    }
324}
325
326// ── Tool builders ────────────────────────────────────────────────────
327
328pub fn pool_status_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
329    ToolBuilder::new("pool_status")
330        .title("Pool Status")
331        .description("Get pool status: slots, tasks in flight, budget, server metadata")
332        .read_only()
333        .no_params_handler(move || {
334            let state = Arc::clone(&state);
335            async move {
336                match state.pool.status().await {
337                    Ok(status) => {
338                        let mut response = serde_json::to_value(&status).unwrap();
339                        let response_obj = response.as_object_mut().unwrap();
340                        let server_obj = serde_json::to_value(&state.server_info).unwrap();
341                        if let Some(server_map) = server_obj.as_object() {
342                            for (key, value) in server_map.iter() {
343                                response_obj.insert(format!("server_{key}"), value.clone());
344                            }
345                        }
346                        Ok(CallToolResult::json(response))
347                    }
348                    Err(e) => Ok(CallToolResult::error(e.to_string())),
349                }
350            }
351        })
352        .build()
353}
354
355pub fn pool_run_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
356    ToolBuilder::new("pool_run")
357        .title("Run a Task")
358        .description(
359            "Run a task on the next available slot. Blocks until completion. \
360             Use this for single, clear actions with one clear output.",
361        )
362        .handler(move |input: RunInput| {
363            let state = Arc::clone(&state);
364            async move {
365                let config = task_config_from(input.model, input.effort, input.mcp_servers);
366                match state.pool.run_with_config(&input.prompt, config).await {
367                    Ok(result) => Ok(CallToolResult::json(serde_json::to_value(&result).unwrap())),
368                    Err(e) => Ok(CallToolResult::error(e.to_string())),
369                }
370            }
371        })
372        .build()
373}
374
375pub fn pool_submit_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
376    ToolBuilder::new("pool_submit")
377        .title("Fire a Task")
378        .description("Fire off a task for async execution. Returns a task_id immediately. Check on it later with pool_result.")
379        .handler(move |input: SubmitInput| {
380            let state = Arc::clone(&state);
381            async move {
382                let config = task_config_from(input.model, input.effort, input.mcp_servers);
383                let tags = input.tags.unwrap_or_default();
384                match state
385                    .pool
386                    .submit_with_config(&input.prompt, config, tags)
387                    .await
388                {
389                    Ok(task_id) => Ok(CallToolResult::json(
390                        serde_json::json!({ "task_id": task_id.0 }),
391                    )),
392                    Err(e) => Ok(CallToolResult::error(e.to_string())),
393                }
394            }
395        })
396        .build()
397}
398
399pub fn pool_result_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
400    ToolBuilder::new("pool_result")
401        .title("Check on a Task")
402        .description(
403            "Check on a fired task. Returns the result if complete or pending_review, \
404             null if still running. Tasks with review_required=true will have \
405             state='pending_review' when done -- use pool_approve_result or \
406             pool_reject_result to finalize.",
407        )
408        .read_only()
409        .handler(move |input: TaskIdInput| {
410            let state = Arc::clone(&state);
411            async move {
412                let task_id = claude_pool::TaskId(input.task_id.clone());
413                // Fetch full task record for state info.
414                let task = state.pool.store().get_task(&task_id).await.ok().flatten();
415
416                match state.pool.result(&task_id).await {
417                    Ok(Some(r)) => {
418                        let mut val = serde_json::to_value(&r).unwrap();
419                        if let Some(ref t) = task
420                            && let Some(obj) = val.as_object_mut()
421                        {
422                            obj.insert("state".to_string(), serde_json::to_value(t.state).unwrap());
423                            if t.review_required {
424                                obj.insert(
425                                    "review_required".to_string(),
426                                    serde_json::Value::Bool(true),
427                                );
428                                obj.insert(
429                                    "rejection_count".to_string(),
430                                    serde_json::json!(t.rejection_count),
431                                );
432                                obj.insert(
433                                    "max_rejections".to_string(),
434                                    serde_json::json!(t.max_rejections),
435                                );
436                            }
437                        }
438                        Ok(CallToolResult::json(val))
439                    }
440                    Ok(None) => Ok(CallToolResult::json(
441                        serde_json::json!({ "status": "running" }),
442                    )),
443                    Err(e) => Ok(CallToolResult::error(e.to_string())),
444                }
445            }
446        })
447        .build()
448}
449
450pub fn pool_cancel_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
451    ToolBuilder::new("pool_cancel")
452        .title("Cancel a Task")
453        .description("Cancel a pending or running task.")
454        .handler(move |input: TaskIdInput| {
455            let state = Arc::clone(&state);
456            async move {
457                let task_id = claude_pool::TaskId(input.task_id);
458                match state.pool.cancel(&task_id).await {
459                    Ok(()) => Ok(CallToolResult::text("cancelled")),
460                    Err(e) => Ok(CallToolResult::error(e.to_string())),
461                }
462            }
463        })
464        .build()
465}
466
467pub fn pool_fan_out_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
468    ToolBuilder::new("pool_fan_out")
469        .title("Fan Out Tasks")
470        .description(
471            "Fan out multiple independent tasks in parallel across available slots. Returns all results.",
472        )
473        .handler(move |input: FanOutInput| {
474            let state = Arc::clone(&state);
475            async move {
476                let prompts: Vec<&str> = input.prompts.iter().map(|s| s.as_str()).collect();
477                match state.pool.fan_out(&prompts).await {
478                    Ok(results) => Ok(CallToolResult::json(
479                        serde_json::json!({ "results": results }),
480                    )),
481                    Err(e) => Ok(CallToolResult::error(e.to_string())),
482                }
483            }
484        })
485        .build()
486}
487
488pub fn pool_drain_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
489    ToolBuilder::new("pool_drain")
490        .title("Drain the Pool")
491        .description(
492            "Gracefully shut down the pool. Waits for in-flight tasks, then stops all slots.",
493        )
494        .destructive()
495        .no_params_handler(move || {
496            let state = Arc::clone(&state);
497            async move {
498                match state.pool.drain().await {
499                    Ok(summary) => Ok(CallToolResult::json(
500                        serde_json::to_value(&summary).unwrap(),
501                    )),
502                    Err(e) => Ok(CallToolResult::error(e.to_string())),
503                }
504            }
505        })
506        .build()
507}
508
509pub fn context_set_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
510    ToolBuilder::new("context_set")
511        .title("Set Context")
512        .description("Set a shared context value. Context is injected into slot system prompts.")
513        .handler(move |input: ContextSetInput| {
514            let state = Arc::clone(&state);
515            async move {
516                state.pool.set_context(input.key, input.value);
517                Ok(CallToolResult::text("ok"))
518            }
519        })
520        .build()
521}
522
523pub fn context_get_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
524    ToolBuilder::new("context_get")
525        .title("Get Context")
526        .description("Get a shared context value by key.")
527        .read_only()
528        .handler(move |input: ContextKeyInput| {
529            let state = Arc::clone(&state);
530            async move {
531                match state.pool.get_context(&input.key) {
532                    Some(value) => Ok(CallToolResult::text(value)),
533                    None => Ok(CallToolResult::error(format!(
534                        "key not found: {}",
535                        input.key
536                    ))),
537                }
538            }
539        })
540        .build()
541}
542
543pub fn context_delete_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
544    ToolBuilder::new("context_delete")
545        .title("Delete Context")
546        .description("Delete a shared context value by key.")
547        .handler(move |input: ContextKeyInput| {
548            let state = Arc::clone(&state);
549            async move {
550                state.pool.delete_context(&input.key);
551                Ok(CallToolResult::text("ok"))
552            }
553        })
554        .build()
555}
556
557pub fn context_list_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
558    ToolBuilder::new("context_list")
559        .title("List Context")
560        .description("List all shared context keys and values.")
561        .read_only()
562        .no_params_handler(move || {
563            let state = Arc::clone(&state);
564            async move {
565                let entries = state.pool.list_context();
566                let map: serde_json::Map<String, serde_json::Value> = entries
567                    .into_iter()
568                    .map(|(k, v)| (k, serde_json::Value::String(v)))
569                    .collect();
570                Ok(CallToolResult::json(serde_json::Value::Object(map)))
571            }
572        })
573        .build()
574}
575
576pub fn pool_configure_slot_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
577    ToolBuilder::new("pool_configure_slot")
578        .title("Configure Slot")
579        .description("Set name/role/description for a slot to give it persistent identity")
580        .handler(move |input: ConfigureSlotInput| {
581            let state = Arc::clone(&state);
582            async move {
583                let slot_id = claude_pool::SlotId(input.slot_id.clone());
584
585                match state.pool.store().get_slot(&slot_id).await {
586                    Ok(Some(mut slot)) => {
587                        // Update identity fields
588                        if let Some(name) = input.name {
589                            slot.config.name = Some(name);
590                        }
591                        if let Some(role) = input.role {
592                            slot.config.role = Some(role);
593                        }
594                        if let Some(description) = input.description {
595                            slot.config.description = Some(description);
596                        }
597
598                        // Persist updated slot
599                        match state.pool.store().put_slot(slot.clone()).await {
600                            Ok(_) => {
601                                let response = serde_json::json!({
602                                    "slot_id": slot_id.0,
603                                    "name": slot.config.name,
604                                    "role": slot.config.role,
605                                    "description": slot.config.description,
606                                });
607                                Ok(CallToolResult::json(response))
608                            }
609                            Err(e) => {
610                                Ok(CallToolResult::error(format!("failed to update slot: {e}")))
611                            }
612                        }
613                    }
614                    Ok(None) => Ok(CallToolResult::error(format!(
615                        "slot not found: {}",
616                        input.slot_id
617                    ))),
618                    Err(e) => Ok(CallToolResult::error(format!("failed to fetch slot: {e}"))),
619                }
620            }
621        })
622        .build()
623}
624
625// ── Skill + chain tools ──────────────────────────────────────────────
626
627#[derive(Debug, Deserialize, JsonSchema)]
628pub struct SkillRunInput {
629    /// Name of the skill to run.
630    pub skill: String,
631    /// Skill arguments as key-value pairs.
632    pub arguments: std::collections::HashMap<String, String>,
633    /// Model override.
634    pub model: Option<String>,
635    /// Effort override.
636    pub effort: Option<String>,
637}
638
639#[derive(Debug, Deserialize, JsonSchema)]
640pub struct ChainInput {
641    /// Ordered list of chain steps.
642    pub steps: Vec<ChainStepInput>,
643}
644
645#[derive(Debug, Deserialize, JsonSchema)]
646pub struct SubmitChainInput {
647    /// Ordered list of chain steps.
648    pub steps: Vec<ChainStepInput>,
649    /// Tags for grouping/filtering.
650    pub tags: Option<Vec<String>>,
651    /// Isolation mode: "worktree" for per-chain git worktree, or omit for default (none).
652    pub isolation: Option<String>,
653}
654
655#[derive(Debug, Deserialize, JsonSchema)]
656pub struct FanOutChainsInput {
657    /// List of chains, each a list of steps.
658    pub chains: Vec<Vec<ChainStepInput>>,
659    /// Tags for grouping/filtering.
660    pub tags: Option<Vec<String>>,
661    /// Isolation mode: "worktree" for per-chain git worktree, or omit for default (none).
662    pub isolation: Option<String>,
663}
664
665#[derive(Debug, Deserialize, JsonSchema)]
666pub struct ChainStepInput {
667    /// Step name.
668    pub name: String,
669    /// Step type: "prompt" or "skill".
670    #[serde(rename = "type")]
671    pub step_type: String,
672    /// For prompt steps: the prompt text. For skill steps: the skill name.
673    pub value: String,
674    /// For skill steps: arguments as key-value pairs.
675    pub arguments: Option<std::collections::HashMap<String, String>>,
676    /// Model override for this step.
677    pub model: Option<String>,
678    /// Effort override for this step.
679    pub effort: Option<String>,
680    /// Number of retries on failure (default: 0).
681    pub retries: Option<u32>,
682    /// Recovery prompt template on exhausted retries. {error} and {previous_output} are substituted.
683    pub recovery_prompt: Option<String>,
684    /// Extract named values from this step's JSON output for use in later steps.
685    /// Key = variable name, Value = dot-path (e.g. "files_changed", "result.summary", ".").
686    /// Reference in later prompts as: {steps.STEP_NAME.VAR_NAME}
687    pub output_vars: Option<std::collections::HashMap<String, String>>,
688}
689
690fn convert_chain_steps(steps: Vec<ChainStepInput>) -> Vec<claude_pool::ChainStep> {
691    steps
692        .into_iter()
693        .map(|s| {
694            let action = match s.step_type.as_str() {
695                "skill" => claude_pool::StepAction::Skill {
696                    skill: s.value,
697                    arguments: s.arguments.unwrap_or_default(),
698                },
699                _ => claude_pool::StepAction::Prompt { prompt: s.value },
700            };
701            let config = task_config_from(s.model, s.effort, None);
702            let failure_policy = claude_pool::StepFailurePolicy {
703                retries: s.retries.unwrap_or(0),
704                recovery_prompt: s.recovery_prompt,
705            };
706            claude_pool::ChainStep {
707                name: s.name,
708                action,
709                config,
710                failure_policy,
711                output_vars: s.output_vars.unwrap_or_default(),
712            }
713        })
714        .collect()
715}
716
717pub fn pool_skill_run_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
718    ToolBuilder::new("pool_skill_run")
719        .title("Run a Skill")
720        .description("Run a registered skill by name with arguments. Blocks until completion.")
721        .handler(move |input: SkillRunInput| {
722            let state = Arc::clone(&state);
723            async move {
724                let registry = state.skills.read().await;
725                let skill = match registry.get(&input.skill) {
726                    Some(s) => s.clone(),
727                    None => {
728                        return Ok(CallToolResult::error(format!(
729                            "skill not found: {}",
730                            input.skill
731                        )));
732                    }
733                };
734                drop(registry);
735
736                let prompt = match skill.render(&input.arguments) {
737                    Ok(p) => p,
738                    Err(e) => return Ok(CallToolResult::error(e.to_string())),
739                };
740
741                // Merge skill config with per-call overrides.
742                let mut config = skill.config.unwrap_or_default();
743                if let Some(model) = input.model {
744                    config.model = Some(model);
745                }
746                if let Some(effort) = input.effort {
747                    config.effort = parse_effort(&effort);
748                }
749
750                match state.pool.run_with_config(&prompt, Some(config)).await {
751                    Ok(result) => Ok(CallToolResult::json(serde_json::to_value(&result).unwrap())),
752                    Err(e) => Ok(CallToolResult::error(e.to_string())),
753                }
754            }
755        })
756        .build()
757}
758
759pub fn pool_chain_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
760    ToolBuilder::new("pool_chain")
761        .title("Chain Steps")
762        .description(
763            "Chain a sequential pipeline of steps. Each step's output feeds the next. \
764             Steps can be inline prompts or skill references. Blocks until all steps \
765             complete. For long chains, fire a chain with pool_submit_chain instead.",
766        )
767        .handler(move |input: ChainInput| {
768            let state = Arc::clone(&state);
769            async move {
770                let steps = convert_chain_steps(input.steps);
771                let skills = state.skills.read().await;
772                match claude_pool::execute_chain(&state.pool, &skills, &steps).await {
773                    Ok(result) => Ok(CallToolResult::json(serde_json::to_value(&result).unwrap())),
774                    Err(e) => Ok(CallToolResult::error(e.to_string())),
775                }
776            }
777        })
778        .build()
779}
780
781pub fn pool_submit_chain_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
782    ToolBuilder::new("pool_submit_chain")
783        .title("Fire a Chain")
784        .description(
785            "Fire off a chain for async execution. Returns a task_id immediately. \
786             Check on it with pool_chain_result for per-step progress.",
787        )
788        .handler(move |input: SubmitChainInput| {
789            let state = Arc::clone(&state);
790            async move {
791                let steps = convert_chain_steps(input.steps);
792                let isolation = parse_isolation(input.isolation.as_deref());
793                let options = claude_pool::ChainOptions {
794                    tags: input.tags.unwrap_or_default(),
795                    isolation,
796                };
797                let skills = state.skills.read().await;
798                match state.pool.submit_chain(steps, &skills, options).await {
799                    Ok(task_id) => Ok(CallToolResult::json(
800                        serde_json::json!({ "task_id": task_id.0 }),
801                    )),
802                    Err(e) => Ok(CallToolResult::error(e.to_string())),
803                }
804            }
805        })
806        .build()
807}
808
809pub fn pool_fan_out_chains_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
810    ToolBuilder::new("pool_fan_out_chains")
811        .title("Fan Out Chains")
812        .description(
813            "Fan out multiple chains in parallel, each on its own slot. \
814             Returns all task IDs. Check on each with pool_chain_result.",
815        )
816        .handler(move |input: FanOutChainsInput| {
817            let state = Arc::clone(&state);
818            async move {
819                let chains = input.chains.into_iter().map(convert_chain_steps).collect();
820                let isolation = parse_isolation(input.isolation.as_deref());
821                let options = claude_pool::ChainOptions {
822                    tags: input.tags.unwrap_or_default(),
823                    isolation,
824                };
825                let skills = state.skills.read().await;
826                match state.pool.fan_out_chains(chains, &skills, options).await {
827                    Ok(task_ids) => Ok(CallToolResult::json(serde_json::json!({
828                        "task_ids": task_ids.iter().map(|id| &id.0).collect::<Vec<_>>()
829                    }))),
830                    Err(e) => Ok(CallToolResult::error(e.to_string())),
831                }
832            }
833        })
834        .build()
835}
836
837pub fn pool_chain_result_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
838    ToolBuilder::new("pool_chain_result")
839        .title("Check on a Chain")
840        .description(
841            "Check on a fired chain. Shows per-step progress: which step is running, \
842             completed steps, and overall status.",
843        )
844        .read_only()
845        .handler(move |input: TaskIdInput| {
846            let state = Arc::clone(&state);
847            async move {
848                let task_id = claude_pool::TaskId(input.task_id.clone());
849                match state.pool.chain_progress(&task_id) {
850                    Some(progress) => Ok(CallToolResult::json(
851                        serde_json::to_value(&progress).unwrap(),
852                    )),
853                    None => {
854                        // Fall back to checking if the task exists at all.
855                        match state.pool.result(&task_id).await {
856                            Ok(Some(r)) => {
857                                Ok(CallToolResult::json(serde_json::to_value(&r).unwrap()))
858                            }
859                            Ok(None) => Ok(CallToolResult::error(format!(
860                                "no chain found for task_id: {}",
861                                input.task_id,
862                            ))),
863                            Err(e) => Ok(CallToolResult::error(e.to_string())),
864                        }
865                    }
866                }
867            }
868        })
869        .build()
870}
871
872/// Cancel a running chain, skipping remaining steps.
873pub fn pool_cancel_chain_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
874    ToolBuilder::new("pool_cancel_chain")
875        .title("Cancel a Chain")
876        .description(
877            "Cancel a running chain that was fired with pool_submit_chain or pool_fan_out_chains. \
878             The current step finishes before cancellation takes effect. Remaining steps are \
879             skipped. Check on the chain with pool_chain_result to confirm.",
880        )
881        .handler(move |input: TaskIdInput| {
882            let state = Arc::clone(&state);
883            async move {
884                let task_id = claude_pool::TaskId(input.task_id.clone());
885                match state.pool.cancel_chain(&task_id).await {
886                    Ok(()) => Ok(CallToolResult::json(serde_json::json!({
887                        "status": "cancellation_requested",
888                        "task_id": input.task_id,
889                    }))),
890                    Err(e) => Ok(CallToolResult::error(e.to_string())),
891                }
892            }
893        })
894        .build()
895}
896
897pub fn pool_invoke_workflow_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
898    ToolBuilder::new("pool_invoke_workflow")
899        .title("Invoke Workflow")
900        .description(
901            "Submit a named workflow template with arguments. Returns a task_id immediately. \
902             Example workflows: 'issue_to_pr', 'refactor_and_test', 'review_and_fix'.",
903        )
904        .handler(move |input: InvokeWorkflowInput| {
905            let state = Arc::clone(&state);
906            async move {
907                let skills = state.skills.read().await;
908                match state
909                    .pool
910                    .submit_workflow(
911                        &input.workflow,
912                        input.arguments,
913                        &skills,
914                        &state.workflows,
915                        input.tags.unwrap_or_default(),
916                    )
917                    .await
918                {
919                    Ok(task_id) => Ok(CallToolResult::json(serde_json::json!({
920                        "task_id": task_id.0,
921                        "workflow": input.workflow,
922                    }))),
923                    Err(e) => Ok(CallToolResult::error(e.to_string())),
924                }
925            }
926        })
927        .build()
928}
929
930/// Build all pool tools.
931pub fn pool_scale_up_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
932    ToolBuilder::new("pool_scale_up")
933        .title("Scale Up the Pool")
934        .description("Add N new slots to the pool. Returns the new total slot count.")
935        .handler(move |input: ScalingInput| {
936            let state = Arc::clone(&state);
937            async move {
938                match state.pool.scale_up(input.count).await {
939                    Ok(new_count) => Ok(CallToolResult::json(serde_json::json!({
940                        "success": true,
941                        "new_slot_count": new_count,
942                        "details": format!("Scaled up by {} slots", input.count),
943                    }))),
944                    Err(e) => Ok(CallToolResult::error(e.to_string())),
945                }
946            }
947        })
948        .build()
949}
950
951pub fn pool_scale_down_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
952    ToolBuilder::new("pool_scale_down")
953        .title("Scale Down the Pool")
954        .description(
955            "Remove N slots from the pool. Removes idle slots first, \
956             then waits for busy slots to complete. Returns the new total slot count.",
957        )
958        .handler(move |input: ScalingInput| {
959            let state = Arc::clone(&state);
960            async move {
961                match state.pool.scale_down(input.count).await {
962                    Ok(new_count) => Ok(CallToolResult::json(serde_json::json!({
963                        "success": true,
964                        "new_slot_count": new_count,
965                        "details": format!("Scaled down by {} slots", input.count),
966                    }))),
967                    Err(e) => Ok(CallToolResult::error(e.to_string())),
968                }
969            }
970        })
971        .build()
972}
973
974pub fn pool_set_target_slots_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
975    ToolBuilder::new("pool_set_target_slots")
976        .title("Set Pool Size")
977        .description("Set the pool to a specific number of slots, scaling up or down as needed.")
978        .handler(move |input: SetTargetSlotsInput| {
979            let state = Arc::clone(&state);
980            async move {
981                match state.pool.set_target_slots(input.target).await {
982                    Ok(new_count) => Ok(CallToolResult::json(serde_json::json!({
983                        "success": true,
984                        "new_slot_count": new_count,
985                        "target": input.target,
986                    }))),
987                    Err(e) => Ok(CallToolResult::error(e.to_string())),
988                }
989            }
990        })
991        .build()
992}
993
994// ── Skill management tools ──────────────────────────────────────────
995
996/// List registered skills with optional scope/source filters.
997pub fn pool_skill_list_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
998    ToolBuilder::new("pool_skill_list")
999        .title("List Skills")
1000        .description("List skills available in the pool, with optional scope/source filters. Skills come from builtins, global (~/.claude-pool/skills/), or project (.claude-pool/skills/).")
1001        .read_only()
1002        .handler(move |input: SkillListInput| {
1003            let state = Arc::clone(&state);
1004            async move {
1005                let registry = state.skills.read().await;
1006                let scope_filter = input.scope.as_deref().map(parse_scope);
1007                let source_filter = input.source.as_deref().and_then(parse_source);
1008
1009                let mut results: Vec<_> = registry
1010                    .list_registered()
1011                    .into_iter()
1012                    .filter(|rs| {
1013                        if let Some(scope) = scope_filter
1014                            && rs.skill.scope != scope
1015                        {
1016                            return false;
1017                        }
1018                        if let Some(source) = source_filter
1019                            && rs.source != source
1020                        {
1021                            return false;
1022                        }
1023                        true
1024                    })
1025                    .map(|rs| {
1026                        let mut entry = serde_json::json!({
1027                            "name": rs.skill.name,
1028                            "description": rs.skill.description,
1029                            "scope": rs.skill.scope.to_string(),
1030                            "source": rs.source.to_string(),
1031                        });
1032                        if let Some(ref hint) = rs.skill.argument_hint {
1033                            entry["argument_hint"] = serde_json::json!(hint);
1034                        }
1035                        entry
1036                    })
1037                    .collect();
1038                results.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str()));
1039                Ok(CallToolResult::json(serde_json::json!(results)))
1040            }
1041        })
1042        .build()
1043}
1044
1045/// Get full details of a skill by name, including prompt template.
1046pub fn pool_skill_get_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
1047    ToolBuilder::new("pool_skill_get")
1048        .title("Get Skill Details")
1049        .description("Get full details of a skill by name, including prompt template.")
1050        .read_only()
1051        .handler(move |input: SkillGetInput| {
1052            let state = Arc::clone(&state);
1053            async move {
1054                let registry = state.skills.read().await;
1055                match registry.get_registered(&input.name) {
1056                    Some(rs) => {
1057                        // A skill is "customized" if it's a builtin name but
1058                        // loaded from project/global/runtime source.
1059                        let customized = rs.source != SkillSource::Builtin
1060                            && claude_pool::skill::builtin_skills()
1061                                .iter()
1062                                .any(|b| b.name == rs.skill.name);
1063                        let response = serde_json::json!({
1064                            "name": rs.skill.name,
1065                            "description": rs.skill.description,
1066                            "prompt": rs.skill.prompt,
1067                            "arguments": rs.skill.arguments.iter().map(|a| serde_json::json!({
1068                                "name": a.name,
1069                                "description": a.description,
1070                                "required": a.required,
1071                            })).collect::<Vec<_>>(),
1072                            "scope": rs.skill.scope.to_string(),
1073                            "source": rs.source.to_string(),
1074                            "customized": customized,
1075                            "config": rs.skill.config,
1076                        });
1077                        Ok(CallToolResult::json(response))
1078                    }
1079                    None => Ok(CallToolResult::error(format!(
1080                        "skill not found: {}",
1081                        input.name
1082                    ))),
1083                }
1084            }
1085        })
1086        .build()
1087}
1088
1089/// Register a skill at runtime. Ephemeral unless saved with pool_skill_save.
1090pub fn pool_skill_add_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
1091    ToolBuilder::new("pool_skill_add")
1092        .title("Add a Skill")
1093        .description(
1094            "Register a skill at runtime. Ephemeral (lost on restart) unless saved \
1095             with pool_skill_save. Overwrites any existing skill with the same name.",
1096        )
1097        .handler(move |input: SkillAddInput| {
1098            let state = Arc::clone(&state);
1099            async move {
1100                let scope = input.scope.as_deref().map(parse_scope).unwrap_or_default();
1101                let arguments = input
1102                    .arguments
1103                    .into_iter()
1104                    .map(|a| claude_pool::SkillArgument {
1105                        name: a.name,
1106                        description: a.description,
1107                        required: a.required,
1108                    })
1109                    .collect();
1110                let config: Option<SlotConfig> =
1111                    input.config.and_then(|v| serde_json::from_value(v).ok());
1112                let skill = claude_pool::Skill {
1113                    name: input.name.clone(),
1114                    description: input.description,
1115                    prompt: input.prompt,
1116                    arguments,
1117                    config,
1118                    scope,
1119                    argument_hint: None,
1120                    skill_dir: None,
1121                };
1122                let mut registry = state.skills.write().await;
1123                let overwritten = registry.get(&input.name).is_some();
1124                registry.register(skill, SkillSource::Runtime);
1125                Ok(CallToolResult::json(serde_json::json!({
1126                    "name": input.name,
1127                    "overwritten": overwritten,
1128                    "source": "runtime",
1129                })))
1130            }
1131        })
1132        .build()
1133}
1134
1135/// Remove a skill by name. Runtime-only, does not delete files.
1136pub fn pool_skill_remove_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
1137    ToolBuilder::new("pool_skill_remove")
1138        .title("Remove a Skill")
1139        .description("Remove a skill by name. Runtime-only, does not delete files on disk.")
1140        .handler(move |input: SkillRemoveInput| {
1141            let state = Arc::clone(&state);
1142            async move {
1143                let mut registry = state.skills.write().await;
1144                match registry.remove(&input.name) {
1145                    Some(_) => Ok(CallToolResult::json(serde_json::json!({
1146                        "removed": input.name,
1147                    }))),
1148                    None => Ok(CallToolResult::error(format!(
1149                        "skill not found: {}",
1150                        input.name
1151                    ))),
1152                }
1153            }
1154        })
1155        .build()
1156}
1157
1158/// Persist a skill to the project skills directory as a SKILL.md folder.
1159pub fn pool_skill_save_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
1160    ToolBuilder::new("pool_skill_save")
1161        .title("Save Skill to Disk")
1162        .description(
1163            "Persist a skill to the project skills directory as a SKILL.md folder \
1164             (Agent Skills standard). Creates/overwrites {dir}/{name}/SKILL.md.",
1165        )
1166        .handler(move |input: SkillSaveInput| {
1167            let state = Arc::clone(&state);
1168            async move {
1169                let skill = {
1170                    let registry = state.skills.read().await;
1171                    match registry.get(&input.name) {
1172                        Some(s) => s.clone(),
1173                        None => {
1174                            return Ok(CallToolResult::error(format!(
1175                                "skill not found: {}",
1176                                input.name
1177                            )));
1178                        }
1179                    }
1180                };
1181
1182                let base_dir = input
1183                    .dir
1184                    .map(PathBuf::from)
1185                    .unwrap_or_else(|| state.skills_dir.clone());
1186
1187                let skill_dir = base_dir.join(&skill.name);
1188                if let Err(e) = std::fs::create_dir_all(&skill_dir) {
1189                    return Ok(CallToolResult::error(format!(
1190                        "failed to create directory {}: {e}",
1191                        skill_dir.display()
1192                    )));
1193                }
1194
1195                let skill_md = skill_to_skill_md(&skill);
1196                let path = skill_dir.join("SKILL.md");
1197
1198                if let Err(e) = std::fs::write(&path, &skill_md) {
1199                    return Ok(CallToolResult::error(format!(
1200                        "failed to write {}: {e}",
1201                        path.display()
1202                    )));
1203                }
1204
1205                // Update source to Project since it's now persisted.
1206                {
1207                    let mut registry = state.skills.write().await;
1208                    if let Some(existing) = registry.get(&input.name).cloned() {
1209                        registry.register(existing, SkillSource::Project);
1210                    }
1211                }
1212
1213                Ok(CallToolResult::json(serde_json::json!({
1214                    "saved": input.name,
1215                    "path": path.display().to_string(),
1216                    "format": "SKILL.md",
1217                })))
1218            }
1219        })
1220        .build()
1221}
1222
1223/// Convert a Skill to SKILL.md format (YAML frontmatter + markdown body).
1224fn skill_to_skill_md(skill: &claude_pool::Skill) -> String {
1225    use std::fmt::Write;
1226
1227    let mut out = String::new();
1228    out.push_str("---\n");
1229    writeln!(out, "name: {}", skill.name).unwrap();
1230    writeln!(
1231        out,
1232        "description: \"{}\"",
1233        skill.description.replace('"', "\\\"")
1234    )
1235    .unwrap();
1236
1237    let has_metadata = skill.scope != claude_pool::SkillScope::Task
1238        || !skill.arguments.is_empty()
1239        || skill.config.is_some();
1240
1241    if has_metadata {
1242        out.push_str("metadata:\n");
1243        if skill.scope != claude_pool::SkillScope::Task {
1244            writeln!(out, "  scope: {}", skill.scope).unwrap();
1245        }
1246        if !skill.arguments.is_empty() {
1247            out.push_str("  arguments:\n");
1248            for arg in &skill.arguments {
1249                writeln!(out, "    - name: {}", arg.name).unwrap();
1250                writeln!(
1251                    out,
1252                    "      description: \"{}\"",
1253                    arg.description.replace('"', "\\\"")
1254                )
1255                .unwrap();
1256                writeln!(out, "      required: {}", arg.required).unwrap();
1257            }
1258        }
1259    }
1260
1261    out.push_str("---\n\n");
1262    out.push_str(&skill.prompt);
1263    out.push('\n');
1264    out
1265}
1266
1267/// Eject a builtin skill to disk for customization.
1268pub fn pool_skill_eject_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
1269    ToolBuilder::new("pool_skill_eject")
1270        .title("Eject Builtin Skill")
1271        .description(
1272            "Write a builtin skill to disk as a SKILL.md folder for customization. \
1273             The disk version shadows the builtin. Delete the folder to restore the default.",
1274        )
1275        .handler(move |input: SkillEjectInput| {
1276            let state = Arc::clone(&state);
1277            async move {
1278                // Find the builtin version of the skill.
1279                let builtin = claude_pool::skill::builtin_skills()
1280                    .into_iter()
1281                    .find(|s| s.name == input.name);
1282
1283                let skill = match builtin {
1284                    Some(s) => s,
1285                    None => {
1286                        return Ok(CallToolResult::error(format!(
1287                            "not a builtin skill: {} (only builtins can be ejected)",
1288                            input.name
1289                        )));
1290                    }
1291                };
1292
1293                let base_dir = input
1294                    .dir
1295                    .map(PathBuf::from)
1296                    .unwrap_or_else(|| state.skills_dir.clone());
1297
1298                let skill_dir = base_dir.join(&skill.name);
1299                if let Err(e) = std::fs::create_dir_all(&skill_dir) {
1300                    return Ok(CallToolResult::error(format!(
1301                        "failed to create directory {}: {e}",
1302                        skill_dir.display()
1303                    )));
1304                }
1305
1306                let skill_md = skill_to_skill_md(&skill);
1307                let path = skill_dir.join("SKILL.md");
1308
1309                if let Err(e) = std::fs::write(&path, &skill_md) {
1310                    return Ok(CallToolResult::error(format!(
1311                        "failed to write {}: {e}",
1312                        path.display()
1313                    )));
1314                }
1315
1316                // Re-register as Project source so it shows as customized.
1317                {
1318                    let mut registry = state.skills.write().await;
1319                    registry.register(skill, SkillSource::Project);
1320                }
1321
1322                Ok(CallToolResult::json(serde_json::json!({
1323                    "ejected": input.name,
1324                    "path": path.display().to_string(),
1325                    "hint": "edit the SKILL.md to customize, delete the folder to restore builtin",
1326                })))
1327            }
1328        })
1329        .build()
1330}
1331
1332pub fn pool_send_message_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
1333    ToolBuilder::new("pool_send_message")
1334        .title("Send Message Between Slots")
1335        .description("Send a message from one slot to another. Returns the message ID.")
1336        .handler(move |input: SendMessageInput| {
1337            let state = Arc::clone(&state);
1338            async move {
1339                let from = claude_pool::types::SlotId(input.from);
1340                let to = claude_pool::types::SlotId(input.to);
1341                let message_id = state.pool.send_message(from, to, input.content);
1342                Ok(CallToolResult::json(serde_json::json!({
1343                    "message_id": message_id,
1344                })))
1345            }
1346        })
1347        .build()
1348}
1349
1350pub fn pool_read_messages_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
1351    ToolBuilder::new("pool_read_messages")
1352        .title("Read Messages from Slot")
1353        .description("Drain and read all messages for a slot, removing them from the inbox.")
1354        .handler(move |input: ReadMessagesInput| {
1355            let state = Arc::clone(&state);
1356            async move {
1357                let slot_id = claude_pool::types::SlotId(input.slot_id);
1358                let messages = state.pool.read_messages(&slot_id);
1359                Ok(CallToolResult::json(
1360                    serde_json::to_value(&messages).unwrap(),
1361                ))
1362            }
1363        })
1364        .build()
1365}
1366
1367pub fn pool_peek_messages_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
1368    ToolBuilder::new("pool_peek_messages")
1369        .title("Peek Messages from Slot")
1370        .description("Read messages from a slot's inbox without removing them.")
1371        .read_only()
1372        .handler(move |input: PeekMessagesInput| {
1373            let state = Arc::clone(&state);
1374            async move {
1375                let slot_id = claude_pool::types::SlotId(input.slot_id);
1376                let messages = state.pool.peek_messages(&slot_id);
1377                Ok(CallToolResult::json(
1378                    serde_json::to_value(&messages).unwrap(),
1379                ))
1380            }
1381        })
1382        .build()
1383}
1384
1385pub fn pool_broadcast_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
1386    ToolBuilder::new("pool_broadcast")
1387        .title("Broadcast Message to All Slots")
1388        .description(
1389            "Send a message from one slot to all other active slots. Returns the list of message IDs.",
1390        )
1391        .handler(move |input: BroadcastInput| {
1392            let state = Arc::clone(&state);
1393            async move {
1394                let from = claude_pool::types::SlotId(input.from);
1395                match state.pool.broadcast_message(from, input.content).await {
1396                    Ok(ids) => {
1397                        let count = ids.len();
1398                        Ok(CallToolResult::json(serde_json::json!({
1399                            "message_ids": ids,
1400                            "recipients": count,
1401                        })))
1402                    }
1403                    Err(e) => Ok(CallToolResult::error(e.to_string())),
1404                }
1405            }
1406        })
1407        .build()
1408}
1409
1410pub fn pool_find_slots_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
1411    ToolBuilder::new("pool_find_slots")
1412        .title("Find Slots by Name, Role, or State")
1413        .description(
1414            "Query slots by name, role, and/or state. All filters are optional; omitted filters match everything.",
1415        )
1416        .read_only()
1417        .handler(move |input: FindSlotsInput| {
1418            let state = Arc::clone(&state);
1419            async move {
1420                let slot_state = input.state.as_deref().and_then(|s| match s {
1421                    "idle" => Some(claude_pool::types::SlotState::Idle),
1422                    "busy" => Some(claude_pool::types::SlotState::Busy),
1423                    "stopped" => Some(claude_pool::types::SlotState::Stopped),
1424                    "errored" => Some(claude_pool::types::SlotState::Errored),
1425                    _ => None,
1426                });
1427                match state
1428                    .pool
1429                    .find_slots(input.name.as_deref(), input.role.as_deref(), slot_state)
1430                    .await
1431                {
1432                    Ok(slots) => {
1433                        let results: Vec<_> = slots
1434                            .iter()
1435                            .map(|s| {
1436                                serde_json::json!({
1437                                    "id": s.id.0,
1438                                    "state": s.state,
1439                                    "name": s.config.name,
1440                                    "role": s.config.role,
1441                                    "description": s.config.description,
1442                                    "current_task": s.current_task.as_ref().map(|t| &t.0),
1443                                    "tasks_completed": s.tasks_completed,
1444                                    "cost_microdollars": s.cost_microdollars,
1445                                })
1446                            })
1447                            .collect();
1448                        Ok(CallToolResult::json(serde_json::json!({
1449                            "slots": results,
1450                            "count": results.len(),
1451                        })))
1452                    }
1453                    Err(e) => Ok(CallToolResult::error(e.to_string())),
1454                }
1455            }
1456        })
1457        .build()
1458}
1459
1460pub fn pool_claim_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
1461    ToolBuilder::new("pool_claim")
1462        .title("Claim Next Pending Task")
1463        .description(
1464            "Self-service task claiming: an idle slot grabs the next pending task from the queue. \
1465             Returns the claimed task ID, or null if no tasks are waiting. The task executes \
1466             in the background on the claiming slot.",
1467        )
1468        .handler(move |input: ClaimInput| {
1469            let state = Arc::clone(&state);
1470            async move {
1471                let slot_id = claude_pool::types::SlotId(input.slot_id);
1472                match state.pool.claim(&slot_id).await {
1473                    Ok(Some(task_id)) => Ok(CallToolResult::json(serde_json::json!({
1474                        "claimed": true,
1475                        "task_id": task_id.0,
1476                    }))),
1477                    Ok(None) => Ok(CallToolResult::json(serde_json::json!({
1478                        "claimed": false,
1479                        "task_id": null,
1480                        "reason": "no pending tasks or slot not idle",
1481                    }))),
1482                    Err(e) => Ok(CallToolResult::error(e.to_string())),
1483                }
1484            }
1485        })
1486        .build()
1487}
1488
1489pub fn pool_submit_with_review_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
1490    ToolBuilder::new("pool_submit_with_review")
1491        .title("Fire a Task with Review Gate")
1492        .description(
1493            "Fire off a task that requires coordinator approval before completion. \
1494             When the task finishes, it enters 'pending_review' state instead of 'completed'. \
1495             Use pool_approve_result to accept or pool_reject_result to reject with feedback.",
1496        )
1497        .handler(move |input: SubmitWithReviewInput| {
1498            let state = Arc::clone(&state);
1499            async move {
1500                let config = task_config_from(input.model, input.effort, input.mcp_servers);
1501                let tags = input.tags.unwrap_or_default();
1502                match state
1503                    .pool
1504                    .submit_with_review(&input.prompt, config, tags, input.max_rejections)
1505                    .await
1506                {
1507                    Ok(task_id) => Ok(CallToolResult::json(
1508                        serde_json::json!({ "task_id": task_id.0, "review_required": true }),
1509                    )),
1510                    Err(e) => Ok(CallToolResult::error(e.to_string())),
1511                }
1512            }
1513        })
1514        .build()
1515}
1516
1517pub fn pool_approve_result_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
1518    ToolBuilder::new("pool_approve_result")
1519        .title("Approve Task Result")
1520        .description(
1521            "Approve a task that is pending review. Transitions the task from \
1522             'pending_review' to 'completed'.",
1523        )
1524        .handler(move |input: ApproveResultInput| {
1525            let state = Arc::clone(&state);
1526            async move {
1527                let task_id = claude_pool::TaskId(input.task_id);
1528                match state.pool.approve_result(&task_id).await {
1529                    Ok(()) => Ok(CallToolResult::text("approved")),
1530                    Err(e) => Ok(CallToolResult::error(e.to_string())),
1531                }
1532            }
1533        })
1534        .build()
1535}
1536
1537pub fn pool_reject_result_tool<S: PoolStore + 'static>(state: Arc<State<S>>) -> Tool {
1538    ToolBuilder::new("pool_reject_result")
1539        .title("Reject Task Result")
1540        .description(
1541            "Reject a task that is pending review. The task is re-queued with the \
1542             original prompt plus rejection feedback appended. If the task has been \
1543             rejected max_rejections times, it is marked as failed.",
1544        )
1545        .handler(move |input: RejectResultInput| {
1546            let state = Arc::clone(&state);
1547            async move {
1548                let task_id = claude_pool::TaskId(input.task_id);
1549                match state.pool.reject_result(&task_id, &input.feedback).await {
1550                    Ok(()) => Ok(CallToolResult::text("rejected and re-queued")),
1551                    Err(e) => Ok(CallToolResult::error(e.to_string())),
1552                }
1553            }
1554        })
1555        .build()
1556}
1557
1558pub fn all_tools<S: PoolStore + 'static>(state: &Arc<State<S>>) -> Vec<Tool> {
1559    vec![
1560        pool_status_tool(Arc::clone(state)),
1561        pool_run_tool(Arc::clone(state)),
1562        pool_submit_tool(Arc::clone(state)),
1563        pool_result_tool(Arc::clone(state)),
1564        pool_cancel_tool(Arc::clone(state)),
1565        pool_fan_out_tool(Arc::clone(state)),
1566        pool_drain_tool(Arc::clone(state)),
1567        pool_skill_run_tool(Arc::clone(state)),
1568        pool_chain_tool(Arc::clone(state)),
1569        pool_submit_chain_tool(Arc::clone(state)),
1570        pool_fan_out_chains_tool(Arc::clone(state)),
1571        pool_chain_result_tool(Arc::clone(state)),
1572        pool_cancel_chain_tool(Arc::clone(state)),
1573        pool_invoke_workflow_tool(Arc::clone(state)),
1574        pool_scale_up_tool(Arc::clone(state)),
1575        pool_scale_down_tool(Arc::clone(state)),
1576        pool_set_target_slots_tool(Arc::clone(state)),
1577        context_set_tool(Arc::clone(state)),
1578        context_get_tool(Arc::clone(state)),
1579        context_delete_tool(Arc::clone(state)),
1580        context_list_tool(Arc::clone(state)),
1581        pool_send_message_tool(Arc::clone(state)),
1582        pool_read_messages_tool(Arc::clone(state)),
1583        pool_peek_messages_tool(Arc::clone(state)),
1584        pool_broadcast_tool(Arc::clone(state)),
1585        pool_find_slots_tool(Arc::clone(state)),
1586        pool_configure_slot_tool(Arc::clone(state)),
1587        pool_skill_list_tool(Arc::clone(state)),
1588        pool_skill_get_tool(Arc::clone(state)),
1589        pool_skill_add_tool(Arc::clone(state)),
1590        pool_skill_remove_tool(Arc::clone(state)),
1591        pool_skill_save_tool(Arc::clone(state)),
1592        pool_skill_eject_tool(Arc::clone(state)),
1593        pool_claim_tool(Arc::clone(state)),
1594        pool_submit_with_review_tool(Arc::clone(state)),
1595        pool_approve_result_tool(Arc::clone(state)),
1596        pool_reject_result_tool(Arc::clone(state)),
1597    ]
1598}