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:
- MCP — listed on the Signals API MCP server as
plugin:{package}:{name} - CLI — listed by
php artisan signals:api list --pluginsand invoked withsignals: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):
namemust use the package vendor prefix (acme.foracme/hello)permissionmust be in the plugin'spermissions[]or a registered core permissionactionmust be a fully-qualified class name (existence checked at registrar wiring)surfacesis a non-empty subset ofmcp,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(plusmcp.access/cli.accessat the transport layer). read_only/destructivemap 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 undertests/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.