# 01 · Quickstart

**Lo que hay que construir al final es un servidor de lectura y escritura**: el agente consulta su
catálogo y sus clientes, pero también abre casos, actualiza datos, agenda y cancela citas y mueve
oportunidades. De las 25 herramientas del catálogo de referencia, **12 mutan** su sistema
([página 04](./04-capabilities.md)).

Esta página cubre solo **la primera hora**: una herramienta de lectura corriendo en local, para
validar que el protocolo, el esquema y las pruebas funcionan antes de escribir la lógica de negocio.
Conviene validar el protocolo con una sola herramienta antes de escribir las veinticinco.

El orden recomendado, y el que menos retrabajo produce, es el de tres fases de la
[página 04](./04-capabilities.md): primero 5 de lectura, después casos y actualización de cliente
(donde entran las garantías de escritura), y al final agenda y embudo.

> **Atajo recomendado.** Hay un **servidor de referencia ejecutable** que ya implementa todo el
> contrato (protocolo, autenticación, idempotencia, los seis errores) y trae la verificación de
> conformidad como comando: `asixto-mcp-reference`. Se clona, se reemplaza el módulo que simula el sistema propio por las llamadas reales, y el
> resto del contrato ya está resuelto. Esta página explica lo que ese repo hace, para
> que pueda construirlo desde cero si lo prefiere.

## 1. Instalar el SDK

Los SDK oficiales implementan el protocolo completo: descubrimiento, validación de esquemas,
serialización y transporte. Usted no escribe JSON-RPC.

```bash
# TypeScript / Node
npm install @modelcontextprotocol/server @modelcontextprotocol/express @modelcontextprotocol/node express zod

# Python
uv add "mcp[cli]"     # o: pip install "mcp[cli]"
```

## 2. Un servidor con una herramienta

### TypeScript con Streamable HTTP sobre Express

```ts
import { McpServer } from '@modelcontextprotocol/server';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import * as z from 'zod/v4';

const server = new McpServer({ name: 'acme-tools', version: '1.0.0' });

server.registerTool(
  'consultar_cliente',
  {
    description:
      'Busca un cliente por número de documento y devuelve sus datos de contacto y estado. ' +
      'Úsala para identificar a quién se está atendiendo. No la uses para modificar datos.',
    inputSchema: z.object({
      documento: z.string().min(5).max(20),
    }),
  },
  async ({ documento }) => {
    const cliente = await miBackend.buscarCliente(documento); // su API actual

    if (!cliente) {
      return {
        content: [{ type: 'text', text: 'No encontré un cliente con ese documento.' }],
        structuredContent: { encontrado: false },
      };
    }

    return {
      content: [{ type: 'text', text: `Cliente ${cliente.nombre}, estado ${cliente.estado}.` }],
      structuredContent: {
        encontrado: true,
        nombre: cliente.nombre,
        estado: cliente.estado,
        telefono: cliente.telefono,
      },
    };
  },
);

const app = createMcpExpressApp();

app.post('/mcp', async (req, res) => {
  // Sin estado: un transporte por petición. Es el modo que exige el Gateway.
  const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined });
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});

app.listen(8080);
```

### Python

```python
from mcp.server import MCPServer

mcp = MCPServer("acme-tools")


@mcp.tool()
def consultar_cliente(documento: str) -> dict:
    """Busca un cliente por documento y devuelve sus datos de contacto y estado.

    Úsala para identificar a quién se está atendiendo. No la uses para modificar datos.
    """
    cliente = mi_backend.buscar_cliente(documento)
    if cliente is None:
        return {"encontrado": False}
    return {
        "encontrado": True,
        "nombre": cliente.nombre,
        "estado": cliente.estado,
        "telefono": cliente.telefono,
    }
```

Las anotaciones de tipo **son** el esquema y el docstring **es** la descripción: no hay que
escribir JSON Schema a mano.

> **Antes de añadir el segundo parámetro, lea esto.** Todo parámetro **opcional** debe admitir
> `null` en el esquema final (`"type": ["string","null"]`, no `"type": "string"`). Si no, el día que
> el modelo mande `null` en vez de omitir el campo, el proveedor responde 400 y **el agente se queda
> sin respuesta en mitad de la conversación**. Revise el JSON Schema que genera su SDK, no solo el
> tipo del lenguaje, y compárelo con la [página 02](./02-tool-contract.md). Es el error más común y
> el más difícil de diagnosticar desde su lado, porque su servidor nunca recibe la llamada.

El SDK sirve stdio, Streamable HTTP y SSE. El Gateway solo consume **Streamable HTTP**;
la forma de exponerlo (servidor propio o montado en una app FastAPI existente) está en la
página *Running your server* de la documentación del SDK.

## 3. Probarlo en local

### Con el Inspector

```bash
uv run mcp dev server.py     # Python; abre el MCP Inspector
```

El Inspector lista sus herramientas y le permite invocarlas a mano. Es la forma más rápida
de verificar que el esquema es el que usted cree.

### Con un test automatizado

El mismo paquete es cliente, y puede conectarse al objeto servidor en memoria: sin puerto,
sin subproceso, sin transporte.

```python
import pytest
from mcp import Client

from server import mcp


@pytest.mark.anyio
async def test_consultar_cliente() -> None:
    async with Client(mcp) as client:
        result = await client.call_tool("consultar_cliente", {"documento": "CC1032"})
        assert result.structured_content["encontrado"] is True
```

Haga esto por cada herramienta antes de registrarla. La certificación de Asixto (página 08)
repite comprobaciones equivalentes contra su entorno de pruebas.

## 4. Añadir autenticación

Un servidor sin autenticación **no pasa la certificación**. Vea la
[página 03](./03-authentication.md); con el SDK de TypeScript son dos líneas
(`requireBearerAuth` y `mcpAuthMetadataRouter`).

## 5. Siguiente paso

1. Complete el núcleo obligatorio de tres herramientas: [página 04](./04-capabilities.md).
2. Aplique las garantías de la [página 05](./05-guarantees.md) a cualquier herramienta que
   escriba en sus sistemas.
3. Publíquelo en HTTPS y registre la URL cuando reciba credenciales de la API (etapa 2).

## Errores frecuentes en el primer intento

| Síntoma | Causa habitual |
|---|---|
| **El agente se queda sin responder a mitad de la conversación** | Un parámetro opcional con tipo estricto recibió `null`: el proveedor del modelo devuelve 400 y se cae toda la generación. Todo opcional debe admitir `null` ([página 02](./02-tool-contract.md)). |
| El agente no usa la herramienta | La descripción no dice *cuándo* usarla, o el nombre no se parece a la intención del usuario. |
| El agente inventa un parámetro | El esquema no está cerrado: falta `additionalProperties: false` o el campo no es un `enum`. |
| El agente pide datos que su sistema ya podría resolver | `required` demasiado largo. Déjelo en lo imprescindible. |
| El agente repite la operación | Falta idempotencia: la clave llega en `_meta` ([página 05](./05-guarantees.md)). |
| El agente dice «tengo un problema técnico» cuando simplemente no hay resultados | Devolvió `error` en vez de una lista vacía ([página 06](./06-errors.md)). |
| Respuesta truncada en la conversación | Devuelve demasiado. El recorte es a **3.000 caracteres**: [página 07](./07-limits.md). |
| La herramienta no se ejecuta en llamadas telefónicas | Hay familias bloqueadas en voz por diseño ([página 04](./04-capabilities.md)). |
