Agent skill

API Utils

by cyanheads in cyanheads/pubmed-mcp-server

API reference for all utilities exported from @cyanheads/mcp-ts-core/utils.

Apache-2.0Auto-check passedResearch & Science

Install API Utils

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

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

GitHub CLI
$ gh skill install cyanheads/pubmed-mcp-server api-utils --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-utils .claude/skills/api-utils && 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-utils
GitHub stars
155
Token cost
~8.1k tokens
SKILL.md length
3,821 words
Files
4 (incl. references)
Skills in repo
30
Repo updated
First seen
Licence
Apache-2.0

At a glance

API reference for all utilities exported from @cyanheads/mcp-ts-core/utils.

  • Looking up utility method signatures
  • SKILL.md covers Overview, References, @cyanheads/mcp-ts-core/utils —… and @cyanheads/mcp-ts-core/utils —…, plus 9 more sections
  • Calls bun
  • Peer dependencies

What it does

API Utils is an agent skill from cyanheads/pubmed-mcp-server. API reference for all utilities exported from @cyanheads/mcp-ts-core/utils. Use when looking up utility method signatures, options, peer dependencies, or usage patterns.

Its SKILL.md is about 8.1k tokens, which your agent loads only when the skill is triggered. The skill folder holds 4 other files, including reference files (for example `references/formatting.md`, `references/parsing.md` and `references/security.md`).

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 utility method signatures
  • Peer dependencies

Example prompts

  • “/api-utils”

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 Utils loads about 8.1k tokens when it runs, and up to ~16k if it reads all its reference files. Until then it costs about 45 tokens; SKILL.md has 3,821 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~45
When it runs · the whole SKILL.md, loaded when a task matches
~8.1k
With references · SKILL.md plus every file in references/, read only if the agent opens them
~16k

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). 3,821 words, ~8,122 tokens.

