Skip to main content
Glama
Sundeepg98

linkedin-mcp

by Sundeepg98

linkedin

MCP-сервер, который показывает ваши данные аккаунта LinkedIn как структурированные результаты инструментов, а не страницы, по которым нужно вручную щёлкать.

Четырнадцать из семнадцати его инструментов читают данные и ничего не меняют. Три записывают.

До 2026-08-23 этот абзац гласил: «Он читает. И это всё, что он делает. В этом репозитории нет пути записи — не отключённого, не заглушенного, не спрятанного за флагом». Это было правдой — и не просто заявлением, а фактом, подкреплённым проверками, — и перестало быть правдой в день, когда появился linkedin_save_job. README, который сохраняет удобную фразу, — первое, чему доверяет читатель, и первое, что его вводит в заблуждение.

Что верно сейчас:

  • В пакете есть ровно один вызов, способный что-либо изменить в LinkedIn: одиночный привязанный клик в writes.perform. Сканер исходников по-прежнему его распознает — его не учили перестать искать, — и это признано по пути, функции и типу в однострочном списке разрешённого; тесты со сборкой не проходят, если этот список расширяется.

  • Запись выключена, пока вы её не включите. LINKEDIN_ENABLE_WRITES=1, на процесс. Свежий клон вообще не может ничего записать в LinkedIn.

  • Каждая запись — это два вызова. Первый ничего не выполняет и передаёт вам блок для чтения; второй выкупает из него одноразовый токен. Токен привязан к одному действию над одной целью и живёт 120 секунд. Это делает запланированную или автоматическую запись структурно невозможной, а не просто нежелательной.

  • linkedin_unsave_job реализован, ограничен и однозначно отказывается действовать. См. Тот, который отказывается.

  • Мы не отправляем отклики на вакансии, и это не отмашка. linkedin_job_detail сообщает apply_path: подаётся ли заявка на LinkedIn или вас перенаправляют во внешнюю систему отслеживания кандидатов — и указывает эту систему. Идентифицирующая половина поставляется как чтение. Отправляющая половина отклонена по взвешенной причине — см. Подача заявки: половина, которая выложена, и половина, которая нет.


Прочтите эту часть в первую очередь

Пользовательское соглашение LinkedIn ограничивает автоматический доступ к сайту. Это верно независимо от того, как устроен этот сервер, и ничто ниже этого не изменяет.

Что делает этот дизайн — сводит угрозу к минимуму, а не делает вид, что её нет:

Выбор

Почему это снижает риск

Только действие человека

Каждый вызов — это один, сделанный вами прямо сейчас. Ничто не запускается по таймеру, ничто не работает, пока вы спите.

Одно действие за раз

Одна загрузка страницы на каждый вызов инструмента. Без циклов прокрутки, без автоматического перелистывания, без веерных разветвлений.

Ваша сессия, ваша машина, ваш IP

Никакие куки не экспортируются третьим лицам. Нет прокси, нет IP дата-центра, нет фермы безголовых браузеров.

Обычный браузер, один флаг

Нет stealth-плагинов, подмены user-agent или платформы, патчинга отпечатков, прокси и нет искусственно подобранных задержек, имитирующих человека. Передаётся один флаг Chromium — --disable-blink-features=AutomationControlled, который не даёт Blink выставлять navigator.webdriver, — потому что LinkedIn проверяет его при входе и без него отказывается работать. Вот и всё; это обеспечивается при запуске функцией readonly.assert_launch_flags_permitted, а tests/test_launch_boundary.py останавливает сборку, если появляется третий флаг.

Только ваши данные

Ваши просмотры профиля, ваши отклики, сохранённые вакансии, ваш профиль, ваши уведомления. Никакого извлечения или сбора данных других участников.

Чтение, кроме трёх поименованных записей

Ничего не применяется, не сохраняется, не отправляется, не постится, не одобряется, не отправляет приглашение и не редактируется. Исключения — сохранение, снятие со сохранения и отписка: по умолчанию выключены, по одному за раз, каждое подтверждается вами на основе свежего прочтённого блока и одноразового токена, который живёт две минуты. Эта строка до 2026-08-23 гласОтказываюсь только читать, но сейчас это отражается точной формулировкой, а не тихим расширением.

Это снижает угрозу. Но не устраняет её. Автоматизированный доступ всё равно может привести к лимитам, проверке или блокировке аккаунта, и этот риск вы берёте на себя. Примите это осознанно, прежде чем регистрировать сервер.

Это не инструмент против детектирования — и вот чем подтверждается, а не просто утверждается

Один флаг Chromium выше — та самая деталь, из-за которой репозиторий выглядит как проект по обходу систем обнаружения. Здесь стоит прямо сказать, что именно было измерено, потому что это легко проверить, а читателю не нужно верить на слово.

