AI Tooling

Social MCP Ops Product Bible

A product bible for operating a social media MCP (Model Context Protocol) tool: scope, workflows, and governance for AI-assisted social operations.

Free, no email requiredRead onlineAI Tooling

Who it is for

Teams building or evaluating AI-assisted social media tooling.

After reading it

You will understand the operating model for a governed, AI-assisted social media tool, from scope to workflow.

1. Product Thesis

Build a local-first MCP-enabled social operations system that can observe platform events, classify intent, propose actions, route public/consumer-facing operations for human approval, learn operator preferences over time, and execute only approved or tightly scoped auto-approved actions through authorized connectors.

The system must treat every platform action as a governed workflow, not a direct bot action.

2. Core Principles

  1. Observe first, act later. Inbound posts, comments, messages, mentions, and search hits are stored as observations before any action is proposed.
  2. Propose-first MCP tools. MCP tools create observations, action candidates, drafts, approvals, and execution requests. They do not directly post public content unless an approved execution token exists.
  3. Official APIs first. LinkedIn, Meta/Facebook/Instagram, Nextdoor, X, Slack, and Telegram should be integrated through supported APIs and approved permissions wherever possible.
  4. Browser automation is an exception. Browser automation and scraping are only allowed for explicitly authorized, low-risk, read-only or operator-assisted flows.
  5. Human approval by default. Public replies, posts, DMs, solicitations, comments, moderation, and account-facing operations require approval unless a specific auto-approval policy permits them.
  6. Every action is auditable. Store the originating observation, draft, revisions, operator decision, final payload, execution result, platform object ID, and errors.
  7. Preference learning starts as retrieval. Store approved/rejected examples and retrieve similar prior examples for prompt grounding before considering fine-tuning.
  8. Platform capabilities are explicit. Never assume a platform/account can read, reply, DM, moderate, post, like, or scrape. The capability registry is the source of truth.

3. Target Deployment

Local MVP

  • Raspberry Pi 5 or equivalent ARM64 device
  • Docker Compose
  • Python 3.12+
  • PostgreSQL 16+
  • pgvector
  • pgmq if available; otherwise use a tasks table plus FOR UPDATE SKIP LOCKED
  • FastAPI
  • MCP Python SDK / FastMCP
  • Slack approval integration
  • Telegram fallback integration

Cloud Phase

  • Same containers deployed to VPS, Fly.io, Render, AWS ECS, Kubernetes, or similar
  • Managed Postgres
  • HTTPS through Caddy, Traefik, or cloud ingress
  • Secrets manager
  • Centralized logs
  • Backups
  • OAuth callback domains

4. System Architecture


MCP Client / Agent
        |
        v
MCP Server
        |
        v
Action Gateway
        |
        +--------------------+
        |                    |
        v                    v
Observation Service      Direct Draft Tools
        |                    |
        v                    v
Observation Store       Action Candidate Store
        |                    |
        +----------+---------+
                   |
                   v
Classifier + Planner
                   |
                   v
Risk Grade Engine
                   |
                   v
Approval Queue
        |--------------------|
        v                    v
Slack Approval        Telegram Approval
        |                    |
        +----------+---------+
                   |
                   v
Execution Worker
                   |
                   v
Platform Adapter Layer
                   |
                   v
LinkedIn / Meta / Nextdoor / X / Slack / Telegram / Other

5. Bounded Contexts

5.1 Identity and Access

Manages operators, platform accounts, OAuth tokens, account roles, and permission snapshots.

5.2 Observation

Reads platform events and stores them in a normalized form.

5.3 Classification

Determines intent, urgency, sentiment, risk, relevance, and whether an action should be proposed.

5.4 Action Planning

Creates one or more action candidates for an observation.

5.5 Approval

Routes action candidates to Slack or Telegram, captures approvals, edits, rejections, and escalations.

5.6 Execution

Executes only approved or auto-approved action candidates.

5.7 Memory

Stores prior drafts, edits, approvals, rejections, embeddings, and operator preferences.

5.8 Compliance and Audit

Stores policy findings, capability checks, blocked actions, raw payload hashes, and execution records.

6. Canonical Domain Model

6.1 Platform


linkedin
facebook
instagram
threads
nextdoor
x
youtube
tiktok
slack
telegram
manual

6.2 Surface Type


profile
organization_page
facebook_page
facebook_group
instagram_business_account
nextdoor_public_search
nextdoor_business
x_filtered_stream
x_user_timeline
slack_channel
telegram_chat
manual_import

6.3 Observation Event Types


new_post
new_comment
new_reply
new_message
new_mention
new_reaction
post_updated
comment_updated
message_updated
post_deleted
comment_deleted
manual_import

6.4 Action Types


NO_OP
CREATE_PUBLIC_POST
CREATE_PUBLIC_REPLY
CREATE_COMMENT
CREATE_COMMENT_REPLY
CREATE_DIRECT_MESSAGE
RESHARE_OR_QUOTE
LIKE_OR_REACT
HIDE_COMMENT
UNHIDE_COMMENT
DELETE_COMMENT
DELETE_POST
ESCALATE_TO_OPERATOR
CREATE_SUPPORT_TICKET
CREATE_LEAD
REQUEST_MORE_INFO
BLOCKED_UNSUPPORTED

6.5 Intent Classes


support_request
sales_lead
solicitation_opportunity
complaint
reputation_risk
comment_on_our_post
reply_to_our_prior_reply
spam_or_abuse
urgent_safety_issue
partnership_opportunity
competitor_mention
local_service_request
praise_or_testimonial
question
no_action
unknown