Download SKILL.mdSave it as .claude/skills/api-utils/SKILL.md (or your agent's skills folder). This skill also uses 3 other files; get the full folder from GitHub.
name
api-utils
description
API reference for all utilities exported from `@cyanheads/mcp-ts-core/utils`. Use when looking up utility method signatures, options, peer dependencies, or usage patterns.
metadata.author
cyanheads
metadata.version
2.15
metadata.audience
external
metadata.type
reference

Overview

Utility exports from @cyanheads/mcp-ts-core/utils. Utilities with complex APIs have dedicated reference files; simpler utilities are documented inline below.

Tier 3 = optional peer dependency. Install as needed (e.g., bun add js-yaml). All Tier 3 methods are async (lazy-load deps on first call).

Context parameters. Every helper below that takes a context accepts the handler Context as well as a RequestContext bag — pass ctx straight through, no slicing.

References

ReferencePathCovers
Formattingreferences/formatting.mdmarkdown(), MarkdownBuilder, diffFormatter, tableFormatter, treeFormatter — builder patterns, option types, style variants, usage examples
Parsingreferences/parsing.mdyamlParser, xmlParser, csvParser, jsonParser, pdfParser, dateParser, frontmatterParser — method signatures, option types, peer deps, Allow flags, PDF workflows
Securityreferences/security.mdsanitization, RateLimiter, IdGenerator — config types, method details, sensitive fields, usage examples

@cyanheads/mcp-ts-core/utils — network

ExportAPINotes
fetchWithTimeout(url, timeoutMs, context, options?: FetchWithTimeoutOptions) -> Promise<Response>Wraps fetch with AbortController timeout. timeoutMs bounds the whole exchange: on a 2xx carrying a body the returned Response is a passthrough wrapper that keeps the deadline armed until the body closes, errors, or is cancelled, so a stalled stream rejects the caller's .text()/.json() with the same Timeout error the header phase raises. status, statusText, headers, url, redirected, and type carry across the wrapper; the original body is locked by it, and bodyless/null-body responses (HEAD, 204/205/304) come back untouched. FetchWithTimeoutOptions extends RequestInit (minus signal) and adds rejectPrivateIPs?: boolean, expectedStatuses?: number[] (listed non-2xx statuses logged at debug not error, still thrown), errorBodyLimit?: number (bytes of a non-2xx body kept, default 500), errorHeaders?: string[] (response headers copied onto error.data.headers on a non-2xx — same selector as httpErrorFromResponse below; location is selectable under redirect: 'manual' but does not compose with rejectPrivateIPs, whose per-hop branch consumes the 3xx before the throw path sees it), and signal?: AbortSignal (external cancellation — an abort on it throws RequestCancelled (-32011), logged at info and outside withRetry's transient set, since the caller is gone and no retry can reach them; an abort whose reason is a TimeoutError — AbortSignal.timeout(), or an AbortSignal.any whose timeout member fired — is a deadline instead, and throws Timeout (-32004) with data.errorSource: 'FetchSignalTimeout', distinct from the helper's own timeoutMs expiry, 'FetchTimeout', and likewise outside withRetry's default transient set, since every retry would reuse the fired signal). Validation rejections carry data.reason and a recovery.hint: invalid_url (not an absolute http:/https: URL, including a redirect target), private_address_blocked (the SSRF guard refused the host by name, literal IP, or DNS answer), too_many_redirects (past the 5-hop cap, with data.maxRedirects). On a non-2xx, error.data carries status/body plus the legacy statusCode/responseBody aliases (identical values; consolidating in a future major); a body over errorBodyLimit is captured from both ends — 40% head, 60% tail, joined by …[N bytes elided]… — so a diagnostic behind a boilerplate preamble survives the cap, while a body still streaming at the 16 KiB scan ceiling stays head-only with a trailing …. SSRF guard (best-effort, not hard isolation): blocks RFC 1918, loopback, link-local, CGNAT, cloud metadata. DNS validation on Node, Bun, and Cloudflare Workers under nodejs_compat; hostname-only fallback otherwise. Both resolvers are queried — resolve4/resolve6 (c-ares) and lookup (the system resolver, which is what reads /etc/hosts, split DNS, and NSS modules) — and a non-global answer from either rejects. Runtimes differ in which resolver the connection uses (Bun 1.4 moved net.connect() on Linux to getaddrinfo while leaving dns.resolve*() on c-ares), so checking one alone leaves a name the other can see unguarded; each probe settles independently, so a resolver absent from the runtime is skipped rather than fatal. Manual redirect following (max 5) with per-hop SSRF check. DNS rebinding / TOCTOU gap — the validation lookup and fetch's own resolution are independent; pair with egress controls or a DNS-pinning fetch proxy for strong isolation. Error/log redaction: URLs written into thrown errors and log lines are reduced to origin + pathname — the query string (where API keys commonly ride: ?api-key=…, ?api_key=…) never reaches the client or the logs. The actual request still uses the full URL.
withRetry<T>(fn: (attempt: RetryAttempt) => Promise<T>, options?: RetryOptions) -> Promise<T>Executes fn with exponential backoff. Retries on transient errors (ServiceUnavailable, Timeout, RateLimited); non-transient errors fail immediately. Honors an upstream Retry-After on data.retryAfter (delta-seconds or HTTP-date) over exponential backoff, capped at maxDelayMs; a requested wait beyond the cap fails fast rather than sleeping. On exhaustion, enriches the final error with attempt count in message and data.retryAttempts. Place the retry boundary around the full pipeline (fetch + parse), not just the network call. RetryOptions: maxRetries (default 3), baseDelayMs (default 1000), maxDelayMs (default 30000), jitter (default 0.25), operation (log label), context (RequestContext), signal (AbortSignal), isTransient (custom predicate), deadlineMs (total wall-clock budget — see below).
RetryAttempt{ readonly signal: AbortSignal; readonly remainingMs: number }What fn receives each attempt. signal is AbortSignal.any over the deadlineMs clock and options.signal; remainingMs is what is left of the total budget as the attempt starts, never negative and Number.POSITIVE_INFINITY when no deadline is set — so Math.min(perAttemptMs, remainingMs) is correct either way. A zero-argument fn stays assignable, so existing callers compile unchanged.
deadlineMsRetryOptions fieldOne wall-clock budget across every attempt, backoff, and honored Retry-After — the bound maxRetries plus a per-attempt timeout cannot express. Four 30s attempts outlast a client's 60s request timeout, so the caller gets a transport timeout instead of the server's classified error. Thread attempt.signal into the attempt's I/O (fetchWithTimeout(url, Math.min(30_000, remainingMs), ctx, { signal })) or the deadline overshoots by one in-flight request. Clock is AbortController + setTimeout (never AbortSignal.timeout(), per the Bun realm mismatch), cleared on return — no timer outlives the call. Expiry rejects with Timeout (-32004) carrying data: { reason: 'retry_deadline_exceeded', deadlineMs, elapsedMs, retryAttempts } and the last attempt's error as cause; one shape for every expiry, including the per-attempt Timeout (errorSource: 'FetchSignalTimeout') the clock's abort raises inside fetchWithTimeout and the raw abort reason a mid-backoff expiry would otherwise surface. No retryable flag (a narrower call can still succeed) and no attempt index (retryAttempts carries it). A backoff that would outlast the remaining budget fails fast with the expiry instead of sleeping into a certain timeout; an honored Retry-After that would outlast it takes the maxDelayMs exit instead — the attempt's error unchanged, data.retryAfter intact, since "wait the window the upstream named" is still the caller's action. Three clocks stay distinct: a caller abort on options.signal keeps precedence — mid-attempt it rethrows the attempt's error unchanged, mid-backoff it rejects with signal.reason itself (an AbortError DOMException for a reason-less abort()), and the handler factory reports either as RequestCancelled when the request signal is the one that fired — a single attempt's timeout is Timeout with errorSource: 'FetchTimeout' and no reason, and the expiry is Timeout with the reason. Unset, behavior is identical to before — attempt counts, delays, log lines, and the exhausted-error shape untouched. Bounds one ladder: a tool making three upstream calls threads its own remaining budget into each.
defaultIsTransient(error: unknown) -> booleanThe predicate withRetry uses when isTransient is omitted: an McpError with a transient code (ServiceUnavailable, Timeout, RateLimited) unless it carries data.retryable === false, data.reason === 'pacer_shed', or data.errorSource === 'FetchSignalTimeout' (a caller-side deadline that already fired); any non-McpError throw is assumed transient. Exported so isTransient — which replaces the default outright — can compose instead of mirroring the transient set, which drifts silently when the framework's classification changes: isTransient: (error) => !isMyBudgetRefusal(error) && defaultIsTransient(error), or the inverse defaultIsTransient(error) || isMyRetryableShape(error). The transient code set itself stays private (a module-level Set an exported binding could be mutated into framework-wide retry behavior).
httpErrorFromResponse(response: Response, options?: HttpErrorFromResponseOptions) -> Promise<McpError>Maps an HTTP Response to a properly classified McpError — full status table including 401/403/408/422/429/5xx, body capture (truncated), retry-after header, optional cause. error.data carries status/body plus the legacy statusCode/responseBody aliases (identical values), so a consumer can classify either helper's error without knowing which raised it. Use this instead of hand-rolling if (status === 429) ... ladders. Reads the response body — clone() first if you need it elsewhere. error.data is client-facing — the framework forwards it verbatim as structuredContent.error.data — so the full upstream URL is omitted by default: a request URL routinely carries user input, internal identifiers, or an API key in its query string. includeUrl: true opts into data.url carrying the full response.url; with an empty response.url no key is added either way, and the message still names the host. Response headers are opt-in on the same footing: errorHeaders: ['x-ratelimit-remaining-usd', 'x-request-id'] copies the named headers onto data.headers under lowercase keys — selection is case-insensitive and entries differing only in case collapse to one key, presence follows Headers.has() (an empty value is captured as '', an absent header adds no key), and a multi-valued field is captured comma-joined as Headers.get() returns it. Omitted, empty, or matching nothing, no headers key is emitted. set-cookie is never captured whatever the selector says: it is credential-bearing and Headers.get() joins its values into a string that is not a valid reconstruction. Every selected value reaches the client, so never name a header that carries a credential — and a selected Location can itself carry a sensitive path, query, or token. HttpErrorFromResponseOptions: service? (logical name in message, e.g. 'NCBI'), captureBody? (default true), bodyLimit? (default 500), includeUrl? (default false), errorHeaders? (default none), data? (extra fields merged into error.data, overriding defaults on key collision — a caller's own url or headers still reaches the wire), cause?, codeOverride? (per-status mapping override). Pairs naturally with withRetry — both classify codes the same way. A 501 also carries data.retryable: false, so retry fails it fast instead of re-asking for a method the upstream does not implement.
createPacer(options: PacerOptions) -> PacerFIFO queue in front of one rate-limited upstream — the outbound counterpart to RateLimiter (utils/security), which is inbound, per-caller, and reject-only, so it cannot queue work against an upstream budget. pacer.run(task, { signal?, maxWaitMs? }) holds task until every limits window, minStartGapMs, maxConcurrent, and the cooldown gate allow it, then calls it with the caller's signal. PacerOptions: name (author-set telemetry label), limits ({ requests, perMs }[] — each a sliding window over recorded start times, so a slow response never widens the rate the upstream sees; all must allow a start), minStartGapMs (not expressible through limits: { requests: 10, perMs: 1000 } permits ten starts in the same millisecond), maxConcurrent, maxQueueDepth (absolute backpressure for callers passing no maxWaitMs; rejects without arming a timer; bounds waiters only — an arrival whose slot is open that instant starts without queueing, so 0 means "run when a slot is free, never wait"), cooldown ({ baseMs, maxMs }). Shed: maxWaitMs bounds queue time only, never the task. The projected wait is exact over the windows and the gap but a lower bound once maxConcurrent binds (a slot frees on an unknowable completion), so enqueue rejects only when that lower bound already exceeds maxWaitMs — no false sheds — and a still-queued entry rejects when maxWaitMs elapses, unless its slot opens that same instant. The shed error is rateLimited (-32003) with data: { reason: 'pacer_shed', shedKind, retryAfter, queueDepth }. shedKind (PacerShedKind) is queue_full (the call would wait behind maxQueueDepth waiters), wait_projected (the enqueue projection exceeds maxWaitMs), or wait_elapsed (maxWaitMs ran out while queued), and the message follows the kind — a queue_full shed names the full queue, not a wait budget. retryAfter is seconds until a caller joining behind every remaining waiter could start; while maxConcurrent is saturated — a release the projection cannot see — it is floored at the longest wait of any queued caller, the shed one included, minimum 1. queueDepth is the waiters still queued. No retryable: false — to the calling agent a shed is an ordinary rate limit (wait retryAfter, call again) and that flag would say the opposite; defaultIsTransient reads the reason instead, so an enclosing withRetry fails fast rather than sleeping past the deadline the shed enforces. Cooldown gate: a RateLimited thrown by the task closes the gate for every queued caller until an absolute instant, min(max(baseMs · 2^(consecutive−1), retryAfter), maxMs) — maxMs caps both the doubling and an honored Retry-After, so a pathological upstream value cannot park the queue. Absent or unparseable retryAfter leaves the doubling; any other error leaves the gate open and the count untouched, and a shed (reason: 'pacer_shed') from a pacer nested inside the task is local backpressure, never a gate closure. The first success resets the count, and so does a gate that has stood open for maxMs: the next rate limit starts over at baseMs, while one arriving sooner — the gate still closed included — keeps doubling, so continuous demand under a sustained limit keeps its capped backoff. pacer.cooldown samples the gate as PacerCooldownState { remainingMs, consecutive }: remainingMs is the shared gate, not one rate limit's own computation (rate limits landing together close one gate at the later instant), so a task's rejection handler can report it on the server's own error — the pacer never writes to the task's error. Both stay 0 without cooldown. Composition: withRetry(({ signal }) => pacer.run(fn, { signal }), { signal, deadlineMs }) — retry outside, pacer inside, so each attempt re-queues and is re-paced. Because the gate is an absolute instant rather than a duration counted from dequeue, retry's Retry-After sleep and the gate overlap in wall-clock instead of summing: the window is waited once, not twice. Lifecycle: timers and AbortSignal only, process-local; the dispatch timer is unref()'d where supported; dispose() / [Symbol.dispose]() clears it and rejects queued waiters with RequestCancelled (in-flight tasks are left to finish) — wire it through createApp({ teardown }). On Workers state is per-isolate so the limits bind per isolate, OTel is off so the metrics are inert, and createWorkerHandler accepts no teardown. Metrics: mcp.pacer.queue_depth, mcp.pacer.wait, mcp.pacer.sheds, mcp.pacer.cooldowns, attributed by mcp.pacer.name only — see api-telemetry.
httpStatusToErrorCode(status: number) -> JsonRpcErrorCode | undefinedSync status → code lookup. Returns undefined for 1xx/2xx. A 3xx maps to InvalidRequest — it reaches error mapping under redirect: 'manual', where the request as sent cannot be served at this URL, and that code is outside withRetry's transient set since re-issuing returns the same redirect. Use when you need just the code without a Response object handy. No status maps to InternalError — that code means this server failed, which a remote status cannot establish; every 5xx is ServiceUnavailable (or Timeout for 504) and so picks up withRetry's default transient policy.

