agentleFS
ficaviolaodorata520ficaviolaodorata520/met-museum-mcp-server/skills/api-errors/SKILL.md

api-errors

Skill0 starsChanged 6 months agoApache-2.0View on GitHub

McpError constructor, JsonRpcErrorCode reference, and error handling patterns for `@cyanheads/mcp-ts-core`. Use when looking up error codes, understanding where errors should be thrown vs. caught, or using ErrorHandler.tryCatch in services.

The file

We split this skill into the rules it gives an agent

6 rules. About 0 are common, 0 are distinctive, and 6 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Use when no contract entry fits — ad-hoc throws, tools without a contract, or service-layer code.

    line 172
  2. Very specific:

    When a handler throws a plain Error (or any non-McpError value), the framework classifies it to the most specific JsonRpcErrorCode …

    line 265
  3. Very specific:

    Use factories or McpError directly when the code must be exact — auto-classification is best-effort pattern matching and not guaranteed for …

    line 267
  4. Very specific:

    Recovery hint mirroring is automatic: When the thrown McpError carries data.recovery.hint, the handler factory appends it to the content[] …

    line 361
  5. Very specific:

    Use ErrorHandler.tryCatch in service code, not in tool handlers.

    line 385
  6. Very specific:

    When you bypass fetchWithTimeout and use raw fetch (typically because you need granular code classification or response body access), use …

    line 427

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Overview

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 structured logging and wrapping.

Imports:

