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-gateway

v1.0.0

Published

Redis Context Gateway REST and MCP SDK

Readme

@redis-iris/context-gateway

TypeScript SDK for Redis Context Gateway. One package holds both clients:

  • ContextGatewayService is the control-plane client for Gateway and Upstream administration. Speakeasy generates it from the Gateway control-plane API, the same way as the other Iris SDKs.
  • ContextGateway is the data-plane client. It connects agents to a Gateway's MCP endpoint with the official @modelcontextprotocol/client 2.x library.

Both are exported from the package entry. ContextGateway is also exported from @redis-iris/context-gateway/mcp.

Releases go to the npm latest dist tag, and npm lists the published versions. Works in Node 22 or later and in browsers, without Node polyfills.

npm install @redis-iris/context-gateway

Control plane

import { ContextGatewayService } from "@redis-iris/context-gateway";

const service = new ContextGatewayService({ serverURL: controlPlaneUrl, adminToken: () => loadAdminToken() });

const gateway = await service.gateways.create({ name: "Docs", toolExposureMode: "SEARCH" });
const upstream = await service.upstreams.create(gateway.gatewayId, {
  kind: "MCP",
  name: "Docs MCP",
  namespace: "docs",
  mcp: {
    endpoint: upstreamMcpUrl,
    authentication: { methods: [{ bearer: { value: upstreamToken } }] },
  },
});

Every product call sends the admin token. The operational health probes (service.health.live(), service.health.ready()) and public service probes (service.health.serviceLive() and service.health.serviceReady()) send no credential. See USAGE.md, FUNCTIONS.md and docs/ for every operation and model.

An oversized REST request returns the typed PayloadTooLargeErrorResponseContent error for HTTP 413, with the server's problem details. Catch it from @redis-iris/context-gateway/models/errors.

An Upstream has a kind and exactly one settings object named after it: mcp (endpoint, transport, authentication) for an MCP Upstream or contextSurface (databaseId, dataModel) for a Context Surface Upstream. The control plane refuses a request whose kind and settings object disagree. Reads return the same object. A Context Surface Upstream reports contextSurface.databaseBinding, and a Gateway reports databaseBinding. Each holds a databaseId and a name, and name is absent when the registry lookup fails, so print it with a fallback:

const binding = gateway.databaseBinding;
const label = binding ? (binding.name ?? `database ${binding.databaseId} (name unavailable)`) : "no database";

Omit databaseId on Gateway creation when no database is selected; do not send null. On-prem deployments can supply a default database. A Context Surface still needs a database, either explicitly selected or supplied by the deployment.

The client sends databaseId exactly as given and leaves the verdict to the control plane, so an empty string or an unknown ID comes back as a 400 that names the field. gateways.update(id, { databaseId: null }) sends JSON null, which clears the database. On gateways.create the schema types databaseId as a plain string, so null is refused by the client with an SDKValidationError and nothing is sent, where the Python SDK reads None as omitted. Both SDKs run the same table of cases from tests/context-gateway/parity/control-plane-cases.json.

Gateway and Upstream IDs are checked before a request goes out. An ID that does not use the required gw_ (Gateway) or up_ (Upstream) prefix followed by 32 lowercase hexadecimal characters, such as an empty string, throws an UnexpectedClientError whose cause is an InvalidRequestError that says so, and nothing is sent.

A refused connection throws ConnectionError and a timeout RequestTimeoutError, as before, and their messages now name the URL.

Context Surface data model

A Context Surface Upstream reads a Redis database through the data model you give it. The DataModel type, exported from the package entry and from @redis-iris/context-gateway/datamodel, follows the iris data model, so the compiler catches a misspelled key or an unknown index type. It is a type only. The control plane validates the model and names each refused member by its path under contextSurface.dataModel.

import type { DataModel } from "@redis-iris/context-gateway";

const dataModel = {
  title: "Healthcare scheduling",
  description: "Clinics and the providers who work at them",
  entities: [
    {
      name: "Location",
      description: "A medical facility where providers see patients",
      redis_key_template: "location:{id}",
      fields: [
        { name: "id", type: "str", description: "Location ID", is_key_component: true, redis_indices: [] },
        { name: "name", type: "str", description: "Facility name", redis_indices: [{ type: "text", weight: 1.0 }] },
        { name: "city", type: "str", description: "City", redis_indices: [{ type: "tag" }] },
      ],
      relationships: [],
    },
    {
      name: "Provider",
      description: "A clinician who sees patients at one location",
      redis_key_template: "provider:{id}",
      fields: [
        { name: "id", type: "str", description: "Provider ID", is_key_component: true, redis_indices: [] },
        { name: "specialty", type: "str", description: "Specialty", redis_indices: [{ type: "tag" }] },
        { name: "location_id", type: "str", description: "Home location", redis_indices: [{ type: "tag" }] },
      ],
      relationships: [
        { name: "works_at", target: "Location", description: "The provider's home location", source_field: "location_id" },
      ],
    },
  ],
  entity_count: 2,
} satisfies DataModel;