Проверено 2026-08-24 по всем 105 отслеживаемым файлам:

  • Формирование отпечатков: ноль. Ни подмен user-agent, платформы, локали, часового пояса, геолокации, viewport-спуфинга, device-scale, WebGL или canvas; ни перехвата через page.route, ни инъекций init-скриптов, ни дополнительных заголовков, ни прокси. Каждый из этих методов искали по имени по всему пакету, и каждый дал ноль точек вызова. Одна оговорка, чтобы читатель, который грепает, не сбился с толку: add_init_script встречается в readonly.py дважды, и оба раза это собственный паттерн самого сканера для поиска таких вызовов. Сканер называет в том, что запрещает, поэтому его исходники это содержат, а остальной пакет — нет: partition_mutation_hits независимо подтверждает это оп — один разрешённый изменяющий вызов и ноль несанкционированных.

  • Никаких stealth-зависимостей. Четыре зависимости, и ни одна из них не является библиотекой обхода антидоров; readonly.scan_source_for_evasion возвращает ноль срабатываний по всему пакету, а единственные срабатывания вообще — это намеренно подсаженный тестовый критерий.

  • Задержки фиксированные, не "очеловеченные". Любое ожидание — константа. import random встречается 0 раз. Трёхсекундная пауза между загрузками страниц — это MIN_INTERVAL - elapsed, засыпается ровно на это значение, без вариаций. Случайный джиттер — то, что делает инструмент, имитирующий человека; ровный интервал — это ограничение частоты.

  • Сам флаг ограничен и разрешён, а не предоставлен добрым намерениям. readonly.assert_launch_flags_permitted исполняется при каждом запуске, а tests/test_launch_boundary.py блокирует сборку, если появляется любой лишний флаг.

Флаг не даёт Blink объявлять navigator.webdriver, который LinkedIn проверяет при входе и который делает автоматический браузер бесполезным даже для самого владельца аккаунта. Это всё. Сделать automатизированный браузер работающим — это одно действие; нет для этого собственнонедопущения — и здесь есть только первое.

Из этого вытекает лицензия, и она намеренно не permissive

Этот репозиторий проприетарный: все права защищены, предоставлен только для ознакомления, без разрешения использовать, копировать, изменять иая распространять.

Это не досадная случайность и не временная заглушка. Этот сервер управляет аутентифицированной сессией LinkedIn на условиях Соглашения, запрещающего автоматизацию. Разрешительная (permissive) лицензия позволила бы незнакомцам использовать его со своими аккаунтами — или с занятыми чужими аккаунтами — а в репозиторий было бы написано имя автора.

Это, прежде всего, пример для портфолио, а не инструмент. Он создан для чтения, не для развёртывания. Читать его дизайн, границы, ворота и аудиторный след — для этого и для них вс работает.


Инструмент

Что читает

linkedin_who_viewed_me

Кто просматривал ваш профиль. Если у аккаунта есть Premium Career, доступная глубина — 365 дней; это сигнал с самым высоким намерением в поиске работы.

linkedin_my_applications

Вакансии, на которые вы откликнулись, со статусом, который показывает LinkedIn.

linkedin_saved_jobs

Вакансии, которые вы добавили в закладки.

linkedin_search_jobs

Поиск вакансий по ключевым словам, местоположению, удалённой работе, дате публикации и уровню опыта.

linkedin_job_detail

Одна вакансия полностью — диапазон зарплаты, число откликнувшихся по данным LinkedIn, формат и тип занятости, статус найма и описание. Ничего из этого нет в карточке поиска или сохранённых вакансий. Также apply_path: какой из двух путей отклика использует эта вакансия, и для внешнего пути — в какую систему отслеживания кандидатов вас передаст.

linkedin_followed_companies

Страницы компаний, на которые вы подписаны, с числовым id каждой — это то, по чему адресуется linkedin_unfollow_company. LinkedIn показывает около двадцати строк любых своих подписок и не даёт возможности пролистать остальные, поэтому инструмент сообщает, что именно охвачено, а не подразумевает, что охвачено всё.

linkedin_my_profile

Ваш собственный профиль: заголовок, «О себе», навыки и то, какие секции оставились. LinkedIn не подгружает Experience/Education/Skills до прокрутки страницы, поэтому эти секции возвращают UNKNOWN, а не ноль.

linkedin_notifications

Ваш список уведомлений.

linkedin_auth_status

Есть ли живая сессия; проверка производится аутентифицированным запросом.

linkedin_login_browser

Открывает окно, в котором вы входите сами.

linkedin_session_info

Жива ли сессия и когда она истекает — берётся из собственного cookie-хранилища браузера. Сообщает учетные данные, csrf-cookie, которая их поддерживает, долговечность и причину, почему здесь нет тихой переавторизации. renewal.session_lapses_at — это дата, после которой обновление уже не поможет и придётся входить вручную; это поле для сравнения между серверами, и в LinkedIn же равно собственному сроку cookie, потому что ничто здесь не сможет продлить сессию за этот предел.

linkedin_logout

Завершает локальный вход, стирая cookie-хранилище этой машины. Это единственный разрушительный инструмент здесь: confirm=False (по умолчанию) ничего не выполняет и показывает предварительно, что будет. Не делает ни одного запроса, поэтому LinkedIn не узнаёт.

linkedin_cdp_status

