Agent skill

API Errors

by cyanheads in cyanheads/pubmed-mcp-server

McpError constructor, JsonRpcErrorCode reference, and error handling patterns for @cyanheads/mcp-ts-core.

Apache-2.0Auto-check passedResearch & Science

Install API Errors

skills CLI
$ npx skills add cyanheads/pubmed-mcp-server --skill api-errors -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install cyanheads/pubmed-mcp-server api-errors --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/cyanheads/pubmed-mcp-server.git skills-src && mkdir -p .claude/skills && cp -r skills-src/framework-skills/api-errors .claude/skills/api-errors && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
api-errors
GitHub stars
155
Token cost
~13k tokens
SKILL.md length
5,990 words
Files
1
Skills in repo
30
Repo updated
First seen
Licence
Apache-2.0

At a glance

McpError constructor, JsonRpcErrorCode reference, and error handling patterns for @cyanheads/mcp-ts-core.

  • Works in 9 steps: Request signal aborted —… → McpError instance — error.code is… → SDK transport-closed rejection — an… → …
  • Looking up error codes
  • SKILL.md covers Overview, Type-Driven Error Contract…, When not to throw and Error Factories (fallback), plus 8 more sections
  • Calls bun

What it does

API Errors is an agent skill from cyanheads/pubmed-mcp-server. 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.

Its SKILL.md is about 13k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.

It sits in Research & Science, covering MCP servers. It works with Model Context Protocol. The repository describes itself as: Search PubMed/Europe PMC, fetch articles and full text (PMC/EPMC/Unpaywall), citations, MeSH terms via MCP. STDIO or Streamable HTTP. The licence is Apache-2.0.

When your agent uses it

  • Looking up error codes
  • Understanding where errors should be thrown vs

Example prompts

  • “/api-errors”

Workflow steps

9 steps, taken from the first numbered list in SKILL.md.

  1. Request signal aborted — ctx.signal.aborted is true when the handler unwinds → RequestCancelled. Resolved before the thrown value is…
  2. McpError instance — error.code is preserved as-is; no classification needed.
  3. SDK transport-closed rejection — an SdkError carrying SdkErrorCode.ConnectionClosed → RequestCancelled. The SDK rejects every in-flight…
  4. Engine resource limit — a RangeError whose whole message is one the engine raises when it runs out of a resource → InternalError: Maximum…
  5. JS constructor name — matched against a fixed table (e.g. ZodError → ValidationError, SyntaxError → ValidationError). Note: TypeError is…
  6. Provider-specific patterns — HTTP status codes, AWS exception names, Supabase, OpenRouter. Checked before common patterns because they are…
  7. Common message/name patterns — broad keyword patterns covering auth, not-found, validation, etc. First match wins; order matters.
  8. AbortError name — error.name === 'AbortError' → Timeout.
  9. Fallback — InternalError.

What it can do on your machine

Read from SKILL.md and the folder at commit 5a417fb. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • bun

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

API Errors loads about 13k tokens when it runs. Until then it costs about 63 tokens; SKILL.md has 5,990 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~63
When it runs · the whole SKILL.md, loaded when a task matches
~13k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from cyanheads/pubmed-mcp-server at commit 5a417fb, republished under its Apache-2.0 licence (© cyanheads). 5,990 words, ~12,978 tokens.

Download SKILL.mdSave it as .claude/skills/api-errors/SKILL.md (or your agent's skills folder).
name
api-errors
description
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.
metadata.author
cyanheads
metadata.version
1.19
metadata.audience
external
metadata.type
reference

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:

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

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:

ts
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.
Runtime (recovery)A failure whose data.reason names a declared entry and carries no data.recovery gets data.recovery.hint set to the entry's recovery at the handler boundary — see below.
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. The inverse is checked too: a declared reason no ctx.fail in the handler names warns as error-contract-unthrown (mark it thrownBy: 'service' when the service layer produces it).

recovery is the wire default for its reason. 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), and it is what the caller receives. When a failure whose data.reason names a declared entry reaches the tool or resource handler factory with no data.recovery, the factory sets data.recovery.hint to that entry's recovery before it logs the failure and builds the envelope, so the Error in tool:<name> record, structuredContent.error.data, and the Recovery: line in content[] carry the same hint. It matches on the reason alone — a bare ctx.fail('reason'), a service throwing notFound(msg, { reason }), and a declared reason raised through a factory with a different code all get it. A throw-site recovery always wins, whatever its shape. An undeclared reason, a tool without errors[], a non-McpError throw, and a cancelled call get nothing, and the framework-owned invalid_arguments / client_capability_missing refusals keep their own hints. The thrown McpError is never changed — a handler-level test of ctx.fail sees exactly what the throw site wrote — and runToolContract applies the same fill, so a contract test sees the production envelope. Prompts declare no contract.

