UNPKG

@cyanheads/pubmed-mcp-server

Version:

Search PubMed/Europe PMC, fetch articles and full text (PMC/EPMC/Unpaywall), citations, MeSH terms via MCP. STDIO or Streamable HTTP.

121 lines 5.38 kB
/** * @fileoverview Canonical service-layer error contracts. Single source of truth * for the failure modes both services throw and tools declare in `errors[]`. * * Service-layer code can't reach `ctx.recoveryFor` (no Context), so it spreads * `recoveryFor(reason)` from this module into the error factory's `data` arg. * The framework mirrors `data.recovery.hint` into the wire payload's * `content[]` text, so clients get the same actionable hint they would from a * handler-level `ctx.fail`. * * Tool definitions import the contract arrays directly and spread them into * their `errors: [...]` declarations to surface the failure modes to the LLM. * * @module src/services/error-contracts */ import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors'; /** * Failure modes the NCBI service layer can surface. Tools that consume * `getNcbiService()` should spread these into their own `errors[]` so the * declared contract matches what actually reaches the wire. */ export const NCBI_SERVICE_ERRORS = [ { reason: 'queue_full', code: JsonRpcErrorCode.RateLimited, when: 'Local NCBI request queue is at capacity.', recovery: 'Retry after 1-2 seconds; the request queue hit the NCBI rate limit.', retryable: true, }, { reason: 'ncbi_unreachable', code: JsonRpcErrorCode.ServiceUnavailable, when: 'NCBI E-utilities is unreachable after all retry attempts.', recovery: 'Retry after a brief delay; NCBI was unreachable across all retry attempts.', retryable: true, }, { reason: 'ncbi_deadline_exceeded', code: JsonRpcErrorCode.Timeout, when: 'Total request deadline expired before NCBI returned a response.', recovery: 'Reduce batch size or retry; NCBI may be under temporary load.', retryable: true, }, { reason: 'ncbi_invalid_response', code: JsonRpcErrorCode.SerializationError, when: 'NCBI returned a body that could not be parsed (invalid XML/JSON).', recovery: 'Retry the request; NCBI returned a malformed response that could not be parsed.', retryable: true, }, { reason: 'ncbi_resource_not_found', code: JsonRpcErrorCode.NotFound, when: 'NCBI returned a structured "not found" error for the requested ID(s).', recovery: 'Verify the ID exists in PubMed; the resource was not found in NCBI and retrying will not help.', retryable: false, }, ]; /** * Failure modes the Unpaywall service layer can surface. Tools that consume * `getUnpaywallService()` (currently `pubmed_fetch_fulltext`) should spread * these into their `errors[]`. */ export const UNPAYWALL_SERVICE_ERRORS = [ { reason: 'unpaywall_unreachable', code: JsonRpcErrorCode.ServiceUnavailable, when: 'Unpaywall was unreachable when resolving a DOI or fetching content.', recovery: 'Retry after a brief delay; Unpaywall was unreachable. The PMC source remains the primary path.', retryable: true, }, ]; /** * Failure modes the Europe PMC service layer can surface. Tools that consume * `getEuropePmcService()` should spread these into their `errors[]`. */ export const EUROPEPMC_SERVICE_ERRORS = [ { reason: 'europepmc_unreachable', code: JsonRpcErrorCode.ServiceUnavailable, when: 'Europe PMC was unreachable after all retry attempts.', recovery: 'Retry after a brief delay; Europe PMC was unreachable. NCBI PMC and Unpaywall remain available.', retryable: true, }, { reason: 'europepmc_invalid_response', code: JsonRpcErrorCode.SerializationError, when: 'Europe PMC returned a body that could not be parsed (invalid JSON or XML).', recovery: 'Retry the request; Europe PMC returned a malformed response that could not be parsed.', retryable: true, }, { reason: 'europepmc_invalid_input', code: JsonRpcErrorCode.ValidationError, when: 'Europe PMC rejected the request input (empty query, unknown sort field, malformed parameter).', recovery: 'Adjust the input — usually the query or sort field — before retrying; the same input will be rejected again.', retryable: false, }, ]; const REASON_TO_RECOVERY = new Map([...NCBI_SERVICE_ERRORS, ...UNPAYWALL_SERVICE_ERRORS, ...EUROPEPMC_SERVICE_ERRORS].map((entry) => [entry.reason, entry.recovery])); /** * Service-layer counterpart to `ctx.recoveryFor`. Returns `{ recovery: { hint } }` * for the contract reason. Use at every service throw that stamps a `reason` so * the wire payload carries the same actionable hint the LLM gets from * handler-level `ctx.fail`. * * The parameter is constrained to `ServiceErrorReason`, so typos fail at compile * time. The runtime guard catches the impossible case where the reason union * and the recovery map drift apart in future edits. * * @example * throw serviceUnavailable(msg, { reason: 'ncbi_unreachable', ...recoveryFor('ncbi_unreachable') }); */ export function recoveryFor(reason) { const hint = REASON_TO_RECOVERY.get(reason); if (hint === undefined) { throw new Error(`recoveryFor: no recovery hint registered for reason "${reason}"`); } return { recovery: { hint } }; } //# sourceMappingURL=error-contracts.js.map