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.
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:
POSTandGET /apis/erpbridge.io/v1/pluginsDELETE /apis/erpbridge.io/v1/plugins?name=<name>&version=<version>POSTandGET /apis/erpbridge.io/v1/pluginbindingsDELETE /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.