Last week I wrote about Slack enterprise tokens being a mess. That was the authentication layer. This week I hit the tooling layer — and ended up ripping out the entire Slack MCP integration in favor of raw curl.
Here’s why.
What MCP does
MCP (Model Context Protocol) is how Claude Code talks to external services. A Slack MCP server wraps the Slack API and exposes tools like slack_send_message and slack_get_thread. Claude calls the tool, the MCP server calls Slack, results come back.
Sounds clean. In practice, the tools are deferred — they’re not available until Claude calls ToolSearch to discover and load them. This adds a step that seems trivial until it isn’t.
The loop
I asked Claude to send a Slack message. What happened:
Turn 1: "Let me search for the Slack tools..." [no tool call emitted]
Turn 2: "I'll call ToolSearch now..." [no tool call emitted]
Turn 3: "Trying ToolSearch..." [no tool call emitted]
...
Turn 15: "Let me try one more time..." [no tool call emitted]
Fifteen turns of Claude describing calling ToolSearch without ever actually calling it. The model generated text about the action instead of the action itself.
This is a real failure mode of the deferred tool pattern. Once the context fills with failed attempts, the pattern self-reinforces. The model sees a history of “let me try → nothing happened” and generates more of the same. The LLM equivalent of spinning tires in mud.
I tried rephrasing, being explicit, minimal prompts. Nothing worked. The context was poisoned.
The fix
Gave up on MCP. Ran curl:
TOKENS=$(cat ~/.slack-mcp-tokens.json)
TOKEN=$(echo "$TOKENS" | python3 -c "import sys,json; print(json.load(sys.stdin)['SLACK_TOKEN'])")
COOKIE=$(echo "$TOKENS" | python3 -c "import sys,json; print(json.load(sys.stdin)['SLACK_COOKIE'])")
curl -s "https://slack.com/api/chat.postMessage" \
-H "Authorization: Bearer $TOKEN" \
-H "Cookie: d=$COOKIE" \
-H "Content-Type: application/json" \
-d '{"channel":"D0ABVL73A56","text":"hello","thread_ts":"1772853690.964289"}'
Worked on the first try. Same API. Same tokens. Same auth.
Why curl wins
| MCP route | Curl route | |
|---|---|---|
| Path to Slack | ToolSearch → load tool → call tool → MCP server → Slack API | Bash → curl → Slack API |
| Failure modes | ToolSearch loop, MCP crash, model routing errors, deferred tool load failure, context pollution | Token expired |
| Capabilities | Send, read, search, user info | Send, read, search, user info |
| Auth | Reads ~/.slack-mcp-tokens.json |
Reads ~/.slack-mcp-tokens.json |
Same capabilities. Same auth source. Five failure modes vs one. The MCP server is a wrapper around the same HTTP calls curl makes — it adds no functionality, only indirection.
The hidden cost of having the plugin
The Slack MCP plugin caused problems even when I wasn’t using it:
- 15 deferred tool definitions loaded into every session’s context — token cost for tools I might never call
- Keyword triggers — mentioning “Slack” in conversation could auto-invoke associated skills, which spawned subagent requests, which hit the model proxy, which sometimes returned 400 errors
The tool designed to make Slack easier was making everything harder. Just talking about Slack could trigger a failure cascade.
What I lost
One thing: automatic token refresh. The MCP server had logic to re-extract tokens from the Slack desktop app when they expired.
Without it, I manually update ~/.slack-mcp-tokens.json every few weeks. Two minutes of work. I’ll take that over debugging generation loops.
When MCP makes sense vs when it doesn’t
This isn’t “MCP bad.” It’s about matching the tool to the problem:
| Use MCP when… | Use curl when… |
|---|---|
| The API is complex (auth flows, pagination, state) | The API is simple HTTP + JSON |
| You need the server to maintain state between calls | Each call is independent |
| The tool is called frequently (worth the loading cost) | The tool is called occasionally |
| ToolSearch reliably loads the tool | You’ve seen ToolSearch loops |
Slack’s API is simple REST. Each call is independent. I use it a few times per session. ToolSearch loops. Four out of four says curl.
The philosophical point: an abstraction that adds failure modes without adding capabilities is not an abstraction. It’s overhead. MCP is valuable when it simplifies something genuinely complex. For a REST API that takes a token and returns JSON, it’s just another thing that can break.
The setup
My Claude Code config now has curl templates for every Slack operation — send, read thread, search, channel history, user info. No MCP server running. No deferred tools. No ToolSearch. Claude reads the template, fills in parameters, runs curl through Bash.
If you’re using MCP for a simple REST API and it’s giving you trouble: try curl. You might not go back.