Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Parses YAML → Generates Rust code
  2. Creates Handler Registry → Maps tool names to handlers
  3. Builds pmcp Server → Uses pmcp::ServerBuilder
  4. Enforces Quality → PMAT gates, TDD methodology
  5. 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 parsing
  • Serialize for output JSON
  • JsonSchema for MCP schema generation
  • Send + 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 Versionpmcp VersionNotes
0.1.01.6.0Initial release
FutureLatestWill 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

Featurepmcppforge
FoundationMCP protocol implYAML → pmcp code
You WriteRust codeYAML + handlers
PerformanceFastFaster (perfect hash)
FlexibilityComplete4 handler types
Built OnNothingpmcp
Can UseStandaloneStandalone or with pmcp
Crates.iopmcppforge-* (uses pmcp)