import { notFound, validationError, McpError, JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
import { ErrorHandler } from '@cyanheads/mcp-ts-core/utils';

Type-Driven Error Contract (recommended)

The recommended path for new tools and resources. Declare failure modes as a const tuple under errors; the reason union flows into the handler's ctx.fail and TypeScript enforces that you can only fail with a declared reason:

import { tool, z } from '@cyanheads/mcp-ts-core';
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';

export const fetchTool = tool('fetch_articles', {
  description: 'Fetch articles by PMID',
  input: z.object({ pmids: z.array(z.string()).describe('PMIDs') }),
  output: z.object({ articles: z.array(z.unknown()).describe('Articles') }),

  errors: [
    { reason: 'no_match', code: JsonRpcErrorCode.NotFound,
      when: 'No requested PMID returned data',
      recovery: 'Try pubmed_search_articles to discover valid PMIDs first.' },
    { reason: 'queue_full', code: JsonRpcErrorCode.RateLimited,
      when: 'Local request queue is at capacity', retryable: true,
      recovery: 'Wait 30 seconds and retry, or reduce batch size.' },
    { reason: 'ncbi_down', code: JsonRpcErrorCode.ServiceUnavailable,
      when: 'NCBI E-utilities unreachable after retries', retryable: true,
      recovery: 'NCBI is degraded; retry in a few minutes.' },
  ],

  async handler(input, ctx) {
    const articles = await ncbi.fetch(input.pmids);
    if (articles.length === 0) {
      throw ctx.fail('no_match', `None of ${input.pmids.length} PMIDs returned data`);
    }
    // ctx.fail('typo')   ← TypeScript error: 'typo' isn't in the contract
    return { articles };
  },
});

What you get:

SurfaceBehavior
Compile timectx.fail('typo') is a TS error. Auto-completes declared reasons.
Runtimectx.fail(reason, msg?, data?, options?) builds an McpError(contract.code, msg, { ...data, reason }, options) — data.reason is auto-populated from the contract and cannot be overridden by caller-supplied data (spread first, then reason written last), so observers see a stable identifier. options accepts { cause } for ES2022 error chaining.
Lint (devcheck)Each code validated against JsonRpcErrorCode. Reasons validated as snake_case + unique within contract. recovery validated as non-empty and ≥ 5 words. Build-time only — not invoked at server startup.
Lint (conformance)If the handler throw new McpError(JsonRpcErrorCode.X) outside ctx.fail, conformance check warns when X isn't declared.

recovery is opt-in resolution, not auto-population. The contract recovery is required metadata documenting the agent's next move when this failure mode fires (a forcing function for thoughtful guidance — placeholders like "Try again." get flagged by the linter). It does not automatically appear in runtime data.recovery.hint — the framework never injects it without an explicit signal at the throw site. Authors opt in by spreading ctx.recoveryFor('reason') into the data argument, the same way ctx.fail('reason') opts into resolving the contract code. What the author types at the throw site is what flows to the wire, with no hidden transformation; the resolver is just a typed lookup keyed by the same reason the author already typed.

ctx.recoveryFor — opt-in contract resolution

ctx.recoveryFor(reason) returns { recovery: { hint: <contract.recovery> } } for a declared reason, ready to spread into data. Always available on Context (returns {} when no contract is attached or the reason is unknown — spread-safe with no optional chaining). On HandlerContext<R> it tightens to a typed signature constrained to the declared reason union.

export const calculateTool = tool('calculate', {
  // ...
  errors: [
    { reason: 'empty_expression', code: JsonRpcErrorCode.ValidationError,
      when: 'Expression is empty or whitespace-only.',
      recovery: 'Provide a non-empty mathematical expression to evaluate.' },
  ],
  handler(input, ctx) {
    if (!input.expression.trim()) {
      // Static recovery — resolve from the contract.
      throw ctx.fail('empty_expression', undefined, { ...ctx.recoveryFor('empty_expression') });
    }
    // ...
  },
});

Same pattern works inside services that accept ctx:

export class MathService {
  parse(expr: string, ctx: Context) {
    try {
      return mathjs.parse(expr);
    } catch (err) {
      throw validationError(`Parse failed: ${err.message}`, {
        reason: 'parse_failed',
        ...ctx.recoveryFor('parse_failed'),  // {} if calling tool has no matching reason
      });
    }
  }
}

The contract is the single source of truth — write the recovery once, lint validates ≥5 words, the resolver carries it to every throw site that opts in. For runtime-context recovery (interpolating input values, attempted IDs, queue state), override at the throw site:

throw ctx.fail('no_match', `No item ${id}`, {
  recovery: { hint: `No item ${id}; try IDs 1-100 instead.` },
});

ctx.recoveryFor is the first member of a planned family of opt-in resolution helpers. Future contract-bound fields (troubleshootingFor, userMessageFor, …) follow the same shape: single-purpose, spreadable wire-shape, {} fallback when not applicable.

Skip the contract for one-off internal tools or quick prototypes — ctx is plain Context (no fail) and you throw via factories directly. Behavior is identical at the wire; the contract just adds compile-time safety.

Declare contracts inline on each tool, even when similar across tools. The contract is part of the tool's documented public surface — reading one tool definition file should give the full picture (input, output, errors, handler, format). Don't extract a shared errors[] constant or contract module to deduplicate near-identical entries; per-tool repetition is the intended cost of locality, and dynamic recovery hints often need tool-specific runtime context anyway. If a code-cleanup pass suggests consolidating contracts, decline — the duplication is load-bearing for tool-def readability.

Limits of the conformance lint. The conformance and prefer-fail rules scan the handler's source text for throw statements. Errors thrown from called services (e.g. await myService.fetch() raising RateLimited internally) are invisible — the lint only sees what's lexically in the handler. Treat the contract as the advertised failure surface; bubbled-up codes still reach the client correctly via the auto-classifier, just without lint enforcement.

Carrying contract reason from services

Services don't receive ctx automatically (unlike handlers), so they can't call ctx.fail directly — though ctx can be passed as a parameter when needed. To make a service-thrown failure carry the contract's reason on the wire, pass data: { reason: 'X' } to the factory. The framework's auto-classifier preserves data unchanged, so clients see the same error.data.reason they'd see from ctx.fail:

// my-service.ts
throw validationError('Expression cannot be empty.',  { reason: 'empty_expression' });
throw serviceUnavailable('Upstream timeout',          { reason: 'evaluation_timeout' });
// my-tool.tool.ts
errors: [
  { reason: 'empty_expression',   code: JsonRpcErrorCode.ValidationError,
    when: 'Input is empty.',
    recovery: 'Provide a non-empty expression to evaluate.' },
  { reason: 'evaluation_timeout', code: JsonRpcErrorCode.ServiceUnavailable,
    when: 'Upstream exceeded the configured timeout.',
    recovery: 'Simplify the expression or retry the request after a brief delay.' },
]

The handler doesn't catch and re-throw — letting service errors bubble unchanged keeps "logic throws, framework catches" intact. The wire payload still carries code + data.reason, and clients can switch on reason without parsing message text. What's lost is lint-time enforcement that every reason is reachable; compensate with one wire-shape test per reason.

To carry the contract recovery from a service throw, accept ctx and spread the resolver:

throw validationError(message, {
  reason: 'parse_failed',
  ...ctx.recoveryFor('parse_failed'),  // {} when calling tool has no matching reason
});

ctx.recoveryFor is always present on Context (no-op when no contract), so services don't need to know which tool called them — the spread is safe either way.


When not to throw

Throw when the server has authoritative classification — auth failure, rate limit, schema violation, upstream 5xx, missing required input. Don't throw when "this looks wrong" depends on intent the server can't see. For mutators, surface raw pre- and post-mutation observable state in the response and let the agent decide whether it matches intent — the server can detect that the file shrunk, but only the agent knows whether it was supposed to. Tell: defensive code justified as a free rider on other work — audit it standalone, and it usually doesn't earn its keep.


Error Factories (fallback)

Use when no contract entry fits — ad-hoc throws, tools without a contract, or service-layer code. Shorter than new McpError(...) and self-documenting. All return McpError instances and 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:

FactoryCode
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)
internalError(msg, data?, options?)InternalError (-32603)
serializationError(msg, data?, options?)SerializationError (-32070) — JSON/XML/parser failures
databaseError(msg, data?, options?)DatabaseError (-32010)

