| name | linkedin-weekly |
| description | Use when collecting LinkedIn posts from followed targets and generating a weekly newsletter digest with AI summaries |
LinkedIn Weekly Newsletter
Overview
Collect new LinkedIn posts from configured targets using Chrome DevTools MCP (real browser), summarize each, and generate a weekly HTML newsletter.
Prerequisites
- User must be logged into LinkedIn in their Chrome browser
- Chrome DevTools MCP must be connected
Workflow
Step 1: Get targets
Call list_targets to get all configured targets. Each target includes an activity_url field -- this is the URL to visit for that target's posts.
Step 2: Scrape each target via Chrome DevTools
For each target:
- Navigate: Use
navigate_page (Chrome DevTools MCP) to open the target's activity_url
- Wait for content: Use
wait_for to wait for post content to appear (e.g. text "activity" or a known page element)
- Login wall check: Use
take_snapshot and check for login wall indicators (text like "Sign in", "Join now", or no post content). If detected, stop and inform the user:
"LinkedIn requires login. Please log into LinkedIn in your Chrome browser, then try again."
- Scroll once: Use
evaluate_script to scroll down once and load a few more posts:
async () => {
window.scrollBy(0, 1500);
await new Promise(r => setTimeout(r, 2000));
}
- Extract posts via JS: Use
evaluate_script to extract the top 3 posts directly from the DOM. This avoids large snapshots and returns compact JSON with stable IDs:
() => {
const posts = document.querySelectorAll(
'[data-urn*="urn:li:activity"], .feed-shared-update-v2, .occludable-update'
);
return Array.from(posts).map(el => {
const urn = el.getAttribute('data-urn') || '';
const idMatch = urn.match(/urn:li:activity:\d+/);
const authorEl = el.querySelector(
'.update-components-actor__title span[aria-hidden="true"], ' +
'.update-components-actor__name span[aria-hidden="true"], ' +
'.feed-shared-actor__name span[aria-hidden="true"]'
);
const textEl = el.querySelector(
'.feed-shared-update-v2__description, ' +
'.update-components-text, .feed-shared-text'
);
const linkEl = el.querySelector('a[href*="feed/update"]');
const timeEl = el.querySelector('time');
const subDesc = el.querySelector('.update-components-actor__sub-description');
const imgs = el.querySelectorAll(
'.feed-shared-image__image, .update-components-image img'
);
const linkedinId = idMatch ? idMatch[0] : null;
let postedAt = '';
if (timeEl) {
postedAt = timeEl.getAttribute('datetime') || timeEl.innerText.trim();
} else if (subDesc) {
postedAt = subDesc.innerText.trim().split('•')[0].trim();
}
return {
linkedin_id: linkedinId,
author: authorEl ? authorEl.innerText.trim() : 'Unknown',
text: textEl ? textEl.innerText.trim().slice(0, 500) : '',
url: linkEl ? linkEl.href : (linkedinId ? 'https://www.linkedin.com/feed/update/' + linkedinId : ''),
media_urls: Array.from(imgs).map(i => i.src).filter(Boolean),
posted_at: postedAt,
};
}).filter(p => p.linkedin_id).slice(0, 3);
}
- Fallback: If the JS extraction returns an empty array (e.g. LinkedIn changed selectors), fall back to
take_snapshot of the current visible area only (no additional scrolling) and extract posts from the snapshot text. Use snapshot-extracted data as best-effort -- IDs may not be stable.
- Store posts: Call
store_posts(target_url=<target's base url>, posts=<extracted posts list>)
Step 3: Generate newsletter
- Call
get_new_posts(since_days=7) to retrieve all posts from the past week
- For each post, generate a one-sentence English summary
- If original post text is non-English, also generate an English translation
- Group posts by
target_name, sort by posted_at (newest first)
- Call
generate_newsletter passing the list of posts, each with added summary field (and translation field if applicable)
- Call
open_newsletter with the returned file path
- Report to user: total targets checked, new posts found, newsletter file path
Summary Guidelines
- Always write summaries in English regardless of original language
- One sentence per post capturing the core point
- If post contains links or media, mention briefly
- Keep original post text in its original language
- For non-English posts, add a
translation field with full English translation