Skip to content

feat(post): comment_on_post tool + activity URNs in posts references#424

Open
Dominien wants to merge 2 commits into
stickerdaniel:mainfrom
Dominien:feature/comment-on-post
Open

feat(post): comment_on_post tool + activity URNs in posts references#424
Dominien wants to merge 2 commits into
stickerdaniel:mainfrom
Dominien:feature/comment-on-post

Conversation

@Dominien
Copy link
Copy Markdown

@Dominien Dominien commented May 4, 2026

Summary

Adds the engagement-loop pair upstream is missing in v4.10.1: callers can chain get_person_profile(sections=["posts"])comment_on_post end-to-end without external URL discovery.

  • comment_on_post — write tool mirroring send_message's confirmation gate. confirm_post=False does a dry run that locates the composer; confirm_post=True types and submits.
  • Activity URNs in posts referencesclassify_link now recognises share-style /posts/<slug>-activity-<id>-<sig>/ permalinks, and a data-urn harvest in the is_activity branch picks up the URNs that the recent-activity DOM exposes only on post wrappers (timestamps render as <button>, not <a>, so anchor-only references missed them). Both surface as feed_post references with the canonical /feed/update/urn:li:activity:NNN/ URL.

End-to-end live-tested against a German LinkedIn account: get_person_profile(<user>, sections=posts) returned 12 feed_post URNs; comment_on_post(<urn>, <text>, confirm_post=true) posted, composer cleared, comment visible in thread.

Why bundled

comment_on_post is dead weight without a way to discover post URLs programmatically; the URN harvest is the missing other half. Happy to split into two PRs if preferred.

Design highlights

