Skip to content

About

BlogCaster is an intelligent CLI-based agent that automates the process of sharing blog content on LinkedIn, X and Facebook. Built on the MCP architecture, it uses three MCP servers, an RSS Fetcher for parsing blog feeds, a Record Keeper for tracking publishing history, and a Poster for API integration, all coordinated by a central orchestrator.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

BlogCaster

A CLI-based agent that fetches blog posts from Hugo-based blog platforms via RSS feeds, formats them into platform-specific social media posts using an LLM, and publishes them to LinkedIn, X (Twitter), and Facebook.

The agent is platform-driven — each blog platform is registered in a JSON registry and the agent can operate independently per platform. Adding a new blog platform requires only a new entry in the registry file, no code changes.

How It Works

RSS Feed ──> Record Check ──> Post Selection ──> LLM Formatting ──> LinkedIn + X + Facebook Post ──> Record Save

Five FastMCP servers handle the core operations, coordinated by a central orchestrator:

MCP Server Role
rss-fetcher Fetches and parses Hugo RSS feeds and individual blog posts
record-keeper Tracks published posts in JSON, prevents duplicates
linkedin-poster Posts formatted content to LinkedIn personal profile
x-poster Posts formatted tweets to X (Twitter) via tweepy
facebook-poster Posts formatted content to a Facebook Page via Graph API

Two Operating Modes

Manual Mode

Provide a specific blog post URL. The agent auto-detects the platform, fetches the content, formats it, posts it to LinkedIn, X, and Facebook, and records the result. Also used to retry failed platforms for a previously posted URL.

python -m agent_engine.social_agent.main --url "https://blog.aspose.com/some-post"

Auto Mode

Specify a platform ID. The agent fetches the RSS feed, picks the latest unpublished post, formats it, posts it to LinkedIn, X, and Facebook, and records the result.

python -m agent_engine.social_agent.main --auto --platform aspose
python -m agent_engine.social_agent.main --auto --platform groupdocs
python -m agent_engine.social_agent.main --auto --platform conholdate

Prerequisites

  • Python 3.10+
  • A self-hosted GPT-OSS LLM endpoint (OpenAI-compatible API)
  • A LinkedIn OAuth 2.0 access token with w_member_social scope
  • X (Twitter) OAuth 1.0a credentials with Read and Write permissions
  • A Facebook Page Access Token with pages_read_engagement and pages_manage_posts permissions

Setup

1. Clone and navigate to the project

cd socialAgent

2. Install dependencies

pip install "mcp<2.0.0" feedparser httpx beautifulsoup4 aiofiles pydantic-settings openai tweepy

3. Configure environment variables

Copy the example file and fill in your credentials:

cp .env.example .env

Edit .env with your values:

# LLM Configuration (GPT-OSS)
PROFESSIONALIZE_BASE_URL=https://your-gpt-oss-endpoint.com/v1
PROFESSIONALIZE_API_KEY_2=your-api-key
PROFESSIONALIZE_LLM_MODEL=your-model-name

# LinkedIn Configuration
LINKEDIN_ACCESS_TOKEN=your-linkedin-oauth-token

# X (Twitter) Configuration
X_API_KEY=your-x-api-key
X_API_SECRET=your-x-api-secret
X_ACCESS_TOKEN=your-x-access-token
X_ACCESS_TOKEN_SECRET=your-x-access-token-secret

# Facebook Page Configuration (default — used by aspose-cloud)
FACEBOOK_PAGE_ID=your-facebook-page-id
FACEBOOK_PAGE_ACCESS_TOKEN=your-facebook-page-access-token

# Facebook Page Configuration (platform-specific overrides)
FACEBOOK_GROUPDOCS_PAGE_ID=your-groupdocs-facebook-page-id
FACEBOOK_GROUPDOCS_PAGE_ACCESS_TOKEN=your-groupdocs-facebook-page-access-token
FACEBOOK_CONHOLDATE_PAGE_ID=your-conholdate-facebook-page-id
FACEBOOK_CONHOLDATE_PAGE_ACCESS_TOKEN=your-conholdate-facebook-page-access-token
FACEBOOK_ASPOSE_PAGE_ID=your-aspose-facebook-page-id
FACEBOOK_ASPOSE_PAGE_ACCESS_TOKEN=your-aspose-facebook-page-access-token

