Read GitHub
AgentTeam-TaichuAI/ScienceClaw
Read and search GitHub repository documentation via gitmcp.io MCP service.
McpError constructor, JsonRpcErrorCode reference, and error handling patterns for @cyanheads/mcp-ts-core.
$ npx skills add cyanheads/pubmed-mcp-server --skill api-errors -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install cyanheads/pubmed-mcp-server api-errors --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ 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-srcUse ~/.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/
Install the "api-errors" agent skill from https://github.com/cyanheads/pubmed-mcp-server/tree/main/framework-skills/api-errors into .claude/skills/api-errors/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "api-errors", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/cyanheads/pubmed-mcp-server/tree/main/framework-skills/api-errorsType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add cyanheads/pubmed-mcp-server --skill api-errors -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install cyanheads/pubmed-mcp-server api-errors --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/cyanheads/pubmed-mcp-server.git skills-src && mkdir -p .agents/skills && cp -r skills-src/framework-skills/api-errors .agents/skills/api-errors && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "api-errors" agent skill from https://github.com/cyanheads/pubmed-mcp-server/tree/main/framework-skills/api-errors into .agents/skills/api-errors/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "api-errors", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add cyanheads/pubmed-mcp-server --skill api-errors -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install cyanheads/pubmed-mcp-server api-errors --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/cyanheads/pubmed-mcp-server.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/framework-skills/api-errors .cursor/skills/api-errors && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "api-errors" agent skill from https://github.com/cyanheads/pubmed-mcp-server/tree/main/framework-skills/api-errors into .cursor/skills/api-errors/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "api-errors", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/cyanheads/pubmed-mcp-server.git --path framework-skills/api-errors--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add cyanheads/pubmed-mcp-server --skill api-errors -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install cyanheads/pubmed-mcp-server api-errors --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/cyanheads/pubmed-mcp-server.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/framework-skills/api-errors .gemini/skills/api-errors && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "api-errors" agent skill from https://github.com/cyanheads/pubmed-mcp-server/tree/main/framework-skills/api-errors into .gemini/skills/api-errors/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "api-errors", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install cyanheads/pubmed-mcp-server api-errorsInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add cyanheads/pubmed-mcp-server --skill api-errors -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/cyanheads/pubmed-mcp-server.git skills-src && mkdir -p .github/skills && cp -r skills-src/framework-skills/api-errors .github/skills/api-errors && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "api-errors" agent skill from https://github.com/cyanheads/pubmed-mcp-server/tree/main/framework-skills/api-errors into .github/skills/api-errors/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "api-errors", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add cyanheads/pubmed-mcp-server --skill api-errors -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install cyanheads/pubmed-mcp-server api-errors --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/cyanheads/pubmed-mcp-server.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/framework-skills/api-errors .opencode/skills/api-errors && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "api-errors" agent skill from https://github.com/cyanheads/pubmed-mcp-server/tree/main/framework-skills/api-errors into .opencode/skills/api-errors/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "api-errors", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
api-errorsMcpError 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. 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.
9 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit 5a417fb. It shows what the files ask for, not the result of running them.
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.
Shell commands in SKILL.md call:
bunFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
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.
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.
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.
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.
.claude/skills/api-errors/SKILL.md (or your agent's skills folder).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';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:
| Surface | Behavior |
|---|---|
| Compile time | ctx.fail('typo') is a TS error. Auto-completes declared reasons. |
| Runtime | ctx.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). |
recoveryis the wire default for its reason. The contractrecoveryis 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 whosedata.reasonnames a declared entry reaches the tool or resource handler factory with nodata.recovery, the factory setsdata.recovery.hintto that entry'srecoverybefore it logs the failure and builds the envelope, so theError in tool:<name>record,structuredContent.error.data, and theRecovery:line incontent[]carry the same hint. It matches on the reason alone — a barectx.fail('reason'), a service throwingnotFound(msg, { reason }), and a declared reason raised through a factory with a different code all get it. A throw-siterecoveryalways wins, whatever its shape. An undeclared reason, a tool withouterrors[], a non-McpErrorthrow, and a cancelled call get nothing, and the framework-ownedinvalid_arguments/client_capability_missingrefusals keep their own hints. The thrownMcpErroris never changed — a handler-level test ofctx.failsees exactly what the throw site wrote — andrunToolContractapplies the same fill, so a contract test sees the production envelope. Prompts declare no contract.
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:
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:
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 toolis 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 sitectx.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 errorAn 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:
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.
| Surface | Under a declared severity |
|---|---|
The Error in tool:<name> log record | Emitted at the declared level. Same message, same structured fields, the stack included. |
mcp.errors.classified | Gains 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.errors | Unchanged — 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 dynamicrecoveryhints 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
throwstatements. Errors thrown from called services (e.g.await myService.fetch()raisingRateLimitedinternally) 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.
reason from servicesServices 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 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:
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.
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.
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:
| 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) |
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.
For codes not covered by factories (rare — MethodNotFound, ParseError, InitializationFailed, UnknownError):
throw new McpError(code, message?, data?, options?)code — a JsonRpcErrorCode enum valuemessage — optional human-readable description of the failuredata — 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 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 |
RequestCancelled | -32011 | Caller abandoned the request — client disconnect, external abort signal. Framework-raised; never retried, logged at info |
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:
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.McpError instance — error.code is preserved as-is; no classification needed.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.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.ZodError → ValidationError, SyntaxError → ValidationError). Note: TypeError is intentionally excluded — runtime TypeErrors are programmer errors, not validation failures.status code 429 beats the generic rate limit pattern).AbortError name — error.name === 'AbortError' → Timeout.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.
| Constructor | Mapped Code |
|---|---|
SyntaxError | ValidationError |
RangeError | ValidationError (an engine resource limit is settled first, as InternalError — step 4) |
URIError | ValidationError |
ZodError | ValidationError |
ReferenceError | InternalError |
EvalError | InternalError |
AggregateError | InternalError |
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.
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 (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 transport | Log the failure, then answer the JSON-RPC error with the thrown McpError's data plus data.requestId |
| Services/setup code | ErrorHandler.tryCatch for structured logging and wrapping (always rethrows — never swallows) |
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:
| Surface | Content | Read 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 present | Claude Desktop and other format()-only clients |
structuredContent.error | JSON { code, message, data? } carrying the error code, message, any structured data from the thrown McpError or ZodError, and data.requestId | Claude Code and other structuredContent-only clients |
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.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.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..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.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.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.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.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:
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()).
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'>):
| Option | Type | Required | Purpose |
|---|---|---|---|
operation | string | Yes | Name logged with the error |
context | ErrorContext | No | Structured fields merged into the log record only — never the thrown error's client-visible data; 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 |
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 }, // 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.datareaches the client. It is forwarded to the MCP client asstructuredContent.error.data(tool errors) or JSON-RPCerror.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, passcaptureBody: false(orbodyLimit: 0) so the body stays out ofdata. Defaults remaincaptureBody: truebecause 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: trueputs the fullresponse.urlondata.url. The message names the host either way. Response headers are opt-in the same way:errorHeaders: ['x-request-id']copies the named headers ontodata.headersunder lowercase keys, and everything selected is client-facing — never name a header that carries a credential, and note that a selectedLocationcan itself carry a sensitive path, query, or token.set-cookieis never captured whatever the selector says.
Full status table:
| Status | Code |
|---|---|
| 3xx | InvalidRequest — reachable under redirect: 'manual', and outside withRetry's transient set since re-issuing returns the same redirect |
| 400 | InvalidParams |
| 401 | Unauthorized |
| 402, 403 | Forbidden |
| 404 | NotFound |
| 408, 425, 504 | Timeout |
| 409, 423, 424 | Conflict |
| 422 | ValidationError |
| 429 | RateLimited |
| 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.
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.
| Rule | Catches |
|---|---|
prefer-mcp-error-in-handler | throw new Error(...) inside a handler — use McpError or a factory so the framework returns a specific code |
prefer-error-factory | new McpError(JsonRpcErrorCode.NotFound, ...) when notFound(...) exists |
preserve-cause-on-rethrow | catch (e) { throw new McpError(...) } without { cause: e } |
no-stringify-upstream-error | JSON.stringify(...) inside a thrown message — risks leaking internal traces; use data payload instead |
The linter validates the structure of errors[] and (when present) cross-checks the handler body against the declared contract.
| Rule | Severity | Catches |
|---|---|---|
error-contract-type | error | errors is present but not an array |
error-contract-empty | warning | errors: [] — drop the field instead, or declare actual failure modes |
error-contract-entry-type | error | An entry isn't an object |
error-contract-code-type | error | code missing or not a number |
error-contract-code-unknown | error | code isn't a real JsonRpcErrorCode value |
error-contract-code-unknown-error | warning | code is JsonRpcErrorCode.UnknownError (the giveup-fallback — pick a more specific code) |
error-contract-reason-required | error | reason missing or empty |
error-contract-reason-format | warning | reason not snake_case |
error-contract-reason-unique | error | Duplicate reason within one contract |
error-contract-when-required | error | when missing or empty |
error-contract-recovery-required | error | recovery missing or not a string |
error-contract-recovery-empty | error | recovery is empty/whitespace-only |
error-contract-recovery-min-words | warning | recovery has fewer than 5 words — placeholders like "Try again." or "Check input." get flagged in favor of specific guidance |
error-contract-retryable-type | warning | retryable is present but not a boolean |
error-contract-severity-unknown | error | severity 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 |
| Rule | Severity | Catches |
|---|---|---|
error-contract-conformance | warning | Handler 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-fail | warning | Handler 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-unthrown | warning | A 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'. |
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 unexpectedServiceUnavailable — upstream/network failuresTimeout — request deadline exceeded, abortValidationError — schema violations, malformed inputSerializationError — JSON/XML parse failuresRequestCancelled — the caller disconnected or aborted mid-callIf 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.
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.
| Pattern | Use for |
|---|---|
throw ctx.fail('reason', …) | Declared domain failures — typed, contract-checked, data.reason populated |
throw notFound(…) / factories | Errors not in the contract; the auto-classifier handles them. Prefer ctx.fail when a matching contract entry exists. |
| Bubble up from services | Upstream 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
Just SKILL.md in framework-skills/api-errors of cyanheads/pubmed-mcp-server.
Open the folder on GitHubat commit 5a417fb
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.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| API Errors this skillcyanheads/pubmed-mcp-server | 155 | — | ~13k | Automated safety check: Pass | Apache-2.0 | |
| Read GitHubAgentTeam-TaichuAI/ScienceClaw | 670 | 2 repos | ~638 | Automated safety check: Pass | None | |
| FirstdataMLT-OSS/FirstData | 183 | — | ~3.1k | Automated safety check: Pass | MIT | |
| Serply Search MCPsickn33/agentic-awesome-skills | 47k | 1 repos | ~1.5k | Automated safety check: Pass | MIT | |
| Bgpt MCPClawBio/ClawBio | 1.2k | 1 repos | ~3.2k | Automated safety check: Pass | MIT | |
| Setup MedsciAperivue/medsci-skills | 329 | — | ~960 | Automated safety check: Pass | MIT |
AgentTeam-TaichuAI/ScienceClaw
Read and search GitHub repository documentation via gitmcp.io MCP service.
MLT-OSS/FirstData
Find official portals, APIs, and download paths for authoritative primary data sources (governments, international organizations, research institutions, etc.).
sickn33/agentic-awesome-skills
Search Google, Bing, Google News and Google Scholar, and read public pages, with the Serply MCP server.
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.
Aperivue/medsci-skills
A skill your agent uses when a skill fails for a missing tool or the environment needs checking.
brycewang-stanford/Auto-Empirical-Research-Skills
VS-Enhanced Journal Matcher with Journal Intelligence MCP — Real-time journal data pipeline with checkpoint-based human decisions.
cyanheads/pubmed-mcp-server
Scaffold an MCP App tool + UI resource pair. An agent skill from cyanheads/pubmed-mcp-server.
cyanheads/pubmed-mcp-server
Scaffold a new MCP prompt template. An agent skill from cyanheads/pubmed-mcp-server.
cyanheads/pubmed-mcp-server
Scaffold a new MCP resource definition. An agent skill from cyanheads/pubmed-mcp-server.
cyanheads/pubmed-mcp-server
Scaffold a new service integration. An agent skill from cyanheads/pubmed-mcp-server.
cyanheads/pubmed-mcp-server
Scaffold a test file for an existing tool, resource, or service.
cyanheads/pubmed-mcp-server
Authentication, authorization, and multi-tenancy patterns for @cyanheads/mcp-ts-core.
Works with
Categories
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.
API Errors fits situations like: looking up error codes; understanding where errors should be thrown vs.
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.
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.
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.
Going by SKILL.md and its folder, API Errors needs the command-line tools its instructions call (bun).
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.
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.
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.
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.
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.
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.