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

@redis-iris/context-retriever

v0.1.1

Published

TypeScript SDK for Redis Context Retriever REST and MCP APIs

Readme

@redis-iris/context-retriever

TypeScript SDK for Redis Context Retriever: retrieve context for an agent over MCP, and administer the context surfaces that serve it over REST.

Built by Speakeasy License: MIT

Summary

Redis Context Retriever: Administrative REST API for Redis Context Retriever.

Table of Contents

Guides

| Guide | What it covers | | ----- | -------------- | | MCP.md | The data plane: connecting to a surface's MCP endpoint, persistent versus one-shot clients, agent keys, pooling. Start here if you are retrieving context. | | examples/ | Runnable scripts, including the canonical retrieval example. | | FUNCTIONS.md | Tree-shakable standalone functions. | | RUNTIMES.md | Supported JavaScript runtimes and recommended compiler options. | | CHANGELOG.md / RELEASES.md | The handwritten layer's changes, and the generated release history (empty until the first publish). |

Handwritten sections of this README, plus everything in MCP.md and examples/, are maintained by hand and are not overwritten by SDK generation. See Contributions.

Before you start

You need an on-prem Context Retriever deployment, its base URL, and an administrative API key provisioned by that deployment. The SDK ships no default serverURL, and it must be https: unless it points at a loopback host.

This package intentionally exposes only the on-prem API surface. Cloud browser-session authentication, EULA operations, and session-only admin-key creation are not included.

SDK Installation

The SDK can be installed with either npm, pnpm, bun or yarn package managers.

NPM

npm add @redis-iris/context-retriever

PNPM

pnpm add @redis-iris/context-retriever

Bun

bun add @redis-iris/context-retriever

Yarn

yarn add @redis-iris/context-retriever

[!NOTE] This package is published with CommonJS and ES Modules (ESM) support.

[!NOTE] The first release has not been published yet, so the commands above will not resolve from npm until it is. Until then, build from this repository: see DEVELOPMENT.md.

Requirements

For supported JavaScript runtimes, please consult RUNTIMES.md.

Control plane and data plane

The SDK covers two distinct surfaces with two distinct credentials, and mixing them up is the most common source of a puzzling 401.

| | Control plane | Data plane | | - | ------------- | ---------- | | Protocol | Administrative REST | Streamable HTTP MCP | | Entry point | createContextRetriever({ ... }) | ContextRetrieverMCPClient.connect({ ... }), or client.mcp.* | | Credential | On-prem admin API key (X-API-Key) | Agent API key (X-API-Key), scoped to one surface, plus an optional end-user bearer token | | Audience | The platform or data engineer who provisions surfaces | The application developer whose agent asks questions | | Typical calls | Create and update surfaces, infer schemas, import data, mint and revoke agent keys, read activity logs | listTools, callTool | | Runs where | A provisioning script, a Terraform-adjacent job, an internal admin console | Your request path, agent loop, or worker | | Guide | This README | MCP.md |

An admin key must never be shipped to an agent, and an agent key cannot administer anything. The bridge between the two planes is createAgentKey, which returns the agent key and the surface's mcpUrl together.

Quickstart: retrieve context

This is what the product is for. Given a surface's MCP URL and an agent key, discover the tools the surface exposes and call one. No admin key is involved.

import { ContextRetrieverMCPClient } from "@redis-iris/context-retriever/mcp";

const mcp = await ContextRetrieverMCPClient.connect({
  mcpURL: process.env["CONTEXT_RETRIEVER_MCP_URL"] ?? "",
  agentApiKey: process.env["CONTEXT_RETRIEVER_AGENT_KEY"] ?? "",
  timeoutMs: 30_000,
});

try {
  // Tool names and argument shapes come from the surface's data model, so read
  // them at runtime rather than hard-coding them.
  const tools = await mcp.listTools();
  console.log(tools.map(tool => tool.name));

  const result = await mcp.callTool("search_customer_by_text", { query: "Ada" });
  console.log(result.structuredContent);
} finally {
  await mcp.close();
}

Hold one client for as long as you need it. client.mcp.listTools(...) and client.mcp.callTool(...) on the unified client do the same work in one line but open and tear down an MCP session per call, so they suit scripts rather than hot paths.

Tool results are untrusted server data. Pass them to a model as data, never interpolated into a system prompt. MCP.md covers this, plus pooling, credential providers, and the full option set.

Quickstart: provision a surface

The control-plane half. This is what produces the mcpURL and agentApiKey above.

import { createContextRetriever } from "@redis-iris/context-retriever";

const client = createContextRetriever({
  serverURL: process.env["CONTEXT_RETRIEVER_SERVER_URL"] ?? "",
  auth: {
    type: "apiKey",
    apiKey: process.env["CONTEXT_RETRIEVER_API_KEY_AUTH"] ?? "",
  },
});

const { result: surface } = await client.contextSurfaces.createContextSurface({
  name: "customers",
  dataSource: {
    type: "redis",
    connectionConfig: { addr: "redis.example.com:6379", tlsEnabled: true },
  },
});

// A new surface starts in `provisioning`. Poll getContextSurface until its status is
// exactly `ContextSurfaceStatus.Active`, and fail on `failed` or `indices_failed`.
// Succeed only on `active`: treating "not pending" as ready reports a concurrently
// deleted surface, or any status the service adds later, as usable. Tools do not exist
// until the surface is active.
const { result: agentKey } = await client.agentKeys.createAgentKey(surface.id, {
  name: "checkout-agent",
});

