<?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: Jhonny CommentKeyword</title>
    <description>The latest articles on DEV Community by Jhonny CommentKeyword (@jhonny_commentkeyword_6a0).</description>
    <link>https://dev.to/jhonny_commentkeyword_6a0</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%2F3946342%2F5dc41e46-cb0c-4b24-b9a7-15c87c968611.png</url>
      <title>DEV Community: Jhonny CommentKeyword</title>
      <link>https://dev.to/jhonny_commentkeyword_6a0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jhonny_commentkeyword_6a0"/>
    <language>en</language>
    <item>
      <title>Building an Instagram MCP server with zero dependencies: what we learned wiring Claude to comment-to-DM</title>
      <dc:creator>Jhonny CommentKeyword</dc:creator>
      <pubDate>Tue, 27 Oct 2026 21:49:39 +0000</pubDate>
      <link>https://dev.to/jhonny_commentkeyword_6a0/building-an-instagram-mcp-server-with-zero-dependencies-what-we-learned-wiring-claude-to-1cc5</link>
      <guid>https://dev.to/jhonny_commentkeyword_6a0/building-an-instagram-mcp-server-with-zero-dependencies-what-we-learned-wiring-claude-to-1cc5</guid>
      <description>&lt;p&gt;We run an Instagram comment-to-DM service: someone comments a keyword like GUIDE on a post, and they get a DM with the link. This month we published an open-source &lt;strong&gt;Instagram MCP server&lt;/strong&gt; for it, so Claude, Cursor or Codex can set those automations up from a chat while the creator is still editing the video.&lt;/p&gt;

&lt;p&gt;This post is about the engineering, not the product. The server is two files and about 400 lines of JavaScript, and most of the decisions in it are about one problem: an agent is calling tools that send messages to real people. If you're building an MCP server for an API with side effects, most of this applies to you too.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/commentkeyword/instagram-mcp-server" rel="noopener noreferrer"&gt;github.com/commentkeyword/instagram-mcp-server&lt;/a&gt; (MIT).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an Instagram MCP server with no dependencies
&lt;/h2&gt;

&lt;p&gt;Most MCP servers start from the official SDK. We didn't, for two reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;People run it on their own machine with an API token.&lt;/strong&gt; Anyone installing a server that holds a token should be able to read all of it in ten minutes. With no dependencies there's nothing in &lt;code&gt;node_modules&lt;/code&gt; to audit, and no supply chain beyond Node itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It installs straight from a Git tag.&lt;/strong&gt; &lt;code&gt;npx -y github:commentkeyword/instagram-mcp-server#v0.2.0&lt;/code&gt; works with no build step and no npm publish, and pinning a tag means the code you audited is the code that runs.&lt;/p&gt;

&lt;p&gt;The cost is that we implement the protocol ourselves. For a tools-only server over stdio, that turned out to be small.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stdio transport is 30 lines
&lt;/h2&gt;

&lt;p&gt;MCP over stdio is newline-delimited JSON-RPC 2.0. The entry point reads lines from stdin, parses them, and writes one JSON line per response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;createInterface&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;crlfDelay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;Infinity&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;line&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;32700&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Parse error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;handleMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handlers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;jsonrpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;32603&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Internal error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Logs go to stderr, never stdout.&lt;/strong&gt; Anything on stdout that isn't a JSON-RPC message breaks the client.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The server starts without a token.&lt;/strong&gt; Clients and registries spawn servers just to list their tools. If a missing token killed the process, the listing would fail, so each tool call reports the missing token instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;handleMessage&lt;/code&gt; answers &lt;code&gt;initialize&lt;/code&gt;, &lt;code&gt;ping&lt;/code&gt;, &lt;code&gt;tools/list&lt;/code&gt; and &lt;code&gt;tools/call&lt;/code&gt;, ignores notifications, and negotiates the protocol version: it echoes the client's version if we support it, and offers our newest one otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design tools around intent, not the API's internals
&lt;/h2&gt;

&lt;p&gt;Our REST API stores a keyword's reach as a &lt;code&gt;scope&lt;/code&gt; (&lt;code&gt;global&lt;/code&gt; or &lt;code&gt;post_specific&lt;/code&gt;), an &lt;code&gt;includeNextPost&lt;/code&gt; flag and a list of media IDs. That's a fine data model and a bad tool interface. Give an agent three fields that interact and it will eventually send a combination that means nothing.&lt;/p&gt;