ts
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 — the framework fills the contract's hint onto the wire.
      throw ctx.fail('empty_expression');
    }
    // ...
  },
});

Same for a service, which needs no ctx to get the hint — the reason is enough:

ts
export class MathService {
  parse(expr: string) {
    try {
      return mathjs.parse(expr);
    } catch (err) {
      throw validationError(`Parse failed: ${err.message}`, { reason: 'parse_failed' });
    }
  }
}

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

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

A recovery hint names a capability, never an internal method. The reader is a model whose only reachable surface is this server's tool names — it cannot call a TypeScript method, set a library option, or re-run an internal function. Re-stage the table via registerTable() is unfollowable and invites a hallucinated tool call; Re-run the tool that produced this table to stage it again, or list the currently staged tables with this server's dataframe-describe tool is actionable from where the reader sits. Name a condition the caller cannot observe — an option flag they never set — and the hint is noise for the same reason. The framework holds its own throws to this rule: the canvas SQL gate's rejections point at the dataframe-query and dataframe-describe capabilities rather than the provider methods behind them.

ctx.recoveryFor — the entry's hint at the throw site

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.

It is not needed to put a declared hint on the wire — the fill above does that. Reach for it when the hint has to ride the thrown error itself: a test asserting data.recovery on the handler's own throw, or a site that deliberately sends another entry's guidance (ctx.fail('a', msg, ctx.recoveryFor('b'))), which the fill respects as authored.

severity — log a modeled outcome below error

An outcome a tool declares in errors[] is a modeled result, not an incident. A caller who answers no to a confirmation prompt, a lookup whose miss is an ordinary answer — logging those at error alongside upstream faults and bugs leaves the error stream unreadable at the level log-based alerting works on. severity moves that one record's level:

ts
errors: [
  { reason: 'consent_declined', code: JsonRpcErrorCode.InvalidRequest,
    when: 'The caller declined the confirmation prompt.', severity: 'notice',
    recovery: 'Re-run the tool and confirm the prompt to proceed with the change.' },
],

Values are the logger's own level names below error — debug, info, notice, warning. Omitting the field keeps error, byte for byte, for every server that does not opt in.

SurfaceUnder a declared severity
The Error in tool:<name> log recordEmitted at the declared level. Same message, same structured fields, the stack included.
mcp.errors.classifiedGains an mcp.error.severity attribute. The reason itself never becomes a metric attribute.
isError, the JSON-RPC code, structuredContent.error, content[]Byte-identical to the undeclared case.
Span status, mcp.tool.calls, mcp.tool.duration, mcp.tool.errorsUnchanged — the call still failed, and splitting those series would redefine what an error rate means.

Tools only. Resolution happens in the tool handler factory, against the thrown error's data.reason — the same reason-to-entry lookup that fills data.recovery. Resources declare errors[] but write no failure record, so the field is accepted there and inert. A reason thrown below the handler that the contract never declared, an entry with no severity, and a non-McpError throw all keep error. A cancelled request keeps its own info, stack-free path regardless.

The framework's own refusals log at notice. An argument rejection (invalid_arguments, raised only by the schema gate before the handler runs) and a ctx.requestInput the connection cannot serve (client_capability_missing) are routine caller or connection traffic, not server faults, so their Error in tool:<name> record — and the failure-payload record when LOG_TOOL_FAILURE_PAYLOADS=true — is emitted at notice, and mcp.errors.classified counts them with mcp.error.severity: "notice". Nothing to declare; an errors[] entry naming either reason with its own severity still wins. The wire envelope and mcp.tool.rejections are unchanged, and a schema that wrongly rejects valid calls still shows per tool on mcp.tool.rejections.

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:

