| Crates.io | mcplease |
| lib.rs | mcplease |
| version | 0.2.3 |
| created_at | 2025-06-29 01:47:49.788067+00 |
| updated_at | 2025-07-18 22:24:41.87239+00 |
| description | simple mcp framework |
| homepage | |
| repository | https://github.com/jbr/mcplease |
| max_upload_size | |
| id | 1730240 |
| size | 69,164 |
MCPlease is a lightweight Rust framework for building MCP (Model Context Protocol) servers. It provides a simple, macro-driven approach to defining tools and managing state, with optional support for session persistence and cross-process synchronization.
tools! macroschemarsclapThe fastest way to get started is with the mcplease CLI tool:
# Install the CLI
cargo install mcplease-cli
# Create a new MCP server with tools
mcplease create my-server --tools hello,goodbye,status --state MyServerState
# Navigate to your project
cd my-server
# Add more tools as needed
mcplease add health_check
mcplease add ping
# Test that it compiles
cargo check
# Run your MCP server
cargo run serve
This creates a fully functional MCP server with:
For detailed CLI documentation, see cli/README.md
cargo new my-mcp-server
cd my-mcp-server
Cargo.toml[dependencies]
anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] }
fieldwork = "0.4.6"
mcplease = "0.2.0"
schemars = "1.0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Create src/state.rs:
use anyhow::Result;
use mcplease::session::SessionStore;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct SharedData {
pub working_directory: Option<PathBuf>,
// Add other shared state fields here
}
#[derive(Debug, fieldwork::Fieldwork)]
pub struct MyToolsState {
#[fieldwork(get, get_mut)]
session_store: SessionStore<SharedData>,
}
impl MyToolsState {
pub fn new() -> Result<Self> {
let session_store = SessionStore::new(Some(
dirs::home_dir()
.unwrap_or_default()
.join(".ai-tools/sessions/my-tools.json")
))?;
Ok(Self { session_store })
}
pub fn get_working_directory(&mut self) -> Result<Option<PathBuf>> {
Ok(self.session_store.get_or_create("default")?.working_directory.clone())
}
pub fn set_working_directory(&mut self, path: PathBuf) -> Result<()> {
self.session_store.update("default", |data| {
data.working_directory = Some(path);
})
}
}
Create src/tools/ directory and add tool implementations. Each tool should be in its own module:
src/tools/hello.rs:
use crate::state::MyToolsState;
use anyhow::Result;
use mcplease::{
traits::{Tool, WithExamples},
types::Example,
};
use serde::{Deserialize, Serialize};
/// Say hello to someone
#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema, clap::Args)]
#[serde(rename = "hello")]
pub struct Hello {
/// The name to greet
pub name: String,
/// Whether to be enthusiastic
#[serde(skip_serializing_if = "Option::is_none")]
#[arg(long)]
pub enthusiastic: Option<bool>,
}
impl WithExamples for Hello {
fn examples() -> Vec<Example<Self>> {
vec![
Example {
description: "A simple greeting",
item: Self {
name: "World".into(),
enthusiastic: None,
},
},
Example {
description: "An enthusiastic greeting",
item: Self {
name: "Alice".into(),
enthusiastic: Some(true),
},
},
]
}
}
impl Tool<MyToolsState> for Hello {
fn execute(self, _state: &mut MyToolsState) -> Result<String> {
let greeting = if self.enthusiastic.unwrap_or(false) {
format!("Hello, {}! 🎉", self.name)
} else {
format!("Hello, {}", self.name)
};
Ok(greeting)
}
}
src/tools/set_working_directory.rs:
use crate::state::MyToolsState;
use anyhow::Result;
use mcplease::{
traits::{Tool, WithExamples},
types::Example,
};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
/// Set the working directory for relative path operations
#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema, clap::Args)]
#[serde(rename = "set_working_directory")]
pub struct SetWorkingDirectory {
/// New working directory path
pub path: String,
}
impl WithExamples for SetWorkingDirectory {
fn examples() -> Vec<Example<Self>> {
vec![
Example {
description: "Set working directory to a project folder",
item: Self {
path: "/path/to/my/project".into(),
},
},
]
}
}
impl Tool<MyToolsState> for SetWorkingDirectory {
fn execute(self, state: &mut MyToolsState) -> Result<String> {
let path = PathBuf::from(&*shellexpand::tilde(&self.path));
if !path.exists() {
return Ok(format!("Directory {} does not exist", path.display()));
}
state.set_working_directory(path.clone())?;
Ok(format!("Set working directory to {}", path.display()))
}
}
src/tools.rs:
use crate::state::MyToolsState;
mcplease::tools!(
MyToolsState,
(Hello, hello, "hello"),
(SetWorkingDirectory, set_working_directory, "set_working_directory")
);
src/main.rs:
mod state;
mod tools;
use anyhow::Result;
use mcplease::server_info;
use state::MyToolsState;
const INSTRUCTIONS: &str = "This is my custom MCP server. Use set_working_directory to establish context.";
fn main() -> Result<()> {
let mut state = MyToolsState::new()?;
mcplease::run::<tools::Tools, _>(&mut state, server_info!(), Some(INSTRUCTIONS))
}
# Run as MCP server (stdio mode)
cargo run serve
# Or use tools directly from command line
cargo run hello --name "World"
cargo run set-working-directory --path "/tmp"
tools! macro: Generates the enum that implements MCP tool dispatchTool trait: Defines how individual tools executeWithExamples trait: Provides example usage for documentationSessionStore: Handles persistent state with cross-process syncschemarsEach tool follows this pattern:
#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema, clap::Args)]
#[serde(rename = "tool_name")]
pub struct MyTool {
// Tool parameters with proper documentation
/// Description of the parameter
pub required_param: String,
/// Optional parameter with skip_serializing_if
#[serde(skip_serializing_if = "Option::is_none")]
#[arg(long)]
pub optional_param: Option<bool>,
}
impl WithExamples for MyTool { /* ... */ }
impl Tool<StateType> for MyTool { /* ... */ }
The framework uses SessionStore<T> for persistent state:
// Get or create session data
let data = store.get_or_create("session_id")?;
// Update data with closure
store.update("session_id", |data| {
data.some_field = new_value;
})?;
// Get without creating
let maybe_data = store.get("session_id")?;
// Set directly
store.set("session_id", new_data)?;
Tools should return anyhow::Result<String> for consistent error propagation:
impl Tool<State> for MyTool {
fn execute(self, state: &mut State) -> Result<String> {
// Use ? for error propagation
let data = std::fs::read_to_string(&self.path)
.with_context(|| format!("Failed to read {}", self.path))?;
// Return success message
Ok(format!("Successfully processed {} bytes", data.len()))
}
}
Provide meaningful examples to help users understand tool usage:
impl WithExamples for MyTool {
fn examples() -> Vec<Example<Self>> {
vec![
Example {
description: "Basic usage with default settings",
item: Self {
path: "example.txt".into(),
options: None,
},
},
Example {
description: "Advanced usage with custom options",
item: Self {
path: "/absolute/path/file.txt".into(),
options: Some(CustomOptions { verbose: true }),
},
},
]
}
}
Use Option<T> with proper serialization handling:
#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema, clap::Args)]
pub struct MyTool {
/// Required parameter
pub required: String,
/// Optional parameter (won't appear in JSON if None)
#[serde(skip_serializing_if = "Option::is_none")]
#[arg(long)]
pub optional: Option<String>,
/// Boolean flag (defaults to false)
#[serde(skip_serializing_if = "Option::is_none")]
#[arg(long, action = clap::ArgAction::SetTrue)]
pub flag: Option<bool>,
}
impl MyTool {
fn flag(&self) -> bool {
self.flag.unwrap_or(false)
}
}
For tools that need to share context across processes:
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct SharedContext {
pub working_directory: Option<PathBuf>,
pub recent_files: Vec<PathBuf>,
pub user_preferences: HashMap<String, String>,
}
// In your state struct:
impl MyState {
pub fn new() -> Result<Self> {
// Use a shared file for cross-server communication
let shared_store = SessionStore::new(Some(
dirs::home_dir()
.unwrap_or_default()
.join(".ai-tools/sessions/shared-context.json")
))?;
Ok(Self { shared_store })
}
}
anyhow::Context for descriptive error messages#[serde(default)] for backward compatibilityReturn user-friendly messages that help with debugging:
// Good: Specific and actionable
Ok(format!("File {} does not exist. Use an absolute path or set_working_directory first.", path))
// Bad: Generic and unhelpful
Err(anyhow!("File not found"))
Use consistent path resolution patterns:
fn resolve_path(base: Option<&Path>, input: &str) -> Result<PathBuf> {
let path = PathBuf::from(&*shellexpand::tilde(input));
if path.is_absolute() {
Ok(path)
} else if let Some(base) = base {
Ok(base.join(path))
} else {
Err(anyhow!("Relative path requires working directory to be set"))
}
}
Set MCP_LOG_LOCATION environment variable to enable logging:
export MCP_LOG_LOCATION="~/.ai-tools/logs/my-server.log"
cargo run serve
Log levels: RUST_LOG=trace,warn,error,debug,info
Use the command-line interface for testing:
# Test individual tools
cargo run my-tool --param value
# Get help
cargo run help
cargo run my-tool --help
When adding new tools to existing servers:
src/tools/tools! macro in src/tools.rssrc/tests.rsThe framework is designed to be extensible - new MCP servers should follow the established patterns for consistency and maintainability.