> Niveau 10 de Agent Harness Patterns, un parcours de patterns sur le fonctionnement des agents d'IA. Version web : https://harnesspatterns.dev/fr/patterns/mcp · Tous les patterns (en anglais) : https://harnesspatterns.dev/llms.txt

# MCP

Jusqu'ici, chaque outil était une fonction que vous écriviez dans votre propre agent. MCP est une prise standard : n'importe quel programme peut proposer des outils par elle, et n'importe quel agent peut les utiliser.

## Le problème

Chaque service que votre agent devrait atteindre (un agenda, un dépôt, une base de données, un navigateur) veut dire écrire un outil à la main : son schéma, son code, sa gestion d'erreurs. Puis l'agent suivant, ou l'app suivante, réécrit le même outil. Et quand le service change, chaque copie casse de son côté.

## La solution

Le Model Context Protocol (MCP) sépare les deux côtés. Un **serveur MCP** est un programme qui propose des outils ; celui qui construit le service l'écrit une fois. Un **client MCP** vit dans l'agent et parle le protocole. Ils peuvent tourner sur la même machine, le serveur en processus enfant qui parle par stdin et stdout, ou loin l'un de l'autre, par HTTP. La conversation a toujours les trois mêmes étapes :

- `initialize`
   La poignée de main : le client et le serveur disent qui ils sont et quelle version du protocole ils parlent.
- `tools/list`
   Le serveur décrit ses outils : nom, description et schéma d'entrée, les trois mêmes choses qu'au niveau 3.
- `tools/call`
   Chaque fois que le modèle demande l'un d'eux, le client envoie son nom et ses arguments, et reçoit le résultat.

Les outils que vous obtenez sont des outils ordinaires. Ils rejoignent la liste envoyée avec chaque requête, la boucle les exécute comme les autres et le modèle ne peut pas savoir d'où ils viennent. Et comme la prise est standard, le même serveur fonctionne dans Claude Code, Codex, Cursor, VS Code ou votre propre agent.

## Les personnages

Les mêmes personnages que d'habitude, à Dream Land, avec un nouvel ami.

- **Kirby** (le client MCP): Une partie de votre agent. Une bouche qui va à tous les serveurs, et copie ce que chacun sait faire.
- **Les ennemis** (serveurs MCP): Des programmes à part, chacun à sa porte. Ils y retournent après que Kirby les a copiés : ils continuent de tourner.
- **Les pouvoirs** (tools/list): PARASOL et WHEEL : les outils que chaque serveur a décrits, désormais dans la barre d'état que lit le modèle.
- **Les câbles** (le protocole): Le même genre de prise pour tous les serveurs. Un tools/call descend par l'un, et son résultat remonte.
- **Astor** (la boucle): Fait tourner la boucle comme d'habitude, et passe chaque appel MCP à Kirby.
- **L'Oracle** (le modèle): Choisit dans la boîte à outils par nom et description, MCP ou non.

## Le code

**Avec astorlm :** `mountMcpServer()` se connecte, liste les outils et les adapte, nommés `<serveur>__<outil>` pour que deux serveurs n'entrent pas en collision. Ses `tools` entrent dans l'agent comme les autres, et `close()` raccroche.

**À partir de zéro :** un client stdio, c'est un processus enfant et du JSON-RPC, un objet JSON par ligne : la poignée de main, `tools/list`, et une fonction par outil qui envoie `tools/call`. Les outils entrent ensuite dans la boucle du niveau 2.

**Avec astorlm**