6.6 Risk Grades


A = low-risk, template-like, high prior approval similarity, official API, operator-enabled auto mode possible
B = low-risk but moderate novelty, may be auto-eligible with sampling
C = normal public-facing action; human approval required
D = high-impact, sensitive, complaint, solicitation, or uncertain; senior/operator approval required
F = unsupported, unauthorized, spam-like, platform-risky, unsafe, or blocked

7. Adapter Architecture

Every platform adapter must implement the same interface shape.

7.1 Python Protocol


from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import Any, Protocol, Sequence
from uuid import UUID


class Platform(str, Enum):
    LINKEDIN = "linkedin"
    FACEBOOK = "facebook"
    INSTAGRAM = "instagram"
    THREADS = "threads"
    NEXTDOOR = "nextdoor"
    X = "x"
    SLACK = "slack"
    TELEGRAM = "telegram"
    MANUAL = "manual"


class ActionType(str, Enum):
    NO_OP = "NO_OP"
    CREATE_PUBLIC_POST = "CREATE_PUBLIC_POST"
    CREATE_PUBLIC_REPLY = "CREATE_PUBLIC_REPLY"
    CREATE_COMMENT = "CREATE_COMMENT"
    CREATE_COMMENT_REPLY = "CREATE_COMMENT_REPLY"
    CREATE_DIRECT_MESSAGE = "CREATE_DIRECT_MESSAGE"
    RESHARE_OR_QUOTE = "RESHARE_OR_QUOTE"
    LIKE_OR_REACT = "LIKE_OR_REACT"
    HIDE_COMMENT = "HIDE_COMMENT"
    UNHIDE_COMMENT = "UNHIDE_COMMENT"
    DELETE_COMMENT = "DELETE_COMMENT"
    DELETE_POST = "DELETE_POST"
    ESCALATE_TO_OPERATOR = "ESCALATE_TO_OPERATOR"
    CREATE_SUPPORT_TICKET = "CREATE_SUPPORT_TICKET"
    CREATE_LEAD = "CREATE_LEAD"
    REQUEST_MORE_INFO = "REQUEST_MORE_INFO"
    BLOCKED_UNSUPPORTED = "BLOCKED_UNSUPPORTED"


@dataclass(frozen=True)
class Capability:
    platform: Platform
    account_id: UUID
    surface_type: str
    can_read_posts: bool = False
    can_read_comments: bool = False
    can_read_messages: bool = False
    can_receive_webhooks: bool = False
    can_poll: bool = False
    can_stream: bool = False
    can_post: bool = False
    can_comment: bool = False
    can_reply: bool = False
    can_dm: bool = False
    can_hide: bool = False
    can_delete: bool = False
    can_like_or_react: bool = False
    requires_app_review: bool = True
    requires_user_auth: bool = True
    requires_page_admin: bool = False
    prohibited_reason: str | None = None
    last_verified_at: datetime | None = None


@dataclass(frozen=True)
class Observation:
    platform: Platform
    account_id: UUID
    event_type: str
    surface_type: str
    source_id: str | None
    platform_object_id: str
    parent_object_id: str | None
    author_platform_id_hash: str | None
    author_display_name: str | None
    content_text: str | None
    content_media: list[dict[str, Any]]
    visibility: str
    observed_at: datetime
    received_via: str
    raw_payload: dict[str, Any]


@dataclass(frozen=True)
class ActionCandidate:
    observation_id: UUID | None
    platform: Platform
    account_id: UUID
    action_type: ActionType
    target_object_id: str | None
    draft_payload: dict[str, Any]
    reason: str
    idempotency_key: str


@dataclass(frozen=True)
class ExecutionResult:
    ok: bool
    platform: Platform
    platform_object_id: str | None
    url: str | None
    raw_response: dict[str, Any] | None
    error_code: str | None
    error_message: str | None


class PlatformAdapter(Protocol):
    platform: Platform

    async def get_capabilities(self, account_id: UUID) -> Sequence[Capability]: ...

    async def validate_action(self, candidate: ActionCandidate) -> tuple[bool, list[str]]: ...

    async def observe(self, account_id: UUID, cursor: str | None = None) -> tuple[list[Observation], str | None]: ...

    async def search(self, account_id: UUID, query: dict[str, Any]) -> list[Observation]: ...

    async def dry_run(self, candidate: ActionCandidate) -> dict[str, Any]: ...

    async def execute(self, candidate: ActionCandidate) -> ExecutionResult: ...

7.2 Adapter Rules

  1. validate_action() must run before grading.
  2. dry_run() must run before operator approval.
  3. execute() must require an approved action_candidate_id or signed execution token.
  4. Adapters must return structured errors, not raw stack traces.
  5. All platform IDs should be stored as strings.
  6. Raw payloads should be stored for audit, but sensitive tokens must never be stored in raw payloads.

8. Platform Adapter Capability Matrix

