diff --git a/content/develop/ai/_index.md b/content/develop/ai/_index.md index f4ece77378..98cd9b06de 100644 --- a/content/develop/ai/_index.md +++ b/content/develop/ai/_index.md @@ -26,7 +26,7 @@ Redis is an in-memory data platform purpose-built for the speed and structure th - **Vector search**: Store and query vector embeddings using KNN and range queries with metadata filters, across hashes and JSON documents - **Semantic caching**: Reduce LLM API costs by reusing cached responses for semantically similar prompts with LangCache - **Agent memory**: Give agents short-term session memory and long-term persistent memory that survives across interactions -- **Structured data access**: Turn your business data into governed tools agents can reliably query with Context Retriever +- **Tool and data access**: Give agents one endpoint for the tools of your MCP servers and your business data with Context Gateway - **Live data sync**: Stream changes from relational databases into Redis in near real time so agents always work with current information ## Why use Redis for AI and search? @@ -70,7 +70,7 @@ The [Context Engine]({{< relref "/develop/ai/context-engine" >}}) (Redis Iris) i - [LangCache]({{< relref "/develop/ai/context-engine/langcache" >}}): Semantic caching that reduces LLM API costs and improves response times by reusing cached responses for similar queries. - [Redis Agent Memory]({{< relref "/develop/ai/context-engine/agent-memory" >}}): Two-tier persistent memory (session and long-term) for agents, available through Python and TypeScript SDKs and a REST API. -- [Context Retriever]({{< relref "/develop/ai/context-engine/context-retriever" >}}): Turns your business data into structured, governed tools that agents can reliably use, defined once and reused across all agents. +- [Context Gateway]({{< relref "/develop/ai/context-engine/context-gateway" >}}): Gives agents one Model Context Protocol (MCP) endpoint for the tools of any MCP server, and serves your Redis data as tools with Context Surfaces. - [Data Integration]({{< relref "/develop/ai/context-engine/data-integration" >}}): Keeps your Redis Cloud database in sync with relational databases in near real time using Change Data Capture. ## How to's @@ -82,7 +82,7 @@ The [Context Engine]({{< relref "/develop/ai/context-engine" >}}) (Redis Iris) i 1. [**Build an AI agent**]({{< relref "/develop/ai/agent-builder" >}}): Use the interactive agent builder to generate complete working code for conversational assistants and recommendation engines. 1. [**Add semantic caching**]({{< relref "/develop/ai/context-engine/langcache" >}}): Reduce LLM API calls by caching and reusing responses for semantically similar queries. 1. [**Add agent memory**]({{< relref "/develop/ai/context-engine/agent-memory" >}}): Give your agent persistent session and long-term memory using the Agent Memory REST API. -1. [**Access structured business data**]({{< relref "/develop/ai/context-engine/context-retriever" >}}): Use Context Retriever to define your business data as governed tools that any agent can query reliably. +1. [**Connect tools and business data**]({{< relref "/develop/ai/context-engine/context-gateway" >}}): Use Context Gateway to give any agent one endpoint for your MCP servers and your Redis data. 1. [**Sync live data to Redis**]({{< relref "/develop/ai/context-engine/data-integration" >}}): Use Data Integration to keep your Redis Cloud database in sync with your primary relational database using Change Data Capture. #### Learn how to index and query vector embeddings @@ -139,7 +139,7 @@ The Context Engine provides managed services for agent memory and data access. * [Get started with LangCache]({{< relref "/develop/ai/context-engine/langcache" >}}): Add semantic caching to reduce LLM costs in minutes. * [Get started with Agent Memory]({{< relref "/develop/ai/context-engine/agent-memory" >}}): Add persistent two-tier memory to any agent using the REST API. -* [Get started with Context Retriever]({{< relref "/develop/ai/context-engine/context-retriever" >}}): Expose your business data as governed tools that agents can reliably query. +* [Get started with Context Gateway]({{< relref "/develop/ai/context-engine/context-gateway" >}}): Give agents one MCP endpoint for the tools of your MCP servers and your Redis data. * [Get started with Data Integration]({{< relref "/develop/ai/context-engine/data-integration" >}}): Keep Redis in sync with your primary database so agents always have fresh data. ## Tutorials diff --git a/content/develop/ai/agent-builder/agent-concepts.md b/content/develop/ai/agent-builder/agent-concepts.md index 164aea2b27..80ba56dc53 100644 --- a/content/develop/ai/agent-builder/agent-concepts.md +++ b/content/develop/ai/agent-builder/agent-concepts.md @@ -90,10 +90,10 @@ You can build each part of an agent's context layer yourself from Redis data str |:--|:--|:--| | Remember the user across sessions | Streams, Hashes, and vector search for session and long-term memory | [Agent Memory](/content/develop/ai/context-engine/agent-memory/_index.md) stores session events, summarizes long sessions, and extracts long-term memories in the background. | | Avoid repeat LLM calls | Vector search plus your own cache logic | [LangCache](/content/develop/ai/context-engine/langcache/_index.md) returns a cached response when a new prompt is semantically similar to a cached one. | -| Query business data safely | Hand-written tools or generated queries per agent | [Context Retriever](/content/develop/ai/context-engine/context-retriever/_index.md) generates tools from a data model you define once. Agents call them over MCP (Model Context Protocol), and agent keys limit what each agent can reach. | +| Query business data safely | Hand-written tools or generated queries per agent | [Context Gateway](/content/develop/ai/context-engine/context-gateway/_index.md) generates tools from a data model you define once. Agents call them over MCP (Model Context Protocol), and agent keys limit what each agent can reach. | | Keep that data current | Your own sync jobs from the source database | [Data Integration](/content/develop/ai/context-engine/data-integration/_index.md) streams changes from relational databases into Redis within seconds. | -LangCache, Agent Memory, and Context Retriever are currently in preview. To see where each service fits in a single request, see [how a request flows through Redis Iris](/content/develop/ai/context-engine/concepts/request-flow.md). +LangCache and Agent Memory are currently in preview. To see where each service fits in a single request, see [how a request flows through Redis Iris](/content/develop/ai/context-engine/concepts/request-flow.md). ## Types of agents you can build @@ -327,7 +327,7 @@ Production-ready agents include built-in reliability features: - Data encryption: Encrypt sensitive data at rest and in transit - Access controls: Implement proper authentication and authorization -- Governed data access: Give agents defined tools instead of direct database access. [Context Retriever](/content/develop/ai/context-engine/context-retriever/concepts.md) generates these tools and uses access tags to filter what each agent can see +- Governed data access: Give agents defined tools instead of direct database access. [Context Gateway](/content/develop/ai/context-engine/context-gateway/concepts.md) serves these tools through one MCP endpoint, so agents never hold database or Upstream credentials. - Data retention: Automatic cleanup of personal data per regulations - Sensitive data in memory: Keep information such as payment card numbers out of long-term memory. Agent Memory [sensitive-data exclusions](/content/operate/iris/agent-memory/create-service.md#sensitive-data-exclusions) guide automatic extraction away from it - Audit logging: Track all data access and modifications @@ -377,7 +377,7 @@ Ready to build your AI agent with Redis? **Learn more:** - [Redis Iris Context Engine — Agent Memory](/content/develop/ai/context-engine/agent-memory/_index.md) for managed session and long-term agent memory - [Redis Iris concepts](/content/develop/ai/context-engine/concepts/_index.md) for how memory, caching, and governed data access work together -- Interactive demos for [Agent Memory](/content/develop/ai/context-engine/agent-memory/interactive-demo.md), [LangCache](/content/develop/ai/context-engine/langcache/interactive-demo.md), and [Context Retriever](/content/develop/ai/context-engine/context-retriever/interactive-demo.md) that run in your browser +- Interactive demos for [Agent Memory](/content/develop/ai/context-engine/agent-memory/interactive-demo.md), [LangCache](/content/develop/ai/context-engine/langcache/interactive-demo.md) that run in your browser - [Redis Vector Search documentation](/content/develop/ai/search-and-query/vectors/_index.md) - [RedisVL Python library](/content/develop/clients/redis-vl.md) for vector operations and AI workflows - [Redis data structures guide](/content/develop/data-types/_index.md) diff --git a/content/develop/ai/context-engine/_index.md b/content/develop/ai/context-engine/_index.md index 635742a8a4..7b10031da0 100644 --- a/content/develop/ai/context-engine/_index.md +++ b/content/develop/ai/context-engine/_index.md @@ -19,7 +19,7 @@ Redis Iris eliminates the infrastructure burden of building context-aware AI age
{{< tile-card icon="images/dev/icons/icon-redis-iris-64-duotone.png" title="Concepts" description="What happens when an agent asks Redis Iris for context" url="/develop/ai/context-engine/concepts" >}} {{< tile-card icon="images/dev/icons/icon-redis-agent-memory-64-duotone.png" title="Agent Memory" description="Persistent short-term and long-term memory across agent interactions" url="/develop/ai/context-engine/agent-memory" >}} - {{< tile-card icon="images/dev/icons/icon-redis-context-retriever-64-duotone.png" title="Context Retriever" description="Governed, schema-first data access tools for agents" url="/develop/ai/context-engine/context-retriever" >}} + {{< tile-card icon="images/dev/icons/icon-redis-iris-64-duotone.png" title="Context Gateway" description="One MCP endpoint for the tools of any MCP server and your Redis data" url="/develop/ai/context-engine/context-gateway" >}} {{< tile-card icon="images/dev/icons/icon-redis-iris-64-duotone.png" title="Agent Playbooks" description="Publish reusable answers and procedures for agents" url="/develop/ai/context-engine/agent-playbook" >}}
@@ -29,7 +29,7 @@ Redis Iris is a production-ready context engine for AI agents that: - **Reduces LLM costs**: Semantic caching returns cached responses for similar queries in milliseconds - **Adds persistent memory**: Agents remember past interactions and user preferences across sessions -- **Structures business data access**: Context Retriever generates governed tools agents can safely call at runtime +- **Connects tools and business data**: Context Gateway gives agents one MCP endpoint for the tools of any MCP server and your Redis data - **Keeps data fresh**: Data Integration streams live changes from relational databases into Redis within seconds - **Deploys your way**: Check each service’s operator guide for its managed and self-managed deployment options. diff --git a/content/develop/ai/context-engine/agent-memory/interactive-demo.md b/content/develop/ai/context-engine/agent-memory/interactive-demo.md index e24a472693..3493d28a85 100644 --- a/content/develop/ai/context-engine/agent-memory/interactive-demo.md +++ b/content/develop/ai/context-engine/agent-memory/interactive-demo.md @@ -13,7 +13,7 @@ weight: 9 Redis Agent Memory gives an AI agent two tiers of memory. Session memory holds the current conversation, and long-term memory holds what the agent should remember across conversations. This demo follows one customer of a food delivery app through two conversations. You choose what she says, and the demo shows what each tier stores, when it changes, and how long it lasts. -The demo runs in your browser and doesn't connect to an Agent Memory service. Its requests and responses follow the shapes of the [Agent Memory API](/content/develop/ai/context-engine/agent-memory/api-reference.md). The customer, restaurants, and order come from the same sample data as the [Context Retriever interactive demo](/content/develop/ai/context-engine/context-retriever/interactive-demo.md). +The demo runs in your browser and doesn't connect to an Agent Memory service. Its requests and responses follow the shapes of the [Agent Memory API](/content/develop/ai/context-engine/agent-memory/api-reference.md). The customer, restaurants, and order come from the same sample data as the [Redis Iris interactive demo](/content/develop/ai/context-engine/concepts/interactive-demo.md). {{< agent-memory-demo >}} diff --git a/content/develop/ai/context-engine/concepts/_index.md b/content/develop/ai/context-engine/concepts/_index.md index 585ac04de2..4f4374b67b 100644 --- a/content/develop/ai/context-engine/concepts/_index.md +++ b/content/develop/ai/context-engine/concepts/_index.md @@ -79,12 +79,12 @@ Run all three fully managed on Redis Cloud, or self-managed on your own infrastr - Let Iris decide what's cached and what's fresh. Don't build a separate caching layer on top of the model call. - Write session events as they happen. Let background extraction decide what's durable, rather than deciding yourself on every turn. -- Model your business data as entities and relationships once. Let Context Retriever generate the tools your agent calls, instead of writing a tool per query. +- Model your business data as entities and relationships once. Let a Context Gateway Context Surface generate the tools your agent calls, instead of writing a tool per query. - Scope every write and lookup by user, session, or namespace. This keeps agents and users from colliding. ## Next steps - [How a request flows through Redis Iris]({{< relref "/develop/ai/context-engine/concepts/request-flow" >}}) - [Agent Memory overview]({{< relref "/develop/ai/context-engine/agent-memory/overview" >}}) -- [Context Retriever concepts]({{< relref "/develop/ai/context-engine/context-retriever/concepts" >}}) -- Interactive demos for [Agent Memory]({{< relref "/develop/ai/context-engine/agent-memory/interactive-demo" >}}) and [Context Retriever]({{< relref "/develop/ai/context-engine/context-retriever/interactive-demo" >}}) that run in your browser +- [Context Gateway concepts]({{< relref "/develop/ai/context-engine/context-gateway/concepts" >}}) +- An [interactive demo]({{< relref "/develop/ai/context-engine/agent-memory/interactive-demo" >}}) of Agent Memory that runs in your browser diff --git a/content/develop/ai/context-engine/concepts/interactive-demo.md b/content/develop/ai/context-engine/concepts/interactive-demo.md index f96414e86c..824fcb3609 100644 --- a/content/develop/ai/context-engine/concepts/interactive-demo.md +++ b/content/develop/ai/context-engine/concepts/interactive-demo.md @@ -25,7 +25,7 @@ The tour runs in your browser and doesn't connect to any service. Each step link | Service | What it does in this request | Without it | |:--|:--|:--| | [Data Integration](/content/develop/ai/context-engine/data-integration/_index.md) | Syncs a change from PostgreSQL to Redis seconds after it happens | The agent reads last night's data and makes a promise it can't keep | -| [Context Retriever](/content/develop/ai/context-engine/context-retriever/_index.md) | Looks up delivery times, alternatives, and past orders through governed tools | The agent can't check whether the food will arrive in time | +| Context Retriever | Looks up delivery times, alternatives, and past orders through governed tools | The agent can't check whether the food will arrive in time | | [Agent Memory](/content/develop/ai/context-engine/agent-memory/_index.md) | Recalls last week's order and the customer's peanut allergy | The agent can find the order, but not the allergy | | [LangCache](/content/develop/ai/context-engine/langcache/_index.md) | Answers the general delivery fee question from the cache | The model answers it, which takes longer and costs output tokens | diff --git a/content/develop/ai/context-engine/concepts/request-flow.md b/content/develop/ai/context-engine/concepts/request-flow.md index 00d1d43229..2d85fda2cd 100644 --- a/content/develop/ai/context-engine/concepts/request-flow.md +++ b/content/develop/ai/context-engine/concepts/request-flow.md @@ -14,9 +14,9 @@ bannerText: Agent Memory and Context Retriever are currently available in previe [How Redis Iris works]({{< relref "/develop/ai/context-engine/concepts" >}}) covers the mental model. This page traces the same idea through an actual request, capability by capability. -Every capability in Iris manages the same limited resource: the model's context window. Semantic caching skips a model call when a similar one already ran. Agent Memory decides what's worth keeping from a conversation. Context Retriever returns exactly the data a tool call needs, instead of a raw query result. Treat "what goes into the next model call" as a budget. Manage it actively. It doesn't take care of itself once a request reaches Iris. +Every capability in Iris manages the same limited resource: the model's context window. Semantic caching skips a model call when a similar one already ran. Agent Memory decides what's worth keeping from a conversation. Context Gateway returns the result of one tool call instead of a raw query result, and holds large results as handles. Treat "what goes into the next model call" as a budget. Manage it actively. It doesn't take care of itself once a request reaches Iris. -These capabilities aren't a fixed sequence. A request can draw on any combination of them before a model call happens: semantic caching for responses to similar prompts, [Agent Memory]({{< relref "/develop/ai/context-engine/agent-memory" >}}) for session and long-term recall, and [Context Retriever]({{< relref "/develop/ai/context-engine/context-retriever" >}}) for governed access to business data. [Data Integration]({{< relref "/develop/ai/context-engine/data-integration" >}}) runs in the background, keeping that business data fresh for Context Retriever to query. +These capabilities aren't a fixed sequence. A request can draw on any combination of them before a model call happens: semantic caching for responses to similar prompts, [Agent Memory]({{< relref "/develop/ai/context-engine/agent-memory" >}}) for session and long-term recall, and [Context Gateway]({{< relref "/develop/ai/context-engine/context-gateway" >}}) for tool calls to your MCP servers and your business data. [Data Integration]({{< relref "/develop/ai/context-engine/data-integration" >}}) runs in the background, keeping that business data fresh for Context Gateway to query. Select a node in the diagram for a description of what that capability does, with a link to its docs. Select a scenario button to trace the path a request takes. @@ -57,24 +57,20 @@ nodes: url: "/develop/ai/context-engine/agent-memory/interactive-demo" docsUrl: "/develop/ai/context-engine/agent-memory" contextRetriever: - label: "Context Retriever" + label: "Context Gateway" type: process col: 1 row: 2 description: | - Context Retriever: governed tool calls. Calls governed, schema-first tools to fetch business data the agent needs. - links: - demo: - label: "Interactive demo" - url: "/develop/ai/context-engine/context-retriever/interactive-demo" - docsUrl: "/develop/ai/context-engine/context-retriever" + Context Gateway: tool calls through one endpoint. Calls the tools of MCP servers and Context Surfaces to fetch the business data the agent needs. + docsUrl: "/develop/ai/context-engine/context-gateway" dataIntegration: label: "Data Integration" type: process col: 0 row: 3 description: | - Data Integration: keeps business data fresh. Streams changes from source databases into the data layer Context Retriever queries. + Data Integration: keeps business data fresh. Streams changes from source databases into the data layer Context Gateway queries. docsUrl: "/develop/ai/context-engine/data-integration" cachedResponse: label: "Cached response" @@ -169,9 +165,9 @@ paths: description: | For a question that depends on earlier turns or what's known about the user, Agent Memory supplies that recall before the model call. context: - label: "Needs business data: retrieves via Context Retriever" + label: "Needs business data: retrieves via Context Gateway" description: | - For a question that depends on live business data, Context Retriever calls governed tools to fetch it before the model call. Data Integration keeps that data fresh. + For a question that depends on live business data, Context Gateway calls tools to fetch it before the model call. Data Integration keeps that data fresh. ``` ## Two things Iris changes @@ -183,5 +179,5 @@ paths: ## Next steps - [Agent Memory overview]({{< relref "/develop/ai/context-engine/agent-memory/overview" >}}) -- [Context Retriever concepts]({{< relref "/develop/ai/context-engine/context-retriever/concepts" >}}) -- Interactive demos for [Agent Memory]({{< relref "/develop/ai/context-engine/agent-memory/interactive-demo" >}}) and [Context Retriever]({{< relref "/develop/ai/context-engine/context-retriever/interactive-demo" >}}) that run in your browser +- [Context Gateway concepts]({{< relref "/develop/ai/context-engine/context-gateway/concepts" >}}) +- An [interactive demo]({{< relref "/develop/ai/context-engine/agent-memory/interactive-demo" >}}) of Agent Memory that runs in your browser diff --git a/content/develop/ai/context-engine/context-gateway/_index.md b/content/develop/ai/context-engine/context-gateway/_index.md new file mode 100644 index 0000000000..972b7ccf7e --- /dev/null +++ b/content/develop/ai/context-engine/context-gateway/_index.md @@ -0,0 +1,97 @@ +--- +Title: Redis Context Gateway +alwaysopen: false +categories: +- docs +- develop +- ai +description: Give an agent one MCP endpoint for the tools of any MCP server, plus tools over your Redis data. +linkTitle: Context Gateway +hideListLinks: true +weight: 22 +aliases: +- /develop/ai/context-engine/context-retriever/ +- /develop/ai/context-engine/context-retriever/interactive-demo/ +--- + +Give each agent one Model Context Protocol (MCP) endpoint for every tool it needs, with one key and no Upstream credentials in the agent. + +Context Gateway lists and calls the tools of its Upstreams. An Upstream is any MCP server the Gateway can reach over Streamable HTTP, or a Context Surface that serves the records of a Redis database as tools. Agents connect with any MCP client that supports Streamable HTTP. + +> [!NOTE] +> Upstreams are not limited to Redis. Attach any MCP server, whether a third-party service such as a ticketing, billing, or search server, or one your team builds. Context Surfaces are optional. + +
+ {{< tile-card icon="images/icon_logo/icon-data-structures-64-midnight.png" title="Concepts" description="Gateways, Upstreams, tool exposure modes, and result handles" url="/develop/ai/context-engine/context-gateway/concepts" >}} + {{< tile-card icon="images/icon_logo/icon-developers-64-midnight.png" title="Quickstart" description="List, search, and call your first tools through a Gateway" url="/develop/ai/context-engine/context-gateway/quickstart" >}} + {{< tile-card icon="images/dev/icons/icon-redis-code-64-duotone.png" title="Agent integration" description="Connect a Gateway to OpenAI Agents SDK or LangGraph" url="/develop/ai/context-engine/context-gateway/agent-integration" >}} + {{< tile-card icon="images/dev/icons/icon-redis-insight-64-duotone.png" title="Manage Gateways" description="Register MCP servers and Context Surfaces and set tool policies" url="/develop/ai/context-engine/context-gateway/manage-gateways" >}} +
+ +## What is Context Gateway? + +Redis Context Gateway is an MCP gateway for AI agents that: + +- **Works with any MCP server**: Attach third-party and in-house MCP servers, not only Redis tools +- **Serves one endpoint per Gateway**: An agent connects to one Gateway instead of to every MCP server it uses +- **Keeps credentials out of agents**: The Gateway stores each Upstream's credential in your secrets backend and sends it on every call +- **Turns Redis data into tools**: Optionally, a Context Surface generates tools from a data model over a Redis database +- **Scales to large tool sets**: In `search` mode, the agent searches for tools instead of loading every definition +- **Handles large and repeated results**: An optional result cache serves repeated calls, and result handles keep large results out of the agent's context + +## Why use Context Gateway? + +
+
+

For AI applications

+ +
+
+

For platform teams

