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 +21 -0
- package/README.md +231 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +456 -0
- package/dist/instagram.d.ts +23 -0
- package/dist/instagram.js +170 -0
- package/dist/types.d.ts +149 -0
- package/dist/types.js +2 -0
- package/package.json +52 -0
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
|
+
[](https://www.npmjs.com/package/mcp-instagram-dm)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](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)
|
package/dist/index.d.ts
ADDED
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;
|
package/dist/types.d.ts
ADDED
|
@@ -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
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
|
+
}
|