PlatformRead postsRead commentsRead DMsWebhook/streamPostComment/replyDMNotes
LinkedIn OrganizationYes, with approved permissionsYes, on accessible postsLimited/no generic DMPolling-orientedYesYesNot generalStrong for organization content; broad scraping is not allowed.
Facebook PageYes, Page-scopedYes, Page-scopedMessenger Page-scopedWebhooks where approvedYesYesYes, constrainedFocus on Pages, not Groups.
Facebook GroupsGenerally no for new buildsLimited/unsupportedNoNo reliable pathAvoidAvoidNoTreat as unsupported unless official app permissions exist.
Instagram BusinessYes, account/media scopedYes, media commentsLimitedWebhooks where approvedYes through media container flowYes on media commentsLimitedBusiness/creator only.
NextdoorPublic content search with accessDepends on accessNo genericPolling/searchPublish API with approvalComments with approved APINo genericPublic Search API and Publish API are access-gated.
XYes via filtered stream/searchReplies are postsDM requires accessStrong stream/webhookYesYesAccess-dependentBest pilot for streaming detection.
SlackChannels/messages where bot installedThreads/reactionsDMs with botEvents APIMessagesThread repliesYesPrimary approval channel.
TelegramBot chatsReplies/messages to botBot chatWebhooks/long pollingBot messagesBot repliesYesFallback approval channel.

9. Database Schema

9.1 Extensions


CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS vector;
-- Optional if available:
-- CREATE EXTENSION IF NOT EXISTS pgmq;

9.2 Enum Types


CREATE TYPE platform AS ENUM (
  'linkedin', 'facebook', 'instagram', 'threads', 'nextdoor', 'x', 'slack', 'telegram', 'manual'
);

CREATE TYPE task_status AS ENUM (
  'draft', 'queued', 'awaiting_approval', 'approved', 'rejected', 'executing', 'succeeded', 'failed', 'blocked', 'expired'
);

CREATE TYPE risk_grade AS ENUM ('A', 'B', 'C', 'D', 'F');

CREATE TYPE approval_decision AS ENUM ('approve', 'edit', 'reject', 'escalate', 'expire');

9.3 Operators


CREATE TABLE operators (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  display_name text NOT NULL,
  email text UNIQUE,
  slack_user_id text UNIQUE,
  telegram_user_id text UNIQUE,
  role text NOT NULL DEFAULT 'operator',
  is_active boolean NOT NULL DEFAULT true,
  created_at timestamptz NOT NULL DEFAULT now(),
  updated_at timestamptz NOT NULL DEFAULT now()
);

9.4 Platform Accounts


CREATE TABLE platform_accounts (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  platform platform NOT NULL,
  external_account_id text NOT NULL,
  display_name text NOT NULL,
  surface_type text NOT NULL,
  owner_operator_id uuid REFERENCES operators(id),
  auth_status text NOT NULL DEFAULT 'unknown',
  metadata jsonb NOT NULL DEFAULT '{}',
  created_at timestamptz NOT NULL DEFAULT now(),
  updated_at timestamptz NOT NULL DEFAULT now(),
  UNIQUE(platform, external_account_id, surface_type)
);

9.5 Credential Metadata

Store encrypted tokens outside this table if possible. This table stores references and scope snapshots only.


CREATE TABLE credential_metadata (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  platform_account_id uuid NOT NULL REFERENCES platform_accounts(id) ON DELETE CASCADE,
  secret_ref text NOT NULL,
  scopes text[] NOT NULL DEFAULT '{}',
  token_type text,
  expires_at timestamptz,
  last_rotated_at timestamptz,
  created_at timestamptz NOT NULL DEFAULT now(),
  updated_at timestamptz NOT NULL DEFAULT now()
);

9.6 Platform Capabilities


CREATE TABLE platform_capabilities (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  platform_account_id uuid NOT NULL REFERENCES platform_accounts(id) ON DELETE CASCADE,
  surface_type text NOT NULL,
  can_read_posts boolean NOT NULL DEFAULT false,
  can_read_comments boolean NOT NULL DEFAULT false,
  can_read_messages boolean NOT NULL DEFAULT false,
  can_receive_webhooks boolean NOT NULL DEFAULT false,
  can_poll boolean NOT NULL DEFAULT false,
  can_stream boolean NOT NULL DEFAULT false,
  can_post boolean NOT NULL DEFAULT false,
  can_comment boolean NOT NULL DEFAULT false,
  can_reply boolean NOT NULL DEFAULT false,
  can_dm boolean NOT NULL DEFAULT false,
  can_hide boolean NOT NULL DEFAULT false,
  can_delete boolean NOT NULL DEFAULT false,
  can_like_or_react boolean NOT NULL DEFAULT false,
  requires_app_review boolean NOT NULL DEFAULT true,
  requires_user_auth boolean NOT NULL DEFAULT true,
  requires_page_admin boolean NOT NULL DEFAULT false,
  prohibited_reason text,
  evidence jsonb NOT NULL DEFAULT '{}',
  last_verified_at timestamptz NOT NULL DEFAULT now(),
  UNIQUE(platform_account_id, surface_type)
);

9.7 Observations


CREATE TABLE observations (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  platform platform NOT NULL,
  platform_account_id uuid NOT NULL REFERENCES platform_accounts(id),
  event_type text NOT NULL,
  surface_type text NOT NULL,
  source_id text,
  platform_object_id text NOT NULL,
  parent_object_id text,
  author_platform_id_hash text,
  author_display_name text,
  content_text text,
  content_media jsonb NOT NULL DEFAULT '[]',
  visibility text NOT NULL DEFAULT 'unknown',
  received_via text NOT NULL,
  raw_payload jsonb NOT NULL DEFAULT '{}',
  normalized_payload jsonb NOT NULL DEFAULT '{}',
  observed_at timestamptz NOT NULL,
  ingested_at timestamptz NOT NULL DEFAULT now(),
  dedupe_hash text NOT NULL,
  UNIQUE(platform, platform_account_id, platform_object_id, event_type, dedupe_hash)
);

