linkedin-mcp
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 — |
Только ваши данные | Ваши просмотры профиля, ваши отклики, сохранённые вакансии, ваш профиль, ваши уведомления. Никакого извлечения или сбора данных других участников. |
Чтение, кроме трёх поименованных записей | Ничего не применяется, не сохраняется, не отправляется, не постится, не одобряется, не отправляет приглашение и не редактируется. Исключения — сохранение, снятие со сохранения и отписка: по умолчанию выключены, по одному за раз, каждое подтверждается вами на основе свежего прочтённого блока и одноразового токена, который живёт две минуты. Эта строка до 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) лицензия позволила бы незнакомцам использовать его со своими аккаунтами — или с занятыми чужими аккаунтами — а в репозиторий было бы написано имя автора.
Это, прежде всего, пример для портфолио, а не инструмент. Он создан для чтения, не для развёртывания. Читать его дизайн, границы, ворота и аудиторный след — для этого и для них вс работает.
Инструмент | Что читает |
| Кто просматривал ваш профиль. Если у аккаунта есть Premium Career, доступная глубина — 365 дней; это сигнал с самым высоким намерением в поиске работы. |
| Вакансии, на которые вы откликнулись, со статусом, который показывает LinkedIn. |
| Вакансии, которые вы добавили в закладки. |
| Поиск вакансий по ключевым словам, местоположению, удалённой работе, дате публикации и уровню опыта. |
| Одна вакансия полностью — диапазон зарплаты, число откликнувшихся по данным LinkedIn, формат и тип занятости, статус найма и описание. Ничего из этого нет в карточке поиска или сохранённых вакансий. Также |
| Страницы компаний, на которые вы подписаны, с числовым id каждой — это то, по чему адресуется |
| Ваш собственный профиль: заголовок, «О себе», навыки и то, какие секции оставились. LinkedIn не подгружает Experience/Education/Skills до прокрутки страницы, поэтому эти секции возвращают UNKNOWN, а не ноль. |
| Ваш список уведомлений. |
| Есть ли живая сессия; проверка производится аутентифицированным запросом. |
| Открывает окно, в котором вы входите сами. |
| Жива ли сессия и когда она истекает — берётся из собственного cookie-хранилища браузера. Сообщает учетные данные, csrf-cookie, которая их поддерживает, долговечность и причину, почему здесь нет тихой переавторизации. |
| Завершает локальный вход, стирая cookie-хранилище этой машины. Это единственный разрушительный инструмент здесь: |
| Диагностика восстановления: есть ли Chrome, к которому сервер может подключиться? Ничего на LinkedIn не трогает. |
| Границы, настройки лимитов и флаги запуска — без чтения исходного кода. |
Related MCP server: LinkedIn MCP Server
Три инструмента, которые записывают
Инструмент | Что он делает |
| Добавляет одну вакансию в закладки. Если вызвать без |
| Та же механика, те же проверки — и он отказывается. См. ниже. |
| Отписка от одной страницы компании. Та же механика и те же пять проверок. Адресуется числовым 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— сведения нет. Это честный ответ и он существенно менее; вот это важно.
Это полезная половина: она не стоит отдельной загрузки страницы и является чистым чтением.
Но отклик не отправляет. Не потому, что подача откликов не входит в функции этого сервера, а по причинам, которые были измерены:
Процесс подачи отклика (apply FLOW) никогда не был захвачен. За все тринадцать захватов вакансий — ни одной формы, ни одного файлового ввода, ни одного диалога, ни одного скринингового вопроса и ни одного управляющего элемента, который что-то отправляет. Ничто здесь не видело того, что нужно было бы заполнять или нажимать. Это тот же стандарт, что и для
unsave_job, применённый к действию, которое заслуживает его сильнее всего.Отсюда подачу заявки нельзя отменить — ни на каком уровне подтверждения, ни при каких обстоятельствах. Отзыв заявки запрещён навсегда.
Внешняя (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.
Два побочных эффекта, которые мы говорим прямо, а не прячем
Судьёт действие, которое что-то меняет, обязано это объявить:
Открытие страницы уведомлений сбрасывает бейдж непрочитанного у LinkedIn — ровно как было бы при открытии той же страницы человеком. Это измерено, а не теоретизировано: один вызов от 2026-08-21 снял бейдж с 1 на 0, и он не возвращается. Обойти невозможно: LinkedIn помечает список просмотренным на сервере в момент отдачи страницы — значит, без изменения бейджа эта поверхность не читается. Никакого клика, скролла или пообъектного открытия нет, и вызовов, которое помечали бы прочитанным, в пакете ни одного. Единственный способ не стирать бейдж — не вызывать
linkedin_notificationsВедь бейдж уйдёт в одну из двух сторон, у каждой строки сохранено полеunread, каким LinkedIn видел его в момент чтения — единственный факт, который эта загрузка страницы уничтожает.Поиск вакансий добавляет в вашу историю недавних поисков, как если бы вы сами набрали такой запрос на сайте.
Оба про раскрыты в 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 — то есть когда соблюдение нарушено внамеренно, падающим, — прежде чем доверять реальному пакету. Проверка, которая не умеет упасть, ничего не подтверждает.
Разрешающий список навигации.
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— не пропускается по тем же обусловлю: слаг — это название должности, а текст названия — строка.Сканер исходников. Пакет прогоняется через 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.
Разрешающий список инструментов. В имени ни одного инструмента нет глагола записи, и ни в одной docstring нет утвердительного обещания что-то записать или изменить. В docstring может рассказываться о невозможном — «не имеет никакого способа что-либо добавить или удалить» — именно такое предложения должно содержать read-only инструмент; поэтому проверка, further, ищет отрицание, а не запрещает слова.
Граница запуска.
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(, пронесли её через существующие вызов и выложили с зелёным тестами. Дыра закрыта, и ровно она сейчас является тестом.
Вход в игру: cookie — это не вход в дверь
За день до этого сервера в одной экосистеме был выпущен соседний, развивающий обратный принцип: он говорил 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, который запустили вы.
Две вещи молча рушат эту схему, обе проверены на этой машине:
Chrome, открытый из панели задач, не имеет порта DevTools. «Мой браузер открыт» — недостаточно; он должен быть запущен с
--remote-debugging-port.Синглетон 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 | было | стало |
| выдавал ошибку: нельзя было прочитать имя | читает имя, заголовок, местоположение, About и фото со страницы, на которой ноль |
| ошибка на редиректе | читают |
| строки с шумом | текст screen-reader вычесть по количеству, а не по фразе, и |
навыки внутри | вернул | возвращает реальный список — 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 не выпускает, в каждую позицию каждой зафиксированной строки и требуют, чтобы ответ не сдвигался; контрольный вариант показывает, что та же вставка ломает поля, как только якоря убираются.
Maintenance
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
- AlicenseAqualityAmaintenanceEnables AI assistants to interact with LinkedIn by scraping profiles, companies, job postings, and getting personalized job recommendations using authenticated browser automation.173,204Apache 2.0
- FlicenseAqualityDmaintenanceEnables LLMs and agents to interact with LinkedIn's REST API for managing profiles, creating posts, viewing connections, and overseeing organizations.52
- AlicenseNot gradedqualityDmaintenanceEnables interaction with LinkedIn's Community Management API, allowing users to retrieve profile information and create posts via natural language.17738MIT
- AlicenseBqualityCmaintenanceEnables AI agents to manage LinkedIn profiles, posts, connections, skills, education, and certifications through the LinkedIn API.1817664MIT
Related MCP Connectors
Live LinkedIn data for AI agents: profiles, companies, jobs, posts, email finding. No account risk.
Let AI tools securely access your LinkedIn network and DMs
Search, label and export your LinkedIn saved posts, then draft, schedule and publish from them.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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