const surface = await service.upstreams.create(gateway.gatewayId, {
  kind: "CONTEXT_SURFACE",
  name: "Scheduling",
  contextSurface: { databaseId, dataModel },
});

tests/context-gateway/healthcare-datamodel.json is a complete six-entity model that passes the control plane's validation. acl_field_mappings is not in the type because the control plane refuses it as not supported yet. The compact JSON of a model is at most 1 MiB.

Agents (MCP)

import { ContextGateway } from "@redis-iris/context-gateway";

const client = new ContextGateway({ url: gateway.mcpEndpoint, agentKey });
const mcp = await client.connect();
try {
  const found = await mcp.callTool({ name: "search_tools", arguments: { query: "find an issue" } });
  const result = await mcp.callTool({
    name: "run_tool",
    arguments: { tool: { upstreamId: upstream.upstreamId, originalName: "get_issue" }, toolArgs: { id: 42 } },
  });
} finally {
  await mcp.close();
}

client.connect() returns an initialized official MCP Client. Each call opens a new one. The constructor checks the URL, so an unsafe one throws before any request. Search tools, run_tool and the handle tools are ordinary tool calls. A tool failure comes back as a result with isError: true, not an exception.

connect() gives up after 30 seconds (connectTimeoutMs, or connect({ timeoutMs }) for one call). A request after it waits at most 5 minutes (requestTimeoutMs) for the Gateway to start answering, and the timeout ends when the response starts, so a long streamed answer is not cut off. Failures are typed and name the URL, never the agent key:

  • HTTP 401, 403 or 404 throw GatewayHTTPError, with status and url. It replaces the MCP library's SdkHttpError with CLIENT_HTTP_NOT_IMPLEMENTED, and matches Python's GatewayHTTPError.
  • An unreachable host throws GatewayConnectionError, a ConnectionError.
  • A timeout throws GatewayTimeoutError, a RequestTimeoutError.
  • A malformed URL or an empty agentKey throws InvalidRequestError from the constructor.

A JSON-RPC error, such as an unknown direct tools/call name, comes out as the MCP client's own error.

The credential is either of these, and both go in the agentKey option:

  • An Identity Service agent key with a context-gateway / gateway / use grant for the Gateway.
  • An access token from the deployment's identity provider, when the deployment has IdP sign-in on. The Gateway still answers 403 when the signed-in caller has no access grant for it.

The client sends the value unchanged as Authorization: Bearer, the only place the Gateway accepts an access token. It doesn't sign users in or refresh tokens. To find the identity provider, read the Gateway's protected resource metadata (RFC 9728) without a credential at /.well-known/oauth-protected-resource/iris/data/v1/context-gateway/gateways/{gatewayId}/mcp on the data-plane host, or take the resource_metadata URL from the WWW-Authenticate challenge of a 401. Its authorization_servers names the issuer and scopes_supported the scopes to request. When a token expires, build a new client with a fresh one.

The Gateway negotiates MCP protocol 2026-07-28, 2025-11-25, 2025-06-18 and 2025-03-26. @modelcontextprotocol/client 2.3.1, the version the lockfile pins for ^2.3.1, connects with the initialize handshake by default and offers 2025-11-25, so that is the version a session uses. It also accepts a server that answers 2025-06-18 or 2025-03-26, and sends the negotiated version on every later request. The library can negotiate 2026-07-28 through its opt-in versionNegotiation setting, which ContextGateway doesn't turn on.

In full mode, use the tool names returned by discovery unchanged: a namespace such as docs and tool search appear as docs__search. The service enforces a 32-character namespace limit; generated SDK models accept a plain string and leave this validation to the service. Search-mode calls continue to identify tools by upstreamId and originalName.

An unknown or inaccessible target passed to run_tool or run_tool_as_handle returns a tool error result with isError, unknown_tool, and not_dispatched. The MCP SDK preserves this result instead of raising a transport exception.

Handles

A Gateway can hold a large tool result and answer with a small handle stub instead. The agent then reads the result in pieces through seven tools:

