flexway
FRENTalk to an expert

Agents and MCP

GuideOperationsSince 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.

Goal: make your APIs and your existing MCP servers callable by AI agents, behind one authorization, one quota and common metrics - without writing an MCP server.

Three modes, one surface

%%{init: {'theme':'base','themeVariables':{'fontSize':'13px','lineColor':'#94a3b8'}}}%%
graph LR
  A["AI agent<br/>MCP client"]:::ext
  subgraph FW["Flexway - a host's /mcp surface"]
    S["Token issued for the surface<br/>tools/list filtered by scopes<br/>the caller's quota"]:::hub
    P["Mode 1 - projected tool<br/>a contract operation"]:::hub
    F["Mode 2 - federated tool<br/>an existing MCP server"]:::hub
  end
  R["REST service"]:::spoke
  M["Existing MCP server"]:::spoke
  I["Identity provider"]:::secret

  A -->|"MCP"| S
  S --> P -->|"the REST decision, replayed"| R
  S --> F -->|"MCP"| M
  P -->|"exchanged token, if the upstream needs one"| I
  F -->|"exchanged token, if the upstream needs one"| I

  classDef hub    fill:#4f46e5,stroke:#3730a3,color:#fff,font-weight:bold
  classDef spoke  fill:#0891b2,stroke:#155e75,color:#fff,font-weight:bold
  classDef secret fill:#a855f7,stroke:#7e22ce,color:#fff,font-weight:bold
  classDef ext    fill:#64748b,stroke:#334155,color:#fff,font-weight:bold

The token exchange happens only when the upstream needs it (token_exchange). Everything drawn is built (0.1.0).

You have Mode What you write Example
REST services described in OpenAPI 1 - projected tools from the contract x-flexway-tool on each operation to offer Agent tools (MCP)
MCP servers already running 2 - federation an upstream protocol: mcp and agents.mcp.servers Existing MCP server, declared list, Existing MCP server, everything it announces
Both 3 - mixed both, on the same surface Projected and federated tools together
Agents talking to each other in A2A A2A (not MCP: outside the /mcp surface) a declared card and a protocol: a2a upstream A2A agents, A2A agent behind the gateway

Common to all three, once per host: the surface's public URL, which is the audience an agent's token must carry (RFC 8707). It is declared, never guessed from the request - a token issued for another MCP server must not open this one.

agents:
  mcp:
    resources:
      api.example.com: https://api.example.com/mcp

Mode 1 - tools projected from the contract

Nothing to build: an operation already served over REST becomes a tool.

      x-flexway-tool: true                           # or { name: get_order, description: "…" }
Step What the gateway does
tools/list Name (operationId, or name), description (summary), input schema from the declared parameters and body
tools/call Builds the operation's REST request and runs it, in memory, through the same decision as a REST call: the contract's scopes, quota, schema validation, policies, budgets, breaker, metrics
Result The response body; isError for a status >= 400, with the RFC 9457 problem; bounded to 1 MiB

An unmarked operation is never offered. An operation answering a stream cannot be a tool: refused at load.

Mode 2 - existing MCP servers (federation)

Nothing to change on the server or in its agents: the gateway stands in front of it.

upstreams:
  orders-mcp:
    url: http://order-service:8080/mcp               # its MCP endpoint
    protocol: mcp
agents:
  mcp:
    servers:
      orders:
        upstream: orders-mcp
        tools: [get_order, create_order]             # or "*"
Setting Effect
tools: [...] Declared list - the default, closed: a tool added on the server does not appear without a decision
tools: "*" Everything the server announces - an explicit choice
rename Another name on the surface, against a clash for instance
scopes, tool_scopes Scopes required of every tool, or of one; empty: an authenticated caller - never anonymous
rate_classes A tool's rate class; otherwise read when the server announces it read-only (readOnlyHint), else write
hosts The surfaces serving it; default: all
discovery.issuer The server needs a token even to list: the gateway obtains one for itself (client_credentials)
discovery.refresh How often tools are listed again; default 5 min
How it works Rule
Discovery In the background: the server's tools/list, retried, refreshed. The start never waits for a server; when unreachable its tools are absent, and appear once it answers
Names Original. A clash is refused at load for a declared list; with "*", the clashing tool is not served, and it is said (/config, log)
Call Identity required, scopes, the caller's quota, the upstream's bulkhead and breaker, the upstream's budget, a bounded result
State /config (admin port), mcp_federation: discovered or unreachable, tools served, tools not served and why

Transformation policies (headers, path) do not apply to federated tools: they are MCP calls, not REST requests of the contract. Only the rate-limit policy (global and host levels) applies.

