LinkedIn Sales Navigator について
AI アシスタントを使う営業チーム向けに、手動ログイン済みの Chromium ブラウザー経由で LinkedIn Sales Navigator の連絡先とアカウントを検索します。結果をローカルに保存し、ガード付きのリード補完とアウトリーチ機能を備えています。
主な機能
- 01連絡先とアカウントの検索
- 02検索結果の SQLite 保存
- 03リード補完とプロフィール閲覧
- 04アウトリーチと返信の追跡
- 05ガード付きメッセージ作成と送信
- 06JSON または CSV へのエクスポート
活用シーン
- 01Sales Navigator で見込み客を収集
- 02保存済みリードを分析用に確認
- 03返信とアウトリーチ状況を確認
- 04検索結果を後で使うために出力
MCP 対応
MCP クライアントに検索、情報補完、プロフィール、パイプライン、アウトリーチのツールを提供し、保存済みクエリのリソースと営業ワークフローのプロンプトも公開します。LinkedIn へのリクエストは、ユーザーが手動でログインした永続ブラウザーセッションを通じて実行されます。
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
ツール
6掲載時のツール一覧 · 2 週間前に更新
| パラメーター | 型 | 必須 | 説明 |
|---|---|---|---|
| pages | integer | いいえ | 今回の呼び出しで取得する、25 件単位のページ数(1-10)。続きは再度呼び出して取得します。 |
| resume | boolean | いいえ | true(デフォルト)の場合、同じ URL を使った前回の停止位置から続行します。false の場合、1 ページ目から開始します(保存済みレコードは保持され、重複が除去されます)。 |
| refresh | boolean | いいえ | true の場合、このクエリの保存済みの進捗とレコードを破棄し、1 ページ目から再取得します。 |
| search_url | string | はい | Sales Navigator の完全な URL(/sales/search/people、 /sales/search/leads、または /sales/lists/people)。 |
| include_raw | boolean | いいえ | 各レコードとともに LinkedIn の生の要素 JSON を保持します (フィールドの値がおかしい場合に便利です)。 |
詳細
LinkedIn Sales Navigator MCP サーバー
AI アシスタント(Claude Desktop、Claude Code、あらゆる MCP クライアント)から、LinkedIn Sales Navigator の連絡先とアカウントを検索できる MCP サーバーです。 お使いのマシン上で実際にログインしたブラウザーを操作し、Sales Navigator 自身の検索 API レスポンスを取得します。
この設計を採用した理由(Cookie の再生を採用しない理由)
一般的な方法、つまり li_at と JSESSIONID の Cookie をコピーして、
サーバーから HTTP リクエストとして再送すると、何度もログアウトされます。
LinkedIn は各セッションを IP、ブラウザーフィンガープリント、TLS、Cookie セット全体に基づいて判定します。
別のマシンから再送された 2 つの Cookie は、セッションが乗っ取られたように見えるため、
無効化されます。
このサーバーは逆の方法を採用しています。手動で一度だけログインする永続的な ブラウザープロファイルを保持し、その正規のセッションに処理を任せます。
MCP client (Claude) ──stdio/HTTP──> this server ──drives──> your logged-in Chromium ──> Sales Navigator
│
captures the JSON the browser
itself receives (page.on "response")
LinkedIn へのすべてのリクエストは、実際のブラウザーから送信されます。つまり、お使いの IP、 フィンガープリント、Cookie jar 全体、ブラウザーが生成する CSRF/トラッキングヘッダーを使用し、 セッションも通常どおりブラウザーによって更新されます。再送も再構築も行いません。 これによってログイン状態が維持されます。
ログイン自体を自動化することは決してありません。認証情報の入力は、ボットであることを示す強いシグナルです。 一度手動でログインすれば、プロファイルはその後も保持されます。
ツール
| ツール | 機能 |
|---|---|
search_contacts | Sales Navigator URLから人物/リードを検索します。ブラウザーでページを移動しながらページネーションを行い、レコードをSQLiteに保存して、簡単な進捗の概要を返します。 |
search_accounts | Sales Navigator URLから企業/アカウントを検索します。アカウントを対象に同じ処理を行います。 |
enrich_leads | 保存済みの連絡先検索にOpen Profile/InMailのステータスを追加します。デフォルト(method="compose")では、各リードのメッセージパネルを開き、連絡先情報やインサイト(最近の投稿、転職、共通のつながり)も記録します。オプトイン方式で、再開も可能です。Open Profile statusを参照してください。 |
fetch_lead_profiles | 深度3:メッセージ作成用の詳細なプロフィール(リードあたり約15 KB)を取得します。ENABLE_PROFILEによるオプトインが必要です。 |
get_lead_profile | 保存済みの詳細なプロフィールを1件読み込みます。ローカルのみで動作し、LinkedInへのアクセスはありません。 |
pipeline_status | 収集済み → 情報追加済み → Open Profile → プロフィール取得済み → 送信済みという、ファネル全体の状況を1つのビューで表示します。 |
reconcile_outreach | sendingのまま停滞している送信を、LinkedIn自体と照合して確定します。 |
run_outreach_batch | MCPサンプリングを使い、1回の呼び出しで複数のリード向けにメッセージを作成して送信します。同じガードが適用されます。dry_runのデフォルトはtrueです。 |
outreach_next | ライブセッション:検索結果を順に確認し、次に対象となるリードの行からメッセージパネルを開いて情報を取得し、メッセージ作成用のリードデータを返します。Open Profileではないリードと既存の会話は自動的にスキップされます。modeはdraft、supervised(30秒のカウントダウン)、またはauto(5秒)です。 |
outreach_submit | 開いているパネルに下書きを入力します。draftモードでは下書きを保存します。それ以外のモードでは、ページ内のバナーでスキップ/今すぐ送信/一時停止を選べるようカウントダウンが表示されます。カウントダウンがゼロになった時点でパネルに入力されている内容(編集内容を含む)を再確認し、send_messageと同じ2フェーズコミット、上限、重複排除を経て送信します。 |
outreach_skip | 送信せずに開いているパネルを閉じ、理由を記録します。 |
ライブセッションでコードが行うこととモデルが行うこと。 データから判断できることはすべて、トークンを消費する前にコードで判断します。ICPスクリーニング(FIT_RULES_FILE。パネルを開く前に適用されるため、対象外となったリードについてはLinkedInへのリクエストも発生しません)、Open Profileと既存スレッドの確認、リード情報の固定予算への切り詰め(概要、現在の職務、最近の投稿3件)、企業情報のブロック(LinkedIn独自のAccount IQ資料を、優先事項、課題、収益モデル、人員、ニュースに要約したもの。資料が空の場合はTinyFishのニュース(TINYFISH_API_KEY)を使用)です。オファーはリードごとではなく、セッションごとに1回送信されます。モデルが行うのはメッセージの作成だけです。
| check_replies | 受信トレイを読み、返信したリードに印を付けます。送信は行いません。 |
| next_outreach_batch | 初回メッセージの送信対象となるリードを取得します。Open Profileを優先し、すでに連絡済みのリードは除外します。読み取り専用です。 |
| send_message | メッセージを1件送信します。LinkedInに書き込みを行う唯一のツールです。デフォルトでは無効で、dry_run=trueがデフォルトです。 |
| outreach_status | ステータスとチャネル別の件数、および残りの1日あたりの上限を表示します。 |
| check_session_status | ブラウザープロフィールに有効なSales Navigatorセッションがあるかを報告します(--loginを再実行する必要があるかどうかを確認できます)。 |
| list_queries | 保存済み検索をすべて、その進捗(url_hash、status、last_page、records_count)とともに表示します。 |
| get_results | 保存済みクエリのレコードから、指定した件数(1~200件)を会話に取り込み、分析できるようにします。 |
| export_results | 保存済みクエリのレコードを、出力フォルダー内のJSONおよび/またはCSVに書き出します。 |
どちらの検索ツールも、Sales Navigatorの完全なURL(UIで検索を作成し、アドレスバーからコピー)と、pagesの件数(1~10、各ページ25件)を受け取ります。
ツールに加えて、サーバーは1つのリソース(sales-nav://queries — 保存済みクエリとその進捗を、添付可能なJSONコンテキストとして提供)と、1つのプロンプト(sales_nav_search_workflow — MCPプロンプトに対応するクライアント向けの、ステップごとの見込み客開拓手順)を公開します。
検索ツールはレコードを返しません
これは意図的な仕様であり、最も驚かれる点でもあります。レコードは SQLite に保存され、ツールが返すのはステータスオブジェクトだけです。そのため、250 件のスクレイピングを行っても、モデルのコンテキストに 250 件分のデータが流れ込むことはありません。
{
"url_hash": "a6ca46c9365bce93",
"scraper_type": "contacts",
"status": "paused", // new | in_progress | paused | complete
"new_records_this_call": 25,
"total_records": 25,
"total_available": 11897313,
"pages_fetched": 1,
"last_page": 1,
"next_page": 2, // null once exhausted
"raw_dir": null, // set when include_raw=true
"suggestion": "Saved 25 records so far (through page 1) ..."
}
データを取得するには、get_results(サンプル)または export_results(ファイル)を呼び出すか、SQLite データベースを直接読み取ってください。
パイプラインの深さ
クエリをどこまで処理するかは、呼び出しの内容ではなくクエリ自体の設定です。そのため、翌日に再開した実行でも、何のために検索結果を収集したのかが分かります。
| 深さ | エンドポイント | リードあたりのコスト | 条件 |
|---|---|---|---|
search | リード検索 | 25 件ごとに 1 ページ | 常に有効 |
open_profile | + enrich_leads | 約 240 B | ENABLE_ENRICH(デフォルトで有効) |
full | + fetch_lead_profiles | 約 15 KB | ENABLE_PROFILE(デフォルトで無効) |
search_contacts(url, pages=4, depth="open_profile")
深さはクエリに保存され、next_step とともに返されます。サーバーで有効になっていない深さを要求すると、黙って引き下げられるのではなく、その理由とともに拒否されます。そのため、デフォルトのインストール状態でリストを指定し、数千件もの完全なプロフィールを取得させることはできません。
enrich_leads と fetch_lead_profiles は、異なるプロジェクションを使って同じエンドポイントにアクセスします。意図的に分けてあるのは、画面表示用の処理ではリスト全体を確認してメッセージを送れる相手を探し、完全な取得処理では、これから連絡するリードだけを対象にするためです。これらを統合すると、連絡しないリードについても大量のペイロードを取得することになり、「最近のアクティビティ」も画面表示用のデータと同じくらい古くなってしまいます。
メッセージの送信
これは LinkedIn に書き込みを行う唯一の機能であり、ほかのすべての機能とは異なる扱いになっています。閲覧は人がブラウジングしているように見えますが、大量のメッセージ送信はその行為が何であるかを明確に示し、その影響を受けるのはコードではなくあなたのアカウントです。そのため、すべてのデフォルト設定は慎重なものになっています。
| ガード | デフォルト |
|---|---|
ENABLE_SENDING | false — 新規インストール状態では、誰にもメッセージを送信できません |
dry_run | true — 正確な下書きを返しますが、送信はしません |
ALLOW_CREDIT_SPEND | false — InMail クレジットを消費する処理は拒否されます |
SEND_DAILY_CAP | 40 件、ローリング 24 時間、すべてのキャンペーン合計 |
SEND_DELAY_MIN/MAX | 送信間隔は 45~120 秒 |
無料と有料、そしてエンリッチメントを先に行う理由
Open Profile メンバーには、InMail クレジットを消費せずにメッセージを送信できます。それ以外の相手への送信には、有限の月間予算から 1 クレジットが消費されます。作成ウィンドウにはどちらに該当するかが「Free to Open Profile」または「Use 1 of N credits」と表示されます。送信処理はこの表示を読み取り、チャネルを記録したうえで、明示的に許可されていない限り有料の送信経路を拒否します。
そのため、enrich_leads は単なるメタデータの取得にとどまらず、商業的にも重要です。next_outreach_batch はデフォルトで、Open Profile であることが確認済みのリードだけを返すため、無料のチャネルが最も利用しやすい経路になります。
同じ相手には二度送らない
アプローチの状態は lead_outreach に保存され、(member_id, campaign) をキーとします。除外はグローバルで、いずれかのキャンペーンに sent 行がある相手は、以後のすべてのバッチから除外されます。このサーバーのほかの場所にある重複排除は無駄なリクエストを防ぎますが、ここでは、別々の検索で同じ人が見つかった場合に、その人へ二度メッセージを送るのを防ぎます。検索をまたいで安定している識別子は member_id だけなので、これがキーとして使われています。
状態はバッチ単位ではなく、送信ごとに確定されます。クラッシュが起きても、LinkedIn でメッセージが配信されたのに、このサーバーには記録されないという事態が発生してはなりません。
オファーファイルはあなたのものです
サーバーが文面を生成したり、あなたが何を販売しているかを学習したりすることはありません。あなたのポジショニングは、Git で無視され、パッケージにも含まれない Markdown ファイルに保存されます。
cp offer.example.md offer.md # then edit it
echo "OFFER_FILE=./offer.md" >> .env
sales_nav_compose_message プロンプトは、そのファイルをリードのレコードおよび下書き作成ルールと組み合わせて表示します。メッセージを作成するのは、あなたの MCP クライアントのモデルです。オファーファイルが設定されていない場合、プロンプトの表示は拒否されます。
エビデンスゲート
下書き生成モデルは、根拠として使用したレコードのフィールドを返す必要があります。
{"subject": "...", "body": "...", "evidence_used": ["positions[0].title", "companyName"]}
各項目は、実際に取得したリードのレコードと照合されます。存在しないフィールドを指定すると、メッセージは送信前に拒否されます。これは、「パーソナライズ」が実際に保有するデータに基づいていることを確認する、簡単で決定論的なチェックです。たとえば、カンファレンスでの講演について触れたメッセージは、それを裏付ける情報がないため拒否されます。evidence_used が空の場合も拒否されます。これはパーソナライズではなく、テンプレートだからです。
無人バッチ処理
それ以外の部分では、役割分担を意図的に設けています。サーバーが「誰に」送るかを決め、クライアントのモデルが「何を伝えるか」を決めます。これには人間が操作するターンが必要です。デスクでの利用には適していますが、スケジュール実行には役立ちません。
run_outreach_batch は、MCP サンプリングによってこのギャップを埋めます。サーバーは下書きごとに(ctx.sample を使って)クライアントに生成を依頼するため、ここではモデルは動作せず、API キーも保持されず、このプロセスが文面を生成することもありません。行うのは、生成を依頼することだけです。
自動化しても、何も緩和されません。すべての下書きは、手書きのものと同じ送信経路を通ります。エビデンスゲート、全体の重複排除、1日の上限、無料チャネルのみを使うデフォルト設定、二相コミットが適用されます。dry_run のデフォルトも引き続き true です。解析または検証に失敗した下書きは記録されてスキップされ、送信されたり、バッチの終了を招いたりすることはありません。また、上限に達するとバッチは早期に停止します。
run_outreach_batch(url_or_hash, campaign="q3", limit=5) # drafts only
run_outreach_batch(url_or_hash, campaign="q3", limit=5, dry_run=False) # sends
モデルに渡されるリード情報は、意図的に絞られています。名前、役職、会社、在籍期間、概要、職歴です。セッショントークンを含む entity_urn は渡されません。
返信
効果を測定せずに送信するのはキャンペーンとは言えません。また、すでに返信した相手へのフォローアップは、フォローアップしないよりも悪い結果を招きます。check_replies は Sales Navigator の受信トレイを読み取り、返信を記録します。
check_replies は salesApiMessagingThreads を使うため、レンダリングされたテキストを解析することはありません。author が自分以外の参加者であるメッセージを返信と見なします。このペイロードについて、実際に苦労して判明した知っておくべきことが2つあります。
participantsResolutionResultsは*<urn>を同じ<urn>に対応付けます。これは解決済みのプロフィールではなく、参照です。また、includedは空で返されます。そのため、objectUrnはなく、したがってペイロードのどこにも member_id はありません。照合には、参加者の URN に埋め込まれた profileId を使います。これがentity_urnの安定した部分です(その後の authToken は検索の対象範囲に応じて変わります)。- 閲覧者を識別できない場合、何も分類されません。危険なのは返信を見落とすことではなく、自分が送ったメッセージをリードの返信として読み取ってしまうことです。そのため、推測するのではなく処理を拒否します。
このサーバーが送信を記録したリードのみが照合されます。手動でメッセージを送った相手との会話には手を加えません。replied は連絡済みとしてカウントされるため、返信によって初回接触が重複することはありません。
一連の流れ
enrich_leads -> who is free to message
next_outreach_batch -> who is eligible, never-contacted
get_results -> the lead's own words
sales_nav_compose_message prompt -> draft
send_message dry_run=true -> review
send_message dry_run=false -> send
check_replies -> who answered
outreach_status -> where the campaign stands
再開可能な設計になっています。10件で停止し、翌日に戻って next_outreach_batch を再度呼び出せば、中断したところから続行されます。
職位レベル
Lead の検索では、Sales Navigator 自身のウェブクライアントが要求する
-14 ではなく、装飾 ID
com.linkedin.sales.deco.desktop.searchv2.LeadSearchResult-16 が要求されます。-16 は厳密な上位互換で、フィールドは同一ですが、各 Lead あたり約 230 バイト分の seniorityV2s が追加されます。サーバーがブラウザーの要求内容を変更するのはここだけで、LeadSearchResult ID を含まない URL では書き換えは何も行いません。
seniorityV2s は LinkedIn 独自の職位レベル分類で、複数の値を持ちます。創業者の場合、Owner / Partner + CXO + Senior として返されます。列挙値は ID 順に並んでいます。
| ID | レベル | ID | レベル |
|---|---|---|---|
| 320 | オーナー/パートナー | 210 | 経験豊富なマネージャー |
| 310 | CXO | 200 | エントリーレベルのマネージャー |
| 300 | 副社長 | 130 | 戦略担当 |
| 220 | ディレクター | 120 | シニア |
| 110 | エントリーレベル | 100 | 研修中 |
レコードには、値が上位の職位から並ぶ全リストが seniorities に保存され、さらに、多くの呼び出し元が必要とする単一の値が seniorityTop / seniorityTopId に保存されます。行は seniorities 子テーブルに格納されます。CSV エクスポートには seniority_top、seniority_top_id、seniority_summary が含まれます。
なお、これは推定値であり、確定情報ではありません。「Director of Client Engagement」は Director + Senior として返されます。この変更より前に取得した Lead には職位レベルがなく、列は誤った値ではなく空欄になります。
Open Profile のステータス
Sales Navigator の検索ペイロードには openLink フィールドが含まれていますが、これは落とし穴です。プレミアム会員を含め、すべてのリードで false になります。実際には意味のない飾りです。そのため、このサーバーでは一切公開しません。全員について「Open Profile ではない」と断定するかのような列を公開するより、何も公開しないほうがよいからです。
実際のフラグは memberBadges.openLink で、プロフィールエンドポイントからのみ取得できます。リードごとに 1 回リクエストする必要があります。一括取得する方法はありません。ids=List(...) のバッチを指定した salesApiProfiles は、試したどの形式でも 400 を返します。検索エンドポイントから取得する方法もありません。登録済みの decorationId のみ受け付け、自由形式の投影は受け付けません。また、登録済み ID(LeadSearchResult-13 … -16)にはこのフィールドが含まれていません。
そのため、これは独立したオプトイン型のツールです。
enrich_leads(url_or_hash, limit=50, only_missing=true, method="compose")
2 つのメソッドがあります。
method | 動作 | 取得内容 | 速度 |
|---|---|---|---|
compose(デフォルト) | 検索結果で各リードの行にある Message をクリックし、パネルに読み込まれた内容を記録して、パネルを閉じます。独自のリクエストは発行せず、リードのページも読み込みません。 | Open Profile のステータス、連絡先情報(メールアドレス、ウェブサイト、電話番号、ハンドル)、インサイト(最近の投稿や記事、転職、共通のつながり)、既存の会話のメッセージ数、会社情報(生データ) | 取得に約 2 秒 + 間隔調整の一時停止(実測でリードあたり約 11 秒) |
fetch | ページ内から、リードごとに必要最小限のプロフィールリクエストを 1 回送信します | Open Profile のステータスのみ | リードあたり約 1 秒 |
compose はクエリの検索結果ページを、行ごとに順にたどります。訪問の間に LinkedIn が検索順位を変更するため、リードは訪問したページのうち、見つかったページから取得されます。見つからなかったリードのみ、リードのページにフォールバックします(via="lead_page" を指定するとこの経路が強制され、処理が遅くなり、実際にプロフィールを閲覧します)。パネルを閉じる際は必ず「Close conversation」ボタンを使用します。フィールドへの入力や送信ボタンの操作は一切行いません。
クエリ全体を処理する前に、小さな limit でサンプルを取得してください。どちらのメソッドも再開可能で冪等です。処理済みのリードはスキップされ、失敗したものは未処理のまま再試行されるため、繰り返し呼び出すとクエリの処理が完了します。compose による取得に失敗しても、以前の正常な結果が上書きされることはありません。
データの保存先。 拡充データは専用の lead_enrichment テーブルに書き込まれ、leads.raw_json や正規化されたレコードには書き込まれません。これには 2 つの理由があります。
iter_recordsは読み込み時にすべてのレコードをraw_jsonから再生成するため、それ以外の場所に書き込まれたデータは気づかれずに破棄されます。また、raw_jsonに書き込むと、normalize.pyが依存する「生データは LinkedIn が送信した内容そのもの」という不変条件が壊れます。- このテーブルは、検索ごとに変わる認証トークンが埋め込まれた
entity_urnではなく、検索をまたいで安定しているmember_idをキーにしています。3 つの異なる検索で見つかったリードも 1 回だけ取得され、3 つすべてで共有されます。
get_results と export_results は、このデータを結合して返します。JSON では enrichment ブロック(compose で取得した場合は contactInfo と insights のリストを含む)、CSV では open_profile/inmail_restriction/enriched_at 列に加え、取得済みのリードについて emails/websites/phones/other_contacts/latest_post/job_change/existing_thread_messages/captured_at を返します。連絡先情報とインサイトは、それぞれ専用の lead_contact_info テーブルと lead_insights テーブルに保存されます。
値が空であることは「未確認」を意味し、false と同じではありません。この 2 つを分けて扱う最大の理由は、この違いにあります。
注意点として、inmailRestriction が示すのは、相手に InMail を送信できるかどうかであり、相手の Open Profile ステータスではありません。ほとんどの場合 NO_RESTRICTION になるため、代用として使わないでください。
検索は再開可能です。 URL は url_hash にハッシュ化されます。同じ URL を再度呼び出すと、最初からではなく next_page から続行します。Sales Navigator は、total_available の報告値にかかわらず、1 回の検索を最大 100 ページ(2,500 件)に制限します。それ以上を取得するには、検索をより細かなフィルターに分け、重複排除によって結果を統合してください。
ブラウザーのライフサイクル
Chromium は初回の使用時に起動し、その後はツール呼び出しを待って常駐します。終了時のライフスパンフック、またはアイドル監視機能によって終了します。呼び出しごとに起動することはありません。毎回起動し直すと遅く、同じプロファイルに対して起動を繰り返すと、セッションに問題が生じるおそれがあります。
アイドルタイムアウト。 IDLE_BROWSER_TIMEOUT 秒間ツール呼び出しがないと(デフォルトは3600秒、つまり1時間)、ブラウザーは終了し、次の呼び出し時に再起動します。スクレイピングをしていない間は数百MBのメモリを解放でき、次の呼び出しで数秒かかる以外のコストはありません。ログイン情報はプロセス内ではなく、プロファイルディレクトリに保存されています。サーバーの稼働中ずっとブラウザーを起動しておくには、値を0に設定してください。
監視機能はツールと同じロックを取得するため、操作中にブラウザーが終了することはありません。スクレイピングに時間がかかる場合は監視機能が待機し、ロックが解放される頃にはブラウザーはすでにアイドル状態ではなくなっています。
孤立プロセス。 ライフスパンフックが実行されるのは、正常にシャットダウンした場合のみです。サーバーが強制終了したり、クラッシュしたり、stdio トランスポートが切断されたりすると、Chromium は実行されたままプロファイルをロックし続けます。その後の起動はすべて、手動でプロセスを終了するまで "Opening in existing browser session" というエラーで失敗します。
サーバーは孤立プロセスから自動で復旧できるようになりました。プロファイルのロックが原因と思われる起動エラーが発生すると、そのプロファイルディレクトリを正確に使用しているプロセスを見つけ、穏当な方法で終了してから強制終了し、一度だけ再試行します。孤立プロセスを引き継ぐことはできません。patchright は --remote-debugging-pipe を使って起動するため、接続先となるデバッグエンドポイントがないからです。ただし、セッションはプロセスではなくディスク上のプロファイルに保存されているため、失われるものはありません。
照合には、解決済みのプロファイルパスがコマンドラインに文字列として含まれていることと、実行ファイルがブラウザーらしいことの両方が必要です。普段使っている Chrome、その他の自動化ブラウザー、パスに言及しているだけのプロセスは対象になりません。判定に確信が持てない場合は、何も照合しません。
セットアップ
PyPI からインストールする場合(リポジトリのクローンは不要です):
uvx --from linkedin-sales-nav-mcp patchright install chromium # one-time browser download
ソースからインストールする場合:
git clone https://github.com/nick-choudhary/linkedin-sales-nav-mcp
cd linkedin-sales-nav-mcp
uv sync
uv run patchright install chromium # one-time browser download
cp .env.example .env # optional; defaults are fine on your machine
1. 一度だけログインする
uvx linkedin-sales-nav-mcp --login # PyPI install
# or, from a clone: uv run linkedin-sales-nav-mcp --login
ブラウザーウィンドウが開きます。LinkedIn にサインインし、Sales Navigator を開いて、2FA やチェックポイントなどがあれば完了してください。サーバーがサインイン済みセッションを検出してプロファイルを保存し、終了します。
2. サーバーを起動する
uvx linkedin-sales-nav-mcp # stdio, PyPI install
# or, from a clone: uv run linkedin-sales-nav-mcp
Claude Desktop/Claude Code の設定
PyPI からインストールした場合:
{
"mcpServers": {
"sales-navigator": {
"command": "uvx",
"args": ["linkedin-sales-nav-mcp"]
}
}
}
クローンからインストールした場合:
{
"mcpServers": {
"sales-navigator": {
"command": "uv",
"args": ["run", "--project", "/path/to/linkedin-sales-nav-mcp", "linkedin-sales-nav-mcp"]
}
}
}
設定にシークレットは含まれません。セッションはブラウザープロファイルに保存されます。
--directory ではなく --project を使用してください。 どちらも uv にリポジトリを指定しますが、--directory は作業ディレクトリをリポジトリに変更するため、エクスポートが作業中のプロジェクトではなくリポジトリに出力されます。--project は作業ディレクトリを変更しません。これは、以下のエクスポート先の構成で想定されている動作です。
すべてのプロジェクトで一度だけインストールする
各設定でリポジトリのパスを指定するのは手間がかかります。代わりに、コマンドを PATH にインストールします。
uv tool install linkedin-sales-nav-mcp # from PyPI
# or: uv tool install /path/to/linkedin-sales-nav-mcp (from a clone)
そうすれば、各プロジェクトの設定は次のとおりです。
{
"mcpServers": {
"sales-navigator": {
"command": "linkedin-sales-nav-mcp"
}
}
}
パスもフラグも不要で、リポジトリの場所を移動しても更新は必要ありません。変更を取り込むには、--force を指定してインストールを再実行してください。
いずれの場合も、データベースは共有され、ログイン状態も引き継がれるため、新しいプロジェクトで --login を実行する必要はありません。プロジェクトごとの .mcp.json エントリだけが必要です。
同時に起動できるサーバーは1つだけ
いくつのプロジェクトに設定してもかまいませんが、同時に起動できるのは1つだけです。ブラウザープロファイルは永続的な Chromium プロファイルであり、Chromium は排他ロックを取得するため、1つ目のサーバーが稼働中に2つ目のサーバーを起動すると、ブラウザーの起動に失敗します。uv run を使う場合は、1つ目のサーバーがリポジトリの .venv も保持するため、2つ目の uv run は同期中に失敗することがあります。
環境変数
| 変数 | デフォルト値 | 用途 |
|---|---|---|
USER_DATA_DIR | ~/.linkedin-sales-nav/profile | 永続的なブラウザープロファイル |
HEADLESS | false | false = ウィンドウを表示(最も安全)。true = ヘッドレス(検出されやすい) |
CHROME_PATH | — | バンドルされた Chromium の代わりに独自の Chrome を使用します |
PROXY_SERVER | — | 自分のマシンでは空のままにします。リモートの場合は、住宅用エグジットノードでのみ使用します |
IDLE_BROWSER_TIMEOUT | 3600 | アイドル状態がこの秒数続くとブラウザーを閉じます。必要に応じて再起動します。0 の場合は開いたままにします |
NAV_TIMEOUT / CAPTURE_WAIT / LOGIN_TIMEOUT | 60 / 25 / 300 | タイムアウト(秒) |
TOOL_TIMEOUT | 600.0 | ツールごとの MCP タイムアウト(秒)。下記のペーシング予算を上回る必要があります |
PACING_ENABLED | true | ページ間に人間らしい遅延を入れます(下記を参照) |
PAGE_DELAY_MIN / PAGE_DELAY_MAX | 3.0 / 8.0 | 次のページへ進む前のランダムな滞在時間(秒) |
LONG_PAUSE_EVERY | 5 | N ページごとに長めの休憩を取ります(0 で無効) |
LONG_PAUSE_MIN / LONG_PAUSE_MAX | 20.0 / 45.0 | 休憩時間(秒) |
STATE_DIR | ~/.linkedin-sales-nav | sales_nav.db と生のキャプチャの保存場所。プロジェクト間で引き継がれます |
OUTPUT_DIR | output | JSON/CSV エクスポートの書き込み先。サーバーの実行場所からの相対パスです |
TRANSPORT / HOST / PORT / HTTP_PATH | stdio / 127.0.0.1 / 9000 / /mcp | トランスポート |
LOG_LEVEL | WARNING | DEBUG、INFO、WARNING、ERROR |
データの保存先
データにはライフサイクルが2種類あるため、ディレクトリも2つあります。
状態データは<STATE_DIR>(デフォルトでは~/.linkedin-sales-nav。ブラウザープロファイルと同じ場所)に保存されます。SQLite データベースのsales_nav.dbと、<url_hash>/raw/以下の生データのキャプチャが含まれます。これは特定のプロジェクトではなく、LinkedIn アカウントに紐づきます。そのため、どの場所からサーバーを起動しても同じデータベースが使われ、list_queriesにはすべてのフォルダーをまたいだ履歴が表示されます。
エクスポートデータはプロジェクトの成果物なので、作業ディレクトリを基準に保存先が決まります。export_resultsはJSON/CSVを<OUTPUT_DIR>/<url_hash>/(デフォルトではoutput/<url_hash>/)に書き込み、検索を実行したプロジェクトに保存します。データベースが正本であり、エクスポートは必要に応じてそこから生成されます。
1.0からのアップグレード。 以前はデータベースが起動ディレクトリを基準にした
output/sales_nav.dbに保存されていました。1.1以降は~/.linkedin-sales-nav/sales_nav.dbに保存され、古いパスからは読み込まれません。そのため、既存のoutput/sales_nav.dbは空のように見えます。sales_nav.db、sales_nav.db-wal、sales_nav.db-shm、<url_hash>/ディレクトリをまとめて移動する(-walファイルには最近の書き込みが保存されています)、削除して検索を再実行する、またはSTATE_DIR=./outputを設定して古い構成を維持してください。
スキーマバージョン2(PRAGMA user_version = 2):
| テーブル | 保存内容 |
|---|---|
queries | 検索URLごとに1行:url_hash、ステータス、last_page、total_available、records_count |
leads | 人物(34列):名前の各要素、member_id、役職、会社とcompany_id、業種、所在地、在籍期間、open_link、premium、raw_json |
accounts | 会社(16列):company_id、名前、業種、従業員数の範囲、説明、raw_json |
positions | 現在の職歴ごとに1行。2つの職務を同時に持つリードは2行になります。検索レスポンスには過去の職歴が含まれないため、保存されません |
badges | リード/アカウントのハイライトバッジ(共通のつながり、「最近転職」など) |
知っておくべきことが2つあります。
raw_jsonは常に保存されます。 LinkedInの内部APIはドキュメント化されておらず、時間とともに変更されるため、元の要素全体が各行に保存されます。ノーマライザーがフィールドを取りこぼした場合でも、再スクレイピングせずに後から復元できます。- レコードは実行をまたいで重複排除されます。
UNIQUE(url_hash, record_key)とINSERT OR IGNOREにより、再開、再実行、検索範囲の重複があっても、行が重複して作成されることはありません。
SQLiteに対応した任意のツールでクエリできます。
sqlite3 ~/.linkedin-sales-nav/sales_nav.db \
"SELECT full_name, title, company_name FROM leads LIMIT 10;"
データベースとエクスポートには、どちらも実際の個人データが含まれます。そのためoutput/は.gitignoreに含まれています。この設定は維持してください。<STATE_DIR>はデフォルトでリポジトリの外にあるため、コミットに含まれるリスクはありません。ただし、すべてのプロジェクトを通じてデータが蓄積されるため、保護すべきなのはこちらのコピーです。
ペース配分
ページは連続して取得されません。各ページの取得後、サーバーは「Next」をクリックする前にランダムな3~8秒間待機します。また、約5ページごと(±1ページの揺らぎあり)に、代わりに20~45秒の休止を挟みます。スクロールのリズムも変化し、ステップ数、距離、その間隔が一定にならないようにしています。
これは速度ではなく、アクセスの間隔を意識したものです。どれほど自然なセッションであっても、休止なしに2秒ごとにページを読み込む動作を延々と続ければ、機械的なパターンとして認識されます。そのため、10ページを取得する呼び出し全体には数分かかり、その大半は意図的な待機時間です。これは想定どおりの動作です。
PAGE_DELAY_*/LONG_PAUSE_*で調整するか、PACING_ENABLED=falseを設定すると、待機時間をすべて無効にできます(推奨しません)。大幅に増やす場合は、TOOL_TIMEOUTも引き上げてください。
これにより利用の痕跡は減りますが、見えなくなるわけではありません。 アカウントにフラグが立つ原因は利用量であり、揺らぎの設定を変えても、その日に取得したプロフィール数は変わりません。必要なデータだけを取得し、間隔を空けて、自分が所有するアカウントを使用してください。
使用例
ユーザー: ベルリンの中規模フィンテック企業で、エンジニアリング責任者を探してください。
エージェント: Sales Navigator の検索 URL を作成または取得して(貼り付けるか、URL ビルダーのスキルを使います)、次を実行します。
search_contacts( search_url="https://www.linkedin.com/sales/search/people?query=(...)", pages=2 )→
{"url_hash": "a6ca...", "new_records_this_call": 50, "total_records": 50, "next_page": 3, "status": "paused", ...}— 50 件のレコードは返信には含まれず、SQLite に保存されています。エージェント: 次に、回答に必要なデータを取得します。
get_results(url_or_hash="a6ca...", limit=25) # 推論用のサンプル export_results(url_or_hash="a6ca...", format="csv") # またはディスク上のファイル
重要な注意事項
- フィールドのマッピングはベストエフォートです。 LinkedIn の内部 sales-api JSON を取得しますが、これは文書化されておらず、時間とともに変更されます。ノーマライザー(
sales_nav_mcp/normalize.py)は、安定しているフィールドを抽出します。空に見える場合は、再スクレイピングせずに元データを確認する方法が 2 つあります。各行のraw_json列、または検索時にinclude_raw=trueを指定すると、加工されていない API レスポンス全体が<STATE_DIR>/<url_hash>/raw/に書き込まれます。それらをもとにマッパーを拡張してください。これが想定されている保守方法です。 - 「次へ」コントロールのページネーションセレクターは変更されることがあります。コードはいくつかの代替手段を試し、いずれも一致しなければ正常に停止します。ページネーションが途中で停止する場合は、
sales_nav_mcp/capture.pyの_NEXT_SELECTORSを更新してください。 - 自分のマシンで実行してください。 データセンターやクラウドの IP を使うと、この設計で回避しようとしているログアウトのリスクが再び生じます。
- LinkedIn のスクレイピングには、LinkedIn の利用規約が適用されます。自分が所有するアカウントを使い、控えめな頻度で利用してください。
開発
uv run pytest # full unit suite, no browser and no network
対象範囲:URL の検証、実際に取得した要素の形状に基づく JSON の正規化(値には合成データを使用)、SQLite ストアとエクスポート、ペーシングです。ペーシングについては、遅延の計算とキャプチャループへの組み込みの両方を、偽のページと注入したクロックを使ってテストするため、テストスイートで実際に待機することはありません。
対象外:実際のブラウザーを使った処理です。--login に続けて実際の検索を実行し、確認してください。
すべてのプッシュと PR で、同じテストスイートに加えて CI での ruff リント/フォーマットチェックが実行されます(Python 3.12–3.14 の Linux、3.13 の Windows と macOS)。また、リリースパイプラインでは、公開前にテストが再実行されます。
ライセンスと利用できる人
MIT — LICENSE を参照してください。簡単に言うと、誰でも無料で、商用かどうかを問わず、このソフトウェアを使用、複製、改変、再配布できます。唯一の要件は、複製物に著作権表示とライセンス表示を残すことです。このソフトウェアには保証がありません。
このライセンスが対象としないのは、LinkedIn との関係です。
- このプロジェクトは、LinkedIn とは提携しておらず、LinkedIn の承認やサポートも受けていません。LinkedIn 独自のウェブアプリケーションに対して、ブラウザーを自動操作します。
- LinkedIn への自動アクセスは、LinkedIn の利用規約によって制限されています。このサーバーを使用するかどうかは、あなた自身の判断と責任に委ねられます。自分が所有するアカウントを使い、利用量を控えめに保ち、アカウントが制限される可能性を受け入れてください。
- 収集するデータは、実在する人々に関する実際の個人データです。その取り扱いには、あなたとデータ主体の所在地に応じて、GDPR や CCPA などのプライバシー法が適用される場合があります。コンプライアンスへの対応はあなたの責任であり、このツールの責任ではありません。
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。