Instagram MCP
Allows interaction with Instagram Business/Creator accounts through Meta's official APIs, including profile and media management, publishing images/carousels/reels/stories, scheduling content, managing comments, reading and sending messages, retrieving insights, and archiving conversation history. Also supports optional read-only Direct inbox access through imported private sessions.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
Community source release (MIT). Self-host using your own credentials. No NabuxAI production accounts or hosted-service access are included.
Instagram MCP by Nabux
Multi-user Instagram MCP Gateway. Official Meta login and sending stay on their existing path; an optional private-session account type can read Direct messages only.
Official API, Nabux gateway: Instagram operations use Meta's supported Instagram API / Business Login. The MCP server itself is built and operated by Nabux; it is not a Meta-authored MCP server.
Production target: https://ig.nabuxai.com
MCP endpoint: https://ig.nabuxai.com/mcp
Architecture
A user visits ig.nabuxai.com, authorizes an Instagram Business or Creator account through Meta's Instagram Business Login, and may connect additional official accounts to the same Nabux owner. Meta access tokens are encrypted at rest and automatically refreshed. Separately, an owner can opt in to a private session reader from the dashboard. It stores the imported session settings encrypted and exposes Direct inbox reads only.
ChatGPT, Claude, Cursor, Claude Code, and other MCP clients connect to /mcp using OAuth 2.1 + PKCE. The AI client receives a Nabux MCP token, not the Meta access token. Protected-resource metadata is served at both /.well-known/oauth-protected-resource/mcp and /.well-known/oauth-protected-resource. An unauthenticated POST /mcp returns 401 with WWW-Authenticate pointing at the resource metadata.
Instagram Business/Creator
│
│ Meta official OAuth / Instagram Business Login
▼
https://ig.nabuxai.com
│
├── encrypted Meta tokens + multi-account mapping
├── /mcp ─────────────► ChatGPT / Claude / Cursor / MCP clients
└── /internal/v1/... ──► NabuGen and trusted Nabux services
Instagram operations ─────────► graph.instagram.com (Meta official API)
Private inbox reads ──────────► isolated session account store (read-only)The official Meta account store and all publish/send handlers remain unchanged. Private session accounts live in a separate table and are not eligible for official publishing or sending. The private reader uses the unofficial instagrapi session API, and Instagram can reject the cookie or restrict the account. Use a dedicated account if that risk is unacceptable. No private send, reaction, request-approval, mark-read, or thread-mutation tool is exposed.
Related MCP server: Instagram MCP Server
MCP tools
Accounts & content
instagram_accountsinstagram_profile— reads the professional profile and stores the username when Meta returns oneinstagram_account_refresh— same read, for a row whose username is still emptyinstagram_list_mediainstagram_publishing_limitinstagram_publish_imageinstagram_publish_carousel— 2 to 10 children, images and videos. A video child ismedia_type=VIDEOand is waited on untilFINISHEDbefore the parent is published.instagram_publish_reel— public HTTPSvideo_url; a staged MP4 URL from this gateway works.instagram_publish_story— one public HTTPS image or video URL; a staged JPEG or MP4 URL works.instagram_stage_image— stage Base64 JPEG/PNG/WebP bytes; returns opaque temporary URL and ID.instagram_publish_staged_image— publish one staged image, clean it after success.instagram_publish_staged_carousel— publish 2–10 staged images in order and clean them after success.
Content calendar
These publish on this gateway. They do not need Postiz.
instagram_schedule— queuefeed_image,carousel,reel,story_image,story_video, ortext_card.accountis optional (the default account). Passtarget_idinstead ofaccountto hold the post until that exact Instagram account is connected.media_urlsandmedia_ids(library files) both work.scheduled_atis ISO-8601; an offset is stored as UTC, and a naive value usestimezoneor the owner's timezone (Asia/Muscatuntil they set one).instagram_list_scheduled— filter bystatusand astart/enddate range. Each row includesattempt_logandlast_error.instagram_update_scheduled— change caption, time, media, or type while the post is still scheduled.instagram_cancel_scheduledinstagram_publish_scheduled_now
text_card draws the caption onto a 1080×1350 image (1080×1920 when frame is story) with a font that includes Persian and Arabic, then publishes that image. The full text is also the caption. A background worker publishes due posts once, with a database lease so a restart or a second process does not publish the same row twice. A post more than six hours late is marked failed with missed. Transient errors retry with backoff up to SCHEDULE_MAX_ATTEMPTS (default 5). Every attempt's error is stored on the row and shown by instagram_list_scheduled and the calendar day view. A post aimed at a target that is not connected yet stays awaiting_account and is never marked missed and never published to another account.
/calendar is the month view. /calendar/new is the schedule form.
Comments
instagram_get_commentsinstagram_reply_commentinstagram_hide_commentinstagram_delete_comment
Inbox / Messaging
instagram_list_conversations— live, from Meta (first page)instagram_get_conversationinstagram_get_messageinstagram_send_messageinstagram_send_media
Optional private-session inbox (read-only)
instagram_private_accountsinstagram_private_conversationsinstagram_private_thread
The private tools read one-to-one Direct threads, including message requests, through the imported session. The dashboard accepts the cookie only over the authenticated, CSRF-protected HTTPS session; the cookie is encrypted at rest and never returned by an MCP tool. The validated session settings remain saved while the account is connected, so repeated reads do not require another login; disconnect the account to erase the saved settings. Reads reserve a persistent per-account slot at least five seconds apart, including calls from separate MCP workers. The thread tools keep a resumable cursor so historical reads can continue in bounded pages. These tools cannot send, react, approve a request, mark a thread read, or edit a conversation.
Meta's official Send API is conversation-based: it is not a cold-DM/spam API. A recipient must be eligible under Meta's messaging rules (normally they have already messaged/interacted with the professional account).
Conversation archive
Meta's Conversations API cannot return a full history. It exposes only the 20 most recent messages of a thread, and never returns Requests-folder threads idle for 30 days. A complete history therefore cannot be fetched on demand — it has to be accumulated, which is what this gateway does:
instagram_backfill_history— walk every conversation Meta still returns and archive what it holds. A bounded snapshot of the past, subject to the limits above.instagram_conversations— list archived threads, most recently active first. Reads the local archive, so it sees the whole recorded inbox rather than one page.instagram_thread— read one thread with a person, oldest first, paging back withbefore.
Every verified webhook delivery is archived before Meta is answered, for the same reason the relay queue is: Meta does not redeliver what it has acked, and no later API call can fetch the message back. Both directions are kept, so threads read as conversations rather than monologues. Run the backfill once per account at connect time; from then on the live stream keeps the archive complete.
Insights
instagram_account_insightsinstagram_media_insights
Metrics are passed through to Meta so the MCP can follow metric-specific requirements as the Graph API evolves.
Every tool resolves account only inside the authenticated user's own connected accounts. account can be a 1-based position, @username, Instagram user id, or the Nabux Account Key.
Multi-account dashboard
/dashboard shows every connected Instagram account and an opaque Account Key, for example:
ig_Q9m...random...This is not a Meta token. It is a random identifier that trusted Nabux services can use to address an account through the gateway. Raw Meta tokens never leave this service.
The first connected account is the default for MCP calls that omit account. Additional Instagram accounts can be connected from the dashboard.
Appearance
User-facing pages share one stylesheet, app/static/nabux.css, served at /static/nabux.css. Other Nabux MCP dashboards can reuse that file. It follows a minimal Swiss layout: one violet primary action, self-hosted DM Sans and Space Grotesk (no third-party font host), and a right-to-left page when the browser language is Persian or Arabic.
Sign in with NabuAuth
The dashboard owner is a NabuAuth user (https://auth.nabuxai.com), the same account NabuCRM, NabuPilot and NabuGate use. Every Instagram account connected while that user is signed in is attached to them. The three MCP connectors resolve to that same user, so they share the accounts instead of each creating an owner.
Register this client in NabuAuth apps.yaml, then redeploy NabuAuth (clients load at boot):
- id: instagram-mcp
name: Instagram
description: Instagram accounts for Nabux MCP clients
url: https://ig.nabuxai.com
redirect_uris:
- https://ig.nabuxai.com/auth/nabu/callback
scopes: [openid, profile, email]
secret_env: NABUAUTH_SECRET_INSTAGRAM_MCPGenerate one secret (openssl rand -base64 32). Set it on the NabuAuth host as NABUAUTH_SECRET_INSTAGRAM_MCP and on this gateway as NABUAUTH_CLIENT_SECRET. The redirect URI is matched exactly.
NABUAUTH_URL=https://auth.nabuxai.com
NABUAUTH_CLIENT_ID=instagram-mcp
NABUAUTH_CLIENT_SECRET=<the same secret>
NABUAUTH_REDIRECT_URI=https://ig.nabuxai.com/auth/nabu/callbackLeave NABUAUTH_CLIENT_ID empty only as a fallback: the old per-browser owner is used, and each connect without a cookie can still create an owner.
Log out clears this gateway's session cookie and sends the browser to GET https://auth.nabuxai.com/oauth/logout (end_session_endpoint) with post_logout_redirect_uri set to https://ig.nabuxai.com/signed-out. NabuAuth allows that redirect when the origin matches the app's url or any redirect_uris entry, so the url: https://ig.nabuxai.com line above is the field that covers it. There is no separate post-logout list. /oauth/logout does not read id_token_hint. Logging out does not delete Instagram accounts or tokens.
The first time someone signs in, the browser's existing owner (the one in the session cookie, with no Nabu identity yet) is adopted, tokens included. Other accounts connected before sign-in are listed once on the dashboard as "Add to my account". Adding one moves the encrypted token onto this login. An account that already belongs to a different Nabu user is left where it is.
Connect on a phone
/connect shows the Instagram authorize link on a full page. It does not open a popup and it does not depend on window.opener. Use Continue for a normal browser. If the Instagram app opens the link and shows no Allow button, copy the link into Chrome or Safari.
Instagram sends the browser back to GET /instagram/callback?code=...&state=... (POST is accepted as well). The state value is an HMAC signed with APP_SECRET_KEY and carries the owner id and the MCP transaction id, so the flow can start in one browser and finish in another without the session cookie. The signature is single-use. MCP transactions are stored in SQLite, not only in process memory, so another worker can finish the redirect back to the AI client.
The page you land on is a success or error page. When an AI client is waiting, it also links back to that client's redirect URI. The account is saved before that hop, so a phone that cannot open cursor:// or localhost still keeps the connection.
A Graph error that says Unsupported request - method type: get comes from Meta, after this server has already accepted the browser return. The usual cause is Access verification on the Meta app (role-less accounts) or the long-lived token exchange. The account is still saved when the code exchange returned a user id. Confirm:
Redirect URI is exactly
https://ig.nabuxai.com/instagram/callbackThe Meta app is Live and Access verification (Tech Provider) is approved
IG_APP_ID/IG_APP_SECRETare the Instagram app id and secret, not only the Facebook app id
Cursor connectors
Instagram-Nabux, nabux-instagram, and ig-nabux-zoey are three MCP clients of this one server. They are not three codebases. All three use:
https://ig.nabuxai.com/mcpOAuth issues a Nabux token for the owner who approved Instagram. Reconnecting an Instagram user id stays on the owner that already has it, so a second connector sees the same accounts when the same professional account is approved. Pass account as a position, @username, Instagram user id, or Account Key.
To pin one connector to one account without sharing a browser session, open /dashboard and generate a connector URL. Paste it into that connector as the MCP URL:
https://ig.nabuxai.com/t/<token>/mcpThe URL is a secret and is shown once. Calls that omit account use the account you picked for that connector. Existing OAuth tokens are not revoked. nabux-instagram is the name in mcp.json. ig-nabux-zoey is the same server, meant to be pinned to the Zoey account.
Postiz
Postiz cannot store an Instagram token through its public API. What it does support is an OAuth app that hands this server a pos_ token, plus GET /public/v1/social/instagram-standalone, which returns Postiz's own Instagram authorize URL.
In Postiz open Settings → Apps → Create OAuth App and enter:
Field | Value |
App Name |
|
Description | Schedule Instagram posts from ig.nabuxai.com through this Postiz organization. |
Profile picture | optional |
Redirect URL |
|
On the dashboard, open Advanced and save that login's Postiz address, client id (pca_…), and client secret (pcs_…). The secret is encrypted with APP_SECRET_KEY and the page shows only the last four characters. Leave the API address empty to use the Postiz address plus /api. Postiz Cloud uses https://platform.postiz.com and https://api.postiz.com. Then choose Connect Postiz. The pos_ token is encrypted the same way and is not shown again.
Server env vars are only a fallback for a login that has not saved an app:
POSTIZ_FRONTEND_URL=https://<your-postiz-origin>
POSTIZ_API_URL=https://<your-postiz-origin>/api
POSTIZ_CLIENT_ID=pca_...
POSTIZ_CLIENT_SECRET=pcs_...Add Instagram to Postiz on the dashboard loads Postiz's authorize URL onto the same full-page handoff (copy it into a mobile browser if the Instagram app intercepts it). After Allow, the browser returns to Postiz, not to this gateway. On the Meta app, keep this gateway's redirect URI and add Postiz's as well:
https://ig.nabuxai.com/instagram/callback
https://<your-postiz-origin>/integrations/social/instagram-standaloneSet Postiz's INSTAGRAM_APP_ID and INSTAGRAM_APP_SECRET to the same Meta app as IG_APP_ID and IG_APP_SECRET.
MCP tools instagram_postiz_status and instagram_schedule_post are optional. They use the stored organization token when a post should live in Postiz. The gateway's own calendar (instagram_schedule and /calendar) does not use Postiz. Postiz scheduling defaults to a draft. Media must be a staged URL from this gateway; the server uploads those bytes to Postiz and does not fetch arbitrary URLs.
The dashboard also has a separate Private session reader section. It accepts a browser sessionid cookie for optional read-only Direct imports. The private account gets a separate igp_… key and can be mapped to a NabuCRM business with channel-bridge:link-instagram-private --account=KEY --business=ID. Never paste a private key or cookie into NabuCRM environment variables.
Token lifecycle
Instagram long-lived tokens are encrypted with the gateway's APP_SECRET_KEY. The service performs a refresh pass every 12 hours and refreshes accounts that are within seven days of expiry. MCP calls also lazily refresh a near-expiry token before using it.
This means an actively connected account does not depend on a person manually replacing a token every few weeks.
NabuCRM intake
Every Instagram account connected here is reported to NabuCRM as an
instagram_connected event (contract: NabuCRM's docs/crm-intake.md), so the
person lands in the Nabux CRM. The report is queued in the crm_outbox table
by the OAuth callback and sent by a background loop every 30 seconds, signed
with NABUGATE_SECRET as app instagram-mcp.
2xx: done. 5xx, 429 and network errors: retried with backoff (1 minute doubling to 6 hours, 24 attempts). Any other 4xx: marked failed, not retried.
NABUGATE_SECRETmust equal NabuCRM's. While it is empty, reports wait in the queue and are sent once it is set.CRM_INTAKE_URL(defaulthttps://crm.nabuxai.com/api/v1/crm/intake),CRM_INTAKE_ENABLED(defaulttrue).
python -m app.crm_intake backfill # report accounts connected before this existed (idempotent)
python -m app.crm_intake status # pending / done / failed counts
python -m app.crm_intake requeue-failed # retry refused reports after fixing the causeNabuGen integration
NabuGen uses a server-to-server bearer secret (INTERNAL_SERVICE_TOKEN) and the Account Key. Current internal endpoints:
GET /internal/v1/accounts/{account_key}
POST /internal/v1/accounts/{account_key}/publish/image
POST /internal/v1/accounts/{account_key}/publish/carousel
POST /internal/v1/accounts/{account_key}/publish/reel
POST /internal/v1/accounts/{account_key}/publish/storyMeta app setup
Create/configure a Meta developer app for Instagram API with Instagram Login and use Instagram Business Login. Default requested permissions:
instagram_business_basic
instagram_business_content_publish
instagram_business_manage_comments
instagram_business_manage_messages
instagram_business_manage_insightsSet this exact OAuth redirect URI in Meta:
https://ig.nabuxai.com/instagram/callbackCompliance callbacks:
https://ig.nabuxai.com/instagram/deauthorize
https://ig.nabuxai.com/instagram/data-deletionWebhook (for real-time messaging/comments subscriptions):
Callback URL: https://ig.nabuxai.com/webhook
Verify Token: <any string you pick — must equal IG_WEBHOOK_VERIFY_TOKEN>Set IG_WEBHOOK_VERIFY_TOKEN in your environment to that same string before clicking
"Verify and save" in Meta — the server only accepts the handshake when they match exactly
(no leading/trailing spaces). Payloads are additionally checked against Meta's
X-Hub-Signature-256 header using IG_APP_SECRET.
This gateway shares its Meta app with other Nabux products (Zooey uses the same
META_APP_ID/INSTAGRAM_CLIENT_ID). A Meta app allows one callback per product, so
point both the Instagram product and the Page/Messenger product at /webhook here
and list the other products' own Meta webhook URLs in META_RELAY_TARGETS: every
verified delivery is forwarded to them byte-for-byte with Meta's original
X-Hub-Signature-256, so they keep verifying with their own copy of the app secret
(Page deliveries are signed with FB_APP_SECRET, Instagram ones with IG_APP_SECRET;
both are accepted here).
Once verified, subscribe the app to the messages and comments webhook fields. A
signed DM or comment for a connected account is parsed and relayed to NabuCRM
(NABUCRM_INBOUND_URL, authenticated with INTERNAL_SERVICE_TOKEN) as a lead;
NabuCRM's reply comes back through POST /internal/v1/accounts/{account_key}/messages/send,
the same internal-token pattern as the publish/* routes above. An account this
gateway doesn't recognize is a no-op — Meta still gets its 200 OK either way.
Meta gets its 200 OK as soon as the signature checks out and the delivery is on disk;
the relays run after it, so a slow downstream can never make Meta retry or switch the
subscription off.
The queue is what makes that safe. Acking first means Meta considers the delivery
made and never sends it again, so this process holds the only copy. Every verified
delivery is therefore written to the meta_outbox table — one row per target, before
the 200 — and app/meta_relay.py delivers it, retrying with backoff (10s doubling to
an hour, 18 attempts ≈ a day) until the sibling takes it. A sibling redeploying, timing
out or answering 5xx now costs latency instead of the message; only a refusal it would
repeat (a 4xx that is not 408/429) is given up on, loudly.
python -m app.meta_relay status # pending / done / failed, and the configured targets
python -m app.meta_relay requeue-failed # try everything given up on again, once the cause is fixedWhat the relay did is in the log:
gave up relaying a Meta delivery to <url> after N attempts ... # the one place a message is lost
META_RELAY_TARGETS is empty: ... # once per process: siblings get nothing
NABUCRM_INBOUND_URL is not set: ... # once per process: no CRM relay
rejected a webhook delivery: ... (ig_secret_set=… fb_secret_set=…) # at most once a minuteA DM does not have to carry words: photos, voice notes, stickers, GIFs, shared reels and
media-only story replies arrive with attachments and no text, and reactions arrive
with neither. All of them are relayed, filed under what they are ([photo],
[voice message], [reacted ❤️]), so nothing inbound is invisible. Comments on a live
video (live_comments) count as comments.
Graph API defaults to v26.0 and can be changed with IG_GRAPH_VERSION.
Media library
Library files are kept until instagram_media_delete. Nothing expires them. They live under MEDIA_DIR (default /data/media). Coolify must mount a persistent volume there. docker-compose.coolify.yml mounts instagram-mcp-data at /data, so the default path is on that volume. If you set MEDIA_DIR somewhere else, mount that path too. Do not use the container's own disk.
The public URL is https://ig.nabuxai.com/media/{id}/{token}.ext. It needs no Authorization header. Videos are Content-Type: video/mp4 and answer HTTP Range with 206. Instagram fetches that URL itself.
Upload paths:
POST /api/v1/mediamultipart fieldfile, plusaccountortarget_id. Authorize with the same bearer token the MCP uses (Authorization: Bearer …) or with the dashboard session cookie and acsrffield. The body is written to disk as it arrives. Default limit is 1 GiB (MEDIA_MAX_BYTES).instagram_media_upload_urldownloads one public HTTPS URL into the library.instagram_media_upload_beginreturns a one-time signed multipart URL and a curl example for a file that is already on the caller's machine. The URL works once and does not need the bearer.instagram_media_listandinstagram_media_delete.
The same sha256 for one account (or one unbound target) is stored once. A remote URL that the calendar re-stages still uses the temporary staging cap (STAGED_VIDEO_MAX_BYTES, 100 MiB). A library file is not copied again; Meta fetches the stable URL, so a reel or carousel video can be the full library size.
Reverse proxy body size. This process does not cap the body below MEDIA_MAX_BYTES. Coolify's Traefik proxy does not add a limit. nginx's default client_max_body_size is 1m, which rejects the upload before the app sees it. Set it to at least the library limit, for example client_max_body_size 1024m;, on any nginx in front. Cloudflare's free upload cap is smaller than 1 GiB; a direct origin upload is required for files that large.
ffprobe, when it is installed, fills duration, width, and height. Images use Pillow. Without ffprobe, an MP4 still stores duration when the mvhd box is readable from the start of the file.
Targets
instagram_target_create stores a username (lowercased, without @) and/or a numeric Instagram id for an account that is not connected yet. instagram_target_list shows them. Uploads (POST /api/v1/media field target_id, and the media MCP tools) and instagram_schedule accept target_id instead of an account. Those posts stay awaiting_account.
When this owner connects that account, or refreshes its profile, an exact username or exact id match binds the target. Future posts become scheduled. Posts whose time has already passed become needs_review and are not published. The dashboard lists them with Approve schedule, Edit, and Cancel. A different username or id, including a longer handle, is not a match, and another owner's target is never bound.
Upload a local MP4 for @athr.omani before that account is connected, then schedule it:
instagram_target_create username=athr.omani label="Athr Omani"
instagram_media_upload_begin target_id=<id from the create result>
curl -X POST -F "file=@clip.mp4" "<upload_url from the begin result>"
instagram_schedule type=reel target_id=<id> media_ids=[<media id>] scheduled_at=2026-11-05T18:00:00 timezone=Asia/Muscat caption="..."The post stays awaiting_account until an account whose username is exactly athr.omani (any capitalization) is connected by this same owner.
Profile host
Instagram Login tokens are read from graph.instagram.com, /me first, with fields user_id,username,name,profile_picture_url,account_type. Facebook Login tokens (they start with EAA) use graph.facebook.com. A Meta Unsupported request - method type: get tries the unversioned host, the stored id, then the other host. instagram_account_refresh and the dashboard Refresh profile button write the username onto the row. Tokens are not logged.
Graph tools ported from mcpware/instagram-mcp
Already on this gateway, so not duplicated: profile, media list, account and media insights, publishing (image, carousel, reel, story), publishing limit, comments (list, reply, hide, delete), and Instagram Login messaging (instagram_list_conversations, instagram_get_conversation, instagram_send_message, instagram_send_media). Those DMs use graph.instagram.com. mcpware's DM tools go through a Facebook Page id on graph.facebook.com; replacing the working path would drop Instagram Login inboxes.
Added:
instagram_post_comment— top-level comment. This gateway already had replies.instagram_storiesinstagram_mentions— thetagsedgeinstagram_hashtag_searchandinstagram_hashtag_media— Meta's 30 unique hashtags per 7 days still applies. Instagram Login often cannot callig_hashtag_search; the token's host is tried first and Meta's error is returned.instagram_business_discovery— public profile of another Business or Creator account. The handle is passed only asbusiness_discovery.username(handle).
Not ported:
get_account_pages— needs a Facebook user token that administers a Page. Instagram Login, which this gateway uses, has no Page list.validate_access_token—instagram_profileandinstagram_account_refreshalready read/me. A second tool would only encourage handling the token. The token is never returned.Their single
INSTAGRAM_ACCESS_TOKEN, in-memory cache, prompts, and MCP resources — this gateway is multi-owner and keeps Meta tokens encrypted.
Coolify deployment
Use docker-compose.coolify.yml and assign the mcp service:
https://ig.nabuxai.com -> container port 8000Required environment variables:
PUBLIC_BASE_URL=https://ig.nabuxai.com
APP_SECRET_KEY=<random 32+ char secret>
IG_APP_ID=<Meta app id>
IG_APP_SECRET=<Meta app secret>
IG_REDIRECT_URI=https://ig.nabuxai.com/instagram/callback
INTERNAL_SERVICE_TOKEN=<random server-to-server secret>
IG_WEBHOOK_VERIFY_TOKEN=<random string — same value entered in Meta's Webhooks setup>
NABUAUTH_URL=https://auth.nabuxai.com
NABUAUTH_CLIENT_ID=instagram-mcp
NABUAUTH_CLIENT_SECRET=<same value as NABUAUTH_SECRET_INSTAGRAM_MCP on NabuAuth>
NABUAUTH_REDIRECT_URI=https://ig.nabuxai.com/auth/nabu/callback
POSTIZ_FRONTEND_URL=https://<your-postiz-origin>
POSTIZ_API_URL=https://<your-postiz-origin>/api
POSTIZ_CLIENT_ID=<pca_ client id from Postiz Settings → Apps>
POSTIZ_CLIENT_SECRET=<pcs_ client secret from that app>
MEDIA_DIR=/data/media
MEDIA_MAX_BYTES=1073741824
SCHEDULE_MAX_ATTEMPTS=5POSTIZ_* is optional. Leave it unset until the OAuth app exists. Changing APP_SECRET_KEY makes stored Meta tokens and the Postiz token unreadable; do not rotate it to turn this feature on.
MEDIA_DIR defaults to /data/media on the instagram-mcp-data volume mounted at /data. That volume has to be persistent. MEDIA_MAX_BYTES defaults to 1 GiB. If an nginx proxy is in front of Coolify, set client_max_body_size 1024m; or the upload never reaches the app. Traefik does not apply a body limit.
Connect ChatGPT / Claude / MCP clients
Use:
https://ig.nabuxai.com/mcpThe server supports MCP OAuth 2.1 with dynamic client registration and PKCE. For clients that cannot perform OAuth, /dashboard can generate a personal MCP bearer token.
Security model
Meta tokens are encrypted at rest using a Fernet key derived from
APP_SECRET_KEY.MCP access/refresh tokens are stored only as SHA-256 hashes.
Account resolution is tenant-scoped.
NabuGen has a separate server-to-server bearer secret.
Private session settings are encrypted with
APP_SECRET_KEYin a separate account table; disconnecting clears the stored session locally.Private tools use only inbox-list and thread-read calls. The existing official publish, send, and webhook routes do not resolve private account keys.
Private cookies can be rejected or trigger account restrictions; this mode is optional and should use an account selected for that risk.
Temporary media staging (no permanent public media hosting)
Instagram's official publishing API fetches images and videos from a public
HTTPS URL; a local chat file path or sandbox:/ URL is not accessible to
Meta. This gateway supports temporary, opaque HTTPS URLs under
https://ig.nabuxai.com/media/{staged_id}.jpg and
https://ig.nabuxai.com/media/{staged_id}.mp4.
Authenticated MCP clients call
instagram_stage_imagewith actual image bytes encoded as Base64 (or a standard image data URL). This step requires the MCP host/connector to forward attachment bytes. A text-only connector cannot automatically read ChatGPT-generated files merely by adding a schema field; connect an attachment-byte bridge if your host does not expose them.Trusted Nabux backends can HTTP POST raw image bytes to
/internal/v1/accounts/{account_key}/stagewithAuthorization: Bearer <INTERNAL_SERVICE_TOKEN>and the matchingContent-Type: image/png(orimage/jpeg,image/webp). The result hasstaged_id,image_url, andexpires_at.Publish using the MCP tools
instagram_publish_staged_imageorinstagram_publish_staged_carousel, or post{"staged_id":"..."}/{"staged_ids":["...","..."]}to the existing internalpublish/image/publish/carouselendpoints.Staged images are converted to JPEG for Meta, limited to 8 MiB input and 25 megapixels, scoped to a specific owner and Instagram account, and restricted to 40 outstanding staged files per account (images and videos share this cap).
Media URLs are bearer URLs. They are publicly fetchable by anyone with the opaque URL during their validity, so never stage secret or private material without consent. Paths cannot be listed.
The server checks Meta's ingestion status, publishes, and deletes the temporary image files only after Meta returns a published media ID. Failed requests keep images for retries until the expiry janitor removes them. Default TTL is 4 hours (
STAGED_MEDIA_TTL=14400); cleanup runs every 5 minutes and after service restart.Images reside under
DATA_DIR/staged-media. The existing Coolifyinstagram-mcp-data:/datavolume persists them across restarts. Do not add the staged directory to backups.
Reels and video Stories (MP4)
The same stage route accepts video/mp4 and video/quicktime. The body is
streamed to disk (not re-encoded), checked for an ISO BMFF ftyp box, and
published at video_url. QuickTime uploads must already be ftyp files; they
are served as video/mp4 from the .mp4 URL. Instagram still requires a
codec it can ingest (typically H.264 + AAC); this gateway does not transcode.
curl -X POST -H "Authorization: Bearer $INTERNAL_SERVICE_TOKEN" -H "Content-Type: video/mp4" --data-binary @reel.mp4 https://ig.nabuxai.com/internal/v1/accounts/<account_key>/stageThe JSON result has staged_id, video_url, and expires_at. Pass
video_url to instagram_publish_reel or instagram_publish_story, or POST
{"video_url":"<video_url>"} to
/internal/v1/accounts/{account_key}/publish/reel or publish/story.
GET /media/{staged_id}.mp4 is public: no Authorization header and no
redirect. The response is Content-Type: video/mp4 with Content-Length and
Accept-Ranges: bytes, including 206 for a Range request. After
STAGED_MEDIA_TTL the URL is 404. Unlike staged images, the file is kept
until that expiry (not deleted when publish returns) so Meta can finish
fetching it.
Default cap is 100 MiB (STAGED_VIDEO_MAX_BYTES=104857600). Videos share the
40 outstanding staged files per account with images. Set
STAGED_VIDEO_MAX_BYTES and STAGED_MEDIA_TTL in the deploy environment to
override either limit; both have defaults, so an existing deploy keeps working
without new variables.
The existing URL-based publishing tools remain backward compatible. This feature does not authorize publication without a user action or solve attachment forwarding outside the Nabux service.
This server cannot be deployed
Maintenance
Related MCP Connectors
Instagram for AI agents: publish, read comments and DMs, insights, and engage from your account.
Publish and schedule Instagram posts, Reels and carousels from your AI assistant.
Create, review, publish and schedule Instagram images, carousels and Reels with AI assistants.
Publish to Instagram from chat — official Graph API, OAuth 2.1, free for your own posts.
Related MCP Servers
- FlicenseNot gradedqualityBmaintenanceEnables AI applications to interact with Instagram Business accounts through Instagram's Graph API, including profile and media insights, content publishing, and, with advanced access, direct message management.-
- FlicenseNot gradedqualityAmaintenanceEnables AI applications to interact with Instagram Business accounts through Meta's Graph API. Supports retrieving profile details, media posts, and engagement insights, publishing images and videos, and reading or replying to direct messages (with Advanced Access).-
- FlicenseNot gradedqualityAmaintenanceEnables AI applications to interact programmatically with Instagram Business accounts through Meta's Graph API, supporting profile and media retrieval, publishing, insights, and direct messaging workflows.-
- FlicenseNot gradedqualityAmaintenanceEnables AI applications to retrieve profile and media insights, publish media, and manage Instagram Business account interactions through the Instagram Graph API, with direct messaging available when advanced permissions are granted.-