# Agents et MCP

> Objectif : rendre vos API et vos serveurs MCP existants appelables par des agents IA, derrière une autorisation, un quota et des métriques communs - sans…

Guide - Depuis la version 0.1.0 - Flexway v1.6.3 - https://flexway.io/docs/operations-agents-mcp/

> Guide d'exploitation : configurer, déployer et observer la passerelle. Ce que fait la capacité côté produit : [guide produit](https://flexway.io/docs/product-overview/) ; l'appeler : [guide développeur](https://flexway.io/docs/developer-overview/).

**Objectif** : rendre vos API et vos serveurs MCP existants appelables par des agents IA, derrière
**une** autorisation, **un** quota et **des** métriques communs - sans écrire de serveur MCP.

## Trois modes, une surface

```mermaid
%%{init: {'theme':'base','themeVariables':{'fontSize':'13px','lineColor':'#94a3b8'}}}%%
graph LR
  A["Agent IA<br/>client MCP"]:::ext
  subgraph FW["Flexway - surface /mcp d'un hôte"]
    S["Jeton émis pour la surface<br/>tools/list filtré par portées<br/>quota de l'appelant"]:::hub
    P["Mode 1 - outil projeté<br/>une opération du contrat"]:::hub
    F["Mode 2 - outil fédéré<br/>un serveur MCP existant"]:::hub
  end
  R["Service REST"]:::spoke
  M["Serveur MCP existant"]:::spoke
  I["Fournisseur d'identité"]:::secret

  A -->|"MCP"| S
  S --> P -->|"la décision REST, rejouée"| R
  S --> F -->|"MCP"| M
  P -->|"jeton échangé, si l'amont l'exige"| I
  F -->|"jeton échangé, si l'amont l'exige"| 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
```

> L'échange de jeton n'a lieu que si l'amont l'exige (`token_exchange`). Tout ce schéma est construit (`0.1.0`).

| Vous avez | Mode | Ce que vous écrivez | Exemple |
| --- | --- | --- | --- |
| Des services REST décrits en OpenAPI | **1 - outils projetés** du contrat | `x-flexway-tool` sur chaque opération à offrir | [Outils pour agents (MCP)](https://flexway.io/docs/operations-examples-agents/#outils-pour-agents-mcp) |
| Des serveurs MCP qui tournent déjà | **2 - fédération** | un amont `protocol: mcp` et `agents.mcp.servers` | [Serveur MCP existant, liste déclarée](https://flexway.io/docs/operations-examples-agents/#serveur-mcp-existant-liste-d%C3%A9clar%C3%A9e), [Serveur MCP existant, tout ce qu'il annonce](https://flexway.io/docs/operations-examples-agents/#serveur-mcp-existant-tout-ce-quil-annonce) |
| Les deux | **3 - mixte** | les deux, sur la même surface | [Outils projetés et fédérés ensemble](https://flexway.io/docs/operations-examples-agents/#outils-projet%C3%A9s-et-f%C3%A9d%C3%A9r%C3%A9s-ensemble) |
| Des agents qui se parlent en A2A | **A2A** (pas MCP : hors de la surface `/mcp`) | une carte déclarée et un amont `protocol: a2a` | [Agents A2A](https://flexway.io/docs/operations-a2a/), [Agent A2A derrière la passerelle](https://flexway.io/docs/operations-examples-a2a/) |

**Commun aux trois**, une fois par hôte : l'URL publique de la surface, qui est l'audience qu'un
jeton d'agent doit porter (RFC 8707). Elle est déclarée, jamais devinée depuis la requête - un jeton
émis pour un autre serveur MCP ne doit pas ouvrir celui-ci.

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

## Mode 1 - outils projetés du contrat

Rien à développer : une opération déjà servie en REST devient un outil.

```yaml
      x-flexway-tool: true                           # ou { name: get_order, description: "…" }
```

| Étape | Ce que fait la passerelle |
| --- | --- |
| `tools/list` | Nom (`operationId`, ou `name`), description (`summary`), schéma d'entrée tiré des paramètres et du corps déclarés |
| `tools/call` | Construit la requête REST de l'opération et la fait passer, en mémoire, par **la même décision** qu'un appel REST : portées du contrat, quota, validation du schéma, politiques, délais, coupe-circuit, métriques |
| Résultat | Le corps de la réponse ; `isError` pour un statut `>= 400`, avec le problème RFC 9457 ; borné à 1 Mio |

Une opération non marquée n'est jamais offerte. Une opération à réponse en flux ne peut pas être un
outil : elle est refusée au chargement.

## Mode 2 - serveurs MCP existants (fédération)

Rien à changer chez le serveur ni chez ses agents : la passerelle se place devant lui.

```yaml
upstreams:
  orders-mcp:
    url: http://order-service:8080/mcp               # son endpoint MCP
    protocol: mcp
agents:
  mcp:
    servers:
      orders:
        upstream: orders-mcp
        tools: [get_order, create_order]             # ou "*"
```

| Réglage | Effet |
| --- | --- |
| `tools: [...]` | **Liste déclarée** - le défaut, fermé : un outil ajouté côté serveur n'apparaît pas sans décision |
| `tools: "*"` | Tout ce que le serveur annonce - un choix explicite |
| `rename` | Un autre nom sur la surface, par exemple contre une collision |
| `scopes`, `tool_scopes` | Portées exigées de chaque outil, ou d'un outil ; vide : un appelant **authentifié** - jamais anonyme |
| `rate_classes` | Classe de débit d'un outil ; sinon `read` si le serveur l'annonce en lecture seule (`readOnlyHint`), autrement `write` |
| `hosts` | Les surfaces qui le servent ; défaut : toutes |
| `discovery.issuer` | Le serveur exige un jeton même pour lister : la passerelle en demande un pour elle-même (`client_credentials`) |
| `discovery.refresh` | Intervalle de redécouverte ; défaut 5 min |

| Fonctionnement | Règle |
| --- | --- |
| **Découverte** | En arrière-plan : `tools/list` du serveur, retenté, rafraîchi. Le démarrage n'attend **jamais** un serveur ; injoignable, ses outils sont absents, puis apparaissent quand il répond |
| **Noms** | D'origine. Une collision est refusée au chargement pour une liste déclarée ; pour `"*"`, l'outil en collision n'est pas servi, et c'est dit (`/config`, journal) |
| **Appel** | Identité exigée, portées, quota de l'appelant, cloisonnement et coupe-circuit de l'amont, délai de l'amont, résultat borné |
| **État** | `/config` (port d'administration), `mcp_federation` : découvert ou injoignable, outils servis, outils non servis et pourquoi |

Les politiques de transformation (en-têtes, chemin) ne s'appliquent pas aux outils fédérés : ce sont
des appels MCP, pas des requêtes REST du contrat. Seule la politique `rate-limit` (niveaux global et
hôte) s'applique.

## Mode 3 - mixte

Les deux modes sur la même surface : l'agent voit **une** liste, triée par nom. Les noms doivent être
uniques sur un hôte - vérifié au chargement pour les outils déclarés.

## Le jeton que reçoit l'amont

Le jeton de l'agent n'est **jamais** relayé : il a été émis pour la surface, pas pour l'amont, et la
spécification MCP l'interdit. Ce que l'amont reçoit dépend de ce qu'il attend :

| L'amont | Configuration | Il reçoit |
| --- | --- | --- |
| Fait confiance à la passerelle (réseau privé, maillage) | Rien | Aucun `Authorization` ; le tenant par l'en-tête que pose la passerelle (outils projetés) |
| Vérifie lui-même les jetons | `token_exchange: { audience }` sur l'amont, `client` sur l'émetteur | Un jeton **émis pour lui**, au nom de l'appelant, par le fournisseur de l'appelant (RFC 8693) - [Jeton échangé vers l'amont](https://flexway.io/docs/operations-examples-agents/#jeton-%C3%A9chang%C3%A9-vers-lamont) |

```yaml
upstreams:
  orders-mcp:
    token_exchange: { audience: order-service }      # le jeton que le serveur attend
auth:
  issuers:
    - name: customers
      client: { id: flexway, secret: "file:/etc/flexway/secrets/client" }   # une référence, relue à chaque échange
```

Si le fournisseur refuse l'échange, l'appel est refusé ; s'il est injoignable, l'appel échoue -
jamais de repli sur le jeton de l'agent.

## Chaîne d'agents bornée

Une chaîne réelle est `humain -> agent A -> agent B -> service`. Le fournisseur d'identité l'écrit
dans le jeton, en revendications `act` imbriquées (RFC 8693) : `sub` reste l'humain, chaque `act` nomme
un agent. La passerelle la lit et la borne :

```yaml
agents:
  delegation:
    max_depth: 2               # agents au plus ; 0 : aucun jeton délégué accepté
    max_age: 15m               # depuis l'authentification de l'humain (auth_time, sinon iat)
    introspection:
      cache: 10s               # délai maximal pour voir une révocation
```

```yaml
# contrat - une opération sensible resserre, jamais n'élargit
      x-flexway-delegation: { max_depth: 1, max_age: 5m }
```

| Règle | Comportement |
| --- | --- |
| **Jeton direct** (sans `act`) | Pas une délégation : ni borne, ni introspection |
| **Profondeur, âge** | Au-delà de la borne -> `403`. La borne d'une opération est le minimum de la configuration et de la sienne |
| **Révocation** | Un jeton délégué est vérifié auprès de son émetteur (introspection RFC 7662, avec le `client` de l'émetteur) ; révoqué -> `401` dans le délai `cache` ; émetteur injoignable -> `503`, jamais un passage sans vérification |
| **Rétrécissement** | Un échange de jeton ([Le jeton que reçoit l'amont](#le-jeton-que-reçoit-lamont)) demande au plus les portées de l'appelant ; un fournisseur qui en accorde davantage -> `403`, l'amont n'est pas appelé |
| **Partout** | REST, connexion à la surface MCP, outils projetés (bornes de l'opération) et fédérés |
| **Traces** | `delegation.depth`, `delegation.actors` - les agents, jamais l'humain |

Exemple complet : [Chaîne d'agents bornée](https://flexway.io/docs/operations-examples-agents/#cha%C3%AEne-dagents-born%C3%A9e).

## Autorisation, quota, observabilité

| Sujet | Règle |
| --- | --- |
| **Jeton de l'agent** | Émis pour l'URL de la surface (indicateur de ressource `resource=` à la demande du jeton) ; un `401` donne `resource_metadata`, et `GET /.well-known/oauth-protected-resource/mcp` dit où l'obtenir |
| **`tools/list`** | Ne montre que ce que l'appelant peut appeler |
| **Outil projeté** | Les exigences du contrat (`security`), comme en REST |
| **Outil fédéré** | `scopes` / `tool_scopes` ; une identité toujours |
| **Quota** | Le même seau par appelant, quelle que soit la porte - REST, outil projeté, outil fédéré |
| **Métriques** | `flexway_mcp_calls_total` (outil, issue, client, tenant, surveillé, cause de refus), `flexway_mcp_connections_refused_total` (motif), `flexway_mcp_federated_duration_seconds` (outil), `flexway_mcp_discoveries_total` (serveur, issue), `flexway_token_exchanges_total` (amont, issue), `flexway_delegation_refused_total` (raison : `depth`, `age`, `revoked`, `widened`, `unverifiable`) |
| **`/config`** | `mcp_tools` (outils projetés par hôte), `mcp_federation` (état de chaque serveur, en direct) ; `agents.mcp.clients` et `tool_unattended` |

**Quels clients, et avec ou sans humain** (`1.0.0`) - la surface admet par défaut tout client dont
le jeton est valide. `agents.mcp.clients` la ferme aux clients déclarés, nommés par l'URL de leur
document de métadonnées (révision MCP 2026-07-28) ; un outil peut être retiré aux agents **non
surveillés** - un client qui agit pour lui-même, dont le jeton a pour sujet le client :

```yaml
agents:
  mcp:
    clients: ["https://agents.example.com/*", "https://partner.example/agent.json"]
```

```yaml
      x-flexway-tool: { name: create_order, unattended: false }   # caché et refusé sans humain
```

Un fournisseur qui donne au compte de service un sujet à lui (Keycloak : un identifiant
d'utilisateur) se déclare : `service_accounts: { claim: preferred_username, prefix: service-account- }`
sur l'émetteur.

Déclarer `agents.mcp.clients` est aussi ce qui rend chaque agent visible dans la console : sans
liste, tous les clients comptent sous `_undeclared`.

## Vérifier

- `GET /.well-known/oauth-protected-resource/mcp` renvoie la ressource et son serveur
  d'autorisation ;
- un client MCP sans jeton reçoit `401` avec `resource_metadata` ;
- avec un jeton émis pour l'URL de la surface, `tools/list` montre les outils permis - projetés et
  fédérés ;
- un appel compte dans le même quota que l'appel REST ;
- `/config` montre `mcp_tools` et `mcp_federation` ;
- `flexway_mcp_calls_total` porte le `client` de l'agent et, sur un refus, sa cause.

vers l'amont ; la chaîne d'agents bornée. Les clés d'API arrivent avec le change suivant ; les
ressources et les invites MCP d'un serveur fédéré ne sont pas relayées.*
