Skip to main content
Glama
selvin-paul-raj

Instagram MCP Server

README.md
# Instagram MCP Server

A production-quality [Model Context Protocol](https://modelcontextprotocol.io) server that exposes Instagram
actions (posts, media containers/carousels, comments, direct messages, insights, Messenger profile) to Claude
and ChatGPT. Built for **personal, single-user** use with [Composio](https://composio.dev) as the auth/action
layer, deployed on [Alpic](https://alpic.ai).

This project mirrors the sibling `Linkedin-alpic-mcp` server's architecture exactly — same stack, same
production fixes, just scoped to Instagram's Composio toolkit.

## Features

- **Curated tool scope** — the Composio session and tool discovery are both scoped to exactly the 34
  Instagram actions enabled for this account (see `ENABLED_INSTAGRAM_TOOLS` in `src/composio_client.py`),
  not every action Composio's Instagram toolkit could theoretically expose. Call `instagram_mcp_list_tools`
  to see the current live list with full JSON Schemas.
- **Automatic Instagram auth** — on startup, checks whether the fixed user is connected; if not, starts the
  OAuth flow, logs the authorization URL, and waits (bounded timeout) for the connection to complete.
- **Structured logging** — JSON logs via the stdlib `logging` module, best-effort rotating log file under
  `logs/` (falls back to stdout-only on a read-only filesystem, e.g. serverless deploys).
- **Local stats tracking** — total/successful/failed calls, average latency, last execution, startup time,
  and uptime; best-effort persistence to a local JSON file (falls back to in-memory-only if unwritable).
- **Health reporting** — `instagram_mcp_health` reports server, Instagram connection, and Composio
  reachability.
- **Never crashes** — every Composio/network/auth failure (including Composio's own
  `PermissionDeniedError` for a playground-key/user_id mismatch) is caught and returned as a normal (but
  error-flagged) tool result, never an unhandled exception.
- **Streamable-HTTP transport**, binding `PORT`/`HOST` — ready for Alpic's hosting model.

## Architecture

```
src/
├── server.py           # FastMCP app + entrypoint; wires everything together
├── composio_client.py  # Thin wrapper around the Composio SDK (Instagram toolkit only)
├── tool_registry.py    # Dynamic MCP tool list/dispatch: static utility tools + every Instagram action
├── config.py           # Settings loaded from environment / .env
├── logger.py           # Logging setup (best-effort file handler)
├── stats.py            # JSON-backed call statistics tracker (best-effort persistence)
├── health.py            # Health status aggregation
├── models.py             # Shared Pydantic models
└── utils.py              # Retry/backoff decorator, Timer
```

The four static tools (`instagram_mcp_ping`, `instagram_mcp_health`, `instagram_mcp_version`,
`instagram_mcp_list_tools`) are always available. Every other tool name is a Composio Instagram action slug
(e.g. `INSTAGRAM_CREATE_POST`, `INSTAGRAM_GET_USER_INFO`, `INSTAGRAM_SEND_TEXT_MESSAGE`, ...) — call
`instagram_mcp_list_tools` at any time to see the current live list with descriptions.

## Available Instagram Tools

The server's Composio session is scoped to exactly these 34 actions. Parameters below are best-effort,
inferred from action names and the Instagram Graph API's conventions (this project's Composio account
hadn't completed its Instagram connection yet at doc-writing time, so schemas weren't live-fetched the way
the LinkedIn project's were) — call `instagram_mcp_list_tools` once connected to confirm exact fields.

| Tool | Likely parameters | Use case |
|---|---|---|
| `INSTAGRAM_CREATE_POST` | image/video URL, caption | Publish a single-media feed post. |
| `INSTAGRAM_CREATE_MEDIA_CONTAINER` | media URL, caption, media type | Stage a single media item before publishing (required step before `..._PUBLISH`). |
| `INSTAGRAM_CREATE_CAROUSEL_CONTAINER` | list of child media container IDs, caption | Stage a multi-image/video carousel post from already-created media containers. |
| `INSTAGRAM_POST_IG_USER_MEDIA` | media container params | Create IG user media (container-based posting flow). |
| `INSTAGRAM_POST_IG_USER_MEDIA_PUBLISH` | `creation_id` (container id) | Publish a previously staged media container (single or carousel). |
| `INSTAGRAM_GET_POST_STATUS` | container/media id | Check whether a staged container has finished processing and is ready to publish. |
| `INSTAGRAM_GET_IG_MEDIA` | media id | Get metadata for one media item (post). |
| `INSTAGRAM_GET_IG_MEDIA_CHILDREN` | carousel media id | List the individual media items inside a carousel post. |
| `INSTAGRAM_GET_USER_MEDIA` / `INSTAGRAM_GET_IG_USER_MEDIA` | ig user id, pagination | List media (posts) on an account, paginated. |
| `INSTAGRAM_GET_IG_USER_STORIES` | ig user id | List currently active stories for an account. |
| `INSTAGRAM_GET_IG_USER_LIVE_MEDIA` | ig user id | List currently live video sessions for an account. |
| `INSTAGRAM_GET_IG_USER_TAGS` | ig user id | List media where the account has been tagged. |
| `INSTAGRAM_GET_IG_USER_CONTENT_PUBLISHING_LIMIT` | ig user id | Check remaining publish quota (IG rate-limits posts per 24h). |
| `INSTAGRAM_GET_POST_COMMENTS` / `INSTAGRAM_GET_IG_MEDIA_COMMENTS` | media id, pagination | List comments on a post. |
| `INSTAGRAM_POST_IG_MEDIA_COMMENTS` | media id, comment text | Add a top-level comment to a post. |
| `INSTAGRAM_REPLY_TO_COMMENT` / `INSTAGRAM_POST_IG_COMMENT_REPLIES` | comment id, reply text | Reply to an existing comment. |
| `INSTAGRAM_GET_IG_COMMENT_REPLIES` | comment id, pagination | List replies to a comment. |
| `INSTAGRAM_DELETE_COMMENT` | comment id | Delete a comment or reply. |
| `INSTAGRAM_GET_USER_INFO` | *(none — authenticated user)* | Get your own connected account's profile info (username, id, account type). |
| `INSTAGRAM_GET_USER_INSIGHTS` | ig user id, metrics, period | Account-level insights (reach, impressions, follower demographics). |
| `INSTAGRAM_GET_POST_INSIGHTS` / `INSTAGRAM_GET_IG_MEDIA_INSIGHTS` | media id, metrics | Per-post insights (impressions, engagement, saves). |
| `INSTAGRAM_LIST_ALL_CONVERSATIONS` / `INSTAGRAM_GET_PAGE_CONVERSATIONS` | ig user id, pagination | List Instagram Direct conversations for the account. |
| `INSTAGRAM_GET_CONVERSATION` | conversation id | Get one conversation's message history. |
| `INSTAGRAM_LIST_ALL_MESSAGES` | conversation id, pagination | List messages within a conversation. |
| `INSTAGRAM_SEND_TEXT_MESSAGE` | recipient id, text | Send a text DM. |
| `INSTAGRAM_SEND_IMAGE` | recipient id, image URL | Send an image DM. |
| `INSTAGRAM_MARK_SEEN` | conversation/message id | Mark a DM/conversation as read. |
| `INSTAGRAM_POST_IG_USER_MENTIONS` | media id, comment/mention id | Handle a mention notification (e.g. tag reply flows). |
| `INSTAGRAM_GET_MESSENGER_PROFILE` | *(none)* | Get the account's Messenger platform profile config (greeting text, ice breakers, etc.). |
| `INSTAGRAM_UPDATE_MESSENGER_PROFILE` | profile fields to update | Update the Messenger platform profile config. |
| `INSTAGRAM_DELETE_MESSENGER_PROFILE` | field names to clear | Remove specific Messenger profile fields. |

## Installation

Requires Python 3.12+ and [uv](https://docs.astral.sh/uv/).

```bash
git clone <this-repo>
cd instagram-mcp
uv venv
uv pip install -e ".[dev]"
```

## Configuration

Copy `.env.example` to `.env` and fill in your Composio credentials:

```bash
cp .env.example .env
```

| Variable                  | Required | Default            | Description                                                    |
|----------------------------|----------|---------------------|------------------------------------------------------------------|
| `COMPOSIO_API_KEY`         | Yes      | —                   | Your Composio API key. A **project-scoped** key, not a playground key — playground keys are locked to one bound user and will 403 on any other `user_id`. |
| `COMPOSIO_USER_ID`         | Yes      | —                   | Your Composio account's user id — tool calls and the Instagram connection are scoped to this user. |
| `INSTAGRAM_AUTH_CONFIG_ID` | No       | Composio default    | Use a specific Instagram auth config instead of the default one   |
| `LOG_LEVEL`                | No       | `INFO`              | `DEBUG` / `INFO` / `WARNING` / `ERROR`                            |
| `LOG_DIR`                  | No       | `logs`              | Directory for the rotating log file (best-effort — falls back to stdout-only if unwritable) |
| `STATS_FILE`               | No       | `logs/stats.json`   | Path to the persisted stats JSON file (best-effort — falls back to in-memory-only if unwritable) |
| `CONNECT_TIMEOUT_MS`       | No       | `300000` (5 min)    | Max time to wait for Instagram OAuth to complete at startup        |
| `PORT`                     | No       | `8000`              | Bind port (Alpic sets this automatically at deploy time)          |
| `COMPOSIO_CACHE_DIR`       | No (baked into Dockerfile) | — | Composio's SDK writes a cache dir at import time; set to a writable path (e.g. `/tmp/.composio`) on read-only-home sandboxes like Alpic's. Already set in `Dockerfile`. |

Never hardcode `COMPOSIO_API_KEY` anywhere — it's read exclusively from the environment.

## Running locally

```bash
uv run python -m src.server
```

On first run, if Instagram isn't connected yet, the server logs an authorization URL and waits for you to
complete the OAuth flow in a browser before finishing startup (or continues anyway on timeout — the server
never crashes on this, it just starts without a working connection until you finish the OAuth link).

Test it's alive — note streamable-HTTP requires the MCP handshake (`initialize` →
`notifications/initialized` → your call, all carrying the same `mcp-session-id` response header), a bare
`tools/call` will fail with "Missing session ID". Also: on Windows PowerShell, bare `curl` is aliased to
`Invoke-WebRequest` and will silently mangle `-X`/`-d` — use `curl.exe` explicitly, or Git Bash's real curl.

```bash
curl -s -X POST http://localhost:8000/mcp \
  -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'
```

### Running with Docker

```bash
docker build -t instagram-mcp .
docker run --rm -p 8000:8000 --env-file .env instagram-mcp
```

## Deploying to Alpic

```bash
npm install -g alpic   # once
alpic login            # or set ALPIC_API_KEY

alpic environment-variable add --env-file .env --environment-id <id>
alpic deploy --runtime python3.13
```

Alpic prints the live MCP server URL and a `/try` playground URL on success. Note: Alpic's managed Python
runtime does **not** use this repo's `Dockerfile` — it builds directly from `pyproject.toml` with its own
buildpack, running out of a sandboxed home directory that's read-only. That's why `COMPOSIO_CACHE_DIR`,
best-effort file logging, and best-effort stats persistence all exist in this codebase — they were required
to get a Composio-based server running on Alpic at all (see Troubleshooting below).

## Connecting from Claude Desktop

1. Deploy first (or run locally and expose it, e.g. with `ngrok http 8000`).
2. In Claude Desktop: **Settings → Connectors → Add custom connector**.
3. Paste the Alpic-hosted MCP URL (ends in `/mcp`).
4. Claude will list `instagram_mcp_ping`, `instagram_mcp_health`, and every enabled Instagram action.

## Connecting from ChatGPT

1. In ChatGPT: **Settings → Connectors → Advanced settings → Developer mode** (required for full custom
   tool calling, not just search/fetch).
2. **Add custom connector**, paste the Alpic-hosted MCP URL.
3. Enable the connector in a chat and ChatGPT can call any Instagram action directly.

## Troubleshooting

| Symptom | Likely cause | Fix |
|---|---|---|
| Server won't start / crashes immediately | `COMPOSIO_API_KEY` or `COMPOSIO_USER_ID` missing | Both are mandatory — set them before first deploy/run |
| `403 ... user_id does not match the user this playground API key is locked to` | You're using a Composio **playground** key | Mint a **project-scoped** API key in the Composio dashboard instead — playground keys are hard-locked to one bound user server-side, no client-side `user_id` can override that |
| Alpic build fails: `Read-only file system: '/home/.../.composio'` | Composio's SDK creates a cache dir at import time; Alpic's sandbox `$HOME` is read-only | Set `COMPOSIO_CACHE_DIR=/tmp/.composio` (already in this repo's `Dockerfile`, but Alpic ignores the Dockerfile for its managed Python runtime — add it as an Alpic environment variable too: `alpic environment-variable add --name COMPOSIO_CACHE_DIR --value /tmp/.composio --environment-id <id>`) |
| Alpic build fails: `Read-only file system: '.../logs/instagram-mcp.log'` | Same read-only-filesystem class of issue, this time our own log/stats file writes | Already fixed in this codebase (`logger.py`/`stats.py` both fall back gracefully) — if you still see this, you're running an older copy |
| `instagram_mcp_health` shows `instagram_connected: false` | OAuth never completed | Check server logs for the authorization URL, visit it, then call `instagram_mcp_list_tools` to refresh |
| `instagram_mcp_health` shows `composio_reachable: false` | Bad API key or Composio outage | Verify `COMPOSIO_API_KEY` is valid; check https://status.composio.dev |
| A Instagram tool call errors out | Expired auth, missing scope, or bad input | The error message is returned verbatim from Composio/Instagram in the tool result — read it, it's usually actionable |
| `curl` from PowerShell gives `Not Acceptable: Client must accept text/event-stream` despite the header being set | `curl` in PowerShell is aliased to `Invoke-WebRequest`, which silently sends `GET` instead of your intended `POST` | Use `curl.exe` explicitly, `Invoke-RestMethod`, or Git Bash's real curl |

## Testing

```bash
uv run pytest
```

## License

MIT — see `LICENSE`.