CREATE INDEX idx_observations_platform_time ON observations(platform, observed_at DESC);
CREATE INDEX idx_observations_account_time ON observations(platform_account_id, observed_at DESC);
CREATE INDEX idx_observations_parent ON observations(parent_object_id);

9.8 Observation Classifications


CREATE TABLE observation_classifications (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  observation_id uuid NOT NULL REFERENCES observations(id) ON DELETE CASCADE,
  intent_class text NOT NULL,
  sentiment text,
  urgency text,
  relevance_score numeric(5,4) NOT NULL DEFAULT 0,
  confidence numeric(5,4) NOT NULL DEFAULT 0,
  reasons jsonb NOT NULL DEFAULT '[]',
  model_name text,
  prompt_version text,
  created_at timestamptz NOT NULL DEFAULT now()
);

9.9 Action Candidates


CREATE TABLE action_candidates (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  observation_id uuid REFERENCES observations(id) ON DELETE SET NULL,
  platform platform NOT NULL,
  platform_account_id uuid NOT NULL REFERENCES platform_accounts(id),
  action_type text NOT NULL,
  target_object_id text,
  draft_payload jsonb NOT NULL DEFAULT '{}',
  final_payload jsonb,
  intent_class text,
  risk_grade risk_grade NOT NULL,
  confidence numeric(5,4) NOT NULL DEFAULT 0,
  policy_status text NOT NULL DEFAULT 'approval_required',
  policy_reasons jsonb NOT NULL DEFAULT '[]',
  assigned_operator_id uuid REFERENCES operators(id),
  status task_status NOT NULL DEFAULT 'awaiting_approval',
  idempotency_key text NOT NULL UNIQUE,
  created_by text NOT NULL DEFAULT 'agent',
  created_at timestamptz NOT NULL DEFAULT now(),
  updated_at timestamptz NOT NULL DEFAULT now(),
  expires_at timestamptz
);

CREATE INDEX idx_action_candidates_status ON action_candidates(status, created_at);
CREATE INDEX idx_action_candidates_operator ON action_candidates(assigned_operator_id, status);
CREATE INDEX idx_action_candidates_observation ON action_candidates(observation_id);

9.10 Action Revisions


CREATE TABLE action_revisions (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  action_candidate_id uuid NOT NULL REFERENCES action_candidates(id) ON DELETE CASCADE,
  revision_number int NOT NULL,
  source text NOT NULL,
  content_before jsonb,
  content_after jsonb NOT NULL,
  diff_summary text,
  operator_feedback text,
  created_by_operator_id uuid REFERENCES operators(id),
  created_at timestamptz NOT NULL DEFAULT now(),
  UNIQUE(action_candidate_id, revision_number)
);

9.11 Approval Requests


CREATE TABLE approval_requests (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  action_candidate_id uuid NOT NULL REFERENCES action_candidates(id) ON DELETE CASCADE,
  channel text NOT NULL,
  channel_message_id text,
  channel_thread_id text,
  sent_to_operator_id uuid REFERENCES operators(id),
  approval_url text,
  status text NOT NULL DEFAULT 'pending',
  sent_at timestamptz NOT NULL DEFAULT now(),
  responded_at timestamptz
);

9.12 Approval Events


CREATE TABLE approval_events (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  approval_request_id uuid NOT NULL REFERENCES approval_requests(id) ON DELETE CASCADE,
  action_candidate_id uuid NOT NULL REFERENCES action_candidates(id) ON DELETE CASCADE,
  operator_id uuid REFERENCES operators(id),
  decision approval_decision NOT NULL,
  edited_payload jsonb,
  comment text,
  raw_payload jsonb NOT NULL DEFAULT '{}',
  created_at timestamptz NOT NULL DEFAULT now()
);

9.13 Execution Attempts


CREATE TABLE execution_attempts (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  action_candidate_id uuid NOT NULL REFERENCES action_candidates(id) ON DELETE CASCADE,
  platform platform NOT NULL,
  attempt_number int NOT NULL,
  status text NOT NULL,
  request_payload jsonb NOT NULL DEFAULT '{}',
  response_payload jsonb NOT NULL DEFAULT '{}',
  platform_object_id text,
  platform_url text,
  error_code text,
  error_message text,
  started_at timestamptz NOT NULL DEFAULT now(),
  completed_at timestamptz,
  UNIQUE(action_candidate_id, attempt_number)
);

9.14 Content Memory