ts
// my-service.ts
throw validationError('Expression cannot be empty.',  { reason: 'empty_expression' });
throw serviceUnavailable('Upstream timeout',          { reason: 'evaluation_timeout' });
ts
// 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 carries code, data.reason, and the declared entry's recovery as data.recovery.hint (filled at the handler boundary, whatever code the service picked), so 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.

Mark the entries the service produces. error-contract-unthrown reads the handler body alone, so in a handler that mixes one local precondition with service-thrown reasons it flags each service reason as dead. Add thrownBy: 'service' to those entries:

ts
errors: [
  { reason: 'empty_expression',   code: JsonRpcErrorCode.ValidationError,
    when: 'Input is empty.',
    recovery: 'Provide a non-empty expression to evaluate.',
    thrownBy: 'service' },
]

The field is lint-only metadata — nothing at runtime reads it, so the entry is typed, advertised, and thrown exactly as an unmarked one (its recovery filled like any other), and its reason stays in the ctx.fail / ctx.recoveryFor union. It suppresses the one rule that cannot see below the handler, and only for the entries it marks; the handler's own reasons keep being checked.


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.

A best-effort call that catches and degrades must still rethrow on ctx.signal?.aborted: catch (err) { if (ctx.signal?.aborted) throw err; return degraded(); }. One example is an enrichment lookup whose failure should return the primary result with a notice. The factory maps a cancelled handler to RequestCancelled only when the handler throws. A catch-all degrade turns the caller's cancellation into a "successful" response and logs a false failure warning.


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 }.

ts
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)
requestCancelled(msg, data?, options?)RequestCancelled (-32011) — caller went away

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


McpError Constructor

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

ts
throw new McpError(code, message?, data?, options?)
  • code — a JsonRpcErrorCode enum value
  • message — optional human-readable description of the failure
  • data — optional structured data (plain object), returned to the client verbatim. Pass the explicit fields the caller acts on (the rejected key, a limit, a reason), never ctx or another request context: a handler ctx carries request metadata and, after an elicitation round, what the user typed. Framework helpers follow the same rule — a storage, parser, or formatter failure carries only its offending field or a reason, whatever context you pass them.
  • options — optional { cause?: unknown } for error chaining

Example:

ts
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
RequestCancelled-32011Caller abandoned the request — client disconnect, external abort signal. Framework-raised; never retried, logged at info
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. Request signal aborted — ctx.signal.aborted is true when the handler unwinds → RequestCancelled. Resolved before the thrown value is classified at all — by the tool and resource handler factories, and by the HTTP transport's error handler against the inbound request's signal, which catches a caller that hangs up before any handler runs (mid-body, say) and answers it 499 — so it outranks every step below, McpError included: the caller withdrew the request, and what the handler threw on the way out does not change that. Covers every shape an abort leaves behind — a notifications/cancelled reason string, the DOMException named AbortError a reason-less cancellation produces, a service's own McpError, and the SDK's SdkError(ConnectionClosed) on transport close. The accepted cost is that an unrelated fault raised after the abort is recorded as a cancellation too; it is bounded, because the SDK writes no response for a request whose signal it aborted. A handler that throws while the signal is live is untouched by this step.
  2. McpError instance — error.code is preserved as-is; no classification needed.
  3. SDK transport-closed rejection — an SdkError carrying SdkErrorCode.ConnectionClosed → RequestCancelled. The SDK rejects every in-flight request when the transport closes, which is what a client disconnect looks like from inside a handler. Matched on the code, not the message: one of its wordings says "aborted" and would otherwise be caught by the generic abort pattern in step 7 and read as a Timeout. Still the rule for a throw raised where no request signal is in scope — a service, an outbound leg, a background task.
  4. Engine resource limit — a RangeError whose whole message is one the engine raises when it runs out of a resource → InternalError: Maximum call stack size exceeded (JavaScriptCore adds a trailing period) and the maximum string size (V8 Invalid string length, JavaScriptCore Out of memory). A handler that recurses without bound names nothing a caller can change, so it is a server fault. Every other RangeError — new Array(-1), (1).toFixed(101), an invalid date, 1n / 0n, or one whose message merely contains a limit text — continues to step 5.
  5. 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.
  6. 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).
  7. Common message/name patterns — broad keyword patterns covering auth, not-found, validation, etc. First match wins; order matters.
  8. AbortError name — error.name === 'AbortError' → Timeout.
  9. Fallback — InternalError.

