Skip to main content
Version: ERPBridge + bridgectl ยท v0.5.0-alpha.2

External plugins

ERPBridge can send an ERP response to an externally operated HTTP plugin before it returns the final result to an MCP client or direct REST caller. A raw_response plugin can adapt bounded media, such as an ERP image to OCR text, before normal response processing. The plugin process is outside ERPBridge. ERPBridge stores its endpoint and configuration, but it does not install, start, download, upgrade, or schedule plugin code.

Separate deployment boundary

Deploy and operate plugin images separately. Use a pinned image from the ERPBridge-Plugins repository (or your own compatible process), then apply the matching ERPBridge resources.

Resource modelโ€‹

A Plugin identifies one exact plugin release and its endpoint:

apiVersion: erpbridge.io/v1
kind: Plugin
metadata:
name: response-transformer
version: 1.0.0
type: api
isActive: true
spec:
endpoint: https://plugin-host:9000
timeoutMilliseconds: 5000
auth:
type: bearer
credentialRef: PLUGIN_RESPONSE_TOKEN

The endpoint must be an absolute http or https URL without userinfo, query parameters, or a fragment. ERPBridge appends /v1/process. The timeout is bounded from 1 millisecond to 5 minutes. metadata.type is api or docker and defaults to api; it describes the plugin deployment boundary and does not make ERPBridge manage Docker.

Credentialed plugins support only bearer and api-key authentication. Store only an environment-variable reference, such as PLUGIN_RESPONSE_TOKEN or PLUGIN_RESPONSE_API_KEY; never put a token or key in the resource. API-key authentication can set a validated custom header:

spec:
endpoint: https://plugin-host:9000
auth:
type: api-key
credentialRef: PLUGIN_RESPONSE_API_KEY
# credentialSource: file # Reads ERPBRIDGE_CREDENTIALS_DIR/PLUGIN_RESPONSE_API_KEY
header: X-API-Key

Credentialed plugin endpoints must use HTTPS. For an isolated development fixture, add the exact normalized host:port to INSECURE_AUTH_ALLOWED_HOSTS; ERPBridge emits a warning and rejects all other credentialed HTTP endpoints. PLUGIN_ENDPOINT_ALLOWLIST must also contain the exact normalized host:port for a credentialed plugin. A raw_response binding requires that allowlist even when the plugin has no authentication. Raw admission also requires a configured API_AUTH_TOKEN, an authenticated admin request, an active HTTP-backed tool, and an explicit object-shaped output schema. Userinfo, query parameters, fragments, redirects, and broad wildcard allowlists are rejected.

credentialSource is optional and defaults to env. Set it to file only when ERPBRIDGE_CREDENTIALS_DIR is mounted in the plugin-calling ERPBridge process. ERPBridge reads the validated credentialRef filename immediately before each plugin request, fails closed on missing or invalid file content, and never falls back to the environment. Authenticated file-backed plugin bindings bypass the tool response cache.

A PluginBinding connects exact plugin and tool versions:

apiVersion: erpbridge.io/v1
kind: PluginBinding
metadata:
name: transform-orders
isActive: true
spec:
pluginRef:
name: response-transformer
version: 1.0.0
toolRef:
name: list-orders
version: 1.0.0
phase: after_response
priority: 10
failurePolicy: continue
config:
mode: safe

The supported phases are raw_response and after_response. Raw bindings run first, before responsePath and output-schema validation. After-response bindings run after successful normalization and validation. Each phase uses ascending priority order. A raw binding must target an active HTTP-backed tool with an explicit object-shaped final output schema. Missing raw admission requirements keep the binding inactive during reconciliation.

A binding never moves automatically to a new tool or plugin version. A plugin that changes the meaning or type of output requires a new MCP-visible tool name and exact tool version. For example, keep read-invoice for invoice objects and publish read-invoice-text version 1.0.0 for OCR text.

Soft deletion retains a resource with isActive: false. A plugin cannot be hard-deleted while an active binding references that exact version. Binding admission requires active exact plugin and tool references.

HTTP protocolโ€‹

The protocol is synchronous JSON over POST /v1/process.

Normalized responseโ€‹

An after_response binding receives only the normalized result:

{
"protocolVersion": "v1",
"invocationId": "generated-id",
"tool": {"name": "list-orders", "version": "1.0.0"},
"result": {"id": "order-1"},
"config": {"mode": "safe"}
}

Raw responseโ€‹

A raw_response binding receives the bounded ERP response before normalization:

{
"protocolVersion": "v1",
"invocationId": "generated-id",
"tool": {"name": "read-invoice-text", "version": "1.0.0"},
"rawResponse": {
"status": 200,
"contentType": "image/png",
"body": {"encoding": "base64", "value": "..."}
},
"config": {"mode": "ocr"}
}