+ +
+
+ +## Quick example + +Install the Redis Iris Python package, which includes the `iris` CLI: + +```bash +pip install redis-iris +``` + +With a Gateway's MCP endpoint and an agent key in your environment, list and call its tools: + +*Short Python example with `ContextGateway.from_env()`, `tools()`, and `call()`, and the same calls with `iris context-gateway tools list` and `tools call`.* + +The [redis-iris package](https://pypi.org/project/redis-iris/) covers MCP calls and Gateway administration in Python and the CLI. For TypeScript, the generated client [@redis-iris/context-gateway](https://www.npmjs.com/package/@redis-iris/context-gateway) covers MCP calls and the Control Plane API. See the [developer guide](/content/develop/ai/context-engine/context-gateway/developer-guide.md) to choose a client. + +Follow the [quickstart](/content/develop/ai/context-engine/context-gateway/quickstart.md) to list, search, and call tools, or see [Manage Gateways and Upstreams](/content/develop/ai/context-engine/context-gateway/manage-gateways.md) to register your own MCP servers and Context Surfaces. + +## Get started with Context Gateway + +Context Gateway is available for self-managed deployment on Kubernetes. See [Self-managed Context Gateway](/content/operate/iris/context-gateway/self-managed/_index.md), then [create a Gateway and an agent key](/content/operate/iris/context-gateway/self-managed/api-examples.md). + +You need a license key to deploy: [contact Redis](https://redis.io/contact/) to request access. + +## Manage Gateways and Upstreams + +- [Manage Gateways and Upstreams](/content/develop/ai/context-engine/context-gateway/manage-gateways.md): create Gateways, register MCP servers, store their credentials, and set tool policies. +- [Manage Context Surfaces](/content/develop/ai/context-engine/context-gateway/manage-context-surfaces.md): serve the records of a Redis database as tools. + +## Serve tools at runtime + +- [Connect with MCP](/content/develop/ai/context-engine/context-gateway/mcp.md) from any Streamable HTTP client. +- [Integrate with an agent](/content/develop/ai/context-engine/context-gateway/agent-integration.md) using OpenAI Agents SDK or LangGraph. + +Use the [API reference](/content/develop/ai/context-engine/context-gateway/api-reference.md) for MCP tools and metadata, and [limits and errors](/content/develop/ai/context-engine/context-gateway/limits.md) for constraints and recovery guidance. diff --git a/content/develop/ai/context-engine/context-gateway/agent-integration.md b/content/develop/ai/context-engine/context-gateway/agent-integration.md new file mode 100644 index 0000000000..2df0e7cd25 --- /dev/null +++ b/content/develop/ai/context-engine/context-gateway/agent-integration.md @@ -0,0 +1,77 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Give an OpenAI Agents SDK or LangGraph agent a Gateway's tools over MCP, in full or search mode. +hideListLinks: true +linktitle: Agent integration +title: Integrate with an agent +weight: 65 +--- + +Attach a Gateway to an agent as one Streamable HTTP Model Context Protocol (MCP) server. The agent lists the Gateway's tools and calls them from your application runtime. + +![A task starts an agent turn. During the turn, the model calls tools through Context Gateway, which calls the MCP servers and Context Surfaces behind it and returns the results. The turn ends with a reply.](/images/ai/context-engine/context-gateway-session.svg) +{width="700"} + +Both framework examples use the same task, over the sample clinic directory from the [quickstart](/content/develop/ai/context-engine/context-gateway/quickstart.md): + +> Which clinic has the most cardiologists, and how many does it have? + +## Before you begin + +Follow the [quickstart setup](/content/develop/ai/context-engine/context-gateway/quickstart.md#before-you-begin) and export `IRIS_CONTEXT_GATEWAY_URL` and `IRIS_CONTEXT_GATEWAY_API_KEY`. Use the search mode Gateway, so the agent can find the tools it needs and count providers through a result handle. + +These programs use an OpenAI model. Export a provider key separately: + +```bash +export OPENAI_API_KEY='' +``` + +These programs use Python. Follow one framework. The dependency versions are pinned to the tested examples. + +## Choose a tool exposure mode + +| Gateway mode | What the agent does | +| --- | --- | +| `full` | Sees every tool definition at the start of the turn and calls tools by qualified name. | +| `search` | Calls `search_tools` for the task, then calls the chosen tool through `run_tool`. | + +Search mode keeps tool definitions out of the context until the agent needs them. Tell the agent in its instructions to search before it calls a tool. + +In `full` mode, tool names contain a dot, such as `scheduling.search_provider_by_text`. OpenAI and some other model providers accept only letters, digits, underscores, and hyphens in tool names. Map the names in your application, or use `search` mode, where every call goes through `run_tool`. + + + +## Use OpenAI Agents SDK + +The application attaches the Gateway as a Streamable HTTP MCP server with the agent key in a request header. The [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/mcp/) runs the model's tool calls. + +*Install command, pinned to tested versions.* + +*Example set `iris_gateway_openai_agent`, step `gateway-openai-agent`, in Python (Iris).* + +## Use LangGraph + +A LangGraph agent loads the Gateway's tools through the LangChain MCP adapter and runs the tool loop. See the [LangChain MCP documentation](https://docs.langchain.com/oss/python/langchain/mcp). + +*Install command, pinned to tested versions.* + +*Example set `iris_gateway_langgraph_agent`, step `gateway-langgraph-agent`, in Python (Iris).* + +## Check the result + +Expect a reply that names one clinic and a provider count. Check that the agent called `search_tools`, then `run_tool_as_handle` or `run_tool` for the provider list, then `count_results` on the handle, and that it released the handle. + +| Case | Expected behavior | +| --- | --- | +| The task needs one tool | The agent calls it once and answers from its result. | +| Search mode | The agent calls `search_tools` before `run_tool` and copies the returned reference. | +| A tool returns an error | The agent reports the failure rather than inventing a result. | +| The result comes back as a handle | The agent reads only the pages it needs and releases the handle. | + +## Troubleshoot an agent run + +Inspect the tools the agent received before the first model call. If the agent invents a tool name or a `run_tool` reference, check its instructions and the Gateway's mode. See [MCP troubleshooting](/content/develop/ai/context-engine/context-gateway/mcp.md#troubleshoot-the-connection). diff --git a/content/develop/ai/context-engine/context-gateway/api-reference.md b/content/develop/ai/context-engine/context-gateway/api-reference.md new file mode 100644 index 0000000000..b893c7c55b --- /dev/null +++ b/content/develop/ai/context-engine/context-gateway/api-reference.md @@ -0,0 +1,111 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Reference for the Redis Context Gateway MCP endpoint, its tools and arguments, its metadata keys, and its errors. +hideListLinks: true +linktitle: API reference +title: Redis Context Gateway MCP API reference +weight: 80 +--- + +Agents call Redis Context Gateway through its Model Context Protocol (MCP) endpoint, the Data Plane API. Platform teams manage Gateways and Upstreams through the [Control Plane API](/content/operate/iris/context-gateway/self-managed/control-plane-api-reference.md). + +For how to connect, see [MCP](/content/develop/ai/context-engine/context-gateway/mcp.md). For limits and recovery, see [Limits and errors](/content/develop/ai/context-engine/context-gateway/limits.md). + +## Endpoint + +| Item | Value | +| --- | --- | +| Endpoint | `POST /v1/gateways//mcp`, the Gateway's `mcpEndpoint` | +| Transport | Streamable HTTP. Responses can be JSON or a server-sent event stream, so send `Accept: application/json, text/event-stream`. | +| Credential | An agent key in `Authorization: Bearer ` or `X-Api-Key: `, or an identity provider (IdP) access token in `Authorization: Bearer ` | +| Protocol versions | `2026-07-28`, `2025-11-25`, `2025-06-18`, and `2025-03-26` | +| Sign-in metadata | `GET /.well-known/oauth-protected-resource/v1/gateways//mcp`, served without a credential when the deployment turns on sign-in | + +Before any MCP message, the endpoint answers `401` without a valid credential, `404` for a Gateway that does not exist or belongs to another tenant, and `403` without a grant on the Gateway. + +## Tools + +### Upstream tools + +In `full` mode, `tools/list` returns every tool of every enabled Upstream under its qualified name, `.`. Call one with `tools/call` and that name. Each definition keeps the Upstream's input schema, output schema, and annotations. + +### `search_tools` + +Listed in `search` mode only. + +| Argument | Type | Description | +| --- | --- | --- | +| `query` | string | Required. 1 to 200 characters after trimming. `*` matches every tool. | +| `limit` | integer | Optional. 1 to 100. Default `5`. | + +Any other argument is refused with the reason `invalid_arguments`. Search lowercases the query and matches its words against each tool's exposed name and original name first, then its title and description. Ties sort by exposed name, so the order is stable for the same catalog and query. + +The result, in `structuredContent` and in one text part: + +| Field | Description | +| --- | --- | +| `query` | The trimmed query. | +| `returned` | The number of definitions in `tools`. | +| `matched` | The number of tools that match the query. | +| `truncated` | `true` when `returned` is smaller than `matched`. | +| `tools` | The matching tool definitions, in ranked order. | + +### `run_tool` + +Listed in `search` mode only. + +| Argument | Type | Description | +| --- | --- | --- | +| `tool` | object | Required. `upstreamId` and `originalName`, copied from a definition's `_meta["com.redis.context-gateway/tool"]`. No other members. | +| `toolArgs` | object | The tool's arguments, as its input schema describes. | + +The result is the tool's result. + +### Result handle tools + +Listed in both modes when the deployment turns on result handles. + +| Tool | Arguments | Result | +| --- | --- | --- | +| `run_tool_as_handle` | `full` mode: `name` and `toolArgs`. `search` mode: `tool` and `toolArgs`, as `run_tool` takes them. | A stub for the held result, or the result whole when it cannot be held. | +| `page_results` | `handle`, `offset` (default `0`), `limit` | For a collection, items from `offset`, `limit` 1 to 100, default 10. For text, bytes, `limit` up to 16384, default 4096. | +| `query_results` | `handle`, `jsonpath`, `offset`, `limit` 1 to 100, default 20 | The matches of a JSONPath expression over the held result, in pages. | +| `count_results` | `handle`, and `jsonpath` or `groupBy` | The number of items, of selected nodes, or of items for each value of one field. | +| `aggregate_results` | `handle`, `jsonpath` | `count`, `sum`, `min`, `max`, and `avg` of the selected numbers, and `ignored` for values that are not numbers. | +| `describe_result` | `handle` | The held result's description without its preview. | +| `release_results` | `handle` | Deletes the held result before its time to live (TTL) ends. | + +A handle is `h_` followed by 32 hexadecimal digits. A JSONPath expression follows RFC 9535, starts with `$`, is at most 1 kibibyte (KiB), and cannot use the descendant segment `..`. + +## Metadata keys + +The Gateway adds these entries to `_meta`. Read them by key, and ignore members you do not know. + +| Key | Where | Members | +| --- | --- | --- | +| `com.redis.context-gateway/tool` | Each tool definition | `upstreamId`, `originalName`, and the catalog revision | +| `com.redis.context-gateway/invocation` | Each tool result | `resultCache` with `status` `hit` and `ageMs` when the cache answered. `upstream` and `upstreamDurationMs` when the call reached the Upstream. | +| `com.redis.context-gateway/handle` | A handle stub | The handle and its description, without the preview and the next call | +| `com.redis.context-gateway/failure` | A tool result with its error flag set | `schemaVersion`, `reason`, `completion`, and when the failure happened at the Upstream, `stage` and `upstreamCode` | + +## Errors + +A failure reaches the client in one of two ways. + +### JSON-RPC errors + +These failures happen before any call to an Upstream. The JSON-RPC `error.data` holds `schemaVersion`, `reason`, and `traceId`. + +| Code | `reason` | Cause | +| --- | --- | --- | +| `-32602` | `unknown_tool` | The Gateway serves no tool with that name or reference. A disabled Upstream and a tool removed by a catalog change also answer this way. | +| `-32602` | `invalid_request` | The request is not a valid tool call. | +| `-32603` | `gateway_unavailable` | The Gateway could not read its own records. | + +### Tool results with an error flag + +Every other failure comes back as a tool result with `isError: true` and the failure entry in `_meta`. Act on `reason` and `completion`. The text of the result is written for people and can change in any release. For every reason and how to retry, see [Limits and errors](/content/develop/ai/context-engine/context-gateway/limits.md). diff --git a/content/develop/ai/context-engine/context-gateway/concepts.md b/content/develop/ai/context-engine/context-gateway/concepts.md new file mode 100644 index 0000000000..528ee32816 --- /dev/null +++ b/content/develop/ai/context-engine/context-gateway/concepts.md @@ -0,0 +1,123 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Understand Gateways, Upstreams, credentials, tool exposure modes, the result cache, result handles, and agent access. +hideListLinks: true +linktitle: Concepts +title: Redis Context Gateway concepts +weight: 3 +aliases: +- /develop/ai/context-engine/context-retriever/concepts/ +--- + +## Gateways + +A **Gateway** is one Model Context Protocol (MCP) endpoint, `/v1/gateways//mcp` on the Data Plane. It lists and calls the tools of its **Upstreams**. Create a Gateway for each set of tools that an agent or team should see together. + +![An agent sends one agent key to Context Gateway, which checks it with the Identity Service. The Gateway calls an MCP server and a Context Surface over a Redis database, and reads Upstream credentials from the secrets backend.](/images/ai/context-engine/context-gateway-overview.svg) +{width="750"} + +One Helm release serves one tenant, and every Gateway belongs to that tenant. A Gateway of another tenant answers like a Gateway that does not exist. + +### Access + +An agent reaches a Gateway in one of two ways: + +- An **agent key** from the Identity Service, sent as `Authorization: Bearer ` or `X-Api-Key: `. +- An **identity provider (IdP) access token**, when the deployment turns on sign-in. MCP clients that support MCP authorization find the IdP from the Gateway and sign in by themselves. The token is accepted only in `Authorization: Bearer`. + +The key's or token's grants must name the Gateway: + +| Action | What it allows | +| --- | --- | +| `use` | List and call every tool on the Gateway, including tools that change data in an Upstream. | +| `full` | Everything `use` allows. | + +MCP read-only and destructive annotations do not limit what `use` allows. Give an agent a Gateway that holds only the tools it should call. Neither the agent key nor the IdP token reaches an Upstream. + +For key administration, see [Authentication and authorization](/content/operate/iris/context-gateway/self-managed/authentication.md). + +## Upstreams + +An **Upstream** is a source of tools. A Gateway supports two kinds: + +- An **MCP server** (`mcp`): any MCP server the Gateway reaches over Streamable HTTP. It can come from any vendor or from your own team. It does not need to use Redis. +- A **Context Surface** (`context-surface`): tools generated from a data model over the records of a Redis database. See [Manage Context Surfaces](/content/develop/ai/context-engine/context-gateway/manage-context-surfaces.md). + + + +Each Upstream has a **namespace**: 1 to 63 lowercase letters and digits, with single hyphens between them. The Control Plane derives it from the Upstream's display name when you leave it out. The Gateway serves each tool under its qualified name, the namespace, a dot, and the tool's original name, such as `billing.get_invoice`. Namespaces keep two Upstreams with the same tool name apart. + +Some model providers accept only letters, digits, underscores, and hyphens in tool names. If yours rejects the dot, map it in your application, or use `search` mode, where the agent calls tools through `run_tool`. + +An Upstream is enabled when you create it. A disabled Upstream contributes no tools until you enable it again. + +### Tool catalogs + +The Gateway discovers an Upstream's tools when you register it and stores them as the Upstream's **catalog**. `tools/list` serves the stored catalog and does not contact the Upstream, so an Upstream outage shows up only when an agent calls one of its tools. + +Refresh the catalog when the Upstream's tools change. Each refresh publishes a new catalog revision. Changing an Upstream's endpoint or credential also changes its revision. + +### Credentials + +The Gateway sends each MCP server the credential you register for it, as a bearer token or a named header. Credential values live in the deployment's secrets backend: an encrypted Redis database, HashiCorp Vault, or AWS Secrets Manager. Reads of an Upstream never return them. + + + +## Tool exposure modes + +A Gateway's `toolExposureMode` decides what an MCP `tools/list` request returns: + +| Mode | `tools/list` returns | Use it when | +| --- | --- | --- | +| `full` (default) | Every tool under its qualified name. | The agent works with a small, known set of tools. | +| `search` | Only `search_tools` and `run_tool`. | The Gateway holds more tools than fit in the agent's context. | + +![In full mode, listing tools returns every tool definition and the agent calls a tool by name. In search mode, listing tools returns search_tools and run_tool, search returns the matching tools, and run_tool calls the chosen one.](/images/ai/context-engine/context-gateway-exposure-modes.svg) +{width="650"} + +In `search` mode the agent calls `search_tools` and receives complete tool definitions. Search matches the words of the query against each tool's name, title, and description, so write queries with the words a tool's name or description would contain. The agent then calls a tool through `run_tool` with the `upstreamId` and `originalName` from its definition. A mode change applies on the next request. + +## Tool policies + +A **tool policy** sets optional behavior for one tool of one Upstream. Both behaviors need a Gateway database, selected with the Gateway's `databaseId`, and both are off until the deployment turns them on. + +### Result cache + +The result cache serves a repeated call from the Gateway database instead of calling the Upstream again. Each tool opts in with a `resultCache` policy and a time to live (TTL). + +A tool can be cached unless its definition says `readOnlyHint: false` or `destructiveHint: true`. An absent hint does not block caching, and no annotation turns caching on. An entry is served until its TTL expires or the Upstream's revision changes. + +![A tool call without a cache policy goes straight to the Upstream. A call with a policy checks for a stored result. If one exists, the Gateway returns it. If not, the Gateway calls the Upstream, stores the result for the TTL, and returns it.](/images/ai/context-engine/context-gateway-result-cache.svg) +{width="850"} + +All callers with access to a Gateway share its cache entries. + +### Result handles + +A result handle holds a large tool result in the Gateway database. When the deployment turns on result handles, every eligible result over 32 kibibytes (KiB) becomes a handle, on every Gateway with a `databaseId`. The agent receives a small stub with the handle and reads the result in pages, queries, counts, or aggregates. + +A tool opts out with a `resultHandle` policy of `capture: never`, or sets its own handle lifetime with `ttlSeconds`. An agent can also hold a result of any size with `run_tool_as_handle`. + +A handle is bound to the caller that created it, and it survives a rotation of that caller's agent key. It ends when the agent releases it or its TTL expires. Handles are bounded by their TTL and the Gateway database's memory. + +## Key terms + +| Term | Meaning | +| --- | --- | +| Gateway | One MCP endpoint that serves the tools of its Upstreams. | +| Upstream | An MCP server or Context Surface whose tools a Gateway serves. | +| Context Surface | An Upstream that serves Redis records as tools from a data model. | +| Namespace | The prefix that makes an Upstream's tool names unique within a Gateway. | +| Catalog | The stored list of an Upstream's tools. | +| Tool policy | Per-tool settings for the result cache and result handles. | +| Gateway database | The Redis database that holds cached results and result handles. | + +## Next steps + +- [Quickstart](/content/develop/ai/context-engine/context-gateway/quickstart.md) to list and call your first tools. +- [MCP](/content/develop/ai/context-engine/context-gateway/mcp.md) to connect any MCP client. +- [API reference](/content/develop/ai/context-engine/context-gateway/api-reference.md) for every tool, argument, and metadata key. diff --git a/content/develop/ai/context-engine/context-gateway/developer-guide.md b/content/develop/ai/context-engine/context-gateway/developer-guide.md new file mode 100644 index 0000000000..5491499769 --- /dev/null +++ b/content/develop/ai/context-engine/context-gateway/developer-guide.md @@ -0,0 +1,61 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Connect an application to Redis Context Gateway and work with its tools through Python, the Iris CLI, TypeScript, or HTTP. +hideListLinks: true +linktitle: Developer guide +title: Redis Context Gateway developer guide +weight: 5 +--- + +Use the Python package, the Iris command-line interface (CLI), the TypeScript client, or HTTP to list and call a Gateway's tools, read large results, and administer Gateways. + +## Integration workflow + +1. Get a Gateway's Model Context Protocol (MCP) endpoint and an agent key with a `use` grant on it. +1. Connect an MCP client, or your agent framework, to the endpoint. +1. List the Gateway's tools, or search them when the Gateway runs in `search` mode. +1. Call a tool, and read a large result through its handle. + +Platform teams create the Gateway and register its Upstreams through the Control Plane. See [Manage Gateways and Upstreams](/content/develop/ai/context-engine/context-gateway/manage-gateways.md). + +## Choose a client + +| Client | Use it when | Package and guide | +|:-------|:------------|:------------------| +| Python | Your application or agent uses Python. | Install [`redis-iris`](https://pypi.org/project/redis-iris/) and follow the [quickstart](/content/develop/ai/context-engine/context-gateway/quickstart.md). `ContextGateway` lists, searches, and calls tools, and its `service` property administers Gateways. | +| CLI | You work from a shell, a script, or continuous integration. | The `iris context-gateway` commands ship in the same [`redis-iris`](https://pypi.org/project/redis-iris/) package. | +| TypeScript | Your application or agent uses JavaScript or TypeScript. | Install [`@redis-iris/context-gateway`](https://www.npmjs.com/package/@redis-iris/context-gateway). `ContextGatewayClient` opens MCP sessions, and `ContextGatewayService` calls the Control Plane. | +| HTTP | You need language-independent access or don't want a package dependency. | No package required. Send MCP requests to the Gateway endpoint as shown in [MCP](/content/develop/ai/context-engine/context-gateway/mcp.md). | + +The `redis-iris` package requires Python 3.11 or later. The TypeScript client requires Node.js 22 or later. + +## Connect to a Gateway + +Every client requires: + +* The Gateway's MCP endpoint, its `mcpEndpoint`, ending in `/mcp`. +* An agent key with a `use` grant on the Gateway, or an identity provider access token when the deployment turns on sign-in. + +The Python package and the CLI read both from `IRIS_CONTEXT_GATEWAY_URL` and `IRIS_CONTEXT_GATEWAY_API_KEY`. When you use HTTP, send the key as a bearer token on every request. + +Ask your platform team for these values, or [create a Gateway and an agent key](/content/operate/iris/context-gateway/self-managed/api-examples.md) on a self-managed deployment. + +## Work with tools + +In `full` mode, `tools/list` returns every tool under its qualified name. In `search` mode it returns `search_tools` and `run_tool`. In Python, `ContextGateway.call()` takes the qualified name in either mode and calls `run_tool` for you in `search` mode. See [Tool exposure modes](/content/develop/ai/context-engine/context-gateway/concepts.md#tool-exposure-modes). + +## Work with large results + +When the deployment turns on result handles, a large result comes back as a stub with a handle. Read the parts you need with the handle tools, then release it. See [Read result handles](/content/develop/ai/context-engine/context-gateway/mcp.md#read-result-handles). + +## References + +* [Quickstart](/content/develop/ai/context-engine/context-gateway/quickstart.md) +* [Python and CLI package reference](https://pypi.org/project/redis-iris/) +* [TypeScript client reference](https://www.npmjs.com/package/@redis-iris/context-gateway) +* [Redis Context Gateway MCP API reference](/content/develop/ai/context-engine/context-gateway/api-reference.md) +* [Control Plane API reference](/content/operate/iris/context-gateway/self-managed/control-plane-api-reference.md) diff --git a/content/develop/ai/context-engine/context-gateway/limits.md b/content/develop/ai/context-engine/context-gateway/limits.md new file mode 100644 index 0000000000..edb12c7df3 --- /dev/null +++ b/content/develop/ai/context-engine/context-gateway/limits.md @@ -0,0 +1,112 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Find tool and result limits, tool call failure reasons, Control Plane error types, conflicts, and retry guidance. +hideListLinks: true +linktitle: Limits and errors +title: Redis Context Gateway limits and errors +weight: 90 +--- + +Check these limits and error types when you handle a failed request or a tool result that did not come from the Upstream. Confirm deployment settings with your administrator. + +The [API reference](/content/develop/ai/context-engine/context-gateway/api-reference.md) defines the Model Context Protocol (MCP) tools and their arguments. The [Control Plane API reference](/content/operate/iris/context-gateway/self-managed/control-plane-api-reference.md) defines Control Plane field formats. + +## Tool and result limits + +| Item | Limit or default | +| --- | --- | +| Gateway and Upstream display name | 1 to 200 characters | +| Upstream namespace | 1 to 63 lowercase letters and digits, with single hyphens between them | +| Gateway `databaseId` | 1 to 32 letters, digits, and hyphens | +| `search_tools` query | 1 to 200 characters. `limit` 1 to 100, default 5. | +| Result cache time to live (TTL) (`resultCache.ttlSeconds`) | 1 to 86400 seconds | +| Result handle threshold | Results over 32 kibibytes (KiB) become handles when the deployment turns handles on. | +| Largest result a handle holds | 8 mebibytes (MiB). A larger result comes back whole. | +| Result handle lifetime | 15 minutes by default, at most 24 hours. A tool policy can set 1 to 86400 seconds. | +| Handle preview | The first 3 items or 2 KiB | +| Context Surface data model | 1 MiB of JSON | +| Context Surface tool results | Up to 100 records per call. A handle over a larger answer holds one page and is marked partial. | +| Agent key revocation | Takes effect within the Identity Service cache lifetime, 300 seconds by default. | + +## Handle tool call failures + +A failed call reaches the client as a JSON-RPC error or as a tool result with its error flag set. Act on the `reason`, never on the message or the text: those are written for people and can change in any release. + +### JSON-RPC errors + +These happen before any call to an Upstream. `error.data` holds `reason` and a `traceId` to give operators. + +| Code | `reason` | Meaning | +| --- | --- | --- | +| `-32602` | `unknown_tool` | The Gateway serves no tool with that name or reference. A disabled Upstream and a tool removed by a catalog change also answer this way. | +| `-32602` | `invalid_request` | The request is not a valid tool call. | +| `-32603` | `gateway_unavailable` | The Gateway could not read its own records. | + +### Tool results with an error flag + +The failure entry in `_meta["com.redis.context-gateway/failure"]` holds `reason` and `completion`. When the failure happened at the Upstream, it also holds `stage` and, for an Upstream JSON-RPC error, `upstreamCode`. + +| `reason` | Meaning | +| --- | --- | +| `invalid_arguments` | The arguments do not match the tool's input schema. | +| `upstream_unavailable` | The Gateway could not reach the Upstream. | +| `upstream_credentials_unavailable` | The Gateway could not read the Upstream's credential from the secrets backend. | +| `upstream_rejected_credentials` | The Upstream refused the credential the Gateway sent. | +| `upstream_permission_denied` | The Upstream accepted the credential but refused the call. | +| `upstream_error` | The Upstream returned an error. | +| `unsupported_upstream_result` | The Upstream returned a result the Gateway cannot pass on. | +| `timeout` | The call did not finish in time. | +| `connection_lost` | The connection to the Upstream closed during the call. | +| `unknown_handle` | The handle expired, was released, or belongs to another caller. | +| `gateway_unavailable` | A handle tool could not read the Gateway database. | +| `embedding_unavailable` | A Context Surface semantic or hybrid search tool could not reach its embedding service. | + +| `completion` | Meaning | Retry | +| --- | --- | --- | +| `not_dispatched` | The Gateway did not send the call to the Upstream. | Safe to retry after you fix the cause. | +| `unknown` | The Upstream might have run the call. | Check the Upstream's state before you retry a call that changes data. | + +*Example set `iris_gateway_limits`, steps `gateway-error-reason` and `gateway-error-jsonrpc`, in Python (Iris) and cURL (Iris): read the reason from a failed tool result and from a JSON-RPC error.* + +A result the cache answered carries `resultCache` with `status` `hit` and `ageMs` in `_meta["com.redis.context-gateway/invocation"]`. A result without it came from the Upstream. + +## Handle request errors + +Control Plane errors are problem details. Act on the HTTP status, the problem `type`, and the `fields[]` paths, not on `detail`. + +| Status | Problem types | Action | +| --- | --- | --- | +| `400` | `/errors/invalid-data` | Correct the fields named in `fields[]`. | +| `401` | `/errors/authentication-failed` | Check the admin token. An agent key is not accepted here. | +| `403` | `/errors/insufficient-permissions` | Use a credential that can administer Gateways. | +| `404` | `/errors/gateway-not-found`, `/errors/upstream-not-found` | Check identifiers. A Gateway or Upstream being deleted reads as not found. | +| `408` | `/errors/timeout` | Read the resource before you repeat a write. | +| `409` | See [Resolve conflicts](#resolve-conflicts). | Read the current state before you repeat the request. | +| `422` | `/errors/resource-unavailable` | Updating or refreshing an Upstream found its database or credential unusable. Check the referenced database or credential. | +| `423` | `/errors/resource-suspended` | The Gateway or its tenant is suspended. Ask your administrator to check its state. | +| `424` | `/errors/resource-unavailable`, `/errors/database-out-of-memory` | A dependency failed, such as the Upstream or the Gateway database. | +| `500` | `/errors/unexpected-error` | Keep the trace identifier and report persistent failures. | +| `503` | `/errors/service-unavailable` | Check readiness and retry with bounded backoff. | + + + +## Resolve conflicts + +Updates and deletes take the resource's current `revision` as `expectedRevision`. A `409` names the conflict: + +| Problem type | Action | +| --- | --- | +| `/errors/revision-conflict` | Another change landed first. Read the Gateway or Upstream again, then send your change with its new `revision`. | +| `/errors/namespace-conflict` | Another Upstream of the Gateway uses the namespace. Choose another one. | +| `/errors/gateway-not-empty` | Delete the Gateway's Upstreams before you delete the Gateway. | +| `/errors/catalog-publication-conflict` | The Upstream changed while its tools were discovered, so nothing was published. Refresh the catalog again. | + +## Retry uncertain operations + +Use bounded retries for reads and for tool calls that report `not_dispatched`. A timeout on a Control Plane write does not prove the write failed. Read the Gateway or Upstream before you repeat it. + +A delete that stopped partway leaves the Upstream marked for deletion, and reads report it as not found. Repeat the delete with the Upstream ID to finish it. diff --git a/content/develop/ai/context-engine/context-gateway/manage-context-surfaces.md b/content/develop/ai/context-engine/context-gateway/manage-context-surfaces.md new file mode 100644 index 0000000000..f445ba7535 --- /dev/null +++ b/content/develop/ai/context-engine/context-gateway/manage-context-surfaces.md @@ -0,0 +1,109 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Serve the records of a Redis database as tools with a Context Surface, write its data model, and change, refresh, or delete it. +hideListLinks: true +linktitle: Manage Context Surfaces +title: Manage Context Surfaces +weight: 30 +--- + +A Context Surface is an Upstream that serves the records of a Redis database as tools. You describe the records in a data model, and the Control Plane generates read-only tools from it, such as a text search, a filter, and a lookup by ID for each entity. + +## Before you begin + +These tasks need the Control Plane URL and the admin token. The examples use Python, cURL, and the Iris command-line interface (CLI). Export them as shown in [Manage Gateways and Upstreams](/content/develop/ai/context-engine/context-gateway/manage-gateways.md#before-you-begin). + +Your administrator registers the Redis database in the deployment's database registry under an ID, and gives the Control Plane and the Data Plane each a Redis user for it. See [Context Surface databases](/content/operate/iris/context-gateway/self-managed/configuration.md#context-surface-databases). You reference the database by that ID. + +Read these limits before you create a Surface: + +- Every agent with access to the Gateway can read every record of every Surface on it. Context Surfaces do not narrow results by access tags. +- Gateways whose Surfaces share a database can read each other's records. Give a Surface its own database when its records must stay separate, and never use one Redis database as both a Surface database and a Gateway database. +- Creating a Surface, changing its data model, or refreshing it starts background indexing on the Surface database. Search tools can return partial results until indexing finishes. + +## Write a data model + +A data model lists the entities stored in the database. Each entity has a Redis key template, its fields, the index types that make each field searchable, and its relationships to other entities. + +```json +{ + "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 +} +``` + +A data model is at most 1 mebibyte (MiB) of JSON. The Control Plane refuses `acl_field_mappings`. + + + +## Create a Context Surface + +Create an Upstream of kind `context-surface` with the database ID and the data model. A Context Surface takes no endpoint and no credential. + +*Example set `iris_gateway_surfaces`, step `gateway-surface-create`, in Python (Iris), cURL (Iris), and CLI (Iris).* + +The Control Plane generates and publishes the tools in the same request. When it cannot, the response is still `201`, and `initialCatalogFailure` explains why: + +| `initialCatalogFailure.status` | Meaning | What to do | +| --- | --- | --- | +| `422` | The Surface database refused the login. | Ask your administrator to fix the registry login, then refresh. | +| `424` | The Surface database was unreachable, answered an error, or refused a command. | Ask your administrator to check the database and its Redis users, then refresh. | +| `408` | Tool generation timed out. | Refresh. | +| `423` | The Gateway or its tenant was suspended. | Refresh once the suspension ends. | + +A `400` response names each invalid part of the data model in `fields[]`, such as `contextSurface.dataModel.entities[0].fields`. Nothing was written. + +## List the generated tools + +List the Surface's tools. Agents see them under the Surface's namespace, for example `scheduling.search_provider_by_text`. Every generated tool is read-only. + +*Example step `gateway-surface-tools`.* + +A lookup by an ID that no record has succeeds and returns `null`. It is an answer for the agent, not a failure. + +## Change the data model or refresh + +Send a new data model with an Upstream update at the Upstream's current `revision`. The data model and the new tools commit together, so agents see only the new tools. + +*Example step `gateway-surface-update`.* + +Refresh the Surface to generate its tools again from the stored data model. A refresh also recreates any missing index. A call that runs during a change or a refresh can fail once with `upstream_error` because its index is being replaced. Retry it. + +## Delete a Context Surface + +Delete the Surface at its current revision. The delete also drops the Surface's indexes from its database. + +*Example step `gateway-surface-delete`.* + +If the delete keeps answering `503` because the indexes cannot be dropped, repeat it with `ignoreIndexSweepFailure=true`. The Surface is deleted and its indexes stay in the database, where your administrator can drop them. diff --git a/content/develop/ai/context-engine/context-gateway/manage-gateways.md b/content/develop/ai/context-engine/context-gateway/manage-gateways.md new file mode 100644 index 0000000000..d9a004820c --- /dev/null +++ b/content/develop/ai/context-engine/context-gateway/manage-gateways.md @@ -0,0 +1,108 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Create and change Gateways, register MCP servers with their credentials, set tool policies, and remove Upstreams. +hideListLinks: true +linktitle: Manage Gateways +title: Manage Gateways and Upstreams +weight: 20 +--- + +Use the Control Plane to create Gateways, register their Upstreams, and decide how each tool behaves. Agents see a change on their next request. To serve Redis records as tools, see [Manage Context Surfaces](/content/develop/ai/context-engine/context-gateway/manage-context-surfaces.md). + +## Before you begin + +These tasks need the Control Plane URL and the admin token, which a platform team holds. See [Authentication and authorization](/content/operate/iris/context-gateway/self-managed/authentication.md). The admin token manages every Gateway in the deployment, so keep it out of agent and application configuration. + +Export these values in the shell where you run the examples: + +```bash +export IRIS_CONTEXT_GATEWAY_SERVICE_URL='' +export IRIS_CONTEXT_GATEWAY_SERVICE_TOKEN='' +``` + +The examples use Python (`ContextGateway.from_env().service`), cURL, and the `iris context-gateway` command-line interface (CLI), which all read these variables. Follow one tab throughout the page. To choose a client, see the [developer guide](/content/develop/ai/context-engine/context-gateway/developer-guide.md#choose-a-client). + +## Create a Gateway + +Create a Gateway and save its `id` and `mcpEndpoint`. The Gateway serves Model Context Protocol (MCP) at that endpoint immediately. + +*Example set `iris_gateway_manage`, step `gateway-create`, in Python (Iris), cURL (Iris), and CLI (Iris).* + +To use the result cache or result handles, select a Gateway database with `databaseId`. Your administrator names the available databases. + +## List and inspect Gateways and Upstreams + +List the deployment's Gateways and a Gateway's Upstreams, and read one by ID. Every read returns the current `revision`, which updates and deletes need. + +*Example step `gateway-list`.* + +## Change a Gateway + +Update a Gateway's `toolExposureMode` to `full` or `search`, its display name, or its `databaseId`. Agents see a new mode on their next request. See [Tool exposure modes](/content/develop/ai/context-engine/context-gateway/concepts.md#tool-exposure-modes). + +*Example step `gateway-update-mode`.* + +## Register an MCP server + +Register an MCP server as an Upstream with a namespace, its endpoint, and its credential. Any MCP server that serves Streamable HTTP works, including third-party and in-house servers. The Gateway discovers the server's tools during registration. + +*Example step `gateway-register-mcp`.* + +| Credential method | What the Gateway sends | +| --- | --- | +| `bearer` | `Authorization: Bearer ` | +| `header` | The value in the header you name. | + +The Control Plane writes the credential value to the secrets backend. Reads of the Upstream never return it. + +## Inspect the tool catalog + +List an Upstream's tools to see their qualified names and whether each one can be cached. + +*Example step `gateway-list-tools`.* + +Refresh the catalog when the Upstream's tools change. A refresh publishes a new catalog revision, and cached results from the old revision are no longer served. + +*Example step `gateway-refresh`.* + +## Enable or disable an Upstream + +Disable an Upstream to take its tools away from agents without deleting it. Its tools return when you enable it again. + +*Example step `gateway-disable`.* + +## Set tool policies + +Set a tool's policies with `toolPolicies` on an Upstream update. The result cache is opt-in for each tool. Result handles are on for every eligible result once the deployment turns them on, so a tool policy only opts a tool out or changes its handle lifetime. + +| Policy | Member | Effect | +| --- | --- | --- | +| `resultCache` | `ttlSeconds`, 1 to 86400 | Caches the tool's results for that long. | +| `resultHandle` | `capture: never` | Always returns the tool's results whole, also from `run_tool_as_handle`. | +| `resultHandle` | `ttlSeconds`, 1 to 86400 | Keeps the tool's handles for that long instead of the deployment's default. | + +*Example step `gateway-tool-policy`.* + +- Cache only tools whose data changes more slowly than the time to live (TTL). All callers with access to the Gateway share its cache entries. +- A tool whose definition says `readOnlyHint: false` or `destructiveHint: true` is never cached. +- When you move an Upstream to a new endpoint, the response lists any policies it removed in `droppedToolPolicies`. + +## Rotate an Upstream credential + +Update the Upstream with a new credential value. If you rotate a secret in place in Vault or AWS Secrets Manager instead, refresh the catalog so that cached results stop being served. + +*Example step `gateway-rotate-credential`.* + +## Delete an Upstream or a Gateway + +Delete an Upstream at its current revision. A Gateway can be deleted only when it has no Upstreams. With the CLI, `iris context-gateway service delete --cascade` deletes the Upstreams first. + +*Example step `gateway-delete`.* + +A delete marks the Upstream first, then cleans up. If a delete stops after the mark, for example on a timeout, repeat it with the Upstream ID to finish. + +Revoke the agent keys for a deleted Gateway. See [Operations](/content/operate/iris/context-gateway/self-managed/operations.md#rotate-credentials). diff --git a/content/develop/ai/context-engine/context-gateway/mcp.md b/content/develop/ai/context-engine/context-gateway/mcp.md new file mode 100644 index 0000000000..9a5247ba99 --- /dev/null +++ b/content/develop/ai/context-engine/context-gateway/mcp.md @@ -0,0 +1,120 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Connect a Streamable HTTP MCP client to a Gateway with an agent key or your identity provider, use full or search mode, and read result handles. +hideListLinks: true +linktitle: MCP +title: Connect with MCP +weight: 60 +aliases: +- /develop/ai/context-engine/context-gateway/connect-agents/ +--- + +Connect any Model Context Protocol (MCP) client that supports Streamable HTTP and custom request headers. For framework examples, see [Integrate with an agent](/content/develop/ai/context-engine/context-gateway/agent-integration.md). For every tool, argument, and metadata key, see the [API reference](/content/develop/ai/context-engine/context-gateway/api-reference.md). + +## Before you begin + +Get the Gateway's MCP endpoint and an agent key with a `use` grant. Export `IRIS_CONTEXT_GATEWAY_URL` and `IRIS_CONTEXT_GATEWAY_API_KEY` as shown in [Save connection values](/content/develop/ai/context-engine/context-gateway/quickstart.md#save-connection-values). + +Install the dependencies for your language as shown in the [quickstart](/content/develop/ai/context-engine/context-gateway/quickstart.md#before-you-begin). + +## Configure the connection + +| Setting | Value | +| --- | --- | +| Transport | Streamable HTTP | +| Endpoint | `/v1/gateways//mcp` | +| Request header | `Authorization: Bearer ` or `X-Api-Key: ` | +| Protocol versions | `2026-07-28`, `2025-11-25`, `2025-06-18`, and `2025-03-26` | + +The endpoint must be reachable from the client runtime. Keep the agent key in connection settings and out of agent instructions. + +In Python, `ContextGateway` from the `redis-iris` package opens a session with these settings and sends the agent key as `Authorization: Bearer`. + +The command-line interface (CLI) writes the configuration for Claude Code, Cursor, the Agent Development Kit (ADK), or a generic JSON client. It reads the Gateway's endpoint from the Control Plane, so it needs the Gateway ID and the Control Plane settings: + +```bash +export IRIS_CONTEXT_GATEWAY_ID='' +iris context-gateway connect --client claude-code +``` + +Use `--client cursor`, `adk`, or `json` for the other clients. The written configuration reads the key from an environment variable and never contains it. + +## Use the tools + +What the agent sees depends on the Gateway's [tool exposure mode](/content/develop/ai/context-engine/context-gateway/concepts.md#tool-exposure-modes). + +### Full mode + +`tools/list` returns every tool under its qualified name. Call a tool by that name with `tools/call`. + +### Search mode + +`tools/list` returns two tools: + +| Tool | Input | Result | +| --- | --- | --- | +| `search_tools` | `query`, 1 to 200 characters, or `*` for every tool. Optional `limit`, 1 to 100, default 5. | `matched`, `returned`, `truncated`, and the matching tool definitions in ranked order. | +| `run_tool` | `tool` with `upstreamId` and `originalName`, and the tool's arguments in `toolArgs`. | The tool's result. | + +Use the tools in this order: + +1. Search with words that a tool's name, title, or description would contain. Search matches words, not meaning. +2. Pick a definition and read its input schema. When `truncated` is `true`, raise `limit` or narrow the query. +3. Call it through `run_tool`, copying `upstreamId` and `originalName` from the definition's `_meta["com.redis.context-gateway/tool"]`. Do not construct them, and send no other members of that metadata. `run_tool` refuses extra members, such as the catalog revision. + +## Connect a client + +This client connects, lists tools, and calls one in either mode. + +*Example set `iris_gateway_mcp_client`, step `gateway-mcp-client`, in Python (Iris) and cURL (Iris).* + +## Read result handles + +When the deployment turns on result handles, the Gateway lists these tools in both modes: + +| Tool | What it does | +| --- | --- | +| `run_tool_as_handle` | Runs one tool and holds its result whatever its size. In `full` mode it takes the tool's `name`. In `search` mode it takes a `tool` reference, as `run_tool` does. | +| `page_results` | Reads a held result in order. | +| `query_results` | Selects from a held JSON result with a JSONPath expression. | +| `count_results` | Counts items, selected nodes, or items for each value of one field. | +| `aggregate_results` | Computes the count, sum, minimum, maximum, or average of selected numbers. | +| `describe_result` | Describes a held result's shape, size, fields, and expiry. | +| `release_results` | Deletes a held result before it expires. | + +![A large tool result is stored in the Gateway database, automatically or through run_tool_as_handle, and the agent receives a small stub. The agent reads a page or a query from the stored result, then releases the handle.](/images/ai/context-engine/context-gateway-handles.svg) +{width="700"} + +Any ordinary tool call whose result is over the handle threshold also comes back as a stub, unless the tool's policy opts out. The stub carries the handle in `_meta["com.redis.context-gateway/handle"]`, a short preview, and the next call to make. A result can still come back whole, for example when the Gateway database is low on memory, so check for the handle before you read it. + +*Example set `iris_gateway_handles`, steps `gateway-handle-page`, `gateway-handle-query`, `gateway-handle-count`, and `gateway-handle-release`, in Python (Iris) and cURL (Iris).* + +Handles are bound to the caller. Another caller's handle, or one that expired or was released, returns a tool error with the reason `unknown_handle`. + +## Sign in with your identity provider + +When the deployment turns on sign-in, an MCP client can use an access token from your identity provider (IdP) instead of an agent key. Send it as `Authorization: Bearer `. The Gateway does not accept an IdP token in `X-Api-Key`. + +A request without a credential receives `401` and a `WWW-Authenticate` header that points to the Gateway's protected resource metadata. Clients that support MCP authorization read that document, find the IdP, and sign the user in. The IdP's token never reaches an Upstream. + +The signed-in user still needs a grant on the Gateway. See [Identity provider sign-in](/content/operate/iris/context-gateway/self-managed/authentication.md#identity-provider-sign-in). + + + +## Troubleshoot the connection + +| Response | Meaning | Action | +| --- | --- | --- | +| `401` | No credential, or the key or token was not accepted. | Check the key, its expiry, and the header. | +| `403` | The credential has no grant for this Gateway. | Ask your administrator for a `use` grant. | +| `404` | No Gateway answers at this URL for your tenant. | Check the full endpoint. | +| JSON-RPC error with `reason` `unknown_tool` | The tool name is wrong, its Upstream is disabled, or its catalog changed. | List the tools again and use a returned name or reference. | +| Tool result with its error flag set | The tool or its Upstream failed. | Read `reason` and `completion` in the failure metadata. See [Limits and errors](/content/develop/ai/context-engine/context-gateway/limits.md). | + +Give your platform team the `traceId` from a JSON-RPC error's `data` to find the failure in their logs. With the Control Plane token, they can run `iris context-gateway service check `, which checks the token, the Gateway, the Identity Service, the agent key, and one `tools/list`, in that order, and stops at the first failure. `iris context-gateway service diagnose ` prints a report of the Gateway's setup. + +Keep diagnostic details without logging keys or tool results that hold private data. diff --git a/content/develop/ai/context-engine/context-gateway/quickstart.md b/content/develop/ai/context-engine/context-gateway/quickstart.md new file mode 100644 index 0000000000..b5835b6903 --- /dev/null +++ b/content/develop/ai/context-engine/context-gateway/quickstart.md @@ -0,0 +1,126 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: List, search, and call a Gateway's tools, and read a large result through a handle, with Python, HTTP, or the Iris CLI. +hideListLinks: true +linktitle: Quickstart +title: Redis Context Gateway quickstart +weight: 10 +aliases: +- /develop/ai/context-engine/context-retriever/quickstart/ +--- + +Use Redis Context Gateway to give a clinic front-desk assistant the tools of a clinic directory: find a provider by specialty, look up a clinic, and count providers without loading every record into the agent's context. + +## Before you begin + +Get two Gateways over the sample clinic directory from your administrator, and one agent key with a `use` grant on both: + +- A `full` mode Gateway, which lists every tool. +- A `search` mode Gateway with a Gateway database, which lists `search_tools` and `run_tool` and holds large results as handles. + +For self-managed setup, [create the Gateways and the agent key](/content/operate/iris/context-gateway/self-managed/api-examples.md). + +This walkthrough needs no agent framework or model-provider key. + +Follow one tab throughout the page. The Python package and command-line interface (CLI) are alpha prereleases pinned to the version used here. Both require Python 3.11 or later. The cURL and CLI examples use Bash and jq. + + + +**Python and CLI** + +```bash +python -m venv .venv +source .venv/bin/activate +python -m pip install redis-iris==0.1.0a4 +``` + +> [!NOTE] +> For TypeScript, use the generated client [@redis-iris/context-gateway](https://www.npmjs.com/package/@redis-iris/context-gateway). It opens Model Context Protocol (MCP) sessions with `ContextGatewayClient` and calls the Control Plane with `ContextGatewayService`. See the [developer guide](/content/develop/ai/context-engine/context-gateway/developer-guide.md). + +**HTTP** + +```bash +curl --version +jq --version +``` + +## Save connection values + +Export these values in the shell where you run the examples. Each endpoint is a Gateway's full `mcpEndpoint`, ending in `/mcp`. + +```bash +export IRIS_CONTEXT_GATEWAY_URL='' +export SEARCH_GATEWAY_URL='' +export IRIS_CONTEXT_GATEWAY_API_KEY='' +``` + +Keep the agent key out of source control and model prompts. + +The Python examples use `ContextGateway` from the unified `redis-iris` package. `ContextGateway.from_env()` reads the endpoint and the key, and `ContextGateway.call()` takes a tool's qualified name in either mode. Its `client` property exposes the SDK's MCP client. The CLI tab runs `iris context-gateway` commands directly and reads the same variables. Commands use `--json` when saving a response or processing it with jq. + +## Connect to the Gateway + +Expand an example to copy the complete program. Save it as `quickstart.py`, `quickstart.sh` (cURL), or `quickstart.cli.sh`, then run `python quickstart.py`, `bash quickstart.sh`, or `bash quickstart.cli.sh`. + +*Example set `iris_gateway_quickstart`, step `gateway-connect`, in Python (Iris), cURL (Iris), and CLI (Iris): open a session with the full mode Gateway.* + +A `401` means the key was not accepted, `403` means it has no grant for this Gateway, and `404` means no Gateway answers at the URL. A key with no `context-gateway` grant at all gets `403` for every Gateway. + +## 1. List the Gateway's tools + +List the tools the full mode Gateway serves. Each tool appears under its qualified name, the Upstream's namespace and the tool's name. + +*Example step `gateway-quickstart-list`.* + +*Expected output: the clinic directory's tools, such as a provider search and a clinic lookup, each under the directory's namespace.* + +## 2. Call a tool + +Search the directory for cardiologists by calling one listed tool by its qualified name. + +*Example step `gateway-quickstart-call`.* + +*Expected output: a JSON list of provider records with their specialty and clinic.* + +A tool's own failure comes back as a result with its error flag set. Read the failure's `reason` and `completion`, described in [Limits and errors](/content/develop/ai/context-engine/context-gateway/limits.md). + +## 3. Search for tools + +Switch to the search mode Gateway and list its tools: only `search_tools` and `run_tool` appear. Search with words a tool's name or description contains, then call the best match through `run_tool` with the `upstreamId` and `originalName` from its definition. + +*Example step `gateway-quickstart-search`.* + +*Expected output: `matched` and `returned` counts, then the matching tool definitions, then the call's result.* + +In Python, `ContextGateway.call()` with the qualified name does the search and the `run_tool` call for you. + +## 4. Read a large result through a handle + +On the search mode Gateway, list every provider as a handle, read the first page, and count providers by specialty without loading the full list. Release the handle when you finish. + +*Example step `gateway-quickstart-handle`.* + +*Expected output: a stub with the handle and a short preview, one page of providers, then a count for each specialty.* + +See [Result handles](/content/develop/ai/context-engine/context-gateway/concepts.md#result-handles). + +## Clean up + +The quickstart creates no data. Ask your administrator to revoke the agent key when you no longer need it. A key minted with the CLI expires after 90 days unless it was created with another lifetime. + +## Use Python asynchronously + +Every `ContextGateway` method has an async twin, such as `atools()`, `asearch()`, `acall()`, and `acall_as_handle()`. This standalone example lists and calls tools from an `asyncio` program. + +*Example set `iris_gateway_async`, step `gateway-async-call`, in Python (Iris).* + +## Next steps + +- [Connect with MCP](/content/develop/ai/context-engine/context-gateway/mcp.md) to configure any MCP client and sign in with your identity provider. +- [Integrate with an agent](/content/develop/ai/context-engine/context-gateway/agent-integration.md) to give an OpenAI Agents SDK or LangGraph agent the Gateway's tools. +- [Manage Gateways and Upstreams](/content/develop/ai/context-engine/context-gateway/manage-gateways.md) to add MCP servers and Context Surfaces. +- Review the [redis-iris package](https://pypi.org/project/redis-iris/) for the full Python and CLI reference. diff --git a/content/develop/ai/context-engine/context-retriever/_index.md b/content/develop/ai/context-engine/context-retriever/_index.md deleted file mode 100644 index ba40a4264b..0000000000 --- a/content/develop/ai/context-engine/context-retriever/_index.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -Title: Redis Context Retriever -alwaysopen: false -categories: -- docs -- develop -- ai -description: Expose tools to Agents to query your Redis databases. -linkTitle: Context Retriever -hideListLinks: true -weight: 20 -bannerText: Redis Context Retriever is currently available in preview. Features and behavior are subject to change. -bannerChildren: true ---- - -Give your agents structured, governed access to business data, without building custom tools for every project. - -Context Retriever lets you define your data model once. It automatically generates the retrieval tools agents call at runtime, so agents always work with accurate, live data through a controlled interface rather than guessing at SQL or calling databases directly. - -
- {{< tile-card icon="images/icon_logo/icon-data-structures-64-midnight.png" title="Concepts" description="Governed tool-calling instead of direct database access, and why it matters" url="/develop/ai/context-engine/context-retriever/concepts" >}} - {{< tile-card icon="images/icon_logo/icon-developers-64-midnight.png" title="Quick Start" description="Create a Context Retriever service on Redis Cloud" url="/operate/iris/context-retriever/create-service" >}} - {{< tile-card icon="images/dev/icons/icon-redis-code-64-duotone.png" title="Python SDK & CLI" description="Model entities and deploy tools with the redis-context-retriever package" url="https://pypi.org/project/redis-context-retriever/" >}} - {{< tile-card icon="images/dev/icons/icon-redis-insight-64-duotone.png" title="Manage Access" description="Create and manage agent keys to control what each agent can access" url="/operate/iris/context-retriever/view-admin-keys" >}} -
- -## What is Context Retriever? - -Redis Context Retriever is a schema-first context layer for AI agents that: - -- **Defines business context once**: Model your entities, fields, and relationships in one place, reused across all agents -- **Auto-generates retrieval tools**: Tools are created from your data model, not hand-coded per agent -- **Keeps agents out of your database**: Agents call generated tools; the system handles data access safely -- **Governs access by design**: Each agent key has access tags that automatically filter what data it can see -- **Exposes tools via MCP**: Agents call tools through a standard Model Context Protocol (MCP) interface at runtime - -## Why use Context Retriever? - -
-
-

