Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions slack-mcp/server/lib/event-publisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,39 @@ async function fetchThreadMessages(
}
}

/**
* The thread_ts the agent must use when replying.
*
* - If the user message is already inside a thread → that thread's ts.
* - If the user message is top-level → its own ts, which starts a new
* thread under it (Slack's "first reply with thread_ts=parent" rule).
*
* Pre-computing this on the publisher side guarantees that the agent
* NEVER has to decide whether to start or continue a thread — every
* reply is anchored to a single thread per subject.
*/
function replyInThreadTs(event: SlackEvent): string | undefined {
return event.thread_ts ?? event.ts;
}

/**
* Reply instruction baked into the trigger payload so a trigger-driven
* agent knows exactly how to respond without prompt engineering on the
* subscriber side.
*/
function buildReplyInstruction(
channelId: string | undefined,
threadTs: string | undefined,
): string {
return [
"When you respond, ALWAYS call the SLACK_REPLY_IN_THREAD tool with:",
` channel = "${channelId ?? ""}"`,
` thread_ts = "${threadTs ?? ""}"`,
"Never send a top-level message — every reply must live in this thread",
"so each subject stays isolated.",
].join("\n");
}

export async function publishMessageReceived(
connectionId: string,
event: SlackEvent,
Expand All @@ -71,6 +104,8 @@ export async function publishMessageReceived(
fetchThreadMessages(event.channel, event.thread_ts),
]);

const reply_in_thread_ts = replyInThreadTs(event);

triggers.notify(connectionId, "slack.message.received", {
event: "slack.message.received",
channel_id: event.channel,
Expand All @@ -79,6 +114,8 @@ export async function publishMessageReceived(
text: event.text ?? "",
ts: event.ts,
thread_ts: event.thread_ts,
reply_in_thread_ts,
reply_instruction: buildReplyInstruction(event.channel, reply_in_thread_ts),
is_dm:
event.channel?.startsWith("D") || (event as any).channel_type === "im",
has_files: !!(event as any).files?.length,
Expand All @@ -100,6 +137,8 @@ export async function publishAppMention(
fetchThreadMessages(event.channel, event.thread_ts),
]);

const reply_in_thread_ts = replyInThreadTs(event);

triggers.notify(connectionId, "slack.app_mention", {
event: "slack.app_mention",
channel_id: event.channel,
Expand All @@ -108,6 +147,8 @@ export async function publishAppMention(
text: event.text ?? "",
ts: event.ts,
thread_ts: event.thread_ts,
reply_in_thread_ts,
reply_instruction: buildReplyInstruction(event.channel, reply_in_thread_ts),
has_files: !!(event as any).files?.length,
thread_messages,
timestamp: new Date().toISOString(),
Expand Down
16 changes: 14 additions & 2 deletions slack-mcp/server/lib/trigger-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,13 @@ export const triggers = createTriggers({
definitions: [
{
type: "slack.message.received",
description: "Triggered when a message is sent in a Slack channel or DM",
description:
"Triggered when a message is sent in a Slack channel or DM. " +
"The payload carries `channel_id`, `reply_in_thread_ts`, `text`, " +
"`user_name`, and (when applicable) `thread_messages` with the full " +
"thread history. To respond, ALWAYS call SLACK_REPLY_IN_THREAD with " +
"channel=channel_id and thread_ts=reply_in_thread_ts so every answer " +
"lives inside the user's thread and each subject stays isolated.",
params: z.object({
channel_id: z
.string()
Expand All @@ -58,7 +64,13 @@ export const triggers = createTriggers({
},
{
type: "slack.app_mention",
description: "Triggered when the bot is mentioned with @",
description:
"Triggered when the bot is @mentioned in a channel. The payload " +
"carries `channel_id`, `reply_in_thread_ts`, `text`, `user_name`, " +
"and (when applicable) `thread_messages`. To respond, ALWAYS call " +
"SLACK_REPLY_IN_THREAD with channel=channel_id and " +
"thread_ts=reply_in_thread_ts so the answer lives inside the user's " +
"thread.",
params: z.object({
channel_id: z
.string()
Expand Down
Loading