Back to Blog

From MCP Server to MPP Package: A Migration Guide

MPP Protocol·April 10, 2026·11 min read
MPPMCPMigrationTutorialWASMDeveloper Experience

You have MCP tools. They work. Your agents use them daily. Maybe they're Node.js servers that call external APIs, or Python scripts that query databases, or Go services that manage infrastructure.

The question is not whether those tools have security gaps — they do, because MCP has no verification, no sandboxing, and no privacy filtering. The question is how much effort it takes to close those gaps by migrating to MPP.

The answer: less than you expect, if you approach it methodically.

This guide walks through the complete migration path, from an existing MCP tool server to a signed, sandboxed MPP package. The core insight is that your business logic doesn't change. What changes is the packaging — how the tool is delivered, verified, and executed.


The Conceptual Shift

Before diving into steps, it's worth understanding what's actually changing.

MCP Model: Live Server

Agent → JSON-RPC → MCP Server Process → (full OS access) → Response

Your MCP tool is a running process. It listens for JSON-RPC requests over stdio or HTTP. It has full access to the filesystem, network, and environment. It runs until stopped.

MPP Model: Signed Artifact

Agent → JSON-RPC → Host Runtime → Gatekeeper → Sandbox → Tool (WASM) → Privacy Filter → Response

Your MPP tool is a compiled WebAssembly binary inside a signed ZIP archive. It declares its capabilities. It runs in a sandbox with only the access it declared. Its output is privacy-filtered before returning to the agent.

What Stays the Same

  • The wire protocol. MPP uses the same JSON-RPC 2.0 format as MCP. The request and response shapes are compatible.
  • The business logic. Your tool still takes input, does work, and returns output. The algorithm is unchanged.
  • The tool interface. Tool definitions (name, description, parameters, returns) map directly to MPP's tool_definitions schema.

What Changes

  • The runtime. Your code compiles to WebAssembly instead of running as a native process.
  • The I/O model. Instead of listening on a socket or stdio pipe, your tool reads from stdin and writes to stdout within a WASM sandbox.
  • The permissions model. Instead of ambient access to everything, you declare what you need and receive only that.
  • The distribution model. Instead of git clone + npm install + node server.js, you publish a signed .mpp file to a registry.

Step 1: Inventory Your Tool's Dependencies

Before touching code, catalog what your MCP tool actually depends on:

| Dependency Type | Examples | Migration Impact | |-----------------|---------|-----------------| | Language runtime | Node.js, Python, Go | Must compile to WASM target | | Network access | APIs, databases, services | Must declare in capabilities.network | | Filesystem access | Config files, temp dirs, logs | Must declare in capabilities.filesystem | | Environment variables | API keys, tokens, config | Must declare in capabilities.env_vars | | Native libraries | OpenSSL, libpq, sqlite3 | Must have WASM-compatible alternatives | | State | In-memory caches, databases | Use MPP KV store or declare filesystem |

The inventory determines migration complexity. A tool that calls one HTTP API and uses two environment variables is a straightforward migration. A tool that depends on libpq, reads from /etc/ssl/certs, and maintains an in-memory state machine requires more adaptation.


Step 2: Choose Your Compilation Path

MPP tools compile to wasm32-wasip1 (WebAssembly with WASI). Your path depends on your tool's current language:

Rust (Native Path)

The most direct route. The MPP SDK is Rust, and wasm32-wasip1 is a first-class Rust target.

# Add the target
rustup target add wasm32-wasip1

# Compile
cargo build --target wasm32-wasip1 --release

If your MCP tool is already in Rust, migration is primarily a matter of replacing the I/O layer (network listener → stdin/stdout) and adding the MPP SDK.

C / C++ (Emscripten or wasi-sdk)

# Using wasi-sdk
export WASI_SDK_PATH=/opt/wasi-sdk
$WASI_SDK_PATH/bin/clang --target=wasm32-wasi -o tool.wasm tool.c

C tools compile cleanly to WASM if they don't depend on platform-specific syscalls. Socket-based networking requires adaptation to use WASI's socket APIs.

Go (TinyGo)

tinygo build -o tool.wasm -target=wasi ./main.go

TinyGo's WASI support covers standard I/O, filesystem, and basic networking. The standard net/http package works for outbound HTTP calls. CGo dependencies will not compile.

