Inspiration
We already ship a Gemini + MongoDB MCP shopping concierge inside our React Native app. What we saw the moment we let a small trading team share access: they wanted the agent in the room where they already talk — Slack — and they wanted it to do more than tell them what to do. They wanted it to do something. The Slack Agent Builder Challenge gave us a forcing function to move the agent from a mobile-app curio into an actual co-seller's control room, and to close the loop with real on-chain settlement.
What it does
Kajota Coach in Slack is one bot that composes three surfaces the co-seller's team already lives in:
- Live merchant catalogue reads through the official MongoDB MCP server — every reply grounded in what's actually in the database this second, no RAG indexer.
- Proactive agent turns on demand —
/kajota statusfires a multi-tool ADK turn (recent orders, wishlist deltas, catalogue drops) and posts a Slack Block Kit card carousel in-channel with a "Used 3 MCP tool calls: find, find, find" footer. - Team-approved on-chain escrow settlement —
/kajota pay yeezy-hoodie 150doesn't broadcast. It resolves the on-chain listing id, then posts a Block Kit card with Approve + broadcast / Deny buttons. A workspace teammate clicks Approve → the buttons vanish, the card updates in place with the approver, and a threaded reply fills with live progress as each on-chain tx confirms:
🔄 USDC.approve — broadcasting… ✅ USDC.approve confirmed
0x…🔄 CosellEscrow.deposit — broadcasting… ✅ CosellEscrow.deposit confirmed0x…🔏 Escrow settled — 150 USDC locked for yeezy-hoodie
That's the co-seller's approval workflow delivered through Slack primitives — Block Kit buttons, chat_update to disable a card after click, threaded receipts — not a chatbot pretending to be one.
One agent, three transports — including MCP both ways
Kajota Coach uses MCP twice: as a client composing MongoDB MCP + Fetch MCP inside the ADK Runner, AND as a server re-exposing its own capabilities. The FastAPI process mounts an MCP endpoint at /mcp (streamable-HTTP) offering five tools any MCP client — Claude Desktop, Cursor, another ADK build, or Slack's own Agent Builder runtime — can call:
resolve_listing_id(product_hint) — on-chain CosellRegistry read
propose_escrow(hint, amount) — dry-run, returns listing id + calldata
settle_escrow(hint, amount) — signs + broadcasts both txs
get_status(user_id) — proactive agent turn (3 MongoDB reads)
add_to_watchlist(product, user_id) — MongoDB insert-one via the agent
How I built it
Stack — every track requirement met:
- Slack agent:
slack-bolt>=1.21(async)AsyncAppmounted inside the existing FastAPI process viaAsyncSlackRequestHandler. Routes:POST /slack/events,POST /slack/commands/kajota,POST /slack/actions. - AI reasoning: Gemini 2.5 Pro on Google Cloud Agent Development Kit (ADK), running as
Agent(tools=[mongodb_mcp, fetch_mcp]). - MCP integration: Two MCP servers composed under one ADK runner: MongoDB MCP (official Node server via
npx) and Fetch MCP (Python module). PLUS Coach itself exposes an MCP server at/mcp— the "MCP both ways" story. - Real action:
/kajota paycallskajota_concierge.mesh— a web3.py client that composesUSDC.approve+CosellEscrow.depositagainst our own Mesh contracts on Mantle Sepolia.
The team-approval flow uses Slack's actual collaboration primitives:
- Block Kit
actionsblock with two buttons (kajota_approve,kajota_deny) on the pending card. - In-memory intent store keyed by an opaque id — the button
valueis the id, not the tx params, so an untrusted user can't rewrite the amount in-flight. chat_updateon click — the approver is stamped in and the buttons vanish, so no double-click race.- Threaded progress via
client.chat_postMessage(thread_ts=…)as each tx is broadcast + confirmed on Mantle Sepolia. - 15-min TTL sweep — unclaimed proposals get pruned so the store stays bounded.
Each Slack user gets an isolated ADK session, keyed as slack:{team_id}:{user_id} — so /kajota status twenty minutes later picks up where the last turn left off, and cross-workspace sharing of a display name never leaks state.
One ack budget, one real reply: Slack requires a slash-command ack within 3 seconds; Gemini agent turns with 2–3 MCP tool calls routinely take 8–15s. Bolt's process_before_response=False handles the split: ack() immediately with an ephemeral, then post the actual Block Kit reply via client.chat_postMessage once the agent turn finishes.
Challenges I ran into
- Slack's 3-second ack window meets multi-tool agent turns. Bolt's
respond()alone won't cut it — a 12s agent turn expires the ack context. Fixed withack()+chat_postMessagesplit. - Cross-workspace session collision. ADK's
InMemorySessionServiceresolves by(app_name, user_id, session_id). Raw Slackuser_idwould collide across workspaces. Fixed by folding the workspace into both —slack:{team}:{user}. - Web3.py v7 API drift. Between v6 and v7,
SignedTransaction.rawTransactionbecameraw_transactionandHexBytes.hex()stopped emitting the0xprefix. Handled both defensively. - Escrow deploy coverage. Kajota Mesh registry is live on Polygon Amoy, Ethereum Sepolia, and Mantle Sepolia. Full stack (Registry + Escrow + MockUSDC) only on Mantle Sepolia. Defaulted
MESH_CHAIN=mantle-sepoliaand raise a clearMeshConfigErrorif you point at a chain missing an escrow.
Accomplishments that I'm proud of
- One agent, three surfaces. The exact same
_run_agent_turncoroutine drives the mobile/chat, mobile/proactive, every Slack slash command + @mention, AND the MCP server tools. Zero changes to the agent itself — Slack was ~450 lines of Python (Bolt + Block Kit + mesh web3.py client). /kajota payproduces a real receipt. Most agent submissions stop at "the agent talks about doing the thing." Ours composes two on-chain calls (approve + deposit) against contracts we deployed ourselves. Judges can click the explorer links in the demo.- MCP both ways. We consume MongoDB MCP + Fetch MCP as a client, AND expose our own capabilities as an MCP server at
/mcp. Same code paths as the Slack surface — same mesh client, same ADK Runner. - Faults surface where the user is looking. A Mesh config error, a bad amount, a missing listing — all render as a Block Kit
:warning:card inside the Slack channel, not a 500 the user has to chase in server logs.
What I learned
- A "Slack agent" is a transport, not an agent. Once we stopped thinking "Slack integration" and started thinking "Slack is another surface on our existing agent," the code got a lot smaller.
- The
/command <subcommand> <args>idiom keeps you inside one Slack app manifest. Routing on the first token oftextinsideslack_app.pyis one manifest entry and one code entry. - Chain-agnostic env var overrides + a
MeshConfigErrorare worth the ten extra lines. The mesh client falls back toDEFAULT_CHAINS[chain_key]and letsMESH_*env vars override every address.
What's next for Kajota Coach in Slack
- Bring the proactive push into Slack. Wire the mobile-side proactive turn into a Render cron so a wishlist item hitting its target price posts a Slack card unprompted.
- Wallet-connect flow for the unsigned path. A Slack interactivity handler + WalletConnect deep link would close the "user signs their own tx" loop.
- Third MCP. Adding an Elasticsearch MCP for full-text catalogue search is one
McpToolsetregistration + one system-prompt paragraph.
Log in or sign up for Devpost to join the conversation.