options is { cause?: unknown } — the standard ES2022 ErrorOptions type.


McpError Constructor

For codes not covered by factories (rare — MethodNotFound, ParseError, InitializationFailed, UnknownError):

throw new McpError(code, message?, data?, options?)
  • code — a JsonRpcErrorCode enum value
  • message — optional human-readable description of the failure
  • data — optional structured context (plain object)
  • options — optional { cause?: unknown } for error chaining

Example:

import { McpError, JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';

throw new McpError(JsonRpcErrorCode.DatabaseError, 'Connection pool exhausted', {
  pool: 'primary',
});

Error Codes

Standard JSON-RPC 2.0 codes:

CodeValueWhen to Use
ParseError-32700Malformed JSON received
InvalidRequest-32600Unsupported operation, missing client capability
MethodNotFound-32601Requested method does not exist
InvalidParams-32602Bad input, missing required fields, schema validation failure
InternalError-32603Unexpected failure, catch-all for programmer errors

Implementation-defined codes (-32000 to -32099):

CodeValueWhen to Use
ServiceUnavailable-32000External dependency down, upstream failure
NotFound-32001Resource, entity, or record doesn't exist
Conflict-32002Duplicate key, version mismatch, concurrent modification
RateLimited-32003Rate limit exceeded
Timeout-32004Operation exceeded time limit
Forbidden-32005Authenticated but insufficient scopes/permissions
Unauthorized-32006No auth, invalid token, expired credentials
ValidationError-32007Business rule violation (not schema — use InvalidParams for that)
ConfigurationError-32008Missing env var, invalid config
InitializationFailed-32009Server/component startup failure
DatabaseError-32010Storage/persistence layer failure
SerializationError-32070Data serialization/deserialization failed
UnknownError-32099Generic fallback when no other code fits

Auto-Classification

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.

Resolution Order

The framework applies these steps in order — first match wins:

  1. McpError instance — error.code is preserved as-is; no classification needed.
  2. JS constructor name — matched against a fixed table (e.g. ZodError → ValidationError, SyntaxError → ValidationError). Note: TypeError is intentionally excluded — runtime TypeErrors are programmer errors, not validation failures.
  3. Provider-specific patterns — HTTP status codes, AWS exception names, Supabase, OpenRouter. Checked before common patterns because they are more specific (e.g. status code 429 beats the generic rate limit pattern).
  4. Common message/name patterns — broad keyword patterns covering auth, not-found, validation, etc. First match wins; order matters.
  5. AbortError name — error.name === 'AbortError' → Timeout.
  6. Fallback — InternalError.

JS Constructor Name Mappings

ConstructorMapped Code
SyntaxErrorValidationError
RangeErrorValidationError
URIErrorValidationError
ZodErrorValidationError
ReferenceErrorInternalError
EvalErrorInternalError
AggregateErrorInternalError

TypeError is intentionally excluded from the constructor table — runtime TypeErrors (e.g. "Cannot read property X of undefined") are programmer errors, not validation failures. They fall through to message-pattern matching, then to the InternalError fallback.

Common Message Patterns

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_-]+tokenUnauthorized
permission|forbidden|access.*denied|not.*allowedForbidden
not found|no such|doesn't exist|couldn't findNotFound
invalid|validation|malformed|bad request|wrong format|missing\s+(?:required|param|field|input|value|arg)ValidationError
conflict|already exists|duplicate|unique constraintConflict
rate limit|too many requests|throttledRateLimited
timeout|timed out|deadline exceededTimeout
abort(ed)?|cancell?edTimeout
service unavailable|bad gateway|gateway timeout|upstream errorServiceUnavailable
zod|zoderror|schema validationValidationError

