SIGNALS Documentation
API Reference

Plugin MCP / CLI tools

Declare and register plugin tools for the Signals API MCP server and signals:api CLI.

Overview

Plugins can expose first-class tools on two sibling surfaces that share the same definition:

  1. MCP — listed on the Signals API MCP server as plugin:{package}:{name}
  2. CLI — listed by php artisan signals:api list --plugins and invoked with signals:api call plugin:{package}:{name}

Both paths use PluginToolRegistry + PluginToolInvoker. Permission checks run as the acting MCP/CLI user via Gate.

Declaring tools in signals.yaml

tools:
  - name: acme.hello.echo          # vendor-prefixed like permissions
    title: Echo Hello
    description: Echo a message back to the caller.
    action: Acme\Hello\Actions\EchoTool   # FQCN of an invocable class
    permission: acme.hello.view           # declared plugin permission OR core permission
    read_only: true
    destructive: false
    surfaces: [mcp, cli]                  # default when omitted: both
    args:
      - name: message
        type: string                      # string | integer | number | boolean
        required: false
        description: Optional message to echo.

Validator rules (see Manifest reference):

  • name must use the package vendor prefix (acme. for acme/hello)
  • permission must be in the plugin's permissions[] or a registered core permission
  • action must be a fully-qualified class name (existence checked at registrar wiring)
  • surfaces is a non-empty subset of mcp, cli

Registering at boot

public function register(PluginRegistrar $registrar): void
{
    $registrar->permission('acme.hello.view', [/* … */]);

    // Name must match tools[]; action FQCN must match the declared action.
    $registrar->tool('acme.hello.echo', EchoTool::class);
}

Scoped registrars enforce wiring ⊆ declaration (assertToolDeclared). Unscoped registrars (tests) accept the optional metadata arguments on tool().

Action class contract

namespace Acme\Hello\Actions;

class EchoTool
{
    /** @param  array<string, mixed>  $args */
    public function __invoke(array $args): array
    {
        return ['message' => (string) ($args['message'] ?? 'hello')];
    }
}

Return an array for structured MCP/CLI JSON output. Scalar returns are wrapped as {"result": …}.

Permissions and annotations

  • Invocation always requires the declared permission (plus mcp.access / cli.access at the transport layer).
  • read_only / destructive map to MCP annotation hints (readOnlyHint, destructiveHint).
  • Disable a plugin (PluginStatus ≠ Enabled) and the bridge stops exposing its tools — same pattern as dashboard widget bridging.

Surfaces

Surface How to reach
mcp MCP client → Signals API server tool list / call
cli signals:api list --plugins · signals:api call plugin:acme/hello:acme.hello.echo --data='{"message":"hi"}' --user=1

Omit a surface to hide the tool from that transport while keeping the other.

Testing

  • Unit: PluginToolRegistry, ManifestTool / invalid fixtures under tests/Fixtures/plugins/invalid/
  • Feature: registrar assertToolDeclared, bridge enable/disable flush, MCP exposure, CLI list/call, permission denial, surface filtering
  • Enable settings in setup: settings()->set('mcp.enabled', true, 'boolean') / api_cli.enabled (see MCP/CLI settings gating)

Reference implementation: plugins/signals/slots-demo tool signals.slotsdemo.echo.