Announcing CIMD support for MCP Client registration
Learn more

Instagram MCP vs API for AI Agents Integration

Nishant Choudhary
Tech Evangelist

Meta does not ship an MCP server for organic Instagram. Its only first-party MCP server is the Meta Ads MCP, whose Instagram tools boost posts and nothing more, while community servers wrap the Instagram API around one long-lived token. The Instagram API itself covers publishing, comments, DMs and insights for professional accounts. This guide compares what each option lets a social agent do, the logins and tokens each one puts you on, and how to serve many brands' accounts in production.

TL;DR

  • Meta does not ship an official MCP server for organic Instagram. Its only first-party MCP server is the Meta Ads MCP at https://mcp.facebook.com/ads, announced in April 2026. Its Instagram tools are three ad tools (ads_get_ig_accounts, ads_get_ig_media, ads_boost_ig_post). It cannot publish a post, reply to a comment, or answer a DM.
  • Every "Instagram MCP server" for organic work is a community project, such as AleemHaider/instagram-mcp (24 tools) or jlbadano/ig-mcp (8 tools). They run locally and read one long-lived token from an environment variable, so every request carries one account's identity.
  • The Instagram API works only with professional (Business or Creator) accounts. You reach it through Instagram Login (graph.instagram.com, instagram_business_* scopes, 60-day tokens you refresh), Facebook Login (graph.facebook.com, a linked Facebook Page, Page tokens), or Meta's newer Unified Login. Serving accounts you don't own requires Advanced Access, which means Meta App Review and Business Verification.
  • MCP is the faster path for a marketer asking Claude to boost last week's best reel or pull ad performance. The API is the right path for agents that publish on a schedule, moderate comments as webhooks arrive, answer DMs inside Meta's 24-hour window, or serve many brands' accounts.
  • Scalekit's instagram connector exposes 30 prebuilt tools over the Instagram API (21 read, 8 write, 1 destructive) with OAuth 2.0. Each user connects their Business or Creator account once, Scalekit stores and refreshes the token, and the MCP vs API decision doesn't change your auth infrastructure.

The decision your Instagram agent forces first

Your agent needs to work inside your customers' Instagram accounts: publish a carousel at 9am, hide a spam comment the moment it lands, send a private reply to someone asking about an order, or tell a brand manager which reels drove the most saves last month. You go looking for an Instagram Model Context Protocol (MCP) server and find a crowd of them on registries, plus a Meta-hosted MCP server that turns out to be for ads. Meta ships the Instagram API, three login configurations, webhooks, and an Ads MCP. It does not ship an Instagram MCP server. That shapes the whole decision: the MCP options are either first-party and ads-only, or full-featured and unofficial. Here's how to pick.

What Instagram MCP and Instagram API actually are

Instagram MCP

There is no official Instagram MCP server for organic content. The nearest first-party server is the Meta Ads MCP server, which Meta describes as "a Meta-hosted Model Context Protocol server" at https://mcp.facebook.com/ads. Meta announced it in April 2026 as part of its ads AI connectors. Its tools fall into seven groups: reporting, ad creation and management, catalogs, signals and datasets, help, A/B tests and lift studies, and activity logs. Its ad creation tools include an Instagram section with three entries: ads_get_ig_accounts lists Business or Creator accounts linked to an ad account, ads_get_ig_media lists boost-eligible media, and ads_boost_ig_post will "turn an existing organic Instagram post into a boosted ad." That's the full Instagram surface.

Auth on the Ads MCP is Facebook Login for Business, or a user or Employee system user access token sent as a Bearer token. The token needs ads_mcp_management plus ads_read or ads_management, according to Meta's get started guide. Owning a Meta app is optional for Claude, and the guide does not mention Dynamic Client Registration (DCR).

