@tensakulabs/discord-mcp 0.1.4 → 0.1.7

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/auth.js CHANGED
@@ -4,22 +4,34 @@ import { join } from "path";
4
4
  import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
5
5
  import { execSync } from "child_process";
6
6
  const SERVICE = "discord-mcp";
7
- const ACCOUNT = "user-token";
8
7
  const CONFIG_DIR = join(homedir(), ".config", "discord-mcp");
9
- const TOKEN_FILE = join(CONFIG_DIR, "token.enc");
10
- const KEY_FILE = join(CONFIG_DIR, "key.bin");
8
+ function keychainAccount(account) {
9
+ return account === "default" ? "user-token" : `user-token-${account}`;
10
+ }
11
+ function tokenFilePath(account) {
12
+ if (account === "default")
13
+ return join(CONFIG_DIR, "token.enc");
14
+ return join(CONFIG_DIR, account, "token.enc");
15
+ }
16
+ function keyFilePath(account) {
17
+ if (account === "default")
18
+ return join(CONFIG_DIR, "key.bin");
19
+ return join(CONFIG_DIR, account, "key.bin");
20
+ }
11
21
  // macOS: use built-in `security` CLI — no native module compilation required
12
- function saveMacOSKeychain(token) {
22
+ function saveMacOSKeychain(token, account) {
23
+ const acct = keychainAccount(account);
13
24
  // Delete existing entry first to avoid "already exists" error
14
25
  try {
15
- execSync(`security delete-generic-password -s "${SERVICE}" -a "${ACCOUNT}" 2>/dev/null`);
26
+ execSync(`security delete-generic-password -s "${SERVICE}" -a "${acct}" 2>/dev/null`);
16
27
  }
17
28
  catch { /* not found, ok */ }
18
- execSync(`security add-generic-password -s "${SERVICE}" -a "${ACCOUNT}" -w "${token}"`);
29
+ execSync(`security add-generic-password -s "${SERVICE}" -a "${acct}" -w "${token}"`);
19
30
  }
20
- function getMacOSKeychain() {
31
+ function getMacOSKeychain(account) {
32
+ const acct = keychainAccount(account);
21
33
  try {
22
- return execSync(`security find-generic-password -s "${SERVICE}" -a "${ACCOUNT}" -w 2>/dev/null`).toString().trim() || null;
34
+ return execSync(`security find-generic-password -s "${SERVICE}" -a "${acct}" -w 2>/dev/null`).toString().trim() || null;
23
35
  }
24
36
  catch {
25
37
  return null;
@@ -37,55 +49,64 @@ async function getKeytar() {
37
49
  }
38
50
  return _keytar;
39
51
  }
40
- export async function saveToken(token) {
52
+ export async function saveToken(token, account = "default") {
41
53
  // macOS: security CLI always works — no native module needed
42
54
  if (platform() === "darwin") {
43
- saveMacOSKeychain(token);
55
+ saveMacOSKeychain(token, account);
44
56
  return;
45
57
  }
46
58
  // Other platforms: try keytar
47
59
  const keytar = await getKeytar();
60
+ const acct = keychainAccount(account);
48
61
  try {
49
62
  if (!keytar)
50
63
  throw new Error("keytar not available");
51
- await keytar.setPassword(SERVICE, ACCOUNT, token);
64
+ await keytar.setPassword(SERVICE, acct, token);
52
65
  return;
53
66
  }
54
67
  catch { /* fall through */ }
55
68
  // Fallback: encrypt to file (for headless/CI environments)
56
69
  console.warn("⚠️ Keychain unavailable, using encrypted file fallback.");
70
+ const tokenFile = tokenFilePath(account);
71
+ const keyFile = keyFilePath(account);
57
72
  mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
73
+ if (account !== "default")
74
+ mkdirSync(join(CONFIG_DIR, account), { recursive: true, mode: 0o700 });
58
75
  let key;
59
- if (existsSync(KEY_FILE)) {
60
- key = readFileSync(KEY_FILE);
76
+ if (existsSync(keyFile)) {
77
+ key = readFileSync(keyFile);
61
78
  }
62
79
  else {
63
80
  key = randomBytes(32);
64
- writeFileSync(KEY_FILE, key, { mode: 0o600 });
81
+ writeFileSync(keyFile, key, { mode: 0o600 });
65
82
  }
66
83
  const iv = randomBytes(16);
67
84
  const cipher = createCipheriv("aes-256-cbc", key, iv);
68
85
  const enc = Buffer.concat([cipher.update(token, "utf8"), cipher.final()]);
69
- writeFileSync(TOKEN_FILE, Buffer.concat([iv, enc]), { mode: 0o600 });
86
+ writeFileSync(tokenFile, Buffer.concat([iv, enc]), { mode: 0o600 });
70
87
  }
71
- export async function getToken() {
88
+ export async function getToken(account = "default") {
72
89
  // macOS: try security CLI first
73
90
  if (platform() === "darwin") {
74
- const token = getMacOSKeychain();
91
+ const token = getMacOSKeychain(account);
75
92
  if (token)
76
93
  return token;
77
94
  }
78
95
  // Try keytar
79
96
  const keytar = await getKeytar();
80
- const keychainToken = keytar ? await keytar.getPassword(SERVICE, ACCOUNT).catch(() => null) : null;
97
+ const acct = keychainAccount(account);
98
+ const keychainToken = keytar ? await keytar.getPassword(SERVICE, acct).catch(() => null) : null;
81
99
  if (keychainToken)
82
100
  return keychainToken;
83
101
  // Fallback: encrypted file
84
- if (!existsSync(TOKEN_FILE) || !existsSync(KEY_FILE)) {
85
- throw new Error("No Discord token found. Run: npx @tensakulabs/discord-mcp setup");
102
+ const tokenFile = tokenFilePath(account);
103
+ const keyFile = keyFilePath(account);
104
+ if (!existsSync(tokenFile) || !existsSync(keyFile)) {
105
+ const flag = account === "default" ? "" : ` --account ${account}`;
106
+ throw new Error(`No Discord token found. Run: npx @tensakulabs/discord-mcp setup${flag}`);
86
107
  }
87
- const key = readFileSync(KEY_FILE);
88
- const raw = readFileSync(TOKEN_FILE);
108
+ const key = readFileSync(keyFile);
109
+ const raw = readFileSync(tokenFile);
89
110
  const iv = raw.slice(0, 16);
90
111
  const enc = raw.slice(16);
91
112
  const decipher = createDecipheriv("aes-256-cbc", key, iv);
package/dist/cli.js CHANGED
@@ -10,14 +10,17 @@ 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.0");
13
+ .version("0.1.6");
14
14
  program
15
15
  .command("setup")
16
16
  .description("Configure Discord token and register MCP server")
17
- .action(async () => {
17
+ .option("--account <name>", "Account name (e.g. work, personal)", "default")
18
+ .action(async (opts) => {
19
+ const account = opts.account;
20
+ const accountLabel = account === "default" ? "" : ` [${account}]`;
18
21
  console.log(`
19
22
  ╔══════════════════════════════════════════════════╗
20
- ║ discord-mcp setup
23
+ ║ discord-mcp setup${accountLabel.padEnd(22)}
21
24
  ╚══════════════════════════════════════════════════╝
22
25
 
23
26
  Step 1: Extract your Discord token
@@ -32,24 +35,23 @@ Step 1: Extract your Discord token
32
35
 
33
36
  `);
34
37
  const rl = createInterface({ input: process.stdin, output: process.stdout });
35
- const token = await new Promise(resolve => {
36
- rl.question("Paste your Discord token: ", ans => { rl.close(); resolve(ans.trim()); });
38
+ const token = await new Promise(res => {
39
+ rl.question("Paste your Discord token: ", ans => { rl.close(); res(ans.trim()); });
37
40
  });
38
41
  if (!token || token.length < 20) {
39
42
  console.error("❌ Invalid token. Try again.");
40
43
  process.exit(1);
41
44
  }
42
- await saveToken(token);
45
+ await saveToken(token, account);
43
46
  console.log("✅ Token saved securely to OS keychain (or encrypted file fallback).");
44
- // Auto-patch Claude Code settings.json
45
- const configPaths = [
46
- join(homedir(), ".claude", "settings.json"), // Claude Code (all platforms)
47
- ];
48
- const configPath = configPaths.find(existsSync);
49
- // Write launchd plist (macOS only) — ISC-D6
47
+ // MCP key and daemon label — namespaced for non-default accounts
48
+ const mcpKey = account === "default" ? "discord" : `discord-${account}`;
49
+ const daemonLabel = account === "default" ? "com.discord-mcp.daemon" : `com.discord-mcp.daemon.${account}`;
50
+ const accountArgs = account === "default" ? [] : ["--account", account];
51
+ // Write launchd plist (macOS only)
50
52
  if (platform() === "darwin") {
51
53
  const launchAgentsDir = join(homedir(), "Library", "LaunchAgents");
52
- const plistPath = join(launchAgentsDir, "com.discord-mcp.daemon.plist");
54
+ const plistPath = join(launchAgentsDir, `${daemonLabel}.plist`);
53
55
  const npxPath = (() => { try {
54
56
  return execSync("which npx").toString().trim();
55
57
  }
@@ -62,18 +64,20 @@ Step 1: Extract your Discord token
62
64
  catch {
63
65
  return "/usr/local/bin";
64
66
  } })();
65
- const logDir = join(homedir(), ".config", "discord-mcp");
67
+ const logDir = account === "default"
68
+ ? join(homedir(), ".config", "discord-mcp")
69
+ : join(homedir(), ".config", "discord-mcp", account);
70
+ const daemonCmdArgs = [npxPath, "@tensakulabs/discord-mcp", "daemon-start", ...accountArgs];
71
+ const plistArgs = daemonCmdArgs.map(a => ` <string>${a}</string>`).join("\n");
66
72
  const plist = `<?xml version="1.0" encoding="UTF-8"?>
67
73
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
68
74
  <plist version="1.0">
69
75
  <dict>
70
76
  <key>Label</key>
71
- <string>com.discord-mcp.daemon</string>
77
+ <string>${daemonLabel}</string>
72
78
  <key>ProgramArguments</key>
73
79
  <array>
74
- <string>${npxPath}</string>
75
- <string>@tensakulabs/discord-mcp</string>
76
- <string>daemon-start</string>
80
+ ${plistArgs}
77
81
  </array>
78
82
  <key>EnvironmentVariables</key>
79
83
  <dict>
@@ -91,61 +95,75 @@ Step 1: Extract your Discord token
91
95
  </dict>
92
96
  </plist>`;
93
97
  mkdirSync(launchAgentsDir, { recursive: true });
98
+ mkdirSync(logDir, { recursive: true, mode: 0o700 });
94
99
  writeFileSync(plistPath, plist);
95
100
  try {
96
101
  execSync(`launchctl unload "${plistPath}" 2>/dev/null; launchctl load "${plistPath}"`);
97
- console.log(`✅ Daemon registered: com.discord-mcp.daemon (starts at login, running now)`);
102
+ console.log(`✅ Daemon registered: ${daemonLabel} (starts at login, running now)`);
98
103
  }
99
104
  catch {
100
105
  console.log(`✅ Plist written: ${plistPath} (run: launchctl load "${plistPath}")`);
101
106
  }
102
107
  }
108
+ // Auto-patch Claude Code settings.json
109
+ const configPath = [join(homedir(), ".claude", "settings.json")].find(existsSync);
103
110
  if (configPath) {
104
111
  const config = JSON.parse(readFileSync(configPath, "utf8"));
105
112
  config.mcpServers ??= {};
106
- config.mcpServers["discord"] = {
113
+ config.mcpServers[mcpKey] = {
107
114
  command: "npx",
108
- args: ["-y", "@tensakulabs/discord-mcp"],
115
+ args: ["-y", "@tensakulabs/discord-mcp", "mcp", ...accountArgs],
109
116
  };
110
117
  writeFileSync(configPath, JSON.stringify(config, null, 2));
111
- console.log(`✅ Registered in Claude config: ${configPath}`);
118
+ console.log(`✅ Registered in Claude config as "${mcpKey}": ${configPath}`);
112
119
  console.log("\n🎉 Done! Restart Claude Code to start using Discord tools.");
113
120
  }
114
121
  else {
122
+ const extraArgs = accountArgs.length ? `, ${accountArgs.map(a => `"${a}"`).join(", ")}` : "";
115
123
  console.log(`
116
124
  ⚠️ Could not find Claude Code config automatically.
117
125
 
118
126
  Add this to your ~/.claude/settings.json manually:
119
127
 
120
128
  "mcpServers": {
121
- "discord": {
129
+ "${mcpKey}": {
122
130
  "command": "npx",
123
- "args": ["-y", "@tensakulabs/discord-mcp"]
131
+ "args": ["-y", "@tensakulabs/discord-mcp", "mcp"${extraArgs}]
124
132
  }
125
133
  }
126
134
  `);
127
135
  }
128
136
  });
137
+ program
138
+ .command("mcp")
139
+ .description("Start the MCP server (stdio transport — used by Claude Code)")
140
+ .option("--account <name>", "Account name", "default")
141
+ .action(async (opts) => {
142
+ process.env.DISCORD_MCP_ACCOUNT = opts.account;
143
+ await import("./index.js");
144
+ });
129
145
  program
130
146
  .command("daemon-start")
131
147
  .description("Start the daemon (called by launchd — not intended for direct use)")
132
- .action(async () => {
133
- // Dynamically import daemon to start it
148
+ .option("--account <name>", "Account name", "default")
149
+ .action(async (opts) => {
150
+ process.env.DISCORD_MCP_ACCOUNT = opts.account;
134
151
  await import("./daemon.js");
135
152
  });
136
153
  program
137
154
  .command("status")
138
155
  .description("Check if token is set and valid")
139
- .action(async () => {
156
+ .option("--account <name>", "Account name", "default")
157
+ .action(async (opts) => {
140
158
  try {
141
- const token = await getToken();
142
- // Validate token with a lightweight API call
159
+ const token = await getToken(opts.account);
143
160
  const res = await fetch("https://discord.com/api/v10/users/@me", {
144
161
  headers: { Authorization: token },
145
162
  });
146
163
  if (res.ok) {
147
164
  const user = await res.json();
148
- console.log(`✅ Connected as: ${user.username}#${user.discriminator}`);
165
+ const label = opts.account === "default" ? "" : ` [${opts.account}]`;
166
+ console.log(`✅ Connected as${label}: ${user.username}#${user.discriminator}`);
149
167
  }
150
168
  else {
151
169
  console.error(`❌ Token invalid: HTTP ${res.status}`);
@@ -153,10 +171,34 @@ program
153
171
  }
154
172
  }
155
173
  catch {
156
- console.error(" No token found. Run: npx @tensakulabs/discord-mcp setup");
174
+ const flag = opts.account === "default" ? "" : ` --account ${opts.account}`;
175
+ console.error(`❌ No token found. Run: npx @tensakulabs/discord-mcp setup${flag}`);
157
176
  process.exit(1);
158
177
  }
159
178
  });
179
+ program
180
+ .command("list-accounts")
181
+ .description("List all configured Discord accounts")
182
+ .action(() => {
183
+ const configPath = join(homedir(), ".claude", "settings.json");
184
+ if (!existsSync(configPath)) {
185
+ console.log("No Claude config found.");
186
+ return;
187
+ }
188
+ const config = JSON.parse(readFileSync(configPath, "utf8"));
189
+ const servers = Object.entries(config.mcpServers ?? {})
190
+ .filter(([k]) => k === "discord" || k.startsWith("discord-"));
191
+ if (servers.length === 0) {
192
+ console.log("No Discord accounts configured. Run: npx @tensakulabs/discord-mcp setup");
193
+ return;
194
+ }
195
+ console.log("Configured Discord accounts:");
196
+ for (const [key, val] of servers) {
197
+ const idx = val.args?.indexOf("--account") ?? -1;
198
+ const name = idx >= 0 && val.args ? (val.args[idx + 1] ?? "default") : "default";
199
+ console.log(` ${key} → account: ${name}`);
200
+ }
201
+ });
160
202
  // Default: no subcommand → run MCP server
161
203
  program.action(async () => {
162
204
  await import("./index.js");
package/dist/config.js CHANGED
@@ -2,7 +2,11 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
2
2
  import { homedir } from "os";
3
3
  import { join } from "path";
4
4
  const CONFIG_DIR = join(homedir(), ".config", "discord-mcp");
5
- const CONFIG_FILE = join(CONFIG_DIR, "config.json");
5
+ function configFilePath(account) {
6
+ return account === "default"
7
+ ? join(CONFIG_DIR, "config.json")
8
+ : join(CONFIG_DIR, account, "config.json");
9
+ }
6
10
  const DEFAULTS = {
7
11
  retention: {
8
12
  guild_channels: 30,
@@ -17,11 +21,12 @@ const DEFAULTS = {
17
21
  on_message: [],
18
22
  },
19
23
  };
20
- export function loadConfig() {
21
- if (!existsSync(CONFIG_FILE))
24
+ export function loadConfig(account = "default") {
25
+ const configFile = configFilePath(account);
26
+ if (!existsSync(configFile))
22
27
  return DEFAULTS;
23
28
  try {
24
- const raw = JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
29
+ const raw = JSON.parse(readFileSync(configFile, "utf8"));
25
30
  return {
26
31
  retention: { ...DEFAULTS.retention, ...(raw.retention ?? {}) },
27
32
  hooks: {
@@ -36,9 +41,12 @@ export function loadConfig() {
36
41
  return DEFAULTS;
37
42
  }
38
43
  }
39
- export function writeDefaultConfig() {
44
+ export function writeDefaultConfig(account = "default") {
45
+ const configFile = configFilePath(account);
40
46
  mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
41
- if (!existsSync(CONFIG_FILE)) {
42
- writeFileSync(CONFIG_FILE, JSON.stringify(DEFAULTS, null, 2));
47
+ if (account !== "default")
48
+ mkdirSync(join(CONFIG_DIR, account), { recursive: true, mode: 0o700 });
49
+ if (!existsSync(configFile)) {
50
+ writeFileSync(configFile, JSON.stringify(DEFAULTS, null, 2));
43
51
  }
44
52
  }
package/dist/daemon.js CHANGED
@@ -12,6 +12,7 @@ import { loadConfig } from "./config.js";
12
12
  import { fireHooks } from "./hooks.js";
13
13
  const GATEWAY_URL = "wss://gateway.discord.gg/?v=10&encoding=json";
14
14
  const IDENTIFY_INTENTS = (1 << 0) | (1 << 9) | (1 << 12); // GUILDS | GUILD_MESSAGES | MESSAGE_CONTENT
15
+ const ACCOUNT = process.env.DISCORD_MCP_ACCOUNT ?? "default";
15
16
  let ws = null;
16
17
  let heartbeatInterval = null;
17
18
  let sequence = null;
@@ -19,10 +20,10 @@ let sessionId = null;
19
20
  let resumeGatewayUrl = null;
20
21
  let reconnectDelay = 1000;
21
22
  let selfId = null; // set from READY event
22
- const db = initDb();
23
- schedulePurge();
23
+ const db = initDb(ACCOUNT);
24
+ schedulePurge(ACCOUNT);
24
25
  async function connect(resume = false) {
25
- const token = await getToken();
26
+ const token = await getToken(ACCOUNT);
26
27
  const url = resume && resumeGatewayUrl ? resumeGatewayUrl : GATEWAY_URL;
27
28
  ws = new WebSocket(url);
28
29
  ws.on("open", () => {
@@ -116,7 +117,7 @@ function handleEvent(type, data) {
116
117
  mention_type: mentionType,
117
118
  });
118
119
  // Fire hooks
119
- const cfg = loadConfig();
120
+ const cfg = loadConfig(ACCOUNT);
120
121
  const ctx = {
121
122
  author: msg.author.username,
122
123
  content: msg.content,
package/dist/db.js CHANGED
@@ -3,27 +3,36 @@ import { mkdirSync, existsSync } from "fs";
3
3
  import { homedir } from "os";
4
4
  import { join } from "path";
5
5
  const CONFIG_DIR = join(homedir(), ".config", "discord-mcp");
6
- const DB_FILE = join(CONFIG_DIR, "messages.db");
7
- let _db = null;
8
- export function getDb() {
9
- if (_db)
10
- return _db;
11
- if (!existsSync(DB_FILE))
6
+ function dbFilePath(account) {
7
+ return account === "default"
8
+ ? join(CONFIG_DIR, "messages.db")
9
+ : join(CONFIG_DIR, account, "messages.db");
10
+ }
11
+ const _dbs = {};
12
+ export function getDb(account = "default") {
13
+ if (_dbs[account])
14
+ return _dbs[account];
15
+ const dbFile = dbFilePath(account);
16
+ if (!existsSync(dbFile))
12
17
  return null; // ISC-A5: graceful miss
13
18
  try {
14
- _db = new Database(DB_FILE);
19
+ const db = new Database(dbFile);
15
20
  // ISC-A4: WAL mode prevents corruption on crash
16
- _db.pragma("journal_mode = WAL");
17
- _db.pragma("synchronous = NORMAL");
18
- return _db;
21
+ db.pragma("journal_mode = WAL");
22
+ db.pragma("synchronous = NORMAL");
23
+ _dbs[account] = db;
24
+ return db;
19
25
  }
20
26
  catch {
21
27
  return null;
22
28
  }
23
29
  }
24
- export function initDb() {
30
+ export function initDb(account = "default") {
31
+ const dbFile = dbFilePath(account);
25
32
  mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
26
- const db = new Database(DB_FILE);
33
+ if (account !== "default")
34
+ mkdirSync(join(CONFIG_DIR, account), { recursive: true, mode: 0o700 });
35
+ const db = new Database(dbFile);
27
36
  db.pragma("journal_mode = WAL");
28
37
  db.pragma("synchronous = NORMAL");
29
38
  db.exec(`
@@ -61,7 +70,7 @@ export function initDb() {
61
70
  if (!cols.includes("mention_type")) {
62
71
  db.exec("ALTER TABLE messages ADD COLUMN mention_type TEXT DEFAULT NULL");
63
72
  }
64
- _db = db;
73
+ _dbs[account] = db;
65
74
  return db;
66
75
  }
67
76
  export function insertMessage(db, msg) {
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ 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
+ const ACCOUNT = process.env.DISCORD_MCP_ACCOUNT ?? "default";
11
12
  const server = new Server({ name: "discord-mcp", version: "0.2.0" }, { capabilities: { tools: {} } });
12
13
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
13
14
  tools: [
@@ -106,28 +107,28 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
106
107
  let result;
107
108
  switch (name) {
108
109
  case "discord_list_guilds":
109
- result = await listGuilds();
110
+ result = await listGuilds(ACCOUNT);
110
111
  break;
111
112
  case "discord_list_channels":
112
- result = await listChannels(args.guildId);
113
+ result = await listChannels(args.guildId, ACCOUNT);
113
114
  break;
114
115
  case "discord_get_messages": {
115
116
  const a = args;
116
- result = await getMessages(a.channelId, a.limit, a.since, a.until);
117
+ result = await getMessages(a.channelId, a.limit, a.since, a.until, ACCOUNT);
117
118
  break;
118
119
  }
119
120
  case "discord_get_dms":
120
- result = await getDMChannels();
121
+ result = await getDMChannels(ACCOUNT);
121
122
  break;
122
123
  case "discord_send_message":
123
- result = await sendMessage(args);
124
+ result = await sendMessage(args, ACCOUNT);
124
125
  break;
125
126
  case "discord_get_unread":
126
- result = await getUnread(args.channels);
127
+ result = await getUnread(args.channels, ACCOUNT);
127
128
  break;
128
129
  case "discord_search": {
129
130
  const a = args;
130
- result = await searchDiscord(a.query, a.channelId, a.since, a.until, a.limit);
131
+ result = await searchDiscord(a.query, a.channelId, a.since, a.until, a.limit, ACCOUNT);
131
132
  break;
132
133
  }
133
134
  default:
package/dist/purge.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { getDb, deleteOlderThan } from "./db.js";
2
2
  import { loadConfig } from "./config.js";
3
- export function runPurge() {
4
- const db = getDb();
3
+ export function runPurge(account = "default") {
4
+ const db = getDb(account);
5
5
  if (!db)
6
6
  return;
7
- const config = loadConfig();
7
+ const config = loadConfig(account);
8
8
  const now = Date.now();
9
9
  const guildCutoff = now - config.retention.guild_channels * 24 * 60 * 60 * 1000;
10
10
  const dmCutoff = now - config.retention.dms * 24 * 60 * 60 * 1000;
@@ -19,7 +19,7 @@ export function runPurge() {
19
19
  deleteOlderThan(db, dmCutoff, true, true);
20
20
  console.error(`[discord-mcp] Purge complete.`);
21
21
  }
22
- export function schedulePurge() {
22
+ export function schedulePurge(account = "default") {
23
23
  // Run at next 2am, then every 24h
24
24
  const now = new Date();
25
25
  const next2am = new Date(now);
@@ -28,7 +28,7 @@ export function schedulePurge() {
28
28
  next2am.setDate(next2am.getDate() + 1);
29
29
  const msUntil2am = next2am.getTime() - now.getTime();
30
30
  setTimeout(() => {
31
- runPurge();
32
- setInterval(runPurge, 24 * 60 * 60 * 1000);
31
+ runPurge(account);
32
+ setInterval(() => runPurge(account), 24 * 60 * 60 * 1000);
33
33
  }, msUntil2am);
34
34
  }
package/dist/state.js CHANGED
@@ -2,47 +2,54 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
2
2
  import { homedir } from "os";
3
3
  import { join } from "path";
4
4
  const CONFIG_DIR = join(homedir(), ".config", "discord-mcp");
5
- const STATE_FILE = join(CONFIG_DIR, "state.json");
6
- function load() {
7
- if (!existsSync(STATE_FILE))
5
+ function stateFilePath(account) {
6
+ return account === "default"
7
+ ? join(CONFIG_DIR, "state.json")
8
+ : join(CONFIG_DIR, account, "state.json");
9
+ }
10
+ function load(account) {
11
+ const stateFile = stateFilePath(account);
12
+ if (!existsSync(stateFile))
8
13
  return {};
9
14
  try {
10
- return JSON.parse(readFileSync(STATE_FILE, "utf8"));
15
+ return JSON.parse(readFileSync(stateFile, "utf8"));
11
16
  }
12
17
  catch {
13
18
  return {};
14
19
  }
15
20
  }
16
- function save(state) {
21
+ function save(state, account) {
17
22
  mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
18
- writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), { mode: 0o600 });
23
+ if (account !== "default")
24
+ mkdirSync(join(CONFIG_DIR, account), { recursive: true, mode: 0o700 });
25
+ writeFileSync(stateFilePath(account), JSON.stringify(state, null, 2), { mode: 0o600 });
19
26
  }
20
- export function getChannelMode(channelId) {
21
- const state = load();
27
+ export function getChannelMode(channelId, account = "default") {
28
+ const state = load(account);
22
29
  const s = state[channelId] ?? { mode: "review" };
23
30
  // Auto-expire
24
31
  if (s.expiresAt && Date.now() > s.expiresAt) {
25
32
  state[channelId] = { ...s, mode: "review", expiresAt: undefined };
26
- save(state);
33
+ save(state, account);
27
34
  return "review";
28
35
  }
29
36
  return s.mode;
30
37
  }
31
- export function setChannelMode(channelId, mode, durationMs) {
32
- const state = load();
38
+ export function setChannelMode(channelId, mode, durationMs, account = "default") {
39
+ const state = load(account);
33
40
  state[channelId] = {
34
41
  ...(state[channelId] ?? {}),
35
42
  mode,
36
43
  expiresAt: durationMs ? Date.now() + durationMs : undefined,
37
44
  };
38
- save(state);
45
+ save(state, account);
39
46
  }
40
- export function markSeen(channelId, timestamp) {
41
- const state = load();
47
+ export function markSeen(channelId, timestamp, account = "default") {
48
+ const state = load(account);
42
49
  state[channelId] = { ...(state[channelId] ?? { mode: "review" }), lastSeen: timestamp };
43
- save(state);
50
+ save(state, account);
44
51
  }
45
- export function getLastSeen(channelId) {
46
- const state = load();
52
+ export function getLastSeen(channelId, account = "default") {
53
+ const state = load(account);
47
54
  return state[channelId]?.lastSeen ?? 0;
48
55
  }
@@ -1,7 +1,7 @@
1
1
  import { getToken } from "../auth.js";
2
2
  import { makeDiscordHeaders, rateLimitedFetch } from "../ratelimit.js";
3
- export async function getDMChannels() {
4
- const token = await getToken();
3
+ export async function getDMChannels(account = "default") {
4
+ const token = await getToken(account);
5
5
  const res = await rateLimitedFetch("https://discord.com/api/v10/users/@me/channels", { headers: makeDiscordHeaders(token) });
6
6
  if (!res.ok)
7
7
  throw new Error(`Discord API error: ${res.status}`);
@@ -4,16 +4,16 @@ import { makeDiscordHeaders, rateLimitedFetch } from "../ratelimit.js";
4
4
  import { markSeen } from "../state.js";
5
5
  import { loadConfig } from "../config.js";
6
6
  export async function getMessages(channelId, limit = 25, since, // ISO date string e.g. "2026-02-25" or "2026-02-25T00:00:00Z"
7
- until) {
7
+ until, account = "default") {
8
8
  const sinceMs = since ? new Date(since).getTime() : undefined;
9
9
  const untilMs = until ? new Date(until).getTime() : undefined;
10
10
  // ISC-D4: SQLite-first
11
- const db = getDb();
11
+ const db = getDb(account);
12
12
  if (db) {
13
13
  const rows = queryMessages(db, channelId, limit, sinceMs, untilMs);
14
14
  if (rows.length > 0) {
15
15
  if (rows.length > 0)
16
- markSeen(channelId, rows[0].timestamp);
16
+ markSeen(channelId, rows[0].timestamp, account);
17
17
  return rows.map(r => ({
18
18
  id: r.id,
19
19
  author: r.author_name,
@@ -23,19 +23,19 @@ until) {
23
23
  }
24
24
  }
25
25
  // ISC-D4: REST fallback on miss
26
- const config = loadConfig();
26
+ const config = loadConfig(account);
27
27
  if (!config.retention.fallback_to_api) {
28
28
  return []; // privacy mode — no REST fallback
29
29
  }
30
- const token = await getToken();
31
- let url = `https://discord.com/api/v10/channels/${channelId}/messages?limit=${Math.min(limit, 100)}`;
30
+ const token = await getToken(account);
31
+ const url = `https://discord.com/api/v10/channels/${channelId}/messages?limit=${Math.min(limit, 100)}`;
32
32
  const res = await rateLimitedFetch(url, { headers: makeDiscordHeaders(token) });
33
33
  if (!res.ok)
34
34
  throw new Error(`Discord API error: ${res.status}`);
35
35
  const messages = await res.json();
36
36
  if (messages.length > 0) {
37
37
  const latestTs = new Date(messages[0].timestamp).getTime();
38
- markSeen(channelId, latestTs);
38
+ markSeen(channelId, latestTs, account);
39
39
  }
40
40
  return messages.map(m => ({
41
41
  id: m.id,
@@ -3,9 +3,9 @@ import { getToken } from "../auth.js";
3
3
  import { makeDiscordHeaders, rateLimitedFetch } from "../ratelimit.js";
4
4
  import { getLastSeen, markSeen } from "../state.js";
5
5
  import { loadConfig } from "../config.js";
6
- export async function getUnread(channels) {
6
+ export async function getUnread(channels, account = "default") {
7
7
  // ISC-D4: SQLite-first path
8
- const db = getDb();
8
+ const db = getDb(account);
9
9
  if (db) {
10
10
  const unreadRows = queryUnread(db);
11
11
  if (unreadRows.length > 0) {
@@ -34,13 +34,13 @@ export async function getUnread(channels) {
34
34
  }
35
35
  }
36
36
  // REST fallback (daemon not running or no unread in SQLite)
37
- const config = loadConfig();
37
+ const config = loadConfig(account);
38
38
  if (!config.retention.fallback_to_api)
39
39
  return [];
40
- const token = await getToken();
40
+ const token = await getToken(account);
41
41
  const results = [];
42
42
  for (const ch of channels) {
43
- const lastSeen = getLastSeen(ch.channelId);
43
+ const lastSeen = getLastSeen(ch.channelId, account);
44
44
  const res = await rateLimitedFetch(`https://discord.com/api/v10/channels/${ch.channelId}/messages?limit=25`, { headers: makeDiscordHeaders(token) });
45
45
  if (!res.ok)
46
46
  continue;
@@ -49,7 +49,7 @@ export async function getUnread(channels) {
49
49
  .filter(m => !m.author.bot && new Date(m.timestamp).getTime() > lastSeen)
50
50
  .reverse();
51
51
  if (unread.length > 0) {
52
- markSeen(ch.channelId, new Date(messages[0].timestamp).getTime());
52
+ markSeen(ch.channelId, new Date(messages[0].timestamp).getTime(), account);
53
53
  results.push({
54
54
  guild: ch.guildName,
55
55
  channel: ch.channelName,
@@ -1,7 +1,7 @@
1
1
  import { getToken } from "../auth.js";
2
2
  import { makeDiscordHeaders, rateLimitedFetch } from "../ratelimit.js";
3
- export async function listChannels(guildId) {
4
- const token = await getToken();
3
+ export async function listChannels(guildId, account = "default") {
4
+ const token = await getToken(account);
5
5
  const res = await rateLimitedFetch(`https://discord.com/api/v10/guilds/${guildId}/channels`, { headers: makeDiscordHeaders(token) });
6
6
  if (!res.ok)
7
7
  throw new Error(`Discord API error: ${res.status}`);
@@ -1,7 +1,7 @@
1
1
  import { getToken } from "../auth.js";
2
2
  import { makeDiscordHeaders, rateLimitedFetch } from "../ratelimit.js";
3
- export async function listGuilds() {
4
- const token = await getToken();
3
+ export async function listGuilds(account = "default") {
4
+ const token = await getToken(account);
5
5
  const res = await rateLimitedFetch("https://discord.com/api/v10/users/@me/guilds", { headers: makeDiscordHeaders(token) });
6
6
  if (!res.ok)
7
7
  throw new Error(`Discord API error: ${res.status}`);
@@ -1,6 +1,6 @@
1
1
  import { getDb, searchMessages } from "../db.js";
2
- export async function searchDiscord(query, channelId, since, until, limit = 50) {
3
- const db = getDb();
2
+ export async function searchDiscord(query, channelId, since, until, limit = 50, account = "default") {
3
+ const db = getDb(account);
4
4
  if (!db) {
5
5
  throw new Error("Daemon not running — no local message history to search. Start discord-mcp daemon first.");
6
6
  }
@@ -1,7 +1,7 @@
1
1
  import { getToken } from "../auth.js";
2
2
  import { makeDiscordHeaders, rateLimitedFetch } from "../ratelimit.js";
3
- export async function sendMessage(opts) {
4
- const token = await getToken();
3
+ export async function sendMessage(opts, account = "default") {
4
+ const token = await getToken(account);
5
5
  let targetChannelId = opts.channelId;
6
6
  // If userId given, open DM channel first
7
7
  if (!targetChannelId && opts.userId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tensakulabs/discord-mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.7",
4
4
  "description": "Discord selfbot MCP server — read & send messages via Claude",
5
5
  "license": "MIT",
6
6
  "type": "module",