```ts
import { OpenAIProvider, createLocalAgent, mountMcpServer } from 'astorlm'

// 1. Mount each MCP server: connect, ask for its tools (tools/list), adapt them.
//    Each one is a separate program; nobody wrote its schemas in your code.
const weather = await mountMcpServer({
  name: 'weather', // becomes the prefix: weather__forecast, weather__alerts
  transport: { type: 'stdio', command: 'npx', args: ['-y', 'dreamland-weather-mcp'] }, // runs it as a child process
})

const maps = await mountMcpServer({
  name: 'maps',
  transport: { type: 'http', url: 'https://maps.dreamland.example/mcp' }, // or a server that's already running
})

const agent = await createLocalAgent({
  // Any OpenAI-compatible endpoint: OpenAI, Ollama, LM Studio, vLLM, a proxy…
  provider: new OpenAIProvider({
    baseURL: 'http://localhost:11434/v1', // e.g. Ollama's default address
    model: 'your-model', // e.g. 'llama3.1', 'gpt-4o-mini'
    apiKey: 'YOUR_API_KEY', // local servers usually ignore it
  }),
  // 2. MCP tools are tools: mix them with your own, or keep only the ones you need.
  tools: [...weather.tools, ...maps.tools],
  maxTurns: 10,
})

agent.on('tool-start', ({ name }) => console.log('→', name)) // maps__route, then weather__forecast

try {
  await agent.run('Picnic at the Fountain of Dreams tomorrow. How do I get there from Cappy Town, and will it rain?')
} finally {
  // 3. Hang up when you're done: a stdio server is a process of yours.
  await Promise.all([weather.close(), maps.close()])
}
```

**TypeScript**

```ts
// An MCP client from scratch, over stdio. Node's standard library only, no SDK.
// MCP is JSON-RPC: one JSON object per line, requests with an id, replies with the same id.
import { spawn } from 'node:child_process'
import { createInterface } from 'node:readline'

type Tool = { name: string; description: string; inputSchema: object; run: (args: object) => Promise<string> }

// 1. Start the server as a child process, and pair each reply with its request by id.
async function connect(name: string, command: string, args: string[] = []): Promise<Tool[]> {
  const server = spawn(command, args, { stdio: ['pipe', 'pipe', 'inherit'] })
  const waiting = new Map<number, (result: any) => void>()
  let nextId = 1
  createInterface({ input: server.stdout }).on('line', (line) => {
    const message = JSON.parse(line)
    waiting.get(message.id)?.(message.result)
    waiting.delete(message.id)
  })
  const send = (message: object) => server.stdin.write(JSON.stringify({ jsonrpc: '2.0', ...message }) + '\n')
  const request = (method: string, params: object = {}) =>
    new Promise<any>((resolve) => {
      const id = nextId++
      waiting.set(id, resolve)
      send({ id, method, params })
    })

  // 2. The handshake: who we are, which protocol version we speak.
  await request('initialize', {
    protocolVersion: '2025-06-18',
    capabilities: {},
    clientInfo: { name: 'my-agent', version: '1.0.0' },
  })
  send({ method: 'notifications/initialized' })

  // 3. tools/list: the server describes its own tools. That's what the model will see.
  const { tools } = await request('tools/list')
  return tools.map((tool: { name: string; description: string; inputSchema: object }) => ({
    name: `${name}__${tool.name}`, // prefixed, so two servers can't clash
    description: tool.description,
    inputSchema: tool.inputSchema,
    // 4. tools/call: run it on the server, and turn its content blocks into text for the model.
    run: async (input: object) => {
      const result = await request('tools/call', { name: tool.name, arguments: input })
      return result.content.map((block: { text?: string }) => block.text ?? '').join('\n')
    },
  }))
}

// 5. Into the loop from level 2: one list of tools, wherever they came from.
const mcpTools = [...(await connect('weather', 'npx', ['-y', 'dreamland-weather-mcp'])), ...(await connect('maps', 'npx', ['-y', 'dreamland-maps-mcp']))]
const tools = Object.fromEntries(mcpTools.map((tool) => [tool.name, tool.run]))
const toolSchemas = mcpTools.map(({ name, description, inputSchema }) => ({ type: 'function', function: { name, description, parameters: inputSchema } }))
await runAgent('Picnic at the Fountain of Dreams tomorrow. How do I get there from Cappy Town, and will it rain?', tools, toolSchemas)
```