For organic work, the community filled the gap. AleemHaider/instagram-mcp is a FastMCP server with 24 tools across profile and media, hashtags, publishing (including publish_reel, publish_story, and publish_carousel), comments, DMs, and insights. It reads IG_USER_ID and IG_ACCESS_TOKEN from the environment and recommends a Page access token that "never expires." jlbadano/ig-mcp has 8 tools, needs FACEBOOK_APP_ID, FACEBOOK_APP_SECRET, INSTAGRAM_ACCESS_TOKEN, and INSTAGRAM_BUSINESS_ACCOUNT_ID, and uses a 60-day token. Both launch as local processes from a client config. Both are thin wrappers over the Instagram API, which is the good case. Some servers on registries log in with a username and password against Instagram's private mobile API instead, which puts the account at risk of a ban.

Instagram API

The Instagram Platform serves Instagram professional accounts, meaning Business and Creator accounts. Personal accounts lost API access when Meta ended the Instagram Basic Display API on December 4, 2024. The API covers content publishing, comment moderation, mentions, messaging, media and account insights, hashtag search, and business discovery. Meta documents three setups:

  • Instagram Login: the Instagram API with Instagram Login at graph.instagram.com. No Facebook Page is required. Scopes are instagram_business_basic, instagram_business_content_publish, instagram_business_manage_comments, and instagram_business_manage_messages. Hashtag search is not supported, and Meta says this setup "cannot access ads or tagging."
  • Facebook Login: graph.facebook.com, for accounts linked to a Facebook Page, with scopes such as instagram_basic, instagram_content_publish, instagram_manage_comments, instagram_manage_messages, and pages_read_engagement. Messaging runs through the Messenger Platform.
  • Unified Login: Meta's recommendation for new integrations, where users log in with Instagram or Facebook, without a Page.

Comparing them where it matters for agents

What your agent can actually do

Capability
Meta Ads MCP (mcp.facebook.com/ads)
Community MCP servers
Instagram API
Publish feed images, reels, and carousels
No
Yes
Yes
Publish stories
No
Yes
Yes: Story containers, though Scalekit's container tool does not accept stories
Read and reply to comments
No
Yes
Yes
Hide or delete comments
No
Yes
Yes
Read and send DMs
No
Limited: Read and send text only:; jlbadano needs Advanced Access
Yes
Private reply to a commenter
No
No: Not in either tool list
Yes
Organic account and media insights
No
Yes
Yes
Hashtag search and research
No
Yes
Limited: Facebook Login or Unified Login only
Competitor lookup (business discovery)
No
No
Yes
Mentions and photo tags
No
Limited
Yes
Boost an organic post into an ad
Yes
No
Limited: Marketing API, a separate surface; not in Scalekit's connector
Real-time events for comments, mentions, and messages
No
No
Yes: Webhooks:, and more

The table makes the gap plain. Meta's own MCP server and the organic Instagram API barely overlap: one is for spending money on posts, the other is for making and managing them. The community servers cover most of the organic surface, but they are one developer's wrapper, pinned to a Graph API version they chose (v21.0 for AleemHaider, v19.0 for jlbadano), with no webhook path.

What the MCP path makes easier

Publishing on the raw API is a three-step state machine. You create a container from a publicly hosted JPEG or video URL, poll its status_code until it reads FINISHED (Meta suggests "once per minute, for no more than 5 minutes"), then call media_publish with the container ID. A carousel adds a container per child, up to 10, before the parent. Containers expire after 24 hours unpublished. AleemHaider's publish_reel and publish_carousel fold that sequence into one tool call, which is the main thing a community server does for a model. Writing the schema is the hard part. Not the API call.

The Meta Ads MCP makes a different job easier: it resolves ad accounts, Pages, and linked Instagram accounts for you (ads_get_ad_accounts, ads_get_ig_accounts) and turns "boost my best reel from last week to people in Austin" into a handful of tool calls. Meta says new ad objects are created paused, which is a sensible guard for a model with a credit card.

The tradeoff is ownership. Meta owns the Ads MCP tool list and can change it under you. A community server is owned by whoever maintains the repository, and you inherit their choices on retries, rate limits, and which permissions they ask for.

