A recent Hacker News discussion about MCP in production kept returning to the same argument. MCP can make a service easier to connect to an agent, but a normal API or CLI can often perform the same underlying work. That is true and incomplete. The useful question is not whether MCP can replace REST. It is whether the tool boundary gives an agent a small, inspectable surface while the system behind it handles the parts a language model should never decide.
Publishing to social media is a good test. A post is an externally visible side effect. A timeout can leave its outcome unknown. A retry can create a duplicate. A broad credential can send it to the wrong account. A correct MCP tool schema does not prevent any of those failures by itself.
I built PostSider’s agent surface around this distinction. MCP is the interface. The public API, approval state, idempotency store, webhook delivery, and publishing controls are the execution system. Here is the architecture I would use for any agent that can publish in public.
MCP should expose intent, not unchecked authority #
An MCP server should give the model a constrained vocabulary. Reads and writes need to be visibly different, and a general shell or unrestricted HTTP client should not sit beside a publishing key.
The current PostSider MCP server exposes 19 tools. Each one declares whether it is read-only, destructive, idempotent, and connected to an external system. The remote transport also checks scopes at the client boundary: reads require posts:read, while POST, PUT, and DELETE calls require posts:write. A connection with read-only access cannot quietly cross into mutation because the model wrote a persuasive tool call.
That matches the core guidance in the OWASP AI Agent Security Cheat Sheet: give an agent the minimum tools required, scope permissions per tool, and require explicit authorization for sensitive operations. The OWASP MCP Security Cheat Sheet applies the same idea to credentials: use a separate, narrowly scoped credential for each server instead of one token with broad access.
The practical rule is simple. Let the agent discover channels, inspect drafts, find a slot, and check status with read access. Grant write access only to the workflow that needs it. Keep deletion and immediate publishing behind a separate confirmation path.
The agent proposes, deterministic code executes #
Model output is a proposal. It is not authorization.
Before execution, normalize the proposed action into a record with the workspace, channel IDs, post type, publish time, content, media references, and a revision number. Validate it against a strict schema. Then apply policy outside the model: is this connection allowed to write to these channels, is direct publishing allowed, and does this action need approval?
OWASP classifies externally visible actions as high impact. Its guidance recommends separating decision-making from execution and binding approval to the exact action, including the actor, tool, target, normalized parameters, timestamp, and expiry. If the caption, media, account, or schedule changes after approval, the old approval should no longer authorize the new payload.
PostSider supports this operating pattern with separate tools for creating a draft, requesting approval, and reading the approval status. The agent can prepare the work without becoming the final approver. For a concrete review ladder, see human-in-the-loop patterns that do not become the bottleneck.
A minimal state machine looks like this:
PROPOSED -> VALIDATED -> APPROVAL_PENDING -> APPROVED
|
v
ENQUEUED
|
v
IN_FLIGHT
/ | \
SUCCEEDED FAILED UNKNOWN
|
v
RECONCILED
UNKNOWN matters. If the upstream platform accepted a post but the response disappeared, the operation is not safely classified as failed. Blindly calling create again is how one intent becomes two public posts.
Idempotency makes retries safe enough to automate #
Every logical create needs a stable identity. Generate an idempotency key from your own operation ID, not from the attempt number, and reuse it for every retry of that operation.
PostSider accepts Idempotency-Key on POST /public/v1/posts, and the MCP create tool exposes the same field. The server stores a SHA-256 hash of the request body with the key and organization. Repeating the same completed request returns the stored response. Reusing the key with a different body returns a conflict. If the first request is still processing, a concurrent repeat is rejected instead of racing a second create.
This pattern is not specific to social media. Stripe documents the same contract: a client-generated key identifies a retry, while changed parameters under the same key are rejected. The important part for an agent is that deterministic code owns the key. The model does not get to decide that a fresh key is a good way around an error.
A useful key can be boring:
publish:{workspace_id}:{proposal_id}:{channel_id}:{revision}
Do not place email addresses, captions, or other sensitive data inside it. Store the full mapping in your database.
The full implementation pattern, including replay conflicts and failure injection, is covered in idempotency for social posting.
Retry policy belongs in a worker, not the agent loop #
An LLM loop is the wrong place to implement backoff. It may reinterpret an error, alter the payload, or call a neighboring tool. A deterministic worker should classify the result and make the retry decision.
Retry network failures, rate limits, and selected 5xx responses with capped exponential backoff and jitter. Honor Retry-After when the provider sends it. Do not retry validation failures or missing authorization until the underlying state changes. Put a hard ceiling on attempts and total elapsed time.
For a timeout after sending, move the operation to UNKNOWN. Query your own idempotency record, check the post list, or reconcile against a provider identifier before another create. A queue redelivery should carry the same operation ID and the same idempotency key.
PostSider’s outbound post.published webhook worker follows a bounded rule: up to 3 attempts, with waits of about one second and two seconds after network errors or 5xx responses. A 4xx response is not retried because repetition cannot repair a bad request or rejected credential. The webhook failure is logged without failing the already completed publish workflow.
Signed webhooks close the loop without becoming the loop #
A create response tells you the API accepted work. A signed event tells you the post reached the published state. Neither should erase the other.
A webhook consumer should verify the signature over the raw request body, enforce timestamp freshness, deduplicate the event or post ID, return a success response quickly, and process the heavier work asynchronously. Stripe’s webhook guidance recommends the same raw-body signature check and quick acknowledgment, and warns that event delivery may be duplicated or arrive out of order.
PostSider signs the timestamp and raw JSON body with HMAC-SHA256. Each delivery includes X-Postsider-Event, X-Postsider-Timestamp, X-Webhook-Attempt, and X-Postsider-Signature. The SDK provides signature verification, but your consumer still needs a freshness window and durable deduplication.
Do not use a webhook as the sole source of truth. Use it to advance local state, then reconcile when the event conflicts with the API response or never arrives. The detailed handler pattern is in webhooks that tell your stack a post went live.
Observability should reconstruct one publication #
A production incident should be answerable from one operation ID. You should be able to reconstruct who requested the post, which policy approved it, which payload revision was sent, how many attempts ran, what the provider returned, and which webhook confirmed the result.
Carry these identifiers through the MCP gateway, queue, API call, and webhook consumer:
trace_id
proposal_id
approval_id
idempotency_key
workspace_id
channel_id
payload_revision
attempt
provider_request_id
Log decisions and transitions, not secrets. Record the authorization outcome, approval state, error class, timing, and a content hash when you need integrity evidence. Redact tokens and avoid copying full private drafts into every log line.
The metrics that expose real failure are equally plain: publish success by error class, retry count, operations stuck in UNKNOWN, webhook delay, duplicate suppression, reconciliation drift, and age of the oldest dead-lettered operation. None needs an AI dashboard.
A kill switch belongs in the publishing surface #
Preflight and approval reduce bad actions. They do not help when the agent is already running the wrong job at scale.
Your operator needs one control that stops publishing without deleting the queue or revoking every integration. PostSider exposes postsider__publishing and postsider_get_publishing_state for that reason. The tool is marked destructive in its MCP annotations, so clients can put a confirmation in front of it. Once d, the operator can inspect drafts, credentials, and recent tool calls before resuming.
Test the switch before you need it. Then test duplicate calls, post-approval edits, revoked credentials, queue redelivery, webhook replay, a timeout after provider acceptance, and a dead webhook endpoint. A passing drill has two properties: no unauthorized duplicate post appears, and every ambiguous outcome is visible enough to reconcile.
MCP earns its place when it gives agents a portable, narrow tool surface. Production reliability comes from everything behind that surface. If you are building a publishing agent, PostSider’s developer surface gives you MCP, REST, and the Node SDK over the same execution system, with the human dashboard still available when public actions need a person.
Lukasz Blania is the solo founder of PostSider.
Frequently asked questions #
Does MCP make a social publishing agent reliable?
No. MCP standardizes tool discovery and invocation. Reliability still comes from authorization, validation, approval policy, idempotency, bounded retries, signed webhooks, and reconciliation behind the tool.
Should an AI agent publish directly or create drafts?
Start with drafts. Let a person approve the exact target, content, media, and time. Direct publishing can be appropriate later for a narrow class of low-risk posts with strict scopes, budgets, monitoring, and a kill switch.
How should an agent retry a failed publish request?
Retry transient failures in deterministic code, not inside an open-ended model loop. Reuse the same idempotency key, honor rate-limit guidance, cap attempts, and reconcile an ambiguous timeout before sending another create request.
Can webhooks be the source of truth for published posts?
Treat webhooks as signed event notifications, not as your only state store. Verify the raw payload, reject stale signatures, deduplicate deliveries, acknowledge quickly, and compare events with the publishing API when an outcome is unclear.