Overview

MPE Flow is a multi-protocol workflow orchestration engine. It runs as either a desktop GUI or a CLI tool — from the same binary.

Two Modes, One Binary

  • No arguments → Opens the Tauri desktop GUI
  • With arguments → Runs in CLI mode

On Linux, only the CLI binary is available — no GUI dependencies required.

Installation

Windows & macOS

Download the installer from the Changelog page. The same binary supports both GUI and CLI modes.

Linux (CLI)

Download the pre-built binary and add it to your PATH:

# Download and extract
curl -LO https://download.mpe.run/releases/latest/mpe-vX.Y.Z-linux-x86_64.tar.gz
tar xzf mpe-vX.Y.Z-linux-x86_64.tar.gz

# Make executable and move to PATH
chmod +x mpe
sudo mv mpe /usr/local/bin/

# Verify
mpe --help

Note

The Linux build is CLI-only — no GUI, no GTK/WebKit dependencies required.

Quick Example

Create a file hello.mpf:

{
  "_version": 1,
  "flow": {
    "uuid": "example-flow",
    "name": "Hello World",
    "nodes": [
      {
        "uuid": "entry-1",
        "type": "entry",
        "name": "Start",
        "data": { "type": "entry" }
      },
      {
        "uuid": "http-1",
        "type": "http",
        "name": "Fetch Data",
        "data": {
          "type": "http",
          "url": "https://httpbin.org/get",
          "method": "get"
        }
      },
      {
        "uuid": "end-1",
        "type": "end",
        "name": "End",
        "data": { "type": "end" }
      }
    ],
    "connections": [
      {
        "id": "conn-1",
        "source_node_uuid": "entry-1",
        "target_node_uuid": "http-1",
        "source_port_id": "out",
        "target_port_id": "in"
      },
      {
        "id": "conn-2",
        "source_node_uuid": "http-1",
        "target_node_uuid": "end-1",
        "source_port_id": "true",
        "target_port_id": "in"
      }
    ]
  }
}

Run it:

mpe run -f hello.mpf

CLI 命令参考

命令 说明 示例
mpe run 执行工作流文件 (.mpf) mpe run -f flow.mpf
mpe validate 校验工作流结构(不执行) mpe validate -f flow.mpf
mpe debug 单步调试输出(NDJSON 协议) mpe debug -f flow.mpf
mpe stress 运行压力测试 mpe stress run -f flow.mpf
mpe flow 工作流管理(list, show, create, delete) mpe flow list
mpe report 报告管理(list, show, delete) mpe report list
mpe run-node 单节点协议连通性验证 mpe run-node '{"type":"redis:connect","host":"127.0.0.1","port":6379}'
mpe plugin 插件管理(dir, list, install) mpe plugin list
# 查看所有命令
mpe --help

# 查看特定命令帮助
mpe run --help

CI/CD 集成 (GitHub Actions)

mpe run 支持将执行结果导出为标准 JUnit XML 格式,可直接被 Jenkins、GitHub Actions 和 GitLab CI 等持续集成平台识别与解析。工作流中的每个节点对应一个 <testcase>;执行失败和跳过的节点分别映射为 <failure><skipped> 元素。

# 导出 JUnit XML 报告供 CI 使用
mpe run -f flow.mpf --report-format junit --report-file results.xml

# 或者直接将 JUnit XML 输出到 stdout(不带 --report-file)
mpe run -f flow.mpf --report-format junit

在仓库中添加工作流步骤,通过 dorny/test-reporter 在 Pull Request 中自动执行工作流并展示测试报告:

name: Flow Tests

on:
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run MPE flows
        run: |
          mpe run -f tests/flows/api-smoke.mpf --report-format junit --report-file results.xml

      - name: Publish test report
        if: always()
        uses: dorny/test-reporter@v1
        with:
          name: MPE Flow Results
          path: results.xml
          reporter: java-junit
          fail-on-error: 'true'

提示

JUnit 报告如实反映节点级别的测试结果:断言、HTTP、WebSocket 等协议节点映射为独立的测试用例。任何节点执行失败都会导致非零退出码,因此 mpe run --report-format junit --report-file results.xml 默认会在失败时使 CI Job 报错中断。

Output Format

All commands output JSON to stdout:

{
  "success": true,
  "data": null,
  "error": null,
  "execution_time": 156,
  "node_reports": [
    {
      "node_uuid": "entry-001",
      "node_type": "entry",
      "node_name": "Start",
      "status": "success",
      "duration_ms": 1,
      "used_port_id": "out"
    },
    {
      "node_uuid": "http-001",
      "node_type": "http",
      "node_name": "Fetch Data",
      "status": "success",
      "duration_ms": 233,
      "output_data": {
        "success": true,
        "status": 200,
        "body": { ... },
        "headers": { ... }
      }
    }
  ],
  "flow_report": {
    "flow_name": "Hello World",
    "total_nodes": 3,
    "executed_count": 3,
    "status": "success",
    "duration_ms": 234
  }
}