Mode 3 - mixed

Both modes on one surface: the agent sees one list, sorted by name. Names must be unique on a host - checked at load for declared tools.

The token the upstream receives

The agent's token is never relayed: it was issued for the surface, not the upstream, and the MCP specification forbids it. What the upstream receives depends on what it expects:

The upstream Configuration It receives
Trusts the gateway (private network, mesh) Nothing No Authorization; the tenant in the header the gateway sets (projected tools)
Verifies tokens itself token_exchange: { audience } on the upstream, client on the issuer A token issued for it, on the caller's behalf, by the caller's provider (RFC 8693) - Token exchanged toward the upstream
upstreams:
  orders-mcp:
    token_exchange: { audience: order-service }      # the token the server expects
auth:
  issuers:
    - name: customers
      client: { id: flexway, secret: "file:/etc/flexway/secrets/client" }   # a reference, read at each exchange

When the provider refuses the exchange, the call is refused; when it cannot be reached, the call fails - never a fallback to the agent's token.

Bounded agent chain

A real chain is human -> agent A -> agent B -> service. The identity provider writes it in the token, as nested act claims (RFC 8693): sub stays the human, each act names an agent. The gateway reads it and bounds it:

agents:
  delegation:
    max_depth: 2               # agents at most; 0: no delegated token accepted
    max_age: 15m               # since the human authenticated (auth_time, else iat)
    introspection:
      cache: 10s               # the longest a revocation goes unseen
# contract - a sensitive operation tightens, never widens
      x-flexway-delegation: { max_depth: 1, max_age: 5m }
Rule Behaviour
Direct token (no act) Not a delegation: no bound, no introspection
Depth, age Past the bound -> 403. An operation's bound is the lower of the configuration's and its own
Revocation A delegated token is checked at its issuer (RFC 7662 introspection, with the issuer's client); revoked -> 401 within the cache delay; issuer unreachable -> 503, never a pass without a check
Narrowing A token exchange (The token the upstream receives) asks for no more than the caller's scopes; a provider granting more -> 403, the upstream is not called
Everywhere REST, connecting to the MCP surface, projected tools (the operation's bounds) and federated ones
Traces delegation.depth, delegation.actors - the agents, never the human

Full example: Bounded agent chain.

Authorization, quota, observability

Topic Rule
Agent's token Issued for the surface's URL (resource indicator resource= on the token request); a 401 gives resource_metadata, and GET /.well-known/oauth-protected-resource/mcp says where to obtain it
tools/list Shows only what the caller may call
Projected tool The contract's requirements (security), as over REST
Federated tool scopes / tool_scopes; an identity always
Quota One bucket per caller, whatever the door - REST, projected tool, federated tool
Metrics flexway_mcp_calls_total (tool, outcome, client, tenant, unattended, cause of a refusal), flexway_mcp_connections_refused_total (reason), flexway_mcp_federated_duration_seconds (tool), flexway_mcp_discoveries_total (server, outcome), flexway_token_exchanges_total (upstream, outcome), flexway_delegation_refused_total (reason: depth, age, revoked, widened, unverifiable)
/config mcp_tools (projected tools per host), mcp_federation (each server's state, live); agents.mcp.clients and tool_unattended

Which clients, with or without a person (1.0.0) - by default the surface admits any client whose token is valid. agents.mcp.clients closes it to the clients declared, named by the URL of their metadata document (MCP revision 2026-07-28); a tool can be kept from unattended agents - a client acting for itself, whose token's subject is the client:

agents:
  mcp:
    clients: ["https://agents.example.com/*", "https://partner.example/agent.json"]
      x-flexway-tool: { name: create_order, unattended: false }   # hidden and refused with no person

A provider that gives a service account a subject of its own (Keycloak: a user id) is declared on the issuer: service_accounts: { claim: preferred_username, prefix: service-account- }.

Declaring agents.mcp.clients is also what makes each agent visible in the console: without a list, every client counts as _undeclared.

Check

  • GET /.well-known/oauth-protected-resource/mcp returns the resource and its authorization server;
  • an MCP client without a token gets 401 with resource_metadata;
  • with a token issued for the surface's URL, tools/list shows the allowed tools - projected and federated;
  • a call counts against the same quota as the REST call;
  • /config shows mcp_tools and mcp_federation;
  • flexway_mcp_calls_total carries the agent's client and, on a refusal, its cause.

toward the upstream; the bounded agent chain. API keys arrive with the next change; a federated server's MCP resources and prompts are not relayed.*

Next steps