Skip to main content
Glama
aayushjain1811

Instagram MCP Server

README.md
# šŸ“ø Instagram MCP Server

A production-ready **Remote MCP (Model Context Protocol) Server** that gives
Claude direct, tool-based access to your Instagram Business account through
the **Meta Graph API** — profile data, posts, comments, publishing,
insights, analytics, hashtags, messaging, and real-time webhooks.

Built with TypeScript, Node.js, Express, and the official
`@modelcontextprotocol/sdk`.

---

## Table of Contents

- [What this is (and isn't)](#what-this-is-and-isnt)
- [Features / Tool Reference](#features--tool-reference)
- [Architecture](#architecture)
- [Quick Start](#quick-start)
- [Meta App Setup](#meta-app-setup)
  - [1. Create a Meta App](#1-create-a-meta-app)
  - [2. Link a Facebook Page + Instagram Business Account](#2-link-a-facebook-page--instagram-business-account)
  - [3. Generate an Access Token](#3-generate-an-access-token)
  - [4. Permissions & App Review](#4-permissions--app-review)
- [Configuration](#configuration)
- [Running Locally](#running-locally)
- [Deployment](#deployment)
  - [Docker](#docker)
  - [Render](#render)
  - [Railway](#railway)
  - [Fly.io](#flyio)
- [Connecting Claude](#connecting-claude)
- [Webhooks (real-time updates)](#webhooks-real-time-updates)
- [Testing](#testing)
- [Security Notes](#security-notes)
- [Known API Limitations](#known-api-limitations)
- [Troubleshooting](#troubleshooting)
- [FAQ](#faq)
- [License](#license)

---

## What this is (and isn't)

This project is a real, working integration between Claude and the
**Instagram Graph API for Professional (Business/Creator) accounts**. It is
**not** a wrapper around the consumer Instagram app, and it cannot do
anything the Graph API itself doesn't support. Where Meta's API has a real
limitation (no follower-list endpoint, no general story-history endpoint,
no competitor-audience data), this server surfaces a clear error explaining
the limitation instead of fabricating data — see
[Known API Limitations](#known-api-limitations).

You need:

- An **Instagram Professional account** (Business or Creator)
- Linked to a **Facebook Page**
- A **Meta Developer App** with the Instagram Graph API product added

---

## Features / Tool Reference

Every feature is exposed as an MCP tool Claude can call directly.

| Category | Tools |
|---|---|
| **Profile** | `getProfile`, `getUsername`, `getBiography`, `getFollowers`, `getFollowing`, `getProfilePicture`, `getMediaCount` |
| **Media** | `getRecentPosts`, `getReels`, `getStories`*, `getMediaMetadata`, `getMediaUrl`, `getCaption`, `getTaggedUsers`, `getLocation` |
| **Comments** | `getComments`, `replyComment`, `deleteComment`, `hideComment` |
| **Publishing** | `publishPost`, `publishCarouselPost`, `publishReel`, `createMediaContainer`, `schedulePost`, `getPublishingStatus` |
| **Insights** | `getFollowerGrowth`, `getReach`, `getImpressions`, `getProfileVisits`, `getWebsiteClicks`, `getEngagement`, `getAccountsReached`, `getAccountsEngaged`, `getMediaInsights` |
| **Analytics** | `getTopPerformingPosts`, `getTopPerformingReels`, `getAverageEngagement`, `getBestPostingTime`, `weeklyAnalytics`, `monthlyAnalytics`, `comparePosts`, `compareReels`, `generateAnalyticsSummary` |
| **Messaging** | `getMessages`, `getConversationHistory`, `sendMessage`, `replyMessage` |
| **Hashtags** | `searchHashtag`, `getHashtags` |
| **Business** | `getAccountInformation`, `businessDiscovery`, `competitorAnalysis`* |
| **Webhooks** | `getRecentMentions`, `getRecentCommentEvents` |

\* `getStories` and `competitorAnalysis` intentionally return an explanatory
error — see [Known API Limitations](#known-api-limitations).

54 tools total. Run the server and call `tools/list` over MCP, or see
[`examples/api-requests.md`](examples/api-requests.md), for the live list.

---

## Architecture

```
instagram-mcp/
ā”œā”€ā”€ src/
│   ā”œā”€ā”€ index.ts               # process bootstrap (listen, graceful shutdown)
│   ā”œā”€ā”€ server.ts               # Express app assembly (middleware + routes)
│   ā”œā”€ā”€ config/                 # env validation (Zod) + constants
│   ā”œā”€ā”€ routes/                 # /mcp, /auth, /webhooks, /health
│   ā”œā”€ā”€ controllers/             # OAuth setup controller
│   ā”œā”€ā”€ services/
│   │   ā”œā”€ā”€ graph/               # low-level Graph API HTTP client (auth, retries, error mapping)
│   │   └── instagram/           # profile, media, comments, publishing, insights, analytics, hashtags, messaging, business discovery
│   ā”œā”€ā”€ mcp/
│   │   ā”œā”€ā”€ mcpServer.ts         # McpServer factory — registers every tool
│   │   └── tools/                # one file per tool category (Zod schemas + handlers)
│   ā”œā”€ā”€ middlewares/              # auth, rate limiting, validation, error handling, logging
│   ā”œā”€ā”€ utils/                    # logger, errors, cache, retry, crypto
│   ā”œā”€ā”€ types/                    # shared TypeScript types
│   ā”œā”€ā”€ auth/                      # OAuth token exchange logic
│   ā”œā”€ā”€ storage/                   # encrypted-at-rest token store
│   └── webhooks/                  # signature verification + event router
ā”œā”€ā”€ tests/
│   ā”œā”€ā”€ unit/                      # crypto, retry, cache, service-level (mocked via nock)
│   └── integration/                # full Express app via supertest
ā”œā”€ā”€ examples/                       # sample MCP config + curl requests
ā”œā”€ā”€ Dockerfile / docker-compose.yml
ā”œā”€ā”€ render.yaml / railway.json / fly.toml
└── .github/workflows/ci.yml
```

**Design choices worth knowing about:**

- The Remote MCP endpoint (`POST /mcp`) uses the SDK's **stateless
  Streamable HTTP transport** — a fresh `McpServer` + transport pair per
  request. This makes the server trivially horizontally scalable. It does
  **not** support server-initiated push over a long-lived connection; use
  the polling-style `getRecentMentions` / `getRecentCommentEvents` tools (fed
  by the webhook listener) instead.
- Tokens are encrypted at rest with **AES-256-GCM** using a key derived from
  `TOKEN_ENCRYPTION_KEY`.
- All Graph API calls go through a single `GraphClient` that adds
  `appsecret_proof`, retries transient/rate-limited failures with backoff,
  and normalizes Meta's error shapes into typed `AppError` subclasses.
- Analytics like "best posting time" and "compare posts" are **computed
  locally** from real data pulled via Graph API primitives — Meta does not
  expose these as first-class endpoints.

---

## Quick Start

```bash
git clone <this-repo-url> instagram-mcp
cd instagram-mcp
npm install
cp .env.example .env
# edit .env with your Meta App credentials (see below)
npm run dev
```

Server starts on `http://localhost:3000`. Health check:
`curl http://localhost:3000/health`.

---

## Meta App Setup

### 1. Create a Meta App

1. Go to <https://developers.facebook.com/apps> → **Create App**.
2. Choose type **Business**.
3. In the App Dashboard, add the **Instagram Graph API** product (search
   "Instagram" in "Add Product").
4. Note your **App ID** and **App Secret** (Settings → Basic) — these go in
   `META_APP_ID` / `META_APP_SECRET`.

### 2. Link a Facebook Page + Instagram Business Account

The Instagram Graph API only works through a Facebook Page connection:

1. Convert your Instagram account to a **Professional Account** (Business or
   Creator) in the Instagram app: Settings → Account type.
2. Create or use an existing **Facebook Page**.
3. Link them: Instagram app → Settings → Account → Linked accounts →
   Facebook, or Facebook Page → Settings → Instagram → Connect Account.
4. Note the **Facebook Page ID** (Page → About → Page ID), used as
   `FACEBOOK_PAGE_ID`.

### 3. Generate an Access Token

**Option A — Graph API Explorer (fastest for personal use):**

1. Go to <https://developers.facebook.com/tools/explorer/>.
2. Select your app, select your Page, and add the permissions listed below.
3. Generate a **User Access Token**, then exchange it for a **long-lived
   token** (~60 days) using:

   ```bash
   curl -G "https://graph.facebook.com/v21.0/oauth/access_token" \
     -d "grant_type=fb_exchange_token" \
     -d "client_id=$META_APP_ID" \
     -d "client_secret=$META_APP_SECRET" \
     -d "fb_exchange_token=$SHORT_LIVED_TOKEN"
   ```

4. Put the resulting token in `IG_LONG_LIVED_ACCESS_TOKEN`.
5. Find your Instagram Business Account ID:

   ```bash
   curl -G "https://graph.facebook.com/v21.0/$FACEBOOK_PAGE_ID" \
     -d "fields=instagram_business_account" \
     -d "access_token=$IG_LONG_LIVED_ACCESS_TOKEN"
   ```

   Put the returned `id` in `IG_BUSINESS_ACCOUNT_ID`.

This is the simplest path for a single-account, self-hosted deployment — set
those three env vars and skip the `/auth/*` OAuth endpoints entirely.

**Option B — Full OAuth flow (multi-user / production apps):**

1. Set `META_REDIRECT_URI` to `https://your-domain.com/auth/callback` and
   add it as a **Valid OAuth Redirect URI** in the Meta App Dashboard
   (Facebook Login product → Settings).
2. `GET /auth/authorize` returns a URL — open it in a browser, approve the
   permissions.
3. Meta redirects to `/auth/callback`, which exchanges the code for a
   long-lived token and stores it encrypted automatically.
4. `GET /auth/status` confirms the connection.

### 4. Permissions & App Review

Required scopes (already requested by `buildAuthorizationUrl`):

```
instagram_basic
instagram_manage_insights
instagram_manage_comments
instagram_content_publish
instagram_manage_messages
pages_show_list
pages_read_engagement
pages_manage_metadata
business_management
```

For personal/self-use on your own account, most of these work in **Development
Mode** with your own account added as a Test User — no App Review needed.
To let *other* Instagram accounts authorize your app, Meta requires **App
Review** for `instagram_manage_comments`, `instagram_content_publish`,
`instagram_manage_messages`, and `instagram_manage_insights`.

---

## Configuration

See [`.env.example`](.env.example) for the full, documented list. Key
variables:

| Variable | Required | Description |
|---|---|---|
| `META_APP_ID` / `META_APP_SECRET` | āœ… | From your Meta App dashboard |
| `TOKEN_ENCRYPTION_KEY` | āœ… | ≄32 chars, encrypts tokens at rest |
| `IG_LONG_LIVED_ACCESS_TOKEN` / `IG_BUSINESS_ACCOUNT_ID` | Option A | Skip OAuth entirely |
| `META_REDIRECT_URI` | Option B | Needed only for the `/auth/*` OAuth flow |
| `MCP_AUTH_TOKEN` | Recommended | Bearer token required to call `/mcp` |
| `META_WEBHOOK_VERIFY_TOKEN` | Optional | Needed for real-time webhooks |

The server **refuses to start** if required variables are missing or
malformed (validated with Zod on boot) — check the console output for the
exact field that failed.

---

## Running Locally

```bash
npm install
npm run dev          # tsx watch mode
# or
npm run build && npm start   # production build
```

Other scripts: `npm run lint`, `npm run format`, `npm run typecheck`,
`npm test`, `npm run test:coverage`.

---

## Deployment

### Docker

```bash
docker build -t instagram-mcp-server .
docker run -p 3000:3000 --env-file .env -v $(pwd)/data:/app/data instagram-mcp-server
```

Or with Compose:

```bash
docker compose up -d --build
```

### Render

1. Push this repo to GitHub.
2. In Render: **New → Blueprint**, point at your repo — `render.yaml` is
   auto-detected.
3. Fill in the secret env vars flagged `sync: false` in the Render dashboard.
4. Deploy. Your Remote MCP URL will be
   `https://<your-service>.onrender.com/mcp`.

### Railway

```bash
railway init
railway up
```

`railway.json` configures the Dockerfile build and health check
automatically. Set env vars via `railway variables set KEY=value` or the
dashboard.

### Fly.io

```bash
fly launch --no-deploy   # detects fly.toml
fly secrets set META_APP_ID=... META_APP_SECRET=... TOKEN_ENCRYPTION_KEY=... MCP_AUTH_TOKEN=...
fly deploy
```

### Self-host (bare metal / VM)

```bash
npm ci --omit=dev
npm run build
NODE_ENV=production node dist/index.js
```

Put it behind a reverse proxy (nginx/Caddy) for TLS termination — Claude's
Remote MCP requires HTTPS in production.

---

## Connecting Claude

Once deployed with a public HTTPS URL:

1. In **claude.ai**: Settings → Connectors → **Add custom connector**.
2. Enter your MCP URL: `https://your-domain.com/mcp`.
3. If you set `MCP_AUTH_TOKEN`, add it as a Bearer token / custom header
   (`Authorization: Bearer <token>`) in the connector's auth settings.
4. Claude will call `initialize` and `tools/list` automatically and the
   Instagram tools become available in your conversation.

For Claude Desktop or other MCP clients that read a JSON config file, see
[`examples/claude-mcp-config.json`](examples/claude-mcp-config.json).

See [`examples/api-requests.md`](examples/api-requests.md) for raw
`curl`-based examples of the MCP JSON-RPC protocol if you want to test
independent of Claude.

---

## Webhooks (real-time updates)

To receive comments/mentions in near-real-time instead of polling:

1. Set `META_WEBHOOK_VERIFY_TOKEN` to a random string.
2. In the Meta App Dashboard → Webhooks → Instagram, subscribe to
   `comments`, `mentions` (and others as needed) with callback URL
   `https://your-domain.com/webhooks` and the same verify token.
3. Meta will `GET /webhooks` once to verify (handled automatically), then
   `POST /webhooks` on every event.
4. Call the `getRecentMentions` / `getRecentCommentEvents` MCP tools to read
   the most recent events received (kept in an in-memory ring buffer — swap
   for persistent storage if you need durability across restarts).

---

## Testing

```bash
npm test                 # everything
npm run test:unit        # unit tests (crypto, retry, cache, services via nock mocks)
npm run test:integration # full Express app via supertest, including a live MCP handshake
npm run test:coverage
```

27 tests ship with the project, covering encryption round-trips, retry/backoff
logic, caching, comment/publishing service behavior against a mocked Graph
API, and an end-to-end MCP `initialize` handshake against the real app.

---

## Security Notes

- Tokens are **never** logged (redacted in the pino logger config) and are
  encrypted at rest with AES-256-GCM.
- `.env` is git-ignored; only `.env.example` (no real secrets) is committed.
- `appsecret_proof` is sent on every Graph API call by default
  (`META_APP_SECRET_PROOF=true`) to prevent token replay if a token leaks.
- Webhook payloads are (optionally) signature-verified against
  `X-Hub-Signature-256` using your App Secret — wire `verifySignature` into
  `webhookRoutes.ts` if you want to reject unsigned/forged payloads outright
  (currently logged; enable strict rejection for production hardening).
- Set `MCP_AUTH_TOKEN` in any environment reachable from the public internet.

---

## Known API Limitations

The Instagram Graph API does not support everything a full "clone" of the
Instagram app would need. This server is honest about it:

| Requested feature | Reality |
|---|---|
| List of follower/following **usernames** | Not exposed — only aggregate counts, for user privacy. |
| Reading **Stories** | Only stories *this app itself published* are readable, and only within 24h. No general story-history endpoint exists. |
| **Competitor analysis** (ads, demographics, audience overlap) | Business Discovery exposes only *public* profile/media stats of other accounts — nothing beyond what's visible on their public profile. |
| Native **"schedule for later"** | No such flag exists on the Graph API; this server creates the container immediately and defers the `media_publish` call with an in-process timer (persist this in a real queue for production-grade durability across restarts). |
| DM automation outside the 24h window | Meta restricts free-form messaging to a 24-hour window after the user last messaged you, or requires approved message tags — this is a platform policy, not a limitation of this code. |

---

## Troubleshooting

**"Environment validation failed" on startup**
Check the console output — it lists exactly which env var is missing/invalid.

**`AuthenticationError: Instagram access token is invalid or expired`**
Long-lived tokens last ~60 days. Call `POST /auth/refresh`, or re-run the
Graph API Explorer token exchange and update `IG_LONG_LIVED_ACCESS_TOKEN`.

**`GraphApiError` with code 100 on publishing**
Usually means the media URL isn't publicly reachable/valid, or the account
lacks `instagram_content_publish` permission.

**Rate limit errors (`RateLimitError`)**
The client retries automatically with backoff; if it persists, you're
hitting Meta's per-app/per-user call limits — space out requests or wait.

**Claude can't connect to `/mcp`**
Confirm the URL is HTTPS and publicly reachable, and that any
`MCP_AUTH_TOKEN` you configured matches exactly what's set in Claude's
connector settings.

---

## FAQ

**Do I need App Review to use this on my own account?**
No — Development Mode + adding your own account as a Test User is enough for
personal use of all read/write tools.

**Can this manage multiple Instagram accounts?**
The current token store holds one account's credentials. For multi-account
support, extend `TokenStore` to key by account ID and add an `accountId`
parameter to the relevant tools.

**Does this work with Creator accounts, not just Business?**
Yes — the Graph API treats Business and Creator accounts the same way for
these endpoints.

**Is there a database?**
No — token storage is a single encrypted JSON file by default, so the
project runs anywhere without provisioning infrastructure. Swap
`src/storage/tokenStore.ts` for a Redis/Postgres-backed implementation if you
need multi-instance deployments.

---

## License

MIT — see [LICENSE](LICENSE).