pforge is a declarative framework for building Model Context Protocol (MCP) servers with zero boilerplate.
- Quick Start
- Installation
- Creating Your First Server
- Configuration
- Handlers
- Advanced Features
- Examples
- Best Practices
# Create a new project
pforge new my-server
# Navigate to project
cd my-server
# Build the project
pforge build
# Run the server
pforge servegit clone https://github.com/paiml/pforge
cd pforge
cargo install --path crates/pforge-clicargo install pforge-clipforge new hello-world
cd hello-worldThis creates:
hello-world/
├── Cargo.toml
├── pforge.yaml # Configuration
└── src/
├── main.rs
└── handlers/
├── mod.rs
└── hello.rs # Example handler
Edit pforge.yaml:
forge:
name: hello-world
version: 0.1.0
transport: stdio
tools:
- type: native
name: greet
description: "Greet a person by name"
handler:
path: handlers::hello::greet
params:
name:
type: string
required: true
greeting:
type: string
required: false
default: "Hello"src/handlers/hello.rs:
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Deserialize)]
pub struct GreetInput {
pub name: String,
pub greeting: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct GreetOutput {
pub message: String,
}
pub async fn greet(input: GreetInput) -> Result<GreetOutput, Box<dyn std::error::Error>> {
let greeting = input.greeting.unwrap_or_else(|| "Hello".to_string());
Ok(GreetOutput {
message: format!("{}, {}!", greeting, input.name),
})
}pforge servePure Rust functions with full type safety:
tools:
- type: native
name: process_data
description: "Process incoming data"
handler:
path: handlers::process::process_data
params:
data:
type: object
required: true
options:
type: object
required: false
timeout_ms: 5000Execute shell commands:
tools:
- type: cli
name: git_status
description: "Get git status"
command: git
args: ["status", "--short"]
cwd: /path/to/repo
env:
GIT_PAGER: ""
stream: falseMake HTTP requests:
tools:
- type: http
name: fetch_user
description: "Fetch user data from API"
endpoint: "https://api.example.com/users/{{user_id}}"
method: GET
auth:
type: bearer
token: ${API_TOKEN}
headers:
Content-Type: application/jsonChain multiple tools:
tools:
- type: pipeline
name: process_pipeline
description: "Multi-step processing"
steps:
- tool: fetch_data
output_var: raw_data
- tool: transform_data
input:
data: "{{raw_data}}"
output_var: transformed
- tool: store_data
input:
data: "{{transformed}}"
condition: "{{transformed.valid}}"
error_policy: continueExpose files and data:
resources:
- uri_template: "file:///{path}"
handler:
path: handlers::file_resource
supports:
- read
- write
- subscribe
- uri_template: "db://{table}/{id}"
handler:
path: handlers::db_resource
supports:
- readTemplate-based prompts:
prompts:
- name: code_review
description: "Generate code review prompt"
template: |
Review the following {{language}} code:
{{code}}
Focus on: {{focus_areas}}
arguments:
language:
type: string
required: true
code:
type: string
required: true
focus_areas:
type: string
required: false
default: "correctness, performance, style"Persistent state with Sled:
state:
backend: sled
path: ./data/state.db
options:
cache_capacity: 1073741824 # 1GBIn-memory state for testing:
state:
backend: memory
path: ""use serde::{Deserialize, Serialize};
use pforge_runtime::Result;
#[derive(Deserialize)]
pub struct CalculateInput {
pub operation: String,
pub a: f64,
pub b: f64,
}
#[derive(Serialize)]
pub struct CalculateOutput {
pub result: f64,
}
pub async fn calculate(input: CalculateInput) -> Result<CalculateOutput> {
let result = match input.operation.as_str() {
"add" => input.a + input.b,
"subtract" => input.a - input.b,
"multiply" => input.a * input.b,
"divide" => input.a / input.b,
_ => return Err(pforge_runtime::Error::Handler(
"Unknown operation".to_string()
)),
};
Ok(CalculateOutput { result })
}use pforge_runtime::{StateManager, Result};
pub async fn get_counter(
state: &dyn StateManager,
) -> Result<u64> {
let value = state.get("counter").await?;
match value {
Some(bytes) => {
let count = u64::from_le_bytes(bytes.try_into().unwrap());
Ok(count)
}
None => Ok(0),
}
}
pub async fn increment_counter(
state: &dyn StateManager,
) -> Result<u64> {
let current = get_counter(state).await?;
let new_count = current + 1;
state.set("counter", new_count.to_le_bytes().to_vec(), None).await?;
Ok(new_count)
}Add cross-cutting concerns:
use pforge_runtime::{MiddlewareChain, LoggingMiddleware, ValidationMiddleware};
let mut chain = MiddlewareChain::new();
// Add logging
chain.add(Arc::new(LoggingMiddleware::new("api")));
// Add validation
chain.add(Arc::new(ValidationMiddleware::new(vec![
"user_id".to_string(),
"action".to_string(),
])));
// Execute with middleware
chain.execute(request, handler).await?;use pforge_runtime::{RetryPolicy, with_timeout, retry_with_policy};
use std::time::Duration;
// Simple timeout
let result = with_timeout(
Duration::from_secs(5),
expensive_operation()
).await?;
// Retry with exponential backoff
let policy = RetryPolicy::new(3)
.with_backoff(
Duration::from_millis(100),
Duration::from_secs(5),
)
.with_multiplier(2.0);
let result = retry_with_policy(&policy, || async {
api_call().await
}).await?;Prevent cascading failures:
use pforge_runtime::{CircuitBreaker, CircuitBreakerConfig};
use std::time::Duration;
let config = CircuitBreakerConfig {
failure_threshold: 5,
timeout: Duration::from_secs(60),
success_threshold: 2,
};
let cb = CircuitBreaker::new(config);
// Protected call
let result = cb.call(|| async {
potentially_failing_service().await
}).await?;use pforge_runtime::{RecoveryMiddleware, CircuitBreakerConfig};
let recovery = RecoveryMiddleware::new()
.with_circuit_breaker(CircuitBreakerConfig::default());
let tracker = recovery.error_tracker();
// Use in middleware chain
chain.add(Arc::new(recovery));
// Check error stats
println!("Total errors: {}", tracker.total_errors());forge:
name: rest-api
version: 1.0.0
tools:
- type: http
name: list_users
endpoint: "https://api.example.com/users"
method: GET
auth:
type: bearer
token: ${API_KEY}
- type: http
name: create_user
endpoint: "https://api.example.com/users"
method: POST
auth:
type: bearer
token: ${API_KEY}tools:
- type: pipeline
name: etl_pipeline
steps:
- tool: extract_data
output_var: raw
- tool: transform_data
input:
data: "{{raw}}"
output_var: clean
- tool: load_data
input:
data: "{{clean}}"Always use proper error types:
use pforge_runtime::{Error, Result};
pub async fn my_handler(input: Input) -> Result<Output> {
let data = fetch_data()
.await
.map_err(|e| Error::Handler(format!("Fetch failed: {}", e)))?;
Ok(process(data))
}Use environment variables for secrets:
tools:
- type: http
name: api_call
endpoint: ${API_ENDPOINT}
auth:
type: bearer
token: ${API_TOKEN}Write comprehensive tests:
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_handler() {
let input = MyInput { value: 42 };
let result = my_handler(input).await.unwrap();
assert_eq!(result.value, 43);
}
}- Use connection pooling for HTTP tools
- Enable caching for expensive operations
- Set appropriate timeouts
- Use circuit breakers for external services
- Never commit secrets to version control
- Use environment variables for sensitive data
- Validate all inputs
- Set resource limits
- Enable audit logging
# Create new project
pforge new <name>
# Build project
pforge build
# Run server
pforge serve
# Development mode with hot reload
pforge dev
# Show version
pforge --version
# Show help
pforge --help- Check configuration syntax:
pforge build - Verify all handlers exist
- Check environment variables
- Review logs for errors
- Verify input types match schema
- Check error messages for details
- Enable debug logging
- Test handlers in isolation
- Enable caching
- Use connection pooling
- Set appropriate timeouts
- Profile with flamegraphs
- Read the Architecture Guide
- Explore Examples
- Check the API Documentation
- Report issues on GitHub
- GitHub Issues: https://github.com/paiml/pforge/issues
- Documentation: https://docs.rs/pforge-runtime
- Community: https://github.com/paiml/pforge/discussions