Skill: jaimin-345/fb-page-publisher (version 1.0.0)

--- name: fb-page-publisher description: "Publish posts, upload photos, schedule content, read insights, and manage comments on Facebook Pages via the Graph API." version: "1.0.0" homepage: "https://clawhub.ai/Yashthesiya1/fb-page-publisher" user-invocable: true metadata: openclaw: os: - windows - macos - linux requires: bins: - uv env: - FB_PAGE_ID - FB_ACCESS_TOKEN emoji: "📘" tags: - facebook - social-media - marketing - content-publishing - graph-api --- # Facebook Page Publisher Manage your Facebook Page directly from an AI agent. Create posts, upload photos, schedule content, view analytics, and moderate comments -- all through natural language. ## Required Environment Variables - `FB_PAGE_ID` — The numeric ID of your Facebook Page - `FB_ACCESS_TOKEN` — A long-lived (non-expiring) Facebook Page Access Token with permissions: pages_manage_posts, pages_read_engagement, pages_manage_engagement ## create_post Create and immediately publish a text post to the Facebook Page. - `message` (string, required): The text content of the post. ## upload_photo_post Upload a photo to the Facebook Page with an optional caption. The photo must be a publicly accessible URL. - `photo_url` (string, required): A publicly accessible URL to the image file (JPEG, PNG, etc.). - `caption` (string, optional): Text caption for the photo post. ## schedule_post Schedule a text post for future publication. Time must be at least 10 minutes ahead and no more than 6 months. - `message` (string, required): The text content of the post. - `scheduled_time` (string, required): ISO 8601 datetime (e.g., "2026-03-10T09:00:00"). ## get_page_insights Retrieve engagement metrics (impressions, reach, engagement, views) for the Facebook Page. - `metric` (string, optional): One of "impressions", "reach", "engagement", "views", or "all". Default: "all". - `period` (string, optional): Time period: "day", "week", or "days_28". Default: "day". ## get_recent_posts Fetch the most recent posts from the Page with engagement statistics (likes, comments, shares). - `limit` (integer, optional): Number of posts to retrieve (1-100). Default: 10. ## delete_post Delete a specific post from the Facebook Page. This action is irreversible. - `post_id` (string, required): The full post ID (format: pageId_postId). ## get_post_comments Retrieve comments on a specific post. - `post_id` (string, required): The full post ID (format: pageId_postId). - `limit` (integer, optional): Number of comments to retrieve (1-100). Default: 25. ## reply_to_comment Reply to a comment on a post as the Page. - `comment_id` (string, required): The ID of the comment to reply to. - `message` (string, required): The reply message text. ## Example Usage ```text User: Post "We're hiring! Check our careers page." to my Facebook page. Agent: I'll publish that now using create_post. User: Schedule a post for tomorrow at 9 AM saying "Flash sale starts now!" Agent: I'll schedule that using schedule_post with the computed datetime. User: Show me my page insights for the past week. Agent: I'll fetch that using get_page_insights with period="week". ``` ## Setup 1. Set `FB_PAGE_ID` to your Facebook Page's numeric ID. 2. Set `FB_ACCESS_TOKEN` to a non-expiring Page Access Token. 3. Run with `uv run src/server.py`.

path: pyproject.toml
content: [project]
name = "fb-page-publisher"
version = "1.0.0"
description = "OpenClaw MCP skill for publishing content to Facebook Pages via the Graph API"
readme = "README.md"
license = { text = "MIT" }
requires-python = ">=3.12"
dependencies = [
    "mcp[cli]>=1.2.0",
    "httpx>=0.27.0",
    "python-dotenv>=1.0.0",
]

[project.scripts]
fb-page-publisher = "src.server:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src"]


path: README.md
content: # Facebook Page Publisher - OpenClaw MCP Skill

Manage your Facebook Page directly from an AI agent. Create posts, upload photos, schedule content, view analytics, and moderate comments -- all through natural language.

## Features

| Tool | Description |
|------|-------------|
| `create_post` | Publish a text post immediately |
| `upload_photo_post` | Upload a photo with optional caption |
| `schedule_post` | Schedule a post for future publication |
| `get_page_insights` | View page engagement metrics |
| `get_recent_posts` | List recent posts with engagement stats |
| `delete_post` | Remove a post from the page |
| `get_post_comments` | Read comments on a post |
| `reply_to_comment` | Reply to a comment as the Page |

