Chapter 1.5: How pforge Uses pmcp Under the Hood
This chapter reveals the architectural relationship between pforge and pmcp (rust-mcp-sdk). Understanding this relationship is crucial for knowing when to use each tool and how they complement each other.
The Architecture: pforge Built on pmcp
Key Insight: pforge is not a replacement for pmcp - it’s a framework built on top of pmcp.
┌─────────────────────────────────────┐
│ pforge (Declarative Framework) │
│ • YAML Configuration │
│ • Code Generation │
│ • Handler Registry │
│ • Quality Gates │
└─────────────────────────────────────┘
▼
┌─────────────────────────────────────┐
│ pmcp (Low-Level MCP SDK) │
│ • ServerBuilder │
│ • TypedTool API │
│ • Transport Layer (stdio/SSE/WS) │
│ • JSON-RPC Protocol │
└─────────────────────────────────────┘
▼
┌─────────────────────────────────────┐
│ Model Context Protocol (MCP) │
│ • Tools, Resources, Prompts │
│ • Sampling, Logging │
└─────────────────────────────────────┘
Dependency Chain
From crates/pforge-runtime/Cargo.toml
:
[dependencies]
pmcp = "1.6" # ← pforge runtime depends on pmcp
schemars = { version = "0.8", features = ["derive"] }
# ... other deps
This means:
- Every pforge server is a pmcp server under the hood
- pforge translates YAML → pmcp API calls
- All pmcp features are available to pforge
What pforge Adds on Top of pmcp
pforge is essentially a code generator + framework that:
- Parses YAML → Generates Rust code
- Creates Handler Registry → Maps tool names to handlers
- Builds pmcp Server → Uses
pmcp::ServerBuilder
- Enforces Quality → PMAT gates, TDD methodology
- Optimizes Dispatch → Perfect hashing, compile-time optimization
Example: The Same Server in Both
With Pure pmcp (What You Write)
// main.rs - Direct pmcp usage
use pmcp::{ServerBuilder, TypedTool};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
struct GreetArgs {
name: String,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let server = ServerBuilder::new()
.name("greeter")
.version("1.0.0")
.tool_typed("greet", |args: GreetArgs, _extra| {
Box::pin(async move {
Ok(serde_json::json!({
"message": format!("Hello, {}!", args.name)
}))
})
})
.build()?;
server.run_stdio().await?;
Ok(())
}
With pforge (What You Write)
# forge.yaml
forge:
name: greeter
version: 1.0.0
tools:
- type: native
name: greet
handler:
path: handlers::greet_handler
params:
name: { type: string, required: true }
// src/handlers.rs
use pforge_runtime::{Handler, Result, Error};
use serde::{Deserialize, Serialize};
use schemars::JsonSchema;
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GreetInput {
name: String,
}
#[derive(Debug, Serialize, JsonSchema)]
pub struct GreetOutput {
message: String,
}
pub struct GreetHandler;
#[async_trait::async_trait]
impl Handler for GreetHandler {
type Input = GreetInput;
type Output = GreetOutput;
type Error = Error;
async fn handle(&self, input: Self::Input) -> Result<Self::Output> {
Ok(GreetOutput {
message: format!("Hello, {}!", input.name)
})
}
}
pub use GreetHandler as greet_handler;
What pforge Generates (Under the Hood)
When you run pforge build
, it generates something like:
// Generated by pforge codegen
use pmcp::ServerBuilder;
use pforge_runtime::HandlerRegistry;
pub fn build_server() -> Result<pmcp::Server> {
let mut registry = HandlerRegistry::new();
// Register handlers
registry.register("greet", handlers::greet_handler);
// Build pmcp server
let server = ServerBuilder::new()
.name("greeter")
.version("1.0.0")
.tool_typed("greet", |args: handlers::GreetInput, _extra| {
Box::pin(async move {
let handler = handlers::greet_handler;
let output = handler.handle(args).await?;
Ok(serde_json::to_value(output)?)
})
})
.build()?;
Ok(server)
}
Key Point: pforge generates pmcp code!
The Handler Abstraction
pforge defines a Handler
trait that’s compatible with pmcp’s TypedTool
:
// pforge-runtime/src/handler.rs
#[async_trait::async_trait]
pub trait Handler: Send + Sync {
type Input: for<'de> Deserialize<'de> + JsonSchema;
type Output: Serialize + JsonSchema;
type Error: Into<Error>;
async fn handle(&self, input: Self::Input)
-> Result<Self::Output, Self::Error>;
}
This trait is designed to be zero-cost and directly map to pmcp’s TypedTool
API.
Real Example: How pforge Uses pmcp in Runtime
From pforge-runtime/src/handler.rs
:
// pforge integrates with pmcp's type system
use schemars::JsonSchema; // Same as pmcp uses
use serde::{Deserialize, Serialize}; // Same as pmcp uses
/// Handler trait compatible with pmcp TypedTool
#[async_trait::async_trait]
pub trait Handler: Send + Sync {
type Input: for<'de> Deserialize<'de> + JsonSchema;
type Output: Serialize + JsonSchema;
type Error: Into<Error>;
async fn handle(&self, input: Self::Input)
-> Result<Self::Output, Self::Error>;
}
Notice: The trait bounds match pmcp’s requirements exactly:
Deserialize
for input parsingSerialize
for output JSONJsonSchema
for MCP schema generationSend + Sync
for async runtime
When pforge Calls pmcp
Here’s the actual flow when you run pforge serve
:
1. pforge CLI parses forge.yaml
↓
2. pforge-codegen generates Rust code
↓
3. Generated code creates HandlerRegistry
↓
4. Registry wraps handlers in pmcp TypedTool
↓
5. pmcp ServerBuilder builds the server
↓
6. pmcp handles MCP protocol (stdio/SSE/WebSocket)
↓
7. pmcp routes requests to handlers
↓
8. pforge Handler executes and returns
↓
9. pmcp serializes response to JSON-RPC
Performance: Why pforge is Faster for Dispatch
pmcp: General-purpose HashMap lookup
// In pmcp (simplified)
let tool = tools.get(tool_name)?; // HashMap lookup
tool.execute(args).await
pforge: Compile-time perfect hash
// Generated by pforge (simplified)
match tool_name {
"greet" => greet_handler.handle(args).await,
"calculate" => calculate_handler.handle(args).await,
// ... compile-time matched
_ => Err(ToolNotFound)
}
Result: <1μs dispatch in pforge vs <10μs in pmcp
Using Both Together
You can mix pforge and pmcp in the same project!
Example: pforge for Simple Tools, pmcp for Complex Logic
# forge.yaml - Simple tools in pforge
tools:
- type: native
name: greet
handler:
path: handlers::greet_handler
// main.rs - Add complex pmcp tool
use pmcp::ServerBuilder;
#[tokio::main]
async fn main() -> Result<()> {
// Load pforge-generated server
let mut server = pforge_runtime::build_from_config("forge.yaml")?;
// Add custom pmcp tool with complex logic
server.add_tool_typed("complex_stateful", |args, extra| {
Box::pin(async move {
// Custom logic not expressible in pforge YAML
// Maybe database transactions, WebSocket, etc.
todo!()
})
});
server.run_stdio().await
}
Dependency Versions
pforge tracks pmcp versions:
pforge Version | pmcp Version | Notes |
---|---|---|
0.1.0 | 1.6.0 | Initial release |
Future | Latest | Will track pmcp updates |
Summary: The Relationship
Think of it like this:
- pmcp = Express.js (low-level web framework)
- pforge = Next.js (opinionated framework on Express)
Or in Rust terms:
- pmcp = actix-web (low-level HTTP server)
- pforge = Rocket (high-level framework on actix)
Both are necessary:
- pmcp provides the MCP protocol implementation
- pforge provides the declarative YAML layer + quality tools
You’re using pmcp whether you know it or not:
- Every pforge server is a pmcp server
- pforge just generates the pmcp code for you
When to Drop Down to pmcp
Use pure pmcp directly when pforge’s handler types don’t fit:
❌ Can’t express in pforge:
- Custom server lifecycle hooks
- Stateful request correlation
- Custom transport implementations
- Dynamic tool registration
- WebAssembly compilation
- Database connection pools with transactions
✅ Can express in pforge:
- Standard CRUD operations
- CLI tool wrappers
- HTTP API proxies
- Simple data transformations
- Multi-tool pipelines
- Standard state management
Verification: Check the Dependency
# See pmcp in pforge's dependencies
$ grep pmcp crates/pforge-runtime/Cargo.toml
pmcp = "1.6"
# See pforge using pmcp types
$ rg "pmcp::" crates/pforge-runtime/src/
# (Currently minimal direct usage - trait compat layer)
Future: pforge May Expose More pmcp Features
Future pforge versions may expose:
- Custom middleware (pmcp has this)
- Sampling requests (pmcp has this)
- Logging handlers (pmcp has this)
- Custom transports (pmcp has this)
For now, drop down to pmcp for these features.
Next: Migration Between Them
Quick Reference
Feature | pmcp | pforge |
---|---|---|
Foundation | MCP protocol impl | YAML → pmcp code |
You Write | Rust code | YAML + handlers |
Performance | Fast | Faster (perfect hash) |
Flexibility | Complete | 4 handler types |
Built On | Nothing | pmcp |
Can Use | Standalone | Standalone or with pmcp |
Crates.io | pmcp | pforge-* (uses pmcp) |