Skip to main content
Glama

linkedin-pilot

Servidor MCP (y CLI) que controla LinkedIn con tu sesión real, en un navegador persistente. Cubre las tres cosas de punta a punta:

  1. Actualizar tu perfil completo: titular, acerca de, experiencia, educación, aptitudes, certificaciones, proyectos, idiomas, foto, portada, información de contacto, URL personalizada y "Abierto a trabajar".

  2. Interactuar con otros perfiles: buscar personas, invitar a conectar con nota, enviar mensajes, seguir, validar aptitudes, gestionar invitaciones, reaccionar y comentar publicaciones, publicar en el feed.

  3. Postular a ofertas de empleo: buscar con todos los filtros de LinkedIn, leer el detalle y completar la Solicitud sencilla (Easy Apply) paso a paso.


Por qué está hecho así

La API oficial no sirve para esto. Desde 2015 LinkedIn exige entrar al Partner Program para cualquier acceso real, y ni siquiera los partners tienen escritura sobre el perfil: la API pública devuelve nombre, titular y correo, y poco más. SNAP dejó de aceptar solicitudes nuevas. No hay ruta oficial para "actualiza mi experiencia" ni para "postula a esta oferta".

Entonces la vía es la interfaz real, conducida por Playwright con tu propia sesión.

Leer y escribir van por caminos distintos

