{
  "markdown": "# mcp-fb-publisher\n\n<!-- mcp-name: io.github.anthonyjbolo/mcp-fb-publisher -->\n\n> An [MCP](https://modelcontextprotocol.io) server that lets Claude (or any MCP-compatible LLM) safely publish posts to **multiple Facebook Pages** through the Meta Graph API, with built-in guardrails: brand-voice config, banned-topic blocklists, image-required enforcement, and anti-duplication checks across recent feed posts.\n\n[![Tests](https://github.com/anthonyjbolo/mcp-fb-publisher/actions/workflows/ci.yml/badge.svg)](https://github.com/anthonyjbolo/mcp-fb-publisher/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)\n[![#BuiltOnClaudeCode](https://img.shields.io/badge/Built_on-Claude_Code-blueviolet)](https://claude.com/claude-code)\n\n## Why this exists\n\nLetting an LLM agent post directly to your Facebook Pages is a footgun unless you put guardrails in front. Common failure modes I have hit running 5+ pages:\n\n1. **Token expired silently** — post fails at midnight, nobody notices for 3 days.\n2. **Same angle posted twice in 14 days** — audience tunes out, reach drops.\n3. **Text-only post** when the brand voice mandates an image — engagement craters.\n4. **Banned topic leaked** — competitor name, internal codename, or deprecated product mentioned.\n\n`mcp-fb-publisher` ships an MCP server that wraps the Meta Graph API behind 4 deterministic tools, all driven by a single `config.yaml`. Every publish call goes through validation by default. Validation is pure-Python (no LLM), reproducible in CI, and runs offline.\n\n## What it does\n\n4 MCP tools:\n\n| Tool | What it does |\n|------|--------------|\n| `fb_publish_post` | Publishes (or schedules) a post on a configured page. Runs full validation by default; pass `skip_validation=True` to bypass. |\n| `fb_validate_pre_publish` | Dry-run all guardrails. Returns `verdict: go|block` plus per-check details. Network-optional. |\n| `fb_anti_duplicate_check` | Compares a candidate message against the page's recent posts using Jaccard similarity over word 4-grams. |\n| `fb_generate_post_with_image` | Generates an image via OpenAI (`gpt-image-1`) or fal.ai (`flux-pro`) and returns a URL ready for `fb_publish_post`. |\n\n## 5-minute quickstart\n\n```bash\n# 1. Install\npip install mcp-fb-publisher\n\n# 2. Copy and edit the example config\ncp config.example.yaml config.yaml\n# -> set page_id values, brand voices, banned_topics\n\n# 3. Set required env\nexport META_USER_TOKEN=\"<your long-lived Meta page/user token>\"\nexport MCP_FB_PUBLISHER_CONFIG=\"$PWD/config.yaml\"\n\n# 4. (Optional) for image generation\nexport OPENAI_API_KEY=\"sk-...\"        # or\nexport FAL_KEY=\"...\"\n\n# 5. Run the MCP server (stdio transport)\nmcp-fb-publisher\n```\n\n### Wire it into Claude Desktop / Claude Code\n\nAdd to your `claude_desktop_config.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"fb-publisher\": {\n      \"command\": \"mcp-fb-publisher\",\n      \"env\": {\n        \"META_USER_TOKEN\": \"your_token_here\",\n        \"MCP_FB_PUBLISHER_CONFIG\": \"/absolute/path/to/config.yaml\",\n        \"OPENAI_API_KEY\": \"sk-...\"\n      }\n    }\n  }\n}\n```\n\nThen ask Claude things like:\n\n> *\"Post on the marketing page: 'New collection drops Friday'. Generate an image first, validate, then publish.\"*\n\nClaude will call `fb_generate_post_with_image` → `fb_validate_pre_publish` → `fb_publish_post`.\n\n## Architecture\n\n```mermaid\nflowchart LR\n  A[Claude / MCP client] -->|tool call| B[FastMCP server]\n  B --> C{tool}\n  C -->|generate| D[OpenAI / fal.ai]\n  C -->|validate| E[Validator<br/>pure Python]\n  C -->|anti-dup| F[Meta Graph API<br/>fetch_recent_posts]\n  C -->|publish| G[Meta Graph API<br/>POST /feed or /photos]\n  E -.reads.-> H[(config.yaml<br/>per-page rules)]\n  G -.reads.-> H\n  F -.reads.-> H\n```\n\nThe split between **generation**, **validation** and **publish** is intentional: it lets the LLM iterate on the visual without burning Meta API quota, and it makes the guardrails inspectable in CI without a Meta account.\n\n## Anti-duplication strategy\n\nWe compare the candidate message against every post within `anti_duplicate_lookback_days` (per-page, default 14) using **Jaccard similarity over word 4-grams**:\n\n1. Normalize: lowercase, strip accents (NFKD), drop URLs, drop punctuation, collapse whitespace.\n2. Build the set of word-level 4-grams for both texts.\n3. `similarity = |A ∩ B| / |A ∪ B|`\n4. If `similarity >= similarity_threshold` (default 0.5), block.\n\nWhy this and not embeddings: deterministic, free, no extra API key, fast enough for 50 candidates per call. If you want LLM-grade semantic comparison, add another layer on top — the `fb_validate_pre_publish` tool returns the score so your agent can decide.\n\n## Brand voice config\n\n```yaml\ndefaults:\n  language: en\n  brand_voice: |\n    Direct, professional, no fluff.\n  banned_topics: []\n  image_required: true\n  anti_duplicate_lookback_days: 14\n\npages:\n  marketing_main:\n    page_id: \"0000000000000000\"\n    name: \"My Brand — Main\"\n    brand_voice: |\n      Confident, concise, customer-first.\n    banned_topics:\n      - competitor_brand_a\n      - leaked_codename\n    image_required: true\n\n  community:\n    page_id: \"0000000000000002\"\n    name: \"Community\"\n    image_required: false\n```\n\nNote: the `brand_voice` field is informational — it's surfaced to the calling LLM via the tool description but the server itself does not LLM-validate against it. This is by design (tests must run offline). Layer your own LLM check on top if you want enforcement.\n\n## Use cases\n\n### 1. Multi-page agency\n\nYou manage 5 Facebook pages for clients. Each has its own brand voice, banned topics (competitor names), and image policy. Configure them all in one `config.yaml`, give Claude the tool, and let the agent draft + validate + publish across all of them with safety rails.\n\n### 2. Solo founder\n\nYou run a single product page and want Claude to schedule the next 30 days of posts. Set `image_required: true`, give the agent your product brief, and use `fb_anti_duplicate_check` to make sure no two posts land on the same angle within a fortnight.\n\n### 3. E-commerce\n\nYou rotate flash promos. Set `anti_duplicate_lookback_days: 7` for the promo page and `14` for the evergreen content page. Combine with `scheduled_at` to queue a week's worth of posts in one shot.\n\n## Development\n\n```bash\ngit clone https://github.com/anthonyjbolo/mcp-fb-publisher.git\ncd mcp-fb-publisher\n\npython3.12 -m venv .venv\nsource .venv/bin/activate\npip install -e \".[dev,openai,fal]\"\n\n# Run the test suite (offline, no Meta credentials needed)\npytest\n\n# Lint\nruff check .\n```\n\nAll tests use `httpx.MockTransport` and `pytest-mock`. **No real Meta API calls in tests.**\n\n## Security\n\n- The Meta token is read from `META_USER_TOKEN` only. Never hard-code it.\n- Token strings are redacted from error messages (`***REDACTED***`) before they leave the process.\n- The validator is sync and offline — safe to run in CI without exposing credentials.\n- `config.yaml` is in `.gitignore`. Only `config.example.yaml` is committed.\n\n## Roadmap\n\n- [ ] Instagram Graph API support (the validator already works, only the meta_client publish path needs adapting).\n- [ ] Optional ntfy webhook on publish failure.\n- [ ] LLM-grade brand-voice scoring as an opt-in tool.\n- [ ] Token rotation helper (`fb_check_token_expiry`).\n\n## En français — pourquoi ce projet\n\nConstruit en Nouvelle-Calédonie pour gérer 5 pages FB en parallèle (auto-école, marketplace de bingo, atelier d'apps, marque de tee-shirts, page média). Tous les écueils ci-dessus sont des bugs que j'ai vraiment vécus. Le serveur MCP est la couche que j'aurais aimé avoir le premier jour — maintenant elle est libre.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n\n## Contributing\n\nIssues + PRs welcome. Please run `pytest` and `ruff check .` before opening a PR. For substantial features, open an issue first to discuss.\n\n#BuiltOnClaudeCode\n",
  "bytes": 7977,
  "sha": "a14726b2b7e1c12e0a7a9afc4bb796962fb20498314acda139847871499cdbad",
  "repo_slug": "anthonyjbolo/mcp-fb-publisher",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_anthonyjbolo_mcp_fb_publisher_a68627cc/readme"
}