Skip to main content
Guides

Running the MCP relay

The MCP relay sits between an AI agent and one or more upstream Model Context Protocol (MCP) servers. Every tool call passes through the Keep policy engine before reaching the upstream, and every response is evaluated before returning to the agent.

This guide walks through configuring the relay, writing rules, and verifying that policy enforcement works.

Prerequisites

  • Keep installed (keep-mcp-relay binary on your PATH)
  • An upstream MCP server to proxy to — either an HTTP endpoint or a local command that speaks MCP over stdio

Write the relay config

Create a file called relay.yaml. The relay needs a listen address, a rules directory, and at least one route.

listen: ":8090"
rules_dir: "./rules"
routes:
  - scope: linear-tools
    upstream: "https://mcp.linear.app/mcp"
    auth:
      type: bearer
      token_env: "LINEAR_API_KEY"
  - scope: slack-tools
    upstream: "https://slack-mcp.example.com"
log:
  format: json
  output: stdout

Each route maps a scope to an upstream MCP server. The scope ties the route to a set of rules — the engine evaluates rules whose scope matches the route’s scope.

Config fields

FieldRequiredDescription
listenYesAddress to bind the relay’s HTTP server (e.g. :8090)
rules_dirYesPath to the directory containing rule files
profiles_dirNoPath to profile YAML files
packs_dirNoPath to starter pack files
routesYesList of routes (at least one)
log.formatNoLog format, defaults to json
log.outputNoLog destination, defaults to stdout

Route fields

FieldRequiredDescription
scopeYesScope name that binds this route to a set of rules
upstreamOne of upstream or commandURL of an HTTP-based MCP server
commandOne of upstream or commandPath to a command that speaks MCP over stdio
argsNoArguments passed to the stdio command
auth.typeNoAuthentication type (bearer)
auth.token_envNoEnvironment variable containing the auth token

Two transport modes

The relay connects to upstreams in two ways.

HTTP upstream — the relay sends MCP requests over HTTP to a remote server:

routes:
  - scope: linear-tools
    upstream: "https://mcp.linear.app/mcp"
    auth:
      type: bearer
      token_env: "LINEAR_API_KEY"

Set auth when the upstream requires authentication. The relay reads the token from the environment variable specified in token_env and sends it as a Bearer token.

Stdio subprocess — the relay spawns a local process and communicates over stdin/stdout:

routes:
  - scope: demo-sqlite
    command: "uvx"
    args: ["mcp-server-sqlite", "--db-path", "./data.db"]

This is useful for local MCP servers or tools distributed as CLI programs. The relay starts the subprocess on launch and manages its lifecycle.

Write rules

Create a rule file in the rules_dir directory. The file’s scope field must match a route’s scope.

Create ./rules/linear.yaml:

scope: linear-tools
mode: enforce

rules:
  # Block deletion of issues
  - name: block-delete
    match:
      operation: "delete_issue"
    action: deny
    message: "Issue deletion is not permitted. Archive the issue instead."

  # Redact internal account IDs from responses
  - name: redact-account-ids
    match:
      operation: "*"
      when: "context.direction == 'response'"
    action: redact
    redact:
      target: "params.content"
      patterns:
        - match: "ACCT-[0-9]{8}"
          replace: "ACCT-XXXXX"

  # Log all other calls
  - name: audit-all
    match:
      operation: "*"
    action: log

Rules are sorted by operation specificity — exact matches (e.g. delete_issue) evaluate before glob patterns (e.g. *), which evaluate before catch-all rules. Within the same specificity tier, rules preserve their file order. A deny short-circuits evaluation immediately. All matching redact and log rules are applied:

  • deny — the call is blocked and the agent receives a structured error containing the rule name and message
  • redact — specified fields are mutated before the call is forwarded (on requests) or before the response is returned to the agent (on responses)
  • log — the call is allowed and an audit entry is recorded

Rules with context.direction == 'response' in their when clause evaluate against the upstream’s response rather than the agent’s request.

Start the relay

$ keep-mcp-relay --config relay.yaml

keep-mcp-relay listening on :8090 (12 tools from 2 upstreams)

The relay connects to all configured upstreams, discovers their tools, and starts accepting MCP connections.

To reload rules without restarting (upstream connections stay open):

$ kill -HUP $(pgrep keep-mcp-relay)

The relay logs a confirmation:

received SIGHUP, reloading rules (upstream connections unchanged)...
rules reloaded successfully

Connect an agent

Point the agent’s MCP client configuration at the relay’s listen address. The relay exposes a standard MCP endpoint over HTTP.

For example, in an MCP client config:

{
  "mcpServers": {
    "keep-relay": {
      "url": "http://localhost:8090"
    }
  }
}

The agent sees the union of all tools from all configured upstreams. The relay routes each tool call to the correct upstream based on which route originally advertised that tool.

Verify policy enforcement

Test that deny rules work by invoking a blocked tool call.

If an agent calls delete_issue against the config above, the relay evaluates the block-delete rule and returns an error:

policy denied: Issue deletion is not permitted. Archive the issue instead. (rule: block-delete)

The audit log records the decision:

{
  "timestamp": "2026-03-23T14:30:00Z",
  "scope": "linear-tools",
  "operation": "delete_issue",
  "decision": "deny",
  "rule": "block-delete"
}

For redact rules, the upstream response passes through normally, but matched patterns are replaced before reaching the agent. A response containing ACCT-12345678 becomes ACCT-XXXXX.

Troubleshooting

“relay config: listen is required” — the listen field is missing from relay.yaml. Add a listen address like :8090.

“relay config: routes[0]: either upstream or command is required” — each route needs one of upstream (HTTP) or command (stdio). Both cannot be set on the same route, and one must be present.

“scope not found” — the route’s scope does not match any rule file’s scope field. Check that the scope names match exactly between relay.yaml and the rule files in rules_dir.

Relay starts but agent gets no tools — the upstream MCP server may not be reachable. Check that the upstream URL or command is correct and that any required credentials are set in the environment.