Standardized event classes for MCP (Model Context Protocol) tool execution lifecycle in PHP.
Zero dependencies - pure PHP 8.1+, framework-agnostic.
composer require code-wheel/mcp-eventsThese events follow a standard structure for MCP tool execution observability. Use them with any PSR-14 compatible event dispatcher.
use CodeWheel\McpEvents\ToolExecutionStartedEvent;
use CodeWheel\McpEvents\ToolExecutionSucceededEvent;
use CodeWheel\McpEvents\ToolExecutionFailedEvent;
// When tool execution starts
$startEvent = new ToolExecutionStartedEvent(
toolName: 'create_user',
pluginId: 'my_module.create_user',
arguments: ['username' => 'john', 'email' => '[redacted]'],
requestId: 'req-123',
timestamp: microtime(true),
);
$dispatcher->dispatch($startEvent);
// On success
$successEvent = new ToolExecutionSucceededEvent(
toolName: 'create_user',
pluginId: 'my_module.create_user',
arguments: ['username' => 'john', 'email' => '[redacted]'],
result: $callToolResult,
durationMs: 45.2,
requestId: 'req-123',
);
$dispatcher->dispatch($successEvent);
// On failure
$failEvent = new ToolExecutionFailedEvent(
toolName: 'create_user',
pluginId: 'my_module.create_user',
arguments: ['username' => 'john'],
reason: ToolExecutionFailedEvent::REASON_VALIDATION,
result: null,
exception: $validationException,
durationMs: 12.5,
requestId: 'req-123',
);
$dispatcher->dispatch($failEvent);use CodeWheel\McpEvents\ToolExecutionStartedEvent;
use CodeWheel\McpEvents\ToolExecutionSucceededEvent;
use CodeWheel\McpEvents\ToolExecutionFailedEvent;
class ToolExecutionLogger {
public function onStart(ToolExecutionStartedEvent $event): void {
$this->logger->info('Tool started', [
'tool' => $event->toolName,
'request_id' => $event->requestId,
]);
}
public function onSuccess(ToolExecutionSucceededEvent $event): void {
$this->metrics->histogram('tool_duration_ms', $event->durationMs, [
'tool' => $event->toolName,
]);
}
public function onFailure(ToolExecutionFailedEvent $event): void {
$context = [
'tool' => $event->toolName,
'reason' => $event->reason,
'duration_ms' => $event->durationMs,
];
if ($event->hasException()) {
$context['exception'] = $event->exception->getMessage();
}
if ($event->isPolicyFailure()) {
$this->logger->warning('Tool blocked by policy', $context);
} else {
$this->logger->error('Tool execution failed', $context);
}
}
}Dispatched when tool execution begins.
| Property | Type | Description |
|---|---|---|
toolName |
string | MCP tool name |
pluginId |
string | Implementation plugin ID |
arguments |
array | Sanitized tool arguments |
requestId |
string|int|null | MCP request correlation ID |
timestamp |
float | Start timestamp (microtime) |
Dispatched when tool execution completes successfully.
| Property | Type | Description |
|---|---|---|
toolName |
string | MCP tool name |
pluginId |
string | Implementation plugin ID |
arguments |
array | Sanitized tool arguments |
result |
object | Result object (e.g., CallToolResult from mcp/sdk) |
durationMs |
float | Execution duration in ms |
requestId |
string|int|null | MCP request correlation ID |
Dispatched when tool execution fails.
| Property | Type | Description |
|---|---|---|
toolName |
string | MCP tool name |
pluginId |
string | Implementation plugin ID |
arguments |
array | Sanitized tool arguments |
reason |
string | Failure reason constant |
result |
object|null | Result object if available |
exception |
Throwable|null | Exception if thrown |
durationMs |
float | Duration until failure in ms |
requestId |
string|int|null | MCP request correlation ID |
| Constant | Description |
|---|---|
REASON_VALIDATION |
Input validation failed |
REASON_ACCESS_DENIED |
Permission denied |
REASON_INSTANTIATION |
Failed to create tool instance |
REASON_INVALID_TOOL |
Tool not found or invalid |
REASON_RESULT |
Result processing failed |
REASON_EXECUTION |
General execution failure |
REASON_POLICY |
Blocked by policy |
REASON_POLICY_APPROVAL |
Requires approval |
REASON_POLICY_BUDGET |
Budget exceeded |
REASON_POLICY_DRY_RUN |
Dry run mode |
REASON_POLICY_SCOPE |
Insufficient scope |
// Check if failure was policy-related
if ($event->isPolicyFailure()) {
// Handle policy block differently
}
// Check if exception was thrown
if ($event->hasException()) {
$exception = $event->exception;
}
// Get all valid failure reasons
$allReasons = ToolExecutionFailedEvent::allReasons();
// ['REASON_VALIDATION' => 'validation_failed', ...]
// Validate a reason string
if (ToolExecutionFailedEvent::isValidReason($reason)) {
// Valid reason
}All events implement JsonSerializable for easy logging:
// Direct serialization
$json = json_encode($event);
// Or get the array
$data = $event->jsonSerialize();
// Example output for ToolExecutionFailedEvent:
// {
// "event": "tool_execution_failed",
// "tool_name": "create_user",
// "plugin_id": "my_module.create_user",
// "reason": "validation_failed",
// "duration_ms": 12.5,
// "request_id": "req-123",
// "is_policy_failure": false,
// "has_exception": true,
// "exception_class": "InvalidArgumentException",
// "exception_message": "Email is required"
// }Note: The result object is intentionally excluded from serialization as it may contain sensitive data.
This package has zero dependencies. When using with mcp/sdk, the result object will be a CallToolResult:
// With mcp/sdk (optional)
use Mcp\Schema\Result\CallToolResult;
$event = new ToolExecutionSucceededEvent(
toolName: 'my_tool',
pluginId: 'my_module.my_tool',
arguments: [],
result: $callToolResult, // CallToolResult from mcp/sdk
durationMs: 10.5,
requestId: null,
);
// Access result properties
$structured = $event->result->structuredContent;MIT