Provider-Specific Patterns

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.

PatternMapped Code
ThrottlingException|TooManyRequestsExceptionRateLimited
AccessDenied|UnauthorizedOperationForbidden
ResourceNotFoundExceptionNotFound
status code 401Unauthorized
status code 403Forbidden
status code 404NotFound
status code 409Conflict
status code 429RateLimited
status code 5xxServiceUnavailable
ECONNREFUSED|connection refusedServiceUnavailable
ETIMEDOUT|connection timeoutTimeout
unique constraint|duplicate keyConflict
foreign key constraintValidationError
JWT expiredUnauthorized
row level securityForbidden
insufficient_quota|quota exceededRateLimited
model_not_foundNotFound
context_length_exceededValidationError
ENOTFOUND|DNSServiceUnavailable
ECONNRESET|connection resetServiceUnavailable

Where Errors Are Handled

LayerPattern
Tool/resource handlersThrow McpError — no try/catch
Handler factory (tools)Catches all errors, normalizes to McpError, sets isError: true, mirrors error across both client surfaces (see Error-path parity)
Handler factory (resources)Catches and re-throws to the SDK, which routes through the JSON-RPC error envelope
Services/setup codeErrorHandler.tryCatch for structured logging and wrapping (always rethrows — never swallows)

Error-path parity

MCP clients differ in which CallToolResult surface they forward to the agent. Tool errors mirror the success-path format-parity invariant — both surfaces carry the same payload:

SurfaceContentRead by
content[]Text rendering: Error: <message> (plus Recovery: <hint> when data.recovery.hint is present)Claude Desktop and other format()-only clients
structuredContent.errorJSON { code, message, data? } carrying the error code, message, and any structured data from the thrown McpError or ZodErrorClaude Code and other structuredContent-only clients

Important properties:

  • _meta.error is NOT emitted. Error code/data live on structuredContent.error instead. Don't read _meta.error in clients or tests — it doesn't exist.
  • data propagation is restricted to explicitly-thrown McpError.data and ZodError.issues. Auto-classified plain errors (TypeError, network errors, etc.) emit code + message only — no data — so internal classification context never leaks to clients.
  • Recovery hint mirroring is automatic. When the thrown McpError carries data.recovery.hint, the handler factory appends it to the content[] text so the markdown surface matches the JSON surface. Authors don't need to format the hint manually.

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;
  },
});

ErrorHandler.tryCatch (Services)

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'>):

OptionTypeRequiredPurpose
operationstringYesName logged with the error
contextErrorContextNoExtra structured fields merged into the log record; requestId and timestamp receive special treatment
errorCodeJsonRpcErrorCodeNoCode used if the caught error is not already an McpError
inputunknownNoInput value sanitized and logged alongside the error
criticalbooleanNoMarks the error as critical in logs (default false)
includeStackbooleanNoInclude stack trace in log output (default true)
errorMapper(error: unknown) => ErrorNoCustom transform applied instead of default McpError wrapping

HTTP Response → McpError

When you bypass fetchWithTimeout and use raw fetch (typically because you need granular code classification or response body access), use httpErrorFromResponse instead of writing your own status mapping ladder:

import { httpErrorFromResponse } from '@cyanheads/mcp-ts-core/utils';

const response = await fetch(url, { signal: ctx.signal });
if (!response.ok) {
  throw await httpErrorFromResponse(response, {
    service: 'NCBI',                  // included in message
    data: { endpoint, requestId: ctx.requestId },
  });
}

Captures the response body (truncated, configurable limit) and Retry-After header (stored as data.retryAfter) into error.data. The codes it produces line up with withRetry's transient-code set, so retryable responses are retried automatically.