CREATE TABLE content_memory (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  platform platform NOT NULL,
  platform_account_id uuid REFERENCES platform_accounts(id),
  operator_id uuid REFERENCES operators(id),
  action_type text NOT NULL,
  intent_class text,
  source_observation_id uuid REFERENCES observations(id) ON DELETE SET NULL,
  draft_text text,
  final_text text,
  rejection_reason text,
  feedback text,
  outcome text NOT NULL,
  embedding vector(1536),
  metadata jsonb NOT NULL DEFAULT '{}',
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX idx_content_memory_lookup ON content_memory(platform, action_type, intent_class, outcome, created_at DESC);
-- Add HNSW after you have data:
-- CREATE INDEX idx_content_memory_embedding ON content_memory USING hnsw (embedding vector_cosine_ops);

9.15 Audit Log


CREATE TABLE audit_log (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  actor_type text NOT NULL,
  actor_id text,
  event_type text NOT NULL,
  entity_type text NOT NULL,
  entity_id uuid,
  before_state jsonb,
  after_state jsonb,
  ip_address inet,
  user_agent text,
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX idx_audit_entity ON audit_log(entity_type, entity_id, created_at DESC);
CREATE INDEX idx_audit_event ON audit_log(event_type, created_at DESC);

10. JSON Schemas

10.1 Observation Schema


{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://local.social-mcp/schemas/observation.schema.json",
  "title": "Observation",
  "type": "object",
  "required": [
    "platform",
    "platform_account_id",
    "event_type",
    "surface_type",
    "platform_object_id",
    "visibility",
    "observed_at",
    "received_via"
  ],
  "properties": {
    "platform": {"type": "string"},
    "platform_account_id": {"type": "string", "format": "uuid"},
    "event_type": {"type": "string"},
    "surface_type": {"type": "string"},
    "source_id": {"type": ["string", "null"]},
    "platform_object_id": {"type": "string"},
    "parent_object_id": {"type": ["string", "null"]},
    "author_platform_id_hash": {"type": ["string", "null"]},
    "author_display_name": {"type": ["string", "null"]},
    "content_text": {"type": ["string", "null"]},
    "content_media": {"type": "array", "items": {"type": "object"}},
    "visibility": {"enum": ["public", "private", "semi_private", "unknown"]},
    "received_via": {"enum": ["webhook", "stream", "poll", "manual_import"]},
    "observed_at": {"type": "string", "format": "date-time"},
    "raw_payload": {"type": "object"},
    "normalized_payload": {"type": "object"}
  }
}

10.2 Action Candidate Schema


{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://local.social-mcp/schemas/action-candidate.schema.json",
  "title": "ActionCandidate",
  "type": "object",
  "required": [
    "platform",
    "platform_account_id",
    "action_type",
    "draft_payload",
    "risk_grade",
    "confidence",
    "policy_status",
    "idempotency_key"
  ],
  "properties": {
    "observation_id": {"type": ["string", "null"], "format": "uuid"},
    "platform": {"type": "string"},
    "platform_account_id": {"type": "string", "format": "uuid"},
    "action_type": {"type": "string"},
    "target_object_id": {"type": ["string", "null"]},
    "draft_payload": {"type": "object"},
    "risk_grade": {"enum": ["A", "B", "C", "D", "F"]},
    "confidence": {"type": "number", "minimum": 0, "maximum": 1},
    "policy_status": {"enum": ["allowed", "approval_required", "blocked", "unsupported"]},
    "policy_reasons": {"type": "array", "items": {"type": "string"}},
    "idempotency_key": {"type": "string"}
  }
}

10.3 Draft Payload Schema


{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://local.social-mcp/schemas/social-draft-payload.schema.json",
  "title": "SocialDraftPayload",
  "type": "object",
  "required": ["text"],
  "properties": {
    "text": {"type": "string", "minLength": 1, "maxLength": 10000},
    "media": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["type", "url_or_asset_id"],
        "properties": {
          "type": {"enum": ["image", "video", "document", "link"]},
          "url_or_asset_id": {"type": "string"},
          "alt_text": {"type": ["string", "null"]},
          "metadata": {"type": "object"}
        }
      }
    },
    "reply_to_object_id": {"type": ["string", "null"]},
    "visibility": {"enum": ["public", "private", "semi_private", "unknown"]},
    "scheduled_for": {"type": ["string", "null"], "format": "date-time"},
    "tags": {"type": "array", "items": {"type": "string"}},
    "compliance_notes": {"type": "array", "items": {"type": "string"}}
  }
}

11. MCP Tool Catalog

11.1 Tool Naming Rules

  • Tools should be atomic.
  • Tools should create records, not silently mutate external platforms.
  • Public-facing actions should return an action candidate ID, not a platform post ID.
  • Execution should be separate from proposal.

11.2 MVP Tools


platforms.list_capabilities
observations.ingest_manual
observations.search
observations.classify
actions.propose
actions.grade
actions.enqueue_approval
actions.list_pending
actions.get
actions.approve
actions.edit
actions.reject
actions.execute_approved
memory.search_examples
memory.record_outcome

11.3 MCP Tool: actions.propose


{
  "name": "actions.propose",
  "title": "Propose Social Platform Action",
  "description": "Create an approval-gated action candidate for a platform observation or direct draft. This tool never posts directly.",
  "inputSchema": {
    "type": "object",
    "required": ["platform", "platform_account_id", "action_type"],
    "properties": {
      "observation_id": {"type": ["string", "null"], "format": "uuid"},
      "platform": {"type": "string"},
      "platform_account_id": {"type": "string", "format": "uuid"},
      "action_type": {"type": "string"},
      "target_object_id": {"type": ["string", "null"]},
      "draft_payload": {"type": "object"},
      "reason": {"type": "string"}
    }
  },
  "outputSchema": {
    "type": "object",
    "required": ["action_candidate_id", "risk_grade", "policy_status"],
    "properties": {
      "action_candidate_id": {"type": "string", "format": "uuid"},
      "risk_grade": {"enum": ["A", "B", "C", "D", "F"]},
      "policy_status": {"type": "string"},
      "requires_approval": {"type": "boolean"},
      "reasons": {"type": "array", "items": {"type": "string"}}
    }
  }
}

11.4 MCP Tool: actions.execute_approved


{
  "name": "actions.execute_approved",
  "title": "Execute Approved Social Platform Action",
  "description": "Execute an action candidate only if it is approved or auto-approved under a valid policy.",
  "inputSchema": {
    "type": "object",
    "required": ["action_candidate_id"],
    "properties": {
      "action_candidate_id": {"type": "string", "format": "uuid"},
      "execution_token": {"type": ["string", "null"]}
    }
  },
  "outputSchema": {
    "type": "object",
    "required": ["ok"],
    "properties": {
      "ok": {"type": "boolean"},
      "platform_object_id": {"type": ["string", "null"]},
      "url": {"type": ["string", "null"]},
      "error_code": {"type": ["string", "null"]},
      "error_message": {"type": ["string", "null"]}
    }
  }
}

