Skip to content

MCP tools

Every tool the loom server exposes over php artisan loom:mcp, with its inputs and the shape of what it returns. For how to register the server and what to ask it, see Ask an agent about your events.

All tools are read-only and return one JSON document as text. Tool names, input names and the output key names below are stable as of 1.0: renaming or removing one is a major change; adding an optional input or an output key is minor. The examples below are real responses from a small order app in which OrderController fires OrderPlaced, SendReceipt handles it, and SendReceipt fires ReceiptSent and queues SendMail.

Tool Answers Inputs
list-entities What did the scan find in one section? section
get-entity What is the full record for one class? kind, fqcn
dispatch-sites-for Where is this event fired? event_fqcn
handlers-for Who handles this event? event_fqcn
dispatches-from What does this method fire directly? method_fqcn
events-following What does this event set off, transitively? event_fqcn, depth
events-from-method What does this method set off, transitively? method_fqcn, depth
route-to-events What does this HTTP route set off? method, uri, depth
impact-of-change What does removing or renaming this class affect? fqcn, change
find-orphans Which events and listeners are dead weight? none
find-unresolved-dispatches Which dispatches couldn't Loom resolve? none

Conventions

Class names are fully qualified and case-sensitive, written the way PHP writes them (App\Events\OrderPlaced). In JSON output the backslashes are escaped, so they appear as App\\Events\\OrderPlaced.

depth is an integer number of handler-to-dispatch hops, default 3. Values outside 1 to 6 are clamped, not rejected. The response echoes the depth actually used.

A method reference (method_fqcn) is Class::method, Class@method, or a bare Class for every method it has.

Output keys are snake_case. Wherever a handler is described, handler_kind is listener or closure.

Errors come back as a text message instead of JSON. Empty results are not errors, except where a tool below says otherwise.

Chain output always carries truncated (true when depth cut a chain short) and cycles (events revisited on a path). events_reached lists the events whose handlers were expanded, so a target that isn't in it was left unexpanded.

Lookups

list-entities

Lists one section of the index verbatim, with a count. Use it to find a class name before asking about it.

Input Type Required Notes
section string yes One of events, listeners, observers, model_events, jobs, routes, scheduled_tasks, mailables, notifications, unresolved_dispatches, closure_listeners
{
  "section": "jobs",
  "count": 1,
  "items": [
    {
      "fqcn": "App\\Jobs\\SendMail",
      "file": "app/Jobs/SendMail.php",
      "line": 6,
      "queued": true,
      "queue_config": null,
      "dispatched_from": [{ "file": "app/Listeners/SendReceipt.php", "line": 9, "method": "dispatch" }],
      "dispatches": [
        { "target": "App\\Events\\Pong", "kind": "event", "confidence": "high", "file": "app/Jobs/SendMail.php", "line": 15 }
      ]
    }
  ]
}

items has the same fields as that section of the index. An unknown section returns Unknown section [widgets]; expected one of: events, listeners, ... with the full list.

get-entity

Fetches one class's record by kind and name.

Input Type Required Notes
kind string yes One of event, listener, observer, job, mailable, notification
fqcn string yes Fully qualified class name
{
  "kind": "listener",
  "fqcn": "App\\Listeners\\PingListener",
  "found": true,
  "entity": {
    "fqcn": "App\\Listeners\\PingListener",
    "file": "app/Listeners/PingListener.php",
    "line": 5,
    "handles": [{ "event": "App\\Events\\Ping", "method": "handle" }],
    "registration": "auto_discovered",
    "queued": true,
    "dispatches": [
      { "target": "App\\Events\\Pong", "kind": "event", "confidence": "high", "file": "app/Listeners/PingListener.php", "line": 7 }
    ]
  }
}

A missing class returns No job found for App\Nope. (with the kind you asked for), and an unknown kind returns Unknown kind [route]; expected one of: event, listener, observer, job, mailable, notification.

entity is the raw index entry, snake_case, identical to what list-entities and index.json carry.

Edges

One hop, no recursion.

dispatch-sites-for

Input Type Required Notes
event_fqcn string yes The event class
{
  "event": "App\\Events\\OrderPlaced",
  "count": 1,
  "dispatch_sites": [{ "file": "app/Http/Controllers/OrderController.php", "line": 20, "method": "dispatch" }]
}

An event nothing fires returns "count": 0 and an empty list. An empty event_fqcn returns event_fqcn is required.

handlers-for

Input Type Required Notes
event_fqcn string yes The event class
{
  "event": "App\\Events\\ReceiptSent",
  "count": 2,
  "listeners": [{ "listener": "App\\Listeners\\ArchiveReceipt", "method": "handle", "queued": false }],
  "closure_listeners": [{ "file": "app/Providers/EventServiceProvider.php", "line": 30, "queued": true }]
}

count is listeners plus closure listeners. Closure listeners have no class name, so they're identified by file and line.

dispatches-from

