firecrawl_mcp/
controller.rs

1#[cfg(feature = "batch-scrape")]
2pub mod batch_scrape;
3#[cfg(feature = "batch-scrape")]
4pub use batch_scrape::{BATCH_SCRAPE_TOOL_NAME, get_firecrawl_batch_scrape};
5#[cfg(feature = "crawl")]
6pub mod crawl;
7#[cfg(feature = "crawl")]
8pub use crawl::{CRAWL_TOOL_NAME, get_firecrawl_crawl};
9#[cfg(feature = "map")]
10pub mod map;
11#[cfg(feature = "map")]
12pub use map::{MAP_TOOL_NAME, get_firecrawl_map};
13#[cfg(feature = "scrape")]
14pub mod scrape;
15#[cfg(feature = "scrape")]
16pub use scrape::{SCRAPE_TOOL_NAME, get_firecrawl_scrape};
17#[cfg(feature = "search")]
18pub mod search;
19#[cfg(feature = "search")]
20pub use search::{SEARCH_TOOL_NAME, get_firecrawl_search};
21
22use firecrawl_sdk::FirecrawlApp;
23use rmcp::{
24    ErrorData as McpError, RoleServer, ServerHandler,
25    model::{
26        CallToolRequestParam, CallToolResult, Content, Implementation, ListToolsResult,
27        PaginatedRequestParam, ProtocolVersion, ServerCapabilities, ServerInfo, Tool,
28    },
29    service::RequestContext,
30};
31use std::sync::{Arc, LazyLock};
32use tracing::error;
33
34/// Extension trait to convert FirecrawlApp into FirecrawlMCP
35pub trait IntoFirecrawlMCP {
36    /// Converts a FirecrawlApp instance into a FirecrawlMCP instance
37    fn into_mcp(self) -> FirecrawlMCP;
38}
39
40impl IntoFirecrawlMCP for FirecrawlApp {
41    fn into_mcp(self) -> FirecrawlMCP {
42        FirecrawlMCP::new_with_app(self)
43    }
44}
45
46// Define the static tools using Arc<[Tool]> to avoid cloning
47pub static TOOLS: LazyLock<Arc<[Tool]>> = LazyLock::new(|| {
48    // Create a Vec and then convert it to Arc<[Tool]>
49    Arc::from(vec![
50        #[cfg(feature = "batch-scrape")]
51        {
52            let batch_scrape_tool = get_firecrawl_batch_scrape().unwrap();
53            Tool {
54                name: batch_scrape_tool.name.clone(),
55                description: batch_scrape_tool.description.clone(),
56                input_schema: Arc::new(
57                    batch_scrape_tool
58                        .input_schema
59                        .as_object()
60                        .expect("Tool schema must be an object")
61                        .clone(),
62                ),
63                annotations: None,
64                icons: None,
65                output_schema: None,
66                title: None,
67            }
68        },
69        #[cfg(feature = "crawl")]
70        {
71            let crawl_tool = get_firecrawl_crawl().unwrap();
72            Tool {
73                name: crawl_tool.name.clone(),
74                description: crawl_tool.description.clone(),
75                input_schema: Arc::new(
76                    crawl_tool
77                        .input_schema
78                        .as_object()
79                        .expect("Tool schema must be an object")
80                        .clone(),
81                ),
82                annotations: None,
83                icons: None,
84                output_schema: None,
85                title: None,
86            }
87        },
88        #[cfg(feature = "map")]
89        {
90            let map_tool = get_firecrawl_map().unwrap();
91            Tool {
92                name: map_tool.name.clone(),
93                description: map_tool.description.clone(),
94                input_schema: Arc::new(
95                    map_tool
96                        .input_schema
97                        .as_object()
98                        .expect("Tool schema must be an object")
99                        .clone(),
100                ),
101                annotations: None,
102                icons: None,
103                output_schema: None,
104                title: None,
105            }
106        },
107        #[cfg(feature = "scrape")]
108        {
109            let scrape_tool = get_firecrawl_scrape().unwrap();
110            Tool {
111                name: scrape_tool.name.clone(),
112                description: scrape_tool.description.clone(),
113                input_schema: Arc::new(
114                    scrape_tool
115                        .input_schema
116                        .as_object()
117                        .expect("Tool schema must be an object")
118                        .clone(),
119                ),
120                annotations: None,
121                icons: None,
122                output_schema: None,
123                title: None,
124            }
125        },
126        #[cfg(feature = "search")]
127        {
128            let search_tool = get_firecrawl_search().unwrap();
129            Tool {
130                name: search_tool.name.clone(),
131                description: search_tool.description.clone(),
132                input_schema: Arc::new(
133                    search_tool
134                        .input_schema
135                        .as_object()
136                        .expect("Tool schema must be an object")
137                        .clone(),
138                ),
139                annotations: None,
140                icons: None,
141                output_schema: None,
142                title: None,
143            }
144        },
145    ])
146});
147
148#[derive(Clone)]
149pub struct FirecrawlMCP {
150    pub client: FirecrawlApp,
151}
152
153impl FirecrawlMCP {
154    pub fn new(api_key: impl AsRef<str>, client: reqwest::Client) -> Self {
155        Self {
156            client: FirecrawlApp::new_with_client(api_key, client).unwrap(),
157        }
158    }
159
160    pub fn new_with_app(app: FirecrawlApp) -> Self {
161        Self { client: app }
162    }
163
164    pub fn new_selfhosted(
165        api_url: impl AsRef<str>,
166        api_key: Option<impl AsRef<str>>,
167        client: reqwest::Client,
168    ) -> Self {
169        Self {
170            client: FirecrawlApp::new_selfhosted_with_client(api_url, api_key, client).unwrap(),
171        }
172    }
173}
174
175impl ServerHandler for FirecrawlMCP {
176    fn get_info(&self) -> ServerInfo {
177        ServerInfo {
178            protocol_version: ProtocolVersion::V_2024_11_05,
179            capabilities: ServerCapabilities::builder().enable_tools().build(),
180            server_info: Implementation::from_build_env(),
181            instructions: Some(
182                "This server provides tools to crawl, scrape, and search the web using Firecrawl."
183                    .to_string(),
184            ),
185        }
186    }
187
188    async fn call_tool(
189        &self,
190        request: CallToolRequestParam,
191        _context: RequestContext<RoleServer>,
192    ) -> Result<CallToolResult, McpError> {
193        let tool_name = request.name;
194        let params = request.arguments.unwrap();
195
196        match tool_name.as_ref() {
197            #[cfg(feature = "batch-scrape")]
198            BATCH_SCRAPE_TOOL_NAME => match self.batch_scrape(params).await {
199                Ok(result) => Ok(CallToolResult::success(vec![Content::text(result)])),
200                Err(err) => {
201                    error!("Batch scraping URLs failed: {}", err);
202                    Err(err)
203                }
204            },
205            #[cfg(feature = "crawl")]
206            CRAWL_TOOL_NAME => {
207                match self.crawl(params).await {
208                    Ok(result) => Ok(CallToolResult::success(vec![Content::text(result)])),
209                    Err(err) => Err(err), // crawl now returns rmcp::Error
210                }
211            }
212            #[cfg(feature = "map")]
213            MAP_TOOL_NAME => {
214                match self.map(params).await {
215                    Ok(result) => Ok(CallToolResult::success(vec![Content::text(result)])),
216                    Err(err) => Err(err), // map now returns rmcp::Error
217                }
218            }
219            #[cfg(feature = "scrape")]
220            SCRAPE_TOOL_NAME => {
221                match self.scrape(params).await {
222                    Ok(result) => Ok(CallToolResult::success(vec![Content::text(result)])),
223                    Err(err) => Err(err), // scrape already returns rmcp::Error
224                }
225            }
226            #[cfg(feature = "search")]
227            SEARCH_TOOL_NAME => match self.search(params).await {
228                Ok(result) => Ok(CallToolResult::success(vec![Content::text(result)])),
229                Err(err) => Err(McpError::internal_error(
230                    format!("Search error: {}", err),
231                    None,
232                )),
233            },
234            _ => Err(McpError::invalid_request(
235                format!("Tool not found: {}", tool_name),
236                None,
237            )),
238        }
239    }
240
241    async fn list_tools(
242        &self,
243        _request: Option<PaginatedRequestParam>,
244        _context: RequestContext<RoleServer>,
245    ) -> Result<ListToolsResult, McpError> {
246        // Just clone the Arc pointer, not the actual tools
247        Ok(ListToolsResult {
248            tools: Vec::from(TOOLS.as_ref()),
249            next_cursor: None,
250        })
251    }
252}