**Python**

```python
# An MCP client from scratch, over stdio. Standard library only, no SDK.
# MCP is JSON-RPC: one JSON object per line, requests with an id, replies with the same id.
import itertools
import json
import subprocess

class McpServer:
    # 1. Start the server as a child process. One request at a time keeps the pairing simple.
    def __init__(self, name, command):
        self.name = name
        self.process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
        self.ids = itertools.count(1)

    def send(self, message):
        self.process.stdin.write(json.dumps({"jsonrpc": "2.0", **message}) + "\n")
        self.process.stdin.flush()

    def request(self, method, params=None):
        request_id = next(self.ids)
        self.send({"id": request_id, "method": method, "params": params or {}})
        for line in self.process.stdout:  # skip anything that isn't our reply
            message = json.loads(line)
            if message.get("id") == request_id:
                return message["result"]

    # 2. The handshake, then tools/list: the server describes its own tools.
    def connect(self):
        self.request("initialize", {
            "protocolVersion": "2025-06-18",
            "capabilities": {},
            "clientInfo": {"name": "my-agent", "version": "1.0.0"},
        })
        self.send({"method": "notifications/initialized"})
        return self.request("tools/list")["tools"]

    # 3. tools/call: run it on the server, and turn its content blocks into text for the model.
    def call(self, tool, arguments):
        result = self.request("tools/call", {"name": tool, "arguments": arguments})
        return "\n".join(block.get("text", "") for block in result["content"])

    def close(self):
        self.process.terminate()

# 4. Into the loop from level 2: one list of tools, wherever they came from.
def remote(server, tool):
    return lambda **args: server.call(tool, args)

TOOLS, TOOL_SCHEMAS = {}, []
for server in [McpServer("weather", ["npx", "-y", "dreamland-weather-mcp"]), McpServer("maps", ["npx", "-y", "dreamland-maps-mcp"])]:
    for tool in server.connect():
        name = f"{server.name}__{tool['name']}"  # prefixed, so two servers can't clash
        TOOLS[name] = remote(server, tool["name"])
        TOOL_SCHEMAS.append({"type": "function", "function": {"name": name, "description": tool.get("description", ""), "parameters": tool["inputSchema"]}})

run_agent("Picnic at the Fountain of Dreams tomorrow. How do I get there from Cappy Town, and will it rain?")
```

## Points de vigilance

- **Chaque outil voyage dans chaque requête.** Un serveur à quarante outils ajoute quarante schémas à chaque tour, et une longue liste rend le bon choix plus difficile. Ne montez que les serveurs nécessaires, et ne transmettez que les outils utilisés : `weather.tools.filter(…)`.
- **Un serveur est du code que vous exécutez.** Un serveur stdio tourne avec vos permissions, sur votre machine. Traitez-le comme une dépendance : installez-le depuis une source fiable et figez sa version.
- **Les descriptions sont aussi des instructions.** Le modèle lit chaque description d'outil comme si vous l'aviez écrite, et chaque résultat comme des données qui peuvent porter des ordres. Un serveur que vous ne contrôlez pas peut piloter votre agent par l'un ou l'autre chemin (niveau 18).
- **Les serveurs distants demandent des identifiants.** En HTTP, passez un token dans les headers, limité à ce dont l'agent a besoin, jamais votre propre clé toute-puissante.
- **Fermez ce que vous ouvrez.** Un serveur stdio vit aussi longtemps que sa connexion. Fermez-le quand l'agent a fini, sinon il continue de tourner en arrière-plan.

## Patterns liés

- [3 · Concevoir un outil](https://harnesspatterns.dev/fr/patterns/designing-a-tool.md)
- [11 · Des skills à la demande](https://harnesspatterns.dev/fr/patterns/skills.md)
- [18 · Sécurité et sandboxing](https://harnesspatterns.dev/fr/patterns/security.md)
- [19 · Les sous-agents](https://harnesspatterns.dev/fr/patterns/subagents.md)
