npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sbissoli/mcp-upstream

v0.5.1

Published

Fetch comum dos servidores MCP: retry com backoff e Retry-After, timeout por tentativa e orçamento por chamada, e a contagem de tentativas e anomalias que alimenta o `retrieval` do contrato de proveniência v1.1

Readme

@sbissoli/mcp-upstream

🇧🇷 Leia em Português

Common fetch for the portfolio's MCP servers: retry with backoff and Retry-After, timeout per attempt, total budget per trip to the origin — and the count of trips, attempts and anomalies that feeds the retrieval block of the provenance contract v1.1 (in Portuguese) (@sbissoli/mcp-provenance ≥ 0.2.0).

Maintained for my own portfolio of MCP servers. Third-party use is welcome, but the roadmap follows what my servers need.

The package classifies; the server decides. It says "the origin answered 404", "the body came back as HTML", "I retried twice because of a 503" — and never what that means for the data (legitimate absence × key outside coverage × non-existent series is each server's semantics, and stays in it).

Usage

import { createUpstream, UpstreamError } from "@sbissoli/mcp-upstream";

// 1. Once, when the server starts: the network policy.
const upstream = createUpstream({
  userAgent: "bcb-br-mcp/2.0 (+https://…)",
  timeoutMs: 10_000,   // cap of ONE attempt (headers + body in json/text; headers only in response)
  retries: 2,          // beyond the first attempt (up to 3 in total)
  budgetMs: 30_000,    // TOTAL budget of the trip, waits included
  backoff: { baseMs: 1_000, maxMs: 8_000, jitterMs: 500 },
  honorRetryAfter: true,
  inspectBody: (_res, body) => (body.trimStart().startsWith("<") ? "malformed_body" : null),
});

// 2. In each tool call: ONE collector, explicit.
const call = upstream.call();
const serie = await call.json(`${BASE}/serie/${codigo}`);      // 1 trip; counts attempts
const meta  = await call.json(`${BASE}/serie/${codigo}/meta`); // 2nd trip
call.recordCache(urlDoCatalogo, catalogo.retrievedAt);         // a cache hit of the SERVER

return prov.result(dados, prov.from(PRESET, {
  source_url: …,
  retrieved_at: call.retrievedAt(),      // the oldest between network and cache
  served_from_cache: call.servedFromCache(),
  retrieval: call.retrieval(),           // { requests: 2, attempts: 3, anomalies: [...] } | null
}));

call.retrieval() returns the raw RetrievalInput: whoever adds unstable and normalises is the provenance library, as it already does. It is null when the call did not go to the origin even once (pure cache, local data) — the contract requires null, not an invented { attempts: 1 }.

A response that joins parts of different provenance (field_sources)

When the response merges sub-sources — one part from the cache, another fetched now —, call.fieldSource() builds the field_sources item (provenance contract v1.2, @sbissoli/mcp-provenance ≥ 0.3.0) from the recorded accesses. The server says which fields come from which URL; the collector puts in that sub-source's oldest instant and its served_from_cache:

field_sources: [
  call.fieldSource({ fields: ["serie"], source_url: urlSerie }),                    // accesses with an equal URL
  call.fieldSource({ fields: ["meta"],  source_url: urlMeta, filter: (u) => u.startsWith(urlMeta) }),
],

A sub-source with no access in this call comes out with retrieved_at and served_from_cache null — unlike retrievedAt(), which returns "now" for the block, the item never asserts a retrieval that did not happen.

Three trip modes

| method | body | inspectBody | malformed_body | |---|---|---|---| | call.json(url, init?) | read and parsed | yes | body rejected or invalid JSON | | call.text(url, init?) | read | yes | body rejected | | call.response(url, init?) | not consumed | no | never (the body is yours) |

In response mode the deadline ends at the headers. timeoutMs and budgetMs cover the trip until the Response is returned; the timer is cleared then, and the body you read afterwards has no deadline from the package. An origin that sends the headers and stalls the body hangs your read. If you read the body, either use text()/json() (the body is read inside the attempt, under the deadline, and a stalled body is a timeout), or pass a signal of yours that stays alive while you read and abort it when your own deadline runs out — the package combines it with the fetch, so aborting it after the headers also aborts the body stream. Abort it only after the call returns: aborting during the trip is aborted, not timeout, and is not counted as an anomaly.

init is a normal RequestInit; a signal of yours is combined with the timeout's. Four extra keys change the policy for that trip only, in the same collector, because a fair deadline and retry depend on the SHAPE of the request, not on the server: timeoutMs (cap of ONE attempt — bcb gives 6 s to a request for 20 observations and 30 s to a wide window), retries, backoff (partial, merged over the policy's) and retryOn (ibge gives 4 retries of 2→16 s to the main query, 2 of 0.5→2 s to a best-effort enrichment, and does not retry the deterministic 500 of the Aggregates API). None of them leaks into fetch; budgetMs follows the policy's, on top.

Classification and retry