However it is reached, a RequestCancelled is logged at info with no stack — neither the thrown value's own nor one reached through its cause chain. Step 1 settles the completion log too, which carries metrics.errorCode: "-32011" alongside isSuccess: false; a raw SdkError that reaches the code through step 3 alone is not an McpError, so that log still reads UNHANDLED_ERROR.

The code this ladder picks is the one the caller receives, and it is also the origin every error counter records: mcp.tool.error_category, mcp.prompt.error_category, and mcp.error.category on mcp.errors.classified all bucket that same code, so a plain Error('Request timed out') files as upstream everywhere, never server on one counter and upstream on another. See api-telemetry's Error category.

The framework's own output-contract parses are not caller errors. A result that breaks the definition's output schema (tools and resources) or its enrichment block fails as InternalError (-32603), with a message naming the definition and the contract — Tool my_tool returned output that does not match its output schema: items.0.id: … — and no data. It is the handler's bug, so it files as server, not the ValidationError a raw ZodError would get. A ZodError the handler throws from its own validation keeps ValidationError.

JS Constructor Name Mappings
ConstructorMapped Code
SyntaxErrorValidationError
RangeErrorValidationError (an engine resource limit is settled first, as InternalError — step 4)
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, fills a declared recovery, normalizes to McpError, sets isError: true, adds data.requestId, mirrors error across both client surfaces (see Error-path parity)
Handler factory (resources)Catches, fills a declared recovery, adds data.requestId, and re-throws to the SDK, which routes through the JSON-RPC error envelope
Prompt registration, HTTP transportLog the failure, then answer the JSON-RPC error with the thrown McpError's data plus data.requestId
Services/setup codeErrorHandler.tryCatch for structured logging and wrapping (always rethrows — never swallows)
Show full SKILL.md (2,939 more words)Show less
Error-path parity

MCP clients differ in which CallToolResult surface they forward to the agent. Tool errors mirror the success-path format-parity invariant — the text carries the message, the recovery hint, the two fields a caller branches on, and the request id, while the numeric code and data.issues stay JSON-only:

SurfaceContentRead by
content[]Text rendering: Error: <message>, then Recovery: <hint> when data.recovery.hint adds something the message does not already say, then (reason <reason> · not retryable · request <id>) for whichever of data.reason / data.retryable / data.requestId is presentClaude Desktop and other format()-only clients
structuredContent.errorJSON { code, message, data? } carrying the error code, message, any structured data from the thrown McpError or ZodError, and data.requestIdClaude Code and other structuredContent-only clients
text
Error: No data for 3 PMIDs

Recovery: Use pubmed_search_articles to discover valid PMIDs.

