mcp-instagram-dm 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kynux
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,231 @@
1
+ # mcp-instagram-dm
2
+
3
+ [![npm version](https://img.shields.io/npm/v/mcp-instagram-dm.svg)](https://www.npmjs.com/package/mcp-instagram-dm)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org/)
6
+
7
+ Read, send, and manage Instagram Direct Messages through AI assistants via the [Model Context Protocol](https://modelcontextprotocol.io).
8
+
9
+ Works with **personal Instagram accounts** using cookie-based authentication. Supports reading conversations, sending messages, reacting, searching, and more — all through natural language with your AI assistant.
10
+
11
+ ## Quick Start
12
+
13
+ ### Claude Desktop / Claude Code
14
+
15
+ Add to your MCP config:
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "instagram": {
21
+ "command": "npx",
22
+ "args": ["-y", "mcp-instagram-dm"],
23
+ "env": {
24
+ "INSTAGRAM_SESSION_ID": "your_session_id",
25
+ "INSTAGRAM_CSRF_TOKEN": "your_csrf_token",
26
+ "INSTAGRAM_DS_USER_ID": "your_user_id"
27
+ }
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ That's it. Ask Claude: *"Read my Instagram DMs"* and it works.
34
+
35
+ ## Features
36
+
37
+ ### Read
38
+ - **`instagram_get_inbox`** — List recent DM conversations with unread indicators
39
+ - **`instagram_get_thread`** — Get messages from a conversation (auto-paginates, e.g. fetch 500 messages at once)
40
+ - **`instagram_get_pending`** — List pending DM requests
41
+ - **`instagram_user_info`** — Get a user's profile details (bio, followers, posts)
42
+ - **`instagram_thread_info`** — Get conversation metadata (participants, group info, mute status)
43
+
44
+ ### Write
45
+ - **`instagram_send_message`** — Send a text message
46
+ - **`instagram_send_link`** — Share a URL in a conversation
47
+ - **`instagram_create_thread`** — Start a new DM with any user
48
+ - **`instagram_like_message`** — React to a message with any emoji
49
+ - **`instagram_unsend_message`** — Unsend your own messages
50
+ - **`instagram_mark_seen`** — Mark a conversation as read
51
+ - **`instagram_approve_pending`** — Approve a pending DM request
52
+
53
+ ### Search
54
+ - **`instagram_search_inbox`** — Search conversations by username or name
55
+ - **`instagram_search_messages`** — Find messages containing specific text within a thread
56
+ - **`instagram_search_users`** — Search Instagram users (to start new conversations)
57
+
58
+ ## Installation
59
+
60
+ ### npx (recommended, zero install)
61
+
62
+ ```bash
63
+ npx mcp-instagram-dm
64
+ ```
65
+
66
+ ### npm global
67
+
68
+ ```bash
69
+ npm install -g mcp-instagram-dm
70
+ mcp-instagram-dm
71
+ ```
72
+
73
+ ### From source
74
+
75
+ ```bash
76
+ git clone https://github.com/Kynuxdev/mcp-instagram-dm.git
77
+ cd mcp-instagram-dm
78
+ npm install
79
+ npm run build
80
+ node dist/index.js
81
+ ```
82
+
83
+ ## Configuration
84
+
85
+ ### Getting Your Cookies
86
+
87
+ 1. Open [Instagram](https://www.instagram.com) in Chrome and log in
88
+ 2. Press **F12** to open DevTools
89
+ 3. Go to **Application** tab > **Cookies** > `https://www.instagram.com`
90
+ 4. Copy these values:
91
+
92
+ | Cookie Name | Environment Variable |
93
+ |---|---|
94
+ | `sessionid` | `INSTAGRAM_SESSION_ID` |
95
+ | `csrftoken` | `INSTAGRAM_CSRF_TOKEN` |
96
+ | `ds_user_id` | `INSTAGRAM_DS_USER_ID` |
97
+
98
+ ### Environment Variables
99
+
100
+ | Variable | Required | Description |
101
+ |---|---|---|
102
+ | `INSTAGRAM_SESSION_ID` | Yes | Your Instagram session cookie |
103
+ | `INSTAGRAM_CSRF_TOKEN` | Yes | CSRF token from cookies |
104
+ | `INSTAGRAM_DS_USER_ID` | Yes | Your numeric user ID |
105
+ | `INSTAGRAM_RATE_LIMIT_MS` | No | Delay between paginated requests (default: 300ms) |
106
+
107
+ ### Client Configuration
108
+
109
+ <details>
110
+ <summary><b>Claude Desktop</b></summary>
111
+
112
+ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
113
+
114
+ ```json
115
+ {
116
+ "mcpServers": {
117
+ "instagram": {
118
+ "command": "npx",
119
+ "args": ["-y", "mcp-instagram-dm"],
120
+ "env": {
121
+ "INSTAGRAM_SESSION_ID": "your_session_id",
122
+ "INSTAGRAM_CSRF_TOKEN": "your_csrf_token",
123
+ "INSTAGRAM_DS_USER_ID": "your_user_id"
124
+ }
125
+ }
126
+ }
127
+ }
128
+ ```
129
+ </details>
130
+
131
+ <details>
132
+ <summary><b>Claude Code</b></summary>
133
+
134
+ Add to your project's `.mcp.json`:
135
+
136
+ ```json
137
+ {
138
+ "mcpServers": {
139
+ "instagram": {
140
+ "command": "npx",
141
+ "args": ["-y", "mcp-instagram-dm"],
142
+ "env": {
143
+ "INSTAGRAM_SESSION_ID": "your_session_id",
144
+ "INSTAGRAM_CSRF_TOKEN": "your_csrf_token",
145
+ "INSTAGRAM_DS_USER_ID": "your_user_id"
146
+ }
147
+ }
148
+ }
149
+ }
150
+ ```
151
+ </details>
152
+
153
+ <details>
154
+ <summary><b>Cursor</b></summary>
155
+
156
+ Add to `.cursor/mcp.json` in your project:
157
+
158
+ ```json
159
+ {
160
+ "mcpServers": {
161
+ "instagram": {
162
+ "command": "npx",
163
+ "args": ["-y", "mcp-instagram-dm"],
164
+ "env": {
165
+ "INSTAGRAM_SESSION_ID": "your_session_id",
166
+ "INSTAGRAM_CSRF_TOKEN": "your_csrf_token",
167
+ "INSTAGRAM_DS_USER_ID": "your_user_id"
168
+ }
169
+ }
170
+ }
171
+ }
172
+ ```
173
+ </details>
174
+
175
+ ## Examples
176
+
177
+ Just talk to your AI assistant naturally:
178
+
179
+ - *"Read my unread Instagram DMs"*
180
+ - *"Send 'Hey, are you free tonight?' to my conversation with @username"*
181
+ - *"Search my DMs for messages about 'meeting'"*
182
+ - *"Start a new conversation with @johndoe and say hello"*
183
+ - *"Show me my pending DM requests and approve them"*
184
+ - *"Get the profile info for user 12345678"*
185
+
186
+ ## Tools Reference
187
+
188
+ | Tool | Description | Key Parameters |
189
+ |---|---|---|
190
+ | `instagram_get_inbox` | List DM conversations | `limit`, `cursor` |
191
+ | `instagram_get_thread` | Get thread messages | `thread_id`, `limit` (auto-paginates) |
192
+ | `instagram_get_pending` | List pending requests | `limit`, `cursor` |
193
+ | `instagram_user_info` | Get user profile | `user_id` (PK) |
194
+ | `instagram_thread_info` | Get thread details | `thread_id` |
195
+ | `instagram_send_message` | Send text message | `thread_id`, `text` |
196
+ | `instagram_send_link` | Share a URL | `thread_id`, `url`, `text` |
197
+ | `instagram_create_thread` | Start new DM | `recipient_ids`, `text` |
198
+ | `instagram_like_message` | React with emoji | `thread_id`, `item_id`, `emoji` |
199
+ | `instagram_unsend_message` | Unsend a message | `thread_id`, `item_id` |
200
+ | `instagram_mark_seen` | Mark as read | `thread_id`, `item_id` |
201
+ | `instagram_approve_pending` | Approve request | `thread_id` |
202
+ | `instagram_search_inbox` | Search conversations | `query`, `max_pages` |
203
+ | `instagram_search_messages` | Search within thread | `thread_id`, `query`, `max_messages` |
204
+ | `instagram_search_users` | Find users | `query` |
205
+
206
+ ## How It Works
207
+
208
+ This server uses Instagram's private web API (the same API the instagram.com website uses) with cookie-based authentication. It communicates with AI assistants through the Model Context Protocol (MCP) via stdio transport.
209
+
210
+ **Architecture:**
211
+ - `src/index.ts` — MCP server with 15 tools
212
+ - `src/instagram.ts` — Instagram API client
213
+ - `src/types.ts` — TypeScript interfaces
214
+
215
+ **Single dependency:** Only `@modelcontextprotocol/sdk`. No axios, no puppeteer, no bloat.
216
+
217
+ ## Disclaimer
218
+
219
+ - This project uses Instagram's **unofficial** web API, which may change without notice
220
+ - **Personal use only** — do not use for spam, mass messaging, or automation that violates Instagram's Terms of Service
221
+ - Your session cookies are sensitive credentials — never share them or commit them to version control
222
+ - This project is **not affiliated with, endorsed by, or connected to Meta or Instagram**
223
+ - Use at your own risk. The authors are not responsible for any account restrictions
224
+
225
+ ## Contributing
226
+
227
+ Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
228
+
229
+ ## License
230
+
231
+ [MIT](LICENSE)
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,456 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
5
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+ const zod_1 = require("zod");
7
+ const instagram_js_1 = require("./instagram.js");
8
+ const ig = new instagram_js_1.InstagramClient();
9
+ // --- Helpers ---
10
+ function buildUserMap(thread) {
11
+ const map = new Map();
12
+ for (const u of thread.users ?? []) {
13
+ map.set(String(u.pk), u.username);
14
+ }
15
+ if (ig.viewerUsername) {
16
+ const viewerId = process.env.INSTAGRAM_DS_USER_ID || "";
17
+ if (viewerId)
18
+ map.set(viewerId, ig.viewerUsername);
19
+ }
20
+ return map;
21
+ }
22
+ function formatMessage(item, users) {
23
+ const sender = users.get(String(item.user_id)) ?? String(item.user_id);
24
+ const date = new Date(item.timestamp / 1000).toISOString().replace("T", " ").slice(0, 19);
25
+ let content;
26
+ switch (item.item_type) {
27
+ case "text":
28
+ content = item.text ?? "";
29
+ break;
30
+ case "media":
31
+ content = "[Media]";
32
+ if (item.media?.image_versions2?.candidates?.[0]?.url) {
33
+ content += ` ${item.media.image_versions2.candidates[0].url.split("?")[0]}`;
34
+ }
35
+ break;
36
+ case "voice_media":
37
+ content = "[Voice message]";
38
+ if (item.voice_media?.media?.audio?.audio_src) {
39
+ content += ` ${item.voice_media.media.audio.audio_src.split("?")[0]}`;
40
+ }
41
+ break;
42
+ case "reel_share":
43
+ content = `[Reel] ${item.reel_share?.text ?? ""}`;
44
+ break;
45
+ case "link":
46
+ content = `[Link] ${item.link?.text ?? ""} ${item.link?.link_url ?? ""}`.trim();
47
+ break;
48
+ case "clip":
49
+ content = `[Clip] ${item.clip?.clip?.caption?.text ?? ""}`;
50
+ break;
51
+ case "animated_media":
52
+ content = "[GIF]";
53
+ if (item.animated_media?.images?.fixed_height?.url) {
54
+ content += ` ${item.animated_media.images.fixed_height.url.split("?")[0]}`;
55
+ }
56
+ break;
57
+ case "media_share":
58
+ content = `[Post share] ${item.media_share?.caption?.text ?? ""}`.trim();
59
+ break;
60
+ case "story_share":
61
+ content = `[Story share] ${item.story_share?.title ?? ""} ${item.story_share?.message ?? ""}`.trim();
62
+ break;
63
+ case "action_log":
64
+ content = `[${item.action_log?.description ?? "Action"}]`;
65
+ break;
66
+ default:
67
+ content = `[${item.item_type}]`;
68
+ }
69
+ if (item.reply_to_message?.text) {
70
+ content = `(reply: "${item.reply_to_message.text.slice(0, 50)}") ${content}`;
71
+ }
72
+ if (item.reactions?.emojis?.length) {
73
+ const reacts = item.reactions.emojis.map((r) => r.emoji).join("");
74
+ content += ` ${reacts}`;
75
+ }
76
+ return `[${date}] ${sender}: ${content}`;
77
+ }
78
+ function formatThread(t) {
79
+ const users = t.users?.map((u) => u.username).join(", ") || "";
80
+ const title = t.thread_title || users;
81
+ const userMap = buildUserMap(t);
82
+ const lastItem = t.items?.[0];
83
+ const lastText = lastItem ? formatMessage(lastItem, userMap) : "(empty)";
84
+ const unread = t.read_state === 0 ? " [UNREAD]" : "";
85
+ const group = t.is_group ? " [GROUP]" : "";
86
+ const muted = t.muted ? " [MUTED]" : "";
87
+ const pending = t.pending ? " [PENDING]" : "";
88
+ return `${unread}${group}${muted}${pending} ${title} (thread_id: ${t.thread_id})\n Last: ${lastText}`;
89
+ }
90
+ function formatUserInfo(user) {
91
+ const parts = [
92
+ `User: @${user.username}`,
93
+ `Name: ${user.full_name || "-"}`,
94
+ user.is_verified ? "Verified" : null,
95
+ user.is_private ? "Private account" : "Public account",
96
+ user.biography ? `Bio: ${user.biography}` : null,
97
+ user.follower_count != null ? `Followers: ${user.follower_count}` : null,
98
+ user.following_count != null ? `Following: ${user.following_count}` : null,
99
+ user.media_count != null ? `Posts: ${user.media_count}` : null,
100
+ user.external_url ? `Link: ${user.external_url}` : null,
101
+ `PK: ${user.pk}`,
102
+ ];
103
+ return parts.filter(Boolean).join("\n");
104
+ }
105
+ function ok(text) {
106
+ return { content: [{ type: "text", text }] };
107
+ }
108
+ function err(e) {
109
+ const msg = e instanceof Error ? e.message : String(e);
110
+ return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
111
+ }
112
+ // --- MCP Server ---
113
+ const server = new mcp_js_1.McpServer({ name: "instagram-dm", version: "2.1.0" });
114
+ // ===== 1. INBOX =====
115
+ server.tool("instagram_get_inbox", "List recent Instagram DM conversations", {
116
+ cursor: zod_1.z.string().optional().describe("Pagination cursor from previous response"),
117
+ limit: zod_1.z.number().optional().default(20).describe("Number of conversations to fetch (max 20)"),
118
+ }, async ({ cursor, limit }) => {
119
+ try {
120
+ const data = await ig.getInbox(cursor, limit);
121
+ const threads = (data.inbox?.threads || []).map(formatThread).join("\n\n");
122
+ const pendingCount = data.pending_requests_total ?? 0;
123
+ const pendingInfo = pendingCount > 0 ? `\nPending requests: ${pendingCount}` : "";
124
+ const footer = data.inbox?.has_older
125
+ ? `\n\n--- More: cursor="${data.inbox.oldest_cursor}" ---`
126
+ : "\n\n--- End ---";
127
+ return ok(`Inbox (${data.inbox?.threads?.length || 0})${pendingInfo}\n\n${threads}${footer}`);
128
+ }
129
+ catch (e) {
130
+ return err(e);
131
+ }
132
+ });
133
+ // ===== 2. THREAD =====
134
+ server.tool("instagram_get_thread", "Get messages from a specific DM conversation. Automatically paginates when limit > 20 (e.g. limit=500 fetches 500 messages at once).", {
135
+ thread_id: zod_1.z.string().describe("Thread ID (from inbox)"),
136
+ cursor: zod_1.z.string().optional().describe("Pagination cursor"),
137
+ limit: zod_1.z.number().optional().default(20).describe("Number of messages to fetch (auto-paginates above 20, e.g. 500)"),
138
+ }, async ({ thread_id, cursor, limit }) => {
139
+ try {
140
+ const allItems = [];
141
+ let userMap = new Map();
142
+ let title = "";
143
+ let currentCursor = cursor;
144
+ let hasOlder = true;
145
+ let oldestCursor = "";
146
+ const perPage = 20;
147
+ while (allItems.length < limit && hasOlder) {
148
+ const fetchLimit = Math.min(perPage, limit - allItems.length);
149
+ const data = await ig.getThread(thread_id, currentCursor, fetchLimit);
150
+ const t = data.thread;
151
+ if (!title) {
152
+ title = t.thread_title || t.users?.map((u) => u.username).join(", ");
153
+ userMap = buildUserMap(t);
154
+ }
155
+ const items = t.items || [];
156
+ if (items.length === 0)
157
+ break;
158
+ allItems.push(...items);
159
+ hasOlder = !!t.has_older;
160
+ oldestCursor = t.oldest_cursor || "";
161
+ currentCursor = oldestCursor;
162
+ if (allItems.length < limit && hasOlder) {
163
+ await new Promise((r) => setTimeout(r, 300));
164
+ }
165
+ }
166
+ const messages = [...allItems]
167
+ .reverse()
168
+ .map((item) => formatMessage(item, userMap))
169
+ .join("\n");
170
+ const footer = hasOlder
171
+ ? `\n\n--- Older messages: cursor="${oldestCursor}" ---`
172
+ : "\n\n--- Start ---";
173
+ return ok(`${title} (${allItems.length} messages)\n\n${messages}${footer}`);
174
+ }
175
+ catch (e) {
176
+ return err(e);
177
+ }
178
+ });
179
+ // ===== 3. SEARCH =====
180
+ server.tool("instagram_search_inbox", "Search inbox by username or name (scans all pages)", {
181
+ query: zod_1.z.string().describe("Username or name to search"),
182
+ max_pages: zod_1.z.number().optional().default(5).describe("Max pages to scan (20 threads per page)"),
183
+ }, async ({ query, max_pages }) => {
184
+ try {
185
+ const q = query.toLowerCase();
186
+ const matched = [];
187
+ let cursor;
188
+ let pages = 0;
189
+ while (pages < max_pages) {
190
+ const data = await ig.getInbox(cursor, 20);
191
+ const threads = data.inbox?.threads || [];
192
+ for (const t of threads) {
193
+ if (t.thread_title?.toLowerCase().includes(q)) {
194
+ matched.push(t);
195
+ continue;
196
+ }
197
+ if (t.users?.some((u) => u.username.toLowerCase().includes(q) || u.full_name?.toLowerCase().includes(q))) {
198
+ matched.push(t);
199
+ }
200
+ }
201
+ if (!data.inbox?.has_older)
202
+ break;
203
+ cursor = data.inbox.oldest_cursor;
204
+ pages++;
205
+ }
206
+ if (!matched.length) {
207
+ return ok(`"${query}" not found. (${pages + 1} pages scanned)`);
208
+ }
209
+ return ok(`"${query}" - ${matched.length} result(s) (${pages + 1} pages scanned)\n\n${matched.map(formatThread).join("\n\n")}`);
210
+ }
211
+ catch (e) {
212
+ return err(e);
213
+ }
214
+ });
215
+ // ===== 4. SEND MESSAGE =====
216
+ server.tool("instagram_send_message", "Send a text message in an Instagram DM thread", {
217
+ thread_id: zod_1.z.string().describe("Thread ID"),
218
+ text: zod_1.z.string().describe("Message text to send"),
219
+ }, async ({ thread_id, text }) => {
220
+ try {
221
+ await ig.sendMessage(thread_id, text);
222
+ return ok(`Message sent: "${text.slice(0, 100)}${text.length > 100 ? "..." : ""}"`);
223
+ }
224
+ catch (e) {
225
+ return err(e);
226
+ }
227
+ });
228
+ // ===== 5. SEND LINK =====
229
+ server.tool("instagram_send_link", "Share a link in an Instagram DM thread", {
230
+ thread_id: zod_1.z.string().describe("Thread ID"),
231
+ url: zod_1.z.string().describe("URL to share"),
232
+ text: zod_1.z.string().optional().describe("Optional text to send with the link"),
233
+ }, async ({ thread_id, url, text }) => {
234
+ try {
235
+ await ig.sendLink(thread_id, url, text);
236
+ return ok(`Link sent: ${url}`);
237
+ }
238
+ catch (e) {
239
+ return err(e);
240
+ }
241
+ });
242
+ // ===== 6. REACT =====
243
+ server.tool("instagram_like_message", "React to a message with an emoji (default: heart)", {
244
+ thread_id: zod_1.z.string().describe("Thread ID"),
245
+ item_id: zod_1.z.string().describe("Message ID"),
246
+ emoji: zod_1.z.string().optional().default("\u2764\uFE0F").describe("Reaction emoji (default: heart)"),
247
+ }, async ({ thread_id, item_id, emoji }) => {
248
+ try {
249
+ await ig.reactToMessage(thread_id, item_id, emoji);
250
+ return ok(`Reaction sent: ${emoji}`);
251
+ }
252
+ catch (e) {
253
+ return err(e);
254
+ }
255
+ });
256
+ // ===== 7. UNSEND =====
257
+ server.tool("instagram_unsend_message", "Unsend a message you sent (your own messages only)", {
258
+ thread_id: zod_1.z.string().describe("Thread ID"),
259
+ item_id: zod_1.z.string().describe("Message ID to unsend"),
260
+ }, async ({ thread_id, item_id }) => {
261
+ try {
262
+ await ig.unsendMessage(thread_id, item_id);
263
+ return ok("Message unsent.");
264
+ }
265
+ catch (e) {
266
+ return err(e);
267
+ }
268
+ });
269
+ // ===== 8. MARK SEEN =====
270
+ server.tool("instagram_mark_seen", "Mark a conversation as read", {
271
+ thread_id: zod_1.z.string().describe("Thread ID"),
272
+ item_id: zod_1.z.string().describe("Last seen message ID"),
273
+ }, async ({ thread_id, item_id }) => {
274
+ try {
275
+ await ig.markSeen(thread_id, item_id);
276
+ return ok("Marked as seen.");
277
+ }
278
+ catch (e) {
279
+ return err(e);
280
+ }
281
+ });
282
+ // ===== 9. PENDING INBOX =====
283
+ server.tool("instagram_get_pending", "List pending (unapproved) DM requests", {
284
+ cursor: zod_1.z.string().optional().describe("Pagination cursor"),
285
+ limit: zod_1.z.number().optional().default(20).describe("Number of requests to fetch"),
286
+ }, async ({ cursor, limit }) => {
287
+ try {
288
+ const data = await ig.getPendingInbox(cursor, limit);
289
+ const threads = (data.inbox?.threads || []).map(formatThread).join("\n\n");
290
+ const footer = data.inbox?.has_older
291
+ ? `\n\n--- More: cursor="${data.inbox.oldest_cursor}" ---`
292
+ : "\n\n--- End ---";
293
+ return ok(`Pending requests (${data.inbox?.threads?.length || 0})\n\n${threads}${footer}`);
294
+ }
295
+ catch (e) {
296
+ return err(e);
297
+ }
298
+ });
299
+ // ===== 10. APPROVE PENDING =====
300
+ server.tool("instagram_approve_pending", "Approve a pending DM request", {
301
+ thread_id: zod_1.z.string().describe("Thread ID to approve"),
302
+ }, async ({ thread_id }) => {
303
+ try {
304
+ await ig.approvePending(thread_id);
305
+ return ok("Request approved.");
306
+ }
307
+ catch (e) {
308
+ return err(e);
309
+ }
310
+ });
311
+ // ===== 11. USER INFO =====
312
+ server.tool("instagram_user_info", "Get a user's profile information", {
313
+ user_id: zod_1.z.string().describe("User PK ID (from thread user list)"),
314
+ }, async ({ user_id }) => {
315
+ try {
316
+ const data = await ig.getUserInfo(user_id);
317
+ return ok(formatUserInfo(data.user));
318
+ }
319
+ catch (e) {
320
+ return err(e);
321
+ }
322
+ });
323
+ // ===== 12. SEARCH USERS =====
324
+ server.tool("instagram_search_users", "Search Instagram users (useful for finding users to message)", {
325
+ query: zod_1.z.string().describe("Username or name to search"),
326
+ }, async ({ query }) => {
327
+ try {
328
+ const data = await ig.searchUsers(query);
329
+ const recipients = data.ranked_recipients || [];
330
+ if (!recipients.length) {
331
+ return ok(`No results for "${query}".`);
332
+ }
333
+ const lines = recipients
334
+ .filter((r) => r.user)
335
+ .map((r) => {
336
+ const u = r.user;
337
+ return `@${u.username} - ${u.full_name || ""} (pk: ${u.pk})${u.is_verified ? " [Verified]" : ""}`;
338
+ })
339
+ .join("\n");
340
+ const threads = recipients
341
+ .filter((r) => r.thread)
342
+ .map((r) => {
343
+ const t = r.thread;
344
+ return `[Thread] ${t.thread_title || "Unnamed"} (thread_id: ${t.thread_id})`;
345
+ })
346
+ .join("\n");
347
+ let result = `Results for "${query}":\n\n`;
348
+ if (lines)
349
+ result += `Users:\n${lines}\n`;
350
+ if (threads)
351
+ result += `\nExisting threads:\n${threads}\n`;
352
+ return ok(result);
353
+ }
354
+ catch (e) {
355
+ return err(e);
356
+ }
357
+ });
358
+ // ===== 13. CREATE THREAD =====
359
+ server.tool("instagram_create_thread", "Start a new DM conversation by sending a message to one or more users", {
360
+ recipient_ids: zod_1.z.array(zod_1.z.string()).describe("User PK IDs (use instagram_search_users to find)"),
361
+ text: zod_1.z.string().describe("Initial message text"),
362
+ }, async ({ recipient_ids, text }) => {
363
+ try {
364
+ const data = await ig.createThread(recipient_ids, text);
365
+ const threadId = data.payload?.thread_id || data.thread_id || "unknown";
366
+ return ok(`Thread created (thread_id: ${threadId}). Message sent: "${text.slice(0, 100)}${text.length > 100 ? "..." : ""}"`);
367
+ }
368
+ catch (e) {
369
+ return err(e);
370
+ }
371
+ });
372
+ // ===== 14. SEARCH MESSAGES =====
373
+ server.tool("instagram_search_messages", "Search for messages containing specific text within a DM thread", {
374
+ thread_id: zod_1.z.string().describe("Thread ID to search in"),
375
+ query: zod_1.z.string().describe("Text to search for"),
376
+ max_messages: zod_1.z.number().optional().default(200).describe("Max messages to scan (default 200)"),
377
+ }, async ({ thread_id, query, max_messages }) => {
378
+ try {
379
+ const q = query.toLowerCase();
380
+ const allItems = [];
381
+ let userMap = new Map();
382
+ let title = "";
383
+ let currentCursor;
384
+ let hasOlder = true;
385
+ while (allItems.length < max_messages && hasOlder) {
386
+ const data = await ig.getThread(thread_id, currentCursor, 20);
387
+ const t = data.thread;
388
+ if (!title) {
389
+ title = t.thread_title || t.users?.map((u) => u.username).join(", ");
390
+ userMap = buildUserMap(t);
391
+ }
392
+ const items = t.items || [];
393
+ if (items.length === 0)
394
+ break;
395
+ allItems.push(...items);
396
+ hasOlder = !!t.has_older;
397
+ currentCursor = t.oldest_cursor || "";
398
+ if (allItems.length < max_messages && hasOlder) {
399
+ await new Promise((r) => setTimeout(r, 300));
400
+ }
401
+ }
402
+ const matches = allItems
403
+ .filter((item) => {
404
+ const text = item.text?.toLowerCase() || "";
405
+ const reel = item.reel_share?.text?.toLowerCase() || "";
406
+ const link = item.link?.text?.toLowerCase() || "";
407
+ const clip = item.clip?.clip?.caption?.text?.toLowerCase() || "";
408
+ const share = item.media_share?.caption?.text?.toLowerCase() || "";
409
+ return text.includes(q) || reel.includes(q) || link.includes(q) || clip.includes(q) || share.includes(q);
410
+ })
411
+ .reverse()
412
+ .map((item) => formatMessage(item, userMap));
413
+ if (!matches.length) {
414
+ return ok(`No messages matching "${query}" in ${title}. (${allItems.length} messages scanned)`);
415
+ }
416
+ return ok(`Found ${matches.length} message(s) matching "${query}" in ${title} (${allItems.length} scanned):\n\n${matches.join("\n")}`);
417
+ }
418
+ catch (e) {
419
+ return err(e);
420
+ }
421
+ });
422
+ // ===== 15. THREAD INFO =====
423
+ server.tool("instagram_thread_info", "Get conversation details: participants, group name, mute status, and more", {
424
+ thread_id: zod_1.z.string().describe("Thread ID"),
425
+ }, async ({ thread_id }) => {
426
+ try {
427
+ const data = await ig.getThread(thread_id, undefined, 1);
428
+ const t = data.thread;
429
+ const participants = t.users?.map((u) => `@${u.username} (${u.full_name || "-"}, pk: ${u.pk})${u.is_verified ? " [Verified]" : ""}`).join("\n ") || "None";
430
+ const parts = [
431
+ `Thread: ${t.thread_title || t.users?.map((u) => u.username).join(", ") || "Unnamed"}`,
432
+ `Thread ID: ${t.thread_id}`,
433
+ `Type: ${t.is_group ? "Group" : "Direct"}`,
434
+ `Participants:\n ${participants}`,
435
+ `Muted: ${t.muted ? "Yes" : "No"}`,
436
+ `Archived: ${t.archived ? "Yes" : "No"}`,
437
+ `Pending: ${t.pending ? "Yes" : "No"}`,
438
+ `Has older messages: ${t.has_older ? "Yes" : "No"}`,
439
+ t.inviter ? `Inviter: @${t.inviter.username}` : null,
440
+ ];
441
+ return ok(parts.filter(Boolean).join("\n"));
442
+ }
443
+ catch (e) {
444
+ return err(e);
445
+ }
446
+ });
447
+ // --- Start ---
448
+ async function main() {
449
+ await ig.validateSession();
450
+ const transport = new stdio_js_1.StdioServerTransport();
451
+ await server.connect(transport);
452
+ }
453
+ main().catch((e) => {
454
+ console.error("Fatal:", e);
455
+ process.exit(1);
456
+ });
@@ -0,0 +1,23 @@
1
+ import type { InstaInboxResponse, InstaThreadResponse, InstaUserInfoResponse, InstaActionResponse, InstaSearchUsersResponse } from "./types.js";
2
+ export declare class InstagramClient {
3
+ private sessionId;
4
+ private csrfToken;
5
+ private dsUserId;
6
+ viewerUsername: string;
7
+ constructor();
8
+ private get headers();
9
+ private request;
10
+ validateSession(): Promise<void>;
11
+ getInbox(cursor?: string, limit?: number): Promise<InstaInboxResponse>;
12
+ getThread(threadId: string, cursor?: string, limit?: number): Promise<InstaThreadResponse>;
13
+ getPendingInbox(cursor?: string, limit?: number): Promise<InstaInboxResponse>;
14
+ getUserInfo(userId: string): Promise<InstaUserInfoResponse>;
15
+ sendMessage(threadId: string, text: string): Promise<InstaActionResponse>;
16
+ sendLink(threadId: string, linkUrl: string, text?: string): Promise<InstaActionResponse>;
17
+ reactToMessage(threadId: string, itemId: string, emoji?: string): Promise<InstaActionResponse>;
18
+ unsendMessage(threadId: string, itemId: string): Promise<InstaActionResponse>;
19
+ markSeen(threadId: string, itemId: string): Promise<InstaActionResponse>;
20
+ approvePending(threadId: string): Promise<InstaActionResponse>;
21
+ searchUsers(query: string): Promise<InstaSearchUsersResponse>;
22
+ createThread(recipientIds: string[], text: string): Promise<InstaActionResponse>;
23
+ }
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InstagramClient = void 0;
4
+ const IG_APP_ID = "936619743392459";
5
+ const RATE_LIMIT_MS = parseInt(process.env.INSTAGRAM_RATE_LIMIT_MS || "300", 10);
6
+ const ERROR_TRUNCATE = 500;
7
+ const DEFAULT_PAGE_SIZE = 20;
8
+ class InstagramClient {
9
+ sessionId;
10
+ csrfToken;
11
+ dsUserId;
12
+ viewerUsername = "";
13
+ constructor() {
14
+ this.sessionId = process.env.INSTAGRAM_SESSION_ID || "";
15
+ this.csrfToken = process.env.INSTAGRAM_CSRF_TOKEN || "";
16
+ this.dsUserId = process.env.INSTAGRAM_DS_USER_ID || "";
17
+ if (!this.sessionId || !this.csrfToken || !this.dsUserId) {
18
+ throw new Error("Missing Instagram credentials. Required env variables: INSTAGRAM_SESSION_ID, INSTAGRAM_CSRF_TOKEN, INSTAGRAM_DS_USER_ID");
19
+ }
20
+ }
21
+ get headers() {
22
+ return {
23
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
24
+ "X-CSRFToken": this.csrfToken,
25
+ "X-IG-App-ID": IG_APP_ID,
26
+ "X-Requested-With": "XMLHttpRequest",
27
+ Referer: "https://www.instagram.com/direct/inbox/",
28
+ Cookie: `sessionid=${this.sessionId}; csrftoken=${this.csrfToken}; ds_user_id=${this.dsUserId}`,
29
+ };
30
+ }
31
+ async request(path, params, method = "GET", body) {
32
+ const url = new URL(path, "https://www.instagram.com");
33
+ if (params) {
34
+ for (const [k, v] of Object.entries(params)) {
35
+ url.searchParams.set(k, v);
36
+ }
37
+ }
38
+ const opts = {
39
+ method,
40
+ headers: this.headers,
41
+ };
42
+ if (body && method === "POST") {
43
+ const formBody = new URLSearchParams(body).toString();
44
+ opts.body = formBody;
45
+ opts.headers["Content-Type"] =
46
+ "application/x-www-form-urlencoded";
47
+ }
48
+ const res = await fetch(url.toString(), opts);
49
+ if (!res.ok) {
50
+ const text = await res.text().catch(() => "");
51
+ throw new Error(`Instagram API error: ${res.status} ${res.statusText} - ${text.slice(0, ERROR_TRUNCATE)}`);
52
+ }
53
+ const text = await res.text();
54
+ try {
55
+ return JSON.parse(text);
56
+ }
57
+ catch {
58
+ throw new Error(`JSON parse error. Response (first ${ERROR_TRUNCATE} chars): ${text.slice(0, ERROR_TRUNCATE)}`);
59
+ }
60
+ }
61
+ // --- Session Validation ---
62
+ async validateSession() {
63
+ try {
64
+ await this.request("/api/v1/direct_v2/inbox/", {
65
+ limit: "1",
66
+ });
67
+ }
68
+ catch (e) {
69
+ const msg = e instanceof Error ? e.message : String(e);
70
+ if (msg.includes("401") || msg.includes("403") || msg.includes("login")) {
71
+ throw new Error("Instagram session expired or invalid. Please update your cookies (INSTAGRAM_SESSION_ID, INSTAGRAM_CSRF_TOKEN, INSTAGRAM_DS_USER_ID).");
72
+ }
73
+ throw e;
74
+ }
75
+ }
76
+ // --- READ ---
77
+ async getInbox(cursor, limit = DEFAULT_PAGE_SIZE) {
78
+ const params = {
79
+ visual_message_return_type: "unseen",
80
+ thread_message_limit: "1",
81
+ persistentBadging: "true",
82
+ limit: String(limit),
83
+ };
84
+ if (cursor)
85
+ params.cursor = cursor;
86
+ const data = await this.request("/api/v1/direct_v2/inbox/", params);
87
+ if (data.viewer?.username && !this.viewerUsername) {
88
+ this.viewerUsername = data.viewer.username;
89
+ }
90
+ return data;
91
+ }
92
+ async getThread(threadId, cursor, limit = DEFAULT_PAGE_SIZE) {
93
+ const params = {
94
+ visual_message_return_type: "unseen",
95
+ limit: String(limit),
96
+ };
97
+ if (cursor)
98
+ params.cursor = cursor;
99
+ return this.request(`/api/v1/direct_v2/threads/${threadId}/`, params);
100
+ }
101
+ async getPendingInbox(cursor, limit = DEFAULT_PAGE_SIZE) {
102
+ const params = {
103
+ visual_message_return_type: "unseen",
104
+ thread_message_limit: "1",
105
+ persistentBadging: "true",
106
+ limit: String(limit),
107
+ };
108
+ if (cursor)
109
+ params.cursor = cursor;
110
+ return this.request("/api/v1/direct_v2/pending_inbox/", params);
111
+ }
112
+ async getUserInfo(userId) {
113
+ return this.request(`/api/v1/users/${userId}/info/`);
114
+ }
115
+ // --- WRITE ---
116
+ async sendMessage(threadId, text) {
117
+ return this.request(`/api/v1/direct_v2/threads/${threadId}/items/`, undefined, "POST", {
118
+ item_type: "text",
119
+ text,
120
+ });
121
+ }
122
+ async sendLink(threadId, linkUrl, text) {
123
+ const body = {
124
+ item_type: "link",
125
+ link_urls: JSON.stringify([linkUrl]),
126
+ };
127
+ if (text)
128
+ body.link_text = text;
129
+ return this.request(`/api/v1/direct_v2/threads/${threadId}/items/`, undefined, "POST", body);
130
+ }
131
+ async reactToMessage(threadId, itemId, emoji = "\u2764\uFE0F") {
132
+ return this.request(`/api/v1/direct_v2/threads/${threadId}/items/${itemId}/reactions/`, undefined, "POST", {
133
+ reaction_type: "like",
134
+ reaction_status: "created",
135
+ emoji,
136
+ item_id: itemId,
137
+ node_type: "item",
138
+ client_context: String(Date.now()),
139
+ });
140
+ }
141
+ async unsendMessage(threadId, itemId) {
142
+ return this.request(`/api/v1/direct_v2/threads/${threadId}/items/${itemId}/delete/`, undefined, "POST", {});
143
+ }
144
+ async markSeen(threadId, itemId) {
145
+ return this.request(`/api/v1/direct_v2/threads/${threadId}/items/${itemId}/seen/`, undefined, "POST", {
146
+ use_unified_inbox: "true",
147
+ action: "mark_seen",
148
+ });
149
+ }
150
+ async approvePending(threadId) {
151
+ return this.request(`/api/v1/direct_v2/threads/${threadId}/approve/`, undefined, "POST", {});
152
+ }
153
+ async searchUsers(query) {
154
+ return this.request("/api/v1/direct_v2/ranked_recipients/", {
155
+ mode: "raven",
156
+ query,
157
+ show_threads: "true",
158
+ });
159
+ }
160
+ // --- NEW ---
161
+ async createThread(recipientIds, text) {
162
+ return this.request("/api/v1/direct_v2/threads/broadcast/text/", undefined, "POST", {
163
+ recipient_users: JSON.stringify(recipientIds),
164
+ action: "send_item",
165
+ text,
166
+ client_context: String(Date.now()),
167
+ });
168
+ }
169
+ }
170
+ exports.InstagramClient = InstagramClient;
@@ -0,0 +1,149 @@
1
+ export interface InstaUser {
2
+ pk: string;
3
+ username: string;
4
+ full_name: string;
5
+ profile_pic_url: string;
6
+ is_verified?: boolean;
7
+ is_private?: boolean;
8
+ }
9
+ export interface InstaMessageItem {
10
+ item_id: string;
11
+ user_id: string;
12
+ timestamp: number;
13
+ item_type: string;
14
+ text?: string;
15
+ media?: {
16
+ image_versions2?: {
17
+ candidates: {
18
+ url: string;
19
+ }[];
20
+ };
21
+ };
22
+ link?: {
23
+ text: string;
24
+ link_url: string;
25
+ };
26
+ reel_share?: {
27
+ text?: string;
28
+ media?: {
29
+ caption?: {
30
+ text: string;
31
+ };
32
+ };
33
+ };
34
+ voice_media?: {
35
+ media: {
36
+ audio: {
37
+ audio_src: string;
38
+ };
39
+ };
40
+ };
41
+ clip?: {
42
+ clip: {
43
+ caption?: {
44
+ text: string;
45
+ };
46
+ };
47
+ };
48
+ animated_media?: {
49
+ images?: {
50
+ fixed_height?: {
51
+ url: string;
52
+ };
53
+ };
54
+ };
55
+ media_share?: {
56
+ caption?: {
57
+ text: string;
58
+ };
59
+ image_versions2?: {
60
+ candidates: {
61
+ url: string;
62
+ }[];
63
+ };
64
+ };
65
+ story_share?: {
66
+ title?: string;
67
+ message?: string;
68
+ };
69
+ action_log?: {
70
+ description: string;
71
+ };
72
+ reactions?: {
73
+ emojis?: {
74
+ emoji: string;
75
+ sender_id: string;
76
+ }[];
77
+ };
78
+ is_sent_by_viewer?: boolean;
79
+ reply_to_message?: {
80
+ text?: string;
81
+ item_id: string;
82
+ };
83
+ }
84
+ export interface InstaThread {
85
+ thread_id: string;
86
+ thread_title: string;
87
+ users: InstaUser[];
88
+ items: InstaMessageItem[];
89
+ last_activity_at: number;
90
+ read_state: number;
91
+ has_older: boolean;
92
+ oldest_cursor?: string;
93
+ newest_cursor?: string;
94
+ is_group: boolean;
95
+ pending: boolean;
96
+ archived: boolean;
97
+ muted: boolean;
98
+ named: boolean;
99
+ thread_type?: string;
100
+ inviter?: InstaUser;
101
+ }
102
+ export interface InstaViewer {
103
+ pk: string;
104
+ username: string;
105
+ full_name: string;
106
+ profile_pic_url: string;
107
+ is_verified: boolean;
108
+ }
109
+ export interface InstaInboxResponse {
110
+ viewer: InstaViewer;
111
+ inbox: {
112
+ threads: InstaThread[];
113
+ has_older: boolean;
114
+ oldest_cursor?: string;
115
+ };
116
+ pending_requests_total: number;
117
+ seq_id: number;
118
+ }
119
+ export interface InstaThreadResponse {
120
+ thread: InstaThread;
121
+ }
122
+ export interface InstaUserInfoResponse {
123
+ user: InstaUser & {
124
+ biography?: string;
125
+ follower_count?: number;
126
+ following_count?: number;
127
+ media_count?: number;
128
+ external_url?: string;
129
+ };
130
+ }
131
+ export interface InstaActionResponse {
132
+ status: string;
133
+ payload?: {
134
+ thread_id?: string;
135
+ client_context?: string;
136
+ };
137
+ thread_id?: string;
138
+ }
139
+ export interface InstaRankedRecipient {
140
+ user?: InstaUser;
141
+ thread?: InstaThread;
142
+ }
143
+ export interface InstaSearchUsersResponse {
144
+ ranked_recipients: InstaRankedRecipient[];
145
+ expires: number;
146
+ filtered: boolean;
147
+ request_id: string;
148
+ rank_token: string;
149
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "mcp-instagram-dm",
3
+ "version": "2.1.0",
4
+ "description": "MCP server for Instagram Direct Messages — read inbox, send messages, search conversations, and manage DMs through AI assistants",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "mcp-instagram-dm": "dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "start": "node dist/index.js",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "keywords": [
15
+ "mcp",
16
+ "model-context-protocol",
17
+ "instagram",
18
+ "dm",
19
+ "direct-messages",
20
+ "ai",
21
+ "claude",
22
+ "chatgpt",
23
+ "llm",
24
+ "mcp-server",
25
+ "automation"
26
+ ],
27
+ "author": "Kynux",
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/Kynuxdev/mcp-instagram-dm.git"
32
+ },
33
+ "homepage": "https://github.com/Kynuxdev/mcp-instagram-dm#readme",
34
+ "bugs": {
35
+ "url": "https://github.com/Kynuxdev/mcp-instagram-dm/issues"
36
+ },
37
+ "engines": {
38
+ "node": ">=18.0.0"
39
+ },
40
+ "files": [
41
+ "dist/**/*",
42
+ "README.md",
43
+ "LICENSE"
44
+ ],
45
+ "dependencies": {
46
+ "@modelcontextprotocol/sdk": "^1.12.1"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^22.0.0",
50
+ "typescript": "^5.7.0"
51
+ }
52
+ }