Note: FACEBOOK_ASPOSE_* is for the plain aspose platform (blog.aspose.com), a separate blog and Facebook Page from aspose-cloud (blog.aspose.cloud), which keeps using the default FACEBOOK_PAGE_ID/FACEBOOK_PAGE_ACCESS_TOKEN above.

Variable Description
PROFESSIONALIZE_BASE_URL Base URL of your OpenAI-compatible LLM endpoint
PROFESSIONALIZE_API_KEY_2 API key for the LLM endpoint
PROFESSIONALIZE_LLM_MODEL Model name to use for text generation
LINKEDIN_ACCESS_TOKEN Pre-generated LinkedIn OAuth 2.0 access token
X_API_KEY X (Twitter) API consumer key
X_API_SECRET X (Twitter) API consumer secret
X_ACCESS_TOKEN X (Twitter) OAuth 1.0a access token
X_ACCESS_TOKEN_SECRET X (Twitter) OAuth 1.0a access token secret
FACEBOOK_PAGE_ID Facebook Page numeric ID (default, used by aspose-cloud)
FACEBOOK_PAGE_ACCESS_TOKEN Facebook Page Access Token (default, used by aspose-cloud)
FACEBOOK_{BRAND}_PAGE_ID Platform-specific Facebook Page ID (e.g. FACEBOOK_GROUPDOCS_PAGE_ID)
FACEBOOK_{BRAND}_PAGE_ACCESS_TOKEN Platform-specific Facebook Page Access Token

LinkedIn Token Renewal

LinkedIn OAuth tokens expire every 60 days. When the agent logs LinkedIn token is invalid or expired, follow these steps to renew:

Step 1: Generate a new token

  1. Go to LinkedIn Developer Apps
  2. Select your app
  3. Go to the Auth tab
  4. Under OAuth 2.0 tools, click Generate token
  5. Select scopes: w_member_social, openid, profile
  6. Authorize and copy the new token

Note: Token generation works on Safari but may fail on Google Chrome. If you get errors in Chrome, switch to Safari.

Step 2: Update credentials

  1. Update LINKEDIN_ACCESS_TOKEN in your .env file (local)
  2. Update LINKEDIN_ACCESS_TOKEN in GitHub repo Settings > Secrets and variables > Actions (for GitHub Actions)
  3. Update LINKEDIN_TOKEN_EXPIRES_AT to the new expiry date (today + 60 days):
LINKEDIN_TOKEN_EXPIRES_AT=2026-07-19

The agent prints a console warning when the token is within 7 days of expiring, so you can renew proactively.

Expiry Alert (GitHub Issue)

Since LinkedIn doesn't support automated token renewal (unlike Facebook — see below), a scheduled workflow (.github/workflows/linkedin-token-alert.yml) runs every 2 weeks (1st and 15th) and checks the LINKEDIN_TOKEN_EXPIRES_AT repo variable (Settings → Secrets and variables → Actions → Variables tab, not Secrets — it's just a date). It automatically opens a GitHub issue titled "LinkedIn token expiring soon" once 14 days or fewer remain — the wider 14-day threshold (vs. the biweekly check interval) guarantees the alert can't be missed between runs. The issue includes the renewal steps above and auto-closes itself once you update the variable to a date more than 14 days out.

Remember: after renewing the token, update both LINKEDIN_ACCESS_TOKEN (secret) and LINKEDIN_TOKEN_EXPIRES_AT (variable) — the alert only clears once the variable reflects the new expiry.

Facebook Token Auto-Renewal

Unlike LinkedIn, Facebook Page tokens can be renewed automatically without a browser login, because Meta allows re-exchanging a still-valid long-lived user token for a fresh one via API. A scheduled workflow (.github/workflows/facebook-token-renewal.yml) does this on the 1st of every month, well inside the ~60-day expiry window, for the Aspose.Cloud, GroupDocs.Cloud, Conholdate, and Aspose pages.

