@pilat/mcp-notify 1.0.0

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.
Files changed (46) hide show
  1. package/README.md +64 -0
  2. package/dist/cache/channels.d.ts +3 -0
  3. package/dist/cache/channels.d.ts.map +1 -0
  4. package/dist/cache/channels.js +45 -0
  5. package/dist/cache/channels.js.map +1 -0
  6. package/dist/cache/db.d.ts +4 -0
  7. package/dist/cache/db.d.ts.map +1 -0
  8. package/dist/cache/db.js +56 -0
  9. package/dist/cache/db.js.map +1 -0
  10. package/dist/cache/sync.d.ts +15 -0
  11. package/dist/cache/sync.d.ts.map +1 -0
  12. package/dist/cache/sync.js +88 -0
  13. package/dist/cache/sync.js.map +1 -0
  14. package/dist/cache/users.d.ts +4 -0
  15. package/dist/cache/users.d.ts.map +1 -0
  16. package/dist/cache/users.js +72 -0
  17. package/dist/cache/users.js.map +1 -0
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +24 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/mentions/resolver.d.ts +10 -0
  23. package/dist/mentions/resolver.d.ts.map +1 -0
  24. package/dist/mentions/resolver.js +42 -0
  25. package/dist/mentions/resolver.js.map +1 -0
  26. package/dist/server.d.ts +4 -0
  27. package/dist/server.d.ts.map +1 -0
  28. package/dist/server.js +108 -0
  29. package/dist/server.js.map +1 -0
  30. package/dist/slack/client.d.ts +3 -0
  31. package/dist/slack/client.d.ts.map +1 -0
  32. package/dist/slack/client.js +20 -0
  33. package/dist/slack/client.js.map +1 -0
  34. package/dist/slack/send.d.ts +3 -0
  35. package/dist/slack/send.d.ts.map +1 -0
  36. package/dist/slack/send.js +90 -0
  37. package/dist/slack/send.js.map +1 -0
  38. package/dist/types.d.ts +15 -0
  39. package/dist/types.d.ts.map +1 -0
  40. package/dist/types.js +2 -0
  41. package/dist/types.js.map +1 -0
  42. package/dist/utils/errors.d.ts +14 -0
  43. package/dist/utils/errors.d.ts.map +1 -0
  44. package/dist/utils/errors.js +28 -0
  45. package/dist/utils/errors.js.map +1 -0
  46. package/package.json +54 -0
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # mcp-notify
2
+
3
+ Minimal MCP server for sending Slack messages as user (xoxc + cookie auth). Fire-and-forget — single `send_message` tool, nothing else.
4
+
5
+ > **Looking for a full-featured Slack MCP?** Check out [korotovsky/slack-mcp-server](https://github.com/korotovsky/slack-mcp-server) — it supports reading, searching, reactions, threads, DMs, and much more. We recommend it for most use cases.
6
+ >
7
+ > This project exists because we needed two things it doesn't offer:
8
+ > - **Bot signature** — every message gets a `:robot_face:` context block so it's clear the message was sent by an AI assistant, not a human
9
+ > - **Concurrent safety** — SQLite with WAL mode and check-lock-recheck sync pattern, safe for multiple MCP instances running in parallel
10
+
11
+ ## Installation
12
+
13
+ ### Claude Code
14
+
15
+ Add to your MCP config (`~/.claude/settings.json` or project settings):
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "mcp-notify": {
21
+ "type": "stdio",
22
+ "command": "npx",
23
+ "args": ["-y", "@pilat/mcp-notify"],
24
+ "env": {
25
+ "SLACK_MCP_XOXC_TOKEN": "xoxc-...",
26
+ "SLACK_MCP_XOXD_TOKEN": "xoxd-..."
27
+ }
28
+ }
29
+ }
30
+ }
31
+ ```
32
+
33
+ ### Other MCP clients
34
+
35
+ Use `npx @pilat/mcp-notify` as the command with stdio transport. Pass credentials as environment variables:
36
+
37
+ ```bash
38
+ SLACK_MCP_XOXC_TOKEN=xoxc-... SLACK_MCP_XOXD_TOKEN=xoxd-... npx @pilat/mcp-notify
39
+ ```
40
+
41
+ ## Environment Variables
42
+
43
+ | Variable | Required | Description |
44
+ |----------|----------|-------------|
45
+ | `SLACK_MCP_XOXC_TOKEN` | Yes | User's `xoxc-...` token |
46
+ | `SLACK_MCP_XOXD_TOKEN` | Yes | User's `xoxd-...` session token (value of the `d` cookie) |
47
+ | `SLACK_MCP_DATA_DIR` | No | Custom path for SQLite cache (default: `~/.local/share/mcp-notify`) |
48
+
49
+ ### How to get credentials
50
+
51
+ 1. Open Slack in browser (not desktop app)
52
+ 2. Open DevTools → Network tab
53
+ 3. Make any action in Slack (switch channel, send message)
54
+ 4. Find any request to `api.slack.com` → Headers tab
55
+ 5. **SLACK_MCP_XOXC_TOKEN**: from request payload, find `token=xoxc-...`
56
+ 6. **SLACK_MCP_XOXD_TOKEN**: from Cookie header, find `d=xoxd-...` — copy only the `xoxd-...` part (without `d=`)
57
+
58
+ ## Architecture
59
+
60
+ Single tool: `send_message`. Messages are sent with Block Kit (section + context `:robot_face:`) with plain text fallback.
61
+
62
+ - **SQLite cache** (`~/.local/share/mcp-notify/data.db`, WAL mode) — channels, users, user groups with lazy sync on first cache miss, 24h TTL
63
+ - **Mention resolution** — `@username` → `<@U123>`, `@grouphandle` → `<!subteam^ID>`. Groups take priority. Resolved in parallel.
64
+ - **Concurrent sync safety** — CAS-based check-lock-recheck pattern via `sync_meta` table
@@ -0,0 +1,3 @@
1
+ import type { WebClient } from '@slack/web-api';
2
+ export declare function getChannelId(name: string, client: WebClient): Promise<string | null>;
3
+ //# sourceMappingURL=channels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channels.d.ts","sourceRoot":"","sources":["../../src/cache/channels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AA+ChD,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAS1F"}
@@ -0,0 +1,45 @@
1
+ import { getDb } from './db.js';
2
+ import { syncIfNeeded, isTableSynced, CACHE_TTL_MS } from './sync.js';
3
+ function lookupChannel(name) {
4
+ const db = getDb();
5
+ const row = db.prepare('SELECT id FROM channels WHERE name = ? AND synced_at > ?').get(name, Date.now() - CACHE_TTL_MS);
6
+ return row?.id ?? null;
7
+ }
8
+ async function fetchChannels(client) {
9
+ const db = getDb();
10
+ const now = Date.now();
11
+ const channels = [];
12
+ let cursor;
13
+ do {
14
+ const result = await client.conversations.list({
15
+ types: 'public_channel,private_channel',
16
+ exclude_archived: true,
17
+ limit: 1000,
18
+ cursor,
19
+ });
20
+ for (const ch of result.channels ?? []) {
21
+ if (ch.id && ch.name) {
22
+ channels.push({ id: ch.id, name: ch.name });
23
+ }
24
+ }
25
+ cursor = result.response_metadata?.next_cursor || undefined;
26
+ } while (cursor);
27
+ const insert = db.prepare('INSERT OR REPLACE INTO channels (id, name, synced_at) VALUES (?, ?, ?)');
28
+ const tx = db.transaction(() => {
29
+ db.prepare('DELETE FROM channels').run();
30
+ for (const ch of channels) {
31
+ insert.run(ch.id, ch.name, now);
32
+ }
33
+ });
34
+ tx();
35
+ }
36
+ export async function getChannelId(name, client) {
37
+ const cached = lookupChannel(name);
38
+ if (cached !== null)
39
+ return cached;
40
+ if (!isTableSynced('channels')) {
41
+ return syncIfNeeded('channels', () => lookupChannel(name), () => fetchChannels(client));
42
+ }
43
+ return null;
44
+ }
45
+ //# sourceMappingURL=channels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channels.js","sourceRoot":"","sources":["../../src/cache/channels.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAEtE,SAAS,aAAa,CAAC,IAAY;IACjC,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC;IACnB,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CACpB,0DAA0D,CAC3D,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,YAAY,CAA+B,CAAC;IACrE,OAAO,GAAG,EAAE,EAAE,IAAI,IAAI,CAAC;AACzB,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,MAAiB;IAC5C,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC;IACnB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,QAAQ,GAAmC,EAAE,CAAC;IAEpD,IAAI,MAA0B,CAAC;IAC/B,GAAG,CAAC;QACF,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC;YAC7C,KAAK,EAAE,gCAAgC;YACvC,gBAAgB,EAAE,IAAI;YACtB,KAAK,EAAE,IAAI;YACX,MAAM;SACP,CAAC,CAAC;QAEH,KAAK,MAAM,EAAE,IAAI,MAAM,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;YACvC,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;gBACrB,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;YAC9C,CAAC;QACH,CAAC;QAED,MAAM,GAAG,MAAM,CAAC,iBAAiB,EAAE,WAAW,IAAI,SAAS,CAAC;IAC9D,CAAC,QAAQ,MAAM,EAAE;IAEjB,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CACvB,wEAAwE,CACzE,CAAC;IACF,MAAM,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;QAC7B,EAAE,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC,GAAG,EAAE,CAAC;QACzC,KAAK,MAAM,EAAE,IAAI,QAAQ,EAAE,CAAC;YAC1B,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;QAClC,CAAC;IACH,CAAC,CAAC,CAAC;IACH,EAAE,EAAE,CAAC;AACP,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAY,EAAE,MAAiB;IAChE,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC;IAEnC,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,OAAO,YAAY,CAAC,UAAU,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC;IAC1F,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,4 @@
1
+ import Database from 'better-sqlite3';
2
+ export declare function getDb(): Database.Database;
3
+ export declare function closeDb(): void;
4
+ //# sourceMappingURL=db.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/cache/db.ts"],"names":[],"mappings":"AAGA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAItC,wBAAgB,KAAK,IAAI,QAAQ,CAAC,QAAQ,CAkDzC;AAED,wBAAgB,OAAO,IAAI,IAAI,CAK9B"}
@@ -0,0 +1,56 @@
1
+ import { mkdirSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import Database from 'better-sqlite3';
5
+ let db = null;
6
+ export function getDb() {
7
+ if (db)
8
+ return db;
9
+ const dataDir = process.env.SLACK_MCP_DATA_DIR ?? join(homedir(), '.local', 'share', 'mcp-notify');
10
+ mkdirSync(dataDir, { recursive: true });
11
+ const dbPath = join(dataDir, 'data.db');
12
+ db = new Database(dbPath);
13
+ db.pragma('journal_mode = WAL');
14
+ db.exec(`
15
+ CREATE TABLE IF NOT EXISTS channels (
16
+ id TEXT PRIMARY KEY,
17
+ name TEXT NOT NULL,
18
+ synced_at INTEGER NOT NULL
19
+ );
20
+ CREATE INDEX IF NOT EXISTS idx_channels_name ON channels(name);
21
+
22
+ CREATE TABLE IF NOT EXISTS users (
23
+ id TEXT PRIMARY KEY,
24
+ name TEXT NOT NULL,
25
+ synced_at INTEGER NOT NULL
26
+ );
27
+ CREATE INDEX IF NOT EXISTS idx_users_name ON users(name);
28
+
29
+ CREATE TABLE IF NOT EXISTS user_groups (
30
+ id TEXT PRIMARY KEY,
31
+ handle TEXT NOT NULL,
32
+ synced_at INTEGER NOT NULL
33
+ );
34
+ CREATE INDEX IF NOT EXISTS idx_user_groups_handle ON user_groups(handle);
35
+
36
+ CREATE TABLE IF NOT EXISTS sync_meta (
37
+ table_name TEXT PRIMARY KEY,
38
+ started_at INTEGER NOT NULL DEFAULT 0,
39
+ completed_at INTEGER NOT NULL DEFAULT 0
40
+ );
41
+ `);
42
+ const upsert = db.prepare('INSERT OR IGNORE INTO sync_meta (table_name, started_at, completed_at) VALUES (?, 0, 0)');
43
+ db.transaction(() => {
44
+ upsert.run('channels');
45
+ upsert.run('users');
46
+ upsert.run('user_groups');
47
+ })();
48
+ return db;
49
+ }
50
+ export function closeDb() {
51
+ if (db) {
52
+ db.close();
53
+ db = null;
54
+ }
55
+ }
56
+ //# sourceMappingURL=db.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/cache/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAEtC,IAAI,EAAE,GAA6B,IAAI,CAAC;AAExC,MAAM,UAAU,KAAK;IACnB,IAAI,EAAE;QAAE,OAAO,EAAE,CAAC;IAElB,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC;IACnG,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAExC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACxC,EAAE,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC;IAE1B,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAEhC,EAAE,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BP,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CACvB,yFAAyF,CAC1F,CAAC;IACF,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;QAClB,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACvB,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACpB,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IAC5B,CAAC,CAAC,EAAE,CAAC;IAEL,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,OAAO;IACrB,IAAI,EAAE,EAAE,CAAC;QACP,EAAE,CAAC,KAAK,EAAE,CAAC;QACX,EAAE,GAAG,IAAI,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -0,0 +1,15 @@
1
+ export declare const CACHE_TTL_MS: number;
2
+ export declare function isTableSynced(tableName: string): boolean;
3
+ /**
4
+ * Generic check-lock-recheck sync coordinator.
5
+ *
6
+ * 1. lookupFn() — if found, return immediately
7
+ * 2. Check sync_meta — if another instance is syncing, poll until done
8
+ * 3. Claim sync atomically (CAS) — set started_at only if no one else is syncing
9
+ * 4. Call apiFetchFn() — fetch data from Slack API
10
+ * 5. Write results — apiFetchFn handles the DB writes
11
+ * 6. Mark completed — set completed_at (only on success)
12
+ * 7. lookupFn() — return final result
13
+ */
14
+ export declare function syncIfNeeded<T>(tableName: string, lookupFn: () => T | null, apiFetchFn: () => Promise<void>): Promise<T | null>;
15
+ //# sourceMappingURL=sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/cache/sync.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,YAAY,QAAsB,CAAC;AAMhD,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAMxD;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAClC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,CAAC,GAAG,IAAI,EACxB,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAC9B,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CA8EnB"}
@@ -0,0 +1,88 @@
1
+ import { getDb } from './db.js';
2
+ import { SlackMcpError, ErrorCode } from '../utils/errors.js';
3
+ const SYNC_TIMEOUT_MS = 30_000;
4
+ const POLL_INTERVAL_MS = 500;
5
+ export const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
6
+ function sleep(ms) {
7
+ return new Promise((resolve) => setTimeout(resolve, ms));
8
+ }
9
+ export function isTableSynced(tableName) {
10
+ const db = getDb();
11
+ const meta = db.prepare('SELECT completed_at FROM sync_meta WHERE table_name = ?').get(tableName);
12
+ return (meta?.completed_at ?? 0) > Date.now() - CACHE_TTL_MS;
13
+ }
14
+ /**
15
+ * Generic check-lock-recheck sync coordinator.
16
+ *
17
+ * 1. lookupFn() — if found, return immediately
18
+ * 2. Check sync_meta — if another instance is syncing, poll until done
19
+ * 3. Claim sync atomically (CAS) — set started_at only if no one else is syncing
20
+ * 4. Call apiFetchFn() — fetch data from Slack API
21
+ * 5. Write results — apiFetchFn handles the DB writes
22
+ * 6. Mark completed — set completed_at (only on success)
23
+ * 7. lookupFn() — return final result
24
+ */
25
+ export async function syncIfNeeded(tableName, lookupFn, apiFetchFn) {
26
+ // Step 1: check cache
27
+ const cached = lookupFn();
28
+ if (cached !== null)
29
+ return cached;
30
+ const db = getDb();
31
+ const now = Date.now();
32
+ // Step 2: check if another instance is syncing
33
+ const meta = db.prepare('SELECT started_at, completed_at FROM sync_meta WHERE table_name = ?').get(tableName);
34
+ if (meta && meta.started_at > meta.completed_at) {
35
+ const elapsed = now - meta.started_at;
36
+ if (elapsed < SYNC_TIMEOUT_MS) {
37
+ // Another instance is syncing — poll
38
+ const deadline = meta.started_at + SYNC_TIMEOUT_MS;
39
+ while (Date.now() < deadline) {
40
+ await sleep(POLL_INTERVAL_MS);
41
+ const result = lookupFn();
42
+ if (result !== null)
43
+ return result;
44
+ const updated = db.prepare('SELECT completed_at FROM sync_meta WHERE table_name = ?').get(tableName);
45
+ if (updated && updated.completed_at >= meta.started_at) {
46
+ break;
47
+ }
48
+ }
49
+ const afterWait = lookupFn();
50
+ if (afterWait !== null)
51
+ return afterWait;
52
+ }
53
+ }
54
+ // Step 3: CAS claim — atomic check-and-set, also claims stale locks
55
+ const claimTime = Date.now();
56
+ const claimed = db.prepare(`UPDATE sync_meta SET started_at = ?
57
+ WHERE table_name = ? AND (completed_at >= started_at OR started_at + ? < ?)`).run(claimTime, tableName, SYNC_TIMEOUT_MS, claimTime);
58
+ if (claimed.changes === 0) {
59
+ // Someone else just claimed it — poll and return
60
+ const deadline = claimTime + SYNC_TIMEOUT_MS;
61
+ while (Date.now() < deadline) {
62
+ await sleep(POLL_INTERVAL_MS);
63
+ const result = lookupFn();
64
+ if (result !== null)
65
+ return result;
66
+ const updated = db.prepare('SELECT completed_at FROM sync_meta WHERE table_name = ?').get(tableName);
67
+ if (updated && updated.completed_at >= claimTime) {
68
+ break;
69
+ }
70
+ }
71
+ return lookupFn();
72
+ }
73
+ // Step 4-5: fetch and write
74
+ try {
75
+ await apiFetchFn();
76
+ // Step 6: mark completed only on success
77
+ db.prepare('UPDATE sync_meta SET completed_at = ? WHERE table_name = ?').run(Date.now(), tableName);
78
+ }
79
+ catch (err) {
80
+ // Reset lock to allow immediate retry
81
+ db.prepare('UPDATE sync_meta SET started_at = completed_at WHERE table_name = ?').run(tableName);
82
+ const msg = err instanceof Error ? err.message : String(err);
83
+ throw new SlackMcpError(ErrorCode.SYNC_FAILED, `Failed to sync ${tableName}: ${msg}`);
84
+ }
85
+ // Step 7: final lookup
86
+ return lookupFn();
87
+ }
88
+ //# sourceMappingURL=sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.js","sourceRoot":"","sources":["../../src/cache/sync.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { WebClient } from '@slack/web-api';
2
+ export declare function resolveUser(name: string, client: WebClient): Promise<string | null>;
3
+ export declare function resolveUserGroup(handle: string, client: WebClient): Promise<string | null>;
4
+ //# sourceMappingURL=users.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"users.d.ts","sourceRoot":"","sources":["../../src/cache/users.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AA4ChD,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASzF;AAiCD,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAShG"}
@@ -0,0 +1,72 @@
1
+ import { getDb } from './db.js';
2
+ import { syncIfNeeded, isTableSynced, CACHE_TTL_MS } from './sync.js';
3
+ // --- Users ---
4
+ function lookupUser(name) {
5
+ const db = getDb();
6
+ const row = db.prepare('SELECT id FROM users WHERE name = ? AND synced_at > ?').get(name, Date.now() - CACHE_TTL_MS);
7
+ return row?.id ?? null;
8
+ }
9
+ async function fetchUsers(client) {
10
+ const db = getDb();
11
+ const now = Date.now();
12
+ const users = [];
13
+ let cursor;
14
+ do {
15
+ const result = await client.users.list({ limit: 1000, cursor });
16
+ for (const member of result.members ?? []) {
17
+ if (member.id && member.name) {
18
+ users.push({ id: member.id, name: member.name });
19
+ }
20
+ }
21
+ cursor = result.response_metadata?.next_cursor || undefined;
22
+ } while (cursor);
23
+ const insert = db.prepare('INSERT OR REPLACE INTO users (id, name, synced_at) VALUES (?, ?, ?)');
24
+ const tx = db.transaction(() => {
25
+ db.prepare('DELETE FROM users').run();
26
+ for (const u of users) {
27
+ insert.run(u.id, u.name, now);
28
+ }
29
+ });
30
+ tx();
31
+ }
32
+ export async function resolveUser(name, client) {
33
+ const cached = lookupUser(name);
34
+ if (cached !== null)
35
+ return cached;
36
+ if (!isTableSynced('users')) {
37
+ return syncIfNeeded('users', () => lookupUser(name), () => fetchUsers(client));
38
+ }
39
+ return null;
40
+ }
41
+ // --- User Groups ---
42
+ function lookupUserGroup(handle) {
43
+ const db = getDb();
44
+ const row = db.prepare('SELECT id FROM user_groups WHERE handle = ? AND synced_at > ?').get(handle, Date.now() - CACHE_TTL_MS);
45
+ return row?.id ?? null;
46
+ }
47
+ async function fetchUserGroups(client) {
48
+ const db = getDb();
49
+ const now = Date.now();
50
+ const result = await client.usergroups.list({ include_users: false });
51
+ const groups = result.usergroups ?? [];
52
+ const insert = db.prepare('INSERT OR REPLACE INTO user_groups (id, handle, synced_at) VALUES (?, ?, ?)');
53
+ const tx = db.transaction(() => {
54
+ db.prepare('DELETE FROM user_groups').run();
55
+ for (const g of groups) {
56
+ if (g.id && g.handle) {
57
+ insert.run(g.id, g.handle, now);
58
+ }
59
+ }
60
+ });
61
+ tx();
62
+ }
63
+ export async function resolveUserGroup(handle, client) {
64
+ const cached = lookupUserGroup(handle);
65
+ if (cached !== null)
66
+ return cached;
67
+ if (!isTableSynced('user_groups')) {
68
+ return syncIfNeeded('user_groups', () => lookupUserGroup(handle), () => fetchUserGroups(client));
69
+ }
70
+ return null;
71
+ }
72
+ //# sourceMappingURL=users.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"users.js","sourceRoot":"","sources":["../../src/cache/users.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ import { runServer } from './server.js';
3
+ import { closeDb } from './cache/db.js';
4
+ function shutdown() {
5
+ try {
6
+ closeDb();
7
+ }
8
+ catch { /* ignore */ }
9
+ process.exit(0);
10
+ }
11
+ async function main() {
12
+ process.on('SIGINT', shutdown);
13
+ process.on('SIGTERM', shutdown);
14
+ await runServer();
15
+ }
16
+ main().catch((error) => {
17
+ try {
18
+ closeDb();
19
+ }
20
+ catch { /* ignore */ }
21
+ console.error('Fatal error:', error instanceof Error ? error.message : String(error));
22
+ process.exit(1);
23
+ });
24
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAExC,SAAS,QAAQ;IACf,IAAI,CAAC;QAAC,OAAO,EAAE,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;IACzC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAEhC,MAAM,SAAS,EAAE,CAAC;AACpB,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,IAAI,CAAC;QAAC,OAAO,EAAE,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;IACzC,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACtF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { WebClient } from '@slack/web-api';
2
+ /**
3
+ * Convert @mentions in message text to Slack format.
4
+ *
5
+ * - @grouphandle → <!subteam^ID> (groups take priority)
6
+ * - @username → <@ID>
7
+ * - Unknown mentions are left as-is.
8
+ */
9
+ export declare function convertMentions(message: string, client: WebClient): Promise<string>;
10
+ //# sourceMappingURL=resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../src/mentions/resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAmCzF"}
@@ -0,0 +1,42 @@
1
+ import { resolveUserGroup, resolveUser } from '../cache/users.js';
2
+ /**
3
+ * Convert @mentions in message text to Slack format.
4
+ *
5
+ * - @grouphandle → <!subteam^ID> (groups take priority)
6
+ * - @username → <@ID>
7
+ * - Unknown mentions are left as-is.
8
+ */
9
+ export async function convertMentions(message, client) {
10
+ if (!message.includes('@'))
11
+ return message;
12
+ const regex = /(?<!\w)@([\w.-]+\w)/g;
13
+ const names = new Set();
14
+ let match;
15
+ while ((match = regex.exec(message)) !== null) {
16
+ if (match[1])
17
+ names.add(match[1]);
18
+ }
19
+ if (names.size === 0)
20
+ return message;
21
+ // Resolve all mentions in parallel
22
+ const entries = await Promise.all([...names].map(async (name) => {
23
+ const groupId = await resolveUserGroup(name, client);
24
+ if (groupId)
25
+ return [name, `<!subteam^${groupId}>`];
26
+ const userId = await resolveUser(name, client);
27
+ if (userId)
28
+ return [name, `<@${userId}>`];
29
+ return [name, null];
30
+ }));
31
+ const replacements = new Map();
32
+ for (const [name, replacement] of entries) {
33
+ if (replacement)
34
+ replacements.set(name, replacement);
35
+ }
36
+ if (replacements.size === 0)
37
+ return message;
38
+ return message.replace(/(?<!\w)@([\w.-]+\w)/g, (full, name) => {
39
+ return replacements.get(name) ?? full;
40
+ });
41
+ }
42
+ //# sourceMappingURL=resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver.js","sourceRoot":"","sources":["../../src/mentions/resolver.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAElE;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,OAAe,EAAE,MAAiB;IACtE,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,OAAO,CAAC;IAE3C,MAAM,KAAK,GAAG,sBAAsB,CAAC;IACrC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,IAAI,KAAK,CAAC;IACV,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC9C,IAAI,KAAK,CAAC,CAAC,CAAC;YAAE,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACpC,CAAC;IAED,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC;IAErC,mCAAmC;IACnC,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAC/B,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAoC,EAAE;QAC9D,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACrD,IAAI,OAAO;YAAE,OAAO,CAAC,IAAI,EAAE,aAAa,OAAO,GAAG,CAAC,CAAC;QAEpD,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC/C,IAAI,MAAM;YAAE,OAAO,CAAC,IAAI,EAAE,KAAK,MAAM,GAAG,CAAC,CAAC;QAE1C,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACtB,CAAC,CAAC,CACH,CAAC;IAEF,MAAM,YAAY,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC/C,KAAK,MAAM,CAAC,IAAI,EAAE,WAAW,CAAC,IAAI,OAAO,EAAE,CAAC;QAC1C,IAAI,WAAW;YAAE,YAAY,CAAC,GAAG,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IACvD,CAAC;IAED,IAAI,YAAY,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,OAAO,CAAC;IAE5C,OAAO,OAAO,CAAC,OAAO,CAAC,sBAAsB,EAAE,CAAC,IAAI,EAAE,IAAY,EAAE,EAAE;QACpE,OAAO,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC;IACxC,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,4 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ export declare function createServer(): Server;
3
+ export declare function runServer(): Promise<void>;
4
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAUnE,wBAAgB,YAAY,IAAI,MAAM,CAoGrC;AAED,wBAAsB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAI/C"}
package/dist/server.js ADDED
@@ -0,0 +1,108 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
+ import { sendMessage } from './slack/send.js';
5
+ import { SlackMcpError, ErrorCode } from './utils/errors.js';
6
+ const SERVER_INSTRUCTIONS = `Slack message sender. Use send_message to post messages to channels or reply to threads.
7
+ @mentions like @username are automatically resolved to Slack user IDs.`;
8
+ export function createServer() {
9
+ const server = new Server({
10
+ name: 'mcp-notify',
11
+ version: '1.0.0',
12
+ }, {
13
+ capabilities: {
14
+ tools: {},
15
+ },
16
+ instructions: SERVER_INSTRUCTIONS,
17
+ });
18
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
19
+ tools: [
20
+ {
21
+ name: 'send_message',
22
+ description: `Send a message to a Slack channel or thread as the authenticated user. Messages are tagged with a :robot_face: indicator so recipients know AI assisted.
23
+
24
+ WHEN TO USE: User asks to post, send, write, notify, tell, or reply in Slack.
25
+
26
+ MENTIONS: Write @username or @groupname naturally — they are auto-resolved to Slack IDs. Do NOT manually construct <@U...> syntax.
27
+
28
+ THREAD REPLIES: To reply in a thread, provide thread_ts. Extract from Slack URLs by converting the p-prefixed ID: p1718033467085279 → 1718033467.085279 (insert dot before last 6 digits).
29
+
30
+ CRITICAL — Use Slack mrkdwn, NOT Markdown:
31
+ *bold* (NOT **bold**)
32
+ _italic_ (NOT *italic*)
33
+ ~strikethrough~
34
+ \`inline code\`
35
+ \`\`\`code block\`\`\`
36
+ <https://url.com|link text> (NOT [text](url))
37
+ > blockquote
38
+ • or - for bullets, 1. for numbered lists
39
+ :emoji_name: for emoji
40
+
41
+ NEVER use: **bold**, [link](url), # headers, tables, ---, ![images]. These render as literal text in Slack.`,
42
+ inputSchema: {
43
+ type: 'object',
44
+ properties: {
45
+ channel_name: {
46
+ type: 'string',
47
+ description: 'Channel name without # prefix (e.g. "general", "team-backend"). Works with public and private channels you have access to.',
48
+ },
49
+ message: {
50
+ type: 'string',
51
+ description: 'Message content in Slack mrkdwn format. IMPORTANT: Use *bold* not **bold**, use <url|text> not [text](url). @mentions like @username are auto-resolved.',
52
+ },
53
+ thread_ts: {
54
+ type: 'string',
55
+ description: 'Thread timestamp to reply in a thread (e.g. "1718033467.085279"). From Slack URLs: strip the "p" prefix and insert a dot before the last 6 digits.',
56
+ },
57
+ },
58
+ required: ['channel_name', 'message'],
59
+ },
60
+ },
61
+ ],
62
+ }));
63
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
64
+ const { name, arguments: args } = request.params;
65
+ try {
66
+ switch (name) {
67
+ case 'send_message': {
68
+ const params = args;
69
+ if (!params?.channel_name || typeof params.channel_name !== 'string') {
70
+ throw new SlackMcpError(ErrorCode.SEND_FAILED, 'channel_name is required and must be a string');
71
+ }
72
+ if (!params?.message || typeof params.message !== 'string') {
73
+ throw new SlackMcpError(ErrorCode.SEND_FAILED, 'message is required and must be a string');
74
+ }
75
+ const result = await sendMessage({
76
+ channel_name: params.channel_name,
77
+ message: params.message,
78
+ thread_ts: typeof params.thread_ts === 'string' ? params.thread_ts : undefined,
79
+ });
80
+ return {
81
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
82
+ };
83
+ }
84
+ default:
85
+ throw new Error(`Unknown tool: ${name}`);
86
+ }
87
+ }
88
+ catch (error) {
89
+ if (error instanceof SlackMcpError) {
90
+ return {
91
+ content: [{ type: 'text', text: JSON.stringify(error.toJSON(), null, 2) }],
92
+ isError: true,
93
+ };
94
+ }
95
+ if (error instanceof Error) {
96
+ throw error;
97
+ }
98
+ throw new Error(String(error));
99
+ }
100
+ });
101
+ return server;
102
+ }
103
+ export async function runServer() {
104
+ const server = createServer();
105
+ const transport = new StdioServerTransport();
106
+ await server.connect(transport);
107
+ }
108
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,oCAAoC,CAAC;AAEnG,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAE7D,MAAM,mBAAmB,GAAG;uEAC2C,CAAC;AAExE,MAAM,UAAU,YAAY;IAC1B,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB;QACE,IAAI,EAAE,YAAY;QAClB,OAAO,EAAE,OAAO;KACjB,EACD;QACE,YAAY,EAAE;YACZ,KAAK,EAAE,EAAE;SACV;QACD,YAAY,EAAE,mBAAmB;KAClC,CACF,CAAC;IAEF,MAAM,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;QAC5D,KAAK,EAAE;YACL;gBACE,IAAI,EAAE,cAAc;gBACpB,WAAW,EAAE;;;;;;;;;;;;;;;;;;;4GAmBuF;gBACpG,WAAW,EAAE;oBACX,IAAI,EAAE,QAAQ;oBACd,UAAU,EAAE;wBACV,YAAY,EAAE;4BACZ,IAAI,EAAE,QAAQ;4BACd,WAAW,EAAE,4HAA4H;yBAC1I;wBACD,OAAO,EAAE;4BACP,IAAI,EAAE,QAAQ;4BACd,WAAW,EAAE,yJAAyJ;yBACvK;wBACD,SAAS,EAAE;4BACT,IAAI,EAAE,QAAQ;4BACd,WAAW,EAAE,oJAAoJ;yBAClK;qBACF;oBACD,QAAQ,EAAE,CAAC,cAAc,EAAE,SAAS,CAAC;iBACtC;aACF;SACF;KACF,CAAC,CAAC,CAAC;IAEJ,MAAM,CAAC,iBAAiB,CAAC,qBAAqB,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QAChE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC;QAEjD,IAAI,CAAC;YACH,QAAQ,IAAI,EAAE,CAAC;gBACb,KAAK,cAAc,CAAC,CAAC,CAAC;oBACpB,MAAM,MAAM,GAAG,IAA2C,CAAC;oBAC3D,IAAI,CAAC,MAAM,EAAE,YAAY,IAAI,OAAO,MAAM,CAAC,YAAY,KAAK,QAAQ,EAAE,CAAC;wBACrE,MAAM,IAAI,aAAa,CAAC,SAAS,CAAC,WAAW,EAAE,+CAA+C,CAAC,CAAC;oBAClG,CAAC;oBACD,IAAI,CAAC,MAAM,EAAE,OAAO,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;wBAC3D,MAAM,IAAI,aAAa,CAAC,SAAS,CAAC,WAAW,EAAE,0CAA0C,CAAC,CAAC;oBAC7F,CAAC;oBACD,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC;wBAC/B,YAAY,EAAE,MAAM,CAAC,YAAY;wBACjC,OAAO,EAAE,MAAM,CAAC,OAAO;wBACvB,SAAS,EAAE,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;qBAC/E,CAAC,CAAC;oBACH,OAAO;wBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;qBACnE,CAAC;gBACJ,CAAC;gBACD;oBACE,MAAM,IAAI,KAAK,CAAC,iBAAiB,IAAI,EAAE,CAAC,CAAC;YAC7C,CAAC;QACH,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,IAAI,KAAK,YAAY,aAAa,EAAE,CAAC;gBACnC,OAAO;oBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;oBAC1E,OAAO,EAAE,IAAI;iBACd,CAAC;YACJ,CAAC;YACD,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;gBAC3B,MAAM,KAAK,CAAC;YACd,CAAC;YACD,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACjC,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS;IAC7B,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { WebClient } from '@slack/web-api';
2
+ export declare function getSlackClient(): WebClient;
3
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/slack/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAK3C,wBAAgB,cAAc,IAAI,SAAS,CAiB1C"}
@@ -0,0 +1,20 @@
1
+ import { WebClient } from '@slack/web-api';
2
+ import { SlackMcpError, ErrorCode } from '../utils/errors.js';
3
+ let _client = null;
4
+ export function getSlackClient() {
5
+ if (_client)
6
+ return _client;
7
+ const token = process.env.SLACK_MCP_XOXC_TOKEN;
8
+ const xoxd = process.env.SLACK_MCP_XOXD_TOKEN;
9
+ if (!token) {
10
+ throw new SlackMcpError(ErrorCode.CONFIG_MISSING, 'SLACK_MCP_XOXC_TOKEN environment variable is required');
11
+ }
12
+ if (!xoxd) {
13
+ throw new SlackMcpError(ErrorCode.CONFIG_MISSING, 'SLACK_MCP_XOXD_TOKEN environment variable is required');
14
+ }
15
+ _client = new WebClient(token, {
16
+ headers: { Cookie: `d=${xoxd}` },
17
+ });
18
+ return _client;
19
+ }
20
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/slack/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE9D,IAAI,OAAO,GAAqB,IAAI,CAAC;AAErC,MAAM,UAAU,cAAc;IAC5B,IAAI,OAAO;QAAE,OAAO,OAAO,CAAC;IAE5B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC;IAC/C,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC;IAE9C,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,aAAa,CAAC,SAAS,CAAC,cAAc,EAAE,uDAAuD,CAAC,CAAC;IAC7G,CAAC;IACD,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,aAAa,CAAC,SAAS,CAAC,cAAc,EAAE,uDAAuD,CAAC,CAAC;IAC7G,CAAC;IAED,OAAO,GAAG,IAAI,SAAS,CAAC,KAAK,EAAE;QAC7B,OAAO,EAAE,EAAE,MAAM,EAAE,KAAK,IAAI,EAAE,EAAE;KACjC,CAAC,CAAC;IACH,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { SendMessageParams, SendMessageResult } from '../types.js';
2
+ export declare function sendMessage(params: SendMessageParams): Promise<SendMessageResult>;
3
+ //# sourceMappingURL=send.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"send.d.ts","sourceRoot":"","sources":["../../src/slack/send.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAMxE,wBAAsB,WAAW,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA4EvF"}
@@ -0,0 +1,90 @@
1
+ import { SlackMcpError, ErrorCode } from '../utils/errors.js';
2
+ import { getSlackClient } from './client.js';
3
+ import { getChannelId } from '../cache/channels.js';
4
+ import { convertMentions } from '../mentions/resolver.js';
5
+ export async function sendMessage(params) {
6
+ const client = getSlackClient();
7
+ // Resolve mentions
8
+ const convertedMessage = await convertMentions(params.message, client);
9
+ // Resolve channel
10
+ const channelId = await getChannelId(params.channel_name, client);
11
+ if (!channelId) {
12
+ throw new SlackMcpError(ErrorCode.CHANNEL_NOT_FOUND, `Channel "#${params.channel_name}" not found. Check the channel name and ensure you have access.`);
13
+ }
14
+ // Build Block Kit blocks
15
+ const blocks = [
16
+ {
17
+ type: 'section',
18
+ text: { type: 'mrkdwn', text: convertedMessage },
19
+ },
20
+ {
21
+ type: 'context',
22
+ elements: [
23
+ { type: 'mrkdwn', text: ':robot_face: Sent by AI assistant' },
24
+ ],
25
+ },
26
+ ];
27
+ const baseArgs = {
28
+ channel: channelId,
29
+ unfurl_links: false,
30
+ unfurl_media: false,
31
+ ...(params.thread_ts ? { thread_ts: params.thread_ts } : {}),
32
+ };
33
+ try {
34
+ // Try with Block Kit first
35
+ const result = await client.chat.postMessage({
36
+ ...baseArgs,
37
+ text: convertedMessage,
38
+ blocks,
39
+ });
40
+ return {
41
+ status: 'success',
42
+ message: 'Message sent successfully',
43
+ channel_name: params.channel_name,
44
+ channel_id: channelId,
45
+ message_ts: result.ts,
46
+ sent_message: convertedMessage,
47
+ thread_ts: params.thread_ts,
48
+ };
49
+ }
50
+ catch (blockKitError) {
51
+ // Fallback to plain text without blocks
52
+ try {
53
+ const result = await client.chat.postMessage({
54
+ ...baseArgs,
55
+ text: `:robot_face: ${convertedMessage}`,
56
+ });
57
+ return {
58
+ status: 'success',
59
+ message: 'Message sent successfully (plain text fallback)',
60
+ channel_name: params.channel_name,
61
+ channel_id: channelId,
62
+ message_ts: result.ts,
63
+ sent_message: convertedMessage,
64
+ thread_ts: params.thread_ts,
65
+ };
66
+ }
67
+ catch {
68
+ // Both failed — report the original Block Kit error with specifics
69
+ const err = blockKitError instanceof Error ? blockKitError : new Error(String(blockKitError));
70
+ throw mapSlackError(err, params.channel_name);
71
+ }
72
+ }
73
+ }
74
+ function mapSlackError(error, channelName) {
75
+ const msg = error.message;
76
+ if (msg.includes('channel_not_found')) {
77
+ return new SlackMcpError(ErrorCode.CHANNEL_NOT_FOUND, `Channel "#${channelName}" not found. It may not exist or you may not have access.`);
78
+ }
79
+ if (msg.includes('not_in_channel')) {
80
+ return new SlackMcpError(ErrorCode.SEND_FAILED, `You are not a member of "#${channelName}". Join the channel first.`);
81
+ }
82
+ if (msg.includes('restricted_action')) {
83
+ return new SlackMcpError(ErrorCode.SEND_FAILED, `Restricted action: you don't have permission to post in "#${channelName}".`);
84
+ }
85
+ if (msg.includes('thread_not_found')) {
86
+ return new SlackMcpError(ErrorCode.SEND_FAILED, 'Thread not found. The thread_ts may be invalid or the parent message was deleted.');
87
+ }
88
+ return new SlackMcpError(ErrorCode.SEND_FAILED, `Slack API error: ${msg}`);
89
+ }
90
+ //# sourceMappingURL=send.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"send.js","sourceRoot":"","sources":["../../src/slack/send.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ export interface SendMessageParams {
2
+ channel_name: string;
3
+ message: string;
4
+ thread_ts?: string;
5
+ }
6
+ export interface SendMessageResult {
7
+ status: 'success' | 'error';
8
+ message: string;
9
+ channel_name?: string;
10
+ channel_id?: string;
11
+ message_ts?: string;
12
+ sent_message?: string;
13
+ thread_ts?: string;
14
+ }
15
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,14 @@
1
+ export declare const ErrorCode: {
2
+ readonly CHANNEL_NOT_FOUND: "CHANNEL_NOT_FOUND";
3
+ readonly SEND_FAILED: "SEND_FAILED";
4
+ readonly SYNC_FAILED: "SYNC_FAILED";
5
+ readonly CONFIG_MISSING: "CONFIG_MISSING";
6
+ };
7
+ export type ErrorCodeType = typeof ErrorCode[keyof typeof ErrorCode];
8
+ export declare class SlackMcpError extends Error {
9
+ readonly code: ErrorCodeType;
10
+ readonly details?: Record<string, unknown>;
11
+ constructor(code: ErrorCodeType, message: string, details?: Record<string, unknown>);
12
+ toJSON(): Record<string, unknown>;
13
+ }
14
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/utils/errors.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,SAAS;;;;;CAKZ,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,OAAO,SAAS,CAAC,MAAM,OAAO,SAAS,CAAC,CAAC;AAErE,qBAAa,aAAc,SAAQ,KAAK;IACtC,SAAgB,IAAI,EAAE,aAAa,CAAC;IACpC,SAAgB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;gBAEtC,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAWnF,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAQlC"}
@@ -0,0 +1,28 @@
1
+ export const ErrorCode = {
2
+ CHANNEL_NOT_FOUND: 'CHANNEL_NOT_FOUND',
3
+ SEND_FAILED: 'SEND_FAILED',
4
+ SYNC_FAILED: 'SYNC_FAILED',
5
+ CONFIG_MISSING: 'CONFIG_MISSING',
6
+ };
7
+ export class SlackMcpError extends Error {
8
+ code;
9
+ details;
10
+ constructor(code, message, details) {
11
+ super(message);
12
+ this.name = 'SlackMcpError';
13
+ this.code = code;
14
+ this.details = details;
15
+ if (Error.captureStackTrace) {
16
+ Error.captureStackTrace(this, SlackMcpError);
17
+ }
18
+ }
19
+ toJSON() {
20
+ return {
21
+ name: this.name,
22
+ code: this.code,
23
+ message: this.message,
24
+ details: this.details,
25
+ };
26
+ }
27
+ }
28
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/utils/errors.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,SAAS,GAAG;IACvB,iBAAiB,EAAE,mBAAmB;IACtC,WAAW,EAAE,aAAa;IAC1B,WAAW,EAAE,aAAa;IAC1B,cAAc,EAAE,gBAAgB;CACxB,CAAC;AAIX,MAAM,OAAO,aAAc,SAAQ,KAAK;IACtB,IAAI,CAAgB;IACpB,OAAO,CAA2B;IAElD,YAAY,IAAmB,EAAE,OAAe,EAAE,OAAiC;QACjF,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QAEvB,IAAI,KAAK,CAAC,iBAAiB,EAAE,CAAC;YAC5B,KAAK,CAAC,iBAAiB,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC;IAED,MAAM;QACJ,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,OAAO,EAAE,IAAI,CAAC,OAAO;SACtB,CAAC;IACJ,CAAC;CACF"}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@pilat/mcp-notify",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for sending Slack messages as user (xoxc + cookie auth)",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "mcp-notify": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "prepare": "npm run build",
13
+ "prepublishOnly": "npm run lint",
14
+ "test": "vitest --run --passWithNoTests",
15
+ "lint": "eslint src"
16
+ },
17
+ "keywords": [
18
+ "mcp",
19
+ "slack",
20
+ "claude",
21
+ "claude-code",
22
+ "model-context-protocol"
23
+ ],
24
+ "author": "Vladimir Urushev",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/pilat/mcp-notify"
28
+ },
29
+ "license": "MIT",
30
+ "homepage": "https://github.com/pilat/mcp-notify",
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "engines": {
38
+ "node": ">=22.0.0"
39
+ },
40
+ "dependencies": {
41
+ "@modelcontextprotocol/sdk": "^1.0.0",
42
+ "@slack/web-api": "^7.0.0",
43
+ "better-sqlite3": "^12.6.0"
44
+ },
45
+ "devDependencies": {
46
+ "@eslint/js": "^9.39.2",
47
+ "@types/better-sqlite3": "^7.6.0",
48
+ "@types/node": "^22.0.0",
49
+ "eslint": "^9.39.2",
50
+ "typescript": "^5.3.0",
51
+ "typescript-eslint": "^8.51.0",
52
+ "vitest": "^4.0.17"
53
+ }
54
+ }