Python (Experimental)

Python-to-WASM remains experimental. Options include Pyodide (CPython compiled to WASM) and RustPython. The resulting binaries are large (10–50 MB) and have startup overhead. For production tools, consider rewriting in Rust with the MPP SDK.

TypeScript / JavaScript (Experimental)

JavaScript engines can be compiled to WASM (e.g., via SpiderMonkey or QuickJS). This is viable but adds a JS runtime to the package size. For simple tools, the MPP SDK's Rust API is more efficient.


Step 3: Replace the I/O Layer

Your MCP tool reads JSON-RPC from a transport (stdio, HTTP, SSE) and writes responses back. In MPP, tools read from stdin and write to stdout — the WASM sandbox handles the framing.

Before (MCP Server in Rust)

// Typical MCP server — listens on stdio
use std::io::{self, BufRead, Write};
use serde_json::{json, Value};

fn main() {
    let stdin = io::stdin();
    let stdout = io::stdout();
    
    for line in stdin.lock().lines() {
        let request: Value = serde_json::from_str(&line.unwrap()).unwrap();
        let method = request["method"].as_str().unwrap();
        
        match method {
            "tools/list" => { /* return tool list */ }
            "tools/call" => {
                let tool_name = request["params"]["name"].as_str().unwrap();
                let args = &request["params"]["arguments"];
                let result = handle_tool_call(tool_name, args);
                writeln!(stdout.lock(), "{}", result).unwrap();
            }
            _ => { /* handle other methods */ }
        }
    }
}

After (MPP Tool with SDK)

use mpp_sdk::{tool_handler, ToolRequest, ToolResponse};
use serde_json::json;

fn main() {
    tool_handler(|req: ToolRequest| -> ToolResponse {
        // The same business logic as before
        let result = handle_tool_call(&req.tool, &req.arguments);
        ToolResponse::success(result)
    });
}

The MPP SDK handles JSON-RPC framing, stdin/stdout communication, error serialisation, and the tool dispatch protocol. Your handler function receives a typed ToolRequest and returns a ToolResponse. The boilerplate disappears.


Step 4: Write the Manifest

The manifest is where you declare your tool's identity, capabilities, and privacy configuration. This is the step where implicit assumptions become explicit declarations.

Before (MCP)

Your MCP tool implicitly has:

  • Access to all network endpoints
  • Access to all environment variables
  • Access to the entire filesystem
  • No privacy filtering
  • No capability restrictions

After (MPP Manifest)

{
  "mpp_version": "1.0.0",
  "package_id": "com.mycompany.crm-lookup",
  "version": "1.0.0",
  "name": "crm-lookup",
  "description": "Look up customer records in the CRM",
  "publisher": {
    "name": "My Company",
    "url": "https://mycompany.com",
    "verified_key_id": "pub_a1b2c3d4e5f67890"
  },
  "license": "MIT",
  "runtime": {
    "type": "wasm32-wasi",
    "entrypoint": "bin/main.wasm",
    "memory_limit_mb": 64,
    "timeout_ms": 30000
  },
  "capabilities": {
    "network": ["api.crm.mycompany.com"],
    "filesystem": {
      "read": [],
      "write": []
    },
    "env_vars": ["CRM_API_KEY"]
  },
  "tool_definitions": [
    {
      "name": "lookup",
      "description": "Look up a customer by ID or email",
      "parameters": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "description": "Customer ID or email address"
          }
        },
        "required": ["query"]
      },
      "returns": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "company": { "type": "string" },
          "status": { "type": "string" }
        }
      },
      "security_level": "medium"
    }
  ],
  "privacy_filters": {
    "enabled": true,
    "patterns": ["email", "phone", "credit_card"],
    "custom_patterns": [
      {
        "id": "customer_id",
        "regex": "CUST-\\d{8}",
        "description": "Internal customer identifier"
      }
    ]
  }
}

Writing the manifest forces you to answer questions that the MCP model never asks:

  • What network endpoints does this tool actually need?
  • Which environment variables does it actually read?
  • What sensitive data might appear in its output?
  • How sensitive are the operations it performs?

For most teams, this is the most valuable part of the migration — not because of the packaging itself, but because it surfaces the implicit trust assumptions that have been invisible in the MCP deployment.


Step 5: Build, Sign, and Verify