12. A-F Grade Engine

12.1 Grade Inputs


@dataclass(frozen=True)
class GradeInput:
    platform: str
    action_type: str
    visibility: str
    surface_type: str
    is_public_facing: bool
    is_solicitation: bool
    is_reply_to_owned_post: bool
    is_dm: bool
    has_official_api_support: bool
    has_required_auth: bool
    platform_policy_block: bool
    content_sensitivity: str
    factual_claim_support: float
    operator_similarity_score: float
    model_confidence: float
    recent_rejection_similarity: float
    spam_or_repetition_score: float
    rate_limit_risk: float

12.2 Deterministic Grade Rules


def grade_action(x: GradeInput) -> tuple[str, list[str]]:
    reasons: list[str] = []

    if x.platform_policy_block:
        return "F", ["Platform policy or capability registry blocks this action."]

    if not x.has_required_auth:
        return "F", ["Missing required authorization for this account/surface/action."]

    if not x.has_official_api_support and x.is_public_facing:
        return "F", ["Public-facing action lacks supported official API path."]

    if x.spam_or_repetition_score >= 0.75:
        return "F", ["High spam or repetitive outreach risk."]

    if x.content_sensitivity in {"legal", "medical", "financial", "political", "emergency"}:
        return "D", ["Sensitive content category requires senior/operator approval."]

    if x.is_solicitation and not x.is_reply_to_owned_post:
        return "D", ["Cold or semi-cold solicitation requires explicit human approval."]

    if x.is_public_facing and x.model_confidence < 0.85:
        return "C", ["Public-facing action with insufficient confidence."]

    if x.recent_rejection_similarity >= 0.70:
        return "D", ["Similar recent content was rejected."]

    if x.operator_similarity_score >= 0.92 and x.model_confidence >= 0.90 and not x.is_solicitation:
        return "A", ["Highly similar to prior approved operator examples."]

    if x.operator_similarity_score >= 0.80 and x.model_confidence >= 0.85:
        return "B", ["Moderately similar to prior approved operator examples."]

    return "C", ["Default human approval required."]

13. Auto-Approval Policy Schema


CREATE TABLE auto_approval_policies (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  name text NOT NULL,
  platform platform NOT NULL,
  platform_account_id uuid REFERENCES platform_accounts(id),
  action_type text NOT NULL,
  surface_type text,
  max_grade risk_grade NOT NULL DEFAULT 'B',
  min_operator_similarity numeric(5,4) NOT NULL DEFAULT 0.90,
  min_model_confidence numeric(5,4) NOT NULL DEFAULT 0.90,
  min_prior_approved_count int NOT NULL DEFAULT 20,
  max_recent_rejection_count int NOT NULL DEFAULT 0,
  max_daily_executions int NOT NULL DEFAULT 10,
  requires_official_api boolean NOT NULL DEFAULT true,
  enabled boolean NOT NULL DEFAULT false,
  enabled_by_operator_id uuid REFERENCES operators(id),
  created_at timestamptz NOT NULL DEFAULT now(),
  updated_at timestamptz NOT NULL DEFAULT now()
);

14. Approval UX

14.1 Slack Message Fields


Platform: LinkedIn
Account: Company Page
Action: Reply to comment
Grade: C
Observation: User asked for pricing
Draft: "Thanks for asking — we can help..."
Reasons:
- Public-facing reply
- Support/sales intent
- Similar to 8 prior approved replies
Buttons:
[Approve] [Edit] [Reject] [Escalate] [Schedule]

14.2 Slack Callback Payload Handling


async def handle_slack_approval(action_id: str, operator_id: str, payload: dict):
    if action_id == "approve_action":
        await approvals.approve(candidate_id=payload["candidate_id"], operator_id=operator_id)
        await queue.enqueue_execution(payload["candidate_id"])
    elif action_id == "edit_action":
        await slack.open_edit_modal(candidate_id=payload["candidate_id"], trigger_id=payload["trigger_id"])
    elif action_id == "reject_action":
        await approvals.reject(candidate_id=payload["candidate_id"], operator_id=operator_id)

14.3 Telegram Approval


{
  "chat_id": "<operator_chat_id>",
  "text": "Approve LinkedIn reply? Grade C\n\nDraft: ...",
  "reply_markup": {
    "inline_keyboard": [
      [
        {"text": "Approve", "callback_data": "approve:<candidate_id>"},
        {"text": "Reject", "callback_data": "reject:<candidate_id>"}
      ],
      [
        {"text": "Needs Edit", "callback_data": "edit:<candidate_id>"}
      ]
    ]
  }
}

15. Platform Adapter Specs

15.1 LinkedIn Adapter

Supported MVP Surfaces

  • Organization posts
  • Organization comments
  • Social metadata summaries
  • Member posting only if approved and explicitly authorized

Capabilities


platform: linkedin
surfaces:
  organization_page:
    read_posts: true
    read_comments: true
    read_reactions_summary: true
    create_post: true
    create_comment: true
    delete_comment: true
    dm: false
    broad_feed_monitoring: false
    group_monitoring: false

Methods