Диагностика восстановления: есть ли Chrome, к которому сервер может подключиться? Ничего на LinkedIn не трогает.

linkedin_server_info

Границы, настройки лимитов и флаги запуска — без чтения исходного кода.

Related MCP server: LinkedIn MCP Server

Три инструмента, которые записывают

Инструмент

Что он делает

linkedin_save_job

Добавляет одну вакансию в закладки. Если вызвать без confirm_token, ничего не делает: на лету читает вакансию и ваш список сохранённого и возвращает блок, где вакансия названа по должности и работодателю, куда перешёл бы переключатель, откуда взновается каждый факт и как это отменить. Когда вы вызовете его повторно с токеном из этого блока — действие выполняется.

linkedin_unsave_job

Та же механика, те же проверки — и он отказывается. См. ниже.

linkedin_unfollow_company

Отписка от одной страницы компании. Та же механика и те же пять проверок. Адресуется числовым id компании, не именем: имена совпадают, меняются, и на них нельзя полагаться, а клик привязан к строке с этим id, так что то, что вы назвали, и то, что нажимается, — та же самая строка по построению.

После клика результат подтверждается с другой поверхности — вашего сохранённого списка, со своим счётчиком LinkedIn на вкладке, — а не по кнопке, которую только что нажали. performed возвращается true, false или "unknown". При "unknown" действие не повторяйте: повтор по тумблеру, который уже сработал, выполнит совершенно противоположное действие.

Тот, который отказывается

LinkedIn опознаёт элемент сохранения по его доступному имени. Каждыйснимок в этом репозитории — четыре вакансии, оба состояния гидрации, два разных дня — показывает aria-label="Save" и в скотийном виде "Save" — это несохранённое состояние. Имя, которое оно носит, когда вакансия сохранена, никогда не наблюдалось; его нельзя увидеть чтением: на аккаунте нет ничего сохранённого, чтобы на нем это застать.

