Salesbot — LinkedIn MCP Server
Servidor MCP de LinkedIn y MCP de Sales Navigator para descubrimiento de prospectos, divulgación aprobada por humanos, flujos de trabajo de bandeja de entrada y un CRM de ventas integrado con 48 herramientas con controles de seguridad.
Documentación
Servidor MCP de LinkedIn con CRM (Salesbot)
¿Qué es esto?
linkedin-mcp-server-salesbotes un servidor MCP (Model Context Protocol) para operaciones de relaciones en LinkedIn asistidas por IA. Permite que asistentes de IA — Claude Desktop, ChatGPT, Cursor — te ayuden a investigar y organizar contactos profesionales, redactar mensajes profundamente personalizados para tu revisión y aprobación, sincronizar conversaciones de la bandeja de entrada, enriquecer perfiles y obtener contexto web, todo bajo tu dirección. Está diseñado para contactos hiperespecíficos y significativos (encontrar 5 contactos ideales, leer sus publicaciones recientes, escribir 5 notas reflexivas), no para envíos masivos. Cada envío está controlado por aprobación humana en el circuito y límites de seguridad diarios/horarios aplicados en el servidor que mantienen tu cuenta de LinkedIn dentro de límites seguros.
Se ejecuta como una Supabase Edge Function (Deno + Hono + mcp-lite) que expone el transporte MCP Streamable HTTP. Las acciones de LinkedIn se realizan a través de un proveedor de integración de LinkedIn de terceros; las credenciales de LinkedIn nunca son almacenadas por la IA.
- Palabras clave: model context protocol, mcp server, linkedin api, linkedin automation, claude desktop, cursor, ai agents, sales automation.
- Clientes compatibles: Claude Desktop, Claude API/MCP, Cursor, cualquier cliente MCP Streamable-HTTP.
Datos rápidos
| Endpoint | https://app.salesbot.cz/api/mcp |
| Transporte | MCP Streamable HTTP (POST + SSE) |
| Cabecera de autenticación | x-mcp-api-key: sb_mcp_… (un JWT de Supabase en Authorization también funciona) |
| Número de herramientas | 49 |
| Licencia | MIT |
¿Cómo me conecto? (Claude Desktop / Cursor)
Añade esto a la configuración de tu cliente MCP. Obtén la clave sb_mcp_… en la aplicación Salesbot en Configuración → MCP.
{
"mcpServers": {
"linkedin-automation": {
"url": "https://app.salesbot.cz/api/mcp",
"headers": {
"x-mcp-api-key": "sb_mcp_YOUR_API_KEY",
"Accept": "application/json, text/event-stream"
}
}
}
}
Importante: envía la clave en la cabecera
x-mcp-api-key, no enAuthorization: Bearer. La pasarela API de Supabase rechaza tokens Bearer desconocidos antes de que lleguen al servidor.
Autenticación
- Clave API MCP (
sb_mcp_…) — de larga duración; generada en la aplicación Salesbot, almacenada solo como hash SHA-256. Enviar enx-mcp-api-key. - JWT de Supabase — un token de sesión de usuario con sesión iniciada en
Authorization: Bearer. - Se requiere una suscripción/prueba activa.
¿Cómo autentico LinkedIn?
La IA puede hacerlo sin salir del chat:
- Llama a
get_linkedin_status— informa si LinkedIn está conectado/activo/bloqueado. - Si no está conectado, llama a
connect_linkedin— devuelve un enlacehttps://auth.salesbot.cz/…de marca blanca. El usuario lo abre, completa el inicio de sesión de LinkedIn, listo.
O conéctate en la aplicación: Configuración → LinkedIn → Conectar.
Herramientas
Cada herramienta devuelve contenido de texto; los errores devuelven { "ok": false, "code": "<CODE>", "error": "<message>" }.
Conexión
{ "name": "get_linkedin_status", "input": { "profile_id": "uuid (optional)" } }
{ "name": "connect_linkedin", "input": { "profile_id": "uuid (optional)", "reconnect": "boolean (optional)" } }
Descubrimiento de clientes potenciales
{ "name": "search_linkedin_people", "input": { "title": "string (required)", "location": "string", "locationId": "string", "network": "['S'|'O']", "limit": "number 1-50" } }
{ "name": "search_google_xray", "input": { "jobTitle": "string (required)", "location": "string", "keywords": "string[]", "excludeWords": "string[]", "limit": "number 1-100" } }
{ "name": "search_linkedin_navigator", "input": { "search_url": "string (required)", "limit": "number 1-100" } }
{ "name": "search_job_postings", "input": { "keywords": "string (required)", "location": "string", "locationId": "string", "seniority": "string[]", "job_type": "string[]", "presence": "string[]", "date_posted": "number", "easy_apply": "boolean", "limit": "number 1-50" } }
{ "name": "search_web", "input": { "query": "string (required)", "limit": "number 1-30", "country": "string (default cz)", "language": "string (default cs)" } }
{ "name": "get_job_posting_details", "input": { "job_id": "string (required)" } }
{ "name": "scrape_website", "input": { "url": "string (required)", "max_chars": "number (default 8000, max 20000)" } }
search_google_xray guarda los perfiles que encuentra en una lista de contactos "Google X-Ray" (deduplicada) y devuelve sus contact_id — listos para enriquecer, añadir a una campaña o enviar al CRM.
search_job_postings busca ofertas de empleo en LinkedIn a través de la cuenta conectada (búsqueda clásica, sin necesidad de Recruiter). Devuelve ofertas de trabajo con información de la empresa — ideal para encontrar empresas que contratan activamente para un rol específico. Combínalo con search_linkedin_people para encontrar al responsable de contratación.
search_web es una búsqueda general de Google (no restringida a LinkedIn). Usa operadores de Google como site:jobs.cz, intitle:, OR para buscar portales de empleo, sitios web de empresas o noticias. Los resultados NO se guardan en contactos — esta es una herramienta de investigación/descubrimiento.
get_job_posting_details toma un job_id de search_job_postings y devuelve la publicación completa — lo más importante es hiring_team, el reclutador o responsable de contratación que publicó el rol, con su id de LinkedIn y si hay un InMail gratuito disponible. También devuelve applicants_counter / views_counter como señales de urgencia. Flujo típico: search_job_postings → get_job_posting_details → enrich_contacts → campaña.
Contactos
{ "name": "upsert_linkedin_contact", "input": { "profile_url": "string (required)", "full_name": "string", "company": "string", "position": "string", "headline": "string" } }
{ "name": "get_contact_profile", "input": { "contact_id": "uuid (required)" } }
{ "name": "list_lead_lists", "input": {} }
{ "name": "list_contacts", "input": { "list_id": "uuid (required)", "limit": "number", "offset": "number" } }
{ "name": "enrich_contacts", "input": { "contact_ids": "uuid[] (required, max 8)", "profile_id": "uuid (optional)" } }
upsert_linkedin_contact es la ruta idempotente para una URL de perfil de LinkedIn exacta y ya conocida. Crea el contacto en la lista CRM Imports o devuelve el contact_id existente, para que las integraciones de CRM puedan llamarlo de forma segura antes de add_contacts_to_campaign sin depender de la búsqueda de Google.
list_lead_lists devuelve el list_id, nombre, descripción y número de contactos de cada grupo de contactos. Pasa un list_id devuelto a list_contacts.
Flujo de trabajo típico de contactos: list_lead_lists → list_contacts → add_contacts_to_campaign. Los contactos siguen perteneciendo a una lista de clientes potenciales, pero añadir un contacto existente a una campaña solo requiere su contact_id y el campaign_id objetivo.
search_linkedin_people y search_linkedin_navigator devuelven resultados brutos de búsqueda de LinkedIn. No persisten contactos; llama a upsert_linkedin_contact para cada perfil que quieras guardar o añadir a una campaña.
enrich_contacts extrae el perfil completo de LinkedIn de cada contacto a través de la cuenta conectada (titular, ubicación, empresa y puesto actuales, historial laboral completo, educación, habilidades) y lo guarda en el contacto. Ideal justo después de search_google_xray.
Campañas
{ "name": "list_campaigns", "input": { "status": "draft|running|paused|completed|stopped (optional)" } }
{ "name": "create_campaign", "input": { "name": "string (required)", "profile_id": "uuid (required)", "description": "string", "daily_limit": "number", "sender_context": "string", "steps": "[{ action: 'connect'|'message'|'visit', delay_hours, use_ai, ai_prompt, ai_template, send_without_message }] (required)" } }
{ "name": "update_campaign_settings", "input": { "campaign_id": "uuid (required)", "name": "string", "description": "string", "daily_limit": "number", "sender_context": "string", "auto_approve_messages": "boolean", "status": "running|paused|draft|stopped" } }
{ "name": "start_campaign", "input": { "campaign_id": "uuid (required)" } }
{ "name": "stop_campaign", "input": { "campaign_id": "uuid (required)" } }
{ "name": "add_contacts_to_campaign", "input": { "campaign_id": "uuid (required)", "contact_ids": "uuid[] (required)" } }
Mensajería con IA (escribir → aprobar → enviar)
{ "name": "generate_campaign_message", "input": { "campaign_contact_id": "uuid (required)", "step_id": "uuid (required)", "custom_instructions": "string" } }
{ "name": "list_pending_approvals", "input": { "campaign_id": "uuid", "limit": "number" } }
{ "name": "approve_message", "input": { "campaign_contact_id": "uuid (required)", "edited_messages": "[{step_id, message}]", "skip_gpt_check": "boolean" } }
{ "name": "reject_message", "input": { "campaign_contact_id": "uuid (required)", "reason": "string (required)" } }
Acciones directas de LinkedIn
{ "name": "send_connection_request", "input": { "linkedin_id": "string (required)", "profile_id": "uuid (required)", "contact_id": "uuid" } }
{ "name": "send_linkedin_message", "input": { "linkedin_id": "string (required)", "message": "string ≤5000 (required)", "profile_id": "uuid (required)" } }
{ "name": "publish_linkedin_post", "input": { "profile_id": "uuid (required)", "text": "string ≤3000 (required)", "external_link": "string", "as_organization": "string", "auto_publish": "boolean" } }
{ "name": "get_daily_limits", "input": { "profile_id": "uuid (optional)" } }
Bandeja de entrada (tiempo real)
{ "name": "list_inbox_chats", "input": { "profile_id": "uuid (optional)", "limit": "number 1-50", "cursor": "string" } }
{ "name": "get_chat_messages", "input": { "chat_id": "string (required)", "profile_id": "uuid (optional)", "limit": "number 1-50", "cursor": "string" } }
{ "name": "reply_to_chat", "input": { "chat_id": "string (required)", "message": "string ≤5000 (required)", "profile_id": "uuid (optional)" } }
{ "name": "mark_chat_read", "input": { "chat_id": "string (required)", "profile_id": "uuid (optional)" } }
CRM (embudo, notas, tareas, almacén de mensajes)
El CRM es un embudo persistente separado de los contactos. Un cliente potencial entra cuando se añade a una campaña, o cuando cualquiera de estas herramientas lo toca por primera vez. También actúa como almacén duradero para el texto de contacto generado: guarda borradores de email/LinkedIn y seguimientos con save_lead_message, léelos de nuevo con list_lead_messages o get_lead_context, y luego envíalos a través del MCP del canal correspondiente (por ejemplo, Smartlead para email) — este servidor nunca los envía por sí mismo.
{ "name": "set_deal_stage", "input": { "contact_id": "uuid (required)", "stage": "string (required)", "note": "string" } }
{ "name": "log_crm_note", "input": { "contact_id": "uuid (required)", "summary": "string (required)", "pain_points": "string[]", "sentiment": "positive|neutral|negative" } }
{ "name": "save_lead_message", "input": { "contact_id": "uuid (required)", "body": "string (required)", "channel": "email|linkedin", "kind": "string e.g. initial|followup", "subject": "string", "status": "draft|queued|sent", "message_id": "uuid (update existing)" } }
{ "name": "list_lead_messages", "input": { "contact_id": "uuid (required)", "channel": "email|linkedin", "kind": "string", "limit": "number" } }
{ "name": "create_task", "input": { "title": "string (required)", "contact_id": "uuid", "due_at": "ISO 8601", "details": "string" } }
{ "name": "list_tasks", "input": { "status": "open|done|cancelled|all", "contact_id": "uuid", "limit": "number" } }
{ "name": "complete_task", "input": { "task_id": "uuid (required)", "status": "done|open|cancelled" } }
{ "name": "get_lead_context", "input": { "contact_id": "uuid (required)", "notes_limit": "number" } }
{ "name": "update_contact", "input": { "contact_id": "uuid (required)", "email": "string", "phone": "string", "location": "string", "company": "string", "position": "string", "headline": "string" } }
{ "name": "set_lead_fields", "input": { "contact_id": "uuid (required)", "fields": "object { field_key: value }" } }
{ "name": "export_crm", "input": { "limit": "number (default 5000, max 20000)" } }
get_lead_context devuelve el contexto completo de 360° para un cliente potencial — perfil, etapa del embudo, campos personalizados, mensajes de contacto guardados, resúmenes de conversaciones, tareas abiertas, interacciones recientes de LinkedIn e historial de etapas.
Configuración del CRM (etapas y campos personalizados)
Las etapas del embudo y los campos personalizados son configurables por el usuario.
{ "name": "list_crm_stages", "input": {} }
{ "name": "add_crm_stage", "input": { "label": "string (required)", "color": "hex string" } }
{ "name": "rename_crm_stage", "input": { "key": "string (required)", "label": "string", "color": "hex string" } }
{ "name": "delete_crm_stage", "input": { "key": "string (required)", "reassign_to": "string" } }
{ "name": "list_crm_fields", "input": {} }
{ "name": "add_crm_field", "input": { "label": "string (required)", "type": "text|number|date|url" } }
{ "name": "delete_crm_field", "input": { "key": "string (required)" } }
Ejemplo de llamada
Solicitud (MCP tools/call):
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": { "name": "get_daily_limits", "arguments": {} } }
Contenido del resultado exitoso (JSON dentro de la parte de texto):
{ "profile_active": true,
"limits": { "connections": { "used": 0, "limit": 30, "effective_limit": 30 },
"messages": { "used": 0, "limit": 40, "effective_limit": 40 } } }
Contenido del resultado de error:
{ "ok": false, "code": "ACCOUNT_NOT_CONNECTED", "error": "Profile has no connected LinkedIn account." }
Códigos de error
| Código | Significado |
|---|---|
AUTH_MISSING / AUTH_INVALID / AUTH_EXPIRED | clave faltante / incorrecta / caducada |
SUBSCRIPTION_REQUIRED | prueba caducada o sin plan activo |
RATE_LIMITED | demasiadas solicitudes MCP — reduce la velocidad |
ACCOUNT_NOT_CONNECTED | el perfil no tiene LinkedIn conectado (llama a connect_linkedin) |
ACCOUNT_BLOCKED | LinkedIn restringió la cuenta (campañas pausadas automáticamente) |
PROFILE_INACTIVE / PROFILE_NOT_FOUND / ACCESS_DENIED | perfil / propiedad |
DAILY_LIMIT_REACHED / HOURLY_LIMIT_REACHED | cuota alcanzada |
OUTSIDE_ALLOWED_HOURS | fuera de la ventana de envío de la cuenta |
BLACKLISTED | empresa/dominio objetivo en lista negra |
APPROVAL_REQUIRED | en cola para aprobación humana antes del envío |
SAFETY_BLOCKED | el texto parece inyección de prompt / URL no solicitada |
REPLY_LIMIT_REACHED | ya hay 2 respuestas de IA en esta conversación |
SCRAPE_LIMIT_REACHED | cuota semanal de extracción web alcanzada |
VALIDATION_ERROR / NOT_FOUND / UPSTREAM_ERROR | entrada incorrecta / no encontrado / fallo ascendente |
Seguridad y uso responsable
Protección algorítmica integrada de LinkedIn y límites de seguridad diarios. Esta es una herramienta de relaciones, no un correo masivo — está diseñada para enviar unos pocos mensajes altamente personalizados y aprobados por humanos, y el servidor previene activamente el abuso masivo:
- Límites diarios por cuenta con aumento gradual para cuentas nuevas; limitación MCP por hora; límite general de tasa de solicitudes por usuario.
- Cola de aprobación humana en el circuito para acciones salientes (configurable).
- Ventanas de horas/días permitidos y retrasos aleatorios anti-detección.
- Defensa contra inyección de prompt: el texto no confiable del CRM/bandeja de entrada se trata como datos; el texto saliente se escanea antes del envío.
- Bandeja de entrada: máximo 2 respuestas de IA por conversación (anti-desbordamiento); las respuestas se escanean contra inyección.
- Protección de cuenta: en caso de bloqueo de LinkedIn (403 del proveedor), las campañas se pausan automáticamente y se envía un email al usuario.
Preguntas frecuentes
¿Qué clientes de IA funcionan? Cualquier cliente MCP Streamable-HTTP — Claude Desktop, la API de Claude, Cursor y similares.
¿Por qué x-mcp-api-key y no Authorization? La pasarela de Supabase valida tokens bearer Authorization y rechaza los desconocidos; la cabecera personalizada pasa sin cambios.
¿La IA ve mi contraseña de LinkedIn? No. La autenticación ocurre a través de un flujo de proveedor alojado (de marca blanca en auth.salesbot.cz); el servidor MCP solo usa un identificador de cuenta.
¿Puede la IA enviar mensajes sin mí? Solo si desactivas la aprobación. Por defecto, las acciones salientes se ponen en cola para aprobación humana.
¿Es seguro para mi cuenta de LinkedIn? Los límites diarios/horarios, el aumento gradual, las horas permitidas, los retrasos aleatorios y la pausa automática ante un bloqueo detectado se aplican todos en el servidor.
Despliegue
Se ejecuta en el backend de Supabase de Salesbot. Con la CLI de Supabase:
supabase functions deploy mcp-server --no-verify-jwt --project-ref <your-project-ref>
Secretos de función requeridos: SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_ANON_KEY, las credenciales del proveedor de LinkedIn, CRON_SECRET, APP_URL. El servidor hace su propia autenticación, por lo tanto --no-verify-jwt.
Licencia
MIT — ver LICENSE.