| what happened | kind | retried by default | transport | |---|---|---|---| | an attempt exceeded timeoutMs (or the budget) | timeout | yes | yes (no, if it was while reading the body in json/text) | | fetch threw (DNS/TCP/TLS) | network | yes | yes | | HTTP 429 | rate_limited | yes, honouring Retry-After | no | | HTTP 5xx | http_5xx | yes | no | | HTTP 4xx (except 404) | http_4xx | no | no | | 200 with a rejected body / invalid JSON | malformed_body | yes | no | | HTTP 404 | not_found | no | no | | the caller's signal aborted | aborted | no | yes |

The first six are the closed vocabulary of retrieval.anomalies. not_found and aborted are not anomalies: absence is an answer from the origin, and cancellation is the caller's decision. retryOn(ctx) replaces the retry policy (e.g. retrying a 4xx that the origin uses as "try again"); retries and budgetMs still apply on top. The ctx carries url, attempt, kind, status, response, body and cause (what fetch or the parse threw) — it is through cause that a server tells an ECONNRESET from an error another layer threw inside the fetch.

Every failed attempt counts as an anomaly — the overcome one and the final one. Since retrieval only comes out when the tool succeeds, the difference only shows when the server swallows the failure of a slice and answers partially: and then the response is unstable, and the block has to say so.

Wait between attempts

max(Retry-After, exponential backoff) + jitter, with backoff = min(baseMs · 2ⁿ, maxMs). A wait that would exceed what is left of budgetMs gives up immediately, with the retryable error — waiting would only postpone the same timeout. Each attempt gets, at most, what is left of the budget.

The error

try { await call.json(url); }
catch (e) {
  if (e instanceof UpstreamError) {
    e.kind;        // "timeout" | "network" | "rate_limited" | "http_5xx" | "http_4xx" | "malformed_body" | "not_found" | "aborted"
    e.status;      // a number when a response arrived
    e.retryable;   // the class was retryable (informative: the retries are already exhausted)
    e.transport;   // true only when NOTHING arrived from the origin
    e.attempts;    // attempts of this trip
    e.body;        // body read (text/json modes) — this is where the server reads the 404
    e.response;    // Response with the body not consumed (response mode)
    e.retryAfterMs;
    e.isAnomaly;   // kind belongs to the contract's vocabulary
  }
}

Did the origin deliver anything measurable? (entregaDaOrigem)

Contract tests, post-deploy smokes and recovery probes run against the live source and need to tell "the source changed" (drift, a defect to fix) from "the source is down" (nothing to fix, try again later). Deciding by the error's wording is brittle: measured on 2026-10-22, two formats in this portfolio turned a deploy and a contract run red with the origin answering 502. The decision is a function of kind, so it lives here, the same for every consumer:

import { entregaDaOrigem } from "@sbissoli/mcp-upstream";

try { await call.json(url); }
catch (e) {
  const r = entregaDaOrigem(e);   // "delivery from the origin"
  r.veredito; // verdict: "indisponivel" | "respondeu" | "ambiguo" | "alheio"
  r.motivo;   // one pt-BR sentence for the log — never the basis of the decision
  r.kind; r.status; r.url;
}

| Verdict | When | Meaning | |---|---|---| | indisponivel (unavailable) | timeout, network, 429, 5xx | The origin delivered no body to compare. Drift was not measured; it is not drift. A 5xx lands here even though "a response arrived": what arrived is the gateway's error page, not the resource. | | respondeu (answered) | 404, other 4xx | The origin answered, and the answer is a refusal of the request. Contract territory (route, key, parameter); whoever reads status/body decides. | | ambiguo (ambiguous) | malformed_body | A body arrived that is not the expected one: either the format changed (real drift) or the origin served its error page to a sound request (the Central Bank's SGS does that for series that exist). Resolved by repeating on another trip, not by reading this one. | | alheio (not the origin's) | aborted, or an error that is not an UpstreamError | The caller aborted, or it is a defect in our own code. Failing the contract for it would blame the source for what is ours. |

The function follows the error's cause chain (since 0.5.1): a server that translates the package error into its own class keeps the verdict of the origin as long as it passes the package error as cause (new MyError(msg, { cause: err })). An error with no UpstreamError anywhere in the chain is alheio.

What the function does not do: policy. Whether indisponivel becomes a warning, a skipped test or an exit code is the reporter's decision, per format — that reporter is @sbissoli/mcp-fonte-viva.

AsyncLocalStorage adapter (optional)

For servers that already propagate context this way:

import { withCall, currentCall, requireCall } from "@sbissoli/mcp-upstream/als";

server.tool("x", schema, (args) =>
  withCall(upstream, async (call) => {
    const dados = await buscar(args);            // deep down: requireCall().json(url)
    return prov.result(dados, prov.from(PRESET, { retrieval: call.retrieval(), … }));
  }),
);

A separate entrypoint so that the core does not depend on node:async_hooks. Requires Node or a Worker with nodejs_compat.

Offline tests

fetchImpl, sleep, now and random are injectable: the package's tests prove exact counts, exact waits and giving up by budget without network and without waiting. Servers that adopt it test the same way.

License

MIT