Input Type Required Notes
method_fqcn string yes Class::method, Class@method, or a bare Class
{
  "method_fqcn": "App\\Http\\Controllers\\OrderController::store",
  "count": 2,
  "dispatches": [
    { "target": "App\\Events\\OrderPlaced", "kind": "event", "confidence": "high", "file": "app/Http/Controllers/OrderController.php", "line": 20 },
    { "target": "App\\Jobs\\SendMail", "kind": "job", "confidence": "high", "file": "app/Http/Controllers/OrderController.php", "line": 22 }
  ]
}

Only routes resolve to an exact method. For a listener, observer or job, the answer covers the whole class.

Chains

events-following

Follows an event to its handlers, what they dispatch, and the handlers of those events, down to depth.

Input Type Required Default Notes
event_fqcn string yes none The event to start from
depth integer no 3 Clamped to 1-6
{
  "root": "App\\Events\\ReceiptSent",
  "depth": 3,
  "edges": [
    { "event": "App\\Events\\ReceiptSent", "handler": "App\\Listeners\\ArchiveReceipt::handle", "handler_kind": "listener", "dispatches": [] },
    {
      "event": "App\\Events\\ReceiptSent",
      "handler": "app/Providers/EventServiceProvider.php:30",
      "handler_kind": "closure",
      "dispatches": [
        { "target": "App\\Events\\Ping", "kind": "event", "confidence": "high", "file": "app/Providers/EventServiceProvider.php", "line": 32 }
      ]
    }
  ],
  "events_reached": ["App\\Events\\ReceiptSent", "App\\Events\\Ping"],
  "cycles": [],
  "truncated": false
}

Each edge is one handler of one event. handler_kind is listener or closure, and a closure handler is named file:line. Each event is expanded once, so a cycle doesn't repeat. An event that isn't in the index returns an empty edges list and no error.

events-from-method

Starts from a method: what it dispatches, then the chain following each dispatched event.

Input Type Required Default Notes
method_fqcn string yes none Class::method, Class@method, or a bare Class
depth integer no 3 Clamped to 1-6, applied per chain

The response is { "method_fqcn", "dispatches", "chains" }. dispatches is what dispatches-from returns, and chains holds one events-following result per dispatched event. An unknown method returns empty dispatches and chains.

route-to-events

Resolves an HTTP route to its controller method, then behaves like events-from-method.

Input Type Required Default Notes
method string yes none HTTP verb, case-insensitive
uri string yes none Matched exactly; leading slash optional
depth integer no 3 Clamped to 1-6
{
  "route": {
    "method": "POST",
    "uri": "orders",
    "name": "orders.store",
    "controller_fqcn": "App\\Http\\Controllers\\OrderController",
    "controller_method": "store",
    "middleware": ["web"],
    "file": "routes/web.php",
    "line": 12
  },
  "note": null,
  "chain": {
    "method_fqcn": "App\\Http\\Controllers\\OrderController::store",
    "dispatches": ["..."],
    "chains": ["..."]
  }
}

chain holds what events-from-method returns. note is always present: null when chain resolved, and a sentence when it did not. A route whose action is a closure returns the route with "chain": null and that note. A verb and URI with no match returns No route found for GET orders. and an error.

Analysis

impact-of-change

Reports what removing or renaming a class touches.

Input Type Required Default Notes
fqcn string yes none An event, listener or job
change string no remove remove or rename. Changes the wording of notes only

For a listener, the response says which events it handles and which would be left with no handler:

{
  "fqcn": "App\\Listeners\\ArchiveReceipt",
  "change": "remove",
  "kind": "listener",
  "handles": ["App\\Events\\ReceiptSent"],
  "would_orphan_events": [],
  "dispatches": [],
  "notes": [
    "No handled event would be left without a handler by this change.",
    "Downstream dispatches from this class are listed; their own chains are not expanded here."
  ]
}

For an event ("kind": "event"), the response has dispatchers, handlers, and a downstream chain instead; each handlers entry is { handler, handler_kind }, named the same way as a chain edge: Class::method for a listener and file:line for a closure. A class Loom doesn't know returns "kind": "unknown", the event-shaped keys with "downstream": null, and a note. An unrecognized change returns Unknown change [explode]; expected one of: remove, rename.

find-orphans

Takes no input. Returns events that nothing fires and nothing handles, and listeners that handle no event.

{
  "orphan_events": [{ "fqcn": "App\\Events\\Lonely", "kind": "class", "file": "app/Events/Lonely.php", "line": 10 }],
  "idle_listeners": [{ "fqcn": "App\\Listeners\\Idle", "file": "app/Listeners/Idle.php", "line": 5 }]
}

An event that's fired only from a dispatch Loom couldn't resolve can look orphaned. Cross-check with find-unresolved-dispatches.

find-unresolved-dispatches

Takes no input. Returns every dispatch Loom couldn't trace to a class.

{
  "count": 1,
  "unresolved_dispatches": [
    { "file": "app/A.php", "line": 10, "expression": "dispatch($job)", "reason": "dynamic_class_name" }
  ]
}

The reason values and their fixes are in Why was my code missed.