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
123pub 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}