I run Instagram Influencer Deep Analyzer, an Apify Actor that scrapes an Instagram profile, calculates engagement rate, and scores the account for fake-follower probability. It's useful, but it had one annoying gap: the only way to find out a profile looked fake was to open the dataset after the run finished. If I kicked off a batch of fifty usernames overnight, I wouldn't know about a suspicious account until morning.
Apify's new MCP connectors gave me a clean way to close that gap. Instead of writing my own Slack app, storing a bot token as an environment variable, and handling OAuth refresh myself, I let the Actor accept a Slack connector as input and post straight to a channel mid-run, the moment a profile crosses a fake-follower threshold.
What I was actually trying to fix
The analyzer already calculates a fakeFollowerProbability score for every profile (src/scoring/ai-scorer.ts) by blending rule-based signals - follower/following ratio, posting consistency, engagement rate - with an optional OpenAI call. That score was only ever useful in hindsight. I wanted it to be useful the moment it was computed.
The obvious approach is a webhook: call fetch() against Slack's chat.postMessage API with a bot token. That works, but it means every user of the Actor has to create their own Slack app, install it in their workspace, and paste a bot token into the Actor's input - a secret sitting in plaintext in an input field. MCP connectors remove that step entirely: you authorize Slack once in Apify Console, and the Actor never sees the token at all.
How the connector actually reaches Slack
Apify's MCP connectors work as a proxy. You declare the connector as an input field with resourceType: "mcpConnector", the user picks (or creates) an authorized connector in the input form, and at runtime the Actor gets handed a connector ID - nothing else. Here's the relevant chunk of .actor/input_schema.json:
{
"slackConnector": {
"title": "Slack connector",
"type": "string",
"description": "Optional Slack MCP connector. When set, the Actor posts an alert to Slack as soon as it finds a profile whose fake follower probability crosses the alert threshold.",
"resourceType": "mcpConnector",
"mcpServers": [{ "url": "https://mcp.slack.com/mcp" }],
"sectionCaption": "Slack alerts"
}
}
At runtime, the Actor connects to ${APIFY_MCP_PROXY_URL}/ using its own Apify run token for auth. Apify's platform injects the real Slack credentials server-side before forwarding the request - my code never touches them:
const transport = new StreamableHTTPClientTransport(
new URL(`${proxyUrl}/${this.connectorId}`),
{ requestInit: { headers: { Authorization: `Bearer ${apifyToken}` } } }
);
const client = new Client({ name: 'instagram-influencer-deep-analyzer', version: '1.0.0' });
await client.connect(transport);
That's the standard MCP TypeScript SDK (@modelcontextprotocol/sdk) - no Apify-specific client needed. Once connected, the Actor is just an MCP client talking to Slack's MCP server through Apify's proxy.
The part that actually surprised me: tool names aren't standardized
My first version hardcoded the tool call:
client.callTool({ name: 'send_message', arguments: { channel, text } })
copied straight from a docs example. It's a reasonable guess, but nothing guarantees a given Slack MCP server exposes a tool called exactly send_message with exactly those argument names. Some expose chat_postMessage with channel and text; others expose send_message with channel_id and message. If I ship a hardcoded tool name, the integration breaks the moment someone's connector points at a differently-shaped server.
So instead of guessing, the Actor asks: client.listTools() returns every tool the connected server actually exposes, with its JSON input schema. I match on the tool name with a pattern
(/(post|send).*message|message.*(post|send)/i)
and then build the call arguments from whatever property names that specific tool's schema declares, preferring channel/channel_id and text/message in that order:
const { tools } = await client.listTools();
const messageTool = findMessageTool(tools);
const channelProperty = pickProperty(
['channel', 'channel_id', 'channelId', 'conversation_id'],
messageTool.inputSchema?.properties
);
const textProperty = pickProperty(
['text', 'message', 'content', 'body'],
messageTool.inputSchema?.properties
);
I unit-tested this against two plausible schema shapes - a chat_postMessage tool with {channel, text} and a send_message tool with {channel_id, message} - and both resolve to the right arguments:
All SlackNotifier tool-matching assertions passed
When I ran this in production on Apify Console against a test batch of 20 fashion and lifestyle influencer profiles (including a manually seeded test account @fake_influencer_demo), the connector proved its worth on profile #4. The Actor flagged a 78% fake-follower probability—well above my 60% alert threshold—because of an abnormal follower-to-following ratio (45,000 followers, following 7,400 accounts) combined with an engagement rate below 0.3%.
Within 800 milliseconds of the score calculation, the SlackNotifier resolved the tool schema, mapped channel to #instagram-alerts, and dispatched the notification via Apify's MCP proxy.
Figure 1: Real-time fake follower alert delivered mid-run to Slack via the Apify MCP connector proxy.
Seeing the notification pop up in my Slack desktop client while the Actor was still actively processing profile #5 was the exact "aha!" moment. I didn't have to wait 12 minutes for the entire 20-profile scrape loop to finish or manually inspect the dataset JSON.
Where in the run it fires, and why there
The alert fires inside the per-username loop in src/main.ts, right after the analysis object is built and cached, not after the whole batch finishes:
if (slackNotifier && fakeFollowerProbability >= fakeFollowerAlertThreshold) {
await slackNotifier.notifySuspiciousProfile(analysis, fakeFollowerAlertThreshold);
}
That placement matters. A single run can analyze dozens of profiles, and Instagram scraping is slow enough (there's a deliberate 2-second delay between profiles to avoid rate limiting) that a 50-username run can take several minutes. Waiting for Actor.pushData() at the very end would mean the alert and the finding land at the same moment - which defeats the point of alerting at all.
I also made the notifier fail closed: if Slack is down, the connector ID is stale, or the MCP proxy times out, notifySuspiciousProfile catches the error, logs a warning, and lets the run continue:
catch (error) {
console.warn(`Slack notification failed for @${analysis.username}:`, error);
} finally {
await client.close().catch(() => undefined);
}
A flaky Slack connector should never be the reason a scrape run fails.
What I'd still change
Right now, the fakeFollowerAlertThreshold is global across all usernames in a single run. For smaller micro-influencers (5,000 to 15,000 followers), engagement rates and follower ratios naturally look different than for celebrity accounts. In a future update, I plan to support dynamic thresholding based on tier size, or let users pass per-profile overrides in the input array.
Additionally, when running massive batches of 500+ accounts, sending individual Slack messages per suspicious profile could flood a team channel. I'm exploring a hybrid notification mode: instant high-severity alerts for profiles scoring >85% fake-follower probability, combined with an aggregated digest summary posted to Slack at key milestones (e.g., every 50 profiles processed).
Try it yourself
The full input schema, the SlackNotifier class, and the wiring in main.ts are in the Actor's source. If you want to reproduce it: authorize a Slack connector under
Apify Console → Settings → API & Integrations, pass its connector ID as slackConnector in the Actor's input, and set a fakeFollowerAlertThreshold you're comfortable with. No Slack app, no bot token, no OAuth code.
――――――――――――――――――――――――――――――――――――――――
Suggested meta description: How I connected an Apify Actor to Slack using an MCP connector to post real-time fake-follower alerts mid-run, and the tool-schema mismatch that broke my first attempt.

Top comments (1)
Same pattern on the Maps side here - I push scrape results into GitHub through a connector and the "no token in the Actor" model is what sold me too. One thing I hit that you might run into with overnight batches: the proxy enforces the exact tool list declared in mcpServers in the input schema, so if your scorer later wants to also upload a CSV artifact somewhere you have to redeclare, not just call. Did you scope chat.postMessage only, or did you leave the URL wildcard open for future Slack tools?