Exit Codes

Code Meaning
0 Success
1 Failure (error details in stderr and JSON output)
2 Invalid arguments

Flow File Structure

MPE flow files use the .mpf extension and follow the FlowFile wrapper format:

{
  "_version": 1,
  "flow": {
    "uuid": "my-flow",
    "name": "My Flow",
    "initial_variables": {
      "api_base": "https://api.example.com",
      "token": "abc123"
    },
    "nodes": [ ... ],
    "connections": [ ... ]
  }
}

Top-level Fields

Field Type Required Description
_version number No File format version (default: 1)
flow object Yes Flow data container

Flow Fields

Field Type Required Description
uuid string Yes Unique flow identifier
name string Yes Human-readable flow name
initial_variables object No Variables available via {{key}} syntax
nodes array Yes List of nodes
connections array Yes Connections between nodes

Node Structure

{
  "uuid": "node-uuid",
  "type": "http",
  "name": "My HTTP Request",
  "data": {
    "type": "http",
    "url": "https://api.example.com",
    "method": "get"
  }
}
Field Type Required Description
uuid string Yes Unique node identifier
type string Yes Node type (e.g. http, entry)
name string Yes Display name
data object Yes Node configuration. Must include "type" matching the node type
on_error string No Error strategy: "route_to_false" (default), "ignore", "abort_flow"

Important: data.type Field

Every data object must include "type": "<node_type>" for Rust deserialization. The value must match the node's top-level type field.

Connections

Connections define the execution flow between nodes.

{
  "id": "conn-1",
  "source_node_uuid": "entry-1",
  "target_node_uuid": "http-1",
  "source_port_id": "out",
  "target_port_id": "in"
}
Field Description
source_node_uuid UUID of the source node
target_node_uuid UUID of the target node
source_port_id Output port: out (single output), true/false (dual output)
target_port_id Must be "in" for all nodes

Critical Port Rules

  • target_port_id must always be "in" (not "input")
  • Nodes with dual output (true/false) should have both ports connected to prevent flow termination on failure
  • entry uses out, end has no output

Port Reference

Node Type Input Output
entrynoneout
endinnone
httpintrue / false
conditionalintrue / false
scriptintrue / false
assertionintrue / false
variable_extractorinout
ws_*, tcp_*, udp_*, sse_connect/sse_listen, graphql_connect/query/subscribe/introspectintrue / false
sse_disconnect, graphql_disconnect, proxyinout

Variables

Variable Pool vs Node Output

{{var}} syntax can only access the Variable Pool. Protocol node outputs are stored separately in last_node_output and do NOT automatically become variables.

To use node output data with {{var}}, you must first extract it using a variable_extractor or script node.

Syntax

Syntax Description
{{variable_name}} Reference a variable from the pool
{{obj.field}} Access nested field with dot notation

Adding Variables to the Pool

  1. Initial variables: Define in flow.initial_variables
  2. variable_extractor: Extract from last_node_output using JSONPath
  3. script: Use fn.variables.set(name, value)
// variable_extractor example
{
  "type": "variable_extractor",
  "data": {
    "type": "variable_extractor",
    "output_mappings": [
      { "source": "$.body.id", "target": "user_id" },
      { "source": "$.status", "target": "http_status" }
    ]
  }
}

// Later use: {{user_id}}, {{http_status}}

Conditional / Assertion Paths

conditional and assertion nodes read directly from last_node_output:

Node Path Format Example
conditional.field_path Plain name, {{var}}, or $.path status, $.body.id
assertion.target JSONPath with $. prefix (required) $.status, $.body.data.id

Limitations

  • Only dot notation: {{obj.field.subfield}}
  • No array indexing: {{items[0]}} — not supported
  • No function calls: {{func()}} — not supported

Entry & End Nodes

Entry Node

Starting point of every flow. Exactly one required.

{
  "uuid": "entry-1",
  "type": "entry",
  "name": "Start",
  "data": { "type": "entry" }
}

End Node

Terminal node. Flow ends when reaching end.

{
  "uuid": "end-1",
  "type": "end",
  "name": "End",
  "data": { "type": "end" }
}

HTTP Node

Send HTTP requests. Supports GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS.

{
  "uuid": "http-1",
  "type": "http",
  "name": "API Call",
  "data": {
    "type": "http",
    "url": "{{api_base}}/users",
    "method": "post",
    "headers": {
      "Authorization": "Bearer {{token}}"
    },
    "body_type": "body",
    "content_type": "json",
    "body": "{\"name\": \"test\"}",
    "timeout_ms": 30000
  }
}

Core Fields

Field Type Default Description
url string Request URL (supports {{var}})
method string get HTTP method
headers object {} Request headers
body_type string none none / body / form-data / x-www-form-urlencoded / binary
content_type string json json / text / html / xml
timeout_ms number 30000 Timeout (1000–300000ms)

HTTP Output

{
  "success": true,
  "status": 200,
  "body": { "id": 1, "name": "test" },
  "headers": { "content-type": "application/json" },
  "timing": { "connect_ms": 45, "total_ms": 150 }
}

Conditional Node

Branch execution based on conditions. Routes to true or false port.

Simple Condition

{
  "uuid": "cond-1",
  "type": "conditional",
  "name": "Check Status",
  "data": {
    "type": "conditional",
    "condition_type": "simple",
    "field_path": "status",
    "operator": "equal",
    "expected_value": 200
  }
}

Advanced Expression

{
  "data": {
    "type": "conditional",
    "condition_type": "advanced",
    "advanced_expression": "status >= 200 && status < 300"
  }
}

Operators

Operator Description
equalEquals
not_equalNot equals
greaterGreater than
greater_or_equalGreater or equal
lessLess than
less_or_equalLess or equal
containsContains substring
starts_withStarts with
ends_withEnds with
existsField exists

Script Node

Execute JavaScript code in a QuickJS runtime (ES2020 subset).

{
  "uuid": "script-1",
  "type": "script",
  "name": "Process Data",
  "data": {
    "type": "script",
    "script": "const data = JSON.parse(fn.response.raw); fn.variables.set('user_id', data.body.id);",
    "timeout_ms": 5000
  }
}

Script API

API Description
fn.variables.get(name)Read variable from pool
fn.variables.set(name, value)Write variable to pool
fn.response.rawFull upstream node output (JSON string)
fn.response.codeHTTP status code (HTTP nodes only)
fn.console.log(msg)Log to execution output
fn.flow.stop(reason)Stop flow execution
fn.util.encodeBase64(str)Base64 encode
fn.util.md5(str)MD5 hash
fn.util.sha256(str)SHA-256 hash
fn.util.uuid()Generate UUID v4
fn.util.sleep(ms)Pause execution

Assertion Node

Validate conditions with detailed pass/fail reporting.

{
  "uuid": "assert-1",
  "type": "assertion",
  "name": "Validate Response",
  "data": {
    "type": "assertion",
    "mode": "all",
    "assertions": [
      { "id": "a1", "description": "Status is 200", "target": "$.status", "operator": "eq", "expected": 200 },
      { "id": "a2", "description": "Has data", "target": "$.body.data", "operator": "exists" }
    ]
  }
}

Assertion Operators

Category Operators
Comparisoneq, ne
Numericgt, gte, lt, lte
Stringcontains, not_contains, matches
Existenceexists, not_exists
Collectionin, not_in
Typetype_is
Rangebetween

Target Path Format

Assertion target must use JSONPath with $. prefix (e.g. $.status, $.body.id). Do not add body. prefix for non-HTTP nodes.

WebSocket Nodes

Three nodes for WebSocket communication: connect, send/collect, close.

// Connect
{ "type": "ws_connect", "data": { "type": "ws_connect", "url": "wss://echo.example.com/ws" } }

// Send and collect response
{ "type": "ws_send_collect", "data": {
    "type": "ws_send_collect",
    "connection_id": "ws-connect-uuid",
    "message": "{\"type\":\"ping\"}",
    "collect_timeout_ms": 10000
  }
}

// Close
{ "type": "ws_close", "data": { "type": "ws_close", "connection_id": "ws-connect-uuid" } }

Other Protocols

TCP

{ "type": "tcp_connect", "data": { "type": "tcp_connect", "host": "localhost", "port": 8080 } }
{ "type": "tcp_send", "data": { "type": "tcp_send", "connection_id": "uuid", "data": "Hello" } }
{ "type": "tcp_receive", "data": { "type": "tcp_receive", "connection_id": "uuid" } }
{ "type": "tcp_close", "data": { "type": "tcp_close", "connection_id": "uuid" } }

UDP

{ "type": "udp_bind", "data": { "type": "udp_bind", "local_addr": "0.0.0.0", "local_port": 8888 } }
{ "type": "udp_send_to", "data": { "type": "udp_send_to", "connection_id": "uuid", "data": "Hello", "target_addr": "192.168.1.100", "target_port": 9999 } }
{ "type": "udp_recv_from", "data": { "type": "udp_recv_from", "connection_id": "uuid" } }
{ "type": "udp_close", "data": { "type": "udp_close", "connection_id": "uuid" } }