Show full SKILL.md (1,543 more words)Show less

@cyanheads/mcp-ts-core/utils — pagination

ExportAPINotes
extractCursor(params?) -> string | undefinedExtracts opaque cursor string from MCP request params. Checks params.cursor then params._meta.cursor. Returns undefined when no cursor is present. Does not decode.
paginateArray<T>(items, cursorStr, defaultPageSize, maxPageSize, context: RequestContext) -> PaginatedResult<T>Decodes cursor, slices array, returns { items, nextCursor?, totalCount }. nextCursor omitted on last page. Throws McpError(InvalidParams) on invalid cursor, inherited from decodeCursor (data.reason: 'invalid_cursor'). On a continued call the page size comes from the cursor, not defaultPageSize — a tool with a caller-facing limit input must slice on decodeCursor(...).offset itself to honor limit past page 1.
encodeCursor(state: PaginationState) -> stringEncodes { offset, limit, ...extra } to opaque base64url string.
decodeCursor(cursor, context: RequestContext) -> PaginationStateDecodes opaque base64url cursor. Throws McpError(InvalidParams) if malformed, with data: { cursor, reason: 'invalid_cursor', recovery: { hint } } — the hint tells the caller to omit cursor or pass the previous nextCursor unchanged, so the rejection renders the Recovery: line and (reason invalid_cursor) trailer like any other classified failure.

