Skip to main content

forge_core/mcp/
traits.rs

1use std::future::Future;
2use std::pin::Pin;
3
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize, de::DeserializeOwned};
6
7use super::context::McpToolContext;
8use crate::error::{ForgeError, Result};
9
10#[derive(Debug, Clone, Copy, Serialize)]
11pub struct McpToolIcon {
12    pub src: &'static str,
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub mime_type: Option<&'static str>,
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub sizes: Option<&'static [&'static str]>,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    pub theme: Option<&'static str>,
19}
20
21#[derive(Debug, Clone, Copy, Serialize, Default)]
22pub struct McpToolAnnotations {
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub title: Option<&'static str>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub read_only_hint: Option<bool>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub destructive_hint: Option<bool>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub idempotent_hint: Option<bool>,
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub open_world_hint: Option<bool>,
33}
34
35#[derive(Debug, Clone, Copy)]
36pub struct McpToolInfo {
37    pub name: &'static str,
38    pub title: Option<&'static str>,
39    pub description: Option<&'static str>,
40    pub required_role: Option<&'static str>,
41    pub is_public: bool,
42    pub timeout: Option<std::time::Duration>,
43    pub rate_limit_requests: Option<u32>,
44    pub rate_limit_per_secs: Option<u64>,
45    pub rate_limit_key: Option<&'static str>,
46    pub annotations: McpToolAnnotations,
47    pub icons: &'static [McpToolIcon],
48}
49
50impl McpToolInfo {
51    pub fn validate(&self) -> Result<()> {
52        if self.name.is_empty() {
53            return Err(ForgeError::config("MCP tool name cannot be empty"));
54        }
55        Ok(())
56    }
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
60#[serde(tag = "type", rename_all = "snake_case")]
61#[non_exhaustive]
62pub enum McpContentBlock {
63    Text { text: String },
64    ResourceLink(McpContent),
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
68pub struct McpContent {
69    pub uri: String,
70    pub name: String,
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub description: Option<String>,
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub mime_type: Option<String>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
78pub struct McpToolResult {
79    pub content: Vec<McpContentBlock>,
80    #[serde(rename = "structuredContent", skip_serializing_if = "Option::is_none")]
81    pub structured_content: Option<serde_json::Value>,
82    #[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
83    pub is_error: Option<bool>,
84}
85
86impl McpToolResult {
87    pub fn success_text(text: impl Into<String>) -> Self {
88        Self {
89            content: vec![McpContentBlock::Text { text: text.into() }],
90            structured_content: None,
91            is_error: None,
92        }
93    }
94
95    pub fn success_json(value: serde_json::Value) -> Self {
96        let text = match serde_json::to_string(&value) {
97            Ok(v) => v,
98            Err(_) => "{}".to_string(),
99        };
100        let structured = match value {
101            serde_json::Value::Object(_) => Some(value),
102            _ => None,
103        };
104
105        Self {
106            content: vec![McpContentBlock::Text { text }],
107            structured_content: structured,
108            is_error: None,
109        }
110    }
111
112    pub fn tool_error(message: impl Into<String>) -> Self {
113        Self {
114            content: vec![McpContentBlock::Text {
115                text: message.into(),
116            }],
117            structured_content: None,
118            is_error: Some(true),
119        }
120    }
121}
122
123/// Trait implemented by all MCP tools.
124pub trait ForgeMcpTool: crate::__sealed::Sealed + Send + Sync + 'static {
125    type Args: DeserializeOwned + JsonSchema + Send + Sync;
126    type Output: Serialize + JsonSchema + Send;
127
128    fn info() -> McpToolInfo;
129
130    fn execute(
131        ctx: &McpToolContext,
132        args: Self::Args,
133    ) -> Pin<Box<dyn Future<Output = Result<Self::Output>> + Send + '_>>;
134
135    fn input_schema() -> serde_json::Value {
136        let schema = schemars::schema_for!(Self::Args);
137        serde_json::to_value(schema)
138            .unwrap_or_else(|_| serde_json::json!({ "type": "object", "properties": {} }))
139    }
140
141    fn output_schema() -> Option<serde_json::Value> {
142        let schema = schemars::schema_for!(Self::Output);
143        Some(
144            serde_json::to_value(schema)
145                .unwrap_or_else(|_| serde_json::json!({ "type": "object", "properties": {} })),
146        )
147    }
148}