<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Annie Yang</title>
    <description>The latest articles on DEV Community by Annie Yang (@annie_yang_5d8d95f1dcf2ff).</description>
    <link>https://dev.to/annie_yang_5d8d95f1dcf2ff</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4234229%2F1ee5f1ad-1309-444f-95cb-2a94cada846b.jpg</url>
      <title>DEV Community: Annie Yang</title>
      <link>https://dev.to/annie_yang_5d8d95f1dcf2ff</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/annie_yang_5d8d95f1dcf2ff"/>
    <language>en</language>
    <item>
      <title>I gave my social media scheduler an MCP server. Here's what I learned building it.</title>
      <dc:creator>Annie Yang</dc:creator>
      <pubDate>Mon, 09 Nov 2026 00:35:39 +0000</pubDate>
      <link>https://dev.to/annie_yang_5d8d95f1dcf2ff/i-gave-my-social-media-scheduler-an-mcp-server-heres-what-i-learned-building-it-2332</link>
      <guid>https://dev.to/annie_yang_5d8d95f1dcf2ff/i-gave-my-social-media-scheduler-an-mcp-server-heres-what-i-learned-building-it-2332</guid>
      <description>&lt;p&gt;Social media scheduling is the kind of task AI agents should be perfect at. It's structured (a caption, some media, a list of accounts, a time), it's repetitive, and the rules are knowable - character limits, aspect ratios, which platform takes what. The only thing missing was a way for the agent to actually &lt;em&gt;do&lt;/em&gt; it instead of just drafting captions I'd still have to paste somewhere.&lt;/p&gt;

&lt;p&gt;So when I built &lt;a href="https://www.useampli.com" rel="noopener noreferrer"&gt;Ampli&lt;/a&gt;, my social media scheduler, I didn't bolt an API on at the end. The MCP server is a first-class surface: anything a user can do in the app - bring their own media, schedule and publish to eight networks (TikTok, Instagram, Pinterest, LinkedIn, Threads, Facebook, YouTube, X), keep an ideas board, read analytics - an agent can do through tools.&lt;/p&gt;

&lt;p&gt;Here's what building that actually involved, including the parts I got wrong first.&lt;/p&gt;

&lt;h2&gt;
  
  
  One behaviour, two surfaces
&lt;/h2&gt;

&lt;p&gt;The decision that saved me the most pain: &lt;strong&gt;the MCP tools are not a separate implementation.&lt;/strong&gt; They call the same v1 REST endpoints the app uses, with the same ownership checks and the same publishing validation. Some tools add defaults, combine calls, or trim the response for an agent-sized context window, but there is exactly one place where "can this user post this to this account?" is answered.&lt;/p&gt;

&lt;p&gt;If you're adding MCP to an existing product, I'd strongly recommend this shape over a parallel agent-only backend. Every bug you fix, you fix once, and the agent can never become a backdoor around your own rules.&lt;/p&gt;

&lt;p&gt;The server itself is remote Streamable HTTP at &lt;code&gt;https://www.useampli.com/api/mcp&lt;/code&gt;, stateless, POST-only. (That last part confuses every health checker on the internet: a GET returns 405, which is correct behaviour for this transport, and at least one directory still lists us as "Unhealthy" because its checker knocks with GET and no credentials. If you ship a remote server, expect this mail.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Auth: OAuth for agents, keys for scripts
&lt;/h2&gt;

&lt;p&gt;Remote MCP forced the auth question early, and the answer turned out to be "both":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OAuth 2.1 with PKCE and dynamic client registration&lt;/strong&gt; for clients that support it (Claude.ai, Claude Desktop, ChatGPT, and others). The server publishes the standard discovery documents (&lt;code&gt;/.well-known/oauth-authorization-server&lt;/code&gt;, &lt;code&gt;/.well-known/oauth-protected-resource&lt;/code&gt;), registration lives at &lt;code&gt;/api/oauth/register&lt;/code&gt;, and the user just clicks Authorize - no key to copy, paste, or leak into a config file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personal API keys&lt;/strong&gt; for everything else: scripts, REST calls, and stdio-only clients. For those, there's a small bridge package so a JSON-config client can still connect:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ampli"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"viralfarm-mcp@latest"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"VIRALFARM_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vf_your_key"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or with Claude Code, it's one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http ampli https://www.useampli.com/api/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer vf_your_key"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One design detail I'm happy with: completing the OAuth flow &lt;strong&gt;mints an API key behind the scenes&lt;/strong&gt;, named after the client (you'll see something like "Claude (OAuth)" in Settings → API Keys). It counts toward the plan's key allowance and can be revoked like any other key. One credential model to reason about, one revocation list, no special OAuth-token storage to audit later.&lt;/p&gt;

