opamp-core

Adding Your Own Custom Action

Before using this walkthrough, read Provider Command And Event Flow Guide for the full provider-side model, including history behavior and the rule that MCP activity should be excluded from UI-history requirements.

This guide explains how to add and deploy a new custom action end-to-end across:

It uses nullcommand as a template.

Baseline: How nullcommand Works

Provider baseline class:

Key parts in that class:

The provider exposes this command in the UI metadata endpoint (/api/commands/custom) and can queue it.

Current behavior: the consumer includes a built-in handler for org.mp3monster.opamp_provider.nullcommand that logs the dummyValue payload field when this command is executed.

Provider: Add a New Custom Action

1) Create a command implementation

Add a new file in:

Implement:

Model it after CommandNullCommand:

Use this naming/location/interface pattern because provider discovery scans opamp_provider.command_implementations, imports modules that match the command* convention, and registers classes that satisfy the command interfaces. The custom command factory then resolves discovered classes dynamically by capability/operation/action, so matching these conventions is what makes your command discoverable and queueable without per-command factory edits.

2) Provider command routing (custom commands: no per-command edit required)

Provider custom command routing uses wildcard handling for classifier custom, which means you do not add per-command entries in COMMAND_BUILDERS. This matters because queueing still validates (classifier, action) and uses _build_custom_command_payload(...) to emit ServerToAgent.custom_message, so custom commands can flow through one routing path while still being validated.

3) Restart provider

Restart the provider after code changes because command discovery/registration is built at startup and new modules are not re-discovered live.

If you want state continuity across restart (clients, pending approvals, queued command state), use state persistence:

  1. In the provider UI (Global Settings -> Server Settings -> State Persistence), set state_save_folder, retention_count, and autosave_interval_seconds_since_change, then click OK + Save.
  2. If persistence is currently disabled, enable it in config/opamp.json by setting provider.state_persistence.enabled to true (the UI currently manages persistence settings but does not toggle this enable flag), then restart the provider once.
  3. Check runtime status with GET /api/settings/diagnostic and confirm state_persistence_enabled is true.
  4. Optionally force a snapshot before restart using POST /api/settings/state/save.
  5. Restart provider with restore enabled so persisted state is loaded: opamp-provider --config-path ./config/opamp.json --restore

After restart, verify GET /api/settings/diagnostic again and check state_persistence.restore_status for the restore result.

Consumer: Add a Handler for Your Capability

1) Create handler class

Add a handler file in:

Subclass:

Implement:

Reference implementations:

Do this because consumer dispatch is capability-driven: handle_custom_message(...) resolves by CustomMessage.capability and executes the matching handler. If your handler FQDN does not exactly match what the provider sends, the message is rejected.

2) Ensure handler discovery is enabled

Set in consumer config:

The default client dynamically discovers handlers from:

via build_factory_lookup(...).

Set this because the handler registry stays empty when allow_custom_capabilities is false. Discovery is dynamic, but only from this folder and only for classes implementing CustomMessageHandlerInterface.

3) Restart consumer

Restart the consumer so the new handler module is discovered and loaded into the in-memory lookup created when client instances initialize.

Deploy and Verify

1) Verify provider metadata

Call:

Confirm your command appears with:

This confirms provider discovery worked and that the command is visible to UI/API clients before runtime dispatch testing.

2) Queue command

From UI:

Or via API with key/value pairs (include at minimum classifier, operation, capability).

Queueing verifies provider route validation and command normalization in /api/clients/<client_id>/commands.

3) Verify consumer execution

Check consumer logs for:

This confirms the end-to-end contract: provider emitted the expected capability/type/data and consumer mapped that capability to the intended handler.

Existing Code Changes Needed Today

For a new UI-visible custom action that behaves like nullcommand, the current codebase requires these edits:

  1. Add a new command implementation file under provider/src/opamp_provider/command_implementations/, because provider module/class discovery is startup-based and convention-driven.
  2. Add a consumer handler class under consumer/src/opamp_consumer/custom_handlers/ with matching capability FQDN, because custom messages are dispatched by capability and unmatched capabilities are rejected.
  3. Set consumer.allow_custom_capabilities=true in config, because handler discovery/lookup is disabled otherwise.

You do not need per-command edits in:

  1. provider/src/opamp_provider/commands.py for custom capability/operation mapping.
  2. provider/src/opamp_provider/app.py to register custom actions in COMMAND_BUILDERS.

Optional but recommended:

  1. Add tests in provider/tests for queueing and payload shape.
  2. Add tests in consumer/tests for handler discovery and execute_action(...).