flexway
FRENTalk to an expert

Examples: agents, API keys, certificates

ExampleOperationsSince version 0.1.0

Markdown

On this page

Operations guide: configure, deploy and observe the gateway. What the capability does, product side: product guide; calling it: developer guide.

Agent tools (MCP)

# Agent tools from the contract: one line per host declares the MCP surface, then x-flexway-tool
# per operation. A token must be issued for that URL (RFC 8707); it is never relayed upstream.
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://orders.internal:8080
auth:
  tenant_header: X-Tenant
  issuers:
    - name: customers
      discovery: https://login.example.net/.well-known/openid-configuration
      audience: api                              # REST tokens; MCP tokens carry the resource below
      tenant_claim: org
agents:
  mcp:
    resources:
      api.example.com: https://api.example.com/mcp
# excerpt of examples/mcp-tools/openapi.yaml
paths:
  /orders/{id}:
    get:
      operationId: getOrder
      x-flexway-tool: true                       # a tool named getOrder, described by the summary
  /orders:
    post:
      operationId: createOrder
      x-flexway-tool: { name: create_order, description: Place an order for the caller }
  /orders/export:
    get:
      operationId: exportOrders                  # not marked: never offered to an agent

Existing MCP server, declared list

# An existing MCP server behind the gateway (federation): its tools keep their names and their code;
# the gateway adds one authorization, the caller's quota and metrics in front. The server checks
# its own tokens, so it receives one exchanged for it.
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://gateway-health.internal:8080
  orders-mcp:
    url: http://order-service:8080/mcp           # the server's MCP endpoint
    protocol: mcp
    token_exchange: { audience: order-service }  # the token the server expects
auth:
  issuers:
    - name: workforce
      discovery: https://id.example.com/realms/staff/.well-known/openid-configuration
      audience: api
      client: { id: flexway, secret: "file:/etc/flexway/secrets/client" }
agents:
  mcp:
    resources:
      api.example.com: https://api.example.com/mcp
    servers:
      orders:
        upstream: orders-mcp
        tools: [get_order, list_orders, create_order]   # a declared list: closed by default
        scopes: [orders:read]                    # required of every tool; empty: any authenticated caller
        tool_scopes: { create_order: [orders:write] }
        rate_classes: { list_orders: expensive } # otherwise readOnlyHint -> read, else write
        discovery: { issuer: workforce }         # tools/list needs a token: the gateway's own
policies:
  definitions:
    per-caller:
      type: rate-limit
      config:
        key: caller
        classes: { read: { rps: 20, burst: 40 }, write: { rps: 5, burst: 10 }, expensive: { rps: 1, burst: 3 } }
  chains:
    global: [per-caller]

Existing MCP server, everything it announces

# Every tool a server announces ("*" - a choice, since a tool added on the server becomes public),
# one renamed against a clash. This server trusts the gateway inside a private network: no
# token_exchange, so it receives no token at all - never the agent's.
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://gateway-health.internal:8080
  catalog-mcp:
    url: http://catalog-service:8080/mcp
    protocol: mcp
auth:
  issuers:
    - name: customers
      discovery: https://login.example.net/.well-known/openid-configuration
      audience: api
agents:
  mcp:
    resources:
      api.example.com: https://api.example.com/mcp
    servers:
      catalog:
        upstream: catalog-mcp
        tools: "*"
        rename: { search: catalog_search }       # the original name clashes elsewhere
        discovery: { refresh: 1m }               # listed anonymously, every minute

Projected and federated tools together

# Both modes on one surface: an operation of the REST contract projected as a tool, and the tools of
# an existing MCP server - one list for the agent, one authorization, one quota. Both upstreams
# verify tokens, so both receive one exchanged for them.
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://orders.internal:8080
    token_exchange: { audience: orders-api }
  support-mcp:
    url: http://support-service:8080/mcp
    protocol: mcp
    token_exchange: { audience: support-service }
auth:
  tenant_header: X-Tenant
  issuers:
    - name: customers
      discovery: https://login.example.net/.well-known/openid-configuration
      audience: api
      tenant_claim: org
      client: { id: flexway, secret: "${FLEXWAY_CLIENT_SECRET}" }
agents:
  mcp:
    resources:
      api.example.com: https://api.example.com/mcp
    servers:
      support:
        upstream: support-mcp
        tools: [open_ticket, ticket_status]      # names must not clash with get_order: checked at load
        scopes: [support]
        discovery: { issuer: customers }
# excerpt of examples/mcp-mixed/openapi.yaml
paths:
  /orders/{id}:
    get:
      operationId: getOrder
      x-flexway-tool: { name: get_order, description: Read one order }   # projected from REST
      security: [ { customers: [orders:read] } ]

Token exchanged toward the upstream

# An upstream that verifies tokens itself receives one issued for it, on the caller's behalf, by
# the caller's provider (RFC 8693) - never the caller's own, over REST and MCP alike.
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://orders.internal:8080
    token_exchange:
      audience: orders-api                       # what the upstream expects a token to be issued for
      scope: [orders:read]                       # optional: narrow it