// `key` is returned exactly once. Store it now.
console.log(agentKey.mcpUrl, agentKey.key);

See examples/provisionSurface.example.ts for the complete flow, including schema inference, a correct polling loop, and inspecting an import response for partial failures.

SDK Example Usage

Example

import { createContextRetriever } from "@redis-iris/context-retriever";

const contextRetriever = createContextRetriever({
  serverURL: "https://api.example.com",
  auth: {
    type: "apiKey",
    apiKey: process.env["CONTEXT_RETRIEVER_API_KEY"] ?? "",
  },
});

async function run() {
  const result = await contextRetriever.contextSurfaces
    .listContextSurfaces();

  console.log(result);
}

run();

The returned ContextRetrieverClient is a unified facade: its REST resources are available directly (health, contextSurfaces, agentKeys, and others), the raw generated REST client is available as rest, and one-shot MCP operations are available as mcp.listTools(...) and mcp.callTool(...). Use the /mcp entry point when a persistent MCP session is more efficient.

Authentication

Per-Client Security Schemes

Use createContextRetriever for supported deployment-aware authentication. It applies credentials immediately before dispatch, so per-request headers cannot replace the configured API key.

auth is required, and there is no zero-configuration path. The generated security layer does read CONTEXT_RETRIEVER_API_KEY_AUTH from the environment, but only in resolveGlobalSecurity, which serves the operations that take no security argument (including the unauthenticated health probes). createContextRetriever never forwards an apiKeyAuth option, and because auth is mandatory and the SDK's auth hook runs last, the credential you configure always wins over an ambient one.

| Name | Type | Scheme | Configured via | | ------------ | ------ | ------- | ------------------------------------------- | | apiKeyAuth | apiKey | API key | auth: { type: "apiKey", apiKey } (required) |

For API-key authentication:

import { createContextRetriever } from "@redis-iris/context-retriever";

const contextRetriever = createContextRetriever({
  serverURL: "https://api.example.com",
  auth: { type: "apiKey", apiKey: "<CONTEXT_RETRIEVER_API_KEY>" },
});

async function run() {
  const result = await contextRetriever.contextSurfaces.listContextSurfaces();

  console.log(result);
}

run();

The generated ContextRetriever class remains available as a low-level client.

Authentication matrix

ApiKeyAuth is an on-prem administrative API key sent as X-API-Key.

| Operations | Authentication | | ---------- | -------------- | | contextSurfaces.* (including inferSchema and importData) | Admin API key | | agentKeys.* | Admin API key | | activity.* | Admin API key | | adminKeys.listAdminKeys and adminKeys.deleteAdminKey | Admin API key | | adminKeys.validateAdminKey | The key under test is passed as the method argument | | health.* (getHealth, getLiveness, getReadiness) | Unauthenticated | | MCP listTools / callTool | Agent key plus an optional end-user bearer token |

Admin-key creation is deliberately absent because the upstream operation requires a cloud browser session. Provision the on-prem administrative key outside this SDK.

Validating an inbound key

validateAdminKey is the endpoint you reach for when your own service receives a Context Retriever key and needs to know whether it is real. It takes the key under test as a normal first argument, not through security:

const result = await client.adminKeys.validateAdminKey(inboundKey);
if (result.valid !== true) {
  // Reject the caller.
}

The key you pass is the key that gets validated. This is worth stating because it is the one operation where the SDK's own credential must not be applied: X-API-Key is the request's payload here, not its authentication. The auth hook detects a request to /api/v1/keys/validate that already carries an X-API-Key header and leaves it alone, so the answer is about inboundKey and not about the client's configured credential. Without that carve-out the call would be an always-true oracle that reported the SDK's own key as valid no matter what you passed in.

Credential providers

apiKey and csrfToken each accept a string or a function returning a string or a promise of one. A provider is re-resolved on every request, including every retry attempt, so a function is the supported way to rotate a credential without restarting the process:

const client = createContextRetriever({
  serverURL: process.env["CONTEXT_RETRIEVER_SERVER_URL"] ?? "",
  auth: { type: "apiKey", apiKey: () => vault.read("context-retriever/admin-key") },
});

A provider that returns an empty string, or anything that is not a string, is rejected rather than sent: the REST client raises RequestValidationError and the MCP client raises MCPError. Neither message echoes the provider or the value. An accidental async () => ({ key }) therefore fails loudly instead of putting [object Object] in an X-API-Key header.

Available Resources and Operations

Activity

AdminKeys

AgentKeys

ContextSurfaces

Health

Retrieval is not in the table above. It happens over MCP: see Quickstart: retrieve context and MCP.md.

Standalone functions

All the methods listed above are available as standalone functions. These functions are ideal for use in applications running in the browser, serverless runtimes or other environments where application bundle size is a primary concern. When using a bundler to build your application, all unused functionality will be either excluded from the final bundle or tree-shaken away.

To read more about standalone functions, check FUNCTIONS.md.

