api-errors — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited api-errors (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Error handling in @cyanheads/mcp-ts-core follows a strict layered pattern: tool and resource handlers throw McpError freely (no try/catch), the handler factory catches and normalizes all errors, and services use ErrorHandler.tryCatch for graceful recovery.
Imports:
import { notFound, validationError, McpError, JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
import { ErrorHandler } from '@cyanheads/mcp-ts-core/utils';Shorter than new McpError(...) and self-documenting. All return McpError instances. All accept an optional options parameter for error chaining via { cause }.
throw notFound('Item not found', { itemId: '123' });
throw validationError('Missing required field: name', { field: 'name' });
throw unauthorized('Token expired');
// With cause for error chaining
throw serviceUnavailable('API call failed', { url }, { cause: error });Available factories:
| Factory | Code |
|---|---|
invalidParams(msg, data?, options?) | InvalidParams (-32602) |
invalidRequest(msg, data?, options?) | InvalidRequest (-32600) |
notFound(msg, data?, options?) | NotFound (-32001) |
forbidden(msg, data?, options?) | Forbidden (-32005) |
unauthorized(msg, data?, options?) | Unauthorized (-32006) |
validationError(msg, data?, options?) | ValidationError (-32007) |
conflict(msg, data?, options?) | Conflict (-32002) |
rateLimited(msg, data?, options?) | RateLimited (-32003) |
timeout(msg, data?, options?) | Timeout (-32004) |
serviceUnavailable(msg, data?, options?) | ServiceUnavailable (-32000) |
configurationError(msg, data?, options?) | ConfigurationError (-32008) |
options is { cause?: unknown } — the standard ES2022 ErrorOptions type.
For codes not covered by factories (InternalError, DatabaseError, etc.):
throw new McpError(code, message?, data?, options?)code — a JsonRpcErrorCode enum valuemessage — optional human-readable description of the failuredata — optional structured context (plain object)options — optional { cause?: unknown } for error chainingExample:
import { McpError, JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
throw new McpError(JsonRpcErrorCode.DatabaseError, 'Connection pool exhausted', {
pool: 'primary',
});Standard JSON-RPC 2.0 codes:
| Code | Value | When to Use |
|---|---|---|
ParseError | -32700 | Malformed JSON received |
InvalidRequest | -32600 | Unsupported operation, missing client capability |
MethodNotFound | -32601 | Requested method does not exist |
InvalidParams | -32602 | Bad input, missing required fields, schema validation failure |
InternalError | -32603 | Unexpected failure, catch-all for programmer errors |
Implementation-defined codes (-32000 to -32099):
| Code | Value | When to Use |
|---|---|---|
ServiceUnavailable | -32000 | External dependency down, upstream failure |
NotFound | -32001 | Resource, entity, or record doesn't exist |
Conflict | -32002 | Duplicate key, version mismatch, concurrent modification |
RateLimited | -32003 | Rate limit exceeded |
Timeout | -32004 | Operation exceeded time limit |
Forbidden | -32005 | Authenticated but insufficient scopes/permissions |
Unauthorized | -32006 | No auth, invalid token, expired credentials |
ValidationError | -32007 | Business rule violation (not schema — use InvalidParams for that) |
ConfigurationError | -32008 | Missing env var, invalid config |
InitializationFailed | -32009 | Server/component startup failure |
DatabaseError | -32010 | Storage/persistence layer failure |
SerializationError | -32070 | Data serialization/deserialization failed |
UnknownError | -32099 | Generic fallback when no other code fits |
When a handler throws a plain Error (or any non-McpError value), the framework classifies it to the most specific JsonRpcErrorCode automatically. This matters when you don't control what a third-party library throws and can't predict its error type.
Use factories or McpError directly when the code must be exact — auto-classification is best-effort pattern matching and not guaranteed for ambiguous messages. For errors from your own code where the code matters, be explicit.
The framework applies these steps in order — first match wins:
error.code is preserved as-is; no classification needed.TypeError → ValidationError).status code 429 beats the generic rate limit pattern).error.name === 'AbortError' → Timeout.InternalError.| Constructor | Mapped Code |
|---|---|
SyntaxError | ValidationError |
TypeError | ValidationError |
RangeError | ValidationError |
URIError | ValidationError |
ReferenceError | InternalError |
EvalError | InternalError |
AggregateError | InternalError |
Patterns are tested against both the error message and name, case-insensitively. First match wins.
| Pattern (regex) | Mapped Code | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| `unauthorized\ | unauthenticated\ | not\s+authorized\ | not.logged.in\ | invalid[\s_-]+token\ | expired[\s_-]+token` | Unauthorized | |||||
| `permission\ | forbidden\ | access.*denied\ | not.*allowed` | Forbidden | |||||||
| `not found\ | no such\ | doesn't exist\ | couldn't find` | NotFound | |||||||
| `invalid\ | validation\ | malformed\ | bad request\ | wrong format\ | missing\s+(?:required\ | param\ | field\ | input\ | value\ | arg)` | ValidationError |
| `conflict\ | already exists\ | duplicate\ | unique constraint` | Conflict | |||||||
| `rate limit\ | too many requests\ | throttled` | RateLimited | ||||||||
| `timeout\ | timed out\ | deadline exceeded` | Timeout | ||||||||
| `abort(ed)?\ | cancell?ed` | Timeout | |||||||||
| `service unavailable\ | bad gateway\ | gateway timeout\ | upstream error` | ServiceUnavailable | |||||||
| `zod\ | zoderror\ | schema validation` | ValidationError |
Checked before common patterns. Cover: AWS exception names, HTTP status codes, DB connection/constraint errors, Supabase JWT/RLS, OpenRouter/LLM quota errors, and low-level network errors.
| Pattern | Mapped Code | |
|---|---|---|
| `ThrottlingException\ | TooManyRequestsException` | RateLimited |
| `AccessDenied\ | UnauthorizedOperation` | Forbidden |
ResourceNotFoundException | NotFound | |
status code 401 | Unauthorized | |
status code 403 | Forbidden | |
status code 404 | NotFound | |
status code 409 | Conflict | |
status code 429 | RateLimited | |
status code 5xx | ServiceUnavailable | |
| `ECONNREFUSED\ | connection refused` | ServiceUnavailable |
| `ETIMEDOUT\ | connection timeout` | Timeout |
| `unique constraint\ | duplicate key` | Conflict |
foreign key constraint | ValidationError | |
JWT expired | Unauthorized | |
row level security | Forbidden | |
| `insufficient_quota\ | quota exceeded` | RateLimited |
model_not_found | NotFound | |
context_length_exceeded | ValidationError | |
| `ENOTFOUND\ | DNS` | ServiceUnavailable |
| `ECONNRESET\ | connection reset` | ServiceUnavailable |
| Layer | Pattern |
|---|---|
| Tool/resource handlers | Throw McpError — no try/catch |
| Handler factory | Catches all errors, normalizes to McpError, sets isError: true |
| Services/setup code | ErrorHandler.tryCatch for graceful recovery |
Handler — throw freely, no try/catch:
import { notFound } from '@cyanheads/mcp-ts-core/errors';
export const myTool = tool('my_tool', {
input: z.object({ id: z.string().describe('Item ID') }),
output: z.object({ id: z.string(), name: z.string(), status: z.string() }),
async handler(input, ctx) {
const item = await db.find(input.id);
if (!item) {
throw notFound(`Item not found: ${input.id}`, { id: input.id });
}
return item;
},
});Use ErrorHandler.tryCatch in service code, not in tool handlers. It wraps arbitrary exceptions into McpError and supports structured logging context.
import { ErrorHandler } from '@cyanheads/mcp-ts-core/utils';
// Works with both async and sync functions
const result = await ErrorHandler.tryCatch(
() => externalApi.fetch(url),
{
operation: 'ExternalApi.fetch',
context: { url },
errorCode: JsonRpcErrorCode.ServiceUnavailable,
},
);
const parsed = await ErrorHandler.tryCatch(
() => JSON.parse(raw),
{
operation: 'parseConfig',
errorCode: JsonRpcErrorCode.ConfigurationError,
},
);tryCatch always logs and rethrows — it never swallows errors. The fn argument may be synchronous or return a Promise; both are handled via Promise.resolve(fn()).
Options (Omit<ErrorHandlerOptions, 'rethrow'>):
| Option | Type | Required | Purpose |
|---|---|---|---|
operation | string | Yes | Name logged with the error |
context | ErrorContext | No | Extra structured fields merged into the log record; requestId and timestamp receive special treatment |
errorCode | JsonRpcErrorCode | No | Code used if the caught error is not already an McpError |
input | unknown | No | Input value sanitized and logged alongside the error |
critical | boolean | No | Marks the error as critical in logs (default false) |
includeStack | boolean | No | Include stack trace in log output (default true) |
errorMapper | (error: unknown) => Error | No | Custom transform applied instead of default McpError wrapping |
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.