&lt;p&gt;So the tool takes one enum, &lt;code&gt;target&lt;/code&gt;, and the server translates it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scopeFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mediaIds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next_post&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;post_specific&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;includeNextPost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;mediaIds&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;mediaIds&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all_posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;global&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;specific_posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;mediaIds&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;target "specific_posts" needs at least one media ID in mediaIds.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;post_specific&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;includeNextPost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mediaIds&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;next_post&lt;/code&gt; is the target creators use most. A video that's still in the edit has no Instagram media ID yet. With &lt;code&gt;next_post&lt;/code&gt;, the automation waits and attaches itself to the next post or Reel the account publishes, so the agent can set up the DM in the same session it writes the caption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools that message people need guardrails
&lt;/h2&gt;

&lt;p&gt;A keyword automation messages everyone who comments the word. A wrong DM to a few hundred people can't be unsent, so the guardrails are spread across several layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Server instructions.&lt;/strong&gt; The &lt;code&gt;initialize&lt;/code&gt; response tells the agent to confirm the keyword, the DM text and the target with the user before calling &lt;code&gt;create_keyword&lt;/code&gt; or &lt;code&gt;update_keyword&lt;/code&gt;. Clients that support MCP instructions put this in front of the model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool descriptions repeat it&lt;/strong&gt;, because not every client shows server instructions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Annotations.&lt;/strong&gt; &lt;code&gt;create_keyword&lt;/code&gt; is marked &lt;code&gt;destructiveHint: false, openWorldHint: true&lt;/code&gt;, and &lt;code&gt;delete_keyword&lt;/code&gt; is marked destructive, so clients can ask for approval.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A second step for deletes that lose work.&lt;/strong&gt; If a keyword has an AI agent attached, the first &lt;code&gt;delete_keyword&lt;/code&gt; call returns &lt;code&gt;DELETE_CONFIRMATION_REQUIRED&lt;/code&gt; with a summary of what would be lost. The agent has to show it to the user and call again with &lt;code&gt;confirm: true&lt;/code&gt;. The tool description also nudges the agent toward pausing (&lt;code&gt;isActive: false&lt;/code&gt;) instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat API text as data.&lt;/strong&gt; Keywords, DM texts and agent names come from the user's workspace, and the instructions say plainly that this text is data, not instructions. It's a cheap defense against prompt injection through stored content.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In practice the conversation looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You:    I'm posting a Reel tomorrow about my budget template. Anyone who comments
        BUDGET should get https://example.com/budget by DM.
Claude: Here's what I'll set up:
          Keyword: BUDGET (whole word)
          Posts: your next post or Reel
          DM: "Here's the budget template, {{namefirst}} 👉 https://example.com/budget"
        Create it?
You:    Yes
Claude: Done. BUDGET is waiting for your next post.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Return a sentence, then the JSON
&lt;/h2&gt;

&lt;p&gt;Tool results go back to the model as text. If you return only JSON, the model has to work out what the state means, and sometimes it guesses wrong. So each write returns a one-line summary first and the raw JSON after it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextPostStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;waiting&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;It is waiting for the next post or reel; it attaches itself once that post gets its first comments.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextPostStatus&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reconnect_required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Instagram needs to be reconnected in CommentKeyword before it can catch the next post.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since we added these lines, the agent reports "waiting for your next post" or "reconnect Instagram first" accurately instead of a cheerful "all set". The summary also gives the model the right words to use with a non-technical user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate before you call the API
&lt;/h2&gt;

&lt;p&gt;The server checks arguments against each tool's JSON Schema before calling the API, with a small validator (types, enums, required fields, string and array limits). A bad call comes back as an &lt;code&gt;isError&lt;/code&gt; result like &lt;code&gt;Invalid arguments: arguments.dmMessages allows at most 10 items.&lt;/code&gt;, which the model can fix and retry. An HTTP 400 from deep inside the API is harder for it to act on.&lt;/p&gt;

&lt;p&gt;API errors are passed through with their body, so a &lt;code&gt;409&lt;/code&gt; for a keyword that already exists reaches the model with the reason attached. In practice the agent then calls &lt;code&gt;list_keywords&lt;/code&gt; and suggests another word.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hard part was server-side
&lt;/h2&gt;

