feat(network): add get_pending_invitations tool#447
Conversation
Greptile SummaryThis PR adds a new read-only
Confidence Score: 5/5Safe to merge; the change is additive and read-only, all three previously raised issues have been resolved, and the implementation mirrors the established get_inbox pattern faithfully. All prior feedback has been addressed — the invitations reference cap is correctly set, the expand selector now guards against re-collapsing notes, and references are trimmed to the per-call limit. The remaining notes are minor ordering and iteration-count trade-offs that do not affect correctness. No files require special attention; extractor.py has a minor ordering opportunity around the zero-count guard, but nothing that affects correctness. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant MCP as MCP Client
participant Tool as get_pending_invitations
participant Ext as LinkedInExtractor
participant Page as Browser Page
MCP->>Tool: get_pending_invitations(limit, kind)
Tool->>Ext: get_pending_invitations(limit, kind)
Ext->>Page: "_navigate_to_page(/mynetwork/invitation-manager/{kind}/)"
Ext->>Page: detect_rate_limit()
Ext->>Page: _wait_for_main_text()
Ext->>Page: handle_modal_close()
Ext->>Page: "_scroll_main_scrollable_region(attempts=max(1, limit//10))"
alt "kind == received"
Ext->>Page: _received_invitation_count_is_zero()
Page-->>Ext: True / False
end
alt count is zero
Ext-->>Tool: "{url, sections:{}}"
else "count > 0"
Ext->>Page: _expand_invitation_note_toggles() [2 passes, 400ms each]
Ext->>Page: _extract_root_content([main])
Page-->>Ext: "{text, references}"
Ext->>Ext: strip_linkedin_noise(raw)
Ext->>Ext: build_references(...)[:limit]
Ext-->>Tool: "{url, sections:{invitations:...}, references:{invitations:[...]}}"
end
Tool-->>MCP: result
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant MCP as MCP Client
participant Tool as get_pending_invitations
participant Ext as LinkedInExtractor
participant Page as Browser Page
MCP->>Tool: get_pending_invitations(limit, kind)
Tool->>Ext: get_pending_invitations(limit, kind)
Ext->>Page: "_navigate_to_page(/mynetwork/invitation-manager/{kind}/)"
Ext->>Page: detect_rate_limit()
Ext->>Page: _wait_for_main_text()
Ext->>Page: handle_modal_close()
Ext->>Page: "_scroll_main_scrollable_region(attempts=max(1, limit//10))"
alt "kind == received"
Ext->>Page: _received_invitation_count_is_zero()
Page-->>Ext: True / False
end
alt count is zero
Ext-->>Tool: "{url, sections:{}}"
else "count > 0"
Ext->>Page: _expand_invitation_note_toggles() [2 passes, 400ms each]
Ext->>Page: _extract_root_content([main])
Page-->>Ext: "{text, references}"
Ext->>Ext: strip_linkedin_noise(raw)
Ext->>Ext: build_references(...)[:limit]
Ext-->>Tool: "{url, sections:{invitations:...}, references:{invitations:[...]}}"
end
Tool-->>MCP: result
Reviews (8): Last reviewed commit: "fix(network): omit empty invitation sugg..." | Re-trigger Greptile |
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.
|
Thanks for the careful review — all three P1 items addressed in 3920cb1. 1. Limit not enforced. References are now sliced to 2. References truncated at the default cap of 12. Added an 3. Expanded notes could collapse. Strengthened the expand-toggle selector to CI green on the new commit; full suite passes locally aside from one pre-existing umask-sensitive case in |
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.
3920cb1 to
b2b42c6
Compare
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.
78e6b0b to
5b1dccd
Compare
Satisfies the CONTRIBUTING "Adding a New Tool" checklist item for extractor-level coverage and locks the two review-feedback fixes so a regression cannot silently reintroduce either: - test_references_trimmed_to_limit: feeds 30 inviter anchors with limit=5 and asserts the returned references are sliced to 5 — guards the [:limit] fix and the invitations reference cap. - test_expand_selector_skips_expanded_and_clicked: asserts the note-expansion selector keys on the locale-independent data-testid and carries both :not([aria-expanded="true"]) and :not([data-mcp-clicked]) guards, so a pass can never re-collapse a note it previously revealed. - test_sent_kind_navigates_to_sent_page: covers the sent surface. Helpers stubbed via patch.object to match the module's existing style.
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
Summary
Adds a new read-only MCP tool
get_pending_invitationsfor 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 reachget_inboxuntil accepted, so an agent assisting with inbound triage cannot see them today.What's in scope
tools/network.py::register_network_toolswith signatureget_pending_invitations(ctx, limit=20, kind="received"|"sent").LinkedInExtractor.get_pending_invitations(limit, kind)that follows theget_inboxtemplate 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._expand_invitation_note_toggleshelper. Uses the locale-independentdata-testid="expandable-text-button"selector, injects a scoped one-shot stylesheet to re-enablepointer-events(LinkedIn ships these buttons with inlinepointer-events: none), and dispatches a synthetic bubbling MouseEvent so the React handler fires. A button is tagged withdata-mcp-clickedonly after a successful dispatch, so a click that throws is left unmarked and retried on the second pass; the selector also skipsaria-expanded="true"so an already-expanded note is never toggled closed.kind="received": when the selected ALL-count tab reads zero, the method returns an empty single-section result instead of scraping the unrelated "people you may know" recommendation cards LinkedIn renders below the empty invitations list. The zero check is locale-independent — it locates the selected tab viaaria-current="true"+ the stable/invitation-manager/received/ALLURL, and only the digit count is read as text (guarded behind that structural selector, documented in the helper docstring).limitbounds the returned references (build_references(...)[:limit]), and_REFERENCE_CAPS["invitations"]is set to 100 to match the tool'sField(le=100)ceiling so larger invitation lists are not silently truncated at the default cap of 12.server.py::create_mcp_server().TestNetworkToolsintests/test_tools.py(happy path,sentkind, invalid kind rejection, limit-bound rejection;_make_mock_extractorextended; tool added to both timeout-coverage tuples) plusTestGetPendingInvitationsintests/test_scraping.py(extractor-level regression coverage:[:limit]reference trimming, thesentnavigation target, thearia-expanded="true"/data-mcp-clickedskip guards, and the zero-count empty-state path).docs/docker-hub.mdfeatures list, andmanifest.jsontools array all updated.Intentionally out of scope
fields.py— this is a one-page tool, not a sectioned scraper, matchingget_feed/get_inbox.Testing
uv run pytest: 551 passed locally. One pre-existing failure intest_browser_security.py::test_harden_linkedin_tree_noop_outside_linkedinreproduces onmainbefore this branch withumask=002; it's environmental and unrelated.uv run ruff check .: clean.uv run ty check: clean.uv run pre-commit run --all-files: all hooks pass.get_pending_invitations(read-only) on a real account in two states:kind="received": result shape conforms to{url, sections, references}; the section text contained inviter blocks with every truncated invitation note auto-expanded (textmatched the collapse verb N times, the expand verb 0 times); references carriedkind: "person"entries with/in/USERNAME/URLs, sliced tolimit.kind="received": returnssections: {}with no references, confirming the "people you may know" recommendation cards are not surfaced as invitations.Synthetic prompt
Generated with Claude Opus 4.7 and GPT-5.5
Closes #446