(reason no_match · not retryable · request UTFAC-QE0MB)

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, ZodError.issues, and the request id. Auto-classified plain errors (TypeError, network errors, etc.) emit code, message, and data: { requestId } only, so internal classification context never leaks to clients.
  • data.requestId names the request. The framework sets it on every error envelope it builds — a tool result (handler throws, argument rejections, auth refusals, output-contract failures), a failed resource read, a failed prompt, and the JSON-RPC errors httpErrorHandler returns — to the requestId that call's log records carry. On a tool, resource, or prompt call that is a generated XXXXX-XXXXX token, or the client's JSON-RPC id when that id is a string; httpErrorHandler generates its own token, the one on its Client error: record. A failure reported from the client resolves to its Error in tool:<name> record by that value. A resource read refused before it is measured (an auth refusal, or URI variables that fail params) carries an id no log record shares, since resources write no failure record of their own. It is added where the envelope is built, never to the thrown McpError.data, so ErrorHandler.handleError / tryCatch results and the log record's errorData stay context-free; it replaces a thrown data.requestId, the way canonical fields win in log records. Two envelopes go without it: a resource -32602 whose data is exactly { uri } (the resource-not-found shape clients match exactly), and runToolContract results, which have no real request. It closes the content[] terms line, alone as (request <id>) when there is no reason or retryable — so a test pinning content[0].text exactly sees it.
  • Recovery hint mirroring is automatic, unless the hint repeats the message. 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. The one exception is a hint the trimmed message already contains verbatim (case-sensitively) — an argument rejection whose every hint sentence restates an issue, where the hint is the message's issue text verbatim, and an author hint that restates its own message. There the line adds no next step, so it is dropped from the text; structuredContent.error.data.recovery.hint stays populated either way.
  • reason, retryable, and requestId render as a trailing term line. (reason malformed_id · not retryable · request UTFAC-QE0MB) closes the text whenever data.reason is a non-empty string, data.retryable is a boolean, or data.requestId is a non-empty string — retryable for true, not retryable for false, in that order. None present (an McpError with no data built outside a request, as runToolContract does) appends nothing at all. The numeric code and data.issues stay JSON-only on purpose: the code is the one envelope field a model cannot act on, and the message already renders each issue as a sentence. A consumer test pinning content[0].text exactly, rather than asserting it contains the diagnostic, therefore moves for any error carrying a reason.
  • Argument-schema rejection is a tool error with the same envelope. An unknown root key, a wrong type, a missing required field, or a failed constraint returns isError: true with structuredContent.error.code = -32602 (InvalidParams) and the readable Invalid arguments for tool <name>: … diagnostic in content[]. The handler never runs. Two neighbouring failures keep the protocol error path instead, arriving as a JSON-RPC error rather than a tool result: an unknown or disabled tool name, and a malformed request envelope.
  • invalid_arguments is the framework-owned reason on every argument rejection. The rejection carries data.reason: "invalid_arguments" and a data.recovery.hint the framework synthesizes from the Zod issues, the arguments as sent, and the root schema — an unknown root key names the root properties the tool does accept, an unknown key inside a nested strict object names its full path and that object's own keys (Unknown key opts.b. opts accepts: a., Unknown key items.1.b. items.1 accepts: name.), a wrong type names the type to send instead (a fractional number on an integer field reads Send rows as an integer, not a fractional number.), missing fields collapse into one Provide … sentence, and anything else restates its diagnostic line, field path included (start: Must be a parseable ISO 8601 date), so identical constraints on different fields stay distinguishable. When every sentence restates an issue, the hint is the message's issue text verbatim, and its Recovery: line is dropped from content[]. Beside a framework sentence each restatement is terminated and a repeated sentence appears once: start: Must be a parseable ISO 8601 date. Send n as a number, not a string. When pre-validation rewrote or dropped a key the caller wrote, the rejection says so, since its issues name only the keys that were validated: data.input carries { aliased: [{ alias, target }], ignored: [...] } — keys only, in argument order, ignored holding the undeclared underscore-prefixed keys the drop discarded — and the hint closes with Validated query as targetQuery. / Dropped undeclared key _max., framework sentences that keep the Recovery: line. An ignore-list drop (_meta, toolCallId, a server's input.ignoreKeys) is a client artifact and is never reported, and a rejection the step changed nothing on carries no data.input. The reason renders on the closing (reason invalid_arguments · request <id>); this path sets no retryable. Its Error in tool:<name> record logs at notice, not error. Authors declare nothing for this: the rejection happens before the handler and the hint is derived from the schema.
  • client_capability_missing is the other framework-owned reason. When a handler returns ctx.requestInput({ inputRequests: … }) on a 2025-era connection whose client declared no matching capability, ctx.requestInput throws this failure in place of the input-required signal, before anything reaches the wire. It is an ordinary handler throw from there on, logged at notice: measured as the failed call it is, and shaped by the family's usual error path — a tool gets structuredContent.error.code = -32600 (InvalidRequest), data.reason: "client_capability_missing", and a data.recovery.hint naming the capability; a resource read gets the same code, reason, and hint through the JSON-RPC error envelope. The hint ends at reconnecting with a client that declares the capability, since a consent gate has no argument that could stand in for its answer; a handler whose arguments can appends its own sentence per call with ctx.requestInput(spec, { fallbackHint }). Like invalid_arguments, a definition cannot declare it in errors[]: it names a property of the connection, not a domain outcome. See api-context's ctx.requestInput.
  • A schema constraint cannot carry a declared reason. Because the handler never runs, a rejection by .max(), .regex(), .min(), or any other Zod refinement bypasses errors[] entirely: it arrives as InvalidParams with data.issues under the framework's invalid_arguments, never the reason and authored recovery of a contract entry — so a caller has nothing tool-specific to branch on and gets only the schema-derived hint. Decide per constraint which surface it belongs on. A bound that is purely structural — the input is the wrong shape and no guidance beyond the diagnostic would help — belongs on the schema, where it also advertises itself in inputSchema. A bound a caller is expected to recover from belongs in the handler as ctx.fail('reason', message) against a declared errors[] entry, whose recovery the framework puts on the wire, with the limit restated in the field's .describe() so it is still visible before the call. Enforcing the same bound in both places is the trap: the schema wins, and the contract entry becomes unreachable while still reading as covered.
  • A rejected value never reaches the client. The rendered sentence distinguishes an omitted field from a wrong one (what: Missing required field. Expected one of "os"|"cpu" rather than the invalid-option text), and a union renders the branch that says what would have been accepted instead of Zod's Invalid input placeholder. Both read the arguments in-process for the absent/present bit and the arriving type only — data.issues ships the Zod issues as-is, and no value the caller sent is copied onto them.
  • A union branch names its own field. Each branch issue is prefixed with the path it names relative to that branch, so two alternatives differing only in which field they require stay distinguishable: spec: kind: Invalid option: expected one of "x"|"y"; n: Invalid input: expected number, received undefined or other: Invalid input: expected string, received undefined. Issues within one branch join on ; , across branches on or, and top-level issues on , — three nestings, three separators. A scalar branch carries no path and renders as before. data.issues still ships the raw nested Zod issues, and data.recovery.hint restates the same line, field path included.
  • A one-or-many union renders like the field it wraps. Once a union branch fails below its root, every branch whose only issue is a root type mismatch is dropped — for z.union([z.array(Item), Item]) given a list, that is the object branch saying only that the value is an array. If one branch remains, its issues render and hint under the field's path exactly as they would on a non-union field: items.1.name: Invalid input: expected string, received boolean, hinted Send items.1.name as a string, not a boolean. A missing element field is hinted Provide items.1.name., and the rule applies again at every nested level. When every branch fails at its root (items: "x"), all of them render, joined by or. data.issues keeps Zod's single invalid_union issue.
  • Some rejections never happen at all. An ordered pre-validation step wraps the parse: a client-added root key is dropped, a declared or case-style key alias is rewritten to its canonical name, and — only after a failed parse — a JSON-stringified array or object, or a safe integer sent for a string, is repaired and the arguments parsed once more. When that still fails and the drop discarded a key, the step retries alias-first. A call the step rescues succeeds outright and produces no error envelope; a call it cannot rescue throws the rejection above exactly as it would under input: { coerce: false } — same code, message, data.issues, data.input, and data.recovery.hint — so a discarded repair leaves no trace. An integer sent to a string field, or a stringified object to an object field, is therefore a success, not a wrong-type case — a test that needs a wrong-type rejection sends a boolean. See the add-tool skill for the boundaries and the per-server switches.

Handler — throw freely, no try/catch:

ts
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.

ts
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()).

