Exercise: Building a Calculator Tool

ch02-02-calculator
⭐ beginner ⏱️ 25 min

Now that you've created your first MCP server, let's build something more useful: a calculator. But this isn't just about math - it's about learning how to handle different operations, validate inputs, and return meaningful errors.

Think about it: when an AI asks your calculator to divide by zero, what should happen? When someone passes "abc" instead of a number, how do you respond helpfully?

Production MCP servers must handle edge cases gracefully. This exercise teaches you how.

🎯 Learning Objectives

Thinking

Doing

💬 Discussion

  • If you were an AI trying to use a calculator, what operations would you expect?
  • What should happen if someone tries to divide by zero?
  • How can error messages help an AI correct its request?
  • Should a calculator tool accept 'two plus three' or just '2 + 3'?
src/main.rs

💡 Hints

Hint 1: Start with the match

Use pattern matching to handle each operation:

#![allow(unused)]
fn main() {
fn calculate(input: &CalculateInput) -> Result<CalculateResult> {
    let (result, op_symbol) = match input.operation {
        Operation::Add => (input.a + input.b, "+"),
        // Add other operations...
    };
// Build the result
}

}

Hint 2: Handle division safely

Check for division by zero before computing:

#![allow(unused)]
fn main() {
Operation::Divide => {
    if input.b == 0.0 {
        return Err(anyhow!("Cannot divide by zero"));
    }
    (input.a / input.b, "/")
}
}
Hint 3: Complete calculate function
#![allow(unused)]
fn main() {
fn calculate(input: &CalculateInput) -> Result<CalculateResult> {
    let (result, op_symbol) = match input.operation {
        Operation::Add => (input.a + input.b, "+"),
        Operation::Subtract => (input.a - input.b, "-"),
        Operation::Multiply => (input.a * input.b, "*"),
        Operation::Divide => {
            if input.b == 0.0 {
                return Err(anyhow!("Cannot divide by zero"));
            }
            (input.a / input.b, "/")
        }
    };
if result.is_nan() || result.is_infinite() {
    return Err(anyhow!("Invalid result"));
}

Ok(CalculateResult {
    result,
    expression: format!("{} {} {} = {}", input.a, op_symbol, input.b, result),
})
}

}

⚠️ Try the exercise first! Show Solution
#![allow(unused)]
fn main() {
use pmcp::{Server, ServerCapabilities, ToolCapabilities};
use pmcp::server::TypedTool;
use serde::{Deserialize, Serialize};
use schemars::JsonSchema;
use anyhow::{Result, anyhow};

#[derive(Deserialize, JsonSchema)] #[serde(rename_all = "lowercase")] enum Operation { Add, Subtract, Multiply, Divide, }

#[derive(Deserialize, JsonSchema)] struct CalculateInput { a: f64, b: f64, operation: Operation, }

#[derive(Serialize)] struct CalculateResult { result: f64, expression: String, }

fn calculate(input: &CalculateInput) -> Result<CalculateResult> { let (result, op_symbol) = match input.operation { Operation::Add => (input.a + input.b, "+"), Operation::Subtract => (input.a - input.b, "-"), Operation::Multiply => (input.a * input.b, "*"), Operation::Divide => { if input.b == 0.0 { return Err(anyhow!( "Cannot divide by zero. Please provide a non-zero divisor." )); } (input.a / input.b, "/") } };

if result.is_nan() || result.is_infinite() {
    return Err(anyhow!(
        &quot;Calculation produced an invalid result (NaN or Infinity)&quot;
    ));
}

Ok(CalculateResult {
    result,
    expression: format!(&quot;{} {} {} = {}&quot;, input.a, op_symbol, input.b, result),
})
}

}

#[tokio::main] async fn main() -> Result<()> { let server = Server::builder() .name("calculator") .version("1.0.0") .capabilities(ServerCapabilities { tools: Some(ToolCapabilities::default()), ..Default::default() }) .tool("calculate", TypedTool::new("calculate", |input: CalculateInput| { Box::pin(async move { match calculate(&input) { Ok(result) => Ok(serde_json::to_value(result)?), Err(e) => Ok(serde_json::json!({ "error": e.to_string(), "suggestion": "Check your inputs and try again" })), } }) })) .build()?;

println!(&quot;Calculator server ready!&quot;);
Ok(())

}

Explanation

This solution demonstrates several important patterns:

1. Enum for Operations Using an enum instead of a string for operations:

  • Compile-time validation of operation types
  • Pattern matching ensures all cases are handled
  • #[serde(rename_all = "lowercase")] allows JSON like "add" instead of "Add"

2. Separation of Concerns The calculate() function is separate from the tool handler:

  • Easier to test (pure function, no async)
  • Cleaner error handling
  • Reusable logic

3. Defensive Error Handling

  • Check for division by zero BEFORE computing
  • Check for NaN/Infinity AFTER computing
  • Return helpful error messages that guide the AI

4. Human-Readable Output

  • The expression field shows the full calculation
  • Helps debugging and transparency
  • AI can show this to users

5. Error Response Pattern Instead of returning a tool error (which might retry), we return a structured error response. This lets the AI understand what went wrong and explain it to the user.

🧪 Tests

Run these tests locally with:

cargo test
View Test Code
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;
#[test]
fn test_addition() {
    let input = CalculateInput {
        a: 5.0,
        b: 3.0,
        operation: Operation::Add,
    };
    let result = calculate(&amp;input).unwrap();
    assert_eq!(result.result, 8.0);
}

#[test]
fn test_division_by_zero() {
    let input = CalculateInput {
        a: 10.0,
        b: 0.0,
        operation: Operation::Divide,
    };
    assert!(calculate(&amp;input).is_err());
}

#[test]
fn test_expression_format() {
    let input = CalculateInput {
        a: 10.0,
        b: 5.0,
        operation: Operation::Multiply,
    };
    let result = calculate(&amp;input).unwrap();
    assert!(result.expression.contains(&quot;10 * 5 = 50&quot;));
}
}

}

🤔 Reflection

  • Why do we check for division by zero before computing, not after?
  • What's the advantage of returning a structured error vs failing the tool call?
  • How would you add a 'power' operation to this calculator?
  • What might go wrong with floating-point math that integers wouldn't have?