| Tool | What it does | | --- | --- | | run_tool_as_handle | Runs one tool and holds its result whatever its size | | page_results | Reads a held result in order, by item for a collection or by byte for text | | query_results | Selects from a held JSON result with an RFC 9535 JSONPath | | count_results | Counts items or JSONPath selections, optionally grouped by one groupBy field | | aggregate_results | Reduces the numbers a JSONPath selects: count, sum, min, max and avg | | describe_result | Describes a held result again: shape, size, fields, completeness, source and expiry | | release_results | Deletes a held result before it expires |

The Gateway lists them in both exposure modes when the data plane runs with compression.enabled: true. Handles are held in the Gateway's database, so the Gateway also needs a databaseId. Without one, automatic capture returns the ordinary result whole; explicit run_tool_as_handle capture returns a tool error, and handle reads return an unknown-handle error.

count_results accepts jsonpath and groupBy together: for example, {"handle": "h_...", "jsonpath": "$.issues[*]", "groupBy": "/user/login"} groups the selected issues. A selected array contributes its elements; a selected null remains a node. Field forms include user.login, /user/login, and $.user.login. Omit optional arguments when unused. Counts cover the captured items; check completeness and warning before treating them as whole-match totals.

run_tool_as_handle takes the arguments of the call it wraps. In full mode that is { name: <tool name from tools/list>, toolArgs: {...} }. In search mode it is { tool: { upstreamId, originalName }, toolArgs: {...} }, as run_tool takes. The other tools take a handle, an ID that matches ^h_[0-9a-f]{32}$. Explicit capture failures remain ordinary MCP results with isError: true and their failure metadata; the client does not turn them into exceptions.

import { HANDLE_META_KEY, PAGE_RESULTS, RELEASE_RESULTS, RUN_TOOL_AS_HANDLE } from "@redis-iris/context-gateway";

const stub = await mcp.callTool({
  name: RUN_TOOL_AS_HANDLE,
  arguments: { tool: { upstreamId: upstream.upstreamId, originalName: "list_issues" }, toolArgs: {} },
});
const held = stub._meta?.[HANDLE_META_KEY] as { handle: string } | undefined;
if (held) {
  try {
    const page = await mcp.callTool({ name: PAGE_RESULTS, arguments: { handle: held.handle, offset: 0, limit: 10 } });
  } finally {
    // Release even if paging fails, or the result stays in the Gateway database until its TTL.
    await mcp.callTool({ name: RELEASE_RESULTS, arguments: { handle: held.handle } });
  }
}

A stub carries the envelope as structuredContent and JSON text, and the same envelope without its preview and next step under _meta["com.redis.context-gateway/handle"]. An ordinary tool call can fall back to its whole result when automatic capture fails, so check for that key before reading a stub. An explicit run_tool_as_handle call instead returns a tool error when storage cannot create the handle, including an out-of-memory refusal. An unknown, expired, released or another caller's handle is a tool error result, not an exception. Handles are bound to the caller and the tenant.

The package entry and @redis-iris/context-gateway/mcp export the tool names (RUN_TOOL_AS_HANDLE, PAGE_RESULTS, QUERY_RESULTS, COUNT_RESULTS, AGGREGATE_RESULTS, DESCRIBE_RESULT, RELEASE_RESULTS, and all seven in the frozen HANDLE_TOOLS), HANDLE_META_KEY and HANDLE_ID_PATTERN. Check an ID with new RegExp(HANDLE_ID_PATTERN).test(value).

Examples

examples/ builds two agents, on OpenAI function calling, over a Redis fleet inventory kept in Redis, each on its own Gateway: a Redis helpdesk chat agent (full mode, a Context Surface plus the public Redis docs MCP server, tool cache) and a scheduled capacity planner (search mode, handles). An admin script creates both Gateways. Its README has the run order.

Moving to 1.0.0b1

1.0.0b1 follows the unified Iris API (/iris/v1/context-gateway/... and /iris/data/v1/context-gateway/...).

| Before | 1.0.0b1 | | --- | --- | | displayName | name (1-128 characters) | | gateway.id, upstream.id | gateway.gatewayId, upstream.upstreamId; IDs are gw_ / up_ plus 32 lowercase hex characters | | toolExposureMode: "search", kind: "mcp", kind: "context-surface" | "SEARCH" / "FULL", "MCP", "CONTEXT_SURFACE"; every enum value is UPPER_SNAKE_CASE | | nextToken / maxResults | cursor / limit (default 50, at most 200); lists return items and nextCursor | | RFC 9728 metadata at /.well-known/oauth-protected-resource/v1/gateways/{gatewayId}/mcp | /.well-known/oauth-protected-resource/iris/data/v1/context-gateway/gateways/{gatewayId}/mcp |

Renamed in 0.1.0-alpha.5