The thrown error's data is wire-visible. A handler that lets it propagate forwards it as structuredContent.error.data (tools) or JSON-RPC error.data (resources, prompts). It carries the caught McpError's own data, originalErrorName, originalMessage, and rootCause ({ name, message }) — never a stack and never context: originalStack, the full causeChain, and every context field (requestId, sessionId, traceId, tenantId, extra, …) go to the log record only. A field the caller should act on belongs in the thrown McpError's data, not in context. (The handler factory adds the call's own data.requestId when it builds the envelope; that value never comes from context here.)

Options (Omit<ErrorHandlerOptions, 'rethrow'>):

OptionTypeRequiredPurpose
operationstringYesName logged with the error
contextErrorContextNoStructured fields merged into the log record only — never the thrown error's client-visible data; 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:

ts
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 },               // the framework adds data.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.

error.data reaches the client. It 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. The upstream URL defaults the other way and is omitted, since a request URL routinely carries user input, internal identifiers, or an API key in its query string; includeUrl: true puts the full response.url on data.url. The message names the host either way. Response headers are opt-in the same way: errorHeaders: ['x-request-id'] copies the named headers onto data.headers under lowercase keys, and everything selected is client-facing — never name a header that carries a credential, and note that a selected Location can itself carry a sensitive path, query, or token. set-cookie is never captured whatever the selector says.