Body reaches the client. error.data is forwarded to the MCP client as structuredContent.error.data (tool errors) or JSON-RPC error.data (resource errors). Upstream 401/403/422 responses sometimes echo token claims, internal user IDs, or schema validation hints — that text becomes client-visible. For sensitive endpoints, pass captureBody: false (or bodyLimit: 0) so the body stays out of data. Defaults remain captureBody: true because most upstreams return useful diagnostic text and silent dropping helps no one debug.

Full status table:

StatusCode
400InvalidParams
401Unauthorized
402, 403Forbidden
404NotFound
408, 425, 504Timeout
409, 423, 424Conflict
422ValidationError
429RateLimited
405, 406, 410, 412, 415, 416, 417, 428, 431, 451, 4xx (other)InvalidRequest
500, 501InternalError
502, 503, 5xx (other)ServiceUnavailable

Also exports httpStatusToErrorCode(status) for sync mapping when you don't have a Response object.


Handler-Body Lint Rules

The startup linter (bun run lint:mcp and createApp() startup) checks handler bodies for common anti-patterns. All emit warnings (not errors) — they don't block startup but show up in devcheck output.

RuleCatches
prefer-mcp-error-in-handlerthrow new Error(...) inside a handler — use McpError or a factory so the framework returns a specific code
prefer-error-factorynew McpError(JsonRpcErrorCode.NotFound, ...) when notFound(...) exists
preserve-cause-on-rethrowcatch (e) { throw new McpError(...) } without { cause: e }
no-stringify-upstream-errorJSON.stringify(...) inside a thrown message — risks leaking internal traces; use data payload instead

Error Contract Lint Rules

The linter validates the structure of errors[] and (when present) cross-checks the handler body against the declared contract.

Structural rules

RuleSeverityCatches
error-contract-typeerrorerrors is present but not an array
error-contract-emptywarningerrors: [] — drop the field instead, or declare actual failure modes
error-contract-entry-typeerrorAn entry isn't an object
error-contract-code-typeerrorcode missing or not a number
error-contract-code-unknownerrorcode isn't a real JsonRpcErrorCode value
error-contract-code-unknown-errorwarningcode is JsonRpcErrorCode.UnknownError (the giveup-fallback — pick a more specific code)
error-contract-reason-requirederrorreason missing or empty
error-contract-reason-formatwarningreason not snake_case
error-contract-reason-uniqueerrorDuplicate reason within one contract
error-contract-when-requirederrorwhen missing or empty
error-contract-recovery-requirederrorrecovery missing or not a string
error-contract-recovery-emptyerrorrecovery is empty/whitespace-only
error-contract-recovery-min-wordswarningrecovery has fewer than 5 words — placeholders like "Try again." or "Check input." get flagged in favor of specific guidance
error-contract-retryable-typewarningretryable is present but not a boolean

Conformance rules

RuleSeverityCatches
error-contract-conformancewarningHandler throws a non-baseline code that isn't in the contract. Suggests adding it to errors[] so the contract is the canonical source of truth for declared failure modes.
error-contract-prefer-failwarningHandler throws a code that is in the contract directly (via factory or new McpError) instead of through ctx.fail(reason, …). Encourages routing through the typed helper so observers see consistent data.reason values.

Baseline codes (auto-allowed)

These codes bubble up from anywhere — services, framework utilities, the auto-classifier — and are implicitly always-possible on any tool. They're skipped by the conformance check, so the contract can stay focused on intentional domain failures:

  • InternalError — bug, programmer error, truly unexpected
  • ServiceUnavailable — upstream/network failures
  • Timeout — request deadline exceeded, abort
  • ValidationError — schema violations, malformed input
  • SerializationError — JSON/XML parse failures

If you want to declare one of these as a domain-specific failure (e.g., a tool that intentionally times out under defined conditions), put it in errors[] anyway — the contract still binds ctx.fail(reason) and the conformance lint will catch undeclared throws. The lint just doesn't require you to enumerate baselines.

When to declare vs. let it bubble

The contract describes the public failure surface — the failures clients/agents can plan around. Modeled after how OpenAPI-driven frameworks treat 5xx: enumerated 4xx for intentional failures, implicit 5xx for infrastructure.

PatternUse for
throw ctx.fail('reason', …)Declared domain failures — typed, contract-checked, data.reason populated
throw notFound(…) / factoriesErrors not in the contract; the auto-classifier handles them. Prefer ctx.fail when a matching contract entry exists.
Bubble up from servicesUpstream classification already produced an McpError — don't re-wrap

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.