The data-plane client takes the product name, as Playbook and Lens do. There is no alias for the old names.

| 0.1.0-alpha.4 | 0.1.0-alpha.5 | | --- | --- | | new ContextGatewayClient(options) | new ContextGateway(options) | | ContextGatewayClientOptions | ContextGatewayOptions |

ContextGatewayService is unchanged.

Renamed in 0.1.0-alpha.2

0.1.0-alpha.2 renames the clients so that <Product>Service is the control-plane client and <Product>Client is the data-plane client, the naming every Iris SDK follows.

| 0.1.0-alpha.1 | 0.1.0-alpha.2 | | --- | --- | | new ContextGatewayClient({ serverURL, adminToken }) (control plane) | new ContextGatewayService({ serverURL, adminToken }) | | ContextGatewayClientCore from @redis-iris/context-gateway/core.js | ContextGatewayServiceCore | | connectMcp(options) from @redis-iris/context-gateway/mcp (removed after 0.1.0-alpha.2) | new ContextGatewayClient(options).connect() | | ConnectMcpOptions (removed after 0.1.0-alpha.2) | ContextGatewayClientOptions | | ContextGatewayClientError (base of every control-plane error) | ContextGatewayError, as in Python |

connectMcp and ConnectMcpOptions were deprecated in 0.1.0-alpha.2 and are removed in the next alpha, so importing them fails.

Safety

  • Neither client follows redirects, so the admin token and agent keys stay on the Gateway.
  • Plain HTTP is refused unless the host is loopback (localhost, 127.0.0.0/8 or ::1).
  • The SDK never reads credentials from environment variables, and requests are sent without browser cookies. Your application supplies and stores credentials. The redis-iris CLI owns IRIS_CONTEXT_GATEWAY_* settings.
  • Upstream secrets are write-only: no response model carries them.
  • Generated payload debugging is disabled, even when debugLogger is supplied, because it would log request secrets. Application logging and custom HTTP transports are outside this guarantee. Do not log whole generated exceptions: validation diagnostics can retain supplied credentials, and response errors retain server-provided bodies.
  • No call is retried automatically.

With a server negotiating MCP protocol 2025-11-25, cancelling an MCP call stops the client's wait but does not abort that individual HTTP request. The server may still complete the tool call; cancellation does not roll back a write. Per-request transport cancellation is deferred to a later increment.

Development

npm ci
npm run lint && npm run build && npm test

npm run test:package packs the package with its lifecycle scripts, checks the file list and the public README, and imports the installed tarball as ESM, CommonJS and TypeScript. npm pack and npm publish rewrite this README in place for the package page (prepack), so run them from a clean checkout or restore the README afterwards. The CI browser check clean-installs the packed package and runs tests/context-gateway/browser-smoke.mjs in Chromium. Its Playwright/esbuild dependencies live under tests/context-gateway, outside the SDK package.

Hand-written files are src/hooks/registration.ts, src/mcp.ts, src/datamodel.ts, src/index.extras.ts, tests/, this README and the hand-written examples with their README, env template and package files, protected by .genignore. Speakeasy's generated src/index.ts re-exports src/index.extras.ts whenever that file exists, which is how ContextGateway reaches the package entry. The patch file in .speakeasy/patches/ wraps the generated outbound Connection union with the check in and Speakeasy reapplies it on every generation. Everything else is regenerated by speakeasy run -t context-gateway-ts-sdk from the repository root, or by the sdk_generation_context_gateway_ts_sdk.yaml workflow.

Upgrading positional arguments from 1.0.0-beta.1

The optional projectId argument now follows pagination and Upstream deletion arguments. Calls that previously supplied projectId first must use these orders:

| Method | Arguments before the final optional RequestOptions | | --- | --- | | gateways.list | limit?, cursor?, projectId? | | upstreams.list | gatewayId, limit?, cursor?, projectId? | | upstreams.listTools | gatewayId, upstreamId, limit?, cursor?, projectId? | | upstreams.delete | gatewayId, upstreamId, expectedRevision?, ignoreIndexSweepFailure?, projectId? |

For example, upstreams.delete(gatewayId, upstreamId, revision) now sends expectedRevision=revision and no projectId. To pass a project explicitly, use upstreams.delete(gatewayId, upstreamId, revision, false, projectId). Projects remain unsupported by the current server, so ordinary calls should omit them. The exported tree-shakable functions use the same order after their client argument.

Other Gateway methods retain their signatures. projectId remains immediately before the final RequestOptions, so pass undefined for it when supplying only request options, for example gateways.get(gatewayId, undefined, { timeoutMs: 5000 }). Python callers are unchanged because these query arguments are keyword-only.