@cyanheads/mcp-ts-core/utils — runtime

ExportAPINotes
runtimeCapsRuntimeCapabilities objectSnapshot at import time. Fields: isNode, isBun, isWorkerLike, isBrowserLike, hasProcess, hasBuffer, hasTextEncoder, hasPerformanceNow. All booleans. Never throws.

@cyanheads/mcp-ts-core/utils — scheduling

ExportAPINotes
schedulerService.schedule(id, schedule, taskFunction, description) -> Promise<Job> .start(id) -> void .stop(id) -> void .remove(id) -> void .listJobs() -> Job[]Async schedule() — Tier 3 peer: node-cron. Node-only (throws ConfigurationError in Workers). Jobs start in stopped state; call start(id) to activate. Skips overlapping executions. Each tick gets fresh RequestContext. Job: { id, schedule, description, isRunning, task }. taskFunction: (context: RequestContext) => void | Promise<void>.

@cyanheads/mcp-ts-core/utils — types

The utils export includes two type guards. The full set of guards lives in the internal module and is not part of the public API.

ExportSignatureNotes
isErrorWithCode(error: unknown) -> error is Error & { code: unknown }Type guard — true when value is an Error instance with a code property
isRecord(value: unknown) -> value is Record<string, unknown>Type guard for plain objects (non-null, non-array)

