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