agentsclimarketplace

Lhremote mcp

Skill alexey-pelykh/lhremote/skills/lhremote-mcp

CLI and MCP server for LinkedHelper automation — let AI assistants control LinkedIn outreachFrom the repository description

Install
npx -y skills add alexey-pelykh/lhremote --skill lhremote-mcp

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 12 stars12 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

18.7 KB, as published. Nobody here has run it

lhremote MCP — Tool Surface & Workflow Guide

This skill teaches lhremote MCP workflow patterns, conventions, and error handling for automating LinkedHelper via Chrome DevTools Protocol (CDP).

Prerequisites

LinkedHelper must be installed locally with an active license. The MCP server connects to it via CDP on a configurable port (default: 9222).

Tool Discovery

Tools are autodiscovered via the MCP protocol handshake (tools/list). Use the MCP tool listing to see available tools and their parameters.

Workflow Patterns

Discovery Flow

Always start here when connecting to LinkedHelper for the first time in a session:

find-app → list-accounts → check-status
  1. find-app — Detect if LinkedHelper is running and get its CDP port
  2. list-accounts — See available accounts (needed for targeting)
  3. check-status — Verify instance health and database connectivity

If find-app returns nothing, use launch-app first.

Instance Lifecycle

An instance must be running before any campaign or query operations:

launch-app → start-instance → [work] → stop-instance → quit-app
  • start-instance auto-selects the account when only one exists
  • Most tools require a running instance (they will error if not started)
  • stop-instance and quit-app are separate — stop the instance before quitting the app

Collection Workflow (Primary)

The primary way to populate a campaign with targets. Discovers sources from LinkedIn pages, collects people automatically, and monitors progress:

[build search URL] → collect-people → campaign-status → campaign-start

Step 1 — Build a source URL:

Construct a LinkedIn page URL to collect from. See Building LinkedIn Search URLs for search URL construction, or use any supported source page (company people, group members, event attendees, etc.). See Source Type Reference for the full list.

Step 2 — Collect people into a campaign:

collect-people accepts a LinkedIn page URL and campaign ID. The source type is auto-detected from the URL. Collection runs asynchronously — the call returns immediately.

collect-people(campaignId, sourceUrl)

Optional parameters:

  • limit — Maximum profiles to collect
  • maxPages — Maximum pages to process
  • pageSize — Results per page
  • sourceType — Explicit source type to bypass URL auto-detection

Step 3 — Monitor collection progress:

Poll campaign-status to track collection progress. The campaign's people list grows as profiles are collected.

Step 4 — Start the campaign:

Once collection is complete (or enough people are collected), use campaign-start with the collected personIds.

Collection rate limiting: Collection respects LinkedHelper's internal pacing. For large source pages (1000+ results), use limit or maxPages to control scope. Only one collection can run at a time per instance — attempting a second returns a CollectionBusyError.

Campaign Creation & Execution

Full workflow for creating and running a campaign:

describe-actions → campaign-create → [populate targets] → campaign-start → campaign-status / campaign-statistics

Step 1 — Discover action types:

Use describe-actions to see available action types and their configuration schemas before building a campaign config.

Step 2 — Create the campaign:

campaign-create accepts YAML (default) or JSON configuration:

version: "1"
name: "Visit & Connect"
actions:
  - type: "VisitAndExtract"
    cooldownMs: 60000
    maxActionsPerRun: 10
  - type: "InvitePerson"
    config:
      message: "Hi {firstName}, I'd like to connect!"

Step 3 — Populate targets:

Three methods for adding people to a campaign:

MethodToolBest For
Collection (recommended)collect-peopleAutomated discovery from LinkedIn pages
From Listsimport-people-from-collectionReusing curated people sets across campaigns
Direct URL importimport-people-from-urlsKnown LinkedIn profile URLs

For import-people-from-urls: This is idempotent — re-importing the same person is a no-op. For bulk imports (1000+ URLs), use the CLI instead:

npx lhremote import-people-from-urls <campaignId> --urls-file <path> --cdp-port <port>

URL file: one LinkedIn profile URL per line. Get cdp-port from find-app output.

Step 4 — Start execution:

campaign-start requires both campaignId and personIds (the internal IDs, not LinkedIn URLs). It returns immediately — execution is asynchronous.

Step 5 — Monitor progress:

  • campaign-status — Real-time execution state (with optional includeResults)
  • campaign-statistics — Aggregated success/error counts per action

Campaign Action Chain Management

Campaigns contain ordered action chains. Manage them with:

  • campaign-add-action — Append an action (use describe-actions to discover types)
  • campaign-remove-action — Remove by action ID
  • campaign-reorder-actions — Reorder by providing action IDs in desired order
  • campaign-move-next — Advance specific persons to the next action

Lists Management

LinkedHelper collections (Lists) are reusable sets of people that persist across campaigns. Use them to curate target audiences and import into multiple campaigns.

CRUD operations:

create-collection → add-people-to-collection → [use in campaigns] → delete-collection
  • create-collection — Create a named List. Returns the new collectionId.
  • list-collections — List all named Lists with people counts.
  • delete-collection — Delete a List and all its people associations. Does not delete the people themselves.
  • add-people-to-collection — Add people by personIds. Idempotent — adding an already-present person is a no-op.
  • remove-people-from-collection — Remove people by personIds.

Import into campaign:

import-people-from-collection reads all LinkedIn profile URLs from a List and feeds them into a campaign. Large sets are automatically chunked.

import-people-from-collection(collectionId, campaignId)

Typical workflow:

1. Create a List                → create-collection("Senior Engineers SF")
2. Collect into it              → collect-people(campaignId, sourceUrl) or add-people-to-collection
3. Review/curate                → list-collections to verify counts
4. Import into campaign         → import-people-from-collection(collectionId, campaignId)
5. Reuse for another campaign   → import-people-from-collection(collectionId, anotherCampaignId)

Messaging Workflow

check-replies → query-messages
  • check-replies triggers LinkedHelper to fetch new replies from LinkedIn, then returns messages since a cutoff (default: last 24 hours)
  • query-messages searches the local database — use personId to filter by contact, chatId for a specific thread, or search for text search
  • scrape-messaging-history does a full scrape of all LinkedIn messages into the local database

Data Queries (No Campaign Needed)

Profile and message queries work against the local LinkedHelper database — no campaign execution required, but an instance must be running:

  • query-profile — Look up by personId (internal) or publicId (LinkedIn URL slug like jane-doe-12345)
  • query-profiles — Search by name/headline (query) or company, with limit/offset pagination

Parameter Conventions

  • cdpPort: Optional on all tools, defaults to 9222. Only change if LinkedHelper runs on a non-default port.
  • accountId: Optional when only one account exists (auto-resolved). Required when multiple accounts are configured.
  • campaignId / actionId / personId: Internal LinkedHelper integer IDs (not LinkedIn public IDs).
  • format: Campaign config format — "yaml" (default) or "json".
  • publicId: The LinkedIn profile URL slug (e.g., jane-doe-12345 from linkedin.com/in/jane-doe-12345).

Error Patterns

ErrorCauseFix
"No running LinkedHelper instances found"App not runningUse launch-app
"Failed to connect to LinkedHelper"Wrong CDP port or app crashedUse find-app to discover correct port
"Instance not running"Instance not started for accountUse start-instance
"No accounts found" / "Multiple accounts"Account resolution failedUse list-accounts, then pass explicit accountId
"Campaign not found"Invalid campaign IDUse campaign-list to find valid IDs
"Campaign start timed out"LinkedHelper unresponsiveCheck check-status, retry
"Cannot collect — instance is busy"Another collection in progressWait for current collection to finish, then retry
"Collection failed"Source URL invalid or unsupportedCheck URL against source type reference, try explicit sourceType

Action Type Reference

Use describe-actions to get full schemas. The available action types are:

TypeCategoryPurpose
VisitAndExtractpeopleVisit LinkedIn profile and extract data
InvitePersonpeopleSend connection request
MessageToPersonmessagingSend message to connection
InMailmessagingSend InMail to non-connection
CheckForRepliesmessagingCheck for new message replies
FollowengagementFollow a LinkedIn profile
EndorseSkillsengagementEndorse skills on a profile
PersonPostsLikerengagementLike posts by a person
FilterContactsOutOfMyNetworkpeopleFilter out non-connections
RemoveFromFirstConnectionpeopleRemove from first connections
DataEnrichmentcrmEnrich profile data
ScrapeMessagingHistorymessagingScrape messaging history
WaiterworkflowWait for a configured delay

Source Type Reference

collect-people auto-detects the source type from the URL. You can also pass sourceType explicitly.

Free Tier (LinkedIn Basic)

Source TypeURL PatternDescription
SearchPage/search/results/people/People search results
MyConnections/mynetwork/invite-connect/connections/Your connections list
Alumni/school/{id}/people/School alumni page
OrganizationPeople/company/{id}/people/Company people page
Group/groups/{id}/members/Group members
Event/events/{id}/attendees/Event attendees
LWVYPP/me/profile-views/Who viewed your profile
SentInvitationPage/mynetwork/invitation-manager/sent/Sent connection invitations
FollowersPage/me/my-network/followers/Your followers
FollowingPage/me/my-network/following/People you follow

Sales Navigator Tier

Source TypeURL PatternDescription
SNSearchPage/sales/search/peopleSales Navigator people search
SNListPage/sales/lists/people/Sales Navigator saved lists (people)
SNOrgsPage/sales/search/companySales Navigator company search
SNOrgsListsPage/sales/lists/company/Sales Navigator saved lists (companies)

Recruiter Tier

Source TypeURL PatternDescription
TSearchPage/talent/search/Recruiter search
TProjectPage/talent/projects/Recruiter projects
RSearchPage/recruiter/search/Recruiter Lite search
RProjectPage/recruiter/projects/Recruiter Lite projects

Building LinkedIn Search URLs for Import

To populate a campaign with targets, you need LinkedIn profile URLs. The most common source is LinkedIn's people search. You can construct search URLs programmatically and then use LinkedHelper or browser automation to collect the resulting profile URLs for import.

Basic People Search URL

Base: https://www.linkedin.com/search/results/people/?

Parameters are appended as &key=value. Faceted filters use URL-encoded JSON arrays of strings.

Encoding Rule

All faceted parameters (except keywords, firstName, lastName, title) use URL-encoded JSON arrays:

["12345"]       → %5B%2212345%22%5D
["12345","678"] → %5B%2212345%22%2C%22678%22%5D

Characters: [ → %5B, ] → %5D, " → %22, , → %2C

Parameter Reference

ParameterTypeDescription
keywordsPlain textFree-text search across all profile fields. Supports Boolean: AND, OR, NOT, "exact phrase", (grouping)
networkJSON arrayConnection degree: "F" (1st), "S" (2nd), "O" (3rd+)
geoUrnJSON arrayLocation IDs (see geo table below)
currentCompanyJSON arrayCompany IDs for current employer
pastCompanyJSON arrayCompany IDs for previous employers
schoolJSON arrayEducational institution IDs
industryJSON arrayIndustry code IDs
profileLanguageJSON arrayISO 639-1 codes: "en", "fr", "de", "es"
serviceCategoryJSON arrayService category IDs (for freelancers)
firstNamePlain textFirst name filter
lastNamePlain textLast name filter
titlePlain textJob title filter. Supports Boolean
connectionOfJSON arrayProfile hash — search within someone's connections

Common Geo URN IDs

LocationID
United States103644278
Canada101174742
United Kingdom101165590
France105015875
Germany101282230
Spain105646813
Italy103350119
Netherlands102890719
Switzerland106693272
India102713980
Australia101452733
Brazil106057199
Japan101355337
SF Bay Area90000084

Discovering IDs

To find IDs for companies, schools, industries, or locations not listed above:

  1. URL inspection: Apply the filter in LinkedIn's UI, then read the ID from the browser URL bar
  2. Company page trick: Visit a company page → click "See all jobs" → the URL contains f_C=<companyId>
  3. Network tab: Open browser DevTools → Network tab → type in a filter box → inspect the typeahead XHR request for the returned IDs

Example URLs

2nd-degree connections at Microsoft in SF Bay Area with "Senior Engineer" title:

https://www.linkedin.com/search/results/people/?currentCompany=%5B%221035%22%5D&geoUrn=%5B%2290000084%22%5D&network=%5B%22S%22%5D&title=Senior%20Engineer&origin=FACETED_SEARCH

French-speaking software industry professionals:

https://www.linkedin.com/search/results/people/?industry=%5B%224%22%5D&profileLanguage=%5B%22fr%22%5D&origin=FACETED_SEARCH

Boolean keyword search for founders or CEOs in Germany:

https://www.linkedin.com/search/results/people/?keywords=founder%20OR%20CEO&geoUrn=%5B%22101282230%22%5D&origin=FACETED_SEARCH

Sales Navigator Search URLs

Sales Navigator uses a different base URL and encoding:

  • Lead search: https://www.linkedin.com/sales/search/people?query=...
  • Account search: https://www.linkedin.com/sales/search/company?query=...

The query parameter uses a proprietary nested list syntax:

query=(filters:List((type:REGION,values:List((id:105015875,text:France,selectionType:INCLUDED)))))

This gets percent-encoded in the URL. Each filter has type, id, text, and selectionType (INCLUDED or EXCLUDED).

Common Sales Navigator filter types and IDs:

Filter TypeExample IDs
FUNCTION8 (Engineering), 13 (IT), 19 (Product Management)
SENIORITY_LEVEL220 (Director), 300 (VP), 310 (CXO), 320 (Owner/Partner)
REGIONSame geo IDs as basic search
INDUSTRYSame industry IDs as basic search
CURRENT_COMPANYCompany IDs (may use urn:li:organization:<id> format)
COMPANY_HEADCOUNTB (1-10), C (11-50), D (51-200), E (201-500), F (501-1000), G (1001-5000)

Search-to-Campaign Workflow

1. Build search URL    → construct URL with desired filters (see above)
2. Collect into campaign → collect-people(campaignId, sourceUrl) — automated, no browser needed
3. Monitor progress    → poll campaign-status until collection completes
4. Start campaign      → campaign-start with collected person IDs

This replaces the manual browser-based workflow. collect-people handles page navigation and profile extraction internally.

Alternative (manual URL import):

If you already have a list of LinkedIn profile URLs from another source:

1. Import to campaign → import-people-from-urls (MCP or CLI)
2. Start campaign     → campaign-start with imported person IDs

LinkedIn limits search results to ~2,500 per query. For larger target lists, split the search into smaller segments (e.g., by sub-region or industry) so each sub-query stays under the limit.

Rate Limiting

LinkedIn enforces undisclosed rate limits. Exceeding them triggers warnings or account restrictions that are difficult to reverse.

Campaign Actions

ParameterRecommended
Daily safe volume100–200 visits/day
Conservative start50/day, scale up after validation
Cooldown between visits60s minimum

Start conservative. LinkedIn warnings are easier to prevent than to recover from. Configure cooldownMs (60000–90000) and maxActionsPerRun (5–10) on campaign actions accordingly.

Collection Operations

ParameterRecommended
Profiles per collection runUse limit to cap at 1000 per run
Pages per runUse maxPages to limit pagination depth
Concurrent collections1 (enforced — CollectionBusyError if exceeded)
Between collection runsWait for one to complete before starting another

Collection uses LinkedHelper's internal pacing. For large source pages, limit scope with limit or maxPages rather than collecting everything at once.

Common Pitfalls

PitfallCorrect Approach
Hardcoding CDP portRead from find-app or launch-app output each session — the port is dynamic
Skipping startup sequenceAlways: launch-app → start-instance → operate
Bulk import via MCP tool for large listsUse CLI with --urls-file for 1000+ URLs
Starting at maximum rateStart at 50/day, scale after confirming no LinkedIn warnings
Starting a second collection while one runsWait for the first to complete — only one collection per instance
Collecting without limit on large searchesUse limit or maxPages to control scope on searches with 1000+ results
Using import-people-from-urls when collect-people worksPrefer collect-people — it handles page navigation and extraction automatically

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.