Skip to main content
Glama

linkedin-mcp

Сервер Model Context Protocol, который управляет вашим собственным вошедшим в систему сеансом LinkedIn в реальном браузере Chromium, с явным шагом подтверждения перед каждой операцией записи.


⚠️ Сначала прочтите это

Это не обычный API-клиент. Пожалуйста, прочитайте всё это до установки.

  • Он управляет реальным, вошедшим в систему браузерным сеансом от вашего имени. Сервер запускает Chromium, восстанавливает сохранённые cookie-файлы LinkedIn и нажимает те же кнопки, что нажал бы человек.

  • Он не использует официальный API LinkedIn. За ним нет OAuth-приложения, партнёрского соглашения и поддерживаемой интеграции. Это браузерная автоматизация публичного сайта.

  • Автоматизация LinkedIn таким способом действует за пределами вашего аккаунта с пользовательским соглашением LinkedIn. Условия LinkedIn запрещают скрейпинг и автоматический доступ к сайту.

  • Ваш аккаунт может получить rate-limit, быть ограниченным или заблокированным навсегда. Этот риск реален и не является гипотетическим. LinkedIn обнаруживает автоматизацию, и принудительные меры могут прийти без предупреждения и без возможности обжаловать.

  • Cookie-файлы вашего сеанса хранятся на этой машине. После успешного входа Playwright записывает файл storageState на локальный диск. Любой, кто может прочитать этот файл, сможет действовать на LinkedIn от вашего имени.

  • Использование происходит полностью на риски владельца аккаунта. Здесь нет никаких гарантий, явных или подразумеваемых, и нет возможности восстановить аккаунт после блокировки.

Из вышеперечисленного следует три проектных решения, которые невозможно настроить:

  1. Один аккаунт, один пользователь. Сервер имеет один каталог состояния с одним сохранённым сеансом. Нет поддержки нескольких аккаунтов и нет понятия «пользователи» — это локальный инструмент для человека, сидящего за клавиатурой.

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

  3. Сервер никогда не будет решать CAP-КА. Сервер не будет вводить ваш пароль, не будет проходить 2FA-запрос и не будет пытаться обойти контроль безопасности. Когда LinkedIn вызывает защитную проверку, сервер останавливается и просит вас пройти её в обычном браузере.

Если что-то из этого неприемлмо для вашего аккаунта, не устанавливайте этот сервер.


Related MCP server: remote-mcp-linkedin

Как поддерживать функ "подтверждение перед выполнением"

Каждый инструмент записи (linkedin_create_post, linkedin_send_connection_request, linkedin_send_message, linkedin_apply_to_job) — это ракопожатие из двух вызовов:

Вызов 1 — предпросмотр. Вызовите инструмент с реальными аргументами и без confirm. Сервер проверяет страницу, выясняет, что именно произошло бы, и возвращает обработчик предпросмотра. Ничего не отправляется, и ваша дневная квота не тратится.

{
  "profileUrl": "https://www.linkedin.com/in/dana-whitfield-example/",
  "note": "Hi Dana — we both worked on the OpenTelemetry collector. Would like to connect."
}

Вызов 2 — подтверждение. Повторяте тот же вызов с указанием confirm: true. При желании укажите previewToken, который вы получили; если сделать это, сервер проверяет, что аргументы не изменились с момента предпросмотра, и при изменении возвращает confirmation_mismatch.

{
  "profileUrl": "https://www.linkedin.com/in/dana-whitfield-example/",
  "note": "Hi Dana — we both worked on the OpenTelemetry collector. Would like to connect.",
  "previewToken": "9f2c41ab77e05d13",
  "confirm": true
}

Обёртка предпросмотра выглядит так (реальный предпросмотр linkedin_send_connection_request):

{
  "status": "preview",
  "action": "linkedin_send_connection_request",
  "executed": false,
  "confirmationRequired": true,
  "summary": {
    "profileUrl": "https://www.linkedin.com/in/dana-whitfield-example/",
    "name": "Dana Whitfield",
    "headline": "Staff Engineer, Observability",
    "note": "Hi Dana — we both worked on the OpenTelemetry collector. Would like to connect.",
    "noteLength": 88,
    "notePreview": "Hi Dana — we both worked on the OpenTelemetry collector. Would like to connect.",
    "connectionDegree": 2,
    "connectionDegreeLabel": "2nd",
    "connectPathway": "direct",
    "alreadyPending": false,
    "wouldSucceed": true
  },
  "previewToken": "9f2c41ab77e05d13",
  "quota": {
    "action": "connectionRequests",
    "used": 3,
    "cap": 20,
    "remaining": 17,
    "resetsAt": "2026-08-25T07:00:00.000Z",
    "allowed": true
  },
  "warnings": [],
  "howToConfirm": "Nothing has been sent to LinkedIn yet. To execute, re-issue the exact same `linkedin_send_connection_request` call with `confirm: true` (optionally echoing `previewToken: \"9f2c41ab77e05d13\"` so the arguments are verified as unchanged)."
}

Замечания по чтению предпросмотра:

  • executed: false и confirmationRequired: true присутствуют всегда в предпросмотре. Результат выполнения вместо них содержит "status": "executed", "executed": true и объект result.

  • В warnings сервер сообщает о всех волнующих его событиях — усечённый пост, слишком длинное примечание к запросу на соединение, исчерпанная дневная квота, режим dry-run.

  • Если предпросмотр говорит, что вызов не может быть успешным (пользователь уже в контактах, приглашение уже было отправлено, нет Controls для Connect, вы уже 1-й степени связи), howToConfirm прямо это указывает, и подтверждение будут отклонено с invalid_input без расходования квоты.

  • previewToken — это короткий дайджест имени действия и канонизированной копии полезной нагрузки. Это защита от расхождений, а не защитный токен.


Требования

  • Node.js 20 или новее ("engines". { "node": ">=20" }).

  • Desktop-окружение с возможностью открыть видимое окно браузера — интерактивный вход без него невозможен.

  • Собранная для Playwright сборка Chromium (устанetype ниже).

Установка

Установите зависимости.

npm install

Скачайте Chromium-сборку, которую использует Playwright:

npx playwright install chromium

Создайте файл окружежра (кажда переменная в нёмля, и он не хдранит учётные аднные):

cp .env.example .env

Создайте файл конфигурации (только дневные лимиты и таймаууты):

cp config.example.json config.json

Скомпилируйте TypeScript в папку dist/:

npm run build

Первый запуск, по порядку

Ничто здесь не изменяет ваш аккаунт LinkedIn до самого последнего шага.

  1. npm install

  2. npx playwright install chromium

  3. cp .env.example .env and cp config.example.json config.json — оба необязательны, и ни один из них не содержит учётные данные

  4. npm run build

  5. npm test — 75 герметичных модульных тестов; без браузера, без сети

  6. npm run verify:dry — проверBuyет все 11 инструментов на локальных тестовых данных, чтобы вы знать, что связка работает исправно после подключения к своему аккаунту (подробности—linkedin))

  7. Зарегистрируйте сервер в своём MCP-клиенте, первым с --dry-run в args (подробности). Убедитесь, что клиент показывает 11 инструментов и что инструмент записи возвращает предпросмотр.

  8. Удуйте --dry-run из args, перезапустите клиент и вызовите linkedin_login (подробности). Этот шаг уже обращается к linkedin.com. Открывается видимое окно Chromium и вы входите вручную.

  9. Вызовите linkedin_session_status, чтобы убедиться, что сохранённый сеанс работает.

Шаг 8 — это граница. Всё до него может одатить удаление каталога.


Вход в систему