# Build the WASM binary and create the .mpp package
mpp build .

# Generate a signing keypair (once per publisher)
mpp keygen -o ~/.mpp/keys

# Sign the package
mpp sign crm-lookup-1.0.0.mpp -k ~/.mpp/keys/publisher.key

# Verify locally (same check the Gatekeeper will run)
mpp verify crm-lookup-1.0.0.mpp

The mpp verify command runs the full Gatekeeper pipeline: archive safety checks, size limits, path traversal detection, content hash computation, signature verification, manifest validation, and capability parsing. If it passes locally, it will pass on any conforming host.


Step 6: Test Equivalence

The critical question: does the MPP-packaged tool produce the same results as the original MCP server?

# Run the packaged tool locally with the full security pipeline
mpp run crm-lookup-1.0.0.mpp \
  --tool lookup \
  --args '{"query": "CUST-00012345"}'

Compare the output to the original MCP tool's output for the same input. The business logic output should be identical. The only differences should be:

  • Privacy filter redactions (which are new and intentional)
  • Error format (MPP uses structured JSON-RPC error objects)

For comprehensive testing, capture a set of representative inputs and outputs from the MCP tool, then replay them against the MPP package and diff the results. Automate this as part of your CI pipeline.


Step 7: Publish

# Register as a publisher (once)
mpp register --registry https://registry.corp.com

# Publish the package
mpp publish crm-lookup-1.0.0.mpp --registry https://registry.corp.com

The registry runs its own Gatekeeper verification on upload. Only valid, signed packages are accepted. Once published, any agent host configured to use this registry can discover and install the tool.


Migration Complexity by Tool Type

Not all tools migrate equally. Here's a realistic assessment:

Low Complexity (1-2 days)

  • Pure computation tools. Formatters, parsers, validators, calculators. No network, no filesystem, no state. Port the algorithm, wrap in the SDK, done.
  • Single-API tools. Call one HTTP endpoint, parse the response, return structured data. Declare one network capability, one or two env vars.

Medium Complexity (3-5 days)

  • Multi-API tools. Call multiple endpoints, aggregate results. Declare multiple network capabilities. May need to handle WASM networking differences.
  • Stateful tools. Use in-memory state between calls. Migrate state to the MPP KV store (SQLite-backed, declared in manifest).
  • File-processing tools. Read config files or write output. Map paths to the WASM filesystem sandbox.

High Complexity (1-2 weeks)

  • Tools with native dependencies. Depend on libpq, OpenSSL, or other native libraries that don't have WASM-compatible alternatives. May require finding alternative libraries or rewriting I/O layers.
  • Tools with complex state machines. Long-running processes that accumulate state. Need architectural adaptation to the MPP model of per-invocation execution.
  • Tools in languages with experimental WASM support. Python, Ruby, or languages where the WASM toolchain is immature.

Not Yet Practical

  • Tools that require raw socket access. WASI's socket support is still evolving. Tools that need low-level TCP/UDP access may need to wait.
  • Tools that embed large runtimes. A Python tool with heavy dependencies (NumPy, pandas) produces very large WASM binaries with significant startup overhead.

The Incremental Path

You don't need to migrate everything at once. The practical approach:

  1. Start with your highest-risk tools. Tools that access production databases, handle customer data, or call payment APIs. These benefit most from sandboxing and privacy filtering.

  2. Migrate your simplest tools next. Low-complexity tools build team familiarity with the packaging workflow without the debugging overhead of complex migrations.

  3. Run MPP and MCP tools side by side. Your agent host can resolve some tools via MPP (verified, sandboxed) and others via MCP (legacy, unverified). This is an explicit transitional state, not a permanent architecture.

  4. Automate packaging in CI/CD. Once the migration pattern is established, integrate mpp build, mpp sign, and mpp publish into your CI pipeline. New tools are born as MPP packages.

  5. Deprecate MCP servers as MPP coverage grows. When all active tools are available as MPP packages, the MCP servers can be decommissioned.

The goal is not a big-bang migration. It's a gradual shift from "tools run with full trust and no oversight" to "tools run verified, sandboxed, and audited." Each migrated tool is a step toward that goal.


For the complete tool authoring workflow, read Building Your First MPP Tool. For the SDK API reference, see the SDK Documentation. For why the packaging model matters, read Why AI Tool Portability Matters (coming soon).