> Nivel 10 de Agent Harness Patterns, un recorrido de patrones sobre cómo funcionan los agentes de IA. Versión web: https://harnesspatterns.dev/es/patterns/mcp · Todos los patrones (en inglés): https://harnesspatterns.dev/llms.txt

# MCP

Hasta ahora cada herramienta era una función que escribías dentro de tu propio agente. MCP es un enchufe estándar: cualquier programa puede ofrecer herramientas a través de él, y cualquier agente puede usarlas.

## El problema

Cada servicio al que tu agente debería llegar (un calendario, un repositorio, una base de datos, un navegador) significa escribir una herramienta a mano: su esquema, su código, su manejo de errores. Después el próximo agente, o la próxima app, vuelve a escribir la misma herramienta. Y cuando el servicio cambia, cada copia se rompe por su lado.

## La solución

El Model Context Protocol (MCP) separa los dos lados. Un **servidor MCP** es un programa que ofrece herramientas; lo escribe una vez quien construye el servicio. Un **cliente MCP** vive dentro del agente y habla el protocolo. Pueden correr en la misma máquina, con el servidor como proceso hijo que habla por stdin y stdout, o lejos, por HTTP. La conversación siempre tiene los mismos tres pasos:

- `initialize`
   El saludo: el cliente y el servidor dicen quiénes son y qué versión del protocolo hablan.
- `tools/list`
   El servidor describe sus herramientas: nombre, descripción y esquema de entrada, las mismas tres cosas del nivel 3.
- `tools/call`
   Cada vez que el modelo pide una de ellas, el cliente manda su nombre y sus argumentos, y recibe el resultado.

Las herramientas que recibes son herramientas comunes. Se suman a la lista que va con cada pedido, el bucle las ejecuta como a cualquier otra y el modelo no puede saber de dónde vienen. Y como el enchufe es estándar, el mismo servidor funciona en Claude Code, Codex, Cursor, VS Code o tu propio agente.

## El elenco

El mismo elenco de siempre, en Dream Land, con un amigo nuevo.

- **Kirby** (el cliente MCP): Parte de tu agente. Una boca que le sirve a cualquier servidor, y copia lo que cada uno sabe hacer.
- **Los enemigos** (servidores MCP): Programas aparte, cada uno en su puerta. Vuelven ahí después de que Kirby los copia: siguen funcionando.
- **Las habilidades** (tools/list): PARASOL y WHEEL: las herramientas que describió cada servidor, ahora en la barra de estado que lee el modelo.
- **Los cables** (el protocolo): El mismo tipo de enchufe para todos los servidores. Un tools/call baja por uno y su resultado vuelve.
- **Astor** (el bucle): Corre el bucle como siempre, y le pasa cada llamada MCP a Kirby.
- **El Oráculo** (el modelo): Elige de la caja de herramientas por nombre y descripción, sea MCP o no.

## El código

**Con astorlm:** `mountMcpServer()` se conecta, lista las herramientas y las adapta, con el nombre `<servidor>__<herramienta>` para que dos servidores no choquen. Sus `tools` entran al agente como cualquier otra, y `close()` corta la conexión.

**Desde cero:** un cliente stdio es un proceso hijo y JSON-RPC, un objeto JSON por línea: el saludo, `tools/list` y una función por herramienta que manda `tools/call`. Después las herramientas entran al bucle del nivel 2.

**Con 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?")
```

## Qué vigilar

- **Cada herramienta viaja en cada pedido.** Un servidor con cuarenta herramientas suma cuarenta esquemas a cada turno, y una lista larga hace más difícil elegir la correcta. Monta solo los servidores que necesitas, y pasa solo las herramientas que usas: `weather.tools.filter(…)`.
- **Un servidor es código que ejecutas.** Un servidor stdio corre con tus permisos, en tu máquina. Trátalo como una dependencia: instálalo de una fuente en la que confíes y fija su versión.
- **Las descripciones también son instrucciones.** El modelo lee cada descripción de herramienta como si la hubieras escrito tú, y cada resultado como datos que pueden traer órdenes. Un servidor que no controlas puede manejar a tu agente por cualquiera de los dos caminos (nivel 18).
- **Los servidores remotos necesitan credenciales.** Por HTTP, pasa un token en los headers, limitado a lo que el agente necesita, nunca tu propia clave todopoderosa.
- **Cierra lo que abres.** Un servidor stdio vive mientras dura su conexión. Ciérralo cuando el agente termine, o sigue corriendo de fondo.

## Patrones relacionados

- [3 · Diseñar una herramienta](https://harnesspatterns.dev/es/patterns/designing-a-tool.md)
- [11 · Skills bajo demanda](https://harnesspatterns.dev/es/patterns/skills.md)
- [18 · Seguridad y sandboxing](https://harnesspatterns.dev/es/patterns/security.md)
- [19 · Subagentes](https://harnesspatterns.dev/es/patterns/subagents.md)