@cyanheads/mcp-ts-core/utils — logger

ExportAPINotes
LoggerClassThe Logger class itself. Use Logger.getInstance() if needed; most consumers use the logger singleton.
loggerLogger instance (wraps Pino). .debug(msg, ctx?) .info(msg, ctx?) .notice(msg, ctx?) .warning(msg, ctx?) .error(msg, errorOrCtx, ctx?) .crit(msg, errorOrCtx, ctx?) .alert(msg, errorOrCtx, ctx?) .emerg(msg, errorOrCtx, ctx?) .fatal(msg, errorOrCtx, ctx?) .isLevelEnabled(level) -> booleanGlobal structured logger. Use ctx.log in handlers instead. logger is for lifecycle/background contexts (startup, shutdown, setup()). Auto-redacts sensitive fields. The context's extra bag is flattened into the record, but a canonical field the context carries (requestId, timestamp, traceId, spanId, sessionId, tenantId, operation) always wins over an extra key of the same name. Records logged before the framework initializes the logger — anything in setup() — are held in a 250-record buffer and replayed once the sinks exist, filtered against the level the logger starts with. isLevelEnabled(level) is that filter: true when a record at level would be written, compared on the RFC 5424 order of all eight levels (a notice level drops info though pino emits both at info, a crit level drops error), and true for every level before initialization. The ctx.log mirror to the client is gated by the same check. Note: .error() and higher accept (msg, Error, ctx?) or (msg, ctx?) — the second arg is overloaded. .fatal() is an alias for .emerg(). Full RFC 5424 severity set.
McpLogLevelTypeLog level union type for typing level variables.