Поэтому у linkedin_unsave_job нет якоря, и этот сервер его не выдумывает. "Saved" и ``"Unsave the job"` — обе версии правдободобно, и он не видел ни одной. Отказ называет именно эту причину, а не «не реализовано», потому что «не реализовано» побуждает кого-то реализовать это, можно выбрать строку.

Исправление — одна измеренная строка. Первое же сохраняемое действие выдаёт её: perform читает rthat же, какой подписью изменился переключатель, и сообщает эту подпись. Внесите её в shape.SAVE_LABELS — и unsave_job получает якорь. Это одна строка таблицы, а не отсутствующий кодовая ветка.

Поданние отклика: половина работает, и половина недоступна

linkedin_job_detail сообщает, как выполнtories отклик на вакансию. LinkedIn рисует элемент управления откликом как ссылку, а не кнопку, поэтому адрес виден без нажатия, и apply_path возвращает один из трёх ответов:

  • linkedin_apply — отклик заполняется и отправляется на LinkedIn.

  • offsite — LinkedIn передаёт вас на ATS работодателя. Назначение декодируется из исходящей обёртки LinkedIn только строкой: переходы не выполняются, третья сторона не привлекается. Вы получите узел хоста, чтобы знать, чью форму вам tessл предстоит заполнять.

  • unknown — сведения нет. Это честный ответ и он существенно менее; вот это важно.

Это полезная половина: она не стоит отдельной загрузки страницы и является чистым чтением.

Но отклик не отправляет. Не потому, что подача откликов не входит в функции этого сервера, а по причинам, которые были измерены:

  1. Процесс подачи отклика (apply FLOW) никогда не был захвачен. За все тринадцать захватов вакансий — ни одной формы, ни одного файлового ввода, ни одного диалога, ни одного скринингового вопроса и ни одного управляющего элемента, который что-то отправляет. Ничто здесь не видело того, что нужно было бы заполнять или нажимать. Это тот же стандарт, что и для unsave_job, применённый к действию, которое заслуживает его сильнее всего.

  2. Отсюда подачу заявки нельзя отменить — ни на каком уровне подтверждения, ни при каких обстоятельствах. Отзыв заявки запрещён навсегда.

  3. Внешняя (off-site) половина — это вообще не задача этого сервера, каким бы удачным ни получился захват. Управлять чужой формой, размещённой на чужом домене, по их условиям — это отдельный программный продукт.

Поэтому apply_job полностью специфицирован и закрыт в writes.py: он не регистрирует никакого инструмента и не хранит url, а тем самым разрешение на него отклоняется при выдаче, а не при использовании.

А у пробела есть адрес — это и делает его неизмеренным, а не вечным. Скрипт scripts/_probe_apply_flow.py захватывает поток, размещённый на LinkedIn, и перечисляет ровно те элементы управления, которых не хватает в каждом существующем захвате, — формы, файловые вводы, диалоги, скрининговые вопросы, элемент, отправляющий данные. К потоку он попадает по навигации, а не кликом (LinkedIn рисует управляющий элемент подачи как ссылку), собственный сканер мутаций пакета не находит в нём ни одного мутирующего вызова, а job-идентификатор он принимает как обязательный аргумент, чтобы никакое значение по умолчанию не выбирало вакансию за вас. Он также читает счётчик LinkedIn на вкладке Applied до и после — потому что открытие потока Easy Apply может создать черновик: гипотеза, которую никто не проверил, помечена как гипотеза и измерена, а не принята на веру.

Он не запускался. Запустите его в присутствии наблюдателя, на вакансии, у которой apply_path имеет значение linkedin_apply.

Почему классификатор требует совпадения нескольких полей, когда одно очевидное поле выглядит достаточным. Каждый кандидат был промерен, и по отдельности каждый не справляется: селектор data-view-name="job-apply-button" присутствует в одном захвате из тринадцати и отсутствует на полностью гидратированном внешнем объявлении, так что его отсутствие не несёт никакого сигнала. Обёртка внешнего перехода у всех одинаковая — в одном захвате их две, и только одна является элементом подачи. Доступное имя (accessible name) — самое сильное поле по отдельности, и именно его LinkedIn уже изменило: строки "Easy Apply" нет ни в одном доступном имени, а в тексте страницы она встречается дважды, так что парсер, который полагается на известное всем название функции, не совпадает ни с чем. А контент до гидратации (pre-hydration) и вовсе хуже бесполезного: внешнее объявление, как подтвердилось при замере, несёт на себе собственный маркер внутреннего потока — для того же job id, потому что LinkedIn раздаёт всю машину состояний подачи заявки как шаблон каждой вакансии.

Что он намеренно не умеет

Переписка, InMail, запросы на связи. Редактирование профиля. Open To Work. Подписка на компанию. Публикации, лайки, комментарии, одобрения. Отметка уведомлений прочитанными. Сбор данных о других участниках. Подача заявлений — как в разделе выше.

Это не отсутствующие функции, и «нет» здесь не одного вида. linkedin_server_info помечает каждый пункт как POLICY, MEASURED или UNMEASURED, потому что «такое нам принципиально недоступно», «мы посмотрели и это не сработает» и «никто пока не смотрел» — это три разных утверждения, а список, который сглаживает их между собой, — это способ превратить непроверенный пробел в объектов каждый ответ.

Подписка на компанию — самый интересный случай, и его причина поменялась 2026-08-24, не меняя результата. Раньше это было запрещено, потому что не существовало отписки — сервер мог создать состояние, которое сам не был способен снять. Теперь такая возможность появилась. Но действие всё равно недоступно, потому что отмену нельзя нацелить: вакансия называет работодателя слагом, поверхность для отписки адресует строки по числовому company id, а ни один захват в репозитории не носит этих двух значений для одной компании на одной поверхности. К тому же эта поверхность отриизовывает лишь около двадцати пятьюдесять строки без пагинации, так что большая часть списка недостижима при одной загрузке страницы. В отказе названы обе причины — и названно то, что их сняло бы.

Чтение собственного ящика — это UNMEASURED, а не запрет. Граница чтения блокирует /messaging, а каждая письменная формулировка, которая это объясняет, составлена в терминах отправки. Возможно ли чтение вообще, ещё никогда не проверялось. scripts/_probe_messaging.py существует, чтобы проверить это — и чтобы проверить то, что вопрос обычно обходит. Гипотеза — и непроверенность здесь и в этом-то и суть — состоит в том, что настольный вид сообщений LinkedIn при переходе открывает какой-то разговор, и «прочтение» входящих может отметить нить прочитанной, то есть к вопросу об уведомлениях инструмент придёл через действие, названное «чтением». Чтобы найти это, нужно прочитать навигационный бейдж с /feed/ до и после перехода — поверхность, которую та самая загрузка не трогает. Скрипт пока не запускался, и список запрещённого не изменится, пока он не запущен: граница не сдвигается на непроверенном утверждении.

Всё остальное, что могло бы что-то менять на серверах LinkedIn, вне рамок, и tests/test_readonly.py ломает сборку, если где-либо в пакете появится второй мутирующий вызов.

Один инструмент меняет нечто отражается на этой машине: linkedin_logout(confirm=True). Запрос не выполняется, так что LinkedIn не узнаёт об этом, а linkedin_server_info указывает его в local_state_writes, не в read_only.

Два побочных эффекта, которые мы говорим прямо, а не прячем

Судьёт действие, которое что-то меняет, обязано это объявить:

  1. Открытие страницы уведомлений сбрасывает бейдж непрочитанного у LinkedIn — ровно как было бы при открытии той же страницы человеком. Это измерено, а не теоретизировано: один вызов от 2026-08-21 снял бейдж с 1 на 0, и он не возвращается. Обойти невозможно: LinkedIn помечает список просмотренным на сервере в момент отдачи страницы — значит, без изменения бейджа эта поверхность не читается. Никакого клика, скролла или пообъектного открытия нет, и вызовов, которое помечали бы прочитанным, в пакете ни одного. Единственный способ не стирать бейдж — не вызывать linkedin_notifications Ведь бейдж уйдёт в одну из двух сторон, у каждой строки сохранено поле unread, каким LinkedIn видел его в момент чтения — единственный факт, который эта загрузка страницы уничтожает.

  2. Поиск вакансий добавляет в вашу историю недавних поисков, как если бы вы сами набрали такой запрос на сайте.

Оба про раскрыты в docstring инструментов и в linkedin_server_info.


Установка

cd D:\Sundeep\projects\job-hunting\mcp-servers\linkedin
pip install -r requirements.txt
playwright install chromium
python -m pytest            # 986 passed

Затем, как только сервер зарегистрирован у клиента, сразу вызовите linkedin_login_browser. Откроется окно в браузере на сайт linkedin.com/login. Войдите там самостоятельно — этот сервер никогда не видит, не вводит, не хранит и не передаёт пароль. Постоянный Chrome-профиль сохранит сессии далее, так что этот шаг одноразовый до того момента, пока LinkedIn не закончит сессию.

Сверьтесь с linkedin_auth_status, прежде чем довериться данным чтения.

Регистрация

Транспорт stdio, точка входа linkedin.py:

{
  "mcpServers": {
    "linkedin": {
      "command": "python",
      "args": ["D:\\Sundeep\\projects\\job-hunting\\mcp-servers\\linkedin\\linkedin.py"]
    }
  }
}

Как «только чтение» обеспечивается, а не декларируется

В linkedin_server/readonly.py собраны четыре механизма, и тесты показывают каждый из них напровóл dy planted violation — то есть когда соблюдение нарушено внамеренно, падающим, — прежде чем доверять реальному пакету. Проверка, которая не умеет упасть, ничего не подтверждает.

  1. Разрешающий список навигации. assert_read_url — это единая дверь до page.goto. Каждый разрешённый адрес — это паттерн, закреплённый якорем; ключевое слово, введённое пользователем, не может стать переходом на экшн-url. Заблокированы цели: /jobs/application/, /messaging/, приглашения, /edit/, open-to-work, всё, где есть action=, и любой хост кроме www.linkedin.com. Паттерн для вакансий — самый строгий в списке: принимается только цифровой id и вообще никакой query-строки, — потому что сборка url выполняется из целого числа, и сама по себе строки там никогда не сохранялось. Неpast вариант слагаемая форма, которую LinkedIn отдаёт тоже — /jobs/view/senior-node-engineer-at-acme-4600000042 — не пропускается по тем же обусловлю: слаг — это название должности, а текст названия — строка.

  2. Сканер исходников. Пакет прогоняется через grep по всем вызовам, которые могут изменить состояние — click, fill, type, press, select_option, set_input_files, отправке форм и резлюбых прочих не-GET-запросов. Находится ровно один: клик внутри ru.perform (writesperlab) & #44.not have notion.

Сканер не был ослаблен, чтобы под него подстроиться. Он продолжает безусловно сообщать о каждом мутирующим вызове, а признаёт этот конкретный отдель не краткий со списком разрешённых: readonly.SANCTIONED_MUTATIONS с ключом (path, function, kind). Все три компоненты отбрасывают что-то реальное: клик в dom.py, клик в другой функции writes_amount, и fill внутри perform — каждый отклоняется. Отклоняется и клик, замаскированный под замыкание уровнем выше, потому что атрибуция идёт к глубочайшей внутренней функции. Все эти пять промахов показаны падающими. Дополнительно проверяется, что в пакете ровно столько мутирующих вызовов, сколько записей в списке; это доказывает, что именно * в etot* половине второй клик внутри perform, который просто по ключу (p,f,block) не отличи уже нельзя.

evaluate тоже помечен: третий read-only DOM-сборщик ослаблен в нем, в конце у каждой вызова # ма' т навер does matter, — поэтому всякое новое evaluate в пакете ломает сборку, пока его тогда кто-нибудь в я-виденной diff.

  1. Разрешающий список инструментов. В имени ни одного инструмента нет глагола записи, и ни в одной docstring нет утвердительного обещания что-то записать или изменить. В docstring может рассказываться о невозможном — «не имеет никакого способа что-либо добавить или удалить» — именно такое предложения должно содержать read-only инструмент; поэтому проверка, further, ищет отрицание, а не запрещает слова.

  2. Граница запуска. assert_launch_flags_permitted отбрасывает любой флаг Chromium, кроме двух одобренных, и запрещает --disable-blink-features с любым чем-либо, кроме значения AutomationControlled. Этот флаг может выключаться причину диантного поведения Blink, поэтому разрешить одно лишь название недостаточно. browser.py вызывает assert_launch_flags_permitted перед каждым запуском, так что правило срабатывает в рантайме, а не только в CI. Соседний сканер бракует библиотеки антиобнаружения, прокрашивающиеся как зависимости (например playwright_stealth, undetected_chromedriver, решатели капчи, клиенты обхода TLS-фингер), и считает строку импорта — так, что можно спокойно описать границу словами.

Вся инолжённые скрипты анализируются отдельно на substitutions (любой мутации на странице: .click(, .value =, dispatchEvent, fetch(...). Их допустимо только запрашивать DOM и читать текст.

Этот сканинг на то, что реально выполняется, а не на «называется нужным именем». Тесты парсят пакет, берут первый аргумент каждого вызова page.evaluate(...), определяют его значению константе модуля и сканируют её, так что инолжить скрипт без его чтения нельзя: тот, который этой схеме нерезолвится (например, собирается прямо в рантайме) — просто ломает сборку blanket-сом. В ранней версии, кстати, сканировалась короткая рукописная список из трёх имён с суффиксом _JS; однажды в "холодном массиве" предложили постоянной EVIL_INLINE строку localStorage.setItem и fetch(, пронесли её через существующие вызов и выложили с зелёным тестами. Дыра закрыта, и ровно она сейчас является тестом.

За день до этого сервера в одной экосистеме был выпущен соседний, развивающий обратный принцип: он говорил success в момент появления сессионной команды. LinkedIn издаёт cookie и не залогиненым посетителям, так что этот успех ни стоил.

Здесь вердикт получен из GET /voyager/api/me — тот же вызов идентификации, который LinkedIn делает при открытии живого web-приложения. Появление cookie li_at — это лишь повод спросить endpoint ещё раз.

Результатов три, а не два:

  • authenticated: true — endpoint вернул личность.

  • authenticated: false — endpoint отказал, либо feed/лента незалогинаштым кейсом выдала страницу «Войти».

  • authenticated: null — ни то, ни другое не подтвердилось. «Неизвестно» не превращается в «вышел»: иначе можно было бы отправить в вход заново, в то время как сессия жива.

Дальнейшее освидетельствование имеет значение только как перевод из «неизвестно» в false. Никогда — нельзя превратить это в true на более слабитивным данных.

Значения cookie — это учётные данные: они никогда не логируются, никогда не сохраняются этим сервером и никогда не появляются в результате работы инструмента. Сообщается только о факте их наличия — и это утверждают два теста.

Вход в систему и как долго он действует

Вызовите linkedin_login_browser. Откроется окно Chrome на странице входа в LinkedIn, и вы вводите данные прямо в нём. Этот сервер никогда не видит, не вводит, не хранит и не передаёт пароль — нет такого пути в коде, который мог бы это сделать. Окно остаётся открытым, пока эндпоинт идентификации не подтвердит реальную сессию, вы не закроете его или не истечёт wait_seconds (по умолчанию 300; передайте большее число, если нужно больше времени).

Это одноразовый шаг, а не шаг для каждой сессии. Сессия живёт в профиле Chrome на диске по пути _state/chrome-profile/, поэтому она переживает:

Событие

Сессия переживает?

Почему

Перезапуск этого сервера

Да

Сессия на диске, а не в процессе.

Перезагрузка машины

Да

То же самое.

Удаление каталога профиля

Нет

Этот каталог и есть сессия.

Выход из аккаунта в окне

Нет

LinkedIn её отзывает.

Истечение срока cookie у LinkedIn

Нет

См. ниже.

Сколько времени вам даёт LinkedIn. linkedin_session_info сообщает дату истечения cookie li_at и количество оставшихся дней — данные считываются напрямую из браузерного хранилища cookie, так что вам не нужно гадать: это измерение, а не утверждение в README. Для калибровки: собственные долгоживущие cookie LinkedIn в этом профиле (bcookie, bscookie) были выданы со сроком 365 дней. Величина li_at — та, что определяет вход, и получить её может только реальный вход в систему.

Значения cookie — это учётные данные: никогда не логируются, никогда не сохраняются этим сервером, никогда в результате работы инструмента. Сообщается только имя, что они есть, и когда они истекают.

Когда сессия истекает, каждый инструмент чтения сообщает об этом — {"error": "not_authenticated", "message": "..."} и указывает на linkedin_login_browser как на путь назад. Пустой список он вместо этого не возвращает: пустой список из-за истёкшей сессии неотличим от пустого списка, потому что у вас действительно ничего нет.

Холодный старт и скрытая ловушка

li_at — это долгосрочная cookie. JSESSIONID — которую веб-приложение LinkedIn копирует в заголовок csrf-token и без которой эндпоинт идентификации не отвечает на аутентифицированный запрос, — это сессионная cookie (is_persistent=0 в хранилище cookie этого профиля). Поэтому каждый раз при запуске браузера в хранилище лежит полностью рабочий вход, а вот csrf-токена нет.

Сервер, который обратился бы к эндпоинту идентификации немедленно, отправил бы запрос без токена, получил бы отказ и сказал бы вам войти заново, хотя сессия была в порядке. Поэтому при холодном хранилище check_auth сначала загружает страницу LinkedIn, чтобы LinkedIn выдал cookie, и только потом спрашивает. Эта загрузка одновременно служит и контрольным чтением, так что дополнительных запросов не требует.

Восстановление: работа с вашим Chrome

Это не ежедневный способ. Основной — указанный выше постоянный профиль; это запасной вариант на день, когда сессия в том профиле умрёт и свежий вход будет отклоняться. Запустите его с LINKEDIN_CDP_ATTACH=1 — и этот сервер ничего не запускает: он подключается по CDP к Chrome, который запустили вы.

Две вещи молча рушат эту схему, обе проверены на этой машине:

  1. Chrome, открытый из панели задач, не имеет порта DevTools. «Мой браузер открыт» — недостаточно; он должен быть запущен с --remote-debugging-port.

  2. Синглетон Chrome проглатывает флаг. Если любой Chrome уже запущен, при запуске второго Chrome с этим флагом аргументы передаются первому и второй завершается — нет порта, нет ошибки, код возврата ноль.

Так что либо полностью закройте Chrome (окна и фоновый экземпляр), сохранив тем самым ваш настоящий профиль и настоящую LinkedIn-сессию:

"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9224

или дайте ему отдельный профиль — тогда он будет работать рядом с вашим запущенным Chrome, но будет ещё не в чем не залогинен, так что вы один раз войдёте в LinkedIn внутри этого окна:

"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9224 --user-data-dir="%LOCALAPPDATA%\linkedin-cdp"

Проверить, что всё получилось, можно, открыв http://127.0.0.1:9224/json/version — если там JSON, значит порт жив. Или вызвать linkedin_cdp_status: он проверяет его за вас, а когда там никто не отвечает, выводит команду. Адрес — 127.0.0.1, а не localhost: Chrome привязывает порт только к IPv4, и [::1] — имя резолвится в [::1] и съедает тайм-аут (измерено при 2085 мс против 35 мс).

Порт 9224, специально не порт 9223 соседнего сервера Naukri.

В режиме attach сервер не берёт блокировку профиля (профиль ему не принадлежит), работает в отдельной вкладке, а не использует одну из ваших, а при завершении отсоединяется, не закрывая ваш браузерclose() у Playwright для CDP-подключения просто привязку клиента, что было проверено на реальном Chrome, прежде чем на это. Список разрешённых инструментов только для чтения одинаков в обоих режимах.

Частотная дисциплина

  • Ровно минимум в 3 секунды между первыми загрузками страниц, реализуется глобально. Это троттлинг, а не маскировка: намеренно без джиттера, поэтому ни на что не похож.

  • Одна загрузка страницы на вызов инструмента. Единственное исключение — linkedin_my_profile(include_skills=True), которое загружает вторую страницу и сообщает pages_loaded: 2.

  • Без автоперелистывания. Просите следующую страницу поиска намеренно с start=25. Каждый результат содержит capped, page_had и limit, так что «25 результатов» никогда не примут за «25 результатов существуют».

  • Один вызов за раз, сериализуется в процессе; один процесс за раз, сериализуется блокировкой между процессами на профиль Chrome. Два процесса на один профиль Chromium повреждают его, и сессия пропадает — один соседний сервер за это потерял 37 минут.

  • Окно не задерживается. Браузер закрывается после 5 минут простоя и снимает блокировку.

Когда что-то невозможно прочитать

Сервер при ошибке вместо возврата пустого списка вызывает исключение. Пустой список со страницы failed to render, неотличим от пустого списка, когда у вас действительно ничего нет, и эти два случая нельзя смешивать. Неудачное чтение возвращается как {"error": "extraction_failed", "url": ..., "hint": ...}, так что вы сами можете открыть ту же страницу и посмотреть, что там видела.

Исключение одно: linkedin_search_jobs — там ноль результатов является нормальным ответом; эта возвращает results: [] с note.

Как читаются страницы

Имена классов у LinkedIn генерируются, а идентификаторы GraphQL-запросов меняются при каждом деплое, поэтому их как якорь ненадёжны. Не меняется форма ссылки: за /in/<slug> стоит человек, за /jobs/view/<id> — вакансия. Любой список собирается путём поиска таких ссылок и чтения текста карточки вокруг них, а затем разбирается чистыми функциями в shape.py — поэтому разбор можно тестировать без браузера, сети или аккаунта.

Уведомления — единственная поверхность, где нет надёжной ссылки в каждом элементе, поэтому они привязываются к структуре. Она скорее всего потребует обновления, и при сбое она отдаёт исключение, а не пустой список.

Структура

linkedin.py              entry point (stdio)
linkedin_server/
  config.py                  paths, timeouts, caps, the rate floor,
                             the two launch flags
  readonly.py                the allowlist, the scanners, the verb list,
                             the launch boundary
  profile_lock.py            cross-process lock on the Chrome profile
  browser.py                 persistent context, single-flight, idle close
  auth.py                    the login gate, session lifetime, cold start
  cdp_bridge.py              the recovery path: attach to a running Chrome
  dom.py                     the read-only harvesters and the control readers
  shape.py                   pure parsers and the result envelope
  server.py                  the seventeen tools
  errors.py
tests/                       1393 tests, no network, no account
  fixtures/                  frozen LinkedIn markup, scrubbed

Статус

Собрано и протестировано: 1393 теста, без сети и без аккаунта. Большинство вообще без браузера; модули на фикстурах запускают локальный headless Chromium, чтобы прогонять реальные читатели против замороженной разметки, извне на машину это не выходит.

Именно эти числа чаще бывали в этом файле неверны. В трёх волнах после пересечения тысячей тестов они писали «986», само по себе это безвредно, но это та же привычка, из-за которой четыре документа продолжали писать, что этот сервер не умеет писать. Теперь их измерять заново в каждую волну, а не переносить из прошлой.

Первый живой прогон: 2026-08-21. Вход прошёл успешно и сессия сохранилась — для этой машины флаг выше теперь подтверждён как достаточный, подтверждены /voyager/api/me и срок жизни li_at (365 дней). Затем каждый инструмент чтения был прогнан один раз против реального аккаунта. Четыре из одиннадцати заработали; результаты прогона описаны в ../_audit/2026-08-21-linkedin-parse-fix.md — вот что он показал.

linkedin_who_viewed_me возвращал имена, которые не были именами. В каждой строке была шапка страницы «Кто просмотрел ваш профиль», приклеенная к ссылке реального человека: четыре строки, одно повторяющееся имя, все четыре ссылки настоящие. Оно было исправлено в тот же день: граница строки больше не зависит от атрибута, который LinkedIn добавляет после гидрации, просмотры с ограниченной приватностью больше не отбрасываются молча (их было шесть из десяти), а временные метки теперь читаются. Проверено вживую: 10 строк, никак 10 различных имён, ни одного поля не пропущено.

Второй проход, 2026-08-22. Три поверхности, которые проход оставил битыми, были исправлены и проверены вживую. Все четыре дефекта имели одинаковыйвид: читатель был привязан к разметке, который LinkedIn больше не выдаёт, либо к разметке, наличие которой зависит от того, как далеко отрендерилась страница.

tool

было

стало

linkedin_my_profile

выдавал ошибку: нельзя было прочитать имя

читает имя, заголовок, местоположение, About и фото со страницы, на которой ноль h1. Секция теперь — самый верхний предок ее заголовка, содержащий одну h1 — той же правила, которым пользуется и строковый обход, — что даёт одинаковый результат до и после гидрации. Проверено вживую.

linkedin_saved_jobs, linkedin_my_applications

ошибка на редиректе

читают /jobs-tracker/?stage=saved и ?stage=applied вместо этого. и эти списки действительно пусты, и пустой результат теперь. явно о том, с собственным счётчиком вкладок от LinkedIn и формулировкой empty-state. Нулевое значение, не подтверждённое страницей, по-прежнему ошибка. Проверено вживую.

linkedin_notifications

строки с шумом

текст screen-reader вычесть по количеству, а не по фразе, и when берётся из элемента времени карточки. В каждой строке также сохраняется unread, как на момент чтения. Проверено против замороженного снимка живая страница.

навыки внутри my_profile

вернул All, Industry Knowledge, Tools & Technologies

возвращает реальный список — 20 навыков на живом аккаунте — с опорой на единственный якорь на странице для каждого навыка.

Чего профильный читатель не делает: Опыт, Образование и Навыки вообще отсутствуют на странице профиля. LinkedIn подтягивает их только после её прокрутки, а этот сервер не прокручивает. Эти разделы сообщаются как UNKNOWN, никогда как ноль, а details_urls даёт вам страницу для каждого.

Третий проход, 2026-08-22. linkedin_search_jobs оставался в последнем сломанном инструментом. В строке для подтверждённого работодателя LinkedIn добавляет строку для screen-reader: «<название> с подтверждением»; при чтении по местоположению эта строка становилась company, а настоящая компания уезжала в location — так было в 5 из 14 строк в двух живых поисках.

Исправление — это не правило про конкретную строку. Поля больше не читаются как «строка 1, строка 2, строка 3», ведь любая строка, которую вставляет LinkedIn, сдвигает все поля после себя, а на одних и тех же страницах встречались «Promoted», «Apply», «Viewed», «Actively reviewing applicants», зарплатная плашка и строка о выпускниках. Теперь каждое поле закреплено за тем, что его ИДЕНТИФИЦИРУЕТ: заголовок — за текстом ссылки, которая делает строку строкой вакансии, при этом собственные скрытые копии страницы для скринридера вычитаются по количеству; компания — за доступным именем (accessible name), которое LinkedIn даёт логотипу работодателя, а логотип — это изображение, которое строка не может сдвинуть; местоположение — за списком метаданных внутри блока сущности (entity lockup), при этом сам блок находится без какого-либо имени класса: он наименьший предок ссылки, который содержит и логотип. Если поверхность не даёт ни одного из этих якорей (трекер вакансий не даёт ни одного), остаётся прежнее чтение строк подряд.

Проверено вживую на том же запросе: 7 из 7 строк совпадают с собственными элементами artdeco-entity-lockup от LinkedIn, которые исправление намеренно не использует, и на 3 из 7 этих строк был проверочный декоративный элемент. Тесты вставляют декоративный элемент, которого LinkedIn не выпускает, в каждую позицию каждой зафиксированной строки и требуют, чтобы ответ не сдвигался; контрольный вариант показывает, что та же вставка ломает поля, как только якоря убираются.

Install Server
F
license - not found
A
quality
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Sundeepg98/linkedin-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server