Locale-independence (per AGENTS.md Scraping Rules)

  • Composer detection: [role=textbox][contenteditable=true] + aria-label hint with structural fallback.
  • Submit-button detection: componentkey*="commentButtonSection" (LinkedIn's React component identity, locale-independent), with exact-match aria-label fallbacks ("Comment" / "Kommentieren" / "Commenter") for older renderings.
  • URN harvest: [data-urn^="urn:li:activity:"] + [data-id^="urn:li:activity:"] — attribute presence + URN prefix.
  • Share permalink regex: ^/posts/[^/?#]*-activity-(\d{15,})-[^/?#]+/?$.

Reliability lessons baked in

  • The Tiptap composer hydrates async; wait_for_selector lets it mount before the focus/type evaluate.
  • Submit click uses a full mousedown → mouseup → click MouseEvent sequence; bare btn.click() registers but skips React parent gates that gate the actual submit.
  • Verify-after-action is two-stage: composer-empty (the reliable structural success signal — LinkedIn clears Tiptap only on server-accept) then body-text-visible. The composer-clear stage eliminates the false positive where typed-but-unsubmitted text matches body.innerText.includes(comment).
  • Candidate buttons re-ranked so componentkey matches always win — an earlier draft used aria-label*="Post" and substring-matched "Open control menu for post by NAME", clicking the post overflow menu instead of the comment submit.
  • Harvest entries prepended (not appended) so URNs win the per-section reference cap over generic post-attachment anchors.

Verification

  • uv run pytest --cov — 486 passing (461 baseline + 25 new). New classes:
    • TestPostTool — happy path, dry-run, composer-unavailable, error.
    • TestNormalizeActivityUrl — URL normalisation across all four input shapes.
    • TestClassifyShareActivityPermalink — share-permalink regex + dedup with /feed/update/ form.
    • TestActivityUrnExtraction — wrapper harvest, non-activity-section skip, dedup with anchor-derived refs, helper-level filtering.
    • comment_on_post added to TestToolTimeouts.test_all_tools_have_global_timeout.
  • uv run ruff check . && uv run ruff format --check . && uv run ty check && uv run pre-commit run --all-files — clean.
  • Live E2E confirmed on a real post (German UI).

Synthetic prompt

Add a comment_on_post MCP tool to linkedin-mcp-server v4.10.1 that mirrors send_message's confirmation-gated, JS-driven write pattern, with locale-independent composer + componentkey-anchored submit detection. Surface activity URNs in get_person_profile(posts) section so callers can chain into comment_on_post — both via a /posts/-activity--/ regex in classify_link and a data-urn wrapper harvest inside the is_activity branch of _extract_page_once. Tests, docs, manifest.

Generated with Claude Opus 4.7 (1M context)

Closes the engagement loop the upstream MCP is missing: callers
can now chain get_person_profile(sections=posts) → comment_on_post
without a separate URL-discovery step.

comment_on_post tool
- Mirrors send_message's confirmation-gated write pattern. confirm_post
  must be True for the write; False does a dry run that locates the
  composer and returns confirmation_required.
- Composer detection is locale-independent: role + contenteditable +
  aria-label hint with structural fallback. wait_for_selector lets the
  Tiptap editor hydrate before the focus/type evaluate.
- Submit-button detection anchors on componentkey*="commentButtonSection"
  (LinkedIn's React component identity, locale-independent), with
  exact-match aria-label fallbacks for older renderings. Candidates are
  re-ranked so componentkey matches always win — the broader aria-label
  *= "Post" filter that earlier drafts used substring-matched the post
  overflow's "Open control menu for post by NAME" and clicked the wrong
  button.
- Click is a full mousedown → mouseup → click MouseEvent sequence so
  React handlers fire reliably; bare btn.click() can register but not
  trigger parent gates.
- Verify-after-action is two-stage: wait for composer to empty (the
  reliable structural success signal — LinkedIn clears Tiptap only after
  server-side accept), then confirm comment text is body-visible. The
  composer-clear check eliminates the false positive where typed-but-
  unsubmitted text matches body.innerText.includes(comment).

Activity URN harvest in posts section
- _SHARE_POST_PATH_RE in classify_link recognises /posts/<slug>-activity
  -<id>-<sig>/ share permalinks alongside the existing /feed/update/
  shape; both canonicalize to /feed/update/urn:li:activity:{id}/.
- _harvest_activity_urns reads [data-urn^=urn:li:activity:] /
  [data-id^=urn:li:activity:] from post wrappers (the recent-activity
  DOM renders timestamps as <button>, not <a>, so anchor-only references
  miss the URN). Synthesises feed_post references with the canonical
  permalink. Detection is structural — attribute presence + URN prefix,
  no locale-dependent text per AGENTS.md scraping rules.
- Harvest runs only inside the existing is_activity branch in
  _extract_page_once and is *prepended* to the references list so URNs
  win the section reference cap over generic post-attachment anchors.
- dedupe_references collapses URN entries from the wrapper harvest with
  any share-permalink anchors that classify_link picked up.

Tests
- 25 new tests, all green, ruff/format/ty/pre-commit clean (486 total).
  TestPostTool, TestNormalizeActivityUrl, TestActivityUrnExtraction,
  TestClassifyShareActivityPermalink. comment_on_post added to
  TestToolTimeouts coverage.

Docs
- README tool table: comment_on_post row + posts-section URN note.
- docs/docker-hub.md features list: post engagement entry.
- manifest.json tools: comment_on_post entry.

## Synthetic prompt

> Add a comment_on_post MCP tool to linkedin-mcp-server v4.10.1 that
> mirrors send_message's confirmation-gated, JS-driven write pattern,
> with locale-independent composer + componentkey-anchored submit
> detection. Surface activity URNs in the get_person_profile(posts)
> section so callers can chain into comment_on_post — both via a
> /posts/<slug>-activity-<id>-<sig>/ regex in classify_link and a
> data-urn wrapper harvest inside the is_activity branch of
> _extract_page_once. Tests, docs, manifest.

Generated with Claude Opus 4.7 (1M context)
@Dominien Dominien force-pushed the feature/comment-on-post branch from 8e7173a to 626b044 Compare May 4, 2026 19:54
@Dominien Dominien marked this pull request as ready for review May 4, 2026 19:57
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 4, 2026

Greptile Summary

This PR adds a comment_on_post tool and surfaces activity URNs in get_person_profile(sections=["posts"]) so callers can chain the two end-to-end. The implementation is well-structured — locale-independence requirements from AGENTS.md are met via a dedicated _COMMENT_SUBMIT_LABELS_BY_LOCALE table, structural selectors, and componentkey-anchored submit detection. The two previous review concerns (inconsistent digit floor in _normalize_activity_url and inline locale labels) appear to be addressed in this revision.

Confidence Score: 5/5

Safe to merge — no P0 or P1 findings; all previous review concerns are addressed.

Only P2-or-lower observations remain. The implementation correctly applies the ≥15-digit floor consistently across _normalize_activity_url and _SHARE_POST_PATH_RE, the locale-label concern from the prior review cycle is resolved with _COMMENT_SUBMIT_LABELS_BY_LOCALE, the test naming ambiguity is resolved by the updated docstring, and the overall pattern (exception handling, confirmation gate, two-stage verify) faithfully mirrors the send_message precedent. 486 tests pass including 25 new ones covering all new code paths.

No files require special attention.

Important Files Changed

Filename Overview
linkedin_mcp_server/scraping/extractor.py Adds post_comment, _normalize_activity_url, _harvest_activity_urns, and _wait_for_composer_clear; _normalize_activity_url consistently uses the ≥15-digit floor; activity URN harvest correctly prepends refs before anchor refs so URNs win the section cap.
linkedin_mcp_server/tools/post.py New tool registration file mirroring send_message's confirmation-gated pattern; exception handling matches the established codebase pattern used by every other tool.
linkedin_mcp_server/scraping/link_metadata.py Adds _SHARE_POST_PATH_RE with ≥15-digit guard and a classify_link branch that canonicalizes /posts/slug-activity-id-sig/ share permalinks to /feed/update/urn:li:activity:id/.
tests/test_scraping.py Adds TestNormalizeActivityUrl and TestActivityUrnExtraction with thorough coverage; test_harvest_helper_filters_invalid_urns docstring correctly documents that dedup is intentionally deferred to build_references, resolving the naming ambiguity from the previous review cycle.
tests/test_tools.py Adds TestPostTool with happy-path, dry-run, composer-unavailable, and error cases; comment_on_post added to TestToolTimeouts.
tests/test_link_metadata.py Adds TestClassifyShareActivityPermalink with 7 cases including short-id rejection, missing-sig rejection, and dedup with /feed/update/ anchors.
linkedin_mcp_server/server.py Imports and registers register_post_tools alongside the other tool registration calls.
manifest.json Adds comment_on_post entry to the manifest tools array.

Sequence Diagram

sequenceDiagram
    participant Caller as MCP Caller
    participant Tool as comment_on_post tool
    participant Ext as LinkedInExtractor
    participant Page as LinkedIn Page

    Caller->>Tool: comment_on_post(post_url, text, confirm_post)
    Tool->>Ext: post_comment(post_url, text, confirm_post=...)
    Ext->>Ext: _normalize_activity_url(post_url)
    Note over Ext: Accepts /feed/update/URN, /posts/slug-activity-id-sig/, bare URN, bare id
    Ext->>Page: navigate to canonical /feed/update/ URL
    Ext->>Page: wait_for_selector(main) + handle_modal_close
    Ext->>Page: wait_for_selector(div[role=textbox][contenteditable])
    Ext->>Page: evaluate() — focus first visible composer
    alt composer not found
        Ext-->>Tool: status: composer_unavailable
    else confirm_post=False
        Ext-->>Tool: status: confirmation_required
    else confirm_post=True
        Ext->>Page: keyboard.type(comment_text, delay=15)
        Ext->>Page: evaluate() — click submit (componentkey → aria-label → Ctrl+Enter fallback)
        Ext->>Page: _wait_for_composer_clear (poll until innerText empty)
        Ext->>Page: _message_text_visible(comment_text)
        alt composer cleared + text visible
            Ext-->>Tool: status: posted, posted: true, comment_visible: true
        else composer not cleared
            Ext-->>Tool: status: post_unverified, posted: false
        else text not visible
            Ext-->>Tool: status: post_unverified, posted: true, comment_visible: false
        end
    end
    Tool-->>Caller: result dict
Loading

Reviews (2): Last reviewed commit: "fix(post): address Greptile review on #4..." | Re-trigger Greptile

Comment thread linkedin_mcp_server/scraping/extractor.py
- _normalize_activity_url: apply the same >=15-digit floor as
  _SHARE_POST_PATH_RE to all three branches. Previously the
  -activity-(\d+)- branch and the urn:li:activity:(\d+) branch had
  no floor, so a URL like /posts/foo-activity-123-XYZ/ would silently
  canonicalize to /feed/update/urn:li:activity:123/ instead of
  returning None. Adds two regression tests
  (test_short_id_in_share_permalink_rejected and
  test_short_id_in_urn_rejected).

- comment_on_post submit selector: replace the inline aria-label
  fallbacks with a module-level _COMMENT_SUBMIT_LABELS_BY_LOCALE table
  per AGENTS.md scraping rules ("text-only signals must live behind
  an explicit per-locale table"). Documents currently-supported
  locales (en/de/fr) and known-unsupported ones (es/pt/nl/it/pl)
  in-comment, with a one-line extension path. The Ctrl+Enter shortcut
  remains the locale-agnostic last-resort fallback.

- test_harvest_helper_filters_invalid_urns_and_dedupes renamed to
  test_harvest_helper_filters_invalid_urns. The helper itself does not
  dedupe — the JS seen-set runs at the DOM level in the live evaluate,
  and cross-source dedup happens downstream in dedupe_references.
  Docstring + inline comment now say so explicitly.

488 tests pass (486 → 488), ruff/format/ty/pre-commit clean.
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.

1 participant