The auth path each one puts you on

On the Meta Ads MCP path, auth is Facebook Login for Business in the browser, or a user or system user token you supply. The identity is the Facebook user who signed in, and what they can reach is decided by their roles in Meta Business Manager. That's a reasonable model for ads. It doesn't help an organic agent, because none of the tools publish or moderate.

On the community MCP path, auth is whatever token you paste into an environment variable. AleemHaider's README walks you through exchanging a short-lived user token for a long-lived one (about 60 days), then pulling the Page's access_token, which Meta says does "not have an expiration date." That token sits in a .env file or a client config on someone's laptop. Every user of that server acts as that one Page.

On the direct API path, you run OAuth yourself. With Instagram Login, the user authorizes at https://www.instagram.com/oauth/authorize, the one-hour code is exchanged at https://api.instagram.com/oauth/access_token for a short-lived token, and you trade that at graph.instagram.com/access_token for a long-lived token valid for 60 days. You refresh it with refresh_access_token once it is at least 24 hours old, and Meta is blunt about the failure mode: "Tokens that have not been refreshed in 60 days will expire and can no longer be refreshed." With Facebook Login, you get a user token that expires in hours, exchange it for a roughly 60-day token, and derive Page tokens from it.

Then comes access level. Standard Access only works for accounts you own or manage, or that have a role on your app. To serve your customers' accounts, you need Advanced Access, which "requires App Review and Business Verification." Plan for that before you promise a launch date.

What the user can't do, the agent can't do. On Instagram that holds only when each brand's account authorizes your app separately and your agent uses that account's token.

Both paths still need per-user isolation

In a multi-tenant B2B agent, MCP's OAuth flow gives you a token per user and the direct API gives you a credential per user. In neither case does the path itself solve storage, rotation, or revocation; those are infrastructure problems regardless of which path you choose.

Instagram sharpens this because the account is the brand. A social media tool serving 200 brands holds 200 Instagram professional accounts' publishing rights. A token mixed up between two tenants doesn't leak a report; it posts one brand's product launch on a competitor's feed, or sends a DM from the wrong account. The community servers can't express this at all: one process, one IG_ACCESS_TOKEN, one account.

Recommended reading: access control for multi-tenant AI agents

What you own in production

On the Meta Ads MCP path, Meta hosts the server and owns the schemas. You own the gap: everything organic. You also own the risk of an agent that can activate spend, since ads_activate_entity switches an item to active.

On the community MCP path, you own the server process, its Python dependencies, its pinned Graph API version, and the token on disk. When Meta deprecates the version the server targets, you patch it or wait for the maintainer.

On the API path, you own the full lifecycle, and Instagram has more moving parts than most APIs:

  • Rate limits. Most endpoints use Instagram Business Use Case limits: "Calls within 24 hours = 4800 * Number of Impressions," where impressions are views of the account's content in the last 24 hours. A small account gets a small budget. Messaging has its own ceilings: 2 calls per second for the Conversations API, 750 per hour for private replies to post and reel comments, and 100 per second for text sends.
  • Publishing quota. Meta's content publishing guide says accounts are limited to 100 API-published posts in a moving 24-hour period, while its carousel section says 50, and Scalekit's tool description cites 25. Don't hard-code any of them. Read config and quota_usage from the content_publishing_limit endpoint before a batch.
  • Hashtag quota. 30 unique hashtags per account in a rolling 7-day window.
  • Media hosting. JPEG is the only image format, and the file "must be hosted on a publicly accessible server at the time of the attempt." Your agent needs somewhere to put assets.
  • The 24-hour window. "Your app has 24 hours to respond to any message sent from an Instagram user," and you can only message someone after they messaged the account first.
  • Webhooks. Instagram webhooks cover comments, live_comments, mentions, messages, story_insights, and messaging events. The app must be Live, comment webhooks need Advanced Access, and the account must be public. Each delivery carries X-Hub-Signature-256, a SHA256 HMAC of the payload with your App Secret prefixed with sha256=. Meta retries failed deliveries with decreasing frequency for 36 hours, may batch up to 1,000 updates, and expects you to deduplicate.