class LinkedInAdapter:
    async def observe(self, account_id, cursor=None):
        """Poll accessible organization posts sorted by last modified and fetch comments/social metadata."""

    async def execute(self, candidate):
        """Map CREATE_PUBLIC_POST or CREATE_COMMENT to LinkedIn REST endpoints."""

Blocking Rules

  • Block scraping LinkedIn web pages.
  • Block automated activity that does not use authorized APIs.
  • Block broad member/feed/group monitoring unless official approved capability exists.

15.2 Facebook Page Adapter

Supported MVP Surfaces

  • Page feed posts
  • Page post comments
  • Page mentions if permissioned
  • Messenger conversations within policy windows
  • Page publishing
  • Comment replies and moderation

Capabilities


platform: facebook
surfaces:
  page:
    read_posts: true
    read_comments: true
    read_messages: true
    receive_webhooks: true
    create_post: true
    create_comment: true
    create_comment_reply: true
    hide_comment: true
    delete_comment: true
    dm: true
  group:
    read_posts: false
    create_post: false
    comment: false
    reason: "Treat as unsupported unless official API permissions are specifically granted."

15.3 Instagram Business Adapter

Supported MVP Surfaces

  • Business/creator account media
  • Comments on media
  • Publishing through media containers
  • Comment replies where allowed

Capabilities


platform: instagram
surfaces:
  business_account:
    read_posts: true
    read_comments: true
    receive_webhooks: true
    create_media_container: true
    publish_media: true
    comment: true
    dm: limited

15.4 Nextdoor Adapter

Supported MVP Surfaces

  • Public Search API for recent public posts where access is approved
  • Trending/public content APIs where access is approved
  • Publish API where approved
  • Share Plugin for manual user-assisted posting

Capabilities


platform: nextdoor
surfaces:
  public_search:
    read_posts: true
    search_by_keyword_location: true
    create_post: false
    comment: false
  publish_api:
    create_post: true
    edit_post: true
    delete_post: true
    comment: true
    requires_user_auth: true
  share_plugin:
    create_post: user_finalizes_manually

15.5 X Adapter

Supported MVP Surfaces

  • Filtered stream
  • Search rules
  • Post/reply creation
  • User mentions

Capabilities


platform: x
surfaces:
  filtered_stream:
    stream_posts: true
    webhook_delivery: true
    rule_based_search: true
  authenticated_user:
    create_post: true
    delete_post: true
    reply: true
    quote: true

15.6 Slack Adapter

Slack is an approval channel and can also be an observation source if installed in relevant channels.


platform: slack
surfaces:
  approval_channel:
    send_message: true
    interactive_buttons: true
    modals: true
    reactions: true
    thread_replies: true

15.7 Telegram Adapter

Telegram is a fallback approval channel.


platform: telegram
surfaces:
  bot_chat:
    send_message: true
    inline_keyboard: true
    callback_query: true
    reply_message: true

16. Queue Design

16.1 Queue Names


observe.linkedin
observe.facebook
observe.instagram
observe.nextdoor
observe.x
classify.observation
propose.action
approval.send
execute.action
memory.embed

16.2 Queue Message Schema


{
  "job_id": "uuid",
  "job_type": "execute.action",
  "entity_type": "action_candidate",
  "entity_id": "uuid",
  "attempt": 1,
  "not_before": "2026-04-27T15:00:00Z",
  "trace_id": "uuid",
  "metadata": {}
}

16.3 Fallback SQL Queue Pattern


