1use crate::client::XClient;
4use crate::error::XResult;
5use crate::types::SearchTweetsParams;
6use rmcp::{
7 model::ErrorData as McpError, RoleServer, ServerHandler,
8 handler::server::{
9 router::{tool::ToolRouter},
10 tool::Parameters,
11 },
12 model::*,
13 service::RequestContext,
14 tool, tool_handler, tool_router,
15 ServiceExt, transport::stdio,
16};
17use serde::{Deserialize, Serialize};
18use serde_json::json;
19use std::future::Future;
20
21#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
23pub struct GetUserArgs {
24 pub identifier: String,
26 #[serde(default)]
28 pub is_user_id: bool,
29}
30
31
32
33#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
35pub struct SearchTweetsArgs {
36 pub query: String,
38 #[serde(default = "default_max_results")]
40 pub max_results: u32,
41 #[serde(default)]
43 pub include_users: bool,
44 #[serde(default)]
46 pub include_metrics: bool,
47}
48
49#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
51pub struct GetTweetArgs {
52 pub tweet_id: String,
54}
55
56#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
58pub struct GetUserTweetsArgs {
59 pub identifier: String,
61 #[serde(default)]
63 pub is_user_id: bool,
64 #[serde(default = "default_max_results")]
66 pub max_results: u32,
67}
68
69fn default_max_results() -> u32 {
70 10
71}
72
73#[derive(Clone)]
75pub struct XMcpServer {
76 client: XClient,
77 tool_router: ToolRouter<XMcpServer>,
78}
79
80#[tool_router]
81impl XMcpServer {
82 pub fn new(client: XClient) -> Self {
84 Self {
85 client,
86 tool_router: Self::tool_router(),
87 }
88 }
89
90 pub fn from_env() -> XResult<Self> {
92 let client = XClient::from_env()?;
93 Ok(Self::new(client))
94 }
95
96 pub async fn run_stdio(self) -> XResult<()> {
98 let service = self.serve(stdio()).await?;
99 service.waiting().await?;
100 Ok(())
101 }
102
103 #[tool(description = "Get user information by username or user ID")]
105 async fn get_user(
106 &self,
107 Parameters(args): Parameters<GetUserArgs>,
108 ) -> Result<CallToolResult, McpError> {
109 let user = if args.is_user_id {
110 self.client.get_user_by_id(&args.identifier).await
111 } else {
112 self.client.get_user_by_username(&args.identifier).await
113 };
114
115 match user {
116 Ok(Some(user)) => {
117 let result = json!({
118 "success": true,
119 "user": user
120 });
121 Ok(CallToolResult::success(vec![Content::text(
122 serde_json::to_string_pretty(&result).unwrap_or_default(),
123 )]))
124 }
125 Ok(None) => {
126 let result = json!({
127 "success": false,
128 "error": "User not found"
129 });
130 Ok(CallToolResult::success(vec![Content::text(
131 serde_json::to_string_pretty(&result).unwrap_or_default(),
132 )]))
133 }
134 Err(e) => {
135 let result = json!({
136 "success": false,
137 "error": format!("Error: {}", e)
138 });
139 Ok(CallToolResult::success(vec![Content::text(
140 serde_json::to_string_pretty(&result).unwrap_or_default(),
141 )]))
142 }
143 }
144 }
145
146
147
148 #[tool(description = "Search for tweets")]
150 async fn search_tweets(
151 &self,
152 Parameters(args): Parameters<SearchTweetsArgs>,
153 ) -> Result<CallToolResult, McpError> {
154 let mut tweet_fields = vec![
155 "id".to_string(),
156 "text".to_string(),
157 "author_id".to_string(),
158 "created_at".to_string(),
159 ];
160
161 let mut user_fields = vec![];
162 let mut expansions = vec![];
163
164 if args.include_metrics {
165 tweet_fields.push("public_metrics".to_string());
166 }
167
168 if args.include_users {
169 user_fields.extend(vec![
170 "id".to_string(),
171 "name".to_string(),
172 "username".to_string(),
173 ]);
174 expansions.push("author_id".to_string());
175 }
176
177 let search_params = SearchTweetsParams {
178 query: args.query,
179 max_results: Some(args.max_results.min(100)), tweet_fields: Some(tweet_fields),
181 user_fields: if user_fields.is_empty() { None } else { Some(user_fields) },
182 expansions: if expansions.is_empty() { None } else { Some(expansions) },
183 };
184
185 match self.client.search_tweets(search_params).await {
186 Ok(tweets) => {
187 let result = json!({
188 "success": true,
189 "tweets": tweets,
190 "count": tweets.len()
191 });
192 Ok(CallToolResult::success(vec![Content::text(
193 serde_json::to_string_pretty(&result).unwrap_or_default(),
194 )]))
195 }
196 Err(e) => {
197 let result = json!({
198 "success": false,
199 "error": format!("Error: {}", e)
200 });
201 Ok(CallToolResult::success(vec![Content::text(
202 serde_json::to_string_pretty(&result).unwrap_or_default(),
203 )]))
204 }
205 }
206 }
207
208 #[tool(description = "Get a specific tweet by ID")]
210 async fn get_tweet(
211 &self,
212 Parameters(args): Parameters<GetTweetArgs>,
213 ) -> Result<CallToolResult, McpError> {
214 match self.client.get_tweet(&args.tweet_id).await {
215 Ok(Some(tweet)) => {
216 let result = json!({
217 "success": true,
218 "tweet": tweet
219 });
220 Ok(CallToolResult::success(vec![Content::text(
221 serde_json::to_string_pretty(&result).unwrap_or_default(),
222 )]))
223 }
224 Ok(None) => {
225 let result = json!({
226 "success": false,
227 "error": "Tweet not found"
228 });
229 Ok(CallToolResult::success(vec![Content::text(
230 serde_json::to_string_pretty(&result).unwrap_or_default(),
231 )]))
232 }
233 Err(e) => {
234 let result = json!({
235 "success": false,
236 "error": format!("Error: {}", e)
237 });
238 Ok(CallToolResult::success(vec![Content::text(
239 serde_json::to_string_pretty(&result).unwrap_or_default(),
240 )]))
241 }
242 }
243 }
244
245 #[tool(description = "Get user's recent tweets")]
247 async fn get_user_tweets(
248 &self,
249 Parameters(args): Parameters<GetUserTweetsArgs>,
250 ) -> Result<CallToolResult, McpError> {
251 let user_id = if args.is_user_id {
253 args.identifier.clone()
254 } else {
255 match self.client.get_user_by_username(&args.identifier).await {
256 Ok(Some(user)) => user.id,
257 Ok(None) => {
258 let result = json!({
259 "success": false,
260 "error": "User not found"
261 });
262 return Ok(CallToolResult::success(vec![Content::text(
263 serde_json::to_string_pretty(&result).unwrap_or_default(),
264 )]));
265 }
266 Err(e) => {
267 let result = json!({
268 "success": false,
269 "error": format!("Error: {}", e)
270 });
271 return Ok(CallToolResult::success(vec![Content::text(
272 serde_json::to_string_pretty(&result).unwrap_or_default(),
273 )]));
274 }
275 }
276 };
277
278 match self
279 .client
280 .get_user_tweets(&user_id, Some(args.max_results.min(100)))
281 .await
282 {
283 Ok(tweets) => {
284 let result = json!({
285 "success": true,
286 "tweets": tweets,
287 "count": tweets.len(),
288 "user_id": user_id
289 });
290 Ok(CallToolResult::success(vec![Content::text(
291 serde_json::to_string_pretty(&result).unwrap_or_default(),
292 )]))
293 }
294 Err(e) => {
295 let result = json!({
296 "success": false,
297 "error": format!("Error: {}", e)
298 });
299 Ok(CallToolResult::success(vec![Content::text(
300 serde_json::to_string_pretty(&result).unwrap_or_default(),
301 )]))
302 }
303 }
304 }
305}
306
307#[tool_handler]
308impl ServerHandler for XMcpServer {
309 fn get_info(&self) -> ServerInfo {
310 ServerInfo {
311 protocol_version: ProtocolVersion::V_2024_11_05,
312 capabilities: ServerCapabilities::builder()
313 .enable_tools()
314 .build(),
315 server_info: Implementation {
316 name: "x-mcp-server".to_string(),
317 version: crate::VERSION.to_string(),
318 },
319 instructions: Some("This server provides X (Twitter) API tools for read-only operations. Available tools: get_user, search_tweets, get_tweet, get_user_tweets.".to_string()),
320 }
321 }
322
323 async fn initialize(
324 &self,
325 _request: InitializeRequestParam,
326 _context: RequestContext<RoleServer>,
327 ) -> Result<InitializeResult, McpError> {
328 Ok(self.get_info())
329 }
330}