В этом проекте нет конфигурации учётных данных — это сделано намеренно. Вы входите вручную, один раз, в реальном браузере.

  1. Вызовите инструмент linkedin_login. Он не принимает аргументов.

  2. Открывается реальное видимое окно Chromium со страницей входа LinkedIn. Оно видно даже при конфигурации headless: true; интеракитивный вход всегда принудительно открывает окно браузера.

  3. Вы вводизв email и пароль, и вы проходитепроверки LinkedIn — SMS или код из аутентификатора, PIN-код из электронной почты, подтверждение устройства, CAPTCHA. Сервер не вводичит учётные данные, не читает поле пароля и не трогает элементы проверки. Он только каждые две секундыone poll, ожидая обнаружения входа в элемента навигации LinkedIn.

  4. Не спешите. Ожидание ограничено таймаутом loginTimeoutMs, по умолчанию 300000 (пять минут). Если нужно больше, повысьте его в config.json.

  5. Как только LinkedIn покажет ленту пользователя, браузерный сеанс будет сохранён в файл storageState.json в каталоге состояния, с правачном файла 0600 (доступ только на чтение и запись владельцу). Этот файл игнорируется Git.

  6. Каждый последующий вызов инструмента использует этот сохранённый сеанс заново. Вам, скорее всего, не понадобится входить несколько недель.

Проверить состояние сеанса вызовом linkedin_session_status (без аргументов). Он один раз загружает ленту и отдаёт отчёт:

{
  "valid": true,
  "lastVerified": "2026-08-24T18:42:10.114Z",
  "sessionSavedAt": "2026-08-11T09:03:55.002Z"
}

When he reports valid: false, in the result there is reason (for example, that LinkedIn redirected the feed to login page itself). No cookie or session data is ever returned neither by this tool nor any other.

Сеансы истекают. Когда он истекает, инструменты начинают завершаться с session_expired, и решение всегда одинаково: снова запустите linkedin_login и войдите вручную.


Rate limits и скорость работы

Сервер ограничивает свой собственный дневной потолок и вставляет рандомизированную задержку перед действиями в браузере. Это самая важная защита вашего аккаунт.

