From ee90b7d951d124f5c99cd6cfa0272e3705c4748c Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 12 Oct 2026 14:26:42 +0000 Subject: [PATCH 1/3] [Automated] Draft docs (agentgateway): mcpGuardrails: CEL processor (+4 related) Signed-off-by: GitHub Action --- assets/agw-docs/standalone/setup/ui/launch.md | 8 +++ .../main/reference/cel/variables.md | 4 ++ .../main/release-notes/release-notes.md | 2 + .../main/documentation/llm/playground.md | 4 ++ .../documentation/mcp/guardrails/setup.md | 63 ++++++++++++++++++- .../observability/access-logs/view.md | 10 +++ .../main/reference/cel/variables.md | 4 ++ .../main/release-notes/release-notes.md | 2 + 8 files changed, 95 insertions(+), 2 deletions(-) diff --git a/assets/agw-docs/standalone/setup/ui/launch.md b/assets/agw-docs/standalone/setup/ui/launch.md index caa375889..f749f4072 100644 --- a/assets/agw-docs/standalone/setup/ui/launch.md +++ b/assets/agw-docs/standalone/setup/ui/launch.md @@ -110,6 +110,10 @@ Use the UI to review what agentgateway loaded and to manage the resources that y Whether the UI can save the changes that you make depends on your storage mode. In the binary and Docker installations, the UI writes to your configuration file by default. In the Helm installation, the ConfigMap is read-only and a save fails unless you switch the chart to database mode. For more information, see [Configuration storage]({{< link-hextra path="/documentation/setup/storage/" >}}). +{{< version exclude-if="1.0.x,1.1.x,1.2.x,1.3.x,1.4.x,1.5.x,1.6.x,2.2.x" >}} +In **Settings > UI**, choose the public gateway that serves the UI and configure policies that protect the UI. In **Settings > Observability**, configure tracing and access logs for all traffic. +{{< /version >}} + ## Generate LLM client settings {#client-setup} For an example of using the UI to set something up, use the **LLM > Client Setup** page. It generates connection settings and snippets for curl, Claude Code, Claude Desktop, Codex CLI, OpenCode, Cursor, GitHub Copilot, Windsurf, and the OpenAI JavaScript and Python SDKs{{< version exclude-if="1.0.x,1.1.x,1.2.x,1.3.x,1.4.x,1.5.x,2.2.x" >}}. The page also includes a Pi recipe that writes `baseUrl` and `apiKey` values into `~/.pi/agent/models.json`{{< /version >}}. @@ -121,6 +125,10 @@ For an example of using the UI to set something up, use the **LLM > Client Setup Client Setup does not create a route, model, authentication policy, or provider credential. It generates client-side values from the configuration that already exists. For client-specific prerequisites, see [LLM clients]({{< link-hextra path="/integrations/llm/clients/" >}}). +{{< version exclude-if="1.0.x,1.1.x,1.2.x,1.3.x,1.4.x,1.5.x,1.6.x,2.2.x" >}} +When the UI and the selected LLM gateway are served by the same gateway, **Gateway base URL** uses the browser origin. Using the browser origin keeps generated client settings correct when a reverse proxy changes the public scheme, hostname, or port. +{{< /version >}} + The selected model appears only in recipes that accept a model setting. For example, the Claude Desktop recipe outputs a gateway URL and API key, but does not configure a model name in Claude Desktop. ## Next steps diff --git a/content/docs/kubernetes/main/reference/cel/variables.md b/content/docs/kubernetes/main/reference/cel/variables.md index cf51292f8..df98bc083 100644 --- a/content/docs/kubernetes/main/reference/cel/variables.md +++ b/content/docs/kubernetes/main/reference/cel/variables.md @@ -18,6 +18,8 @@ Each policy execution consistently gets the current view of the request and resp For the full list of fields and types on every top-level object, see the [Interactive CEL reference]({{< link-hextra path="/reference/cel/cel-context-interactive" >}}) page. +When an agent session can be identified, the detected session is available as `request.agent.session`. By default, common agent session headers such as `x-claude-code-session-id` and `x-session-id` are detected. + When a policy can run against both directly addressed and Service backends, check `has(backend.endpoint)` before reading `backend.endpoint`. The field is populated only for directly addressed backends, such as static hostnames. Service backends leave `backend.endpoint` unset because workload endpoints are selected separately. > [!NOTE] @@ -36,6 +38,8 @@ Depending on the policy, different fields are accessible based on when in the re |HTTP Authorization| `source`, `request`, `jwt`, `mcp`, `backend` | |External Authorization| `source`, `request`, `jwt`, `mcp`, `backend` | |MCP Authorization| `source`, `request`, `jwt`, `mcp`, `backend` — `mcp.methodName` distinguishes methods such as `tools/list` and `tools/call`. For list methods, rules run once per listed item. `mcp.sessionId` and `mcp.tool.arguments` aren't set. | +|MCP guardrails (CEL request processor)| `source`, `request`, `jwt`, `mcp`, `backend` — `mcp.params` contains the current MCP request parameters. | +|MCP guardrails (CEL response processor)| Same as the request processor, plus `mcp.result`, which contains the current MCP response result. | |Logging| `source`, `request`, `jwt`, `mcp`, `backend`, `extauthz`, `response`, `llm`| |Tracing| `source`, `request`, `jwt`, `mcp`, `backend`, `extauthz`, `response`, `llm`| |Metrics| `source`, `request`, `jwt`, `mcp`, `backend`, `extauthz`, `response`, `llm`| diff --git a/content/docs/kubernetes/main/release-notes/release-notes.md b/content/docs/kubernetes/main/release-notes/release-notes.md index 2ec6cd80c..c6f614330 100644 --- a/content/docs/kubernetes/main/release-notes/release-notes.md +++ b/content/docs/kubernetes/main/release-notes/release-notes.md @@ -148,10 +148,12 @@ Agentgateway now includes a built-in model cost catalog. Requests to common publ + - **List pagination**: List responses for tools, prompts, and resources carry a `nextCursor`. When an endpoint federates several targets, the client gets one combined cursor that tracks every target. For more information, see [List pagination]({{< link-hextra path="/documentation/mcp/spec-compatibility/#list-pagination" >}}). - **Request size limit**: An MCP request body that is larger than the buffer limit returns `413`. For more information, see [Buffer limits]({{< link-hextra path="/documentation/traffic-management/buffering/#about-buffer-limits" >}}). - **Method names in policies**: Agentgateway automatically sets `mcp.methodName` from the request's JSON-RPC method, such as `tools/call`. Route and backend policies can use this CEL variable for rate limits and authorization. For more information, see [MCP rate limits]({{< link-hextra path="/documentation/mcp/rate-limit/#how-tool-calls-map-to-http-requests" >}}). +- **CEL MCP guardrails**: MCP guardrail processors can now use `kind: expression` to reject matching MCP operations or transform request `mcp.params` and response `mcp.result` bodies without a remote ExtMCP server. Agent sessions are also available to CEL as `request.agent.session`, from common agent session headers or `config.standardAttributes.session`. ### Traffic management and operations {#v16-traffic} diff --git a/content/docs/standalone/main/documentation/llm/playground.md b/content/docs/standalone/main/documentation/llm/playground.md index 11cb18185..63579ca96 100644 --- a/content/docs/standalone/main/documentation/llm/playground.md +++ b/content/docs/standalone/main/documentation/llm/playground.md @@ -38,6 +38,10 @@ Use the built-in LLM playground in the agentgateway UI to send a chat completion {{< reuse-image-light src="img/ui-llm-playground.png" >}} {{< reuse-image-dark srcDark="img/ui-llm-playground-dark.png" >}} +## Authentication behavior + +The playground sends the request from your browser, but it does not forward browser credentials such as cookies or HTTP basic authentication to the LLM gateway. If the gateway requires a bearer token, enter the token in the playground's API key field so the request uses an `Authorization` header. + ## Next steps - [Observe LLM traffic]({{< link-hextra path="/documentation/llm/observability/" >}}) with metrics, logs, and traces. diff --git a/content/docs/standalone/main/documentation/mcp/guardrails/setup.md b/content/docs/standalone/main/documentation/mcp/guardrails/setup.md index 3ba3c76ac..e750c4576 100644 --- a/content/docs/standalone/main/documentation/mcp/guardrails/setup.md +++ b/content/docs/standalone/main/documentation/mcp/guardrails/setup.md @@ -1,13 +1,13 @@ --- title: Set up MCP guardrails weight: 20 -description: Gate and mutate MCP method calls with an external ExtMCP policy server. +description: Gate and mutate MCP method calls with an external ExtMCP policy server or in-process CEL expressions. test: mcp-guardrails: - path: mcp-guardrails --- -Gate and mutate Model Context Protocol (MCP) method calls with an external policy server. For more information about how MCP guardrails work, see [About MCP guardrails]({{< link-hextra path="/documentation/mcp/guardrails/about" >}}). +Gate and mutate Model Context Protocol (MCP) method calls with an external policy server or in-process Common Expression Language (CEL) expressions. For more information about how MCP guardrails work, see [About MCP guardrails]({{< link-hextra path="/documentation/mcp/guardrails/about" >}}). In this guide, you route `tools/call` and `tools/list` requests through a sample ExtMCP server that denies any tool whose name contains `forbidden` and annotates each tool description in `tools/list` responses. @@ -118,6 +118,65 @@ In this guide, you route `tools/call` and `tools/list` requests through a sample | `failureMode: failClosed` | Deny requests if the policy server is unreachable or returns an error. To allow requests instead, set `failOpen`. | | `methods` | The MCP methods to route through the policy server, and the phase for each. `tools/call: request` sends each tool call to the server *before* it reaches the MCP backend, so the server can allow, mutate, or deny the call. `tools/list: response` sends the tool listing to the server *after* the backend returns it, so the server can filter or annotate the list. For the full list of phases and method matching, see [About MCP guardrails]({{< link-hextra path="/documentation/mcp/guardrails/about" >}}). | + To enforce guardrails without a remote ExtMCP server, use `kind: expression`. A CEL processor can reject a matching MCP operation or return a transformed request or response body. + + ```yaml + mcpGuardrails: + processors: + # Deny before masking, so this rule sees the original message. + - kind: expression + methods: {"tools/call": request} + condition: >- + mcp.params.name == 'echo' && + has(mcp.params.arguments) && + has(mcp.params.arguments.message) && + mcp.params.arguments.message.contains('DO_NOT_SEND') + reject: "Messages containing DO_NOT_SEND may not be sent upstream" + + # Mask every email-like match in echo's message before sending it upstream. + - kind: expression + methods: {"tools/call": request} + condition: >- + mcp.params.name == 'echo' && + has(mcp.params.arguments) && + has(mcp.params.arguments.message) + transform: >- + mcp.params.with(p, p.merge({ + 'arguments': p.arguments.merge({ + 'message': p.arguments.message.regexReplaceAll( + '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+[.][A-Za-z]{2,}', + '[EMAIL]' + ) + }) + })) + + # Mask demo secrets in all text blocks returned by tools. + - kind: expression + methods: {"tools/call": response} + condition: has(mcp.result.content) + transform: >- + mcp.result.with(r, r.merge({ + 'content': r.content.map(item, + item.type == 'text' + ? item.merge({'text': item.text.regexReplaceAll( + 'demo-secret-[A-Za-z0-9]+', '[SECRET]' + )}) + : item + ) + })) + ``` + + The following table describes the CEL processor settings. + + | Setting | Description | + |---------|-------------| + | `kind: expression` | Use an in-process CEL guardrail instead of a remote ExtMCP server. | + | `condition` | Run the action only when the CEL expression returns `true`. If you omit `condition`, the action always runs for matched methods. | + | `reject` | Reject the MCP operation with this message. Each processor must set exactly one of `reject` or `transform`. | + | `transform` | Return a replacement body for the current phase. Use `mcp.params` for request-phase processors and `mcp.result` for response-phase processors. Return `null` to leave the body unchanged, and use `merge` to preserve fields that the transform does not change. | + + Processors run in list order within their phase. Each processor sees mutations from previous processors. The first rejection stops the chain. Expression errors and invalid transformed MCP bodies reject the operation. + 3. Run agentgateway with the configuration file. ```sh agentgateway -f config.yaml diff --git a/content/docs/standalone/main/documentation/observability/access-logs/view.md b/content/docs/standalone/main/documentation/observability/access-logs/view.md index 1eea35dc0..2eb358b3b 100644 --- a/content/docs/standalone/main/documentation/observability/access-logs/view.md +++ b/content/docs/standalone/main/documentation/observability/access-logs/view.md @@ -119,6 +119,16 @@ frontendPolicies: cost: 'string(llm.cost)' ``` +For requests from agent harnesses, you can also log the detected agent session. The `request.agent.session` value comes from common agent headers such as `x-claude-code-session-id` and `x-session-id`, unless you override that value with `config.standardAttributes.session`. + +```yaml +# yaml-language-server: $schema=https://agentgateway.dev/schema/config +frontendPolicies: + accessLog: + add: + agent_session: 'request.agent.session' +``` + For the full list of available fields, see the [CEL variables reference]({{< link-hextra path="/reference/cel/variables/" >}}). ### Log the selected backend endpoint {#backend-endpoint} diff --git a/content/docs/standalone/main/reference/cel/variables.md b/content/docs/standalone/main/reference/cel/variables.md index ac5a56689..e082d11aa 100644 --- a/content/docs/standalone/main/reference/cel/variables.md +++ b/content/docs/standalone/main/reference/cel/variables.md @@ -16,6 +16,8 @@ Each policy execution consistently gets the current view of the request and resp For the full list of fields and types on every top-level object, see the [CEL reference]({{< link-hextra path="/reference/cel/cel-context" >}}) page. It is generated from the [agentgateway CEL schema](https://github.com/agentgateway/agentgateway/blob/main/schema/cel.md) and is the source of truth for nested fields (for example, `source.address` or `llm.inputTokens`). +When an agent session can be identified, the detected session is available as `request.agent.session`. By default, common agent session headers such as `x-claude-code-session-id` and `x-session-id` are detected. To override the detected value, set `config.standardAttributes.session` to a CEL expression. Return `null` from that expression to mark a request as having no agent session. + When a policy can run against both directly addressed and Service backends, check `has(backend.endpoint)` before reading `backend.endpoint`. The field is populated only for directly addressed backends, such as static hostnames. Service backends leave `backend.endpoint` unset because workload endpoints are selected separately. > [!NOTE] @@ -36,6 +38,8 @@ Depending on the policy, different top-level variables are bound when CEL runs. | Network authorization | `env`, `source` [^3] | | External Authorization | `request`, `response`, `env`, `jwt`, `apiKey`, `basicAuth`, `llm`, `source`, `mcp`, `backend`, `extauthz`, `extproc`, `metadata` — some expressions run after the authorization service returns and can read `response`. [^4] | | MCP Authorization | `request`, `env`, `jwt`, `apiKey`, `basicAuth`, `llm`, `source`, `mcp`, `backend`, `extauthz`, `extproc`, `metadata` — `mcp.methodName` distinguishes methods such as `tools/list` and `tools/call`. For list methods, rules run once per listed item. `mcp.sessionId` and `mcp.tool.arguments` aren't set. | +| MCP guardrails (CEL request processor) | `request`, `env`, `jwt`, `apiKey`, `basicAuth`, `llm`, `source`, `mcp`, `backend`, `extauthz`, `extproc`, `metadata` — `mcp.params` contains the current MCP request parameters. | +| MCP guardrails (CEL response processor) | Same as the request processor, plus `mcp.result`, which contains the current MCP response result. | | External processing (ExtProc) | Request-phase rules: same as Transformation (request). Response-phase rules: same as Transformation (response). | | LLM policy | `request`, `env`, `jwt`, `apiKey`, `basicAuth`, `llm`, `llmRequest`, `source`, `backend`, `extauthz`, `extproc`, `metadata` — `llmRequest` is the raw JSON body during LLM request handling (not `mcp`). [^5] | | Logging | `request`, `response`, `env`, `jwt`, `apiKey`, `basicAuth`, `llm`, `source`, `mcp`, `backend`, `extauthz`, `extproc`, `metadata` [^6] | diff --git a/content/docs/standalone/main/release-notes/release-notes.md b/content/docs/standalone/main/release-notes/release-notes.md index 8a5dca869..5a9737ed3 100644 --- a/content/docs/standalone/main/release-notes/release-notes.md +++ b/content/docs/standalone/main/release-notes/release-notes.md @@ -163,6 +163,7 @@ The `AGENTGATEWAY_LEGACY_LLM_USAGE_TOKEN_SEMANTICS` environment variable is remo + - **Conditional virtual MCP targets**: Set `mcp.targets[].condition` to a CEL expression to select which targets participate in a request before agentgateway initializes or contacts them. Conditions require at least two configured targets. For more information, see [Target conditions]({{< link-hextra path="/integrations/mcp/servers/virtual/#target-conditions" >}}). - **List pagination**: List responses for tools, prompts, and resources carry a `nextCursor`. When an endpoint federates several targets, the client gets one combined cursor that tracks every target. For more information, see [List pagination]({{< link-hextra path="/documentation/mcp/spec-compatibility/#list-pagination" >}}). @@ -170,6 +171,7 @@ The `AGENTGATEWAY_LEGACY_LLM_USAGE_TOKEN_SEMANTICS` environment variable is remo - **Server information overrides**: An `mcp.server` block replaces the `serverInfo` and instructions that a multiplexed gateway reports. For more information, see [Server information overrides]({{< link-hextra path="/integrations/mcp/servers/virtual/#server-information-overrides" >}}). - **SSE keep-alive**: `mcp.sseKeepAlive` sends periodic comments on long-lived MCP streams so that idle proxies do not close them. For more information, see [SSE keep-alive]({{< link-hextra path="/documentation/mcp/configuration-modes/#sse-keep-alive" >}}). - **Request size limit**: An MCP request body that is larger than the buffer limit returns `413`. For more information, see [MCP request body limits]({{< link-hextra path="/integrations/mcp/servers/http/#mcp-request-body-limits" >}}). +- **CEL MCP guardrails**: MCP guardrail processors can now use `kind: expression` to reject matching MCP operations or transform request `mcp.params` and response `mcp.result` bodies without a remote ExtMCP server. Agent sessions are also available to CEL as `request.agent.session`, from common agent session headers or `config.standardAttributes.session`. ### Traffic management and operations {#v16-operations} From 541c1a54d0ed68fa59982a7affe894e3a99aef05 Mon Sep 17 00:00:00 2001 From: Kristin Brown Date: Wed, 14 Oct 2026 09:12:37 -0400 Subject: [PATCH 2/3] Drop launch.md edits covered by #1238; move release notes to v1.7 Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Kristin Brown --- assets/agw-docs/standalone/setup/ui/launch.md | 8 -------- .../kubernetes/main/release-notes/release-notes.md | 10 ++++++++-- .../standalone/main/release-notes/release-notes.md | 10 ++++++++-- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/assets/agw-docs/standalone/setup/ui/launch.md b/assets/agw-docs/standalone/setup/ui/launch.md index f749f4072..caa375889 100644 --- a/assets/agw-docs/standalone/setup/ui/launch.md +++ b/assets/agw-docs/standalone/setup/ui/launch.md @@ -110,10 +110,6 @@ Use the UI to review what agentgateway loaded and to manage the resources that y Whether the UI can save the changes that you make depends on your storage mode. In the binary and Docker installations, the UI writes to your configuration file by default. In the Helm installation, the ConfigMap is read-only and a save fails unless you switch the chart to database mode. For more information, see [Configuration storage]({{< link-hextra path="/documentation/setup/storage/" >}}). -{{< version exclude-if="1.0.x,1.1.x,1.2.x,1.3.x,1.4.x,1.5.x,1.6.x,2.2.x" >}} -In **Settings > UI**, choose the public gateway that serves the UI and configure policies that protect the UI. In **Settings > Observability**, configure tracing and access logs for all traffic. -{{< /version >}} - ## Generate LLM client settings {#client-setup} For an example of using the UI to set something up, use the **LLM > Client Setup** page. It generates connection settings and snippets for curl, Claude Code, Claude Desktop, Codex CLI, OpenCode, Cursor, GitHub Copilot, Windsurf, and the OpenAI JavaScript and Python SDKs{{< version exclude-if="1.0.x,1.1.x,1.2.x,1.3.x,1.4.x,1.5.x,2.2.x" >}}. The page also includes a Pi recipe that writes `baseUrl` and `apiKey` values into `~/.pi/agent/models.json`{{< /version >}}. @@ -125,10 +121,6 @@ For an example of using the UI to set something up, use the **LLM > Client Setup Client Setup does not create a route, model, authentication policy, or provider credential. It generates client-side values from the configuration that already exists. For client-specific prerequisites, see [LLM clients]({{< link-hextra path="/integrations/llm/clients/" >}}). -{{< version exclude-if="1.0.x,1.1.x,1.2.x,1.3.x,1.4.x,1.5.x,1.6.x,2.2.x" >}} -When the UI and the selected LLM gateway are served by the same gateway, **Gateway base URL** uses the browser origin. Using the browser origin keeps generated client settings correct when a reverse proxy changes the public scheme, hostname, or port. -{{< /version >}} - The selected model appears only in recipes that accept a model setting. For example, the Claude Desktop recipe outputs a gateway URL and API key, but does not configure a model name in Claude Desktop. ## Next steps diff --git a/content/docs/kubernetes/main/release-notes/release-notes.md b/content/docs/kubernetes/main/release-notes/release-notes.md index c6f614330..2dccf798a 100644 --- a/content/docs/kubernetes/main/release-notes/release-notes.md +++ b/content/docs/kubernetes/main/release-notes/release-notes.md @@ -10,6 +10,14 @@ Review the release notes for agentgateway on Kubernetes. > [!NOTE] > For more details, review the [GitHub release notes in the agentgateway repository](https://github.com/agentgateway/agentgateway/releases). +## 🌟 New features {#v17-new-features} + +### MCP {#v17-mcp} + + + +- **CEL MCP guardrails**: MCP guardrail processors can now use `kind: expression` to reject matching MCP operations or transform request `mcp.params` and response `mcp.result` bodies without a remote ExtMCP server. Agent sessions are also available to CEL as `request.agent.session`, from common agent session headers or `config.standardAttributes.session`. + ## ✨ Highlights {#v16-highlights} Version 1.6 provides many updates to existing features, including the following quick highlights. Before you upgrade, review the [breaking changes](#v16-breaking-changes), because several defaults change. @@ -148,12 +156,10 @@ Agentgateway now includes a built-in model cost catalog. Requests to common publ - - **List pagination**: List responses for tools, prompts, and resources carry a `nextCursor`. When an endpoint federates several targets, the client gets one combined cursor that tracks every target. For more information, see [List pagination]({{< link-hextra path="/documentation/mcp/spec-compatibility/#list-pagination" >}}). - **Request size limit**: An MCP request body that is larger than the buffer limit returns `413`. For more information, see [Buffer limits]({{< link-hextra path="/documentation/traffic-management/buffering/#about-buffer-limits" >}}). - **Method names in policies**: Agentgateway automatically sets `mcp.methodName` from the request's JSON-RPC method, such as `tools/call`. Route and backend policies can use this CEL variable for rate limits and authorization. For more information, see [MCP rate limits]({{< link-hextra path="/documentation/mcp/rate-limit/#how-tool-calls-map-to-http-requests" >}}). -- **CEL MCP guardrails**: MCP guardrail processors can now use `kind: expression` to reject matching MCP operations or transform request `mcp.params` and response `mcp.result` bodies without a remote ExtMCP server. Agent sessions are also available to CEL as `request.agent.session`, from common agent session headers or `config.standardAttributes.session`. ### Traffic management and operations {#v16-traffic} diff --git a/content/docs/standalone/main/release-notes/release-notes.md b/content/docs/standalone/main/release-notes/release-notes.md index 5a9737ed3..cbec88009 100644 --- a/content/docs/standalone/main/release-notes/release-notes.md +++ b/content/docs/standalone/main/release-notes/release-notes.md @@ -10,6 +10,14 @@ Review the release notes for agentgateway standalone. > [!NOTE] > For more details, review the [GitHub release notes in the agentgateway repository](https://github.com/agentgateway/agentgateway/releases). +## 🌟 New features {#v17-new-features} + +### MCP {#v17-mcp} + + + +- **CEL MCP guardrails**: MCP guardrail processors can now use `kind: expression` to reject matching MCP operations or transform request `mcp.params` and response `mcp.result` bodies without a remote ExtMCP server. Agent sessions are also available to CEL as `request.agent.session`, from common agent session headers or `config.standardAttributes.session`. + ## ✨ Highlights {#v16-highlights} Version 1.6 provides many updates to existing features, including the following quick highlights. Before you upgrade, review the [breaking changes](#v16-breaking-changes), because several defaults change. @@ -163,7 +171,6 @@ The `AGENTGATEWAY_LEGACY_LLM_USAGE_TOKEN_SEMANTICS` environment variable is remo - - **Conditional virtual MCP targets**: Set `mcp.targets[].condition` to a CEL expression to select which targets participate in a request before agentgateway initializes or contacts them. Conditions require at least two configured targets. For more information, see [Target conditions]({{< link-hextra path="/integrations/mcp/servers/virtual/#target-conditions" >}}). - **List pagination**: List responses for tools, prompts, and resources carry a `nextCursor`. When an endpoint federates several targets, the client gets one combined cursor that tracks every target. For more information, see [List pagination]({{< link-hextra path="/documentation/mcp/spec-compatibility/#list-pagination" >}}). @@ -171,7 +178,6 @@ The `AGENTGATEWAY_LEGACY_LLM_USAGE_TOKEN_SEMANTICS` environment variable is remo - **Server information overrides**: An `mcp.server` block replaces the `serverInfo` and instructions that a multiplexed gateway reports. For more information, see [Server information overrides]({{< link-hextra path="/integrations/mcp/servers/virtual/#server-information-overrides" >}}). - **SSE keep-alive**: `mcp.sseKeepAlive` sends periodic comments on long-lived MCP streams so that idle proxies do not close them. For more information, see [SSE keep-alive]({{< link-hextra path="/documentation/mcp/configuration-modes/#sse-keep-alive" >}}). - **Request size limit**: An MCP request body that is larger than the buffer limit returns `413`. For more information, see [MCP request body limits]({{< link-hextra path="/integrations/mcp/servers/http/#mcp-request-body-limits" >}}). -- **CEL MCP guardrails**: MCP guardrail processors can now use `kind: expression` to reject matching MCP operations or transform request `mcp.params` and response `mcp.result` bodies without a remote ExtMCP server. Agent sessions are also available to CEL as `request.agent.session`, from common agent session headers or `config.standardAttributes.session`. ### Traffic management and operations {#v16-operations} From c02b8404647c49ca6ef131bb34ac8d840e2413bb Mon Sep 17 00:00:00 2001 From: Kristin Brown Date: Wed, 14 Oct 2026 16:00:19 -0400 Subject: [PATCH 3/3] Fixes Signed-off-by: Kristin Brown --- .../main/reference/cel/variables.md | 4 +- .../main/documentation/llm/playground.md | 2 +- .../documentation/mcp/guardrails/setup.md | 175 ++++++++++++------ .../main/reference/cel/variables.md | 4 +- 4 files changed, 120 insertions(+), 65 deletions(-) diff --git a/content/docs/kubernetes/main/reference/cel/variables.md b/content/docs/kubernetes/main/reference/cel/variables.md index df98bc083..9d29485f3 100644 --- a/content/docs/kubernetes/main/reference/cel/variables.md +++ b/content/docs/kubernetes/main/reference/cel/variables.md @@ -18,7 +18,7 @@ Each policy execution consistently gets the current view of the request and resp For the full list of fields and types on every top-level object, see the [Interactive CEL reference]({{< link-hextra path="/reference/cel/cel-context-interactive" >}}) page. -When an agent session can be identified, the detected session is available as `request.agent.session`. By default, common agent session headers such as `x-claude-code-session-id` and `x-session-id` are detected. +When an agent session can be identified, the session is available as `request.agent.session`. The session is detected from agent headers such as `x-claude-code-session-id` and `x-session-id`. When a policy can run against both directly addressed and Service backends, check `has(backend.endpoint)` before reading `backend.endpoint`. The field is populated only for directly addressed backends, such as static hostnames. Service backends leave `backend.endpoint` unset because workload endpoints are selected separately. @@ -39,7 +39,7 @@ Depending on the policy, different fields are accessible based on when in the re |External Authorization| `source`, `request`, `jwt`, `mcp`, `backend` | |MCP Authorization| `source`, `request`, `jwt`, `mcp`, `backend` — `mcp.methodName` distinguishes methods such as `tools/list` and `tools/call`. For list methods, rules run once per listed item. `mcp.sessionId` and `mcp.tool.arguments` aren't set. | |MCP guardrails (CEL request processor)| `source`, `request`, `jwt`, `mcp`, `backend` — `mcp.params` contains the current MCP request parameters. | -|MCP guardrails (CEL response processor)| Same as the request processor, plus `mcp.result`, which contains the current MCP response result. | +|MCP guardrails (CEL response processor)| Same as the request processor, except that `mcp.result` replaces `mcp.params`. `mcp.result` contains the current MCP response result. | |Logging| `source`, `request`, `jwt`, `mcp`, `backend`, `extauthz`, `response`, `llm`| |Tracing| `source`, `request`, `jwt`, `mcp`, `backend`, `extauthz`, `response`, `llm`| |Metrics| `source`, `request`, `jwt`, `mcp`, `backend`, `extauthz`, `response`, `llm`| diff --git a/content/docs/standalone/main/documentation/llm/playground.md b/content/docs/standalone/main/documentation/llm/playground.md index 63579ca96..756e65850 100644 --- a/content/docs/standalone/main/documentation/llm/playground.md +++ b/content/docs/standalone/main/documentation/llm/playground.md @@ -40,7 +40,7 @@ Use the built-in LLM playground in the agentgateway UI to send a chat completion ## Authentication behavior -The playground sends the request from your browser, but it does not forward browser credentials such as cookies or HTTP basic authentication to the LLM gateway. If the gateway requires a bearer token, enter the token in the playground's API key field so the request uses an `Authorization` header. +The playground sends the request from your browser, but does not forward browser credentials, such as cookies or HTTP basic authentication, to the LLM gateway. If the gateway requires a bearer token, select a saved key in the **Virtual API key** list. Or, select **Raw value** and enter the token in the **Raw API key** field. The playground sends the key in an `Authorization: Bearer` header. The MCP playground also omits browser credentials and sends the value of its **Bearer token** field instead. ## Next steps diff --git a/content/docs/standalone/main/documentation/mcp/guardrails/setup.md b/content/docs/standalone/main/documentation/mcp/guardrails/setup.md index e750c4576..c8f292470 100644 --- a/content/docs/standalone/main/documentation/mcp/guardrails/setup.md +++ b/content/docs/standalone/main/documentation/mcp/guardrails/setup.md @@ -9,7 +9,7 @@ test: Gate and mutate Model Context Protocol (MCP) method calls with an external policy server or in-process Common Expression Language (CEL) expressions. For more information about how MCP guardrails work, see [About MCP guardrails]({{< link-hextra path="/documentation/mcp/guardrails/about" >}}). -In this guide, you route `tools/call` and `tools/list` requests through a sample ExtMCP server that denies any tool whose name contains `forbidden` and annotates each tool description in `tools/list` responses. +In this guide, you route `tools/call` and `tools/list` requests through a sample ExtMCP server that denies any tool whose name contains `forbidden` and annotates each tool description in `tools/list` responses. Then, you add CEL expression processors that reject and mask tool calls without a policy server. ## Before you begin @@ -118,65 +118,6 @@ In this guide, you route `tools/call` and `tools/list` requests through a sample | `failureMode: failClosed` | Deny requests if the policy server is unreachable or returns an error. To allow requests instead, set `failOpen`. | | `methods` | The MCP methods to route through the policy server, and the phase for each. `tools/call: request` sends each tool call to the server *before* it reaches the MCP backend, so the server can allow, mutate, or deny the call. `tools/list: response` sends the tool listing to the server *after* the backend returns it, so the server can filter or annotate the list. For the full list of phases and method matching, see [About MCP guardrails]({{< link-hextra path="/documentation/mcp/guardrails/about" >}}). | - To enforce guardrails without a remote ExtMCP server, use `kind: expression`. A CEL processor can reject a matching MCP operation or return a transformed request or response body. - - ```yaml - mcpGuardrails: - processors: - # Deny before masking, so this rule sees the original message. - - kind: expression - methods: {"tools/call": request} - condition: >- - mcp.params.name == 'echo' && - has(mcp.params.arguments) && - has(mcp.params.arguments.message) && - mcp.params.arguments.message.contains('DO_NOT_SEND') - reject: "Messages containing DO_NOT_SEND may not be sent upstream" - - # Mask every email-like match in echo's message before sending it upstream. - - kind: expression - methods: {"tools/call": request} - condition: >- - mcp.params.name == 'echo' && - has(mcp.params.arguments) && - has(mcp.params.arguments.message) - transform: >- - mcp.params.with(p, p.merge({ - 'arguments': p.arguments.merge({ - 'message': p.arguments.message.regexReplaceAll( - '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+[.][A-Za-z]{2,}', - '[EMAIL]' - ) - }) - })) - - # Mask demo secrets in all text blocks returned by tools. - - kind: expression - methods: {"tools/call": response} - condition: has(mcp.result.content) - transform: >- - mcp.result.with(r, r.merge({ - 'content': r.content.map(item, - item.type == 'text' - ? item.merge({'text': item.text.regexReplaceAll( - 'demo-secret-[A-Za-z0-9]+', '[SECRET]' - )}) - : item - ) - })) - ``` - - The following table describes the CEL processor settings. - - | Setting | Description | - |---------|-------------| - | `kind: expression` | Use an in-process CEL guardrail instead of a remote ExtMCP server. | - | `condition` | Run the action only when the CEL expression returns `true`. If you omit `condition`, the action always runs for matched methods. | - | `reject` | Reject the MCP operation with this message. Each processor must set exactly one of `reject` or `transform`. | - | `transform` | Return a replacement body for the current phase. Use `mcp.params` for request-phase processors and `mcp.result` for response-phase processors. Return `null` to leave the body unchanged, and use `merge` to preserve fields that the transform does not change. | - - Processors run in list order within their phase. Each processor sees mutations from previous processors. The first rejection stops the chain. Expression errors and invalid transformed MCP bodies reject the operation. - 3. Run agentgateway with the configuration file. ```sh agentgateway -f config.yaml @@ -308,6 +249,120 @@ EC=$(curl -s http://localhost:3000/mcp \ echo "$EC" | grep -q 'Echo: hello' && echo "✓ allowed tool call (echo) succeeded" || { echo "FAIL: echo call did not succeed: $EC"; exit 1; } {{< /doc-test >}} +## Add CEL expression processors {#expression-processors} + +You can also run a processor as a CEL expression inside agentgateway, without a policy server. Set `kind: expression` instead of `kind: remote`. One `mcpGuardrails` policy can mix both kinds. Processors run in list order, each processor sees the changes from earlier processors, and the first rejection stops the chain. + +1. Update the `config.yaml` file to add three `expression` processors after the `remote` processor. The first processor rejects `echo` calls whose message contains `DO_NOT_SEND`. The second processor masks email addresses in the `echo` message before the call reaches the MCP backend. The third processor masks `demo-secret-` tokens in the text content of tool results. + ```yaml + # yaml-language-server: $schema=https://agentgateway.dev/schema/config + gateways: + default: + port: 3000 + routes: + - policies: + cors: + allowOrigins: + - "*" + allowHeaders: + - mcp-protocol-version + - content-type + - mcp-session-id + exposeHeaders: + - "Mcp-Session-Id" + mcpGuardrails: + processors: + - kind: remote + host: "localhost:9001" + failureMode: failClosed + methods: + tools/call: request + tools/list: response + # Reject before masking, so this processor sees the original message. + - kind: expression + methods: {"tools/call": request} + condition: >- + mcp.params.name == 'echo' && + has(mcp.params.arguments) && + has(mcp.params.arguments.message) && + mcp.params.arguments.message.contains('DO_NOT_SEND') + reject: "Messages containing DO_NOT_SEND may not be sent upstream" + # Mask every email-like match in the echo message. + - kind: expression + methods: {"tools/call": request} + condition: >- + mcp.params.name == 'echo' && + has(mcp.params.arguments) && + has(mcp.params.arguments.message) + transform: >- + mcp.params.with(p, p.merge({ + 'arguments': p.arguments.merge({ + 'message': p.arguments.message.regexReplaceAll( + '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+[.][A-Za-z]{2,}', + '[EMAIL]' + ) + }) + })) + # Mask demo secrets in all text blocks that tools return. + - kind: expression + methods: {"tools/call": response} + condition: has(mcp.result.content) + transform: >- + mcp.result.with(r, r.merge({ + 'content': r.content.map(item, + item.type == 'text' + ? item.merge({'text': item.text.regexReplaceAll( + 'demo-secret-[A-Za-z0-9]+', '[SECRET]' + )}) + : item + ) + })) + backends: + - mcp: + targets: + - name: everything + stdio: + cmd: npx + args: ["@modelcontextprotocol/server-everything"] + ``` + + Review the following table to understand the `expression` settings. For the variables that an expression can read, see [CEL variables]({{< link-hextra path="/reference/cel/variables" >}}). + + | Setting | Description | + |---------|-------------| + | `kind: expression` | Run the processor as a CEL expression inside agentgateway instead of calling a remote ExtMCP server. Expression processors have no `failureMode`. | + | `condition` | Optional. A CEL expression that must return `true` for the action to run. If you omit `condition`, the action runs for every method that the processor matches. Check optional fields with `has()` first because an expression that fails to evaluate rejects the call with a JSON-RPC internal error. | + | `reject` | The message to return when the processor rejects a call. When the processor rejects a `tools/call` request in the request phase, the client receives a tool result with `isError: true` and this message as its text. Other rejections return a JSON-RPC error with code `-32001`. Set exactly one of `reject` or `transform`. | + | `transform` | A CEL expression that returns a new version of the MCP message. In the request phase, the expression reads the current request parameters from the `mcp.params` variable and the value that the expression returns becomes the new request parameters. In the response phase, the expression reads the current response result from the `mcp.result` variable and the value that the expression returns becomes the new response result. To change one field and keep the others, use `merge` on the variable, as in the example. To leave the message unchanged, return `null`. If the output is not a valid message for the method, agentgateway rejects the call. | + | `methods` | The MCP methods and phases that the processor runs for, the same as for a `remote` processor. | + +2. Stop agentgateway with `Ctrl+C` and run it again with the updated configuration file. + ```sh + agentgateway -f config.yaml + ``` + +3. Start a new MCP session. Repeat steps 1 and 2 in [Verify the guardrails](#verify-the-guardrails). + +4. Call the `echo` tool with a message that contains `DO_NOT_SEND`. Verify that the response is a tool result with `isError: true` and the text `Messages containing DO_NOT_SEND may not be sent upstream`. The call does not reach the MCP backend. + ```sh + curl -s http://localhost:3000/mcp \ + -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -H "MCP-Protocol-Version: 2025-03-26" \ + -H "mcp-session-id: $MCP_SESSION_ID" \ + -d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"echo","arguments":{"message":"DO_NOT_SEND this"}}}' + ``` + +5. Call the `echo` tool with a message that contains an email address and a demo secret. Verify that the result text is `Echo: Contact [EMAIL] with [SECRET]`. The second processor masked the email address in the request, and the third processor masked the secret in the response. + ```sh + curl -s http://localhost:3000/mcp \ + -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -H "MCP-Protocol-Version: 2025-03-26" \ + -H "mcp-session-id: $MCP_SESSION_ID" \ + -d '{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"echo","arguments":{"message":"Contact alice@example.com with demo-secret-abc123"}}}' + ``` + ## Cleanup {{< reuse "agw-docs/snippets/cleanup.md" >}} diff --git a/content/docs/standalone/main/reference/cel/variables.md b/content/docs/standalone/main/reference/cel/variables.md index e082d11aa..196aefc02 100644 --- a/content/docs/standalone/main/reference/cel/variables.md +++ b/content/docs/standalone/main/reference/cel/variables.md @@ -16,7 +16,7 @@ Each policy execution consistently gets the current view of the request and resp For the full list of fields and types on every top-level object, see the [CEL reference]({{< link-hextra path="/reference/cel/cel-context" >}}) page. It is generated from the [agentgateway CEL schema](https://github.com/agentgateway/agentgateway/blob/main/schema/cel.md) and is the source of truth for nested fields (for example, `source.address` or `llm.inputTokens`). -When an agent session can be identified, the detected session is available as `request.agent.session`. By default, common agent session headers such as `x-claude-code-session-id` and `x-session-id` are detected. To override the detected value, set `config.standardAttributes.session` to a CEL expression. Return `null` from that expression to mark a request as having no agent session. +When an agent session can be identified, the session is available as `request.agent.session`. By default, the session is detected from agent headers such as `x-claude-code-session-id` and `x-session-id`. To set the session yourself, set `config.standardAttributes.session` to a CEL expression that returns a string. Return `null` or an empty string to mark a request as having no agent session. If the expression fails or returns another type, the detected session is used. When a policy can run against both directly addressed and Service backends, check `has(backend.endpoint)` before reading `backend.endpoint`. The field is populated only for directly addressed backends, such as static hostnames. Service backends leave `backend.endpoint` unset because workload endpoints are selected separately. @@ -39,7 +39,7 @@ Depending on the policy, different top-level variables are bound when CEL runs. | External Authorization | `request`, `response`, `env`, `jwt`, `apiKey`, `basicAuth`, `llm`, `source`, `mcp`, `backend`, `extauthz`, `extproc`, `metadata` — some expressions run after the authorization service returns and can read `response`. [^4] | | MCP Authorization | `request`, `env`, `jwt`, `apiKey`, `basicAuth`, `llm`, `source`, `mcp`, `backend`, `extauthz`, `extproc`, `metadata` — `mcp.methodName` distinguishes methods such as `tools/list` and `tools/call`. For list methods, rules run once per listed item. `mcp.sessionId` and `mcp.tool.arguments` aren't set. | | MCP guardrails (CEL request processor) | `request`, `env`, `jwt`, `apiKey`, `basicAuth`, `llm`, `source`, `mcp`, `backend`, `extauthz`, `extproc`, `metadata` — `mcp.params` contains the current MCP request parameters. | -| MCP guardrails (CEL response processor) | Same as the request processor, plus `mcp.result`, which contains the current MCP response result. | +| MCP guardrails (CEL response processor) | Same as the request processor, except that `mcp.result` replaces `mcp.params`. `mcp.result` contains the current MCP response result. | | External processing (ExtProc) | Request-phase rules: same as Transformation (request). Response-phase rules: same as Transformation (response). | | LLM policy | `request`, `env`, `jwt`, `apiKey`, `basicAuth`, `llm`, `llmRequest`, `source`, `backend`, `extauthz`, `extproc`, `metadata` — `llmRequest` is the raw JSON body during LLM request handling (not `mcp`). [^5] | | Logging | `request`, `response`, `env`, `jwt`, `apiKey`, `basicAuth`, `llm`, `source`, `mcp`, `backend`, `extauthz`, `extproc`, `metadata` [^6] |