Cookbook
Action Error Handling
Every action call returns either a direct API envelope or an MCP response derived from that envelope. Extension authors should return declared errors for expected failures and let the dispatcher handle authentication, scope, schema, rate-limit, and uncaught-exception failures.
Human Path
Return success with:
return MyronActionResult::ok(['noteId' => $noteId]);
Return a known failure with:
return MyronActionResult::error('precondition_failed', 'title and body must be non-empty.');
Use declared errors for validation or business conditions the caller can fix. Do not throw exceptions for routine precondition failures.
AI Path
An AI coding agent can author error handling in local PHP files and verify the failure modes. An installed MCP client receives mapped error responses and should remediate only within its granted scope.
Safe AI remediation means:
- correct malformed input,
- satisfy a documented precondition,
- ask the operator for missing trust grants,
- wait on rate limits,
- stop when the error indicates policy or authorization denial.
Direct API Envelope
The dispatcher wraps action results in this shape:
{
"ok": true,
"result": {},
"error": null,
"actionId": "act_...",
"auditEventId": "aud_..."
}
On declared errors, ok is false, result is an empty object, and error contains a stable code and human-readable message.
Direct API Errors
| Code | HTTP status | Source | Likely cause | Safe remediation |
|---|---|---|---|---|
action_not_found | 404 | Dispatcher resolution | No registered action matches method and URI. | Check extension load state, URI, method, and namespace. |
auth_required | 401 | Authorization | No session or bearer token for a private action. | Use an authenticated caller. |
auth_invalid | 401 | Authorization | Bearer token is malformed, revoked, production-disallowed, or unknown. | Use an operator-provisioned active token. |
rate_limited | 429 | Rate limiter | AI integration exceeded request budget. | Back off and retry after the window; do not parallel-retry. |
scope_insufficient | 403 | Dispatcher scope check | Caller lacks requiredScope. | Use a caller with the action scope or ask the operator to review grants. |
input_invalid | 400 | Dispatcher schema validation or action-declared validation | Required field missing or field type mismatched. | Fix request shape according to inputSchema. |
precondition_failed | 422 | Action result | Required domain state is missing. | Satisfy the named precondition before retrying. |
business_rule_failed | 422 | Action result | Domain rule rejected the operation. | Fix the domain state or input. |
uncaught_exception | 500 | Dispatcher catch-all | Action threw unexpectedly. | Fix the action to catch known failures and return declared errors. |
input_invalid, precondition_failed, and business_rule_failed can be returned by action code. The dispatcher always uses 400 for schema validation failures and 422 for declared action errors.
MCP Mapping
tools/call delegates to the same dispatcher, then maps the result to MCP:
| Dispatcher result | MCP response |
|---|---|
401 or 403 | JSON-RPC error with data.code = MCP_AUTHZ_DENIED. |
429 | JSON-RPC error with data.code = MCP_RATE_LIMITED and retry metadata. |
| Other action error envelope | CallToolResult with isError: true and textual error content. |
| Successful action envelope | CallToolResult with isError: false and JSON text content. |
| Unknown tool name | JSON-RPC method-not-found style error. |
For MCP clients, isError:true usually means the tool call reached the action. Treat it as a normal action failure, not a protocol failure.
Cookbook Pattern
Use a small helper style inside actions:
public function execute(array $input, MyronActionCallerContext $caller): MyronActionResult
{
$title = trim((string) ($input['title'] ?? ''));
if ($title === '') {
return MyronActionResult::error('precondition_failed', 'title is required.');
}
try {
$noteId = $this->createNote($title);
} catch (InvalidArgumentException $e) {
return MyronActionResult::error('input_invalid', $e->getMessage());
} catch (RuntimeException $e) {
return MyronActionResult::error('business_rule_failed', $e->getMessage());
}
return MyronActionResult::ok(['noteId' => $noteId]);
}
Let unexpected Throwable cases bubble to the dispatcher only when they are genuinely unexpected. If callers can fix the problem, return a declared error.
Scopes
Error handling does not change scope requirements. A missing scope must stay a failed authorization result. Do not catch scope_insufficient in action code; the dispatcher checks scope before execute() runs.
Artifacts
Source contracts:
MyronActionResultMyronActionDispatcherMyronApiAuthorizationMyronRateLimiterMyronMcpServer
Runtime surfaces:
- direct API action envelope,
- MCP
tools/callresult, - audit event metadata when audit logging is configured.
Safety Boundary
Do not include secrets in action errors. The audit layer redacts known secret keys, but action messages should still avoid exposing credentials, tokens, raw request payloads, or provider responses that may contain secrets.
Do not convert authorization failures into business errors. Auth, scope, and rate-limit decisions belong to the dispatcher and server layers.
Verification
For each new action, verify:
- valid input returns
ok: true, - missing required input returns
input_invalid, - unmet business precondition returns a declared 422 error,
- under-scoped caller returns
scope_insufficient, - MCP
tools/callmaps action-declared failures toisError:true, - known failures do not appear as
uncaught_exception.
Failure Modes
| Failure | What it means | Safe response |
|---|---|---|
Every bad input becomes uncaught_exception | The action throws for expected caller-correctable failures. | Catch known failures and return declared errors. |
MCP client retries a write after isError:true | The caller treated a business failure as a transient protocol failure. | Read the error text and satisfy the precondition first. |
| Error message exposes a token or provider secret | The action included sensitive data in failure text. | Replace with a non-secret diagnostic and rely on redacted audit context. |
| Under-scoped caller is told to broaden itself | Documentation or agent remediation violates the trust boundary. | Route grant changes through the operator/admin path. |
| Validation is duplicated inconsistently | Schema and action-level checks disagree. | Keep shape/type checks in inputSchema and domain checks in action code. |