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
34pub trait IntoFirecrawlMCP {
36 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
46pub static TOOLS: LazyLock<Arc<[Tool]>> = LazyLock::new(|| {
48 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), }
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), }
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), }
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 Ok(ListToolsResult {
248 tools: Vec::from(TOOLS.as_ref()),
249 next_cursor: None,
250 })
251 }
252}