skills/emillindfors/mcp-tools-guide/SKILL.md
Master creating MCP tools with type-safe parameters, automatic schema generation, and best practices
npx skillsauth add aiskillstore/marketplace mcp-tool-creationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are an expert in creating MCP tools using the rmcp crate, with deep knowledge of the #[tool] macro system, parameter handling, and tool design patterns.
You guide developers on:
#[tool] macro usage and configurationTools are functions that AI assistants can invoke to perform actions or computations. They are the primary way MCP servers expose capabilities.
use rmcp::prelude::*;
#[tool(tool_box)]
struct MyService;
#[tool(tool_box)]
impl MyService {
#[tool(description = "Add two numbers together")]
async fn add(&self, a: i32, b: i32) -> i32 {
a + b
}
}
#[tool(tool_box)] on impl block
list_tools() methodcall_tool() dispatcher#[tool(description = "...")] on methods
Method signature requirements
async fn&selfSimple types work out of the box:
#[tool(tool_box)]
impl MyService {
#[tool(description = "Process numbers")]
async fn process(
&self,
count: i32,
name: String,
active: bool,
score: f64,
) -> String {
format!("{} {} {} {}", count, name, active, score)
}
}
Supported simple types:
i8, i16, i32, i64, u8, u16, u32, u64f32, f64String, &strboolUse Option<T> for optional parameters:
#[tool(tool_box)]
impl SearchService {
#[tool(description = "Search with optional filters")]
async fn search(
&self,
query: String,
limit: Option<u32>,
offset: Option<u32>,
sort_by: Option<String>,
) -> Vec<String> {
let limit = limit.unwrap_or(10);
let offset = offset.unwrap_or(0);
// Search logic
vec![]
}
}
For complex parameter objects, use #[tool(aggr)]:
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
struct SearchRequest {
query: String,
#[serde(default)]
limit: u32,
#[serde(default)]
offset: u32,
filters: Option<Vec<String>>,
}
#[tool(tool_box)]
impl SearchService {
#[tool(description = "Search with complex parameters")]
async fn search(&self, #[tool(aggr)] req: SearchRequest) -> Vec<String> {
// Use req.query, req.limit, etc.
vec![]
}
}
Requirements for complex parameters:
Deserialize, Serialize, JsonSchema#[tool(aggr)] attribute#[serde] attributesHandle arrays and vectors:
#[tool(tool_box)]
impl BatchService {
#[tool(description = "Process multiple items")]
async fn process_batch(&self, items: Vec<String>) -> Vec<String> {
items.into_iter()
.map(|s| s.to_uppercase())
.collect()
}
#[tool(description = "Sum array of numbers")]
async fn sum_array(&self, numbers: Vec<i32>) -> i32 {
numbers.iter().sum()
}
}
Use enums for constrained choices:
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
enum SortOrder {
Asc,
Desc,
}
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
enum OutputFormat {
Json,
Yaml,
Toml,
}
#[tool(tool_box)]
impl DataService {
#[tool(description = "Fetch data with format and sort options")]
async fn fetch_data(
&self,
format: OutputFormat,
sort: SortOrder,
) -> String {
format!("{:?} {:?}", format, sort)
}
}
Return simple values directly:
#[tool(tool_box)]
impl MyService {
#[tool(description = "Get count")]
async fn count(&self) -> i32 {
42
}
#[tool(description = "Get message")]
async fn message(&self) -> String {
"Hello".to_string()
}
}
Return structured data:
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct User {
id: u64,
name: String,
email: String,
active: bool,
}
#[tool(tool_box)]
impl UserService {
#[tool(description = "Get user by ID")]
async fn get_user(&self, id: u64) -> User {
User {
id,
name: "John Doe".to_string(),
email: "[email protected]".to_string(),
active: true,
}
}
#[tool(description = "List all users")]
async fn list_users(&self) -> Vec<User> {
vec![]
}
}
Handle errors with Result:
use thiserror::Error;
#[derive(Debug, Error)]
enum ServiceError {
#[error("Not found: {0}")]
NotFound(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Database error: {0}")]
DatabaseError(String),
}
#[tool(tool_box)]
impl DataService {
#[tool(description = "Fetch item by ID")]
async fn fetch(&self, id: String) -> Result<String, ServiceError> {
if id.is_empty() {
return Err(ServiceError::InvalidInput(
"ID cannot be empty".to_string()
));
}
// Fetch logic
Ok("Item data".to_string())
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct Item {
id: String,
name: String,
value: i32,
}
#[tool(tool_box)]
struct ItemService {
items: Arc<RwLock<HashMap<String, Item>>>,
}
#[tool(tool_box)]
impl ItemService {
#[tool(description = "Create a new item")]
async fn create(&self, name: String, value: i32) -> Result<Item, String> {
let id = uuid::Uuid::new_v4().to_string();
let item = Item { id: id.clone(), name, value };
let mut items = self.items.write().await;
items.insert(id.clone(), item.clone());
Ok(item)
}
#[tool(description = "Get item by ID")]
async fn get(&self, id: String) -> Result<Item, String> {
let items = self.items.read().await;
items.get(&id)
.cloned()
.ok_or_else(|| format!("Item {} not found", id))
}
#[tool(description = "Update an item")]
async fn update(
&self,
id: String,
name: Option<String>,
value: Option<i32>,
) -> Result<Item, String> {
let mut items = self.items.write().await;
let item = items.get_mut(&id)
.ok_or_else(|| format!("Item {} not found", id))?;
if let Some(name) = name {
item.name = name;
}
if let Some(value) = value {
item.value = value;
}
Ok(item.clone())
}
#[tool(description = "Delete an item")]
async fn delete(&self, id: String) -> Result<(), String> {
let mut items = self.items.write().await;
items.remove(&id)
.ok_or_else(|| format!("Item {} not found", id))?;
Ok(())
}
#[tool(description = "List all items")]
async fn list(&self) -> Vec<Item> {
let items = self.items.read().await;
items.values().cloned().collect()
}
}
#[tool(tool_box)]
impl ValidationService {
#[tool(description = "Validate email address")]
async fn validate_email(&self, email: String) -> Result<bool, String> {
if email.is_empty() {
return Err("Email cannot be empty".to_string());
}
if !email.contains('@') {
return Err("Email must contain @".to_string());
}
// More validation logic
Ok(true)
}
#[tool(description = "Sanitize user input")]
async fn sanitize(&self, input: String) -> String {
// Remove potentially dangerous characters
input.chars()
.filter(|c| c.is_alphanumeric() || c.is_whitespace())
.collect()
}
}
use reqwest::Client;
#[tool(tool_box)]
struct WeatherService {
client: Client,
api_key: String,
}
#[tool(tool_box)]
impl WeatherService {
#[tool(description = "Get weather for a city")]
async fn get_weather(&self, city: String) -> Result<String, String> {
let url = format!(
"https://api.weather.com/data?city={}&key={}",
city, self.api_key
);
let response = self.client.get(&url)
.send()
.await
.map_err(|e| format!("Request failed: {}", e))?;
let text = response.text()
.await
.map_err(|e| format!("Failed to read response: {}", e))?;
Ok(text)
}
}
#[tool(tool_box)]
struct SessionService {
sessions: Arc<RwLock<HashMap<String, Session>>>,
}
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
struct Session {
id: String,
user_id: String,
created_at: i64,
data: HashMap<String, String>,
}
#[tool(tool_box)]
impl SessionService {
#[tool(description = "Create a new session")]
async fn create_session(&self, user_id: String) -> String {
let session_id = uuid::Uuid::new_v4().to_string();
let session = Session {
id: session_id.clone(),
user_id,
created_at: chrono::Utc::now().timestamp(),
data: HashMap::new(),
};
let mut sessions = self.sessions.write().await;
sessions.insert(session_id.clone(), session);
session_id
}
#[tool(description = "Store data in session")]
async fn set_session_data(
&self,
session_id: String,
key: String,
value: String,
) -> Result<(), String> {
let mut sessions = self.sessions.write().await;
let session = sessions.get_mut(&session_id)
.ok_or_else(|| "Session not found".to_string())?;
session.data.insert(key, value);
Ok(())
}
#[tool(description = "Get data from session")]
async fn get_session_data(
&self,
session_id: String,
key: String,
) -> Result<String, String> {
let sessions = self.sessions.read().await;
let session = sessions.get(&session_id)
.ok_or_else(|| "Session not found".to_string())?;
session.data.get(&key)
.cloned()
.ok_or_else(|| format!("Key {} not found", key))
}
}
use thiserror::Error;
#[derive(Debug, Error)]
enum ToolError {
#[error("Invalid parameter: {field} - {message}")]
InvalidParameter { field: String, message: String },
#[error("Resource not found: {0}")]
NotFound(String),
#[error("Permission denied: {0}")]
PermissionDenied(String),
#[error("External service error: {0}")]
ExternalError(String),
#[error("Internal error: {0}")]
Internal(String),
}
#[tool(tool_box)]
impl MyService {
#[tool(description = "Process data")]
async fn process(&self, data: String) -> Result<String, ToolError> {
if data.is_empty() {
return Err(ToolError::InvalidParameter {
field: "data".to_string(),
message: "Data cannot be empty".to_string(),
});
}
if data.len() > 1000 {
return Err(ToolError::InvalidParameter {
field: "data".to_string(),
message: "Data exceeds maximum length of 1000 characters".to_string(),
});
}
// Process data
Ok(data.to_uppercase())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_add() {
let service = Calculator;
assert_eq!(service.add(2, 3).await, 5);
}
#[tokio::test]
async fn test_validate_email() {
let service = ValidationService;
assert!(service.validate_email("[email protected]".to_string())
.await
.is_ok());
assert!(service.validate_email("invalid".to_string())
.await
.is_err());
}
}
#[cfg(test)]
mod prop_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn test_add_commutative(a in -1000i32..1000, b in -1000i32..1000) {
let calc = Calculator;
let runtime = tokio::runtime::Runtime::new().unwrap();
runtime.block_on(async {
let result1 = calc.add(a, b).await;
let result2 = calc.add(b, a).await;
assert_eq!(result1, result2);
});
}
}
}
// ❌ Bad: Blocking in async
#[tool(description = "Read file")]
async fn read_file(&self, path: String) -> Result<String, String> {
std::fs::read_to_string(path) // BLOCKS!
.map_err(|e| e.to_string())
}
// ✅ Good: Use async operations
#[tool(description = "Read file")]
async fn read_file(&self, path: String) -> Result<String, String> {
tokio::fs::read_to_string(path).await
.map_err(|e| e.to_string())
}
use sqlx::PgPool;
#[tool(tool_box)]
struct DatabaseService {
pool: PgPool,
}
#[tool(tool_box)]
impl DatabaseService {
#[tool(description = "Query database")]
async fn query(&self, sql: String) -> Result<Vec<String>, String> {
sqlx::query(&sql)
.fetch_all(&self.pool)
.await
.map_err(|e| e.to_string())?;
Ok(vec![])
}
}
When helping with tool development:
Understand Requirements
Design Tool API
Implement Tool
Add Tests
Review and Refine
Your goal is to help developers create robust, well-designed tools that AI assistants can reliably use.
development
Apple Human Interface Guidelines for content display components. Use this skill when the user asks about charts component, collection view, image view, web view, color well, image well, activity view, lockup, data visualization, content display, displaying images, rendering web content, color pickers, or presenting collections of items in Apple apps. Also use when the user says how should I display charts, what's the best way to show images, should I use a web view, how do I build a grid of items, what component shows media, or how do I present a share sheet. Cross-references: hig-foundations for color/typography/accessibility, hig-patterns for data visualization patterns, hig-components-layout for structural containers, hig-platforms for platform-specific component behavior.
tools
Automate HelpDesk tasks via Rube MCP (Composio): list tickets, manage views, use canned responses, and configure custom fields. Always search tools first for current schemas.
testing
Expert Haskell engineer specializing in advanced type systems, pure functional design, and high-reliability software. Use PROACTIVELY for type-level programming, concurrency, and architecture guidance.
tools
GraphQL gives clients exactly the data they need - no more, no less. One endpoint, typed schema, introspection. But the flexibility that makes it powerful also makes it dangerous. Without proper controls, clients can craft queries that bring down your server. This skill covers schema design, resolvers, DataLoader for N+1 prevention, federation for microservices, and client integration with Apollo/urql. Key insight: GraphQL is a contract. The schema is the API documentation. Design it carefully.