Skip to content

feat(network): add get_pending_invitations tool#447

Open
ghul0 wants to merge 4 commits into
stickerdaniel:mainfrom
ghul0:feature/446-get-pending-invitations
Open

feat(network): add get_pending_invitations tool#447
ghul0 wants to merge 4 commits into
stickerdaniel:mainfrom
ghul0:feature/446-get-pending-invitations

Conversation

@ghul0
Copy link
Copy Markdown

@ghul0 ghul0 commented May 15, 2026

Summary

Adds a new read-only MCP tool get_pending_invitations for listing pending LinkedIn network invitations (received or sent). This fills the gap noted in #446 — recruiter "Invite + note" messages live in /mynetwork/invitation-manager/ and never reach get_inbox until accepted, so an agent assisting with inbound triage cannot see them today.

What's in scope

  • New tool exposed via tools/network.py::register_network_tools with signature get_pending_invitations(ctx, limit=20, kind="received"|"sent").
  • New extractor method LinkedInExtractor.get_pending_invitations(limit, kind) that follows the get_inbox template line-for-line: _navigate_to_page -> detect_rate_limit -> _wait_for_main_text -> handle_modal_close -> _scroll_main_scrollable_region -> _expand_invitation_note_toggles -> _extract_root_content(["main"]) -> strip_linkedin_noise -> build_references -> _single_section_result.
  • Auto-expansion of truncated invitation notes via the new _expand_invitation_note_toggles helper. Uses the locale-independent data-testid="expandable-text-button" selector, injects a scoped one-shot stylesheet to re-enable pointer-events (LinkedIn ships these buttons with inline pointer-events: none), and dispatches a synthetic bubbling MouseEvent so the React handler fires. Clicked buttons are tagged with data-mcp-clicked so a second pass picks up lazy-loaded cards without re-toggling already-expanded notes.
  • Registration in server.py::create_mcp_server().
  • TestNetworkTools in tests/test_tools.py (4 cases: happy path, sent kind, invalid kind rejection, limit-bound rejection); _make_mock_extractor extended; get_pending_invitations added to both timeout-coverage tuples.
  • README tool table, docs/docker-hub.md features list, and manifest.json tools array all updated.

Intentionally out of scope

  • Accept / ignore / withdraw actions are deferred to a follow-up PR if this lands. Keeping the surface area read-only here both shrinks the diff and avoids exposing destructive operations on a surface that hasn't been exercised yet.
  • No new entry in fields.py — this is a one-page tool, not a sectioned scraper, matching get_feed / get_inbox.

Testing

  • uv run pytest: 510 passed locally. One pre-existing failure in test_browser_security.py::test_harden_linkedin_tree_noop_outside_linkedin reproduces on main before this branch with umask=002; it's environmental and unrelated.
  • uv run ruff check . --fix && uv run ruff format .: clean.
  • uv run ty check: clean.
  • uv run pre-commit run --all-files: all hooks pass.
  • Live verified end-to-end via LinkedInExtractor.get_pending_invitations(limit=5, kind="received") on a real account. Result shape conforms to the standard {url, sections, references} contract; sections text contained 12 inviter blocks with 11/11 truncated invitation notes auto-expanded (text matched pokaż mniej x11, pokaż więcej x0); references carried 12 kind: "person" entries with /in/USERNAME/ URLs.

Synthetic prompt

Add a read-only get_pending_invitations MCP tool that lists pending LinkedIn network invitations (received or sent) by navigating to /mynetwork/invitation-manager/{kind}/, mirroring the existing get_inbox extractor pattern. Before extracting, auto-expand any truncated invitation notes by dispatching a synthetic click on each data-testid="expandable-text-button" (after injecting a scoped stylesheet override for pointer-events, and marking each clicked button with data-mcp-clicked so a second pass can pick up lazy-loaded cards without re-toggling already-expanded ones). Wire it through a new tools/network.py and server.py::create_mcp_server(), add a TestNetworkTools class in tests/test_tools.py, and update README, docs/docker-hub.md, and manifest.json. Accept/ignore/withdraw actions are intentionally out of scope.

Generated with Claude Opus 4.7

Closes #446

@ghul0 ghul0 marked this pull request as ready for review May 16, 2026 18:53
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 16, 2026

Greptile Summary

This PR adds a read-only tool for viewing pending LinkedIn network invitations. The main changes are:

  • New get_pending_invitations MCP tool for received and sent invitations.
  • Extractor support for navigating to LinkedIn's invitation manager and scraping visible invitation content.
  • Auto-expansion of truncated invitation notes before extraction.
  • Invitation reference cap and server/tool registration updates.
  • README, Docker docs, manifest, and tool tests updated for the new tool.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
linkedin_mcp_server/scraping/extractor.py Adds the invitation-manager scrape flow and note-expansion helper.
linkedin_mcp_server/tools/network.py Adds the FastMCP wrapper for the new read-only invitations tool.
linkedin_mcp_server/scraping/link_metadata.py Adds the invitation reference cap used by the new extractor path.

Reviews (4): Last reviewed commit: "Merge branch 'main' into feature/446-get..." | Re-trigger Greptile

Comment thread linkedin_mcp_server/scraping/extractor.py
Comment thread linkedin_mcp_server/scraping/extractor.py
Comment thread linkedin_mcp_server/scraping/extractor.py
ghul0 pushed a commit to ghul0/linkedin-mcp-server that referenced this pull request May 16, 2026
Three fixes for the Greptile review on stickerdaniel#447:

1. Trim returned references to the per-call `limit`. Previously `limit`
   only governed how many scroll passes ran; the build_references
   pipeline emitted every profile anchor it saw. References are now
   sliced to `[:limit]` after build_references runs, so a `limit=5`
   call cannot return more than five inviter references. The docstring
   now explicitly states that `sections` text may still include extra
   cards because LinkedIn pages invitations a screenful at a time.

2. Add an `invitations` entry to `_REFERENCE_CAPS` matching the tool's
   `Field(le=100)` ceiling. The previous fallback to the default cap
   of 12 silently truncated profile links for invitation lists larger
   than twelve, leaving most returned invitations without usable
   references.

3. Strengthen the expand-toggle selector so it skips
   `aria-expanded="true"` nodes in addition to the existing
   `data-mcp-clicked` marker. LinkedIn re-uses the same testid for
   the post-expand collapse control, so the previous selector could
   click an already-expanded note and silently re-truncate it. The
   collapsed state ships without an `aria-expanded` attribute at all
   on this surface, so `:not([aria-expanded="true"])` is the
   conservative shape: it matches the unset and explicit `"false"`
   states while skipping the expanded ones.
@ghul0
Copy link
Copy Markdown
Author

ghul0 commented May 16, 2026

Thanks for the careful review — all three P1 items addressed in 3920cb1.

1. Limit not enforced. References are now sliced to [:limit] after build_references, so a limit=5 call cannot return more than five inviter references. The sections text can still include slightly more cards because LinkedIn lazy-loads invitations a screenful at a time; the docstring now states this explicitly so callers don't get a surprise.

2. References truncated at the default cap of 12. Added an invitations entry to _REFERENCE_CAPS with a value of 100, matching the Field(le=100) ceiling on the tool. The per-call slice from item 1 is the operative ceiling in practice; the cap entry just removes the silent 12-link truncation at the build_references layer.

3. Expanded notes could collapse. Strengthened the expand-toggle selector to [data-testid="expandable-text-button"]:not([aria-expanded="true"]):not([data-mcp-clicked]). One nuance versus the inline suggestion: on this surface LinkedIn ships collapsed toggles without an aria-expanded attribute at all (verified during the original DOM debug pass — the inline suggestion used [aria-expanded="false"] which matches zero buttons in that state). The :not([aria-expanded="true"]) form is the conservative shape that matches both the unset and the explicit "false" states while skipping the expanded ones.

CI green on the new commit; full suite passes locally aside from one pre-existing umask-sensitive case in test_browser_security.py that reproduces on main unchanged.

Tomasz Młynek added 3 commits May 17, 2026 08:35
Read-only tool that lists pending LinkedIn network invitations from
/mynetwork/invitation-manager/{received|sent}/, mirroring the existing
get_inbox extractor pattern. Accept/ignore/withdraw actions are
intentionally out of scope for this PR.

Closes stickerdaniel#446
Click `data-testid="expandable-text-button"` toggles before extracting
text so the returned `sections["invitations"]` carries full invite
bodies instead of LinkedIn's "... see more" preview.

The testid attribute is the locale-independent signal — the visible
verb varies by locale and is unsafe to depend on per repo guidelines.

LinkedIn renders these buttons with inline `pointer-events: none`,
which blocks Playwright's standard click from reaching the React
handler. To work around that the implementation injects a scoped
one-shot stylesheet re-enabling pointer events on these specific
testid'd nodes and dispatches a synthetic bubbling MouseEvent so the
handler fires.

Buttons are tagged with `data-mcp-clicked` after the first dispatch
so a second pass picks up newly lazy-loaded cards without re-toggling
already-expanded notes (the post-click "collapse" button shares the
same testid).
Three fixes for the Greptile review on stickerdaniel#447:

1. Trim returned references to the per-call `limit`. Previously `limit`
   only governed how many scroll passes ran; the build_references
   pipeline emitted every profile anchor it saw. References are now
   sliced to `[:limit]` after build_references runs, so a `limit=5`
   call cannot return more than five inviter references. The docstring
   now explicitly states that `sections` text may still include extra
   cards because LinkedIn pages invitations a screenful at a time.

2. Add an `invitations` entry to `_REFERENCE_CAPS` matching the tool's
   `Field(le=100)` ceiling. The previous fallback to the default cap
   of 12 silently truncated profile links for invitation lists larger
   than twelve, leaving most returned invitations without usable
   references.

3. Strengthen the expand-toggle selector so it skips
   `aria-expanded="true"` nodes in addition to the existing
   `data-mcp-clicked` marker. LinkedIn re-uses the same testid for
   the post-expand collapse control, so the previous selector could
   click an already-expanded note and silently re-truncate it. The
   collapsed state ships without an `aria-expanded` attribute at all
   on this surface, so `:not([aria-expanded="true"])` is the
   conservative shape: it matches the unset and explicit `"false"`
   states while skipping the expanded ones.
@ghul0 ghul0 force-pushed the feature/446-get-pending-invitations branch from 3920cb1 to b2b42c6 Compare May 17, 2026 06:36
@socket-security
Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedpytest@​9.0.387100100100100
Addedpython-dotenv@​1.2.299100100100100
Addedty@​0.0.33100100100100100
Addedruff@​0.15.12100100100100100
Addedpytest-cov@​7.1.0100100100100100
Addedpytest-asyncio@​1.3.0100100100100100
Addedpytest-xdist@​3.8.0100100100100100

View full report

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Add get_pending_invitations tool for network invitations

1 participant