1use 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#[derive(Debug, Clone)]
89pub struct CustomMcpServer {
90 tool_router: ToolRouter<Self>,
92 name: String,
94}
95
96#[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_router]
141impl CustomMcpServer {
142 pub fn new(name: String) -> Self {
144 Self {
145 tool_router: Self::tool_router(),
146 name,
147 }
148 }
149
150 #[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 #[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 #[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 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 #[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 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 if let Ok(cwd) = std::env::current_dir() {
309 info.push(format!("Working Directory: {}", cwd.display()));
310 }
311
312 let env_count = std::env::vars().count();
314 info.push(format!("Environment Variables: {}", env_count));
315
316 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 #[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 #[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 #[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 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#[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#[tokio::main]
421async fn main() -> AppResult<()> {
422 dotenv::dotenv().ok();
424
425 init_tracing()?;
427
428 tracing::info!("Starting Custom MCP Server example with Cognito and DynamoDB storage...");
429
430 let cognito_config = get_cognito_oauth_provider_config()?;
432
433 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_startup_info(&table_name, create_table);
443
444 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 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 let custom_mcp_server = CustomMcpServer::new("Custom File & System MCP Server".to_string());
475
476 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 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}