@cyanheads/mcp-ts-core/utils — requestContext

ExportAPINotes
requestContextService.createRequestContext(params?) -> RequestContext .withAuthInfo(authInfo, parentContext?) -> RequestContextCreates tracing context with requestId, timestamp, traceId, spanId, tenantId, auth. Internal — most consumers use ctx from handlers.
RequestContextType: { requestId, timestamp, operation?, traceId?, spanId?, tenantId?, auth?, [key: string]: unknown }Request tracing metadata.
CreateRequestContextParamsType: { parentContext?, additionalContext?, operation?, [key: string]: unknown }Params accepted by createRequestContext. Named fields get special merge handling; other properties spread directly onto the context.
AuthContextType: { clientId, scopes, sub, token, tenantId?, [key: string]: unknown }Structured auth data attached to RequestContext.auth after token verification.

createRequestContext merge order (later wins, except requestId/timestamp): parentContext → spread rest params → additionalContext (strips requestId/timestamp) → pinned requestId/timestamp → resolved tenantId → operation → OTel traceId/spanId.

withAuthInfo(authInfo, parentContext?) builds a context and populates auth from a validated token. Does not write to AsyncLocalStorage — ALS propagation is the auth middleware's responsibility.


@cyanheads/mcp-ts-core/utils — errorHandler

ExportAPINotes
ErrorHandler.tryCatch<T>(fn, opts) -> Promise<T> .handleError(error, opts) -> Error .classifyOnly(error) -> { code, message, data? } .determineErrorCode(error) -> JsonRpcErrorCode .mapError(error, mappings, defaultFactory?) -> T | Error .formatError(error) -> Record<string, unknown>Service-level error handling. tryCatch wraps async or sync fn, logs via handleError, and always rethrows. No .tryCatchSync(). Use in services, NOT in tool handlers (those throw raw McpError). tryCatch accepts Omit<ErrorHandlerOptions, 'rethrow'> — required: operation. Optional: context, errorCode, input, includeStack, critical, errorMapper. handleError accepts the full ErrorHandlerOptions including rethrow. The returned error's data (client-visible once thrown toward a handler) keeps the caught McpError's own data plus originalErrorName/originalMessage/rootCause; context goes to the log record only.

