custom_mcp_server_example/
custom_mcp_server_example.rs

1//! Custom MCP Server Example with OAuth 2.0 and DynamoDB
2//!
3//! This example demonstrates how to create a custom MCP server implementation
4//! and use it with the microkernel architecture. It shows how users can extend
5//! the framework by implementing their own MCP servers with custom tools and
6//! capabilities while leveraging the existing OAuth and storage infrastructure.
7//!
8//! # Features
9//! - Custom MCP server implementation with specialized tools
10//! - AWS Cognito OAuth 2.0 authentication
11//! - DynamoDB persistent storage for OAuth tokens and client data
12//! - MCP over HTTP (streamable)
13//! - MCP over Server-Sent Events (SSE)
14//! - Microkernel architecture with independent, composable handlers
15//!
16//! # Custom MCP Server Features
17//! The custom server implements:
18//! - File system operations (list, read, write files)
19//! - System information tools (CPU, memory, disk usage)
20//! - Text processing utilities (word count, text search)
21//! - Time and date utilities
22//!
23//! # Required Environment Variables
24//! ## Cognito Configuration
25//! - `COGNITO_CLIENT_ID`: Your Cognito app client ID
26//! - `COGNITO_CLIENT_SECRET`: Your Cognito app client secret (optional for public clients)
27//! - `COGNITO_DOMAIN`: Your Cognito domain (e.g., mydomain.auth.us-east-1.amazoncognito.com)
28//! - `AWS_REGION`: AWS region (e.g., us-east-1)
29//! - `COGNITO_USER_POOL_ID`: Your Cognito user pool ID (e.g., us-east-1_XXXXXXXXX)
30//! - `COGNITO_SCOPE`: OAuth scopes (default: 'openid email profile phone')
31//!
32//! ## AWS Configuration (for DynamoDB)
33//! - `AWS_ACCESS_KEY_ID`: Your AWS access key ID
34//! - `AWS_SECRET_ACCESS_KEY`: Your AWS secret access key
35//! - `AWS_REGION`: AWS region (should match AWS_REGION)
36//!
37//! ## Server Configuration
38//! - `MCP_HOST`: Server host (default: localhost)
39//! - `MCP_PORT`: Server port (default: 8080)
40//! - `DYNAMODB_TABLE_NAME`: DynamoDB table name (default: oauth-storage)
41//! - `DYNAMODB_CREATE_TABLE`: Whether to auto-create table (default: true)
42//!
43//! # Usage
44//! ```bash
45//! # Set environment variables
46//! export COGNITO_CLIENT_ID="your_client_id"
47//! export COGNITO_CLIENT_SECRET="your_client_secret"
48//! export COGNITO_DOMAIN="mydomain.auth.us-east-1.amazoncognito.com"
49//! export AWS_REGION="us-east-1"
50//! export COGNITO_USER_POOL_ID="us-east-1_XXXXXXXXX"
51//! export AWS_ACCESS_KEY_ID="your_aws_access_key"
52//! export AWS_SECRET_ACCESS_KEY="your_aws_secret_key"
53//! export AWS_REGION="us-east-1"
54//!
55//! # Run the server
56//! cargo run --example custom_mcp_server_example
57//! ```
58
59use oauth_provider_rs::OAuthProvider;
60use oauth_provider_rs::storage::create_dynamodb_storage;
61use remote_mcp_kernel::{
62    config::{
63        get_bind_socket_addr, get_cognito_client_id, get_cognito_client_secret, get_cognito_domain,
64        get_cognito_oauth_provider_config, get_cognito_region, get_cognito_scope,
65        get_cognito_user_pool_id, get_logging_level, get_server_host, get_server_port,
66        get_server_version,
67    },
68    error::AppResult,
69    handlers::SseHandlerConfig,
70    microkernel::MicrokernelServer,
71};
72use rmcp::{
73    Error as McpError, ServerHandler,
74    handler::server::router::tool::ToolRouter,
75    handler::server::tool::Parameters,
76    model::{CallToolResult, Content, Implementation, ServerCapabilities, ServerInfo},
77    tool, tool_handler, tool_router,
78};
79use schemars::JsonSchema;
80use serde::{Deserialize, Serialize};
81use std::env;
82use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
83
84// Custom MCP Server Implementation
85// =============================================================================
86
87/// Custom MCP server with specialized tools for file operations and system utilities
88#[derive(Debug, Clone)]
89pub struct CustomMcpServer {
90    /// MCP tool router for handling tool calls
91    tool_router: ToolRouter<Self>,
92    /// Server name for identification
93    name: String,
94}
95
96// Tool parameter definitions
97#[derive(Debug, Deserialize, Serialize, JsonSchema)]
98pub struct ListFilesRequest {
99    #[schemars(description = "Directory path to list files from")]
100    pub path: String,
101    #[schemars(description = "Include hidden files")]
102    pub include_hidden: Option<bool>,
103}
104
105#[derive(Debug, Deserialize, Serialize, JsonSchema)]
106pub struct ReadFileRequest {
107    #[schemars(description = "File path to read")]
108    pub path: String,
109    #[schemars(description = "Maximum number of lines to read")]
110    pub max_lines: Option<usize>,
111}
112
113#[derive(Debug, Deserialize, Serialize, JsonSchema)]
114pub struct WriteFileRequest {
115    #[schemars(description = "File path to write to")]
116    pub path: String,
117    #[schemars(description = "Content to write")]
118    pub content: String,
119    #[schemars(description = "Append to file instead of overwriting")]
120    pub append: Option<bool>,
121}
122
123#[derive(Debug, Deserialize, Serialize, JsonSchema)]
124pub struct WordCountRequest {
125    #[schemars(description = "Text to count words in")]
126    pub text: String,
127}
128
129#[derive(Debug, Deserialize, Serialize, JsonSchema)]
130pub struct TextSearchRequest {
131    #[schemars(description = "Text to search in")]
132    pub text: String,
133    #[schemars(description = "Pattern to search for")]
134    pub pattern: String,
135    #[schemars(description = "Case sensitive search")]
136    pub case_sensitive: Option<bool>,
137}
138
139// Tool implementations using the tool_router macro
140#[tool_router]
141impl CustomMcpServer {
142    /// Create a new custom MCP server
143    pub fn new(name: String) -> Self {
144        Self {
145            tool_router: Self::tool_router(),
146            name,
147        }
148    }
149
150    /// List files in a directory
151    #[tool(description = "List files and directories in the specified path")]
152    async fn list_files(
153        &self,
154        Parameters(req): Parameters<ListFilesRequest>,
155    ) -> Result<CallToolResult, McpError> {
156        let path = std::path::Path::new(&req.path);
157
158        if !path.exists() {
159            return Ok(CallToolResult::error(vec![Content::text(format!(
160                "Path does not exist: {}",
161                req.path
162            ))]));
163        }
164
165        if !path.is_dir() {
166            return Ok(CallToolResult::error(vec![Content::text(format!(
167                "Path is not a directory: {}",
168                req.path
169            ))]));
170        }
171
172        let mut files = Vec::new();
173        let include_hidden = req.include_hidden.unwrap_or(false);
174
175        match std::fs::read_dir(path) {
176            Ok(entries) => {
177                for entry in entries {
178                    match entry {
179                        Ok(entry) => {
180                            let file_name = entry.file_name().to_string_lossy().to_string();
181                            let is_hidden = file_name.starts_with('.');
182
183                            if include_hidden || !is_hidden {
184                                let file_type = if entry.path().is_dir() {
185                                    "directory"
186                                } else {
187                                    "file"
188                                };
189                                files.push(format!("{} ({})", file_name, file_type));
190                            }
191                        }
192                        Err(e) => {
193                            return Ok(CallToolResult::error(vec![Content::text(format!(
194                                "Error reading directory entry: {}",
195                                e
196                            ))]));
197                        }
198                    }
199                }
200            }
201            Err(e) => {
202                return Ok(CallToolResult::error(vec![Content::text(format!(
203                    "Error reading directory: {}",
204                    e
205                ))]));
206            }
207        }
208
209        files.sort();
210        let result = files.join("\n");
211        Ok(CallToolResult::success(vec![Content::text(result)]))
212    }
213
214    /// Read contents of a file
215    #[tool(description = "Read the contents of a file")]
216    async fn read_file(
217        &self,
218        Parameters(req): Parameters<ReadFileRequest>,
219    ) -> Result<CallToolResult, McpError> {
220        let path = std::path::Path::new(&req.path);
221
222        if !path.exists() {
223            return Ok(CallToolResult::error(vec![Content::text(format!(
224                "File does not exist: {}",
225                req.path
226            ))]));
227        }
228
229        if !path.is_file() {
230            return Ok(CallToolResult::error(vec![Content::text(format!(
231                "Path is not a file: {}",
232                req.path
233            ))]));
234        }
235
236        match std::fs::read_to_string(path) {
237            Ok(content) => {
238                let result = if let Some(max_lines) = req.max_lines {
239                    content
240                        .lines()
241                        .take(max_lines)
242                        .collect::<Vec<_>>()
243                        .join("\n")
244                } else {
245                    content
246                };
247                Ok(CallToolResult::success(vec![Content::text(result)]))
248            }
249            Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
250                "Error reading file: {}",
251                e
252            ))])),
253        }
254    }
255
256    /// Write content to a file
257    #[tool(description = "Write content to a file")]
258    async fn write_file(
259        &self,
260        Parameters(req): Parameters<WriteFileRequest>,
261    ) -> Result<CallToolResult, McpError> {
262        let path = std::path::Path::new(&req.path);
263
264        // Create parent directories if they don't exist
265        if let Some(parent) = path.parent() {
266            if !parent.exists() {
267                if let Err(e) = std::fs::create_dir_all(parent) {
268                    return Ok(CallToolResult::error(vec![Content::text(format!(
269                        "Error creating parent directories: {}",
270                        e
271                    ))]));
272                }
273            }
274        }
275
276        let result = if req.append.unwrap_or(false) {
277            std::fs::write(path, &req.content)
278        } else {
279            std::fs::write(path, &req.content)
280        };
281
282        match result {
283            Ok(()) => Ok(CallToolResult::success(vec![Content::text(format!(
284                "Successfully wrote {} bytes to {}",
285                req.content.len(),
286                req.path
287            ))])),
288            Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
289                "Error writing file: {}",
290                e
291            ))])),
292        }
293    }
294
295    /// Get system information
296    #[tool(description = "Get system information including CPU, memory, and disk usage")]
297    async fn get_system_info(&self) -> Result<CallToolResult, McpError> {
298        let mut info = Vec::new();
299
300        // Get current timestamp
301        let now = std::time::SystemTime::now()
302            .duration_since(std::time::UNIX_EPOCH)
303            .unwrap()
304            .as_secs();
305        info.push(format!("Timestamp: {}", now));
306
307        // Get current working directory
308        if let Ok(cwd) = std::env::current_dir() {
309            info.push(format!("Working Directory: {}", cwd.display()));
310        }
311
312        // Get environment variables count
313        let env_count = std::env::vars().count();
314        info.push(format!("Environment Variables: {}", env_count));
315
316        // Get OS information
317        info.push(format!("OS: {}", std::env::consts::OS));
318        info.push(format!("Architecture: {}", std::env::consts::ARCH));
319
320        let result = info.join("\n");
321        Ok(CallToolResult::success(vec![Content::text(result)]))
322    }
323
324    /// Count words in text
325    #[tool(description = "Count words, lines, and characters in text")]
326    async fn count_words(
327        &self,
328        Parameters(req): Parameters<WordCountRequest>,
329    ) -> Result<CallToolResult, McpError> {
330        let text = &req.text;
331        let lines = text.lines().count();
332        let words = text.split_whitespace().count();
333        let chars = text.chars().count();
334        let bytes = text.len();
335
336        let result = format!(
337            "Lines: {}\nWords: {}\nCharacters: {}\nBytes: {}",
338            lines, words, chars, bytes
339        );
340
341        Ok(CallToolResult::success(vec![Content::text(result)]))
342    }
343
344    /// Search for text patterns
345    #[tool(description = "Search for patterns in text")]
346    async fn search_text(
347        &self,
348        Parameters(req): Parameters<TextSearchRequest>,
349    ) -> Result<CallToolResult, McpError> {
350        let text = &req.text;
351        let pattern = &req.pattern;
352        let case_sensitive = req.case_sensitive.unwrap_or(false);
353
354        let (search_text, search_pattern) = if case_sensitive {
355            (text.to_string(), pattern.to_string())
356        } else {
357            (text.to_lowercase(), pattern.to_lowercase())
358        };
359
360        let mut matches = Vec::new();
361        for (line_num, line) in search_text.lines().enumerate() {
362            if line.contains(&search_pattern) {
363                matches.push(format!(
364                    "Line {}: {}",
365                    line_num + 1,
366                    text.lines().nth(line_num).unwrap_or("")
367                ));
368            }
369        }
370
371        let result = if matches.is_empty() {
372            format!("No matches found for pattern: {}", pattern)
373        } else {
374            format!("Found {} matches:\n{}", matches.len(), matches.join("\n"))
375        };
376
377        Ok(CallToolResult::success(vec![Content::text(result)]))
378    }
379
380    /// Get current date and time
381    #[tool(description = "Get current date and time in various formats")]
382    async fn get_datetime(&self) -> Result<CallToolResult, McpError> {
383        let now = std::time::SystemTime::now();
384        let unix_timestamp = now.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
385
386        let result = format!(
387            "Unix Timestamp: {}\nISO 8601 (approx): {}",
388            unix_timestamp,
389            // Simple ISO 8601 approximation (not perfect but sufficient for demo)
390            chrono::DateTime::from_timestamp(unix_timestamp as i64, 0)
391                .unwrap_or_default()
392                .format("%Y-%m-%dT%H:%M:%SZ")
393        );
394
395        Ok(CallToolResult::success(vec![Content::text(result)]))
396    }
397}
398
399// Implement the ServerHandler trait for MCP protocol support
400#[tool_handler]
401impl ServerHandler for CustomMcpServer {
402    fn get_info(&self) -> ServerInfo {
403        ServerInfo {
404            protocol_version: Default::default(),
405            capabilities: ServerCapabilities::builder()
406                .enable_tools()
407                .build(),
408            server_info: Implementation {
409                name: self.name.clone(),
410                version: "1.0.0".to_string(),
411            },
412            instructions: Some("A custom MCP server with file operations, system utilities, and text processing tools".to_string()),
413        }
414    }
415}
416
417// Main application
418// =============================================================================
419
420#[tokio::main]
421async fn main() -> AppResult<()> {
422    // Load environment variables
423    dotenv::dotenv().ok();
424
425    // Initialize tracing
426    init_tracing()?;
427
428    tracing::info!("Starting Custom MCP Server example with Cognito and DynamoDB storage...");
429
430    // Create Cognito OAuth configuration
431    let cognito_config = get_cognito_oauth_provider_config()?;
432
433    // Get DynamoDB configuration
434    let table_name =
435        env::var("DYNAMODB_TABLE_NAME").unwrap_or_else(|_| "oauth-storage".to_string());
436    let create_table = env::var("DYNAMODB_CREATE_TABLE")
437        .unwrap_or_else(|_| "true".to_string())
438        .parse::<bool>()
439        .unwrap_or(true);
440
441    // Log configuration
442    log_startup_info(&table_name, create_table);
443
444    // Create DynamoDB storage
445    let (storage, client_manager) = create_dynamodb_storage(
446        table_name.clone(),
447        create_table,
448        Some("expires_at".to_string()),
449    )
450    .await
451    .map_err(|e| {
452        remote_mcp_kernel::error::AppError::Internal(format!(
453            "Failed to create DynamoDB storage: {}",
454            e
455        ))
456    })?;
457
458    // Create Cognito OAuth provider with DynamoDB storage
459    let oauth_handler = oauth_provider_rs::CognitoOAuthHandler::new_simple(
460        storage,
461        client_manager,
462        cognito_config,
463        get_cognito_domain()?,
464        get_cognito_region()?,
465        get_cognito_user_pool_id()?,
466    );
467
468    let oauth_provider = OAuthProvider::new(
469        oauth_handler,
470        oauth_provider_rs::http_integration::config::OAuthProviderConfig::default(),
471    );
472
473    // Create custom MCP server
474    let custom_mcp_server = CustomMcpServer::new("Custom File & System MCP Server".to_string());
475
476    // Build microkernel with custom MCP server using convenience methods
477    let microkernel = MicrokernelServer::new()
478        .with_oauth_provider(oauth_provider)
479        .with_mcp_streamable_handler(custom_mcp_server.clone())
480        .with_mcp_sse_handler(custom_mcp_server, SseHandlerConfig::default());
481
482    // Start the microkernel server
483    let bind_address = get_bind_socket_addr()?;
484    tracing::info!("🚀 Starting microkernel server on {}", bind_address);
485    microkernel.serve(bind_address).await?;
486
487    Ok(())
488}
489
490fn init_tracing() -> AppResult<()> {
491    tracing_subscriber::registry()
492        .with(
493            tracing_subscriber::EnvFilter::try_from_default_env()
494                .unwrap_or_else(|_| get_logging_level().as_str().into()),
495        )
496        .with(tracing_subscriber::fmt::layer())
497        .init();
498
499    Ok(())
500}
501
502fn log_startup_info(table_name: &str, create_table: bool) {
503    println!("🚀 Starting Custom MCP Server example with Cognito and DynamoDB storage...");
504    println!("📋 Configuration:");
505    println!("  - Architecture: Microkernel (independent handlers)");
506    println!("  - MCP Server: Custom implementation with specialized tools");
507    println!("  - OAuth Provider: AWS Cognito");
508    println!("  - Storage Backend: DynamoDB");
509    println!(
510        "  - Server: {}:{}",
511        get_server_host(),
512        get_server_port().unwrap_or(8080)
513    );
514    println!("  - Version: {}", get_server_version());
515    println!();
516
517    println!("🔧 Custom MCP Server Tools:");
518    println!("  - list_files: List files and directories");
519    println!("  - read_file: Read file contents");
520    println!("  - write_file: Write content to files");
521    println!("  - get_system_info: Get system information");
522    println!("  - count_words: Count words, lines, and characters");
523    println!("  - search_text: Search for patterns in text");
524    println!("  - get_datetime: Get current date and time");
525    println!();
526
527    println!("🔐 AWS Cognito Configuration:");
528    println!(
529        "  - Client ID: {}",
530        if get_cognito_client_id().is_ok() {
531            "Configured"
532        } else {
533            "Not configured"
534        }
535    );
536    println!(
537        "  - Client Secret: {}",
538        match get_cognito_client_secret() {
539            Some(secret) if !secret.is_empty() => "Configured",
540            _ => "Not configured (Public Client)",
541        }
542    );
543    println!(
544        "  - Domain: {}",
545        get_cognito_domain().unwrap_or_else(|_| "Not configured".to_string())
546    );
547    println!(
548        "  - Region: {}",
549        get_cognito_region().unwrap_or_else(|_| "Not configured".to_string())
550    );
551    println!(
552        "  - User Pool ID: {}",
553        get_cognito_user_pool_id().unwrap_or_else(|_| "Not configured".to_string())
554    );
555    println!("  - Scopes: {}", get_cognito_scope());
556    println!();
557
558    println!("🗄️  DynamoDB Storage Configuration:");
559    println!("  - Table Name: {}", table_name);
560    println!("  - Auto-create Table: {}", create_table);
561    println!("  - TTL Attribute: expires_at");
562    println!();
563
564    println!("🔧 Handlers:");
565    println!("  - OAuth Provider (Cognito authentication & authorization)");
566    println!("  - Streamable HTTP Handler (MCP over HTTP with custom server)");
567    println!("  - SSE Handler (MCP over SSE with custom server)");
568    println!();
569
570    println!("🏗️  Microkernel Architecture:");
571    println!("  - Custom MCP server with specialized tools");
572    println!("  - Independent handlers that can operate standalone");
573    println!("  - Runtime composition of services");
574    println!("  - Single responsibility per handler");
575    println!("  - Easy testing and maintenance");
576    println!();
577
578    println!("🌐 MCP Protocol Endpoints:");
579    let host = get_server_host();
580    let port = get_server_port().unwrap_or(8080);
581    println!("  - HTTP (streamable): http://{}:{}/mcp/http", host, port);
582    println!("  - SSE: http://{}:{}/mcp/sse", host, port);
583    println!("  - SSE Messages: http://{}:{}/mcp/message", host, port);
584    println!();
585
586    println!("🔐 OAuth 2.0 Endpoints:");
587    let cognito_domain = get_cognito_domain().unwrap_or_else(|_| "Not configured".to_string());
588    println!(
589        "  - Authorization: https://{}/oauth2/authorize",
590        cognito_domain
591    );
592    println!("  - Token: https://{}/oauth2/token", cognito_domain);
593    println!("  - JWKS: https://{}/oauth2/jwks", cognito_domain);
594    println!("  - UserInfo: https://{}/oauth2/userInfo", cognito_domain);
595    println!();
596}