Protocolo MCP
MCP usa JSON-RPC 2.0 sobre HTTP. Tu servidor debe implementar estos métodos:| Método | Descripción |
|---|---|
initialize | Handshake inicial |
tools/list | Listar herramientas disponibles |
tools/call | Ejecutar una herramienta |
Implementación básica
Node.js con SDK oficial
Copy
npm install @modelcontextprotocol/sdk
Copy
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const server = new Server(
{
name: 'mi-servidor-mcp',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// Definir herramientas
server.setRequestHandler('tools/list', async () => {
return {
tools: [
{
name: 'get_weather',
description: 'Obtiene el clima actual de una ciudad',
inputSchema: {
type: 'object',
properties: {
city: {
type: 'string',
description: 'Nombre de la ciudad',
},
},
required: ['city'],
},
},
{
name: 'search_products',
description: 'Busca productos en el catálogo',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Texto de búsqueda',
},
category: {
type: 'string',
description: 'Categoría (opcional)',
},
limit: {
type: 'number',
description: 'Cantidad máxima de resultados',
default: 10,
},
},
required: ['query'],
},
},
],
};
});
// Implementar ejecución
server.setRequestHandler('tools/call', async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case 'get_weather': {
const weather = await fetchWeather(args.city);
return {
content: [
{
type: 'text',
text: JSON.stringify(weather),
},
],
};
}
case 'search_products': {
const products = await searchProducts(args.query, args.category, args.limit);
return {
content: [
{
type: 'text',
text: JSON.stringify(products),
},
],
};
}
default:
throw new Error(`Herramienta desconocida: ${name}`);
}
});
// Iniciar servidor
const transport = new StdioServerTransport();
await server.connect(transport);
HTTP Endpoint (Express)
Para un servidor HTTP accesible via REST:Copy
import express from 'express';
const app = express();
app.use(express.json());
const tools = [
{
name: 'create_order',
description: 'Crea una nueva orden en el sistema',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string' },
items: {
type: 'array',
items: {
type: 'object',
properties: {
productId: { type: 'string' },
quantity: { type: 'number' },
},
},
},
},
required: ['customerId', 'items'],
},
},
{
name: 'get_order_status',
description: 'Obtiene el estado de una orden',
inputSchema: {
type: 'object',
properties: {
orderId: { type: 'string' },
},
required: ['orderId'],
},
},
];
// Endpoint principal MCP
app.post('/mcp', async (req, res) => {
const { jsonrpc, method, params, id } = req.body;
try {
let result;
switch (method) {
case 'initialize':
result = {
protocolVersion: '2024-11-05',
capabilities: { tools: {} },
serverInfo: { name: 'mi-servidor', version: '1.0.0' },
};
break;
case 'tools/list':
result = { tools };
break;
case 'tools/call':
result = await executeToolCall(params.name, params.arguments);
break;
default:
throw new Error(`Método no soportado: ${method}`);
}
res.json({ jsonrpc: '2.0', result, id });
} catch (error) {
res.json({
jsonrpc: '2.0',
error: { code: -32603, message: error.message },
id,
});
}
});
async function executeToolCall(name: string, args: any) {
switch (name) {
case 'create_order':
const order = await createOrderInDatabase(args);
return {
content: [{ type: 'text', text: JSON.stringify(order) }],
};
case 'get_order_status':
const status = await getOrderStatus(args.orderId);
return {
content: [{ type: 'text', text: JSON.stringify(status) }],
};
default:
throw new Error(`Tool not found: ${name}`);
}
}
app.listen(3001, () => {
console.log('MCP Server running on port 3001');
});
Python (FastAPI)
Copy
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Any, Dict, List, Optional
app = FastAPI()
TOOLS = [
{
"name": "query_database",
"description": "Consulta la base de datos de clientes",
"inputSchema": {
"type": "object",
"properties": {
"sql": {"type": "string", "description": "Query SQL"},
"limit": {"type": "number", "default": 100}
},
"required": ["sql"]
}
}
]
class MCPRequest(BaseModel):
jsonrpc: str = "2.0"
method: str
params: Optional[Dict[str, Any]] = None
id: int
@app.post("/mcp")
async def handle_mcp(request: MCPRequest):
try:
if request.method == "initialize":
result = {
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {}},
"serverInfo": {"name": "python-mcp", "version": "1.0.0"}
}
elif request.method == "tools/list":
result = {"tools": TOOLS}
elif request.method == "tools/call":
name = request.params["name"]
args = request.params.get("arguments", {})
result = await execute_tool(name, args)
else:
raise ValueError(f"Unknown method: {request.method}")
return {"jsonrpc": "2.0", "result": result, "id": request.id}
except Exception as e:
return {
"jsonrpc": "2.0",
"error": {"code": -32603, "message": str(e)},
"id": request.id
}
async def execute_tool(name: str, args: dict):
if name == "query_database":
# Ejecutar query de forma segura
results = await run_safe_query(args["sql"], args.get("limit", 100))
return {
"content": [{"type": "text", "text": str(results)}]
}
raise ValueError(f"Unknown tool: {name}")
Conectar a Horneross
1. Deploy del servidor
Desplegá tu servidor en un host accesible (Vercel, Railway, AWS, etc).2. Configurar en el agente
Copy
{
"custom_mcp": [
{
"name": "mi-sistema",
"type": "streamable-http",
"config": {
"url": "https://mi-servidor.com/mcp",
"headers": {
"Authorization": "Bearer mi_token_secreto"
}
},
"enabledTools": [
"create_order",
"get_order_status"
]
}
]
}
3. Probar con discovery
Copy
curl -X POST https://app.horneross.com/api/mcp/discover-tools \
-H "Authorization: Bearer API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "streamable-http",
"config": {
"url": "https://mi-servidor.com/mcp"
}
}'
Definición de herramientas
JSON Schema para inputs
Cada herramienta necesita uninputSchema válido:
Copy
{
"name": "send_notification",
"description": "Envía una notificación push al usuario",
"inputSchema": {
"type": "object",
"properties": {
"userId": {
"type": "string",
"description": "ID del usuario destinatario"
},
"title": {
"type": "string",
"description": "Título de la notificación",
"maxLength": 100
},
"body": {
"type": "string",
"description": "Contenido de la notificación"
},
"priority": {
"type": "string",
"enum": ["low", "normal", "high"],
"default": "normal"
},
"data": {
"type": "object",
"description": "Datos adicionales (opcional)",
"additionalProperties": true
}
},
"required": ["userId", "title", "body"]
}
}
Tipos soportados
| Tipo | Ejemplo |
|---|---|
string | "texto" |
number | 42, 3.14 |
boolean | true, false |
array | ["a", "b", "c"] |
object | {"key": "value"} |
Formato de respuesta
Copy
{
"content": [
{
"type": "text",
"text": "Resultado de la herramienta..."
}
]
}
Copy
{
"content": [
{
"type": "text",
"text": "Orden creada exitosamente"
},
{
"type": "text",
"text": "{\"orderId\": \"ORD-123\", \"status\": \"pending\"}"
}
]
}
Autenticación
Bearer Token
Copy
{
"config": {
"url": "https://mi-servidor.com/mcp",
"headers": {
"Authorization": "Bearer TOKEN_SECRETO"
}
}
}
API Key
Copy
{
"config": {
"url": "https://mi-servidor.com/mcp",
"headers": {
"X-API-Key": "mi_api_key"
}
}
}
Basic Auth
Copy
{
"config": {
"url": "https://mi-servidor.com/mcp",
"headers": {
"Authorization": "Basic base64(user:pass)"
}
}
}
Manejo de errores
Error en la herramienta
Copy
case 'risky_operation':
try {
const result = await performRiskyOperation(args);
return {
content: [{ type: 'text', text: JSON.stringify(result) }],
};
} catch (error) {
return {
isError: true,
content: [
{
type: 'text',
text: `Error: ${error.message}`,
},
],
};
}
Códigos de error JSON-RPC
| Código | Significado |
|---|---|
-32700 | Parse error |
-32600 | Invalid Request |
-32601 | Method not found |
-32602 | Invalid params |
-32603 | Internal error |
Mejores prácticas
1. Descripciones claras
Copy
{
"name": "search_inventory",
"description": "Busca productos en el inventario. Retorna nombre, precio, stock disponible y ubicación en almacén. Útil para consultas de disponibilidad."
}
2. Validación de inputs
Copy
server.setRequestHandler('tools/call', async (request) => {
const { name, arguments: args } = request.params;
// Validar parámetros requeridos
if (name === 'create_order') {
if (!args.customerId) {
throw new Error('customerId es requerido');
}
if (!Array.isArray(args.items) || args.items.length === 0) {
throw new Error('items debe ser un array con al menos un elemento');
}
}
// ... ejecutar
});
3. Logs y monitoring
Copy
app.post('/mcp', async (req, res) => {
const startTime = Date.now();
const { method, params, id } = req.body;
console.log(`[MCP] ${method}`, { params, id });
try {
const result = await handleMethod(method, params);
console.log(`[MCP] ${method} OK`, {
id,
duration: Date.now() - startTime,
});
res.json({ jsonrpc: '2.0', result, id });
} catch (error) {
console.error(`[MCP] ${method} ERROR`, {
id,
error: error.message,
duration: Date.now() - startTime,
});
res.json({
jsonrpc: '2.0',
error: { code: -32603, message: error.message },
id,
});
}
});
4. Rate limiting
Copy
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minuto
max: 100, // 100 requests por minuto
});
app.use('/mcp', limiter);
Ejemplo completo: E-commerce
Un servidor MCP para integrar con tu sistema de e-commerce:Copy
const ecommerceTools = [
{
name: 'search_products',
description: 'Busca productos por nombre, categoría o SKU',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string' },
category: { type: 'string' },
minPrice: { type: 'number' },
maxPrice: { type: 'number' },
inStock: { type: 'boolean', default: true },
},
required: ['query'],
},
},
{
name: 'get_product_details',
description: 'Obtiene detalles completos de un producto',
inputSchema: {
type: 'object',
properties: {
productId: { type: 'string' },
},
required: ['productId'],
},
},
{
name: 'check_availability',
description: 'Verifica stock disponible en tiempo real',
inputSchema: {
type: 'object',
properties: {
productId: { type: 'string' },
quantity: { type: 'number', default: 1 },
},
required: ['productId'],
},
},
{
name: 'create_cart',
description: 'Crea un carrito de compras',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string' },
items: {
type: 'array',
items: {
type: 'object',
properties: {
productId: { type: 'string' },
quantity: { type: 'number' },
},
},
},
},
required: ['items'],
},
},
{
name: 'get_shipping_options',
description: 'Obtiene opciones de envío disponibles',
inputSchema: {
type: 'object',
properties: {
cartId: { type: 'string' },
zipCode: { type: 'string' },
},
required: ['cartId', 'zipCode'],
},
},
];
- Buscar productos según las consultas del cliente
- Verificar disponibilidad en tiempo real
- Armar carritos de compra
- Calcular opciones de envío