How it works

  1. scripts/renew_facebook_tokens.py exchanges the stored long-lived user token for a fresh one (fb_exchange_token grant) for each brand.
  2. It re-derives that brand's Page Access Token from the refreshed user token.
  3. The workflow pushes both refreshed values back into the repo's GitHub Actions secrets via gh secret set, using a dedicated PAT.

One-time setup

Each brand (Aspose.Cloud, GroupDocs.Cloud, Conholdate, Aspose) has its own Facebook App, so credentials are not shared between them.

Variable Where to get it
FACEBOOK_APP_ID / FACEBOOK_APP_SECRET Facebook Developer Apps → select the app tied to the Aspose.Cloud page → Settings → Basic
FACEBOOK_USER_ACCESS_TOKEN Graph API Explorer → select that same app → Generate Access Token → grant pages_read_engagement, pages_manage_posts (this seeds the automation; only needs to be pasted in once)
FACEBOOK_GROUPDOCS_APP_ID / FACEBOOK_GROUPDOCS_APP_SECRET Same steps, but select the app tied to the GroupDocs.Cloud page
FACEBOOK_GROUPDOCS_USER_ACCESS_TOKEN Same steps, with the GroupDocs app selected in Graph API Explorer
FACEBOOK_CONHOLDATE_APP_ID / FACEBOOK_CONHOLDATE_APP_SECRET Same steps, but select the app tied to the Conholdate page
FACEBOOK_CONHOLDATE_USER_ACCESS_TOKEN Same steps, with the Conholdate app selected in Graph API Explorer
FACEBOOK_ASPOSE_APP_ID / FACEBOOK_ASPOSE_APP_SECRET Same steps, but select the app tied to the plain Aspose (blog.aspose.com) page
FACEBOOK_ASPOSE_USER_ACCESS_TOKEN Same steps, with the Aspose app selected in Graph API Explorer — must grant pages_manage_posts, not just pages_read_engagement, or posting will fail with (#200) Insufficient permissions to post
FACEBOOK_TOKEN_RENEWAL_PAT A fine-grained GitHub PAT, scoped to this repo only, with the "Secrets" repository permission set to Read and write. Lets the workflow call gh secret set.

Add all of the above to GitHub repo Settings → Secrets and variables → Actions (the app/user-token ones also belong in your local .env if you want to test the script locally).

After that, renewal is fully automatic — the workflow keeps overwriting FACEBOOK_USER_ACCESS_TOKEN, FACEBOOK_PAGE_ACCESS_TOKEN, FACEBOOK_GROUPDOCS_USER_ACCESS_TOKEN, FACEBOOK_GROUPDOCS_PAGE_ACCESS_TOKEN, FACEBOOK_CONHOLDATE_USER_ACCESS_TOKEN, FACEBOOK_CONHOLDATE_PAGE_ACCESS_TOKEN, FACEBOOK_ASPOSE_USER_ACCESS_TOKEN, and FACEBOOK_ASPOSE_PAGE_ACCESS_TOKEN with fresh values each month, so you should never need to manually regenerate a Facebook token again unless the automation itself fails (e.g. an app credential is revoked).

To test manually: Actions → Facebook Token Renewal → Run workflow.

Facebook Page Setup

Facebook requires a Page Access Token (not a User Access Token) to post to a Page. Follow these steps:

Step 1: Create a Facebook App

  1. Go to Facebook Developers and create an app (type: Business)
  2. Add the "Facebook Login for Business" product to your app

Step 2: Get a User Access Token

  1. Go to the Graph API Explorer
  2. Select your app from the dropdown
  3. Click "Generate Access Token"
  4. Grant these permissions: pages_read_engagement, pages_manage_posts

Step 3: Exchange for a Page Access Token

The User token gives you access to your Pages, but you need the Page's own token to post. Run:

curl "https://graph.facebook.com/v21.0/me/accounts?access_token=YOUR_USER_TOKEN"

This returns a list of Pages you manage:

{
  "data": [
    {
      "access_token": "EAAxxxxx_PAGE_TOKEN_xxxxx",
      "name": "Your Page Name",
      "id": "123456789012345"
    }
  ]
}

Copy the access_token and id from the Page entry.

Step 4: Verify the Page Token

curl "https://graph.facebook.com/v21.0/me?access_token=YOUR_PAGE_TOKEN"

This should return the Page's name and ID (not your personal name). If it shows the Page name, the token is correct.

Step 5: Update .env

FACEBOOK_PAGE_ID=123456789012345
FACEBOOK_PAGE_ACCESS_TOKEN=EAAxxxxx_PAGE_TOKEN_xxxxx

Step 6: Test

python -m agent_engine.social_agent.main --url "https://blog.aspose.com/some-post" --target facebook

Common Mistake: Using a User Token Instead of a Page Token

If you see this error:

Insufficient permissions to post: (#200) If posting to a group, requires app being
installed in the group...

You are using a User Access Token instead of a Page Access Token. Go back to Step 3 and use the me/accounts endpoint to get the Page's own token.

Token Expiration & Long-Lived Tokens

Tokens from the Graph API Explorer expire in ~1 hour. For production use, exchange for a long-lived token (~60 days):

1. Exchange short-lived User Token for a long-lived User Token:

curl -G "https://graph.facebook.com/v19.0/oauth/access_token" \
  --data-urlencode "grant_type=fb_exchange_token" \
  --data-urlencode "client_id=YOUR_APP_ID" \
  --data-urlencode "client_secret=YOUR_APP_SECRET" \
  --data-urlencode "fb_exchange_token=YOUR_SHORT_LIVED_USER_TOKEN"

Response:

{
  "access_token": "LONG_LIVED_USER_TOKEN",
  "token_type": "bearer",
  "expires_in": 5183423
}

expires_in is in seconds (~60 days).

2. Exchange long-lived User Token for a Page Token:

curl "https://graph.facebook.com/v19.0/me/accounts?access_token=LONG_LIVED_USER_TOKEN"

The Page Access Token returned from a long-lived User Token does not expire. Copy the access_token and id from your Page entry and update .env / GitHub Actions secrets.

3. Verify your token:

curl "https://graph.facebook.com/v19.0/debug_token?input_token=YOUR_PAGE_TOKEN&access_token=YOUR_APP_ID|YOUR_APP_SECRET"

Check expires_at — a value of 0 means the token never expires.

Security notes:

  • Never expose client_secret in frontend code
  • Store tokens in environment variables or GitHub Actions secrets
  • Rotate credentials immediately if exposed

Usage

All commands must be run from the project root directory.

Post a specific blog URL (Manual Mode)

python -m agent_engine.social_agent.main --url "https://blog.aspose.com/some-post"

Post the latest unposted blog from a platform (Auto Mode)

python -m agent_engine.social_agent.main --auto --platform aspose

Target a specific social media platform

# Post to Facebook only
python -m agent_engine.social_agent.main --url "https://blog.aspose.com/some-post" --target facebook

# Post to LinkedIn only
python -m agent_engine.social_agent.main --url "https://blog.aspose.com/some-post" --target linkedin

# Post to X only
python -m agent_engine.social_agent.main --url "https://blog.aspose.com/some-post" --target x

# Post to all platforms (default)
python -m agent_engine.social_agent.main --url "https://blog.aspose.com/some-post" --target all

List available platforms

python -m agent_engine.social_agent.main --auto

Output:

Error: --platform is required with --auto

Available platforms:
  - aspose          (Aspose Blog)
  - groupdocs       (GroupDocs Blog)
  - conholdate      (Conholdate Blog)

Show usage help

python -m agent_engine.social_agent.main

Adding a New Blog Platform

Adding a new brand requires no code changes — just configuration:

1. Add to platform registry

Edit registry/platforms_registry.json and add a new entry:

{
  "id": "conholdate",
  "name": "Conholdate Blog",
  "url": "https://blog.conholdate.com",
  "rss_feed": "https://blog.conholdate.com/index.xml",
  "active": true
}

2. Add platform-specific Facebook credentials

Facebook credentials are resolved dynamically based on the platform name. The naming convention:

FACEBOOK_{BRAND}_PAGE_ID
FACEBOOK_{BRAND}_PAGE_ACCESS_TOKEN

Where {BRAND} is the first part of the platform ID (before -), uppercased. Exception: aspose-cloud is hardcoded to always use the default FACEBOOK_PAGE_ID/FACEBOOK_PAGE_ACCESS_TOKEN and never does a brand lookup — this is because its brand prefix (aspose) would otherwise collide with the separate, plain aspose platform below. If you add a new hyphenated platform whose prefix collides with an existing bare platform ID, apply the same exclusion in mcp_tools.py and orchestrator.py.

Platform ID Facebook env vars
aspose-cloud FACEBOOK_PAGE_ID + FACEBOOK_PAGE_ACCESS_TOKEN (default, hardcoded — see exception above)
groupdocs-cloud FACEBOOK_GROUPDOCS_PAGE_ID + FACEBOOK_GROUPDOCS_PAGE_ACCESS_TOKEN
conholdate FACEBOOK_CONHOLDATE_PAGE_ID + FACEBOOK_CONHOLDATE_PAGE_ACCESS_TOKEN
aspose FACEBOOK_ASPOSE_PAGE_ID + FACEBOOK_ASPOSE_PAGE_ACCESS_TOKEN

Add these to your .env file locally and as GitHub Actions secrets.

If no platform-specific credentials are found, the default FACEBOOK_PAGE_ID / FACEBOOK_PAGE_ACCESS_TOKEN are used.

LinkedIn and X credentials are shared across all brands.

3. Create a GitHub Actions workflow

Copy an existing workflow file (e.g. .github/workflows/blogcaster-groupdocs-cloud.yml) and change:

  • name: BlogCaster — Your Brand
  • schedule: Pick days that don't overlap with existing workflows
  • platform default: Your platform ID (e.g. conholdate)
  • Facebook env vars: Use your brand-specific secret names

Current schedule:

Workflow Days Cron
BlogCaster — Aspose Cloud Mon & Fri 0 5 * * 1,5
BlogCaster — GroupDocs Cloud Tue & Thu 0 5 * * 2,4
BlogCaster — Conholdate Wed & Sat 0 5 * * 3,6
BlogCaster — Aspose Sun (latest) & Mon (gsc) 0 5 * * 0,1

4. Test locally

python -m agent_engine.social_agent.main --auto --platform conholdate --target facebook

5. Test on GitHub Actions

Trigger manually via workflow_dispatch in the Actions tab, then confirm the scheduled run fires on the expected day.

Project Structure

socialAgent/
├── agent_engine/
│   └── social_agent/
│       ├── main.py                  # CLI entry point
│       ├── config.py                # Pydantic BaseSettings config
│       ├── agent_logic/
│       │   └── orchestrator.py      # Core workflow (manual + auto modes)
│       ├── tools/
│       │   └── mcp_tools.py         # MCP server wrappers
│       └── utils/
│           ├── helpers.py           # Logger setup + registry helpers
│           ├── llm_service.py       # LLM formatting via OpenAI SDK
│           └── prompts.py           # Platform-specific prompt templates
├── mcp-servers/
│   ├── rss-fetcher/server.py        # RSS feed parsing (feedparser + httpx + bs4)
│   ├── linkedin-poster/server.py    # LinkedIn API posting (httpx)
│   ├── x-poster/server.py           # X (Twitter) API posting (tweepy)
│   ├── facebook-poster/server.py    # Facebook Page posting (httpx + Graph API)
│   └── record-keeper/server.py      # JSON-based record storage (aiofiles)
├── registry/
│   └── platforms_registry.json      # Blog platform registry
├── content/
│   └── records/
│       └── published_record.json    # Published post records
├── logs/
│   └── logs.txt                     # Application logs
├── .env                             # Credentials (not committed)
├── .env.example                     # Credential template
└── .gitignore

Logs

All operations are logged to logs/logs.txt with timestamps. Console output shows INFO-level messages, the log file captures DEBUG-level detail.

2026-03-17 03:28:49 | INFO     | social_agent | Starting Auto Mode for platform: aspose
2026-03-17 03:28:50 | INFO     | social_agent | Fetched 10 posts from RSS feed
2026-03-17 03:28:51 | INFO     | social_agent | Selected unpublished post: "Convert OneNote..." — https://...
2026-03-17 03:28:58 | INFO     | social_agent | LLM formatted post (102 words)
2026-03-17 03:29:01 | INFO     | social_agent | LinkedIn post successful: post_id=urn:li:share:...
2026-03-17 03:29:01 | INFO     | social_agent | Record saved for https://...

Duplicate Prevention & Per-Platform Retry

The agent tracks posting results per platform. In Auto Mode, any URL with an existing record is skipped — it always picks a fresh post. In Manual Mode (--url), if a URL was previously posted to some platforms but not all, the agent retries only the failed platforms.

URLs are normalized (lowercased, trailing slashes stripped, query parameters removed) to catch variations like:

  • https://blog.aspose.com/post/ vs https://blog.aspose.com/post
  • https://blog.aspose.com/post?utm=123 vs https://blog.aspose.com/post

Records are only saved when at least one platform succeeds. If all platforms fail, no record is written so the URL can be retried on the next run.

Post Formats

LinkedIn

  • Professional tone, 150-200 words
  • Hook line that grabs attention
  • 3-5 relevant hashtags
  • Blog URL at the end

X (Twitter)

  • One punchy sentence + 2-3 relevant hashtags
  • Blog URL appended automatically
  • Total tweet fits within 280 characters

Facebook

  • Community-focused, friendly conversational tone, 150-200 words
  • Curiosity-driven hook line
  • Clear call-to-action (read the article, share thoughts, tag someone)
  • 3-5 relevant hashtags
  • Blog URL attached as a link

Troubleshooting

Issue Solution
PROFESSIONALIZE_BASE_URL is not set Create .env file from .env.example and fill in LLM credentials
LinkedIn token is expired or invalid (401) Generate a new LinkedIn OAuth token and update .env
Insufficient permissions (403) Ensure your LinkedIn token has w_member_social scope
All recent posts have already been shared All RSS feed posts are already in the record — wait for new blog posts
X credentials have insufficient permissions (403) Regenerate X Access Token & Secret after enabling Read and Write permissions
X API rate limit exceeded (429) Wait a few minutes and try again
Facebook token is expired or invalid Get a new Page Access Token (see Facebook Page Setup above)
Insufficient permissions to post: (#200) You are using a User token instead of a Page token — use me/accounts to get the Page token
Facebook API rate limit exceeded Wait a few minutes and try again
Platform 'xxx' not found Check registry/platforms_registry.json for available platform IDs

Future Developments

Additional Social Media Platforms

  • Medium / Dev.to — Full article republishing (not just social snippets) for stronger SEO backlinks
  • Blogger / WordPress — Cross-post full articles to blogging platforms for wider reach and backlink injection

Performance Improvements

  • Parallel LLM calls — Run format_for_linkedin(), format_for_x(), and format_for_facebook() concurrently via asyncio.gather() instead of sequentially, reducing total formatting time as more platforms are added
  • Content quality validation — Pre-validate scraped content quality before sending to LLM, preventing wasted LLM calls on low-quality or garbage pages

Architecture Enhancements

  • YAML-based prompt configuration — Move platform prompts from code to YAML config files, enabling non-developers to tweak tone, length, and style without code changes
  • REST API wrapper — Expose the orchestrator as HTTP endpoints via FastAPI, enabling integration with web interfaces and workflow automation tools
  • Lightweight connector pattern — As platform count grows beyond 5+, evaluate a plugin-based connector architecture as an alternative to spinning up one MCP subprocess per platform

Extensibility (No Code Changes)

Goal Change Required
Add a new blog platform New entry in registry/platforms_registry.json
Add a web UI FastAPI layer wrapping the existing MCP tools
Switch to a database Replace record-keeper/server.py internals (same MCP interface)

About

BlogCaster is an intelligent CLI-based agent that automates the process of sharing blog content on LinkedIn, X and Facebook. Built on the MCP architecture, it uses three MCP servers, an RSS Fetcher for parsing blog feeds, a Record Keeper for tracking publishing history, and a Poster for API integration, all coordinated by a central orchestrator.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages