Add Context Gateway
One MCP endpoint and one agent key in place of every MCP server the agent configures today. The Gateway holds each Upstream's secret, namespaces tool names so they never collide, can expose a large catalog as search_tools and run_tool instead of every schema, caches read-only tools you opt in, and can hand a large result back as a handle. The agent keeps its tools; only where it reaches them changes. Ask first, provision, then repoint.
1. Discover
Read the project:
- Find every MCP server the agent configures:
.mcp.json,.cursor/mcp.json,McpToolset,MCPServerStreamableHttp,MultiServerMCPClient,stdio_client, or a hand-written MCP client. Note each endpoint and where its secret comes from. - Find where tools are handed to the model and where the turn runs.
- Identify the framework from imports, and whether it has its own MCP client.
- Note stdio-only servers: an Upstream is an HTTP MCP endpoint (
streamable-http). A stdio server has to be served over HTTP first, or stays outside the Gateway.
First ask where the Gateway runs:
- Redis Cloud (console). The developer creates the Gateway and its Upstreams in the console, and the CLI only talks to the Gateway's MCP endpoint (step 3, "In Redis Cloud"). Cloud keeps the Gateway's cache and compression off, so skip questions 3 and 4.
- Self-managed (on-prem, or the local Compose stack). The CLI provisions everything (step 3).
Then ask the developer these five questions, nothing more. A shrug takes the default.
| Ask | If the answer is | Configure |
|---|---|---|
| Which MCP servers does the agent use, and where do their secrets live? | A list of HTTP endpoints | One Upstream each: upstream add --name NAME --endpoint URL with --bearer-secret env:VAR or --header NAME --header-secret env:VAR |
| A Redis database the agent should query | A Context Surface Upstream: --kind context-surface --surface-database-id ID --data-model FILE (self-managed, the ID can be left out and defaults from the deployment config. In Redis Cloud the console's Create Context Surface asks for the database). Write the data model first: see "Write the data model" | |
| How many tools does the agent see in total? | Tens (default) | --tool-exposure-mode full: every tool, namespaced |
| Hundreds, or schemas crowd the prompt | --tool-exposure-mode search: the model sees search_tools and run_tool | |
| Which read-only tools return the same answer for the same arguments? | Lookups, searches, reference data | A Gateway with a database and cache enabled (see "Gateway switches and database"), then policy set UPSTREAM TOOL --cache-ttl DURATION per tool |
| None, or not sure | No cache policies | |
| Which tools return results too large to read whole? | Exports, list endpoints, logs | A Gateway with a noeviction database and compression enabled (see "Gateway switches and database"). Then the deployment's handle defaults, or --compression-ttl DURATION per tool, and --compression-capture never for a tool that must always return whole |
| Does the framework connect to MCP servers itself? | Yes: Claude Code, Cursor, ADK, OpenAI Agents SDK, LangChain MCP adapters | Point its MCP client at the Gateway URL and key |
| No, or the agent wants plain function tools | The SDK: gateway.model_tools() or gateway.hooks() |
2. Get current facts from MCP
Do this before writing code. Never guess framework APIs from memory.
- Framework docs: query an available docs MCP (Context7, or the framework's own) for the installed version's MCP client: how it takes a streamable HTTP URL and an
Authorizationheader. - Redis docs: if the Redis docs MCP is connected, use it for Context Gateway concepts (Upstreams, exposure modes, cache, handles).
- No MCP available: read the installed package source in the virtualenv. Say so in the report.
3. Provision
Install the CLI
pip install redis-iris
It needs Python 3.11 or later. On LangGraph, install redis-iris[langgraph]. If tools call crm__lookup_customer fails with tool_not_found on a search-mode Gateway while a Gateway ID is set (--gateway-id or IRIS_CONTEXT_GATEWAY_ID), the installed release splits tool names at the first .: upgrade redis-iris, or run tools call with IRIS_CONTEXT_GATEWAY_URL set and no Gateway ID, so the CLI resolves the name through search_tools.
In Redis Cloud
Redis Cloud has no API for Gateway setup yet. The control plane takes only the console session, so a cloud profile refuses service, upstream and policy commands with cloud_unsupported, and the agent can't provision. The developer does it in the console:
- Iris → Context Gateway: create the Gateway, or open an existing one.
- On the Gateway, Upstreams: Add MCP server for each HTTP server from question 1, or Create Context Surface for a Redis database (write the data model first: see "Write the data model").
- On the connect step, copy the MCP endpoint and create an agent key.
- Put them in
IRIS_CONTEXT_GATEWAY_URLandIRIS_CONTEXT_GATEWAY_API_KEYin the agent's environment or its.env. The developer sets them, never the agent: don't ask for the key in chat, and never echo,catorgrepthe env file.
Add both names to .env.example with empty values, and never commit .env. Then skip the rest of this step and go to step 4. Cloud keeps cache and compression off, so there are no policies or handles to set up.
Self-managed
Let the person export IRIS_CONTEXT_GATEWAY_SERVICE_URL, IRIS_CONTEXT_GATEWAY_SERVICE_TOKEN, IRIS_IDENTITY_URL and IRIS_IDENTITY_TOKEN. For the local Compose stack, --local after the command (service list --local) supplies the service URL and token, but not the Identity URL, so IRIS_IDENTITY_URL is still needed for create.
Identity tokens. Each product chart can run its own Identity Service. Then set the product's own IRIS_CONTEXT_GATEWAY_IDENTITY_TOKEN (and IRIS_CONTEXT_GATEWAY_IDENTITY_URL, or the profile's identity_url), and leave IRIS_IDENTITY_URL unset: it overrides every product's identity_url in the profile. On a suite with one shared Identity Service, IRIS_IDENTITY_TOKEN and IRIS_IDENTITY_URL cover every product. The CLI takes the token from --identity-token, then IRIS_CONTEXT_GATEWAY_IDENTITY_TOKEN, then IRIS_IDENTITY_TOKEN, then the product's service token, which only works when the two are the same secret.
iris --json --no-input context-gateway service list
iris --json --no-input context-gateway service create --name support --tool-exposure-mode full --write-env .env
set -a; . ./.env; set +a; iris --json --no-input context-gateway service check <gatewayId from the result>
The Gateway CLI reads IRIS_CONTEXT_GATEWAY_API_KEY from the environment only, never from .env, so service check, tools and handle fail with configuration_error until the key is in the command's environment. Load the file inside the same shell command, as above (or uv run --env-file .env iris ...), so the key never reaches your output.
Gateway switches and database
Each Gateway has two switches, cache and compression, each ENABLED or DISABLED. service show gw_0123456789abcdef0123456789abcdef prints both. A --cache-ttl policy is refused with a 400 while the Gateway's cache is DISABLED, so enable cache first. A compression policy is stored but does nothing while compression is off. Both features also need the operator's deployment switches (cache.enabled and compression.enabled in the data plane config, off by default).
| Create | cache and compression |
|---|---|
With a database: --database-id ID, or the on-prem default defaultDatabases.gatewayDb | Both ENABLED |
| Without one | Both DISABLED. Sending ENABLED with no database is a 400 on that field |
With a database that can't take compression (below, or unreachable), and compression omitted | cache ENABLED, compression stored DISABLED |
| Feature | Works only when |
|---|---|
| Caching | Gateway cache is ENABLED, the tool is cacheable (not marked readOnlyHint: false or destructiveHint: true; a tool with no hints counts, so opt in only tools you know are read-only), and it has a --cache-ttl policy |
| Compression | Gateway compression is ENABLED, and the tool's policy is not --compression-capture never |
service create and service update accept --cache enabled|disabled and --compression enabled|disabled in either case and send uppercase API values. Omitted flags leave the server defaults or existing values unchanged. Adding a database later does not enable either feature, so enable them explicitly:
iris --json --no-input context-gateway service update gw_0123456789abcdef0123456789abcdef --database-id gateway-db --cache enabled --compression enabled
To detach the database, pass --clear-database --cache disabled --compression disabled in one update. A stale --expected-revision returns 412 revision_conflict. Read the resource again before deciding whether to retry.
Database requirements:
- Compression needs a Gateway database with
maxmemory-policy noevictionand the JSON module. Enabling it (create, update, or adatabaseIdchange while it stays enabled) on another policy is a 400 with anerrors[]entry pointing to/compression, with a reason such asdatabase lru-db uses allkeys-lru, compression needs noeviction. When you sendcompression enabled, an unreachable database is a 503: retry. A create that omitscompressionon an unsuitable or unreachable database is not refused: it storescompressionDISABLED, so read it back withservice show. - Cache only works on any Gateway database. Handles and cache entries share it, so give it room under
maxmemory. On a cache-only database with an evicting policy, Redis evicts entries and the share limits below don't apply. - Each side has its own share of
maxmemory. Handles stop atcompression.max_memory_share(default 0.5) and the cache atcache.max_memory_share(default 0.4), at most 0.95 together, so neither takes the other's room. Both also stop above 90% ofmaxmemory, but that's only a backstop for what the shares miss. - When the handle share is full, an automatic capture returns the result whole, and an explicit
run_tool_as_handleanswers a tool error:handle storage is full, release handles or narrow the query. A full cache share just skips the write and returns the result. Releasing handles frees their share at once, and expired ones free it within about 10 s. - Size the Gateway database for its TTLs. Handles and cache entries stay until their TTL ends (15 minutes for handles by default), so each share needs room for
creates per second x TTL x stored size. Don't estimate the stored size from the result size. Measure it:service diagnose --metrics-urlshows the cache and compression counters, and a risingskipped_low_memoryorout_of_memoryoutcome means that side is full. Lower--compression-ttlor--cache-ttl, or raisemaxmemoryor the operator's share, if it doesn't fit. - On-prem, the databases come from the deployment config, never from the CLI:
shared.config.databasesregisters each ID,databaseConnectionssupplies the URLs, anddefaultDatabases.gatewayDbanddefaultDatabases.surfaceDbpick the defaults. Ask the operator when an ID is missing. With compression on, keep the Gateway database and the Surface database on separate Redis instances:noevictionwould otherwise apply to customer data.
service diagnose gw_0123456789abcdef0123456789abcdef reports the Gateway's database (no Gateway database means every call bypasses the cache), each Upstream's catalog and the cache policies. Add --metrics-url for the cache and compression counters, where a rising eviction_policy, skipped_low_memory or out_of_memory outcome means handles are not being held.
Run the CLI as an agent
- Always put
--json --no-inputbefore the product name. You get one JSON value on stdout, errors as one JSON value on stderr, and no prompts. - Read before you write.
service createhas no--dry-run, so runservice listfirst: it proves the service URL and token, and aconfiguration_error,unauthenticatedorforbiddenthere means hand off (step 4) before anything is created. - Always deliver keys with
--write-env PATH(or--write PATHonservice key). Never runcreateorkeywith--jsonand no file: that prints the key into your transcript. - On
configuration_error,unauthenticatedorforbidden, stop. Tell the person which variable to set (the message, ordetails.env) and linkdetails.obtainwhen present. Never ask for a secret in chat, pass one on the command line, or echo,catorgrepan env file. - Upstream secrets go in by reference only:
env:NAME,file:PATHor-. Ask the person to export the variable; never write the secret yourself. - On
resource_exists, ask whether to--reusedetails.gatewayIdor pick another name. Never delete to retry. Ifcreatefails withdetails.created: true, re-run with--reuse --write-env.
Every error is one JSON value on stderr with error.code; credential errors also carry details.env and details.obtain, never the value. Stop on the first failure and relay it:
error.code | Who acts | Action |
|---|---|---|
credential_required | person | Set the variable in details.env in the agent's shell. See "Identity tokens" above. |
unauthenticated | person | The token from details.source was refused: it expired or belongs to another deployment. Issue a new one. |
forbidden | person | The token is valid but lacks the role or tenant. Use a token with the right role for that product. |
configuration_error | person or agent | A URL is missing, such as url. Add it to the profile. |
invalid_request | agent | HTTP 400. Fix the request the message names. |
resource_exists | person | Decide: re-run with --reuse to adopt details.gatewayId, or choose another name. |
unavailable | person | The hop is unreachable. Check the URL, the ingress and the service's pods. |
check_failed | person | details.hop and details.cause name the broken hop. Fix it and re-run the check. |
cloud_unsupported | person | The profile is kind = "cloud". Follow details.steps in the console. |
input_error, usage_error | agent | Fix the command. A destructive command under --no-input needs --yes from a person. |
--write-env .env writes IRIS_CONTEXT_GATEWAY_URL (the Gateway's MCP endpoint), IRIS_CONTEXT_GATEWAY_ID and IRIS_CONTEXT_GATEWAY_API_KEY. Add the same three names to .env.example with empty values. Never commit .env.
Add the Upstreams (question 1). IRIS_CONTEXT_GATEWAY_ID is not a secret; pass --gateway-id with the gatewayId create printed (gw_ and 32 hex characters):
iris --json --no-input context-gateway --gateway-id gw_0123456789abcdef0123456789abcdef upstream add --name CRM --endpoint https://crm.example.com/mcp --bearer-secret env:CRM_TOKEN
iris --json --no-input context-gateway --gateway-id gw_0123456789abcdef0123456789abcdef upstream add --name Tickets --endpoint https://tickets.example.com/mcp --header X-Api-Key --header-secret env:TICKETS_KEY
iris --json --no-input context-gateway --gateway-id gw_0123456789abcdef0123456789abcdef upstream tools crm
An upstream add that prints initialCatalogFailure still created the Upstream: 422 means the Upstream refused the credentials (fix with upstream update, then upstream refresh), 424 means discovery failed (check the control plane can reach the endpoint), 408 means it timed out. For a --kind context-surface Upstream, 422 means the Surface database refused the control plane's login and 424 means tool generation failed (for example a Redis NOPERM): both are fixed in the deployment config, then upstream refresh.
Write the data model
A Context Surface Upstream takes a data model: JSON that says which records the agent can read and how they are indexed. The Gateway generates the tools from it, so write it before upstream add --kind context-surface.
| Level | Members |
|---|---|
| Model | title, description, entities[] |
| Entity | name, description, redis_key_template (for example order:{id}, default <Name>:{id}), relationships[], fields[], optional document_type |
| Field | name, type, description, is_key_component, redis_indices[], optional allowed_values for a tag |
| Relationship | name, target (an entity), description, source_field (a field of this entity) |
Rules:
- Every
{placeholder}in the key template is a field withis_key_component: true. A key field takes no index. The text before the first placeholder (order:) must be a literal that ends in:,_,-,/or.: it is the index prefix. - Field
typeisstr,int,float,bool,list[str],list[float],geo,timestamporobject.objectcan't be indexed. - Records are stored as RedisJSON by default.
"document_type": "hash"stores an entity as a Hash: noobjectfields,list[float]only with a vector index, and key fields must be scalar. - The Gateway refuses
acl_field_mappingswith a 400, so leave it out: every agent key with access to the Gateway reads every record of the Surface. The example file in the iris repo carries it, so don't copy that one.
Pick an index by how the agent asks:
| Index | Field types | Use it for | Generated tool |
|---|---|---|---|
tag | str, bool, list[str] | Exact filters: status, IDs, categories. allowed_values lists the legal values | filter_<entity> |
text | str | Full text over names, descriptions, notes. weight ranks fields, no_stem turns stemming off | search_<entity>_by_text |
numeric | int, float, timestamp | Ranges and sorting. Sortable unless you set sortable: false; list_ needs a sortable field, and a timestamp can't be sortable: false | filter_<entity>, list_<entity>, summarize_<entity> |
vector | list[float] | Meaning-based search. Needs vector_dim and distance_metric (cosine, euclidean, dot_product), and an embedding service on the data plane. Redis Cloud can't embed queries yet, so these tools fail there today | search_<entity>_by_<field>_semantic when source_field names the text field it embeds, else _similarity. Also search_<entity>_by_<field>_hybrid (text and vector ranked together) when that text field has a text index and the Surface Redis supports native hybrid search over RESP3 |
geo | geo | Records within a radius | find_<entity>_near_<field> |
An entity whose key template has one placeholder also gets get_<entity>_by_id, and count_<entity> counts records. Across entities, the set tools intersect_results, union_results, except_results and expand_results combine filters and follow relationships. Each tool is read-only and appears as <namespace>__<tool>, for example orders__search_order_by_text. A tool only exists when the model has the field it needs: no text index, no text search.
A minimal orders model:
{
"title": "Orders",
"description": "Customer orders",
"entities": [
{
"name": "Order",
"description": "A customer order",
"redis_key_template": "order:{id}",
"relationships": [],
"fields": [
{"name": "id", "type": "str", "description": "Order ID", "is_key_component": true, "redis_indices": []},
{"name": "status", "type": "str", "description": "open, shipped or cancelled", "redis_indices": [{"type": "tag"}]},
{"name": "notes", "type": "str", "description": "Free-text order notes", "redis_indices": [{"type": "text", "weight": 1.0}]},
{"name": "total", "type": "float", "description": "Order total in dollars", "redis_indices": [{"type": "numeric", "sortable": true}]}
]
}
]
}
Save it as orders.json, and add it with --surface-database-id ID --data-model orders.json. Records live at order:<id> as JSON documents with those field names. After upstream add, upstream tools orders lists what was generated. Search results can be partial until Redis finishes indexing existing keys (FT.INFO <index>, percent_indexed reaches 1). A data model change (upstream update) reindexes, so do it when the database has room.
Set policies (questions 3 and 4). upstream tools says which tools are cacheable; only those take --cache-ttl:
iris --json --no-input context-gateway --gateway-id gw_0123456789abcdef0123456789abcdef policy set crm search_customers --cache-ttl 5m
iris --json --no-input context-gateway --gateway-id gw_0123456789abcdef0123456789abcdef policy set crm export_orders --compression-ttl 1h
Rotate keys and secrets
- New agent key:
service key gw_0123456789abcdef0123456789abcdef --write .envmints one and upserts it into the file. Restart the agent so it reads the new value. - A second developer on the same Gateway:
service env gw_0123456789abcdef0123456789abcdef --write .envwrites the URL and ID without minting a key, thenservice keygives them their own. - New Upstream secret: have the person export the new value, then
upstream update crm --bearer-secret env:CRM_TOKEN(or--header-secret) andupstream refresh crm.
Full reference: iris context-gateway <command> --help (for example iris context-gateway upstream add --help).
4. Repoint the agent
MCP-native frameworks (question 5, yes). Replace each server entry with one Gateway entry. connect writes it with the key read from a variable at run time, never printed. In Redis Cloud, run connect with a cloud profile and no --gateway-id: it takes the endpoint from IRIS_CONTEXT_GATEWAY_URL.
iris context-gateway --gateway-id gw_0123456789abcdef0123456789abcdef connect --client claude-code --write .mcp.json
iris context-gateway --gateway-id gw_0123456789abcdef0123456789abcdef connect --client cursor --write .cursor/mcp.json
iris context-gateway --gateway-id gw_0123456789abcdef0123456789abcdef connect --client adk > gateway_tools.py
iris context-gateway --gateway-id gw_0123456789abcdef0123456789abcdef connect --client json
For another MCP client (OpenAI Agents SDK, LangChain MCP adapters), use the json output: the URL, streamable-http, and an Authorization: Bearer header with the value of IRIS_CONTEXT_GATEWAY_API_KEY. Confirm the client's parameter names in step 2. A .env file doesn't reach a running client: the variable must be in the environment the client starts in.
SDK tools (question 5, no):
from redis_iris import ContextGateway
gateway = ContextGateway.from_env()
tools = gateway.model_tools() # framework-free Tool objects, one per Gateway tool
TypeScript agents: see the @redis-iris/context-gateway API.
On LangGraph, langchain_tools(gateway.hooks()) from redis_iris.integrations.langgraph gives LangChain tools for a ToolNode. With Agent Memory or Agent Playbooks also wired, stack them so the model gets every product's tools from one place:
from redis_iris.hooks import stack
from redis_iris.integrations.langgraph import attach, langchain_tools
hooks = stack(memory.hooks(), playbook.disclosure(), gateway.hooks())
graph.add_node("tools", ToolNode(langchain_tools(hooks)))
app = attach(hooks, graph).compile()
gateway.call("crm__lookup_customer", id="cust-1042") calls a tool directly (result.data, result.is_error), and gateway.call_as_handle(...) keeps a large result on the Gateway as a Handle. The reads are on the gateway: gateway.page(handle, limit=5), gateway.query(handle, ...), count, aggregate, describe and gateway.release(handle). Async twins: atools, acall, apage and so on.
Then remove what the Gateway replaced: each per-server MCP entry, each per-server secret from the agent's environment (the Gateway holds them now), and any hand-written namespacing or tool filtering.
Tool names change to namespace__tool, for example crm__lookup_customer. Update any prompt text or code that names a tool.
In search mode the model never sees those names in tools/list. It calls search_tools with a query (and an optional limit, default 5), gets back full MCP tool definitions, and runs one with run_tool: tool holds the upstreamId and originalName from the match, and toolArgs the arguments. Prompt text should describe the task a tool does, not name the tool. Code that calls a tool by name keeps working through the SDK and CLI, which resolve namespace__tool for you.
Handles from the agent's side
With Gateway compression enabled, tools/list adds seven tools, in both exposure modes: run_tool_as_handle, describe_result, count_results, page_results, query_results, aggregate_results and release_results. A tool result over 32 KiB then comes back as a small stub holding a handle (h_ and 32 hex digits) with a preview and the next step, in place of the full payload. In full mode, a tool that declares an outputSchema returns whole. A tool with --compression-capture never returns whole, and run_tool_as_handle on it is a tool error telling the agent to call the tool directly. Tell the agent in its prompt to read a handle with the tools below and never to ask for the whole payload again.
| The agent wants | Call |
|---|---|
| A big result held for several follow-ups, whatever its size | run_tool_as_handle (full mode name and toolArgs, search mode tool with upstreamId and originalName, and toolArgs) |
| The shape, size, fields and expiry of a handle | describe_result |
| How many items, or how many per value of one field | count_results with jsonpath or groupBy |
| A sum, min, max or average of numbers | aggregate_results with a jsonpath |
| Only the items that match a condition | query_results with a JSONPath such as $[[email protected]=="open"], paged |
| To read items in order, or a text result by bytes | page_results with offset and limit |
| To free the memory early | release_results |
When run_tool_as_handle answers handle storage is full, release handles or narrow the query, the agent should call release_results on handles it no longer needs, then retry with a narrower query or call the tool directly.
A handle expires after the deployment's TTL (15 minutes by default) or the tool's --compression-ttl, and a read never extends it. An expired, released or another key's handle is a Gateway refusal with reason unknown_handle, which the CLI's handle commands report as handle_not_found (exit 1): call the tool again for a new one. A partial handle (one page of a longer answer) says so in its completeness and warning. To fetch the next page, call run_tool_as_handle again with the paging arguments. A Context Surface capture reads every page itself, up to the deployment limits (10,000 rows), so one large Surface query can fill handle storage (known issue MOD-20735). With compression DISABLED, the seven tools are gone from tools/list. From the SDK, gateway.call_as_handle(...) returns a Handle, and redis_context_gateway.mcp.HANDLE_TOOLS lists the seven names.
5. Verify
Checks 1, 3, 4, 6 and 7 need the agent key in the command's environment: run them as set -a; . ./.env; set +a; iris ... in one shell command, never by printing the file.
In Redis Cloud, run 3, 4, 8 and 9 without --gateway-id: the tools commands use IRIS_CONTEXT_GATEWAY_URL and the agent key. The other checks call the control plane or need cache and handles, which cloud doesn't offer.
iris context-gateway service check gw_0123456789abcdef0123456789abcdefpasses every step: token, Gateway, Identity Service, agent key, and onetools/listwith the agent key.iris --json context-gateway service show gw_0123456789abcdef0123456789abcdefshowscacheandcompressionas the questions need:ENABLEDfor each feature in use, anddatabaseBindingpresent. ADISABLEDthe agent didn't choose means the database was missing or refused compression (step 3, "Gateway switches and database").iris context-gateway --gateway-id gw_0123456789abcdef0123456789abcdef tools listshows every Upstream's tools namespaced (full mode), orsearch_toolsandrun_tool(search mode). With compression enabled it also lists the seven handle tools.iris context-gateway --gateway-id gw_0123456789abcdef0123456789abcdef tools call crm__lookup_customer --arg id=42returns what the server returned directly.iris context-gateway --gateway-id gw_0123456789abcdef0123456789abcdef policy list crmshows the cache and compression policies from questions 3 and 4.- Cache hit: with a cache-policy tool, run the same
iris --json context-gateway --gateway-id gw_0123456789abcdef0123456789abcdef tools call crm__search_customers --arg query=acmetwice. The second result's_meta["com.redis.context-gateway/invocation"]carries"cache": {"status": "hit", ...}, and the first has nocacheentry. - Handle round trip:
tools call crm__export_orders --as-handleprints a stub withh_..., thenhandle describe h_...,handle page h_... --limit 5andhandle release h_...each answer. A secondhandle describeon the released handle exits 1 withhandle_not_found. - Run the agent once on a task that uses a Gateway tool, and show the call went through the Gateway (the namespaced name in the trace or log).
- Unset
IRIS_CONTEXT_GATEWAY_API_KEYand confirm the SDK raisesConfigurationErrornaming it, or the MCP client reports an auth failure.
iris context-gateway service diagnose gw_0123456789abcdef0123456789abcdef explains a failing Upstream, cache or handle setup, and a missing Gateway database.
6. Report
List where the Gateway runs (Redis Cloud or self-managed), the five answers, each Upstream added (name, namespace, endpoint, secret variable name, never the value), the exposure mode, each policy set, the Gateway cache and compression values and the database behind them, the data model written, files changed, the client configuration written, env vars added and removed, verification output, and anything you could not confirm over MCP.