1use 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#[derive(Debug, Deserialize, JsonSchema)]
20pub struct RunInput {
21 pub prompt: String,
23 pub model: Option<String>,
25 pub effort: Option<String>,
27 pub mcp_servers: Option<std::collections::HashMap<String, serde_json::Value>>,
30}
31
32#[derive(Debug, Deserialize, JsonSchema)]
33pub struct SubmitInput {
34 pub prompt: String,
36 pub model: Option<String>,
38 pub effort: Option<String>,
40 pub tags: Option<Vec<String>>,
42 pub mcp_servers: Option<std::collections::HashMap<String, serde_json::Value>>,
45}
46
47#[derive(Debug, Deserialize, JsonSchema)]
48pub struct TaskIdInput {
49 pub task_id: String,
51}
52
53#[derive(Debug, Deserialize, JsonSchema)]
54pub struct FanOutInput {
55 pub prompts: Vec<String>,
57}
58
59#[derive(Debug, Deserialize, JsonSchema)]
60pub struct ContextSetInput {
61 pub key: String,
63 pub value: String,
65}
66
67#[derive(Debug, Deserialize, JsonSchema)]
68pub struct ContextKeyInput {
69 pub key: String,
71}
72
73#[derive(Debug, Deserialize, JsonSchema)]
74pub struct ConfigureSlotInput {
75 pub slot_id: String,
77 pub name: Option<String>,
79 pub role: Option<String>,
81 pub description: Option<String>,
83}
84
85#[derive(Debug, Deserialize, JsonSchema)]
86pub struct InvokeWorkflowInput {
87 pub workflow: String,
89 #[serde(default)]
91 pub arguments: std::collections::HashMap<String, String>,
92 pub tags: Option<Vec<String>>,
94}
95
96#[derive(Debug, Deserialize, JsonSchema)]
97pub struct ScalingInput {
98 pub count: usize,
100}
101
102#[derive(Debug, Deserialize, JsonSchema)]
103pub struct SetTargetSlotsInput {
104 pub target: usize,
106}
107
108#[derive(Debug, Deserialize, JsonSchema)]
112pub struct SkillListInput {
113 pub scope: Option<String>,
115 pub source: Option<String>,
117}
118
119#[derive(Debug, Deserialize, JsonSchema)]
121pub struct SkillGetInput {
122 pub name: String,
124}
125
126#[derive(Debug, Deserialize, JsonSchema)]
128pub struct SkillArgumentInput {
129 pub name: String,
131 pub description: String,
133 #[serde(default)]
135 pub required: bool,
136}
137
138#[derive(Debug, Deserialize, JsonSchema)]
140pub struct SkillAddInput {
141 pub name: String,
143 pub description: String,
145 pub prompt: String,
147 #[serde(default)]
149 pub arguments: Vec<SkillArgumentInput>,
150 pub scope: Option<String>,
152 pub config: Option<serde_json::Value>,
154}
155
156#[derive(Debug, Deserialize, JsonSchema)]
158pub struct SkillRemoveInput {
159 pub name: String,
161}
162
163#[derive(Debug, Deserialize, JsonSchema)]
165pub struct SkillSaveInput {
166 pub name: String,
168 pub dir: Option<String>,
170}
171
172fn 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
224pub 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 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 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#[derive(Debug, Deserialize, JsonSchema)]
485pub struct SkillRunInput {
486 pub skill: String,
488 pub arguments: std::collections::HashMap<String, String>,
490 pub model: Option<String>,
492 pub effort: Option<String>,
494}
495
496#[derive(Debug, Deserialize, JsonSchema)]
497pub struct ChainInput {
498 pub steps: Vec<ChainStepInput>,
500}
501
502#[derive(Debug, Deserialize, JsonSchema)]
503pub struct SubmitChainInput {
504 pub steps: Vec<ChainStepInput>,
506 pub tags: Option<Vec<String>>,
508 pub isolation: Option<String>,
510}
511
512#[derive(Debug, Deserialize, JsonSchema)]
513pub struct FanOutChainsInput {
514 pub chains: Vec<Vec<ChainStepInput>>,
516 pub tags: Option<Vec<String>>,
518 pub isolation: Option<String>,
520}
521
522#[derive(Debug, Deserialize, JsonSchema)]
523pub struct ChainStepInput {
524 pub name: String,
526 #[serde(rename = "type")]
528 pub step_type: String,
529 pub value: String,
531 pub arguments: Option<std::collections::HashMap<String, String>>,
533 pub model: Option<String>,
535 pub effort: Option<String>,
537 pub retries: Option<u32>,
539 pub recovery_prompt: Option<String>,
541 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 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 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
729pub 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
788pub 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
852pub 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
899pub 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
936pub 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
980pub 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
1003pub 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 {
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}