@cyanheads/mcp-ts-core/utils — encoding

Cross-platform encoding utilities. No peer deps.

ExportSignatureNotes
arrayBufferToBase64(buffer: ArrayBuffer) -> stringEncodes an ArrayBuffer to base64. Uses Buffer on Node/Bun; chunked btoa on Workers/browsers to avoid stack overflow on large buffers.
stringToBase64(str: string) -> stringUTF-8 string → base64. Uses Buffer.from(str, 'utf-8') on Node/Bun; TextEncoder + arrayBufferToBase64 on Workers.
base64ToString(base64: string) -> stringbase64 → UTF-8 string. Uses Buffer on Node/Bun; atob + TextDecoder on Workers. Throws if input is not valid base64.

@cyanheads/mcp-ts-core/utils — token counting

Dependency-free heuristic token estimation. No native/WASM deps.

ExportSignatureNotes
countTokensasync (text: string, context?: RequestContext, model?: string) -> Promise<number>Estimates tokens in a plain string. Normalizes whitespace, divides by charsPerToken. Returns 0 for empty/whitespace input. Falls back to gpt-4o heuristics when model is omitted or unrecognized.
countChatTokensasync (messages: ReadonlyArray<ChatMessage>, context?: RequestContext, model?: string) -> Promise<number>Estimates total tokens for a chat message array. Adds per-message overhead (tokensPerMessage), counts string/array content, name, assistant tool_calls, and tool tool_call_id. Adds replyPrimer once.
ChatMessageType{ role: string, content: string | Array<{type, text?, ...}> | null, name?, tool_calls?, tool_call_id? } — provider-agnostic chat message shape.
ModelHeuristicsInterface{ charsPerToken, replyPrimer, tokensPerMessage, tokensPerName } — heuristic parameters; built-in entries for gpt-4o, gpt-4o-mini, default.

Both functions throw McpError(InternalError) only on unexpected heuristic failure.


@cyanheads/mcp-ts-core/utils — Telemetry

Helper API only. For the catalog of what the framework auto-emits (span names, metric names, attributes, completion log fields, env config, runtime support, cardinality rules), see the api-telemetry skill.

telemetry/instrumentation
ExportSignatureNotes
initializeOpenTelemetry() -> Promise<void>Idempotent. Initializes NodeSDK with OTLP trace + metrics exporters, TraceIdRatioBasedSampler, and HTTP instrumentation, and attaches the framework logger's OTLP log sink when OTEL_EXPORTER_OTLP_LOGS_ENDPOINT is set. Framework log records carry traceId/spanId from the request context; PinoInstrumentation is registered but patches only a pino loaded after the SDK starts, never the framework logger's. No-ops when OTEL_ENABLED=false or in Worker/Edge runtimes where NodeSDK is unavailable. Safe to call multiple times.
shutdownOpenTelemetry(timeoutMs?: number) -> Promise<void>Gracefully flushes and shuts down the SDK. timeoutMs defaults to 5000. Resets internal state so the next initializeOpenTelemetry() call can reinitialize. No-op when SDK was never started.
sdkNodeSDK | nullThe live SDK instance, or null when telemetry is disabled, in a Worker runtime, or after shutdown.
telemetry/metrics
ExportSignatureNotes
getMeter(name?: string) -> MeterReturns an OTel Meter. Defaults to service name + version from config.
createCounter(name: string, description: string, unit?: string) -> CounterMonotonically increasing counter. unit defaults to '1'.
createUpDownCounter(name: string, description: string, unit?: string) -> UpDownCounterBidirectional counter (active connections, queue depth, etc.). unit defaults to '1'.
createHistogram(name: string, description: string, unit?: string) -> HistogramDistribution recording (latency, sizes). unit optional.
createObservableGauge(name: string, description: string, callback: () => Promise<number> | number, unit?: string) -> ObservableGaugePolled gauge. callback is registered via addCallback; invoked on each SDK collection cycle. unit optional. For other observable instrument types, use getMeter() directly.
telemetry/trace
ExportSignatureNotes
withSpanasync <T>(operationName: string, fn: (span: Span) => Promise<T>, attributes?: Record<string, string | number | boolean>) -> Promise<T>Creates an active span, calls fn(span), sets OK on success or records exception + sets ERROR on throw, then ends the span. Always rethrows.
runInContext(ctx: RequestContext | undefined, fn: () => T) -> TRuns fn with the span ctx names (traceId/spanId) re-established as the active OTel span, so spans opened inside fn parent to it. When ctx has no traceId/spanId, calls fn directly. Use for carrying a request's trace across async boundaries (setTimeout, queueMicrotask).
buildTraceparent(ctx?: RequestContext) -> string | undefinedBuilds a W3C traceparent header (00-<traceId>-<spanId>-01) from ctx or the active span. Returns undefined when neither source yields both IDs.
extractTraceparent(headers: Headers | Record<string, string | undefined>) -> TraceparentInfo | undefinedParses a W3C traceparent header. Returns undefined when absent or malformed. TraceparentInfo: { traceId, spanId, sampled }.
createContextWithParentTrace(parentHeaders: Headers | Record<string, string | undefined>, operation: string) -> RequestContextExtracts traceparent from headers and creates a child RequestContext inheriting traceId/parentSpanId.
injectCurrentContextInto<T extends Record<string, unknown>>(carrier: T) -> TInjects the active OTel context (traceparent, tracestate, etc.) into carrier via propagation.inject. Returns the same object.
telemetry/attributes