For AI applications

-
    -
  • Agents reliably follow defined data paths instead of guessing at SQL
  • -
  • Live, structured context from your business data at every agent step
  • -
  • No tool zoo sprawl: one model definition, consistent tool surface
  • -
  • Access control built in: agents only see what they're allowed to see
  • -
-
-
-

For developers

-
    -
  • Python client and ctxctl CLI for modeling and deploying
  • -
  • UI-based setup available in Redis Cloud console
  • -
  • No per-agent tool engineering: the platform handles tool generation
  • -
  • Available fully managed on Redis Cloud or self-managed on your own infrastructure
  • -
-
-
- -## Quick example - -Install the Python client, which also includes the `ctxctl` CLI: - -```bash -pip install redis-context-retriever -``` - -Use the `ctxctl` CLI, the Python client, or the Redis Cloud UI to model your entities and relationships. Context Retriever uses that model to automatically generate retrieval tools that agents call at runtime through its MCP interface. Agents never access your database directly. - -See the [Redis Cloud setup guide](/content/operate/iris/context-retriever/create-service.md) to create your first Context Retriever service, or follow the [quickstart](/content/develop/ai/context-engine/context-retriever/quickstart.md) for a full CLI walkthrough: model entities, generate tools, and call one as an agent would. - -To see how a data model becomes tools before you create a service, try the [interactive demo](/content/develop/ai/context-engine/context-retriever/interactive-demo.md). - -Redis Context Retriever helps teams expose operational context to AI agents through schema-first retrieval. It models the entities, fields, keys, and relationships that matter to an agent workflow, then presents that context through a governed tool surface the agent can call at runtime. Context Retriever helps an AI Agent understand what business objects exist, how they connect, and which paths are safe to use. - -## Overview - -Production agents fail not because the model is wrong, but because the context layer breaks. Enterprise data can be fragmented across multiple different databases, and can be disorganized. Teams try to patch this with text-to-SQL, OpenAPI-to-MCP wrappers, or hand-built tools, which works for demos but creates tool zoo sprawl, SQL risk, and agents that can't reliably choose the right path in production. Redis Context Retriever gives teams a governed, schema-first surface agents can traverse safely. - -When you set up Redis Context Retriever, you model the objects that matter to your agent workflow and connect the relationships between them. You can do this either through the UI, using the [Context Surfaces Python Client](https://pypi.org/project/redis-context-retriever/), or the `ctxctl` CLI (available when you install the python client). Context Retriever will use those relationships to automatically create and deploy retrieval tools from your entity model. - -When an agent needs context during execution, it calls the MCP tools Context Retriever exposes. Instead of guessing which tool to use, or generating SQL, the agent follows the defined entity paths and gets back structured, live, operational context. - -## Get started with Redis Context Retriever - -Get started with Redis Context Retriever on Redis Cloud or join the private preview for Redis Software. - -{{< multitabs id="context-retriever-get-started" - tab1="Redis Cloud" - tab2="Redis Software (private preview)" >}} - -{{< embed-md "rc-context-retriever-get-started.md" >}} - --tab-sep- - -Redis Context Retriever is available for self-managed deployment on Kubernetes as a private preview. See [Install Context Retriever](/content/operate/iris/context-retriever/self-managed/_index.md). - -You need a license key to deploy: [contact Redis](https://redis.io/contact/) to request access. - -{{< /multitabs >}} - diff --git a/content/develop/ai/context-engine/context-retriever/concepts.md b/content/develop/ai/context-engine/context-retriever/concepts.md deleted file mode 100644 index cf5ed0562b..0000000000 --- a/content/develop/ai/context-engine/context-retriever/concepts.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -alwaysopen: false -categories: -- docs -- develop -- ai -description: Learn how Context Retriever's governed tool-calling differs from giving application code direct database access. -hideListLinks: true -linktitle: Concepts -title: Context Retriever concepts -weight: 3 ---- - -## A layer on top of your database, not a replacement for it - -Context Retriever gives an agent a fixed, predefined set of callable tools to use instead of direct query access. Your database, its schema, and its own access controls stay unchanged. Context Retriever sits between the agent and that database, as an added layer. - -![An agent calls Context Retriever through MCP tools, and Context Retriever queries the database.](/images/ai/context-engine/context-retriever-layer.svg) -{width="160"} - -## Direct data access vs. governed tool-calling - -If you've built backend services before, you're used to reasoning about data access as a permissions problem: which role can query which tables. Context Retriever reframes it as an API-design problem: which tools exist, and what does each one return. - -| | Direct database access | Context Retriever | -|:---|:---|:---| -| What the agent gets | A query interface (SQL, an ORM, a generic API) | A fixed set of tools generated from your data model | -| How you scope access | Row/column permissions, roles | Access tags on the agent's key, filtering which tools and data it can reach | -| What a bad request looks like | A malformed or overly broad query | A call to a tool that isn't defined; Context Retriever doesn't guess a path around it | -| Where you invest | Query optimization, permission grants | Modeling entities and relationships once, reused by every agent | - -## Why agents can't access raw queries - -An agent's input often includes content it didn't choose to trust. An agent that summarizes a document, reads a support ticket, or follows a web page can act on text embedded in that content (prompt injection). If that agent also holds a database connection, or can generate arbitrary SQL, injected content can turn into an arbitrary query. A fixed tool surface bounds the blast radius. Context Retriever ensures that the worst an agent can do is call a tool it was already allowed to call, with parameters that tool already accepts. - -## Key terms - -- **Entity**: A business object in your data. You define it with a key template, such as `customer:{id}`, and a set of fields. -- **Field**: An attribute of an entity. One field is the primary key. Others can be indexed for search and filtering, or linked to another entity as a relationship. -- **Index type**: How a field can be searched. Options are text, tag (exact match), numeric, and vector (semantic similarity). -- **Tool**: A callable operation generated from your entity model, such as a search or get-by-ID lookup for an entity. -- **Agent key**: Scopes which tools and data an agent can reach, through access tags. - -## How it works - -1. **Model your entities.** Define your entities, their key templates, fields, primary keys, and relationships. Use the Redis Cloud console, a model file, or the Python client. -2. **Context Retriever generates tools.** It creates a fixed set of tools from your model, such as a search and a get-by-ID tool for each entity. These tools are exposed over MCP. -3. **An agent calls a tool.** The agent uses an agent key scoped by access tags. It calls a generated tool instead of running a query, and gets back structured data. - -Changing your entity model regenerates the tool set. Add a field or relationship, and the tools available to your agents change with it. - -## Modeling is an upfront investment, but it's reusable - -Modeling your entities takes more upfront work than giving an agent a database connection. You define your objects, their key templates, and their relationships once. You don't rely on the agent to work them out from a raw query. - -That upfront work pays off. Every agent that uses the surface reuses the same tools, instead of rebuilding them per agent or per query. - -You can define fields manually, or let Context Retriever auto-detect them by scanning a sample of your existing keys. Auto-detection is faster, but it's a starting point, not a guarantee. The model that infers fields and relationships can get them wrong. Review what it generates before you rely on it. Manual modeling takes longer, but it gives you exact control over which tools get generated and what an agent can reach. - -## FAQ - -**Why can't my agent run SQL directly?** -"Run SQL" means the set of things an agent can do is as large as your schema. Untrusted content the agent is processing can also influence its next action. A fixed tool surface, call this tool with these parameters, bounds that risk to what the tool itself allows. - -**How is Context Retriever different from a regular REST API?** -Context Retriever uses the same idea: a fixed, callable surface instead of a query language. The tools are generated from your entity model, instead of hand-written per endpoint. Access is scoped per agent key through tags, rather than a single API-wide permission model. - -**What happens when an agent needs a query I haven't defined a tool for?** -The agent can't get that data. Context Retriever doesn't fall back to an open query path by design. Extend your entity model and regenerate the tool set instead. - -See the [AI agent context engine FAQ](https://redis.io/blog/faq-real-time-context-engine-agent-memory-and-retrieval/) for how this compares to text-to-SQL and OpenAPI-to-MCP approaches. - -## Next steps - -- Try the [Context Retriever interactive demo]({{< relref "/develop/ai/context-engine/context-retriever/interactive-demo" >}}) to see a data model become the tools an agent calls. -- [Create a Context Retriever service]({{< relref "/operate/iris/context-retriever/create-service" >}}) on Redis Cloud. -- [Install Context Retriever]({{< relref "/operate/iris/context-retriever/self-managed" >}}) on your own Kubernetes infrastructure. -- Follow the [quickstart]({{< relref "/develop/ai/context-engine/context-retriever/quickstart" >}}) to model entities, generate tools, and call one with the `ctxctl` CLI. -- [Manage agent keys and access tags]({{< relref "/operate/iris/context-retriever/view-admin-keys" >}}) to control what each agent can reach. diff --git a/content/develop/ai/context-engine/context-retriever/interactive-demo.md b/content/develop/ai/context-engine/context-retriever/interactive-demo.md deleted file mode 100644 index f07a1b956e..0000000000 --- a/content/develop/ai/context-engine/context-retriever/interactive-demo.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -Title: Context Retriever interactive demo -alwaysopen: false -categories: -- docs -- develop -- ai -description: See how Context Retriever turns the keys in a Redis database into MCP tools that an agent calls to answer questions. -linkTitle: Interactive demo -weight: 20 -hideListLinks: true ---- - -Context Retriever reads a data model that you define once, and generates the tools an agent uses to query your data. The agent calls those tools over MCP (Model Context Protocol) and never connects to Redis itself. - -This demo runs in your browser with sample data from a food delivery app: customers, restaurants, and orders. It doesn't connect to a Context Retriever service, but the tool names, arguments, and responses follow the same shape a real service returns. - -{{< context-retriever-demo >}} - -## What the demo shows - -1. **Keys in Redis.** Each business object is a JSON document, and its key name starts with the object type, such as `restaurant:r201`. -2. **Data model to tools.** A key template such as `restaurant:{id}` tells Context Retriever which keys belong to an entity. The fields you index decide which tools it generates and which arguments those tools accept. -3. **Ask over MCP.** The agent lists the tools, picks one or more for each question, and combines the results into an answer. - -Try removing the index from a field in step 2, then ask a question that needs it. The server rejects the call, because the tool no longer accepts that field. - -## Next steps - -- [Create a Context Retriever service](/content/operate/iris/context-retriever/create-service.md) on Redis Cloud. -- Read the [Context Retriever overview](/content/develop/ai/context-engine/context-retriever/_index.md). diff --git a/content/develop/ai/context-engine/context-retriever/quickstart.md b/content/develop/ai/context-engine/context-retriever/quickstart.md deleted file mode 100644 index 207c164fe0..0000000000 --- a/content/develop/ai/context-engine/context-retriever/quickstart.md +++ /dev/null @@ -1,177 +0,0 @@ ---- -alwaysopen: false -categories: -- docs -- develop -- ai -description: Model a data source, generate MCP tools, and call them from an agent using the ctxctl CLI. -hideListLinks: true -linktitle: Quickstart -title: Redis Context Retriever quickstart -weight: 10 -bannerText: Redis Context Retriever is currently available in preview. Features and behavior are subject to change. ---- - -Use this quickstart to model a Redis data source as a context surface, generate the retrieval tools that Context Retriever exposes, and call one of those tools as an agent would. - -This quickstart walks you through: - -1. [Install the Python client](#install-the-python-client) -1. [Sign in and create an admin key](#sign-in-and-create-an-admin-key) -1. [Define your data model](#define-your-data-model) -1. [Load sample data](#load-sample-data) -1. [Create a surface](#create-a-surface) -1. [Create an agent key](#create-an-agent-key) -1. [List the generated tools](#list-the-generated-tools) -1. [Call a tool](#call-a-tool) -1. [Clean up](#clean-up) - -This quickstart uses Redis Cloud. If you're running Context Retriever self-managed, see [Install Context Retriever]({{< relref "/operate/iris/context-retriever/self-managed" >}}) instead; every step after sign-in and admin-key creation is the same either way. - -## Before you begin - -To complete this quickstart, you need: - -* A Redis Cloud account. -* A Redis Cloud database. If you don't have one, see [Create a database]({{< relref "/operate/rc/databases/create-database" >}}). -* Python 3.11 or later and `pip`. -* `redis-cli`, to load sample data. See [Install redis-cli]({{< relref "/operate/oss_and_stack/install/install-stack/install-redis-cli" >}}). - -## Install the Python client - -The Python client includes the `ctxctl` CLI, which you use to model data, manage keys, and call the tools Context Retriever generates. - -```bash -pip install redis-context-retriever -``` - -## Sign in and create an admin key - -1. Start a session against your Redis Cloud account: - - ```bash - ctxctl auth login -u - ``` - -1. Create an admin key. An admin key authorizes operations such as creating surfaces and agent keys. - - ```bash - ctxctl --output json admin create --name "quickstart-admin" - ``` - -1. Save the returned key. Export it so later commands can use it: - - ```bash - export CTX_ADMIN_KEY='' - ``` - -This quickstart uses a Redis Cloud account for sign-in and admin-key creation. If you're running Context Retriever self-managed instead, see [Install Context Retriever]({{< relref "/operate/iris/context-retriever/self-managed" >}}) to bootstrap your first admin key. Every other step in this quickstart applies to both. - -## Define your data model - -Context Retriever generates tools from a data model, not from CLI flags entered one field at a time. Define your entities in a Python file. - -Create `models.py`: - -```python -from context_surfaces.context_model import ContextField, ContextModel - -class Customer(ContextModel): - __redis_key_template__ = "customer:{id}" - - id: str = ContextField(description="Unique customer ID", is_key_component=True) - name: str = ContextField(description="Customer name", index="text") - email: str = ContextField(description="Customer email address", index="tag") -``` - -## Load sample data - -Load a few customer hashes that match the model, so the tool call later in this quickstart returns predictable results: - -```bash -redis-cli -h -p -a '' HSET customer:1 id 1 name "Jane Doe" email "jane.doe@example.com" -redis-cli -h -p -a '' HSET customer:2 id 2 name "John Smith" email "john.smith@example.com" -redis-cli -h -p -a '' HSET customer:3 id 3 name "Jane Roberts" email "jane.roberts@example.com" -``` - -If you already have data that matches this shape, you can skip this step and use your own keys instead. - -## Create a surface - -1. Create a context surface from your model file, pointing it at your Redis Cloud database: - - ```bash - ctxctl --output json surface create \ - --name "quickstart-surface" \ - --description "Quickstart context surface" \ - --models ./models.py \ - --redis-addr : \ - --redis-password '' \ - --admin-key "$CTX_ADMIN_KEY" - ``` - -1. Save the returned surface ID: - - ```bash - export CTX_SURFACE_ID='' - ``` - -1. Confirm the surface was created: - - ```bash - ctxctl surface describe "$CTX_SURFACE_ID" --admin-key "$CTX_ADMIN_KEY" - ``` - -## Create an agent key - -An agent key authorizes an agent to call the tools generated for a surface. - -1. Create one scoped to the surface you just created: - - ```bash - ctxctl --output json agent create \ - --surface-id "$CTX_SURFACE_ID" \ - --name "quickstart-agent" \ - --admin-key "$CTX_ADMIN_KEY" - ``` - -1. Save the returned key: - - ```bash - export CTX_AGENT_KEY='' - ``` - -## List the generated tools - -As the admin, confirm which tools Context Retriever generated from your model: - -```bash -ctxctl tools list --agent-key "$CTX_AGENT_KEY" -``` - -The list includes tools such as a search tool and a get-by-ID tool for each entity you defined. - -## Call a tool - -Call a generated tool directly, using the agent key instead of the admin key: - -```bash -ctxctl tools call search_customer_by_text --agent-key "$CTX_AGENT_KEY" --args '{"query": "jane", "limit": 5}' -``` - -> [!NOTE] -> **What to expect:** A JSON result containing `Jane Doe` and `Jane Roberts`, the two sample customers whose `name` field matches `jane`. The agent never sends a database query directly. It calls a tool that Context Retriever generated from your model. - -## Clean up - -Delete the surface you created, which also revokes its agent keys: - -```bash -ctxctl surface delete "$CTX_SURFACE_ID" --admin-key "$CTX_ADMIN_KEY" --confirm -``` - -## Next steps - -* Read [Context Retriever concepts]({{< relref "/develop/ai/context-engine/context-retriever/concepts" >}}) to understand tools, providers, and access tags. -* [Create a Context Retriever service in Redis Cloud]({{< relref "/operate/iris/context-retriever/create-service" >}}) using the console instead of the CLI. -* [Manage admin keys]({{< relref "/operate/iris/context-retriever/view-admin-keys" >}}). diff --git a/content/develop/ai/context-engine/data-integration/_index.md b/content/develop/ai/context-engine/data-integration/_index.md index b97c0664ff..63604fe15a 100644 --- a/content/develop/ai/context-engine/data-integration/_index.md +++ b/content/develop/ai/context-engine/data-integration/_index.md @@ -40,7 +40,7 @@ Redis Data Integration (RDI) is a pipeline service, available fully managed on R
  • Eliminate stale data: agents always see current business state
  • Millisecond query performance from Redis instead of slow source DB calls
  • No cache invalidation logic to write or maintain
  • -
  • Combine with Context Retriever for governed, always-fresh agent tools
  • +
  • Combine with Context Gateway Context Surfaces for always-fresh agent tools
  • diff --git a/content/develop/ai/context-engine/data-integration/interactive-demo.md b/content/develop/ai/context-engine/data-integration/interactive-demo.md index 2a86d38fb2..2ce44bc2ad 100644 --- a/content/develop/ai/context-engine/data-integration/interactive-demo.md +++ b/content/develop/ai/context-engine/data-integration/interactive-demo.md @@ -13,7 +13,7 @@ weight: 10 Redis Data Integration (RDI) keeps Redis in sync with a relational database, so your app and its agents read from Redis instead of the source. This demo follows a food delivery app that keeps its data in PostgreSQL. You deploy the pipeline, run SQL statements against the source, and see each change reach Redis. -The demo runs in your browser and doesn't connect to an RDI pipeline or a database. The configuration, job file, key names, and `redis-di status` output follow the [RDI documentation](/content/integrate/redis-data-integration/_index.md). The data and timings are illustrative. The customers, restaurants, and orders are the same sample data as the [Context Retriever interactive demo](/content/develop/ai/context-engine/context-retriever/interactive-demo.md). +The demo runs in your browser and doesn't connect to an RDI pipeline or a database. The configuration, job file, key names, and `redis-di status` output follow the [RDI documentation](/content/integrate/redis-data-integration/_index.md). The data and timings are illustrative. The customers, restaurants, and orders are the same sample data as the [Redis Iris interactive demo](/content/develop/ai/context-engine/concepts/interactive-demo.md). {{< rdi-demo >}} diff --git a/content/develop/ai/context-engine/langcache/interactive-demo.md b/content/develop/ai/context-engine/langcache/interactive-demo.md index 21e1ebf97a..388852979a 100644 --- a/content/develop/ai/context-engine/langcache/interactive-demo.md +++ b/content/develop/ai/context-engine/langcache/interactive-demo.md @@ -13,7 +13,7 @@ weight: 5 Redis LangCache stores the answers your LLM generates and returns them when someone asks a similar question, so your app doesn't call the LLM again. This demo follows the help assistant of a food delivery app. You choose which questions its customers ask, and the demo shows each search, each cache hit or miss, and what the cache stores. -The demo runs in your browser and doesn't connect to a LangCache service or an LLM. Its requests and responses follow the shapes of the [LangCache API](/content/develop/ai/context-engine/langcache/api-reference.md). The similarity scores, answers, and timings are illustrative. The customers and restaurants come from the same sample data as the [Context Retriever interactive demo](/content/develop/ai/context-engine/context-retriever/interactive-demo.md). +The demo runs in your browser and doesn't connect to a LangCache service or an LLM. Its requests and responses follow the shapes of the [LangCache API](/content/develop/ai/context-engine/langcache/api-reference.md). The similarity scores, answers, and timings are illustrative. The customers and restaurants come from the same sample data as the [Redis Iris interactive demo](/content/develop/ai/context-engine/concepts/interactive-demo.md). {{< langcache-demo >}} diff --git a/content/develop/ai/redisvl/interactive-demo.md b/content/develop/ai/redisvl/interactive-demo.md index 5c810b86d9..0fbdd1e737 100644 --- a/content/develop/ai/redisvl/interactive-demo.md +++ b/content/develop/ai/redisvl/interactive-demo.md @@ -6,7 +6,7 @@ weight: 3 RedisVL builds Redis queries from Python objects. This demo runs three of its query types on the restaurants of a food delivery app: a vector query that searches by meaning, a filter query, and a range query that limits the search to a distance threshold. For each query, it shows the RedisVL code, the `FT.SEARCH` command RedisVL sends, and the results. -The demo runs in your browser and doesn't connect to Redis or run an embedding model. Its results are the ones Redis returned for these queries, with descriptions embedded by `sentence-transformers/all-mpnet-base-v2`, and its code and commands are what RedisVL 0.27.1 builds. The restaurants are the same sample data as the [Context Retriever interactive demo]({{< relref "/develop/ai/context-engine/context-retriever/interactive-demo" >}}). +The demo runs in your browser and doesn't connect to Redis or run an embedding model. Its results are the ones Redis returned for these queries, with descriptions embedded by `sentence-transformers/all-mpnet-base-v2`, and its code and commands are what RedisVL 0.27.1 builds. The restaurants are the same sample data as the [Redis Iris interactive demo]({{< relref "/develop/ai/context-engine/concepts/interactive-demo" >}}). {{< redisvl-demo >}} diff --git a/content/develop/ai/search-and-query/query/interactive-demo.md b/content/develop/ai/search-and-query/query/interactive-demo.md index d12c2e51aa..ca3598e0ce 100644 --- a/content/develop/ai/search-and-query/query/interactive-demo.md +++ b/content/develop/ai/search-and-query/query/interactive-demo.md @@ -17,7 +17,7 @@ weight: 11 Redis Search lets you query data through an index you create. This demo uses restaurant and order data from a food delivery app, stored as hashes. It starts by showing the data and a possible index. Next, three kinds of queries are demonstrated: full-text search, filter and sort, and aggregation. For each query, it shows the command, the matching data, and the exact reply. -The demo runs in your browser and doesn't connect to Redis. Its commands and replies were recorded from Redis, and each command works as shown in `redis-cli` once you create the indexes and add the data. The restaurants and orders are the same sample data as the [Context Retriever interactive demo](/content/develop/ai/context-engine/context-retriever/interactive-demo.md). +The demo runs in your browser and doesn't connect to Redis. Its commands and replies were recorded from Redis, and each command works as shown in `redis-cli` once you create the indexes and add the data. The restaurants and orders are the same sample data as the [Redis Iris interactive demo](/content/develop/ai/context-engine/concepts/interactive-demo.md). {{< search-demo >}} diff --git a/content/develop/whats-new/_index.md b/content/develop/whats-new/_index.md index 46c37ed9f1..726d79397c 100644 --- a/content/develop/whats-new/_index.md +++ b/content/develop/whats-new/_index.md @@ -22,8 +22,8 @@ weight: 10 ### Redis Iris (Context Engine) -- Added self-managed deployment documentation for all four Redis Iris services: [LangCache](/content/operate/iris/langcache/self-managed/_index.md), [Agent Memory](/content/operate/iris/agent-memory/self-managed/_index.md), [Context Retriever](/content/develop/ai/context-engine/context-retriever/install/_index.md), and [Data Integration](/content/integrate/redis-data-integration/_index.md) -- Updated [LangCache](/content/develop/ai/context-engine/langcache/_index.md), [Agent Memory](/content/develop/ai/context-engine/agent-memory/_index.md), [Context Retriever](/content/develop/ai/context-engine/context-retriever/_index.md), and [Data Integration](/content/develop/ai/context-engine/data-integration/_index.md) quickstarts with fully managed and self-managed deployment paths +- Added self-managed deployment documentation for all four Redis Iris services: [LangCache](/content/operate/iris/langcache/self-managed/_index.md), [Agent Memory](/content/operate/iris/agent-memory/self-managed/_index.md), Context Retriever, and [Data Integration](/content/integrate/redis-data-integration/_index.md) +- Updated [LangCache](/content/develop/ai/context-engine/langcache/_index.md), [Agent Memory](/content/develop/ai/context-engine/agent-memory/_index.md), Context Retriever, and [Data Integration](/content/develop/ai/context-engine/data-integration/_index.md) quickstarts with fully managed and self-managed deployment paths - Added [Redis Iris concepts](/content/develop/ai/context-engine/concepts/_index.md) explaining how agents get context from Redis Iris - Added new [AI agent builder](/content/develop/ai/agent-builder/_index.md) templates for knowledge assistants (RAG) and Redis Iris conversational assistants diff --git a/content/embeds/rc-context-retriever-get-started.md b/content/embeds/rc-context-retriever-get-started.md deleted file mode 100644 index 771545170f..0000000000 --- a/content/embeds/rc-context-retriever-get-started.md +++ /dev/null @@ -1,5 +0,0 @@ -To set up a Redis Context Retriever on Redis Cloud, you need a database on Redis Cloud that already has relevant data. If you use a relational database, use [Redis Data Integration (RDI)]({{< relref "/operate/rc/rdi" >}}) to ingest data into a Redis Cloud database. - -When you have a database, [Create a context retriever service]({{< relref "/operate/iris/context-retriever/create-service" >}}) for your database on Redis Cloud. - -After you set up Context Retriever, you can [view your service]({{< relref "/operate/iris/context-retriever/view-service" >}}). See the [Context Surfaces Python Client](https://pypi.org/project/redis-context-retriever/) for more information on how to call your tools. \ No newline at end of file diff --git a/content/operate/_index.md b/content/operate/_index.md index 74108fb58a..309596ba51 100644 --- a/content/operate/_index.md +++ b/content/operate/_index.md @@ -84,7 +84,7 @@ Services available on cloud or self-managed infrastructure for AI applications.
    1. }}">Overview
    2. }}">Enable Agent Memory
    3. -
    4. }}">Enable Context Retriever
    5. +
    6. }}">Deploy Context Gateway
    7. }}">Enable LangCache
    diff --git a/content/operate/iris/_index.md b/content/operate/iris/_index.md index 60ca4f2f85..11cb775d07 100644 --- a/content/operate/iris/_index.md +++ b/content/operate/iris/_index.md @@ -21,7 +21,7 @@ Use this section to deploy, configure, and operate Redis Iris services. Develope
    {{< tile-card icon="images/dev/icons/icon-redis-agent-memory-64-duotone.png" title="Redis Agent Memory" description="Create and manage a service on Redis Cloud or deploy it on your own infrastructure" url="/operate/iris/agent-memory" >}} - {{< tile-card icon="images/dev/icons/icon-redis-context-retriever-64-duotone.png" title="Redis Context Retriever" description="Create and manage governed retrieval tools for AI agents" url="/operate/iris/context-retriever" >}} + {{< tile-card icon="images/dev/icons/icon-redis-iris-64-duotone.png" title="Redis Context Gateway" description="Deploy one MCP endpoint for your agents' tools on your own infrastructure" url="/operate/iris/context-gateway" >}}
    ## Deployment options @@ -33,11 +33,10 @@ Redis Iris services are available as managed services on Redis Cloud. All Redis Create and manage Redis Iris services through the Redis Cloud console without deploying the supporting infrastructure yourself. - [Create a Redis Agent Memory service]({{< relref "/operate/iris/agent-memory/create-service" >}}) -- [Create a Redis Context Retriever service]({{< relref "/operate/iris/context-retriever/create-service" >}}) ### Self-managed Deploy Redis Iris services on Kubernetes when you need to operate them on your own infrastructure. - [Self-managed Redis Agent Memory]({{< relref "/operate/iris/agent-memory/self-managed" >}}) -- [Self-managed Redis Context Retriever]({{< relref "/operate/iris/context-retriever/self-managed" >}}) +- [Self-managed Redis Context Gateway]({{< relref "/operate/iris/context-gateway/self-managed" >}}) diff --git a/content/operate/iris/context-gateway/_index.md b/content/operate/iris/context-gateway/_index.md new file mode 100644 index 0000000000..23ddffa213 --- /dev/null +++ b/content/operate/iris/context-gateway/_index.md @@ -0,0 +1,26 @@ +--- +title: Redis Context Gateway +description: Deploy and operate Redis Context Gateway on your own infrastructure. +linkTitle: Context Gateway +alwaysopen: false +categories: +- docs +- operate +- iris +weight: 22 +hideListLinks: true +aliases: +- /operate/iris/context-retriever/ +- /operate/iris/context-retriever/create-service/ +- /operate/iris/context-retriever/view-service/ +- /operate/iris/context-retriever/view-admin-keys/ +- /operate/rc/context-engine/context-retriever/ +--- + +Deploy Redis Context Gateway on Kubernetes to give agents one Model Context Protocol (MCP) endpoint for the tools of any MCP server, plus tools over your Redis data. + +## Self-managed + +[Start with the self-managed deployment guide](/content/operate/iris/context-gateway/self-managed/_index.md). + +After setup, use the [developer quickstart](/content/develop/ai/context-engine/context-gateway/quickstart.md) to list and call your first tools. The [developer guides](/content/develop/ai/context-engine/context-gateway/_index.md) cover Upstream registration, tool policies, and agent integration. diff --git a/content/operate/iris/context-gateway/self-managed/_index.md b/content/operate/iris/context-gateway/self-managed/_index.md new file mode 100644 index 0000000000..14dbdf9887 --- /dev/null +++ b/content/operate/iris/context-gateway/self-managed/_index.md @@ -0,0 +1,74 @@ +--- +Title: Self-managed Redis Context Gateway +alwaysopen: false +categories: +- docs +- operate +- iris +description: Deploy, configure, and operate Redis Context Gateway on a self-managed Kubernetes cluster. +linkTitle: Self-managed +weight: 40 +hideListLinks: true +aliases: +- /operate/iris/context-retriever/self-managed/ +--- + +The `redis-context-gateway` Helm chart deploys one tenant's Context Gateway Control Plane and Data Plane. You provide the Identity Service release, the Redis databases, and the license. The chart does not install Identity Service, Redis, Vault, or Secrets Store CSI (Container Storage Interface) Driver resources. + +Agents call the Data Plane's Model Context Protocol (MCP) endpoint, described in the [MCP API reference](/content/develop/ai/context-engine/context-gateway/api-reference.md). The [Control Plane API reference](/content/operate/iris/context-gateway/self-managed/control-plane-api-reference.md) documents the self-managed administration endpoints for Gateways and Upstreams. + +The chart version that matches these guides is not published yet. Set the chart version and image tags from the approved release manifest when it becomes available. + + + +## What you deploy + +| Component | Purpose | Service port | +| --- | --- | --- | +| Control Plane | Create Gateways, register Upstreams, write credentials, and validate agent-key grants. | `9100` | +| Data Plane | Serve each Gateway's MCP endpoint and call its Upstreams. | `8083` | +| Identity Service | Issue agent keys and check them for the Data Plane. Installed as its own release. | `9200` | +| Metadata Redis | Store Gateway and Upstream records. | Customer-provided | +| Credential storage | Store Upstream credentials: encrypted Redis by default, or Vault or AWS Secrets Manager. | Customer-provided | +| Registered databases | Context Surface data, and the Gateway database for cached results and handles. | Customer-provided | + +Use separately managed Redis deployments for metadata, credentials, and customer data. Use a separate Redis access control list (ACL) user for credentials. + +### How the components work together + +| Flow | Caller | Service | Backing store | +| --- | --- | --- | --- | +| Gateway administration | Platform admin | Control Plane | Metadata Redis and credential storage | +| Agent-key administration | Platform admin | Identity Service | Identity Service metadata Redis | +| Grant validation | Identity Service | Control Plane | Metadata Redis | +| Tool listing and calls | Agent | Data Plane | Metadata Redis, credential storage, and Upstreams | + +1. Platform admins create Gateways and register Upstreams through the Control Plane, then mint agent keys through the Identity Service. +1. The Identity Service asks the Control Plane to confirm that a Gateway exists before it mints a key for it. +1. Agents call a Gateway's MCP endpoint on the Data Plane with an agent key or an identity provider token. +1. The Data Plane checks the key with the Identity Service, reads the Gateway's records, and calls the Upstream with the Upstream's credential. + +### API surfaces + +| API surface | Endpoint | Purpose | +| --- | --- | --- | +| Gateway MCP | `/v1/gateways/{gatewayId}/mcp` | The Streamable HTTP MCP endpoint for one Gateway. | +| Protected resource metadata | `/.well-known/oauth-protected-resource/v1/gateways/{gatewayId}/mcp` | Names the identity provider for MCP client sign-in. | +| Control Plane | `/v1/gateways` | Administration of Gateways and Upstreams. | +| Health | `/v1/health/live`, `/v1/health/ready` | Liveness and readiness on both planes. | +| Identity Service | `/v1/api-keys` | Administration of agent keys. | + +## Get started + +1. Review the [prerequisites](/content/operate/iris/context-gateway/self-managed/prerequisites.md). +2. Prepare the [configuration](/content/operate/iris/context-gateway/self-managed/configuration.md). +3. [Deploy the chart](/content/operate/iris/context-gateway/self-managed/deploy.md). +4. [Load administrative credentials](/content/operate/iris/context-gateway/self-managed/authentication.md). +5. [Create a Gateway and an agent key](/content/operate/iris/context-gateway/self-managed/api-examples.md). + +## Manage the deployment + +- [Manage Gateways and Upstreams](/content/develop/ai/context-engine/context-gateway/manage-gateways.md) and [Context Surfaces](/content/develop/ai/context-engine/context-gateway/manage-context-surfaces.md) through the Control Plane. +- [Rotate or revoke agent keys](/content/operate/iris/context-gateway/self-managed/api-examples.md#rotate-or-revoke-agent-keys). +- [Check health, rotate credentials, back up, and upgrade](/content/operate/iris/context-gateway/self-managed/operations.md). +- [Troubleshoot the deployment](/content/operate/iris/context-gateway/self-managed/reference.md#troubleshooting). diff --git a/content/operate/iris/context-gateway/self-managed/api-examples.md b/content/operate/iris/context-gateway/self-managed/api-examples.md new file mode 100644 index 0000000000..d4c1191495 --- /dev/null +++ b/content/operate/iris/context-gateway/self-managed/api-examples.md @@ -0,0 +1,124 @@ +--- +Title: Self-managed Context Gateway API examples +alwaysopen: false +categories: +- docs +- operate +- iris +description: Create the Gateways and the sample Upstream, mint an agent key, verify agent access, and rotate or revoke keys. +linkTitle: API examples +weight: 70 +hideListLinks: true +--- + +Create the two Gateways the [developer quickstart](/content/develop/ai/context-engine/context-gateway/quickstart.md) uses, register the sample clinic directory on both, issue one agent key, and hand the connection values to a developer. + +Before you begin, [deploy the chart](/content/operate/iris/context-gateway/self-managed/deploy.md) and [load the admin token](/content/operate/iris/context-gateway/self-managed/authentication.md#control-plane-admin-token). + +## Set connection values + +Forward the Control Plane and the Identity Service in separate terminals, as in [Deploy](/content/operate/iris/context-gateway/self-managed/deploy.md#5-verify-pods-and-health), then export the connection values: + +```bash +export IRIS_CONTEXT_GATEWAY_SERVICE_URL='http://localhost:9100' +export IRIS_IDENTITY_URL='http://localhost:9200' +export IRIS_IDENTITY_TOKEN='' +export IRIS_IDENTITY_TENANT='' +umask 077 +``` + +`IRIS_CONTEXT_GATEWAY_SERVICE_TOKEN` holds the admin token. For application deployment, use the Control Plane and Identity Service endpoints your platform exposes. + +## Create the Gateways + +Create a `full` mode Gateway, and a `search` mode Gateway with a Gateway database so that it can hold result handles: + +```bash +curl --fail-with-body --silent --show-error \ + -X POST "$IRIS_CONTEXT_GATEWAY_SERVICE_URL/v1/gateways" \ + -H "Authorization: Bearer $IRIS_CONTEXT_GATEWAY_SERVICE_TOKEN" \ + -H 'Content-Type: application/json' \ + -d '{"displayName": "Front desk", "toolExposureMode": "full"}' > front-desk.json + +curl --fail-with-body --silent --show-error \ + -X POST "$IRIS_CONTEXT_GATEWAY_SERVICE_URL/v1/gateways" \ + -H "Authorization: Bearer $IRIS_CONTEXT_GATEWAY_SERVICE_TOKEN" \ + -H 'Content-Type: application/json' \ + -d '{"displayName": "Operations", "toolExposureMode": "search", "databaseId": ""}' > operations.json + +export FRONT_DESK_ID="$(jq -er '.id' front-desk.json)" +export OPERATIONS_ID="$(jq -er '.id' operations.json)" +jq '{id, mcpEndpoint, toolExposureMode}' front-desk.json operations.json +``` + +Result handles also need `result_handles.enabled` on the Data Plane. See [Result cache and result handles](/content/operate/iris/context-gateway/self-managed/configuration.md#result-cache-and-result-handles). + + + +## Register the sample Upstream + +Load the sample clinic directory into a registered Surface database and register it as a Context Surface on both Gateways. + +*Example set `iris_gateway_setup`, steps `gateway-seed-data` and `gateway-create-surface`, in cURL (Iris) and CLI (Iris).* + +## Mint an agent key + +Issue one key with a `use` grant on each Gateway: + +```bash +jq -n --arg tenant "$IRIS_IDENTITY_TENANT" --arg a "$FRONT_DESK_ID" --arg b "$OPERATIONS_ID" '{ + name: "front-desk-agent", + grants: [ + {tenant: $tenant, product: "context-gateway", resourceType: "gateway", resourceId: $a, actions: ["use"]}, + {tenant: $tenant, product: "context-gateway", resourceType: "gateway", resourceId: $b, actions: ["use"]} + ] +}' > agent-grant.json + +curl --fail-with-body --silent --show-error \ + -X POST "$IRIS_IDENTITY_URL/v1/api-keys" \ + -H "Authorization: Bearer $IRIS_IDENTITY_TOKEN" \ + -H 'Content-Type: application/json' \ + --data-binary @agent-grant.json > agent-key.json +export IRIS_CONTEXT_GATEWAY_API_KEY="$(jq -er '.token' agent-key.json)" +``` + +> [!WARNING] +> The response's `token` is the agent key, and it is returned only on creation or rotation. Save it in your secret manager and keep its `keyId`. + +## Verify agent access + +*Example set `iris_gateway_setup`, step `gateway-verify`, in cURL (Iris) and CLI (Iris): a Model Context Protocol (MCP) `tools/list` request to each Gateway that prints the tool names.* + +## Rotate or revoke agent keys + +Rotate a key to issue a new token for the same grants. The grace period keeps the old token valid while applications switch: + +```bash +curl --fail-with-body --silent --show-error \ + -X POST "$IRIS_IDENTITY_URL/v1/api-keys//rotate?graceSeconds=3600" \ + -H "Authorization: Bearer $IRIS_IDENTITY_TOKEN" +``` + +Revoke a key you no longer need: + +```bash +curl --fail-with-body --silent --show-error \ + -X DELETE "$IRIS_IDENTITY_URL/v1/api-keys/" \ + -H "Authorization: Bearer $IRIS_IDENTITY_TOKEN" +``` + +A revoked key stops working within the Identity Service cache lifetime, 300 seconds by default. + +## Hand off to a developer + +Give the developer the front desk Gateway's `mcpEndpoint` as `IRIS_CONTEXT_GATEWAY_URL`, the operations Gateway's `mcpEndpoint` as `SEARCH_GATEWAY_URL`, and the agent key as `IRIS_CONTEXT_GATEWAY_API_KEY`. Give platform teams who register Upstreams the Control Plane URL and admin token for [Manage Gateways and Upstreams](/content/develop/ai/context-engine/context-gateway/manage-gateways.md). Remove the local key-response files after you save the token. + +## Use the CLI instead + +The `iris context-gateway` command-line interface (CLI) in the [redis-iris package](https://pypi.org/project/redis-iris/) runs the same steps. With `IRIS_IDENTITY_URL` and `IRIS_IDENTITY_TOKEN` exported, create a Gateway, mint its agent key, and write the developer's environment file in one command: + +```bash +iris context-gateway service create --name front-desk --write-env .env +``` + +`--write-env` writes `IRIS_CONTEXT_GATEWAY_URL`, `IRIS_CONTEXT_GATEWAY_ID`, and `IRIS_CONTEXT_GATEWAY_API_KEY` to the file with mode `0600`, and the key is never printed. Add `--tool-exposure-mode search` and `--database-id ` for the operations Gateway. `service key`, `service keys`, and `service revoke` mint, list, and revoke agent keys. See [Manage Gateways and Upstreams](/content/develop/ai/context-engine/context-gateway/manage-gateways.md) for every operation. diff --git a/content/operate/iris/context-gateway/self-managed/authentication.md b/content/operate/iris/context-gateway/self-managed/authentication.md new file mode 100644 index 0000000000..c7beeedecd --- /dev/null +++ b/content/operate/iris/context-gateway/self-managed/authentication.md @@ -0,0 +1,88 @@ +--- +Title: Authentication and authorization +alwaysopen: false +categories: +- docs +- operate +- iris +description: Configure Context Gateway Control Plane credentials, agent-key grants, and identity provider sign-in for agents. +linkTitle: Authentication and authorization +weight: 60 +hideListLinks: true +--- + +## Credentials + +| Credential | Who presents it to whom | Secret / key | Auto-generated? | What it unlocks | +| --- | --- | --- | --- | --- | +| Control Plane admin token | Operator → Control Plane (`:9100`) | The Secret in `controlplane.adminToken` / `token` | No | Gateway and Upstream management: `/v1/gateways` | +| Control Plane internal token | Identity Service → Control Plane | The Secret in `controlplane.internalToken` / `token` | No | Checking that the Gateway in a grant exists | +| Data Plane runtime token | Data Plane → Identity Service | The Secret in `dataplane.runtimeToken` / `token` | No | Checking agent keys for `context-gateway` | +| Identity Service control token | Operator → Identity Service (`:9200`) | Set on the Identity Service release | Depends on the Identity Service release | Creating, listing, rotating, and revoking agent keys | +| Agent key | Agent → Data Plane (`:8083`) | None. Returned once when minted or rotated | No | Listing and calling tools on the Gateways its grants name | +| Identity provider access token | Agent → Data Plane (`:8083`) | None. Issued by your identity provider | No | The Gateways its role or subject grants name | + +The admin, internal, and runtime tokens must be distinct. An agent key gets `401` from the Control Plane. No credential that an agent presents is sent to an Upstream. + + + +## Control Plane admin token + +Every Gateway and Upstream operation needs the admin token in `Authorization: Bearer `. The health routes need no credential. + +Load the token from your Secret before you call the Control Plane: + +```bash +export IRIS_CONTEXT_GATEWAY_SERVICE_TOKEN="$(kubectl -n get secret \ + -o jsonpath='{.data.token}' | base64 --decode)" +``` + +## Gateway authorization and grants + +An agent key needs a grant on each Gateway it uses: + +| Grant field | Value | +| --- | --- | +| `tenant` | The deployment's `tenantId`. | +| `product` | `context-gateway` | +| `resourceType` | `gateway` | +| `resourceId` | The Gateway ID. | +| `actions` | `["use"]` or `["full"]` | + +One grant covers listing and calling every tool on the Gateway, including tools that change data. Model Context Protocol (MCP) tool annotations do not limit it. + +The Identity Service mints a key only for a Gateway that exists in the tenant and is not being deleted. A revoked key stops working within the Identity Service cache lifetime, 300 seconds by default. + +The [API examples](/content/operate/iris/context-gateway/self-managed/api-examples.md#mint-an-agent-key) show the complete request. + +## Identity provider sign-in + +Turn on sign-in to let MCP clients use access tokens from your identity provider (IdP). The Data Plane validates each token itself and serves protected resource metadata so that clients can find the IdP. IdP tokens are accepted only in `Authorization: Bearer`, never in `X-Api-Key`, and they never reach an Upstream. + +Set these keys under `dataplane.config.auth.oidc`: + +| Key | Value | +| --- | --- | +| `enabled` | `true` | +| `issuer` | Your IdP's issuer URL. | +| `audience` | The audience that names the Gateway API, or each Gateway's exact URL. | +| `jwks_uri` | Your IdP's JSON Web Key Set (JWKS) URL. | +| `user_id_claim` | The claim that identifies a user. `sub` by default, `oid` for Microsoft Entra ID. | +| `role_mappings` | Maps IdP role or group claim values to Iris roles, such as `gateway_user`. | + +Set `dataplane.config.oauth.public_base_url` to the Data Plane's public origin, the same value as `publicBaseURL`. The issuer and key set URLs must use HTTPS. + +### Grant signed-in users access + +Grant signed-in users access in either way: + +- Map an IdP group to the `gateway_user` role. The role grants `use` on every Gateway in the tenant. +- Assign Identity Service subject grants to individual users for selected Gateways, with `PUT /v1/subject-grants` on the Identity Service. A subject grant names the IdP's issuer, the user's `user_id_claim` value, and the same product, resource type, Gateway ID, and `use` action as an agent-key grant. + +A specific subject grant cannot narrow the all-Gateway role, and removing a subject grant does not remove access that a role gives. To restrict a user to selected Gateways, don't map them to `gateway_user`. + + + +## Rotate credentials + +See [Operations](/content/operate/iris/context-gateway/self-managed/operations.md#rotate-credentials). diff --git a/content/operate/iris/context-gateway/self-managed/configuration.md b/content/operate/iris/context-gateway/self-managed/configuration.md new file mode 100644 index 0000000000..2c1b188a54 --- /dev/null +++ b/content/operate/iris/context-gateway/self-managed/configuration.md @@ -0,0 +1,137 @@ +--- +Title: Configure self-managed Context Gateway +alwaysopen: false +categories: +- docs +- operate +- iris +description: Prepare Helm values, file references, credential storage, the database registry, Context Surface databases, and optional features for Context Gateway. +linkTitle: Configuration +weight: 30 +hideListLinks: true +--- + +The chart reads non-secret settings from Helm values and every credential from a mounted file. Prepare `gateway-values.yaml` and the Secrets it references before you [deploy](/content/operate/iris/context-gateway/self-managed/deploy.md). + +## Helm values + +| Value | Purpose | +| --- | --- | +| `tenantId` | The deployment's tenant. Must equal the Identity Service's single tenant ID. | +| `publicBaseURL` | The Data Plane's public origin, used in every Gateway's `mcpEndpoint`. | +| `identityService.baseURL` | The Identity Service runtime API that checks agent keys. | +| `identityService.allowInsecureTransport` | `true` for an in-cluster `http://` Identity Service URL. Default `false`. | +| `profile` | `prod` (default) or `dev` logging. | +| `security.profile` | `standard` (default) or `fips`. See [Configure security and tracing](#configure-security-and-tracing). | +| `imagePullSecrets` | Credentials for a private registry. | +| `controlplane.config`, `dataplane.config` | Application settings for each plane. They default to `{}`, so the image's defaults apply. | + +*Complete example `gateway-values.yaml`, copied from the chart's `examples/kubernetes-secret.yaml` at release.* + +## File references + +Each credential has its own reference. Point it at a Kubernetes Secret key with `existingSecret` and `secretKey`, or at a Secrets Store CSI (Container Storage Interface) Driver file with `secretProviderClass` and `fileName`. Change a reference's optional `checksum` to roll out both planes after the file changes. + +| Helm reference | File contents | Used by | +| --- | --- | --- | +| `metadataDB` | YAML with `metadata_db.urls`. | Both planes | +| `credentialStorage.redis.connection` | YAML with `credential_storage.redis.urls`. | Both planes | +| `credentialStorage.redis.keyring` | Directory of base64-encoded 32-byte Advanced Encryption Standard (AES) keys. | Both planes | +| `credentialStorage.vault.credentials.controlplane` | YAML with `credential_storage.vault.token`, with write and delete access. | Control Plane | +| `credentialStorage.vault.credentials.dataplane` | YAML with `credential_storage.vault.token`, with read access. | Data Plane | +| `databaseConnections[]` | YAML with `databases..urls`. | Both planes | +| `controlplane.adminToken` | The admin token operators send to the Control Plane. | Control Plane | +| `controlplane.internalToken` | The token Identity Service sends to the grant callback. | Control Plane | +| `dataplane.runtimeToken` | The token the Data Plane sends to Identity Service. | Data Plane | +| `license` | The license file as issued. | Both planes | +| `tls.caBundle` | A PEM-encoded certificate authority (CA) bundle. | Both planes | + +The admin, internal, and runtime tokens must be distinct. Keep the admin token, the internal token, and the Vault writer token out of every CSI class the Data Plane mounts. + +## Credential storage + +| `credentialStorage.provider` | Configuration | +| --- | --- | +| `redis` (default) | A credential Redis connection and a keyring shared by both planes. `credentialStorage.redis.keyFiles` maps each key ID to a file in the keyring, and `activeKeyId` names the key for new writes. | +| `vault` | Separate writer and reader tokens for the Control Plane and Data Plane. | +| `aws` | Separate Identity and Access Management (IAM) roles for each plane through workload identity. | + + + +## Database registry + +Register every database a Gateway uses under the same ID on both planes: Context Surface databases and Gateway databases. Put display names in `shared.config.databases` and connection URLs in a `databaseConnections` file. + +```yaml +shared: + config: + databases: + clinics: {name: Clinic directory} + results: {name: Gateway database} +databaseConnections: + - existingSecret: gateway-database-connections + secretKey: config.yaml +``` + +Both planes probe every entry at startup. An entry that does not answer does not stop a plane. The plane logs `Gateway database did not answer at startup` with the entry's `database_id` and starts. + +## Context Surface databases + +A Context Surface stores only its database ID. Give each plane its own Redis user on the database, as listed in [Redis users for Surface databases](/content/operate/iris/context-gateway/self-managed/prerequisites.md#redis-users-for-surface-databases), and put each login in that plane's registry entry. To change a password, update the Secret and restart both planes. + +Semantic and hybrid search tools embed the agent's text before they search. Point the Data Plane at an OpenAI-compatible embedding service: + +```yaml +dataplane: + config: + context_surface: + embedding: + endpoint: https://api.openai.com/v1 + model: text-embedding-3-small +``` + + + +Without an embedding service, every other tool still works, and semantic or hybrid search calls fail with `embedding_unavailable`. The embedding client ignores proxy variables and dials the endpoint directly, and with an API key set every endpoint must use HTTPS. + +The Data Plane keeps at most two clients for each Surface database. One Surface database sees at most `2 × client.pool_size × Data Plane replicas` connections, so set `client.pool_size` explicitly. + +## Result cache and result handles + +Both features are off by default. Turn them on in the Data Plane configuration: + +```yaml +dataplane: + config: + result_cache: + enabled: true + result_handles: + enabled: true + ttl: 15m +``` + +| Setting | Default | Meaning | +| --- | --- | --- | +| `result_cache.max_entry_bytes` | `262144` | The largest result the cache stores. | +| `result_handles.ttl` | `15m` | How long a handle lives. At most `24h`. A tool policy can override it. | +| `result_handles.inline_max_bytes` | `32768` | Results above this size become handles. | +| `result_handles.max_capture_bytes` | `8388608` | Results above this size come back whole. | +| `result_handles.preview_items`, `preview_bytes` | `3`, `2048` | The size of a handle stub's preview. | + +The result cache still needs a policy for each tool. Result handles apply to every eligible result on every Gateway with a `databaseId`, unless a tool's policy opts out. Both need the Gateway's `databaseId` to name a registered database. + +## Trust a private CA + +Set `tls.caBundle` to a PEM bundle, or mount a single certificate under `/etc/ssl/certs` with the chart's `examples/certificates.yaml`. Restart both planes after you rotate a certificate. + +## Configure security and tracing + +`security.profile: fips` runs the Federal Information Processing Standards (FIPS)-oriented profile. Both planes then reject plaintext Redis URLs and disabled certificate verification, including in the registered databases. + +Both planes serve Prometheus metrics at `/metrics`. Set `shared.config.opentelemetry.enable_tracing: true` and an OpenTelemetry Protocol (OTLP) endpoint for traces. + +## Scale and schedule the planes + +Each plane starts one replica. Set `replicaCount`, or turn on `autoscaling`, with `resources`, `nodeSelector`, `tolerations`, and `affinity` under `controlplane` and `dataplane`. The probes are chart-managed. Leave headroom for old and new pods during upgrades. + + diff --git a/content/operate/iris/context-gateway/self-managed/control-plane-api-reference.md b/content/operate/iris/context-gateway/self-managed/control-plane-api-reference.md new file mode 100644 index 0000000000..27712197ee --- /dev/null +++ b/content/operate/iris/context-gateway/self-managed/control-plane-api-reference.md @@ -0,0 +1,18 @@ +--- +Title: Redis Context Gateway Control Plane API reference +linkTitle: Control Plane API reference +layout: apireference +type: page +weight: 80 +params: + sourcefile: ./openapi-control-plane.json + sortOperationsAlphabetically: false + schemaDefinitionsTagName: Data models + showPageContent: true +--- + +Use the self-managed Control Plane API to create Gateways, register and update Upstreams, refresh tool catalogs, and list each Upstream's tools. + +[Download the OpenAPI contract]({{< relref "/operate/iris/context-gateway/self-managed/control-plane-api-reference/" >}}openapi-control-plane.json), follow the [administration examples](/content/operate/iris/context-gateway/self-managed/api-examples.md), or review [authentication and authorization](/content/operate/iris/context-gateway/self-managed/authentication.md). + +Agent-key management belongs to the Identity Service. Agents call tools through each Gateway's Model Context Protocol (MCP) endpoint, described in the [MCP API reference](/content/develop/ai/context-engine/context-gateway/api-reference.md). diff --git a/content/operate/iris/context-gateway/self-managed/control-plane-api-reference/openapi-control-plane.json b/content/operate/iris/context-gateway/self-managed/control-plane-api-reference/openapi-control-plane.json new file mode 100644 index 0000000000..d89d7cf966 --- /dev/null +++ b/content/operate/iris/context-gateway/self-managed/control-plane-api-reference/openapi-control-plane.json @@ -0,0 +1,3113 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "ContextGatewayControlPlaneService", + "version": "0.0.1", + "description": "Administration API for Context Gateway resources and their Upstreams.\n\nEvery write completes synchronously and returns the final state. Creating an\nUpstream, changing its connection or authentication, and refreshing its catalog\nrun live discovery against the Upstream, bounded by the discovery timeout.\nThe tenant comes from the control-plane configuration, never from the request,\nand no response shows it.\n" + }, + "paths": { + "/v1/gateways": { + "get": { + "operationId": "ListGateways", + "parameters": [ + { + "name": "nextToken", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "maxResults", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "ListGateways 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListGatewaysResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + } + }, + "post": { + "operationId": "CreateGateway", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateGatewayRequestContent" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "CreateGateway 201 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateGatewayResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + } + } + }, + "/v1/gateways/{gatewayId}": { + "delete": { + "operationId": "DeleteGateway", + "parameters": [ + { + "name": "gatewayId", + "in": "path", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients.", + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "required": true + }, + { + "name": "expectedRevision", + "in": "query", + "description": "Configuration revision. Opaque: each effective change writes a new random value, and a resource never gets a value it had before. Compare for equality only.", + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "Configuration revision. Opaque: each effective change writes a new random value, and a resource never gets a value it had before. Compare for equality only." + }, + "required": true + } + ], + "responses": { + "204": { + "description": "DeleteGateway 204 response" + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "ContextGatewayNotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextGatewayNotFoundErrorResponseContent" + } + } + } + }, + "409": { + "description": "ContextGatewayConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextGatewayConflictErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + } + }, + "get": { + "operationId": "GetGateway", + "parameters": [ + { + "name": "gatewayId", + "in": "path", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients.", + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetGateway 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetGatewayResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "ContextGatewayNotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextGatewayNotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + } + }, + "patch": { + "operationId": "UpdateGateway", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateGatewayRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "gatewayId", + "in": "path", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients.", + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UpdateGateway 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateGatewayResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "ContextGatewayNotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextGatewayNotFoundErrorResponseContent" + } + } + } + }, + "409": { + "description": "ContextGatewayConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextGatewayConflictErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + } + } + }, + "/v1/gateways/{gatewayId}/upstreams": { + "get": { + "operationId": "ListUpstreams", + "parameters": [ + { + "name": "gatewayId", + "in": "path", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients.", + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "required": true + }, + { + "name": "nextToken", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "maxResults", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "ListUpstreams 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListUpstreamsResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "ContextGatewayNotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextGatewayNotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + } + }, + "post": { + "description": "Registers the Upstream, then discovers and publishes its first catalog. A failed\nor timed-out initial discovery, or a failed tool generation, still creates the Upstream, with no activeCatalog,\nand reports the failure in initialCatalogFailure.\n", + "operationId": "CreateUpstream", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUpstreamRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "gatewayId", + "in": "path", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients.", + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "required": true + } + ], + "responses": { + "201": { + "description": "CreateUpstream 201 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUpstreamResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "ContextGatewayNotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextGatewayNotFoundErrorResponseContent" + } + } + } + }, + "409": { + "description": "ContextGatewayConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextGatewayConflictErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + } + } + }, + "/v1/gateways/{gatewayId}/upstreams/{upstreamId}": { + "delete": { + "description": "Marks the Upstream as being deleted, which every reader treats as absent, then\nreleases its namespace, removes its catalogs and current credentials, deletes\nthe record, and removes its Gateway index entry last. A retry of an Upstream\nthat is being deleted resumes the cleanup and returns 204.\n", + "operationId": "DeleteUpstream", + "parameters": [ + { + "name": "gatewayId", + "in": "path", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients.", + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "required": true + }, + { + "name": "upstreamId", + "in": "path", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients.", + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "required": true + }, + { + "name": "expectedRevision", + "in": "query", + "description": "Required for a readable Upstream; otherwise 400. Omitted for an unreadable record, whose revision cannot be read. Not checked when the Upstream is already being deleted.", + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "Required for a readable Upstream; otherwise 400. Omitted for an unreadable record, whose revision cannot be read. Not checked when the Upstream is already being deleted." + } + }, + { + "name": "ignoreIndexSweepFailure", + "in": "query", + "description": "For a context-surface Upstream whose delete keeps answering 503 because its Surface index sweep fails. true still runs the sweep, but when it fails the delete goes on and leaves every index named idx::* on the Surface database, and the control plane logs that prefix. Omitted or false fails the delete with 503 instead. Ignored for an mcp Upstream.", + "schema": { + "type": "boolean", + "description": "For a context-surface Upstream whose delete keeps answering 503 because its Surface index sweep fails. true still runs the sweep, but when it fails the delete goes on and leaves every index named idx::* on the Surface database, and the control plane logs that prefix. Omitted or false fails the delete with 503 instead. Ignored for an mcp Upstream." + } + } + ], + "responses": { + "204": { + "description": "DeleteUpstream 204 response" + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "ContextGatewayNotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextGatewayNotFoundErrorResponseContent" + } + } + } + }, + "409": { + "description": "ContextGatewayConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextGatewayConflictErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + } + }, + "get": { + "operationId": "GetUpstream", + "parameters": [ + { + "name": "gatewayId", + "in": "path", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients.", + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "required": true + }, + { + "name": "upstreamId", + "in": "path", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients.", + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetUpstream 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetUpstreamResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "ContextGatewayNotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextGatewayNotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + } + }, + "patch": { + "description": "displayName and enabled are metadata-only and run no discovery. A change to\nconnection or authentication discovers with the candidate configuration, and a\ncontextSurface swaps the data model of a context-surface Upstream and generates\nits catalog; either commits configuration and catalog together, or commits\nnothing.\n", + "operationId": "UpdateUpstream", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUpstreamRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "gatewayId", + "in": "path", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients.", + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "required": true + }, + { + "name": "upstreamId", + "in": "path", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients.", + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UpdateUpstream 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUpstreamResponseContent" + } + } + } + }, + "400": { + "description": "BadRequestError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "ContextGatewayNotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextGatewayNotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "ContextGatewayConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextGatewayConflictErrorResponseContent" + } + } + } + }, + "422": { + "description": "ContextGatewayUnprocessableError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextGatewayUnprocessableErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + } + } + }, + "/v1/gateways/{gatewayId}/upstreams/{upstreamId}/refresh": { + "post": { + "description": "Discovers with the current configuration and publishes every offered tool, only\nif neither the configuration revision nor the active catalog revision changed\nmeanwhile. A 200 means the active catalog matches this discovery. Allowed on a\ndisabled Upstream.\n", + "operationId": "RefreshUpstreamCatalog", + "parameters": [ + { + "name": "gatewayId", + "in": "path", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients.", + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "required": true + }, + { + "name": "upstreamId", + "in": "path", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients.", + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "RefreshUpstreamCatalog 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefreshUpstreamCatalogResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "ContextGatewayNotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextGatewayNotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "ContextGatewayConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextGatewayConflictErrorResponseContent" + } + } + } + }, + "422": { + "description": "ContextGatewayUnprocessableError 422 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextGatewayUnprocessableErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + } + } + }, + "/v1/gateways/{gatewayId}/upstreams/{upstreamId}/tools": { + "get": { + "description": "Lists every tool of the active catalog in one page, ordered by name, with no\npaging. An Upstream with no published catalog answers an empty list. Each tool\nshows the result of the cacheable rule and, only when one is stored, its policy.\n", + "operationId": "ListUpstreamTools", + "parameters": [ + { + "name": "gatewayId", + "in": "path", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients.", + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "required": true + }, + { + "name": "upstreamId", + "in": "path", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients.", + "schema": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "ListUpstreamTools 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListUpstreamToolsResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "ContextGatewayNotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ContextGatewayNotFoundErrorResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + } + } + }, + "/v1/health/live": { + "get": { + "operationId": "Live", + "responses": { + "200": { + "description": "Live 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LiveResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "security": [] + } + }, + "/v1/health/ready": { + "get": { + "operationId": "Ready", + "responses": { + "200": { + "description": "Ready 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReadyResponseContent" + } + } + } + }, + "500": { + "description": "UnexpectedError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnexpectedErrorResponseContent" + } + } + } + }, + "503": { + "description": "ServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "security": [] + } + } + }, + "components": { + "schemas": { + "ActiveCatalog": { + "type": "object", + "description": "The published catalog. Its catalog revision stays internal: no operation accepts it.", + "properties": { + "toolCount": { + "type": "integer", + "minimum": 0, + "format": "int64" + } + }, + "required": [ + "toolCount" + ] + }, + "AuthenticationErrorResponseContent": { + "type": "object", + "description": "Authentication credentials are missing, malformed, or invalid.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 401, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/AuthenticationErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ], + "externalDocs": { + "description": "RFC 9457", + "url": "https://www.rfc-editor.org/rfc/rfc9457.html" + } + }, + "AuthenticationErrorType": { + "type": "string", + "description": "Problem type URI for authentication errors.", + "enum": [ + "/errors/authentication-failed" + ] + }, + "AuthenticationInput": { + "type": "object", + "description": "One authentication method per header the Upstream receives. At most one method may send each header, compared case-insensitively; bearer sends Authorization.", + "properties": { + "methods": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AuthenticationMethodInput" + } + } + } + }, + "AuthenticationMethodInput": { + "description": "One member per method kind, each with exactly its own fields. A later kind, such\nas basic or oauth2ClientCredentials, is a new member, which is additive.", + "oneOf": [ + { + "type": "object", + "title": "bearer", + "properties": { + "bearer": { + "$ref": "#/components/schemas/BearerMethodInput" + } + }, + "required": [ + "bearer" + ] + }, + { + "type": "object", + "title": "header", + "properties": { + "header": { + "$ref": "#/components/schemas/HeaderMethodInput" + } + }, + "required": [ + "header" + ] + } + ] + }, + "AuthenticationMethodOutput": { + "oneOf": [ + { + "type": "object", + "title": "bearer", + "properties": { + "bearer": { + "$ref": "#/components/schemas/BearerMethodOutput" + } + }, + "required": [ + "bearer" + ] + }, + { + "type": "object", + "title": "header", + "properties": { + "header": { + "$ref": "#/components/schemas/HeaderMethodOutput" + } + }, + "required": [ + "header" + ] + } + ] + }, + "AuthenticationOutput": { + "type": "object", + "description": "Shows which headers the Upstream receives, never a value or a credential ID. A rotation shows as a new revision and updatedAt.", + "properties": { + "methods": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AuthenticationMethodOutput" + } + } + }, + "required": [ + "methods" + ] + }, + "BadRequestErrorResponseContent": { + "type": "object", + "description": "Request validation or input decoding failed.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 400, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/BadRequestErrorType" + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FieldError" + }, + "description": "Optional field-level validation errors." + } + }, + "required": [ + "status", + "title", + "type" + ], + "externalDocs": { + "description": "RFC 9457", + "url": "https://www.rfc-editor.org/rfc/rfc9457.html" + } + }, + "BadRequestErrorType": { + "type": "string", + "description": "Problem type URI for bad request errors.", + "enum": [ + "/errors/invalid-data" + ] + }, + "BearerMethodInput": { + "type": "object", + "description": "A secret is required on create and for a new method. Omitted on update keeps the current secret, unless the endpoint changes.", + "properties": { + "value": { + "type": "string", + "minLength": 1, + "description": "A secret value. Write-only: no output shape has a member that can carry it.", + "format": "password" + } + } + }, + "BearerMethodOutput": { + "type": "object" + }, + "CatalogFailure": { + "type": "object", + "description": "The outcome of a failed initial catalog (discovery or tool generation), in the RFC 9457 shape. Only CreateUpstreamOutput carries it; it is not stored.", + "properties": { + "type": { + "type": "string" + }, + "title": { + "type": "string" + }, + "status": { + "type": "integer", + "format": "int32" + }, + "detail": { + "type": "string" + }, + "occurredAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "occurredAt", + "status", + "title", + "type" + ] + }, + "Connection": { + "description": "Exactly one connection variant. The variant determines the kind on create; an explicit kind must agree. An update cannot change the stored kind.", + "oneOf": [ + { + "$ref": "#/components/schemas/MCPConnection" + }, + { + "$ref": "#/components/schemas/ContextSurfaceConnection" + } + ] + }, + "ConnectionDatabase": { + "type": "object", + "description": "The Surface database of a context-surface Upstream.", + "properties": { + "id": { + "type": "string", + "maxLength": 32, + "minLength": 1, + "pattern": "^[A-Za-z0-9-]+$", + "description": "An unknown ID and an ID the tenant does not own are refused with the same 400." + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, + "ContextGatewayConflictErrorResponseContent": { + "type": "object", + "description": "One schema per HTTP status preserves the complete problem catalog in generated\nclients, following the Playbook data plane's dedicated error-type enum pattern.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 409, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/ContextGatewayConflictErrorType" + }, + "upstreamId": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "Set only for namespace-conflict: the Upstream that holds the namespace, so that a client that lost a create response can read it." + } + }, + "required": [ + "status", + "title", + "type" + ], + "externalDocs": { + "description": "RFC 9457", + "url": "https://www.rfc-editor.org/rfc/rfc9457.html" + } + }, + "ContextGatewayConflictErrorType": { + "type": "string", + "description": "Distinguish resource state conflicts by type, never by human-readable title.", + "enum": [ + "/errors/revision-conflict", + "/errors/namespace-conflict", + "/errors/gateway-not-empty", + "/errors/catalog-publication-conflict" + ] + }, + "ContextGatewayNotFoundErrorResponseContent": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 404, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/ContextGatewayNotFoundErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ], + "externalDocs": { + "description": "RFC 9457", + "url": "https://www.rfc-editor.org/rfc/rfc9457.html" + } + }, + "ContextGatewayNotFoundErrorType": { + "type": "string", + "description": "Distinguish missing resources by type, never by human-readable title. A Gateway\nID owned by another tenant returns gateway-not-found with a body byte-identical\nto an unknown Gateway ID, so the response never discloses that the ID exists.", + "enum": [ + "/errors/gateway-not-found", + "/errors/upstream-not-found" + ] + }, + "ContextGatewayUnprocessableErrorResponseContent": { + "type": "object", + "description": "The request is well formed, but the work it asks for cannot succeed until the\nrejected credentials are fixed: an mcp Upstream's credentials with an update,\nor, for a context-surface Upstream, the Surface database login, which the\ndeployment administrator fixes in the Secret that supplies the deployment's\ndatabases registry. Nothing was changed.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 422, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/ContextGatewayUnprocessableErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ], + "externalDocs": { + "description": "RFC 9457", + "url": "https://www.rfc-editor.org/rfc/rfc9457.html" + } + }, + "ContextGatewayUnprocessableErrorType": { + "type": "string", + "enum": [ + "/errors/resource-unavailable" + ] + }, + "ContextSurface": { + "type": "object", + "description": "The settings of kind context-surface.", + "properties": { + "dataModel": { + "description": "The data model, a JSON object. The API schema does not describe it: the\ncontrol plane validates it and names each error by its path under\ncontextSurface.dataModel." + } + } + }, + "ContextSurfaceConnection": { + "type": "object", + "properties": { + "database": { + "$ref": "#/components/schemas/ConnectionDatabase" + } + }, + "required": [ + "database" + ], + "additionalProperties": false + }, + "CreateGatewayRequestContent": { + "type": "object", + "properties": { + "displayName": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "description": "Operator-facing label." + }, + "description": { + "type": "string" + }, + "toolExposureMode": { + "$ref": "#/components/schemas/ToolExposureMode" + }, + "databaseId": { + "type": "string", + "maxLength": 32, + "minLength": 1, + "pattern": "^[A-Za-z0-9-]+$", + "description": "Selects the Gateway database for the Gateway's result state. Omitted means the Gateway has no Gateway database. An ID that is not in the registry is refused with 400." + } + }, + "required": [ + "displayName" + ] + }, + "CreateGatewayResponseContent": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "displayName": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "description": "Operator-facing label." + }, + "description": { + "type": "string" + }, + "toolExposureMode": { + "$ref": "#/components/schemas/ToolExposureMode" + }, + "databaseId": { + "type": "string", + "maxLength": 32, + "minLength": 1, + "pattern": "^[A-Za-z0-9-]+$", + "description": "Present only when the Gateway has a Gateway database." + }, + "databaseName": { + "type": "string", + "description": "The registry entry's name. Present only when databaseId is present and the registry still holds it; a stored ID that the registry no longer holds is returned without it." + }, + "mcpEndpoint": { + "type": "string", + "pattern": "^[Hh][Tt][Tt][Pp][Ss]?://.+$", + "description": "The URL of this Gateway's MCP endpoint on the data plane: the data plane's public base URL, a deployment setting, plus /v1/gateways/{gatewayId}/mcp." + }, + "revision": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "Configuration revision. Opaque: each effective change writes a new random value, and a resource never gets a value it had before. Compare for equality only." + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "createdAt", + "displayName", + "id", + "mcpEndpoint", + "revision", + "toolExposureMode", + "updatedAt" + ] + }, + "CreateUpstreamRequestContent": { + "type": "object", + "properties": { + "displayName": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "description": "Operator-facing label." + }, + "namespace": { + "type": "string", + "maxLength": 63, + "minLength": 1, + "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", + "description": "Derived from displayName when omitted. A collision is a namespace-conflict; no suffix is generated." + }, + "enabled": { + "type": "boolean", + "description": "Defaults to true." + }, + "kind": { + "$ref": "#/components/schemas/UpstreamKind" + }, + "connection": { + "$ref": "#/components/schemas/Connection" + }, + "authentication": { + "$ref": "#/components/schemas/AuthenticationInput" + }, + "toolPolicies": { + "$ref": "#/components/schemas/ToolPolicies" + }, + "contextSurface": { + "$ref": "#/components/schemas/ContextSurface" + } + }, + "required": [ + "connection", + "displayName" + ] + }, + "CreateUpstreamResponseContent": { + "type": "object", + "description": "Only create returns initialCatalogFailure, so no other operation's generated client\nshows it. A persisted discovery outcome (open item O14) needs a new member, such\nas lastDiscovery, rather than a new meaning for this one.", + "properties": { + "id": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "gatewayId": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "kind": { + "$ref": "#/components/schemas/UpstreamKind" + }, + "displayName": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "description": "Operator-facing label." + }, + "namespace": { + "type": "string", + "maxLength": 63, + "minLength": 1, + "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", + "description": "Gateway-unique lowercase kebab-case name without dots. It prefixes every exposed tool name." + }, + "enabled": { + "type": "boolean" + }, + "revision": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "Configuration revision. Opaque: each effective change writes a new random value, and a resource never gets a value it had before. Compare for equality only." + }, + "connection": { + "$ref": "#/components/schemas/Connection" + }, + "authentication": { + "$ref": "#/components/schemas/AuthenticationOutput" + }, + "contextSurface": { + "$ref": "#/components/schemas/ContextSurface" + }, + "activeCatalog": { + "$ref": "#/components/schemas/ActiveCatalog" + }, + "toolPolicies": { + "$ref": "#/components/schemas/ToolPolicies" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "initialCatalogFailure": { + "$ref": "#/components/schemas/CatalogFailure" + } + }, + "required": [ + "authentication", + "connection", + "createdAt", + "displayName", + "enabled", + "gatewayId", + "id", + "kind", + "namespace", + "revision", + "toolPolicies", + "updatedAt" + ] + }, + "DroppedToolPolicy": { + "type": "object", + "description": "The members of a stored tool policy that an endpoint change or a data model swap deleted, and why.", + "properties": { + "tool": { + "type": "string", + "description": "The original tool name, exact and case-preserved." + }, + "reason": { + "$ref": "#/components/schemas/DroppedToolPolicyReason" + }, + "notCacheableReason": { + "$ref": "#/components/schemas/NotCacheableReason" + }, + "policy": { + "$ref": "#/components/schemas/ToolPolicy" + } + }, + "required": [ + "policy", + "reason", + "tool" + ] + }, + "DroppedToolPolicyReason": { + "type": "string", + "enum": [ + "not-in-catalog", + "not-cacheable" + ] + }, + "FailedDependencyErrorResponseContent": { + "type": "object", + "description": "A dependent resource required to process the request is unavailable or unhealthy.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 424, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/FailedDependencyErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ], + "externalDocs": { + "description": "RFC 9457", + "url": "https://www.rfc-editor.org/rfc/rfc9457.html" + } + }, + "FailedDependencyErrorType": { + "type": "string", + "description": "Problem type URI for failed-dependency errors.", + "enum": [ + "/errors/resource-unavailable", + "/errors/database-out-of-memory" + ] + }, + "FieldError": { + "type": "object", + "description": "Validation error details for a single request field.", + "properties": { + "field": { + "type": "string", + "description": "Name of the invalid request field." + }, + "rule": { + "type": "string", + "description": "Validation rule that was violated." + }, + "message": { + "type": "string", + "description": "Human-readable validation error message." + } + }, + "required": [ + "field", + "message", + "rule" + ] + }, + "ForbiddenErrorResponseContent": { + "type": "object", + "description": "The caller is authenticated but not allowed to access the requested resource.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 403, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/ForbiddenErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ], + "externalDocs": { + "description": "RFC 9457", + "url": "https://www.rfc-editor.org/rfc/rfc9457.html" + } + }, + "ForbiddenErrorType": { + "type": "string", + "description": "Problem type URI for authorization errors.", + "enum": [ + "/errors/insufficient-permissions" + ] + }, + "GatewayOutput": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "displayName": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "description": "Operator-facing label." + }, + "description": { + "type": "string" + }, + "toolExposureMode": { + "$ref": "#/components/schemas/ToolExposureMode" + }, + "databaseId": { + "type": "string", + "maxLength": 32, + "minLength": 1, + "pattern": "^[A-Za-z0-9-]+$", + "description": "Present only when the Gateway has a Gateway database." + }, + "databaseName": { + "type": "string", + "description": "The registry entry's name. Present only when databaseId is present and the registry still holds it; a stored ID that the registry no longer holds is returned without it." + }, + "mcpEndpoint": { + "type": "string", + "pattern": "^[Hh][Tt][Tt][Pp][Ss]?://.+$", + "description": "The URL of this Gateway's MCP endpoint on the data plane: the data plane's public base URL, a deployment setting, plus /v1/gateways/{gatewayId}/mcp." + }, + "revision": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "Configuration revision. Opaque: each effective change writes a new random value, and a resource never gets a value it had before. Compare for equality only." + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "createdAt", + "displayName", + "id", + "mcpEndpoint", + "revision", + "toolExposureMode", + "updatedAt" + ] + }, + "GetGatewayResponseContent": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "displayName": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "description": "Operator-facing label." + }, + "description": { + "type": "string" + }, + "toolExposureMode": { + "$ref": "#/components/schemas/ToolExposureMode" + }, + "databaseId": { + "type": "string", + "maxLength": 32, + "minLength": 1, + "pattern": "^[A-Za-z0-9-]+$", + "description": "Present only when the Gateway has a Gateway database." + }, + "databaseName": { + "type": "string", + "description": "The registry entry's name. Present only when databaseId is present and the registry still holds it; a stored ID that the registry no longer holds is returned without it." + }, + "mcpEndpoint": { + "type": "string", + "pattern": "^[Hh][Tt][Tt][Pp][Ss]?://.+$", + "description": "The URL of this Gateway's MCP endpoint on the data plane: the data plane's public base URL, a deployment setting, plus /v1/gateways/{gatewayId}/mcp." + }, + "revision": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "Configuration revision. Opaque: each effective change writes a new random value, and a resource never gets a value it had before. Compare for equality only." + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "createdAt", + "displayName", + "id", + "mcpEndpoint", + "revision", + "toolExposureMode", + "updatedAt" + ] + }, + "GetUpstreamResponseContent": { + "type": "object", + "description": "The members that every Upstream response shares.", + "properties": { + "id": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "gatewayId": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "kind": { + "$ref": "#/components/schemas/UpstreamKind" + }, + "displayName": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "description": "Operator-facing label." + }, + "namespace": { + "type": "string", + "maxLength": 63, + "minLength": 1, + "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", + "description": "Gateway-unique lowercase kebab-case name without dots. It prefixes every exposed tool name." + }, + "enabled": { + "type": "boolean" + }, + "revision": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "Configuration revision. Opaque: each effective change writes a new random value, and a resource never gets a value it had before. Compare for equality only." + }, + "connection": { + "$ref": "#/components/schemas/Connection" + }, + "authentication": { + "$ref": "#/components/schemas/AuthenticationOutput" + }, + "contextSurface": { + "$ref": "#/components/schemas/ContextSurface" + }, + "activeCatalog": { + "$ref": "#/components/schemas/ActiveCatalog" + }, + "toolPolicies": { + "$ref": "#/components/schemas/ToolPolicies" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "authentication", + "connection", + "createdAt", + "displayName", + "enabled", + "gatewayId", + "id", + "kind", + "namespace", + "revision", + "toolPolicies", + "updatedAt" + ] + }, + "HeaderMethodInput": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[!#$%&'*+.^_`|~0-9A-Za-z-]+$", + "description": "Compared case-insensitively and stored lowercase. Authorization is rejected beside a bearer method." + }, + "value": { + "type": "string", + "minLength": 1, + "description": "Required on create and for a new method. Omitted on update keeps the current secret, unless the endpoint changes.", + "format": "password" + } + }, + "required": [ + "name" + ] + }, + "HeaderMethodOutput": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[!#$%&'*+.^_`|~0-9A-Za-z-]+$", + "description": "Lowercase." + } + }, + "required": [ + "name" + ] + }, + "ListGatewaysResponseContent": { + "type": "object", + "properties": { + "gateways": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GatewayOutput" + } + }, + "nextToken": { + "type": "string", + "description": "Present when more items follow. A client must follow it. This iteration never sets it." + } + }, + "required": [ + "gateways" + ] + }, + "ListUpstreamToolsResponseContent": { + "type": "object", + "properties": { + "tools": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UpstreamTool" + }, + "description": "Every tool of the active catalog, ordered by name. [] when no catalog is published." + } + }, + "required": [ + "tools" + ] + }, + "ListUpstreamsResponseContent": { + "type": "object", + "properties": { + "upstreams": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UpstreamSummary" + }, + "description": "The readable Upstreams. An Upstream that is being deleted is in neither list." + }, + "unreadable": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UnreadableUpstream" + }, + "description": "Upstream records that do not match the stored format. Omitted when empty. A client deletes such a record without expectedRevision." + }, + "nextToken": { + "type": "string", + "description": "Present when more items follow. A client must follow it. This iteration never sets it." + } + }, + "required": [ + "upstreams" + ] + }, + "LiveResponseContent": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + }, + "required": [ + "status" + ] + }, + "MCPConnection": { + "type": "object", + "properties": { + "endpoint": { + "type": "string", + "pattern": "^[Hh][Tt][Tt][Pp][Ss]?://.+$", + "description": "An absolute http or https URL without user information. Any other scheme, such as redis or rediss, is refused. The outbound destination policy, required before GA, is open item O8 in the contract." + }, + "transport": { + "$ref": "#/components/schemas/Transport" + } + }, + "required": [ + "endpoint" + ], + "additionalProperties": false + }, + "NotCacheableReason": { + "type": "string", + "enum": [ + "destructive", + "not-read-only", + "invalid-annotation" + ] + }, + "ReadyResponseContent": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + }, + "required": [ + "status" + ] + }, + "RefreshUpstreamCatalogResponseContent": { + "type": "object", + "description": "The members that every Upstream response shares.", + "properties": { + "id": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "gatewayId": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "kind": { + "$ref": "#/components/schemas/UpstreamKind" + }, + "displayName": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "description": "Operator-facing label." + }, + "namespace": { + "type": "string", + "maxLength": 63, + "minLength": 1, + "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", + "description": "Gateway-unique lowercase kebab-case name without dots. It prefixes every exposed tool name." + }, + "enabled": { + "type": "boolean" + }, + "revision": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "Configuration revision. Opaque: each effective change writes a new random value, and a resource never gets a value it had before. Compare for equality only." + }, + "connection": { + "$ref": "#/components/schemas/Connection" + }, + "authentication": { + "$ref": "#/components/schemas/AuthenticationOutput" + }, + "contextSurface": { + "$ref": "#/components/schemas/ContextSurface" + }, + "activeCatalog": { + "$ref": "#/components/schemas/ActiveCatalog" + }, + "toolPolicies": { + "$ref": "#/components/schemas/ToolPolicies" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "authentication", + "connection", + "createdAt", + "displayName", + "enabled", + "gatewayId", + "id", + "kind", + "namespace", + "revision", + "toolPolicies", + "updatedAt" + ] + }, + "ResourceSuspendedErrorResponseContent": { + "type": "object", + "description": "The requested resource exists but is suspended by an administrator.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 423, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/ResourceSuspendedErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ], + "externalDocs": { + "description": "RFC 9457", + "url": "https://www.rfc-editor.org/rfc/rfc9457.html" + } + }, + "ResourceSuspendedErrorType": { + "type": "string", + "description": "Problem type URI for suspended resources.", + "enum": [ + "/errors/resource-suspended" + ] + }, + "ResultCachePolicy": { + "type": "object", + "properties": { + "ttlSeconds": { + "type": "integer", + "maximum": 86400, + "minimum": 1, + "description": "How long a cached result lives, in seconds.", + "format": "int32" + } + }, + "required": [ + "ttlSeconds" + ] + }, + "ResultHandleCapture": { + "type": "string", + "enum": [ + "never" + ] + }, + "ResultHandlePolicy": { + "type": "object", + "description": "Set exactly one member. An empty resultHandle is refused with 400.", + "properties": { + "capture": { + "$ref": "#/components/schemas/ResultHandleCapture" + }, + "ttlSeconds": { + "type": "integer", + "maximum": 86400, + "minimum": 1, + "description": "How long a result handle of this tool lives, in seconds, in place of the deployment's result_handles.ttl. Refused together with capture never.", + "format": "int32" + } + } + }, + "ServiceUnavailableErrorResponseContent": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 503, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/ServiceUnavailableErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ], + "externalDocs": { + "description": "RFC 9457", + "url": "https://www.rfc-editor.org/rfc/rfc9457.html" + } + }, + "ServiceUnavailableErrorType": { + "type": "string", + "enum": [ + "/errors/service-unavailable" + ] + }, + "TimeoutErrorResponseContent": { + "type": "object", + "description": "The request timed out before the service could complete it.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 408, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/TimeoutErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ], + "externalDocs": { + "description": "RFC 9457", + "url": "https://www.rfc-editor.org/rfc/rfc9457.html" + } + }, + "TimeoutErrorType": { + "type": "string", + "description": "Problem type URI for timeout errors.", + "enum": [ + "/errors/timeout" + ] + }, + "ToolExposureMode": { + "type": "string", + "description": "How a Gateway's clients find tools: the two modes the data plane serves. Adding\nanother mode later is an additive change.", + "enum": [ + "full", + "search" + ] + }, + "ToolPolicies": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ToolPolicy" + }, + "description": "Maps the original name of a tool in the Upstream's catalog, exact and case-preserved, to its policy." + }, + "ToolPolicy": { + "type": "object", + "description": "One member per feature, each optional. A policy with no member is refused with 400.", + "properties": { + "resultCache": { + "$ref": "#/components/schemas/ResultCachePolicy" + }, + "resultHandle": { + "$ref": "#/components/schemas/ResultHandlePolicy" + } + } + }, + "Transport": { + "type": "string", + "enum": [ + "streamable-http" + ] + }, + "UnexpectedErrorResponseContent": { + "type": "object", + "description": "The service failed with an unexpected internal error.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 500, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/UnexpectedErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ], + "externalDocs": { + "description": "RFC 9457", + "url": "https://www.rfc-editor.org/rfc/rfc9457.html" + } + }, + "UnexpectedErrorType": { + "type": "string", + "description": "Problem type URI for unexpected internal errors.", + "enum": [ + "/errors/unexpected-error" + ] + }, + "UnreadableReason": { + "type": "string", + "enum": [ + "invalid-stored-metadata" + ] + }, + "UnreadableUpstream": { + "type": "object", + "description": "An Upstream record that ListUpstreams cannot read. It never carries the stored data.", + "properties": { + "id": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "reason": { + "$ref": "#/components/schemas/UnreadableReason" + } + }, + "required": [ + "id", + "reason" + ] + }, + "UpdateGatewayRequestContent": { + "type": "object", + "properties": { + "expectedRevision": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "Configuration revision. Opaque: each effective change writes a new random value, and a resource never gets a value it had before. Compare for equality only." + }, + "displayName": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "description": "Operator-facing label." + }, + "description": { + "type": "string", + "description": "Omitted leaves it unchanged; null clears it.", + "nullable": true + }, + "toolExposureMode": { + "$ref": "#/components/schemas/ToolExposureMode" + }, + "databaseId": { + "type": "string", + "maxLength": 32, + "minLength": 1, + "pattern": "^[A-Za-z0-9-]+$", + "description": "Omitted leaves it unchanged; null clears it. An ID that is not in the registry is refused with 400.", + "nullable": true + } + }, + "required": [ + "expectedRevision" + ] + }, + "UpdateGatewayResponseContent": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "displayName": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "description": "Operator-facing label." + }, + "description": { + "type": "string" + }, + "toolExposureMode": { + "$ref": "#/components/schemas/ToolExposureMode" + }, + "databaseId": { + "type": "string", + "maxLength": 32, + "minLength": 1, + "pattern": "^[A-Za-z0-9-]+$", + "description": "Present only when the Gateway has a Gateway database." + }, + "databaseName": { + "type": "string", + "description": "The registry entry's name. Present only when databaseId is present and the registry still holds it; a stored ID that the registry no longer holds is returned without it." + }, + "mcpEndpoint": { + "type": "string", + "pattern": "^[Hh][Tt][Tt][Pp][Ss]?://.+$", + "description": "The URL of this Gateway's MCP endpoint on the data plane: the data plane's public base URL, a deployment setting, plus /v1/gateways/{gatewayId}/mcp." + }, + "revision": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "Configuration revision. Opaque: each effective change writes a new random value, and a resource never gets a value it had before. Compare for equality only." + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "createdAt", + "displayName", + "id", + "mcpEndpoint", + "revision", + "toolExposureMode", + "updatedAt" + ] + }, + "UpdateUpstreamRequestContent": { + "type": "object", + "properties": { + "expectedRevision": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "Configuration revision. Opaque: each effective change writes a new random value, and a resource never gets a value it had before. Compare for equality only." + }, + "displayName": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "description": "Operator-facing label." + }, + "enabled": { + "type": "boolean" + }, + "connection": { + "$ref": "#/components/schemas/Connection" + }, + "authentication": { + "$ref": "#/components/schemas/AuthenticationInput" + }, + "toolPolicies": { + "$ref": "#/components/schemas/ToolPolicies" + }, + "contextSurface": { + "$ref": "#/components/schemas/ContextSurface" + } + }, + "required": [ + "expectedRevision" + ] + }, + "UpdateUpstreamResponseContent": { + "type": "object", + "description": "The members that every Upstream response shares.", + "properties": { + "id": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "gatewayId": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "kind": { + "$ref": "#/components/schemas/UpstreamKind" + }, + "displayName": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "description": "Operator-facing label." + }, + "namespace": { + "type": "string", + "maxLength": 63, + "minLength": 1, + "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", + "description": "Gateway-unique lowercase kebab-case name without dots. It prefixes every exposed tool name." + }, + "enabled": { + "type": "boolean" + }, + "revision": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "Configuration revision. Opaque: each effective change writes a new random value, and a resource never gets a value it had before. Compare for equality only." + }, + "connection": { + "$ref": "#/components/schemas/Connection" + }, + "authentication": { + "$ref": "#/components/schemas/AuthenticationOutput" + }, + "contextSurface": { + "$ref": "#/components/schemas/ContextSurface" + }, + "activeCatalog": { + "$ref": "#/components/schemas/ActiveCatalog" + }, + "toolPolicies": { + "$ref": "#/components/schemas/ToolPolicies" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "droppedToolPolicies": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DroppedToolPolicy" + }, + "description": "The stored policies, or policy members, this update deleted, in tool name\norder. Only a connection.endpoint change or a data model swap (contextSurface) that does not send toolPolicies\ndeletes any: it keeps each stored policy the new endpoint's catalog allows\nand deletes the rest in the same commit as the new configuration. For a\ntool the cacheable rule vetoes, only resultCache is deleted and\nresultHandle stays stored. A policy is deleted whole only when the tool is\nnot in the catalog or nothing is left. Omitted when nothing was deleted." + } + }, + "required": [ + "authentication", + "connection", + "createdAt", + "displayName", + "enabled", + "gatewayId", + "id", + "kind", + "namespace", + "revision", + "toolPolicies", + "updatedAt" + ] + }, + "UpstreamKind": { + "type": "string", + "enum": [ + "mcp", + "context-surface" + ] + }, + "UpstreamSummary": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "A UUID v4 as 32 lowercase hexadecimal characters, allocated by the control plane. Opaque to clients." + }, + "displayName": { + "type": "string", + "maxLength": 200, + "minLength": 1, + "description": "Operator-facing label." + }, + "namespace": { + "type": "string", + "maxLength": 63, + "minLength": 1, + "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", + "description": "Gateway-unique lowercase kebab-case name without dots. It prefixes every exposed tool name." + }, + "enabled": { + "type": "boolean" + }, + "revision": { + "type": "string", + "pattern": "^[0-9a-f]{32}$", + "description": "Configuration revision. Opaque: each effective change writes a new random value, and a resource never gets a value it had before. Compare for equality only." + }, + "activeCatalog": { + "$ref": "#/components/schemas/ActiveCatalog" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "createdAt", + "displayName", + "enabled", + "id", + "namespace", + "revision", + "updatedAt" + ] + }, + "UpstreamTool": { + "type": "object", + "description": "A tool of the active catalog. It carries no input or output schema.", + "properties": { + "name": { + "type": "string", + "description": "The original tool name, exact and case-preserved." + }, + "title": { + "type": "string", + "description": "Present when the tool definition has one." + }, + "description": { + "type": "string", + "description": "Present when the tool definition has one." + }, + "annotations": { + "description": "The annotations object as stored in the catalog. Present when the tool definition has one." + }, + "cacheable": { + "type": "boolean", + "description": "The result of the cacheable rule, which reads only readOnlyHint and\ndestructiveHint. An absent hint vetoes nothing; an annotation never turns\ncaching on, only a resultCache policy does." + }, + "notCacheableReason": { + "$ref": "#/components/schemas/NotCacheableReason" + }, + "policy": { + "$ref": "#/components/schemas/ToolPolicy" + } + }, + "required": [ + "cacheable", + "name" + ] + } + }, + "securitySchemes": { + "smithy.api.httpBearerAuth": { + "type": "http", + "description": "HTTP Bearer authentication", + "scheme": "Bearer" + } + } + }, + "security": [ + { + "smithy.api.httpBearerAuth": [] + } + ] +} diff --git a/content/operate/iris/context-gateway/self-managed/deploy.md b/content/operate/iris/context-gateway/self-managed/deploy.md new file mode 100644 index 0000000000..436637f797 --- /dev/null +++ b/content/operate/iris/context-gateway/self-managed/deploy.md @@ -0,0 +1,182 @@ +--- +Title: Deploy Redis Context Gateway +alwaysopen: false +categories: +- docs +- operate +- iris +description: Install Redis Context Gateway with an external Identity Service, then create a Gateway, mint an agent key, and make one authenticated call. +linkTitle: Deploy +weight: 50 +hideListLinks: true +--- + +These steps install the Context Gateway Control Plane and Data Plane with the `redis-context-gateway` Helm chart, and end with one authenticated Model Context Protocol (MCP) call. + +You use three credentials along the way: the Control Plane admin token, the Identity Service control token, and an agent key. For what each credential unlocks, see [Credentials](/content/operate/iris/context-gateway/self-managed/authentication.md#credentials). + +Before you begin, review the [prerequisites](/content/operate/iris/context-gateway/self-managed/prerequisites.md). + +## 1. Create the namespace + +```bash +kubectl create namespace +``` + +## 2. Create Secrets + +### Configure the Identity Service release first + +Set these values on the Identity Service release so that it checks Gateway grants and accepts the Data Plane's runtime token: + +| Identity Service value | Setting | +| --- | --- | +| `runtime.serviceCredentials[]` | A credential for the Data Plane with `allowedOperations: [api-key-introspect]` and `allowedProducts: [context-gateway]`, from the same Secret as `dataplane.runtimeToken`. | +| `productValidation.products.context-gateway.baseURL` | The Control Plane Service, for example `http://gateway-redis-context-gateway-controlplane:9100`. | +| `productValidation.products.context-gateway.credential` | The same Secret as `controlplane.internalToken`. | +| `runtime.apiKeyTenant.singleTenantID` | The same value as `tenantId`. | + +If the Identity Service release also serves Agent Memory or Agent Playbooks, add these entries to its existing lists. + +### Create the connection files + +Save the Metadata Redis connection as `metadata.yaml`: + +```yaml +metadata_db: + urls: ["rediss://:@:6379"] +``` + +Save the credential Redis connection as `credential-redis.yaml`: + +```yaml +credential_storage: + redis: + urls: ["rediss://:@:6379"] +``` + +Keep real URLs and passwords only in these files, and create Secrets from them. + +### Create the Secrets + +```bash +umask 077 +openssl rand -base64 32 > k1 +kubectl -n create secret generic gateway-metadata --from-file=config.yaml=metadata.yaml +kubectl -n create secret generic gateway-credential-redis --from-file=config.yaml=credential-redis.yaml +kubectl -n create secret generic gateway-keyring --from-file=k1 +kubectl -n create secret generic gateway-license --from-file=license=./license +kubectl -n create secret generic gateway-admin --from-file=token=./admin-token +kubectl -n create secret generic gateway-grant-validation --from-file=token=./grant-token +kubectl -n create secret generic gateway-runtime --from-file=token=./runtime-token +``` + +Create a `databaseConnections` Secret for the registered databases. See [Database registry](/content/operate/iris/context-gateway/self-managed/configuration.md#database-registry). + +## 3. Create `gateway-values.yaml` + +*Complete values file: `tenantId`, `publicBaseURL`, `identityService.baseURL`, image settings, `credentialStorage.redis.keyFiles` and `activeKeyId`, and the file references from step 2.* + +When `identityService.baseURL` is an in-cluster `http://` address, such as `http://ids-iris-identity-service:9200` for an Identity Service release named `ids`, also set `identityService.allowInsecureTransport: true`. + +## Before you install: check your values + +Check each value in `gateway-values.yaml` before you install. If one is wrong, you see the symptom in its row. + +{{< table-scrollable >}} +| Check | Symptom | Fix | +| --- | --- | --- | +| `tenantId` equals the Identity Service's `runtime.apiKeyTenant.singleTenantID` | Pods Ready, but minting an agent key for a Gateway fails | Set both to the same tenant. | +| `publicBaseURL` is the Data Plane origin that MCP clients reach | `mcpEndpoint` in Gateway responses points to an address clients cannot reach, and identity provider sign-in fails | Set the public origin, including `https://`. | +| `identityService.allowInsecureTransport` is `true` for an `http://` Identity Service URL | The Data Plane cannot check agent keys | Set it, or use an `https://` URL. | +| The admin, internal, and runtime tokens come from three different Secrets or keys | Rendering fails, or the Identity Service cannot check keys | Create three distinct token files. | +| `license` points to a valid license | `/v1/health/ready` answers `503` | Mount the license file as issued. | +| Both component `image.repository` and `image.tag` are set | Pods `ImagePullBackOff` | Set both to the release images. | +{{< /table-scrollable >}} + + + +## 4. Install + +```bash +helm repo add redis-ai https://helm.redis.io/ai +helm repo update redis-ai +helm install gateway redis-ai/redis-context-gateway \ + --version \ + --namespace \ + -f gateway-values.yaml \ + --wait --timeout 5m +``` + + + +## 5. Verify pods and health + +```bash +kubectl -n rollout status deployment/gateway-redis-context-gateway-controlplane +kubectl -n rollout status deployment/gateway-redis-context-gateway-dataplane +``` + +Run each port-forward in its own terminal and leave it running: + +```bash +kubectl -n port-forward svc/gateway-redis-context-gateway-controlplane 9100:9100 +``` + +```bash +kubectl -n port-forward svc/gateway-redis-context-gateway-dataplane 8083:8083 +``` + +```bash +kubectl -n port-forward svc/ids-iris-identity-service 9200:9200 +``` + +Then check readiness on both planes: + +```bash +curl -sS http://localhost:9100/v1/health/ready +curl -sS http://localhost:8083/v1/health/ready +``` + +## 6. Create a Gateway + +```bash +curl -sS -X POST http://localhost:9100/v1/gateways \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"displayName": "my-gateway"}' +``` + +The response carries `id` and `mcpEndpoint`. + +## 7. Mint an agent key + +```bash +curl -sS -X POST http://localhost:9200/v1/api-keys \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{ + "name": "my-agent-key", + "grants": [ + { + "tenant": "", + "product": "context-gateway", + "resourceType": "gateway", + "resourceId": "", + "actions": ["use"] + } + ] + }' +``` + +> [!WARNING] +> The response's `token` is the agent key. It is shown only once, so save it in your secret manager. + +## 8. Make one authenticated call + +*MCP `initialize` and `tools/list` requests with curl against `mcpEndpoint`. A new Gateway with no Upstreams lists no tools.* + +## Next steps + +- [API examples](/content/operate/iris/context-gateway/self-managed/api-examples.md) to register an Upstream and hand off to a developer. +- [Operations](/content/operate/iris/context-gateway/self-managed/operations.md) for credential rotation and upgrades. diff --git a/content/operate/iris/context-gateway/self-managed/operations.md b/content/operate/iris/context-gateway/self-managed/operations.md new file mode 100644 index 0000000000..82f97a67fe --- /dev/null +++ b/content/operate/iris/context-gateway/self-managed/operations.md @@ -0,0 +1,105 @@ +--- +Title: Operate self-managed Context Gateway +alwaysopen: false +categories: +- docs +- operate +- iris +description: Check health, rotate credentials and encryption keys, monitor the result cache and the Gateway database, back up, upgrade, and uninstall Context Gateway. +linkTitle: Operations +weight: 90 +hideListLinks: true +--- + +## Check service health + +Both planes serve `/v1/health/live` and `/v1/health/ready` without a credential. Readiness answers `503` while a token source cannot be read or is empty, or the license is not valid. The response does not name the failed check, so read the plane's logs. + +With the ports forwarded as in [Deploy](/content/operate/iris/context-gateway/self-managed/deploy.md#5-verify-pods-and-health): + +```bash +curl -sS http://localhost:9100/v1/health/ready +curl -sS http://localhost:8083/v1/health/ready +``` + +`iris context-gateway health` probes both planes, and `iris context-gateway service diagnose ` reports a Gateway's setup, including Upstreams with empty catalogs. + +## Rotate credentials + +| Credential | How to rotate | +| --- | --- | +| Upstream credential | Update the Upstream with a new value through the Control Plane. After an in-place rotation in Vault or AWS Secrets Manager, refresh the Upstream's catalog so cached results stop being served. | +| Agent key | Rotate or revoke it through the Identity Service. See [Rotate or revoke agent keys](/content/operate/iris/context-gateway/self-managed/api-examples.md#rotate-or-revoke-agent-keys). A revoked key stops working within 300 seconds by default. | +| Admin token, internal token | Replace the Secret. The Control Plane rereads the file, so no restart is needed. Coordinate an internal token change with the Identity Service release. | +| Runtime token | Replace the Secret and update the matching Identity Service credential. The Data Plane rereads it on its next key check. | +| Metadata, credential, and registered database URLs, Vault tokens | Replace the Secret, then restart the planes that read it. | + +Set a file reference's `checksum`, then run `helm upgrade`, to roll out the planes predictably after any file change. + +### Rotate the encryption keyring + +The keyring is loaded at startup, so each step needs a rollout of both planes: + +1. Add `k2` to the keyring and to `credentialStorage.redis.keyFiles`, and keep `k1` active. Change `credentialStorage.redis.keyring.checksum` and upgrade. Both planes can now read either key. +1. Set `credentialStorage.redis.activeKeyId: k2` and upgrade. New credential writes use `k2`, and existing credentials still open with `k1`. +1. Rewrite the remaining credentials through the Control Plane. Keep `k1` until you can prove nothing uses it, including rollbacks and retained backups. + +## Monitor the result cache + +A cache hit carries `resultCache` in the call's invocation metadata. Every other outcome shows only in metrics and traces. `context_gateway_result_cache_lookups_total` counts lookups by `outcome` (`hit`, `miss`, `bypass`, `error`) and `reason`: + +| Bypass `reason` | What to check | +| --- | --- | +| `not_opted_in` | The tool has no result cache policy. | +| `no_database` | The Gateway has no `databaseId`. | +| `unknown_database` | The Gateway's database is not registered on the Data Plane. Declare it on both planes. | +| `vetoed_by_annotation` | The tool's definition says it is not read-only, or that it is destructive. | +| `caller_credentials` | The Upstream's credential could depend on the caller. | +| `uncanonical_arguments` | The arguments repeat a member, are not valid UTF-8, or nest too deep. | +| `unreadable_policy` | The Data Plane is older than the release that wrote the policy. Upgrade it. | + +| Error `reason` | What to check | +| --- | --- | +| `lookup_timeout` | The Gateway database answered slower than `result_cache.lookup_timeout`. Check its latency and load. | +| `lookup_failed` | The Gateway database is down, unreachable, or refuses the Data Plane's user. | +| `unusable_entry` | The entry was written by a newer release. The call's own write replaces it. | + +A call whose lookup fails is served from the Upstream. + +## Watch the Gateway database + +When result handles are on, alert on `context_gateway_result_handles_creates_total{outcome="out_of_memory"}`. It means the Gateway database refused a handle write and the agent received the whole result. Raise `maxmemory`, shorten the handle lifetime, or move handles to a dedicated database. + +Above 90 percent of `maxmemory`, the Data Plane pauses handle creation and counts `skipped_low_memory`. Creation resumes when the database has room. The Data Plane also stops holding handles when the database's eviction policy is not `noeviction` or JSON is unavailable. + +## Update configuration and license Secrets + +Replace the Secret, then roll out both planes. Set `checksum` on a file reference to trigger the rollout automatically. + +## Back up the deployment + +Back up Metadata Redis and credential storage together. A Gateway record without its credentials cannot call its Upstreams. Keep every encryption key that a backed-up credential needs. + + + +## Upgrade the chart + +Upgrade both planes together and roll forward, not back. An older Data Plane cannot read tool policy members that a newer Control Plane wrote, and serves those tools uncached. + +After an upgrade, refresh an Upstream's catalog when the release notes ask for it. + +```bash +helm upgrade gateway redis-ai/redis-context-gateway \ + --version \ + --namespace \ + -f gateway-values.yaml \ + --wait --timeout 5m +``` + +## Uninstall the chart + +```bash +helm uninstall gateway --namespace +``` + +Uninstall removes the planes only. Redis databases, Secrets, Container Storage Interface (CSI) provider classes, Upstream credentials, and Gateway records stay. To remove Gateways and their credentials, delete them through the Control Plane before you uninstall. diff --git a/content/operate/iris/context-gateway/self-managed/prerequisites.md b/content/operate/iris/context-gateway/self-managed/prerequisites.md new file mode 100644 index 0000000000..f7fb1d9c99 --- /dev/null +++ b/content/operate/iris/context-gateway/self-managed/prerequisites.md @@ -0,0 +1,126 @@ +--- +Title: Self-managed Context Gateway prerequisites +alwaysopen: false +categories: +- docs +- operate +- iris +description: Review software, Identity Service, Redis, credential storage, network, Secret, and image prerequisites for self-managed Context Gateway. +linkTitle: Prerequisites +weight: 20 +hideListLinks: true +--- + +## What you need + +| Item | Where it comes from | +| --- | --- | +| Container images | The Context Gateway Control Plane and Data Plane images for the release | +| Helm chart | `redis-context-gateway` chart in the Redis AI Helm repository, or a chart package provided by Redis | +| Identity Service | An Identity Service release for the same tenant, shared with other Iris services | +| Redis databases | You provide Metadata Redis, credential storage, and the databases Gateways use | +| License key | Contact your Redis representative or [contact sales](https://redis.io/contact/). | +| Upstream credentials | You provide the tokens or header values each Model Context Protocol (MCP) server needs | + +## Required software + +| Software | Purpose | +| --- | --- | +| Kubernetes cluster and `kubectl` | Run both planes and create Secrets in the installation namespace. | +| [Helm 3](https://docs.helm.sh/docs/v3/intro/install/) | Install the chart. | +| Bash, curl, and jq | Run the API examples. | + + + +## Redis databases + +The chart consumes existing Redis deployments. It does not provision them. + +| Redis deployment | Requirements | Use | +| --- | --- | --- | +| Metadata Redis | Shared by the Control Plane and Data Plane. | Gateway and Upstream records. | +| Credential Redis | A separate access control list (ACL) user. Only with the encrypted Redis backend. | Encrypted Upstream credentials. | +| Surface databases | The Search module, the JSON module for entities stored as JSON, and `FT.HYBRID` support for hybrid search tools. | Records that Context Surfaces serve as tools. | +| Gateway database | JSON support and `maxmemory-policy noeviction` for result handles. Only for the result cache or result handles. | Cached results and result handles. | + +Use separately managed Redis deployments for metadata, credentials, and customer data. Redis endpoint aliases do not prove that two databases are separate. + +- Give a Context Surface its own database when its records must stay separate from other Gateways. Gateways whose Surfaces share a database can read each other's records. +- Never use one Redis database as both a Surface database and a Gateway database. +- Size the Gateway database for result handles: the rate of calls that become handles, times the stored result size, times the handle lifetime. + +Use `rediss://` URLs with certificate verification. The Federal Information Processing Standards (FIPS)-oriented profile rejects plaintext Redis URLs. + +### Redis users for Surface databases + +Give each plane its own Redis user on every Surface database, and put each login in that plane's registry entry. Both users need the `@connection` commands, `PING`, `INFO`, and `MODULE LIST`. + +| Plane | Commands | Keys | +| --- | --- | --- | +| Data Plane | `FT.SEARCH`, `FT.AGGREGATE`, `FT.HYBRID`, `JSON.GET`, `HMGET` | Read access to each Surface key prefix only | +| Control Plane | `FT.CREATE`, `FT.DROPINDEX`, `FT._LIST`, `COMMAND INFO` | None | + +For example, for a Surface whose entities use the key prefixes `provider:` and `location:`: + +```text +ACL SETUSER surface-dp on > resetkeys %R~provider:* %R~location:* -@all +@connection +ping +info +module|list +ft.search +ft.aggregate +ft.hybrid +json.get +hmget +ACL SETUSER surface-cp on > resetkeys -@all +@connection +ping +info +module|list +ft.create +ft.dropindex +ft._list +command|info +``` + +On an OSS Redis Cluster, also grant `+cluster|slots` to both users. Search commands check key permissions differently across Redis versions, so test the users against your Redis version. + + + +## Credential storage + +| Backend | What you provide | +| --- | --- | +| Encrypted Redis (default) | Credential Redis and a keyring of base64-encoded 32-byte Advanced Encryption Standard (AES) keys. | +| HashiCorp Vault key-value (KV) version 2 | A writer token for the Control Plane and a reader token for the Data Plane. | +| AWS Secrets Manager | An Identity and Access Management (IAM) role for each plane through workload identity. | + +## Network access + +| From | To | Purpose | +| --- | --- | --- | +| MCP clients | Data Plane, through your ingress at `publicBaseURL` | Tool listing and calls. | +| Data Plane | Every Upstream MCP server | Tool calls. | +| Data Plane | Identity Service | Agent-key checks. | +| Identity Service | Control Plane | Grant validation. | +| Operators | Control Plane and Identity Service | Administration. Keep both private. | + +The chart creates `ClusterIP` services. Your platform owns ingress, network policy, and TLS termination. + +Configure the ingress for Streamable HTTP: + +- Pass the MCP request headers through unchanged, including `Authorization`, `X-Api-Key`, `Accept`, `Mcp-Protocol-Version`, `Mcp-Method`, and `Mcp-Name`. +- Pass the `WWW-Authenticate` response header, which MCP clients need to start identity provider sign-in. +- Responses can be server-sent event streams. Turn off response buffering for the Data Plane, and allow read timeouts longer than the slowest tool call. + +## Credentials and Secrets + +Redis URLs, tokens, keyrings, and the license belong in Kubernetes Secrets or Secrets Store Container Storage Interface (CSI) Driver files, never in Helm values or source control. See [Credentials](/content/operate/iris/context-gateway/self-managed/authentication.md#credentials) for each credential and [Configuration](/content/operate/iris/context-gateway/self-managed/configuration.md) for each file reference. + +## Release artifacts and image tags + +| Component | Image | +| --- | --- | +| Control Plane | `redislabs/iris-context-gateway-control:` | +| Data Plane | `redislabs/iris-context-gateway-data:` | + +Set both `image.repository` and `image.tag` from the release manifest. + + + +## Air-gapped and private registry installs + +For a disconnected installation, mirror both images to your registry, set each component's `image.repository`, and set `imagePullSecrets`. Install from a downloaded chart package instead of the Redis AI Helm repository. + +## Helm values to review + +| Value | Decide | +| --- | --- | +| `tenantId` | One tenant per release, equal to the Identity Service's single tenant ID. | +| `publicBaseURL` | The Data Plane origin MCP clients reach. | +| `credentialStorage.provider` | `redis`, `vault`, or `aws`. | +| `security.profile` | `standard` (default) or `fips`. | diff --git a/content/operate/iris/context-gateway/self-managed/reference.md b/content/operate/iris/context-gateway/self-managed/reference.md new file mode 100644 index 0000000000..6d4fc4be17 --- /dev/null +++ b/content/operate/iris/context-gateway/self-managed/reference.md @@ -0,0 +1,56 @@ +--- +Title: Configuration and troubleshooting +alwaysopen: false +categories: +- docs +- operate +- iris +description: Review self-managed Context Gateway configuration files, troubleshooting guidance, and reference links. +linkTitle: Configuration and troubleshooting +weight: 100 +hideListLinks: true +--- + +## Configuration reference + +| File | Purpose | +| --- | --- | +| `gateway-values.yaml` | Helm values for both planes. See [step 3](/content/operate/iris/context-gateway/self-managed/deploy.md#3-create-gateway-valuesyaml). | +| `metadata.yaml` | Metadata Redis URLs. Both planes mount it. | +| `credential-redis.yaml` | Credential Redis URLs, with the encrypted Redis backend. | +| `databases.yaml` | URLs for each registered database. | +| `license` | Context Gateway license file provided by Redis. | + +### External secret managers + +Each file reference accepts a Kubernetes Secret or a Secrets Store Container Storage Interface (CSI) Driver file. Scope each CSI class to the files its plane may read, and keep the admin token, internal token, and Vault writer token out of every class the Data Plane mounts. + +## Troubleshooting + +{{< table-scrollable >}} +| Symptom | Likely cause | Fix | +| --- | --- | --- | +| `/v1/health/ready` answers `503` | A token source cannot be read or is empty, or the license is not valid | Check the admin and internal token Secrets and the license. | +| Agent receives `401` | Key is missing, revoked, or expired, or sent in the wrong header | Send it as `Authorization: Bearer` or `X-Api-Key`, and check its status. | +| Agent receives `403` | The key has no `context-gateway` grant for this Gateway | Update the key's grants with the Identity Service control token. | +| Agent receives `404` | The Gateway does not exist, is being deleted, or belongs to another tenant | Check the Gateway ID and `tenantId`. | +| Minting an agent key fails for a grant | Unknown Gateway, a tenant mismatch, or the wrong product or resource type | Create the Gateway first. Use `product: "context-gateway"` and `resourceType: "gateway"`, and match the tenant. | +| A tool call returns `upstream_credentials_unavailable` | The Data Plane cannot read the Upstream's credential | Check the Data Plane's access to credential storage. | +| A tool call returns `upstream_rejected_credentials` | The Upstream refused the stored credential | Rotate the Upstream credential. | +| Cached results keep coming back after a secret rotation | The secret was rotated in place under the same reference | Refresh the Upstream's catalog. | +| Handles stop being created | The Gateway database is above 90 percent of `maxmemory`, refused a write, has another eviction policy than `noeviction`, or has no JSON support | Raise `maxmemory`, shorten the handle lifetime, set `noeviction`, or move handles to a dedicated database with JSON. | +| Large results suddenly come back as stubs | `result_handles.enabled` was turned on, which applies to every Gateway with a `databaseId` | Opt tools out with a `resultHandle` policy of `capture: never`, or remove the Gateway's `databaseId`. | +| Data Plane does not become ready | It cannot reach the Identity Service, or an `http://` Identity Service URL is used without `identityService.allowInsecureTransport: true` | Check `identityService.baseURL` and the network path. | +| Context Surface search tools return partial results | Redis is still indexing existing keys after a create, a data model change, or a refresh | Wait until `FT.INFO` on the Surface database shows `percent_indexed` at `1`. | +| Deleting a Context Surface keeps answering `503` | Its indexes cannot be dropped, often because the Control Plane's Redis user lacks `FT._LIST` or `FT.DROPINDEX` | Grant the commands, or repeat the delete with `ignoreIndexSweepFailure=true`. | +| A tool policy disappeared | Moving the Upstream to a new endpoint removed policies the new catalog refuses | Read `droppedToolPolicies` in the update response, then set the policies again. | +| A Context Surface was created without tools | `initialCatalogFailure` in the create response names the cause | Fix the database login or permissions, then refresh the Upstream. | +{{< /table-scrollable >}} + +## References + +| Need | Reference | +| --- | --- | +| Helm chart values | `values.yaml` in the chart package | +| Chart examples | `examples/` in the chart package, including Vault, AWS, CSI, and certificate overlays | +| Chart README | `README.md` in the chart package | diff --git a/content/operate/iris/context-retriever/_index.md b/content/operate/iris/context-retriever/_index.md deleted file mode 100644 index 7e62cc77fb..0000000000 --- a/content/operate/iris/context-retriever/_index.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -alwaysopen: false -categories: -- docs -- operate -- iris -description: Expose schema-first retrieval tools from your Redis Cloud data to AI agents. -hideListLinks: true -linktitle: Context Retriever -title: Redis Context Retriever on Redis Cloud -weight: 20 -bannerText: Redis Context Retriever on Redis Cloud is currently available in preview. Features and behavior are subject to change. -bannerChildren: true ---- - -Redis Context Retriever helps teams expose operational context to AI agents through schema-first retrieval. It models the entities, fields, keys, and relationships that matter to an agent workflow, then presents that context through a governed tool surface the agent can call at runtime. Context Retriever helps an AI agent understand what business objects exist, how they connect, and which paths are safe to use. - -![The Context Retriever page in the Redis Cloud console, with Create with CLI and Create custom service options.](/images/rc/context-retriever-get-started.png) -{width="75%" class="border border-redis-pen-300 rounded-lg"} - -When you set up Redis Context Retriever, you model the objects that matter to your agent workflow and connect the relationships between them. You can do this through the UI, the [Context Surfaces Python Client](https://pypi.org/project/redis-context-retriever/), or the `ctxctl` CLI (available when you install the Python client). Context Retriever uses those relationships to automatically create and deploy retrieval tools from your entity model. - -When an agent needs context during execution, it calls the MCP tools Context Retriever exposes. Instead of guessing which tool to use or generating SQL, the agent follows the defined entity paths and gets back structured, live, operational context. - -For more details, see the [Redis Context Retriever overview]({{< relref "/develop/ai/context-engine/context-retriever" >}}). - -## Get started with Context Retriever on Redis Cloud - -{{< embed-md "rc-context-retriever-get-started.md" >}} - -## Get started with self-managed Context Retriever - -To deploy Context Retriever on your own Kubernetes infrastructure instead of Redis Cloud, see [self-managed Context Retriever]({{< relref "/operate/iris/context-retriever/self-managed" >}}). diff --git a/content/operate/iris/context-retriever/create-service.md b/content/operate/iris/context-retriever/create-service.md deleted file mode 100644 index 2a681d423c..0000000000 --- a/content/operate/iris/context-retriever/create-service.md +++ /dev/null @@ -1,115 +0,0 @@ ---- -alwaysopen: false -categories: -- docs -- operate -- iris -description: Create a Context Retriever service in Redis Cloud to expose schema-first retrieval tools to AI agents. -hideListLinks: true -linktitle: Create service -title: Create a Context Retriever service -weight: 5 -aliases: -- /operate/rc/context-engine/context-retriever/create-service/ ---- - -Redis Context Retriever helps teams expose operational context to AI agents through schema-first retrieval. This guide walks you through creating and configuring a Context Retriever service in Redis Cloud. - -## Prerequisites and limitations - -To create a Redis Context Retriever service, you will need a Redis Cloud database that already has relevant data. If you don't have one, see [Create a database]({{< relref "/operate/rc/databases/create-database" >}}). - -> [!NOTE] -> Redis Context Retriever does not support the following databases during public preview: -> - [Redis Flex]({{< relref "operate/rc/databases/create-database/create-flex-database">}}) databases -> - [Active-Active]({{< relref "/operate/rc/databases/active-active" >}}) databases - -## Create a Context Retriever service - -From the [Redis Cloud console](https://cloud.redis.io/), select **Context Retriever** from the left-hand menu. - -If you have not already created a Context Retriever service, you'll see a page with an introduction to Context Retriever. Otherwise, select **New service** to go to the Context Retriever introduction page. - -![The New service button.](/images/rc/langcache-new-service.png) -{width="150px"} - -From here, you can either: - -- Select **Get started** to follow a step-by-step guide using the `ctxctl` CLI, which is installed with the [Context Surfaces Python Client](https://pypi.org/project/redis-context-retriever/). -- Select **Create custom service** to manually configure your own context retriever settings. - -For this guide, select **Create custom service**. - -### General settings - -The **General settings** section defines basic properties of your service. - -![The General settings section.](/images/rc/context-retriever-general-settings.png) - -| Setting name | Description | -|:-------------|:------------| -| **Service name** | Enter a name for your Context Retriever service. We recommend you use a name that describes your service's purpose. | -| **Select database** | Select the Redis Cloud database to use for this service from the list. | -| **Description** | Enter a description for your context retriever. | - -Select **Entities** to continue. - -### Define entities - -In the **Define Entities** step, you'll define the objects that Context Retriever will use to generate retrieval tools. - -![The Entities section.](/images/rc/context-retriever-define-entities.png) - -Select **Add Entity** to add an entity. - -![The Entity table with the Entity Name, Key template, and description.](/images/rc/context-retriever-define-entities-table.png) - -- In the **Entity name** field, enter the name of one of the business objects that is in your database. - - For example, your database might have a bunch of product information with keys like `product:1` or `product:2`. In this case, the entity name would be "Product". - -- In the **Key Template** field, enter the key template for your entity. Use `{id}` to denote where in the key pattern the ID is located. - - For the example above, the key template would be `product:{id}`. - -- You can also enter a description to describe the entity. - -- Select the checkmark to confirm the entity. - -After you are done adding all of the entities, select **Fields** to continue. - -### Configure fields - -In the **Configure fields** step, you'll define the fields of each entity and the relationships between them. - -From here, you can either: - -- Select **Auto-detect fields** to scan your database automatically using a model that will detect the fields for you. - - You'll need to agree to let the model scan your key names and schemas to automatically populate the fields. - - > [!NOTE] - > If you use Auto-detect fields, make sure that the fields and relationships that the model generates are accurate. - -- Select **Manually enter fields** to manually enter the fields yourself. - -![The Configure fields step.](/images/rc/context-retriever-fields.png) - -Expand each entry to view the currently defined fields for that entry. Select **Add field** to add a field. - -![The Add field table.](/images/rc/context-retriever-add-field.png) - -- Enter the field name in the **Field** cell. -- Select the **PK** checkbox if the field is a primary key. You must have at least one primary key. -- If the field is related to any other defined entities, select it from the **Related Entity** dropdown. This is usually set if the field contains primary keys of another entity. -- Select the field's **Type** from the dropdown. -- In the **Index** cell, select one or more index types to enable searching and filtering on that field. -- You can also add a description in the **Description** cell. - -After you set all fields for all of your entities, select **Create** to create your Context Retriever service. - -## Next steps - -After your service is created, you can call the MCP tools Context Retriever exposes from your agent. See the [Context Surfaces Python Client](https://pypi.org/project/redis-context-retriever/) for more information on how to call your tools. - -You can also [view your service]({{< relref "/operate/iris/context-retriever/view-service" >}}). diff --git a/content/operate/iris/context-retriever/self-managed/_index.md b/content/operate/iris/context-retriever/self-managed/_index.md deleted file mode 100644 index 802b1817f1..0000000000 --- a/content/operate/iris/context-retriever/self-managed/_index.md +++ /dev/null @@ -1,368 +0,0 @@ ---- -Title: Install Context Retriever (self-managed) -alwaysopen: false -categories: -- docs -- operate -- iris -description: Install and run Redis Context Retriever on a self-managed Kubernetes cluster using Helm. -linkTitle: Install (self-managed) -weight: 40 -hideListLinks: true -bannerText: Redis Context Retriever self-managed is currently in private preview and subject to change. A license key is required to deploy — contact your Redis representative or [contact sales](https://redis.io/contact/). -bannerChildren: true -aliases: -- /develop/ai/context-engine/context-retriever/install/ ---- - -Context Retriever is distributed as container images on Docker Hub plus a Helm chart shipped in the Redis Enterprise Helm repository. Installation pulls the images from Docker Hub (or your own mirror) and deploys the chart against a Redis database you provide. - -> [!NOTE] -> This guide is for system administrators deploying Context Retriever on a self-managed Kubernetes cluster. - -## What you need - -| Item | Where it comes from | -| ---- | ------------------- | -| Container images | Docker Hub: `redislabs/context-retriever` (MCP) and `redislabs/context-retriever-admin` (Admin) | -| Helm chart | `redis-context-retriever` chart in the Redis Enterprise Helm repository (`ai/charts/redis-context-retriever`) | -| Redis database | **You provide it** — see prerequisites below | -| License key | Contact your Redis representative or [contact sales](https://redis.io/contact/). The key starts with `CS-LICENSE.` | - -## Prerequisites - -### Required software - -| Software | Minimum version | Purpose | -| -------- | --------------- | ------- | -| Kubernetes | 1.28+ | Orchestration | -| kubectl | 1.28+ | Kubernetes CLI | -| Helm | 3.x | Package manager | - -### Redis database (bring your own) - -Context Retriever connects to a Redis database that you provide; it does not ship one. The database must have the Search and JSON capabilities enabled (RediSearch and RedisJSON). - -### Network access - -- **Connected install:** the cluster must be able to pull from `docker.io`. -- **Air-gapped install:** mirror the two images into your internal registry first — see [Air-Gapped Installation](#air-gapped-installation). - -### System requirements - -| Component | CPU | Memory | -| --------- | --- | ------ | -| Admin Server | 250m – 1 core | 256Mi – 1Gi | -| MCP Server | 250m – 1 core | 256Mi – 1Gi | - -## Installation - -### Add the Helm repository - -Add the Redis Enterprise Helm repository (the same source as the other Redis Enterprise AI charts) and list the available chart versions: - -```bash -helm repo add redis-ai https://helm.redis.io/ai -helm repo update redis-ai -helm search repo redis-ai/redis-context-retriever --versions -``` - -Set the version you want to install: - -```bash -export CR_VERSION="" -``` - -### Create a namespace - -```bash -export NS=context-retriever -kubectl create namespace $NS -``` - -### Create secrets - -#### Admin database secret - -```bash -# SECRET_ENCRYPTION_KEY encrypts stored credentials at rest. It must be a -# base64-encoded 32-byte (AES-256) key. Generate one and back it up — if lost, -# encrypted data cannot be recovered. -SECRET_ENCRYPTION_KEY=$(openssl rand -base64 32) -ADMIN_REDIS_PASSWORD="" - -kubectl create secret generic cr-config \ - --namespace $NS \ - --from-literal=REDIS_PASSWORD="$ADMIN_REDIS_PASSWORD" \ - --from-literal=SECRET_ENCRYPTION_KEY="$SECRET_ENCRYPTION_KEY" -``` - -#### License key secret - -```bash -export LICENSE_KEY_FILE_PATH="" - -# License key from Redis (starts with CS-LICENSE.) -kubectl create secret generic cr-license \ - --namespace $NS \ - --from-file=LICENSE_KEY="$LICENSE_KEY_FILE_PATH" -``` - -### Install the chart - -Install the chart, pointing it at your Redis database with `--set redis.addr` — **this is required; the chart will not render without it.** - -```bash -helm install cr redis-ai/redis-context-retriever \ - --version $CR_VERSION \ - --namespace $NS \ - --set redis.addr=":" \ - --set secrets.existingSecret=cr-config \ - --set license.existingSecret=cr-license - -kubectl wait --for=condition=ready pod \ - -l app.kubernetes.io/name=redis-context-retriever \ - -n $NS \ - --timeout=180s -``` - -The chart's image repositories default to `redislabs/context-retriever-admin` and `redislabs/context-retriever`; the images are pulled from Docker Hub. - -### Retrieve the admin API key - -On first startup an admin API key is generated and written to a file in the admin pod: - -```bash -ADMIN_POD=$(kubectl get pods -n $NS \ - -l app.kubernetes.io/component=admin \ - -o jsonpath='{.items[0].metadata.name}') - -kubectl exec -n $NS "$ADMIN_POD" -- cat /opt/initialAdminKey.txt -``` - -> [!WARNING] -> Save this key immediately. It is only available on first startup and cannot be recovered if lost (see [Troubleshooting](#lost-admin-api-key)). - -### Install the CLI (optional) - -The CLI ships on PyPI: - -```bash -pip install redis-context-retriever - -ctxctl config set default_output_format table --profile onprem -export CTX_PROFILE=onprem - -ctxctl config set api_url "" -ctxctl config set mcp_url "" -``` - -### Verify - -Check that both services are healthy: - -```bash -curl -sf http:///health | jq -curl -sf http:///health | jq -``` - -If you installed the CLI, you can also list the configured surfaces: - -```bash -ctxctl surface list -``` - -## Air-Gapped Installation - -Context Retriever supports air-gapped clusters by mirroring the published images into your own OCI registry and installing from a locally downloaded copy of the Helm chart — there is no separate air-gapped bundle. - -Mirror the images and download the chart (steps 1 and 2) on a host with internet access, then transfer them into your air-gapped environment before installing (step 3). - -> [!NOTE] -> These commands assume the image tag matches the chart version (`$CR_VERSION`). If the images use a different tag, check the chart defaults with `helm show values redis-ai/redis-context-retriever` and set `admin.image.tag` and `mcp.image.tag` accordingly. - -1. Mirror both images to your internal registry: - - ```bash - for img in context-retriever context-retriever-admin; do - docker pull "redislabs/${img}:${CR_VERSION}" - docker tag "redislabs/${img}:${CR_VERSION}" "registry.internal.example.com/${img}:${CR_VERSION}" - docker push "registry.internal.example.com/${img}:${CR_VERSION}" - done - ``` - -2. Download and unpack the Helm chart for the version you are installing: - - ```bash - helm repo add redis-ai https://helm.redis.io/ai - helm repo update redis-ai - helm pull redis-ai/redis-context-retriever --version "$CR_VERSION" --untar - # unpacks to ./redis-context-retriever/ - ``` - -3. In the air-gapped environment, create an image pull secret for your registry and install from the local chart directory, pointing the image repositories at your registry: - - ```bash - kubectl create secret docker-registry regcred \ - --namespace $NS \ - --docker-server=registry.internal.example.com \ - --docker-username="" --docker-password="" - - helm install cr ./redis-context-retriever \ - --namespace $NS \ - --set redis.addr=":" \ - --set secrets.existingSecret=cr-config \ - --set license.existingSecret=cr-license \ - --set admin.image.repository=registry.internal.example.com/context-retriever-admin \ - --set mcp.image.repository=registry.internal.example.com/context-retriever \ - --set admin.image.tag="${CR_VERSION}" \ - --set mcp.image.tag="${CR_VERSION}" \ - --set 'imagePullSecrets[0].name=regcred' - ``` - -> [!NOTE] -> If your registry allows unauthenticated pulls, skip creating `regcred` and remove the `--set 'imagePullSecrets[0].name=regcred'` line from the command above. - -Your Redis database is separate and must likewise be reachable from the air-gapped cluster. - -## Production notes - -| Area | Recommendation | -| ---- | -------------- | -| Redis | Use a highly available Redis database. Set `redis.addr` to its endpoint, and `redis.tlsEnabled=true` if it terminates TLS. | -| Replicas | Run more than one replica of each service: `--set admin.replicaCount=2 --set mcp.replicaCount=2`. | -| Encryption key | Store `SECRET_ENCRYPTION_KEY` securely. It is required to decrypt stored credentials and has no recovery path. | - -## Trust a custom Redis CA certificate - -If your Redis presents a TLS certificate signed by a private or self-signed certificate authority (CA) — common with Redis Enterprise — Context Retriever must trust that CA, or the connection fails certificate verification. Use `redis.tlsCA.existingSecrets` to mount one or more CA certificates; the chart adds them to the system trust store of both the Admin and MCP services, so they are trusted automatically with no application configuration. The image's built-in public CA bundle stays trusted — your CAs are added on top. - -> [!NOTE] -> This requires `redis.tlsEnabled: true`, and each CA Secret must already exist in the release namespace before you install or upgrade. - -### Create a Secret with the CA certificate - -```bash -kubectl create secret generic my-redis-ca \ - --from-file=ca.crt=/path/to/redis-ca.pem \ - --namespace $NS -``` - -The `--from-file==` syntax sets the Secret key explicitly — `ca.crt` in this example. If you omit the key (`--from-file=/path/to/file`), kubectl uses the file's basename instead. If you use a different key name (for example, `--from-file=root.pem=...`), set `key` accordingly in the values below. - -### Reference the Secret in your values - -```yaml -redis: - tlsEnabled: true - tlsCA: - existingSecrets: - - secretName: my-redis-ca # key defaults to "ca.crt" - - secretName: another-redis-ca # list as many CAs as you need - key: tls.crt # override when the Secret uses another key -``` - -### Values - -| Key | Default | Description | -| --- | --- | --- | -| `redis.tlsCA.existingSecrets` | `[]` | CA certificates to trust for Redis TLS. Empty mounts nothing (the default). | -| `redis.tlsCA.existingSecrets[].secretName` | — | **Required.** Name of a pre-created Secret holding the CA certificate in PEM format. | -| `redis.tlsCA.existingSecrets[].key` | `ca.crt` | Key within the Secret whose value is the PEM certificate. | - -Certificates are mounted read-only and picked up automatically; the default empty list mounts nothing, so existing installs are unaffected. This affects only Context Retriever's own connection to Redis. - -## Configuration reference - -### Environment variables (set by the chart) - -| Variable | Description | Required | -| -------- | ----------- | -------- | -| `AUTH_MODE` | `local` for self-managed (API-key auth only) | Yes | -| `REDIS_ADDR` | Redis database endpoint | Yes | -| `REDIS_USERNAME` | Redis ACL username (Redis 6+). Omit to authenticate as the default user. | No | -| `REDIS_PASSWORD` | Redis password | If your DB requires auth | -| `SECRET_ENCRYPTION_KEY` | AES-256 key (base64) for stored secrets | Yes | -| `LICENSE_KEY` | License key string (`CS-LICENSE...`) | Yes | -| `LOG_LEVEL` | `debug`, `info`, `warn`, `error` | No | - -### Key Helm values - -```yaml -## the defaults are commented -# authMode: local -# -# admin: -# replicaCount: 1 -# image: -# repository: redislabs/context-retriever-admin -# tag: '' # set to the release version -# service: { type: NodePort, port: 8080, nodePort: 30080 } -# -# mcp: -# replicaCount: 1 -# image: -# repository: redislabs/context-retriever -# tag: '' # set to the release version -# service: { type: NodePort, port: 8081, nodePort: 30081 } - -redis: - addr: '' - # username: '' # optional Redis ACL username (Redis 6+); omit to use the default user - tlsEnabled: false - -secrets: - existingSecret: cr-config - -license: - existingSecret: cr-license - -# imagePullSecrets: [] # set for a private/mirror registry -``` - -## Troubleshooting - -### Pod not starting - -```bash -kubectl describe pod -n $NS -l app.kubernetes.io/component=admin -kubectl logs -n $NS -l app.kubernetes.io/component=admin -``` - -Common causes: license secret missing/invalid; Redis unreachable or missing the Search/JSON capabilities; `SECRET_ENCRYPTION_KEY` not set; `redis.addr` not provided (the chart fails to render with a clear error if it is empty). - -### License validation failed - -```bash -kubectl get secret cr-license -n $NS -kubectl logs -n $NS -l app.kubernetes.io/component=admin | grep -i license -``` - -### Lost admin API key - -> [!WARNING] -> There is no recovery mechanism by design. - -If the key is lost and the pod has restarted, wipe and reinstall: - -```bash -kubectl delete namespace $NS -# then reinstall from the beginning and save the new key -``` - -## Cleanup - -```bash -helm uninstall cr -n $NS -kubectl delete namespace $NS -``` - -## Support - -Contact Redis support with your license ID, the release version, `kubectl version`, and sanitized diagnostic logs: - -```bash -kubectl get pods -n $NS -kubectl logs -n $NS -l app.kubernetes.io/component=admin --tail=100 -curl -s http://:8080/health | jq '.license' -``` diff --git a/content/operate/iris/context-retriever/view-admin-keys.md b/content/operate/iris/context-retriever/view-admin-keys.md deleted file mode 100644 index bc2b6690a0..0000000000 --- a/content/operate/iris/context-retriever/view-admin-keys.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -alwaysopen: false -categories: -- docs -- operate -- iris -description: View and manage Context Retriever admin keys in Redis Cloud. -hideListLinks: true -linktitle: View admin keys -title: View and manage Context Retriever admin keys -weight: 25 -aliases: -- /operate/rc/context-engine/context-retriever/view-admin-keys/ ---- - -After you have [created your first Context Retriever service]({{< relref "/operate/iris/context-retriever/create-service" >}}), you can view and manage your admin keys from the **Admin keys** tab in the **Context Retriever** section of the Redis Cloud console. - -A Context Retriever **admin key** authorizes administrative operations against Context Retriever in your Redis Cloud account, such as creating, updating, or deleting services and their entity models. You can use it with the [Context Surfaces Python Client](https://pypi.org/project/redis-context-retriever/) and `cxtctl` CLI to create an Agent key and call your tools. - -## Admin keys tab - -From the [Redis Cloud console](https://cloud.redis.io/), select **Context Retriever** from the left-hand menu, then select the **Admin keys** tab. - -![The Admin keys tab showing the list of admin keys.](/images/rc/context-retriever-admin-keys-list.png) - -This tab shows a list of all admin keys associated with Context Retriever in your account. Here, you can generate a new admin key or remove any keys that are no longer in use. You can generate or remove admin keys at any time. - -| Column | Description | -|:-------|:------------| -| **Name** | The name you assigned to the admin key when you created it. | -| **Value** | An abbreviated value of the admin key. | -| **Created** | The date and time the admin key was generated. | - -## Generate a new admin key - -To generate a new admin key: - -1. Select **New admin key**. - - ![The New admin key button.](/images/rc/context-retriever-new-admin-key.png) - {width="150px"} - -1. Enter a name for your admin key. - - ![The Add admin key window.](/images/rc/context-retriever-add-admin-key.png) - {width="37.5%"} - -1. Select **Generate key** to generate your new admin key. - -1. The new key will appear in a dialog box. Select **Copy** to copy the key to the clipboard. - - ![The Context Retriever admin key window. Use the Copy button to save the admin key to the clipboard.](/images/rc/context-retriever-admin-key.png) - {width="40%"} - - > [!WARNING] - > This is the only time the value of the admin key is available. Save it to a secure location before closing the dialog box.

    - > - > If you lose the admin key value, you will need to generate a new admin key. - -## Delete an admin key - -To delete an admin key, select the **Delete admin key** button next to the key you want to remove. - -![Delete button.](/images/rc/icon-delete-lb.png) -{width="36px"} - -Deleting an admin key immediately revokes any administrative operations that rely on it. This action cannot be undone. diff --git a/content/operate/iris/context-retriever/view-service.md b/content/operate/iris/context-retriever/view-service.md deleted file mode 100644 index 9e9739e698..0000000000 --- a/content/operate/iris/context-retriever/view-service.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -alwaysopen: false -categories: -- docs -- operate -- iris -description: View and manage your Context Retriever service in Redis Cloud. -hideListLinks: true -linktitle: View service -title: View and manage Context Retriever service -weight: 15 -aliases: -- /operate/rc/context-engine/context-retriever/view-service/ ---- - -After you have [created your first Context Retriever service]({{< relref "/operate/iris/context-retriever/create-service" >}}), selecting **Context Retriever** from the Redis Cloud Console menu will take you to the **Context Retriever Services** page. - -This page displays a list of all Context Retriever services associated with your account. - -![The Context Retriever service in the Context Retriever service list.](/images/rc/context-retriever-service-list.png) - -Select your Context Retriever service from the list to view the service's details. - -## Overview tab - -The **Overview** tab lets you view the details of your Context Retriever service. It contains the following sections: - -- The **Details** section provides the connection details and general settings for your Context Retriever service. -- The **Entities** section shows the entities, fields, and relationships that Context Retriever exposes as tools. -- The **Tools** section shows which tools are available to Agents. -- The **Actions** section lets you delete your Context Retriever service. - -### Details - -The **Details** section provides general settings for your Context Retriever service. - -![The General settings for the Context Retriever service.](/images/rc/context-retriever-view-details.png) - -| Setting name | Description | -|:-------------|:------------| -| **Description** | The description of your Context Retriever service. | -| **Created** | The creation date for your service. | -| **Updated** | The date your service was last updated. | - -### Entities - -The **Entities** section shows the entities, fields, and relationships that Context Retriever uses to generate retrieval tools. - -![The Entities section for the Context Retriever service.](/images/rc/context-retriever-view-entities.png) - -Expand each entity to view its fields, primary keys, related entities, types, and indexes. - -### Tools - -The **Tools** section shows the tools that are available to your Agents. - -![The Entities section for the Context Retriever service.](/images/rc/context-retriever-view-tools.png) - -You can use your Agents to call your tools. For more information, see the [Context Surfaces Python Client](https://pypi.org/project/redis-context-retriever/) - -### Actions - -The **Actions** section lets you delete your Context Retriever service. - -![The actions for the Context Retriever service.](/images/rc/context-retriever-view-actions.png) - -#### Delete service - -Deleting your Context Retriever service permanently removes the service configuration, its entity model, and the generated tools. It also immediately terminates all API keys associated with the service. Data stored in the underlying Redis Cloud database remains unaffected. - -To delete your Context Retriever service: - -1. Select **Delete**. - -1. A confirmation dialog will appear. Select the checkbox to confirm that you want to delete the service. - -1. Select **Delete** again to confirm. - -Deleting the Context Retriever service is permanent and cannot be undone. diff --git a/content/operate/rc/changelog/may-2026.md b/content/operate/rc/changelog/may-2026.md index 0058f8abe0..2cd20c414a 100644 --- a/content/operate/rc/changelog/may-2026.md +++ b/content/operate/rc/changelog/may-2026.md @@ -18,7 +18,7 @@ tags: ### Redis Iris -Redis Iris is a suite of fully-managed services that give AI agents the context engine they need to reliably act on business data. Both [Redis Agent Memory](/content/operate/iris/agent-memory/_index.md) and [Redis Context Retriever](/content/operate/iris/context-retriever/_index.md) are now available on Redis Cloud as a public preview. +Redis Iris is a suite of fully-managed services that give AI agents the context engine they need to reliably act on business data. Both [Redis Agent Memory](/content/operate/iris/agent-memory/_index.md) and Redis Context Retriever are now available on Redis Cloud as a public preview. ### Bring your own Cloud resource tags diff --git a/content/operate/rc/context-engine/_index.md b/content/operate/rc/context-engine/_index.md index 5e5b91b085..d08bf91821 100644 --- a/content/operate/rc/context-engine/_index.md +++ b/content/operate/rc/context-engine/_index.md @@ -19,5 +19,4 @@ weight: 36 * [Semantic caching with LangCache on Redis Cloud](/content/operate/iris/langcache/_index.md) Store LLM responses for AI applications in Redis Cloud. -* [Redis Context Retriever on Redis Cloud](/content/operate/iris/context-retriever/_index.md) Expose schema first retrieval tools from your Redis Cloud data to AI agents. diff --git a/content/operate/rc/context-engine/context-retriever/_index.md b/content/operate/rc/context-engine/context-retriever/_index.md deleted file mode 100644 index 871f43108e..0000000000 --- a/content/operate/rc/context-engine/context-retriever/_index.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -alwaysopen: false -categories: -- docs -- operate -- rc -description: Expose schema first retrieval tools from your Redis Cloud data to AI agents. -hideListLinks: true -linktitle: Context Retriever -title: Redis Context Retriever on Redis Cloud -weight: 30 ---- - -Redis Context Retriever is available as a managed service on Redis Cloud. - -Use the Redis Iris operational documentation to [create and manage a Context Retriever service](/content/operate/iris/context-retriever/_index.md). diff --git a/layouts/shortcodes/context-retriever-demo.html b/layouts/shortcodes/context-retriever-demo.html deleted file mode 100644 index 21ab600f05..0000000000 --- a/layouts/shortcodes/context-retriever-demo.html +++ /dev/null @@ -1,20 +0,0 @@ -{{- /* - context-retriever-demo - ---------------------- - Scripted, dependency-free demo of Redis Context Retriever: sample customer - keys, a data model that generates MCP tools, and a scripted agent calling - those tools. Runs entirely in the browser; no service is called. - - Usage (no arguments): - {{< context-retriever-demo >}} -*/ -}} - -{{- if not (.Page.Store.Get "rcrAssetsEmitted") -}} - - -{{- .Page.Store.Set "rcrAssetsEmitted" true -}} -{{- end -}} - -
    - -
    diff --git a/layouts/shortcodes/iris-demo.html b/layouts/shortcodes/iris-demo.html index d336532790..ad83003241 100644 --- a/layouts/shortcodes/iris-demo.html +++ b/layouts/shortcodes/iris-demo.html @@ -35,7 +35,7 @@
    diff --git a/static/images/ai/context-engine/context-gateway-exposure-modes.svg b/static/images/ai/context-engine/context-gateway-exposure-modes.svg new file mode 100644 index 0000000000..f44d5cda5b --- /dev/null +++ b/static/images/ai/context-engine/context-gateway-exposure-modes.svg @@ -0,0 +1 @@ +

    Full mode:
    list tools

    Every tool
    definition

    Call a tool
    by name

    Search mode:
    list tools

    search_tools
    and run_tool

    Search returns
    matching tools

    run_tool calls
    the chosen tool

    \ No newline at end of file diff --git a/static/images/ai/context-engine/context-gateway-handles.svg b/static/images/ai/context-engine/context-gateway-handles.svg new file mode 100644 index 0000000000..b72400a754 --- /dev/null +++ b/static/images/ai/context-engine/context-gateway-handles.svg @@ -0,0 +1 @@ +

    Full result

    Requested part

    Tool call over
    the threshold, or
    run_tool_as_handle

    Agent receives
    a small stub

    Read a page
    or a query

    Release
    the handle

    Gateway database

    \ No newline at end of file diff --git a/static/images/ai/context-engine/context-gateway-overview.svg b/static/images/ai/context-engine/context-gateway-overview.svg new file mode 100644 index 0000000000..9696fbc63e --- /dev/null +++ b/static/images/ai/context-engine/context-gateway-overview.svg @@ -0,0 +1 @@ +

    One agent key

    Check the key

    MCP tools

    Generated tools

    Upstream credentials

    Agent

    Context Gateway

    Identity Service

    MCP server

    Context Surface

    Redis database

    Secrets backend

    \ No newline at end of file diff --git a/static/images/ai/context-engine/context-gateway-result-cache.svg b/static/images/ai/context-engine/context-gateway-result-cache.svg new file mode 100644 index 0000000000..e8e156f8c1 --- /dev/null +++ b/static/images/ai/context-engine/context-gateway-result-cache.svg @@ -0,0 +1 @@ +

    No

    Yes

    Yes

    No

    Tool call

    Cache policy
    for this tool?

    Call the Upstream

    Stored result
    for this call?

    Return the stored result

    Call the Upstream

    Store the result
    for the TTL

    Return the result

    \ No newline at end of file diff --git a/static/images/ai/context-engine/context-gateway-session.svg b/static/images/ai/context-engine/context-gateway-session.svg new file mode 100644 index 0000000000..6a242b530c --- /dev/null +++ b/static/images/ai/context-engine/context-gateway-session.svg @@ -0,0 +1 @@ +

    Tool calls
    and results

    Task

    Agent turn:
    model and MCP tools

    Reply

    Context Gateway

    MCP servers and
    Context Surfaces

    \ No newline at end of file diff --git a/static/js/context-retriever-demo.js b/static/js/context-retriever-demo.js deleted file mode 100644 index 3e94e9fb36..0000000000 --- a/static/js/context-retriever-demo.js +++ /dev/null @@ -1,927 +0,0 @@ -/* ========================================================================= - Redis Context Retriever scripted demo - Dependency-free. Everything runs in the browser against an in-memory - sample dataset; no Context Retriever service is called. - - Tool names, argument shapes, descriptions, and response envelopes mirror - what a live Context Retriever MCP server returned (Sep 2026). Two parts are - modelled rather than observed: relationship names, and text_query / - numeric_conditions on the per-entity filter tool (observed on the set - tools only). - ========================================================================= */ -(function () { - "use strict"; - - /* ------------------------------------------------------------------ data */ - - /* A fictional food delivery app: customers, restaurants, and orders. */ - function ts(y, m, d, h) { return Math.floor(Date.UTC(y, m - 1, d, h == null ? 12 : h) / 1000); } - - var CUSTOMERS = [ - ["u101", "Maya Chen", "austin", "vegetarian", "thai", 1240, ts(2024, 5, 3)], - ["u102", "Liam Ortiz", "austin", "none", "bbq", 860, ts(2025, 1, 19)], - ["u103", "Priya Nair", "seattle", "vegan", "indian", 2310, ts(2023, 9, 12)], - ["u104", "Noah Williams", "denver", "none", "pizza", 410, ts(2025, 11, 2)], - ["u105", "Sofia Rossi", "seattle", "vegetarian", "italian", 1780, ts(2024, 2, 27)], - ["u106", "Ethan Brooks", "denver", "gluten_free", "sushi", 950, ts(2024, 8, 14)], - ["u107", "Aisha Khan", "austin", "halal", "mediterranean", 1530, ts(2023, 12, 6)], - ["u108", "Lucas Meyer", "denver", "none", "burgers", 120, ts(2026, 7, 21)] - ].map(function (r) { - return { id: r[0], name: r[1], email: r[1].toLowerCase().replace(/[^a-z]+/g, ".") + "@example.com", - city: r[2], dietary: r[3], favorite_cuisine: r[4], loyalty_points: r[5], joined_ts: r[6] }; - }); - - var RESTAURANTS = [ - ["r201", "Bangkok Street Kitchen", "thai", "austin", 4.7, 2, 30], - ["r202", "Smokehouse 512", "bbq", "austin", 4.4, 3, 40], - ["r203", "Green Bowl", "vegan", "seattle", 4.8, 2, 25], - ["r204", "Sakura Sushi Bar", "sushi", "denver", 4.6, 3, 35], - ["r205", "Nonna's Trattoria", "italian", "seattle", 4.5, 3, 45], - ["r206", "Slice Society", "pizza", "denver", 4.2, 1, 30], - ["r207", "Olive & Za'atar", "mediterranean", "austin", 4.6, 2, 30], - ["r208", "Spice Route", "indian", "seattle", 4.3, 2, 40], - ["r209", "Lotus Thai", "thai", "austin", 4.1, 1, 25] - ].map(function (r) { - return { id: r[0], name: r[1], cuisine: r[2], city: r[3], rating: r[4], price_level: r[5], avg_delivery_min: r[6] }; - }); - - var ORDERS = [ - ["o3001", "u101", "r201", "delivered", 34.5, ts(2026, 9, 2, 19), "Pad thai with tofu, spring rolls"], - ["o3002", "u101", "r207", "delivered", 28, ts(2026, 9, 10, 13), "Falafel wrap, hummus"], - ["o3003", "u101", "r209", "on_the_way", 22.75, ts(2026, 9, 24, 18), "Green curry without fish sauce, jasmine rice"], - ["o3004", "u102", "r202", "delivered", 61.2, ts(2026, 9, 5, 20), "Brisket platter, mac and cheese"], - ["o3005", "u102", "r201", "cancelled", 19.9, ts(2026, 8, 28, 12), "Pad see ew"], - ["o3006", "u103", "r203", "delivered", 24, ts(2026, 9, 12, 12), "Buddha bowl, oat latte"], - ["o3007", "u103", "r208", "delivered", 38.4, ts(2026, 9, 18, 19), "Chana masala, garlic naan"], - ["o3008", "u104", "r206", "delivered", 42, ts(2026, 9, 6, 20), "Two large pizzas"], - ["o3009", "u104", "r204", "delivered", 67.8, ts(2026, 9, 20, 19), "Omakase set for two"], - ["o3010", "u105", "r205", "delivered", 54.3, ts(2026, 9, 8, 20), "Mushroom risotto, tiramisu"], - ["o3011", "u105", "r203", "preparing", 21.5, ts(2026, 9, 24, 18), "Vegan lasagna"], - ["o3012", "u106", "r204", "delivered", 48.9, ts(2026, 9, 14, 19), "Salmon nigiri, miso soup"], - ["o3013", "u107", "r207", "delivered", 36.2, ts(2026, 9, 11, 13), "Chicken shawarma plate"], - ["o3014", "u107", "r201", "delivered", 26, ts(2026, 8, 30, 19), "Pad thai with chicken"], - ["o3015", "u108", "r206", "delivered", 18.5, ts(2026, 9, 15, 12), "Pepperoni slice combo"], - ["o3016", "u108", "r204", "on_the_way", 29.4, ts(2026, 9, 24, 18), "Spicy tuna roll, edamame"], - ["o3017", "u106", "r206", "delivered", 26, ts(2026, 9, 21, 20), "Gluten-free margherita"], - ["o3018", "u101", "r201", "delivered", 31, ts(2026, 9, 19, 19), "Pad thai with tofu, mango sticky rice"], - ["o3019", "u105", "r205", "delivered", 47.6, ts(2026, 9, 22, 20), "Eggplant parmigiana, focaccia"], - ["o3020", "u102", "r209", "delivered", 25.3, ts(2026, 9, 16, 13), "Drunken noodles, Thai iced tea"] - ].map(function (r) { - return { id: r[0], customer_id: r[1], restaurant_id: r[2], status: r[3], total: r[4], placed_ts: r[5], items: r[6] }; - }); - - var RECORDS = { Customer: CUSTOMERS, Restaurant: RESTAURANTS, Order: ORDERS }; - - /* Keys that exist in the sample database, including some that are not entities. */ - var KEYS = []; - CUSTOMERS.forEach(function (c) { KEYS.push({ key: "customer:" + c.id, type: "JSON", entity: "Customer", doc: c }); }); - RESTAURANTS.forEach(function (r) { KEYS.push({ key: "restaurant:" + r.id, type: "JSON", entity: "Restaurant", doc: r }); }); - ORDERS.forEach(function (o) { KEYS.push({ key: "order:" + o.id, type: "JSON", entity: "Order", doc: o }); }); - KEYS.push({ key: "cart:u104", type: "hash", doc: null }); - KEYS.push({ key: "stats:orders:hourly", type: "TSDB-TYPE", doc: null }); - - function defaultModel() { - return [ - { name: "Customer", template: "customer:{id}", desc: "A person who orders food through the app.", fields: [ - { name: "id", type: "string", pk: true, index: "tag", desc: "Customer ID." }, - { name: "name", type: "string", index: "text", desc: "Full name." }, - { name: "email", type: "string", index: "none", desc: "Contact email." }, - { name: "city", type: "string", index: "tag", desc: "Delivery city." }, - { name: "dietary", type: "string", index: "tag", desc: "Dietary preference: none, vegetarian, vegan, gluten_free, or halal." }, - { name: "favorite_cuisine", type: "string", index: "tag", desc: "Most-ordered cuisine." }, - { name: "loyalty_points", type: "number", index: "numeric", desc: "Loyalty points balance." }, - { name: "joined_ts", type: "number", index: "numeric", desc: "Signup time, Unix seconds." } - ] }, - { name: "Restaurant", template: "restaurant:{id}", desc: "A restaurant that delivers through the app.", fields: [ - { name: "id", type: "string", pk: true, index: "tag", desc: "Restaurant ID." }, - { name: "name", type: "string", index: "text", desc: "Restaurant name." }, - { name: "cuisine", type: "string", index: "tag", desc: "Cuisine type." }, - { name: "city", type: "string", index: "tag", desc: "City the restaurant delivers in." }, - { name: "rating", type: "number", index: "numeric", desc: "Average rating, 1 to 5." }, - { name: "price_level", type: "number", index: "numeric", desc: "Price level, 1 to 4." }, - { name: "avg_delivery_min", type: "number", index: "numeric", desc: "Average delivery time in minutes." } - ] }, - { name: "Order", template: "order:{id}", desc: "A delivery order placed by a customer at a restaurant.", fields: [ - { name: "id", type: "string", pk: true, index: "tag", desc: "Order ID." }, - { name: "customer_id", type: "string", index: "tag", rel: "Customer", desc: "Customer who placed the order." }, - { name: "restaurant_id", type: "string", index: "tag", rel: "Restaurant", desc: "Restaurant that made the order." }, - { name: "status", type: "string", index: "tag", desc: "preparing, on_the_way, delivered, or cancelled." }, - { name: "total", type: "number", index: "numeric", desc: "Order total in USD." }, - { name: "placed_ts", type: "number", index: "numeric", desc: "Order time, Unix seconds." }, - { name: "items", type: "string", index: "text", desc: "What was ordered." } - ] } - ]; - } - - /* --------------------------------------------------------------- helpers */ - - function esc(s) { - return String(s).replace(/[&<>"']/g, function (c) { - return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; - }); - } - function el(tag, cls, html) { - var e = document.createElement(tag); - if (cls) e.className = cls; - if (html != null) e.innerHTML = html; - return e; - } - function lower(s) { return s.charAt(0).toLowerCase() + s.slice(1); } - function snake(s) { return s.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase(); } - function fmtDate(t) { return new Date(t * 1000).toISOString().slice(0, 10); } - function hash8(s) { - var h = 2166136261; - for (var i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } - return ("00000000" + (h >>> 0).toString(16)).slice(-8); - } - - /* Pretty JSON with light syntax colouring. */ - function jsonHtml(v) { - var s = JSON.stringify(v, null, 2); - return esc(s).replace(/("(?:[^&]|&(?!quot;))*?")(\s*:)?|\b(true|false|null)\b|(-?\d+(?:\.\d+)?)/g, - function (m, str, colon, lit, num) { - if (str) return colon ? '' + str + "" + colon : '' + str + ""; - if (lit) return '' + lit + ""; - return '' + num + ""; - }); - } - - function templateRegex(t) { - if (t.indexOf("{id}") === -1) return null; - var parts = t.split("{id}").map(function (p) { return p.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }); - return new RegExp("^" + parts.join("([^:]+)") + "$"); - } - - /* ---------------------------------------------------- model -> tool list */ - - function fieldsBy(ent, idx) { return ent.fields.filter(function (f) { return !f.pk && f.index === idx; }).map(function (f) { return f.name; }); } - - function relationships(model) { - var rels = []; - model.forEach(function (e) { - e.fields.forEach(function (f) { - if (!f.rel) return; - var target = model.filter(function (t) { return t.name === f.rel; })[0]; - if (!target) return; - rels.push({ name: e.name + "." + lower(f.rel), from: e.name, to: f.rel, via: f.name, reverse: false }); - rels.push({ name: f.rel + "." + snake(e.name) + "s", from: f.rel, to: e.name, via: f.name, reverse: true }); - }); - }); - return rels; - } - - function limitProps(max) { - return { - limit: { description: "Maximum number of results to return (default: 10, max: " + max + "). A larger request returns " + max + " results; use offset to page through the rest.", type: "number" }, - offset: { description: "Number of results to skip (default: 0)", type: "number" } - }; - } - - function conditionSchemas(tags, nums, texts) { - var p = {}; - if (tags.length) p.tag_conditions = { - description: "Exact-match tag conditions, ANDed with every other condition. Omit the parameter (or pass []) when there are none.", - type: "array", maxItems: 16, - items: { type: "object", additionalProperties: false, required: ["field", "value"], properties: { - field: { description: "The tag field this condition applies to.", type: "string", enum: tags }, - value: { description: "Exact TAG value to match.", type: "string" }, - exclude: { description: "When true, match records where this field is present and does not contain the exact value.", type: "boolean" } } } - }; - if (nums.length) p.numeric_conditions = { - description: "Inclusive numeric-range conditions, ANDed together.", - type: "array", maxItems: 16, - items: { type: "object", additionalProperties: false, required: ["field"], properties: { - field: { description: "Numeric field on the entity.", type: "string", enum: nums }, - min_value: { description: "Inclusive lower bound; omit for unbounded.", type: "number" }, - max_value: { description: "Inclusive upper bound; omit for unbounded.", type: "number" } } } - }; - if (texts.length) { - p.text_query = { description: "Lexical text to match on the entity's text fields (" + texts.join(", ") + ").", type: "string" }; - p.text_match_mode = { description: "How text_query terms combine: any (default), all, or phrase.", type: "string", enum: ["any", "all", "phrase"] }; - } - return p; - } - - function buildTools(model) { - var tools = []; - var rels = relationships(model); - var names = model.map(function (e) { return e.name; }); - model.forEach(function (e) { - var pk = e.fields.filter(function (f) { return f.pk; })[0]; - var tags = fieldsBy(e, "tag"), nums = fieldsBy(e, "numeric"), texts = fieldsBy(e, "text"); - var sn = snake(e.name); - var sample = (RECORDS[e.name] && RECORDS[e.name][0] || {}).id || "3"; - if (pk) tools.push({ name: "get_" + sn + "_by_id", entity: e.name, kind: "get", - description: "Get " + e.name + " by ID using a key lookup. " + e.desc + " ID field " + pk.name + ": " + pk.desc, - inputSchema: { type: "object", required: ["id"], properties: { id: { - description: "The ID of the " + e.name + " to retrieve. Pass only the key component, for example id=\"" + sample + "\"; do not pass the full Redis key " + e.template.replace("{id}", sample) + ".", - type: "string" } } } }); - if (tags.length || nums.length || texts.length) { - var parts = []; - if (tags.length) parts.push("tag_conditions"); - if (nums.length) parts.push("numeric_conditions"); - if (texts.length) parts.push("text_query"); - var lines = []; - if (tags.length) lines.push("Tag fields (tag_conditions, exact match): " + tags.join(", ") + "."); - if (nums.length) lines.push("Numeric fields (numeric_conditions, inclusive range): " + nums.join(", ") + "."); - if (texts.length) lines.push("Text fields (text_query, full-text): " + texts.join(", ") + "."); - var props = conditionSchemas(tags, nums, texts); - var lp = limitProps(100); props.limit = lp.limit; props.offset = lp.offset; - tools.push({ name: "filter_" + sn, entity: e.name, kind: "filter", - description: "Filter " + e.name + " by one or MORE field conditions in a single call; a record is returned only if it satisfies ALL conditions (AND) across " + parts.join(", ") + ". " + e.desc + " " + lines.join(" "), - inputSchema: { type: "object", properties: props } }); - } - if (tags.length || nums.length) { - var agg = tags.concat(nums); - var cp = { function: { description: "Aggregation function to execute. count takes no field; count_distinct requires one eligible field.", type: "string", enum: ["count", "count_distinct"] }, - field: { description: "Scalar TAG or NUMERIC field whose distinct values should be counted.", type: "string", enum: agg } }; - if (tags.length) cp.group_by = { description: "Optional scalar TAG field to group counts by.", type: "string", enum: tags }; - cp.limit = { description: "Maximum number of groups to return (default: 10)", type: "integer", minimum: 1, maximum: 100, default: 10 }; - tools.push({ name: "count_" + sn, entity: e.name, kind: "count", - description: "Count " + e.name + " records without retrieving individual records. " + e.desc, - inputSchema: { type: "object", additionalProperties: false, required: ["function"], properties: cp } }); - } - }); - var relText = rels.length ? rels.map(function (r) { return r.name; }).join(", ") : "this data model declares none"; - var setItem = { type: "object", additionalProperties: false, required: ["entity"], properties: { - entity: { description: "Which entity this set selects.", type: "string", enum: names }, - tag_conditions: { description: "Exact-match tag conditions, ANDed together.", type: "array" }, - numeric_conditions: { description: "Inclusive numeric-range conditions, ANDed together.", type: "array" }, - text_query: { description: "Lexical text to match on the entity's text fields.", type: "string" }, - expand_to: { description: "Optional relationship to follow after retrieving this set, bringing it to the related entity. Declared: " + relText + ".", type: "string" } } }; - var common = "Each set states which records it selects: the entity (" + names.join(", ") + ") plus tag_conditions, numeric_conditions or text_query. The whole matching set is used, not a page of it."; - function setTool(name, what) { - var p = { sets: { description: "Two or more sets to combine (at most 8), all resolving to the same entity.", type: "array", minItems: 2, maxItems: 8, items: setItem } }; - var lp = limitProps(100); p.limit = lp.limit; p.offset = lp.offset; - return { name: name, kind: "set", description: what + " All sets must resolve to the same entity; use expand_to on one of them to bring it to the same entity first. " + common, - inputSchema: { type: "object", additionalProperties: false, required: ["sets"], properties: p } }; - } - tools.push(setTool("intersect_results", "Return the records present in EVERY given set (AND) -- the conjunctive filter the per-entity tools cannot express.")); - tools.push(setTool("union_results", "Return the records present in ANY given set (OR).")); - tools.push(setTool("except_results", "Return the records in the FIRST set that are not in any of the others (set difference).")); - var ep = { set: setItem, relationship: { description: "Relationship to follow. Declared hops: " + relText + ".", type: "string" } }; - var elp = limitProps(100); ep.limit = elp.limit; ep.offset = elp.offset; - tools.push({ name: "expand_results", kind: "expand", - description: "Follow a declared relationship from one set to the related entity and return that entity's records. This is the only tool here that changes entity; use it when a question joins two entities. Relationships you can follow: " + relText + ".", - inputSchema: { type: "object", additionalProperties: false, required: ["set", "relationship"], properties: ep } }); - return tools; - } - - /* Field options as the live service creates them: TAG fields get - INDEXMISSING (plus INDEXEMPTY when used for access control, which this - demo doesn't model), NUMERIC fields are SORTABLE, TEXT fields are plain. */ - var INDEX_OPTIONS = { tag: "TAG INDEXMISSING", numeric: "NUMERIC SORTABLE", text: "TEXT" }; - - function indexCommands(model, surface) { - return model.filter(function (e) { return e.fields.some(function (f) { return f.index !== "none"; }); }).map(function (e) { - var prefix = e.template.split("{id}")[0]; - var schema = e.fields.filter(function (f) { return f.index !== "none"; }).map(function (f) { - return "'$." + f.name + "' AS " + f.name + " " + INDEX_OPTIONS[f.index]; - }); - return "FT.CREATE idx:" + surface + ":" + hash8(e.name + e.template) + ":" + lower(e.name) + - " ON JSON PREFIX 1 " + prefix + " SCHEMA \\\n " + schema.join(" \\\n "); - }); - } - - /* ------------------------------------------------------- query execution */ - - function entityOf(model, name) { return model.filter(function (e) { return e.name === name; })[0]; } - - /* Fields detected from the JSON documents a template matches, the way - Auto-detect fields works in the console: the field set of each document - type found, merged by name. Returns the document types too, so an entity - can tell when its template starts pointing at different documents. */ - function detectFields(template) { - var re = templateRegex(template), kinds = [], names = {}, fields = []; - if (re) KEYS.forEach(function (k) { if (k.type === "JSON" && re.test(k.key) && kinds.indexOf(k.entity) === -1) kinds.push(k.entity); }); - kinds.forEach(function (kind) { - defaultModel().filter(function (e) { return e.name === kind; })[0].fields.forEach(function (f) { - if (!names[f.name]) { names[f.name] = true; fields.push(f); } - }); - }); - return { kind: kinds.join("+"), fields: fields }; - } - - /* Records an entity can see: JSON documents whose key matches its key template. */ - function rowsFor(ent) { - var re = templateRegex(ent.template); - if (!re) return []; - return KEYS.filter(function (k) { return k.type === "JSON" && re.test(k.key); }).map(function (k) { return k.doc; }); - } - - /* Protocol-level rejection (bad arguments): a JSON-RPC error. */ - function McpError(msg) { this.message = msg; } - /* Tool-level failure (the call ran but failed): a result with isError: true, - the way the live server reports a failed lookup. */ - function ToolError(msg) { this.message = msg; } - - function checkEnum(path, val, en) { - if (!en || en.indexOf(val) === -1) - throw new McpError("invalid params: validating \"arguments\": validating " + path + ": enum: " + val + " does not equal any of: [" + (en || []).join(" ") + "]"); - } - - function selectSet(model, set, pathRoot) { - var e = entityOf(model, set.entity); - if (!e) throw new McpError("invalid params: unknown entity " + set.entity); - var tags = fieldsBy(e, "tag"); - var nums = fieldsBy(e, "numeric"), texts = fieldsBy(e, "text"); - var rows = rowsFor(e).slice(); - (set.tag_conditions || []).forEach(function (c) { - checkEnum(pathRoot + "/tag_conditions/items/properties/field", c.field, tags); - rows = rows.filter(function (r) { return c.exclude ? r[c.field] !== c.value : r[c.field] === c.value; }); - }); - (set.numeric_conditions || []).forEach(function (c) { - checkEnum(pathRoot + "/numeric_conditions/items/properties/field", c.field, nums); - rows = rows.filter(function (r) { - return (c.min_value == null || r[c.field] >= c.min_value) && (c.max_value == null || r[c.field] <= c.max_value); - }); - }); - if (set.text_query) { - if (!texts.length) throw new McpError("invalid params: validating \"arguments\": validating " + pathRoot + ": unexpected additional properties [\"text_query\"]"); - var q = set.text_query.toLowerCase(), mode = set.text_match_mode || "any"; - if (/^".*"$/.test(q)) { mode = "phrase"; q = q.slice(1, -1); } - var terms = q.split(/\s+/).filter(Boolean); - rows = rows.filter(function (r) { - var hay = texts.map(function (f) { return String(r[f] || ""); }).join(" ").toLowerCase(); - var words = hay.split(/[^a-z0-9]+/); - if (mode === "phrase") return (" " + words.join(" ") + " ").indexOf(" " + terms.join(" ") + " ") !== -1; - var hit = function (t) { return words.indexOf(t) !== -1; }; - return mode === "all" ? terms.every(hit) : terms.some(hit); - }); - } - return { entity: e, rows: rows }; - } - - function follow(model, sel, relName, rels) { - var rel = rels.filter(function (r) { return r.name === relName; })[0]; - if (!rel || rel.from !== sel.entity.name) - throw new McpError("invalid params: relationship " + relName + " is not declared from " + sel.entity.name + ". Declared hops: " + (rels.map(function (r) { return r.name; }).join(", ") || "none")); - var target = entityOf(model, rel.to), rows; - if (!rel.reverse) { - var ids = sel.rows.map(function (r) { return r[rel.via]; }); - rows = rowsFor(target).filter(function (t) { return ids.indexOf(t.id) !== -1; }); - } else { - var src = sel.rows.map(function (r) { return r.id; }); - rows = rowsFor(target).filter(function (t) { return src.indexOf(t[rel.via]) !== -1; }); - } - return { entity: target, rows: rows }; - } - - function stamp(rec, surface) { - var out = { _retrieval: { source: surface, retrieved_at: new Date().toISOString(), source_version: 1 } }; - Object.keys(rec).sort().forEach(function (k) { out[k] = rec[k]; }); - return out; - } - - function page(rows, args) { - var limit = Math.min(args.limit || 10, 100), offset = args.offset || 0; - return { rows: rows.slice(offset, offset + limit), limit: limit, offset: offset, has_more: rows.length > offset + limit }; - } - - function execute(model, tools, surface, name, args) { - var tool = tools.filter(function (t) { return t.name === name; })[0]; - if (!tool) throw new McpError("unknown tool \"" + name + "\""); - var rels = relationships(model); - if (tool.kind === "get") { - var ent = entityOf(model, tool.entity); - var rec = rowsFor(ent).filter(function (r) { return r.id === args.id; })[0]; - if (!rec) throw new ToolError("get by ID failed: failed to get document: key " + ent.template.replace("{id}", args.id) + " not found"); - return stamp(rec, surface); - } - if (tool.kind === "filter") { - var props = tool.inputSchema.properties; - Object.keys(args).forEach(function (k) { - if (!props[k]) throw new McpError("invalid params: validating \"arguments\": validating root: unexpected additional properties [\"" + k + "\"]"); - }); - var sel = selectSet(model, { entity: tool.entity, tag_conditions: args.tag_conditions, numeric_conditions: args.numeric_conditions, text_query: args.text_query, text_match_mode: args.text_match_mode }, "/properties"); - var pg = page(sel.rows, args); - return { count: pg.rows.length, has_more: pg.has_more, limit: pg.limit, offset: pg.offset, - results: pg.rows.map(function (r) { return stamp(r, surface); }), total_count: sel.rows.length }; - } - if (tool.kind === "count") { - var ce = entityOf(model, tool.entity), rows = rowsFor(ce); - if (args.group_by) { - checkEnum("/properties/group_by", args.group_by, tool.inputSchema.properties.group_by && tool.inputSchema.properties.group_by.enum); - var g = {}; - rows.forEach(function (r) { g[r[args.group_by]] = (g[r[args.group_by]] || 0) + 1; }); - return { function: "count", group_by: args.group_by, - groups: Object.keys(g).sort().map(function (k) { return { value: g[k], group_value: k }; }), has_more: false }; - } - return { function: "count", value: rows.length }; - } - if (tool.kind === "set") { - var resolved = args.sets.map(function (s, i) { - var sl = selectSet(model, s, "/properties/sets/items/" + i); - return s.expand_to ? follow(model, sl, s.expand_to, rels) : sl; - }); - var ename = resolved[0].entity.name; - resolved.forEach(function (r) { - if (r.entity.name !== ename) throw new McpError("invalid params: all sets must resolve to the same entity (got " + ename + " and " + r.entity.name + ")"); - }); - var idLists = resolved.map(function (r) { return r.rows.map(function (x) { return x.id; }); }); - var ids; - if (name === "intersect_results") ids = idLists[0].filter(function (id) { return idLists.every(function (l) { return l.indexOf(id) !== -1; }); }); - else if (name === "union_results") { ids = []; idLists.forEach(function (l) { l.forEach(function (id) { if (ids.indexOf(id) === -1) ids.push(id); }); }); } - else ids = idLists[0].filter(function (id) { return idLists.slice(1).every(function (l) { return l.indexOf(id) === -1; }); }); - var pr = page(ids, args), tpl = resolved[0].entity.template; - return { complete: true, count: ids.length, entity: ename, has_more: pr.has_more, - ids: pr.rows.map(function (id) { return tpl.replace("{id}", id); }), limit: pr.limit, offset: pr.offset }; - } - if (tool.kind === "expand") { - var from = selectSet(model, args.set, "/properties/set"); - var to = follow(model, from, args.relationship, rels); - var pe = page(to.rows, args); - return { complete: true, count: to.rows.length, entity: to.entity.name, has_more: pe.has_more, - ids: pe.rows.map(function (r) { return to.entity.template.replace("{id}", r.id); }), limit: pe.limit, offset: pe.offset }; - } - throw new McpError("unsupported tool"); - } - - /* ------------------------------------------------------ scripted agent */ - - function idFromKey(k) { return k.slice(k.lastIndexOf(":") + 1); } - function usd2(n) { return "$" + Number(n).toFixed(2); } - var SEPT_START = ts(2026, 9, 1, 0), SEPT_END = ts(2026, 10, 1, 0) - 1; - function label(s) { return String(s).replace(/_/g, " "); } - function city(s) { return String(s).charAt(0).toUpperCase() + String(s).slice(1); } - - /* Look up each distinct restaurant once, the way an agent would. */ - function restaurantNames(call, orders) { - var names = {}; - orders.forEach(function (o) { - if (!names[o.restaurant_id]) names[o.restaurant_id] = call("get_restaurant_by_id", { id: o.restaurant_id }).name; - }); - return names; - } - - /* Each question drives tool calls through `call(name, args)` and returns the answer. */ - var QUESTIONS = [ - { q: "How many customers follow each diet?", - run: function (call) { - var r = call("count_customer", { function: "count", group_by: "dietary" }); - if (!r.groups.length) return "I found no customers. Check that the Customer key template matches your keys."; - return "Here's the split by dietary preference: " + r.groups.slice().sort(function (a, b) { return b.value - a.value; }) - .map(function (g) { return "**" + g.value + "** " + (g.group_value === "none" ? "with no restriction" : label(g.group_value)); }).join(", ") + "."; - } }, - { q: "Which Thai restaurants in Austin are rated 4.5 or higher?", - run: function (call) { - var r = call("filter_restaurant", { tag_conditions: [{ field: "cuisine", value: "thai" }, { field: "city", value: "austin" }], - numeric_conditions: [{ field: "rating", min_value: 4.5 }] }); - if (!r.total_count) return "No Thai restaurants in Austin are rated 4.5 or higher."; - return r.results.map(function (x) { - return "**" + x.name + "** is rated " + x.rating + ", with an average delivery time of " + x.avg_delivery_min + " minutes."; - }).join("\n"); - } }, - { q: "What has Maya Chen ordered this month?", - run: function (call) { - var c = call("filter_customer", { text_query: "Maya Chen", text_match_mode: "all" }); - if (!c.total_count) return "I couldn't find a customer called Maya Chen."; - var cust = c.results[0]; - var o = call("filter_order", { tag_conditions: [{ field: "customer_id", value: cust.id }], - numeric_conditions: [{ field: "placed_ts", min_value: SEPT_START, max_value: SEPT_END }] }); - if (!o.total_count) return cust.name + " hasn't ordered anything this month."; - var names = restaurantNames(call, o.results); - /* Sort a copy: o.results is also what the call card shows as the tool's response. */ - var byDate = o.results.slice().sort(function (a, b) { return a.placed_ts - b.placed_ts; }); - return cust.name + " has placed **" + o.total_count + "** orders this month:\n" + byDate.map(function (x) { - return "- " + fmtDate(x.placed_ts) + ": " + x.items + " from " + names[x.restaurant_id] + ", " + usd2(x.total) + " (" + label(x.status) + ")"; - }).join("\n") + "\n\nHer profile says she's " + label(cust.dietary) + " and her favorite cuisine is " + label(cust.favorite_cuisine) + "."; - } }, - { q: "Which vegetarian customers have ordered from Green Bowl?", - run: function (call) { - var r = call("filter_restaurant", { text_query: "Green Bowl", text_match_mode: "phrase" }); - if (!r.total_count) return "I couldn't find a restaurant called Green Bowl."; - var rest = r.results[0]; - var s = call("intersect_results", { sets: [ - { entity: "Order", tag_conditions: [{ field: "restaurant_id", value: rest.id }], expand_to: "Order.customer" }, - { entity: "Customer", tag_conditions: [{ field: "dietary", value: "vegetarian" }] }] }); - if (!s.count) return "No vegetarian customers have ordered from " + rest.name + "."; - var people = s.ids.slice(0, 3).map(function (k) { var c = call("get_customer_by_id", { id: idFromKey(k) }); return "- " + c.name + " (" + city(c.city) + ", " + c.loyalty_points + " loyalty points)"; }); - return "**" + s.count + "** vegetarian customer" + (s.count === 1 ? " has" : "s have") + " ordered from " + rest.name + ":\n" + people.join("\n"); - } }, - { q: "Find orders that include pad thai.", - run: function (call) { - var r = call("filter_order", { text_query: "pad thai", text_match_mode: "phrase" }); - if (!r.total_count) return "No orders include pad thai."; - return "**" + r.total_count + "** orders include pad thai:\n" + r.results.map(function (x) { - return "- " + x.id + ": " + x.items + ", " + usd2(x.total) + " (" + fmtDate(x.placed_ts) + ")"; - }).join("\n"); - } }, - { q: "Which orders are being prepared or on the way right now?", - run: function (call) { - var s = call("union_results", { sets: [ - { entity: "Order", tag_conditions: [{ field: "status", value: "preparing" }] }, - { entity: "Order", tag_conditions: [{ field: "status", value: "on_the_way" }] }] }); - if (!s.count) return "No orders are in progress."; - var orders = s.ids.slice(0, 5).map(function (k) { return call("get_order_by_id", { id: idFromKey(k) }); }); - var names = restaurantNames(call, orders); - return "**" + s.count + "** orders are in progress:\n" + orders.map(function (x) { - return "- " + x.id + ": " + x.items + " from " + names[x.restaurant_id] + " (" + label(x.status) + ")"; - }).join("\n"); - } } - ]; - - function mdLite(s) { - return s.split("\n").map(function (line) { - var h = esc(line).replace(/\*\*(.+?)\*\*/g, "$1"); - return line.indexOf("- ") === 0 ? "
  • " + h.slice(2) + "
  • " : (h ? "

    " + h + "

    " : ""); - }).join("").replace(/(
  • .*?<\/li>)+/g, function (m) { return "
      " + m + "
    "; }); - } - - /* ------------------------------------------------------------------- UI */ - - function init(root) { - var state = { model: defaultModel(), tools: [], surface: "3f8c2a1e-demo", selectedKey: null, selectedTool: null, busy: false, connected: false }; - state.tools = buildTools(state.model); - var ENDPOINT = "https://.context-surfaces.redis.io/mcp"; - - root.innerHTML = ""; - var tabs = el("div", "rcr-tabs"); - tabs.setAttribute("role", "tablist"); - var TABS = [["keys", "1", "Keys in Redis"], ["model", "2", "Data model to tools"], ["ask", "3", "Ask over MCP"]]; - var panels = {}; - TABS.forEach(function (t, i) { - var b = el("button", "rcr-tab", '' + t[1] + "" + esc(t[2])); - b.type = "button"; b.setAttribute("role", "tab"); b.dataset.tab = t[0]; - b.setAttribute("aria-selected", i === 0 ? "true" : "false"); - b.addEventListener("click", function () { show(t[0]); }); - tabs.appendChild(b); - panels[t[0]] = el("div", "rcr-panel"); - panels[t[0]].setAttribute("role", "tabpanel"); - if (i) panels[t[0]].hidden = true; - }); - root.appendChild(tabs); - Object.keys(panels).forEach(function (k) { root.appendChild(panels[k]); }); - - function show(name) { - Array.prototype.forEach.call(tabs.children, function (b) { b.setAttribute("aria-selected", b.dataset.tab === name ? "true" : "false"); }); - Object.keys(panels).forEach(function (k) { panels[k].hidden = k !== name; }); - if (name === "keys") renderKeys(); - if (name === "model") renderModel(); - if (name === "ask") renderAsk(); - } - - /* The row at the end of a step: back to the step before, if there is one, and on to `target`. */ - function next(label, target, back) { - var w = el("div", "rcr-next"); - if (back) { - var t = TABS.filter(function (x) { return x[0] === back; })[0]; - var pb = el("button", "rcr-btn rcr-prev", "← Previous: " + esc(t[2].charAt(0).toLowerCase() + t[2].slice(1))); - pb.type = "button"; - pb.addEventListener("click", function () { show(back); }); - w.appendChild(pb); - } - if (label) { - var b = el("button", "rcr-btn rcr-btn-primary", esc(label)); - b.type = "button"; - b.addEventListener("click", function () { show(target); }); - w.appendChild(b); - } - return w; - } - - /* ---- step 1: keys ---- */ - function renderKeys() { - var p = panels.keys; p.innerHTML = ""; - p.appendChild(el("p", "rcr-lede", "This sample database belongs to a food delivery app. It stores customers, restaurants, and orders as JSON documents. Each business object is one key, and the key name starts with the object type. Select a key to see its document.")); - var grid = el("div", "rcr-split"); - var list = el("div", "rcr-card rcr-keylist"); - var groups = {}; - KEYS.forEach(function (k) { var pre = k.key.split(":")[0]; (groups[pre] = groups[pre] || []).push(k); }); - Object.keys(groups).forEach(function (pre) { - var g = groups[pre]; - var d = el("details", "rcr-group"); - if (!state.selectedKey && pre === "customer") d.open = true; - if (state.selectedKey && state.selectedKey.key.indexOf(pre + ":") === 0) d.open = true; - d.appendChild(el("summary", "", '' + esc(pre) + ":*" + g.length + "" + esc(g[0].type) + "")); - g.forEach(function (k) { - var b = el("button", "rcr-key" + (state.selectedKey === k ? " is-on" : ""), '' + esc(k.key) + ""); - b.type = "button"; - b.addEventListener("click", function () { state.selectedKey = k; renderKeys(); }); - d.appendChild(b); - }); - list.appendChild(d); - }); - var view = el("div", "rcr-card rcr-docview"); - var k = state.selectedKey || KEYS[0]; - view.appendChild(el("div", "rcr-cardhead", '' + esc(k.key) + '' + esc(k.type) + "")); - if (k.doc && k.type === "JSON") { - view.appendChild(el("pre", "rcr-code", jsonHtml(k.doc))); - view.appendChild(el("p", "rcr-note", "Entity " + esc(k.entity) + ". Key template " + esc(k.entity.toLowerCase()) + ":{id} matches this key with {id} = " + esc(k.doc.id) + ".")); - } else { - view.appendChild(el("p", "rcr-note", "This key is a " + esc(k.type) + ", not a JSON document. Context Retriever entities are JSON documents, so it isn't part of the data model.")); - } - grid.appendChild(list); grid.appendChild(view); p.appendChild(grid); - p.appendChild(next("Next: define the data model", "model")); - } - - /* ---- step 2: model -> tools ---- */ - function renderModel() { - var p = panels.model; p.innerHTML = ""; - p.appendChild(el("p", "rcr-lede", "You describe each entity once: a key template that finds its keys, and the fields an agent can use. Context Retriever turns that description into MCP tools. Change a key template to point the entity at other documents, or change an index type, and watch the tools change. Reset puts an edited template back.")); - if (!state.entityTab) state.entityTab = state.model[0].name; - var sub = el("div", "rcr-subtabs"); - sub.setAttribute("role", "tablist"); - state.model.forEach(function (ent) { - var b = el("button", "rcr-subtab", esc(ent.name) + ' ' + esc(ent.template) + ""); - b.type = "button"; b.setAttribute("role", "tab"); - b.setAttribute("aria-selected", ent.name === state.entityTab ? "true" : "false"); - b.addEventListener("click", function () { - if (ent.name === state.entityTab) return; - state.entityTab = ent.name; - renderModel(); - }); - sub.appendChild(b); - }); - p.appendChild(sub); - var ent = entityOf(state.model, state.entityTab); - p.appendChild(entityCard(ent)); - var right = el("div", "rcr-card rcr-tools"); - p.appendChild(right); - renderTools(right); - var idx = el("details", "rcr-card rcr-behind"); - idx.appendChild(el("summary", "", "Behind the scenes: the search indexes the service creates")); - idx.appendChild(el("pre", "rcr-code", esc(indexCommands(state.model, state.surface).join("\n\n") || "No indexed fields, so no index is created."))); - idx.appendChild(el("p", "rcr-note", "Tag fields use the INDEXMISSING option, so the database must run Redis 7.4 or later. " + - "Tag fields used for access control, which this demo doesn't include, also get INDEXEMPTY. The index names are illustrative.")); - p.appendChild(idx); - p.appendChild(next("Next: ask questions over MCP", "ask", "keys")); - } - - /* Point an entity at the documents a template matches. When they're - different documents, re-detect the fields, keeping the edits made for - the old ones in case the template comes back. True if the fields changed. */ - function setTemplate(ent, value) { - ent.template = value; - var d = detectFields(value), was = ent.kind == null ? ent.name : ent.kind; - if (d.kind === was) return false; - ent.saved = ent.saved || {}; - ent.saved[was] = ent.fields; - ent.fields = ent.saved[d.kind] || d.fields; - ent.kind = d.kind; - return true; - } - /* The Reset button puts an entity's template back to the default. Index - choices made on its own documents stay; anything set while the template - pointed elsewhere is dropped. True if something changed. */ - function resetTemplate(ent) { - var def = defaultModel().filter(function (e) { return e.name === ent.name; })[0]; - if (!def || ent.template === def.template) return false; - setTemplate(ent, def.template); - ent.saved = {}; - return true; - } - - function refreshTools() { - state.tools = buildTools(state.model); - state.connected = false; - var right = panels.model.querySelector(".rcr-tools"); - if (right) { renderTools(right); right.classList.remove("rcr-flash"); void right.offsetWidth; right.classList.add("rcr-flash"); } - var beh = panels.model.querySelector(".rcr-behind pre"); - if (beh) beh.textContent = indexCommands(state.model, state.surface).join("\n\n") || "No indexed fields, so no index is created."; - } - - function entityCard(ent) { - var card = el("div", "rcr-card rcr-entity"); - var head = el("div", "rcr-enthead"); - head.appendChild(el("span", "rcr-entname", esc(ent.name))); - var tplWrap = el("label", "rcr-tpl", 'Key template'); - var inp = el("input", "rcr-input rcr-mono"); - inp.value = ent.template; inp.spellcheck = false; inp.setAttribute("aria-label", ent.name + " key template"); - var match = el("span", "rcr-match"); - /* Count only JSON documents, the keys queries and the ON JSON index use. - A template that only hits other key types says so instead of going green. */ - function updateMatch() { - var re = templateRegex(inp.value); - var hits = re ? KEYS.filter(function (k) { return re.test(k.key); }) : []; - var json = hits.filter(function (k) { return k.type === "JSON"; }).length, other = hits.length - json; - var keys = function (n) { return n + (n === 1 ? " key" : " keys"); }; - match.className = "rcr-match " + (json ? "is-ok" : "is-bad"); - match.textContent = !re ? "Missing {id}" : json ? "Matches " + keys(json) : - other ? "Matches " + keys(other) + ", but not JSON" : "Matches no keys"; - match.title = !json && other ? "Context Retriever only reads JSON documents." : ""; - } - var def = defaultModel().filter(function (e) { return e.name === ent.name; })[0].template; - var reset = el("button", "rcr-btn rcr-reset", "Reset"); - reset.type = "button"; - reset.title = "Put the key template back to " + def; - reset.setAttribute("aria-label", "Reset the " + ent.name + " key template to " + def); - function templateChanged() { - updateMatch(); refreshTools(); - reset.hidden = ent.template === def; - var on = panels.model.querySelector('.rcr-subtab[aria-selected="true"] .rcr-mono'); - if (on) on.textContent = ent.template; - } - inp.addEventListener("input", function () { - if (setTemplate(ent, inp.value)) renderFields(); - templateChanged(); - }); - reset.addEventListener("click", function () { - if (resetTemplate(ent)) { inp.value = ent.template; renderFields(); } - templateChanged(); - inp.focus(); - }); - updateMatch(); - reset.hidden = ent.template === def; - tplWrap.appendChild(inp); head.appendChild(tplWrap); head.appendChild(match); head.appendChild(reset); - card.appendChild(head); - card.appendChild(el("p", "rcr-fieldsnote", "Fields are detected from the JSON documents this template matches, as with Auto-detect fields in the console.")); - var tbl = el("table", "rcr-fields"); - tbl.innerHTML = "FieldPKTypeIndexRelated"; - var tb = el("tbody"); - tbl.appendChild(tb); card.appendChild(tbl); - renderFields(); - return card; - - function renderFields() { - tb.innerHTML = ""; - if (!ent.fields.length) { - tb.appendChild(el("tr", "rcr-nofields", 'No fields. The template matches no JSON documents, so there is nothing to detect.')); - } - ent.fields.forEach(function (f) { - var tr = el("tr"); - tr.appendChild(el("td", "rcr-mono", esc(f.name))); - tr.appendChild(el("td", "", f.pk ? 'PK' : "")); - tr.appendChild(el("td", "", esc(f.type))); - var td = el("td"); - if (f.pk) td.appendChild(el("span", "rcr-faint", "key lookup")); - else { - var sel = el("select", "rcr-select"); - sel.setAttribute("aria-label", ent.name + " " + f.name + " index type"); - (f.type === "number" ? ["none", "numeric"] : ["none", "tag", "text"]).forEach(function (o) { - var op = el("option", "", o); op.value = o; if (o === f.index) op.selected = true; sel.appendChild(op); - }); - sel.addEventListener("change", function () { f.index = sel.value; refreshTools(); }); - td.appendChild(sel); - } - tr.appendChild(td); - tr.appendChild(el("td", "", f.rel ? '' + esc(f.rel) + "" : "")); - tb.appendChild(tr); - }); - } - } - - function renderTools(box) { - box.innerHTML = ""; - box.appendChild(el("div", "rcr-cardhead", "Generated MCP tools" + state.tools.length + "")); - var list = el("div", "rcr-toollist"); - if (!state.selectedTool || !state.tools.some(function (t) { return t.name === state.selectedTool; })) state.selectedTool = state.tools[0].name; - var groups = [["Per entity", state.tools.filter(function (t) { return t.entity; })], ["Across entities", state.tools.filter(function (t) { return !t.entity; })]]; - groups.forEach(function (g) { - list.appendChild(el("div", "rcr-toolgroup", esc(g[0]))); - g[1].forEach(function (t) { - var b = el("button", "rcr-tool" + (t.name === state.selectedTool ? " is-on" : "") + (t.entity && t.entity === state.entityTab ? " is-mine" : ""), '' + esc(t.name) + "" + (t.entity ? '' + esc(t.entity) + "" : "")); - b.type = "button"; - b.addEventListener("click", function () { state.selectedTool = t.name; renderTools(box); }); - list.appendChild(b); - }); - }); - var body = el("div", "rcr-toolsbody"); - body.appendChild(list); - box.appendChild(body); - var t = state.tools.filter(function (x) { return x.name === state.selectedTool; })[0]; - var det = el("div", "rcr-tooldetail"); - det.appendChild(el("p", "rcr-tooldesc", esc(t.description))); - det.appendChild(el("pre", "rcr-code rcr-code-sm", jsonHtml(t.inputSchema))); - body.appendChild(det); - } - - /* ---- step 3: ask ---- */ - /* Built once, so a replay keeps playing and the conversation survives tab - switches. On return, reconnect only if the data model changed the tools. */ - function renderAsk() { - var p = panels.ask; - if (p.dataset.built) { - if (!state.connected) handshake(p.querySelector(".rcr-connlog")); - return; - } - p.dataset.built = "1"; - p.appendChild(el("p", "rcr-lede", "An agent connects to the service's MCP endpoint with an agent key, lists the tools, and picks the right ones for each question. The agent never connects to Redis. Pick a question to watch the calls.")); - var conn = el("div", "rcr-card rcr-conn"); - conn.appendChild(el("div", "rcr-connrow", 'MCP endpoint' + esc(ENDPOINT) + 'HeaderX-API-Key: <your-agent-key>')); - var log = el("div", "rcr-connlog"); - conn.appendChild(log); - p.appendChild(conn); - var chips = el("div", "rcr-chips"); - QUESTIONS.forEach(function (q) { - var b = el("button", "rcr-chip", esc(q.q)); b.type = "button"; - b.addEventListener("click", function () { ask(q); }); - chips.appendChild(b); - }); - p.appendChild(chips); - var chat = el("div", "rcr-chat"); chat.setAttribute("aria-live", "polite"); - p.appendChild(chat); - p.appendChild(next(null, null, "model")); - handshake(log); - } - - function wait(ms) { return new Promise(function (r) { setTimeout(r, reduced ? 0 : ms); }); } - var reduced = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; - - function handshake(log) { - if (state.connected) { log.innerHTML = connectedLine(); return; } - log.innerHTML = ' initialize ...'; - wait(450).then(function () { - log.innerHTML = ' initialize 200 OK, protocol 2025-06-18   tools/list ...'; - return wait(450); - }).then(function () { state.connected = true; log.innerHTML = connectedLine(); }); - } - function connectedLine() { - return ' Connected. tools/list returned ' + state.tools.length + " tools: " + - state.tools.map(function (t) { return '' + esc(t.name) + ""; }).join(" "); - } - - function curlFor(name, args, id) { - var body = JSON.stringify({ jsonrpc: "2.0", id: id, method: "tools/call", params: { name: name, arguments: args } }); - return "curl -X POST " + ENDPOINT + " \\\n -H \"X-API-Key: \" \\\n -H \"Content-Type: application/json\" \\\n -H \"Accept: application/json, text/event-stream\" \\\n -d '" + body + "'"; - } - - function ask(q) { - if (state.busy) return; - state.busy = true; - var chips = panels.ask.querySelectorAll(".rcr-chip"); - Array.prototype.forEach.call(chips, function (c) { c.disabled = true; }); - var chat = panels.ask.querySelector(".rcr-chat"); - chat.innerHTML = ""; - chat.appendChild(el("div", "rcr-msg rcr-user", esc(q.q))); - var thinking = el("div", "rcr-msg rcr-agent rcr-thinking", ""); - chat.appendChild(thinking); - - /* Run the script synchronously to get the calls, then replay with timing. */ - var calls = [], answer, rpcId = 3; - try { - answer = q.run(function (name, args) { - var entry = { name: name, args: args, id: rpcId++ }; - calls.push(entry); - try { entry.result = execute(state.model, state.tools, state.surface, name, args); } - catch (e) { - if (e instanceof McpError) entry.error = e.message; - else if (e instanceof ToolError) entry.toolError = e.message; - throw e; - } - return entry.result; - }); - } catch (e) { - if (!(e instanceof McpError) && !(e instanceof ToolError)) throw e; - var last = calls[calls.length - 1]; - if (e instanceof ToolError) { - var t = state.tools.filter(function (x) { return x.name === last.name; })[0]; - answer = "I couldn't answer that. My call to `" + last.name + "` failed because the record wasn't found. Check the " + - (t && t.entity ? t.entity : "entity") + " key template in step 2: it has to match your keys."; - } else if (/^unknown tool/.test(e.message)) { - var owner = state.model.filter(function (x) { return last.name.replace(/^(get_|filter_|count_)/, "").replace(/_by_id$/, "") === snake(x.name); })[0]; - answer = "I couldn't answer that. The service has no `" + last.name + "` tool, because the " + (owner ? owner.name : "entity") + - " entity has no fields. Check its key template in step 2: it has to match JSON documents."; - } else { - answer = "I couldn't answer that. The server rejected my call to `" + last.name + "`: the data model doesn't index the field I needed. Go back to step 2, index it, and ask again."; - } - } - - var chain = wait(700); - calls.forEach(function (c) { - chain = chain.then(function () { - if (!thinking.parentNode) chat.appendChild(thinking); - chat.insertBefore(callCard(c), thinking); - return wait(650); - }); - }); - chain.then(function () { - thinking.remove(); - chat.appendChild(el("div", "rcr-msg rcr-agent", mdLite(answer).replace(/`([^`]+)`/g, "$1"))); - state.busy = false; - Array.prototype.forEach.call(chips, function (c) { c.disabled = false; }); - }); - } - - function callCard(c) { - var failed = c.error || c.toolError; - var d = el("details", "rcr-call" + (failed ? " is-err" : "")); - var status = failed ? ' error' : ' ok'; - d.appendChild(el("summary", "", 'tools/call ' + esc(c.name) + ' ' + esc(JSON.stringify(c.args)) + "" + status + "")); - var body = el("div", "rcr-callbody"); - var req = { jsonrpc: "2.0", id: c.id, method: "tools/call", params: { name: c.name, arguments: c.args } }; - var res = c.error ? { jsonrpc: "2.0", id: c.id, error: { code: -32602, message: c.error } } - : c.toolError ? { jsonrpc: "2.0", id: c.id, result: { content: [{ type: "text", text: "Error executing tool: " + c.toolError }], isError: true } } - : { jsonrpc: "2.0", id: c.id, result: { content: [{ type: "text", text: JSON.stringify(c.result) }] } }; - body.appendChild(el("div", "rcr-lbl", "Request")); - body.appendChild(el("pre", "rcr-code rcr-code-sm", jsonHtml(req))); - body.appendChild(el("div", "rcr-lbl", failed ? "Response" : "Response (the tool result, parsed from content[0].text)")); - body.appendChild(el("pre", "rcr-code rcr-code-sm", jsonHtml(failed ? res : c.result))); - var cd = el("details", "rcr-curl"); - cd.appendChild(el("summary", "", "Run this call yourself with curl")); - cd.appendChild(el("pre", "rcr-code rcr-code-sm", esc(curlFor(c.name, c.args, c.id)))); - body.appendChild(cd); - d.appendChild(body); - return d; - } - - renderKeys(); - } - - function boot() { Array.prototype.forEach.call(document.querySelectorAll(".rcr[data-rcr]"), init); } - if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot); else boot(); - - /* Exposed for tests. */ - window.ContextRetrieverDemo = { buildTools: buildTools, execute: execute, defaultModel: defaultModel, QUESTIONS: QUESTIONS, McpError: McpError, ToolError: ToolError, - indexCommands: indexCommands, detectFields: detectFields }; -})();