Full status table:

StatusCode
3xxInvalidRequest — reachable under redirect: 'manual', and outside withRetry's transient set since re-issuing returns the same redirect
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, 501, 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 definition linter (bun run lint:mcp, and devcheck's MCP Definitions step) checks handler bodies for common anti-patterns. It runs at build time, never at server startup. All emit warnings (not errors): they show up in devcheck output but don't fail it.

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
error-contract-severity-unknownerrorseverity is present but isn't one of debug / info / notice / warning. It selects a logger method at runtime; omit the field for the default error level
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.
error-contract-unthrownwarningA declared reason that no literal ctx.fail('<reason>' or ctx.recoveryFor('<reason>' in the handler names. Fires only when the handler already holds at least one literal ctx.fail(, and skips the definition entirely when either callee takes a non-literal first argument. Wire the throw, drop the entry, or mark it thrownBy: 'service'.
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
  • RequestCancelled — the caller disconnected or aborted mid-call

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

© cyanheads, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in framework-skills/api-errors of cyanheads/pubmed-mcp-server.

Open the folder on GitHubat commit 5a417fb

Compare with similar skills

API Errors next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

API Errors compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
API Errors this skillcyanheads/pubmed-mcp-server155—~13kAutomated safety check: PassApache-2.0
Read GitHubAgentTeam-TaichuAI/ScienceClaw6702 repos~638Automated safety check: PassNone
FirstdataMLT-OSS/FirstData183—~3.1kAutomated safety check: PassMIT
Serply Search MCPsickn33/agentic-awesome-skills47k1 repos~1.5kAutomated safety check: PassMIT
Bgpt MCPClawBio/ClawBio1.2k1 repos~3.2kAutomated safety check: PassMIT
Setup MedsciAperivue/medsci-skills329—~960Automated safety check: PassMIT

Similar skills

  • Read GitHub

    AgentTeam-TaichuAI/ScienceClaw

    Read and search GitHub repository documentation via gitmcp.io MCP service.

    670 GitHub starsUsed in 2 repos~638 tokens
    Research & ScienceAuto-check passed
  • Firstdata

    MLT-OSS/FirstData

    Find official portals, APIs, and download paths for authoritative primary data sources (governments, international organizations, research institutions, etc.).

    183 GitHub stars~3.1k tokensUpdated 5 days ago
    Research & ScienceAuto-check passed
  • Serply Search MCP

    sickn33/agentic-awesome-skills

    Search Google, Bing, Google News and Google Scholar, and read public pages, with the Serply MCP server.

    47k GitHub starsUsed in 1 repo~1.5k tokens
    Research & ScienceAuto-check passed
  • Bgpt MCP

    ClawBio/ClawBio

    Search scientific papers via the BGPT MCP server and retrieve structured experimental data — methods, results, conclusions, quality scores, and 25+ metadata fields per paper.

    1.2k GitHub starsUsed in 1 repo~3.2k tokens
    Research & ScienceAuto-check passed
  • Setup Medsci

    Aperivue/medsci-skills

    A skill your agent uses when a skill fails for a missing tool or the environment needs checking.

    329 GitHub stars~960 tokensUpdated 2 days ago
    Research & ScienceAuto-check passed
  • G1

    brycewang-stanford/Auto-Empirical-Research-Skills

    VS-Enhanced Journal Matcher with Journal Intelligence MCP — Real-time journal data pipeline with checkpoint-based human decisions.

    4.5k GitHub stars~3.7k tokensUpdated 2 days ago
    Research & ScienceAuto-check passed

More from cyanheads/pubmed-mcp-server

All 30 skills in this repo
  • Add App Tool

    cyanheads/pubmed-mcp-server

    Scaffold an MCP App tool + UI resource pair. An agent skill from cyanheads/pubmed-mcp-server.

    155 GitHub stars~3.2k tokensUpdated 3 days ago
    Auto-check passed
  • Add Prompt

    cyanheads/pubmed-mcp-server

    Scaffold a new MCP prompt template. An agent skill from cyanheads/pubmed-mcp-server.

    155 GitHub stars~1.6k tokensUpdated 3 days ago
    Auto-check passed
  • Add Resource

    cyanheads/pubmed-mcp-server

    Scaffold a new MCP resource definition. An agent skill from cyanheads/pubmed-mcp-server.

    155 GitHub stars~3k tokensUpdated 3 days ago
    Auto-check passed
  • Add Service

    cyanheads/pubmed-mcp-server

    Scaffold a new service integration. An agent skill from cyanheads/pubmed-mcp-server.

    155 GitHub stars~3.6k tokensUpdated 3 days ago
    Auto-check passed
  • Add Test

    cyanheads/pubmed-mcp-server

    Scaffold a test file for an existing tool, resource, or service.

    155 GitHub stars~4.1k tokensUpdated 3 days ago
    Auto-check passed
  • API Auth

    cyanheads/pubmed-mcp-server

    Authentication, authorization, and multi-tenancy patterns for @cyanheads/mcp-ts-core.

    155 GitHub stars~2.7k tokensUpdated 3 days ago
    Auto-check passed

Questions about API Errors

What does API Errors do?

McpError constructor, JsonRpcErrorCode reference, and error handling patterns for @cyanheads/mcp-ts-core. API Errors is an agent skill from cyanheads/pubmed-mcp-server. McpError constructor, JsonRpcErrorCode reference, and error handling patterns for @cyanheads/mcp-ts-core.

When should I use API Errors?

API Errors fits situations like: looking up error codes; understanding where errors should be thrown vs.

How do I install API Errors in Claude Code?

Run `npx skills add cyanheads/pubmed-mcp-server --skill api-errors -a claude-code`. Or copy the skill folder (framework-skills/api-errors in cyanheads/pubmed-mcp-server) into .claude/skills/api-errors in your project. Claude Code loads it when a task matches its description.

How do I install API Errors in Codex?

Run `npx skills add cyanheads/pubmed-mcp-server --skill api-errors -a codex`. Or copy the skill folder (framework-skills/api-errors in cyanheads/pubmed-mcp-server) into .agents/skills/api-errors in your project. Codex loads it when a task matches its description.

Can I use API Errors in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add cyanheads/pubmed-mcp-server --skill api-errors -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/api-errors, .gemini/skills/api-errors, .github/skills/api-errors and .opencode/skills/api-errors in your project.

What does API Errors need to run?

Going by SKILL.md and its folder, API Errors needs the command-line tools its instructions call (bun).

Does API Errors access the network?

SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.

Is API Errors safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does API Errors use?

API Errors is published under the Apache-2.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does API Errors use?

About 13k tokens (SKILL.md is roughly 52k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to API Errors?

Skills that share tags, products or a category with API Errors: Read GitHub (AgentTeam-TaichuAI/ScienceClaw, 670 stars), Firstdata (MLT-OSS/FirstData, 183 stars), Serply Search MCP (sickn33/agentic-awesome-skills, 47k stars) and Bgpt MCP (ClawBio/ClawBio, 1.2k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains API Errors?

cyanheads (a GitHub user) maintains it in cyanheads/pubmed-mcp-server, which has 155 GitHub stars. The repository holds 30 skills in this directory. The repository was last updated on October 4, 2026.

Source: cyanheads/pubmed-mcp-server on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.