## Quick Start

### 1. Install dependencies

```bash
cd fb-page-publisher
uv sync
```

### 2. Configure environment

```bash
cp .env.example .env
# Edit .env with your FB_PAGE_ID and FB_ACCESS_TOKEN
```

### 3. Get your Facebook credentials

1. Go to [developers.facebook.com](https://developers.facebook.com) and create a Business app
2. Open [Graph API Explorer](https://developers.facebook.com/tools/explorer/)
3. Select your app, generate a token with permissions:
   - `pages_manage_posts`
   - `pages_read_engagement`
   - `pages_manage_engagement`
4. Call `GET /me/accounts` to get your Page ID and Page Access Token
5. Extend to a long-lived token (see blueprint docs for details)

### 4. Run the server

```bash
uv run src/server.py
```

### 5. Test with MCP Inspector

```bash
npx @modelcontextprotocol/inspector uv run src/server.py
```

## Claude Desktop Configuration

Add to your `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "fb-page-publisher": {
      "command": "uv",
      "args": [
        "--directory",
        "/absolute/path/to/fb-page-publisher",
        "run",
        "src/server.py"
      ],
      "env": {
        "FB_PAGE_ID": "your_page_id",
        "FB_ACCESS_TOKEN": "your_access_token"
      }
    }
  }
}
```

## Example Prompts

- "Post 'Happy Monday!' to my Facebook page"
- "Upload this image to my FB page with caption 'New product launch'"
- "Schedule a post for tomorrow at 9 AM"
- "Show me my page insights for the last week"
- "Show my last 5 Facebook posts"
- "Reply 'Thank you!' to the latest comment on my recent post"

## Publishing to ClawHub

```bash
clawhub login
clawhub validate .
npx clawhub publish "D:\My_Work\Open-Claw\fb-page-publisher" --slug "fb-page-publisher" --version "1.0.0"
```

## License

MIT


path: src/fb_client.py
content: """
Facebook Graph API client for Page management.

Handles all HTTP communication with the Facebook Graph API v21.0.
Every public method returns a dict on success or raises FacebookAPIError on failure.
"""

from __future__ import annotations

import logging
from datetime import datetime, timezone
from typing import Any

import httpx

logger = logging.getLogger("fb-page-publisher")

BASE_URL = "https://graph.facebook.com/v21.0"

PAGE_INSIGHT_METRICS = {
    "impressions": "page_impressions",
    "reach": "page_impressions_unique",
    "engagement": "page_post_engagements",
    "views": "page_views_total",
}

POST_FIELDS = (
    "id,message,created_time,permalink_url,"
    "shares,likes.limit(0).summary(true),"
    "comments.limit(0).summary(true)"
)

COMMENT_FIELDS = "id,from,message,created_time,like_count"


class FacebookAPIError(Exception):
    """Raised when the Facebook Graph API returns an error response."""

    def __init__(self, message: str, error_code: int | None = None, error_subcode: int | None = None):
        self.error_code = error_code
        self.error_subcode = error_subcode
        super().__init__(message)


class FacebookClient:
    """HTTP client wrapping the Facebook Graph API v21.0."""

    def __init__(self, page_id: str, access_token: str) -> None:
        if not page_id or not page_id.strip():
            raise ValueError("page_id must be a non-empty string")
        if not access_token or not access_token.strip():
            raise ValueError("access_token must be a non-empty string")

        self.page_id = page_id.strip()
        self.access_token = access_token.strip()
        self._client = httpx.AsyncClient(
            base_url=BASE_URL,
            timeout=httpx.Timeout(30.0, connect=10.0),
            headers={"User-Agent": "fb-page-publisher/1.0.0"},
        )

    def _auth_params(self) -> dict[str, str]:
        return {"access_token": self.access_token}

    async def _request(
        self,
        method: str,
        endpoint: str,
        *,
        params: dict[str, Any] | None = None,
        data: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        merged_params = {**self._auth_params(), **(params or {})}

        response = await self._client.request(
            method,
            endpoint,
            params=merged_params,
            data=data,
        )

        try:
            body = response.json()
        except Exception:
            response.raise_for_status()
            return {}

        if "error" in body:
            err = body["error"]
            raise FacebookAPIError(
                message=err.get("message", "Unknown Facebook API error"),
                error_code=err.get("code"),
                error_subcode=err.get("error_subcode"),
            )

        response.raise_for_status()
        return body

    # -- Posts --

    async def create_post(self, message: str) -> dict[str, Any]:
        if not message or not message.strip():
            raise ValueError("message must be a non-empty string")

        result = await self._request(
            "POST",
            f"/{self.page_id}/feed",
            data={"message": message.strip()},
        )
        logger.info("Created post %s", result.get("id"))
        return result

    async def upload_photo(self, photo_url: str, caption: str | None = None) -> dict[str, Any]:
        if not photo_url or not photo_url.strip():
            raise ValueError("photo_url must be a non-empty string")

        payload: dict[str, str] = {"url": photo_url.strip()}
        if caption and caption.strip():
            payload["caption"] = caption.strip()

        result = await self._request(
            "POST",
            f"/{self.page_id}/photos",
            data=payload,
        )
        logger.info("Uploaded photo %s (post %s)", result.get("id"), result.get("post_id"))
        return result

    async def schedule_post(self, message: str, scheduled_time: str) -> dict[str, Any]:
        if not message or not message.strip():
            raise ValueError("message must be a non-empty string")

        try:
            dt = datetime.fromisoformat(scheduled_time)
        except (ValueError, TypeError) as exc:
            raise ValueError(
                f"Invalid datetime format: '{scheduled_time}'. "
                "Use ISO 8601 format, e.g. '2026-03-10T09:00:00'."
            ) from exc

        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=timezone.utc)

        now = datetime.now(timezone.utc)
        diff_seconds = (dt - now).total_seconds()

        if diff_seconds < 600:
            raise ValueError(
                "Scheduled time must be at least 10 minutes in the future. "
                f"Provided: {scheduled_time}"
            )

        if diff_seconds > 15_552_000:
            raise ValueError(
                "Scheduled time must be within 6 months from now. "
                f"Provided: {scheduled_time}"
            )

        unix_timestamp = int(dt.timestamp())

        result = await self._request(
            "POST",
            f"/{self.page_id}/feed",
            data={
                "message": message.strip(),
                "published": "false",
                "scheduled_publish_time": str(unix_timestamp),
            },
        )
        logger.info("Scheduled post %s for %s", result.get("id"), scheduled_time)
        return result

    # -- Insights --

    async def get_insights(self, metric: str = "all", period: str = "day") -> dict[str, Any]:
        valid_periods = {"day", "week", "days_28"}
        if period not in valid_periods:
            raise ValueError(f"period must be one of {valid_periods}, got '{period}'")

        if metric == "all":
            metric_names = ",".join(PAGE_INSIGHT_METRICS.values())
        elif metric in PAGE_INSIGHT_METRICS:
            metric_names = PAGE_INSIGHT_METRICS[metric]
        else:
            raise ValueError(
                f"metric must be one of {list(PAGE_INSIGHT_METRICS.keys())} or 'all', "
                f"got '{metric}'"
            )

        return await self._request(
            "GET",
            f"/{self.page_id}/insights",
            params={"metric": metric_names, "period": period},
        )

    # -- Read Posts --

    async def get_posts(self, limit: int = 10) -> dict[str, Any]:
        limit = max(1, min(100, limit))
        return await self._request(
            "GET",
            f"/{self.page_id}/posts",
            params={"fields": POST_FIELDS, "limit": str(limit)},
        )

    # -- Delete --

    async def delete_post(self, post_id: str) -> dict[str, Any]:
        if not post_id or not post_id.strip():
            raise ValueError("post_id must be a non-empty string")

        result = await self._request("DELETE", f"/{post_id.strip()}")
        logger.info("Deleted post %s", post_id)
        return result

    # -- Comments --

    async def get_comments(self, post_id: str, limit: int = 25) -> dict[str, Any]:
        if not post_id or not post_id.strip():
            raise ValueError("post_id must be a non-empty string")

        limit = max(1, min(100, limit))
        return await self._request(
            "GET",
            f"/{post_id.strip()}/comments",
            params={"fields": COMMENT_FIELDS, "limit": str(limit)},
        )

    async def reply_comment(self, comment_id: str, message: str) -> dict[str, Any]:
        if not comment_id or not comment_id.strip():
            raise ValueError("comment_id must be a non-empty string")
        if not message or not message.strip():
            raise ValueError("message must be a non-empty string")

        result = await self._request(
            "POST",
            f"/{comment_id.strip()}/comments",
            data={"message": message.strip()},
        )
        logger.info("Replied to comment %s with comment %s", comment_id, result.get("id"))
        return result

    # -- Lifecycle --

    async def close(self) -> None:
        await self._client.aclose()


path: src/server.py
content: """
FastMCP server for the Facebook Page Publisher skill.

Usage:
    uv run src/server.py
"""

from __future__ import annotations

import logging
import os
import sys

from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP

from .fb_client import FacebookClient
from . import tools as tool_handlers

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
)
logger = logging.getLogger("fb-page-publisher")

load_dotenv()

FB_PAGE_ID = os.environ.get("FB_PAGE_ID", "")
FB_ACCESS_TOKEN = os.environ.get("FB_ACCESS_TOKEN", "")

if not FB_PAGE_ID:
    logger.error("FB_PAGE_ID environment variable is not set.")
    sys.exit(1)

if not FB_ACCESS_TOKEN:
    logger.error("FB_ACCESS_TOKEN environment variable is not set.")
    sys.exit(1)

fb_client = FacebookClient(page_id=FB_PAGE_ID, access_token=FB_ACCESS_TOKEN)

mcp = FastMCP("Facebook Page Publisher")


@mcp.tool()
async def create_post(message: str) -> str:
    """
    Create and immediately publish a text post to the Facebook Page.

    Args:
        message: The text content of the post to publish.
    """
    return await tool_handlers.create_post(fb_client, message)


@mcp.tool()
async def upload_photo_post(photo_url: str, caption: str = "") -> str:
    """
    Upload a photo to the Facebook Page with an optional caption.
    The photo must be a publicly accessible URL (https://).

    Args:
        photo_url: A publicly accessible URL pointing to the image file (JPEG, PNG, etc.).
        caption: Optional text caption to accompany the photo.
    """
    return await tool_handlers.upload_photo_post(
        fb_client, photo_url, caption if caption else None
    )


@mcp.tool()
async def schedule_post(message: str, scheduled_time: str) -> str:
    """
    Schedule a text post for future publication on the Facebook Page.
    The time must be at least 10 minutes in the future and at most 6 months ahead.

    Args:
        message: The text content of the post.
        scheduled_time: When to publish, in ISO 8601 format (e.g., '2026-03-10T09:00:00').
    """
    return await tool_handlers.schedule_post(fb_client, message, scheduled_time)


@mcp.tool()
async def get_page_insights(metric: str = "all", period: str = "day") -> str:
    """
    Retrieve engagement metrics (impressions, reach, engagement, views) for the Facebook Page.

    Args:
        metric: The metric to retrieve -- 'impressions', 'reach', 'engagement', 'views', or 'all'.
        period: Time aggregation period -- 'day', 'week', or 'days_28'.
    """
    return await tool_handlers.get_page_insights(fb_client, metric, period)


@mcp.tool()
async def get_recent_posts(limit: int = 10) -> str:
    """
    Fetch the most recent posts from the Facebook Page with engagement statistics
    (likes, comments, shares).

    Args:
        limit: Number of posts to retrieve, between 1 and 100. Defaults to 10.
    """
    return await tool_handlers.get_recent_posts(fb_client, limit)


@mcp.tool()
async def delete_post(post_id: str) -> str:
    """
    Delete a specific post from the Facebook Page. This action is irreversible.

    Args:
        post_id: The full post ID in the format 'pageId_postId' (e.g., '123456789_987654321').
    """
    return await tool_handlers.delete_post(fb_client, post_id)


@mcp.tool()
async def get_post_comments(post_id: str, limit: int = 25) -> str:
    """
    Retrieve comments on a specific Facebook Page post.

    Args:
        post_id: The full post ID in the format 'pageId_postId'.
        limit: Number of comments to retrieve, between 1 and 100. Defaults to 25.
    """
    return await tool_handlers.get_post_comments(fb_client, post_id, limit)


@mcp.tool()
async def reply_to_comment(comment_id: str, message: str) -> str:
    """
    Reply to a comment on a Facebook Page post, posting as the Page itself.

    Args:
        comment_id: The ID of the comment to reply to.
        message: The reply message text.
    """
    return await tool_handlers.reply_to_comment(fb_client, comment_id, message)


def main() -> None:
    """Run the MCP server over stdio."""
    logger.info("Starting Facebook Page Publisher MCP server...")
    logger.info("Page ID: %s", FB_PAGE_ID[:6] + "..." if len(FB_PAGE_ID) > 6 else FB_PAGE_ID)
    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()


path: src/tools.py
content: """
MCP tool implementations for the Facebook Page Publisher skill.

Each function accepts primitive arguments, calls the FacebookClient,
and returns a human-readable formatted string for the AI agent.
"""

from __future__ import annotations

from datetime import datetime
from .fb_client import FacebookClient, FacebookAPIError


def _format_error(exc: Exception) -> str:
    if isinstance(exc, FacebookAPIError):
        parts = [f"Facebook API Error: {exc}"]
        if exc.error_code:
            parts.append(f"Error code: {exc.error_code}")
        if exc.error_subcode:
            parts.append(f"Error subcode: {exc.error_subcode}")
        return "\n".join(parts)
    if isinstance(exc, ValueError):
        return f"Validation Error: {exc}"
    return f"Unexpected Error: {type(exc).__name__}: {exc}"


def _format_datetime(iso_string: str) -> str:
    try:
        dt = datetime.fromisoformat(iso_string.replace("Z", "+00:00"))
        return dt.strftime("%B %d, %Y at %I:%M %p %Z")
    except Exception:
        return iso_string


def _truncate(text: str, max_length: int = 120) -> str:
    if len(text) <= max_length:
        return text
    return text[: max_length - 3] + "..."


async def create_post(client: FacebookClient, message: str) -> str:
    try:
        result = await client.create_post(message)
        post_id = result.get("id", "unknown")
        return (
            f"Post published successfully!\n"
            f"Post ID: {post_id}\n"
            f"URL: https://www.facebook.com/{post_id.replace('_', '/posts/')}\n"
            f"Content: {_truncate(message)}"
        )
    except Exception as exc:
        return _format_error(exc)


async def upload_photo_post(
    client: FacebookClient,
    photo_url: str,
    caption: str | None = None,
) -> str:
    try:
        result = await client.upload_photo(photo_url, caption)
        photo_id = result.get("id", "unknown")
        post_id = result.get("post_id", "unknown")
        lines = [
            "Photo uploaded successfully!",
            f"Photo ID: {photo_id}",
            f"Post ID: {post_id}",
            f"Image URL: {photo_url}",
        ]
        if caption:
            lines.append(f"Caption: {_truncate(caption)}")
        return "\n".join(lines)
    except Exception as exc:
        return _format_error(exc)


async def schedule_post(
    client: FacebookClient,
    message: str,
    scheduled_time: str,
) -> str:
    try:
        result = await client.schedule_post(message, scheduled_time)
        post_id = result.get("id", "unknown")
        return (
            f"Post scheduled successfully!\n"
            f"Post ID: {post_id}\n"
            f"Scheduled for: {_format_datetime(scheduled_time)}\n"
            f"Content: {_truncate(message)}\n"
            f"Note: Manage scheduled posts from your Facebook Page's Publishing Tools."
        )
    except Exception as exc:
        return _format_error(exc)


async def get_page_insights(
    client: FacebookClient,
    metric: str = "all",
    period: str = "day",
) -> str:
    try:
        result = await client.get_insights(metric, period)
        data = result.get("data", [])

        if not data:
            return f"No insight data available for metric='{metric}', period='{period}'."

        period_labels = {"day": "Daily", "week": "Weekly", "days_28": "28-Day"}
        period_label = period_labels.get(period, period)

        lines = [f"Facebook Page Insights ({period_label})", "=" * 45]

        for item in data:
            name = item.get("title", item.get("name", "Unknown Metric"))
            description = item.get("description", "")
            values = item.get("values", [])

            lines.append(f"\n{name}")
            if description:
                lines.append(f"  {description}")

            if values:
                latest = values[-1]
                value = latest.get("value", "N/A")
                end_time = latest.get("end_time", "")
                if end_time:
                    lines.append(f"  Value: {value} (as of {_format_datetime(end_time)})")
                else:
                    lines.append(f"  Value: {value}")

                if len(values) >= 2:
                    prev_value = values[-2].get("value", 0)
                    curr_value = latest.get("value", 0)
                    if isinstance(prev_value, (int, float)) and isinstance(curr_value, (int, float)):
                        if prev_value > 0:
                            change_pct = ((curr_value - prev_value) / prev_value) * 100
                            direction = "up" if change_pct > 0 else "down" if change_pct < 0 else "flat"
                            lines.append(f"  Trend: {direction} {abs(change_pct):.1f}% from previous period")

        return "\n".join(lines)
    except Exception as exc:
        return _format_error(exc)


async def get_recent_posts(client: FacebookClient, limit: int = 10) -> str:
    try:
        result = await client.get_posts(limit)
        posts = result.get("data", [])

        if not posts:
            return "No posts found on this Page."

        lines = [f"Recent Facebook Page Posts (showing {len(posts)})", "=" * 50]

        for i, post in enumerate(posts, 1):
            post_id = post.get("id", "unknown")
            message = post.get("message", "(no text)")
            created = post.get("created_time", "unknown")
            permalink = post.get("permalink_url", "")

            likes_count = post.get("likes", {}).get("summary", {}).get("total_count", 0)
            comments_count = post.get("comments", {}).get("summary", {}).get("total_count", 0)
            shares_count = post.get("shares", {}).get("count", 0)

            lines.append(f"\n--- Post #{i} ---")
            lines.append(f"ID: {post_id}")
            lines.append(f"Date: {_format_datetime(created)}")
            lines.append(f"Text: {_truncate(message, 200)}")
            lines.append(f"Likes: {likes_count} | Comments: {comments_count} | Shares: {shares_count}")
            if permalink:
                lines.append(f"Link: {permalink}")

        return "\n".join(lines)
    except Exception as exc:
        return _format_error(exc)


async def delete_post(client: FacebookClient, post_id: str) -> str:
    try:
        result = await client.delete_post(post_id)
        if result.get("success", False):
            return f"Post {post_id} has been deleted successfully."
        return f"Deletion request sent for post {post_id}, but API did not confirm success. Response: {result}"
    except Exception as exc:
        return _format_error(exc)


async def get_post_comments(
    client: FacebookClient,
    post_id: str,
    limit: int = 25,
) -> str:
    try:
        result = await client.get_comments(post_id, limit)
        comments = result.get("data", [])

        if not comments:
            return f"No comments found on post {post_id}."

        lines = [
            f"Comments on post {post_id} (showing {len(comments)})",
            "=" * 50,
        ]

        for i, comment in enumerate(comments, 1):
            comment_id = comment.get("id", "unknown")
            author = comment.get("from", {}).get("name", "Unknown User")
            message = comment.get("message", "(empty)")
            created = comment.get("created_time", "unknown")
            like_count = comment.get("like_count", 0)

            lines.append(f"\n--- Comment #{i} ---")
            lines.append(f"ID: {comment_id}")
            lines.append(f"Author: {author}")
            lines.append(f"Date: {_format_datetime(created)}")
            lines.append(f"Message: {message}")
            lines.append(f"Likes: {like_count}")

        return "\n".join(lines)
    except Exception as exc:
        return _format_error(exc)


async def reply_to_comment(
    client: FacebookClient,
    comment_id: str,
    message: str,
) -> str:
    try:
        result = await client.reply_comment(comment_id, message)
        reply_id = result.get("id", "unknown")
        return (
            f"Reply posted successfully!\n"
            f"Reply ID: {reply_id}\n"
            f"In response to comment: {comment_id}\n"
            f"Reply text: {_truncate(message)}"
        )
    except Exception as exc:
        return _format_error(exc)


path: src/__init__.py
content: """Facebook Page Publisher - OpenClaw MCP Skill."""


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)