claude-threads 1.14.1 → 1.15.1
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/CHANGELOG.md +10 -0
- package/dist/index.js +103 -7
- package/dist/mcp/mcp-server.js +360 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.15.1] - 2026-05-05
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Numeric tool args no longer fail at the MCP boundary when the runtime sends them as strings.** Surfaced live during dogfooding of v1.15.0: calling `search_messages` with `max_results: 5` returned `Invalid input: expected number, received string` from the MCP framework, because the Claude MCP runtime sometimes serializes integer tool arguments as JSON strings before they reach the server, and the receiving `z.number().int()` schema rejected them at parse time. Switched the four affected fields to `z.coerce.number().int()` — `read_post.max_messages`, `list_thread.max_messages`, `read_channel_history.max_messages`, `search_messages.max_results` — so either form parses. Downstream `clamp*` helpers already defend against non-finite / non-positive values, so coercion can't widen the contract beyond the documented caps; non-integer strings like `"1.5"` still fail because `.int()` runs after coercion. Schemas exported so a contract test can verify the coercion without spinning up the full MCP transport. (#375)
|
|
14
|
+
|
|
15
|
+
## [1.15.0] - 2026-05-05
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **Claude can DM channel members directly via the new `send_dm` MCP tool.** When the user asks for a private ping ("DM me when this finishes," "send the report to alice as a DM"), Claude can now call `send_dm(recipient, message)` instead of asking the user to forward the result themselves. The recipient is a Mattermost username (`@anne` or `anne`) or a Slack user id (`U…`/`<@U…>`) — the asymmetry exists because Slack bot tokens can't reverse-look usernames cheaply, and paginating `users.list` per call is wasteful. Six gates run in order: shape (recipient and message non-empty, message under 4000-char cap), recipient resolution (platform API turns the input into a user id + canonical username), self-DM guard, channel membership (recipient must be a current member of the bot channel, fetched once and cached for 60s), rate limit (3 DMs per recipient per session, optimistic counter increment with rollback on deny / timeout / send-error), and a per-recipient interactive permission prompt the first time the session DMs each user. ✅ promotes that recipient — and only that recipient — to no-prompt for the rest of the session; the rate limit still applies. An in-flight set blocks parallel `send_dm` calls to the same recipient from posting duplicate prompts when Claude fans out tool_use blocks in a single turn. Every DM is prefixed with an attribution line — `_(automated message via claude-threads, on behalf of @anne from #channel)_` — so recipients can trace it back to the session that sent it; the channel name is fetched lazily via the platform's channel-info endpoint, the session-owner username is plumbed in from `session.startedBy` through a new `SESSION_OWNER_USERNAME` env var that threads `lifecycle.ts` → `restart-options.ts` → `ClaudeCliOptions` → `buildPermissionArgs` → MCP child (covers all five `new ClaudeCli` sites). New optional `McpPlatformApi` methods `getChannelMembers`, `resolveRecipient`, `sendDirectMessage`; `getChannelInfo` gained a `name?` field. RED-GREEN tests on every load-bearing guard — self-DM check, membership, rate limit, attribution prefix, allow-all-is-per-recipient, counter rollback on deny / timeout, in-flight-prompt deduplication. (#374)
|
|
19
|
+
|
|
10
20
|
## [1.14.1] - 2026-05-05
|
|
11
21
|
|
|
12
22
|
### Fixed
|
package/dist/index.js
CHANGED
|
@@ -51826,6 +51826,22 @@ async function addReaction(config, postId, userId, emojiName) {
|
|
|
51826
51826
|
emoji_name: emojiName
|
|
51827
51827
|
});
|
|
51828
51828
|
}
|
|
51829
|
+
async function getUserByUsernameRaw(config, username) {
|
|
51830
|
+
try {
|
|
51831
|
+
return await mattermostApi(config, "GET", `/users/username/${encodeURIComponent(username)}`);
|
|
51832
|
+
} catch (err) {
|
|
51833
|
+
apiLog.debug(`Failed to lookup user @${username}: ${err}`);
|
|
51834
|
+
return null;
|
|
51835
|
+
}
|
|
51836
|
+
}
|
|
51837
|
+
async function createDirectChannelRaw(config, userIdA, userIdB) {
|
|
51838
|
+
try {
|
|
51839
|
+
return await mattermostApi(config, "POST", "/channels/direct", [userIdA, userIdB]);
|
|
51840
|
+
} catch (err) {
|
|
51841
|
+
apiLog.debug(`Failed to open direct channel ${userIdA}↔${userIdB}: ${err}`);
|
|
51842
|
+
return null;
|
|
51843
|
+
}
|
|
51844
|
+
}
|
|
51829
51845
|
function isUserInAllowList(username, allowList) {
|
|
51830
51846
|
if (allowList.length === 0)
|
|
51831
51847
|
return true;
|
|
@@ -52033,7 +52049,33 @@ class MattermostMcpPlatformApi {
|
|
|
52033
52049
|
return null;
|
|
52034
52050
|
const channelType = channel.type === "O" ? "public" : "private";
|
|
52035
52051
|
this.channelTypeCache.set(channelId, channelType);
|
|
52036
|
-
|
|
52052
|
+
const name = channel.display_name || channel.name;
|
|
52053
|
+
return { id: channel.id, channelType, name };
|
|
52054
|
+
}
|
|
52055
|
+
async getChannelMembers(channelId) {
|
|
52056
|
+
try {
|
|
52057
|
+
const members = await mattermostApi(this.apiConfig, "GET", `/channels/${channelId}/members?per_page=200`);
|
|
52058
|
+
return members.map((m) => m.user_id);
|
|
52059
|
+
} catch (err) {
|
|
52060
|
+
mcpLogger.debug(`getChannelMembers ${channelId} failed: ${err}`);
|
|
52061
|
+
return null;
|
|
52062
|
+
}
|
|
52063
|
+
}
|
|
52064
|
+
async resolveRecipient(recipient) {
|
|
52065
|
+
const normalized = recipient.replace(/^@/, "");
|
|
52066
|
+
const user = await getUserByUsernameRaw(this.apiConfig, normalized);
|
|
52067
|
+
if (!user)
|
|
52068
|
+
return null;
|
|
52069
|
+
return { id: user.id, username: user.username };
|
|
52070
|
+
}
|
|
52071
|
+
async sendDirectMessage(recipientUserId, message) {
|
|
52072
|
+
const botUserId = await this.getBotUserId();
|
|
52073
|
+
const dmChannel = await createDirectChannelRaw(this.apiConfig, botUserId, recipientUserId);
|
|
52074
|
+
if (!dmChannel) {
|
|
52075
|
+
throw new Error("failed to open direct channel with recipient");
|
|
52076
|
+
}
|
|
52077
|
+
const post = await createPost(this.apiConfig, dmChannel.id, message);
|
|
52078
|
+
return { postId: post.id };
|
|
52037
52079
|
}
|
|
52038
52080
|
async searchMessages(query, options) {
|
|
52039
52081
|
const limit = options?.limit ?? 10;
|
|
@@ -52393,13 +52435,62 @@ class SlackMcpPlatformApi {
|
|
|
52393
52435
|
const isPrivate = ch.is_private || ch.is_im || ch.is_mpim || false;
|
|
52394
52436
|
return {
|
|
52395
52437
|
id: ch.id,
|
|
52396
|
-
channelType: isPrivate ? "private" : "public"
|
|
52438
|
+
channelType: isPrivate ? "private" : "public",
|
|
52439
|
+
name: ch.name
|
|
52397
52440
|
};
|
|
52398
52441
|
} catch (err) {
|
|
52399
52442
|
mcpLogger.debug(`getChannelInfo ${channelId} failed: ${err}`);
|
|
52400
52443
|
return null;
|
|
52401
52444
|
}
|
|
52402
52445
|
}
|
|
52446
|
+
async getChannelMembers(channelId) {
|
|
52447
|
+
const maxPages = 20;
|
|
52448
|
+
const all = [];
|
|
52449
|
+
let cursor = undefined;
|
|
52450
|
+
try {
|
|
52451
|
+
for (let i2 = 0;i2 < maxPages; i2++) {
|
|
52452
|
+
const params = {
|
|
52453
|
+
channel: channelId,
|
|
52454
|
+
limit: 1000
|
|
52455
|
+
};
|
|
52456
|
+
if (cursor)
|
|
52457
|
+
params.cursor = cursor;
|
|
52458
|
+
const response = await slackApi("conversations.members", this.config.botToken, params);
|
|
52459
|
+
all.push(...response.members ?? []);
|
|
52460
|
+
cursor = response.response_metadata?.next_cursor;
|
|
52461
|
+
if (!cursor)
|
|
52462
|
+
return all;
|
|
52463
|
+
}
|
|
52464
|
+
mcpLogger.warn(`getChannelMembers ${channelId} hit page cap of ${maxPages}`);
|
|
52465
|
+
return all;
|
|
52466
|
+
} catch (err) {
|
|
52467
|
+
mcpLogger.debug(`getChannelMembers ${channelId} failed: ${err}`);
|
|
52468
|
+
return null;
|
|
52469
|
+
}
|
|
52470
|
+
}
|
|
52471
|
+
async resolveRecipient(recipient) {
|
|
52472
|
+
const id = recipient.replace(/^<@/, "").replace(/>$/, "");
|
|
52473
|
+
if (!/^[UW][A-Z0-9]{8,}$/.test(id)) {
|
|
52474
|
+
return null;
|
|
52475
|
+
}
|
|
52476
|
+
try {
|
|
52477
|
+
const response = await slackApi("users.info", this.config.botToken, { user: id });
|
|
52478
|
+
return { id: response.user.id, username: response.user.name ?? null };
|
|
52479
|
+
} catch (err) {
|
|
52480
|
+
mcpLogger.debug(`resolveRecipient ${id} failed: ${err}`);
|
|
52481
|
+
return null;
|
|
52482
|
+
}
|
|
52483
|
+
}
|
|
52484
|
+
async sendDirectMessage(recipientUserId, message) {
|
|
52485
|
+
const opened = await slackApi("conversations.open", this.config.botToken, { users: recipientUserId });
|
|
52486
|
+
const dmChannelId = opened.channel.id;
|
|
52487
|
+
const post = await slackApi("chat.postMessage", this.config.botToken, {
|
|
52488
|
+
channel: dmChannelId,
|
|
52489
|
+
text: message,
|
|
52490
|
+
mrkdwn: true
|
|
52491
|
+
});
|
|
52492
|
+
return { postId: post.ts };
|
|
52493
|
+
}
|
|
52403
52494
|
}
|
|
52404
52495
|
function slackMessageToMcpPost(message, channelId, username) {
|
|
52405
52496
|
const createAt = Math.floor(parseFloat(message.ts) * 1000);
|
|
@@ -53576,7 +53667,8 @@ function buildPermissionArgs(opts) {
|
|
|
53576
53667
|
PLATFORM_THREAD_ID: opts.threadId || "",
|
|
53577
53668
|
ALLOWED_USERS: opts.platformConfig.allowedUsers.join(","),
|
|
53578
53669
|
DEBUG: opts.debug ? "1" : "",
|
|
53579
|
-
PERMISSION_TIMEOUT_MS: String(opts.permissionTimeoutMs)
|
|
53670
|
+
PERMISSION_TIMEOUT_MS: String(opts.permissionTimeoutMs),
|
|
53671
|
+
SESSION_OWNER_USERNAME: opts.sessionOwnerUsername || ""
|
|
53580
53672
|
};
|
|
53581
53673
|
if (opts.platformConfig.appToken) {
|
|
53582
53674
|
mcpEnv.PLATFORM_APP_TOKEN = opts.platformConfig.appToken;
|
|
@@ -53715,7 +53807,8 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
53715
53807
|
debug: this.debug,
|
|
53716
53808
|
workingDir: this.options.workingDir,
|
|
53717
53809
|
uploadDir: this.options.uploadDir,
|
|
53718
|
-
outboundFiles: this.options.outboundFiles
|
|
53810
|
+
outboundFiles: this.options.outboundFiles,
|
|
53811
|
+
sessionOwnerUsername: this.options.sessionOwnerUsername
|
|
53719
53812
|
});
|
|
53720
53813
|
args.push(...permResult.args);
|
|
53721
53814
|
this.mcpConfigTempFile = permResult.tempFile;
|
|
@@ -55356,7 +55449,8 @@ function buildRestartCliOptions(session, ctx) {
|
|
|
55356
55449
|
permissionTimeoutMs: ctx.permissionTimeoutMs,
|
|
55357
55450
|
account: ctx.account,
|
|
55358
55451
|
uploadDir: getSessionUploadDir(session.platformId, session.threadId),
|
|
55359
|
-
outboundFiles: platformMcpConfig.outboundFiles
|
|
55452
|
+
outboundFiles: platformMcpConfig.outboundFiles,
|
|
55453
|
+
sessionOwnerUsername: session.startedBy
|
|
55360
55454
|
};
|
|
55361
55455
|
}
|
|
55362
55456
|
|
|
@@ -66579,7 +66673,8 @@ async function startSession(options, username, displayName, replyToPostId, platf
|
|
|
66579
66673
|
permissionTimeoutMs: ctx.config.permissionTimeoutMs,
|
|
66580
66674
|
account: claudeAccount ? { id: claudeAccount.id, home: claudeAccount.home, apiKey: claudeAccount.apiKey } : undefined,
|
|
66581
66675
|
uploadDir: getSessionUploadDir(platformId, actualThreadId),
|
|
66582
|
-
outboundFiles: platformMcpConfig.outboundFiles
|
|
66676
|
+
outboundFiles: platformMcpConfig.outboundFiles,
|
|
66677
|
+
sessionOwnerUsername: username
|
|
66583
66678
|
};
|
|
66584
66679
|
const claude = new ClaudeCli(cliOptions);
|
|
66585
66680
|
const session = {
|
|
@@ -66730,7 +66825,8 @@ Please start a new session.`), { action: "Post resume failure notification" });
|
|
|
66730
66825
|
permissionTimeoutMs: ctx.config.permissionTimeoutMs,
|
|
66731
66826
|
account: claudeAccount ? { id: claudeAccount.id, home: claudeAccount.home, apiKey: claudeAccount.apiKey } : undefined,
|
|
66732
66827
|
uploadDir: getSessionUploadDir(platformId, state.threadId),
|
|
66733
|
-
outboundFiles: platformMcpConfig.outboundFiles
|
|
66828
|
+
outboundFiles: platformMcpConfig.outboundFiles,
|
|
66829
|
+
sessionOwnerUsername: state.startedBy
|
|
66734
66830
|
};
|
|
66735
66831
|
const claude = new ClaudeCli(cliOptions);
|
|
66736
66832
|
const session = {
|
package/dist/mcp/mcp-server.js
CHANGED
|
@@ -54866,7 +54866,8 @@ function buildPermissionArgs(opts) {
|
|
|
54866
54866
|
PLATFORM_THREAD_ID: opts.threadId || "",
|
|
54867
54867
|
ALLOWED_USERS: opts.platformConfig.allowedUsers.join(","),
|
|
54868
54868
|
DEBUG: opts.debug ? "1" : "",
|
|
54869
|
-
PERMISSION_TIMEOUT_MS: String(opts.permissionTimeoutMs)
|
|
54869
|
+
PERMISSION_TIMEOUT_MS: String(opts.permissionTimeoutMs),
|
|
54870
|
+
SESSION_OWNER_USERNAME: opts.sessionOwnerUsername || ""
|
|
54870
54871
|
};
|
|
54871
54872
|
if (opts.platformConfig.appToken) {
|
|
54872
54873
|
mcpEnv.PLATFORM_APP_TOKEN = opts.platformConfig.appToken;
|
|
@@ -55005,7 +55006,8 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
55005
55006
|
debug: this.debug,
|
|
55006
55007
|
workingDir: this.options.workingDir,
|
|
55007
55008
|
uploadDir: this.options.uploadDir,
|
|
55008
|
-
outboundFiles: this.options.outboundFiles
|
|
55009
|
+
outboundFiles: this.options.outboundFiles,
|
|
55010
|
+
sessionOwnerUsername: this.options.sessionOwnerUsername
|
|
55009
55011
|
});
|
|
55010
55012
|
args.push(...permResult.args);
|
|
55011
55013
|
this.mcpConfigTempFile = permResult.tempFile;
|
|
@@ -56129,6 +56131,22 @@ async function addReaction(config3, postId, userId, emojiName) {
|
|
|
56129
56131
|
emoji_name: emojiName
|
|
56130
56132
|
});
|
|
56131
56133
|
}
|
|
56134
|
+
async function getUserByUsernameRaw(config3, username) {
|
|
56135
|
+
try {
|
|
56136
|
+
return await mattermostApi(config3, "GET", `/users/username/${encodeURIComponent(username)}`);
|
|
56137
|
+
} catch (err) {
|
|
56138
|
+
apiLog.debug(`Failed to lookup user @${username}: ${err}`);
|
|
56139
|
+
return null;
|
|
56140
|
+
}
|
|
56141
|
+
}
|
|
56142
|
+
async function createDirectChannelRaw(config3, userIdA, userIdB) {
|
|
56143
|
+
try {
|
|
56144
|
+
return await mattermostApi(config3, "POST", "/channels/direct", [userIdA, userIdB]);
|
|
56145
|
+
} catch (err) {
|
|
56146
|
+
apiLog.debug(`Failed to open direct channel ${userIdA}↔${userIdB}: ${err}`);
|
|
56147
|
+
return null;
|
|
56148
|
+
}
|
|
56149
|
+
}
|
|
56132
56150
|
function isUserInAllowList(username, allowList) {
|
|
56133
56151
|
if (allowList.length === 0)
|
|
56134
56152
|
return true;
|
|
@@ -56336,7 +56354,33 @@ class MattermostMcpPlatformApi {
|
|
|
56336
56354
|
return null;
|
|
56337
56355
|
const channelType = channel.type === "O" ? "public" : "private";
|
|
56338
56356
|
this.channelTypeCache.set(channelId, channelType);
|
|
56339
|
-
|
|
56357
|
+
const name = channel.display_name || channel.name;
|
|
56358
|
+
return { id: channel.id, channelType, name };
|
|
56359
|
+
}
|
|
56360
|
+
async getChannelMembers(channelId) {
|
|
56361
|
+
try {
|
|
56362
|
+
const members = await mattermostApi(this.apiConfig, "GET", `/channels/${channelId}/members?per_page=200`);
|
|
56363
|
+
return members.map((m) => m.user_id);
|
|
56364
|
+
} catch (err) {
|
|
56365
|
+
mcpLogger.debug(`getChannelMembers ${channelId} failed: ${err}`);
|
|
56366
|
+
return null;
|
|
56367
|
+
}
|
|
56368
|
+
}
|
|
56369
|
+
async resolveRecipient(recipient) {
|
|
56370
|
+
const normalized = recipient.replace(/^@/, "");
|
|
56371
|
+
const user = await getUserByUsernameRaw(this.apiConfig, normalized);
|
|
56372
|
+
if (!user)
|
|
56373
|
+
return null;
|
|
56374
|
+
return { id: user.id, username: user.username };
|
|
56375
|
+
}
|
|
56376
|
+
async sendDirectMessage(recipientUserId, message) {
|
|
56377
|
+
const botUserId = await this.getBotUserId();
|
|
56378
|
+
const dmChannel = await createDirectChannelRaw(this.apiConfig, botUserId, recipientUserId);
|
|
56379
|
+
if (!dmChannel) {
|
|
56380
|
+
throw new Error("failed to open direct channel with recipient");
|
|
56381
|
+
}
|
|
56382
|
+
const post2 = await createPost(this.apiConfig, dmChannel.id, message);
|
|
56383
|
+
return { postId: post2.id };
|
|
56340
56384
|
}
|
|
56341
56385
|
async searchMessages(query, options) {
|
|
56342
56386
|
const limit = options?.limit ?? 10;
|
|
@@ -56838,13 +56882,62 @@ class SlackMcpPlatformApi {
|
|
|
56838
56882
|
const isPrivate = ch.is_private || ch.is_im || ch.is_mpim || false;
|
|
56839
56883
|
return {
|
|
56840
56884
|
id: ch.id,
|
|
56841
|
-
channelType: isPrivate ? "private" : "public"
|
|
56885
|
+
channelType: isPrivate ? "private" : "public",
|
|
56886
|
+
name: ch.name
|
|
56842
56887
|
};
|
|
56843
56888
|
} catch (err) {
|
|
56844
56889
|
mcpLogger.debug(`getChannelInfo ${channelId} failed: ${err}`);
|
|
56845
56890
|
return null;
|
|
56846
56891
|
}
|
|
56847
56892
|
}
|
|
56893
|
+
async getChannelMembers(channelId) {
|
|
56894
|
+
const maxPages = 20;
|
|
56895
|
+
const all = [];
|
|
56896
|
+
let cursor = undefined;
|
|
56897
|
+
try {
|
|
56898
|
+
for (let i2 = 0;i2 < maxPages; i2++) {
|
|
56899
|
+
const params = {
|
|
56900
|
+
channel: channelId,
|
|
56901
|
+
limit: 1000
|
|
56902
|
+
};
|
|
56903
|
+
if (cursor)
|
|
56904
|
+
params.cursor = cursor;
|
|
56905
|
+
const response = await slackApi("conversations.members", this.config.botToken, params);
|
|
56906
|
+
all.push(...response.members ?? []);
|
|
56907
|
+
cursor = response.response_metadata?.next_cursor;
|
|
56908
|
+
if (!cursor)
|
|
56909
|
+
return all;
|
|
56910
|
+
}
|
|
56911
|
+
mcpLogger.warn(`getChannelMembers ${channelId} hit page cap of ${maxPages}`);
|
|
56912
|
+
return all;
|
|
56913
|
+
} catch (err) {
|
|
56914
|
+
mcpLogger.debug(`getChannelMembers ${channelId} failed: ${err}`);
|
|
56915
|
+
return null;
|
|
56916
|
+
}
|
|
56917
|
+
}
|
|
56918
|
+
async resolveRecipient(recipient) {
|
|
56919
|
+
const id = recipient.replace(/^<@/, "").replace(/>$/, "");
|
|
56920
|
+
if (!/^[UW][A-Z0-9]{8,}$/.test(id)) {
|
|
56921
|
+
return null;
|
|
56922
|
+
}
|
|
56923
|
+
try {
|
|
56924
|
+
const response = await slackApi("users.info", this.config.botToken, { user: id });
|
|
56925
|
+
return { id: response.user.id, username: response.user.name ?? null };
|
|
56926
|
+
} catch (err) {
|
|
56927
|
+
mcpLogger.debug(`resolveRecipient ${id} failed: ${err}`);
|
|
56928
|
+
return null;
|
|
56929
|
+
}
|
|
56930
|
+
}
|
|
56931
|
+
async sendDirectMessage(recipientUserId, message) {
|
|
56932
|
+
const opened = await slackApi("conversations.open", this.config.botToken, { users: recipientUserId });
|
|
56933
|
+
const dmChannelId = opened.channel.id;
|
|
56934
|
+
const post2 = await slackApi("chat.postMessage", this.config.botToken, {
|
|
56935
|
+
channel: dmChannelId,
|
|
56936
|
+
text: message,
|
|
56937
|
+
mrkdwn: true
|
|
56938
|
+
});
|
|
56939
|
+
return { postId: post2.ts };
|
|
56940
|
+
}
|
|
56848
56941
|
}
|
|
56849
56942
|
function slackMessageToMcpPost(message, channelId, username) {
|
|
56850
56943
|
const createAt = Math.floor(parseFloat(message.ts) * 1000);
|
|
@@ -57165,6 +57258,7 @@ var PLATFORM_CHANNEL_ID = process.env.PLATFORM_CHANNEL_ID || "";
|
|
|
57165
57258
|
var PLATFORM_THREAD_ID = process.env.PLATFORM_THREAD_ID || "";
|
|
57166
57259
|
var ALLOWED_USERS = (process.env.ALLOWED_USERS || "").split(",").map((u) => u.trim()).filter((u) => u.length > 0);
|
|
57167
57260
|
var PERMISSION_TIMEOUT_MS = parseInt(process.env.PERMISSION_TIMEOUT_MS || "120000", 10);
|
|
57261
|
+
var SESSION_OWNER_USERNAME = process.env.SESSION_OWNER_USERNAME || "";
|
|
57168
57262
|
var SESSION_WORKING_DIR = process.env[OUTBOUND_ENV.SESSION_WORKING_DIR] || "";
|
|
57169
57263
|
var SESSION_UPLOAD_DIR = process.env[OUTBOUND_ENV.SESSION_UPLOAD_DIR] || "";
|
|
57170
57264
|
var OUTBOUND_FILES_ENABLED = (process.env[OUTBOUND_ENV.OUTBOUND_FILES_ENABLED] ?? "1") !== "0";
|
|
@@ -57176,14 +57270,16 @@ var UPDATE_OWN_POST_TOOL_NAME = "mcp__claude-threads-mcp__update_own_post";
|
|
|
57176
57270
|
var LIST_THREAD_TOOL_NAME = "mcp__claude-threads-mcp__list_thread";
|
|
57177
57271
|
var READ_CHANNEL_HISTORY_TOOL_NAME = "mcp__claude-threads-mcp__read_channel_history";
|
|
57178
57272
|
var SEARCH_MESSAGES_TOOL_NAME = "mcp__claude-threads-mcp__search_messages";
|
|
57179
|
-
var
|
|
57273
|
+
var SEND_DM_TOOL_NAME = "mcp__claude-threads-mcp__send_dm";
|
|
57274
|
+
var SKIP_STANDARD_PERMISSION_PROMPT = new Set([
|
|
57180
57275
|
SEND_FILE_TOOL_NAME,
|
|
57181
57276
|
READ_POST_TOOL_NAME,
|
|
57182
57277
|
REACT_TO_POST_TOOL_NAME,
|
|
57183
57278
|
UPDATE_OWN_POST_TOOL_NAME,
|
|
57184
57279
|
LIST_THREAD_TOOL_NAME,
|
|
57185
57280
|
READ_CHANNEL_HISTORY_TOOL_NAME,
|
|
57186
|
-
SEARCH_MESSAGES_TOOL_NAME
|
|
57281
|
+
SEARCH_MESSAGES_TOOL_NAME,
|
|
57282
|
+
SEND_DM_TOOL_NAME
|
|
57187
57283
|
]);
|
|
57188
57284
|
var apiConfig = PLATFORM_TYPE === "slack" ? {
|
|
57189
57285
|
platformType: "slack",
|
|
@@ -57210,10 +57306,18 @@ function getApi() {
|
|
|
57210
57306
|
return mcpApi;
|
|
57211
57307
|
}
|
|
57212
57308
|
var allowAllSession = false;
|
|
57309
|
+
var SEND_DM_PER_RECIPIENT_LIMIT = 3;
|
|
57310
|
+
var SEND_DM_MEMBER_CACHE_TTL_MS = 60000;
|
|
57311
|
+
var SEND_DM_MAX_MESSAGE_CHARS = 4000;
|
|
57312
|
+
var sendDmCounts = new Map;
|
|
57313
|
+
var sendDmAllowedRecipients = new Set;
|
|
57314
|
+
var sendDmInFlightPrompts = new Set;
|
|
57315
|
+
var sendDmMemberCache = null;
|
|
57316
|
+
var sendDmChannelLabel = null;
|
|
57213
57317
|
async function handlePermissionWith(toolName, toolInput, cfg) {
|
|
57214
57318
|
mcpLogger.debug(`handlePermission called for ${toolName}`);
|
|
57215
|
-
if (
|
|
57216
|
-
mcpLogger.debug(`
|
|
57319
|
+
if (SKIP_STANDARD_PERMISSION_PROMPT.has(toolName)) {
|
|
57320
|
+
mcpLogger.debug(`Skipping standard prompt for ${toolName} (handler enforces its own gate)`);
|
|
57217
57321
|
return { behavior: "allow", updatedInput: toolInput };
|
|
57218
57322
|
}
|
|
57219
57323
|
if (cfg.getAllowAll()) {
|
|
@@ -57312,7 +57416,7 @@ var sendFileInputSchema = {
|
|
|
57312
57416
|
var readPostInputSchema = {
|
|
57313
57417
|
url: exports_external.string().describe("Permalink URL to a post on the chat platform the bot is connected to. Must be on the same host as the bot."),
|
|
57314
57418
|
include_thread: exports_external.boolean().optional().describe("When true, also fetch surrounding messages in the same thread (oldest first). Defaults to false."),
|
|
57315
|
-
max_messages: exports_external.number().int().optional().describe(`Maximum thread messages to return when include_thread is true. Defaults to ${DEFAULT_THREAD_LIMIT}, capped at ${MAX_THREAD_LIMIT}.`)
|
|
57419
|
+
max_messages: exports_external.coerce.number().int().optional().describe(`Maximum thread messages to return when include_thread is true. Defaults to ${DEFAULT_THREAD_LIMIT}, capped at ${MAX_THREAD_LIMIT}.`)
|
|
57316
57420
|
};
|
|
57317
57421
|
var reactToPostInputSchema = {
|
|
57318
57422
|
url: exports_external.string().describe("Permalink URL to a post the bot can already see (its own channel, or a public channel on the same instance)."),
|
|
@@ -57324,15 +57428,19 @@ var updateOwnPostInputSchema = {
|
|
|
57324
57428
|
};
|
|
57325
57429
|
var listThreadInputSchema = {
|
|
57326
57430
|
url: exports_external.string().optional().describe("Permalink to any post in the target thread. If omitted, the current session thread is read."),
|
|
57327
|
-
max_messages: exports_external.number().int().optional().describe(`Maximum messages to return (oldest first). Defaults to ${DEFAULT_THREAD_LIMIT}, capped at ${MAX_THREAD_LIMIT}.`)
|
|
57431
|
+
max_messages: exports_external.coerce.number().int().optional().describe(`Maximum messages to return (oldest first). Defaults to ${DEFAULT_THREAD_LIMIT}, capped at ${MAX_THREAD_LIMIT}.`)
|
|
57328
57432
|
};
|
|
57329
57433
|
var readChannelHistoryInputSchema = {
|
|
57330
57434
|
channel_id: exports_external.string().describe("Channel identifier. Mattermost: the 26-char channel id. Slack: the channel id (C…/G…). " + "Must be the bot's own channel or a public channel on the same instance."),
|
|
57331
|
-
max_messages: exports_external.number().int().optional().describe("Maximum messages to return (oldest first). Defaults to 20, capped at 100.")
|
|
57435
|
+
max_messages: exports_external.coerce.number().int().optional().describe("Maximum messages to return (oldest first). Defaults to 20, capped at 100.")
|
|
57332
57436
|
};
|
|
57333
57437
|
var searchMessagesInputSchema = {
|
|
57334
57438
|
query: exports_external.string().describe("Search query (platform-specific syntax). Mattermost supports phrase quoting and from:user filters."),
|
|
57335
|
-
max_results: exports_external.number().int().optional().describe("Maximum results to return. Defaults to 10, capped at 25.")
|
|
57439
|
+
max_results: exports_external.coerce.number().int().optional().describe("Maximum results to return. Defaults to 10, capped at 25.")
|
|
57440
|
+
};
|
|
57441
|
+
var sendDmInputSchema = {
|
|
57442
|
+
recipient: exports_external.string().describe("Recipient identifier. Mattermost: a username (with or without leading @). " + "Slack: a user ID (e.g. 'U0123ABC' or '<@U0123ABC>'). The recipient must be a " + "current member of the bot's channel."),
|
|
57443
|
+
message: exports_external.string().describe("Message body to send as a DM. Bot prepends an attribution prefix.")
|
|
57336
57444
|
};
|
|
57337
57445
|
async function handleSendFileWith(args, cfg) {
|
|
57338
57446
|
if (!cfg.enabled) {
|
|
@@ -57743,6 +57851,235 @@ async function handleSearchMessages(args) {
|
|
|
57743
57851
|
botChannelId: PLATFORM_CHANNEL_ID
|
|
57744
57852
|
});
|
|
57745
57853
|
}
|
|
57854
|
+
async function handleSendDmWith(args, cfg) {
|
|
57855
|
+
if (!cfg.api.resolveRecipient || !cfg.api.sendDirectMessage || !cfg.api.getChannelMembers) {
|
|
57856
|
+
return { ok: false, reason: "this platform does not support direct messages" };
|
|
57857
|
+
}
|
|
57858
|
+
if (!cfg.botChannelId) {
|
|
57859
|
+
return { ok: false, reason: "platform channel not configured" };
|
|
57860
|
+
}
|
|
57861
|
+
const recipientArg = (args.recipient ?? "").trim();
|
|
57862
|
+
if (recipientArg.length === 0) {
|
|
57863
|
+
return { ok: false, reason: "recipient must be a non-empty string" };
|
|
57864
|
+
}
|
|
57865
|
+
if (typeof args.message !== "string" || args.message.trim().length === 0) {
|
|
57866
|
+
return { ok: false, reason: "message must be a non-empty string" };
|
|
57867
|
+
}
|
|
57868
|
+
if (args.message.length > cfg.maxMessageChars) {
|
|
57869
|
+
return {
|
|
57870
|
+
ok: false,
|
|
57871
|
+
reason: `message exceeds ${cfg.maxMessageChars}-character cap (${args.message.length} chars supplied)`
|
|
57872
|
+
};
|
|
57873
|
+
}
|
|
57874
|
+
const resolved = await cfg.api.resolveRecipient(recipientArg);
|
|
57875
|
+
if (!resolved) {
|
|
57876
|
+
return { ok: false, reason: `could not resolve recipient '${recipientArg}'` };
|
|
57877
|
+
}
|
|
57878
|
+
let botUserId;
|
|
57879
|
+
try {
|
|
57880
|
+
botUserId = await cfg.api.getBotUserId();
|
|
57881
|
+
} catch (err) {
|
|
57882
|
+
return {
|
|
57883
|
+
ok: false,
|
|
57884
|
+
reason: `could not verify bot identity: ${err instanceof Error ? err.message : String(err)}`
|
|
57885
|
+
};
|
|
57886
|
+
}
|
|
57887
|
+
if (resolved.id === botUserId) {
|
|
57888
|
+
return { ok: false, reason: "cannot send a DM to the bot itself" };
|
|
57889
|
+
}
|
|
57890
|
+
const member = await isRecipientChannelMember(resolved.id, cfg);
|
|
57891
|
+
if (!member.ok)
|
|
57892
|
+
return { ok: false, reason: member.reason };
|
|
57893
|
+
const sentSoFar = cfg.counts.get(resolved.id) ?? 0;
|
|
57894
|
+
if (sentSoFar >= cfg.perRecipientLimit) {
|
|
57895
|
+
return {
|
|
57896
|
+
ok: false,
|
|
57897
|
+
reason: `rate-limited: already sent ${sentSoFar} DM${sentSoFar === 1 ? "" : "s"} to this recipient in this session (limit ${cfg.perRecipientLimit})`
|
|
57898
|
+
};
|
|
57899
|
+
}
|
|
57900
|
+
cfg.counts.set(resolved.id, sentSoFar + 1);
|
|
57901
|
+
const rollbackCounter = () => {
|
|
57902
|
+
const current = cfg.counts.get(resolved.id) ?? 0;
|
|
57903
|
+
if (current > 0)
|
|
57904
|
+
cfg.counts.set(resolved.id, current - 1);
|
|
57905
|
+
};
|
|
57906
|
+
if (!cfg.allowedRecipients.has(resolved.id)) {
|
|
57907
|
+
if (cfg.inFlightPrompts.has(resolved.id)) {
|
|
57908
|
+
rollbackCounter();
|
|
57909
|
+
return {
|
|
57910
|
+
ok: false,
|
|
57911
|
+
reason: "a permission prompt for this recipient is already pending — wait for it to resolve before retrying"
|
|
57912
|
+
};
|
|
57913
|
+
}
|
|
57914
|
+
cfg.inFlightPrompts.add(resolved.id);
|
|
57915
|
+
let decision;
|
|
57916
|
+
try {
|
|
57917
|
+
decision = await promptForDmPermission(resolved.id, resolved.username, cfg);
|
|
57918
|
+
} finally {
|
|
57919
|
+
cfg.inFlightPrompts.delete(resolved.id);
|
|
57920
|
+
}
|
|
57921
|
+
if (decision === "deny") {
|
|
57922
|
+
rollbackCounter();
|
|
57923
|
+
return { ok: false, reason: "user denied this DM" };
|
|
57924
|
+
}
|
|
57925
|
+
if (decision === "timeout") {
|
|
57926
|
+
rollbackCounter();
|
|
57927
|
+
return { ok: false, reason: "permission prompt timed out" };
|
|
57928
|
+
}
|
|
57929
|
+
if (decision === "error") {
|
|
57930
|
+
rollbackCounter();
|
|
57931
|
+
return { ok: false, reason: "permission prompt failed; refusing to send" };
|
|
57932
|
+
}
|
|
57933
|
+
if (decision === "allow-all") {
|
|
57934
|
+
cfg.allowedRecipients.add(resolved.id);
|
|
57935
|
+
}
|
|
57936
|
+
}
|
|
57937
|
+
const channelLabel = await resolveChannelLabel(cfg);
|
|
57938
|
+
const prefix = buildAttributionPrefix(cfg.sessionOwnerUsername, channelLabel);
|
|
57939
|
+
const fullMessage = `${prefix}
|
|
57940
|
+
|
|
57941
|
+
${args.message}`;
|
|
57942
|
+
let postId;
|
|
57943
|
+
try {
|
|
57944
|
+
const result = await cfg.api.sendDirectMessage(resolved.id, fullMessage);
|
|
57945
|
+
postId = result.postId;
|
|
57946
|
+
} catch (err) {
|
|
57947
|
+
rollbackCounter();
|
|
57948
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
57949
|
+
mcpLogger.warn(`send_dm failed: ${reason}`);
|
|
57950
|
+
return { ok: false, reason };
|
|
57951
|
+
}
|
|
57952
|
+
return { ok: true, postId };
|
|
57953
|
+
}
|
|
57954
|
+
async function isRecipientChannelMember(recipientUserId, cfg) {
|
|
57955
|
+
const now = cfg.now ?? Date.now;
|
|
57956
|
+
const cache = cfg.memberCache.value;
|
|
57957
|
+
if (cache && cache.channelId === cfg.botChannelId && cache.expiresAt > now()) {
|
|
57958
|
+
if (cache.members.has(recipientUserId))
|
|
57959
|
+
return { ok: true };
|
|
57960
|
+
return {
|
|
57961
|
+
ok: false,
|
|
57962
|
+
reason: "recipient is not a member of the bot channel — only channel members can be DMed"
|
|
57963
|
+
};
|
|
57964
|
+
}
|
|
57965
|
+
if (!cfg.api.getChannelMembers) {
|
|
57966
|
+
return { ok: false, reason: "platform does not expose channel members" };
|
|
57967
|
+
}
|
|
57968
|
+
const members = await cfg.api.getChannelMembers(cfg.botChannelId);
|
|
57969
|
+
if (members === null) {
|
|
57970
|
+
return { ok: false, reason: "could not fetch channel members" };
|
|
57971
|
+
}
|
|
57972
|
+
const set4 = new Set(members);
|
|
57973
|
+
cfg.memberCache.value = {
|
|
57974
|
+
channelId: cfg.botChannelId,
|
|
57975
|
+
members: set4,
|
|
57976
|
+
expiresAt: now() + cfg.memberCacheTtlMs
|
|
57977
|
+
};
|
|
57978
|
+
if (set4.has(recipientUserId))
|
|
57979
|
+
return { ok: true };
|
|
57980
|
+
return {
|
|
57981
|
+
ok: false,
|
|
57982
|
+
reason: "recipient is not a member of the bot channel — only channel members can be DMed"
|
|
57983
|
+
};
|
|
57984
|
+
}
|
|
57985
|
+
async function promptForDmPermission(recipientId, recipientUsername, cfg) {
|
|
57986
|
+
const now = cfg.now ?? Date.now;
|
|
57987
|
+
const formatter = cfg.api.getFormatter();
|
|
57988
|
+
const recipientLabel = recipientUsername ? `@${recipientUsername}` : recipientId;
|
|
57989
|
+
const message = `⚠️ ${formatter.formatBold("Permission requested")}
|
|
57990
|
+
|
|
57991
|
+
` + `claude-threads wants to send a DM to ${formatter.formatBold(recipientLabel)}.
|
|
57992
|
+
|
|
57993
|
+
` + `\uD83D\uDC4D Allow once | ✅ Allow all DMs to this recipient | \uD83D\uDC4E Deny`;
|
|
57994
|
+
let post2;
|
|
57995
|
+
let botUserId;
|
|
57996
|
+
try {
|
|
57997
|
+
botUserId = await cfg.api.getBotUserId();
|
|
57998
|
+
post2 = await cfg.api.createInteractivePost(message, [APPROVAL_EMOJIS[0], ALLOW_ALL_EMOJIS[0], DENIAL_EMOJIS[0]], cfg.threadId);
|
|
57999
|
+
} catch (err) {
|
|
58000
|
+
mcpLogger.error(`send_dm prompt failed: ${err}`);
|
|
58001
|
+
return "error";
|
|
58002
|
+
}
|
|
58003
|
+
const startTime = now();
|
|
58004
|
+
while (true) {
|
|
58005
|
+
const remainingTime = cfg.promptTimeoutMs - (now() - startTime);
|
|
58006
|
+
if (remainingTime <= 0) {
|
|
58007
|
+
await safeUpdatePost(cfg.api, post2.id, `⏱️ ${formatter.formatBold("Timed out")} — DM to ${recipientLabel} not sent`);
|
|
58008
|
+
return "timeout";
|
|
58009
|
+
}
|
|
58010
|
+
const reaction = await cfg.api.waitForReaction(post2.id, botUserId, remainingTime);
|
|
58011
|
+
if (!reaction) {
|
|
58012
|
+
await safeUpdatePost(cfg.api, post2.id, `⏱️ ${formatter.formatBold("Timed out")} — DM to ${recipientLabel} not sent`);
|
|
58013
|
+
return "timeout";
|
|
58014
|
+
}
|
|
58015
|
+
const username = await cfg.api.getUsername(reaction.userId);
|
|
58016
|
+
if (username && cfg.api.isUserAllowed(username)) {
|
|
58017
|
+
const emoji4 = reaction.emojiName;
|
|
58018
|
+
if (isApprovalEmoji(emoji4)) {
|
|
58019
|
+
await safeUpdatePost(cfg.api, post2.id, `✅ ${formatter.formatBold("Allowed")} by ${formatter.formatUserMention(username)} — sending DM to ${recipientLabel}`);
|
|
58020
|
+
return "allow-once";
|
|
58021
|
+
}
|
|
58022
|
+
if (isAllowAllEmoji(emoji4)) {
|
|
58023
|
+
await safeUpdatePost(cfg.api, post2.id, `✅ ${formatter.formatBold("Allow all")} by ${formatter.formatUserMention(username)} — DMs to ${recipientLabel} won't prompt again this session`);
|
|
58024
|
+
return "allow-all";
|
|
58025
|
+
}
|
|
58026
|
+
await safeUpdatePost(cfg.api, post2.id, `❌ ${formatter.formatBold("Denied")} by ${formatter.formatUserMention(username)}`);
|
|
58027
|
+
return "deny";
|
|
58028
|
+
}
|
|
58029
|
+
mcpLogger.debug(`Ignoring unauthorized DM-permission reaction from ${username || reaction.userId}`);
|
|
58030
|
+
}
|
|
58031
|
+
}
|
|
58032
|
+
async function safeUpdatePost(api3, postId, message) {
|
|
58033
|
+
try {
|
|
58034
|
+
await api3.updatePost(postId, message);
|
|
58035
|
+
} catch (err) {
|
|
58036
|
+
mcpLogger.warn(`updatePost failed (cosmetic): ${err}`);
|
|
58037
|
+
}
|
|
58038
|
+
}
|
|
58039
|
+
async function resolveChannelLabel(cfg) {
|
|
58040
|
+
const slot = cfg.channelLabelCache;
|
|
58041
|
+
if (slot.value !== null)
|
|
58042
|
+
return slot.value;
|
|
58043
|
+
if (!cfg.api.getChannelInfo) {
|
|
58044
|
+
slot.value = cfg.botChannelId;
|
|
58045
|
+
return slot.value;
|
|
58046
|
+
}
|
|
58047
|
+
const info = await cfg.api.getChannelInfo(cfg.botChannelId);
|
|
58048
|
+
slot.value = info?.name ? `#${info.name}` : cfg.botChannelId;
|
|
58049
|
+
return slot.value;
|
|
58050
|
+
}
|
|
58051
|
+
function buildAttributionPrefix(ownerUsername, channelLabel) {
|
|
58052
|
+
if (ownerUsername) {
|
|
58053
|
+
return `_(automated message via claude-threads, on behalf of @${ownerUsername} from ${channelLabel})_`;
|
|
58054
|
+
}
|
|
58055
|
+
return `_(automated message via claude-threads from ${channelLabel})_`;
|
|
58056
|
+
}
|
|
58057
|
+
async function handleSendDm(args) {
|
|
58058
|
+
return handleSendDmWith(args, {
|
|
58059
|
+
api: getApi(),
|
|
58060
|
+
platformType: PLATFORM_TYPE,
|
|
58061
|
+
botChannelId: PLATFORM_CHANNEL_ID,
|
|
58062
|
+
sessionOwnerUsername: SESSION_OWNER_USERNAME,
|
|
58063
|
+
threadId: PLATFORM_THREAD_ID || undefined,
|
|
58064
|
+
promptTimeoutMs: PERMISSION_TIMEOUT_MS,
|
|
58065
|
+
counts: sendDmCounts,
|
|
58066
|
+
allowedRecipients: sendDmAllowedRecipients,
|
|
58067
|
+
inFlightPrompts: sendDmInFlightPrompts,
|
|
58068
|
+
memberCache: { get value() {
|
|
58069
|
+
return sendDmMemberCache;
|
|
58070
|
+
}, set value(v) {
|
|
58071
|
+
sendDmMemberCache = v;
|
|
58072
|
+
} },
|
|
58073
|
+
channelLabelCache: { get value() {
|
|
58074
|
+
return sendDmChannelLabel;
|
|
58075
|
+
}, set value(v) {
|
|
58076
|
+
sendDmChannelLabel = v;
|
|
58077
|
+
} },
|
|
58078
|
+
perRecipientLimit: SEND_DM_PER_RECIPIENT_LIMIT,
|
|
58079
|
+
memberCacheTtlMs: SEND_DM_MEMBER_CACHE_TTL_MS,
|
|
58080
|
+
maxMessageChars: SEND_DM_MAX_MESSAGE_CHARS
|
|
58081
|
+
});
|
|
58082
|
+
}
|
|
57746
58083
|
async function resolvePostFromUrl(url2, cfg) {
|
|
57747
58084
|
if (cfg.platformType === "mattermost") {
|
|
57748
58085
|
if (!cfg.platformUrl) {
|
|
@@ -57839,6 +58176,12 @@ async function main() {
|
|
|
57839
58176
|
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
57840
58177
|
};
|
|
57841
58178
|
});
|
|
58179
|
+
server.tool("send_dm", "Send a direct message to a member of the bot's channel. Use this when the user " + "asks to ping someone in private (a status update, a notification, a result they want as a DM). " + "The recipient must be a current member of the bot channel. The first DM to each recipient " + "in a session triggers a permission prompt in the bot channel; ✅ allow-all promotes that " + "specific recipient to no-prompt for the rest of the session. " + "Hard limit: 3 DMs per recipient per session. The bot prepends an attribution line so " + "recipients can see the DM came from a session and who started it. " + "Returns { ok: true, postId } on success or { ok: false, reason } on failure (denied, " + "rate-limited, recipient not in channel, etc.).", sendDmInputSchema, async ({ recipient, message }) => {
|
|
58180
|
+
const result = await handleSendDm({ recipient, message });
|
|
58181
|
+
return {
|
|
58182
|
+
content: [{ type: "text", text: JSON.stringify(result) }]
|
|
58183
|
+
};
|
|
58184
|
+
});
|
|
57842
58185
|
const transport = new StdioServerTransport;
|
|
57843
58186
|
await server.connect(transport);
|
|
57844
58187
|
mcpLogger.info(`Permission server ready (platform: ${PLATFORM_TYPE})`);
|
|
@@ -57848,8 +58191,13 @@ main().catch((err) => {
|
|
|
57848
58191
|
process.exit(1);
|
|
57849
58192
|
});
|
|
57850
58193
|
export {
|
|
58194
|
+
searchMessagesInputSchema,
|
|
58195
|
+
readPostInputSchema,
|
|
58196
|
+
readChannelHistoryInputSchema,
|
|
58197
|
+
listThreadInputSchema,
|
|
57851
58198
|
handleUpdateOwnPostWith,
|
|
57852
58199
|
handleSendFileWith,
|
|
58200
|
+
handleSendDmWith,
|
|
57853
58201
|
handleSearchMessagesWith,
|
|
57854
58202
|
handleReadPostWith,
|
|
57855
58203
|
handleReadChannelHistoryWith,
|