instagram-mcp
Enables agents to interact with Instagram through 49 tools for direct messages, feed, profiles, search, and persona discovery, with write actions disabled by default until explicitly enabled.
README
instagram-mcp
An MCP server that exposes 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
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:
.venv/Scripts/python smoke_test.py
Registering the server
Already registered for this project in ../.mcp.json. To use it elsewhere:
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
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:
.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.jsonand 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_MAXsecond 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.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.
E2B
Using MCP to run code via e2b.