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.
# 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
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
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
uv run mcp dev server.py # Python; abre el MCP InspectorEl 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.
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 TrueHaga 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
- Complete el núcleo obligatorio de tres herramientas: página 04.
- Aplique las garantías de la página 05 a cualquier herramienta que escriba en sus sistemas.
- 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). |
| 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). |
| 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). |
| Respuesta truncada en la conversación | Devuelve demasiado. El recorte es a 3.000 caracteres: página 07. |
| La herramienta no se ejecuta en llamadas telefónicas | Hay familias bloqueadas en voz por diseño (página 04). |