<?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: uplika</title>
    <description>The latest articles on DEV Community by uplika (@uplika).</description>
    <link>https://dev.to/uplika</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%2F4151530%2F49e9d94c-855c-4910-a1ce-2e046ff85f9e.png</url>
      <title>DEV Community: uplika</title>
      <link>https://dev.to/uplika</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/uplika"/>
    <language>en</language>
    <item>
      <title>What we learned putting social media APIs behind a hosted MCP server</title>
      <dc:creator>uplika</dc:creator>
      <pubDate>Wed, 30 Sep 2026 13:50:48 +0000</pubDate>
      <link>https://dev.to/uplika/what-we-learned-putting-social-media-apis-behind-a-hosted-mcp-server-l00</link>
      <guid>https://dev.to/uplika/what-we-learned-putting-social-media-apis-behind-a-hosted-mcp-server-l00</guid>
      <description>&lt;p&gt;We build uplika, a hosted MCP server that lets an AI agent post to the social accounts a person has connected: Threads, Instagram, YouTube, Facebook, Bluesky and Telegram, with Naver Blog through a Chrome extension and TikTok still in app review. This post collects the parts that were harder than we expected, in case you are building something similar.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Drafted with AI assistance and checked against our own code and notes.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  No API key in the agent
&lt;/h2&gt;

&lt;p&gt;The first decision was that nobody should paste a secret into an agent's config. Remote MCP clients already speak OAuth, so the server acts as an OAuth authorization server with dynamic client registration:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The agent calls &lt;code&gt;https://api.uplika.com/mcp&lt;/code&gt; without a token and gets a &lt;code&gt;401&lt;/code&gt; with a &lt;code&gt;WWW-Authenticate&lt;/code&gt; header pointing at the protected-resource metadata.&lt;/li&gt;
&lt;li&gt;The client reads the authorization server metadata and registers itself (dynamic client registration, PKCE).&lt;/li&gt;
&lt;li&gt;The person approves in the browser and the client gets a token.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Claude Code registered itself this way with no pre-shared client id. In Codex, &lt;code&gt;codex mcp add uplika --url https://api.uplika.com/mcp&lt;/code&gt; reads the server metadata and opens the browser on its own, so &lt;code&gt;codex mcp login&lt;/code&gt; is only a recovery command. In Claude Code, &lt;code&gt;--scope user&lt;/code&gt; matters: without it the server is added to the current folder only and "disappears" when you open another project.&lt;/p&gt;

&lt;p&gt;API keys still exist, but only for calling the REST API directly. Two auth axes that do not mix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't issue a token that can publish nowhere
&lt;/h2&gt;

&lt;p&gt;If a person has not connected any channel, approval stops and asks them to connect one first. A token that can publish nowhere is worse than no token, because the agent thinks it succeeded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool hints are part of the contract
&lt;/h2&gt;

&lt;p&gt;ChatGPT's app review looks at &lt;code&gt;readOnlyHint&lt;/code&gt; and &lt;code&gt;destructiveHint&lt;/code&gt;, and also requires &lt;code&gt;openWorldHint&lt;/code&gt;. The last one is not about destructiveness but about whether a tool reaches outside your own system, so it cannot be derived from the other two. We keep all three per tool in one table and a test fails when a tool is missing them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform details that bit us
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Bluesky link facets are UTF-8 byte offsets, not character indexes. A URL in the text is not a link until you build the facet yourself.&lt;/li&gt;
&lt;li&gt;Bluesky AT-URIs are about 70 characters, which overflowed the &lt;code&gt;VARCHAR(64)&lt;/code&gt; id column we had sized for other platforms. YouTube resumable upload session URLs (240+ characters) overflowed another 64-character column the same way.&lt;/li&gt;
&lt;li&gt;Telegram's caption limit counts code points. The Bot API describes entity offsets in UTF-16, so we assumed UTF-16; measuring showed 4,096 rocket emoji pass.&lt;/li&gt;
&lt;li&gt;A Telegram album is several messages. If you store one message id, deleting the post removes only the first photo.&lt;/li&gt;
&lt;li&gt;TikTok requires a privacy level with no default. We made it a required argument and check the creator's allowed options on every publish instead of filling one in.&lt;/li&gt;
&lt;li&gt;Cloudflare replaces 502 and 504 from the origin with its own error page and drops the CORS headers, so the browser cannot read the body. We return 503 for upstream failures.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Publishing is asynchronous
&lt;/h2&gt;

&lt;p&gt;Most platforms process media before a post is live, so publishing returns right away and the result arrives later. Agents that need to tell the person "it's live" pass &lt;code&gt;wait: true&lt;/code&gt;, and the server holds the answer until the platform confirms or a time budget runs out.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to try it
&lt;/h2&gt;

&lt;p&gt;There's a free plan, and setup for each agent is at &lt;a href="https://uplika.com/en/integrations" rel="noopener noreferrer"&gt;uplika.com/en/integrations&lt;/a&gt;. Questions are welcome in the comments or at &lt;a href="mailto:support@uplika.com"&gt;support@uplika.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>oauth</category>
      <category>api</category>
    </item>
  </channel>
</rss>