When to use MCP, when to use the API

Use an Instagram MCP server when:

  • A marketer in Claude asks which organic reels performed best this month and wants the top one boosted, and the Meta Ads MCP's ads_get_ig_media and ads_boost_ig_post handle the boost with the ad created paused.
  • A creator wants to manage their own account from a desktop client, drafting captions, publishing a carousel, and replying to comments, and a local community server with their own token is enough.
  • You are prototyping a social assistant for one brand you own, under Standard Access, and want tools working before you write any OAuth.
  • The job is mostly ads reporting and campaign edits across Facebook and Instagram placements, which is exactly what Meta's server was built for.

Use the Instagram API when:

  • The agent runs headless: a comments webhook fires, the agent classifies the comment, and hides spam with instagram_comment_hide_update or sends an order question to DMs with instagram_comment_private_reply_create.
  • The workflow is a deterministic pipeline: a content calendar publishes approved posts at fixed times, checking the publishing quota and polling container status before each instagram_media_publish.
  • You serve many brands or creators, each with their own account, token, webhook subscription, and rate-limit budget, under Advanced Access.
  • The task needs what no MCP option exposes: private replies, business discovery for competitor research, mentions in captions, or event-driven DMs within the 24-hour window.

The credential problem that exists on both paths

Both paths hand you an Instagram credential per account. Neither hands you a vault. A social tool serving 200 brands, with 3 accounts each, holds 600 tokens. With Instagram Login, each is a 60-day token that dies for good if it goes 60 days without a refresh, and can't be refreshed until it is 24 hours old. With Facebook Login, the Page tokens your agent prefers have no expiration date at all, which makes each one a standing grant to publish as that brand until someone revokes it. Every one must be stored encrypted, kept out of logs and LLM context, mapped to exactly one account and one customer, and deleted when the customer churns or the user removes your app.

The MCP path changes the shape, not the problem. The Ads MCP holds a Business Manager login per user. A community server holds one token per process, so isolating 600 accounts means 600 processes or a fork that adds per-request tokens. The tempting shortcut is one agency-level Page token wired into a shared server. Fewer secrets, larger blast radius: one leak lets anyone post as every brand behind it.

Scalekit's instagram connector runs the OAuth flow, stores each account's token in the token vault, and refreshes it, so the MCP vs API decision doesn't change your auth infrastructure. Your agent passes an identifier; credentials never touch the agent runtime or the LLM context.

Recommended reading: how to handle token refresh for AI agents

How to connect an Instagram agent with Scalekit

Scalekit's instagram connector is an API-path connector built by Scalekit with 30 tools: 21 read, 8 write, and 1 destructive (instagram_comment_delete). It covers profile and media, the two-step publish flow, comments and replies, Direct conversations and sends, account and media insights, hashtags, business discovery, mentions, tags, stories, and oEmbed. It authenticates with OAuth 2.0 through Facebook Login, so each connected account must be a Business or Creator account linked to a Facebook Page.

Tool
What it does
instagram_media_list
Lists the account's posts, reels, and stories, cursor-paginated
instagram_media_insights_get
Returns metrics such as reach, saves, and plays for one media item
instagram_comments_list
Lists top-level comments on a media item
instagram_comment_hide_update
Hides or unhides a comment without deleting it
instagram_media_container_create
Step 1 of publishing: creates a container from a public URL
instagram_media_publish
Step 2 of publishing: publishes a finished container

Prerequisites and the connection name

In the Scalekit dashboard, go to AgentKit > Connections and create an Instagram connection. The name you give it is the connection_name your code passes, and the string must match the dashboard exactly. This is the most common integration error.

