YAML Configuration: Declaring Your Calculator
The calculator’s YAML configuration is 26 lines that replace hundreds of lines of SDK boilerplate. Let’s build it following EXTREME TDD principles.
The Complete Configuration
Here’s the full forge.yaml
for our calculator server:
forge:
name: calculator-server
version: 0.1.0
transport: stdio
optimization: release
tools:
- type: native
name: calculate
description: "Perform arithmetic operations (add, subtract, multiply, divide)"
handler:
path: handlers::calculate_handler
params:
operation:
type: string
required: true
description: "The operation to perform: add, subtract, multiply, or divide"
a:
type: float
required: true
description: "First operand"
b:
type: float
required: true
description: "Second operand"
Section-by-Section Breakdown
1. Forge Metadata
forge:
name: calculator-server
version: 0.1.0
transport: stdio
optimization: release
Key decisions:
name
: Unique identifier for your serverversion
: Semantic versioning (important for client compatibility)transport: stdio
: Standard input/output (most common for MCP)optimization: release
: Build with optimizations enabled (<1μs dispatch)
Alternative transports:
sse
: Server-Sent Events (web-based)websocket
: WebSocket (bidirectional streaming)
For local tools like calculators, stdio
is the right choice.
2. Tool Definition
tools:
- type: native
name: calculate
description: "Perform arithmetic operations (add, subtract, multiply, divide)"
Why a single tool?
Instead of four separate tools (add
, subtract
, multiply
, divide
), we use one tool with an operation parameter. Benefits:
- Cleaner API: Clients see one tool, not four
- Shared logic: Validation happens once
- Easier testing: Test one handler, not four
- Better UX: “I want to calculate” vs “I want to add or subtract or…”
The description field is critical - it’s what LLMs see when deciding which tool to use. Make it specific and actionable.
3. Handler Path
handler:
path: handlers::calculate_handler
This tells pforge where to find your Rust handler:
- Module:
handlers
(thesrc/handlers.rs
file) - Symbol:
calculate_handler
(the exported handler struct)
Convention: Use {module}::{handler_name}
format. The handler must implement the Handler
trait.
4. Parameter Schema
params:
operation:
type: string
required: true
description: "The operation to perform: add, subtract, multiply, or divide"
a:
type: float
required: true
description: "First operand"
b:
type: float
required: true
description: "Second operand"
Parameter types:
string
: For operation names (“add”, “subtract”, etc.)float
: Forf64
numeric values (supports decimals)required: true
: Validation fails if missing
Why float
not number
?
MCP/JSON Schema distinguishes:
integer
: Whole numbers onlyfloat
: Decimal/floating-point numbers
Our calculator supports 10.5 + 3.7
, so we need float
.
Type Safety in Action
pforge uses this YAML to generate Rust types. The params:
params:
operation: { type: string, required: true }
a: { type: float, required: true }
b: { type: float, required: true }
Become this Rust struct (auto-generated):
#[derive(Debug, Deserialize, JsonSchema)]
pub struct CalculateInput {
pub operation: String,
pub a: f64,
pub b: f64,
}
No runtime validation needed - the type system guarantees correctness!
EXTREME TDD: Configuration First
In our 5-minute cycles, the YAML came before the handler:
Cycle 0 (3 minutes):
- RED: Create empty
forge.yaml
, runpforge build
→ fails (no handler) - GREEN: Add forge metadata and basic tool structure
- REFACTOR: Add parameter descriptions
This design-first approach forces you to think about:
- What inputs do I need?
- What types make sense?
- What’s the API contract?
Common YAML Patterns
Pattern 1: Optional Parameters
params:
operation: { type: string, required: true }
precision: { type: integer, required: false, default: 2 }
Pattern 2: Enum Constraints
params:
operation:
type: string
required: true
enum: ["add", "subtract", "multiply", "divide"]
We didn’t use enum constraints because we validate in Rust, giving better error messages.
Pattern 3: Nested Objects
params:
calculation:
type: object
required: true
properties:
operation: { type: string }
operands:
type: array
items: { type: float }
Pattern 4: Arrays
params:
numbers:
type: array
required: true
items: { type: float }
minItems: 2
Validation Strategy
Two-layer validation:
-
YAML validation (at build time):
- pforge validates against its schema
- Catches: missing required fields, invalid types
- Fast fail: Won’t even compile
-
Runtime validation (in handler):
- Check operation is valid
- Check division by zero
- Custom business logic
Philosophy: Use the type system first, runtime validation second.
Configuration vs. Code
Traditional MCP SDK (TypeScript):
// 50+ lines of boilerplate
const server = new Server({
name: "calculator-server",
version: "0.1.0"
}, {
capabilities: {
tools: {}
}
});
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [{
name: "calculate",
description: "Perform arithmetic operations",
inputSchema: {
type: "object",
properties: {
operation: { type: "string", description: "..." },
a: { type: "number", description: "..." },
b: { type: "number", description: "..." }
},
required: ["operation", "a", "b"]
}
}]
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "calculate") {
// ... handler logic
}
});
pforge equivalent:
# 26 lines, zero boilerplate
forge:
name: calculator-server
version: 0.1.0
transport: stdio
optimization: release
tools:
- type: native
name: calculate
# ... (see above)
90% less code. 100% type-safe. 16x faster.
Build-Time Code Generation
When you run pforge build
, this YAML generates:
- Handler registry: O(1) lookup for “calculate” tool
- Type definitions:
CalculateInput
struct with validation - JSON Schema: For MCP protocol compatibility
- Dispatch logic: Routes requests to your handler
All at compile time - zero runtime overhead.
Debugging Configuration
Common errors and fixes:
Error: “Handler not found: handlers::calculate_handler”
# Wrong:
handler:
path: calculate_handler
# Right:
handler:
path: handlers::calculate_handler
Error: “Invalid type: expected float, found string”
# Wrong:
params:
a: { type: string } # User passes "5.0"
# Right:
params:
a: { type: float } # Parsed as 5.0
Error: “Missing required parameter: operation”
# Wrong:
params:
operation: { type: string } # defaults to required: false
# Right:
params:
operation: { type: string, required: true }
Testing Your Configuration
Before writing handler code, validate your YAML:
# Validate configuration
pforge validate
# Build (validates + generates code)
pforge build --debug
# Watch mode (continuous validation)
pforge dev --watch
EXTREME TDD tip: Run pforge validate
after every YAML edit. Fast feedback!
Next Steps
Now that you have a valid configuration, it’s time to implement the handler. Turn to Chapter 3.2 to write the Rust code that powers the calculator.
“Configuration is code. Treat it with the same rigor.” - pforge philosophy