&lt;p&gt;The MCP layer turned out to be the easy part. "Wait for the next post" has real edge cases on the service side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two automations with the same keyword both waiting for the next post would both claim it, and only one would ever answer. We now allow one active waiter per keyword, checked under a per-workspace database lock, so two requests at the same moment can't both get through.&lt;/li&gt;
&lt;li&gt;The Instagram token can expire while a keyword is waiting. That's why &lt;code&gt;reconnect_required&lt;/code&gt; is a status the agent sees, not a silent failure.&lt;/li&gt;
&lt;li&gt;Instagram's messaging rules shape everything: a comment gets one private reply within 7 days, and more messages are only allowed within 24 hours after the person writes back. The tool descriptions don't promise anything the platform won't allow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your MCP server wraps an API, the edge cases in that API will matter more than the protocol code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing it end to end
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;npm test&lt;/code&gt; spawns the real server over stdio against a mock HTTP API and drives it with JSON-RPC: initialize, list tools, valid and invalid calls, the delete confirmation flow, and the missing-token path. Because there's no SDK in between, the tests check the exact bytes a client would see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;You need a CommentKeyword account (the Free plan includes the API), an Instagram professional account and an API token. In Claude Code:&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; stdio &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="nv"&gt;COMMENTKEYWORD_API_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YOUR_TOKEN commentkeyword &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; github:commentkeyword/instagram-mcp-server#v0.2.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setup for Claude Desktop, Cursor, Windsurf and Codex is on the &lt;a href="https://commentkeyword.com/instagram-mcp-server?utm_source=devto" rel="noopener noreferrer"&gt;Instagram MCP server page&lt;/a&gt;. The repo also has an Agent Skill that plans the "comment X" call to action in a Reel's script, and an n8n template.&lt;/p&gt;

&lt;p&gt;Questions about the design, or about building an MCP server for an API with side effects? Ask in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: we build CommentKeyword, the service this server talks to. This post was written with AI assistance; we checked every code sample against the repository.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>instagram</category>
      <category>ai</category>
      <category>node</category>
    </item>
    <item>
      <title>How to Automate Instagram Comment-to-DM Flows on Meta's Official API</title>
      <dc:creator>Jhonny CommentKeyword</dc:creator>
      <pubDate>Fri, 11 Sep 2026 17:09:10 +0000</pubDate>
      <link>https://dev.to/jhonny_commentkeyword_6a0/how-to-automate-instagram-comment-to-dm-flows-on-metas-official-api-2pfl</link>
      <guid>https://dev.to/jhonny_commentkeyword_6a0/how-to-automate-instagram-comment-to-dm-flows-on-metas-official-api-2pfl</guid>
      <description>&lt;p&gt;If you run Instagram lead generation, you know the pattern: a post takes off, hundreds of people comment with the keyword you asked for, and then you spend the evening manually sending DMs one by one. Here's how to automate that flow properly, on Meta's official Business API, without putting the account at risk.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flow at a high level
&lt;/h2&gt;

&lt;p&gt;The official pipeline looks like this: your webhook receives a comment event, your service checks the comment text for a trigger keyword, you post a public reply (engagement boost), then you send the commenter a private DM through the messaging endpoint. Everything runs through the Instagram Graph API with a business account linked to a Facebook Page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Webhooks and signature verification
&lt;/h2&gt;

&lt;p&gt;Meta sends comment events to your webhook URL with an &lt;code&gt;X-Hub-Signature-256&lt;/code&gt; header. Verify it by computing an HMAC SHA-256 of the raw body with your app secret before parsing anything. Skip this and anyone who finds your endpoint can inject fake comment payloads. Process events idempotently too - Meta retries deliveries, and the same comment can arrive twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 24-hour messaging window and private replies
&lt;/h2&gt;

&lt;p&gt;Here the rules get specific. The API's private reply feature lets you send exactly one automated DM in response to a comment. After that, a 24-hour messaging window applies from the user's last interaction; inside it you can send follow-ups, outside it you can't (except with approved message tags). Design the flow so the keyword DM lands immediately, and anything after that treats the 24-hour window as a hard constraint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate limits are real
&lt;/h2&gt;

&lt;p&gt;Every API response includes an &lt;code&gt;X-Business-Use-Case-Usage&lt;/code&gt; header with your current capacity consumption. Sustained automation that ignores it gets throttled, and throttling during a viral post is exactly when you can't afford it. Queue outgoing DMs, spread them over time, and treat the usage header as a gauge you never let hit the ceiling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tokens: short, long, and refresh cycles
&lt;/h2&gt;