LinkedIn migró el perfil, la oferta de empleo y los resultados de búsqueda a renderizado SDUI: ya no hay <h1>, desaparecieron las anclas de sección (#experience, #about), las clases CSS son hashes que cambian solos y el contenido se carga de forma diferida. Raspar ese HTML para leer es frágil y lento.

Por eso el proyecto usa dos vías:

  • Para leer: la API interna (Voyager), que devuelve el perfil completo y el detalle de una oferta en una sola llamada, estructurados. Se aprovecha la sesión del propio navegador, así que las peticiones salen con las mismas cookies, el mismo user-agent y la misma IP que la navegación real.

  • Para escribir: la interfaz, porque no hay otra: no existe endpoint para guardar tu experiencia ni para postular.

Cuando una vía falla se cae a la otra, y ambas quedan reportadas.

Tres decisiones sostienen lo demás

  • Sesión persistente, login manual. El navegador guarda su perfil en disco, así que inicias sesión una sola vez (con verificación en dos pasos y captcha incluidos) y las cookies sobreviven entre ejecuciones. No se automatiza el login a propósito: automatizarlo es justo lo que dispara los bloqueos, y además obligaría a guardar tu contraseña en algún lado.

  • Los campos se identifican por su etiqueta visible, no por clases CSS. LinkedIn cambia el HTML constantemente y sirve la interfaz en el idioma de la cuenta. El motor de formularios lee el nombre accesible de cada control (aria-label, <label for>, legend...) y lo empareja contra un glosario bilingüe: pedir Titular encuentra Headline, y al revés. Esto se ve funcionando en las postulaciones reales, donde una pregunta como «Confirm the name of the company where you work / Confirme o nome da empresa...» se resuelve sola con la respuesta guardada como «Current company».

  • El contenedor del formulario se descubre, no se asume. Los editores de perfil dejaron de ser ventanas modales: ahora son páginas que ni siquiera cuelgan de <main>, y el asistente de Solicitud sencilla tampoco es un role="dialog". En vez de perseguir esa estructura, el motor parte del botón que cierra el paso (Guardar, Siguiente, Enviar) y sube hasta el primer ancestro que agrupa varios campos. Funcione donde funcione el formulario.

  • Una sola pieza rellena todo. Editar el perfil y postular a un empleo son el mismo problema: un modal con campos. El mismo motor sirve para ambos, así que cualquier sección nueva que LinkedIn agregue ya está soportada sin tocar código.

Y hay una salida de emergencia: si algo se rompe, las herramientas linkedin_browser_* dan control directo del navegador (radiografía de la página, clic por referencia, escritura en campos) para terminar el trabajo a mano sin editar el proyecto.


Related MCP server: LinkedIn MCP Server

Instalación

cd linkedin-pilot
npm install
npm run build

Requiere Node 20+. Usa el Chrome instalado en el sistema; si no lo encuentra prueba con Edge y luego con el Chromium de Playwright.

Registrarlo en Claude Code

Ya quedó registrado en el ámbito de usuario:

claude mcp add linkedin --scope user -- node "<ruta>/linkedin-pilot/dist/index.js"

También hay un .mcp.json en la carpeta padre por si prefieres el ámbito de proyecto. Comprueba con claude mcp list.


Puesta en marcha

npm run login     # abre el navegador; inicia sesión a mano una sola vez
npm run doctor    # diagnóstico: navegador, sesión, rutas, banco de respuestas

Todo lo persistente vive en ~/.linkedin-pilot/:

Ruta

Qué guarda

browser-profile/

Perfil de Chrome con las cookies de sesión

state.json

Postulaciones, interacciones y contadores diarios

answers.json

Banco de respuestas reutilizables y ruta del CV

screenshots/

Capturas automáticas cuando algo falla

logs/

Registro diario en JSON


Uso desde la terminal

linkedin-pilot status                      # ¿hay sesión?
linkedin-pilot profile                     # lee tu perfil completo
linkedin-pilot jobs "ingeniero de datos"   # busca ofertas con Solicitud sencilla
linkedin-pilot job 4021234567              # detalle de una oferta
linkedin-pilot apply 4021234567            # SIMULA la postulación
linkedin-pilot apply 4021234567 --send     # postula de verdad
linkedin-pilot applications                # historial local

Herramientas MCP

Sesión

Herramienta

Qué hace

linkedin_session_status

Estado de la sesión y consumo del día. Empieza siempre por aquí.

linkedin_login

Abre el navegador para que inicies sesión a mano.

linkedin_session_cookies

Exporta o importa cookies (li_at, JSESSIONID).

linkedin_close_browser

Cierra el navegador; la sesión queda guardada.

Perfil

Herramienta

Qué hace

linkedin_profile_read

Lee un perfil completo (el tuyo o el de otra persona).

linkedin_profile_sections

Lista las secciones editables y sus campos habituales.

linkedin_profile_inspect_form

Abre el editor y devuelve los campos reales sin guardar nada.

linkedin_profile_edit

Rellena cualquier sección por etiqueta y guarda si se lo pides.

linkedin_profile_headline

Atajo para el titular.

linkedin_profile_about

Atajo para "Acerca de".

linkedin_profile_image

Sube foto de perfil o portada.

linkedin_profile_open_to_work

Configura "Abierto a trabajar".

linkedin_profile_custom_url

Cambia la URL personalizada.

Secciones disponibles en linkedin_profile_edit, con sus rutas verificadas contra LinkedIn el 2026-09-01: intro, about, experience, education, skill, certification, project, language, course, honor, publication, organization, patent, contactInfo, openToWork.

Flujo recomendado la primera vez que tocas una sección:

1. linkedin_profile_inspect_form { "section": "experience" }
   → te dice exactamente qué campos existen hoy y cuáles son obligatorios
2. linkedin_profile_edit { "section": "experience", "values": {...}, "save": false }
   → los rellena y te muestra qué quedó; revisas en pantalla
3. linkedin_profile_edit { ..., "save": true }

Personas y contenido

Herramienta

Qué hace

linkedin_people_search

Busca personas con filtros de grado de contacto.

linkedin_connect

Invitación a conectar, con nota opcional.

linkedin_message

Mensaje directo.

linkedin_follow

Seguir o dejar de seguir.

linkedin_endorse

Validar aptitudes.

linkedin_invitations

Listar, aceptar, ignorar o retirar invitaciones.

linkedin_posts_read

Publicaciones del feed o de un perfil.

linkedin_post_react

Reaccionar (like, celebrate, support, love, insightful, funny).

linkedin_post_comment

Comentar.

linkedin_post_create

Publicar en el feed.

linkedin_notifications

Leer notificaciones.

Empleos

Herramienta

Qué hace

linkedin_jobs_search

Búsqueda con filtros de fecha, experiencia, modalidad y tipo.

linkedin_job_detail

Detalle completo de una oferta.

linkedin_job_apply

Solicitud sencilla paso a paso. Simula por defecto.

linkedin_job_save

Guardar o quitar de guardados.

linkedin_my_jobs

Ofertas guardadas o solicitudes según LinkedIn.

linkedin_applications_log

Historial local con las respuestas que diste.

Soporte

Herramienta

Qué hace

linkedin_answers_bank

Consulta y edita las respuestas reutilizables y el CV por defecto.

linkedin_usage

Consumo frente a los topes diarios.

linkedin_browser_snapshot

Radiografía de la página: enlaces, botones y campos con referencia.

linkedin_browser_navigate / _click / _type / _scroll / _screenshot

Control manual del navegador.

linkedin_voyager_request

Llamada directa a la API interna de LinkedIn.


Cómo funciona la postulación

linkedin_job_apply simula por defecto. Recorre el asistente completo, rellena lo que sabe y se detiene justo antes de enviar:

{ "job": "4021234567" }

Si el formulario pide algo que no sabe, no inventa: devuelve status: "needs-answers" con la lista exacta de preguntas, sus opciones y una captura. Se las pasas y vuelve a intentar:

{
  "job": "4021234567",
  "answers": { "Años de experiencia en Python": "5" },
  "dryRun": false,
  "confirm": true
}

Las respuestas quedan guardadas en el banco, así que la siguiente oferta que pregunte lo mismo (aunque lo redacte distinto o en otro idioma) ya no se atasca. Cuanto más completo el banco, menos intervención necesitas.

Detalles que importan:

  • Las ofertas que no son Solicitud sencilla devuelven status: "external" con el enlace de la empresa. No se puede completar desde LinkedIn.

  • "Seguir a la empresa" viene marcado por defecto en LinkedIn; aquí se desmarca salvo que pidas followCompany: true.

  • Cada postulación queda registrada, así que no repites ofertas.

  • El CV (resumePath, o el del banco) se sube en cada postulación con el botón "Cargar currículum". LinkedIn solo lo guarda en tu cuenta cuando la solicitud se envía: si se descarta, desaparece, así que no sirve subirlo una vez y luego elegirlo de la lista. En la pantalla de revisión se comprueba que el CV adjunto es el pedido; si no lo es, no se envía.

  • Un desplegable que LinkedIn trae ya elegido (el correo de contacto, por ejemplo) se cambia si el banco tiene un dato explícito distinto. El correo tiene que estar registrado en tu cuenta de LinkedIn para aparecer.

  • Si el asistente no avanza de un paso, se detiene con status: "failed", la pregunta que LinkedIn marca como obligatoria y la captura, en vez de repetir "Siguiente" hasta el límite.


Cuidados con la cuenta

Automatizar LinkedIn va contra sus condiciones de uso y las cuentas que se pasan de volumen terminan restringidas. El proyecto está construido para quedarse muy por debajo del umbral:

  • Toda acción visible para terceros exige confirm: true. Invitaciones, mensajes, comentarios, publicaciones y postulaciones no salen por accidente.

  • Topes diarios conservadores, con contador persistente:

    Acción

    Tope

    Variable de entorno

    Invitaciones enviadas

    20

    LINKEDIN_PILOT_MAX_INVITATIONS

    Respuestas a invitaciones

    100

    LINKEDIN_PILOT_MAX_INVITATION_RESPONSES

    Validaciones de aptitudes

    20

    LINKEDIN_PILOT_MAX_ENDORSEMENTS

    Mensajes

    25

    LINKEDIN_PILOT_MAX_MESSAGES

    Postulaciones

    20

    LINKEDIN_PILOT_MAX_APPLICATIONS

    Reacciones

    50

    LINKEDIN_PILOT_MAX_REACTIONS

    Comentarios

    15

    LINKEDIN_PILOT_MAX_COMMENTS

    Seguimientos

    30

    LINKEDIN_PILOT_MAX_FOLLOWS

    Publicaciones

    3

    LINKEDIN_PILOT_MAX_POSTS

  • Ritmo humano: pausas con variación aleatoria, escritura con cadencia irregular y desplazamiento por pasos.

  • Un navegador normal: Chrome real, ventana visible, sin el marcador navigator.webdriver.

Subir los topes es tu decisión, pero los valores por defecto son lo que mantiene la cuenta sana.


Configuración

Variable

Por defecto

Para qué

LINKEDIN_PILOT_HOME

~/.linkedin-pilot

Carpeta de datos

LINKEDIN_PILOT_HEADLESS

false

Ocultar la ventana (no recomendado)

LINKEDIN_PILOT_CHANNEL

chrome

chrome, msedge o chromium

LINKEDIN_PILOT_LOCALE

es-CO

Idioma del navegador

LINKEDIN_PILOT_TZ

America/Bogota

Zona horaria

LINKEDIN_PILOT_REQUIRE_CONFIRM

true

Exigir confirmación explícita

LINKEDIN_PILOT_MIN_DELAY / _MAX_DELAY

700 / 2200

Rango de pausa entre acciones (ms)

LINKEDIN_PILOT_VERBOSE

false

Diagnóstico detallado en stderr


Pruebas

npm test              # emparejado de etiquetas + arranque del servidor MCP
npm run test:forms    # motor de formularios contra el HTML real de LinkedIn
npm run test:live     # editores de perfil, empleos y personas con tu sesión

test:matching cubre lo más delicado: que pedir un campo no escriba en otro, que el glosario funcione en los dos idiomas y que el banco de respuestas no se invente una respuesta para una pregunta que no conoce.

test:live necesita sesión iniciada. Abre los editores de perfil para comprobar que exponen sus campos, y hace una búsqueda de empleos y otra de personas. No guarda ni envía nada. Es lo que hay que correr cuando algo deje de funcionar: dice en qué punto se rompió.


Cuando LinkedIn cambie algo

Pasará. El orden para arreglarlo, de menos a más esfuerzo:

  1. linkedin_profile_inspect_form o linkedin_browser_snapshot para ver qué campos y botones hay ahora.

  2. Terminar la tarea con linkedin_browser_click y linkedin_browser_type.

  3. Si el cambio es permanente, ajustar la ruta en SECTIONS (src/tools/profile.ts) o añadir el término nuevo al glosario (src/text.ts). Casi nunca hace falta más.

Available Tools

39 tools
linkedin_answers_bankC

Consulta o edita el banco de respuestas que se reutiliza al postular (datos de contacto, años de experiencia, disponibilidad, CV por defecto...). Cuanto más completo, menos se atascan las postulaciones.

Inputs: action (required), label, value, entries.

linkedin_applications_logC

Historial local de postulaciones hechas con esta herramienta, con su estado y las respuestas dadas.

Inputs: limit.

linkedin_browser_clickB

Pulsa un elemento por su ref (de un snapshot previo) o por su texto visible.

Inputs: ref, text.

linkedin_browser_navigateC

Navega a una URL de LinkedIn y devuelve la radiografía de la página.

Inputs: url (required).

linkedin_browser_screenshotC

Guarda una captura de la pantalla actual y devuelve la ruta.

Inputs: name.

linkedin_browser_scrollC

Desplaza la página para cargar contenido diferido.

Inputs: steps.

linkedin_browser_snapshotB

Radiografía de la página actual: enlaces, botones y campos con una referencia para pulsarlos o rellenarlos. Es la salida de emergencia cuando una herramienta de alto nivel no encuentra algo.

Inputs: textLimit, scopeSelector.

linkedin_browser_typeC

Escribe en un campo identificado por ref.

Inputs: ref (required), text (required), submit.

linkedin_close_browserA

Cierra el navegador. La sesión sigue guardada en disco: la próxima herramienta lo vuelve a abrir.

Inputs: no named parameters.

linkedin_connectA

Envía una invitación a conectar, con nota opcional (máx. 300 caracteres). Acción visible para la otra persona: exige confirm=true.

Inputs: target (required), note, confirm.

linkedin_endorseC

Valida aptitudes en el perfil de otra persona. Exige confirm=true.

Inputs: skills (required), target (required), confirm.

linkedin_followB

Sigue o deja de seguir un perfil. Exige confirm=true.

Inputs: target (required), follow, confirm.

linkedin_invitationsC

Lista invitaciones pendientes (recibidas o enviadas), y permite aceptar, ignorar o retirar.

Inputs: action (required), confirm, personName.

linkedin_job_applyA

Postula a una oferta con Solicitud sencilla. POR DEFECTO SIMULA: recorre todo el formulario, lo rellena y se detiene antes de enviar, devolviendo qué preguntas faltan por responder. Para enviar de verdad hay que pasar dryRun=false Y confirm=true. Las respuestas nuevas se guardan para reutilizarlas en las siguientes ofertas.

Inputs: job (required), dryRun, answers, confirm, +4 more.

linkedin_job_detailC

Trae el detalle completo de una oferta: descripción, empresa, ubicación, número de solicitantes y si admite Solicitud sencilla.

Inputs: job (required).

linkedin_job_saveC

Guarda o quita una oferta de la lista de guardados.

Inputs: job (required), save.

linkedin_loginA

Abre una ventana de navegador en la pantalla de inicio de sesión y espera a que el usuario entre a mano (contraseña, verificación en dos pasos, captcha). La sesión queda guardada en disco y se reutiliza en llamadas futuras. No automatiza el login a propósito: hacerlo es lo que dispara los bloqueos de LinkedIn.

Inputs: timeoutSeconds.

linkedin_messageC

Envía un mensaje directo a un contacto. Exige confirm=true.

Inputs: text (required), target (required), confirm.

linkedin_my_jobsB

Lista tus ofertas guardadas o tus solicitudes enviadas según LinkedIn ("Mis empleos").

Inputs: tab.

linkedin_notificationsC

Lee las notificaciones recientes.

Inputs: limit.

linkedin_post_commentC

Comenta una publicación. Exige confirm=true.

Inputs: text (required), postUrl (required), confirm.

linkedin_post_createB

Publica en el feed. Exige confirm=true.

Inputs: text (required), confirm, imagePath, visibility.

linkedin_post_reactC

Reacciona a una publicación. Exige confirm=true.

Inputs: postUrl (required), confirm, reaction.

linkedin_posts_readC

Lee publicaciones del feed ("feed") o la actividad reciente de un perfil.

Inputs: limit, source.

linkedin_profile_aboutC

Reescribe la sección "Acerca de".

Inputs: text (required), save.

linkedin_profile_custom_urlC

Cambia la URL personalizada del perfil (linkedin.com/in/loquesea).

Inputs: vanity (required).

linkedin_profile_editA

Rellena una sección del perfil y opcionalmente la guarda. Los campos se identifican por su etiqueta visible (admite coincidencia parcial). Con save=false deja el formulario abierto y relleno para que lo revises: es lo recomendado la primera vez.

Inputs: values (required), section (required), save.

linkedin_profile_headlineC

Cambia el titular del perfil (la línea bajo el nombre).

Inputs: headline (required), save.

linkedin_profile_imageC

Sube la foto de perfil o la imagen de portada.

Inputs: kind (required), filePath (required).

linkedin_profile_inspect_formA

Abre el editor de una sección y devuelve EXACTAMENTE los campos que LinkedIn muestra ahora mismo (etiqueta, tipo, si es obligatorio, opciones disponibles) sin guardar nada. Úsalo antes de editar una sección por primera vez: así los nombres de campo que pases después coinciden con los reales, aunque LinkedIn los haya cambiado.

Inputs: section (required).

linkedin_profile_open_to_workC

Configura "Abierto a trabajar": cargos, ubicaciones, tipos de empleo y quién puede verlo.

Inputs: values (required), save.

linkedin_profile_readA

Lee un perfil completo (el propio por defecto): titular, acerca de, experiencia, educación, aptitudes, certificaciones e idiomas.

Inputs: target.

linkedin_profile_sectionsA

Lista las secciones del perfil que se pueden editar, con los campos habituales de cada una.

Inputs: no named parameters.

linkedin_session_cookiesA

Exporta las cookies de la sesión a un archivo, o importa un li_at/JSESSIONID obtenido del navegador personal. Útil para respaldar la sesión o para arrancar sin pasar por el login.

Inputs: action (required), liAt, filePath, jsessionid.

linkedin_session_statusA

Comprueba si hay sesión de LinkedIn activa en el navegador persistente. Devuelve el nombre, el identificador público y cuándo caduca la cookie. Llama a esto antes que nada.

Inputs: no named parameters.

linkedin_usageB

Muestra cuántas acciones llevas hoy frente a los topes diarios, y el registro de interacciones.

Inputs: showOutreach.

linkedin_voyager_requestA

Llamada directa a la API interna de LinkedIn (Voyager) reutilizando la sesión del navegador. Atajo avanzado para leer datos que la interfaz no expone cómodamente. No es una API pública: las rutas cambian sin aviso, así que trata los fallos como normales y usa las otras herramientas.

Inputs: path (required), body, method.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    C2.9/5.0

    Scored across 39 tools

    Disambiguation4/5

    Most tools have clearly distinct resource+action purposes, and the high-level vs. browser-primitive vs. Voyager boundaries are usually clear. However, the generic profile_edit overlaps with specific wrappers like profile_headline, profile_about, profile_image, and profile_open_to_work, so an agent could reasonably choose either path for the same outcome.

    Naming Consistency4/5

    All tools use snake_case with a consistent linkedin_ prefix, making the set predictable overall. Minor deviations exist, such as linkedin_posts_read (plural) versus linkedin_post_create/react/comment (singular), and linkedin_jobs_search versus linkedin_job_detail/apply/save.

    Tool Count2/5

    With 39 tools, the set is well beyond the 25+ threshold that typically signals excessive surface area. While the domain is broad, several convenience wrappers and low-level browser primitives could be consolidated or exposed through fewer generic tools without losing core capability.

    Completeness4/5

    Core workflows are well covered: session management, profile editing, people search/connect/message, post interaction, job search/apply/save, usage monitoring, and low-level browser fallbacks. The notable gap is reading or managing the message inbox/conversations, though browser_snapshot/navigate and voyager_request provide workarounds.

    Maintenance

    ActivitySlowing
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      C
      maintenance
      Lets an AI assistant operate LinkedIn through an authenticated browser session, enabling profile management, posting, networking, messaging, job search, and automated applications.
      100
      39 npm
      1
      MIT
    • A
      license
      B
      quality
      B
      maintenance
      Enables an AI agent to fully operate a LinkedIn account—posting, messaging, profile edits, and network management—with self-healing selectors, human-like pacing, and approval-based planning to avoid breakage or lockouts.
      39
      3 npm
      MIT