MCP-specific ATTR_* constant exports for span and metric attributes. Covers: code execution (code.function.name, code.namespace), MCP tool execution (name, input/output bytes, duration, success, error code, error category, partial success, batch succeeded/failed counts), MCP resource (URI, the uncut URI length when the recorded URI was capped, name, MIME type, size, duration, success, error code), MCP request context (tenant ID, client ID), MCP session events, MCP storage, GenAI semantic conventions, speech, graph, auth, task, and error classification attributes.

Batch/partial success attributes (mcp.tool.partial_success, mcp.tool.batch.succeeded_count, mcp.tool.batch.failed_count) are set automatically by the framework when a tool handler returns a result containing a non-empty failed array — matching the batch response pattern from the design skill. A tool whose output is built with partialResultSchema() is read under its failedKey/succeededKey instead, including after .extend(), .pick(), .omit(), or a .shape spread.

Standard OTel semantic conventions (HTTP, cloud, service, network, etc.) are NOT re-exported — import those directly from @opentelemetry/semantic-conventions if needed.

© 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

SKILL.md and 3 other files (references) in framework-skills/api-utils of cyanheads/pubmed-mcp-server.

  • SKILL.md
  • references/formatting.md
  • references/parsing.md
  • references/security.md

Open the folder on GitHubat commit 5a417fb

Compare with similar skills

API Utils 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 Utils compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
API Utils this skillcyanheads/pubmed-mcp-server155—~8.1kAutomated 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 Utils

What does API Utils do?

API reference for all utilities exported from @cyanheads/mcp-ts-core/utils. API Utils is an agent skill from cyanheads/pubmed-mcp-server. API reference for all utilities exported from @cyanheads/mcp-ts-core/utils.

When should I use API Utils?

API Utils fits situations like: looking up utility method signatures; peer dependencies.

How do I install API Utils in Claude Code?

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

How do I install API Utils in Codex?

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

Can I use API Utils 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-utils -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-utils, .gemini/skills/api-utils, .github/skills/api-utils and .opencode/skills/api-utils in your project.

What does API Utils need to run?

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

Does API Utils 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 Utils 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 Utils use?

API Utils 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 Utils use?

About 8.1k tokens (SKILL.md is roughly 32k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full. Its references folder adds about 7.6k tokens, read only when the agent opens those files.

What are the alternatives to API Utils?

Skills that share tags, products or a category with API Utils: 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 Utils?

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.