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
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ export async function processAssistantMessage({
threadTs,
organizationId: connection.organizationId,
slackToken: connection.accessToken,
slackTeamId: teamId,
});

// Format actions as text with URLs (enables Slack unfurling)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ export async function processSlackMention({
threadTs,
organizationId: connection.organizationId,
slackToken: connection.accessToken,
slackTeamId: teamId,
});

// Format actions as text with URLs (enables Slack unfurling)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type Anthropic from "@anthropic-ai/sdk";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { createInMemoryMcpClient } from "@superset/mcp/in-memory";

interface McpTool {
Expand All @@ -20,32 +19,6 @@ export async function createSupersetMcpClient({
return createInMemoryMcpClient({ organizationId, userId });
}

export async function createSlackMcpClient({
token,
teamId,
}: {
token: string;
teamId: string;
}): Promise<Client> {
const transport = new StdioClientTransport({
command: "npx",
args: ["-y", "@modelcontextprotocol/server-slack"],
env: {
...process.env,
SLACK_BOT_TOKEN: token,
SLACK_TEAM_ID: teamId,
},
});

const client = new Client({
name: "slack-agent-slack",
version: "1.0.0",
});

await client.connect(transport);
return client;
}

export function mcpToolToAnthropicTool(
tool: McpTool,
prefix: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { integrationConnections } from "@superset/db/schema";
import { and, eq } from "drizzle-orm";
import type { AgentAction } from "../slack-blocks";
import {
createSlackMcpClient,
createSupersetMcpClient,
mcpToolToAnthropicTool,
parseToolName,
Expand Down Expand Up @@ -59,7 +58,6 @@ interface RunSlackAgentParams {
threadTs: string;
organizationId: string;
slackToken: string;
slackTeamId: string;
}

export interface SlackAgentResult {
Expand Down Expand Up @@ -158,30 +156,68 @@ const DENIED_SUPERSET_TOOLS = new Set([
"get_app_context",
]);

const SLACK_GET_CHANNEL_HISTORY_TOOL: Anthropic.Tool = {
name: "slack_get_channel_history",
description:
"Get recent messages from the current Slack channel. Use this to understand what the team has been discussing.",
input_schema: {
type: "object" as const,
properties: {
limit: {
type: "number",
description: "Number of messages to retrieve (default 20, max 100)",
},
},
required: [],
},
};

async function handleGetChannelHistory({
token,
channelId,
limit = 20,
}: {
token: string;
channelId: string;
limit?: number;
}): Promise<string> {
const slack = new WebClient(token);
const result = await slack.conversations.history({
channel: channelId,
limit: Math.min(limit, 100),
});

if (!result.messages || result.messages.length === 0) {
return JSON.stringify({ messages: [] });
}

const messages = result.messages.map((msg) => ({
user: msg.user,
text: msg.text,
ts: msg.ts,
thread_ts: msg.thread_ts,
}));

return JSON.stringify({ messages });
}
Comment on lines +159 to +202
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Slack Web API conversations.history limit parameter valid range behavior

💡 Result:

For conversations.history, Slack documents the limit parameter as:

  • Type: number
  • What it does: “maximum number of items to return”; Slack may return fewer than requested even if more history exists. [1]
  • Default: 100 (unless you’re in the reduced-limit category below). [1]
  • Maximum (normal behavior): 999 (and Slack notes using a limit under 1000 for pagination). [1][4]

Important: reduced limit for some non‑Marketplace apps

Slack introduced lower limits for apps commercially distributed outside the Slack Marketplace:

  • For new apps / new installations (effective May 29, 2025): conversations.history is 1 request/minute and the maximum + default limit are reduced to 15. [1][3]
  • Slack’s developer docs indicate the same reduced limits will apply to existing installations starting March 3, 2026. [4][6]

So in practice, the “valid range” depends on the app category: up to 999 normally, but effectively capped at 15 for affected non‑Marketplace commercially distributed apps. [1][3][4]

Sources: Slack method reference and changelog/rate-limit docs. [1][3][4][6]


🏁 Script executed:

# Check the codebase for similar Slack API calls and validation patterns
rg "conversations\.history" -A 5 -B 5

Repository: superset-sh/superset

Length of output: 1179


🏁 Script executed:

# Look for other instances where the code handles LLM-supplied parameters or similar validation
rg "Math\.min|Math\.max|Number\.isFinite" apps/api/src/app/api/integrations/slack -B 3 -A 3

Repository: superset-sh/superset

Length of output: 800


🏁 Script executed:

# Check if there are existing constants for Slack API limits elsewhere in the codebase
rg "LIMIT|HISTORY|SLACK.*20|SLACK.*100" apps/api/src/app/api/integrations/slack -i

Repository: superset-sh/superset

Length of output: 2824


Extract magic numbers to constants and add input validation for LLM-supplied limit parameter.
limit defaults and caps (20, 100) are magic numbers that should be extracted per guidelines. More critically, limit is LLM-supplied and lacks validation—negative, NaN, or non-integer values will be passed directly to the Slack API, potentially causing issues. The Slack API conversations.history accepts values up to 999 (normally) but effective limits are dropping to 15 for non-Marketplace apps starting March 3, 2026.

🔧 Proposed fix
+const DEFAULT_CHANNEL_HISTORY_LIMIT = 20;
+const MAX_CHANNEL_HISTORY_LIMIT = 100;
+
 const SLACK_GET_CHANNEL_HISTORY_TOOL: Anthropic.Tool = {
 	name: "slack_get_channel_history",
 	description:
 		"Get recent messages from the current Slack channel. Use this to understand what the team has been discussing.",
 	input_schema: {
 		type: "object" as const,
 		properties: {
 			limit: {
 				type: "number",
-				description: "Number of messages to retrieve (default 20, max 100)",
+				description: `Number of messages to retrieve (default ${DEFAULT_CHANNEL_HISTORY_LIMIT}, max ${MAX_CHANNEL_HISTORY_LIMIT})`,
 			},
 		},
 		required: [],
 	},
 };
 
 async function handleGetChannelHistory({
 	token,
 	channelId,
-	limit = 20,
+	limit = DEFAULT_CHANNEL_HISTORY_LIMIT,
 }: {
 	token: string;
 	channelId: string;
 	limit?: number;
 }): Promise<string> {
 	const slack = new WebClient(token);
+	const safeLimit = Number.isFinite(limit)
+		? Math.max(1, Math.min(Math.trunc(limit), MAX_CHANNEL_HISTORY_LIMIT))
+		: DEFAULT_CHANNEL_HISTORY_LIMIT;
 	const result = await slack.conversations.history({
 		channel: channelId,
-		limit: Math.min(limit, 100),
+		limit: safeLimit,
 	});
🤖 Prompt for AI Agents
In `@apps/api/src/app/api/integrations/slack/events/utils/run-agent/run-agent.ts`
around lines 159 - 202, Extract the magic numbers into named constants (e.g.,
DEFAULT_CHANNEL_HISTORY_LIMIT = 20, MAX_CHANNEL_HISTORY_LIMIT = 100,
EFFECTIVE_MAX_CHANNEL_HISTORY_LIMIT = 15) and update
SLACK_GET_CHANNEL_HISTORY_TOOL.input_schema to declare numeric constraints
(integer type, minimum 1, maximum equal to MAX_CHANNEL_HISTORY_LIMIT) and a
documented default; in handleGetChannelHistory, validate and sanitize the
LLM-supplied limit by coercing to an integer, treating
non-numeric/NaN/negative/zero values as DEFAULT_CHANNEL_HISTORY_LIMIT, and clamp
the value to the allowed max (use Math.min(parsedLimit,
MAX_CHANNEL_HISTORY_LIMIT, EFFECTIVE_MAX_CHANNEL_HISTORY_LIMIT) or similar)
before passing it to WebClient.conversations.history to ensure only a safe,
bounded integer is sent.


const SYSTEM_PROMPT = `You are a helpful assistant in Slack for Superset, a task management application.

You can:
- Create, update, search, and manage tasks using superset_* tools
- Read Slack messages and context using slack_* tools
- Read recent channel messages using slack_get_channel_history
- Help users understand conversations and create actionable items from discussions

Guidelines:
- Be concise and clear (this is Slack, not email)
- When creating tasks, extract key details from the conversation
- Use Slack formatting: *bold*, _italic*, \`code\`, > quotes
- Use Slack formatting: *bold*, _italic_, \`code\`, > quotes
- If an action fails, explain what went wrong and suggest alternatives

Context gathering:
- If the user's request references something you don't have context for (a person, a conversation, a decision, etc.), USE THE SLACK TOOLS to find it
- Use slack_search_messages to find relevant discussions by keyword
- Use slack_get_channel_history to read recent channel messages
- Use slack_get_thread_replies to get full thread context
- Use slack_get_users to look up user details when names are mentioned
- Don't ask the user for context you can find yourself - be proactive

Available tool prefixes:
- superset_*: Task management tools (create_task, list_tasks, update_task, etc.)
- slack_*: Slack tools (get_channel_history, search_messages, get_thread, etc.)`;
- Thread context is automatically included if the mention is in a thread
- Use slack_get_channel_history to read recent channel messages for additional context
- Don't ask the user for context you can find yourself - be proactive`;

export async function runSlackAgent(
params: RunSlackAgentParams,
Expand All @@ -203,44 +239,33 @@ export async function runSlackAgent(

let supersetMcp: Client | null = null;
let cleanupSuperset: (() => Promise<void>) | null = null;
let slackMcp: Client | null = null;

try {
const [threadContext, supersetMcpResult, slackMcpResult] =
await Promise.all([
fetchThreadContext({
token: params.slackToken,
channelId: params.channelId,
threadTs: params.threadTs,
}),
createSupersetMcpClient({
organizationId: params.organizationId,
userId: connection.connectedByUserId,
}),
createSlackMcpClient({
token: params.slackToken,
teamId: params.slackTeamId,
}),
]);
const [threadContext, supersetMcpResult] = await Promise.all([
fetchThreadContext({
token: params.slackToken,
channelId: params.channelId,
threadTs: params.threadTs,
}),
createSupersetMcpClient({
organizationId: params.organizationId,
userId: connection.connectedByUserId,
}),
]);

supersetMcp = supersetMcpResult.client;
cleanupSuperset = supersetMcpResult.cleanup;
slackMcp = slackMcpResult;

const [supersetToolsResult, slackToolsResult] = await Promise.all([
supersetMcp.listTools(),
slackMcp.listTools(),
]);
const supersetToolsResult = await supersetMcp.listTools();

const supersetTools = supersetToolsResult.tools
.map((t) => mcpToolToAnthropicTool(t, "superset"))
.filter((t) => !DENIED_SUPERSET_TOOLS.has(t.name));

const slackTools = slackToolsResult.tools.map((t) =>
mcpToolToAnthropicTool(t, "slack"),
);

const tools: Anthropic.Tool[] = [...supersetTools, ...slackTools];
const tools: Anthropic.Tool[] = [
...supersetTools,
SLACK_GET_CHANNEL_HISTORY_TOOL,
];

const contextualSystem = `${SYSTEM_PROMPT}

Expand Down Expand Up @@ -283,30 +308,38 @@ Current context:
const toolResults: Anthropic.ToolResultBlockParam[] = [];

for (const toolUse of toolUseBlocks) {
const { prefix, toolName } = parseToolName(toolUse.name);
const mcp = prefix === "superset" ? supersetMcp : slackMcp;

if (!mcp) {
toolResults.push({
type: "tool_result",
tool_use_id: toolUse.id,
content: JSON.stringify({
error: `Unknown tool prefix: ${prefix}`,
}),
is_error: true,
});
continue;
}

try {
const result = await mcp.callTool({
name: toolName,
arguments: toolUse.input as Record<string, unknown>,
});
let resultContent: string;

if (toolUse.name === "slack_get_channel_history") {
const input = toolUse.input as { limit?: number };
resultContent = await handleGetChannelHistory({
token: params.slackToken,
channelId: params.channelId,
limit: input.limit,
});
} else {
const { prefix, toolName } = parseToolName(toolUse.name);

if (prefix !== "superset" || !supersetMcp) {
toolResults.push({
type: "tool_result",
tool_use_id: toolUse.id,
content: JSON.stringify({
error: `Unknown tool: ${toolUse.name}`,
}),
is_error: true,
});
continue;
}

const result = await supersetMcp.callTool({
name: toolName,
arguments: toolUse.input as Record<string, unknown>,
});

const resultContent = JSON.stringify(result.content);
resultContent = JSON.stringify(result.content);

if (prefix === "superset") {
const action = getActionFromToolResult(toolName, result);
if (action) {
actions.push(action);
Expand Down Expand Up @@ -364,10 +397,5 @@ Current context:
await cleanupSuperset();
} catch {}
}
if (slackMcp) {
try {
await slackMcp.close();
} catch {}
}
}
}