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.
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
- Observe first, act later. Inbound posts, comments, messages, mentions, and search hits are stored as observations before any action is proposed.
- 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.
- Official APIs first. LinkedIn, Meta/Facebook/Instagram, Nextdoor, X, Slack, and Telegram should be integrated through supported APIs and approved permissions wherever possible.
- Browser automation is an exception. Browser automation and scraping are only allowed for explicitly authorized, low-risk, read-only or operator-assisted flows.
- 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.
- Every action is auditable. Store the originating observation, draft, revisions, operator decision, final payload, execution result, platform object ID, and errors.
- Preference learning starts as retrieval. Store approved/rejected examples and retrieve similar prior examples for prompt grounding before considering fine-tuning.
- 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
taskstable plusFOR 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
validate_action()must run before grading.dry_run()must run before operator approval.execute()must require an approvedaction_candidate_idor signed execution token.- Adapters must return structured errors, not raw stack traces.
- All platform IDs should be stored as strings.
- Raw payloads should be stored for audit, but sensitive tokens must never be stored in raw payloads.
8. Platform Adapter Capability Matrix
| Platform | Read posts | Read comments | Read DMs | Webhook/stream | Post | Comment/reply | DM | Notes |
|---|---|---|---|---|---|---|---|---|
| LinkedIn Organization | Yes, with approved permissions | Yes, on accessible posts | Limited/no generic DM | Polling-oriented | Yes | Yes | Not general | Strong for organization content; broad scraping is not allowed. |
| Facebook Page | Yes, Page-scoped | Yes, Page-scoped | Messenger Page-scoped | Webhooks where approved | Yes | Yes | Yes, constrained | Focus on Pages, not Groups. |
| Facebook Groups | Generally no for new builds | Limited/unsupported | No | No reliable path | Avoid | Avoid | No | Treat as unsupported unless official app permissions exist. |
| Instagram Business | Yes, account/media scoped | Yes, media comments | Limited | Webhooks where approved | Yes through media container flow | Yes on media comments | Limited | Business/creator only. |
| Nextdoor | Public content search with access | Depends on access | No generic | Polling/search | Publish API with approval | Comments with approved API | No generic | Public Search API and Publish API are access-gated. |
| X | Yes via filtered stream/search | Replies are posts | DM requires access | Strong stream/webhook | Yes | Yes | Access-dependent | Best pilot for streaming detection. |
| Slack | Channels/messages where bot installed | Threads/reactions | DMs with bot | Events API | Messages | Thread replies | Yes | Primary approval channel. |
| Telegram | Bot chats | Replies/messages to bot | Bot chat | Webhooks/long polling | Bot messages | Bot replies | Yes | Fallback 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:
- A new observation can be ingested.
- The system classifies it.
- A candidate action is created.
- The candidate is graded A-F.
- The candidate is sent to Slack.
- The operator can approve, edit, or reject.
- Approved actions can execute through one official platform adapter.
- Every step is persisted and auditable.
- Rejected and approved examples are saved for future memory retrieval.
- Unsupported actions are blocked before execution.