CREATE TABLE jobs (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  queue_name text NOT NULL,
  payload jsonb NOT NULL,
  status text NOT NULL DEFAULT 'queued',
  attempts int NOT NULL DEFAULT 0,
  max_attempts int NOT NULL DEFAULT 5,
  run_after timestamptz NOT NULL DEFAULT now(),
  locked_at timestamptz,
  locked_by text,
  created_at timestamptz NOT NULL DEFAULT now(),
  updated_at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX idx_jobs_ready ON jobs(queue_name, status, run_after);

WITH next_job AS (
  SELECT id
  FROM jobs
  WHERE queue_name = $1
    AND status = 'queued'
    AND run_after <= now()
  ORDER BY created_at
  FOR UPDATE SKIP LOCKED
  LIMIT 1
)
UPDATE jobs
SET status = 'running', locked_at = now(), locked_by = $2, attempts = attempts + 1
WHERE id IN (SELECT id FROM next_job)
RETURNING *;

17. Prompt Templates

17.1 Observation Classifier Prompt


You classify inbound social platform observations for a governed social operations system.

Return JSON only.

Observation:
{observation_json}

Known account context:
{account_context}

Classify:
- intent_class
- relevance_score 0-1
- urgency: low | medium | high | critical
- sentiment: positive | neutral | negative | mixed | unknown
- should_propose_action: true/false
- recommended_action_types
- risk_notes
- missing_context

17.2 Action Draft Prompt


You draft platform-specific action candidates. Do not claim the action has been approved. Do not produce spam-like, deceptive, or unauthorized outreach.

Observation:
{observation_json}

Action type:
{action_type}

Platform constraints:
{platform_constraints}

Similar approved examples:
{retrieved_approved_examples}

Similar rejected examples:
{retrieved_rejected_examples}

Return JSON:
{
  "draft_payload": {},
  "rationale": "",
  "risks": [],
  "requires_human_review": true
}

17.3 Approval Summary Prompt


Summarize this action candidate for an operator deciding whether to approve, edit, reject, or escalate.

Include:
- What happened
- Proposed action
- Why it was proposed
- Risk grade
- Reasons for grade
- Similar prior examples
- Exact content that would be posted or sent

Candidate:
{candidate_json}

18. API Routes

18.1 Internal HTTP API


GET    /healthz
GET    /platforms/capabilities
POST   /observations/manual
GET    /observations
GET    /observations/{id}
POST   /observations/{id}/classify
POST   /actions/propose
GET    /actions/pending
GET    /actions/{id}
POST   /actions/{id}/approve
POST   /actions/{id}/edit
POST   /actions/{id}/reject
POST   /actions/{id}/execute
POST   /webhooks/slack
POST   /webhooks/telegram
POST   /webhooks/meta
POST   /webhooks/x

19. Repository Layout


social-mcp-ops/
  apps/
    api/
      main.py
      routes/
      webhooks/
    mcp_server/
      server.py
      tools/
    worker/
      main.py
      queues/
  packages/
    adapters/
      base.py
      linkedin.py
      facebook.py
      instagram.py
      nextdoor.py
      x.py
      slack.py
      telegram.py
    core/
      models.py
      schemas.py
      grading.py
      policy.py
      memory.py
    db/
      session.py
      migrations/
    approvals/
      slack.py
      telegram.py
    prompts/
      classify_observation.txt
      draft_action.txt
      approval_summary.txt
  infra/
    docker-compose.yml
    Dockerfile.api
    Dockerfile.worker
    Caddyfile
  tests/
    unit/
    integration/
    fixtures/

20. Docker Compose Starter


services:
  postgres:
    image: pgvector/pgvector:pg16
    environment:
      POSTGRES_DB: social_mcp
      POSTGRES_USER: social_mcp
      POSTGRES_PASSWORD: social_mcp_dev
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  api:
    build:
      context: .
      dockerfile: infra/Dockerfile.api
    env_file: .env
    depends_on:
      - postgres
    ports:
      - "8080:8080"

  worker:
    build:
      context: .
      dockerfile: infra/Dockerfile.worker
    env_file: .env
    depends_on:
      - postgres

volumes:
  pgdata:

21. Environment Variables


DATABASE_URL=postgresql+asyncpg://social_mcp:social_mcp_dev@postgres:5432/social_mcp
APP_ENV=local
PUBLIC_BASE_URL=https://your-tunnel-or-domain.example

SLACK_BOT_TOKEN=
SLACK_SIGNING_SECRET=
SLACK_APPROVAL_CHANNEL_ID=

TELEGRAM_BOT_TOKEN=
TELEGRAM_DEFAULT_OPERATOR_CHAT_ID=

LINKEDIN_CLIENT_ID=
LINKEDIN_CLIENT_SECRET=
META_APP_ID=
META_APP_SECRET=
NEXTDOOR_CLIENT_ID=
NEXTDOOR_CLIENT_SECRET=
X_CLIENT_ID=
X_CLIENT_SECRET=

OPENAI_API_KEY=
EMBEDDING_MODEL=text-embedding-3-small

22. MVP Acceptance Tests

22.1 Observation Test

Given a manual observation is ingested, the system stores a normalized observation and dedupe hash.

22.2 Classification Test

Given an observation that asks for help, the classifier returns support_request and should_propose_action=true.

22.3 Proposal Test

Given a classified observation, actions.propose creates an action candidate with a risk grade and policy status.

22.4 Approval Test

Given an action candidate requiring approval, Slack receives an approval message with Approve/Edit/Reject buttons.

22.5 Edit Test

Given an operator edits the draft, a revision is created, the candidate is regraded, and the final payload is updated.

22.6 Execution Test

Given an approved candidate, the executor calls the platform adapter and stores an execution attempt.

22.7 Blocking Test

Given an unsupported platform action, the candidate is marked blocked and cannot execute.

23. Build Roadmap

Week 1: Core Skeleton

  • Repo layout
  • Docker Compose
  • Postgres migrations
  • FastAPI health route
  • MCP server skeleton
  • Base adapter protocol
  • Manual observation ingestion

Week 2: Approval System

  • Slack app setup
  • Approval message formatting
  • Approve/edit/reject callbacks
  • Telegram fallback
  • Approval tables and events

Week 3: Classification and Grading

  • Observation classifier
  • Action planner
  • A-F grade engine
  • Policy engine
  • Audit log

Week 4: First Platform Connector

Recommended first connector: X filtered stream or LinkedIn organization posts/comments.

  • Capability registration
  • Read/listen method
  • Dry-run previews
  • Approved execution path

Week 5: Memory Layer

  • Store final outcomes
  • Embedding jobs
  • Similar example retrieval
  • Prompt injection with prior approved/rejected examples

Week 6: Auto-Approval Controls

  • Auto-approval policies
  • Threshold checks
  • Sampling review
  • Operator toggle
  • Daily execution caps

24. Non-Goals for MVP

  • No unauthorized scraping of logged-in feeds.
  • No Facebook Group automation unless official access is confirmed.
  • No LinkedIn browser automation.
  • No bulk cold outreach.
  • No engagement manipulation.
  • No fake accounts.
  • No CAPTCHA bypass.
  • No proxy rotation for platform account operation.

25. Definition of Done

The MVP is ready when:

  1. A new observation can be ingested.
  2. The system classifies it.
  3. A candidate action is created.
  4. The candidate is graded A-F.
  5. The candidate is sent to Slack.
  6. The operator can approve, edit, or reject.
  7. Approved actions can execute through one official platform adapter.
  8. Every step is persisted and auditable.
  9. Rejected and approved examples are saved for future memory retrieval.
  10. Unsupported actions are blocked before execution.