Scalekit's shared credentials work for testing. For production, create a Meta app of the Business type, add the Instagram Graph API and Facebook Login products, paste the connection's redirect URI into Facebook Login > Settings > Valid OAuth Redirect URIs, and enter the App ID and App secret as the connection's Client ID and Client Secret. To serve accounts you don't own, that Meta app needs Advanced Access, so budget time for App Review and Business Verification. Copy SCALEKIT_ENVIRONMENT_URL, SCALEKIT_CLIENT_ID, and SCALEKIT_CLIENT_SECRET from Developers > API Credentials into .env.

pip install scalekit-sdk-python python-dotenv langchain-openai

Authorize each user once

get_or_create_connected_account returns the connected account for an identifier. If it is not ACTIVE, get_authorization_link returns the Facebook Login link the user opens once to grant your app access to their Instagram professional account. Scalekit stores the resulting token; your code never receives it. Most tools also take an ig_user_id, the Instagram professional account ID. With Facebook Login, that ID is the instagram_business_account field on the user's Page, and you can read it once through Scalekit's API proxy and store it with the user. The connector page doesn't document the proxy's Graph API version, so if the path below returns an error, check the response body before assuming the account isn't linked.

import os from dotenv import load_dotenv from scalekit import ScalekitClient load_dotenv() scalekit_client = ScalekitClient( env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) actions = scalekit_client.actions CONNECTION_NAME = "instagram" # must match the connection name in AgentKit > Connections identifier = "brand_acme_user_42" # your app's stable ID for this user at this customer account = actions.get_or_create_connected_account( connection_name=CONNECTION_NAME, identifier=identifier, ) if account.connected_account.status != "ACTIVE": link = actions.get_authorization_link( connection_name=CONNECTION_NAME, identifier=identifier, ) print("Connect your Instagram professional account:", link.link) input("Press Enter after approving access...") account = actions.get_or_create_connected_account( connection_name=CONNECTION_NAME, identifier=identifier, ) if account.connected_account.status != "ACTIVE": raise RuntimeError(f"Instagram is {account.connected_account.status}, not ACTIVE") # Resolve the Instagram professional account ID once and store it with the user. pages = actions.request( connection_name=CONNECTION_NAME, identifier=identifier, method="GET", path="/me/accounts", query_params={"fields": "name,instagram_business_account"}, ) if pages.status_code != 200: raise RuntimeError(f"Page lookup failed: {pages.status_code} {pages.text}") linked = [p for p in pages.json().get("data", []) if p.get("instagram_business_account")] if not linked: raise RuntimeError("No Facebook Page with a linked Instagram professional account") IG_USER_ID = linked[0]["instagram_business_account"]["id"] print("Instagram account ID:", IG_USER_ID, "via Page", linked[0]["name"])

Retrieve the Instagram tools this account can call

This is not a catalog load. The agent asks for the tools this connected account is authorized to call, then narrows them to the six a performance analyst needs. All six are read-only. instagram_media_publish, instagram_message_send, and instagram_comment_delete never reach the model, so a reporting question can't turn into a post, a DM, or a deleted comment.

tools = actions.langchain.get_tools( identifier=identifier, connection_names=[CONNECTION_NAME], tool_names=[ "instagram_user_get_profile", "instagram_media_list", "instagram_media_get", "instagram_media_insights_get", "instagram_account_insights_get", "instagram_comments_list", ], page_size=100, ) tool_map = {t.name: t for t in tools} print(sorted(tool_map))

Run the agent loop

The loop runs until the model stops requesting tools. Every call runs with this user's Instagram token, against the one account they connected.

