1use oauth_provider_rs::storage::create_dynamodb_storage;
60use oauth_provider_rs::{CognitoOAuthConfig, OAuthProvider};
61use remote_mcp_kernel::{
62 config::Config, error::AppResult, handlers::SseHandlerConfig, microkernel::MicrokernelServer,
63};
64use rmcp::{
65 Error as McpError, ServerHandler,
66 handler::server::router::tool::ToolRouter,
67 handler::server::tool::Parameters,
68 model::{CallToolResult, Content, Implementation, ServerCapabilities, ServerInfo},
69 tool, tool_handler, tool_router,
70};
71use schemars::JsonSchema;
72use serde::{Deserialize, Serialize};
73use std::env;
74use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
75
76#[derive(Debug, Clone)]
81pub struct CustomMcpServer {
82 tool_router: ToolRouter<Self>,
84 name: String,
86}
87
88#[derive(Debug, Deserialize, Serialize, JsonSchema)]
90pub struct ListFilesRequest {
91 #[schemars(description = "Directory path to list files from")]
92 pub path: String,
93 #[schemars(description = "Include hidden files")]
94 pub include_hidden: Option<bool>,
95}
96
97#[derive(Debug, Deserialize, Serialize, JsonSchema)]
98pub struct ReadFileRequest {
99 #[schemars(description = "File path to read")]
100 pub path: String,
101 #[schemars(description = "Maximum number of lines to read")]
102 pub max_lines: Option<usize>,
103}
104
105#[derive(Debug, Deserialize, Serialize, JsonSchema)]
106pub struct WriteFileRequest {
107 #[schemars(description = "File path to write to")]
108 pub path: String,
109 #[schemars(description = "Content to write")]
110 pub content: String,
111 #[schemars(description = "Append to file instead of overwriting")]
112 pub append: Option<bool>,
113}
114
115#[derive(Debug, Deserialize, Serialize, JsonSchema)]
116pub struct WordCountRequest {
117 #[schemars(description = "Text to count words in")]
118 pub text: String,
119}
120
121#[derive(Debug, Deserialize, Serialize, JsonSchema)]
122pub struct TextSearchRequest {
123 #[schemars(description = "Text to search in")]
124 pub text: String,
125 #[schemars(description = "Pattern to search for")]
126 pub pattern: String,
127 #[schemars(description = "Case sensitive search")]
128 pub case_sensitive: Option<bool>,
129}
130
131#[tool_router]
133impl CustomMcpServer {
134 pub fn new(name: String) -> Self {
136 Self {
137 tool_router: Self::tool_router(),
138 name,
139 }
140 }
141
142 #[tool(description = "List files and directories in the specified path")]
144 async fn list_files(
145 &self,
146 Parameters(req): Parameters<ListFilesRequest>,
147 ) -> Result<CallToolResult, McpError> {
148 let path = std::path::Path::new(&req.path);
149
150 if !path.exists() {
151 return Ok(CallToolResult::error(vec![Content::text(format!(
152 "Path does not exist: {}",
153 req.path
154 ))]));
155 }
156
157 if !path.is_dir() {
158 return Ok(CallToolResult::error(vec![Content::text(format!(
159 "Path is not a directory: {}",
160 req.path
161 ))]));
162 }
163
164 let mut files = Vec::new();
165 let include_hidden = req.include_hidden.unwrap_or(false);
166
167 match std::fs::read_dir(path) {
168 Ok(entries) => {
169 for entry in entries {
170 match entry {
171 Ok(entry) => {
172 let file_name = entry.file_name().to_string_lossy().to_string();
173 let is_hidden = file_name.starts_with('.');
174
175 if include_hidden || !is_hidden {
176 let file_type = if entry.path().is_dir() {
177 "directory"
178 } else {
179 "file"
180 };
181 files.push(format!("{} ({})", file_name, file_type));
182 }
183 }
184 Err(e) => {
185 return Ok(CallToolResult::error(vec![Content::text(format!(
186 "Error reading directory entry: {}",
187 e
188 ))]));
189 }
190 }
191 }
192 }
193 Err(e) => {
194 return Ok(CallToolResult::error(vec![Content::text(format!(
195 "Error reading directory: {}",
196 e
197 ))]));
198 }
199 }
200
201 files.sort();
202 let result = files.join("\n");
203 Ok(CallToolResult::success(vec![Content::text(result)]))
204 }
205
206 #[tool(description = "Read the contents of a file")]
208 async fn read_file(
209 &self,
210 Parameters(req): Parameters<ReadFileRequest>,
211 ) -> Result<CallToolResult, McpError> {
212 let path = std::path::Path::new(&req.path);
213
214 if !path.exists() {
215 return Ok(CallToolResult::error(vec![Content::text(format!(
216 "File does not exist: {}",
217 req.path
218 ))]));
219 }
220
221 if !path.is_file() {
222 return Ok(CallToolResult::error(vec![Content::text(format!(
223 "Path is not a file: {}",
224 req.path
225 ))]));
226 }
227
228 match std::fs::read_to_string(path) {
229 Ok(content) => {
230 let result = if let Some(max_lines) = req.max_lines {
231 content
232 .lines()
233 .take(max_lines)
234 .collect::<Vec<_>>()
235 .join("\n")
236 } else {
237 content
238 };
239 Ok(CallToolResult::success(vec![Content::text(result)]))
240 }
241 Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
242 "Error reading file: {}",
243 e
244 ))])),
245 }
246 }
247
248 #[tool(description = "Write content to a file")]
250 async fn write_file(
251 &self,
252 Parameters(req): Parameters<WriteFileRequest>,
253 ) -> Result<CallToolResult, McpError> {
254 let path = std::path::Path::new(&req.path);
255
256 if let Some(parent) = path.parent() {
258 if !parent.exists() {
259 if let Err(e) = std::fs::create_dir_all(parent) {
260 return Ok(CallToolResult::error(vec![Content::text(format!(
261 "Error creating parent directories: {}",
262 e
263 ))]));
264 }
265 }
266 }
267
268 let result = if req.append.unwrap_or(false) {
269 std::fs::write(path, &req.content)
270 } else {
271 std::fs::write(path, &req.content)
272 };
273
274 match result {
275 Ok(()) => Ok(CallToolResult::success(vec![Content::text(format!(
276 "Successfully wrote {} bytes to {}",
277 req.content.len(),
278 req.path
279 ))])),
280 Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
281 "Error writing file: {}",
282 e
283 ))])),
284 }
285 }
286
287 #[tool(description = "Get system information including CPU, memory, and disk usage")]
289 async fn get_system_info(&self) -> Result<CallToolResult, McpError> {
290 let mut info = Vec::new();
291
292 let now = std::time::SystemTime::now()
294 .duration_since(std::time::UNIX_EPOCH)
295 .unwrap()
296 .as_secs();
297 info.push(format!("Timestamp: {}", now));
298
299 if let Ok(cwd) = std::env::current_dir() {
301 info.push(format!("Working Directory: {}", cwd.display()));
302 }
303
304 let env_count = std::env::vars().count();
306 info.push(format!("Environment Variables: {}", env_count));
307
308 info.push(format!("OS: {}", std::env::consts::OS));
310 info.push(format!("Architecture: {}", std::env::consts::ARCH));
311
312 let result = info.join("\n");
313 Ok(CallToolResult::success(vec![Content::text(result)]))
314 }
315
316 #[tool(description = "Count words, lines, and characters in text")]
318 async fn count_words(
319 &self,
320 Parameters(req): Parameters<WordCountRequest>,
321 ) -> Result<CallToolResult, McpError> {
322 let text = &req.text;
323 let lines = text.lines().count();
324 let words = text.split_whitespace().count();
325 let chars = text.chars().count();
326 let bytes = text.len();
327
328 let result = format!(
329 "Lines: {}\nWords: {}\nCharacters: {}\nBytes: {}",
330 lines, words, chars, bytes
331 );
332
333 Ok(CallToolResult::success(vec![Content::text(result)]))
334 }
335
336 #[tool(description = "Search for patterns in text")]
338 async fn search_text(
339 &self,
340 Parameters(req): Parameters<TextSearchRequest>,
341 ) -> Result<CallToolResult, McpError> {
342 let text = &req.text;
343 let pattern = &req.pattern;
344 let case_sensitive = req.case_sensitive.unwrap_or(false);
345
346 let (search_text, search_pattern) = if case_sensitive {
347 (text.to_string(), pattern.to_string())
348 } else {
349 (text.to_lowercase(), pattern.to_lowercase())
350 };
351
352 let mut matches = Vec::new();
353 for (line_num, line) in search_text.lines().enumerate() {
354 if line.contains(&search_pattern) {
355 matches.push(format!(
356 "Line {}: {}",
357 line_num + 1,
358 text.lines().nth(line_num).unwrap_or("")
359 ));
360 }
361 }
362
363 let result = if matches.is_empty() {
364 format!("No matches found for pattern: {}", pattern)
365 } else {
366 format!("Found {} matches:\n{}", matches.len(), matches.join("\n"))
367 };
368
369 Ok(CallToolResult::success(vec![Content::text(result)]))
370 }
371
372 #[tool(description = "Get current date and time in various formats")]
374 async fn get_datetime(&self) -> Result<CallToolResult, McpError> {
375 let now = std::time::SystemTime::now();
376 let unix_timestamp = now.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs();
377
378 let result = format!(
379 "Unix Timestamp: {}\nISO 8601 (approx): {}",
380 unix_timestamp,
381 chrono::DateTime::from_timestamp(unix_timestamp as i64, 0)
383 .unwrap_or_default()
384 .format("%Y-%m-%dT%H:%M:%SZ")
385 );
386
387 Ok(CallToolResult::success(vec![Content::text(result)]))
388 }
389}
390
391#[tool_handler]
393impl ServerHandler for CustomMcpServer {
394 fn get_info(&self) -> ServerInfo {
395 ServerInfo {
396 protocol_version: Default::default(),
397 capabilities: ServerCapabilities::builder()
398 .enable_tools()
399 .build(),
400 server_info: Implementation {
401 name: self.name.clone(),
402 version: "1.0.0".to_string(),
403 },
404 instructions: Some("A custom MCP server with file operations, system utilities, and text processing tools".to_string()),
405 }
406 }
407}
408
409#[tokio::main]
413async fn main() -> AppResult<()> {
414 dotenv::dotenv().ok();
416
417 let config = Config::from_env()?;
419
420 init_tracing(&config)?;
422
423 tracing::info!("Starting Custom MCP Server example with Cognito and DynamoDB storage...");
424
425 let cognito_config = CognitoOAuthConfig {
427 client_id: config.cognito.client_id.clone(),
428 client_secret: config.cognito.client_secret.clone().unwrap_or_default(),
429 redirect_uri: format!(
430 "http://{}:{}/oauth/callback",
431 config.server.host, config.server.port
432 ),
433 scope: config.cognito.scope.clone(),
434 provider_name: "cognito".to_string(),
435 };
436
437 let table_name =
439 env::var("DYNAMODB_TABLE_NAME").unwrap_or_else(|_| "oauth-storage".to_string());
440 let create_table = env::var("DYNAMODB_CREATE_TABLE")
441 .unwrap_or_else(|_| "true".to_string())
442 .parse::<bool>()
443 .unwrap_or(true);
444
445 log_startup_info(&config, &table_name, create_table);
447
448 let (storage, client_manager) = create_dynamodb_storage(
450 table_name.clone(),
451 create_table,
452 Some("expires_at".to_string()),
453 )
454 .await
455 .map_err(|e| {
456 remote_mcp_kernel::error::AppError::Internal(format!(
457 "Failed to create DynamoDB storage: {}",
458 e
459 ))
460 })?;
461
462 let oauth_handler = oauth_provider_rs::CognitoOAuthHandler::new_simple(
464 storage,
465 client_manager,
466 cognito_config,
467 config.cognito.cognito_domain.clone(),
468 config.cognito.region.clone(),
469 config.cognito.user_pool_id.clone(),
470 );
471
472 let oauth_provider = OAuthProvider::new(oauth_handler);
473
474 let custom_mcp_server = CustomMcpServer::new("Custom File & System MCP Server".to_string());
476
477 let microkernel = MicrokernelServer::new()
479 .with_oauth_provider(oauth_provider)
480 .with_mcp_streamable_handler(custom_mcp_server.clone())
481 .with_mcp_sse_handler(custom_mcp_server, SseHandlerConfig::default());
482
483 let bind_address = config.bind_socket_addr()?;
485 tracing::info!("🚀 Starting microkernel server on {}", bind_address);
486 microkernel.serve(bind_address).await?;
487
488 Ok(())
489}
490
491fn init_tracing(config: &Config) -> AppResult<()> {
492 tracing_subscriber::registry()
493 .with(
494 tracing_subscriber::EnvFilter::try_from_default_env()
495 .unwrap_or_else(|_| config.logging.level.as_str().into()),
496 )
497 .with(tracing_subscriber::fmt::layer())
498 .init();
499
500 Ok(())
501}
502
503fn log_startup_info(config: &Config, table_name: &str, create_table: bool) {
504 println!("🚀 Starting Custom MCP Server example with Cognito and DynamoDB storage...");
505 println!("📋 Configuration:");
506 println!(" - Architecture: Microkernel (independent handlers)");
507 println!(" - MCP Server: Custom implementation with specialized tools");
508 println!(" - OAuth Provider: AWS Cognito");
509 println!(" - Storage Backend: DynamoDB");
510 println!(" - Server: {}:{}", config.server.host, config.server.port);
511 println!(" - Version: {}", config.server.version);
512 println!();
513
514 println!("🔧 Custom MCP Server Tools:");
515 println!(" - list_files: List files and directories");
516 println!(" - read_file: Read file contents");
517 println!(" - write_file: Write content to files");
518 println!(" - get_system_info: Get system information");
519 println!(" - count_words: Count words, lines, and characters");
520 println!(" - search_text: Search for patterns in text");
521 println!(" - get_datetime: Get current date and time");
522 println!();
523
524 println!("🔐 AWS Cognito Configuration:");
525 println!(
526 " - Client ID: {}",
527 if config.cognito.client_id.is_empty() {
528 "Not configured"
529 } else {
530 "Configured"
531 }
532 );
533 println!(
534 " - Client Secret: {}",
535 match &config.cognito.client_secret {
536 Some(secret) if !secret.is_empty() => "Configured",
537 _ => "Not configured (Public Client)",
538 }
539 );
540 println!(
541 " - Domain: {}",
542 if config.cognito.cognito_domain.is_empty() {
543 "Not configured"
544 } else {
545 &config.cognito.cognito_domain
546 }
547 );
548 println!(
549 " - Region: {}",
550 if config.cognito.region.is_empty() {
551 "Not configured"
552 } else {
553 &config.cognito.region
554 }
555 );
556 println!(
557 " - User Pool ID: {}",
558 if config.cognito.user_pool_id.is_empty() {
559 "Not configured"
560 } else {
561 &config.cognito.user_pool_id
562 }
563 );
564 println!(" - Scopes: {}", config.cognito.scope);
565 println!();
566
567 println!("🗄️ DynamoDB Storage Configuration:");
568 println!(" - Table Name: {}", table_name);
569 println!(" - Auto-create Table: {}", create_table);
570 println!(" - TTL Attribute: expires_at");
571 println!();
572
573 println!("🔧 Handlers:");
574 println!(" - OAuth Provider (Cognito authentication & authorization)");
575 println!(" - Streamable HTTP Handler (MCP over HTTP with custom server)");
576 println!(" - SSE Handler (MCP over SSE with custom server)");
577 println!();
578
579 println!("🏗️ Microkernel Architecture:");
580 println!(" - Custom MCP server with specialized tools");
581 println!(" - Independent handlers that can operate standalone");
582 println!(" - Runtime composition of services");
583 println!(" - Single responsibility per handler");
584 println!(" - Easy testing and maintenance");
585 println!();
586
587 println!("🌐 MCP Protocol Endpoints:");
588 println!(
589 " - HTTP (streamable): http://{}:{}/mcp/http",
590 config.server.host, config.server.port
591 );
592 println!(
593 " - SSE: http://{}:{}/mcp/sse",
594 config.server.host, config.server.port
595 );
596 println!(
597 " - SSE Messages: http://{}:{}/mcp/message",
598 config.server.host, config.server.port
599 );
600 println!();
601
602 println!("🔐 OAuth 2.0 Endpoints:");
603 println!(
604 " - Authorization: https://{}/oauth2/authorize",
605 config.cognito.cognito_domain
606 );
607 println!(
608 " - Token: https://{}/oauth2/token",
609 config.cognito.cognito_domain
610 );
611 println!(
612 " - JWKS: https://{}/oauth2/jwks",
613 config.cognito.cognito_domain
614 );
615 println!(
616 " - UserInfo: https://{}/oauth2/userInfo",
617 config.cognito.cognito_domain
618 );
619 println!();
620}