скопируйте config.example.jsonвconfig.json` и отредактируйте:

{
  "dailyCaps": {
    "connectionRequests": 20,
    "messages": 30,
    "posts": 5,
    "jobApplications": 10
  },
  "delayRangeMs": {
    "min": 1500,
    "max": 6000
  },
  "headless": false,
  "navigationTimeoutMs": 30000,
  "actionTimeoutMs": 15000,
  "loginTimeoutMs": 300000
}

Четыре дневных лимита

Cap

Что ограничивает

По умолчанию

connectionRequests

Приглашения отправлены через linkedin_send_connection_request

20 / день

messages

сообointmentsОтправлено mens through через

30 / день

posts

Posts published through

5 / день

jobApplications

Applications submitted by linkedlinkedin_apply−to−job

10 / day

Their behavior:

  • Лимит расходуется только при подтверждённом действии. Предпросмотрбащает текущая квота, но его не тратит; вызов, который сервер отклонил outright, также не расходует квоту.

  • Счётчики хранятся в counters.json в каталоге состояния, поэтому перезапуск сервера не обнуляет их.

  • Когда лимит исчерпан, инструмент завершается с ошибкой rate_limited вместо выполения действия. Установка лимита в 0 полностью отключает это действие.

  • Каждая обёртка предпросмотра и выполнения содержит блок quota с полями used, cap, remaining, resetsAt и allowed.

Случайная задержка

MS — это диапазон, сколько сервер спит равномерно рандомным образом перед действиями браузера — по умолчанию 1500 ms to6000 ms. Если случайность не нарушается: фиксированный интервал — это почерк машины. min может быть равно max (фиксированная задержка), однако min не должен превыстать max или загрузка конфигурации завершится с config_invalid.

Лимиты сбрасываются в локальной полуночь

Счётчики привязаы к локальной календарной дате, так что все четы лимита сбрасьваются в полуночь в собственном временном поясе — не в UTC полуночь и не по 24-часовому окотда. resetsAt в обёртке quota указывает эту следущую лоcalную полуночь, представленную как временная метка UTC ISO.

Начание с значения же, чем по-умочанию

Поставляемые по умчанют постановки — это потолок, а не рекомендация. Если ваш аккаунт новый, с мало контакми или никоlда не автоматиззовался, начинайте с намного более низких значных — например connectionRequests: 5, messages: 5, posts: 1, jobApplications: 2 — с повышинем их медленно в течение недель, слушая предупреждения LinkedIn. Всплеск активности, которая вовсе не похожа на ваш обычный паттов, — это как раз то, с чего начинаются ограничения на аккааунт.


Справочник по инструментам

11 инструментов, в порядк, в котором их регистрирует сервер.

Четыре инструмента сбора данных/выкансиях ниже (linkedin_scrape_profile, linkedin_scrape_feed, linkedin_search_searchjobs, присfrom a common сontract in src/types.ts and src/selectors.ts. Если output your client's tools/list is disagree with an argument name here, tools/list is authoritive — the server truly в provided its real schema.

linkedin_login

Opening a visible Chrome window and waits until you sign in, include any 2FA or CAPTCHA. On succss, save the session to disk. **takes no args. Not available in --dry-run **.

{}

linkedin_session_status

Сообщает, действует ли сохранённый сеанс, когда он был сохранён и когда была проведена последняя проверка. Загружает ленту один раз. Только чтение. В режиме --dry-run всегда valid: true. Не принимает аргументов.

{}

linkedin_create_post (запись — требуется подтверждение)

Публикует текстовый пост в ваш feed. Расходует дневной лимит posts.

Предпросмотр:

{
  "text": "Spent the week reading Playwright's tracing internals. Notes soon.",
  "visibility": "connections"
}

Подтверждение:

{
  "text": "Spent the week reading Playwright's tracing internals. Notes soon.",
  "visibility": "connections",
  "confirm": true
}
  • text — обязательно, от 1 до 3000 символов. Если длина превышает ~1300 симвволов, LinkedIn сворачивает пост под кнопку «see more»; предпросмотр покажет предупреждение.

  • visibility — необязательно, "public" или "connections". По умолчанию "public".

  • mediaUrl — необязательный URL. Сервер никогда не загружает удалённые мультифайлы из интернета. Передача mediaUrl вызывает предупреждение при предпросмотре и будет отклонено с invalid_input при подтверждении.

  • previewToken — необязательно; передайте его обратно, чтобы сервер свернул, что аргументы не изменились.

  • confirm — необязательный boolean; должен быть ровно true для выполнения.

linkedin_send_connection_request (запись — требуется подтверждение)

Отправляет приглашение соединиться, optionally with note. Расходует дневной лимит connectionRequests.

Предпросмотр:

{
  "profileUrl": "https://www.linkedin.com/in/dana-whitfield-example/",
  "note": "Hi Dana — we both worked on the OpenTelemetry collector. Would like to connect."
}

Подтверждение:

{
  "profileUrl": "https://www.linkedin.com/in/dana-whitfield-example/",
  "note": "Hi Dana — we both worked on the OpenTelemetry collector. Would like to connect.",
  "previewToken": "9f2c41ab77e05d13",
  "confirm": true
}
  • profileUrl — обязательное непустое поле. Полный URL профиля или короткий vanityдомен.

  • note — необязательно, не более 300 символов. Примечания длиннее 200 символов обычно max. Long note требует Premiumли плохого аккаунта, поэтому предпросмотр предупреждает при > 200.

  • previewToken, confirm — поэтому.

Отказ с invalid_input перед то, как стратить квоту, если пользовать уже является контактом первого уровня, у вас уже есть приглашение, либо страница не содержит вообще elementа For Connect.

linkedin_send_message (запись — требуется подтверждение)

Отправляет личное сообщение. Расходует квоту messages.

Предпросмотр:

{
  "profileUrlOrConversationId": "https://www.linkedin.com/in/dana-whitfield-example/",
  "text": "Thanks for the pointer to the collector RFC — that answered my question."
}

Подтверждение:

{
  "profileUrlOrConversationId": "https://www.linkedin.com/in/dana-whitfield-example/",
  "text": "Thanks for the pointer to the collector RFC — that answered my question.",
  "confirm": true
}
  • profileUrlOrConversationId — обязательный, непустой. Может быть URL/слагом профиля либо идентификатором существующей ветки переписки (как в /messaging/thread/<id>/).

  • text — обязательный, от 1 до 8000 символов.

  • previewToken, confirm — как выше.

В режиме профиля получатель обязан быть контактом первой степени; всем остальным будет отказано с кодом not_connected, и только после этого квота не тратится. Этот инструмент никогда не отправляет InMail и никогда не нажимает Enter в редакторе сообщения — он нажимает кнопку Send.

linkedin_scrape_profile

Читает один профиль и возвращает структурированный объект Profile: имя, заголовок, раздел «О себе», местоположение, степень связи, признак того, является ли это ваш собственный профиль, записи об опыте, записи об образовании и навыки. Только чтение.

{
  "profileUrl": "https://www.linkedin.com/in/dana-whitfield-example/"
}

linkedin_scrape_feed

Читает свежие публикации из вашей ленты и возвращает элементы FeedPost: имя автора и его заголовок, текст, URL публикации, количество лайков и комментариев, время публикации. Только чтение.

{
  "count": 20
}

linkedin_search_jobs

Выполняет поиск вакансий в LinkedIn и возвращает элементы JobListing: jobId, название, компанию, местоположение, является ли вакансия Easy Apply, и URL вакансии. Только чтение.

{
  "keywords": "site reliability engineer",
  "location": "Berlin, Germany",
  "easyApplyOnly": true,
  "count": 25
}

Параметр keywords обязателен. Всё остальное — по желанию:

  • location — свободно вводимое название места, сопоставляется с собственным параметром location в LinkedIn.

  • easyApplyOnly — ограничивает результаты вакансиями с функции Easy Apply (фасет Twitter). Это стоит включать каждый раз, когда вы планируете откликаться через этот сервер, поскольку linkedin_apply_to_job работает только с вариантами Easy Option.

  • datePosted — "past24h", "pastWeek" or "pastMonth".

  • experienceLevel — "intern", "entry", "associate", "midSenior", "director" or "executive".

  • remote — при true бработка только удалённых вакансий.

  • count — сколько вакансий вернуть (по умолчанию 25, максимум 100). Результаты подгружаются отложено, поэтому большое значение count означает multiple прокрутки с рандомизированной паузой между подгружениями и может занять некоторое время.

Эти четыре фасета можно передавать либо на верхнем уровне, как показано выше, либо сгруппированным в объекте filters — принимаются оба варианта, и filters автоматически выигрывает, если вы передаёте один и тот же ключ дважды:

{
  "keywords": "site reliability engineer",
  "filters": { "easyApplyOnly": true, "datePosted": "pastWeek", "remote": true },
  "count": 50
}

Карточки, которые LinkedIn отображает без пригодной вакансии id (под рекламированные места, placeholders), сразу пропускаются, а не возвращаются половинчающими, и учитываются в поле skipped ответа. Ответ также включает searchUrl, который является тем же запросом, который вы открываете и в своём сервере.

linkedin_apply_to_job (запись — требуется подтверждение)

Подаёт заявку LinkedIn Easy Apply. Расходует квоту jobApplications.

Предпросмотр:

{
  "jobId": "3912847561",
  "resumePath": "/Users/parthbansal/Documents/resume.pdf"
}

Подтверждение:

{
  "jobId": "3912847561",
  "resumePath": "/Users/parthbansal/Documents/resume.pdf",
  "confirm": true
}
  • jobId — обязательный. Обычный идентификатор вакансии, URL /jobs/view/<id>/, URL поиска с ?currentJobId= или job urn — все разрешаются в один и тот же идентификатор.

  • resumePath — необязательный абсолютный путь к файлу резюме этой машины. Отсутствующий файл вызывает ошибку file_not_found. Содержимое не логируется.

  • Вакансии, сколько передают приём внешней системе отслеживания кандидатов, завершаются ошибкой external_ravelication; вакансии без контрола Easy Apply — ошибкой not_easy_apply. Прочтите предпросмотр перед подтверждением — это единственный способ узнать заранее, на какие вопросы форма будет отвечать от вашего имени.

  • previewToken, confirm — как выше.

linkedin_list_pending_invites

Возвращает список ожидающих приглашений в виде элементов PendingInvite: имя, заголовок, URL профиля, время отправки и направление. Только чтение.

{
  "direction": "received",
  "count": 25
}
  • direction — необязательно: "received" (если вы приглашены) или "sent" (вы приглашаете). По имени операции "received".

  • count — необязательное целое число от 1 до 100. По умолчанию 25.

linkedin_list_connections

Возвращает ваши контакты в виде элементов ConnectionSummary: имя, заголовок, URL профиля и время включения в контакты. Только чтение.

{
  "count": 50,
  "query": "observability"
}
  • networkпорядок — count — необязательное целое число от 1 до 200. По умолчанию 50.

  • query — необязательно. Локальный регистронезависимый фильтр по подстроке, применяется к контактам, которые уже прочитал сервер, и не отправляется в LinkedIn как поисковый запрос.


Конфигурация MCP-клиента

Сервер говорит на JSON-RPC через STD util и должен быть запусщен вашим MCP-клиентом, а не вручную. Сначала соберите (npm run build), затем укажите клинту на dist/server.js.

Из-за того, что сервер резолвит config.json, каталог состник и fixture/ **относительно своей рабчей диретори ** — а рабойчая диретория вашего MCP-клиента обычно не этот про — стоит явно установить абсолютные путие в блок env.

Claude Code / Claude Desktop

В claude_desktop_config.json:

{
  "mcpServers": {
    "linkedin": {
      "command": "node",
      "args": ["/Users/parthbansal/Desktop/claude code/linkedin-mcp/dist/server.js"],
      "env": {
        "LINKEDIN_MCP_CONFIG": "/Users/parthbansal/Desktop/claude code/linkedin-mcp/config.json",
        "LINKEDIN_MCP_STATE_DIR": "/Users/parthbansal/Desktop/claude code/linkedin-mcp/.linkedin-mcp",
        "LINKEDIN_MCP_LOG_LEVEL": "info"
      }
    }
  }
}

Cursor

В .cursor/mcp.json:

{
  "mcpServers": {
    "linkedin": {
      "command": "node",
      "args": ["/Users/parthbansal/Desktop/claude code/linkedin-mcp/dist/server.js"],
      "env": {
        "LINKEDIN_MCP_CONFIG": "/Users/parthbansal/Desktop/claude code/linkedin-mcp/config.json",
        "LINKEDIN_MCP_STATE_DIR": "/Users/parthbansal/Desktop/claude code/linkedin-mcp/.linkedin-mcp",
        "LINKEDIN_MCP_LOG_LEVEL": "info"
      }
    }
  }
}

Generic stdio client (например, Codex CLI)

Любый клент, который запускает stdio MCP-сервер, требует три вещи: команду, аргументы и окружение:

{
  "name": "linkedin",
  "command": "node",
  "args": ["/Users/parthbansal/Desktop/claude code/linkedin-mcp/dist/server.js"],
  "env": {
    "LINKEDIN_MCP_CONFIG": "/Users/parthbansal/Desktop/claude code/linkedin-mcp/config.json",
    "LINKEDIN_MCP_STATE_DIR": "/Users/parthbansal/Desktop/claude code/linkedin-mcp/.linkedin-mcp",
    "LINKEDIN_MCP_LOG_LEVEL": "info"
  }
}

Codex CLI использует TOML в ожрсаннии JSON, но соответствие один к одному: command = "node", args = ["/Users/parthbansal/Desktop/claude code/linkedin-mcp/dist/server.js"], плюс таблица [env].

Безопасное экспериментирование

Добавьте "--dry-run" в args, чтобы зарегистрировать полностью рабочий сервер, который не может касаться вашего аккаунта:

"args": [
  "/Users/parthbansal/Desktop/claude code/linkedin-mcp/dist/server.js",
  "--dry-run"
]

В режиме сухого пуска ни один запрос не достигает linkedin.com, и не публикуется ни запись, ни приглашение, ни сообщение, ни заявка. Это правильный способ, позволяя агенту исследовать инструментальный экран в первый раз.

Другие флаги: --headless, --config <path>, --log-level <debug|infor|warning|error> и -h / --help (результат печатается в stderr, потому что stdout работает протокол). Неверные флаги — критическая ошибка: опечатка --dry-runn никогда не должна оставить у вас чувство, что вы находитесь в dry-run, когда это не так. Приоритет всюду: флаги командной строки > переменные окружения > config.json > значения по умолчанию.

Все переменные окружения документированы в .env.example; все они необязательны, и ни в одной не хранутся никакие учётные данные.


Разработка

Проверка типов без инициализации:

npm run typecheck

Запуск модульных тестов:

npm test

Пересборка и сохранение:

npm run dev

--dry-run

npm run dry-run запускает собранный сервер с --dry-run. В этом режиме браузер направлен на локальные HTML-фикстуры в fixtures/, а не на linkedin.com, и финальный клик отправки каждого действия не происходит. Если действие подтвердить, полный формат пройдёт весь путь кода — валидацию, проверку квоты, взаимодействие с диалогом — и останавливается перед выполнением. Результаты содержат dryRun: true, и предпросмотры предупреждают, что dry-run включён. Поиск linkedin_login недоступен, linkedin_session_status всегда сообщает валидность.

Встроенные фикстуры охватывают страницу профиля (в трёх вариантах: профиль второй степени с прямой кнопкой «Connect», профиль второй степени, у которого Connect находится за меню «More», и контакт первой степени с рабочим компо связью), ленту, поиск вакансий, страницу вакансии и для Easy Apply, и для внешней системы приема кандидатов, сообщения, приглашения и контактов. Разрабатывайте здесь. Нет никакой необходимости, чтобы ежедневная работа с этим сервером касалась вашего реального аккаунта.

Какой фикстурой будет разрешаться URL — решает упорядоченный список правил подстрок в src/fixtures.ts. Порядок имеет значение: /mynetwork/invite-connect/connections/ содержит /mynetwork/invite-connect и /in/ содержить /in/, поэтому конкретные правила стоят выше общин, и tests/fixtures.test.ts закрепляет этот порядок.

Проверка без LinkedIn

npm run verify:dry

Это загружает скомпилированный бинарный файл --dry-run, общается с ним по JSON-RPC по протоколу stdio так же, как реальный MCP-клиент, и прогоняет все 11 инструментов в 23 случаях — каждого читающего инструмента против своей фикстуры, каждый пишущий инструмент через полный предпросмотр → подтверждение, и важные запреты (устаревший previewToken, слишком длинное сообщение, сообщение профилю второй степени, внешняя вакансия, linkedin_login в dry-run). Проверяются также протокольные контракты: предпросмотр должен сообщать executed:false, подтверждение — executed:true, и stdout должен содержать только JSON-RPC.

Он вообще не будет выполняться, если сервер в своей строке запуска не сообщает dryRun:true, поэтому случайно сделать что-либо с вашим аккаунтом невозможно. Ему нужен рабочий Chromium: всё, кроме session_status, открывает страницу браузера. В случае любой ошибки он возвращает ненулевой код и сортирует сбои на selector/logic (сервер неверный) и harness (обвязка неверная).

Тесты

Suite запускается Vitest (tests/**/*.test.ts) и в настоящее время покрывает rate limiter (src/rateLimiter.ts), config loader (src/config.ts) и маршрутизацию фикстur (src/fixtures.ts) — 75 случаев в трёх файлах. Все три по построению «герметичны»: каждый случай запускается в новой временной директории mkdtemp, окружение передаётся явно, а не читается из process.env, внутрь инжектируются задержки через RateLimiterDeps. Они не делают сетевых запросов и не запускают браузер.

Всё, что требует браузер, находится в обзоре dry-run выше, а не в модуле, поэтому npm test выполняется меньше чем за секунду и не требует ничего, кроме node_modules.

Соглашения, о которых стоит знать перед редактированием

  • Ничто не должно писать в stdout. В stdout — это канал JSON-RPC; один зателочный байт портит фрейминг, и клиент разрывает соединение. Все диагностические сведения идут в stderr через Logger.

  • Проект построен на ESM с moduleResolution: "NodeNext", поэтому относительные импорты должны заканчиваться на .js, даже в TypeScript-исходниках.

  • src/types.ts и src/errors.ts — это общий контракт. src/selectors.ts содержит только строки и чистые функции.


Устранение неисправностей

«Я запустил и ничего не происходит» — это успех

Запустите npm start вручную и вы увидите одну строку в stderr, а затем очевидное молчание:

{"ts":"...","level":"info","msg":"linkedin-mcp ready on stdio","version":"0.1.0","tools":11,"dryRun":false,"headless":false}

Это здоровый сервер, а не зависание. MCP-studio-сервер — это не демон с портом и не командная строка, которая печатает результат и завершается. Он читает запросы из stdin, а записывает ответы stdout — после приветствия он блокируется в ожидании сообщений от клиента. Когда клиент не подключен, говорить нечего, и корректный сервер молчит, а не пишет что-то в stdout — один лишний байт там нарушил бы протокольную цепочку.

Так что npm start — это не способ использования. Зарегирируйте сервер в MCP-клиенте (ниже) и пусть клиент его запускает. Если вы хотите провериться, что он отвечает, вставьте прямо в его stdin запрос и нажмите Enter — ответ tools/list должен прийти немедленно:

printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node dist/server.js --dry-run

Нажмите Ctrl-C, чтобы остановить запущенный вручную сервер. Он завершается по SIGINT/SIGTERM и пишет shutting down.

Два связанных симптома:

  • В вашем клиенте 0 инструментов или на сервер не появляется. Почти всегда причина — неверный путь в args случаях. Используйте абсолютный путь dist/server.js, выполните npm run build и проверьте свой клиентский лог MCP — там будет stderr сервера, а в нём — config_invalid или ошибка разрешения модулей Node.

  • Вы видите строку готовности, но все инструменты падают. Проверьте, что говорит эта строка. dryRun: true означает только фикстуры, и ничего что вы сделаете, не дойдёт до LinkedIn. not_authenticated в каждом вызове означает, что сохранённой сессии нет, поэтому запустите linkedin_login.

Коды ошибок

Сбои приходят как ошибки инструментов MCP с стабильным code. Те, которые вы чаще всего можете увидеть:

verification_required — LinkedIn показал CAPTCHA, контрольную точку или authwall. Сервер нарочно останавливается здесь и никогда не пытается обойти её. Откройте linkedin.com в обычном браузере, пройдите проверку сами, затем запустите linkedin_login ещё раз. Если это повторяется, воспринимайте это как сигнал снизить дневные квоты.

session_expired — сохранённая сессия больше не проходит аутентификацию, либо LinkedIn перенаправил ленту на страницу входа. Запустите linkedin_login и снова войдите вручную. linkedin_session_status покажет конкретный reason.

selector_not_found — сервер не смог найти нужный элемент. Почти всегда это огачает, что Instagram изменил свою разметку, а не то, что вы сделали что-то неправильно. Все DOM-серекторы проекта находятся в src/selectors.ts — это единственная точка поддержки: найти нужный список кандиетов, добавить в его наalо не new selector and rebuilding. Candidates are ordered lists, so adding a selector doesn't break old ones. Running with --log-level debug will tell you which lookup failed.

rate_limited — you have reached the day limit. The error itself and the quota block tell you which limit and when it resets (next local midnight). Wait it out or raise that limit in config.json — but raising a limit is exactly the behavior that causes accounts to get restricted, so do it intentionally.

external_application — the job posting redirects candidates to an external applicant tracking system instead of LinkedIn's Easy Apply form. The server does not fill out third-party sites. Open the job in a browser and apply there.

not_connected — you tried to message someone who is not a first-degree connection. Send a connection request first, wait for it to be accepted, then write. The server will not bypass that with InMail.

Other codes you may encounter: not_authenticated (no session on disk yet — run linkedin_login), not_easy_apply, invalid_input, confirmation_mismatch (arguments changed between preview and confirm — preview again), navigation_failed, browser_error, dry_run_unsupported, config_invalid (malformed config.json, reported before launch), and file_not_found.


What it does NOT do

  • No InMail. Messages are for first-tier contacts and existing conversations only.

  • ** No external ATS responses.** Easy Apply only; anything that leaves LinkedIn is rejected.

  • No CAPTCHA solving. No 2FA automation, no bypass of checkpoints. Never.

  • No support for multiple accounts. One saved session, one account, one person at the keyboard.

  • No remote media upload. The server never fetches a URL to attach to a post.


Security and privacy

What and where is stored. Everything is on this machine in the state directory — by default <cwd>/.linkedin-mcp/, which can be overridden with LINKEDIN_MCP_STATE_DIR (or per-path with LINKEDIN_MCP_STORAGE_STATE, LINKEDIN_MCP_USER_DATA_DIR, LINKEDIN_MCP_SCREENSHOT_DIR, LINKEDIN_MCP_COUNTERS):

Path

Content

storageState.json

your LinkedIn cookies and origin storage, written in 0600 mode

chromium-profile/

directory of persistent Chromium profile

counters.json

today's local date and four action counters

screenshots/

any diagnostic screenshots, written locally only

Nothing is sent anywhere. No telemetry, no analytics, no crash reports, no “phone calls” to home. The only network destination is linkedin.com, reachable through your own browser session — and under --dry-run not even that.

Screenshots are local-only. They are written to the screenshot directory for your debugging and are never uploaded or included in tool output.

Logging is deliberately minimal. Diagnostics go to stderr. Cookies, contents of storageState, and contents of resume files are never logged or serialized. Failures log the error code rather than arguments, so message body or resume path can’t leak into a stderr captured by the client. redactConfig strips absolute paths (and our home directory) out of the perated configuration string the server logs at startup.

Gitignore guarantees. .gitignore excludes .ev and .env.* (leaving .ev.example), the entire .igore state directory, storageState.json at any depth, chromium-profile/, counters.json, screenshots/, *.log, plus node_modules/, dist/, and coverage/. Only config.example.json and your config.json remain under tracking, and they contain nothing but limit and timeoutces. Never commit storageState.json — it is a vive credential.

Treat the state directory as a secret. Anyone who can read storageState.json can act for you on LinkedIn without a password or a 2FA code. If you think it’s passed to the wrong hands, sign out of all sessions in LinkedIn’s own security settings, delete that file, and go through auth again.

Available Tools

11 tools
linkedin_apply_to_jobApply to a LinkedIn job (Easy Apply only)A
Destructive

Submits a LinkedIn Easy Apply application. jobId accepts a bare id ("3812345678"), a job URL, a ?currentJobId= URL or a urn:li:jobPosting: URN. resumePath is optional — an absolute path to a local .pdf, .doc or .docx file (never uploaded anywhere except LinkedIn itself); omit it to use whatever resume is already attached to your LinkedIn profile. answers maps question text to the answer to give; keys are matched case-insensitively and by substring, so "years of experience" answers "How many years of experience do you have?". CONFIRMATION IS REQUIRED: called without confirm: true this opens the Easy Apply modal read-only, lists every question it will be asked, flags the required ones it cannot answer in unansweredRequired, closes the modal without submitting, and returns a previewToken. Re-issue the identical call with confirm: true to actually submit. Postings that apply through an employer's own site are refused with external_application and their URL handed back for you to use by hand; postings with no Easy Apply button are refused with not_easy_apply. If a required question still cannot be answered at submit time the application is abandoned with invalid_input rather than sent incomplete — this tool never submits a partial application. Counts against the daily jobApplications cap.

NameRequiredDescriptionDefault
jobIdYes
answersNo
confirmNo
resumePathNo
previewTokenNo

TDQS

A4.8/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations indicate readOnlyHint=false, destructiveHint=true, idempotentHint=false, openWorldHint=true. The description goes beyond annotations by disclosing the two-phase confirmation requirement, the refusal reasons, and that it never submits partial applications. It also clarifies that resumePath is never uploaded anywhere except LinkedIn. While it doesn't detail every failure mode, it covers essential behavioral traits beyond the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is comprehensive yet well-structured. It front-loads the core action, then details each parameter, then explains the confirmation flow and failure modes. Every sentence adds value, and it's written in a logical order. It's long but justified given the complexity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (5 params, nested objects, validation logic) and lack of output schema, the description provides complete guidance: parameter formats, confirmation flow, refusal cases, failure handling, and quota impact. No critical information missing for an agent to call it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0%, so the description must fully explain each parameter. It does: jobId formats, resumePath optionality and accepted file types, answers matching semantics (case-insensitive, substring), confirm flag behavior, and previewToken usage. This adds significant meaning beyond the bare schema properties.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: submitting a LinkedIn Easy Apply application. It specifies the verb (submits), resource (LinkedIn Easy Apply application), and key details like jobId formats and confirmation requirement. It distinguishes itself from siblings like linkedin_search_jobs by focusing on the application action.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly explains when to use: for Easy Apply postings, and when NOT to use: external applications and non-Easy Apply postings are refused. It also clarifies the confirm flag flow (preview vs submit). Mentions alternatives implicitly (handling external URL manually). Provides clear context for when this tool is appropriate versus others.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_create_postCreate a LinkedIn postA
Destructive

Publishes a text post to the signed-in member’s LinkedIn feed. This is a two-step tool: called without confirm: true it only returns a preview (character count, line count, audience, and a previewToken) and touches nothing on LinkedIn; call it again with the same arguments plus confirm: true to actually publish. Counts against the daily posts cap. visibility defaults to "public" (Anyone); "connections" restricts the post to 1st-degree connections. LIMITATION: mediaUrl is accepted for validation but images and video are NOT supported — this server never performs its own network fetches, so it cannot download remote media, and confirming a call that sets mediaUrl is refused with invalid_input rather than silently publishing text only. Post without media, or attach the image by hand in LinkedIn afterwards. Posts over roughly 1300 characters are collapsed by LinkedIn behind "…see more" (a warning, not an error). Under --dry-run the composer is driven against a local HTML fixture and the final "Post" click is skipped.

NameRequiredDescriptionDefault
textYes
confirmNo
mediaUrlNo
visibilityNo
previewTokenNo

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already mark destructive=true, but the description adds substantial context: the two-step preview/publish mechanism, daily posts cap, visibility default, media refusal with invalid_input, 1300-character collapse warning, and dry-run behavior. This goes well beyond what annotations provide and contains no contradictions.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

While long, every sentence adds operational value: purpose first, then invocation pattern, then limitations and edge cases. There is no filler or redundant phrasing; the structure logically leads the agent through what to expect.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Covers the preview response fields, error handling for media, warning threshold, and dry-run behavior. The only notable gap is that it doesn't describe what the final publish call returns (e.g., post ID or confirmation response), which would be useful since there is no output schema. Still, this is minor relative to the volume of useful context.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema has zero descriptions, so the description carries the full burden. It explains text (content), confirm (publish gate), mediaUrl (accepted but unsupported, refused on confirm), visibility (default 'public', meaning of 'connections'), and previewToken (returned from preview). All parameters are meaningfully clarified.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: 'Publishes a text post to the signed-in member’s LinkedIn feed.' This clearly distinguishes it from siblings like send_message or scrape_profile and leaves no ambiguity about what the tool does.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explains the two-step invocation pattern (preview without confirm, publish with confirm) and the media limitation as a clear 'not supported' case with manual workaround. It doesn't explicitly mention sibling alternatives, but the usage context and when-to-publish guidance are strong.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_list_connectionsList LinkedIn connectionsA
Read-onlyIdempotent

Lists the signed-in member’s 1st-degree connections, most recently added first (LinkedIn’s default sort). count caps how many are returned (default 50, max 200) and the list is scroll-paginated until that many are loaded, the list stops growing, or an internal scroll cap is reached. query is an optional case-insensitive substring filter applied LOCALLY to the name and headline of the connections that were loaded — it is not LinkedIn’s own search, so a match that sits far down a very long list may not be reached. Read-only: takes no confirm and changes nothing. Each entry carries name, headline, canonical profile URL, and a "connected at" timestamp when LinkedIn exposes one. total is the account-wide count from the page header when it can be read (totalSource: "page-header"), otherwise the number of cards that were loaded.

NameRequiredDescriptionDefault
countNo
queryNo

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint, openWorldHint, idempotentHint, and destructiveHint, so the bar is lower. The description adds significant behavioral detail beyond that: pagination mechanics (scroll-paginated until count met, stops growing, internal cap), how the query filter is applied locally, the total count source behavior, and the per-entry fields including 'connected at' timestamp. This goes well beyond the structured hints.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense but efficient—about 5 sentences covering all important aspects. It is front-loaded with the core purpose and sorted order, then details parameters and output. Could be slightly more concise (e.g., merging some clauses) but every sentence adds value without fluff.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with only 2 optional parameters and no output schema, the description covers all necessary context: purpose, sorting, pagination, parameter semantics, output fields, total count source, and read-only nature. An agent can confidently call it and interpret results without needing additional external knowledge.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0%, so the description must fully explain both parameters. It does: count is given a default (50), maximum (200), and describes pagination; query is defined as a case-insensitive substring filter applied locally to name and headline. This fully compensates for the lack of schema descriptions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool lists the signed-in member's 1st-degree connections with a specific sort order (most recently added first). This distinguishes it from siblings like linkedin_send_connection_request (mutating) and linkedin_scrape_profile (single profile), leaving no ambiguity about what it does.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explains when to use it (listing connections) and clarifies that the query filter is local, not LinkedIn's search, which warns against misuse for large lists. However, it does not explicitly point to alternative tools for actions like sending requests or scraping profiles, though the sibling names make that implicit. Slight gap in explicit exclusions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_list_pending_invitesList pending LinkedIn invitationsA
Read-onlyIdempotent

Lists invitations that are still awaiting a decision. direction: "received" (the default) returns invitations other people sent you; direction: "sent" returns invitations you sent that have not been accepted yet. count caps how many are returned (default 25, max 100). Read-only: this never accepts, ignores or withdraws anything, and takes no confirm. Each entry carries name, headline, canonical profile URL, and a timestamp (the datetime attribute when LinkedIn provides one, otherwise the relative text such as "3 days ago"). An empty list is a normal result, not an error. total is how many invitation cards were found on the page, which can exceed returned when count is smaller.

NameRequiredDescriptionDefault
countNo
directionNo

TDQS

A4.4/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description explicitly states the tool never accepts, ignores, or withdraws anything, and takes no `confirm`, adding real behavioral context beyond the readOnlyHint annotation. It also discloses edge cases such as empty lists being normal and the difference between `total` and `returned`.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense but every sentence earns its place: purpose, parameter semantics, read-only guarantee, output format, and edge-case handling. It is front-loaded with the core action and then efficiently covers nuances without redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description fully explains what entries contain, how timestamps are represented, that an empty list is valid, and how `total` relates to `returned`. Combined with parameter coverage and side-effect disclosure, nothing critical is missing for calling this tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description carries full responsibility for explaining parameters. It thoroughly documents the `direction` enum values with defaults, `count` default and maximum, and even explains result-field meanings that relate to the parameters, fully compensating for the schema gap.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool lists 'invitations that are still awaiting a decision' and specifies the resource precisely. It also distinguishes sent vs received directions, but does not explicitly differentiate from the similar sibling tool `linkedin_list_connections`, so it falls just short of a 5.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives strong operational guidance: direction defaults, count limits, and read-only behavior. However, it never mentions when to prefer this tool over alternatives like `linkedin_list_connections` or `linkedin_send_connection_request`, leaving selection context implied rather than explicit.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_loginLog in to LinkedIn (interactive)A
Idempotent

Opens a visible Chromium window on LinkedIn’s login page and waits for the human to sign in themselves, including any 2FA or CAPTCHA step. This tool never types credentials and never reads the password field. When LinkedIn shows a signed-in feed, the browser session is saved to local disk (permissions 0600) and reused by every other tool. Takes no arguments. Not available under --dry-run.

NameRequiredDescriptionDefault

No parameters

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already indicate readOnlyHint=false and idempotentHint=true, but the description adds valuable context: it opens a visible browser, waits for human input, never types credentials, saves the session to local disk with permissions 0600, and is interactive. This goes beyond annotations and provides security-relevant details that affect agent behavior.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the key action (opens browser, waits for human) and then covers important caveats (never types, session saved, not under dry-run) in a compact set of sentences. Every sentence contributes value, and the structure is scannable.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's interactive nature and reliance on human input, the description covers the essential flow: what it does, how it completes (when feed appears, session saved), and constraints (no credential handling, file permissions). It doesn't specify failure handling, but for a login tool this is acceptable and the description is complete for an agent to invoke it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes no arguments, and the schema confirms an empty properties object. The description redundantly states 'Takes no arguments.' Since there are no parameters to document, the baseline of 4 applies, and the description adds no misleading information.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: opens a visible Chromium window on LinkedIn's login page and waits for the human to sign in. It specifies the verb (opens, waits), resource (LinkedIn login page), and explicitly distinguishes itself from siblings by emphasizing it requires human interaction and never types credentials.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description communicates when to use it: when a human must authenticate interactively, including 2FA/CAPTCHA. It also notes the session is saved and reused by other tools, implying usage before authenticated operations, and states it's unavailable under --dry-run. However, it doesn't explicitly name alternatives like linkedin_session_status for checking login status, or mention when not to use it.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_scrape_feedScrape the LinkedIn feedA
Read-onlyIdempotent

Reads the signed-in member's home feed and returns the posts as structured data: author name and headline, post text, permalink, reaction and comment counts, and a timestamp. count is how many posts to return (default 10, max 50). The feed is lazy-loaded, so reaching a high count means scrolling, and this tool pauses a randomized human-like interval between scrolls — a large count can therefore take a minute or more. Read-only: takes no confirm, consumes no daily quota, and never likes, comments or reposts; the only clicks are "…see more" expanders so post text is captured at full length. Posts are de-duplicated by permalink (LinkedIn recycles cards while scrolling) and sponsored or suggestion cards with no readable author are skipped. Fewer posts than requested is a normal result, not an error.

NameRequiredDescriptionDefault
countNo

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description substantially exceeds the annotations' coverage. It discloses the read-only nature (never likes/comments/reposts, no confirm, no quota), explains the lazy-loading with human-like pauses and the time implication for large counts, de-duplication by permalink, skipping of sponsored cards, and that fewer-than-requested results is normal. This rich behavioral detail helps the agent anticipate side effects and performance without contradicting the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Though longer than average, every sentence earns its place. The opening sentence states the core purpose, then parameter semantics, behavioral safety, de-dup/skipping logic, and the normal-result caveat are each addressed succinctly. The structure is front-loaded and contains no filler or repetition.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with one parameter and no output schema, the description covers everything an agent needs: what it returns, how it behaves, potential pitfalls (time, fewer results), and safety guarantees. There are no meaningful gaps that would prevent correct invocation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must fully compensate. It does: it explains that count is how many posts to return, defaults to 10, has a max of 50, and that a larger count involves scrolling and can take a minute or more. This goes far beyond the schema's bare integer min/max, giving the agent the context needed to choose an appropriate value.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb ('reads') and resource ('signed-in member's home feed'), and enumerates the exact output fields (author name/headline, post text, permalink, reaction/comment counts, timestamp). This clearly distinguishes it from sibling tools like linkedin_scrape_profile, which targets profiles, and linkedin_create_post, which writes.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description unambiguously communicates that this tool is for reading the home feed, which sets it apart from siblings (profiles, jobs, messaging). However, it does not explicitly name alternative tools or state conditions when this tool should be avoided (e.g., 'if you need profile data, use linkedin_scrape_profile'), leaving that inference to the agent.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_scrape_profileScrape a LinkedIn profileA
Read-onlyIdempotent

Reads a LinkedIn member profile and returns it as structured data: name, headline, about, location, connection degree, experience, education and skills. profileUrl accepts a full URL ("https://www.linkedin.com/in/john-doe"), a path ("/in/john-doe") or a bare slug ("john-doe"); omit it entirely to read the signed-in member's own profile. Read-only: takes no confirm, consumes no daily quota, and the only thing it clicks is the "…see more" expander, which reveals text already on the page. Profile sections LinkedIn hides from the viewer come back as null or [] rather than as an error — only a profile with no readable name fails (selector_not_found), because that means the page is not a profile. connectionDegree is 1, 2 or 3, and null when no badge is shown (which includes your own profile). skills is capped at 50 entries, experience at 25, education at 15.

NameRequiredDescriptionDefault
profileUrlNo

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Even though the annotations already declare readOnlyHint and idempotentHint, the description adds substantial behavioral detail: it consumes no daily quota, only clicks the '…see more' expander, returns null/[] for hidden sections, and specifies the sole error condition (selector_not_found). It also clarifies connectionDegree semantics and result caps, going well beyond the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but densely informative, with the core purpose and return fields front-loaded followed by parameter formats, behavioral notes, and edge cases. Every sentence adds operational value; no filler or tautology is present.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With one optional parameter, strong annotations, no output schema, and no nested objects, this description covers all necessary invocation details: input formats, return fields, error behavior, and limits. An agent can correctly select and call this tool without needing additional documentation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema only defines profileUrl as a string with no description, giving 0% coverage. The description fully compensates by explaining all accepted input formats, the default when omitted, and what the tool reads from that URL. This gives an agent everything needed to pass the parameter correctly.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource: 'Reads a LinkedIn member profile and returns it as structured data,' and enumerates the exact fields returned. It is clearly distinguishable from sibling tools like linkedin_scrape_feed and linkedin_search_jobs, which target different data sources.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives concrete usage context: accepted profileUrl formats (full URL, path, bare slug) and the behavior when omitted (reads the signed-in member's own profile). It does not explicitly name alternatives or when not to use this tool, but the profile-scoped purpose and sibling names make the choice reasonably clear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_search_jobsSearch LinkedIn jobsA
Read-onlyIdempotent

Searches LinkedIn job postings and returns them as structured data: job id, title, company, location, whether the posting supports Easy Apply, and a URL. keywords is required; location is a free-text place name ("London", "Remote", "New York, NY"). Four optional facets narrow the search — easyApplyOnly, datePosted ("past24h" | "pastWeek" | "pastMonth"), experienceLevel ("internship" | "entry" | "associate" | "midSenior" | "director" | "executive") and remote — each accepted either at the top level or nested inside a filters object; filters wins if you somehow pass both. count is how many postings to return (default 25, max 100); results are lazy-loaded, so a high count means scrolling with a randomized human-like pause between scrolls and can take a while. Read-only: takes no confirm and consumes no daily quota. Cards LinkedIn renders without a usable job id are skipped and counted in skipped. Pass a returned jobId to linkedin_apply_to_job, which handles Easy Apply postings only — so filter on easyApply before trying to apply.

NameRequiredDescriptionDefault
countNo
remoteNo
filtersNo
keywordsYes
locationNo
datePostedNo
easyApplyOnlyNo
experienceLevelNo

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already mark read-only and idempotent, but description adds substantial behavior: lazy-loading with randomized scroll pauses, scoring and skipping of cards without job IDs (counted in `skipped`), no confirm or daily quota, and the filter precedence rule (filters wins if both present). This exceeds annotation coverage and is genuinely useful.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Although lengthy, every sentence adds value: purpose, parameters, behavior, and downstream workflow. Front-loads the core action and returns, then details. No filler; structure is logical.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a complex tool with 8 params, nested objects, and no output schema, the description covers return fields, skipped count, lazy-loading performance, and the apply workflow. It also clarifies default/max count and filter precedence. No critical missing information for an agent to call it correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, yet the description compensates by explaining each major parameter: required keywords, free-text location examples, all four facets with exact enum values (datePosted and experienceLevel), count default/max, and the nested-versus-top-level filter hierarchy. This is more than the schema provides.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Clear verb+resource: 'Searches LinkedIn job postings' and explicitly lists returned fields (job id, title, company, location, Easy Apply support, URL). Distinguishes from siblings because no other sibling handles job search; it's the obvious entry point for discovery.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides clear context: explains how to use filters and count, and explicitly routes results to linkedin_apply_to_job for Easy Apply postings. Does not explicitly state exclusions (e.g., when not to use) but the workflow guidance is strong. Lacks a direct 'use instead of X' but that's not needed given sibling separation.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_send_connection_requestSend a LinkedIn connection requestA
Destructive

Sends one connection invitation to a member, optionally with a note. Two-call handshake: without confirm: true this only reads the profile (name, headline, connection degree, and whether a Connect control exists at all) and returns a preview plus a previewToken — nothing is clicked and no invitation is sent. Re-issue the same call with confirm: true to send. profileUrl accepts a slug ("john-doe") or any LinkedIn profile URL. note is capped at 300 characters; free accounts are limited to 200 and LinkedIn silently truncates past its own limit, so notes over 200 characters are flagged. The request is REFUSED (with invalid_input, before any quota is spent) when the member is already a 1st-degree connection, when an invitation is already pending, or when LinkedIn offers no Connect control for that profile; the preview says so up front in that case. Counts against the daily connectionRequests cap. Under --dry-run the invitation dialog is driven against a local HTML fixture and the final Send click is skipped.

NameRequiredDescriptionDefault
noteNo
confirmNo
profileUrlYes
previewTokenNo

TDQS

A5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description goes far beyond the annotations (readOnlyHint=false, destructiveHint=true) by detailing concrete side effects: no click or send without confirm, quota consumption, note truncation limits per account type, and refusal before quota is spent. This level of transparency is exemplary.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense but every sentence adds critical information. It is front-loaded with the core purpose and handshake, then layers on parameter details, refusal conditions, and quotas. No redundancy or filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (two-call handshake, refusals, dry-run, quotas, note limits), the description covers every operational aspect. Without an output schema, it explains what the preview returns and what confirm does. An agent can safely and correctly invoke this tool without further context.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0% (no descriptions in the schema), so the description carries full burden. It explains profileUrl accepts slug or URL, note has 300-char cap (200 for free accounts with silent truncation), confirm triggers the actual send, and previewToken is part of the handshake. All four parameters are clarified.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool sends a connection invitation to a member, optionally with a note, and explicitly describes the two-call handshake. It distinguishes itself from siblings like send_message or create_post by focusing on the connection request action and its preview/confirm flow.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides explicit usage instructions: the two-call pattern (preview then confirm), when requests are refused (already 1st-degree, pending invite, no Connect control), and the dry-run behavior. It clarifies when not to call with confirm:true immediately, covering usage conditions thoroughly.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_send_messageSend a LinkedIn direct messageA
Destructive

Sends one direct message, either to a 1st-degree connection (pass a profile URL or slug) or into a conversation that already exists (pass the id from its /messaging/thread// URL). Two-call handshake: without confirm: true this only reads the profile or thread and returns a preview plus a previewToken, and nothing is typed or sent. Messaging is limited to 1st-degree connections — a profile at 2nd/3rd degree (or one whose degree cannot be read) is refused with not_connected, and InMail is never attempted. Counts against the daily messages cap. Under --dry-run everything runs against local fixtures and the final Send click is skipped.

NameRequiredDescriptionDefault
textYes
confirmNo
previewTokenNo
profileUrlOrConversationIdYes

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond annotations (readOnlyHint false, destructiveHint true), the description discloses the two-call handshake, that confirm:true is required to actually send, that it counts against a daily messages cap, the dry-run behavior, and that InMail is never attempted. It also mentions the refusal condition. This is rich, actionable behavioral context that adds real value over the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense yet efficient, front-loading the core action and then packing critical details (handshake, restrictions, dry-run) without fluff. Every sentence adds necessary information and the structure guides the reader through the flow logically.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (two-call handshake, degree restrictions, dry-run mode), the description covers all essential aspects an agent needs to call it correctly: return of previewToken, refusal with not_connected, daily cap effect, and dry-run behavior. Without an output schema, it adequately explains the expected response shape.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 0% schema description coverage, the description carries the burden of explaining parameters. It explains profileUrlOrConversationId (URL/slug or thread ID), confirm (triggers the send in the second call), and previewToken (returned from the first call). It does not explicitly describe the text parameter, but its meaning is obvious as the message body. This nearly compensates for the lack of schema descriptions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'Sends one direct message' and specifies the two distinct target types (1st-degree connection via profile URL/slug, or an existing conversation via thread ID). This unambiguously distinguishes it from siblings like linkedin_create_post or linkedin_send_connection_request.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides explicit usage conditions: only 1st-degree connections, refusal with not_connected, and the two-call handshake required for sending. It explains when the tool will refuse and how confirmation works, though it does not explicitly name an alternative tool for other messaging scenarios. Still, the context is strong enough for an agent to decide when to use it.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_session_statusCheck the saved LinkedIn sessionA
Read-onlyIdempotent

Reports whether the locally saved LinkedIn session still works, when it was saved, and when it was last verified. Loads the feed once to check; no cookie or session content is ever returned. Under --dry-run this always reports valid. Takes no arguments.

NameRequiredDescriptionDefault

No parameters

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the readOnlyHint annotation, the description reveals that it 'loads the feed once to check', which implies a network call despite being read-only. It also assures that 'no cookie or session content is ever returned' and clarifies the --dry-run behavior. These details give the agent important expectations beyond annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is two sentences with no waste. The core purpose is front-loaded, followed by behavioral notes and the no-arguments statement. Every clause adds necessary information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a zero-parameter, no-output-schema tool, the description fully covers what the tool does, how it checks (loads feed), what it returns (status, save time, verification time), and edge case behavior (dry-run). Nothing an agent needs to invoke it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters, and the description explicitly states 'Takes no arguments.' This meets the baseline for no-parameter tools, and the statement removes any ambiguity, so a 4 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb ('reports') and resource ('saved LinkedIn session'), and specifies the exact outputs: whether it still works, when saved, last verified. This clearly differentiates it from action-oriented siblings like linkedin_login or linkedin_create_post.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description makes it clear this is a status check tool by stating what it reports. While no explicit alternatives or when-not-to-use are mentioned, the context of sibling tool names (all performing actions) makes the intended usage obvious. A slight deduction for not explicitly saying 'use before other LinkedIn operations'.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

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

  1. 11 tool updatesv0.1.0
    • First observedlinkedin_apply_to_job
    • First observedlinkedin_create_post
    • First observedlinkedin_list_connections
    • First observedlinkedin_list_pending_invites
    • First observedlinkedin_login
    • First observedlinkedin_scrape_feed
    • First observedlinkedin_scrape_profile
    • First observedlinkedin_search_jobs
    • First observedlinkedin_send_connection_request
    • First observedlinkedin_send_message
    • First observedlinkedin_session_status

TDQS

A4.7/5.0

Scored across 11 tools

Disambiguation5/5

Each tool targets a distinct LinkedIn action (login, session check, create post, send connection, send message, scrape profile, scrape feed, search jobs, apply to job, list invites, list connections). Even similar functions like scraping profile vs. sending connection request are clearly separated by purpose, with no two tools overlapping in behavior.

Naming Consistency5/5

All tools follow the 'linkedin_' prefix plus a snake_case verb_noun pattern (e.g., linkedin_create_post, linkedin_list_connections). Even the outlier 'linkedin_session_status' uses a consistent noun-like phrase, but the naming is uniform and predictable across the entire set.

Tool Count5/5

With 11 tools, the server covers a broad set of LinkedIn capabilities without being bloated. Each tool serves a clear, distinct purpose, from authentication and session management to content creation, messaging, scraping, job search, and application. The count is well within the ideal 3-15 range and feels appropriately scoped for a LinkedIn assistant.

Completeness4/5

The surface covers core workflows: posting, messaging, connection requests, profile/feed reading, job search, and Easy Apply. Minor gaps exist such as accepting/rejecting invitations, editing/deleting posts, or reacting to content, but the available tools handle the most common personal LinkedIn tasks without dead ends.

Maintenance

ActivitySlowing
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers