feat: add 22 new tools — profile read/edit, job apply, connections, notifications#308
feat: add 22 new tools — profile read/edit, job apply, connections, notifications#308Gabrcodes wants to merge 690 commits into
Conversation
docs: sync manifest.json tools and features with current capabilities
…ance chore(deps): lock file maintenance
Lock file already has 3.1.0 since #166; align pyproject.toml floor to prevent accidental downgrades to v2. Resolves: #190
Lock file already has 3.1.0 since #166; align pyproject.toml
floor to prevent accidental downgrades to v2.
Resolves: #190
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
This PR tightens the `fastmcp` minimum version constraint from `>=2.14.0` to `>=3.0.0` in `pyproject.toml` (and the corresponding `uv.lock` metadata), preventing any future resolver from backtracking to the incompatible v2 series. The lock file has already been pinning `fastmcp==3.1.0` since PR #166, so there is no runtime impact — this is purely a spec/metadata alignment.
- `pyproject.toml`: `fastmcp` floor raised to `>=3.0.0`
- `uv.lock`: `package.metadata.requires-dist` updated to match; the resolved package entry (`3.1.0`) is unchanged
- No upper-bound cap (`<4.0.0`) is set, which is consistent with the project's existing open-ended constraints for all other dependencies
<h3>Confidence Score: 5/5</h3>
- This PR is safe to merge — it is a pure metadata alignment with no functional or runtime impact.
- The locked version was already `3.1.0` before this PR; the only change is raising the declared floor to match. Both modified lines are trivially correct, consistent with each other, and have no side-effects on the installed environment.
- No files require special attention.
<h3>Important Files Changed</h3>
| Filename | Overview |
|----------|----------|
| pyproject.toml | Single-line change updating the `fastmcp` floor constraint from `>=2.14.0` to `>=3.0.0`, aligning with the already-resolved version in the lock file. |
| uv.lock | Auto-generated lock file metadata updated to reflect the new `>=3.0.0` specifier; the resolved `fastmcp` version (3.1.0) was already correct and unchanged. |
</details>
<h3>Flowchart</h3>
```mermaid
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["pyproject.toml\nfastmcp >=3.0.0"] -->|uv resolves| B["uv.lock\nfastmcp 3.1.0 (pinned)"]
B --> C["Installed environment\nfastmcp 3.1.0"]
D["Old constraint\nfastmcp >=2.14.0"] -. "could resolve to" .-> E["fastmcp 2.x\n(incompatible)"]
style D fill:#f9d0d0,stroke:#c00
style E fill:#f9d0d0,stroke:#c00
style A fill:#d0f0d0,stroke:#060
style B fill:#d0f0d0,stroke:#060
style C fill:#d0f0d0,stroke:#060
```
<sub>Last reviewed commit: 7d2363e</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Replace dict-returning handle_tool_error() with raise_tool_error() that raises FastMCP ToolError for known exceptions. Unknown exceptions re-raise as-is for mask_error_details=True to handle. Resolves: #185
Add logger.error with exc_info for unknown exceptions before re-raising, and add test coverage for AuthenticationError and ElementNotFoundError.
Re-add optional context parameter to raise_tool_error() for log correlation, and add test for base LinkedInScraperException branch.
Add catch-all comment on base exception branch and NoReturn inline comments on all raise_tool_error() call sites.
…mcp_constraint_to_3.0.0 refactor(error-handler): replace handle_tool_error with ToolError
Replace repeated ensure_authenticated/get_or_create_browser/ LinkedInExtractor boilerplate in all 6 tool functions with FastMCP Depends()-based dependency injection via a single get_extractor() factory in dependencies.py. Resolves: #186
Updated the get_extractor function to route errors through raise_tool_error, ensuring that MCP clients receive structured ToolError responses for authentication failures. Added a test to verify that authentication errors are correctly handled and produce the expected ToolError response.
…epends_to_inject_extractor refactor(tools): Use Depends() to inject extractor
Replace ToolAnnotations(...) with plain dicts, move title to top-level @mcp.tool() param, and add category tags to all tools. Resolves: #189
Replace ToolAnnotations(...) with plain dicts, move title to
top-level @mcp.tool() param, and add category tags to all tools.
Resolves: #189
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
This PR is a clean, well-scoped refactoring that modernises tool metadata across all four changed files to align with the FastMCP 3.x API. It introduces no functional or behavioural changes.
Key changes:
- Removes the `ToolAnnotations(...)` Pydantic wrapper in `company.py`, `job.py`, and `person.py`, replacing it with plain `dict` syntax for the `annotations` parameter — the simpler form supported by FastMCP 3.x.
- Moves `title` from inside `ToolAnnotations` to a top-level keyword argument on `@mcp.tool()`, matching the updated FastMCP 3.x decorator signature.
- Drops the now-redundant `destructiveHint=False` from all read-only tools. Per the MCP spec, `destructiveHint` is only meaningful when `readOnlyHint` is `false`, so omitting it from tools that already declare `readOnlyHint=True` is semantically equivalent.
- Adds `tags` (as Python `set` literals) to every tool for categorisation (`"company"`, `"job"`, `"person"`, `"scraping"`, `"search"`, `"session"`).
- Enriches the previously unannotated `close_session` tool in `server.py` with a title, `destructiveHint=True`, and the `"session"` tag — accurately describing its destructive nature.
The existing test suite in `tests/test_tools.py` covers all tool functions but does not assert on annotation metadata, so no test changes are required. The refactoring is consistent across all tool files and fits naturally within the project's layered registration pattern.
<h3>Confidence Score: 5/5</h3>
- This PR is safe to merge — it is a pure metadata/annotation refactoring with no changes to tool logic, inputs, outputs, or error handling.
- All changes are limited to decorator parameters (`title`, `annotations`, `tags`). The `annotations` dict values are semantically equivalent to the removed `ToolAnnotations` objects, `destructiveHint=False` is correctly dropped only for `readOnlyHint=True` tools, and the new `close_session` annotations accurately reflect its destructive nature. No business logic, scraping behaviour, or error paths were altered.
- No files require special attention.
<h3>Flowchart</h3>
```mermaid
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["@mcp.tool() decorator"] --> B{Annotation style}
B -->|Before| C["ToolAnnotations(title=..., readOnlyHint=..., destructiveHint=False, openWorldHint=...)"]
B -->|After| D["title='...' (top-level param)\nannotations={'readOnlyHint': True, 'openWorldHint': True}\ntags={'category', 'type'}"]
D --> E["person tools\n(get_person_profile, search_people)"]
D --> F["company tools\n(get_company_profile, get_company_posts)"]
D --> G["job tools\n(get_job_details, search_jobs)"]
D --> H["session tool\n(close_session)\nannotations={'destructiveHint': True}"]
```
<sub>Last reviewed commit: c5bf554</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Use lowercase dict instead of Dict, add auth validation log line
…t_lifespan_into_composable_browser_auth_lifespans refactor(server): Split lifespan into composable browser + auth lifespans
# Conflicts: # linkedin_mcp_server/server.py # linkedin_mcp_server/tools/company.py # linkedin_mcp_server/tools/job.py # linkedin_mcp_server/tools/person.py
# Conflicts: # linkedin_mcp_server/server.py # linkedin_mcp_server/tools/company.py # linkedin_mcp_server/tools/job.py # linkedin_mcp_server/tools/person.py
# Conflicts: # linkedin_mcp_server/server.py
…_timeouts feat(tools): add global 90s tool timeouts
…_jobs Extract job IDs from href attributes (the one thing innerText can't capture), scroll the job sidebar instead of the main page, and paginate through multiple result pages with dynamic offsets. Resolves: #195
- Replace custom _secure_profile_dirs/_set_private_mode with thin _harden_linkedin_tree that uses secure_mkdir from common_utils - Fix export_storage_state: chmod 0o600 after Playwright writes - Add test for export_storage_state permission hardening - Add test for no-op outside .linkedin-mcp tree - Revert unrelated loaders.py change
Harden .linkedin-mcp profile/cookie permissions
- Remove unused selector constants (_MESSAGING_THREAD_LINK_SELECTOR, _MESSAGING_RESULT_ITEM_SELECTOR, _MESSAGING_SEND_SELECTOR) - Remove dead _conversation_thread_cache (new extractor per tool call) - Add AuthenticationError handling to get_sidebar_profiles and all messaging tools - Pass CSS selector as evaluate() arg instead of f-string interpolation - Replace deprecated execCommand with press_sequentially - Guard sidebar container walk against depth-limit exhaustion - Update scrape_person docstring to document profile_urn return key - Add messaging tools to README tool-status table
LinkedIn redirects /messaging/ to the most recent thread; capture baseline_thread_id after the SPA settles so search-selected threads can be distinguished from the auto-opened one.
feat: linkedin messaging, get sidebar profiles
…IDs (#300) * fix(scraping): Respect --timeout for messaging, recognize thread URLs Remove all hardcoded timeout=5000 from the send_message flow and messaging helpers so they fall through to the page-level default set from BrowserConfig.default_timeout (configurable via --timeout). Also add /messaging/thread/ URL recognition to classify_link so conversation thread references are captured when they appear in search results or conversation detail views. Raise inbox reference cap to 30 and add proper section context labels. Resolves: #296 See also: #297 * fix(scraping): Extract conversation thread IDs from inbox via click-and-capture LinkedIn's conversation sidebar uses JS click handlers instead of <a> tags, so anchor extraction cannot capture thread IDs. Click each conversation item and read the resulting SPA URL change to build conversation references with thread_id and participant name. Before: get_inbox returned 2 references (active conversation only) After: get_inbox returns all conversation thread IDs (10+ refs) Resolves: #297 * fix(scraping): Respect --timeout across all remaining scraping methods Remove the remaining 10 hardcoded timeout=5000 from profile scraping, connection flow, modal detection, sidebar profiles, conversation resolution, and job search. All Playwright calls now use the page-level default from BrowserConfig.default_timeout. Resolves: #299 * fix: Address PR review feedback - Use saved inbox URL instead of self._page.url (P1: wrong URL after clicks) - Fix docstring to clarify 2s recipient-picker probe is intentional - Replace class-name selectors with aria-label discovery + minimal class fallback - Dedupe references after merging conversation and anchor refs
First-time uvx runs download ~77 Python packages including the 39MB patchright wheel. On slow connections, uv's default 30s HTTP timeout can cause silent failures before the server process starts. Co-authored-by: Daniel Sticker <sticker@ngenn.net> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move UV_HTTP_TIMEOUT=300 into the main uvx config example so it's the default, not an optional troubleshooting step. Fix grammar in the troubleshooting note. Co-authored-by: Daniel Sticker <sticker@ngenn.net> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: use @latest tag in uvx config for auto-updates Without @latest, uvx caches the first downloaded version forever. Adding @latest ensures uvx checks PyPI on each client launch and pulls new versions automatically. * docs: apply @latest consistently to all uvx invocations Update --login examples in README.md and docs/docker-hub.md to use linkedin-scraper-mcp@latest for consistency with the MCP config. --------- Co-authored-by: Daniel Sticker <sticker@ngenn.net>
…otifications Extends the LinkedIn MCP server from 13 to 35 tools with full profile management, job application workflow, connections export, and notification scraping. Also cherry-picks improvements from community PRs and fixes reported bugs. ## New Tools (22) ### Account & Profile Reading (4 tools) - get_my_profile: scrape own profile via /in/me/ redirect - get_my_profile_full: scrape ALL profile sections in one call - get_saved_jobs: list saved/bookmarked jobs - get_my_applications: list jobs already applied to ### Profile Editing (13 tools) - edit_profile_intro: edit name, headline, location, industry - edit_profile_about: edit the About/Summary section - add_experience: add work experience entries - add_education: add education entries - add_skill: add skills (with typeahead support) - add_certification: add certifications - add_volunteer_experience: add volunteer work - add_project: add projects - add_publication: add publications - add_course: add courses - add_language: add languages with proficiency - add_honor: add honors and awards All edit tools use LinkedIn's overlay edit forms and are marked destructiveHint for MCP client confirmation. ### Job Actions (2 tools) - apply_to_job: automate Easy Apply flow with multi-step form handling, reports missing fields when manual input is needed - save_job: bookmark a job posting ### Connections & Networking (3 tools) - get_my_connections: bulk export connections via infinite scroll (cherry-picked from PR #170 by @Desperado) - extract_contact_details: batch enrich profiles with email, phone, website, birthday from contact info overlays (PR #170) - get_connections_at_company: search 1st-degree connections filtered by company name (closes #248) ### Notifications (1 tool) - get_notifications: scrape /notifications/ page (closes #211) ## New Profile Sections (8) Added to PERSON_SECTIONS for reading via get_person_profile and get_my_profile: skills, certifications, volunteer, projects, publications, courses, recommendations, organizations. ## Bug Fixes - fix(connect): sticky navbar viewport issue — added scroll_into_view_if_needed to _open_more_menu and force=True fallback to click_button_by_text (closes #304) - fix(messaging): get_conversation fails with multiple threads — _open_conversation_by_username now tries both display name and URL slug, with clear error suggesting thread_id (closes #307) ## Cherry-picked Improvements - PR #302 (@aspectrr): multi-selector fallback for More menu button (4 selectors instead of 1) - PR #170 (@Desperado): bulk connections export with chunked rate-limit handling and structured contact parser - PR #170: network degree filter ("F"=1st, "S"=2nd, "O"=3rd+) added to search_people tool ## Tests 357 passed, 5 skipped (Windows-only), 0 failures. Updated test_fields.py and test_tools.py for new sections and network parameter. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR significantly expands the LinkedIn MCP server from 13 to 35 tools, adding profile read/edit, Easy Apply job automation, connections bulk export, and notification scraping. It also addresses previously reported bugs: the The architecture is consistent with the existing codebase — new tools delegate to new extractor methods, maintain the same tool shape ( Key remaining observations:
Confidence Score: 4/5Safe to merge after the apply_to_job dialog-close ambiguity is addressed; all other findings are style/cleanup (P2). One P1 logic defect remains: when the Easy Apply dialog closes unexpectedly mid-loop (after Next/Review/Continue), the code returns apply_failed without checking whether the application was actually submitted. A caller acting on that status could re-submit the application. The two P2 findings (dead _open_edit_form method and inline _resolve_my_username() duplication in get_my_profile) do not block merge. Previous review concerns have all been resolved. linkedin_mcp_server/scraping/extractor.py — specifically the apply_to_job step loop (lines 2637-2639) and the unused _open_edit_form helper (line 1896). Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[apply_to_job] --> B[Navigate to job page]
B --> C{Easy Apply button?}
C -->|No| D{Applied indicator?}
D -->|Yes| E[return already_applied]
D -->|No| F[return not_easy_apply]
C -->|Yes| G{confirm_apply?}
G -->|No| H[return confirmation_required]
G -->|Yes| I[Click Easy Apply]
I --> J[Wait for dialog]
J --> K[Step loop max 15]
K --> L{dialog open?}
L -->|No| M[⚠️ break → falls through to apply_failed]
L -->|Yes| N{Submit button?}
N -->|Yes| O[Click Submit]
O --> P{Page text confirms?}
P -->|Yes| Q[return applied]
P -->|No| R[return applied_unconfirmed]
N -->|No| S{Review button?}
S -->|Yes| T[Click Review → continue]
S -->|No| U{Next button?}
U -->|Yes| V[Click Next → continue]
U -->|No| W{Required empty fields?}
W -->|Yes| X[return requires_input]
W -->|No| Y{Continue button?}
Y -->|Yes| Z[Click Continue → continue]
Y -->|No| AA[return apply_failed stuck]
K -->|exhausted| AB[return apply_failed max_steps]
T --> K
V --> K
Z --> K
|
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- fix(extractor): use correct HTMLSelectElement.prototype setter for
<select> elements in _fill_field_by_label — was silently failing on
all dropdown fields (date pickers, employment type, etc.)
- fix(extractor): narrow "already applied" detection to LinkedIn's
specific UI indicators instead of matching any element containing
"Applied" — eliminates false positives on job descriptions
- fix(extractor): catch RateLimitError specifically in scrape_contact_batch
instead of all LinkedInScraperException — non-rate-limit failures no
longer abort the entire batch or set rate_limited=True
- fix(extractor): replace unreliable "Contact info" button text heuristic
for company extraction with headline-based parsing ("X at Company")
and Experience/Current section fallback
- refactor(extractor): use _resolve_my_username() helper in
edit_profile_intro and edit_profile_about instead of duplicating
the /in/me/ navigation + URL parsing logic
- fix(extractor): validate network filter in search_people — reject
invalid values with ValueError instead of silently returning empty
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- fix(extractor): resolve company name to numeric entity ID in search_connections_at_company — LinkedIn ignores plain name strings in the currentCompany URL filter. Adds _resolve_company_id() that navigates to the company page and extracts the entity URN from page metadata. Falls back to keyword search if ID can't be resolved. - fix(extractor): use native HTMLSelectElement.prototype setter in _select_dropdown_by_label for React compatibility — was using direct assignment which React may not observe - fix(extractor): distinguish confirmed vs unconfirmed submissions in apply_to_job — new "applied_unconfirmed" status when page confirmation text cannot be verified after clicking Submit - fix(extractor): remove raw innerText blobs from scrape_contact_batch responses by default — add include_raw parameter (default false) to avoid oversized responses that exceed MCP context limits Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Hey, thanks for your PR but this is really hard to review. Could you split the features in to smaller issues and prs? |
- fix(extractor): replace LinkedIn CSS class-name selectors in scrape_connections_list with innerText-based parsing — class names like .mn-connection-card__name violate project scraping rules and break silently when LinkedIn updates their CSS - fix(extractor): scroll Easy Apply button into view before clicking to avoid sticky navbar obstruction (same fix as #304) - fix(extractor): verify save state changed after clicking Save in save_job — returns save_unavailable if button didn't transition - fix(extractor): remove redundant f-string in fallback keywords Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ff58734 to
fd80f60
Compare
- fix(extractor): check page text when Easy Apply dialog closes unexpectedly — prevents misreporting submitted applications as apply_failed, which could cause duplicate submissions - refactor(extractor): remove unused _open_edit_form dead code - refactor(extractor): use _resolve_my_username() in get_my_profile instead of inline navigation + URL parsing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Too many files changed for review. ( |
|
Thanks for the feedback! You're right — this is too much for one PR. I'll close this and split it into smaller, focused PRs:
Each PR will be self-contained and independently reviewable. |
Summary
Extends the LinkedIn MCP server from 13 to 35 tools with full profile management, job application workflow, connections export, and notification scraping. Also cherry-picks improvements from community PRs and fixes reported bugs.
Bug Fixes
connect_with_personfails when Connect button is behind More menu due to sticky navbar — addedscroll_into_view_if_neededandforce=Truefallbackget_conversationfails when user has multiple threads with same person — now tries both display name and URL slug, with clear error suggestingthread_idFeature Requests Addressed
get_connections_at_companytoolget_notificationstool to scrape LinkedIn notificationsCherry-picked Community Improvements
search_peopleNew Tools Reference
get_my_profileget_my_profile_fullget_saved_jobsget_my_applicationsedit_profile_introedit_profile_aboutadd_experienceadd_educationadd_skilladd_certificationadd_volunteer_experienceadd_projectadd_publicationadd_courseadd_languageadd_honorapply_to_jobsave_jobget_my_connectionsextract_contact_detailsget_connections_at_companyget_notificationsTest plan
test_fields.pyfor new profile sectionstest_tools.pyfornetworkparameter onsearch_peoplepy_compile)ruff checkandruff formatpass