from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage from langchain_openai import ChatOpenAI llm = ChatOpenAI(model="gpt-4o").bind_tools(tools) messages = [ SystemMessage( "You analyze an Instagram professional account's organic performance. " f"The account's ig_user_id is {IG_USER_ID}; pass it to every tool that needs it. " "Use instagram_media_list with fields 'id,caption,media_type,timestamp,permalink' " "and follow the 'after' cursor until posts are older than the requested range. " "For each post in range, call instagram_media_insights_get with metric " "'reach,saved,shares,total_interactions'. If a metric is rejected for a media " "type, retry with 'reach,saved' and say which metrics were unavailable. " "Never guess IDs or numbers." ), HumanMessage( "Which five posts published between 2026-10-01 and 2026-10-28 had the most saves " "relative to reach? For each, give the permalink, media type, reach, saves, and " "a one-line read of what the top comments say." ), ] while True: response = llm.invoke(messages) messages.append(response) if not response.tool_calls: print(response.content) break for tc in response.tool_calls: tool = tool_map.get(tc["name"]) if tool: result = tool.invoke(tc["args"]) else: result = f"Unknown tool: {tc['name']}" messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))

Deterministic calls with execute_tool

For pipelines, skip the model. execute_tool runs one tool with a fixed input and returns data plus an execution_id you can log. The first job publishes an approved post from a content calendar: it checks the quota, creates a container, waits for FINISHED, and publishes. The image URL must be a public JPEG.

import time def publish_approved_post(identifier: str, ig_user_id: str, image_url: str, caption: str) -> str: quota = actions.execute_tool( tool_name="instagram_content_publishing_limit_get", tool_input={"ig_user_id": ig_user_id}, connection_name=CONNECTION_NAME, identifier=identifier, ) entry = quota.data.get("data", [{}])[0] used = entry.get("quota_usage", 0) total = entry.get("config", {}).get("quota_total") print(quota.execution_id, "quota", used, "of", total) if total is not None and used >= total: raise RuntimeError("Publishing quota exhausted for this 24-hour window") container = actions.execute_tool( tool_name="instagram_media_container_create", tool_input={"ig_user_id": ig_user_id, "image_url": image_url, "caption": caption}, connection_name=CONNECTION_NAME, identifier=identifier, ) container_id = container.data["id"] print(container.execution_id, "container", container_id) for _ in range(5): # Meta: check once per minute, for no more than 5 minutes status = actions.execute_tool( tool_name="instagram_media_container_status_get", tool_input={"container_id": container_id}, connection_name=CONNECTION_NAME, identifier=identifier, ) code = status.data.get("status_code") if code == "FINISHED": break if code in ("ERROR", "EXPIRED"): raise RuntimeError(f"Container {container_id} is {code}: {status.data}") time.sleep(60) else: raise TimeoutError(f"Container {container_id} not FINISHED after 5 minutes") published = actions.execute_tool( tool_name="instagram_media_publish", tool_input={"ig_user_id": ig_user_id, "creation_id": container_id}, connection_name=CONNECTION_NAME, identifier=identifier, ) print(published.execution_id, "published", published.data.get("id")) return published.data["id"]

The second job is a webhook handler. Meta posts a comments event to your endpoint; the handler verifies X-Hub-Signature-256 with your Meta App Secret, looks up which customer owns the Instagram account in entry[].id, and hides comments that match a blocklist. A comment asking about an order gets a private reply instead, which moves the conversation to DMs.

import hashlib import hmac import json BLOCKLIST = ("free followers", "dm me to earn", "crypto giveaway") ORDER_WORDS = ("order", "refund", "shipping", "tracking") def verify_meta_signature(raw_body: bytes, header: str, app_secret: str) -> bool: """Meta signs the raw payload with HMAC-SHA256 using the App Secret, prefixed sha256=.""" if not header or not header.startswith("sha256="): return False expected = hmac.new(app_secret.encode(), raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, header.removeprefix("sha256=")) def handle_instagram_webhook(raw_body: bytes, signature_header: str, account_owner: dict) -> None: """account_owner maps an Instagram account ID to your Scalekit identifier.""" if not verify_meta_signature(raw_body, signature_header, os.environ["META_APP_SECRET"]): raise PermissionError("Invalid X-Hub-Signature-256") payload = json.loads(raw_body) for entry in payload.get("entry", []): owner = account_owner.get(str(entry.get("id"))) if owner is None: continue # not an account any customer connected for change in entry.get("changes", []): if change.get("field") != "comments": continue value = change.get("value", {}) comment_id = value.get("id") text = (value.get("text") or "").lower() if not comment_id: continue if any(phrase in text for phrase in BLOCKLIST): hidden = actions.execute_tool( tool_name="instagram_comment_hide_update", tool_input={"comment_id": comment_id, "hide": True}, connection_name=CONNECTION_NAME, identifier=owner, ) print(hidden.execution_id, "hid", comment_id) elif any(word in text for word in ORDER_WORDS): replied = actions.execute_tool( tool_name="instagram_comment_private_reply_create", tool_input={ "comment_id": comment_id, "message": "Thanks for reaching out. Reply here with your order number and we'll check on it.", }, connection_name=CONNECTION_NAME, identifier=owner, ) print(replied.execution_id, "private reply to", comment_id)

The webhook subscription itself isn't a connector tool, and the connector page lists no Scalekit triggers for Instagram. You configure the comments field on your own Meta app, which needs to be Live with Advanced Access, and point it at your endpoint. Meta retries for up to 36 hours, so store handled comment IDs and skip repeats: hiding an already hidden comment is harmless, but a second private reply is not. No MCP option has an equivalent for this step.

Multi-tool, multi-tenant Instagram agents on a virtual MCP server

The instagram connector has 30 tools. At roughly 200 tokens each, that is about 6,000 tokens of definitions before the agent does any work. Accuracy is the sharper problem: a model choosing among instagram_comment_reply_create, instagram_comment_private_reply_create, and instagram_message_send picks the wrong one more often than a model holding only the one it needs, and on Instagram the wrong one is public. A community manager assistant that also holds instagram_media_publish can post on a misread instruction. The fix is not better prompting. It is surface reduction. A Scalekit virtual MCP server declares exactly which tools an agent role can see, and session tokens keep each run bound to one user's connected accounts. Docs: set up a virtual MCP server.

Create the server once per agent role

from scalekit.actions.models.mcp_config import McpConfigConnectionToolMapping vmcp = actions.mcp.create_config( name="instagram-community-manager", description="Triages Instagram comments and DMs for a brand and posts a daily summary to Slack", connection_tool_mappings=[ McpConfigConnectionToolMapping( connection_name="instagram", tools=[ "instagram_media_list", "instagram_comments_list", "instagram_comment_hide_update", "instagram_comment_reply_create", "instagram_conversations_list", "instagram_conversation_messages_list", ], ), McpConfigConnectionToolMapping( connection_name="slack", tools=["slack_send_message"], ), ], ) print(vmcp.config.id, vmcp.config.mcp_server_url)

Mint a session token per user, per run

One server definition serves every brand. Before each run, check that the user's accounts are active and mint a short-lived session token scoped to them. Any MCP client that speaks Streamable HTTP can connect; this example uses langchain-mcp-adapters (pip install langchain-mcp-adapters) and passes the token as a Bearer header.

import asyncio from datetime import timedelta from langchain_mcp_adapters.client import MultiServerMCPClient config_id = vmcp.config.id mcp_server_url = vmcp.config.mcp_server_url user_id = "brand_acme_user_42" accounts = actions.mcp.list_mcp_connected_accounts( config_id=config_id, identifier=user_id, include_auth_link=True, ) not_ready = [ a for a in accounts.connected_accounts if str(a.connected_account_status).upper() != "ACTIVE" ] for a in not_ready: print(f"{a.connection_name} is {a.connected_account_status}: {a.authentication_link}") if not_ready: raise RuntimeError("Connect the accounts above before starting this run") session_token = actions.mcp.create_session_token( mcp_config_id=config_id, identifier=user_id, expiry=timedelta(minutes=30), ).token async def load_tools(): client = MultiServerMCPClient( { "scalekit": { "transport": "streamable_http", "url": mcp_server_url, "headers": {"Authorization": f"Bearer {session_token}"}, } } ) return await client.get_tools() mcp_tools = asyncio.run(load_tools()) print([t.name for t in mcp_tools])

