x_mcp_server/
server.rs

1//! MCP Server implementation for X API using RMCP SDK
2
3use 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/// Tool arguments for getting user information
22#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
23pub struct GetUserArgs {
24    /// Username (without @) or user ID
25    pub identifier: String,
26    /// Whether the identifier is a user ID (true) or username (false)
27    #[serde(default)]
28    pub is_user_id: bool,
29}
30
31
32
33/// Tool arguments for searching tweets
34#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
35pub struct SearchTweetsArgs {
36    /// Search query
37    pub query: String,
38    /// Maximum number of results (default: 10, max: 100)
39    #[serde(default = "default_max_results")]
40    pub max_results: u32,
41    /// Include user information in results
42    #[serde(default)]
43    pub include_users: bool,
44    /// Include tweet metrics
45    #[serde(default)]
46    pub include_metrics: bool,
47}
48
49/// Tool arguments for getting a specific tweet
50#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
51pub struct GetTweetArgs {
52    /// The tweet ID
53    pub tweet_id: String,
54}
55
56/// Tool arguments for getting user's tweets
57#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
58pub struct GetUserTweetsArgs {
59    /// Username or user ID
60    pub identifier: String,
61    /// Whether the identifier is a user ID (true) or username (false)
62    #[serde(default)]
63    pub is_user_id: bool,
64    /// Maximum number of tweets to retrieve (default: 10)
65    #[serde(default = "default_max_results")]
66    pub max_results: u32,
67}
68
69fn default_max_results() -> u32 {
70    10
71}
72
73/// X MCP Server
74#[derive(Clone)]
75pub struct XMcpServer {
76    client: XClient,
77    tool_router: ToolRouter<XMcpServer>,
78}
79
80#[tool_router]
81impl XMcpServer {
82    /// Create a new X MCP Server
83    pub fn new(client: XClient) -> Self {
84        Self {
85            client,
86            tool_router: Self::tool_router(),
87        }
88    }
89
90    /// Create server from environment variables
91    pub fn from_env() -> XResult<Self> {
92        let client = XClient::from_env()?;
93        Ok(Self::new(client))
94    }
95
96    /// Run the server with stdio transport
97    pub async fn run_stdio(self) -> XResult<()> {
98        let service = self.serve(stdio()).await?;
99        service.waiting().await?;
100        Ok(())
101    }
102
103    /// Get user information by username or user ID
104    #[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    /// Search for tweets
149    #[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)), // API limit
180            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    /// Get a specific tweet by ID
209    #[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    /// Get user's recent tweets
246    #[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        // First, get the user to get their ID if we have a username
252        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}