kodegen_mcp_schema/tool/
traits.rs

1use rmcp::handler::server::tool::schema_for_type;
2use rmcp::model::{CallToolResult, Content, Meta, PromptArgument, PromptMessage};
3use schemars::JsonSchema;
4use serde::{Serialize, de::DeserializeOwned};
5use serde_json::Value;
6use std::borrow::Cow;
7use std::collections::HashMap;
8use std::path::PathBuf;
9use std::sync::Arc;
10
11use log::{debug, error, info, warn};
12
13use super::error::McpError;
14
15// Re-export ToolArgs from parent crate
16pub use crate::ToolArgs;
17
18// ============================================================================
19// SEALED PROMPT PROVIDER TRAIT
20// ============================================================================
21
22/// Sealed module - ONLY kodegen-mcp-schema can implement Sealed trait
23mod sealed {
24    /// Sealed trait that prevents external crates from implementing PromptProvider
25    pub trait Sealed {}
26}
27
28/// Trait that ONLY kodegen-mcp-schema can implement.
29///
30/// This trait provides prompt generation for tools. Tools CANNOT implement this
31/// trait directly - they must reference a PromptProvider implementation from
32/// the schema package.
33///
34/// This enforces architectural constraint: ALL prompt logic MUST be centralized
35/// in kodegen-mcp-schema, tools cannot implement prompts inline.
36pub trait PromptProvider: sealed::Sealed + Send + Sync + 'static {
37    /// Prompt arguments type - what customization does the prompt accept?
38    type PromptArgs: DeserializeOwned + JsonSchema + Send + 'static;
39
40    /// Generate prompt messages - ONLY schema can implement.
41    ///
42    /// Returns a conversation showing agents how/when to use the tool.
43    /// Should include examples, common patterns, gotchas, requirements.
44    fn generate_prompts(args: &Self::PromptArgs) -> Vec<PromptMessage>;
45
46    /// What arguments does the teaching prompt accept?
47    ///
48    /// These let agents customize what they want to learn about.
49    /// Example: "repo" (which repo?), "shallow" (learn about shallow clones?)
50    fn prompt_arguments() -> Vec<PromptArgument>;
51}
52
53/// Re-export sealed trait for schema package to implement
54///
55/// Only kodegen-mcp-schema can `impl SealedPromptProvider for MyPrompts {}`.
56/// Tool packages cannot implement this - it's sealed to enforce centralization.
57pub use sealed::Sealed as SealedPromptProvider;
58
59// ============================================================================
60// BRANDED DISPLAY LINE
61// ============================================================================
62
63/// Tool execution status for branded line coloring
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum ToolStatus {
66    /// Tool executed successfully
67    Success,
68    /// Tool execution failed
69    Error,
70}
71
72/// Add branded display line to CallToolResult as new Content[0].
73///
74/// Inserts a branded line at the beginning of the content vector:
75/// - New Content[0]: Branded line (ⓚ icon tool_name duration)
76/// - Content[1]: Original Content[0] (display)
77/// - Content[2]: Original Content[1] (metadata)
78/// - etc.
79///
80/// # Arguments
81/// - `call_result`: The CallToolResult to modify (Success case)
82/// - `tool_name`: Name of the tool being executed
83/// - `icon`: Tool icon character
84/// - `duration_ms`: Execution duration in milliseconds
85/// - `status`: Success or Error status for coloring
86pub fn add_branded_line_to_result(
87    call_result: &mut CallToolResult,
88    tool_name: &str,
89    icon: char,
90    duration_ms: u64,
91    status: ToolStatus,
92) {
93    // Calculate duration in seconds (ceil to >= 1s)
94    let duration_s = ((duration_ms as f64 / 1000.0).ceil() as u64).max(1);
95
96    // Select timing color based on status
97    // Success: green (35 #00af5f)
98    // Error: red (204 #ff5f87)
99    let timing_color = match status {
100        ToolStatus::Success => 35,
101        ToolStatus::Error => 204,
102    };
103
104    // Format branded line with ANSI colors
105    // ⓚ brand symbol: color 132 (#af5f87)
106    // icon + tool_name: color 32 (#0087d7)
107    // duration: status-based color
108    let branded_line = format!(
109        "\x1b[38;5;132mⓚ\x1b[0m   \x1b[38;5;32m{} {}\x1b[0m   \x1b[38;5;{}m{}s\x1b[0m",
110        icon, tool_name, timing_color, duration_s
111    );
112
113    // Insert branded content at position 0, shifting all existing content
114    let branded_content = Content::text(branded_line);
115    call_result.content.insert(0, branded_content);
116}
117
118#[derive(Debug, Clone)]
119pub struct ToolResponse<M> {
120    /// Human-readable display output - goes to Content[0].
121    ///
122    /// This is the PRIMARY output that humans read:
123    /// - Terminal: the full output of the last command (stdout/stderr)
124    /// - File read: the file contents
125    /// - Search: formatted human-readable results
126    /// - Database query: formatted table output
127    ///
128    /// Always present - every tool has human-readable output.
129    /// Use empty string for tools with no display output.
130    pub display: String,
131
132    /// Typed, schema-enforced metadata - goes to Content[1].
133    ///
134    /// This is SEPARATE from display - NO DUPLICATION.
135    /// Contains only structured data (exit_code, duration_ms, etc.)
136    /// NOT the display content.
137    pub metadata: M,
138}
139
140impl<M> ToolResponse<M> {
141    /// Create response with both display and metadata.
142    ///
143    /// # Arguments
144    /// - `display`: Human-readable output (full command output, file contents, etc.)
145    /// - `metadata`: Typed output struct (derived from Args::Output)
146    #[inline]
147    pub fn new(display: impl Into<String>, metadata: M) -> Self {
148        Self {
149            display: display.into(),
150            metadata,
151        }
152    }
153
154    /// Create response with empty display.
155    ///
156    /// Use when there's no human-readable output but metadata is present.
157    #[inline]
158    pub fn empty_display(metadata: M) -> Self {
159        Self {
160            display: String::new(),
161            metadata,
162        }
163    }
164}
165
166impl<M: Serialize> ToolResponse<M> {
167    /// Convert to CallToolResult for MCP response.
168    ///
169    /// # Content Layout
170    /// - `content[0]`: Human-readable display (always present, may be empty)
171    /// - `content[1]`: Typed metadata as pretty-printed JSON
172    ///
173    /// Both display and metadata are in the content Vec - no structured_content.
174    pub fn into_call_tool_result(self) -> Result<CallToolResult, serde_json::Error> {
175        let display_content = Content::text(self.display);
176        let json = serde_json::to_string_pretty(&self.metadata)?;
177        let metadata_content = Content::text(json);
178
179        Ok(CallToolResult {
180            content: vec![display_content, metadata_content],
181            structured_content: None,
182            is_error: None,
183            meta: None,
184        })
185    }
186
187    /// Get metadata as JSON Value (for history recording).
188    pub fn metadata_as_json(&self) -> serde_json::Value {
189        serde_json::to_value(&self.metadata).unwrap_or_else(|_| serde_json::json!({}))
190    }
191
192    /// Convert to CallToolResult with branded display line.
193    ///
194    /// Creates a branded line as a separate Content[0], shifting display and metadata:
195    /// - `content[0]`: Branded line (ⓚ icon tool_name duration)
196    /// - `content[1]`: Human-readable display (was content[0])
197    /// - `content[2]`: Typed metadata as pretty-printed JSON (was content[1])
198    ///
199    /// # Arguments
200    /// - `tool_name`: Name of the tool being executed
201    /// - `icon`: Tool icon character (from metadata or fallback)
202    /// - `duration_ms`: Execution duration in milliseconds
203    /// - `status`: Success or Error status for coloring
204    pub fn into_call_tool_result_with_branding(
205        self,
206        tool_name: &str,
207        icon: char,
208        duration_ms: u64,
209        status: ToolStatus,
210    ) -> Result<CallToolResult, serde_json::Error> {
211        // Calculate duration in seconds (ceil to >= 1s)
212        let duration_s = ((duration_ms as f64 / 1000.0).ceil() as u64).max(1);
213
214        // Select timing color based on status
215        // Success: green (35 #00af5f)
216        // Error: red (204 #ff5f87)
217        let timing_color = match status {
218            ToolStatus::Success => 35,
219            ToolStatus::Error => 204,
220        };
221
222        // Format branded line with ANSI colors
223        // ⓚ brand symbol: color 132 (#af5f87)
224        // icon + tool_name: color 32 (#0087d7)
225        // duration: status-based color
226        let branded_line = format!(
227            "\x1b[38;5;132mⓚ\x1b[0m   \x1b[38;5;32m{} {}\x1b[0m   \x1b[38;5;{}m{}s\x1b[0m",
228            icon, tool_name, timing_color, duration_s
229        );
230
231        // Create content vector with branded line first
232        let branded_content = Content::text(branded_line);
233        let display_content = Content::text(self.display);
234        let json = serde_json::to_string_pretty(&self.metadata)?;
235        let metadata_content = Content::text(json);
236
237        Ok(CallToolResult {
238            content: vec![branded_content, display_content, metadata_content],
239            structured_content: None,
240            is_error: None,
241            meta: None,
242        })
243    }
244}
245
246// ============================================================================
247// PERFORMANCE OPTIMIZATIONS
248// ============================================================================
249
250/// Type alias for the schema cache to reduce complexity
251type SchemaCache =
252    parking_lot::RwLock<HashMap<&'static str, std::sync::Arc<serde_json::Map<String, Value>>>>;
253
254/// Schema cache to avoid repeated serialization
255static SCHEMA_CACHE: std::sync::LazyLock<SchemaCache> =
256    std::sync::LazyLock::new(|| parking_lot::RwLock::new(HashMap::new()));
257
258// ============================================================================
259// CORE TRAIT
260// ============================================================================
261
262/// Core trait that all tools must implement
263///
264/// Tools are STRUCTS that hold their own dependencies (`GitClient`, `GitHubClient`, etc.)
265/// The trait is generic and knows nothing about specific services.
266/// Every method (except execute/prompt) has a sensible default.
267pub trait Tool: Send + Sync + Sized + 'static {
268    /// Tool execution arguments - ALSO determines output type via ToolArgs::Output.
269    ///
270    /// The output type is DERIVED from Args - tools cannot choose wrong output type.
271    /// This binding is defined in `kodegen-mcp-schema` and enforced at compile time.
272    type Args: ToolArgs;
273
274    /// Prompt provider (MUST be from schema - tools cannot implement PromptProvider).
275    ///
276    /// Tools declare which PromptProvider they use, but cannot implement it themselves.
277    /// PromptProvider is sealed - only kodegen-mcp-schema can implement it.
278    /// This enforces that all prompt logic is centralized in the schema package.
279    type Prompts: PromptProvider;
280
281    // ========================================================================
282    // IDENTITY (Required)
283    // ========================================================================
284
285    /// Unique tool name (e.g., "`git_clone`")
286    fn name() -> &'static str;
287
288    /// Human-readable description of what this tool does
289    fn description() -> &'static str;
290
291    // ========================================================================
292    // SCHEMA (Auto-generated with caching)
293    // ========================================================================
294
295    /// Validate that schema generation works for this tool's Args type.
296    /// 
297    /// This method attempts to generate the JSON schema and catches any panics
298    /// that occur during schema generation. Should be called during tool 
299    /// registration to catch schema issues early.
300    ///
301    /// # Returns
302    /// - `Ok(())` if schema generation succeeds
303    /// - `Err(String)` with detailed error message if schema generation fails
304    fn validate_schema() -> Result<(), String> {
305        // Wrap schema_for_type in a panic catch
306        let result = std::panic::catch_unwind(|| {
307            let _ = schema_for_type::<Self::Args>();
308        });
309        
310        match result {
311            Ok(_) => Ok(()),
312            Err(e) => {
313                let error_msg = if let Some(s) = e.downcast_ref::<&str>() {
314                    s.to_string()
315                } else if let Some(s) = e.downcast_ref::<String>() {
316                    s.clone()
317                } else {
318                    "Unknown panic during schema generation".to_string()
319                };
320                Err(format!(
321                    "Schema generation failed for tool '{}': {}",
322                    Self::name(),
323                    error_msg
324                ))
325            }
326        }
327    }
328
329    /// Input schema - AUTO-GENERATED from Args type via `JsonSchema` derive
330    /// Cached for performance - schema is computed once and reused
331    #[inline]
332    fn input_schema() -> std::sync::Arc<serde_json::Map<String, Value>> {
333        let name = Self::name();
334
335        // Fast path: read from cache
336        if let Some(schema) = SCHEMA_CACHE.read().get(name) {
337            return schema.clone();
338        }
339
340        // Slow path: generate and cache
341        // Log schema generation attempt
342        debug!("Generating schema for tool: {}", name);
343        
344        // Validate schema generation with panic catching
345        if let Err(e) = Self::validate_schema() {
346            error!("{}", e);
347            // For now, still proceed but with warning - could be made fatal in future
348            warn!("Tool '{}' registered with potentially invalid schema", name);
349        } else {
350            info!("✓ Schema generated successfully for tool: {}", name);
351        }
352        
353        let schema = std::sync::Arc::new(schema_for_type::<Self::Args>());
354        SCHEMA_CACHE.write().insert(name, schema.clone());
355        schema
356    }
357
358    /// Output schema - AUTO-GENERATED from `<Args as ToolArgs>::Output`.
359    ///
360    /// Unlike input_schema which is optional to override, output_schema is
361    /// always derived from the Args→Output mapping in the schema package.
362    /// This ensures compile-time enforcement of correct output types.
363    #[must_use]
364    #[inline]
365    fn output_schema() -> std::sync::Arc<serde_json::Map<String, Value>> {
366        // Use a separate cache namespace for output schemas
367        static OUTPUT_SCHEMA_CACHE: std::sync::LazyLock<SchemaCache> =
368            std::sync::LazyLock::new(|| parking_lot::RwLock::new(HashMap::new()));
369
370        let name = Self::name();
371        let cache_key = Box::leak(format!("{}_output", name).into_boxed_str());
372
373        // Fast path: read from cache
374        if let Some(schema) = OUTPUT_SCHEMA_CACHE.read().get(cache_key) {
375            return schema.clone();
376        }
377
378        // Slow path: generate and cache from Args::Output
379        let schema = std::sync::Arc::new(
380            schema_for_type::<<Self::Args as ToolArgs>::Output>()
381        );
382        OUTPUT_SCHEMA_CACHE.write().insert(cache_key, schema.clone());
383        schema
384    }
385
386    // ========================================================================
387    // BEHAVIOR ANNOTATIONS (Tool IS its behavior)
388    // ========================================================================
389
390    /// Does this tool only read (never modify) state?
391    ///
392    /// true = read-only (safe, can't break things)
393    /// false = writes/modifies state (requires caution)
394    ///
395    /// Default: true (assumes read-only by default - safe default)
396    #[must_use]
397    #[inline]
398    fn read_only() -> bool {
399        true
400    }
401
402    /// Can this tool delete or overwrite existing data?
403    ///
404    /// Only meaningful when `read_only` = false.
405    /// true = can delete/overwrite (dangerous)
406    /// false = only adds/creates (safer)
407    ///
408    /// Default: false (assumes non-destructive by default - safe default)
409    #[must_use]
410    #[inline]
411    fn destructive() -> bool {
412        false
413    }
414
415    /// Is calling this tool repeatedly with same args safe/idempotent?
416    ///
417    /// Only meaningful when `read_only` = false.
418    /// true = safe to retry (same result every time)
419    /// false = each call has different effect
420    ///
421    /// Default: true (assumes idempotent by default - safe default)
422    #[must_use]
423    #[inline]
424    fn idempotent() -> bool {
425        true
426    }
427
428    /// Does this tool interact with external systems (network, filesystem outside repo)?
429    ///
430    /// true = open world (network calls, external APIs, can fail due to external factors)
431    /// false = closed world (only local operations, deterministic)
432    ///
433    /// Default: false (assumes local operations by default - safe default)
434    #[must_use]
435    #[inline]
436    fn open_world() -> bool {
437        false
438    }
439
440    // ========================================================================
441    // EXECUTION (Required)
442    // ========================================================================
443
444    /// Execute the tool with given arguments.
445    ///
446    /// Return type is `ToolResponse<<Self::Args as ToolArgs>::Output>`.
447    /// The output type is DERIVED from Args - compiler enforces correct type.
448    ///
449    /// # Example
450    ///
451    /// ```rust
452    /// impl Tool for TerminalTool {
453    ///     type Args = TerminalInput;
454    ///     // TerminalInput::Output = TerminalOutput (defined in schema)
455    ///     // So execute() MUST return ToolResponse<TerminalOutput>
456    ///
457    ///     async fn execute(&self, args: Self::Args, ctx: ToolExecutionContext)
458    ///         -> Result<ToolResponse<TerminalOutput>, McpError>
459    ///     {
460    ///         Ok(ToolResponse::new(output_text, TerminalOutput { ... }))
461    ///     }
462    /// }
463    /// ```
464    fn execute(
465        &self,
466        args: Self::Args,
467        ctx: ToolExecutionContext,
468    ) -> impl std::future::Future<Output = Result<
469        ToolResponse<<Self::Args as ToolArgs>::Output>,
470        McpError
471    >> + Send;
472
473    // ========================================================================
474    // RMCP INTEGRATION (Default implementations)
475    // ========================================================================
476
477    /// Prompt name (defaults to "{`tool_name`}_help")
478    #[must_use]
479    #[inline]
480    fn prompt_name() -> Cow<'static, str> {
481        Cow::Owned(format!("{}_help", Self::name()))
482    }
483
484    /// Prompt description (defaults to tool description)
485    #[must_use]
486    #[inline]
487    fn prompt_description() -> &'static str {
488        Self::description()
489    }
490
491    /// Convert this tool into an RMCP `ToolRoute`
492    ///
493    /// This default implementation builds the route from trait methods.
494    /// Tools get this for free - no need to implement `IntoToolRoute` manually.
495    fn into_tool_route<S>(self) -> rmcp::handler::server::router::tool::ToolRoute<S>
496    where
497        S: Send + Sync + 'static,
498    {
499        use rmcp::handler::server::router::tool::ToolRoute;
500        use rmcp::model::{Tool as RmcpTool, ToolAnnotations, Meta};
501        use std::sync::Arc;
502
503        // Build annotations from trait methods
504        let annotations = ToolAnnotations::new()
505            .read_only(Self::read_only())
506            .destructive(Self::destructive())
507            .idempotent(Self::idempotent())
508            .open_world(Self::open_world());
509
510        // Store icon in meta field
511        let mut meta = Meta::new();
512        meta.0.insert("icon".to_string(), serde_json::json!(<Self::Args as ToolArgs>::icon().to_string()));
513
514        // Build RMCP Tool metadata
515        let metadata = RmcpTool {
516            name: Self::name().into(),
517            title: None,
518            description: Some(Self::description().into()),
519            input_schema: Self::input_schema(),
520            output_schema: Some(Self::output_schema()),
521            annotations: Some(annotations),
522            icons: None,
523            meta: Some(meta),
524        };
525
526        // Create handler with ToolHandler wrapper (HRTB-compatible, zero-cost)
527        let handler = ToolHandler {
528            tool: Arc::new(self),
529        };
530
531        // Use ToolRoute::new() - handles HRTB internally
532        ToolRoute::new(metadata, handler)
533    }
534
535    /// Convert this tool into an RMCP `PromptRoute`
536    ///
537    /// This default implementation builds the route from trait methods.
538    /// Tools get this for free - no need to implement `IntoPromptRoute` manually.
539    fn into_prompt_route<S>(self) -> rmcp::handler::server::router::prompt::PromptRoute<S>
540    where
541        S: Send + Sync + 'static,
542    {
543        use rmcp::handler::server::router::prompt::PromptRoute;
544        use rmcp::handler::server::wrapper::Parameters;
545        use rmcp::model::{GetPromptResult, Prompt as RmcpPrompt};
546
547        // Build meta following the same pattern as tools (see line 434-435)
548        let mut meta = Meta::new();
549        meta.0.insert("category".to_string(), serde_json::json!(Self::Args::CATEGORY.name));
550        meta.0.insert("icon".to_string(), serde_json::json!(Self::Args::CATEGORY.icon.to_string()));
551
552        // Build RMCP Prompt metadata using PromptProvider
553        let metadata = RmcpPrompt {
554            name: Self::prompt_name().into_owned(),
555            title: None,
556            description: Some(Self::prompt_description().to_string()),
557            arguments: Some(<Self as ToolPrompts>::prompt_arguments()),
558            icons: None,
559            meta: Some(meta),
560        };
561
562        // Handler calls static PromptProvider methods (no tool instance needed)
563        let handler = move |Parameters(args): Parameters<<Self::Prompts as PromptProvider>::PromptArgs>| {
564            async move {
565                let messages = <Self as ToolPrompts>::prompt(args)?;
566
567                Ok(GetPromptResult {
568                    description: Some(Self::prompt_description().to_string()),
569                    messages,
570                })
571            }
572        };
573
574        PromptRoute::new(metadata, handler)
575    }
576
577    /// Convert Arc-wrapped tool into an RMCP `ToolRoute` (optimized - no extra Arc allocation)
578    ///
579    /// This is more efficient than `into_tool_route(self)` when the tool is already wrapped in Arc.
580    /// The tool is used directly without creating an additional Arc wrapper.
581    fn arc_into_tool_route<S>(self: Arc<Self>) -> rmcp::handler::server::router::tool::ToolRoute<S>
582    where
583        S: Send + Sync + 'static,
584    {
585        use rmcp::handler::server::router::tool::ToolRoute;
586        use rmcp::model::{Tool as RmcpTool, ToolAnnotations, Meta};
587
588        // Build annotations from trait methods
589        let annotations = ToolAnnotations::new()
590            .read_only(Self::read_only())
591            .destructive(Self::destructive())
592            .idempotent(Self::idempotent())
593            .open_world(Self::open_world());
594
595        // Store icon in meta field
596        let mut meta = Meta::new();
597        meta.0.insert("icon".to_string(), serde_json::json!(<Self::Args as ToolArgs>::icon().to_string()));
598
599        // Build RMCP Tool metadata
600        let metadata = RmcpTool {
601            name: Self::name().into(),
602            title: None,
603            description: Some(Self::description().into()),
604            input_schema: Self::input_schema(),
605            output_schema: Some(Self::output_schema()),
606            annotations: Some(annotations),
607            icons: None,
608            meta: Some(meta),
609        };
610
611        // Create handler with ToolHandler wrapper (HRTB-compatible, zero-cost)
612        // Use self directly (already Arc<Self>) - no extra Arc allocation
613        let handler = ToolHandler {
614            tool: self,
615        };
616
617        // Use ToolRoute::new() - handles HRTB internally
618        ToolRoute::new(metadata, handler)
619    }
620
621    /// Convert Arc-wrapped tool into an RMCP `PromptRoute` (optimized - no extra Arc allocation)
622    ///
623    /// This is more efficient than `into_prompt_route(self)` when the tool is already wrapped in Arc.
624    /// The tool is used directly without creating an additional Arc wrapper.
625    fn arc_into_prompt_route<S>(
626        self: Arc<Self>,
627    ) -> rmcp::handler::server::router::prompt::PromptRoute<S>
628    where
629        S: Send + Sync + 'static,
630    {
631        use rmcp::handler::server::router::prompt::PromptRoute;
632        use rmcp::handler::server::wrapper::Parameters;
633        use rmcp::model::{GetPromptResult, Prompt as RmcpPrompt};
634
635        // Build meta following the same pattern as tools (see line 434-435)
636        let mut meta = Meta::new();
637        meta.0.insert("category".to_string(), serde_json::json!(Self::Args::CATEGORY.name));
638        meta.0.insert("icon".to_string(), serde_json::json!(Self::Args::CATEGORY.icon.to_string()));
639
640        // Build RMCP Prompt metadata using PromptProvider
641        let metadata = RmcpPrompt {
642            name: Self::prompt_name().into_owned(),
643            title: None,
644            description: Some(Self::prompt_description().to_string()),
645            arguments: Some(<Self as ToolPrompts>::prompt_arguments()),
646            icons: None,
647            meta: Some(meta),
648        };
649
650        // Handler calls static PromptProvider methods (no tool instance needed)
651        let handler = move |Parameters(args): Parameters<<Self::Prompts as PromptProvider>::PromptArgs>| {
652            async move {
653                let messages = <Self as ToolPrompts>::prompt(args)?;
654
655                Ok(GetPromptResult {
656                    description: Some(Self::prompt_description().to_string()),
657                    messages,
658                })
659            }
660        };
661
662        PromptRoute::new(metadata, handler)
663    }
664}
665
666// ============================================================================
667// TOOL PROMPTS EXTENSION TRAIT
668// ============================================================================
669
670/// Extension trait providing prompt methods via PromptProvider.
671///
672/// This is implemented automatically for all Tool implementations via blanket impl.
673/// Tools get prompt() and prompt_arguments() methods for free by specifying
674/// their Prompts associated type.
675///
676/// The methods delegate to the PromptProvider, which only schema can implement.
677pub trait ToolPrompts: Tool {
678    /// Get prompt arguments (calls schema's PromptProvider)
679    #[inline]
680    fn prompt_arguments() -> Vec<PromptArgument> {
681        <Self::Prompts as PromptProvider>::prompt_arguments()
682    }
683
684    /// Generate prompts (calls schema's PromptProvider)
685    ///
686    /// Takes PromptArgs from the PromptProvider, not from Self.
687    /// This ensures tools cannot override prompt generation.
688    #[inline]
689    fn prompt(args: <Self::Prompts as PromptProvider>::PromptArgs) -> Result<Vec<PromptMessage>, McpError> {
690        Ok(<Self::Prompts as PromptProvider>::generate_prompts(&args))
691    }
692}
693
694/// Blanket implementation - all Tools automatically get ToolPrompts
695impl<T: Tool> ToolPrompts for T {}
696
697// ============================================================================
698// PROGRESS NOTIFICATION CONTEXT
699// ============================================================================
700
701/// Execution context provided to tools for progress notifications and cancellation.
702///
703/// Supports three patterns:
704/// 1. Stream text messages: `ctx.stream("output\n")`
705/// 2. Report numeric progress: `ctx.progress(50, 100)`  
706/// 3. Combined: `ctx.update(50, 100, "Processing file 50/100")`
707#[derive(Clone)]
708pub struct ToolExecutionContext {
709    /// Peer interface for sending progress notifications
710    peer: rmcp::service::Peer<rmcp::RoleServer>,
711
712    /// Cancellation token (tool should check periodically)
713    ct: tokio_util::sync::CancellationToken,
714
715    /// Unique request identifier (used for progress_token)
716    request_id: rmcp::model::RequestId,
717
718    /// Infrastructure context from kodegen stdio server (via HTTP headers)
719    /// These are None for non-HTTP transports
720
721    /// Connection ID - identifies the stdio connection instance
722    connection_id: Option<String>,
723
724    /// Current working directory from client environment
725    pwd: Option<PathBuf>,
726
727    /// Git repository root from client environment
728    git_root: Option<PathBuf>,
729}
730
731impl ToolExecutionContext {
732    /// Create a new ToolExecutionContext with the given peer, cancellation token, and request ID.
733    ///
734    /// This constructor is public to allow custom integration contexts (e.g., bridging
735    /// to non-RMCP transports or in-process sessions). Most tools should not need to
736    /// call this directly - the context is typically provided by the RMCP framework.
737    ///
738    /// # Arguments
739    /// * `peer` - RMCP peer for sending progress notifications
740    /// * `ct` - Cancellation token for this execution
741    /// * `request_id` - Unique identifier for this request
742    #[must_use]
743    pub fn new(
744        peer: rmcp::service::Peer<rmcp::RoleServer>,
745        ct: tokio_util::sync::CancellationToken,
746        request_id: rmcp::model::RequestId,
747    ) -> Self {
748        Self {
749            peer,
750            ct,
751            request_id,
752            connection_id: None,
753            pwd: None,
754            git_root: None,
755        }
756    }
757
758    /// Get connection ID from stdio server (for resource isolation)
759    /// Always present for kodegen stdio connections, None for direct HTTP clients
760    #[must_use]
761    pub fn connection_id(&self) -> Option<&str> {
762        self.connection_id.as_deref()
763    }
764
765    /// Get current working directory from client environment
766    #[must_use]
767    pub fn pwd(&self) -> Option<&std::path::Path> {
768        self.pwd.as_deref()
769    }
770
771    /// Get git repository root from client environment
772    #[must_use]
773    pub fn git_root(&self) -> Option<&std::path::Path> {
774        self.git_root.as_deref()
775    }
776
777    /// Get the request ID for this tool execution
778    ///
779    /// The request ID uniquely identifies this tool call and can be used for:
780    /// - Filtering events in multi-request scenarios
781    /// - Correlating logs and outputs
782    /// - Tracking execution history
783    #[must_use]
784    pub fn request_id(&self) -> &rmcp::model::RequestId {
785        &self.request_id
786    }
787
788    /// Stream a text message (for terminal output, logs, status updates).
789    ///
790    /// Use this for incrementally streaming text output as it becomes available.
791    ///
792    /// # Example
793    /// ```
794    /// // Terminal streaming command output
795    /// ctx.stream("npm info using npm@8.19.2\n").await.ok();
796    /// ctx.stream("added 234 packages in 15s\n").await.ok();
797    /// ```
798    pub async fn stream(&self, message: impl Into<String>) -> Result<(), McpError> {
799        self.notify_internal(0.0, None, Some(message.into())).await
800    }
801
802    /// Report numeric progress (for progress bars, counters).
803    ///
804    /// Use this when you have a known total and current progress value.
805    ///
806    /// # Example
807    /// ```
808    /// // Processing 50 out of 100 files
809    /// ctx.progress(50.0, 100.0).await.ok();
810    /// ```
811    pub async fn progress(&self, current: f64, total: f64) -> Result<(), McpError> {
812        self.notify_internal(current, Some(total), None).await
813    }
814
815    /// Report both numeric progress and a descriptive message.
816    ///
817    /// Use this when you want both a progress bar AND status text.
818    ///
819    /// # Example
820    /// ```
821    /// ctx.update(50.0, 100.0, "Generating embeddings 50/100").await.ok();
822    /// ```
823    pub async fn update(
824        &self,
825        current: f64,
826        total: f64,
827        message: impl Into<String>
828    ) -> Result<(), McpError> {
829        self.notify_internal(current, Some(total), Some(message.into())).await
830    }
831
832    /// Advanced: Full control over progress notification fields.
833    ///
834    /// Use this when you need fine-grained control (e.g., unknown total).
835    ///
836    /// # Example
837    /// ```
838    /// // Unknown total, just report current count
839    /// ctx.notify(lines_read as f64, None, Some("Reading file...")).await.ok();
840    /// ```
841    pub async fn notify(
842        &self,
843        progress: f64,
844        total: Option<f64>,
845        message: Option<String>
846    ) -> Result<(), McpError> {
847        self.notify_internal(progress, total, message).await
848    }
849
850    /// Internal implementation - sends the actual notification
851    async fn notify_internal(
852        &self,
853        progress: f64,
854        total: Option<f64>,
855        message: Option<String>
856    ) -> Result<(), McpError> {
857        use rmcp::model::{ProgressNotificationParam, ProgressToken, NumberOrString};
858
859        // Generate unique progress token from request ID
860        let progress_token = ProgressToken(NumberOrString::String(
861            format!("tool_{}", match &self.request_id {
862                NumberOrString::Number(n) => n.to_string(),
863                NumberOrString::String(s) => s.to_string(),
864            }).into()
865        ));
866
867        let params = ProgressNotificationParam {
868            progress_token,
869            progress,
870            total,
871            message,
872        };
873
874        self.peer
875            .notify_progress(params)
876            .await
877            .map_err(|e| McpError::Other(anyhow::anyhow!(
878                "Failed to send progress notification: {}", e
879            )))
880    }
881
882    /// Check if tool execution was cancelled by the client.
883    ///
884    /// Tools should check this periodically during long operations.
885    ///
886    /// # Example
887    /// ```
888    /// for item in items {
889    ///     if ctx.is_cancelled() {
890    ///         return Err(McpError::cancelled("Operation cancelled"));
891    ///     }
892    ///     process_item(item).await?;
893    /// }
894    /// ```
895    pub fn is_cancelled(&self) -> bool {
896        self.ct.is_cancelled()
897    }
898
899    /// Get the cancellation token for use with `tokio::select!` or custom logic.
900    pub fn cancellation_token(&self) -> &tokio_util::sync::CancellationToken {
901        &self.ct
902    }
903}
904
905// ============================================================================
906// FromContextPart implementation for ToolExecutionContext
907// ============================================================================
908
909impl<S> rmcp::handler::server::common::FromContextPart<rmcp::handler::server::tool::ToolCallContext<'_, S>>
910    for ToolExecutionContext
911where
912    S: Send + Sync + 'static,
913{
914    fn from_context_part(
915        context: &mut rmcp::handler::server::tool::ToolCallContext<'_, S>
916    ) -> Result<Self, rmcp::ErrorData> {
917        // Extract HTTP request Parts (automatically injected by rmcp)
918        let parts = context.request_context.extensions.get::<http::request::Parts>();
919
920        // Extract kodegen headers from Parts
921        use kodegen_config::{X_KODEGEN_CONNECTION_ID, X_KODEGEN_PWD, X_KODEGEN_GITROOT};
922
923        let (connection_id, pwd, git_root) = if let Some(parts) = parts {
924            let conn_id = parts.headers.get(X_KODEGEN_CONNECTION_ID)
925                .and_then(|v| v.to_str().ok())
926                .map(|s| s.to_string());
927
928            let pwd_val = parts.headers.get(X_KODEGEN_PWD)
929                .and_then(|v| v.to_str().ok())
930                .map(PathBuf::from);
931
932            let git_root_val = parts.headers.get(X_KODEGEN_GITROOT)
933                .and_then(|v| v.to_str().ok())
934                .map(PathBuf::from);
935
936            (conn_id, pwd_val, git_root_val)
937        } else {
938            // Non-HTTP transport (direct stdio, child process)
939            (None, None, None)
940        };
941
942        Ok(ToolExecutionContext {
943            peer: context.request_context.peer.clone(),
944            ct: context.request_context.ct.clone(),
945            request_id: context.request_context.id.clone(),
946            connection_id,
947            pwd,
948            git_root,
949        })
950    }
951}
952
953// ============================================================================
954// ToolHandler - Zero-cost wrapper for CallToolHandler implementation
955// ============================================================================
956
957/// Wrapper struct that holds a tool and implements CallToolHandler.
958/// This enables HRTB-compatible tool routing without closure lifetime issues.
959struct ToolHandler<T: Tool> {
960    tool: Arc<T>,
961}
962
963impl<T: Tool> Clone for ToolHandler<T> {
964    fn clone(&self) -> Self {
965        Self {
966            tool: self.tool.clone(),
967        }
968    }
969}
970
971impl<T, S> rmcp::handler::server::tool::CallToolHandler<S, ()> for ToolHandler<T>
972where
973    T: Tool,
974    S: Send + Sync + 'static,
975{
976    fn call(
977        self,
978        mut context: rmcp::handler::server::tool::ToolCallContext<'_, S>,
979    ) -> futures::future::BoxFuture<'_, Result<rmcp::model::CallToolResult, rmcp::ErrorData>> {
980        use rmcp::handler::server::wrapper::Parameters;
981        use rmcp::handler::server::common::FromContextPart;
982
983        Box::pin(async move {
984            // Extract arguments and execution context
985            let Parameters(args) = Parameters::<T::Args>::from_context_part(&mut context)?;
986            let exec_ctx = ToolExecutionContext::from_context_part(&mut context)?;
987
988            // Execute tool - returns ToolResponse<<T::Args as ToolArgs>::Output>
989            let result = self.tool.execute(args, exec_ctx).await;
990
991            match result {
992                Ok(response) => {
993                    // Convert ToolResponse to CallToolResult with structured validation
994                    let result = response.into_call_tool_result()
995                        .map_err(|e| rmcp::ErrorData::internal_error(
996                            format!("Failed to serialize tool output: {}", e),
997                            None
998                        ))?;
999
1000                    Ok(result)
1001                }
1002                Err(e) => {
1003                    Err(rmcp::ErrorData::from(e))
1004                }
1005            }
1006        })
1007    }
1008}