SSE (Server-Sent Events)

{ "type": "sse_connect", "data": { "type": "sse_connect", "url": "https://api.example.com/events" } }
{ "type": "sse_listen", "data": { "type": "sse_listen", "connection_id": "uuid", "max_events": 100 } }
{ "type": "sse_disconnect", "data": { "type": "sse_disconnect", "connection_id": "uuid" } }

GraphQL

{ "type": "graphql_connect", "data": { "type": "graphql_connect", "endpoint": "https://api.example.com/graphql" } }
{ "type": "graphql_query", "data": { "type": "graphql_query", "connection_id": "uuid", "query": "query { users { id name } }" } }
{ "type": "graphql_subscribe", "data": { "type": "graphql_subscribe", "connection_id": "uuid", "query": "subscription { newUser { id } }" } }
{ "type": "graphql_introspect", "data": { "type": "graphql_introspect", "connection_id": "uuid" } }
{ "type": "graphql_disconnect", "data": { "type": "graphql_disconnect", "connection_id": "uuid" } }

Plugin Protocols

More protocols (Redis, MySQL/PostgreSQL, MongoDB, SMTP, gRPC, MCP) are available as installable plugins — see Plugin Development below.

Plugin Overview

Plugins extend MPE Flow with new protocol node types. A plugin runs as a sidecar process that communicates with the host over stdio using JSON-RPC 2.0 — one JSON message per line (LF-delimited, CRLF tolerated). At startup the host spawns each plugin process and performs a describe handshake, registering the declared node types into the shared NodeRegistry. Plugin nodes behave identically to built-in nodes in flows, the executor, and execution reports.

Installation

Place a plugin directory containing a plugin.json manifest into the plugin directory (default get_data_dir()/plugins; %APPDATA%/multi-protocol-flow-executor/plugins on Windows; override with the MPE_PLUGIN_DIR environment variable). The host scans synchronously at startup: invalid manifests are skipped without crashing, and a plugin whose describe fails 3 consecutive times is quarantined for the host's lifetime.

plugin.json Fields

Field Description
namePlugin name (convention: directory name)
versionSemantic version
descriptionHuman-readable description
min_host_versionOptional version gate for host compatibility
entrycommand + args used to launch the plugin; interpreter plugins are first-class citizens (e.g. "python" + ["plugin.py"])
envEnvironment variables injected into the plugin process
permissionsReserved; ignored in P0
capabilities.streamingtrue = resident process; false = spawned on demand and recycled after 60s idle
capabilities.single_nodeNode-level capability; true = runnable standalone via mpe run-node for connectivity verification
locales / default_localeUI language declarations; the host injects the active locale via MPE_LOCALE

Protocol Plugins in This Repository

The plugins/ directory ships protocol plugins for: amqp, db (SQLite/MySQL/PostgreSQL), grpc, imap, kafka, mcp, mongo, mqtt, redis, smtp. Install by building a plugin and placing its directory into the plugin directory, or install from the Plugins page.

SDK & Development

The SDK and wire contract live in the standalone public repository multi-protocol-flow/mpe-plugin-sdk. The Rust SDK is first-class; lightweight Python and Node templates are also provided. Wire types are shared from mpe-plugin-sdk::protocol, so host and plugin never drift.

Development Flow

  1. Clone the SDK repository
  2. Implement the Plugin trait (describe + execute; optional flow_ended to release per-execution resources)
  3. Build the sidecar binary
  4. Place it into the plugin directory

Reference Docs

  • SDK repository: docs/plugin-guide.md (EN) / docs/plugin-guide.zh-CN.md (中文) and docs/plugin-protocol.md
  • Host repository: docs/plugin-architecture.md, docs/plugin-perf.md (visible under the multi-protocol-flow GitHub organization)

Marketplace

Plugins can be installed from the GUI settings panel's Plugin Market tab, or from the CLI:

mpe plugin dir
mpe plugin list
mpe plugin install <name> [--version <v>] [--registry <url>] [--dir <dir>] [--force]

Registry Contract

  • GET {base}/plugins returns the plugin list; each entry has a platform asset (zip) url and an optional sha256 — when provided the host enforces checksum validation and refuses mismatched installs.
  • Platform keys: windows-x64 | windows-arm64 | linux-x64 | linux-arm64 | macos-x64 | macos-arm64.
  • The zip root must contain a single top-level directory named after the plugin, holding plugin.json and the plugin files.

The registry base URL is configured via the MPE_PLUGIN_REGISTRY environment variable or the --registry flag. The public registry is served from GitHub Pages (multi-protocol-flow.github.io/mpe-plugin-registry) as a static JSON implementing this contract — browse and install plugins on the Plugins page.