Examples: agents, API keys, certificates
ExampleOperationsSince version 0.1.0
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 agentExisting 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 minuteProjected 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/mcpBounded 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 consentAPI 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 hereClient 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