&lt;p&gt;And one I'm phasing out: we briefly accepted keys in a &lt;code&gt;?key=&lt;/code&gt; URL parameter because some early clients could do nothing else. URLs get logged, screenshotted, and shared - keys don't belong in them. Newer keys are refused in URLs entirely, and every request that still tries gets a 401 explaining how to reconnect properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing tools for an agent, not a human
&lt;/h2&gt;

&lt;p&gt;This was the real work. Three lessons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. "Success" is not "published."&lt;/strong&gt; Our create-post response returns &lt;code&gt;success: true&lt;/code&gt; when the request was accepted and the post object exists. Whether it actually went live is a separate field - &lt;code&gt;publishNow.outcome&lt;/code&gt; on the create, &lt;code&gt;outcome&lt;/code&gt; on every post read. An agent that reports "posted!" off the success flag will eventually lie to its user, confidently. If your tools wrap anything asynchronous or fallible, give the agent an explicit outcome to read, and say so in the tool description.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Agents retry. Make retries safe.&lt;/strong&gt; &lt;code&gt;POST /posts&lt;/code&gt; (and post creation through MCP) accepts an &lt;code&gt;Idempotency-Key&lt;/code&gt;. The key is written by the same database insert that creates the post, so whatever fails afterwards - a timeout, a publish step that dies mid-flight - a retry with the same key returns the &lt;em&gt;original&lt;/em&gt; post, never a duplicate. Same key with a different body is a 422; same key while the first attempt is still running is a 409. An agent that gets nervous and hammers your endpoint should produce exactly one scheduled post.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Teach the rules before the post, not after.&lt;/strong&gt; Every platform rejects differently - title length here, a TikTok privacy option there. Instead of letting the agent discover this from failed posts, there's a per-account publishing-options call that returns the limits and fields that account actually accepts (Pinterest boards included). The agent reads the rules, builds a valid request, and validation errors - when they still happen - come back in plain language with the specific field named and one line per platform rule, so the agent can fix the request instead of apologising.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like in practice
&lt;/h2&gt;

&lt;p&gt;With the connector added, "take these three photos, write a caption for each channel, and schedule them across my accounts for next week" is a single instruction. The agent lists the connected accounts, reads each one's publishing options, uploads the media, creates the posts with per-platform overrides, and reads back the outcome of each one. It's in the &lt;a href="https://www.useampli.com/docs" rel="noopener noreferrer"&gt;API &amp;amp; MCP reference&lt;/a&gt; if you want the full tool-by-tool picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're adding MCP to your product
&lt;/h2&gt;

&lt;p&gt;Start with the REST API you already trust and wrap it - don't fork your business logic. Do OAuth properly (PKCE + dynamic registration) so the big clients connect with a click, but keep API keys for the long tail. Then spend your remaining effort where it actually matters: outcome fields an agent can trust, idempotency so retries are free, and errors written for a reader that will act on them literally.&lt;/p&gt;

&lt;p&gt;Ampli's MCP access is on every plan including the free one, and the setup page at &lt;a href="https://www.useampli.com/mcp" rel="noopener noreferrer"&gt;useampli.com/mcp&lt;/a&gt; has copy-paste instructions for each client. If you've shipped an MCP server yourself, I'd genuinely like to hear what your version of the 405-health-check problem looked like - that seems to be a universal experience so far.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>ai</category>
      <category>webdev</category>
      <category>mcp</category>
    </item>
  </channel>
</rss>