&lt;p&gt;The initial token exchange gives you a short-lived token. Exchange it for a long-lived one (60 days), then refresh it before expiry. Store the refresh schedule as a first-class job, not an afterthought - the most common outage in homegrown Instagram automation is a dead token nobody remembered to rotate.&lt;/p&gt;

&lt;h2&gt;
  
  
  App Review
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;instagram_manage_comments&lt;/code&gt; and &lt;code&gt;instagram_manage_messages&lt;/code&gt; permissions require App Review. You'll submit screencasts showing each permission in use and a written use case. Budget a few days for it, and make your demo show the real flow end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why scraping is the wrong shortcut
&lt;/h2&gt;

&lt;p&gt;Tools that drive Instagram through the web client or emulated devices work until they don't: Meta's detection surfaces them, and the accounts using them take the hit. The official API path is more setup, but the account you're automating is the asset - treat it that way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this goes from prototype to product
&lt;/h2&gt;

&lt;p&gt;I built CommentKeyword, so read this knowing that. The flow above is the core of it: keyword trigger, public reply, instant DM, all on the official API with rate limiting handled. The parts that took months were the edges - conversation threading, AI follow-ups inside the 24-hour window, and multi-language keyword matching. If you want to see the finished flow, &lt;a href="https://commentkeyword.com/how-it-works" rel="noopener noreferrer"&gt;how CommentKeyword works&lt;/a&gt; breaks it down step by step.&lt;/p&gt;

&lt;p&gt;For a solo founder or a small team, the honest engineering advice is: the API integration is a weekend, the reliability work is the product. Both are worth it - the comment section is the highest-intent surface on Instagram, and answering it in seconds instead of hours is a measurable advantage.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>apigateway</category>
      <category>automation</category>
      <category>backend</category>
    </item>
    <item>
      <title>CommentKeyword</title>
      <dc:creator>Jhonny CommentKeyword</dc:creator>
      <pubDate>Fri, 22 May 2026 15:07:04 +0000</pubDate>
      <link>https://dev.to/jhonny_commentkeyword_6a0/commentkeyword-5e4d</link>
      <guid>https://dev.to/jhonny_commentkeyword_6a0/commentkeyword-5e4d</guid>
      <description>&lt;p&gt;CommentKeyword is an Instagram automation platform that helps creators, coaches, agencies, and businesses turn comment engagement into private conversations that convert. Instead of sending people to a link in bio or manually replying to every “GUIDE,” “LINK,” “PRICE,” or “INFO” comment, users can connect their Instagram Business account, create keyword-based triggers, and automatically respond while the person is still engaged.&lt;/p&gt;

&lt;p&gt;The product is built around a simple comment-to-DM workflow. Users choose a keyword, decide whether it should match exactly or appear anywhere in a comment, write public reply variants, add direct message variants, and launch the campaign. CommentKeyword monitors Instagram comments, detects matching keywords or phrases, replies publicly, and sends the right DM automatically. Campaigns can run globally across all posts or be limited to specific posts and reels.&lt;/p&gt;

&lt;p&gt;This workflow helps on two levels: it encourages people to comment, which improves organic reach, and it helps businesses do more with the attention they already receive by moving interested people into direct messages. A simple CTA like “comment GUIDE” or “comment LINK” can create more participation while also opening a direct, trackable channel for follow-up.&lt;/p&gt;

&lt;p&gt;For teams that need more than a one-off automated message, CommentKeyword includes optional AI-powered DM follow-up. Setting up the AI is designed to be simple: users describe what the agent should do, define the goal of the conversation, test it before going live, and assign it to specific keywords. Once someone replies to the initial automated DM, the AI agent can continue the conversation using the user’s instructions, ask qualifying questions, collect structured data, answer common questions, route people to the right offer or resource, and hand off to a human when needed.&lt;/p&gt;

&lt;p&gt;CommentKeyword is useful for lead magnets, coaching applications, product recommendations, quote requests, waitlists, event signups, setup meetings, content campaigns, and any Instagram workflow. For teams with custom systems, CommentKeyword also offers API access so keyword automation, DM workflows, usage data, and integrations can be managed programmatically.&lt;/p&gt;

&lt;p&gt;The platform starts free, with 300 automated DMs per month included. Users only need to pay when they go beyond that volume or want advanced features such as AI conversations. Pricing is based on automated DMs rather than active contacts, &lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
