@tensakulabs/discord-mcp 0.1.7 → 0.1.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/dist/cli.js +142 -2
- package/dist/index.js +33 -3
- package/dist/tools/resolve_dm_user.js +24 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { createInterface } from "readline";
|
|
4
4
|
import { saveToken, getToken } from "./auth.js";
|
|
5
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, rmSync } from "fs";
|
|
6
6
|
import { execSync } from "child_process";
|
|
7
7
|
import { homedir, platform } from "os";
|
|
8
8
|
import { join, dirname } from "path";
|
|
@@ -10,7 +10,7 @@ const program = new Command();
|
|
|
10
10
|
program
|
|
11
11
|
.name("discord-mcp")
|
|
12
12
|
.description("Discord selfbot MCP server for Claude")
|
|
13
|
-
.version("0.1.
|
|
13
|
+
.version("0.1.8");
|
|
14
14
|
program
|
|
15
15
|
.command("setup")
|
|
16
16
|
.description("Configure Discord token and register MCP server")
|
|
@@ -176,6 +176,146 @@ program
|
|
|
176
176
|
process.exit(1);
|
|
177
177
|
}
|
|
178
178
|
});
|
|
179
|
+
program
|
|
180
|
+
.command("migrate-account")
|
|
181
|
+
.description("Migrate the default account to a named account")
|
|
182
|
+
.requiredOption("--to <name>", "New account name (e.g. personal, work)")
|
|
183
|
+
.option("--cleanup", "Delete original default files after migration", false)
|
|
184
|
+
.action(async (opts) => {
|
|
185
|
+
const to = opts.to.trim();
|
|
186
|
+
if (!to || to === "default") {
|
|
187
|
+
console.error("❌ Invalid account name. Choose a name other than 'default'.");
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
const CONFIG_DIR = join(homedir(), ".config", "discord-mcp");
|
|
191
|
+
const targetDir = join(CONFIG_DIR, to);
|
|
192
|
+
// ISC-A1: Guard — target must not already exist
|
|
193
|
+
if (existsSync(targetDir)) {
|
|
194
|
+
console.error(`❌ Account '${to}' already exists at ${targetDir}. Aborting.`);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
// ISC-A2: Guard — default token must exist
|
|
198
|
+
let token;
|
|
199
|
+
try {
|
|
200
|
+
token = await getToken("default");
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
console.error("❌ No token found for default account. Nothing to migrate.");
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
console.log(`\nMigrating default → ${to}...\n`);
|
|
207
|
+
// 1. Copy token to new keychain slot
|
|
208
|
+
await saveToken(token, to);
|
|
209
|
+
console.log(`✅ Token copied to keychain slot: user-token-${to}`);
|
|
210
|
+
// 2. Copy data files
|
|
211
|
+
mkdirSync(targetDir, { recursive: true, mode: 0o700 });
|
|
212
|
+
const filesToCopy = ["messages.db", "config.json", "state.json"];
|
|
213
|
+
for (const file of filesToCopy) {
|
|
214
|
+
const src = join(CONFIG_DIR, file);
|
|
215
|
+
const dst = join(targetDir, file);
|
|
216
|
+
if (existsSync(src)) {
|
|
217
|
+
copyFileSync(src, dst);
|
|
218
|
+
console.log(`✅ Copied ${file} → ${to}/${file}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// 3. Update settings.json MCP registration
|
|
222
|
+
const claudeConfigPath = join(homedir(), ".claude", "settings.json");
|
|
223
|
+
if (existsSync(claudeConfigPath)) {
|
|
224
|
+
const config = JSON.parse(readFileSync(claudeConfigPath, "utf8"));
|
|
225
|
+
config.mcpServers ??= {};
|
|
226
|
+
if (config.mcpServers["discord"]) {
|
|
227
|
+
config.mcpServers[`discord-${to}`] = {
|
|
228
|
+
command: "npx",
|
|
229
|
+
args: ["-y", "@tensakulabs/discord-mcp", "mcp", "--account", to],
|
|
230
|
+
};
|
|
231
|
+
delete config.mcpServers["discord"];
|
|
232
|
+
writeFileSync(claudeConfigPath, JSON.stringify(config, null, 2));
|
|
233
|
+
console.log(`✅ Claude config: renamed MCP key "discord" → "discord-${to}"`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// 4. Re-register launchd daemon (macOS)
|
|
237
|
+
if (platform() === "darwin") {
|
|
238
|
+
const launchAgentsDir = join(homedir(), "Library", "LaunchAgents");
|
|
239
|
+
const oldPlistPath = join(launchAgentsDir, "com.discord-mcp.daemon.plist");
|
|
240
|
+
const newLabel = `com.discord-mcp.daemon.${to}`;
|
|
241
|
+
const newPlistPath = join(launchAgentsDir, `${newLabel}.plist`);
|
|
242
|
+
if (existsSync(oldPlistPath)) {
|
|
243
|
+
try {
|
|
244
|
+
execSync(`launchctl unload "${oldPlistPath}" 2>/dev/null`);
|
|
245
|
+
}
|
|
246
|
+
catch { /* ok */ }
|
|
247
|
+
const npxPath = (() => { try {
|
|
248
|
+
return execSync("which npx").toString().trim();
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return "/usr/local/bin/npx";
|
|
252
|
+
} })();
|
|
253
|
+
const nodeBinDir = (() => { try {
|
|
254
|
+
return dirname(execSync("which node").toString().trim());
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return "/usr/local/bin";
|
|
258
|
+
} })();
|
|
259
|
+
const logDir = join(homedir(), ".config", "discord-mcp", to);
|
|
260
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
261
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
262
|
+
<plist version="1.0">
|
|
263
|
+
<dict>
|
|
264
|
+
<key>Label</key>
|
|
265
|
+
<string>${newLabel}</string>
|
|
266
|
+
<key>ProgramArguments</key>
|
|
267
|
+
<array>
|
|
268
|
+
<string>${npxPath}</string>
|
|
269
|
+
<string>@tensakulabs/discord-mcp</string>
|
|
270
|
+
<string>daemon-start</string>
|
|
271
|
+
<string>--account</string>
|
|
272
|
+
<string>${to}</string>
|
|
273
|
+
</array>
|
|
274
|
+
<key>EnvironmentVariables</key>
|
|
275
|
+
<dict>
|
|
276
|
+
<key>PATH</key>
|
|
277
|
+
<string>${nodeBinDir}:/usr/local/bin:/usr/bin:/bin</string>
|
|
278
|
+
</dict>
|
|
279
|
+
<key>RunAtLoad</key>
|
|
280
|
+
<true/>
|
|
281
|
+
<key>KeepAlive</key>
|
|
282
|
+
<true/>
|
|
283
|
+
<key>StandardOutPath</key>
|
|
284
|
+
<string>${logDir}/daemon.log</string>
|
|
285
|
+
<key>StandardErrorPath</key>
|
|
286
|
+
<string>${logDir}/daemon.log</string>
|
|
287
|
+
</dict>
|
|
288
|
+
</plist>`;
|
|
289
|
+
writeFileSync(newPlistPath, plist);
|
|
290
|
+
try {
|
|
291
|
+
execSync(`launchctl load "${newPlistPath}"`);
|
|
292
|
+
console.log(`✅ Daemon re-registered: ${newLabel} (running now)`);
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
console.log(`✅ Plist written: ${newPlistPath} (run: launchctl load "${newPlistPath}")`);
|
|
296
|
+
}
|
|
297
|
+
if (opts.cleanup) {
|
|
298
|
+
rmSync(oldPlistPath, { force: true });
|
|
299
|
+
console.log(`🗑 Removed old plist: com.discord-mcp.daemon.plist`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// 5. Cleanup original default files if requested
|
|
304
|
+
if (opts.cleanup) {
|
|
305
|
+
for (const file of filesToCopy) {
|
|
306
|
+
const src = join(CONFIG_DIR, file);
|
|
307
|
+
if (existsSync(src)) {
|
|
308
|
+
rmSync(src, { force: true });
|
|
309
|
+
console.log(`🗑 Removed original: ${file}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
console.log("\n✅ Cleanup complete. Default account data removed.");
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
console.log("\nℹ️ Original default files preserved. Run with --cleanup to remove them.");
|
|
316
|
+
}
|
|
317
|
+
console.log(`\n🎉 Migration complete! Restart Claude Code to use "discord-${to}" MCP.`);
|
|
318
|
+
});
|
|
179
319
|
program
|
|
180
320
|
.command("list-accounts")
|
|
181
321
|
.description("List all configured Discord accounts")
|
package/dist/index.js
CHANGED
|
@@ -8,8 +8,12 @@ import { getDMChannels } from "./tools/get_dms.js";
|
|
|
8
8
|
import { sendMessage } from "./tools/send_message.js";
|
|
9
9
|
import { getUnread } from "./tools/get_unread.js";
|
|
10
10
|
import { searchDiscord } from "./tools/search.js";
|
|
11
|
+
import { resolveDmUser } from "./tools/resolve_dm_user.js";
|
|
11
12
|
const ACCOUNT = process.env.DISCORD_MCP_ACCOUNT ?? "default";
|
|
12
|
-
|
|
13
|
+
// Each account gets its own MCP server name → unique tool namespace in Claude Code.
|
|
14
|
+
// default → "discord", work → "discord-work", suntory → "discord-suntory"
|
|
15
|
+
const SERVER_NAME = ACCOUNT === "default" ? "discord" : `discord-${ACCOUNT}`;
|
|
16
|
+
const server = new Server({ name: SERVER_NAME, version: "0.1.8" }, { capabilities: { tools: {} } });
|
|
13
17
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
14
18
|
tools: [
|
|
15
19
|
{
|
|
@@ -47,6 +51,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
47
51
|
description: "List the user's open DM conversations.",
|
|
48
52
|
inputSchema: { type: "object", properties: {}, required: [] },
|
|
49
53
|
},
|
|
54
|
+
{
|
|
55
|
+
name: "discord_resolve_dm_user",
|
|
56
|
+
description: "Resolve a username or display name to a DM channel ID. Use this before discord_get_messages when you know who to DM but not the channel ID.",
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: "object",
|
|
59
|
+
properties: {
|
|
60
|
+
username: { type: "string", description: "Display name or username to look up (case-insensitive, partial match supported)" },
|
|
61
|
+
},
|
|
62
|
+
required: ["username"],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
50
65
|
{
|
|
51
66
|
name: "discord_send_message",
|
|
52
67
|
description: "Send a message to a channel or user (DM). Specify channelId OR userId.",
|
|
@@ -120,6 +135,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
120
135
|
case "discord_get_dms":
|
|
121
136
|
result = await getDMChannels(ACCOUNT);
|
|
122
137
|
break;
|
|
138
|
+
case "discord_resolve_dm_user":
|
|
139
|
+
result = await resolveDmUser(args.username, ACCOUNT);
|
|
140
|
+
break;
|
|
123
141
|
case "discord_send_message":
|
|
124
142
|
result = await sendMessage(args, ACCOUNT);
|
|
125
143
|
break;
|
|
@@ -137,8 +155,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
137
155
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
138
156
|
}
|
|
139
157
|
catch (err) {
|
|
140
|
-
const
|
|
141
|
-
|
|
158
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
159
|
+
const isTokenMissing = detail.includes("No Discord token found");
|
|
160
|
+
const errorPayload = {
|
|
161
|
+
error_code: isTokenMissing ? "TOKEN_MISSING" : "TOOL_ERROR",
|
|
162
|
+
account: ACCOUNT,
|
|
163
|
+
remedy: isTokenMissing
|
|
164
|
+
? `Run: npx @tensakulabs/discord-mcp setup${ACCOUNT !== "default" ? ` --account ${ACCOUNT}` : ""}`
|
|
165
|
+
: "Check daemon status: npx @tensakulabs/discord-mcp status",
|
|
166
|
+
detail,
|
|
167
|
+
};
|
|
168
|
+
return {
|
|
169
|
+
content: [{ type: "text", text: JSON.stringify(errorPayload, null, 2) }],
|
|
170
|
+
isError: true,
|
|
171
|
+
};
|
|
142
172
|
}
|
|
143
173
|
});
|
|
144
174
|
const transport = new StdioServerTransport();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getDMChannels } from "./get_dms.js";
|
|
2
|
+
export async function resolveDmUser(username, account = "default") {
|
|
3
|
+
const dms = await getDMChannels(account);
|
|
4
|
+
const query = username.toLowerCase().trim();
|
|
5
|
+
// Try exact match first, then partial
|
|
6
|
+
let match = dms.find(dm => dm.with.toLowerCase() === query);
|
|
7
|
+
if (!match) {
|
|
8
|
+
match = dms.find(dm => dm.with.toLowerCase().includes(query));
|
|
9
|
+
}
|
|
10
|
+
if (!match) {
|
|
11
|
+
return {
|
|
12
|
+
error_code: "DM_NOT_FOUND",
|
|
13
|
+
account,
|
|
14
|
+
query: username,
|
|
15
|
+
remedy: `No open DM found with "${username}". They must have an existing DM conversation.`,
|
|
16
|
+
available: dms.map(d => d.with),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
channelId: match.channelId,
|
|
21
|
+
displayName: match.with,
|
|
22
|
+
matchedQuery: username,
|
|
23
|
+
};
|
|
24
|
+
}
|