auth:
  issuers:
    - name: customers
      discovery: https://login.example.net/.well-known/openid-configuration
      audience: api
      client:                                    # the gateway's own client at this provider
        id: flexway
        secret: file:/etc/flexway/secrets/exchange-client   # a reference, read at each exchange
agents:
  mcp:
    resources:
      api.example.com: https://api.example.com/mcp

Bounded agent chain

# Agents acting for a human, bounded: a token whose act claims (RFC 8693) name more agents than
# allowed, or whose human authenticated too long ago, is refused; a delegated token is introspected
# at its issuer, so revoking it breaks the chain within the cache delay.
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://orders.internal:8080
    token_exchange: { audience: orders-api }     # each hop narrows: never more than the caller holds
auth:
  issuers:
    - name: customers
      discovery: https://login.example.net/.well-known/openid-configuration
      audience: api
      client: { id: flexway, secret: "file:/etc/flexway/secrets/client" }   # also authenticates introspection
agents:
  mcp:
    resources:
      api.example.com: https://api.example.com/mcp
  delegation:
    max_depth: 2                                 # human -> agent A -> agent B, no further
    max_age: 15m                                 # since the human authenticated (auth_time, else iat)
    introspection:
      cache: 10s                                 # the longest a revocation goes unseen
# excerpt of examples/agent-chain/openapi.yaml
paths:
  /orders/{id}:
    get:
      operationId: getOrder
  /payments:
    post:
      operationId: pay
      x-flexway-delegation: { max_depth: 1, max_age: 5m }   # sensitive: one agent at most, a fresh consent

API keys

# API keys for known integrations: a key is read from `Authorization: ApiKey <key>`, looked up by its
# SHA-256 in a mounted file of digests, and opens only the operations its entry names, for its tenant,
# until it expires. The key never reaches the upstream.
version: 1
contracts:
  openapi: ./openapi.yaml
upstreams:
  default:
    url: http://billing.internal:8080
auth:
  tenant_header: X-Tenant                       # the key's tenant, whatever the caller sent
  issuers:
    - name: customers
      discovery: https://login.example.net/.well-known/openid-configuration
      audience: api
  api_keys:
    store: "file:./api-keys.yaml"                # in a cluster: a Secret, mounted read-only; re-read at each reload
# What `flexway api-key` prints, one entry per key: the digest, never the key. Adding an entry issues
# a key at the next reload, removing it revokes one; rotating is two entries for a while.
keys:
  - id: acme-billing-2026-10                     # names the key in logs, metrics and its rate-limit bucket
    hash: "sha256:83f1d4483a67a42e2233425091a6c96605dce3e9a9e1c9bd0b72a5df698d2ad8"
    tenant: acme
    operations: [listInvoices, getInvoice]       # nothing else, even an operation that takes keys
    expires_at: 2027-01-01T00:00:00Z             # required: a key never lives for ever
# excerpt of examples/api-keys/openapi.yaml
  securitySchemes:
    key: { type: apiKey, in: header, name: Authorization }    # always Authorization: ApiKey <key>
paths:
  /invoices:
    get:
      operationId: listInvoices
      security: [ { customers: [invoices:read] }, { key: [] } ]   # a token with the scope, or a bound key
  /invoices/{id}:
    get:
      operationId: getInvoice
      security: [ { customers: [invoices:read] }, { key: [] } ]
  /invoices/{id}/void:
    post:
      operationId: voidInvoice
      security: [ { customers: [invoices:write] } ]             # no key alternative: a key is refused here

Client certificates vouched for by the edge

# Client certificates verified by the edge (Envoy, an ingress): it terminates mTLS and vouches for
# the certificate in x-forwarded-client-cert - believed from the edge's own range only, from anyone
# else a forgery and ignored. Each certificate is declared, by URI SAN or by subject, bound to a
# tenant and to operations.
version: 1
contracts:
  openapi: ./openapi.yaml
server:
  trusted_proxies: [10.0.0.0/8]                  # the edge's range - and only it
upstreams:
  default:
    url: http://settlements.internal:8080
auth:
  tenant_header: X-Tenant
  issuers:
    - name: staff
      discovery: https://login.example.net/.well-known/openid-configuration
      audience: api
  mtls:
    clients:
      - id: bank-settler                         # names the caller in logs, metrics, its bucket
        uri: spiffe://bank.example/settler       # the URI SAN - preferred, it does not change with a renewal
        tenant: bank
        operations: [settle, getSettlement]
      - id: legacy-batch
        subject: "CN=batch,O=Acme"               # or the subject, for a CA that issues no URI
        tenant: acme
        operations: [getSettlement]
# excerpt of examples/mtls-edge/openapi.yaml
  securitySchemes:
    partner: { type: mutualTLS }
paths:
  /settlements:
    post:
      operationId: settle
      security: [ { partner: [] } ]                             # a declared certificate, nothing else
  /settlements/{id}:
    get:
      operationId: getSettlement
      security: [ { staff: [settlements:read] }, { partner: [] } ]   # staff with a token, or a partner

Next steps