@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 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.6");
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
- const server = new Server({ name: "discord-mcp", version: "0.2.0" }, { capabilities: { tools: {} } });
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 message = err instanceof Error ? err.message : String(err);
141
- return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tensakulabs/discord-mcp",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Discord selfbot MCP server — read & send messages via Claude",
5
5
  "license": "MIT",
6
6
  "type": "module",