Skip to main content
Si las integraciones de Composio no cubren tu caso de uso, podés crear tu propio servidor MCP y conectarlo a Horneross.

Protocolo MCP

MCP usa JSON-RPC 2.0 sobre HTTP. Tu servidor debe implementar estos métodos:
MétodoDescripción
initializeHandshake inicial
tools/listListar herramientas disponibles
tools/callEjecutar una herramienta

Implementación básica

Node.js con SDK oficial

npm install @modelcontextprotocol/sdk
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:
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)

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

{
  "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

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 un inputSchema válido:
{
  "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

TipoEjemplo
string"texto"
number42, 3.14
booleantrue, false
array["a", "b", "c"]
object{"key": "value"}

Formato de respuesta

{
  "content": [
    {
      "type": "text",
      "text": "Resultado de la herramienta..."
    }
  ]
}
Para múltiples resultados:
{
  "content": [
    {
      "type": "text",
      "text": "Orden creada exitosamente"
    },
    {
      "type": "text",
      "text": "{\"orderId\": \"ORD-123\", \"status\": \"pending\"}"
    }
  ]
}

Autenticación

Bearer Token

{
  "config": {
    "url": "https://mi-servidor.com/mcp",
    "headers": {
      "Authorization": "Bearer TOKEN_SECRETO"
    }
  }
}

API Key

{
  "config": {
    "url": "https://mi-servidor.com/mcp",
    "headers": {
      "X-API-Key": "mi_api_key"
    }
  }
}

Basic Auth

{
  "config": {
    "url": "https://mi-servidor.com/mcp",
    "headers": {
      "Authorization": "Basic base64(user:pass)"
    }
  }
}

Manejo de errores

Error en la herramienta

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ódigoSignificado
-32700Parse error
-32600Invalid Request
-32601Method not found
-32602Invalid params
-32603Internal error

Mejores prácticas

1. Descripciones claras

{
  "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

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

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

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:
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'],
    },
  },
];
Con este servidor, tu agente puede:
  • Buscar productos según las consultas del cliente
  • Verificar disponibilidad en tiempo real
  • Armar carritos de compra
  • Calcular opciones de envío

Recursos