Salesbot — LinkedIn MCP Server
Servidor MCP do LinkedIn e MCP do Sales Navigator para descoberta de leads, divulgação aprovada por humanos, fluxos de trabalho de caixa de entrada e um CRM de vendas integrado com 48 ferramentas protegidas por salvaguardas de segurança.
Documentação
Servidor MCP do LinkedIn com CRM (Salesbot)
O que é isto?
linkedin-mcp-server-salesboté um servidor MCP (Model Context Protocol) para operações de relacionamento no LinkedIn assistidas por IA. Ele permite que assistentes de IA — Claude Desktop, ChatGPT, Cursor — ajudem você a pesquisar e organizar contatos profissionais, redigir mensagens profundamente personalizadas para sua revisão e aprovação, sincronizar conversas da caixa de entrada, enriquecer perfis e buscar contexto da web — tudo sob sua orientação. Foi criado para abordagens hiperdirecionadas e significativas (encontre 5 contatos ideais, leia as postagens recentes deles, escreva 5 notas atenciosas), não para disparos em massa. Cada envio é controlado por aprovação humana no fluxo e limites de segurança diários/horários aplicados no servidor que mantêm sua conta do LinkedIn dentro de limites seguros.
Ele roda como uma Supabase Edge Function (Deno + Hono + mcp-lite) expondo o transporte MCP Streamable HTTP. As ações no LinkedIn passam por um provedor de integração terceirizado; as credenciais do LinkedIn nunca são armazenadas pela IA.
- Palavras-chave: model context protocol, mcp server, linkedin api, linkedin automation, claude desktop, cursor, ai agents, sales automation.
- Clientes compatíveis: Claude Desktop, Claude API/MCP, Cursor, qualquer cliente MCP Streamable-HTTP.
Fatos rápidos
| Endpoint | https://app.salesbot.cz/api/mcp |
| Transporte | MCP Streamable HTTP (POST + SSE) |
| Cabeçalho de autenticação | x-mcp-api-key: sb_mcp_… (um JWT do Supabase em Authorization também funciona) |
| Número de ferramentas | 49 |
| Licença | MIT |
Como me conectar? (Claude Desktop / Cursor)
Adicione isto à configuração do seu cliente MCP. Obtenha a chave sb_mcp_… no aplicativo Salesbot em Configurações → 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: envie a chave no cabeçalho
x-mcp-api-key, não emAuthorization: Bearer. O gateway de API do Supabase rejeita tokens Bearer desconhecidos antes que eles cheguem ao servidor.
Autenticação
- Chave de API MCP (
sb_mcp_…) — de longa duração; gerada no aplicativo Salesbot, armazenada apenas como hash SHA-256. Envie emx-mcp-api-key. - JWT do Supabase — um token de sessão de usuário conectado em
Authorization: Bearer. - É necessária uma assinatura/avaliação ativa.
Como autentico o LinkedIn?
A IA pode fazer isso sem sair do chat:
- Chame
get_linkedin_status— informa se o LinkedIn está conectado/ativo/bloqueado. - Se não estiver conectado, chame
connect_linkedin— retorna um linkhttps://auth.salesbot.cz/…com a marca personalizada. O usuário abre, conclui o login no LinkedIn, pronto.
Ou conecte no aplicativo: Configurações → LinkedIn → Conectar.
Ferramentas
Cada ferramenta retorna conteúdo de texto; erros retornam { "ok": false, "code": "<CODE>", "error": "<message>" }.
Conexão
{ "name": "get_linkedin_status", "input": { "profile_id": "uuid (optional)" } }
{ "name": "connect_linkedin", "input": { "profile_id": "uuid (optional)", "reconnect": "boolean (optional)" } }
Descoberta de leads
{ "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 salva os perfis encontrados em uma lista de contatos "Google X-Ray" (deduplicada) e retorna os contact_ids deles — prontos para enriquecer, adicionar a uma campanha ou enviar para o CRM.
search_job_postings pesquisa vagas de emprego no LinkedIn pela conta conectada (busca clássica, sem necessidade de Recruiter). Retorna ofertas de emprego com informações da empresa — ótimo para encontrar empresas que estão contratando ativamente para uma função específica. Combine com search_linkedin_people para encontrar o gerente de contratação.
search_web é uma busca geral do Google (não restrita ao LinkedIn). Use operadores do Google como site:jobs.cz, intitle:, OR para pesquisar portais de emprego, sites de empresas ou notícias. Os resultados NÃO são salvos em contatos — esta é uma ferramenta de pesquisa/descoberta.
get_job_posting_details recebe um job_id de search_job_postings e retorna a vaga completa — principalmente hiring_team, o recrutador ou gerente de contratação que publicou a vaga, com o ID do LinkedIn e se um InMail gratuito está disponível. Também retorna applicants_counter / views_counter como sinais de urgência. Fluxo típico: search_job_postings → get_job_posting_details → enrich_contacts → campanha.
Contatos
{ "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 é o caminho idempotente para uma URL de perfil do LinkedIn exata e já conhecida. Ele cria o contato na lista CRM Imports ou retorna o contact_id existente, para que integrações de CRM possam chamá-lo com segurança antes de add_contacts_to_campaign sem depender da busca do Google.
list_lead_lists retorna o list_id, nome, descrição e contagem de contatos de cada grupo. Passe um list_id retornado para list_contacts.
Fluxo de trabalho típico de contatos: list_lead_lists → list_contacts → add_contacts_to_campaign. Os contatos ainda pertencem a uma lista de leads, mas adicionar um contato existente a uma campanha requer apenas o contact_id e o campaign_id de destino.
search_linkedin_people e search_linkedin_navigator retornam resultados brutos de busca do LinkedIn. Eles não persistem contatos; chame upsert_linkedin_contact para cada perfil que você quiser salvar ou adicionar a uma campanha.
enrich_contacts extrai o perfil completo do LinkedIn de cada contato pela conta conectada (cargo, localização, empresa e posição atuais, histórico completo de trabalho, educação, habilidades) e o salva no contato. Ótimo logo após search_google_xray.
Campanhas
{ "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)" } }
Mensagens de IA (escrever → aprovar → 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)" } }
Ações diretas no 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)" } }
Caixa de entrada (tempo 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 (funil, notas, tarefas, armazenamento de mensagens)
O CRM é um funil persistente separado dos contatos. Um lead entra nele quando é adicionado a uma campanha ou quando qualquer uma dessas ferramentas o toca pela primeira vez. Ele também atua como um armazenamento durável para o texto de abordagem gerado: salve rascunhos de e-mail/LinkedIn e acompanhamentos com save_lead_message, leia-os de volta com list_lead_messages ou get_lead_context e envie-os pelo MCP do canal certo (por exemplo, Smartlead para e-mail) — este servidor nunca os envia por conta própria.
{ "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 retorna o contexto completo de 360° de um lead — perfil, estágio do funil, campos personalizados, mensagens de abordagem salvas, resumos de conversas, tarefas abertas, interações recentes no LinkedIn e histórico de estágios.
Configuração do CRM (estágios e campos personalizados)
Os estágios do funil e os campos personalizados são configuráveis pelo usuário.
{ "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)" } }
Exemplo de chamada
Solicitação (MCP tools/call):
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": { "name": "get_daily_limits", "arguments": {} } }
Conteúdo de resultado de sucesso (JSON dentro da parte de texto):
{ "profile_active": true,
"limits": { "connections": { "used": 0, "limit": 30, "effective_limit": 30 },
"messages": { "used": 0, "limit": 40, "effective_limit": 40 } } }
Conteúdo de resultado de erro:
{ "ok": false, "code": "ACCOUNT_NOT_CONNECTED", "error": "Profile has no connected LinkedIn account." }
Códigos de erro
| Código | Significado |
|---|---|
AUTH_MISSING / AUTH_INVALID / AUTH_EXPIRED | chave ausente / incorreta / expirada |
SUBSCRIPTION_REQUIRED | avaliação expirada ou sem plano ativo |
RATE_LIMITED | muitas solicitações MCP — reduza o ritmo |
ACCOUNT_NOT_CONNECTED | o perfil não tem LinkedIn conectado (chame connect_linkedin) |
ACCOUNT_BLOCKED | o LinkedIn restringiu a conta (campanhas pausadas automaticamente) |
PROFILE_INACTIVE / PROFILE_NOT_FOUND / ACCESS_DENIED | perfil / propriedade |
DAILY_LIMIT_REACHED / HOURLY_LIMIT_REACHED | cota atingida |
OUTSIDE_ALLOWED_HOURS | fora da janela de envio da conta |
BLACKLISTED | empresa/domínio de destino na lista negra |
APPROVAL_REQUIRED | na fila para aprovação humana antes do envio |
SAFETY_BLOCKED | o texto parece injeção de prompt / URL não solicitada |
REPLY_LIMIT_REACHED | já há 2 respostas de IA nesta conversa |
SCRAPE_LIMIT_REACHED | cota semanal de extração da web atingida |
VALIDATION_ERROR / NOT_FOUND / UPSTREAM_ERROR | entrada inválida / não encontrado / falha upstream |
Segurança e uso responsável
Proteção algorítmica integrada do LinkedIn e limites diários de segurança. Esta é uma ferramenta de relacionamento, não um disparador em massa — foi projetada para enviar algumas mensagens altamente personalizadas e aprovadas por humanos, e o servidor impede ativamente o abuso em massa:
- Limites diários por conta com aumento gradual para contas novas; limite de MCP por hora; limite geral de taxa de solicitações por usuário.
- Fila de aprovação humana no fluxo para ações de saída (configurável).
- Janelas de horários/dias permitidos e atrasos aleatórios de detecção.
- Defesa contra injeção de prompt: texto não confiável de CRM/caixa de entrada é tratado como dado; texto de saída é verificado antes do envio.
- Caixa de entrada: máximo de 2 respostas de IA por conversa (anti-transbordamento); respostas são verificadas contra injeção.
- Proteção da conta: em caso de bloqueio do LinkedIn (403 do provedor), as campanhas são pausadas automaticamente e o usuário é notificado por e-mail.
Perguntas frequentes
Quais clientes de IA funcionam? Qualquer cliente MCP Streamable-HTTP — Claude Desktop, a API do Claude, Cursor e similares.
Por que x-mcp-api-key e não Authorization? O gateway do Supabase valida tokens bearer Authorization e rejeita os desconhecidos; o cabeçalho personalizado passa sem alterações.
A IA vê minha senha do LinkedIn? Não. A autenticação acontece por um fluxo de provedor hospedado (com marca personalizada em auth.salesbot.cz); o servidor MCP usa apenas um identificador de conta.
A IA pode enviar mensagens sem mim? Somente se você desativar a aprovação. Por padrão, as ações de saída ficam na fila para aprovação humana.
É seguro para minha conta do LinkedIn? Limites diários/horários, aumento gradual, janelas de horário permitidas, atrasos aleatórios e pausa automática em caso de bloqueio detectado são todos aplicados no servidor.
Implantação
Roda no backend Supabase do Salesbot. Com a CLI do Supabase:
supabase functions deploy mcp-server --no-verify-jwt --project-ref <your-project-ref>
Segredos de função necessários: SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_ANON_KEY, as credenciais do provedor do LinkedIn, CRON_SECRET, APP_URL. O servidor faz sua própria autenticação, daí --no-verify-jwt.
Licença
MIT — consulte LICENSE.