01Quickstart

Sección 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).

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

Shell
# 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

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

Shell
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; 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.
  2. Aplique las garantías de la página 05 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íntomaCausa habitual
El agente se queda sin responder a mitad de la conversaciónUn 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).
El agente no usa la herramientaLa descripción no dice cuándo usarla, o el nombre no se parece a la intención del usuario.
El agente inventa un parámetroEl esquema no está cerrado: falta additionalProperties: false o el campo no es un enum.
El agente pide datos que su sistema ya podría resolverrequired demasiado largo. Déjelo en lo imprescindible.
El agente repite la operaciónFalta idempotencia: la clave llega en _meta (página 05).
El agente dice «tengo un problema técnico» cuando simplemente no hay resultadosDevolvió error en vez de una lista vacía (página 06).
Respuesta truncada en la conversaciónDevuelve demasiado. El recorte es a 3.000 caracteres: página 07.
La herramienta no se ejecuta en llamadas telefónicasHay familias bloqueadas en voz por diseño (página 04).