encoding is json for one complete decoded JSON document. Empty, malformed, and non-JSON bodies use base64. Raw invocations omit result, while legacy normalized invocations retain result: null when the result is nil.

The plugin returns a JSON object with a result member:

{"result":{"id":"order-1","processed":true}}

The request does not contain original arguments, inbound headers, caller identity, caller tokens, or ERP credentials. Request and response JSON are limited to 1 MiB. ERPBridge disables redirects and retries, and it applies the request context and configured timeout. Authentication failures and plugin failures use safe errors; credential values, endpoints, payloads, and plugin error bodies are not returned or logged. The sample mock-plugin service reads MOCK_PLUGIN_API_KEY; ERPBridge resolves the same runtime value through the server-side PLUGIN_MOCK_API_KEY reference.

A raw plugin can transform the response body for the next phase. For successful 2xx responses, ERPBridge applies responsePath, validates the declared output schema, runs after-response plugins, and validates the final result. Terminal non-2xx responses, including 3xx, remain errors and skip success-only phases. With continue, the original captured response is used only when it satisfies the final schema. Otherwise a safe error is returned. The fail policy returns a generic tool error without exposing the endpoint, payload, credentials, or plugin response body. Direct invocation returns HTTP 500; MCP keeps HTTP 200 with result.isError=true. The continue policy returns the original schema-valid result when processing fails.

Cache and transportsโ€‹

Bindings run only on a cache miss. The cache stores the final transformed MCP result and never stores error results, so a cache hit bypasses ERP and plugins. Applying, changing, soft-deleting, or hard-deleting a plugin or binding flushes affected target-tool cache entries.

The same pipeline runs for MCP tools/call and POST /api/tools/invoke. MCP advertises the developer-owned final output schema and returns structured content with equivalent text content. An image-to-text tool can expose {text: string} as structured {text: ...} plus text containing only the extracted text. Tool execution errors use isError: true. The direct-invoke endpoint preserves its { "result": ... } compatibility envelope. A tool without an active binding keeps its existing result and invocation payload.

Control-plane API and CLIโ€‹

Plugin and binding routes use the admin authentication policy:

  • POST and GET /apis/erpbridge.io/v1/plugins
  • DELETE /apis/erpbridge.io/v1/plugins?name=<name>&version=<version>
  • POST and GET /apis/erpbridge.io/v1/pluginbindings
  • DELETE /apis/erpbridge.io/v1/pluginbindings?name=<name>

Plugin deletion is soft by default. Add hard=true for permanent deletion. The server returns 409 Conflict when an active binding protects a plugin.

Use the CLI for declarative files:

bridgectl plugin validate -f plugin.yaml
bridgectl plugin apply -f plugin.yaml
bridgectl plugin get response-transformer@1.0.0 -o yaml
bridgectl plugin binding validate -f binding.yaml
bridgectl plugin binding apply -f binding.yaml
bridgectl plugin binding get transform-orders -o yaml

Apply accepts JSON, YAML sequences, multi-document YAML, and resource directories. Use --hard --yes for a non-interactive permanent delete.

Credential rotation and legacy migrationโ€‹

Environment references remain the default. To rotate a file-backed plugin credential without restarting ERPBridge, replace the mounted reference-named file with a complete provider-projected generation or, on a writable local mount, use a same-directory temporary file and atomic rename. Provider refresh is eventual; mount a directory rather than Kubernetes subPath, and expect replicas to observe changes at different times. AWS EKS ASCP, AKS Key Vault CSI, and GKE Secret Manager CSI can provide the mount. Use workload identity and least privilege. Adding a new cloud secret may require changing the provider mapping and rolling the workload before its filename is mounted.

To rotate an environment-backed plugin credential, update the PLUGIN_* environment value and recreate or roll the deployment. ERPBridge never stores the resolved value. Cache bypass is automatic for authenticated file-backed plugin bindings; environment-backed bindings keep normal cache behavior.

Older local API registries may contain authKey or authToken. State-changing API commands and bridgectl api test stop until an operator runs the explicit, destructive migration:

bridgectl api scrub-credentials --yes
bridgectl api set-credential-ref get-invoices --credential-ref ERP_API_KEY

Scrubbing atomically removes the legacy fields and does not create a plaintext backup. Set a new environment-backed reference after the scrub.

Docker integration fixtureโ€‹

The ERPBridge repository contains an opt-in test that uses the deterministic mock-plugin from the separate polyrepo and the pinned MockERP fixture:

make test-plugin-integration

The script uses the isolated Compose project erpbridge-plugin-test, waits for both HTTP services, runs MCP initialize/list/call and direct-invoke assertions, and removes only that project's containers, network, and volumes on exit.