Skip to main content
Glama
osAlhaddad1

instagram-mcp

by osAlhaddad1
README.md
# instagram-mcp

An MCP server that exposes [instagrapi](https://github.com/subzeroid/instagrapi) —
Instagram's private mobile API — as 49 tools an agent can call.

Reading is enabled out of the box. Anything that changes the account (posting,
liking, following, commenting, DMing, deleting) is refused until you explicitly
turn writes on.

## Setup

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

Then fill in `.env` with **either** a username and password, **or** a `sessionid`
cookie copied from a browser where you are already signed in (DevTools →
Application → Cookies → instagram.com). The sessionid route is less likely to
trigger a login challenge.

If the account uses two-factor auth, paste the authenticator "setup key" into
`INSTAGRAM_TOTP_SEED` and codes are generated for you. Otherwise, when Instagram
asks for a code, call `instagram_login` with `verification_code`.

Verify the install without touching Instagram:

```bash
.venv/Scripts/python smoke_test.py
```

## Registering the server

Already registered for this project in `../.mcp.json`. To use it elsewhere:

```bash
claude mcp add instagram -- "C:\Users\osami\OneDrive\Documents\GitHub\ayham project 2\instagram-mcp\.venv\Scripts\instagram-mcp.exe"
```

The executable works from any directory — it always reads `.env` and writes
`session.json` next to this README.

## Enabling write actions

```ini
INSTAGRAM_ALLOW_WRITES=true
```

Restart the server afterwards. While this is `false`, write tools fail with an
explanation rather than doing anything, so the read-only tools stay usable.

## Tools

| Group | Tools |
| --- | --- |
| **Writing DMs** | `instagram_prepare_dm`, `instagram_find_person`, `instagram_build_style_profile`, `instagram_get_style_profile` |
| **Persona search** | `instagram_search_start`, `instagram_search_recall`, `instagram_search_gate`, `instagram_search_expand`, `instagram_search_enrich`, `instagram_search_signals`, `instagram_search_shortlist`, `instagram_search_judge`, `instagram_search_results`, `instagram_search_list` |
| Session | `instagram_login_status`, `instagram_login`, `instagram_account_info` |
| Users | `instagram_get_user`, `instagram_search_users`, `instagram_get_followers`, `instagram_get_following`, `instagram_get_user_medias`, `instagram_get_user_stories` |
| Posts | `instagram_get_media`, `instagram_get_media_comments`, `instagram_get_media_likers`, `instagram_download_media` |
| Discovery | `instagram_get_timeline_feed`, `instagram_get_hashtag_info`, `instagram_get_hashtag_medias`, `instagram_search_locations`, `instagram_get_location_medias`, `instagram_search_posts`, `instagram_similar_accounts`, `instagram_account_about` |
| Direct messages | `instagram_list_direct_threads`, `instagram_get_direct_thread`, `instagram_send_direct_message` \* |
| Engagement | `instagram_like_media` \*, `instagram_unlike_media` \*, `instagram_comment_media` \*, `instagram_follow_user` \*, `instagram_unfollow_user` \* |
| Publishing | `instagram_upload_photo` \*, `instagram_upload_video` \*, `instagram_upload_reel` \*, `instagram_upload_album` \*, `instagram_upload_story` \*, `instagram_delete_media` \* |

\* requires `INSTAGRAM_ALLOW_WRITES=true`.

Users are addressed by `username` or `user_id`. Posts are addressed by a `media`
argument that accepts a post URL, a shortcode, or a numeric media id.

## Writing DMs in your own voice

This is what the server is mainly for. The problem with letting a model write
your messages is that it writes *correctly* — punctuated, capitalised, polite —
and everyone who knows you can tell instantly.

So `instagram_build_style_profile` measures how you actually write, from your
own sent DMs: message length, capitalisation, terminal punctuation, emoji rate,
which emoji, how you spell laughter, shorthand, language mixing, and whether you
send bursts of short messages instead of one composed one. It records this
globally *and per contact*, because nobody writes to their mother the way they
write to their closest friend.

Run it once:

```bash
.venv/Scripts/python -c "import asyncio,json;from instagram_mcp.server import server;print(asyncio.run(server.call_tool('instagram_build_style_profile',{})).content[0].text[:400])"
```

After that, `instagram_prepare_dm(person="sarah")` returns — in one call — the
recent conversation, the measured rules of your voice, and samples of how you
write to that specific person. That single call is the whole interface for
drafting; there is no need to stitch together the raw thread tools.

The profile is cached in `style_profile.json` and never sent to Instagram.
Refresh it occasionally as your writing drifts.

### The skill

`~/.claude/skills/instagram-dm/SKILL.md` drives the whole workflow in normal
conversation — "reply to ahmed", "what should I say back to her", "check my
ig messages". It handles finding the person, loading your voice, drafting, and
holding the draft for your approval before anything sends.

Nothing sends without you seeing the exact words first.

## Finding people who match a persona

The other thing the server is for. You describe someone — *female, Amsterdam,
fitness, mid-twenties, blonde* — and get back ranked profiles with a confidence
figure per attribute.

The hard part is that **Instagram has no index for any of that**. It indexes
four things: handle and name text, hashtags, place geotags, and the follow
graph. A persona is none of them. So every attribute is either compiled into a
probe against one of those four, or inferred afterwards from what came back —
which makes this a funnel that trades recall for precision, not a query.

```
recall   hundreds of candidates, mostly wrong, from many cheap probes
gate     free: drops private accounts and shops
expand   chaining off the best survivors — the highest-precision channel
enrich   ~3 API calls each. The expensive stage, so it runs on a ranked subset
signals  free: name, pronouns, geotag clusters, captions, category, birth years
judge    vision, on the shortlist only, from one contact sheet per candidate
results  ranked, with every piece of evidence attached
```

What makes it work is `instagram_similar_accounts`, which reads Instagram's own
"Suggested for you" graph, built from co-follow behaviour it already models. Once
you have *one* good match, chaining outward from it beats any keyword search by a
wide margin — which is why the text and hashtag probes exist mainly to find that
first foothold.

Location is the other thing worth knowing about. Instagram's `city` field is
almost always null and occasionally wrong — one live probe returned a place
called "Hollanda" carrying coordinates in Alexandria, Egypt — and place *names*
fragment badly, with one city arriving as "Amsterdam, Netherlands", "Amsterdam
Canal District", "Red Light District, Amsterdam" and "Amsterdam Canal River".
Coordinates are always present, so geotags are clustered by position rather than
by name: the variants merge, and the mislabelled entry excludes itself.

One thing the design originally leaned on turned out not to exist. Instagram
generates alt text for photos ("may be an image of 1 person, blonde hair,
standing"), which would have been free coarse vision on every post — but it is
only exposed to the web client, and came back empty on all thirty-two posts of a
live probe. Appearance therefore costs a real look at real images, and the
ceilings reflect that rather than pretending otherwise.

A search is a job on disk, not a function call: a real one is several hundred
API calls over ten or twenty minutes against an account Instagram will rate
limit, so it runs stage by stage, survives a crash, and lets you fix a bad probe
plan after twenty calls instead of three hundred.

```
instagram_search_start(persona={"gender": {"value": "female", "required": true},
                                "city": "Amsterdam", "niche": ["fitness"],
                                "age_band": [24, 32], "hair": "blonde"})
instagram_search_recall(search_id, probes={"hashtags": [{"tag": "fitgirlnl"}],
                                           "places":   [{"query": "Amsterdam gym"}],
                                           "accounts": [{"query": "amsterdam fitness"}]})
instagram_search_gate(search_id)      # free
instagram_search_expand(search_id)    # chain off the best
instagram_search_enrich(search_id, limit=40)
instagram_search_signals(search_id)   # free, and resolves most personas outright
instagram_search_results(search_id, limit=20)
```

### Judging the pictures

Appearance is the one thing no free signal reaches, so it has to be looked at.
`instagram_search_shortlist(download_images=true)` fetches each candidate's
profile picture and recent thumbnails and composes them into a **single numbered
contact sheet**, rather than handing over a dozen loose files.

That is not just tidier. It costs a twelfth of the attention, the numbers let a
judgement cite the tile it came from, and it makes the hardest question
answerable: *which of these faces is the account holder?* Feeds are full of
friends, partners and clients, and a judgement made on the wrong face arrives
sounding exactly as confident as a right one. Seeing every picture side by side
turns that into something you can just look at - find the recurring face, check
it against the tile marked `avatar`, which is the only picture certain to be
them, and report the result as `owner_face_confidence`. A low value there
weakens how firmly every vision reading is held, rather than pretending the
person fits worse than they do.

A screenshot of the profile page would show much the same thing, but that page
needs a logged-in browser to render at all, while these thumbnails have already
been fetched and paid for.

### Reading the confidence

Every attribute carries two numbers, never one: **match** is how well the
evidence agrees, **certainty** is how far that evidence can be trusted. A model
that reports a single "85%" has silently multiplied them and thrown away which
one was weak.

Certainty is capped per attribute *and per source*, so the system cannot
overclaim. Hair colour read off one avatar caps at 0.45; read off several
daylight posts, 0.80. Country from Instagram's own "account based in" reaches
0.95. **Height caps at 0.15** — a photograph carries no scale reference — and
`height`, `ethnicity` and `build` are advisory: reported, never allowed to move a
ranking, and rejected outright if you mark them required.

Unknown is not "no". An attribute nobody could observe lowers **coverage**, not
match, and the ranking shrinks toward the prior by how little was verified — so
a 0.9 scored on two observed attributes loses to a 0.75 on six. Anything labelled
`unverified` scored well on too little to act on.

Results are for public accounts. Private ones are dropped at the gate because
they cannot be verified. Nobody under 18 is ever returned: age is read from a
stated birth year and from Instagram's join date, the *bottom* of that estimate
decides, and the check runs when age first becomes readable and again at every
exit - an audit found the original gate-only version protected nothing, because
the gate runs before any age has been read. Every candidate keeps full provenance — which
probes found them, and what each conclusion rests on. Search jobs live in
`searches/` and are gitignored: they hold other people's profiles and photos.

## Staying unblocked

instagrapi drives the private API that the phone app uses. Instagram detects and
blocks automated behaviour, and the account it blocks is yours — so:

- **Sessions are cached** in `session.json` and reused. Logging in from scratch
  repeatedly is the single fastest way to get flagged. Keep that file.
- **Requests are spaced out** by a random `INSTAGRAM_DELAY_MIN`–`INSTAGRAM_DELAY_MAX`
  second pause. Raise it if Instagram starts asking you to wait.
- **Back off on warnings.** "Please wait a few minutes" and "action blocked"
  mean stop, not retry. The tools say so in their error messages.
- **Bulk reads are risky.** Pulling thousands of followers in one go looks
  nothing like a human using the app.
- Use a throwaway or secondary account if you are experimenting.

## Layout

| File | Contains |
| --- | --- |
| `instagram_mcp/server.py` | The 49 tool definitions |
| `instagram_mcp/persona.py` | What a persona is, and the confidence maths |
| `instagram_mcp/signals.py` | Reading a persona off a profile, free and offline |
| `instagram_mcp/names.py` | Given names to a gender prior, offline |
| `instagram_mcp/discovery.py` | The recall channels candidates come from |
| `instagram_mcp/search.py` | A persona search as a resumable job on disk |
| `instagram_mcp/sheets.py` | Candidate pictures composed into one judgeable sheet |
| `instagram_mcp/client.py` | Login, session persistence, the write guard, threading |
| `instagram_mcp/serialize.py` | Compact JSON views of instagrapi's models |
| `instagram_mcp/errors.py` | Instagram exceptions turned into actionable advice |
| `smoke_test.py` | Offline check: schemas, guards, serializers |

`.env` and `session.json` hold credentials and live auth cookies, and `searches/`
holds other people's profiles and photos. All three are gitignored — keep them
that way.

TDQS

A3.9/5.0

Scored across 49 tools

Disambiguation4/5

Most tools have clearly distinct purposes, but there is some overlap between instagram_search_posts and instagram_get_hashtag_medias (both return posts) and between instagram_find_person and instagram_search_users (both find users). The descriptions clarify the intended use cases, so ambiguity is limited.

Naming Consistency5/5

All tools follow a consistent 'instagram_' prefix with a verb_noun pattern (e.g., instagram_get_user_medias, instagram_upload_photo, instagram_search_recall). This makes the API predictable and easy to navigate, with no mixed conventions or vague verbs.

Tool Count2/5

At 49 tools, this exceeds the '25+' threshold for too many. While the server covers a wide range of Instagram features, including a complex persona search pipeline, the sheer number makes the surface overwhelming and likely to cause selection fatigue for agents.

Completeness4/5

The tool set covers the core lifecycle well: retrieval (users, media, stories, comments, likes, hashtags, locations), engagement (like/unlike, comment, follow/unfollow, direct messages), publishing (photo, video, reel, album, story, delete), and account management. Minor gaps exist (e.g., no ability to edit an existing post or delete a comment), but they are not critical for most workflows.

Maintenance

ActivityMaintained
ResponsivenessNo issues