The agent sees seven tools instead of 30 plus everything Slack exposes. The endpoint is static; the identity is per user. This is the piece the community servers are missing: a remote MCP endpoint with organic Instagram tools where every run acts as a different brand's account, with no token in a config file.

Observability: every Instagram action tied to a user

Every execute_tool call returns an execution_id, and every execution is attributed to a connected account, which maps to one user at one customer. When a brand asks who hid 40 comments on Tuesday's launch post, or which run sent a private reply that upset a customer, the answer is a lookup, not an investigation.

Instagram's own record is thinner than you'd expect. A post published through the API, a hidden comment, or a sent DM is attributed to the professional account and your Meta app, not to the person in your product who asked for it. If a team of five social managers shares one connected account, Instagram can't tell them apart, so your execution log is the only place that records who asked. On a community MCP server with one Page token, there is no attribution at all beyond "the token did it." Shared credentials are a single-user solution. They do not survive a second user.

Recommended reading: audit trails for agent auth

Which one to build against

If your agent is a marketer's assistant for paid work, reporting on campaigns and boosting organic posts with a human approving spend, Meta's Ads MCP is the only first-party MCP option, and it is good at that job. If you want a model managing one account you own from a desktop client, a community server like AleemHaider/instagram-mcp works, as long as you accept an unofficial wrapper and a token on disk. If your agent publishes on a schedule, moderates comments from webhooks, answers DMs inside the 24-hour window, or serves many brands, build against the Instagram API, where you choose the login path, the tools, and the retry behavior. Either way, you hold a token per Instagram account that can publish as that brand, sometimes with no expiry, and that's what needs production-grade infrastructure.

FAQs

Does Instagram have an official MCP server?

Not for organic content. Meta's only first-party MCP server is the Meta Ads MCP, announced in April 2026, and its Instagram tools list linked accounts, list boost-eligible media and boost an existing post. It cannot publish, reply to a comment or answer a DM. Every organic Instagram MCP server is a community project.

Are community Instagram MCP servers safe to use?

The good ones are thin wrappers over the Instagram API, but they run locally, read one long-lived token from an environment variable and act as one account, pinned to a Graph API version their maintainer chose. Some servers on registries log in with a username and password against Instagram's private mobile API, which risks a ban.

Which accounts can use the Instagram API?

Only professional accounts, meaning Business and Creator accounts. Personal accounts lost API access when Meta ended the Basic Display API in December 2024. To serve accounts you do not own or manage, your Meta app needs Advanced Access, which requires App Review and Business Verification.

How long do Instagram API tokens last?

With Instagram Login, a long-lived token lasts 60 days and can be refreshed once it is at least 24 hours old, and a token not refreshed within 60 days expires for good. With Facebook Login, a user token is exchanged for a roughly 60-day token, and the Page tokens derived from it have no expiration date.

When should an agent use the Instagram API instead of an MCP server?

Use the API for scheduled publishing, comment moderation driven by webhooks, private replies and DMs inside the 24-hour window, business discovery, and serving many brands' accounts under Advanced Access. Use the Meta Ads MCP for boosting and ads reporting, or a community server for one account you own.

How does Scalekit connect an AI agent to Instagram?

Scalekit's Instagram connector exposes 30 tools over the Instagram API with OAuth through Facebook Login. Each user connects their Business or Creator account once, and Scalekit stores and refreshes the token so the agent never sees it. You narrow tools per agent role and serve them through a virtual MCP server, one brand per run.

Build your Instagram agent with Scalekit

Browse the Scalekit Instagram connector and the Instagram connector docs with the full tool list. See Scalekit pricing, or talk to us.

No items found.
On this page
Share this article
Scalekit
Find us on ProductHunt

Acquire enterprise customers with
‍zero upfront cost.

Every feature unlocked. No hidden fees.