discord-selfbot-mcp 1.2.6 → 1.2.8
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 +26 -6
- package/SKILL.md +2 -6
- package/package.json +1 -1
- package/scripts/daemon.py +109 -9
- package/scripts/dcli.py +96 -4
- package/server.json +2 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Microck
|
|
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
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<img src="https://img.shields.io/badge/license-MIT-green" alt="license">
|
|
13
13
|
<img src="https://img.shields.io/badge/language-python-blue" alt="language">
|
|
14
|
-
<img src="https://img.shields.io/
|
|
14
|
+
<a href="https://www.npmjs.com/package/discord-selfbot-mcp"><img src="https://img.shields.io/npm/v/discord-selfbot-mcp?color=orange&label=npm" alt="npm"></a>
|
|
15
15
|
<img src="https://img.shields.io/badge/mcp-sdk-orange" alt="mcp">
|
|
16
16
|
<img src="https://img.shields.io/badge/skill-cli-purple" alt="skill">
|
|
17
17
|
<a href="https://github.com/Microck/opencode-studio"><img src="https://img.shields.io/badge/opencode-studio-brown?logo=data%3Aimage%2Fpng%3Bbase64%2CiVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAABiElEQVR4nF2Sv0tWcRTGPyeVIpCWwmyJGqQagsqCsL2hhobsD3BvdWhoj/6CiIKaoqXBdMjKRWwQgqZ%2BokSvkIhg9BOT9xPn9Vx79cD3cu6953zP8zznCQB1V0S01d3AKeAKcBVYA94DjyJioru2k9SHE%2Bqc%2Bkd9rL7yf7TUm%2BpQ05yPUM%2Bo626Pp%2BqE2q7GGfWrOpjNnWnAOPAGeAK8Bb4U5D3AJ%2BAQsAAMAHfVvl7gIrAf2Kjiz8BZYB3YC/wFpoGDwHfgEnA0oU7tgHiheEShyXxY/Vn/n6ljye8DcBiYAloRcV3tAdrV1xMRG%2Bo94DywCAwmx33AJHASWK7iiAjzNFOBl7WapPYtYdyo8RlLqVpOVPvq9KoH1NUuOneycaRefqnP1ftdUyiOt5KS%2BqLWdDpVzTXMl5It4Jr6u%2BQ/nhyBc8C7jpowGxGvmxuPqT9qyYuFIKdP71B8WT3SOKexXLrntvqxq3BefaiuFMQ0wqZftxl3M78MjBasfiDN/SAi0kFbtf8ACtKBWZBDoJEAAAAASUVORK5CYII%3D" alt="Add with OpenCode Studio" /></a>
|
|
@@ -190,7 +190,7 @@ powered by the robust `discord.py-self` library.
|
|
|
190
190
|
| **relationships** | 4 | list_friends, send_friend_request, add_friend, remove_friend |
|
|
191
191
|
| **presence** | 2 | set_status, set_activity |
|
|
192
192
|
| **interactions** | 3 | send_slash_command, click_button, select_menu |
|
|
193
|
-
| **threads** | 5 | create_thread, send_thread_message,
|
|
193
|
+
| **threads** | 5 | create_thread, send_thread_message, list_active_threads, read_thread_messages, archive_thread |
|
|
194
194
|
| **members** | 5 | kick_member, ban_member, unban_member, add_role, remove_role |
|
|
195
195
|
| **invites** | 3 | create_invite, list_invites, delete_invite |
|
|
196
196
|
| **profile** | 1 | edit_profile |
|
|
@@ -298,13 +298,13 @@ built-in rate limiting to prevent account bans. configurable via environment var
|
|
|
298
298
|
|
|
299
299
|
| variable | default | description |
|
|
300
300
|
|----------|---------|-------------|
|
|
301
|
-
| `RATE_LIMIT_ENABLED` | `
|
|
301
|
+
| `RATE_LIMIT_ENABLED` | `true` | Enable/disable rate limiting |
|
|
302
302
|
| `RATE_LIMIT_MESSAGES_PER_MINUTE` | `10` | Max messages per minute |
|
|
303
303
|
| `RATE_LIMIT_MESSAGES_PER_SECOND` | `1` | Max messages per second |
|
|
304
304
|
| `RATE_LIMIT_ACTIONS_PER_MINUTE` | `5` | Max actions (joins, etc.) per minute |
|
|
305
305
|
| `RATE_LIMIT_COOLDOWN` | `60` | Cooldown duration when limit hit (seconds) |
|
|
306
306
|
|
|
307
|
-
>
|
|
307
|
+
> rate limiting is enabled by default to reduce ban risk. Only disable it if you are deliberately taking responsibility for raw Discord API pacing yourself.
|
|
308
308
|
|
|
309
309
|
---
|
|
310
310
|
|
|
@@ -329,11 +329,15 @@ discord_py_self_mcp/
|
|
|
329
329
|
├── main.py
|
|
330
330
|
├── setup.py
|
|
331
331
|
├── rate_limiter.py
|
|
332
|
+
├── tool_utils.py
|
|
333
|
+
├── cli_runtime.py
|
|
334
|
+
├── logging_utils.py
|
|
332
335
|
├── captcha/
|
|
333
336
|
│ └── solver.py
|
|
334
337
|
└── tools/
|
|
335
338
|
├── channels.py
|
|
336
339
|
├── discrawl.py
|
|
340
|
+
├── embed.py
|
|
337
341
|
├── guilds.py
|
|
338
342
|
├── interactions.py
|
|
339
343
|
├── invites.py
|
|
@@ -360,7 +364,7 @@ in addition to the mcp server, this package also provides a **skill/cli mode** f
|
|
|
360
364
|
npm install -g discord-selfbot-mcp
|
|
361
365
|
|
|
362
366
|
# create .env file
|
|
363
|
-
echo "DISCORD_TOKEN
|
|
367
|
+
echo "DISCORD_TOKEN=***" > .env
|
|
364
368
|
|
|
365
369
|
# use skill mode (from package directory)
|
|
366
370
|
python3 scripts/dcli.py send-message --channel 123 --content "Hello!"
|
|
@@ -373,6 +377,8 @@ python3 scripts/dcli.py daemon status # check daemon status
|
|
|
373
377
|
python3 scripts/dcli.py send-message --channel CHANNEL_ID --content "Hello"
|
|
374
378
|
python3 scripts/dcli.py list-guilds
|
|
375
379
|
python3 scripts/dcli.py read-messages --channel CHANNEL_ID --limit 20
|
|
380
|
+
python3 scripts/dcli.py get-message-attachments --channel CHANNEL_ID --message MESSAGE_ID
|
|
381
|
+
python3 scripts/dcli.py get-message-attachments --channel CHANNEL_ID --message MESSAGE_ID --download --output-dir ./attachments
|
|
376
382
|
```
|
|
377
383
|
|
|
378
384
|
**when to use skill mode**:
|
|
@@ -387,4 +393,18 @@ see [SKILL.md](SKILL.md) for detailed documentation.
|
|
|
387
393
|
|
|
388
394
|
### license
|
|
389
395
|
|
|
390
|
-
mit
|
|
396
|
+
this project is licensed under the [mit license](./LICENSE).
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
### contributing
|
|
401
|
+
|
|
402
|
+
issues and pull requests are welcome at [github.com/Microck/discord.py-self-mcp](https://github.com/Microck/discord.py-self-mcp).
|
|
403
|
+
|
|
404
|
+
1. fork the repository
|
|
405
|
+
2. create a feature branch (`git checkout -b feature/my-feature`)
|
|
406
|
+
3. commit your changes (`git commit -m 'add my feature'`)
|
|
407
|
+
4. push to the branch (`git push origin feature/my-feature`)
|
|
408
|
+
5. open a pull request
|
|
409
|
+
|
|
410
|
+
please ensure tests pass (`pytest`) before submitting.
|
package/SKILL.md
CHANGED
|
@@ -123,9 +123,6 @@ python3 scripts/dcli.py pin-message --channel CHANNEL_ID --message MESSAGE_ID
|
|
|
123
123
|
# Create thread from message (in text channel)
|
|
124
124
|
python3 scripts/dcli.py create-thread --channel CHANNEL_ID --name "Thread Name" --message MESSAGE_ID
|
|
125
125
|
|
|
126
|
-
# Create standalone thread (in text channel)
|
|
127
|
-
python3 scripts/dcli.py create-thread --channel CHANNEL_ID --name "Thread Name"
|
|
128
|
-
|
|
129
126
|
# Create thread in forum channel (with initial content)
|
|
130
127
|
python3 scripts/dcli.py create-thread --channel FORUM_CHANNEL_ID --name "Thread Name" --content "Initial post content"
|
|
131
128
|
```
|
|
@@ -176,11 +173,10 @@ python3 scripts/dcli.py read-recent-threads --guild GUILD_ID --within 4 --limit-
|
|
|
176
173
|
|
|
177
174
|
#### Get User Info
|
|
178
175
|
```bash
|
|
179
|
-
# Get current user info
|
|
176
|
+
# Get current user info (supported in daemon mode)
|
|
180
177
|
python3 scripts/dcli.py user-info
|
|
181
178
|
|
|
182
|
-
#
|
|
183
|
-
python3 scripts/dcli.py user-info --user USER_ID
|
|
179
|
+
# Specific user lookup is not currently supported in daemon mode
|
|
184
180
|
```
|
|
185
181
|
|
|
186
182
|
#### List Threads in Channel
|
package/package.json
CHANGED
package/scripts/daemon.py
CHANGED
|
@@ -5,6 +5,7 @@ Auto-restart on code change: Enabled
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import asyncio
|
|
8
|
+
import base64
|
|
8
9
|
import hashlib
|
|
9
10
|
import json
|
|
10
11
|
import os
|
|
@@ -17,8 +18,13 @@ from datetime import datetime, timedelta, timezone
|
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
from secrets import compare_digest
|
|
19
20
|
|
|
21
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
22
|
+
PROJECT_ROOT = SCRIPT_DIR.parent
|
|
23
|
+
if str(PROJECT_ROOT) not in sys.path:
|
|
24
|
+
sys.path.insert(0, str(PROJECT_ROOT))
|
|
25
|
+
|
|
20
26
|
env_paths = [
|
|
21
|
-
|
|
27
|
+
PROJECT_ROOT / ".env",
|
|
22
28
|
Path.cwd() / ".env",
|
|
23
29
|
]
|
|
24
30
|
|
|
@@ -29,11 +35,6 @@ for env_path in env_paths:
|
|
|
29
35
|
load_dotenv(env_path)
|
|
30
36
|
break
|
|
31
37
|
|
|
32
|
-
TOKEN = os.getenv("DISCORD_TOKEN")
|
|
33
|
-
if not TOKEN:
|
|
34
|
-
print("Error: DISCORD_TOKEN not found in .env file")
|
|
35
|
-
sys.exit(1)
|
|
36
|
-
|
|
37
38
|
import discord
|
|
38
39
|
|
|
39
40
|
from discord_py_self_mcp.cli_runtime import (
|
|
@@ -46,11 +47,11 @@ from discord_py_self_mcp.cli_runtime import (
|
|
|
46
47
|
)
|
|
47
48
|
from discord_py_self_mcp.logging_utils import log_to_stderr
|
|
48
49
|
from discord_py_self_mcp.tool_utils import NON_MESSAGEABLE_TEXT, validate_message_content
|
|
49
|
-
from discord_py_self_mcp.tools.embed import serialize_message
|
|
50
|
+
from discord_py_self_mcp.tools.embed import serialize_attachment, serialize_message
|
|
50
51
|
|
|
51
|
-
SCRIPT_DIR = Path(__file__).parent
|
|
52
52
|
DAEMON_SCRIPT = SCRIPT_DIR / "daemon.py"
|
|
53
53
|
CHECK_INTERVAL = 2
|
|
54
|
+
MAX_ATTACHMENT_BYTES_DEFAULT = 10 * 1024 * 1024
|
|
54
55
|
|
|
55
56
|
|
|
56
57
|
def _safe_unlink(path: Path) -> None:
|
|
@@ -74,6 +75,15 @@ def _load_or_create_auth_token() -> str:
|
|
|
74
75
|
return token
|
|
75
76
|
|
|
76
77
|
|
|
78
|
+
def _get_token_or_exit() -> str:
|
|
79
|
+
token = os.getenv("DISCORD_TOKEN")
|
|
80
|
+
if token:
|
|
81
|
+
return token
|
|
82
|
+
|
|
83
|
+
print("Error: DISCORD_TOKEN not found in .env file")
|
|
84
|
+
raise SystemExit(1)
|
|
85
|
+
|
|
86
|
+
|
|
77
87
|
class DiscordDaemon:
|
|
78
88
|
def __init__(self):
|
|
79
89
|
self.client = discord.Client()
|
|
@@ -163,6 +173,7 @@ class DiscordDaemon:
|
|
|
163
173
|
|
|
164
174
|
async def connect(self):
|
|
165
175
|
"""Connect to Discord and wait for the initial ready signal."""
|
|
176
|
+
token = _get_token_or_exit()
|
|
166
177
|
|
|
167
178
|
@self.client.event
|
|
168
179
|
async def on_ready():
|
|
@@ -173,7 +184,7 @@ class DiscordDaemon:
|
|
|
173
184
|
async def on_disconnect():
|
|
174
185
|
log_to_stderr(f"[{datetime.now()}] Disconnected from Discord")
|
|
175
186
|
|
|
176
|
-
asyncio.create_task(self.client.start(
|
|
187
|
+
asyncio.create_task(self.client.start(token))
|
|
177
188
|
await asyncio.wait_for(self._connected.wait(), timeout=30)
|
|
178
189
|
|
|
179
190
|
async def handle_command(self, command_data):
|
|
@@ -194,6 +205,14 @@ class DiscordDaemon:
|
|
|
194
205
|
return await self._send_message(
|
|
195
206
|
args.get("channel_id"), args.get("content")
|
|
196
207
|
)
|
|
208
|
+
if cmd == "get_message_attachments":
|
|
209
|
+
return await self._get_message_attachments(
|
|
210
|
+
args.get("channel_id"),
|
|
211
|
+
args.get("message_id"),
|
|
212
|
+
args.get("attachment_index"),
|
|
213
|
+
args.get("download_content", False),
|
|
214
|
+
args.get("max_bytes", MAX_ATTACHMENT_BYTES_DEFAULT),
|
|
215
|
+
)
|
|
197
216
|
if cmd == "list_threads":
|
|
198
217
|
return await self._list_threads(
|
|
199
218
|
args.get("channel_id"), args.get("archived", False)
|
|
@@ -310,6 +329,87 @@ class DiscordDaemon:
|
|
|
310
329
|
message = await channel.send(content)
|
|
311
330
|
return {"message_id": message.id, "success": True}
|
|
312
331
|
|
|
332
|
+
async def _get_message_attachments(
|
|
333
|
+
self,
|
|
334
|
+
channel_id,
|
|
335
|
+
message_id,
|
|
336
|
+
attachment_index=None,
|
|
337
|
+
download_content=False,
|
|
338
|
+
max_bytes=MAX_ATTACHMENT_BYTES_DEFAULT,
|
|
339
|
+
):
|
|
340
|
+
channel = self.client.get_channel(channel_id) or await self.client.fetch_channel(
|
|
341
|
+
channel_id
|
|
342
|
+
)
|
|
343
|
+
if not channel:
|
|
344
|
+
return {"error": "Channel not found"}
|
|
345
|
+
if not isinstance(channel, discord.abc.Messageable):
|
|
346
|
+
return {"error": NON_MESSAGEABLE_TEXT}
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
message = await channel.fetch_message(message_id)
|
|
350
|
+
except discord.NotFound:
|
|
351
|
+
return {"error": "Message not found"}
|
|
352
|
+
except discord.Forbidden:
|
|
353
|
+
return {"error": "Access denied to message"}
|
|
354
|
+
|
|
355
|
+
attachments = list(message.attachments)
|
|
356
|
+
if not attachments:
|
|
357
|
+
return {"error": "Message has no attachments"}
|
|
358
|
+
|
|
359
|
+
if attachment_index is not None:
|
|
360
|
+
if attachment_index < 0 or attachment_index >= len(attachments):
|
|
361
|
+
return {
|
|
362
|
+
"error": (
|
|
363
|
+
f"Attachment index {attachment_index} is out of range for "
|
|
364
|
+
f"{len(attachments)} attachment(s)"
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
indexed_attachments = [(attachment_index, attachments[attachment_index])]
|
|
368
|
+
else:
|
|
369
|
+
indexed_attachments = list(enumerate(attachments))
|
|
370
|
+
|
|
371
|
+
result = {
|
|
372
|
+
"message_id": message.id,
|
|
373
|
+
"attachments": [
|
|
374
|
+
{"index": index, **serialize_attachment(attachment)}
|
|
375
|
+
for index, attachment in indexed_attachments
|
|
376
|
+
],
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if not download_content:
|
|
380
|
+
return result
|
|
381
|
+
|
|
382
|
+
max_bytes = int(max_bytes)
|
|
383
|
+
downloads = []
|
|
384
|
+
skipped = []
|
|
385
|
+
for index, attachment in indexed_attachments:
|
|
386
|
+
if attachment.size is not None and attachment.size > max_bytes:
|
|
387
|
+
skipped.append(
|
|
388
|
+
{
|
|
389
|
+
"index": index,
|
|
390
|
+
"filename": attachment.filename,
|
|
391
|
+
"reason": f"size={attachment.size} exceeds max_bytes={max_bytes}",
|
|
392
|
+
}
|
|
393
|
+
)
|
|
394
|
+
continue
|
|
395
|
+
|
|
396
|
+
blob = await attachment.read()
|
|
397
|
+
downloads.append(
|
|
398
|
+
{
|
|
399
|
+
"index": index,
|
|
400
|
+
"filename": attachment.filename,
|
|
401
|
+
"content_type": attachment.content_type
|
|
402
|
+
or "application/octet-stream",
|
|
403
|
+
"content_base64": base64.b64encode(blob).decode("ascii"),
|
|
404
|
+
}
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
if downloads:
|
|
408
|
+
result["downloads"] = downloads
|
|
409
|
+
if skipped:
|
|
410
|
+
result["skipped"] = skipped
|
|
411
|
+
return result
|
|
412
|
+
|
|
313
413
|
async def _list_threads(self, channel_id, archived=False):
|
|
314
414
|
channel = self.client.get_channel(channel_id) or await self.client.fetch_channel(
|
|
315
415
|
channel_id
|
package/scripts/dcli.py
CHANGED
|
@@ -6,6 +6,7 @@ Commands:
|
|
|
6
6
|
daemon <start|stop|restart|status> - Manage daemon process
|
|
7
7
|
send-message --channel ID --content "msg"
|
|
8
8
|
read-messages --channel ID [--limit N] [--after TIME]
|
|
9
|
+
get-message-attachments --channel ID --message ID [--attachment-index N] [--download --output-dir DIR]
|
|
9
10
|
list-guilds
|
|
10
11
|
list-channels --guild ID
|
|
11
12
|
list-threads --channel ID [--archived]
|
|
@@ -28,6 +29,7 @@ Time formats for --after:
|
|
|
28
29
|
1704067200 - Unix timestamp
|
|
29
30
|
"""
|
|
30
31
|
|
|
32
|
+
import base64
|
|
31
33
|
import json
|
|
32
34
|
import os
|
|
33
35
|
import socket
|
|
@@ -36,6 +38,11 @@ import sys
|
|
|
36
38
|
import time
|
|
37
39
|
from pathlib import Path
|
|
38
40
|
|
|
41
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
42
|
+
PROJECT_ROOT = SCRIPT_DIR.parent
|
|
43
|
+
if str(PROJECT_ROOT) not in sys.path:
|
|
44
|
+
sys.path.insert(0, str(PROJECT_ROOT))
|
|
45
|
+
|
|
39
46
|
from discord_py_self_mcp.cli_runtime import AUTH_FILE, PID_FILE, SOCKET_PATH
|
|
40
47
|
|
|
41
48
|
|
|
@@ -70,8 +77,7 @@ def start_daemon():
|
|
|
70
77
|
"""Auto-start daemon if not running."""
|
|
71
78
|
if not is_daemon_running():
|
|
72
79
|
print("Starting daemon...")
|
|
73
|
-
|
|
74
|
-
daemon_script = script_dir / "daemon.py"
|
|
80
|
+
daemon_script = SCRIPT_DIR / "daemon.py"
|
|
75
81
|
subprocess.Popen(
|
|
76
82
|
[sys.executable, str(daemon_script), "start"],
|
|
77
83
|
stdout=subprocess.DEVNULL,
|
|
@@ -170,6 +176,74 @@ def format_messages(messages, reverse=True, use_local_timezone=True):
|
|
|
170
176
|
print(f"[{created_at}] {author}: {content}")
|
|
171
177
|
|
|
172
178
|
|
|
179
|
+
def print_attachment_metadata(attachments):
|
|
180
|
+
for attachment in attachments:
|
|
181
|
+
details = [
|
|
182
|
+
f"[Attachment {attachment.get('index', '?')}] {attachment.get('filename', 'unknown')}"
|
|
183
|
+
]
|
|
184
|
+
if attachment.get("content_type"):
|
|
185
|
+
details.append(f"type={attachment['content_type']}")
|
|
186
|
+
if attachment.get("size") is not None:
|
|
187
|
+
details.append(f"size={attachment['size']}")
|
|
188
|
+
if attachment.get("width") and attachment.get("height"):
|
|
189
|
+
details.append(f"dimensions={attachment['width']}x{attachment['height']}")
|
|
190
|
+
if attachment.get("url"):
|
|
191
|
+
details.append(f"url={attachment['url']}")
|
|
192
|
+
print(" ".join(details))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def cmd_get_message_attachments(
|
|
196
|
+
channel_id,
|
|
197
|
+
message_id,
|
|
198
|
+
attachment_index=None,
|
|
199
|
+
download=False,
|
|
200
|
+
output_dir=None,
|
|
201
|
+
max_bytes=None,
|
|
202
|
+
):
|
|
203
|
+
if download and not output_dir:
|
|
204
|
+
print("Error: --output-dir is required when --download is used")
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
args = {"channel_id": channel_id, "message_id": message_id}
|
|
208
|
+
if attachment_index is not None:
|
|
209
|
+
args["attachment_index"] = attachment_index
|
|
210
|
+
if download:
|
|
211
|
+
args["download_content"] = True
|
|
212
|
+
if max_bytes is not None:
|
|
213
|
+
args["max_bytes"] = max_bytes
|
|
214
|
+
|
|
215
|
+
result = send_request({"command": "get_message_attachments", "args": args})
|
|
216
|
+
if "error" in result:
|
|
217
|
+
print(f"Error: {result['error']}")
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
attachments = result.get("attachments", [])
|
|
221
|
+
print(f"Found {len(attachments)} attachment(s) on message {result.get('message_id')}:")
|
|
222
|
+
print("-" * 60)
|
|
223
|
+
print_attachment_metadata(attachments)
|
|
224
|
+
|
|
225
|
+
skipped = result.get("skipped", [])
|
|
226
|
+
if skipped:
|
|
227
|
+
print("\nSkipped downloads:")
|
|
228
|
+
for item in skipped:
|
|
229
|
+
print(
|
|
230
|
+
f" [Attachment {item.get('index', '?')}] {item.get('filename', 'unknown')}: {item.get('reason', 'Unknown reason')}"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
downloads = result.get("downloads", [])
|
|
234
|
+
if not downloads:
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
output_path = Path(output_dir)
|
|
238
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
239
|
+
print(f"\nSaved downloads to {output_path}:")
|
|
240
|
+
for item in downloads:
|
|
241
|
+
filename = item.get("filename") or f"attachment-{item.get('index', 'unknown')}"
|
|
242
|
+
destination = output_path / f"attachment-{item.get('index', 'unknown')}-{filename}"
|
|
243
|
+
destination.write_bytes(base64.b64decode(item["content_base64"]))
|
|
244
|
+
print(f" {destination}")
|
|
245
|
+
|
|
246
|
+
|
|
173
247
|
def cmd_send_message(channel_id, content):
|
|
174
248
|
result = send_request(
|
|
175
249
|
{
|
|
@@ -450,8 +524,7 @@ def cmd_create_thread(channel_id, name, message_id, content=None):
|
|
|
450
524
|
|
|
451
525
|
|
|
452
526
|
def cmd_daemon(action):
|
|
453
|
-
|
|
454
|
-
daemon_script = script_dir / "daemon.py"
|
|
527
|
+
daemon_script = SCRIPT_DIR / "daemon.py"
|
|
455
528
|
|
|
456
529
|
if action == "start":
|
|
457
530
|
if is_daemon_running():
|
|
@@ -487,6 +560,16 @@ def main():
|
|
|
487
560
|
read_parser.add_argument("--limit", "-l", type=int, default=10, help="Number of messages")
|
|
488
561
|
read_parser.add_argument("--after", "-a", type=str, help="Only show messages after this time (e.g. '4h', '30m', '2024-01-01T00:00:00')")
|
|
489
562
|
|
|
563
|
+
attachments_parser = subparsers.add_parser(
|
|
564
|
+
"get-message-attachments", help="Show attachment metadata for a message"
|
|
565
|
+
)
|
|
566
|
+
attachments_parser.add_argument("--channel", "-c", required=True, type=int, help="Channel ID")
|
|
567
|
+
attachments_parser.add_argument("--message", "-m", required=True, type=int, help="Message ID")
|
|
568
|
+
attachments_parser.add_argument("--attachment-index", type=int, help="Optional zero-based attachment index")
|
|
569
|
+
attachments_parser.add_argument("--download", action="store_true", help="Download selected attachments to disk")
|
|
570
|
+
attachments_parser.add_argument("--output-dir", type=str, help="Directory to write downloads into when --download is used")
|
|
571
|
+
attachments_parser.add_argument("--max-bytes", type=int, help="Skip downloads larger than this many bytes")
|
|
572
|
+
|
|
490
573
|
subparsers.add_parser("list-guilds", help="List all guilds")
|
|
491
574
|
|
|
492
575
|
channels_parser = subparsers.add_parser("list-channels", help="List channels in a guild")
|
|
@@ -555,6 +638,15 @@ def main():
|
|
|
555
638
|
cmd_send_message(args.channel, args.content)
|
|
556
639
|
elif args.command == "read-messages":
|
|
557
640
|
cmd_read_messages(args.channel, args.limit, args.after)
|
|
641
|
+
elif args.command == "get-message-attachments":
|
|
642
|
+
cmd_get_message_attachments(
|
|
643
|
+
args.channel,
|
|
644
|
+
args.message,
|
|
645
|
+
args.attachment_index,
|
|
646
|
+
args.download,
|
|
647
|
+
args.output_dir,
|
|
648
|
+
args.max_bytes,
|
|
649
|
+
)
|
|
558
650
|
elif args.command == "list-guilds":
|
|
559
651
|
cmd_list_guilds()
|
|
560
652
|
elif args.command == "list-channels":
|
package/server.json
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
"url": "https://github.com/Microck/discord.py-self-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.2.
|
|
9
|
+
"version": "1.2.8",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
14
14
|
"identifier": "discord-selfbot-mcp",
|
|
15
15
|
"runtimeHint": "python",
|
|
16
|
-
"version": "1.2.
|
|
16
|
+
"version": "1.2.8",
|
|
17
17
|
"packageArguments": [],
|
|
18
18
|
"environmentVariables": [
|
|
19
19
|
{
|