[!WARNING] Standalone functions under @redis-iris/context-retriever/funcs/* bypass the ContextRetrieverError hierarchy. They return result objects carrying the raw generated error types instead. See the error-handling caveat.

Retries

Safe, idempotent read operations carry a default retry policy: exponential backoff starting at 200ms, capped at 15s per interval, giving up after 15s of total elapsed time, with an exponent of 1.5, retrying on 500, 502, 503, and 504 as well as connection errors. A 429 is returned immediately so callers can schedule a retry from RateLimitError.retryAfterMs. Non-idempotent operations are not retried by default, and health probes are never retried so a readiness failure answers immediately. See Retries and timeouts for the full policy and how to change it.

To change the retry strategy for a single API call, provide a retries object to the call. An explicit per-call retries always wins, including for non-idempotent operations:

import { createContextRetriever } from "@redis-iris/context-retriever";

const contextRetriever = createContextRetriever({
  serverURL: "https://api.example.com",
  auth: { type: "apiKey", apiKey: "<CONTEXT_RETRIEVER_API_KEY>" },
});

async function run() {
  const result = await contextRetriever.contextSurfaces.listContextSurfaces(
    undefined,
    undefined,
    {
      retries: {
        strategy: "backoff",
        backoff: {
          initialInterval: 200,
          maxInterval: 15000,
          exponent: 1.5,
          maxElapsedTime: 15000,
        },
        retryConnectionErrors: true,
      },
    },
  );

  console.log(result);
}

run();

To override the retry strategy for every operation that supports retries, provide a retryConfig at client construction:

import { createContextRetriever } from "@redis-iris/context-retriever";

const contextRetriever = createContextRetriever({
  serverURL: "https://api.example.com",
  auth: { type: "apiKey", apiKey: "<CONTEXT_RETRIEVER_API_KEY>" },
  retryConfig: {
    strategy: "backoff",
    backoff: {
      initialInterval: 200,
      maxInterval: 15000,
      exponent: 1.5,
      maxElapsedTime: 15000,
    },
    retryConnectionErrors: true,
  },
});

async function run() {
  const result = await contextRetriever.contextSurfaces.listContextSurfaces();

  console.log(result);
}

run();

Retries and timeouts

Default retry policy. Safe reads get a bounded backoff, applied through the generation overlay:

| Setting | Value | | ------- | ----- | | strategy | backoff | | initialInterval | 200ms | | maxInterval | 15s | | maxElapsedTime | 15s | | exponent | 1.5 | | Retried statuses | 500, 502, 503, 504 | | retryConnectionErrors | true |

The covered reads are listContextSurfaces, getContextSurface, listAdminKeys, validateAdminKey, listAgentKeys, and the five activity operations. maxElapsedTime is deliberately short: the generator's own default is one hour, which is unusable inside a user-facing request.

Rate limits are deliberately outside the automatic retry policy. A 429 is returned after one attempt, and RateLimitError.retryAfterMs parses either delta-seconds or an HTTP date from Retry-After. This lets the caller schedule work according to the service's guidance without the SDK adding load during a throttling event.

Non-idempotent operations do not retry. createContextRetriever pins retries: { strategy: "none" } onto deleteAdminKey, createAgentKey, deleteAgentKey, createContextSurface, updateContextSurface, deleteContextSurface, importData, and inferSchema. This holds even when you set a client-wide retryConfig, because a 5xx or a timeout on a write often means the write did land, and replaying it can duplicate a key or re-apply an import batch.

Two ways to change that:

  • retryUnsafeOperations: true at construction lets a client-wide retryConfig cover writes as well. Off by default.
  • A per-call retries option always wins, for any operation. Use it when you know a specific write is safe to replay.
const client = createContextRetriever({
  serverURL: process.env["CONTEXT_RETRIEVER_SERVER_URL"] ?? "",
  auth: { type: "apiKey", apiKey: process.env["CONTEXT_RETRIEVER_API_KEY_AUTH"] ?? "" },
  retryUnsafeOperations: true,
});

Health probes never retry. getHealth, getLiveness, and getReadiness are pinned to retries: { strategy: "none" } unconditionally, independent of retryUnsafeOperations and independent of a client-wide retryConfig, so a getReadiness returning 503 answers at once rather than blocking a load balancer for the retry budget. A per-call retries is still honoured if you deliberately ask for one.

Default timeout. createContextRetriever applies a 30 second deadline to every request, with one exception: importData and inferSchema get 300000ms, because schema inference samples Redis and calls an inference provider, and import is synchronous and scales with batch size. Both would otherwise be killed by the 30 second default, and because both are non-idempotent there is no safe retry to recover with. Override either globally with timeoutMs, or per call:

const surface = await client.contextSurfaces.getContextSurface(id, {
  timeoutMs: 5_000,
});

An SDK deadline that fires raises TimeoutError with statusCode undefined. A signal you pass raises ClientClosedRequestError. The raw generated new ContextRetriever(...) client has no default deadline, so requests through it are unbounded unless you set timeoutMs yourself.

[!WARNING] A signal you pass replaces the deadline; it does not combine with it. The generated transport only arms its own AbortSignal.timeout when no signal is present, so a call that passes signal runs unbounded no matter what timeoutMs says. To get both, compose them yourself:

const surfaces = await client.contextSurfaces.listContextSurfaces(1, 10, {
  signal: AbortSignal.any([callerSignal, AbortSignal.timeout(30_000)]),
});

Retries and the deadline interact: timeoutMs bounds each attempt, while maxElapsedTime is checked between attempts before the next sleep. The conservative wall-clock bound is therefore maxElapsedTime + maxInterval + timeoutMs; with the default read policy that is 15s + 15s + 30s = 60s, not 30s. A caller-provided AbortSignal is the way to impose a tighter end-to-end deadline, and it interrupts an in-progress backoff sleep as well as the HTTP attempt. Compose it with AbortSignal.timeout(...) as shown above when you need both caller cancellation and a fixed call budget.

Error Handling

Every failure from a client built by createContextRetriever is a ContextRetrieverError. The mapping is total: HTTP responses, client-side validation, timeouts, cancellations, DNS and TLS failures, and MCP protocol failures all arrive as subclasses, so a single instanceof ContextRetrieverError check is a reliable backstop and nothing escapes as a bare TypeError or AbortError.

Properties

| Property | Type | Description | | ------------------- | ----------------------- | ------------------------------------------------ | | error.message | string | The service's message, with a remediation hint appended for known statuses | | error.statusCode | number \| undefined | HTTP status. undefined means no HTTP response was received, so a write may or may not have been applied | | error.details | unknown | Parsed JSON error body, validation data, or raw text. Scrubbed and non-enumerable: see below | | error.headers | Headers \| undefined | Response headers, when a response was received | | error.rawResponse | Response \| undefined | The untouched response | | error.requestId | string \| undefined | Server correlation id, from x-request-id or x-correlation-id. Quote it in a support request | | error.cause | unknown | A scrubbed stand-in for HTTP and validation failures, the original error for transport failures: see below |

details is deliberately hard to leak

A createAgentKey response body carries a plaintext key, and a rejected createContextSurface body carries a Redis password. So details is defined as a non-enumerable property holding an already-scrubbed copy of the payload. Two consequences:

  • JSON.stringify(error) and a structured logger that walks own enumerable properties will not include it. Neither will console.error(error) in most runtimes. That is the point; do not "fix" it by copying the error into a plain object.
  • Reading it is a deliberate act: error.details works normally, and known secret-bearing fields are already replaced with <redacted> by the time you see them. Secrets drawn from the same payload are also masked inside error.message, but on weaker terms than in details: see Debug logging and redaction for the length floors and the shape heuristic that gate message masking. Treat error.message as sensitive.

error.headers and error.rawResponse are the unscrubbed originals. They are the escape hatch for callers who need exactly what the service sent, so nothing is masked in them: a response header such as x-api-key echoed by an ingress is readable verbatim. error.rawResponse.bodyUsed is already true, because the generated transport read the body to build the error, so only its headers and status are still readable.

error.cause behaves differently for payloads and for transport failures

Two modes, chosen by whether the underlying error carries a response body:

  • Payload-bearing failures get a scrubbed stand-in Error. That is every status-mapped class (BadRequestError, NotFoundError, RateLimitError, ServerError, and the rest), ResponseValidationError, RequestValidationError raised by a schema failure, MCPError, and APIError. The stand-in carries the original's message, name, and stack, all three with secrets masked, and nothing else: no body, no statusCode, no nested cause, and no instanceof relationship to the generated error class. Use statusCode, details, headers, rawResponse, and requestId for structured detail; the stand-in is for reading, not for branching.
  • Transport failures (NetworkError, TimeoutError from the SDK's own deadline, ClientClosedRequestError from your AbortSignal, and a RequestValidationError from a malformed request) get the original cause, untouched, including its nested chain.

The split exists because the two carry different things. The generated HTTP error inlines the raw response body into its own message and keeps it on body, so retaining it verbatim would put a plaintext agent key within reach of console.error(error), which prints [cause] at the default inspect depth. A transport error carries no payload at all, so scrubbing it buys nothing and costs the only diagnostic an operator has: undici reports a refused connection as { code: "ECONNREFUSED" } on a nested cause, and a stand-in drops both the code and the chain, making a DNS failure and a refused connection indistinguishable.

if (error instanceof NetworkError) {
  // The original chain survives, so the transport's own code is reachable.
  const cause = error.cause as { cause?: { code?: string } } | undefined;
  console.error("transport code:", cause?.cause?.code); // e.g. "ECONNREFUSED"
}

Error classes

| Class | Status | What to do | | ----- | ------ | ---------- | | RequestValidationError | none | Arguments failed client-side validation. Fix the call. Never retry. | | BadRequestError | 400 | The service rejected the request as malformed. Fix the payload. Never retry. | | AuthenticationError | 401 | Credential missing, expired, or wrong. Check the on-prem admin key used for control-plane calls. | | AuthorizationError | 403 | Credential is valid but lacks the required scope. Never retry. | | NotFoundError | 404 | No such resource. Check the identifier; it may also have been deleted. | | ConflictError | 409 | Already exists, or in a conflicting state. Often safe to treat as success when creating idempotently. | | UnprocessableEntityError | 422 | Well-formed but semantically invalid. Fix field values. Never retry. | | ClientDependencyError | 424 | A dependency the service needs was unreachable, typically the surface's Redis data source or an inference provider. Verify the data source address and credentials. Often retryable. | | RateLimitError | 429 | Back off. Use retryAfterMs. | | ClientClosedRequestError | 499 or none | Cancelled. statusCode is 499 when the service reported it, undefined when your AbortSignal fired. Never retry automatically. | | ServerError | 500 | The service failed unexpectedly. Retry idempotent calls; report requestId if it persists. | | UpstreamError | 502 | An upstream dependency failed. Retry idempotent calls with backoff. | | ServiceUnavailableError | 503 | Temporarily unavailable. Retry with backoff. | | TimeoutError | 504 or none | Timed out. statusCode is 504 when the service reported it, undefined when the SDK's own deadline fired, in which case the request may still have been applied. | | NetworkError | none | Transport failure: DNS, TLS, connection refused, socket reset. No response was received. Retry with backoff, but reconcile before replaying a write. | | ResponseValidationError | any | The response did not match the SDK's schema, which usually means the service and the installed SDK version have diverged. Alert and upgrade. Do not retry. | | MCPError | transport status, or none | An MCP transport, protocol, or result-validation failure, including using a closed client. statusCode carries the transport's HTTP status when there was one; 404 means the session is gone and the client must be replaced. See MCP.md. | | APIError | any | Catch-all for a failure with no more specific class, including unmapped statuses. | | ContextRetrieverError | any | Base class. Catch this as a backstop. |

Rate limiting

RateLimitError exposes both the raw header and a normalised delay. retryAfterMs is parsed from delta-seconds, an HTTP-date, or the non-standard Retry-After-Ms header, so callers do not have to handle three formats. Both are undefined when the service sent no hint.

Parsing is strict on purpose. Only a plain integer or decimal ("30", "1.5") or an HTTP-date is accepted; exponent notation and trailing junk such as "3abc" are treated as malformed and yield undefined rather than a silently wrong number. The result is clamped to 24 hours, so retryAfterMs is always safe to hand straight to setTimeout. Because it is parsed from the header rather than from the retry config, it is also not clamped by maxInterval, which makes it the right value to use when the service asks for a backoff longer than the 15s retry budget.

if (error instanceof RateLimitError) {
  const waitMs = error.retryAfterMs ?? 1_000;
  console.log(`raw header: ${error.retryAfter ?? "none"}`);
  await new Promise(resolve => setTimeout(resolve, waitMs));
}

Example

import {
  createContextRetriever,
  ContextRetrieverError,
  NetworkError,
  NotFoundError,
  RateLimitError,
} from "@redis-iris/context-retriever";

const contextRetriever = createContextRetriever({
  serverURL: "https://api.example.com",
  auth: { type: "apiKey", apiKey: "<CONTEXT_RETRIEVER_API_KEY>" },
});

async function run() {
  try {
    const result = await contextRetriever.contextSurfaces.getContextSurface("abc");

    console.log(result);
  } catch (error) {
    if (error instanceof NotFoundError) {
      // The surface does not exist. Do not retry.
    } else if (error instanceof RateLimitError) {
      // Wait error.retryAfterMs before trying again.
    } else if (error instanceof NetworkError) {
      // No response was received; error.statusCode is undefined.
    } else if (error instanceof ContextRetrieverError) {
      // Backstop: the hierarchy is total, so this catches everything else.
      console.error(error.name, error.statusCode, error.requestId, error.details);
    } else {
      throw error;
    }
  }
}

run();

examples/errorHandling.example.ts shows the full taxonomy, a backoff loop driven by retryAfterMs, and how to reconcile a write whose outcome is unknown.

What does not get typed errors

Two escape hatches deliberately keep the generated contract, and neither is translated into this hierarchy:

  • client.rest, the raw generated ContextRetriever instance, and any client built with new ContextRetriever(...). These reject with the generated error types from @redis-iris/context-retriever/models/errors.
  • The tree-shakable functions under @redis-iris/context-retriever/funcs/*, which return result objects carrying generated errors rather than throwing.

Prefer the resource properties on the client returned by createContextRetriever. Reach for rest or funcs/* only when you need the unmodified generated contract, and handle generated error types explicitly when you do.

Debug logging and redaction

Debug logging prints every request header and every JSON body, which would otherwise write API keys, bearer tokens, Redis passwords, and other credential-bearing values into stdout, CI job logs, and any log aggregator.

createContextRetriever guards both routes into that output:

  • A debugLogger you pass is automatically wrapped in redactingLogger.
  • CONTEXT_RETRIEVER_DEBUG=true normally makes the generated core fall back to bare console. createContextRetriever pre-empts that fallback by resolving a redacting logger itself, so the environment variable cannot bypass redaction.

What is matched

Matching is name-based. Names are compared with case and separators removed, so api_key, apiKey, API-KEY, and ApiKey are one and the same name; that is what covers both the wire's snake_case and the models' camelCase.

These header lines are replaced with <redacted>:

authorization, cookie, set-cookie, proxy-authorization, x-api-key, x-csrf-token.

These body field names are replaced with <redacted>:

accesstoken, adminkey, agentkey, apikey, apikeyauth, authorization, cookie, csrftoken, jsessionid, key, passwd, password, secret, sessioncookie, token, useraccesstoken.

A credential in a URI's userinfo is masked too, because no name/value rule can see it: redis://default:[email protected]:6379 logs as redis://default:<redacted>@10.0.0.5:6379. This is the shape a data-source address takes.

Nested payloads are walked to a depth of 64, and cycles are detected and rendered as <circular>.

details masking is unconditional; message masking is not

This is the one asymmetry worth understanding, because it decides how you may treat error.message.

  • ContextRetrieverError.details, and anything a redacting logger prints, is masked purely by field name. If the name matches the list above, the value is replaced, whatever it looks like and however short it is.

  • Error.message masking is best-effort. The generated transport builds its messages by inlining the raw response body, so the same secret is already flattened into a string by the time the SDK sees it, with no field structure left to key off. Masking there works by collecting candidate secret values out of the structured payload and striking them out of the text, and a candidate has to clear two gates:

    | Gate | Rule | | ---- | ---- | | Length floor | 3 characters for an unambiguous credential name (password, apikey, adminkey, agentkey, accesstoken, csrftoken, sessioncookie, jsessionid, authorization, cookie, and their separator variants). 24 characters for key, secret, and token, which are ordinary business field names in customer records while this service's own key format runs past fifty characters. | | Shape | For a name: value or name=value pair found in a non-JSON body, the value must look like a credential: at least 12 characters, or containing a non-alphabetic character. |

    The floors are deliberately lopsided. A Redis password has no minimum length and must be masked even when it is three characters; a 6-character token field on a customer record is business data, and masking it would corrupt the diagnostic. The shape gate exists for the same reason in the other direction: without it, Invalid password: must be at least 12 characters logs as Invalid password: <redacted> be at least 12 characters, destroying the message at the exact moment someone is reading it.

    Auth schemes are skipped rather than masked, so authorization: Bearer <token> keeps the word Bearer and loses only the token. In the other direction, a line that merely looks like a header dump (authorization: ...) is redacted wholesale; that is the safe direction to err in, but it means a log line can lose more than the credential.

[!IMPORTANT] Treat error.message as sensitive. A name-based, floored redactor cannot mask a credential the service returns under a field name nobody anticipated, and it will not mask a short value under an ambiguous name. details is the property with the strong guarantee; message is the one that is merely usually clean.

Exported functions

Three functions are exported.

| Export | Use it for | | ------ | ---------- | | redactingLogger(logger) | Wrap a logger so credentials never reach it. Applied automatically to any debugLogger you pass to createContextRetriever. | | redactForOutput(value) | Redact one structured value, for logging a request or response payload through your own logger rather than the SDK's. Name-based and unconditional, like details. | | redactSecretsInText(text, payload) | Mask secrets drawn from payload wherever they appear in text. This is the best-effort path described above, and the one the SDK uses on Error.message. Reach for it when you are assembling your own message out of a payload the SDK returned. |

import { redactForOutput, redactSecretsInText } from "@redis-iris/context-retriever";

myLogger.info("surface payload", redactForOutput(payload));
myLogger.error(redactSecretsInText(`import rejected: ${rawBody}`, payload));

redactingLogger is exported too, so you can wrap your own logger before handing it to anything else:

import { redactingLogger } from "@redis-iris/context-retriever";

const client = createContextRetriever({
  serverURL: process.env["CONTEXT_RETRIEVER_SERVER_URL"] ?? "",
  auth: { type: "apiKey", apiKey: process.env["CONTEXT_RETRIEVER_API_KEY_AUTH"] ?? "" },
  // Wrapped automatically; wrapping it yourself is harmless and explicit.
  debugLogger: redactingLogger(myLogger),
});

[!WARNING] The raw generated client does not redact. new ContextRetriever({ debugLogger }) and the funcs/* tree write credentials verbatim. Redaction is also name-based, so it cannot cover a secret returned under an unexpected field name. Treat debug output as sensitive regardless, and keep it out of shared CI logs.

CONTEXT_RETRIEVER_DEBUG accepted values

createContextRetriever recognises exactly two values as "on": "true" and "1".

| Value | Effect | | ----- | ------ | | true, 1 | Debug logging on, through a redacting logger. | | false, 0, off, no, TRUE, True, yes, the empty string, or any other value | Debug logging off. | | unset | Off. |

The matching is exact and case-sensitive on purpose. The generated core parses this variable with z.coerce.boolean(), under which every non-empty string including "false" is truthy, so CONTEXT_RETRIEVER_DEBUG=false would otherwise enable logging. createContextRetriever pre-empts that by installing a no-op logger for any value it does not recognise.

[!WARNING] That fix lives in createContextRetriever, not in the generated core. A raw new ContextRetriever({ ... }) still coerces loosely, so CONTEXT_RETRIEVER_DEBUG=false enables unredacted debug logging there.

An explicit debugLogger option always wins over the environment variable, in either direction: passing one enables logging regardless of the variable, and the variable cannot swap out the logger you passed.

Transport security

Credentials are sent as headers, and request bodies can contain Redis passwords, so createContextRetriever refuses a plaintext serverURL:

// Throws RequestValidationError: serverURL must use https:
createContextRetriever({ serverURL: "http://context.example.com", auth });

The check runs at two points. Construction is the early, friendly one; dispatch is the load-bearing one:

| Where | What it sees | Message says | How it surfaces | | ----- | ------------ | ------------ | --------------- | | Construction | options.serverURL | serverURL must use https: | Synchronous RequestValidationError from createContextRetriever | | Immediately before every dispatch, inside the auth hook | The URL of the Request actually being sent | request URL must use https: | RequestValidationError as a promise rejection from the call |

The dispatch-time check is the one that matters, because a constructor-only check is bypassable by anything that rewrites the target afterwards: an sdkInit hook returning a different serverURL, a beforeCreateRequest hook rewriting input.url, a per-call serverURL, or a mutated base URL. One check placed where the credential is attached covers all of them, and nothing is dispatched when it fires.

https: URLs are always accepted, and localhost, 127.0.0.1, and ::1 are exempt so local development needs no opt-in. For a non-loopback plaintext host, for example a test container reached by service name, pass allowInsecureTransport: true and understand that the admin credential then travels in the clear:

const client = createContextRetriever({
  serverURL: "http://context-retriever.test:8080",
  auth: { type: "apiKey", apiKey: "test-key" },
  allowInsecureTransport: true,
});

The same rule and the same option apply to mcpURL on ContextRetrieverMCPClient.connect, except that a rejected MCP URL is reported as MCPError rather than RequestValidationError.

A per-call serverURL override is checked too. It reaches the same credential, so it has to clear the same bar. Because the methods it applies to are async, a rejected per-call override arrives as a promise rejection rather than a synchronous throw, so an ordinary catch sees it:

await client.contextSurfaces
  .listContextSurfaces(1, 10, { serverURL: "http://elsewhere.example.com" })
  .catch((error: unknown) => {
    // RequestValidationError, with no `cause`: the transport check raises it
    // directly rather than wrapping something. The message carries the same
    // "must use https:" text as the constructor check.
  });

Redirects are not followed. Requests are dispatched with redirect: "manual". undici strips Authorization and Cookie across origins but not custom headers, so a followed 3xx would hand X-API-Key to whatever host the redirect named. Failing closed is the safer default, but it has a visible consequence: a legitimate 3xx, for example a deployment that has moved and answers 301, surfaces as an error rather than transparently succeeding. If you hit that, point serverURL at the new location instead of relying on the redirect.

Custom HTTP Client

The TypeScript SDK makes API calls using an HTTPClient that wraps the native Fetch API. This client is a thin wrapper around fetch and provides the ability to attach hooks around the request lifecycle that can be used to modify the request or handle errors and response.

The HTTPClient constructor takes an optional fetcher argument that can be used to integrate a third-party HTTP client or when writing tests to mock out the HTTP client and feed in fixtures.

The following example shows how to:

  • route requests through a proxy server using undici's ProxyAgent
  • use the "beforeRequest" hook to add a custom header and a timeout to requests
  • use the "requestError" hook to log errors
import { createContextRetriever } from "@redis-iris/context-retriever";
import { ProxyAgent } from "undici";
import { HTTPClient } from "@redis-iris/context-retriever/lib/http";

const dispatcher = new ProxyAgent("http://proxy.example.com:8080");

const httpClient = new HTTPClient({
  // 'fetcher' takes a function that has the same signature as native 'fetch'.
  fetcher: (input, init) =>
    // 'dispatcher' is specific to undici and not part of the standard Fetch API.
    fetch(input, { ...init, dispatcher } as RequestInit),
});

httpClient.addHook("beforeRequest", (request) => {
  const nextRequest = new Request(request, {
    signal: request.signal || AbortSignal.timeout(5000)
  });

  nextRequest.headers.set("x-custom-header", "custom value");

  return nextRequest;
});

httpClient.addHook("requestError", (error, request) => {
  console.group("Request Error");
  console.log("Reason:", `${error}`);
  console.log("Endpoint:", `${request.method} ${request.url}`);
  console.groupEnd();
});

// The client is cloned and the SDK's auth hook is appended last, so a hook
// registered above cannot displace the configured credential.
const sdk = createContextRetriever({
  serverURL: "https://api.example.com",
  auth: { type: "apiKey", apiKey: "<CONTEXT_RETRIEVER_API_KEY>" },
  httpClient,
});

Pass either fetch or httpClient, not both; supplying both throws a TypeError.

Lifecycle hooks

options.hooks takes an SDKHooks instance and gives you five points in the request lifecycle. It is the instrumentation seam: tracing spans, metrics, request-id propagation, and response inspection all belong here.

| Hook | Signature | Runs | | ---- | --------- | ---- | | sdkInit | (options) => options | Once, while the client is constructed. Can replace serverURL and the HTTP client. | | beforeCreateRequest | (context, input) => input | Before the Request object exists, so it is the only place input.url can be rewritten. Synchronous. | | beforeRequest | (context, request) => request | Once per attempt, including every retry. May throw to stop the request. | | afterSuccess | (context, response) => response | On a successful status. | | afterError | (context, response, error) => { response, error } | On an error status code. A thrown transport failure does not reach it; catch that at the call site. |

Every hook after sdkInit receives a HookContext carrying operationID, the baseURL, the resolved retryConfig, and timeoutMs. operationID is a fixed string drawn from the OpenAPI document (getContextSurface, importData, and so on), which makes it the right thing to use as a metric label: the cardinality is bounded by the API surface, unlike a URL path that carries surface ids.

import { createContextRetriever, SDKHooks } from "@redis-iris/context-retriever";

const hooks = new SDKHooks();

hooks.registerBeforeRequestHook({
  beforeRequest: (context, request) => {
    console.time(context.operationID);
    return request;
  },
});

hooks.registerAfterSuccessHook({
  afterSuccess: (context, response) => {
    console.timeEnd(context.operationID);
    metrics.increment("context_retriever.request", {
      // Bounded label: one value per API operation.
      operation: context.operationID,
      status: String(response.status),
    });
    return response;
  },
});

const client = createContextRetriever({
  serverURL: "https://api.example.com",
  auth: { type: "apiKey", apiKey: "<CONTEXT_RETRIEVER_API_KEY>" },
  hooks,
});

hooks versus httpClient.addHook

Both let you see a request before it is sent, and they are not interchangeable.

| | options.hooks (SDKHooks) | options.httpClient.addHook(...) | | - | --------------------------- | --------------------------------- | | Sees | Typed lifecycle events, with operationID and the resolved retry config | Only a bare Request, Response, or error | | Knows which operation it is | Yes, via context.operationID | No | | Can rewrite the target URL | Yes, in beforeCreateRequest | No: the Request already exists | | Can replace the fetch implementation | Yes, in sdkInit | Yes, via the fetcher constructor option | | Runs | Before the HTTP client's own hooks | After every SDKHooks hook |

Prefer hooks for anything that wants to know what is being called. Reach for httpClient.addHook when you already hold a configured HTTPClient, or when a bare Request is all you need.

What a hook cannot do

Hook order is a security property here, not an implementation detail. The full beforeRequest order is: your SDKHooks hooks, then your HTTPClient hooks, then the SDK's own auth hook, appended last. Three consequences, each covered by a test:

  • A hook cannot displace the configured credential. The auth hook sets X-API-Key after every hook of yours has run, so a hook that sets that header is overwritten rather than honoured. A caller-supplied httpClient is cloned before the auth hook is appended, so the instance you hold keeps its own hook list and does not start sending the SDK's credential on your own requests.
  • A hook cannot strip redirect: "manual". The auth hook rebuilds the request with it, so a followed 3xx cannot hand X-API-Key to another origin no matter what a hook did.
  • A hook cannot reach a plaintext URL. The auth hook runs the transport check against the URL actually being dispatched, so an sdkInit or beforeCreateRequest hook that rewrites the target to http: produces a RequestValidationError and dispatches nothing. See Transport security.

The one thing a hook can do is stop a request: throwing from beforeRequest aborts it, and the error is translated into the ContextRetrieverError hierarchy like any other failure.

Two limits, stated rather than implied. options.hooks must be an actual SDKHooks instance; the generated core checks instanceof and silently ignores an object that merely has the right shape. And an sdkInit hook that replaces options.httpClient outright discards the SDK's auth hook with it, which removes the credential and the dispatch-time transport check together, so requests then go out unauthenticated rather than authenticated to the wrong place. Mutate the client you are given, or register your hooks on it, rather than substituting a new one.

[!NOTE] hooks and httpClient compose, but fetch and httpClient do not. Passing both throws a TypeError; pass a fetcher to the HTTPClient constructor instead.

Debugging

You can setup your SDK to emit debug logs for SDK requests and responses.

You can pass a logger that matches console's interface as an SDK option.

[!WARNING] Debug logging reveals request and response contents. A client built by createContextRetriever routes debug output through a redacting logger, which masks credential headers and known secret-bearing fields, but debug output should still be treated as sensitive and used only during local development. The raw generated client does not redact at all. See Debug logging and redaction.

import { createContextRetriever } from "@redis-iris/context-retriever";

const sdk = createContextRetriever({
  serverURL: "https://api.example.com",
  auth: { type: "apiKey", apiKey: "<CONTEXT_RETRIEVER_API_KEY>" },
  debugLogger: console,
});

You can also enable a default debug logger by setting an environment variable CONTEXT_RETRIEVER_DEBUG to true.

Development

Maturity

This SDK is in beta, and there may be breaking changes between versions without a major version update. Therefore, we recommend pinning usage to a specific package version. This way, you can install the same version each time without breaking changes unless you are intentionally looking for the latest version.

Nothing has been published to npm yet; this package will publish as @redis-iris/context-retriever 0.1.1. See CHANGELOG.md for what that first release contains and the caveats worth knowing before you adopt it, and RELEASES.md for the generated release history once it exists.

Contributions

This library is mostly generated programmatically from an OpenAPI document, but not entirely. Changes to generated files are overwritten on the next generation. The paths below are maintained by hand and are preserved across generation, as listed in .genignore:

| Path | Contents | | ---- | -------- | | src/client.ts | createContextRetriever, the unified facade, auth, retry pinning, transport checks | | src/errors.ts | The ContextRetrieverError hierarchy and error translation | | src/lib/retries.ts | Generated, but carries a maintained edit: the backoff sleep accepts the caller's AbortSignal. Genignored so the edit is not overwritten. See DEVELOPMENT.md | | src/redaction.ts | SDK-specific policy and non-object adapters around @pinojs/redact | | src/mcp/** | The MCP client and its schemas | | src/index.extras.ts | Re-exports that add the above to the package entry point | | tests/**, tsconfig.test.json | The test suite | | README.md, MCP.md, RUNTIMES.md | Hand-maintained documentation | | .npmignore, jsr.json | Packaging | | examples/.env.template, examples/package.json, examples/README.md | Example environment template, dependencies, and guide | | The four hand-written examples/*.example.ts paths in .genignore | Runnable maintained examples |

When adding another hand-written example, add its explicit path to .genignore in the same change. Generated examples are intentionally not ignored.

examples/*.ts are typechecked by npm run typecheck, which compiles examples, src, and tests under the repository's own strict options (including noPropertyAccessFromIndexSignature, so read environment variables as process.env["NAME"] rather than process.env.NAME). An example that does not compile fails CI, so snippets here and in MCP.md should be written to the same rules.

The npm tarball ships docs/**, MCP.md, USAGE.md, RUNTIMES.md, FUNCTIONS.md, CHANGELOG.md, RELEASES.md, examples/README.md, examples/.env.template, and the examples/*.example.ts scripts themselves, so the relative links in this README resolve on npmjs.com. package.json also carries a repository field pointing at this directory, which is what makes npm rewrite the remaining relative links against the repository rather than dropping them. Verify the list with npm pack --dry-run after changing .npmignore.

Within this README, keep the <!-- Start ... --> and <!-- End ... --> marker comments intact and put new hand-written prose outside them, so a regeneration that rewrites a marked region cannot take your content with it.

We look forward to hearing your feedback. Feel free to open a PR or an issue with a proof of concept and we'll do our best to include it in a future release.

Repository development workflows, including generation, publishing, and the generated versus handwritten split, are documented in DEVELOPMENT.md.

SDK Created by Speakeasy