@tensakulabs/discord-mcp 0.1.2 → 0.1.3

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/config.js CHANGED
@@ -10,6 +10,12 @@ const DEFAULTS = {
10
10
  mentioned: 90,
11
11
  fallback_to_api: true,
12
12
  },
13
+ hooks: {
14
+ on_mention: [],
15
+ on_everyone: [],
16
+ on_here: [],
17
+ on_message: [],
18
+ },
13
19
  };
14
20
  export function loadConfig() {
15
21
  if (!existsSync(CONFIG_FILE))
@@ -18,6 +24,12 @@ export function loadConfig() {
18
24
  const raw = JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
19
25
  return {
20
26
  retention: { ...DEFAULTS.retention, ...(raw.retention ?? {}) },
27
+ hooks: {
28
+ on_mention: raw.hooks?.on_mention ?? [],
29
+ on_everyone: raw.hooks?.on_everyone ?? [],
30
+ on_here: raw.hooks?.on_here ?? [],
31
+ on_message: raw.hooks?.on_message ?? [],
32
+ },
21
33
  };
22
34
  }
23
35
  catch {
package/dist/daemon.js CHANGED
@@ -8,6 +8,8 @@ import { WebSocket } from "ws";
8
8
  import { getToken } from "./auth.js";
9
9
  import { initDb, insertMessage } from "./db.js";
10
10
  import { schedulePurge } from "./purge.js";
11
+ import { loadConfig } from "./config.js";
12
+ import { fireHooks } from "./hooks.js";
11
13
  const GATEWAY_URL = "wss://gateway.discord.gg/?v=10&encoding=json";
12
14
  const IDENTIFY_INTENTS = (1 << 0) | (1 << 9) | (1 << 12); // GUILDS | GUILD_MESSAGES | MESSAGE_CONTENT
13
15
  let ws = null;
@@ -16,6 +18,7 @@ let sequence = null;
16
18
  let sessionId = null;
17
19
  let resumeGatewayUrl = null;
18
20
  let reconnectDelay = 1000;
21
+ let selfId = null; // set from READY event
19
22
  const db = initDb();
20
23
  schedulePurge();
21
24
  async function connect(resume = false) {
@@ -82,14 +85,24 @@ function handleEvent(type, data) {
82
85
  case "READY":
83
86
  sessionId = data.session_id;
84
87
  resumeGatewayUrl = data.resume_gateway_url;
85
- console.error(`[discord-mcp daemon] Ready.`);
88
+ selfId = data.user?.id ?? null;
89
+ console.error(`[discord-mcp daemon] Ready. (selfId: ${selfId})`);
86
90
  break;
87
91
  case "MESSAGE_CREATE": {
88
92
  const msg = data;
89
93
  if (msg.author?.bot)
90
94
  break; // ignore bots
91
- const isMention = Array.isArray(msg.mentions) &&
92
- msg.mentions.some((u) => u.id === (data.self_id ?? ""));
95
+ // Detect mention type
96
+ const isDirect = selfId !== null && Array.isArray(msg.mentions) &&
97
+ msg.mentions.some((u) => u.id === selfId);
98
+ const isEveryone = msg.mention_everyone === true &&
99
+ msg.content.includes("@everyone");
100
+ const isHere = msg.mention_everyone === true &&
101
+ msg.content.includes("@here");
102
+ const mentionType = isDirect ? "direct"
103
+ : isEveryone ? "everyone"
104
+ : isHere ? "here"
105
+ : null;
93
106
  insertMessage(db, {
94
107
  id: msg.id,
95
108
  channel_id: msg.channel_id,
@@ -99,8 +112,25 @@ function handleEvent(type, data) {
99
112
  content: msg.content,
100
113
  timestamp: new Date(msg.timestamp).getTime(),
101
114
  is_dm: msg.guild_id ? 0 : 1,
102
- is_mention: isMention ? 1 : 0,
115
+ is_mention: mentionType !== null ? 1 : 0,
116
+ mention_type: mentionType,
103
117
  });
118
+ // Fire hooks
119
+ const cfg = loadConfig();
120
+ const ctx = {
121
+ author: msg.author.username,
122
+ content: msg.content,
123
+ channel: msg.channel_id,
124
+ guild: msg.guild_id ?? "dm",
125
+ is_dm: !msg.guild_id,
126
+ };
127
+ if (isDirect)
128
+ fireHooks(cfg.hooks.on_mention, ctx);
129
+ if (isEveryone)
130
+ fireHooks(cfg.hooks.on_everyone, ctx);
131
+ if (isHere)
132
+ fireHooks(cfg.hooks.on_here, ctx);
133
+ fireHooks(cfg.hooks.on_message, ctx);
104
134
  break;
105
135
  }
106
136
  }
package/dist/db.js CHANGED
@@ -28,16 +28,17 @@ export function initDb() {
28
28
  db.pragma("synchronous = NORMAL");
29
29
  db.exec(`
30
30
  CREATE TABLE IF NOT EXISTS messages (
31
- id TEXT PRIMARY KEY,
32
- channel_id TEXT NOT NULL,
33
- guild_id TEXT,
34
- author_id TEXT NOT NULL,
35
- author_name TEXT NOT NULL,
36
- content TEXT NOT NULL,
37
- timestamp INTEGER NOT NULL, -- unix ms
38
- is_dm INTEGER NOT NULL DEFAULT 0,
39
- is_mention INTEGER NOT NULL DEFAULT 0,
40
- seen INTEGER NOT NULL DEFAULT 0
31
+ id TEXT PRIMARY KEY,
32
+ channel_id TEXT NOT NULL,
33
+ guild_id TEXT,
34
+ author_id TEXT NOT NULL,
35
+ author_name TEXT NOT NULL,
36
+ content TEXT NOT NULL,
37
+ timestamp INTEGER NOT NULL, -- unix ms
38
+ is_dm INTEGER NOT NULL DEFAULT 0,
39
+ is_mention INTEGER NOT NULL DEFAULT 0,
40
+ mention_type TEXT DEFAULT NULL, -- "direct" | "everyone" | "here" | null
41
+ seen INTEGER NOT NULL DEFAULT 0
41
42
  );
42
43
 
43
44
  CREATE INDEX IF NOT EXISTS idx_channel_ts ON messages(channel_id, timestamp DESC);
@@ -55,15 +56,20 @@ export function initDb() {
55
56
  VALUES (new.rowid, new.content, new.author_name);
56
57
  END;
57
58
  `);
59
+ // Migration: add mention_type column if not present (existing installs)
60
+ const cols = db.pragma("table_info(messages)").map(c => c.name);
61
+ if (!cols.includes("mention_type")) {
62
+ db.exec("ALTER TABLE messages ADD COLUMN mention_type TEXT DEFAULT NULL");
63
+ }
58
64
  _db = db;
59
65
  return db;
60
66
  }
61
67
  export function insertMessage(db, msg) {
62
68
  const stmt = db.prepare(`
63
69
  INSERT OR IGNORE INTO messages
64
- (id, channel_id, guild_id, author_id, author_name, content, timestamp, is_dm, is_mention, seen)
70
+ (id, channel_id, guild_id, author_id, author_name, content, timestamp, is_dm, is_mention, mention_type, seen)
65
71
  VALUES
66
- (@id, @channel_id, @guild_id, @author_id, @author_name, @content, @timestamp, @is_dm, @is_mention, 0)
72
+ (@id, @channel_id, @guild_id, @author_id, @author_name, @content, @timestamp, @is_dm, @is_mention, @mention_type, 0)
67
73
  `);
68
74
  stmt.run(msg);
69
75
  }
package/dist/hooks.js ADDED
@@ -0,0 +1,34 @@
1
+ import { exec } from "child_process";
2
+ function substitute(template, ctx) {
3
+ return template
4
+ .replace(/\{author\}/g, ctx.author)
5
+ .replace(/\{content\}/g, ctx.content.replace(/"/g, '\\"'))
6
+ .replace(/\{channel\}/g, ctx.channel)
7
+ .replace(/\{guild\}/g, ctx.guild)
8
+ .replace(/\{is_dm\}/g, String(ctx.is_dm));
9
+ }
10
+ async function runHook(hook, ctx) {
11
+ if (!hook.enabled)
12
+ return;
13
+ if (hook.type === "command" && hook.command) {
14
+ const cmd = substitute(hook.command, ctx);
15
+ exec(cmd, (err) => {
16
+ if (err)
17
+ console.error(`[discord-mcp hooks] command failed: ${err.message}`);
18
+ });
19
+ }
20
+ if (hook.type === "http" && hook.url) {
21
+ fetch(hook.url, {
22
+ method: "POST",
23
+ headers: { "Content-Type": "application/json" },
24
+ body: JSON.stringify(ctx),
25
+ }).catch((err) => {
26
+ console.error(`[discord-mcp hooks] http failed: ${err.message}`);
27
+ });
28
+ }
29
+ }
30
+ export async function fireHooks(hooks, ctx) {
31
+ for (const hook of hooks) {
32
+ runHook(hook, ctx).catch(() => { }); // never block the daemon
33
+ }
34
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tensakulabs/discord-mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Discord selfbot MCP server — read & send messages via Claude",
5
5
  "license": "MIT",
6
6
  "type": "module",