use rustmcp::{RustMCP, FunctionTool, FunctionResource, FunctionPrompt, create_app, ToolAnnotations};
use rustmcp::{ToolDuplicateBehavior, ResourceDuplicateBehavior, PromptDuplicateBehavior};
use serde_json::Value;
use std::collections::HashMap;
#[tokio::main]
async fn main() {
env_logger::init();
let mut rustmcp = RustMCP::with_behavior(
ToolDuplicateBehavior::Warn,
ResourceDuplicateBehavior::Warn,
PromptDuplicateBehavior::Warn,
);
let echo_tool = FunctionTool::from_function(
|_args: Option<HashMap<String, Value>>| -> Result<Value, String> {
let message = _args
.as_ref()
.and_then(|m| m.get("message"))
.and_then(|v| v.as_str())
.filter(|s| !s.trim().is_empty())
.unwrap_or("Hello, World!");
Ok(Value::String(message.to_string()))
},
Some("echo".to_string()),
Some("Echo Tool".to_string()), Some("Echoes back the provided message".to_string()),
Some(serde_json::json!({
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "The message to echo"
}
},
"required": ["message"]
})),
None, None, Some(vec!["utility".to_string()]),
None,
);
rustmcp.add_tool(echo_tool);
let shell_tool = FunctionTool::from_function(
|_args: Option<HashMap<String, Value>>| -> Result<Value, String> {
let command = _args
.as_ref()
.and_then(|m| m.get("command"))
.and_then(|v| v.as_str())
.ok_or("Missing 'command' argument")?;
let allowed_commands = ["ls", "pwd", "date", "echo", "cat", "which"];
let cmd_parts: Vec<&str> = command.split_whitespace().collect();
if cmd_parts.is_empty() {
return Err("Empty command not allowed".to_string());
}
if !allowed_commands.contains(&cmd_parts[0]) {
return Err(format!("Command '{}' not allowed", cmd_parts[0]));
}
if cmd_parts.iter().any(|&arg| arg.contains(|c| "!\"#$&'()*+,;<=>?@[\\]^`{|}~".contains(c))) {
return Err("Invalid characters in command arguments".to_string());
}
match std::process::Command::new("sh")
.arg("-c")
.arg(command)
.output()
{
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let result = format!("stdout:\n{}\nstderr:\n{}\nexit_code: {}",
stdout, stderr, output.status.code().unwrap_or(-1));
Ok(Value::String(result))
}
Err(e) => Err(format!("Failed to execute command: {}", e)),
}
},
Some("shell".to_string()),
Some("Shell Tool".to_string()), Some("Execute safe shell commands".to_string()),
Some(serde_json::json!({
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The shell command to execute (limited to safe commands)"
}
},
"required": ["command"]
})),
None, Some(ToolAnnotations {
title: Some("Shell Tool".to_string()),
read_only_hint: Some(false),
destructive_hint: Some(false),
idempotent_hint: Some(false),
open_world_hint: Some(true),
}), Some(vec!["system".to_string()]),
None,
);
rustmcp.add_tool(shell_tool);
let hello_resource = FunctionResource::from_function(
|| -> Result<Value, String> {
Ok(Value::String("Hello from resource!".to_string()))
},
"resource://hello".to_string(),
Some("hello".to_string()),
Some("A simple hello resource".to_string()),
Some("text/plain".to_string()),
Some(vec!["example".to_string()]),
None,
None,
);
rustmcp.add_resource(hello_resource);
let greeting_prompt = FunctionPrompt::from_function(
|_args: Option<HashMap<String, Value>>| -> Result<Vec<rustmcp::PromptMessage>, String> {
let name = _args
.as_ref()
.and_then(|m| m.get("name"))
.and_then(|v| v.as_str())
.unwrap_or("World");
Ok(vec![rustmcp::PromptMessage {
role: "user".to_string(),
content: format!("Hello, {}!", name),
name: None,
}])
},
"greeting".to_string(),
Some("A simple greeting prompt".to_string()),
Some(vec!["example".to_string()]),
None,
None,
);
rustmcp.add_prompt(greeting_prompt);
let app = create_app(rustmcp);
println!("Starting RustMCP server on port 3001...");
println!("HTTP endpoints available at http://localhost:3001");
println!("WebSocket endpoint available at ws://localhost:3001/mcp/ws");
println!("MCP JSON-RPC endpoint available at http://localhost:3001/mcp");
let listener = tokio::net::TcpListener::bind("0.0.0.0:3001").await.unwrap();
axum::serve(listener, app).await.unwrap();
}