instagram-mcp
# instagram-mcp
An MCP server that wraps the Instagram API (Instagram Login flow): publish
photos, reels, and stories (from a URL, a local file, or — via the included
subagent — a local file hosted through Google Drive); read comments and
performance insights; moderate comments; draft on-brand captions using MCP
sampling; and schedule reels for later.
Built as a learning project after Anthropic's "Introduction to Model Context
Protocol" course, to turn the course concepts into a real, working
integration — not just tools, but resources, prompts, sampling, logging,
progress notifications, and a Claude Code subagent on top.
For the full story — the Meta developer setup process and every problem hit
along the way, plus a deeper design writeup of every MCP concept used and
why — see [`docs/01-meta-developer-setup.md`](docs/01-meta-developer-setup.md)
and [`docs/02-mcp-server-design.md`](docs/02-mcp-server-design.md).
## Quick start
```bash
npm install
npm run build
```
Then set two environment variables (`IG_USER_ID`, `IG_ACCESS_TOKEN`) in your
MCP client config — see [Setup](#setup) below for how to get them, and
[Connect it to Claude](#connect-it-to-claude) for where they go.
## What's in this project
| Path | What it is |
|---|---|
| `src/instagram.ts` | The actual Instagram API client — auth, GET/POST helpers, publishing flows, resumable upload. No MCP-specific code. |
| `src/index.ts` | The MCP layer — registers everything in `instagram.ts` as tools/resources/prompts with schemas. |
| `.claude/agents/instagram-manager.md` | A Claude Code subagent scoped to just this server (+ Google Drive), with judgment calls baked into its system prompt. |
| `docs/01-meta-developer-setup.md` | Narrative of the Meta app / token setup process, including every blocker hit and how it was resolved. |
| `docs/02-mcp-server-design.md` | Design writeup: problem statement, architecture, and an explanation of every MCP concept this project uses. |
## Setup
### 1. Account & app requirements
- Your Instagram account must be a **Business or Creator account**. This
project uses **Instagram API with Instagram Login** — the account
authorizes the app directly, no Facebook Page needed.
- Create a Meta app at https://developers.facebook.com/apps (Business type),
add the **Instagram** product, and inside it choose
**"API setup with Instagram login."**
- Under **App roles → Roles**, add your Instagram account as an
**Instagram Tester**, then accept the invite from inside the Instagram app
itself (Settings → Apps and Websites → Tester Invites). The app can't
authorize your account until this is accepted.
- On the "API setup with Instagram login" page, step 1 lists required
permissions: `instagram_business_basic`, `instagram_business_manage_comments`,
`instagram_business_manage_messages`. **Add two more** via
"Go to permissions and features": `instagram_business_content_publish`
(needed for `post_image`/`post_reel`/`post_story`) and
`instagram_business_manage_insights` (needed for `get_media_insights`).
See `docs/01-meta-developer-setup.md` if any of this goes sideways — it
documents the specific errors this project hit (business portfolio
blockers, "insufficient developer role," permission-name renames) and how
each was fixed.
### 2. Get your credentials
On the same "API setup with Instagram login" page, under
**"2. Generate access tokens"**:
1. Click **Add account**, log in as your Instagram account, and approve the
permissions. This adds a row showing your account and its
**Instagram User ID** (e.g. `17841471531885841`) — that's your
`IG_USER_ID`.
2. Click **Generate token** in that row to get a token. If it's issued as
long-lived (60 days) you're done. If it's short-lived, exchange it:
```
GET https://graph.instagram.com/access_token
?grant_type=ig_exchange_token
&client_secret={instagram-app-secret}
&access_token={short-lived-token}
```
Long-lived tokens expire after 60 days — for real use you'd add a refresh
job; not included here to keep this a learning-sized project.
**If you add permissions after already generating a token, regenerate the
token.** A token only carries whatever permissions existed at the moment it
was created — adding scopes afterward does nothing until you get a fresh one.
### 3. Configure environment variables
The server reads `IG_USER_ID` and `IG_ACCESS_TOKEN` from the environment —
set these in your MCP client config (below), not in a committed `.env` file.
## Connect it to Claude
**Claude Desktop / Claude Code** — add to your MCP config
(`claude_desktop_config.json` or `.mcp.json`):
```json
{
"mcpServers": {
"instagram": {
"command": "node",
"args": ["/absolute/path/to/instagram-mcp/dist/index.js"],
"env": {
"IG_USER_ID": "your-instagram-scoped-user-id",
"IG_ACCESS_TOKEN": "your-long-lived-token"
}
}
}
}
```
Restart Claude Desktop / Claude Code after saving. You should see the
`instagram` server's tools available in a new conversation.
**Note on config file locations:** newer Claude Desktop builds may manage
connectors through the in-app Settings UI instead of a hand-edited file, or
may only expose custom-connector fields for remote (`https://`) servers, not
local stdio ones like this. If you don't see a `claude_desktop_config.json`
being read, look for a **Developer** settings section with an **Edit
Config** button — that's the one that accepts this JSON.
## Tools exposed
| Tool | Description |
|---|---|
| `list_media` | Recent posts/reels with basic counts |
| `get_media_insights` | Reach, likes, comments, saves, (video) shares/views |
| `get_comments` | Comments on a media item |
| `reply_to_comment` | Public reply to a comment |
| `hide_comment` | Hide/unhide a comment |
| `delete_comment` | Permanently delete a comment |
| `post_image` | Publish a single photo (public URL) |
| `post_reel` | Publish a Reel from a public URL **or a local file** (handles the container → upload/poll → publish flow, reports progress) |
| `post_story` | Publish a 24h story (public URL) |
| `schedule_reel` | Queue a Reel to publish at a future time (see [Scheduling](#scheduling-schedule_reel)) |
| `list_scheduled_posts` | List queued reels and their status |
| `cancel_scheduled_post` | Cancel a pending scheduled reel |
| `suggest_caption` | Uses MCP **sampling** to ask the connected client's model for 3 caption options, grounded in the account's recent captions |
## Resources exposed
| URI | Description |
|---|---|
| `instagram://media` | The account's most recent media, as a browsable resource (not just a tool call) |
| `instagram://media/{media_id}/comments` | Comments on a specific media item, addressed by URI |
| `instagram://scheduled` | The current in-memory queue of scheduled reels |
## Prompts exposed
| Prompt | Description |
|---|---|
| `weekly_engagement_review` | A ready-made prompt (optionally takes `post_count`) that has the model pull recent posts + insights and summarize what's working |
## MCP concepts this project demonstrates
Beyond basic tools, this server exercises the rest of the MCP surface:
- **Resources** — `instagram://media` and `instagram://media/{id}/comments` let
a client browse/attach Instagram data directly, the same way it would
browse a file, rather than only getting data back from a tool call.
- **Prompts** — `weekly_engagement_review` is a reusable prompt template the
client can surface (e.g. as a slash command) instead of the user having to
type out the same multi-step instruction each time.
- **Sampling** — `suggest_caption` calls `server.server.createMessage(...)`,
which asks the *client's* model (whatever Claude the person is running) to
generate text, using account context this server already has. The server
never calls its own separate LLM API key — it borrows the client's model
via the protocol.
- **Logging & progress notifications** — every mutating tool call sends
structured log messages via `sendLoggingMessage`. `post_reel` additionally
sends `notifications/progress` updates while it polls Instagram's video
processing status (which can take 30s–2min), so a client that supports
progress UI can show real-time status instead of a silent hang.
See `docs/02-mcp-server-design.md` for the full writeup of why each of these
was built the way it was.
## Local file uploads (post_reel with video_path)
`post_reel` accepts either `video_url` (public URL) or `video_path` (an
absolute path to a file on this machine). For a local file, the server
uploads the raw bytes directly to Meta's servers via the **resumable
upload** flow (`rupload.facebook.com`) — no need to host the file publicly
yourself.
One honest caveat: Meta's docs describe this resumable path as intended for
apps using "Facebook Login for Business," and this project uses Instagram
Login instead. In practice it works for most Instagram Login apps too, but
if `post_reel` with `video_path` fails with a permission or host error, the
fallback is to host the file somewhere public and use `video_url` instead
(see the Drive-based workflow below for one way to do that without leaving
the conversation).
**`post_image` and `post_story` don't have a local-file option** — Instagram's
photo/story publishing API has no resumable-upload equivalent for images, so
these two tools still require a public `image_url`/`video_url`.
## Local assets for any post type: the instagram-manager subagent
For a workflow where you just hand over a local file — photo, reel, or
story — and don't want to think about hosting at all, use the included
Claude Code subagent instead of calling the tools directly:
```
.claude/agents/instagram-manager.md
```
This subagent has access to both `mcp__instagram` (this server) and a
Google Drive connector, and is instructed to: upload the local file to
Drive → set it to link-shareable → build a direct-download URL → call the
matching Instagram tool with that URL. It also knows to fall back to
`post_reel`'s `video_path` if a Drive-hosted reel fails (large video files
can hit Google's virus-scan interstitial, which serves an HTML warning page
instead of raw bytes).
To use it: copy the file into `~/.claude/agents/` (available in every
project) or your project's own `.claude/agents/`, then check that the
`mcp__google-drive` name in its frontmatter matches whatever your Drive
connector is actually named in your Claude Code config (`claude mcp list`).
See `docs/02-mcp-server-design.md` §6–7 for the full reasoning behind this
design (why it's a subagent concern and not server code).
## Scheduling (schedule_reel)
Instagram's API has no native "publish later" — real scheduling of organic
posts only exists inside Meta's own Business Suite UI, not the API. So
`schedule_reel` holds the job **in this server's memory** and fires it with a
timer at the right time.
**This means scheduled posts only fire while this MCP server process stays
alive** — i.e. while Claude Desktop (or whatever client launched it) stays
open. Quitting the app cancels anything still pending. It's fine for
same-session or same-day scheduling ("post this in 2 hours"); it is *not* a
durable schedule you can set and then close your laptop on.
For real "survive a reboot, fire while I'm asleep" scheduling, you'd want a
system-level mechanism instead — a cron job or macOS `launchd` task that
invokes a small standalone script (reusing `src/instagram.ts`'s `postReel`)
at the target time, independent of Claude Desktop's process lifecycle. Not
implemented here, to keep this project at learning-project scope.
## Notes / gotchas carried over from the Graph API itself
- **`post_image`/`post_story` still need a public URL** — no local-file
option for those two (see above). `post_reel` is the exception, via
`video_path`.
- **Reels take time to process.** `post_reel` polls the container's
`status_code` until it's `FINISHED` before publishing (Meta recommends
polling for up to ~5 minutes; most reels finish in 30s–2min).
- **Rate limit:** 25 published posts per rolling 24h window, shared across
photos/reels/stories.
- **Reel eligibility for the Reels tab** needs 9:16 aspect ratio and 5–90s
duration; longer/differently-shaped video still publishes but as a regular
video post.
- **Metric names have drifted.** Instagram merged `impressions`/`plays` into
a single `views` metric — if insights calls ever start failing across
every post consistently, suspect a metric-name mismatch before assuming
it's a permissions issue.
## Next steps to extend this
- Real persistent scheduling via a generated cron/launchd job.
- A `create_carousel_post` tool (multi-image, same container/publish
pattern as `post_image` but with `children` params).
- Native local-file support for `post_image`/`post_story` in server code
(would still need some public-hosting step, since Instagram has no
resumable upload for photos — possibly a short-lived local HTTP endpoint
via a tunnel, as an alternative to the subagent's Drive-based approach).
- Account-level insights (follower count, profile views) via a resource.
- Token refresh handling instead of manual 60-day renewal.
- An automatic retry/fallback wrapper around Drive-hosted `post_reel` calls,
instead of relying on the subagent's prompt-level instruction to notice
and react to a failure.
- Swap the manual env vars for a proper OAuth flow if you want other people
to connect their own accounts to this server.
TDQS
Scored across 13 tools
Each tool maps to a distinct action/resource: publishing types are separated by medium and timing, scheduled posts have their own list/cancel pair, and comment actions are clearly differentiated. Even the closely related post_reel and schedule_reel are disambiguated by immediate vs. future publication.
All tools use snake_case verb_noun names with clear verbs such as post, schedule, list, cancel, suggest, get, reply, hide, and delete. The pattern is consistent across publishing, scheduling, analytics, and comment moderation.
13 tools is within the ideal 3-15 range and maps cleanly to four functional areas: publishing, scheduling, captions/insights, and comment management. No tool feels redundant.
The server covers publishing via story/image/reel, scheduling and cancellation, recent media listing and insights, captions, and full comment moderation including reversible hiding and permanent deletion. Minor gaps exist, such as no scheduling for stories/images and no direct media deletion/editing, but core Instagram workflows are well covered.