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)]
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#[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#[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#[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#[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#[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
130pub 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}