use std::future::Future;
use std::pin::Pin;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use super::context::McpToolContext;
use crate::error::{ForgeError, Result};
#[derive(Debug, Clone, Copy, Serialize)]
pub struct McpToolIcon {
pub src: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sizes: Option<&'static [&'static str]>,
#[serde(skip_serializing_if = "Option::is_none")]
pub theme: Option<&'static str>,
}
#[derive(Debug, Clone, Copy, Serialize, Default)]
pub struct McpToolAnnotations {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<&'static str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub read_only_hint: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub destructive_hint: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub idempotent_hint: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub open_world_hint: Option<bool>,
}
#[derive(Debug, Clone, Copy)]
pub struct McpToolInfo {
pub name: &'static str,
pub title: Option<&'static str>,
pub description: Option<&'static str>,
pub required_role: Option<&'static str>,
pub is_public: bool,
pub timeout: Option<u64>,
pub rate_limit_requests: Option<u32>,
pub rate_limit_per_secs: Option<u64>,
pub rate_limit_key: Option<&'static str>,
pub annotations: McpToolAnnotations,
pub icons: &'static [McpToolIcon],
}
impl McpToolInfo {
pub fn validate(&self) -> Result<()> {
if self.name.is_empty() {
return Err(ForgeError::Config(
"MCP tool name cannot be empty".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum McpContentBlock {
Text { text: String },
ResourceLink(McpContent),
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct McpContent {
pub uri: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct McpToolResult {
pub content: Vec<McpContentBlock>,
#[serde(rename = "structuredContent", skip_serializing_if = "Option::is_none")]
pub structured_content: Option<serde_json::Value>,
#[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
pub is_error: Option<bool>,
}
impl McpToolResult {
pub fn success_text(text: impl Into<String>) -> Self {
Self {
content: vec![McpContentBlock::Text { text: text.into() }],
structured_content: None,
is_error: None,
}
}
pub fn success_json(value: serde_json::Value) -> Self {
let text = match serde_json::to_string(&value) {
Ok(v) => v,
Err(_) => "{}".to_string(),
};
let structured = match value {
serde_json::Value::Object(_) => Some(value),
_ => None,
};
Self {
content: vec![McpContentBlock::Text { text }],
structured_content: structured,
is_error: None,
}
}
pub fn tool_error(message: impl Into<String>) -> Self {
Self {
content: vec![McpContentBlock::Text {
text: message.into(),
}],
structured_content: None,
is_error: Some(true),
}
}
}
pub trait ForgeMcpTool: Send + Sync + 'static {
type Args: DeserializeOwned + JsonSchema + Send + Sync;
type Output: Serialize + JsonSchema + Send;
fn info() -> McpToolInfo;
fn execute(
ctx: &McpToolContext,
args: Self::Args,
) -> Pin<Box<dyn Future<Output = Result<Self::Output>> + Send + '_>>;
fn input_schema() -> serde_json::Value {
let schema = schemars::schema_for!(Self::Args);
serde_json::to_value(schema)
.unwrap_or_else(|_| serde_json::json!({ "type": "object", "properties": {} }))
}
fn output_schema() -> Option<serde_json::Value> {
let schema = schemars::schema_for!(Self::Output);
Some(
serde_json::to_value(schema)
.unwrap_or_else(|_| serde_json::json!({ "type": "object", "properties": {} })),
)
}
}