slack-analytics-mcp-server 0.0.0-stage → 0.2.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.
- package/README.md +152 -2
- package/dist/index.js +180 -0
- package/dist/slack.js +68 -0
- package/package.json +33 -3
package/README.md
CHANGED
|
@@ -1,3 +1,153 @@
|
|
|
1
|
-
#
|
|
1
|
+
# slack-analytics-mcp-server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
MCP server that lets Analytics Model workflows send Slack messages: dashboard links, alerts, and
|
|
4
|
+
"something new happened" notifications. It delivers messages only. The workflow decides *what* to send.
|
|
5
|
+
|
|
6
|
+
- **One process = one Slack workspace** (one bot token).
|
|
7
|
+
- Transport: stdio. Node 18+.
|
|
8
|
+
- Messages go to a channel, to people by work email (DM), or both.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Slack app setup (once per workspace)
|
|
13
|
+
|
|
14
|
+
1. Go to <https://api.slack.com/apps> → **Create New App** → *From scratch*.
|
|
15
|
+
2. **OAuth & Permissions → Bot Token Scopes**, add:
|
|
16
|
+
|
|
17
|
+
| Scope | Why |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `chat:write` | post messages |
|
|
20
|
+
| `chat:write.public` | post in public channels without inviting the bot |
|
|
21
|
+
| `users:read` | find users |
|
|
22
|
+
| `users:read.email` | find a user by email (needed for DMs) |
|
|
23
|
+
| `im:write` | open a DM |
|
|
24
|
+
| `channels:read` | list public channels (channel picker) |
|
|
25
|
+
|
|
26
|
+
3. **Install to Workspace** and copy the **Bot User OAuth Token** (`xoxb-…`).
|
|
27
|
+
4. For private channels, invite the bot (`/invite @YourApp`).
|
|
28
|
+
|
|
29
|
+
## 2. Run
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
SLACK_BOT_TOKEN=xoxb-your-token npx slack-analytics-mcp-server
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Env var | Required | Purpose |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `SLACK_BOT_TOKEN` | yes | Bot token of the workspace |
|
|
38
|
+
| `SLACK_API_URL` | no | Override the Slack API base URL (tests only) |
|
|
39
|
+
|
|
40
|
+
MCP client config example:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"mcpServers": {
|
|
45
|
+
"slack": {
|
|
46
|
+
"command": "npx",
|
|
47
|
+
"args": ["-y", "slack-analytics-mcp-server"],
|
|
48
|
+
"env": { "SLACK_BOT_TOKEN": "xoxb-your-token" }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 3. Tools
|
|
55
|
+
|
|
56
|
+
All tools return JSON as text. Failures return `isError: true` with a readable message.
|
|
57
|
+
|
|
58
|
+
### `get_workspace`
|
|
59
|
+
Checks the token. Call it right after a user saves a connection.
|
|
60
|
+
Input: none. Output: `{ ok, workspace, workspaceId, url, botUserId, botName }`.
|
|
61
|
+
|
|
62
|
+
### `lookup_user_by_email`
|
|
63
|
+
Input: `{ email }`. Output: `{ id, name, email, deleted, isBot }`.
|
|
64
|
+
|
|
65
|
+
### `list_channels`
|
|
66
|
+
Public channels, for a channel picker.
|
|
67
|
+
Input: `{ limit?: 1-200, query?: string }`. Output: `{ channels: [{ id, name, isMember }] }`.
|
|
68
|
+
|
|
69
|
+
### `send_dm`
|
|
70
|
+
Input: `{ email, text }`. Output: `{ email, userId, channel, ts }`.
|
|
71
|
+
|
|
72
|
+
### `post_to_channel`
|
|
73
|
+
Input: `{ channel, text }` (name with or without `#`, or id). Output: `{ channel, ts }`.
|
|
74
|
+
|
|
75
|
+
### `send_dashboard_link`
|
|
76
|
+
A card with a title, optional summary and an **Open dashboard** button.
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"url": "https://app.example.com/dashboard/42",
|
|
81
|
+
"title": "Churn analysis",
|
|
82
|
+
"message": "Churn rose 3% this week.",
|
|
83
|
+
"emails": ["priya@acme.com"],
|
|
84
|
+
"channel": "growth"
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### `send_notification`
|
|
89
|
+
Generic alert, e.g. "new record in MongoDB". Several items are sent as **one** message (first 10 shown, then "and N more").
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"title": "New dashboard created",
|
|
94
|
+
"text": "Created in the last 5 minutes",
|
|
95
|
+
"items": [
|
|
96
|
+
{ "title": "Churn v2", "url": "https://app.example.com/d/9",
|
|
97
|
+
"fields": { "Owner": "Priya", "Type": "trend", "Created": "2026-10-29 09:10" } }
|
|
98
|
+
],
|
|
99
|
+
"url": "https://app.example.com/dashboards",
|
|
100
|
+
"url_label": "Open dashboards",
|
|
101
|
+
"channel": "growth",
|
|
102
|
+
"emails": []
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Rules for both send tools:
|
|
107
|
+
- Give at least one of `channel` or `emails`.
|
|
108
|
+
- One failed recipient does not stop the others. The result is
|
|
109
|
+
`{ sent, failed, results: [{ to, sent, error? }] }`, and the call is an error only if **every** recipient failed.
|
|
110
|
+
- Text is escaped, so names containing `<`, `>` or `&` are safe.
|
|
111
|
+
- Slack rate limits (HTTP 429) are retried automatically, up to 3 tries.
|
|
112
|
+
|
|
113
|
+
## 4. How it fits the platform
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
Frontend ──(saves connection + node settings)──▶ Backend (FastAPI)
|
|
117
|
+
│ on schedule
|
|
118
|
+
├─ runs Mongo fetch (new records since last run)
|
|
119
|
+
├─ skips Slack if nothing is new
|
|
120
|
+
└─ starts this MCP with the workspace's token
|
|
121
|
+
└─ calls send_notification / send_dashboard_link
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- The frontend never talks to the MCP and never keeps the token. It stores a `connectionId`.
|
|
125
|
+
- The **backend** must remember the last-seen value per workflow (e.g. `createdAt` or `_id`) so nothing is announced twice. This server is stateless.
|
|
126
|
+
|
|
127
|
+
**Slack connection** (created once): display name + bot token.
|
|
128
|
+
**Slack node settings:** connection, channel and/or emails, title, fields to show, optional button link.
|
|
129
|
+
|
|
130
|
+
## 5. Troubleshooting
|
|
131
|
+
|
|
132
|
+
| Error | Meaning / fix |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `invalid_auth`, `not_authed` | Token wrong or revoked. |
|
|
135
|
+
| `missing_scope` | Add the scope in the Slack app and reinstall it. |
|
|
136
|
+
| `users_not_found` | No one in that workspace has that email. |
|
|
137
|
+
| `channel_not_found` | Wrong channel name or id, or a private channel the bot isn't in. |
|
|
138
|
+
| `not_in_channel` | Invite the bot, or add `chat:write.public`. |
|
|
139
|
+
| `is_archived` | The channel is archived. |
|
|
140
|
+
|
|
141
|
+
## 6. Develop and publish
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npm install
|
|
145
|
+
npm test # builds, then runs the server against a fake Slack API (12 checks)
|
|
146
|
+
npm publish # prepublishOnly runs the build
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Source: `src/index.ts` (tools), `src/slack.ts` (Slack client, message blocks).
|
|
150
|
+
|
|
151
|
+
## Changelog
|
|
152
|
+
- **0.2.0** – added `send_notification`.
|
|
153
|
+
- **0.1.0** – `get_workspace`, `lookup_user_by_email`, `list_channels`, `send_dm`, `post_to_channel`, `send_dashboard_link`.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { dashboardBlocks, isEmail, notificationBlocks, slack } from './slack.js';
|
|
6
|
+
// One process = one Slack workspace. The platform starts it with that workspace's bot token.
|
|
7
|
+
const TOKEN = process.env.SLACK_BOT_TOKEN || '';
|
|
8
|
+
const server = new McpServer({ name: 'slack-analytics-mcp-server', version: '0.2.0' });
|
|
9
|
+
const text = (v) => ({ content: [{ type: 'text', text: typeof v === 'string' ? v : JSON.stringify(v) }] });
|
|
10
|
+
const fail = (e) => ({ isError: true, content: [{ type: 'text', text: e instanceof Error ? e.message : String(e) }] });
|
|
11
|
+
const need = () => { if (!TOKEN)
|
|
12
|
+
throw new Error('SLACK_BOT_TOKEN is not set'); return TOKEN; };
|
|
13
|
+
async function dmByEmail(email, msg) {
|
|
14
|
+
const t = need();
|
|
15
|
+
const { user } = await slack(t, 'users.lookupByEmail', { email });
|
|
16
|
+
const { channel } = await slack(t, 'conversations.open', { users: user.id });
|
|
17
|
+
const sent = await slack(t, 'chat.postMessage', { channel: channel.id, ...msg, unfurl_links: false });
|
|
18
|
+
return { email, userId: user.id, channel: channel.id, ts: sent.ts };
|
|
19
|
+
}
|
|
20
|
+
server.registerTool('get_workspace', {
|
|
21
|
+
description: 'Check the connection. Returns the Slack workspace name, id and the bot user. Use it to validate a token when a user adds a workspace.',
|
|
22
|
+
inputSchema: {},
|
|
23
|
+
}, async () => {
|
|
24
|
+
try {
|
|
25
|
+
const r = await slack(need(), 'auth.test');
|
|
26
|
+
return text({ ok: true, workspace: r.team, workspaceId: r.team_id, url: r.url, botUserId: r.user_id, botName: r.user });
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
return fail(e);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
server.registerTool('lookup_user_by_email', {
|
|
33
|
+
description: 'Find a person in the workspace by their email address. Needs the users:read.email scope.',
|
|
34
|
+
inputSchema: { email: z.string().describe('Work email of the person') },
|
|
35
|
+
}, async ({ email }) => {
|
|
36
|
+
try {
|
|
37
|
+
if (!isEmail(email))
|
|
38
|
+
throw new Error(`"${email}" is not a valid email`);
|
|
39
|
+
const { user } = await slack(need(), 'users.lookupByEmail', { email });
|
|
40
|
+
return text({ id: user.id, name: user.real_name || user.name, email, deleted: Boolean(user.deleted), isBot: Boolean(user.is_bot) });
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
return fail(e);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
server.registerTool('list_channels', {
|
|
47
|
+
description: 'List public channels the bot can post to, so the user can pick one in the UI.',
|
|
48
|
+
inputSchema: { limit: z.number().int().min(1).max(200).optional(), query: z.string().optional().describe('Filter by name') },
|
|
49
|
+
}, async ({ limit, query }) => {
|
|
50
|
+
try {
|
|
51
|
+
const r = await slack(need(), 'conversations.list', { types: 'public_channel', exclude_archived: true, limit: limit ?? 200 });
|
|
52
|
+
const q = query?.toLowerCase();
|
|
53
|
+
const channels = r.channels.filter((c) => !q || c.name.includes(q)).map((c) => ({ id: c.id, name: c.name, isMember: Boolean(c.is_member) }));
|
|
54
|
+
return text({ channels });
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
return fail(e);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
server.registerTool('send_dm', {
|
|
61
|
+
description: 'Send a direct message to one person, found by email.',
|
|
62
|
+
inputSchema: { email: z.string(), text: z.string().describe('Message text (Slack mrkdwn)') },
|
|
63
|
+
}, async ({ email, text: body }) => {
|
|
64
|
+
try {
|
|
65
|
+
if (!isEmail(email))
|
|
66
|
+
throw new Error(`"${email}" is not a valid email`);
|
|
67
|
+
return text(await dmByEmail(email, { text: body }));
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
return fail(e);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
server.registerTool('post_to_channel', {
|
|
74
|
+
description: 'Post a message to a channel, by name (e.g. "growth") or id. The bot must be in the channel unless it has chat:write.public.',
|
|
75
|
+
inputSchema: { channel: z.string(), text: z.string() },
|
|
76
|
+
}, async ({ channel, text: body }) => {
|
|
77
|
+
try {
|
|
78
|
+
const r = await slack(need(), 'chat.postMessage', { channel: channel.replace(/^#/, ''), text: body, unfurl_links: false });
|
|
79
|
+
return text({ channel: r.channel, ts: r.ts });
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
return fail(e);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
server.registerTool('send_dashboard_link', {
|
|
86
|
+
description: 'Send a dashboard link to Slack. Give emails to DM each person and/or a channel to post in. One failure does not stop the others; the result lists every recipient with sent true/false.',
|
|
87
|
+
inputSchema: {
|
|
88
|
+
url: z.string().url().describe('Link to the dashboard'),
|
|
89
|
+
title: z.string().optional().describe('Dashboard or workflow name'),
|
|
90
|
+
message: z.string().optional().describe('Short summary shown above the button'),
|
|
91
|
+
emails: z.array(z.string()).optional().describe('People to DM, by work email'),
|
|
92
|
+
channel: z.string().optional().describe('Channel name or id to post in'),
|
|
93
|
+
},
|
|
94
|
+
}, async ({ url, title, message, emails = [], channel }) => {
|
|
95
|
+
try {
|
|
96
|
+
const t = need();
|
|
97
|
+
if (!emails.length && !channel)
|
|
98
|
+
throw new Error('Give at least one email or a channel');
|
|
99
|
+
const fallback = `${title || 'Your dashboard is ready'}: ${url}`;
|
|
100
|
+
const payload = { text: fallback, blocks: dashboardBlocks({ title, url, message }) };
|
|
101
|
+
const results = [];
|
|
102
|
+
for (const email of [...new Set(emails.map((e) => e.trim()).filter(Boolean))]) {
|
|
103
|
+
try {
|
|
104
|
+
if (!isEmail(email))
|
|
105
|
+
throw new Error('not a valid email');
|
|
106
|
+
await dmByEmail(email, payload);
|
|
107
|
+
results.push({ to: email, sent: true });
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
results.push({ to: email, sent: false, error: e instanceof Error ? e.message : String(e) });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (channel) {
|
|
114
|
+
try {
|
|
115
|
+
await slack(t, 'chat.postMessage', { channel: channel.replace(/^#/, ''), ...payload, unfurl_links: false });
|
|
116
|
+
results.push({ to: `#${channel.replace(/^#/, '')}`, sent: true });
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
results.push({ to: `#${channel.replace(/^#/, '')}`, sent: false, error: e instanceof Error ? e.message : String(e) });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const failed = results.filter((r) => !r.sent).length;
|
|
123
|
+
return { ...text({ sent: results.length - failed, failed, results }), ...(failed === results.length ? { isError: true } : {}) };
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
return fail(e);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
server.registerTool('send_notification', {
|
|
130
|
+
description: 'Send a notification about anything (a new record, an alert, a summary) to a channel and/or people by email. Pass the data as fields or items; several items are sent as ONE message. The caller decides WHAT is new; this tool only delivers it.',
|
|
131
|
+
inputSchema: {
|
|
132
|
+
title: z.string().describe('Headline, e.g. "New dashboard created"'),
|
|
133
|
+
text: z.string().optional().describe('Short sentence under the headline'),
|
|
134
|
+
items: z.array(z.object({
|
|
135
|
+
title: z.string().optional(),
|
|
136
|
+
url: z.string().url().optional(),
|
|
137
|
+
fields: z.record(z.string(), z.union([z.string(), z.number(), z.boolean(), z.null()])).optional(),
|
|
138
|
+
})).optional().describe('One entry per new record. Shown as name + key/value lines'),
|
|
139
|
+
url: z.string().url().optional().describe('Button link'),
|
|
140
|
+
url_label: z.string().optional(),
|
|
141
|
+
channel: z.string().optional().describe('Channel name or id'),
|
|
142
|
+
emails: z.array(z.string()).optional().describe('People to DM, by work email'),
|
|
143
|
+
},
|
|
144
|
+
}, async ({ title, text: body, items, url, url_label, channel, emails = [] }) => {
|
|
145
|
+
try {
|
|
146
|
+
const t = need();
|
|
147
|
+
if (!emails.length && !channel)
|
|
148
|
+
throw new Error('Give at least one email or a channel');
|
|
149
|
+
const count = items?.length ? ` (${items.length})` : '';
|
|
150
|
+
const payload = { text: `${title}${count}${body ? `: ${body}` : ''}`, blocks: notificationBlocks({ title: `${title}${count}`, text: body, items, url, urlLabel: url_label }) };
|
|
151
|
+
const results = [];
|
|
152
|
+
for (const email of [...new Set(emails.map((e) => e.trim()).filter(Boolean))]) {
|
|
153
|
+
try {
|
|
154
|
+
if (!isEmail(email))
|
|
155
|
+
throw new Error('not a valid email');
|
|
156
|
+
await dmByEmail(email, payload);
|
|
157
|
+
results.push({ to: email, sent: true });
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
results.push({ to: email, sent: false, error: e instanceof Error ? e.message : String(e) });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (channel) {
|
|
164
|
+
const name = channel.replace(/^#/, '');
|
|
165
|
+
try {
|
|
166
|
+
await slack(t, 'chat.postMessage', { channel: name, ...payload, unfurl_links: false });
|
|
167
|
+
results.push({ to: `#${name}`, sent: true });
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
results.push({ to: `#${name}`, sent: false, error: e instanceof Error ? e.message : String(e) });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const failed = results.filter((r) => !r.sent).length;
|
|
174
|
+
return { ...text({ sent: results.length - failed, failed, results }), ...(failed === results.length ? { isError: true } : {}) };
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
return fail(e);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
await server.connect(new StdioServerTransport());
|
package/dist/slack.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Thin Slack Web API client. One bot token = one workspace.
|
|
2
|
+
const BASE = (process.env.SLACK_API_URL || 'https://slack.com/api').replace(/\/$/, '');
|
|
3
|
+
export class SlackError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
method;
|
|
6
|
+
constructor(code, method) {
|
|
7
|
+
super(`Slack ${method} failed: ${code}${HINTS[code] ? ` (${HINTS[code]})` : ''}`);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.method = method;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const HINTS = {
|
|
13
|
+
invalid_auth: 'the bot token is wrong or revoked',
|
|
14
|
+
not_authed: 'no bot token was provided',
|
|
15
|
+
users_not_found: 'no one in this workspace has that email',
|
|
16
|
+
missing_scope: 'add the missing scope to the Slack app and reinstall it',
|
|
17
|
+
channel_not_found: 'check the channel name or id',
|
|
18
|
+
not_in_channel: 'invite the bot to the channel, or add the chat:write.public scope',
|
|
19
|
+
is_archived: 'the channel is archived',
|
|
20
|
+
};
|
|
21
|
+
export async function slack(token, method, params = {}, tries = 3) {
|
|
22
|
+
const res = await fetch(`${BASE}/${method}`, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json; charset=utf-8' },
|
|
25
|
+
body: JSON.stringify(params),
|
|
26
|
+
});
|
|
27
|
+
if (res.status === 429 && tries > 1) {
|
|
28
|
+
const wait = Math.min(Number(res.headers.get('retry-after')) || 1, 30);
|
|
29
|
+
await new Promise((r) => setTimeout(r, wait * 1000));
|
|
30
|
+
return slack(token, method, params, tries - 1);
|
|
31
|
+
}
|
|
32
|
+
if (!res.ok)
|
|
33
|
+
throw new SlackError(`http_${res.status}`, method);
|
|
34
|
+
const data = (await res.json());
|
|
35
|
+
if (!data.ok)
|
|
36
|
+
throw new SlackError(data.error || 'unknown_error', method);
|
|
37
|
+
return data;
|
|
38
|
+
}
|
|
39
|
+
export const isEmail = (s) => /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(s);
|
|
40
|
+
export function dashboardBlocks(opts) {
|
|
41
|
+
const title = opts.title || 'Your dashboard is ready';
|
|
42
|
+
return [
|
|
43
|
+
{ type: 'header', text: { type: 'plain_text', text: title.slice(0, 150) } },
|
|
44
|
+
...(opts.message ? [{ type: 'section', text: { type: 'mrkdwn', text: opts.message.slice(0, 2900) } }] : []),
|
|
45
|
+
{ type: 'actions', elements: [{ type: 'button', text: { type: 'plain_text', text: 'Open dashboard' }, url: opts.url, style: 'primary' }] },
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
const esc = (v) => String(v ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
49
|
+
// Generic notification card: title, optional text, one block of fields per item, optional button.
|
|
50
|
+
// Many items become ONE message so a burst of new records does not spam the channel.
|
|
51
|
+
export function notificationBlocks(o) {
|
|
52
|
+
const blocks = [{ type: 'header', text: { type: 'plain_text', text: o.title.slice(0, 150) } }];
|
|
53
|
+
if (o.text)
|
|
54
|
+
blocks.push({ type: 'section', text: { type: 'mrkdwn', text: o.text.slice(0, 2900) } });
|
|
55
|
+
const items = o.items || [];
|
|
56
|
+
for (const it of items.slice(0, 10)) {
|
|
57
|
+
const head = it.title ? (it.url ? `*<${it.url}|${esc(it.title)}>*` : `*${esc(it.title)}*`) : '';
|
|
58
|
+
const rows = Object.entries(it.fields || {}).slice(0, 10).map(([k, v]) => `*${esc(k)}:* ${esc(v)}`).join('\n');
|
|
59
|
+
const body = [head, rows].filter(Boolean).join('\n');
|
|
60
|
+
if (body)
|
|
61
|
+
blocks.push({ type: 'section', text: { type: 'mrkdwn', text: body.slice(0, 2900) } });
|
|
62
|
+
}
|
|
63
|
+
if (items.length > 10)
|
|
64
|
+
blocks.push({ type: 'context', elements: [{ type: 'mrkdwn', text: `and ${items.length - 10} more` }] });
|
|
65
|
+
if (o.url)
|
|
66
|
+
blocks.push({ type: 'actions', elements: [{ type: 'button', text: { type: 'plain_text', text: (o.urlLabel || 'Open').slice(0, 75) }, url: o.url }] });
|
|
67
|
+
return blocks;
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "slack-analytics-mcp-server",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MCP server that lets Analytics Model workflows send dashboard links and messages to Slack (DM by email, or post to a channel).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"slack-analytics-mcp-server": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"test": "npm run build && node test/smoke.mjs",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/Analytics-model-LTD/slack-analytics-mcp-server.git"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/Analytics-model-LTD/slack-analytics-mcp-server#readme",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
30
|
+
"zod": "^4.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^22.0.0",
|
|
34
|
+
"typescript": "^5.6.0"
|
|
35
|
+
}
|
|
6
36
|
}
|