@zibby/skills 0.1.26 → 0.1.28
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/bin/mcp-lark.mjs +112 -1
- package/bin/mcp-slack.mjs +404 -0
- package/dist/chat-notify.js +7 -4
- package/dist/index.js +118 -115
- package/dist/lark.js +3 -2
- package/dist/package.json +2 -1
- package/dist/slack.js +4 -2
- package/docs/apps/agent-ops.md +114 -0
- package/docs/apps/deploy.md +120 -0
- package/docs/apps/index.md +74 -0
- package/docs/apps/managing.md +121 -0
- package/docs/cli-reference.md +105 -0
- package/docs/intro.md +12 -0
- package/docs/recipes/index.md +1 -0
- package/docs/recipes/sentry-triage.md +93 -0
- package/package.json +2 -1
package/bin/mcp-lark.mjs
CHANGED
|
@@ -203,7 +203,118 @@ server.registerTool(
|
|
|
203
203
|
},
|
|
204
204
|
);
|
|
205
205
|
|
|
206
|
+
// ── lark_lookup_user_by_email ──────────────────────────────────────
|
|
207
|
+
server.registerTool(
|
|
208
|
+
'lark_lookup_user_by_email',
|
|
209
|
+
{
|
|
210
|
+
title: 'Find Lark User by Email',
|
|
211
|
+
description: 'Resolve a Lark user from their email. Returns { ok:true, user:{open_id,email,name} } on hit, { ok:false } on miss. Use the open_id as `receive_id` in lark_send_message to DM.',
|
|
212
|
+
inputSchema: z.object({
|
|
213
|
+
email: z.string().describe('Email address to look up'),
|
|
214
|
+
}),
|
|
215
|
+
},
|
|
216
|
+
async (args = {}) => {
|
|
217
|
+
try {
|
|
218
|
+
if (!args.email) {
|
|
219
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'email is required' }) }], isError: true };
|
|
220
|
+
}
|
|
221
|
+
const data = await larkApi(
|
|
222
|
+
'POST',
|
|
223
|
+
'/open-apis/contact/v3/users/batch_get_id?user_id_type=open_id',
|
|
224
|
+
{ emails: [args.email] },
|
|
225
|
+
);
|
|
226
|
+
const hit = (data.user_list || []).find((u) => u.email === args.email && u.user_id);
|
|
227
|
+
if (!hit) {
|
|
228
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, reason: 'no_lark_user_for_email' }) }] };
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
content: [{
|
|
232
|
+
type: 'text',
|
|
233
|
+
text: JSON.stringify({
|
|
234
|
+
ok: true,
|
|
235
|
+
user: { open_id: hit.user_id, email: hit.email, name: hit.name || undefined },
|
|
236
|
+
}),
|
|
237
|
+
}],
|
|
238
|
+
};
|
|
239
|
+
} catch (err) {
|
|
240
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
// ── lark_search_users ──────────────────────────────────────────────
|
|
246
|
+
server.registerTool(
|
|
247
|
+
'lark_search_users',
|
|
248
|
+
{
|
|
249
|
+
title: 'Search Lark Users by Name',
|
|
250
|
+
description: 'Fuzzy-search users by name across chats the bot is a member of. Lark has no public org-wide user search API for bots — this walks chat memberships and matches names client-side. Best for "send to Sam" style routing where you have a name but no email. Only users in chats with the bot are reachable.',
|
|
251
|
+
inputSchema: z.object({
|
|
252
|
+
query: z.string().describe('Substring to match against user names (case-insensitive)'),
|
|
253
|
+
limit: z.number().optional().describe('Max matches to return (default 5, max 25)'),
|
|
254
|
+
}),
|
|
255
|
+
},
|
|
256
|
+
async (args = {}) => {
|
|
257
|
+
try {
|
|
258
|
+
if (!args.query || typeof args.query !== 'string') {
|
|
259
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'query is required' }) }], isError: true };
|
|
260
|
+
}
|
|
261
|
+
const q = args.query.trim().toLowerCase();
|
|
262
|
+
if (!q) {
|
|
263
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: true, matches: [] }) }] };
|
|
264
|
+
}
|
|
265
|
+
const limit = Math.max(1, Math.min(Number(args.limit) || 5, 25));
|
|
266
|
+
const MAX_USERS = 200;
|
|
267
|
+
|
|
268
|
+
const chatRes = await larkApi('GET', '/open-apis/im/v1/chats?page_size=100');
|
|
269
|
+
const chatIds = (chatRes.items || []).map((c) => c.chat_id);
|
|
270
|
+
|
|
271
|
+
const seen = new Set();
|
|
272
|
+
const candidates = [];
|
|
273
|
+
for (const chatId of chatIds) {
|
|
274
|
+
if (candidates.length >= MAX_USERS) break;
|
|
275
|
+
try {
|
|
276
|
+
const members = await larkApi(
|
|
277
|
+
'GET',
|
|
278
|
+
`/open-apis/im/v1/chats/${encodeURIComponent(chatId)}/members?member_id_type=open_id&page_size=100`,
|
|
279
|
+
);
|
|
280
|
+
for (const m of members.items || []) {
|
|
281
|
+
if (!m.member_id || seen.has(m.member_id)) continue;
|
|
282
|
+
seen.add(m.member_id);
|
|
283
|
+
candidates.push({ open_id: m.member_id, name: m.name || '' });
|
|
284
|
+
if (candidates.length >= MAX_USERS) break;
|
|
285
|
+
}
|
|
286
|
+
} catch (e) {
|
|
287
|
+
console.error(`[mcp-lark] member scan failed for ${chatId}: ${e.message}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const matches = [];
|
|
292
|
+
for (const c of candidates) {
|
|
293
|
+
const name = (c.name || '').toLowerCase();
|
|
294
|
+
if (!name) continue;
|
|
295
|
+
let score = 0;
|
|
296
|
+
if (name.includes(q)) score += 100 - Math.abs(name.length - q.length);
|
|
297
|
+
if (name === q) score += 200;
|
|
298
|
+
if (score > 0) matches.push({ open_id: c.open_id, name: c.name, _score: score });
|
|
299
|
+
}
|
|
300
|
+
matches.sort((a, b) => b._score - a._score);
|
|
301
|
+
return {
|
|
302
|
+
content: [{
|
|
303
|
+
type: 'text',
|
|
304
|
+
text: JSON.stringify({
|
|
305
|
+
ok: true,
|
|
306
|
+
matches: matches.slice(0, limit).map(({ _score, ...m }) => m),
|
|
307
|
+
scanned: candidates.length,
|
|
308
|
+
}),
|
|
309
|
+
}],
|
|
310
|
+
};
|
|
311
|
+
} catch (err) {
|
|
312
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
);
|
|
316
|
+
|
|
206
317
|
const transport = new StdioServerTransport();
|
|
207
318
|
await server.connect(transport);
|
|
208
319
|
|
|
209
|
-
console.error('[mcp-lark] connected (
|
|
320
|
+
console.error('[mcp-lark] connected (6 tools: lark_send_message, lark_reply, lark_list_chats, lark_get_chat_history, lark_lookup_user_by_email, lark_search_users)');
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Zibby Slack MCP Server — standalone stdio MCP binary.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors bin/mcp-lark.mjs + bin/mcp-sentry.mjs: self-contained MCP
|
|
6
|
+
* server that exposes Slack tools to any MCP client (Claude Code,
|
|
7
|
+
* Cursor, Codex, Gemini). Skill's `resolve()` spawns this binary;
|
|
8
|
+
* everything else runs inside the spawned process.
|
|
9
|
+
*
|
|
10
|
+
* Why we ship our own instead of using
|
|
11
|
+
* `@modelcontextprotocol/server-slack`:
|
|
12
|
+
* 1. We need workspace-defined usergroups (@oncall, @platform) for
|
|
13
|
+
* routing — stock server-slack doesn't expose them.
|
|
14
|
+
* 2. We need users.lookupByEmail + a name-based fuzzy search for
|
|
15
|
+
* "send to Sam" style dispatch rules — stock server-slack only
|
|
16
|
+
* has users.list which the agent has to scan client-side every
|
|
17
|
+
* single call.
|
|
18
|
+
* 3. Single source of truth: in-process callers and MCP agents
|
|
19
|
+
* share the same tool surface (src/slack.js mirrors this file's
|
|
20
|
+
* tool set 1:1).
|
|
21
|
+
*
|
|
22
|
+
* Auth: reads PROJECT_API_TOKEN + PROGRESS_API_URL + EXECUTION_ID +
|
|
23
|
+
* PROJECT_ID + STAGE from the inherited env. The backend's
|
|
24
|
+
* resolveIntegrationToken('slack') endpoint returns the workspace
|
|
25
|
+
* bot token (xoxb-…); we cache it process-locally for the lifetime
|
|
26
|
+
* of the MCP server.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
30
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
31
|
+
import { z } from 'zod';
|
|
32
|
+
import { resolveIntegrationToken } from '@zibby/core/backend-client.js';
|
|
33
|
+
|
|
34
|
+
// Cache the bot token for the life of the MCP server process. Slack
|
|
35
|
+
// bot tokens don't expire, but each call to resolveIntegrationToken
|
|
36
|
+
// is a backend round-trip — cache once so 10 tool calls per
|
|
37
|
+
// dispatch run = 1 token fetch.
|
|
38
|
+
let cachedToken = null;
|
|
39
|
+
async function getSlackToken() {
|
|
40
|
+
if (cachedToken) return cachedToken;
|
|
41
|
+
const { token } = await resolveIntegrationToken('slack');
|
|
42
|
+
cachedToken = token;
|
|
43
|
+
return token;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Slack API endpoints that expect GET (query string) vs POST (JSON
|
|
47
|
+
// body). conversations.* and users.* are split across both — keep
|
|
48
|
+
// this list aligned with src/slack.js to avoid divergence.
|
|
49
|
+
const GET_METHODS = new Set([
|
|
50
|
+
'conversations.list',
|
|
51
|
+
'conversations.history',
|
|
52
|
+
'conversations.replies',
|
|
53
|
+
'users.list',
|
|
54
|
+
'users.profile.get',
|
|
55
|
+
'users.lookupByEmail',
|
|
56
|
+
'usergroups.list',
|
|
57
|
+
'usergroups.users.list',
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
async function slackApi(method, params = {}) {
|
|
61
|
+
const token = await getSlackToken();
|
|
62
|
+
const isGet = GET_METHODS.has(method);
|
|
63
|
+
let url = `https://slack.com/api/${method}`;
|
|
64
|
+
const headers = { Authorization: `Bearer ${token}` };
|
|
65
|
+
let body;
|
|
66
|
+
if (isGet) {
|
|
67
|
+
const qs = new URLSearchParams(params).toString();
|
|
68
|
+
if (qs) url += `?${qs}`;
|
|
69
|
+
} else {
|
|
70
|
+
headers['Content-Type'] = 'application/json; charset=utf-8';
|
|
71
|
+
body = JSON.stringify(params);
|
|
72
|
+
}
|
|
73
|
+
const res = await fetch(url, { method: isGet ? 'GET' : 'POST', headers, body });
|
|
74
|
+
const data = await res.json();
|
|
75
|
+
if (!data.ok) throw new Error(`Slack API error: ${data.error}`);
|
|
76
|
+
return data;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Helper — every handler wraps its JSON result in this shape so MCP
|
|
80
|
+
// emits a single text content block. Errors flip isError so the
|
|
81
|
+
// caller (agent) can short-circuit on them cleanly.
|
|
82
|
+
function ok(payload) {
|
|
83
|
+
return { content: [{ type: 'text', text: JSON.stringify(payload) }] };
|
|
84
|
+
}
|
|
85
|
+
function err(message) {
|
|
86
|
+
return { content: [{ type: 'text', text: `Error: ${message}` }], isError: true };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const server = new McpServer(
|
|
90
|
+
{ name: 'zibby-slack', version: '1.0.0' },
|
|
91
|
+
{ capabilities: { tools: {} } },
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// ── slack_list_channels ───────────────────────────────────────────
|
|
95
|
+
server.registerTool(
|
|
96
|
+
'slack_list_channels',
|
|
97
|
+
{
|
|
98
|
+
title: 'List Slack Channels',
|
|
99
|
+
description: 'List public channels in the workspace.',
|
|
100
|
+
inputSchema: z.object({}),
|
|
101
|
+
},
|
|
102
|
+
async () => {
|
|
103
|
+
try {
|
|
104
|
+
const data = await slackApi('conversations.list', { types: 'public_channel', limit: 100 });
|
|
105
|
+
return ok({
|
|
106
|
+
channels: (data.channels || []).map((c) => ({
|
|
107
|
+
id: c.id,
|
|
108
|
+
name: c.name,
|
|
109
|
+
topic: c.topic?.value,
|
|
110
|
+
})),
|
|
111
|
+
});
|
|
112
|
+
} catch (e) { return err(e.message); }
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// ── slack_post_message ────────────────────────────────────────────
|
|
117
|
+
server.registerTool(
|
|
118
|
+
'slack_post_message',
|
|
119
|
+
{
|
|
120
|
+
title: 'Post Slack Message',
|
|
121
|
+
description: 'Post a text message to a Slack channel OR direct-message a user. `channel` accepts a channel id (C…), a channel name with `#` prefix, OR a user id (U…) for DMs.',
|
|
122
|
+
inputSchema: z.object({
|
|
123
|
+
channel: z.string().describe('Channel id, channel name (#…), or user id (U…) for DMs'),
|
|
124
|
+
text: z.string().describe('Message text'),
|
|
125
|
+
}),
|
|
126
|
+
},
|
|
127
|
+
async (args = {}) => {
|
|
128
|
+
try {
|
|
129
|
+
if (!args.channel || !args.text) return err('channel and text are required');
|
|
130
|
+
const data = await slackApi('chat.postMessage', { channel: args.channel, text: args.text });
|
|
131
|
+
return ok({ ok: true, ts: data.ts, channel: data.channel });
|
|
132
|
+
} catch (e) { return err(e.message); }
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// ── slack_reply_to_thread ─────────────────────────────────────────
|
|
137
|
+
server.registerTool(
|
|
138
|
+
'slack_reply_to_thread',
|
|
139
|
+
{
|
|
140
|
+
title: 'Reply in Slack Thread',
|
|
141
|
+
description: 'Reply to a specific message thread.',
|
|
142
|
+
inputSchema: z.object({
|
|
143
|
+
channel: z.string().describe('Channel id'),
|
|
144
|
+
thread_ts: z.string().describe('Thread timestamp'),
|
|
145
|
+
text: z.string().describe('Reply text'),
|
|
146
|
+
}),
|
|
147
|
+
},
|
|
148
|
+
async (args = {}) => {
|
|
149
|
+
try {
|
|
150
|
+
if (!args.channel || !args.thread_ts || !args.text) return err('channel, thread_ts, and text are required');
|
|
151
|
+
const data = await slackApi('chat.postMessage', { channel: args.channel, thread_ts: args.thread_ts, text: args.text });
|
|
152
|
+
return ok({ ok: true, ts: data.ts });
|
|
153
|
+
} catch (e) { return err(e.message); }
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// ── slack_add_reaction ────────────────────────────────────────────
|
|
158
|
+
server.registerTool(
|
|
159
|
+
'slack_add_reaction',
|
|
160
|
+
{
|
|
161
|
+
title: 'Add Slack Reaction',
|
|
162
|
+
description: 'Add an emoji reaction to a message.',
|
|
163
|
+
inputSchema: z.object({
|
|
164
|
+
channel: z.string().describe('Channel id'),
|
|
165
|
+
timestamp: z.string().describe('Message timestamp'),
|
|
166
|
+
reaction: z.string().describe('Emoji name without colons'),
|
|
167
|
+
}),
|
|
168
|
+
},
|
|
169
|
+
async (args = {}) => {
|
|
170
|
+
try {
|
|
171
|
+
if (!args.channel || !args.timestamp || !args.reaction) return err('channel, timestamp, and reaction are required');
|
|
172
|
+
await slackApi('reactions.add', { channel: args.channel, timestamp: args.timestamp, name: args.reaction });
|
|
173
|
+
return ok({ ok: true });
|
|
174
|
+
} catch (e) { return err(e.message); }
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// ── slack_get_channel_history ─────────────────────────────────────
|
|
179
|
+
server.registerTool(
|
|
180
|
+
'slack_get_channel_history',
|
|
181
|
+
{
|
|
182
|
+
title: 'Get Slack Channel History',
|
|
183
|
+
description: 'Get recent messages from a channel.',
|
|
184
|
+
inputSchema: z.object({
|
|
185
|
+
channel: z.string().describe('Channel id'),
|
|
186
|
+
limit: z.number().optional().describe('Number of messages (default 20)'),
|
|
187
|
+
}),
|
|
188
|
+
},
|
|
189
|
+
async (args = {}) => {
|
|
190
|
+
try {
|
|
191
|
+
if (!args.channel) return err('channel is required');
|
|
192
|
+
const data = await slackApi('conversations.history', { channel: args.channel, limit: args.limit || 20 });
|
|
193
|
+
return ok({
|
|
194
|
+
messages: (data.messages || []).map((m) => ({ user: m.user, text: m.text, ts: m.ts })),
|
|
195
|
+
});
|
|
196
|
+
} catch (e) { return err(e.message); }
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// ── slack_get_thread_replies ──────────────────────────────────────
|
|
201
|
+
server.registerTool(
|
|
202
|
+
'slack_get_thread_replies',
|
|
203
|
+
{
|
|
204
|
+
title: 'Get Slack Thread Replies',
|
|
205
|
+
description: 'Get all replies in a message thread.',
|
|
206
|
+
inputSchema: z.object({
|
|
207
|
+
channel: z.string().describe('Channel id'),
|
|
208
|
+
thread_ts: z.string().describe('Thread timestamp'),
|
|
209
|
+
}),
|
|
210
|
+
},
|
|
211
|
+
async (args = {}) => {
|
|
212
|
+
try {
|
|
213
|
+
if (!args.channel || !args.thread_ts) return err('channel and thread_ts are required');
|
|
214
|
+
const data = await slackApi('conversations.replies', { channel: args.channel, ts: args.thread_ts });
|
|
215
|
+
return ok({
|
|
216
|
+
messages: (data.messages || []).map((m) => ({ user: m.user, text: m.text, ts: m.ts })),
|
|
217
|
+
});
|
|
218
|
+
} catch (e) { return err(e.message); }
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// ── slack_get_users ───────────────────────────────────────────────
|
|
223
|
+
server.registerTool(
|
|
224
|
+
'slack_get_users',
|
|
225
|
+
{
|
|
226
|
+
title: 'List Slack Users',
|
|
227
|
+
description: 'List workspace users with basic profiles. Use slack_search_users for fuzzy name matching or slack_lookup_user_by_email for exact email match — those are usually what you actually want.',
|
|
228
|
+
inputSchema: z.object({}),
|
|
229
|
+
},
|
|
230
|
+
async () => {
|
|
231
|
+
try {
|
|
232
|
+
const data = await slackApi('users.list', { limit: 100 });
|
|
233
|
+
return ok({
|
|
234
|
+
users: (data.members || [])
|
|
235
|
+
.filter((u) => !u.is_bot && !u.deleted)
|
|
236
|
+
.map((u) => ({ id: u.id, name: u.real_name || u.name })),
|
|
237
|
+
});
|
|
238
|
+
} catch (e) { return err(e.message); }
|
|
239
|
+
},
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// ── slack_get_user_profile ────────────────────────────────────────
|
|
243
|
+
server.registerTool(
|
|
244
|
+
'slack_get_user_profile',
|
|
245
|
+
{
|
|
246
|
+
title: 'Get Slack User Profile',
|
|
247
|
+
description: 'Get detailed profile for a specific user.',
|
|
248
|
+
inputSchema: z.object({
|
|
249
|
+
user_id: z.string().describe('Slack user id'),
|
|
250
|
+
}),
|
|
251
|
+
},
|
|
252
|
+
async (args = {}) => {
|
|
253
|
+
try {
|
|
254
|
+
if (!args.user_id) return err('user_id is required');
|
|
255
|
+
const data = await slackApi('users.profile.get', { user: args.user_id });
|
|
256
|
+
return ok({ profile: data.profile });
|
|
257
|
+
} catch (e) { return err(e.message); }
|
|
258
|
+
},
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// ── slack_lookup_user_by_email ────────────────────────────────────
|
|
262
|
+
server.registerTool(
|
|
263
|
+
'slack_lookup_user_by_email',
|
|
264
|
+
{
|
|
265
|
+
title: 'Find Slack User by Email',
|
|
266
|
+
description: 'Resolve a Slack user from their email address. Returns { ok:true, user:{id,name,email} } on hit, { ok:false } on miss (no exception — branch on `ok`). Prefer this over scanning slack_get_users / slack_search_users when you already have an exact email.',
|
|
267
|
+
inputSchema: z.object({
|
|
268
|
+
email: z.string().describe('Email address to look up'),
|
|
269
|
+
}),
|
|
270
|
+
},
|
|
271
|
+
async (args = {}) => {
|
|
272
|
+
try {
|
|
273
|
+
if (!args.email) return err('email is required');
|
|
274
|
+
try {
|
|
275
|
+
const data = await slackApi('users.lookupByEmail', { email: args.email });
|
|
276
|
+
return ok({
|
|
277
|
+
ok: true,
|
|
278
|
+
user: {
|
|
279
|
+
id: data.user?.id,
|
|
280
|
+
name: data.user?.real_name || data.user?.name,
|
|
281
|
+
email: data.user?.profile?.email || args.email,
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
} catch (e) {
|
|
285
|
+
if (/users_not_found/.test(e.message)) {
|
|
286
|
+
return ok({ ok: false, reason: 'users_not_found' });
|
|
287
|
+
}
|
|
288
|
+
throw e;
|
|
289
|
+
}
|
|
290
|
+
} catch (e) { return err(e.message); }
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
// ── slack_list_usergroups ─────────────────────────────────────────
|
|
295
|
+
server.registerTool(
|
|
296
|
+
'slack_list_usergroups',
|
|
297
|
+
{
|
|
298
|
+
title: 'List Slack Usergroups',
|
|
299
|
+
description: 'List workspace-defined user groups (e.g. @oncall, @platform). Each item has { id, handle, name, description, user_count }. Use the id with slack_get_usergroup_members to expand the membership, OR mention as <!subteam^ID> in a channel message.',
|
|
300
|
+
inputSchema: z.object({}),
|
|
301
|
+
},
|
|
302
|
+
async () => {
|
|
303
|
+
try {
|
|
304
|
+
const data = await slackApi('usergroups.list', {});
|
|
305
|
+
return ok({
|
|
306
|
+
usergroups: (data.usergroups || []).map((g) => ({
|
|
307
|
+
id: g.id,
|
|
308
|
+
handle: g.handle,
|
|
309
|
+
name: g.name,
|
|
310
|
+
description: g.description || '',
|
|
311
|
+
user_count: Number(g.user_count || 0),
|
|
312
|
+
})),
|
|
313
|
+
});
|
|
314
|
+
} catch (e) { return err(e.message); }
|
|
315
|
+
},
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
// ── slack_get_usergroup_members ───────────────────────────────────
|
|
319
|
+
server.registerTool(
|
|
320
|
+
'slack_get_usergroup_members',
|
|
321
|
+
{
|
|
322
|
+
title: 'List Slack Usergroup Members',
|
|
323
|
+
description: 'List user ids that belong to a Slack usergroup. Pair with slack_post_message to DM each member, or use the group id directly in a channel message as <!subteam^ID> to @-mention.',
|
|
324
|
+
inputSchema: z.object({
|
|
325
|
+
usergroup: z.string().describe('Usergroup id, e.g. S012ABC'),
|
|
326
|
+
}),
|
|
327
|
+
},
|
|
328
|
+
async (args = {}) => {
|
|
329
|
+
try {
|
|
330
|
+
if (!args.usergroup) return err('usergroup id is required');
|
|
331
|
+
const data = await slackApi('usergroups.users.list', { usergroup: args.usergroup });
|
|
332
|
+
return ok({ users: data.users || [] });
|
|
333
|
+
} catch (e) { return err(e.message); }
|
|
334
|
+
},
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
// ── slack_search_users ────────────────────────────────────────────
|
|
338
|
+
server.registerTool(
|
|
339
|
+
'slack_search_users',
|
|
340
|
+
{
|
|
341
|
+
title: 'Search Slack Users by Name',
|
|
342
|
+
description: 'Fuzzy-search workspace users by display name or real name. Use when the user said something like "send to Sam" without an email. Returns up to `limit` ranked matches { id, name, email }. Slack has no native name-search API — this scans paginated users.list + does substring scoring (real_name > display_name > name). For ambiguous results consider asking the user to confirm.',
|
|
343
|
+
inputSchema: z.object({
|
|
344
|
+
query: z.string().describe('Substring to match against names (case-insensitive)'),
|
|
345
|
+
limit: z.number().optional().describe('Max matches to return (default 5, max 25)'),
|
|
346
|
+
}),
|
|
347
|
+
},
|
|
348
|
+
async (args = {}) => {
|
|
349
|
+
try {
|
|
350
|
+
if (!args.query || typeof args.query !== 'string') return err('query is required');
|
|
351
|
+
const q = args.query.trim().toLowerCase();
|
|
352
|
+
if (!q) return ok({ ok: true, matches: [] });
|
|
353
|
+
const limit = Math.max(1, Math.min(Number(args.limit) || 5, 25));
|
|
354
|
+
|
|
355
|
+
// Paginate up to 5 pages × 200 users = 1000 cap. Workspaces with
|
|
356
|
+
// 10k+ users will miss the long tail — recommend lookupByEmail
|
|
357
|
+
// there, but keep this useful for the common 50-500 user case.
|
|
358
|
+
const all = [];
|
|
359
|
+
let cursor;
|
|
360
|
+
const MAX_PAGES = 5;
|
|
361
|
+
for (let page = 0; page < MAX_PAGES; page += 1) {
|
|
362
|
+
const params = { limit: 200 };
|
|
363
|
+
if (cursor) params.cursor = cursor;
|
|
364
|
+
const data = await slackApi('users.list', params);
|
|
365
|
+
for (const u of data.members || []) {
|
|
366
|
+
if (u.deleted || u.is_bot) continue;
|
|
367
|
+
all.push(u);
|
|
368
|
+
}
|
|
369
|
+
cursor = data.response_metadata?.next_cursor;
|
|
370
|
+
if (!cursor) break;
|
|
371
|
+
}
|
|
372
|
+
const matches = [];
|
|
373
|
+
for (const u of all) {
|
|
374
|
+
const realName = (u.real_name || '').toLowerCase();
|
|
375
|
+
const displayName = (u.profile?.display_name || '').toLowerCase();
|
|
376
|
+
const name = (u.name || '').toLowerCase();
|
|
377
|
+
let score = 0;
|
|
378
|
+
if (realName.includes(q)) score += 100 - Math.abs(realName.length - q.length);
|
|
379
|
+
if (displayName.includes(q)) score += 60 - Math.abs(displayName.length - q.length);
|
|
380
|
+
if (name.includes(q)) score += 30 - Math.abs(name.length - q.length);
|
|
381
|
+
if (realName === q || displayName === q) score += 200;
|
|
382
|
+
if (score > 0) {
|
|
383
|
+
matches.push({
|
|
384
|
+
id: u.id,
|
|
385
|
+
name: u.real_name || u.profile?.display_name || u.name,
|
|
386
|
+
email: u.profile?.email || undefined,
|
|
387
|
+
_score: score,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
matches.sort((a, b) => b._score - a._score);
|
|
392
|
+
return ok({
|
|
393
|
+
ok: true,
|
|
394
|
+
matches: matches.slice(0, limit).map(({ _score, ...m }) => m),
|
|
395
|
+
scanned: all.length,
|
|
396
|
+
});
|
|
397
|
+
} catch (e) { return err(e.message); }
|
|
398
|
+
},
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const transport = new StdioServerTransport();
|
|
402
|
+
await server.connect(transport);
|
|
403
|
+
|
|
404
|
+
console.error('[mcp-slack] connected (12 tools)');
|
package/dist/chat-notify.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import{resolveIntegrationToken as
|
|
1
|
+
import{existsSync as b}from"fs";import{fileURLToPath as S}from"url";import{dirname as v,resolve as N}from"path";import{resolveIntegrationToken as w}from"@zibby/core/backend-client.js";var y=Object.freeze({SENTRY:"sentry",JIRA:"jira",GITHUB:"github",GITLAB:"gitlab",SLACK:"slack",LARK:"lark",OPENAI_BILLING:"openai_billing",ANTHROPIC_BILLING:"anthropic_billing",CURSOR_ADMIN:"cursor_admin",NOTION:"notion"}),P=Object.freeze({sentry:{id:"sentry",name:"Sentry",connectPath:"/integrations?provider=sentry"},jira:{id:"jira",name:"Jira",connectPath:"/integrations?provider=jira"},github:{id:"github",name:"GitHub",connectPath:"/integrations?provider=github"},gitlab:{id:"gitlab",name:"GitLab",connectPath:"/integrations?provider=gitlab"},slack:{id:"slack",name:"Slack",connectPath:"/integrations?provider=slack"},lark:{id:"lark",name:"Lark",connectPath:"/integrations?provider=lark"},openai_billing:{id:"openai_billing",name:"OpenAI Admin",connectPath:"/integrations?provider=openai_billing"},anthropic_billing:{id:"anthropic_billing",name:"Anthropic Admin",connectPath:"/integrations?provider=anthropic_billing"},cursor_admin:{id:"cursor_admin",name:"Cursor Admin",connectPath:"/integrations?provider=cursor_admin"},notion:{id:"notion",name:"Notion",connectPath:"/integrations?provider=notion"}});function O(){if(process.env.MCP_SLACK_PATH)return process.env.MCP_SLACK_PATH;let r=v(S(import.meta.url)),e=N(r,"..","bin","mcp-slack.mjs");return b(e)?e:null}async function d(r,e={}){let{token:t}=await w("slack"),s=["conversations.list","users.list","users.profile.get","users.lookupByEmail","usergroups.list","usergroups.users.list","conversations.history","conversations.replies"].includes(r),n=`https://slack.com/api/${r}`,a={Authorization:`Bearer ${t}`},_;if(s){let l=new URLSearchParams(e).toString();l&&(n+=`?${l}`)}else a["Content-Type"]="application/json; charset=utf-8",_=JSON.stringify(e);let i=await(await fetch(n,{method:s?"GET":"POST",headers:a,body:_})).json();if(!i.ok)throw new Error(`Slack API error: ${i.error}`);return i}var m={id:"slack",serverName:"slack",allowedTools:["mcp__slack__*"],requiresIntegration:y.SLACK,envKeys:["SLACK_BOT_TOKEN","SLACK_TEAM_ID"],description:"Slack MCP Server",promptFragment:`## Slack (connected)
|
|
2
2
|
You have access to the user's Slack workspace. Use these tools:
|
|
3
3
|
- slack_list_channels, slack_post_message, slack_reply_to_thread
|
|
4
4
|
- slack_add_reaction, slack_get_channel_history, slack_get_thread_replies
|
|
5
|
-
- slack_get_users, slack_get_user_profile
|
|
5
|
+
- slack_get_users, slack_get_user_profile
|
|
6
|
+
- slack_lookup_user_by_email (precise email\u2192user_id, prefer this over scanning slack_get_users)
|
|
7
|
+
- slack_list_usergroups, slack_get_usergroup_members (workspace-defined teams like @oncall, @platform)`,resolve(){let r=O();if(!r)return null;let e={};for(let t of["PROJECT_API_TOKEN","PROGRESS_API_URL","EXECUTION_ID","PROJECT_ID","STAGE"])process.env[t]&&(e[t]=process.env[t]);for(let t of this.envKeys)process.env[t]&&(e[t]=process.env[t]);return{type:"stdio",command:"node",args:[r],env:e,alwaysLoad:!0}},async handleToolCall(r,e){try{switch(r){case"slack_list_channels":{let t=await d("conversations.list",{types:"public_channel",limit:100});return JSON.stringify({channels:(t.channels||[]).map(s=>({id:s.id,name:s.name,topic:s.topic?.value}))})}case"slack_post_message":{if(!e.channel||!e.text)return JSON.stringify({error:"channel and text are required"});let t=await d("chat.postMessage",{channel:e.channel,text:e.text});return JSON.stringify({ok:!0,ts:t.ts,channel:t.channel})}case"slack_reply_to_thread":{if(!e.channel||!e.thread_ts||!e.text)return JSON.stringify({error:"channel, thread_ts, and text are required"});let t=await d("chat.postMessage",{channel:e.channel,thread_ts:e.thread_ts,text:e.text});return JSON.stringify({ok:!0,ts:t.ts})}case"slack_add_reaction":return!e.channel||!e.timestamp||!e.reaction?JSON.stringify({error:"channel, timestamp, and reaction are required"}):(await d("reactions.add",{channel:e.channel,timestamp:e.timestamp,name:e.reaction}),JSON.stringify({ok:!0}));case"slack_get_channel_history":{if(!e.channel)return JSON.stringify({error:"channel is required"});let t=await d("conversations.history",{channel:e.channel,limit:e.limit||20});return JSON.stringify({messages:(t.messages||[]).map(s=>({user:s.user,text:s.text,ts:s.ts}))})}case"slack_get_thread_replies":{if(!e.channel||!e.thread_ts)return JSON.stringify({error:"channel and thread_ts are required"});let t=await d("conversations.replies",{channel:e.channel,ts:e.thread_ts});return JSON.stringify({messages:(t.messages||[]).map(s=>({user:s.user,text:s.text,ts:s.ts}))})}case"slack_get_users":{let t=await d("users.list",{limit:100});return JSON.stringify({users:(t.members||[]).filter(s=>!s.is_bot&&!s.deleted).map(s=>({id:s.id,name:s.real_name||s.name}))})}case"slack_get_user_profile":{if(!e.user_id)return JSON.stringify({error:"user_id is required"});let t=await d("users.profile.get",{user:e.user_id});return JSON.stringify({profile:t.profile})}case"slack_lookup_user_by_email":{if(!e.email)return JSON.stringify({error:"email is required"});try{let t=await d("users.lookupByEmail",{email:e.email});return JSON.stringify({ok:!0,user:{id:t.user?.id,name:t.user?.real_name||t.user?.name,email:t.user?.profile?.email||e.email}})}catch(t){if(/users_not_found/.test(t.message))return JSON.stringify({ok:!1,reason:"users_not_found"});throw t}}case"slack_list_usergroups":{let t=await d("usergroups.list",{});return JSON.stringify({usergroups:(t.usergroups||[]).map(s=>({id:s.id,handle:s.handle,name:s.name,description:s.description||"",user_count:Number(s.user_count||0)}))})}case"slack_get_usergroup_members":{if(!e.usergroup)return JSON.stringify({error:"usergroup id is required"});let t=await d("usergroups.users.list",{usergroup:e.usergroup});return JSON.stringify({users:t.users||[]})}case"slack_search_users":{if(!e.query||typeof e.query!="string")return JSON.stringify({error:"query is required"});let t=e.query.trim().toLowerCase();if(!t)return JSON.stringify({ok:!0,matches:[]});let s=Math.max(1,Math.min(Number(e.limit)||5,25)),n=[],a,_=5;for(let i=0;i<_;i+=1){let l={limit:200};a&&(l.cursor=a);let c=await d("users.list",l);for(let o of c.members||[])o.deleted||o.is_bot||n.push(o);if(a=c.response_metadata?.next_cursor,!a)break}let u=[];for(let i of n){let l=(i.real_name||"").toLowerCase(),c=(i.profile?.display_name||"").toLowerCase(),o=(i.name||"").toLowerCase(),p=0;l.includes(t)&&(p+=100-Math.abs(l.length-t.length)),c.includes(t)&&(p+=60-Math.abs(c.length-t.length)),o.includes(t)&&(p+=30-Math.abs(o.length-t.length)),(l===t||c===t)&&(p+=200),p>0&&u.push({id:i.id,name:i.real_name||i.profile?.display_name||i.name,email:i.profile?.email||void 0,_score:p})}return u.sort((i,l)=>l._score-i._score),JSON.stringify({ok:!0,matches:u.slice(0,s).map(({_score:i,...l})=>l),scanned:n.length})}default:return JSON.stringify({error:`Unknown tool: ${r}`})}}catch(t){return JSON.stringify({error:t.message})}},tools:[{name:"slack_list_channels",description:"List public channels in the workspace",input_schema:{type:"object",properties:{}}},{name:"slack_post_message",description:"Post a message to a Slack channel or DM",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID or name"},text:{type:"string",description:"Message text"}},required:["channel","text"]}},{name:"slack_reply_to_thread",description:"Reply to a specific message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"},text:{type:"string",description:"Reply text"}},required:["channel","thread_ts","text"]}},{name:"slack_add_reaction",description:"Add an emoji reaction to a message",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},timestamp:{type:"string",description:"Message timestamp"},reaction:{type:"string",description:"Emoji name without colons"}},required:["channel","timestamp","reaction"]}},{name:"slack_get_channel_history",description:"Get recent messages from a channel",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},limit:{type:"number",description:"Number of messages"}},required:["channel"]}},{name:"slack_get_thread_replies",description:"Get all replies in a message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"}},required:["channel","thread_ts"]}},{name:"slack_get_users",description:"List workspace users with basic profiles",input_schema:{type:"object",properties:{}}},{name:"slack_get_user_profile",description:"Get detailed profile for a specific user",input_schema:{type:"object",properties:{user_id:{type:"string",description:"Slack user ID"}},required:["user_id"]}},{name:"slack_lookup_user_by_email",description:"Find a Slack user by email. Returns { ok:true, user:{id,name,email} } on hit, { ok:false } when no user has that email. Prefer this over slack_get_users for email-based routing \u2014 single API call, exact match.",input_schema:{type:"object",properties:{email:{type:"string",description:"Email address to look up"}},required:["email"]}},{name:"slack_list_usergroups",description:"List workspace-defined user groups (e.g. @oncall, @platform). Each item has { id, handle, name, description, user_count }. Use the id with slack_get_usergroup_members to expand the membership.",input_schema:{type:"object",properties:{}}},{name:"slack_get_usergroup_members",description:"List user IDs that belong to a Slack usergroup. Pair with slack_post_message to DM each member, or use the group id directly in a channel message as <!subteam^ID> to @-mention.",input_schema:{type:"object",properties:{usergroup:{type:"string",description:"Usergroup id, e.g. S012ABC"}},required:["usergroup"]}},{name:"slack_search_users",description:'Fuzzy-search workspace users by display name or real name. Use when the user said something like "send to Sam" without an email. Returns up to `limit` ranked matches { id, name, email }. Slack has no native name-search API \u2014 this scans paginated users.list + does substring scoring (real_name > display_name > name). For large workspaces consider higher limit + ask the user to confirm if multiple hit.',input_schema:{type:"object",properties:{query:{type:"string",description:"Substring to match against names (case-insensitive)"},limit:{type:"number",description:"Max matches to return (default 5, max 25)"}},required:["query"]}}]};import{existsSync as T}from"fs";import{fileURLToPath as I}from"url";import{dirname as A,resolve as L}from"path";import{resolveIntegrationToken as C}from"@zibby/core/backend-client.js";function x(){if(process.env.MCP_LARK_PATH)return process.env.MCP_LARK_PATH;let r=A(I(import.meta.url)),e=L(r,"..","bin","mcp-lark.mjs");return T(e)?e:null}var E=6e3*1e3,g=null;async function R(){let{appId:r,appSecret:e,host:t}=await C("lark");if(g&&g.appId===r&&g.expiresAt>Date.now())return{token:g.token,host:t};let n=await(await fetch(`${t}/open-apis/auth/v3/tenant_access_token/internal`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({app_id:r,app_secret:e})})).json();if(n.code!==0)throw new Error(`Lark tenant_access_token failed: ${n.msg||n.code}`);return g={token:n.tenant_access_token,expiresAt:Date.now()+E,appId:r},{token:n.tenant_access_token,host:t}}async function f(r,e,t={}){let{token:s,host:n}=await R(),a=`${n}${e}`,_={method:r,headers:{Authorization:`Bearer ${s}`,"Content-Type":"application/json; charset=utf-8"}};r!=="GET"&&(_.body=JSON.stringify(t));let i=await(await fetch(a,_)).json();if(i.code!==0)throw new Error(`Lark API ${e} error: ${i.msg||i.code}`);return i.data||{}}function k(r){return JSON.stringify({text:r})}function J(r){return!r||typeof r!="string"||r.startsWith("oc_")?"chat_id":r.startsWith("ou_")?"open_id":r.startsWith("on_")?"union_id":r.startsWith("cli_")?"app_id":r.includes("@")?"email":"chat_id"}var h={id:"lark",serverName:"lark",allowedTools:["mcp__lark__*"],requiresIntegration:y.LARK,description:"Lark / Feishu messaging \u2014 send messages and reply in threads.",envKeys:[],promptFragment:`## Lark (connected)
|
|
6
8
|
You can send messages and replies on Lark. Use:
|
|
7
9
|
- lark_send_message: post a message to a chat, user, or DM
|
|
8
10
|
- lark_reply: reply to an existing message (threaded)
|
|
9
11
|
- lark_list_chats: list chats the bot is a member of
|
|
10
12
|
- lark_get_chat_history: fetch recent messages in a chat
|
|
11
|
-
|
|
13
|
+
- lark_lookup_user_by_email: resolve an email \u2192 open_id for direct DM (prefer this over emailing through lark_send_message when the agent has a user_id already)
|
|
14
|
+
When responding to an incoming event, prefer lark_reply with the source message_id so the response threads cleanly.`,resolve(){let r=x();if(!r)return null;let e={};for(let t of["PROJECT_API_TOKEN","PROGRESS_API_URL","EXECUTION_ID","PROJECT_ID","STAGE"])process.env[t]&&(e[t]=process.env[t]);return{type:"stdio",command:"node",args:[r],env:e,alwaysLoad:!0}},tools:[{name:"lark_send_message",description:"Send a text message to a Lark chat, user, or DM. receive_id can be a chat_id (oc_*), open_id (ou_*), union_id (on_*), or email.",input_schema:{type:"object",properties:{receive_id:{type:"string",description:"Target id: chat_id (oc_*), open_id (ou_*), union_id (on_*), or email"},text:{type:"string",description:"Message text"}},required:["receive_id","text"]}},{name:"lark_reply",description:"Reply to an existing Lark message (creates a thread). Use the message_id from the inbound event.",input_schema:{type:"object",properties:{message_id:{type:"string",description:"Lark message id (om_*) to reply to"},text:{type:"string",description:"Reply text"}},required:["message_id","text"]}},{name:"lark_list_chats",description:"List chats (groups + DMs) the bot is a member of.",input_schema:{type:"object",properties:{page_size:{type:"number",description:"Max results (default 50)"}}}},{name:"lark_get_chat_history",description:"Fetch recent messages in a chat.",input_schema:{type:"object",properties:{chat_id:{type:"string",description:"Chat id (oc_*)"},page_size:{type:"number",description:"Max messages (default 20)"}},required:["chat_id"]}},{name:"lark_lookup_user_by_email",description:"Resolve an email address to a Lark user id (open_id). Returns { ok:true, user:{open_id,email,name} } on hit, { ok:false } if no Lark user has that email. Use the open_id as `receive_id` in lark_send_message to DM.",input_schema:{type:"object",properties:{email:{type:"string",description:"Email address to look up"}},required:["email"]}},{name:"lark_search_users",description:'Fuzzy-search users by name across chats the bot is a member of. Lark has no public org-wide user search API for bots \u2014 this walks the bot\'s chat memberships and matches names client-side. Best for "send to Sam" style routing where you have a name but no email. Returns up to `limit` ranked matches { open_id, name }.',input_schema:{type:"object",properties:{query:{type:"string",description:"Substring to match against user names (case-insensitive)"},limit:{type:"number",description:"Max matches to return (default 5, max 25)"}},required:["query"]}}],async handleToolCall(r,e){try{switch(r){case"lark_send_message":{if(!e.receive_id||!e.text)return JSON.stringify({error:"receive_id and text are required"});let t=J(e.receive_id),s=await f("POST",`/open-apis/im/v1/messages?receive_id_type=${t}`,{receive_id:e.receive_id,msg_type:"text",content:k(e.text)});return JSON.stringify({ok:!0,message_id:s.message_id})}case"lark_reply":{if(!e.message_id||!e.text)return JSON.stringify({error:"message_id and text are required"});let t=await f("POST",`/open-apis/im/v1/messages/${encodeURIComponent(e.message_id)}/reply`,{msg_type:"text",content:k(e.text)});return JSON.stringify({ok:!0,message_id:t.message_id})}case"lark_list_chats":{let t=e.page_size||50,n=((await f("GET",`/open-apis/im/v1/chats?page_size=${t}`)).items||[]).map(a=>({chat_id:a.chat_id,name:a.name,description:a.description,owner_id:a.owner_id,chat_mode:a.chat_mode}));return JSON.stringify({chats:n})}case"lark_get_chat_history":{if(!e.chat_id)return JSON.stringify({error:"chat_id is required"});let t=e.page_size||20,n=((await f("GET",`/open-apis/im/v1/messages?container_id_type=chat&container_id=${encodeURIComponent(e.chat_id)}&page_size=${t}&sort_type=ByCreateTimeDesc`)).items||[]).map(a=>({message_id:a.message_id,sender_id:a.sender?.id,sender_type:a.sender?.sender_type,msg_type:a.msg_type,content:a.body?.content,create_time:a.create_time}));return JSON.stringify({messages:n})}case"lark_lookup_user_by_email":{if(!e.email)return JSON.stringify({error:"email is required"});let s=((await f("POST","/open-apis/contact/v3/users/batch_get_id?user_id_type=open_id",{emails:[e.email]})).user_list||[]).find(n=>n.email===e.email&&n.user_id);return JSON.stringify(s?{ok:!0,user:{open_id:s.user_id,email:s.email,name:s.name||void 0}}:{ok:!1,reason:"no_lark_user_for_email"})}case"lark_search_users":{if(!e.query||typeof e.query!="string")return JSON.stringify({error:"query is required"});let t=e.query.trim().toLowerCase();if(!t)return JSON.stringify({ok:!0,matches:[]});let s=Math.max(1,Math.min(Number(e.limit)||5,25)),n=200,_=((await f("GET","/open-apis/im/v1/chats?page_size=100")).items||[]).map(c=>c.chat_id),u=new Set,i=[];for(let c of _){if(i.length>=n)break;try{let o=await f("GET",`/open-apis/im/v1/chats/${encodeURIComponent(c)}/members?member_id_type=open_id&page_size=100`);for(let p of o.items||[])if(!(!p.member_id||u.has(p.member_id))&&(u.add(p.member_id),i.push({open_id:p.member_id,name:p.name||""}),i.length>=n))break}catch(o){console.warn(`[lark] member scan failed for ${c}: ${o.message}`)}}let l=[];for(let c of i){let o=(c.name||"").toLowerCase();if(!o)continue;let p=0;o.includes(t)&&(p+=100-Math.abs(o.length-t.length)),o===t&&(p+=200),p>0&&l.push({open_id:c.open_id,name:c.name,_score:p})}return l.sort((c,o)=>o._score-c._score),JSON.stringify({ok:!0,matches:l.slice(0,s).map(({_score:c,...o})=>o),scanned:i.length})}default:return JSON.stringify({error:`Unknown tool: ${r}`})}}catch(t){return JSON.stringify({error:t.message})}}};var X={id:"chat_notify",description:"Chat notification meta-skill \u2014 routes to whichever messaging integration (Slack OR Lark) the user has configured for this project.",envKeys:[...m.envKeys||[],...h.envKeys||[]],get serverName(){if(process.env.SLACK_CHANNEL)return m.serverName;if(process.env.LARK_RECEIVE_ID)return h.serverName},get allowedTools(){return process.env.SLACK_CHANNEL?m.allowedTools||[]:process.env.LARK_RECEIVE_ID?h.allowedTools||[]:[]},promptFragment:`## Chat notifications (Slack OR Lark \u2014 at least one connected)
|
|
12
15
|
You can post chat messages via either:
|
|
13
16
|
- slack_post_message (channel, text) \u2014 Slack, when SLACK_CHANNEL is set
|
|
14
17
|
- lark_send_message (receive_id, text) \u2014 Lark, when LARK_RECEIVE_ID is set
|
|
15
|
-
Use whichever the user has configured.`,resolve(
|
|
18
|
+
Use whichever the user has configured.`,resolve(r){return process.env.SLACK_CHANNEL&&typeof m.resolve=="function"?m.resolve(r):process.env.LARK_RECEIVE_ID&&typeof h.resolve=="function"?h.resolve(r):null},async handleToolCall(r,e,t){return typeof r=="string"&&r.startsWith("slack_")?m.handleToolCall(r,e,t):typeof r=="string"&&r.startsWith("lark_")?h.handleToolCall(r,e,t):JSON.stringify({error:`chat_notify: unknown tool "${r}". Expected slack_* or lark_*.`})},get tools(){return[...m.tools||[],...h.tools||[]]}};export{X as chatNotifySkill};
|