<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shrijal Acharya</title>
    <description>The latest articles on DEV Community by Shrijal Acharya (@shricodev).</description>
    <link>https://dev.to/shricodev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1127015%2F1c5e48a2-f602-4e7d-8312-3c0322d155c6.jpg</url>
      <title>DEV Community: Shrijal Acharya</title>
      <link>https://dev.to/shricodev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shricodev"/>
    <language>en</language>
    <item>
      <title>Pi Agent vs OpenCode after 100+ Hours of Real Use ✌️</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Fri, 21 Aug 2026 13:31:28 +0000</pubDate>
      <link>https://dev.to/composiodev/pi-agent-vs-opencode-after-100-hours-of-real-use-1mh7</link>
      <guid>https://dev.to/composiodev/pi-agent-vs-opencode-after-100-hours-of-real-use-1mh7</guid>
      <description>&lt;p&gt;Open-source coding agents had a weird start to 2026.&lt;/p&gt;

&lt;p&gt;In January, Anthropic suddenly blocked third-party tools from using Claude subscriptions. Overnight, with no warning.&lt;/p&gt;

&lt;p&gt;OpenCode got hit the hardest. The team had to remove Claude login support after legal pressure. The commit message was literally just: “anthropic legal requests.”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8jimv4abmhdeojze1lw5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8jimv4abmhdeojze1lw5.png" alt="OpenCode PR removing anthropic oauth" width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;PI never really cared. It was never built around one provider anyway.&lt;/p&gt;

&lt;p&gt;And that kinda brings us to the obvious question. If you’re using your own API keys anyway, which open-source agent do you actually pick?&lt;/p&gt;

&lt;p&gt;OpenCode is sitting at &lt;strong&gt;199K stars&lt;/strong&gt;, probably past 200K by the time you read this, with around 8 million monthly devs.&lt;/p&gt;

&lt;p&gt;Then there’s Pi. Bare minimum by design. Four tools. A system prompt under 1,000 tokens. And somehow, top of our benchmark. Again.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ We recently covered &lt;a href="https://composio.dev/content/pi-agent-vs-claude-code" rel="noopener noreferrer"&gt;Pi Agent vs. Claude Code&lt;/a&gt;. Check that out if you're not sure whether Anthropic or Pi suits you better&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The easiest way I’d frame it is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 OpenCode is VS Code (bit bloaty). Pi is NeoVim (bare minimum).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So which one do you go for? Let's get into the difference.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Pi&lt;/th&gt;
&lt;th&gt;OpenCode&lt;/th&gt;
&lt;th&gt;Winner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Real tool use (our eval)&lt;/td&gt;
&lt;td&gt;21/30 tasks passed, $0.078 per success&lt;/td&gt;
&lt;td&gt;19/30 tasks passed, $0.119 per success&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total eval spend&lt;/td&gt;
&lt;td&gt;$1.64 for all 30 tasks&lt;/td&gt;
&lt;td&gt;$2.25 for all 30 tasks&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;362.9s median per task, slowest of the bunch&lt;/td&gt;
&lt;td&gt;280.6s median per task&lt;/td&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context overhead&lt;/td&gt;
&lt;td&gt;Under 1,000 tokens, prompt + tool defs&lt;/td&gt;
&lt;td&gt;~6,900 tokens per request&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Out-of-the-box features&lt;/td&gt;
&lt;td&gt;Four tools&lt;/td&gt;
&lt;td&gt;Plan mode, MCP, LSP diagnostics, undo/redo, permissions, subagents, and a lot more&lt;/td&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model support&lt;/td&gt;
&lt;td&gt;20+ providers, 300+ models, mid-session switching&lt;/td&gt;
&lt;td&gt;75+ providers via AI SDK and &lt;code&gt;models.dev&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cheap model access&lt;/td&gt;
&lt;td&gt;BYOK or local, that's it&lt;/td&gt;
&lt;td&gt;Zen gateway at cost, plus Go at $10/month&lt;/td&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extensibility&lt;/td&gt;
&lt;td&gt;TypeScript extensions running inside the agent process&lt;/td&gt;
&lt;td&gt;Plugins, custom agents, skills, mostly config-driven&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sessions&lt;/td&gt;
&lt;td&gt;Branchable session trees with fork and rewind&lt;/td&gt;
&lt;td&gt;Linear log with undo/redo, but survives disconnects&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Surfaces&lt;/td&gt;
&lt;td&gt;Terminal&lt;/td&gt;
&lt;td&gt;TUI, desktop app, web UI, IDEs via ACP, GitHub and GitLab CI&lt;/td&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permissions&lt;/td&gt;
&lt;td&gt;Nothing. Full system access from prompt one, which isn't really my preferred style&lt;/td&gt;
&lt;td&gt;allow/ask/deny per tool with glob patterns&lt;/td&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource footprint&lt;/td&gt;
&lt;td&gt;Lightweight, single process&lt;/td&gt;
&lt;td&gt;1GB+ RAM for a TUI is kinda crazy&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  In summary
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Scorecard: Pi 6, OpenCode 6. Dead tie.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And honestly, that feels right. These two are built for pretty different kinds of people anyway.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Remember the VS Code vs. Neovim thing I said earlier?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Pi is more like a platform you program. You get a tiny core and build your own harness around it in TypeScript.&lt;/p&gt;

&lt;p&gt;OpenCode is more like a product you configure. You get something much closer to a full Claude Code replacement on day one, then tweak it through config.&lt;/p&gt;

&lt;p&gt;Pi wins on efficiency, hackability, and, at least in our eval, raw results.&lt;/p&gt;

&lt;p&gt;OpenCode wins on features, provider support, surfaces, and the simple fact that a normal person can pick it up and start using it without building half the setup themselves.&lt;/p&gt;

&lt;p&gt;So yeah, the real tiebreaker is basically what kind of person you are. Keep reading.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Pi?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ A minimal terminal coding agent that ships four tools and expects you to build the rest yourself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0hsa836yiuj6i335nnhm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0hsa836yiuj6i335nnhm.png" alt="pi agent" width="800" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Quick recap if you missed our &lt;a href="https://composio.dev/blog/pi-agent-vs-claude-code" rel="noopener noreferrer"&gt;Pi vs Claude Code&lt;/a&gt; post.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pi.dev/" rel="noopener noreferrer"&gt;Pi&lt;/a&gt; is Mario Zechner's answer to harness bloat. He got tired of Claude Code changing under him, so he built an agent where the model gets &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, and &lt;code&gt;bash&lt;/code&gt;, a system prompt of a few hundred tokens, and nothing else. No MCP. No permissions. No plan mode. No subagents. All on purpose.&lt;/p&gt;

&lt;p&gt;It now lives under &lt;a href="https://github.com/earendil-works/pi" rel="noopener noreferrer"&gt;Earendil&lt;/a&gt; after his April blog post "I've sold out," and the core stays open-source.&lt;/p&gt;

&lt;p&gt;The real product is the extension system. TypeScript files run inside the same process as the agent loop, and Pi can read its own source code. So when you need a missing feature, you literally ask Pi to build it.&lt;/p&gt;

&lt;p&gt;That design is also why Peter Steinberger picked Pi as the engine for OpenClaw.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 &lt;strong&gt;Fun Fact:&lt;/strong&gt; Pi has "only" 94k stars, but the npm package pulls around 1.3 million downloads a week. Growing crazy fast among developers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not everyone is fond of VS Code. 🤷‍♂️&lt;/p&gt;




&lt;h2&gt;
  
  
  What is OpenCode?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ The most starred open source coding agent in the world, and the closest thing to an open, model agnostic Claude Code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0pkn6lanxysm87cxsm0z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0pkn6lanxysm87cxsm0z.png" alt="opencode" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://opencode.ai" rel="noopener noreferrer"&gt;OpenCode&lt;/a&gt; is basically an open-source, model-agnostic coding agent that tries to give you the full Claude Code experience without locking you to Anthropic&lt;/p&gt;

&lt;p&gt;It is built by the team behind SST, which rebranded to Anomaly in 2026 and moved the repo to &lt;code&gt;anomalyco/opencode&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The numbers are crazy for a free tool. Around &lt;strong&gt;199k stars&lt;/strong&gt;. Roughly 8 million monthly active developers. Cloudflare publishes official OpenCode setup docs for its own engineers.&lt;/p&gt;

&lt;p&gt;Can you believe this free thing is doing over $25M ARR? It's just the tool that's free, there's subscription plans they've introduced from where they make tons of money.&lt;/p&gt;

&lt;p&gt;The origin story is crazier than Pi's. The original agent, called TermAI, was created by Kujtim Hoxha back in 2024. &lt;a href="https://x.com/thdxr" rel="noopener noreferrer"&gt;Dax Raad&lt;/a&gt; and Adam Elmore did the OpenCode rebrand and made it popular. Then Charm hired Hoxha and pulled the repo into its own org. That causes rewritten git history, banned contributors, the whole drama. After the backlash, Charm renamed its fork &lt;strong&gt;Crush&lt;/strong&gt;, and the Dax and Adam project kept the OpenCode name as a from scratch rewrite.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsp1sf9ncds2glip7s2gb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsp1sf9ncds2glip7s2gb.png" alt="opencode origin" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not anything interesting if you don't care about "history". Lol.&lt;/p&gt;

&lt;p&gt;So when anyone says OpenCode in 2026, they mean the Anomaly one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 &lt;strong&gt;Fun fact:&lt;/strong&gt; For the 1.0 release, the team threw out their entire Go based TUI and wrote a new framework called OpenTUI in Zig and SolidJS, because the old one "had performance and capability issues." Can't believe Golang had performance issues at that scale.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A coding agent team writing their own Zig rendering engine for the terminal is the most 2026 thing I can think of. Crazy stuff.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Philosophy: Pi vs OpenCode
&lt;/h2&gt;

&lt;p&gt;Our earlier comparisons were open vs closed. This one is open vs open.&lt;/p&gt;

&lt;p&gt;Both teams agree on the big stuff. The harness should be MIT licensed. You should bring your own model. Nobody should be vendor-locked in.&lt;/p&gt;

&lt;p&gt;They completely disagree on how much harness should exist.&lt;/p&gt;

&lt;p&gt;OpenCode's philosophy is that the harness is a real product. Ship plan mode, permissions, LSP integration, MCP, a desktop app, a web UI, IDE support, GitHub actions. Give people everything Claude Code has, minus the lock in.&lt;/p&gt;

&lt;p&gt;Pi's bet is that most of that is overhead (not a feature, what OpenCode believes). Frontier models have been RL trained so hard on coding that they already know what a coding agent is. Every tool schema and every injected instruction is context you pay for on every request. So the harness should be close to zero, and you add back only what you personally need.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/disler/pi-vs-claude-code/blob/main/PI_VS_OPEN_CODE.md" rel="noopener noreferrer"&gt;disler comparison&lt;/a&gt; has the cleanest one liner on this.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Pi gives you control at the runtime level. OpenCode gives you control at the configuration level."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And yes, there is real beef here. Zechner has publicly called out OpenCode's compaction for pruning tool results and breaking prompt caching.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhm8s27cqzhdwh4m2r63d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhm8s27cqzhdwh4m2r63d.png" alt="zechner called out opencode compaction" width="800" height="154"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;He is not a fan of their Vercel AI SDK dependency either. More on that below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz2ty3niibtgm8d4gd8bq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz2ty3niibtgm8d4gd8bq.png" alt="zechner not a fan of vercel ai sdk" width="800" height="190"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Agent Architecture: Pi vs OpenCode
&lt;/h2&gt;

&lt;p&gt;Underneath, both use the same basic loop: read the task, call the model, run tools, feed results back, repeat.&lt;/p&gt;

&lt;p&gt;The architecture around that loop is where they differ.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv09kkh6jojfuvzgrzb0c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv09kkh6jojfuvzgrzb0c.png" alt="agent architecture difference pi vs opencode" width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenCode: client + server
&lt;/h3&gt;

&lt;p&gt;OpenCode splits into a persistent local server and separate clients.&lt;/p&gt;

&lt;p&gt;The server handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;agent logic&lt;/li&gt;
&lt;li&gt;LLM calls&lt;/li&gt;
&lt;li&gt;tool execution&lt;/li&gt;
&lt;li&gt;SQLite session storage&lt;/li&gt;
&lt;li&gt;MCP servers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The TUI, desktop app, web UI, and IDE integrations all talk to that same server.&lt;/p&gt;

&lt;p&gt;This makes sessions more safe to terminal crashes or SSH drops, and the same server can also run headlessly in CI. The downside is a heavier footprint, with people reporting &lt;strong&gt;1GB+ RAM usage&lt;/strong&gt; for the TUI setup.&lt;/p&gt;

&lt;p&gt;You read it right. Read more about it here: &lt;a href="https://news.ycombinator.com/item?id=47465584" rel="noopener noreferrer"&gt;OpenCode is pretty resource inefficient&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F20ol6v5g70zqk4jmoqjq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F20ol6v5g70zqk4jmoqjq.png" alt="Opencode 1gb+ ram tui usage" width="800" height="264"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Pi: single process
&lt;/h3&gt;

&lt;p&gt;Pi keeps everything in one process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;provider layer&lt;/li&gt;
&lt;li&gt;agent loop&lt;/li&gt;
&lt;li&gt;CLI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is all TypeScript and small enough to understand without going through a huge codebase.&lt;/p&gt;

&lt;p&gt;Pi supports four run modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;interactive TUI&lt;/li&gt;
&lt;li&gt;print mode for scripts&lt;/li&gt;
&lt;li&gt;RPC over stdin/stdout&lt;/li&gt;
&lt;li&gt;SDK mode for embedding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That SDK is also how OpenClaw embeds Pi.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenCode's way of LSP
&lt;/h3&gt;

&lt;p&gt;I'd call it a star feature of OpenCode, the way it handles LSP (Language Server Protocol).&lt;/p&gt;

&lt;p&gt;OpenCode can run real language servers such as &lt;code&gt;gopls&lt;/code&gt; and &lt;code&gt;typescript-language-server&lt;/code&gt; and feed diagnostics back into the agent after edits.&lt;/p&gt;

&lt;p&gt;So the flow looks like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;model edits code → LSP reports an error → model sees it → model fixes it&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That gives OpenCode fast compiler and type feedback without needing to run the full test every time.&lt;/p&gt;

&lt;p&gt;Pi does not have this out of the box.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pi's Session trees
&lt;/h3&gt;

&lt;p&gt;Pi way of handling sessions is kinda interesting.&lt;/p&gt;

&lt;p&gt;Pi stores sessions as &lt;code&gt;JSONL&lt;/code&gt;, with every entry pointing to a parent. That means the session history is a tree instead of one linear log.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A → B → C
     ↘ D → E
     ↘ F → G
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use &lt;code&gt;/tree&lt;/code&gt; to jump back to an earlier point&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;/fork&lt;/code&gt; to branch from any past message&lt;/li&gt;
&lt;li&gt;keep multiple debugging paths from the same point&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OpenCode uses a linear history with &lt;code&gt;/undo&lt;/code&gt; and &lt;code&gt;/redo&lt;/code&gt;, which is simpler but less flexible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context compaction
&lt;/h3&gt;

&lt;p&gt;OpenCode uses a hidden system agent to summarize the conversation when context gets tight, then hides older messages from the active context.&lt;/p&gt;

&lt;p&gt;The criticism is that this can remove useful tool outputs and hurt prompt caching.&lt;/p&gt;

&lt;p&gt;Pi also supports compaction, but the implementation is replaceable through extensions, which fits its more hackable architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The benchmark. Same model, different harness
&lt;/h2&gt;

&lt;p&gt;Now for the part I was most curious about.&lt;/p&gt;

&lt;p&gt;We ran &lt;strong&gt;30 hard agentic tool-use tasks&lt;/strong&gt; against real apps. Every harness used the same model, &lt;strong&gt;DeepSeek V4 Pro (0813)&lt;/strong&gt; at max reasoning, through our hosted MCP router.&lt;/p&gt;

&lt;p&gt;Same model. Same tasks. Same tools. Same verifier checks.&lt;/p&gt;

&lt;p&gt;So whatever gap shows up here is mostly the harness.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzcxk81pd9nlfx0ftssdv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzcxk81pd9nlfx0ftssdv.png" alt="benchmark eval of opencode vs pi agent" width="799" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is the full result.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Harness (DeepSeek V4 Pro, hard 30 set)&lt;/th&gt;
&lt;th&gt;Tasks passed&lt;/th&gt;
&lt;th&gt;Cost per success&lt;/th&gt;
&lt;th&gt;Cost per shared success&lt;/th&gt;
&lt;th&gt;Avg tokens per task&lt;/th&gt;
&lt;th&gt;Avg turns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pi Agent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;21/30 (70%)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.078&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.031&lt;/td&gt;
&lt;td&gt;924,990&lt;/td&gt;
&lt;td&gt;16.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex&lt;/td&gt;
&lt;td&gt;20/30 (66.7%)&lt;/td&gt;
&lt;td&gt;n/a*&lt;/td&gt;
&lt;td&gt;$0.031&lt;/td&gt;
&lt;td&gt;383,722&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek Harness&lt;/td&gt;
&lt;td&gt;20/30 (66.7%)&lt;/td&gt;
&lt;td&gt;$0.076&lt;/td&gt;
&lt;td&gt;$0.028&lt;/td&gt;
&lt;td&gt;88,562&lt;/td&gt;
&lt;td&gt;0.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenCode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;19/30 (63.3%)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.119&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.032&lt;/td&gt;
&lt;td&gt;710,140&lt;/td&gt;
&lt;td&gt;13.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;19/30 (63.3%)&lt;/td&gt;
&lt;td&gt;n/a*&lt;/td&gt;
&lt;td&gt;$0.074&lt;/td&gt;
&lt;td&gt;649,900&lt;/td&gt;
&lt;td&gt;12.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hermes Agent&lt;/td&gt;
&lt;td&gt;18/30 (60%)&lt;/td&gt;
&lt;td&gt;n/a*&lt;/td&gt;
&lt;td&gt;$0.037&lt;/td&gt;
&lt;td&gt;113,894&lt;/td&gt;
&lt;td&gt;6.5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ *Not comparable because cost measure was incomplete for some cells. Shared success only counts tasks every harness passed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs67o45spge4sqmtrv2lb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs67o45spge4sqmtrv2lb.png" alt="eval report" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Pi actually topped the board
&lt;/h3&gt;

&lt;p&gt;Pi finished first with &lt;strong&gt;21/30 tasks passed&lt;/strong&gt;, while OpenCode landed at &lt;strong&gt;19/30&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8p1e2vviblg8vkreyzku.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8p1e2vviblg8vkreyzku.jpg" alt="agent leaderboard" width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It was also cheaper across the full run.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pi&lt;/strong&gt; spent $1.64&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenCode&lt;/strong&gt; spent $2.25&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pi cost per success&lt;/strong&gt; was $0.078&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenCode cost per success&lt;/strong&gt; was $0.119&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last number looks rough for OpenCode, but there is a catch.&lt;/p&gt;

&lt;p&gt;On tasks both harnesses actually passed, they were basically tied. Pi came in at &lt;strong&gt;$0.031 per shared success&lt;/strong&gt;, while OpenCode was &lt;strong&gt;$0.032&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So OpenCode was not really more expensive when it worked. It just failed more tasks, and &lt;strong&gt;failed runs still burn tokens&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pi was also painfully slow
&lt;/h3&gt;

&lt;p&gt;Pi had the best pass rate, but it was also the &lt;strong&gt;slowest harness in the entire run&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fekaax7isq4u9zfbsy07i.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fekaax7isq4u9zfbsy07i.jpg" alt="agent time eval comparison" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pi median time&lt;/strong&gt; was 362.9 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenCode median time&lt;/strong&gt; was 280.6 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code median time&lt;/strong&gt; was 181.8 seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pi also averaged &lt;strong&gt;16.3 turns per task&lt;/strong&gt;, compared with OpenCode's &lt;strong&gt;13.1&lt;/strong&gt;, and pushed more raw tokens than anyone else.&lt;/p&gt;

&lt;p&gt;The difference is that its fixed overhead stays tiny.&lt;/p&gt;

&lt;p&gt;The system prompt plus tool definitions stay under 1,000 tokens, and the prefix barely changes between turns. That makes a lot of that repeated context land as cache hits.&lt;/p&gt;

&lt;h3&gt;
  
  
  The task-by-task split is more interesting
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Pi&lt;/th&gt;
&lt;th&gt;OpenCode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CRM migration archive&lt;/td&gt;
&lt;td&gt;✅ 10/10&lt;/td&gt;
&lt;td&gt;❌ 3/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slack action items&lt;/td&gt;
&lt;td&gt;✅ 5/5&lt;/td&gt;
&lt;td&gt;❌ 2/5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vendor directory&lt;/td&gt;
&lt;td&gt;✅ 11/13 checks&lt;/td&gt;
&lt;td&gt;❌ 7/12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Handover audit&lt;/td&gt;
&lt;td&gt;✅ 11/11&lt;/td&gt;
&lt;td&gt;❌ 10/11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PagerDuty timeline audit&lt;/td&gt;
&lt;td&gt;✅ 10/10&lt;/td&gt;
&lt;td&gt;❌ 9/10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Renewal screen&lt;/td&gt;
&lt;td&gt;❌ 5/11&lt;/td&gt;
&lt;td&gt;✅ 11/11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sponsorship screen&lt;/td&gt;
&lt;td&gt;❌ 4/11&lt;/td&gt;
&lt;td&gt;✅ 11/12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sheets to Airtable tracker&lt;/td&gt;
&lt;td&gt;❌ 8/10&lt;/td&gt;
&lt;td&gt;✅ 10/10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn5ccw21opj1h7il64v1r.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn5ccw21opj1h7il64v1r.jpg" alt="task-by-task split of agent test" width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pi did better on the &lt;strong&gt;audit and census-style tasks&lt;/strong&gt;, where patiently reading everything and checking every detail matters.&lt;/p&gt;

&lt;p&gt;OpenCode did better on the &lt;strong&gt;longer screening and multi-step workflows&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Which is kinda perfect.&lt;/p&gt;

&lt;p&gt;Pi wins by being small. OpenCode wins when the workflow benefits from more stuff around the model.&lt;/p&gt;

&lt;p&gt;After all this non-sense, the eval ended up being spot on for both of them. 🤧&lt;/p&gt;

&lt;p&gt;We did a complete breakdown in this tweet. You might want to &lt;a href="https://x.com/composio/status/2090069397050097864" rel="noopener noreferrer"&gt;check it out&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-2090069397050097864-193" src="https://platform.twitter.com/embed/Tweet.html?id=2090069397050097864"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-2090069397050097864-193');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=2090069397050097864&amp;amp;theme=dark"
  }



&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Token overhead
&lt;/h2&gt;

&lt;p&gt;This is basically the argument both tools were built around, so let's put some numbers next to it.&lt;/p&gt;

&lt;p&gt;The most cited comparison here is the &lt;a href="https://systima.ai/blog/claude-code-vs-opencode-token-overhead" rel="noopener noreferrer"&gt;Systima study&lt;/a&gt; from July, which also ended up doing &lt;a href="https://news.ycombinator.com/item?id=48883275" rel="noopener noreferrer"&gt;700+ points on Hacker News&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;They put a logging proxy between the harness and the model, then measured how much context gets sent &lt;strong&gt;before the model even sees your prompt&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Harness&lt;/th&gt;
&lt;th&gt;Fixed overhead per request&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code (measured by Systima)&lt;/td&gt;
&lt;td&gt;~32,800 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenCode (measured by Systima)&lt;/td&gt;
&lt;td&gt;~6,900 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;td&gt;Under 1,000 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;OpenCode already looks pretty good here. Around &lt;strong&gt;4.7x less overhead than Claude Code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Then Pi shows up with &lt;strong&gt;under 1,000 tokens&lt;/strong&gt;, including the tool definitions.&lt;/p&gt;

&lt;p&gt;That makes it roughly &lt;strong&gt;7x leaner than OpenCode&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 Everyone is somebody's bloatware. 🗿&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Matei Zaharia &lt;a href="https://x.com/matei_zaharia/status/2074943619610649059" rel="noopener noreferrer"&gt;said on X&lt;/a&gt; the reason was pretty simple. Pi had the same success rate with &lt;strong&gt;2x less cost.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyjhr0zfzfpglr03e4nkk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyjhr0zfzfpglr03e4nkk.png" alt="pi success rate at 2x less cost compared to other agents" width="800" height="583"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our eval points in the same direction too. Pi had the best pass rate and the lowest total spend.&lt;/p&gt;

&lt;p&gt;But there is an important catch here.&lt;/p&gt;

&lt;p&gt;OpenCode has had some pretty ugly token cases too. There is a &lt;a href="https://github.com/anomalyco/opencode/issues/8234" rel="noopener noreferrer"&gt;GitHub issue&lt;/a&gt; where asking something as basic as "what's the tech stack in this project" burned &lt;strong&gt;13,088 tokens&lt;/strong&gt; on a fresh project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fag06f0ra64r5zoyqw18j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fag06f0ra64r5zoyqw18j.png" alt="excessive token usage of opencode" width="799" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And Pi is not crazy cheap in every sense either. In our eval, it actually pushed the &lt;strong&gt;highest raw token count of all six harnesses&lt;/strong&gt;, mostly because it took more turns to finish tasks.&lt;/p&gt;

&lt;p&gt;So yeah, tiny fixed overhead helps a lot. But &lt;strong&gt;lean overhead does not always mean lean sessions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Worth keeping that distinction in mind.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Pricing and the ban
&lt;/h2&gt;

&lt;p&gt;Both tools are free and MIT licensed. Fork them, modify them, ship them.&lt;/p&gt;

&lt;p&gt;The difference starts after install.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pi
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;BYOK for pretty much any provider&lt;/li&gt;
&lt;li&gt;local models through Ollama, vLLM, or llama.cpp&lt;/li&gt;
&lt;li&gt;OAuth login where providers still allow it&lt;/li&gt;
&lt;li&gt;no paid CLI tier at all&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 Pi is basically just the harness. You bring the model and pay whoever is serving it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  OpenCode
&lt;/h3&gt;

&lt;p&gt;OpenCode does the same BYOK thing, but it also has its own model options.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BYOK&lt;/strong&gt; where the provider bills you directly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zen&lt;/strong&gt; with curated models sold on a pay-as-you-go basis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go&lt;/strong&gt; with open coding models bundled into a cheap monthly plan&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Go starts at &lt;strong&gt;$5 for the first month, then $10/month&lt;/strong&gt;, with models like GLM, Kimi K3, Qwen, MiniMax, and DeepSeek.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Double check the pricing. OpenCode changes this stuff pretty often.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And honestly, that Go plan is hard to ignore.&lt;/p&gt;

&lt;p&gt;For $10/month, it is probably one of the cheapest legit ways to run capable open models through a coding agent right now.&lt;/p&gt;

&lt;p&gt;Pi does not really have an equivalent. Its version of "cheap" is running local models, which is useful, but it is not the same thing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Then Anthropic happened
&lt;/h3&gt;

&lt;p&gt;In January 2026, Anthropic started rejecting Claude Pro and Max OAuth tokens when they were used outside Claude Code.&lt;/p&gt;

&lt;p&gt;The error was pretty clear. The credential was &lt;strong&gt;"only authorized for use with Claude Code."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;OpenCode had been using those subscription tokens by mimicking the Claude Code client, so this hit them directly.&lt;/p&gt;

&lt;p&gt;Then came the legal requests, the Claude login code got removed, and we got the now-famous commit message from the intro.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmpc88vg2fnh7c1erd5x1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmpc88vg2fnh7c1erd5x1.png" alt="george hotz calling out anthropic move of removing sub a huge mistake" width="800" height="306"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;George Hotz called the move &lt;strong&gt;"a huge mistake."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;OpenAI reportedly went in the other direction and opened Codex subscription access to third-party harnesses, including OpenCode and Pi.&lt;/p&gt;

&lt;p&gt;Lol.&lt;/p&gt;

&lt;p&gt;The important part for this comparison is pretty simple now.&lt;/p&gt;

&lt;p&gt;If you want Claude in either Pi or OpenCode, you are basically paying API rates. OpenCode also gives you Zen as another route.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Extensibility
&lt;/h2&gt;

&lt;p&gt;Both are highly customizable, but the extension model is very different.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenCode
&lt;/h3&gt;

&lt;p&gt;OpenCode is mostly config driven through &lt;code&gt;opencode.json&lt;/code&gt;, plugins, agents, skills, and MCP.&lt;/p&gt;

&lt;p&gt;You can add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;custom agents with their own model, prompt, permissions, and step limits&lt;/li&gt;
&lt;li&gt;JS or TS plugins with lifecycle hooks like &lt;code&gt;tool.execute.before&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;custom tools&lt;/li&gt;
&lt;li&gt;local and remote MCP servers&lt;/li&gt;
&lt;li&gt;skills from &lt;code&gt;.opencode/skills&lt;/code&gt; and &lt;code&gt;.claude/skills&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permission"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"bash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"git *"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rm *"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ask"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pi
&lt;/h3&gt;

&lt;p&gt;Pi extensions are TypeScript modules loaded directly into the single agent process.&lt;/p&gt;

&lt;p&gt;They can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rewrite or block tool calls&lt;/li&gt;
&lt;li&gt;replace built-in tools&lt;/li&gt;
&lt;li&gt;filter history&lt;/li&gt;
&lt;li&gt;replace compaction&lt;/li&gt;
&lt;li&gt;persist custom session state&lt;/li&gt;
&lt;li&gt;add TUI components&lt;/li&gt;
&lt;li&gt;probably many more...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives Pi much &lt;strong&gt;deeper runtime control&lt;/strong&gt; than OpenCode.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ OpenCode plugins extend the harness. Pi extensions can modify the harness itself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The tradeoff is security. Pi extensions run in process with full system access, so they need more trust than OpenCode's plugin model.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Permissions and safety
&lt;/h2&gt;

&lt;p&gt;This is probably the biggest gap between the two.&lt;/p&gt;

&lt;p&gt;OpenCode has a proper permission system. Every tool can be set to &lt;code&gt;allow&lt;/code&gt;, &lt;code&gt;ask&lt;/code&gt;, or &lt;code&gt;deny&lt;/code&gt;, with glob patterns and per-agent overrides. The defaults are decent too. &lt;code&gt;.env&lt;/code&gt; reads are denied by default, and the Plan agent asks before edits or bash commands.&lt;/p&gt;

&lt;p&gt;What it does &lt;strong&gt;not&lt;/strong&gt; have is real sandboxing. There is no container or OS-level isolation built in.&lt;/p&gt;

&lt;p&gt;Pi goes the other way and basically ships no permission layer at all. From the first prompt, the agent runs with your full user privileges. 🥶&lt;/p&gt;

&lt;p&gt;That is intentional. Zechner's say is that once an agent can write files and execute code, permission prompts only get you so far. Pi instead expects you to isolate the whole process using Docker, a microVM like Gondolin, or a policy sandbox like OpenShell.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 There is also an optional &lt;code&gt;damage-control.ts&lt;/code&gt; extension that checks bash commands against YAML rules. Helps a bit.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the actual difference is pretty simple.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ OpenCode gives you tool-level guardrails. Pi expects you to handle isolation outside the agent.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Which one should you pick?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pick OpenCode if&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want something close to a full Claude Code replacement without building half the setup yourself&lt;/li&gt;
&lt;li&gt;You want plan mode, permissions, MCP, undo and redo, and LSP support out of the box&lt;/li&gt;
&lt;li&gt;You want the $10 Go plan for cheap access to capable open models&lt;/li&gt;
&lt;li&gt;You work in typed languages and want LSP diagnostics fed straight back into the agent&lt;/li&gt;
&lt;li&gt;You are setting this up for a team and actually need proper &lt;code&gt;allow&lt;/code&gt;, &lt;code&gt;ask&lt;/code&gt;, and &lt;code&gt;deny&lt;/code&gt; controls&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pick Pi if&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You care about token cost at scale. In our eval, the same model passed more tasks and cost less with Pi&lt;/li&gt;
&lt;li&gt;You want a harness small enough that you can actually read and understand the whole thing&lt;/li&gt;
&lt;li&gt;You like branching while debugging. Pi's session trees are really good for that&lt;/li&gt;
&lt;li&gt;You run local or smaller models where the difference between ~1,000 and ~6,900 tokens of fixed overhead actually matters&lt;/li&gt;
&lt;li&gt;You would rather change the harness in TypeScript than spend time configuring someone else's abstraction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The benchmark kinda gives away the answer anyway.&lt;/p&gt;

&lt;p&gt;Pi did better on the careful audit-style tasks. OpenCode did better on longer, more structured workflows.&lt;/p&gt;

&lt;p&gt;So there probably isn't one winner for everything.&lt;/p&gt;

&lt;p&gt;Both are free. Try both and keep the one that fits how you work. ✌️&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftya4fg1aj5uz7ql0o8hu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftya4fg1aj5uz7ql0o8hu.png" alt="Pi Agent vs. OpenCode test summary result" width="800" height="590"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After all that, I still don’t think there’s one obvious winner here. The scorecard ends &lt;strong&gt;6-6&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;OpenCode wins on adoption, features, and ease of use. Pi won our benchmark with &lt;strong&gt;21/30 tasks vs 19/30&lt;/strong&gt;, lower total spend, and way less fixed overhead.&lt;/p&gt;

&lt;p&gt;The bigger difference is still the same one we started with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;OpenCode gives you configuration control. Pi gives you runtime control.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you just want to install something and start working, pick &lt;strong&gt;OpenCode&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you want to understand and modify the harness itself, pick &lt;strong&gt;Pi&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And if cost per successful task starts mattering at scale, keep an eye on Pi. 🤷‍♂️&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>opensource</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Pi Agent vs Claude Code After 100 Hours of Real Use 🔥</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Tue, 11 Aug 2026 13:36:48 +0000</pubDate>
      <link>https://dev.to/composiodev/pi-agent-vs-claude-code-after-100-hours-of-real-use-1dfp</link>
      <guid>https://dev.to/composiodev/pi-agent-vs-claude-code-after-100-hours-of-real-use-1dfp</guid>
      <description>&lt;p&gt;While researching for this post, I found out something funny. Pi, the most interesting coding agent of 2026, was built by a guy who got fed up with Claude Code. 🥶&lt;/p&gt;

&lt;p&gt;&lt;a href="https://x.com/badlogicgames" rel="noopener noreferrer"&gt;Mario Zechner&lt;/a&gt;, the creator of libGDX, was a hardcore Claude Code user. He built &lt;a href="https://mariozechner.at/posts/2025-08-03-cchistory/" rel="noopener noreferrer"&gt;cchistory&lt;/a&gt; just to track its system prompt changes, and even &lt;a href="https://mariozechner.at/posts/2025-08-06-cc-antidebug/" rel="noopener noreferrer"&gt;patched the binary&lt;/a&gt; to add features Anthropic had not shipped.&lt;/p&gt;

&lt;p&gt;Then he quit and wrote his own agent. Four tools, a tiny system prompt, no MCP, no permissions. He named it Pi so nobody could Google it. What a Chad! 🗿&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F18euu1imp5t223k454dv.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F18euu1imp5t223k454dv.gif" alt="swag gif" width="500" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That small project now has 85k+ GitHub stars, powers OpenClaw, and outscored Claude Code on our own tool use benchmark at a fifth of the cost.&lt;/p&gt;

&lt;p&gt;So I put the two side by side. The default everyone uses, against the minimal harness built as a rejection of it.&lt;/p&gt;

&lt;p&gt;Let's go!&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;th&gt;Pi&lt;/th&gt;
&lt;th&gt;Winner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tool price&lt;/td&gt;
&lt;td&gt;Needs a paid plan or API key&lt;/td&gt;
&lt;td&gt;Free, MIT licensed&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real tool use (our eval)&lt;/td&gt;
&lt;td&gt;16/30 tasks passed, $0.195 per success&lt;/td&gt;
&lt;td&gt;20/30 tasks passed, $0.028 per success&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Predictable billing&lt;/td&gt;
&lt;td&gt;$20/month flat on Pro&lt;/td&gt;
&lt;td&gt;Pay per token, bills can spike&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model support&lt;/td&gt;
&lt;td&gt;Claude models only but it’s built for it&lt;/td&gt;
&lt;td&gt;20+ providers, 300+ models, mid session switching&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Out of the box features&lt;/td&gt;
&lt;td&gt;Sub agents, teams, plan mode, MCP, skills, checkpoints&lt;/td&gt;
&lt;td&gt;Four tools and a text box&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context discipline&lt;/td&gt;
&lt;td&gt;Auto compaction, silent context management&lt;/td&gt;
&lt;td&gt;You see and control every token&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extensibility depth&lt;/td&gt;
&lt;td&gt;Shell hooks, MCP, skills, plugins&lt;/td&gt;
&lt;td&gt;TypeScript extensions running inside the agent process&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi agent workflows&lt;/td&gt;
&lt;td&gt;Native sub agents and Agent Teams&lt;/td&gt;
&lt;td&gt;Spawn pi via bash, or install a community extension&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permissions and guardrails&lt;/td&gt;
&lt;td&gt;Deny by default, five modes, sandboxing&lt;/td&gt;
&lt;td&gt;Full system access from the first prompt&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Surfaces&lt;/td&gt;
&lt;td&gt;Terminal, VS Code, JetBrains, desktop, web, mobile, Slack&lt;/td&gt;
&lt;td&gt;Terminal&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session model&lt;/td&gt;
&lt;td&gt;Linear conversation log&lt;/td&gt;
&lt;td&gt;Branchable session trees with fork and rewind&lt;/td&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  In Summary:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Scorecard: Pi 6, Claude Code 5.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pi is the cheaper, more flexible, and more transparent harness, especially if you pay per token, switch models, or want full control. Claude Code remains the better daily driver for most people because it ships with stronger guardrails, predictable subscription pricing, and a polished feature set out of the box. The practical answer is to use Claude Code for everyday work and Pi for custom workflows, local models, and cost-sensitive tasks.&lt;/p&gt;

&lt;p&gt;And here is the twist. The scorecard says Pi, but almost everyone who reviews both, including the biggest Pi fans, still runs Claude Code as their daily driver. I will explain why is that in a minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Pi?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ A minimal, open source terminal coding agent that ships four tools and expects you to build the rest yourself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa91gkp42cqdq38nenpqm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa91gkp42cqdq38nenpqm.png" alt="Pi agent" width="800" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pi.dev/" rel="noopener noreferrer"&gt;Pi&lt;/a&gt; is the coding agent Mario Zechner released in late 2025, now maintained under &lt;a href="https://github.com/earendil-works/pi" rel="noopener noreferrer"&gt;Earendil&lt;/a&gt;. Zechner joined Earendil in April 2026 in a blog post he titled "I've sold out," and the core stayed MIT licensed.&lt;/p&gt;

&lt;p&gt;The whole product fits in literally one sentence. The model gets &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, and &lt;code&gt;bash&lt;/code&gt;, a system prompt of a few hundred tokens, and nothing else.&lt;/p&gt;

&lt;p&gt;His reasoning: frontier models have been RL trained so hard on coding agent behavior that they already know what a coding agent is. You do not need 10,000 tokens of instructions to remind them. Need ripgrep? The model runs &lt;code&gt;rg&lt;/code&gt; through bash. Need GitHub? It runs &lt;code&gt;gh&lt;/code&gt;. Need a browser? Ask Pi to write itself a browser tool.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 If you're on the same philosophy, this might be worth reading: &lt;a href="https://composio.dev/content/top-10-cli-tools-for-claude-code" rel="noopener noreferrer"&gt;Top 10 CLI Tools to Level-Up Claude Code&lt;/a&gt;. No MCPs, nothing. Plain raw Bash tools gets most of the work done.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That last part is the actual product. Pi extensions are TypeScript files that run inside the same process as the agent loop, and Pi can read its own source and docs, so the standard workflow when you need a missing feature is to ask Pi to build it.&lt;/p&gt;

&lt;p&gt;Everything Zechner refused to ship, the community shipped as extensions within weeks. Todo lists, plan mode, sub agents, permission gates, even an MCP adapter.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnbszl2dke891ql0slm3e.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnbszl2dke891ql0slm3e.gif" alt="Laugh GIF" width="480" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Claude Code
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ Anthropic's batteries included coding agent, and the default the entire agent category gets measured against&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foe7igo0vtukw40guj4zm.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foe7igo0vtukw40guj4zm.jpg" alt="Claude Code" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You know this one. &lt;a href="https://code.claude.com/docs/en/overview" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; started as Boris Cherny's internal AppleScript toy at Anthropic in September 2024, hit research preview in February 2025, and became the fastest scaling developer product anyone has ever measured.&lt;/p&gt;

&lt;p&gt;It ships everything: 10+ built in tools, sub agents, Agent Teams, plan mode, MCP as both client and server, Agent Skills, plugins, hooks, checkpoints with rewind, a permission system with five modes, and it runs in the terminal, VS Code, JetBrains, a desktop app, the browser, and your phone.&lt;/p&gt;

&lt;p&gt;More than 80% of Anthropic's own engineers use it daily, and around "90% of Claude Code's code is now written by Claude Code". 🥴&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Philosophy: Pi vs Claude Code
&lt;/h2&gt;

&lt;p&gt;This comparison only makes sense once you understand that Pi exists because of Claude Code.&lt;/p&gt;

&lt;p&gt;Zechner was an early Claude Code user. His complaint was never that it was bad. His complaint was that it kept changing underneath him. The system prompt and tool definitions shifted on every release, which broke his workflows and changed model behavior.&lt;/p&gt;

&lt;p&gt;So the two tools represent opposite answers to the same question: how much harness does a frontier model actually need?&lt;/p&gt;

&lt;p&gt;Anthropic's answer is interesting because it is drifting toward Pi's. Boris Cherny's team deletes harness code every time a new model ships, and in July 2026 Anthropic removed over 80% of Claude Code's system prompt for the Claude 5 generation models with no measurable loss on their coding evals. The bet behind Claude Code is that Anthropic trains the model and the harness together, so scaffolding today can become model behavior tomorrow.&lt;/p&gt;

&lt;p&gt;Pi's answer is that the scaffolding was never load bearing in the first place.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 Anthropic quietly deleting most of its own system prompt is about the strongest validation of Pi's thesis you could ask for. Zechner just got there a year early.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  2. Agent Architecture: Claude Code vs Pi
&lt;/h2&gt;

&lt;p&gt;Both run the same loop underneath. Read the task, call the model, execute tool calls, feed results back, repeat until the model stops asking for tools.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcefop0itn8v17urai239.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcefop0itn8v17urai239.jpg" alt="agent loop" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The difference is everything wrapped around that loop.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code&lt;/strong&gt; gives the model Read, Write, Edit, Bash, Glob, Grep, WebSearch, WebFetch, NotebookEdit, TodoWrite, there's a lot.... and the Task tool that spawns typed sub agents with their own isolated context windows. Plan mode forces a read only explore and plan phase before any file gets touched.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pi&lt;/strong&gt; gives the model four tools, plus optional read only &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;, and &lt;code&gt;ls&lt;/code&gt; behind a &lt;code&gt;-tools&lt;/code&gt; flag. There is no plan mode, but &lt;code&gt;pi --tools read,grep,find,ls&lt;/code&gt; gets you a read only session in one line. There are no sub agents, and they say that if you want one, tell Pi to run itself through bash, ideally in tmux so you can actually watch it work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The system prompt gap is the main highlight:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;System prompt&lt;/th&gt;
&lt;th&gt;Built in tools&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code (pre Claude 5 era)&lt;/td&gt;
&lt;td&gt;~10,000 to 14,000 tokens by community measurement&lt;/td&gt;
&lt;td&gt;10+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code (current)&lt;/td&gt;
&lt;td&gt;Cut by over 80% for Claude 5 gen models&lt;/td&gt;
&lt;td&gt;10+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;td&gt;Under 1,000 tokens including tool definitions&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One less obvious architectural difference: Pi stores sessions as trees, not logs. Every message carries a parent id, so under the hood this is basically a linked list that learned to branch.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A real use case for linked lists, in the wild. All that leetcode grind had something useful. 🤡&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So &lt;code&gt;/tree&lt;/code&gt; lets you jump back to any earlier point in the conversation and branch from there, and &lt;code&gt;/fork&lt;/code&gt; spins a new session off any past message. Claude Code got checkpoints and rewind in 2.0, which covers the common case, but Pi's tree model is the better design.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The benchmark: same model, different harness
&lt;/h2&gt;

&lt;p&gt;We ran 30 hard agentic tool use tasks against real apps, eight harnesses, every single one wired to the same model, DeepSeek V4 Flash, through our hosted MCP router, with a 900 second ceiling per task.&lt;/p&gt;

&lt;p&gt;Same model, same tasks, same tools. The only thing that changes is the harness, so whatever gap shows up is the wrapper, not the model.&lt;/p&gt;

&lt;p&gt;Here is how our two contenders did:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Harness (DeepSeek V4 Flash, hard 30 set)&lt;/th&gt;
&lt;th&gt;Tasks passed&lt;/th&gt;
&lt;th&gt;Median time&lt;/th&gt;
&lt;th&gt;Avg tokens per task&lt;/th&gt;
&lt;th&gt;Total cost&lt;/th&gt;
&lt;th&gt;Cost per success&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;16/30 (53.3%)&lt;/td&gt;
&lt;td&gt;122.7s&lt;/td&gt;
&lt;td&gt;741,659&lt;/td&gt;
&lt;td&gt;$3.12&lt;/td&gt;
&lt;td&gt;$0.195&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;td&gt;20/30 (66.7%)&lt;/td&gt;
&lt;td&gt;132.2s&lt;/td&gt;
&lt;td&gt;558,885&lt;/td&gt;
&lt;td&gt;$0.56&lt;/td&gt;
&lt;td&gt;$0.028&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1g15qyiwhm1p1k1gs8pw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1g15qyiwhm1p1k1gs8pw.png" alt="eight agent harnesses eval" width="799" height="287"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pi passed more tasks than any of the eight harnesses we tested, and did it at the lowest cost of the whole bunch. Cost per successful task is the brutal column: &lt;strong&gt;3 cents against 19&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Claude Code posted the fastest median time. It just burned 741,659 tokens per task getting there, against Pi's 558,885. The overhead is the story, again.&lt;/p&gt;

&lt;p&gt;And yes, that is Claude Code running DeepSeek. That is the whole point of the setup. Only the harness changes.&lt;/p&gt;

&lt;p&gt;For the curious, Codex ran the same and landed at 16/30 for $1.29 total. Right between the two.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8zelou5gp9orqip73xjn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8zelou5gp9orqip73xjn.png" alt="agent harness report" width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;NOTE:&lt;/strong&gt; Pi ran on high reasoning instead of max, and 24 of its 30 trials went through the official DeepSeek API instead of OpenRouter. Make sure to read the exact numbers with that in mind.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  4. Pricing: Claude Code vs Pi
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Claude Code
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Pro, $20/month or $17/month annually, includes Claude Code&lt;/li&gt;
&lt;li&gt;Max 5x at $100/month, Max 20x at $200/month&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Guys, make sure to double check the pricing. It changes frequently.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Pi
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The tool costs nothing. MIT license, fork it, ship it&lt;/li&gt;
&lt;li&gt;You bring your own API keys for any provider, or run local models for free through Ollama, vLLM, or llama.cpp&lt;/li&gt;
&lt;li&gt;OAuth login for subscriptions where providers allow it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On paper Pi wins on price and Claude Code wins on predictability. A $20 flat rate is genuinely hard to beat for a solo dev using Claude models all day, and per token bills on heavy Pi usage can hit hundreds of dollars a month. I usually prefer the subscription system.&lt;/p&gt;

&lt;p&gt;Anthropic locked its subscription OAuth tokens to Claude Code and &lt;a href="http://claude.ai/" rel="noopener noreferrer"&gt;Claude.ai&lt;/a&gt; only. Third party harnesses got cut off, starting with a silent server side block in January, formalized in the docs by February, fully enforced by April. OpenClaw, OpenCode, Cline, and yes, Pi, all lost the ability to run on your Claude Pro or Max subscription.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2a55k881h4c2noc8c2q.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2a55k881h4c2noc8c2q.webp" alt="anthropic locking subscription for third party providers" width="800" height="757"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So the practical situation today: running Claude models through Pi means API rates. Running them through Claude Code means your cheap subscription.&lt;/p&gt;

&lt;p&gt;DHH called the move "very customer hostile," Lol.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq8w1n0o9c5089f3wal0m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq8w1n0o9c5089f3wal0m.png" alt="DHH calling anthropic move costomer hostile" width="800" height="687"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;OpenAI's Codex team publicly went the opposite direction and encouraged third party harness usage.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 If your workflow is Claude models all day every day, the subscription math alone mostly settles this comparison. Pi's cost advantage is real on API keys and irrelevant on a Max plan you already pay for.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  5. Models: one provider vs all of them
&lt;/h2&gt;

&lt;p&gt;Claude Code runs Claude. Opus, Sonnet, and Haiku tiers, switchable with &lt;code&gt;/model&lt;/code&gt;, with thinking effort levels, tuned end to end because Anthropic controls the whole stack.&lt;/p&gt;

&lt;p&gt;That vertical integration is why the model feels so at home in the harness. It was trained in it.&lt;/p&gt;

&lt;p&gt;Pi runs everything. The &lt;code&gt;pi-ai&lt;/code&gt; layer normalizes four API shapes across 20+ providers, from Anthropic and OpenAI to Groq, Cerebras, Mistral, OpenRouter, and local models. Over 300 models total. You can switch models mid session with &lt;code&gt;Ctrl+P&lt;/code&gt;, and Pi converts thinking traces between providers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9nk81lwqadsamqgaf7n2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9nk81lwqadsamqgaf7n2.png" alt="multiple model providers in pi agent" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude Code's giant prompt overhead makes it a poor fit for small local models, which is exactly why Simon Willison flagged Pi as the lightweight option for that use case.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Extensibility
&lt;/h2&gt;

&lt;p&gt;Both tools are deeply customizable.&lt;/p&gt;

&lt;p&gt;Claude Code's model is external and config driven. Hooks are shell scripts that fire on 14 lifecycle events and talk JSON over stdin. MCP servers are separate processes speaking a protocol. Skills are markdown instruction packs with lazy loading. Plugins bundle all of it into installable packages with official and community marketplaces behind a single &lt;code&gt;/plugin&lt;/code&gt; command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Bash(npm run test *)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git status *)"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Read(./.env)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(curl *)"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Pi's model is internal. Extensions are TypeScript modules loaded into the same runtime as the agent loop, hooking 25 event types. They can block or rewrite tool calls before they run, override the built in tools entirely, inject context, replace the compaction logic, persist custom state into the session file, and draw their own TUI widgets. Claude Code's hooks can approve or reject what the agent does. Pi's extensions can change what the agent is.&lt;/p&gt;

&lt;p&gt;The tradeoff is trust. Claude Code's marketplace plugins go through some checks and run outside the core process. Pi packages run with full system access, and its own docs tell you to review them before installing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The sharpest criticism of Pi's ecosystem, from &lt;a href="https://www.thevinter.com/blog/bad-vibes-from-pi" rel="noopener noreferrer"&gt;thevinter's "Bad Vibes From Pi"&lt;/a&gt;, lands exactly here: a lot of those community extensions are themselves vibecoded, so that's something to watch-out for.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8nbc83mudu2tor7799jt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8nbc83mudu2tor7799jt.png" alt="Pi agent extensions probable harm talk" width="800" height="265"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  7. Permissions and safety
&lt;/h2&gt;

&lt;p&gt;This is the widest gap between the two tools, and neither side considers it a flaw.&lt;/p&gt;

&lt;p&gt;Claude Code is deny by default. Every risky action asks first, and you can loosen from there through five permission modes, allow and deny rules, and OS level sandboxing for bash.&lt;/p&gt;

&lt;p&gt;When Claude Code has security incidents, and it has had real ones with CVEs attached, they get found, patched, and written up because millions of people are using it.&lt;/p&gt;

&lt;p&gt;Pi has no permission system. First prompt, full user privileges, go.&lt;/p&gt;

&lt;p&gt;Zechner's argument is that agent security is mostly "theater" 🫩, because once a tool can write code and run code the game is already over. Pi's answer to safety is isolation instead of prompts: run it in Docker, in a micro VM, or behind a sandbox, and there is an opt in extension that checks bash commands against YAML rules if you want a middle ground.&lt;/p&gt;

&lt;p&gt;One of them requires you to have read the docs, and the other protects the "intern" who did not.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There isn't much to discuss here. You probably already know how things work by now...&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  What the Internet thinks
&lt;/h2&gt;

&lt;p&gt;Maybe worth seeing what the internet thinks of Pi and Claude Code by now?&lt;/p&gt;

&lt;p&gt;It's more of what we are discussing so far. The famous endorsements is around Pi, the daily usage clusters around Claude Code, and both facts are true.&lt;/p&gt;

&lt;p&gt;Let me show you the Pi love first, because it is a lot.&lt;/p&gt;

&lt;p&gt;Peter Steinberger built OpenClaw on top of Pi and quote tweeted Zechner with "Mario is the real one we don't deserve." Armin Ronacher wrote that Pi is &lt;a href="https://lucumr.pocoo.org/2026/1/31/pi/" rel="noopener noreferrer"&gt;the coding agent he uses "almost exclusively"&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fai19ky33q5v99uvizdsp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fai19ky33q5v99uvizdsp.png" alt="openclaw creator praising pi agent creator" width="800" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Theo from &lt;a href="http://t3.gg/" rel="noopener noreferrer"&gt;t3.gg&lt;/a&gt; did a whole episode about getting Pi pilled. His take was simple: "The more minimal, the better." Wes Bos and Scott Tolinski spent a Syntax episode arguing Claude Code is overkill for most work.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/3DNkDIVKtK8"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;My favorite line comes from &lt;a href="https://agenticengineer.com/the-only-claude-code-competitor" rel="noopener noreferrer"&gt;IndyDevDan&lt;/a&gt;, who calls Pi the only real Claude Code competitor: "Claude Code is the starter pack. Pi is the endgame."&lt;/p&gt;

&lt;p&gt;About 80% of his work still goes through Claude Code. Only 20% goes through Pi. The endgame guy is still playing the starter pack. The internet is wild!&lt;/p&gt;

&lt;p&gt;Claude Code does not need loud fans anyway. It has $2.5B in revenue (might increase, researched through Claude itself), 141k GitHub stars, most of the enterprise market, and Microsoft reportedly using it inside their own teams. The numbers are just enough, lol. 🤧&lt;/p&gt;


&lt;h2&gt;
  
  
  Which one should you pick?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pick Claude Code if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You want something that works at full power&lt;/li&gt;
&lt;li&gt;You live on Claude models, because the $20 to $200 subscriptions are the cheapest way to run them and Pi is locked out of that pricing&lt;/li&gt;
&lt;li&gt;You are deploying to a team and need permissions, SSO, spend caps, and audit trails&lt;/li&gt;
&lt;li&gt;You want sub agents, plan mode, and multi surface access without building anything&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pick Pi if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You pay per token at scale, since less context per task is real money, a fifth of the cost in our eval&lt;/li&gt;
&lt;li&gt;You want to mix providers, run open models, or go fully local&lt;/li&gt;
&lt;li&gt;You want to read, own, and modify every part of your harness, MIT license and all&lt;/li&gt;
&lt;li&gt;You are the kind of engineer who would rather build the exact workflow you want than configure someone else's&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Do what most reviewers actually do:&lt;/strong&gt; run both. Claude Code as the primary builder on your subscription, Pi as the cheap second opinion, the local model runner, and the place where your custom workflows live. The two tools are converging anyway.&lt;/p&gt;

&lt;p&gt;Claude Code keeps deleting scaffolding, and the Pi community keeps rebuilding Claude Code's features as extensions. 🤡&lt;/p&gt;


&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd12fa7k79ota3dwk50uj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd12fa7k79ota3dwk50uj.png" alt="pi agent vs claude code final score" width="799" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The scorecard reads Pi 6, Claude Code 5. And honestly, Pi deserves those wins. It is cheaper per task, open source, works with any model. What more do you want?&lt;/p&gt;

&lt;p&gt;But here is the thing. When I sit down with a bug and just want it fixed, I open Claude Code. It works right away, the guardrails just works, the $20 plan keeps the bill consistent, and the model just feels at home in it.&lt;/p&gt;

&lt;p&gt;No thinking about the tool itself.&lt;/p&gt;

&lt;p&gt;That is the whole story really. Even the biggest Pi fans ship most of their work through Claude Code. It's a secret. 🤫&lt;/p&gt;

&lt;p&gt;Zechner was right that these agents need way less scaffolding than we thought. Anthropic deleting 80% of its own system prompt basically admitted it. But being right and being the tool everyone opens every day are two different things, and Claude Code still owns the second one for me.&lt;/p&gt;

&lt;p&gt;So use Claude Code for the day job. Keep Pi around for when you want to tinker, go local, or save some money. ✌️&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__1127015"&gt;
    &lt;a href="/shricodev" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1127015%2F1c5e48a2-f602-4e7d-8312-3c0322d155c6.jpg" alt="shricodev image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/shricodev"&gt;Shrijal Acharya&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/shricodev"&gt;SDE • GOLD @Microsoft Student Ambassador • Prev Lead Collab and Dev-Team Lead @oppiaorg • Mail for collaboration&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>ai</category>
      <category>productivity</category>
      <category>opensource</category>
      <category>architecture</category>
    </item>
    <item>
      <title>How to build and deploy an MCP server to production (2026-07-28 spec)</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Wed, 05 Aug 2026 12:13:02 +0000</pubDate>
      <link>https://dev.to/composiodev/how-to-build-and-deploy-an-mcp-server-to-production-2026-07-28-spec-50p3</link>
      <guid>https://dev.to/composiodev/how-to-build-and-deploy-an-mcp-server-to-production-2026-07-28-spec-50p3</guid>
      <description>&lt;p&gt;MCP just had its biggest release since launch.&lt;/p&gt;

&lt;p&gt;On July 28, the maintainers shipped the &lt;strong&gt;2026-07-28 spec&lt;/strong&gt;, and it changes how MCP servers work at a pretty fundamental level. The handshake is gone. Sessions are gone. Three long-standing features are deprecated.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmsfvl2yvl7lis32ovbob.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmsfvl2yvl7lis32ovbob.png" alt="tweet on mcp" width="800" height="680"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The maintainers themselves called it the most substantial change since authorization was added. Their words, not mine.&lt;/p&gt;

&lt;p&gt;Sounds scary. But it actually makes MCP servers much easier to deploy. And what am I here for? I'm here to help you build and deploy one.&lt;/p&gt;

&lt;p&gt;Your MCP server is now just a regular stateless HTTP service. Round-robin load balancing, autoscaling, and caching all work. No sticky sessions or shared session state.&lt;/p&gt;

&lt;p&gt;In this guide, we'll build a small MCP server on the new spec, connect a client to it, see every headline feature actually running, and then deploy it to Cloudflare Workers. For free.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ All the code here uses the new &lt;strong&gt;TypeScript SDK v2&lt;/strong&gt;, released alongside the spec. If you're on the old &lt;code&gt;@modelcontextprotocol/sdk&lt;/code&gt; package, that's v1 now.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What's Covered
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What actually changed in the 2026-07-28 spec in short&lt;/li&gt;
&lt;li&gt;Building an MCP server with the new SDK v2&lt;/li&gt;
&lt;li&gt;Stateless core in action&lt;/li&gt;
&lt;li&gt;MRTR: how a tool requests user confirmation without holding a stream open&lt;/li&gt;
&lt;li&gt;A graceful fallback for clients that don't speak MRTR yet (there are many)&lt;/li&gt;
&lt;li&gt;Cacheable tool lists with &lt;code&gt;ttlMs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Testing it with a client and raw curl&lt;/li&gt;
&lt;li&gt;Deploying it to Cloudflare Workers on the free plan&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Changes in the new MCP Spec (2026-07-28)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fujro30vnaf3p3byd4d7y.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fujro30vnaf3p3byd4d7y.webp" alt="mcp new workflow" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Quick rundown of what's new. If you want the full changelog, it's on the &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/changelog" rel="noopener noreferrer"&gt;official spec site&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The handshake is gone
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;initialize&lt;/code&gt; / &lt;code&gt;initialized&lt;/code&gt; exchange and the Mcp-Session-Id header are officially retired.&lt;/p&gt;

&lt;p&gt;Every request is now &lt;strong&gt;self-describing&lt;/strong&gt;. It carries its own protocol version, client identity, and capabilities in &lt;code&gt;_meta&lt;/code&gt;. Any request can land on any server instance behind a plain load balancer. Such a relief!!&lt;/p&gt;

&lt;p&gt;There's an optional &lt;code&gt;server/discover&lt;/code&gt; RPC if a client wants capabilities up front. But it's optional. One bare POST is a complete conversation now.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi Round-Trip Requests (MRTR)
&lt;/h3&gt;

&lt;p&gt;This one is my favorite.&lt;/p&gt;

&lt;p&gt;Before, if a tool needed something from the user mid-call, such as confirmation or a missing parameter, the server had to push an &lt;code&gt;elicitation/create&lt;/code&gt; request back over a held-open stream. That meant you needed a held-open stream, which was bad for stateless deployments.&lt;/p&gt;

&lt;p&gt;MRTR flips it. The server returns &lt;code&gt;resultType: "input_required"&lt;/code&gt; with the questions it needs answered, and closes the connection. The client collects the answers and retries the original call with them attached, plus an opaque &lt;code&gt;requestState&lt;/code&gt; token so the server knows where it left off.&lt;/p&gt;

&lt;p&gt;No open streams. No sessions. Interactive tools on fully stateless infra.&lt;/p&gt;

&lt;h3&gt;
  
  
  Header-based routing
&lt;/h3&gt;

&lt;p&gt;Requests now carry &lt;code&gt;Mcp-Method&lt;/code&gt; and &lt;code&gt;Mcp-Name&lt;/code&gt; HTTP headers. Your gateway, rate limiter, or WAF can route and meter on headers without parsing JSON bodies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cacheable list results
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;tools/list&lt;/code&gt;, &lt;code&gt;prompts/list&lt;/code&gt;, &lt;code&gt;resources/list&lt;/code&gt;, and &lt;code&gt;resources/read&lt;/code&gt; responses now carry &lt;code&gt;ttlMs&lt;/code&gt; and &lt;code&gt;cacheScope&lt;/code&gt; fields, modeled on HTTP's Cache-Control. Clients cache your tool catalog instead of re-fetching it every time they connect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extensions framework + deprecations
&lt;/h3&gt;

&lt;p&gt;Tasks moved out of the experimental core into an official extension (&lt;code&gt;io.modelcontextprotocol/tasks&lt;/code&gt;). MCP Apps and Enterprise Managed Authorization live there too. You can build your own extensions as well.&lt;/p&gt;

&lt;p&gt;And the deprecations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Roots, Sampling, and Logging&lt;/strong&gt; are deprecated. They keep working for at least 12 months, but new implementations shouldn't use them.&lt;/li&gt;
&lt;li&gt;The legacy &lt;strong&gt;HTTP+SSE transport&lt;/strong&gt; is deprecated with a year-long offramp.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic Client Registration&lt;/strong&gt; is deprecated in favor of Client ID Metadata Documents (CIMD).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's also a formal deprecation policy now: a 12-month minimum window for anything marked deprecated. So you get to plan upgrades, which is noicee!&lt;/p&gt;

&lt;h3&gt;
  
  
  The SDK Split
&lt;/h3&gt;

&lt;p&gt;One more thing before we build: the TypeScript SDK is no longer one package.&lt;/p&gt;

&lt;p&gt;v2 splits it into &lt;code&gt;@modelcontextprotocol/server&lt;/code&gt;, &lt;code&gt;@modelcontextprotocol/client&lt;/code&gt;, and thin framework adapters (&lt;code&gt;@modelcontextprotocol/hono&lt;/code&gt;, &lt;code&gt;express&lt;/code&gt;, &lt;code&gt;fastify&lt;/code&gt;, &lt;code&gt;node&lt;/code&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  Building an MCP Server
&lt;/h2&gt;

&lt;p&gt;Finally, we're onto the build. We will build a quick tiny deploy bot over MCP.&lt;/p&gt;

&lt;p&gt;It has three tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;deploy&lt;/code&gt; asks the user for confirmation before deploying (MRTR in action).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;list_deployments&lt;/code&gt; reads back the deployment history&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;server_stats&lt;/code&gt; proves a fresh server instance handled every request&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the trick that pays off at deploy time: all the MCP logic lives in &lt;strong&gt;one platform-neutral file&lt;/strong&gt; (&lt;code&gt;bot.ts&lt;/code&gt;), and each platform gets a tiny entry file. Node gets &lt;code&gt;server.ts&lt;/code&gt;. Cloudflare gets &lt;code&gt;worker.ts&lt;/code&gt;. Both are about ten lines. An MCP server on the new spec is just a fetch handler; the platform is a serving shim.&lt;/p&gt;

&lt;p&gt;You'll understand everything along the way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Install the SDK v2
&lt;/h3&gt;

&lt;p&gt;Run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;updated-mcp-spec-bot &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;updated-mcp-spec-bot
npm init &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm pkg &lt;span class="nb"&gt;set type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;module
npm &lt;span class="nb"&gt;install&lt;/span&gt; @modelcontextprotocol/server @modelcontextprotocol/client &lt;span class="se"&gt;\&lt;/span&gt;
  @modelcontextprotocol/hono @hono/node-server hono zod tsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;ℹ️ On TypeScript 6+, add &lt;code&gt;"types": ["node"]&lt;/code&gt; to your tsconfig &lt;code&gt;compilerOptions&lt;/code&gt; after installing &lt;code&gt;@types/node&lt;/code&gt;. TS 6 no longer auto-includes &lt;code&gt;@types/*&lt;/code&gt;, and you'll get &lt;code&gt;Cannot find name 'process'&lt;/code&gt; errors without it. Ask me how I know. 😴&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Step 2: The server logic
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;bot.ts&lt;/code&gt;. This is the whole MCP server, with zero platform code in it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 👇 bot.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;CallToolResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;InputRequiredResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@modelcontextprotocol/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;acceptedContent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;CLIENT_CAPABILITIES_META_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;createRequestStateCodec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;inputRequired&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;McpServer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@modelcontextprotocol/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod/v4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deployments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;requestsServed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;DeployState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// set STATE_KEY in production so all instances share the secret&lt;/span&gt;
&lt;span class="c1"&gt;// lazy init: Workers forbids generating random values at module scope&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;codec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;createRequestStateCodec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DeployState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stateCodec&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;codec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;globalThis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;STATE_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;codec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createRequestStateCodec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DeployState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="na"&gt;ttlSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;codec&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CONFIRM_SCHEMA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;boolean&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// runs per request, keep it cheap&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildServer&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;McpServer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;requestsServed&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;McpServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;updated-mcp-spec-bot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;cacheHints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tools/list&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ttlMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cacheScope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;requestState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;stateCodec&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;list_deployments&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;List deployments&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;List all deployments recorded by this server.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CallToolResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;deployments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;deployments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; @ &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No deployments yet.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;server_stats&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Server stats&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;How many requests this process served, each on a fresh server instance.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CallToolResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`pid=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;globalThis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;edge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; requestsServed=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;requestsServed&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deploy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Deploy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Deploy to an environment. Requires confirmation: interactive clients get a prompt, others must pass confirm: true.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;staging&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Target environment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Set true to confirm, only after asking the user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;confirm&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CallToolResult&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;InputRequiredResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;caps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mcpReq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;envelope&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)?.[&lt;/span&gt;
          &lt;span class="nx"&gt;CLIENT_CAPABILITIES_META_KEY&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getClientCapabilities&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canElicit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;caps&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;elicitation&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;elicitation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canElicit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mcpReq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;DeployState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;confirmed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;acceptedContent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mcpReq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inputResponses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;confirmed&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;inputRequired&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;inputRequests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inputRequired&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;elicit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Deploy to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;? This will go live.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;requestedSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CONFIRM_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="p"&gt;}),&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="na"&gt;requestState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;stateCodec&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;mint&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;confirm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
          &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="nx"&gt;deployments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Deployed to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; at &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="c1"&gt;// fallback for clients without elicitation support&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;confirm&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Deploy to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; needs confirmation. Ask the user, then call deploy again with confirm: true.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="nx"&gt;deployments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Deployed to &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; at &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A few things worth explaining here:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;buildServer()&lt;/code&gt; runs on every single request. Not once at startup. Every request gets a brand-new &lt;code&gt;McpServer&lt;/code&gt; instance.&lt;/p&gt;

&lt;p&gt;If that surprises you, I get it. It surprised me too. But this is literally the canonical pattern from the SDK's own examples, and it's the whole point of the release.&lt;/p&gt;

&lt;p&gt;Construction is just object creation and a handler map, microseconds of work. There's no protocol state to preserve anymore, so there's nothing to keep alive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-request server construction, per-process resources&lt;/strong&gt;. App state (our deployments array, the state codec, your DB pool in real life) lives at module level. The server instance is disposable.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;deploy&lt;/code&gt; tool never blocks. When it needs confirmation, it returns &lt;code&gt;inputRequired(...)&lt;/code&gt; and the request is over. Done. Connection closed. The &lt;code&gt;requestState&lt;/code&gt; token is the only thing that survives between rounds, and it round-trips through the client.&lt;/p&gt;

&lt;p&gt;This means the client could tamper with it. That's why we seal it with &lt;code&gt;createRequestStateCodec&lt;/code&gt;, so tampered or expired state gets rejected with a wire-level error before our handler even runs.&lt;/p&gt;

&lt;p&gt;Notice the codec is &lt;strong&gt;lazily created&lt;/strong&gt; on first use instead of at module level. That looks like a pointless indirection on Node. It's not. Cloudflare Workers forbids generating random values in global scope, and this exact line is what lets the same file run on both platforms. Same story with the &lt;code&gt;globalThis.process?.&lt;/code&gt; guards: Workers has no &lt;code&gt;process&lt;/code&gt; global by default.&lt;/p&gt;

&lt;p&gt;So the tool reads the client's declared capabilities from the per-request envelope (that's the &lt;code&gt;CLIENT_CAPABILITIES_META_KEY&lt;/code&gt; lookup, with a legacy-connection fallback) and if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Client supports elicitation then the full MRTR confirmation flow&lt;/li&gt;
&lt;li&gt;Client doesn't then the tool accepts an optional &lt;code&gt;confirm: true&lt;/code&gt; argument, and without it, it returns a plain instruction: "Ask the user, then call deploy again with confirm: true"&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Step 3: The Node entry
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;server.ts&lt;/code&gt;. This is everything Node-specific:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 👇 server.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;serve&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hono/node-server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createMcpHonoApp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@modelcontextprotocol/hono&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createMcpHandler&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@modelcontextprotocol/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;buildServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./bot.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMcpHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buildServer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// in production set ALLOWED_HOSTS to your public domain&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allowedHosts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ALLOWED_HOSTS&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMcpHonoApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allowedHosts&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;allowedHosts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/healthz&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hostname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HOST&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;127.0.0.1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hostname&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`updated-mcp-spec-bot listening on &amp;lt;http://$&amp;gt;{hostname}:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/mcp`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That's it. &lt;code&gt;createMcpHandler&lt;/code&gt; gives you a standard fetch-style handler, and Hono is just routing. &lt;code&gt;createMcpHonoApp()&lt;/code&gt; validates Host/Origin headers (DNS rebinding protection) and only allows localhost out of the box, so the &lt;code&gt;ALLOWED_HOSTS&lt;/code&gt; env var is there for when this runs behind a real domain.&lt;/p&gt;

&lt;p&gt;Everything is env-driven (&lt;code&gt;PORT&lt;/code&gt;, &lt;code&gt;HOST&lt;/code&gt;, &lt;code&gt;ALLOWED_HOSTS&lt;/code&gt;, &lt;code&gt;STATE_KEY&lt;/code&gt;) because that's what a VM or a PaaS like Railway wants. We won't use this file for the Cloudflare deploy, but it's your path if you'd rather run this on Node anywhere.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 4: The client
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;client.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 👇 client.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;StreamableHTTPClientTransport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@modelcontextprotocol/client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MCP_URL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;http://127.0.0.1:3000/mcp&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mcp-demooo-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1.0.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;elicitation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;form&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;versionNegotiation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// use 2026-07-28 when the server does&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// The elicitation handler: in a real app this renders a confirm dialog.&lt;/span&gt;
&lt;span class="c1"&gt;// Here we auto-accept and log what the server asked.&lt;/span&gt;
&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setRequestHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;elicitation/create&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`\n[elicitation] server asks: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" -&amp;gt; answering yes`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;accept&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;confirm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StreamableHTTPClientTransport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`connected, negotiated protocol: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getNegotiatedProtocolVersion&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listTools&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ttlMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cacheScope&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ttlMs&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;cacheScope&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`tools/list: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`cache hints: ttlMs=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ttlMs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; cacheScope=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cacheScope&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listTools&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;second listTools served from cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callTool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;list_deployments&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;`list_deployments: &lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;before&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callTool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deploy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`deploy: &lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callTool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;server_stats&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`server_stats: &lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callTool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;list_deployments&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`list_deployments: &lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;after&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;⚠️ Don't miss &lt;code&gt;versionNegotiation: { mode: 'auto' }&lt;/code&gt;. Without it, the client negotiates the legacy 2025-11-25 protocol and the MRTR flow fails. This took me half an hour to debug.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Notice the elicitation handler is a completely normal &lt;code&gt;elicitation/create&lt;/code&gt; handler, the same one you'd write for the old flow. The SDK's auto-fulfillment engine routes the embedded MRTR request through it and retries the tool call for you. Your code doesn't even see the round trip.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 5: Run it
&lt;/h3&gt;

&lt;p&gt;In two terminals (better with &lt;a href="https://github.com/tmux/tmux/wiki" rel="noopener noreferrer"&gt;tmux&lt;/a&gt;), run the following:&lt;/p&gt;

&lt;p&gt;In the first terminal:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx tsx server.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And in the other:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx tsx client.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is the kinda output you'd get:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;connected, negotiated protocol: 2026-07-28

tools/list: list_deployments, server_stats, deploy
cache hints: ttlMs=30000 cacheScope=public

second listTools served from cache
list_deployments: No deployments yet.

[elicitation] server asks: "Deploy to prod? This will go live." -&amp;gt; answering yes
deploy: Deployed to prod at 2026-08-01T08:02:45.601Z

server_stats: pid=159984 requestsServed=6
list_deployments: prod @ 2026-08-01T08:02:45.601Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Every line here demonstrates a spec feature, and I designed it that way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;2026-07-28&lt;/code&gt;: we're on the new protocol, not the legacy fallback&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ttlMs=30000&lt;/code&gt; + &lt;code&gt;served from cache&lt;/code&gt;: the second &lt;code&gt;listTools()&lt;/code&gt; never touched the network&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The elicitation line, then the deploy&lt;/strong&gt;: that was two &lt;code&gt;tools/call&lt;/code&gt; POSTs. First one returned &lt;code&gt;input_required&lt;/code&gt; and closed. Second had the answer plus the sealed &lt;code&gt;requestState&lt;/code&gt;. No stream was ever held open.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;requestsServed=6&lt;/code&gt;: six requests, six fresh server instances, one process. Under a load balancer, those six could've hit six different machines. How cool is that?&lt;/li&gt;
&lt;li&gt;The final &lt;code&gt;list_deployments&lt;/code&gt;: app state survived even though protocol state didn't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the math is here: 4 tool calls, plus 2 &lt;code&gt;listTools()&lt;/code&gt; where only 1 hit the wire, plus 1 extra round for the MRTR retry = 6 server builds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foopkg0th0lfdarmt41rx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foopkg0th0lfdarmt41rx.png" alt="tool call output" width="800" height="285"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 6: Look at the raw wire
&lt;/h3&gt;

&lt;p&gt;Let's see the "no handshake" thing. One bare curl, with no initialization:&lt;/p&gt;

&lt;p&gt;Run the following command:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:3000/mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Accept: application/json, text/event-stream"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"MCP-Protocol-Version: 2026-07-28"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Mcp-Method: tools/call"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Mcp-Name: list_deployments"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_deployments","arguments":{},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;btw, this curl command was suggested by Claude.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's the result you get back:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prod @ 2026-08-01T08:02:45.601Z"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"resultType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"complete"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"_meta"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"io.modelcontextprotocol/serverInfo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"updated-mcp-spec-bot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Two things worth noticing in that response.&lt;/p&gt;

&lt;p&gt;The result already shows the prod deployment, because I ran this curl against the same server process the client just deployed through.&lt;/p&gt;

&lt;p&gt;A totally separate client, no handshake, no session, and it reads the record the TypeScript client wrote. App state persists, protocol state doesn't.&lt;/p&gt;

&lt;p&gt;Look at those headers. &lt;code&gt;Mcp-Method&lt;/code&gt; and &lt;code&gt;Mcp-Name&lt;/code&gt; are right there for your gateway to route on. And the &lt;code&gt;_meta&lt;/code&gt; makes the request fully self-describing.&lt;/p&gt;

&lt;p&gt;The DX here is genuinely good.&lt;/p&gt;


&lt;h2&gt;
  
  
  Deploying to Cloudflare Workers
&lt;/h2&gt;

&lt;p&gt;We're deploying this to Cloudflare Workers, and it costs nothing: the free plan gives you 100,000 requests a day and a &lt;code&gt;*.workers.dev&lt;/code&gt; subdomain, no credit card needed.&lt;/p&gt;

&lt;p&gt;Why Workers? Because it's the natural way for a stateless MCP server. &lt;code&gt;createMcpHandler&lt;/code&gt; returns a fetch-style handler, and fetch handlers are literally what Workers runs. The entire platform difference fits in one tiny file.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ Cloudflare has quick-start MCP templates (&lt;code&gt;npm create cloudflare -- --template=cloudflare/ai/demos/remote-mcp-authless&lt;/code&gt;). As of writing, Cloudflare's own docs warn that these still scaffold the deprecated &lt;code&gt;McpAgent&lt;/code&gt; path and say, "Do not use that path for a new server." It's the old stateful world, and it doesn't speak 2026-07-28. Skip the template. &lt;code&gt;createMcpHandler&lt;/code&gt; is the recommended path, and it's what we're already using.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Step 1: The Worker entry
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;worker.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 👇 worker.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Hono&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hono&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createMcpHandler&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@modelcontextprotocol/server&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;buildServer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./bot.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createMcpHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buildServer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Hono&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/healthz&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ok&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/mcp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Eleven lines. Same &lt;code&gt;buildServer&lt;/code&gt;, same tools, same MRTR flow.&lt;/p&gt;

&lt;p&gt;Two deliberate differences from the Node entry:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Plain &lt;code&gt;new Hono()&lt;/code&gt; instead of &lt;code&gt;createMcpHonoApp()&lt;/code&gt;. The Host validation in &lt;code&gt;createMcpHonoApp&lt;/code&gt; is DNS rebinding protection &lt;em&gt;for&lt;/em&gt; localhost &lt;em&gt;servers&lt;/em&gt;. Behind Cloudflare's edge, it just gets in the way.&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;serve(...)&lt;/code&gt;. Workers calls your exported fetch handler itself.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Step 2: The wrangler config
&lt;/h3&gt;

&lt;p&gt;Wrangler is Cloudflare's CLI for Workers. It bundles your TypeScript (no build step needed), runs it locally on the real production runtime, manages secrets, and deploys.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; wrangler
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Create &lt;code&gt;wrangler.jsonc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"updated-mcp-spec-bot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"worker.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compatibility_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compatibility_flags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"nodejs_compat"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;nodejs_compat&lt;/code&gt; flag fills in Node-ish globals so npm packages behave.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3: Test on the real runtime, locally
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx wrangler dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This runs &lt;code&gt;worker.ts&lt;/code&gt; on &lt;strong&gt;workerd&lt;/strong&gt;, the same engine that runs in Cloudflare production, at &lt;code&gt;http://localhost:8787&lt;/code&gt;. Point the client at it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;MCP_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8787/mcp npx tsx client.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Same full output as the Node run: &lt;code&gt;2026-07-28&lt;/code&gt; negotiated, cache hints, the MRTR deploy round-trip. Except one line:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;server_stats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pid=1 requestsServed=6&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;pid=1&lt;/code&gt;. That's the edge runtime saying hello. 🫡&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 4: Deploy it
&lt;/h3&gt;

&lt;p&gt;Create a free account at &lt;a href="https://dash.cloudflare.com/sign-up" rel="noopener noreferrer"&gt;dash.cloudflare.com/sign-up&lt;/a&gt; if you don't have one, then:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx wrangler login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Set the production &lt;code&gt;requestState&lt;/code&gt; secret (this is the shared HMAC key, so every edge instance can verify tokens minted by any other):&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32          &lt;span class="c"&gt;# copy the output&lt;/span&gt;
npx wrangler secret put STATE_KEY   &lt;span class="c"&gt;# paste it when prompted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ztb609ovf6yanabtixu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ztb609ovf6yanabtixu.png" alt="wrangler setup" width="797" height="125"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ One gotcha from my own run: the key must be at least 32 bytes or the codec throws at startup. &lt;code&gt;openssl rand -hex 32&lt;/code&gt; gives you 64 hex characters, which is plenty.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And ship it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx wrangler deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;First deploy asks you to pick your free &lt;code&gt;workers.dev&lt;/code&gt; subdomain. Ten seconds later:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://updated-mcp-spec-2026.&amp;lt;your-subdomain&amp;gt;.workers.dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Your MCP server is live on Cloudflare's global edge.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxcja7s8fxhktca8wy70o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxcja7s8fxhktca8wy70o.png" alt="mcp server in cloudflare" width="798" height="262"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 5: Verify from the outside
&lt;/h3&gt;

&lt;p&gt;Run the Step 6 curl against the public URL (just swap the host), hit &lt;code&gt;/healthz&lt;/code&gt; in a browser, and then the real proof:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;MCP_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://updated-mcp-spec-2026.&amp;lt;your-subdomain&amp;gt;.workers.dev/mcp npx tsx client.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Same output. Except now it's on the internet.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzdjl92s674r1jubmq1fn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzdjl92s674r1jubmq1fn.png" alt="remote mcp server" width="800" height="177"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Connect a real agent to it, with no tunnel and no ngrok:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http updated-mcp-spec-2026 &lt;span class="se"&gt;\&lt;/span&gt;
  https://updated-mcp-spec-bot.&amp;lt;your-subdomain&amp;gt;.workers.dev/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Run &lt;code&gt;/mcp&lt;/code&gt; in a Claude Code session to see it connected, then ask it to "deploy to staging". Since Claude Code doesn't declare the elicitation capability yet, our capability-aware fallback kicks in: the tool tells the agent to confirm with you first, you say yes in chat, and the deploy lands.&lt;/p&gt;

&lt;p&gt;Bonus: run &lt;code&gt;npx wrangler tail&lt;/code&gt; while you do it and watch the requests land in your production logs live.&lt;/p&gt;

&lt;p&gt;Here’s a small demo:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/9xQ5RmU-Hrc"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;
&lt;h3&gt;
  
  
  One caveat
&lt;/h3&gt;

&lt;p&gt;Our &lt;code&gt;deployments&lt;/code&gt; array lives in memory, and on Workers, memory is extra ephemeral: isolates spin up and down per location, so two requests might see different histories. That's not a bug in the demo; it's the whole lesson of the spec, one more time. Protocol state is gone by design, and app state belongs in real storage. On Cloudflare, that's KV, D1, or Durable Objects.&lt;/p&gt;


&lt;h2&gt;
  
  
  Where Composio fits
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5wnytjebt7lcoblnckp8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5wnytjebt7lcoblnckp8.png" alt="Composio" width="799" height="205"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What we just built is one server with three tools. Real agents need Gmail, Slack, Notion, GitHub, Linear, and fifty other things.&lt;/p&gt;

&lt;p&gt;You could build and deploy a similar server for every one of those. Handle each app's OAuth. Keep up with every API change. Run all that infra.&lt;/p&gt;

&lt;p&gt;Or you point your agent at Composio, which gives you 1000+ apps behind a single MCP endpoint:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://connect.composio.dev/mcp&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Build custom MCP servers (like the one we built) for your own domain logic, and let Composio be the app layer for everything else.&lt;/p&gt;


&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The 2026-07-28 spec is a breaking release, and it's the good kind of breaking.&lt;/p&gt;

&lt;p&gt;MCP servers are now boring HTTP services. Deploy them like you deploy everything else: stateless, load-balanced, cacheable, autoscaled. The handshake is gone, sessions are gone, and interactive tools work anyway thanks to MRTR.&lt;/p&gt;

&lt;p&gt;If you're starting a new server today: use SDK v2, use the &lt;code&gt;createMcpHandler(buildServer)&lt;/code&gt; factory pattern, keep resources at module level, seal your &lt;code&gt;requestState&lt;/code&gt;, and split your logic from your platform entry. We went from localhost to Cloudflare's global edge with an eleven-line file, and the same split works for Railway, Render, Fly, or a plain VM through &lt;code&gt;server.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you have existing servers: you've got a 12-month window on everything deprecated. Use it.&lt;/p&gt;

&lt;p&gt;Now your MCP server finally gets to be just another web service. 🤌&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr11gknb9xy4kfo0eq31y.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr11gknb9xy4kfo0eq31y.gif" alt="noiceee" width="450" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ You can find the entire source code in &lt;a href="https://github.com/shricodev/updated-2026-july-mcp-spec-tut" rel="noopener noreferrer"&gt;the repository&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;div class="ltag__user ltag__user__id__1127015"&gt;
    &lt;a href="/shricodev" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1127015%2F1c5e48a2-f602-4e7d-8312-3c0322d155c6.jpg" alt="shricodev image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/shricodev"&gt;Shrijal Acharya&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/shricodev"&gt;SDE • GOLD @Microsoft Student Ambassador • Prev Lead Collab and Dev-Team Lead @oppiaorg • Mail for collaboration&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>mcp</category>
      <category>tutorial</category>
      <category>ai</category>
      <category>automation</category>
    </item>
    <item>
      <title>How to connect MCP servers to Slackbot</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Sat, 18 Jul 2026 11:57:47 +0000</pubDate>
      <link>https://dev.to/composiodev/how-to-connect-mcp-servers-to-slackbot-1al4</link>
      <guid>https://dev.to/composiodev/how-to-connect-mcp-servers-to-slackbot-1al4</guid>
      <description>&lt;p&gt;Slackbot recently added support for MCP, which means you can now connect it with external apps and let it take actions across your work tools directly from Slack&lt;/p&gt;

&lt;p&gt;But the native app list is still limited. By the time of writing this post, there's just about &lt;strong&gt;20 apps&lt;/strong&gt; that you can connect from the &lt;a href="https://slack.com/marketplace" rel="noopener noreferrer"&gt;Slack marketplace&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And for most teams, that's not enough.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqlyug2z5afeztlhxrwtd.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqlyug2z5afeztlhxrwtd.gif" alt="not enough gif" width="480" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But luckily, Slack allows you to set up or use your custom MCP servers and not have to be limited by the number of apps available in marketplace.&lt;/p&gt;

&lt;p&gt;That's where &lt;a href="https://composio.dev/" rel="noopener noreferrer"&gt;Composio&lt;/a&gt; helps you. It can connect your slack bots to &lt;strong&gt;1000+ apps&lt;/strong&gt; that you can use.&lt;/p&gt;

&lt;p&gt;In this guide, we’ll go through how to connect Slackbot with Composio’s MCP server in 3 steps.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ The steps will be pretty much the same with other MCP servers as well.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What We're Building
&lt;/h2&gt;

&lt;p&gt;Once this is set up, you can ask Slackbot things like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Find my latest unread Gmail emails.
Search my Notion workspace for launch notes.
Check my Google Calendar for meetings tomorrow.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And a bunch more. Imagine all the stuff you can do with 1000+ apps. 😵‍💫&lt;/p&gt;

&lt;p&gt;I'll leave the rest to your imagination...&lt;/p&gt;

&lt;p&gt;Slackbot sends the request to Composio Connect, Composio finds the right tool, asks you to connect the app if needed (one time), and then executes the action.&lt;/p&gt;


&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before we begin, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Slack workspace with Slackbot MCP client access (comes with Business+ and Enterprise plan)&lt;/li&gt;
&lt;li&gt;Permission to create or configure a Slack app.&lt;/li&gt;
&lt;li&gt;A Composio account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 1: Get the Composio Connect MCP URL
&lt;/h2&gt;

&lt;p&gt;First, you need the MCP server URL from Composio.&lt;/p&gt;

&lt;p&gt;For this setup, use Composio Connect:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://connect.composio.dev/mcp&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is Composio’s hosted MCP server that gives your AI agent access to 1,000+ apps with just &lt;strong&gt;7 meta-tools&lt;/strong&gt; that let the slackbot discover what's available, authorize apps on demand, and execute tools across apps in parallel through a single connection.&lt;/p&gt;

&lt;p&gt;You don’t need to create a custom MCP server for this guide.&lt;/p&gt;

&lt;p&gt;Composio also supports custom MCP servers for more scoped project-specific use cases, but those can require API-key-based auth. For Slackbot, Composio Connect is the simpler path because it works with OAuth-based MCP client flows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9r6it1xz4hl4h3ifel7a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9r6it1xz4hl4h3ifel7a.png" alt="Composio Connect" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 2: Add Composio Connect to a Slack App
&lt;/h2&gt;

&lt;p&gt;Now, we need to register the Composio MCP server inside a Slack app.&lt;/p&gt;

&lt;p&gt;Go to the Slack developer dashboard and create a new app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5kkqe4kem5xnfmpyla6r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5kkqe4kem5xnfmpyla6r.png" alt="Slack new app creation" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the app is created:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your Slack app.&lt;/li&gt;
&lt;li&gt;In the left sidebar, go to &lt;strong&gt;Features&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;MCP Servers&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwvranutnruw5ja1w5vhm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwvranutnruw5ja1w5vhm.png" alt="Slack MCP Servers button" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Get Started&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff2yx87053fw9upha8l28.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff2yx87053fw9upha8l28.png" alt="Slack get started button" width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now fill in the MCP server details.&lt;/p&gt;

&lt;p&gt;Use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name&lt;/strong&gt;: Composio (or anything you wish)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL&lt;/strong&gt;: &lt;a href="https://connect.composio.dev/mcp" rel="noopener noreferrer"&gt;https://connect.composio.dev/mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth Type&lt;/strong&gt;: Dynamic Client Registration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnyzro2ycp7xjqhb3gsnw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnyzro2ycp7xjqhb3gsnw.png" alt="Slack Add MCP" width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the auth type, select &lt;strong&gt;Dynamic Client Registration&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ This is the right option for MCP servers that support OAuth discovery and client registration. Slack handles the client registration automatically, so you don’t need to manually create OAuth credentials first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, if you click the three dots and then &lt;strong&gt;Tools&lt;/strong&gt;, you should see that it currently cannot fetch the tools because the MCP server uses a dynamic connection and must be installed in your workspace first.&lt;/p&gt;

&lt;p&gt;So, now head over to the &lt;strong&gt;Install App&lt;/strong&gt; tab, and install it to the workspace you selected when creating the app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxwv4sjhjmjvmkiflvhyp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxwv4sjhjmjvmkiflvhyp.png" alt="Slack Install App" width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If your workspace requires approval, send the app request to your admin.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 3: Connect Composio inside Slackbot
&lt;/h2&gt;

&lt;p&gt;Once your Slack app is installed and approved, open a DM with Slackbot.&lt;/p&gt;

&lt;p&gt;Then, just type in a prompt that requires using the app, Slack will use the correct app automatically for you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvp5wb4l3ab4anrfqv8y9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvp5wb4l3ab4anrfqv8y9.png" alt="Slack connecting composio" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Connect,&lt;/strong&gt; and you’ll be taken to a confirmation page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4f5nvtnc0j1x1wkbv356.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4f5nvtnc0j1x1wkbv356.png" alt="Slack connecting composio confirmation" width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on Continue, and then confirm it on the Composio end.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fljriezhxmemb38w8apqz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fljriezhxmemb38w8apqz.png" alt="Composio confirmation" width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If everything went well, you should see that your account is connected.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F58g6dsw3n767nztxc4mo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F58g6dsw3n767nztxc4mo.png" alt="Slack final confirmation" width="799" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After that, Slackbot should be able to discover Composio’s MCP tools.&lt;/p&gt;

&lt;p&gt;Start with a simpler test:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What tools are available from Composio?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Once that goes through, now try an actual app action.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Send a mail to x@y.com saying 'Hi, from Composio 👋 inside Slackbot'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If Gmail is not connected yet, Composio should generate an OAuth link for you to connect it. Once you approve it, the connection persists for future use. So, you don't have to repeat this step again and again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5fb169cfpktm2xynzoe3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5fb169cfpktm2xynzoe3.png" alt="Composio connection link" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Slackbot may ask you to approve the action before it writes data to another app.&lt;/p&gt;

&lt;p&gt;That's expected. Once connected, Slackbot can use that app through Composio.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmxlw14j7ed5fi6h0o6dk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmxlw14j7ed5fi6h0o6dk.png" alt="Composio MCP in action" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Voilà, you've successfully connected Slackbot to Composio MCP. 🎊&lt;/p&gt;

&lt;p&gt;Here’s a quick workflow for initiating a connection and running an actual app action:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/m6kv3tqjUgU"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;


&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Slack's own marketplace is good, and if it covers all the tools you require, you can completely stick to it.&lt;/p&gt;

&lt;p&gt;But for some of you, that's simply not enough. I hope this helps overcome that problem.&lt;/p&gt;

&lt;p&gt;So instead of jumping between different tools, you can ask Slackbot to find information, create tasks, update records, and run actions across your apps from inside Slack.&lt;/p&gt;

&lt;p&gt;This is a much-needed quality-of-life improvement for teams that already live in Slack.&lt;/p&gt;

&lt;p&gt;Slackbot gives you the interface. MCP gives you the protocol.&lt;/p&gt;

&lt;p&gt;And Composio gives you the app layer. 👌&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__1127015"&gt;
    &lt;a href="/shricodev" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1127015%2F1c5e48a2-f602-4e7d-8312-3c0322d155c6.jpg" alt="shricodev image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/shricodev"&gt;Shrijal Acharya&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/shricodev"&gt;SDE • GOLD @Microsoft Student Ambassador • Prev Lead Collab and Dev-Team Lead @oppiaorg • Mail for collaboration&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>ai</category>
      <category>productivity</category>
      <category>beginners</category>
      <category>automation</category>
    </item>
    <item>
      <title>Claude Opus vs Kombai in 3 Real-World Frontend AI Tests 🚀</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Tue, 02 Jun 2026 16:06:41 +0000</pubDate>
      <link>https://dev.to/shricodev/claude-opus-vs-kombai-in-3-real-world-frontend-ai-tests-13a2</link>
      <guid>https://dev.to/shricodev/claude-opus-vs-kombai-in-3-real-world-frontend-ai-tests-13a2</guid>
      <description>&lt;p&gt;Frontend automation has been getting pretty wild lately. 🫠&lt;/p&gt;

&lt;p&gt;A few months ago, this comparison would have been much easier to frame.&lt;/p&gt;

&lt;p&gt;On one side, you had the &lt;strong&gt;Claude Opus&lt;/strong&gt; lineup, one of the strongest coding model lineups available, running through tools like Claude Code with Figma MCP support.&lt;/p&gt;

&lt;p&gt;On the other side, you had &lt;strong&gt;Kombai&lt;/strong&gt;, a frontend-focused AI coding agent that was mostly known for turning Figma files, screenshots, prompts, and existing designs into clean frontend code.&lt;/p&gt;

&lt;p&gt;But Kombai has changed quite a bit since then.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu5fj5j52sacbg5j1fgix.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu5fj5j52sacbg5j1fgix.gif" alt="spiderman meme" width="498" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is not just an AI coding agent for frontend anymore. With the newer updates, especially &lt;strong&gt;Design Mode&lt;/strong&gt;, Kombai is now trying to cover a much bigger part of the frontend workflow.&lt;/p&gt;

&lt;p&gt;That changes the comparison a little.&lt;/p&gt;

&lt;p&gt;If we only compare it on code generation, we are kind of testing just one part of what Kombai does now. It can also help create UI designs, iterate on them visually, and then move those designs into code inside the same IDE workflow.&lt;/p&gt;

&lt;p&gt;I will go deeper into that later in the Kombai section, because there is quite a bit to talk about there.&lt;/p&gt;

&lt;p&gt;For this post, though, I still want to focus mainly on the part that matters most in real projects:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🤔 Can it work inside an existing codebase and ship good frontend changes without breaking things?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because at the end of the day, that's the actual pain point of frontend.&lt;/p&gt;

&lt;p&gt;If you’re interested, we already compared the older version of &lt;strong&gt;Kombai&lt;/strong&gt; and &lt;strong&gt;Figma MCP&lt;/strong&gt; here: &lt;a href="https://www.freecodecamp.org/news/figma-mcp-vs-kombai-frontend-clone-comparison" rel="noopener noreferrer"&gt;Figma MCP vs Kombai&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So in this post, we’ll look at what has changed with Kombai, touch on Design Mode, and then compare it with Claude Opus on 3 real open-source projects.&lt;/p&gt;

&lt;p&gt;Let’s find out which one actually feels more useful. 👇&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi9zgn89g85nez1b5u6x2.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi9zgn89g85nez1b5u6x2.gif" alt="Bobs Burger on Fire" width="500" height="284"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;If you just want the takeaway, here’s the quick rundown:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kombai is not just a Figma-to-code tool anymore.&lt;/strong&gt; It is now more of an all-in-one design and frontend coding agent. You can generate designs, iterate on a canvas, visually edit UI, use Figma as input, work with your real codebase, and then ship great frontend code from inside your IDE.&lt;/li&gt;
&lt;/ul&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Design Mode is the biggest new change.&lt;/strong&gt; It gives Kombai an infinite canvas, Style Guides, Themes, reusable Blocks, CSS-level visual editing, and a one-click flow from design to code. It is still early, but it changes how you should think about Kombai.&lt;/li&gt;
&lt;/ul&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;For the coding tests in this post, Kombai was still clearly stronger overall.&lt;/strong&gt; In real open-source projects, it handled frontend implementation, codebase understanding, UI quality, and integration better than Claude Opus in most cases.&lt;/li&gt;
&lt;/ul&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Opus 4.6 with Figma MCP is still totally fine&lt;/strong&gt;, especially if you already work mostly in the CLI with tools like Claude Code, Codex, or whatever else. You can absolutely stick with that setup and still get solid results. It works well.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bigger point is this:&lt;/p&gt;

&lt;p&gt;If you are already comfortable in the CLI, there is no real reason to switch just for this. A good model with Figma MCP is more than enough for a lot of use cases.&lt;/p&gt;

&lt;p&gt;But if you use &lt;strong&gt;Cursor&lt;/strong&gt;, &lt;strong&gt;VS Code&lt;/strong&gt;, &lt;strong&gt;Windsurf&lt;/strong&gt;, &lt;strong&gt;Trae&lt;/strong&gt;, &lt;strong&gt;Antigravity&lt;/strong&gt;, &lt;strong&gt;Kiro&lt;/strong&gt;, or another GUI IDE, you should seriously try Kombai at least once.&lt;/p&gt;

&lt;p&gt;It is just a very good tool for frontend work.&lt;/p&gt;

&lt;p&gt;I’ve been using Kombai for more than &lt;strong&gt;10 months&lt;/strong&gt;, and it still feels kind of wild sometimes. Honestly, I have &lt;strong&gt;Cursor&lt;/strong&gt; installed on my machine for Kombai alone. That’s how useful it has been for me.&lt;/p&gt;




&lt;h2&gt;
  
  
  Brief on Kombai
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 Kombai is an all-in-one design and frontend coding agent for building production-ready frontends&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Kombai used to be easy to describe as a frontend AI agent.&lt;/p&gt;

&lt;p&gt;This is still true, but now its just a small part of what it can do.&lt;/p&gt;

&lt;p&gt;The better way to describe it today is this:&lt;/p&gt;

&lt;p&gt;It's an agent specially designed for frontend unlike usual coding agents like Claude Code, Codex or anything else.&lt;/p&gt;

&lt;p&gt;That specialization shows up in a few places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It understands frontend stacks and component libraries&lt;/li&gt;
&lt;li&gt;It can parse Figma designs natively&lt;/li&gt;
&lt;li&gt;It can work with existing components, tokens, hooks, and design systems&lt;/li&gt;
&lt;li&gt;It can visually inspect and edit the browser output&lt;/li&gt;
&lt;li&gt;It can generate UI designs before writing code&lt;/li&gt;
&lt;li&gt;It focuses more on production-ready frontend changes, and not a generic app generation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest recent update is the &lt;strong&gt;Design Mode&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;With Design Mode, Kombai can generate UI designs directly inside your IDE. You can start from a prompt, image, website, or Figma reference, and Kombai creates editable UI designs that you can change visually.&lt;/p&gt;

&lt;p&gt;Here’s a quick demo of Kombai’s Design Mode in action by &lt;a href="https://www.youtube.com/@beau" rel="noopener noreferrer"&gt;Beau Carnes&lt;/a&gt; (core team member of &lt;strong&gt;freeCodeCamp&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/iaIiP2lr0UI"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;It also has proper design-system-ish primitives now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Style Guides&lt;/strong&gt; for controlling the overall visual direction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Themes&lt;/strong&gt; for reusable design tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blocks&lt;/strong&gt; for reusable design elements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variants&lt;/strong&gt; for trying multiple design directions&lt;/li&gt;
&lt;li&gt;A CSS editor for visual tweaks&lt;/li&gt;
&lt;li&gt;and many more...&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These designs live as &lt;code&gt;.canvas&lt;/code&gt; files in your repo, which means they can be version-controlled with git like your code.&lt;/p&gt;

&lt;p&gt;Once you like a design, you can hit Code design, and Kombai moves it into Code Mode, reads your project, understands your stack, reuses relevant components and tokens, and codes it into working frontend.&lt;/p&gt;

&lt;p&gt;That is what makes it feel different from tools like v0, Lovable, or Bolt.&lt;/p&gt;

&lt;p&gt;Those tools are great for generating prototypes or apps from scratch. Kombai is more focused on your &lt;strong&gt;existing frontend repo&lt;/strong&gt;. It works inside your editor and tries to build the thing using the stack you already have.&lt;/p&gt;

&lt;p&gt;Check out this intro demo Kombai 2.0. The first AI design engineer. &lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-2061825199247614316-192" src="https://platform.twitter.com/embed/Tweet.html?id=2061825199247614316"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-2061825199247614316-192');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=2061825199247614316&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;With Kombai, you can not only create designs, but also get the safety of knowing that no backend code is &lt;strong&gt;ever touched&lt;/strong&gt;, which ensures your business logic is not mistakenly changed.&lt;/p&gt;

&lt;p&gt;You can add it right inside your editor. It works with &lt;strong&gt;VSCode&lt;/strong&gt;, &lt;strong&gt;Cursor&lt;/strong&gt;, &lt;strong&gt;Windsurf&lt;/strong&gt;, and &lt;strong&gt;Trae&lt;/strong&gt;. Just grab it from the extension marketplace, launch it, and you’re ready to go.&lt;/p&gt;

&lt;p&gt;With Kombai, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Generate UI designs from scratch using Design Mode.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Turn Figma designs into code without setting up Figma MCP separately.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Turn Figma designs into code (React, HTML, CSS, etc.) using the component library your project already uses.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use screenshots, images, websites, or natural language as input.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reuse existing components, hooks, design tokens, and frontend conventions from your repo.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Visually inspect and edit your UI through the Kombai Browser.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Work with a frontend-smart engine that understands 30+ libraries, including Next.js, MUI, and Chakra UI.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Most importantly, preview the changes in a sandbox so you can approve or reject the change before committing it to the files.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Head to the &lt;a href="https://docs.kombai.com/get-started/welcome" rel="noopener noreferrer"&gt;docs&lt;/a&gt; to get started and find the setup for your editor.&lt;/p&gt;

&lt;p&gt;You can be up and running in under a minute:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install the extension for your editor&lt;/li&gt;
&lt;li&gt;Sign in and connect your project&lt;/li&gt;
&lt;li&gt;Pick the mode you need: Code, Plan, Design, etc.&lt;/li&gt;
&lt;li&gt;Paste a Figma link or describe what you want to build&lt;/li&gt;
&lt;li&gt;Paste a Figma link, describe what you want, attach an image, or work from your existing code&lt;/li&gt;
&lt;li&gt;Review the output and commit your code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you spend most of your time on the frontend, this is a &lt;strong&gt;no-brainer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Now comes with 100 award winning landing pages that you can make your own.&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-2061825201730695550-647" src="https://platform.twitter.com/embed/Tweet.html?id=2061825201730695550"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-2061825201730695550-647');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=2061825201730695550&amp;amp;theme=dark"
  }



&lt;/p&gt;




&lt;h2&gt;
  
  
  One Important Note Before the Tests
&lt;/h2&gt;

&lt;p&gt;Kombai now has a much bigger features and design support than it did before.&lt;/p&gt;

&lt;p&gt;But this post is still mainly a frontend coding and implementation comparison.&lt;/p&gt;

&lt;p&gt;So, I am not going to deeply test Design Mode here. That deserves its own separate post because the right comparison there would probably be against tools like v0, Lovable, Bolt, and maybe Figma-based workflows.&lt;/p&gt;

&lt;p&gt;For this post, the test is still:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can it understand a real codebase?&lt;/li&gt;
&lt;li&gt;Can it preserve functionality?&lt;/li&gt;
&lt;li&gt;Can it implement a feature cleanly?&lt;/li&gt;
&lt;li&gt;Can it match or improve UI quality?&lt;/li&gt;
&lt;li&gt;Importantly, can it work in a large real-world existing project?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is exactly what we are going to test here.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test Workflow
&lt;/h2&gt;

&lt;p&gt;In this model test, I’ll be using &lt;strong&gt;Claude Opus 4.6&lt;/strong&gt; with everyone’s favorite CLI coding agent, &lt;strong&gt;Claude Code&lt;/strong&gt;, along with Figma MCP support.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 Just in case you're interested in how to add MCP support to Claude Code, you can view the guide &lt;a href="https://code.claude.com/docs/en/mcp" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For Kombai, the choice mostly comes down to the IDE, so I’ll go with Cursor. It does not really matter much which IDE you use, though. VS Code would work just fine too.&lt;/p&gt;

&lt;p&gt;We’ll test both tools on three decently complex tasks in &lt;strong&gt;real-world open-source projects&lt;/strong&gt; with hundreds of thousands of LOC.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A frontend-heavy task with Figma&lt;/li&gt;
&lt;li&gt;A frontend + backend task with more implementation complexity&lt;/li&gt;
&lt;li&gt;A task that relies more on codebase understanding than implementation complexity&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I’ll compare both tools on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;time to complete the task&lt;/li&gt;
&lt;li&gt;quality of the code&lt;/li&gt;
&lt;li&gt;how closely the final output matches the given design or feature intent&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Note:&lt;/strong&gt; I’ll share the source code changes for each task from both tools in a &lt;code&gt;.patch&lt;/code&gt; file. That way, you can easily reproduce them on your local system by cloning the repository and applying the patch with &lt;code&gt;git apply &amp;lt;path_file_name&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; As I’m using a Claude plan and not API usage, price is roughly estimated based on the output tokens.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Real-World Coding Comparison
&lt;/h2&gt;

&lt;p&gt;The entire test in this blog is going to be on top of real-world open-source projects that are used by thousands of people, not toy projects, but ones with &lt;strong&gt;thousands of LOC&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;All three tests are going to be on 3 different projects, all open-source, of course!&lt;/p&gt;

&lt;p&gt;Let's start with an easier one, a shadcn template repository with Figma MCP.&lt;/p&gt;




&lt;h2&gt;
  
  
  Test 1: Rebuild an Open-Source Project UI (with Figma MCP)
&lt;/h2&gt;

&lt;p&gt;For this test, I'll be comparing both of them on a Figma file, giving them access to a Figma MCP server.&lt;/p&gt;

&lt;p&gt;You can find the Figma design template for this test here: &lt;a href="https://www.figma.com/design/xfzmzKLBkvvY6BPteZWukK/-FREE--Food-Delivery-Admin-Dashboard--Community-?node-id=0-1&amp;amp;p=f&amp;amp;t=GmIYunPopZ2pO4SK-0" rel="noopener noreferrer"&gt;Dashboard&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Implement the provided Figma dashboard design in this existing Next.js + shadcn/ui dashboard codebase.

&amp;lt;figma_url&amp;gt;

Constraints:

1. Preserve all existing functionality exactly.
2. Do not break routing, state, existing interactions, or responsiveness.
3. Replace the current dashboard presentation layer with a UI that closely matches the Figma design.
4. The Figma is food-delivery themed. Translate only the content domain, not the visual system.
5. Keep the layout, structure, and styling language of the Figma as intact as possible.
6. Replace food-specific labels and data with content that fits a generic admin dashboard.
7. Reuse existing logic and data bindings wherever possible.
8. Avoid adding fake backend logic.
9. Keep the implementation production-quality and componentized.

Focus areas:

- sidebar
- header/top bar
- summary metric cards
- chart section
- table/list section
- filters/search/actions if present
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Claude Opus 4.6 (with Figma MCP)
&lt;/h3&gt;

&lt;p&gt;Here's the response from Claude Opus 4.6:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/LNB78jQO6f8"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;You can find the code it generated here: &lt;a href="https://gist.github.com/shricodev/ec49249394524a94139a5af16250f2df" rel="noopener noreferrer"&gt;Opus 4.6 source Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Opus did a pretty good job here.&lt;/p&gt;

&lt;p&gt;Given the Figma itself was not even made for a generic admin dashboard in the first place, there was obviously some room where it had to improvise a bit on its own.&lt;/p&gt;

&lt;p&gt;But the main thing I was testing for here was not just whether it could make the frontend look close enough. The actual ask in the prompt was to preserve and support the existing functionalities as well. That part just was not really there. It did not add the interactivity support properly, which was the whole point of the test after all.&lt;/p&gt;

&lt;p&gt;So visually, sure, it looks alright. But if the interaction layer is missing, that is a pretty big miss for this kind of task. If it had nailed that part too, this would have been a really strong result. But it didn’t.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Output token Cost:&lt;/strong&gt; ~$0.125&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; 9 minutes 1 second&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output Token Usage:&lt;/strong&gt; ~8K&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; 6 files changed, 420 insertions(+), 422 deletions(-)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faa8rfsl6zzzdn8ivt4us.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faa8rfsl6zzzdn8ivt4us.png" alt="Opus 4.6 task completion" width="800" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Kombai
&lt;/h3&gt;

&lt;p&gt;Here's the response from Kombai:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/tlWNZd6_a84"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;You can find the code it generated here: &lt;a href="https://gist.github.com/shricodev/6b734610378fbbed6e524f137618d15f" rel="noopener noreferrer"&gt;Kombai source Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kombai nailed this one. Even better how accurately it finds out the tech stack.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkld3s939tqgrvjtx43qg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkld3s939tqgrvjtx43qg.png" alt="Kombai figuring out tech stack" width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The frontend turned out really good. More importantly, the interactivity is there, which is the main thing being tested other than how close it comes to the Figma design. It actually respected the fact that this is an existing app, with behavior that still needs to work.&lt;/p&gt;

&lt;p&gt;And if I compare both the UI itself and the overall build quality from a production POV, Kombai clearly did better here.&lt;/p&gt;

&lt;p&gt;That said, there was one issue I noticed while working with Kombai.&lt;/p&gt;

&lt;p&gt;After it finishes the implementation, it has this nice default feature where it opens up a browser preview and lets you chat there to fix smaller things quickly. In theory, that sounds great. In practice, for apps that require authentication, which this one did, it falls apart. Google OAuth simply flags it as unsafe, so you cannot log in there at all.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwj5n0kq6b23lyfgqfncy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwj5n0kq6b23lyfgqfncy.png" alt="Kombai authentication problem in the test browser" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So yeah, they definitely need to work on that.&lt;/p&gt;

&lt;p&gt;Still, overall, the user experience and the actual result were top notch.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; ~12 minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; 12 files changed, 940 insertions(+), 532 deletions(-)&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;I noticed the project also has standalone Kanban board support, so why not quickly test it on this as well?&lt;/p&gt;

&lt;p&gt;Prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Completely refactor the Kanban board UI in this open source project based on this Figma:

&amp;lt;figma_url&amp;gt;

This should be a real UI redesign, not minor styling tweaks. Study the Figma closely and make the board feel much more polished, modern, clean, and cohesive. Improve layout, spacing, typography, hierarchy, cards, columns, controls, interaction states, and responsiveness.

Preserve functionality, but refactor components and styling where needed so the code is cleaner and the design is more consistent. Focus heavily on UI quality and make the final result feel much closer to the Figma overall.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Claude Opus 4.6 (with Figma MCP)
&lt;/h3&gt;

&lt;p&gt;Here's the response from Claude Opus 4.6:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/-hycEBfoaLw"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;You can find the code it generated here: &lt;a href="https://gist.github.com/shricodev/19c1e6f9f640975f22363f4ad9aa7346" rel="noopener noreferrer"&gt;Opus 4.6 source Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Opus did pretty well here too.&lt;/p&gt;

&lt;p&gt;The good part is that functionality was not broken. The board still works, and that matters a lot for a refactor like this. The redesign itself is also nice. It is clearly better than before.&lt;/p&gt;

&lt;p&gt;That said, if I look closely at the actual design match, it seems to miss a little here and there. It does not feel quite as locked in to the Figma as the best result should.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; negligible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; 5 minutes 26 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; 5 files changed, 162 insertions(+), 56 deletions(-)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fskrb2gjprh475bvt76t4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fskrb2gjprh475bvt76t4.png" alt="Ous 4.6 task completion" width="799" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Kombai
&lt;/h3&gt;

&lt;p&gt;Here's the response from Kombai:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Qce9-dNZv_Q"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;You can find the code it generated here: &lt;a href="https://gist.github.com/shricodev/583d31c69823cb772c7a7188c4b89f73" rel="noopener noreferrer"&gt;Kombai source Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Again, Kombai was excellent here.&lt;/p&gt;

&lt;p&gt;Honestly, this one came out awesome. It matches the Figma really well, preserves the expected behavior, and feels like a proper redesign.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; 4 files changed, 362 insertions(+), 72 deletions(-)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; ~6 minutes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Test 2: Add a Feature in Uptime Kuma
&lt;/h2&gt;

&lt;p&gt;There's this open-source project that's super popular for the self-hosted monitoring service &lt;a href="https://github.com/louislam/uptime-kuma" rel="noopener noreferrer"&gt;Uptime Kuma&lt;/a&gt;, with over &lt;strong&gt;84K stars&lt;/strong&gt; on GitHub.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F41bm2kdpxp4q7gsrbgqt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F41bm2kdpxp4q7gsrbgqt.png" alt="Uptime Kuma" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's an existing issue on the project that we will try to build: &lt;a href="https://github.com/louislam/uptime-kuma/issues/1984" rel="noopener noreferrer"&gt;Calendar Graph&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add a heatmap-style uptime history section to the public status page in this existing Uptime Kuma codebase.

Constraints:

1. Keep all existing functionality working.
2. Do not break the public status page, responsiveness, or current monitor behavior.
3. Build this as a proper feature inside the existing architecture.
4. Reuse existing logic and data flow wherever possible.
5. Avoid fake backend logic or hardcoded mock data.
6. Make the heatmap work independently for each service.
7. Keep the UI consistent with Uptime Kuma’s existing style.
8. Make the implementation clean and production-ready.

Focus areas:

- status page
- per-service uptime history
- heatmap UI
- state management
- backend integration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Claude Opus 4.6 (with Figma MCP)
&lt;/h3&gt;

&lt;p&gt;Here's the response from Claude Opus 4.6:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/paQ4wQ2MyyU"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;You can find the code it generated here: &lt;a href="https://gist.github.com/shricodev/f6e66c056cd2bb1c868e640b25f2a953" rel="noopener noreferrer"&gt;Opus 4.6 source Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Opus actually got the overall feature in place, and to be fair, the core thing does work.&lt;/p&gt;

&lt;p&gt;That said, there is a bug.&lt;/p&gt;

&lt;p&gt;When you try to change the monitoring duration for one service, it also changes it for all the others. That is obviously not how this should behave, and for a feature like this, that kind of state handling bug is a big issue, of course!&lt;/p&gt;

&lt;p&gt;Other than that, there is not a whole lot to complain about. The core functionality works, just with some caveats, and that feels fair. I do not think it is realistic to expect the model to get every detail perfectly right in one shot on a non-trivial codebase.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Output token Cost:&lt;/strong&gt; ~$0.12&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; 13 minutes 5 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output Token Usage:&lt;/strong&gt; ~7.5K&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; 4 files changed, 784 insertions(+)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Kombai
&lt;/h3&gt;

&lt;p&gt;Here's the response from Kombai:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/UKF2VJ6dCyw"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;You can find the code it generated here: &lt;a href="https://gist.github.com/shricodev/423f45e00bf9e9c65e202992c8cf2a6f" rel="noopener noreferrer"&gt;Kombai source Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kombai did this one properly.&lt;/p&gt;

&lt;p&gt;The feature is implemented correctly, the behavior is right, and even the bug that showed up in the Claude Opus implementation is not there at all.&lt;/p&gt;

&lt;p&gt;That part matters because this is not just about working on the UI. It's about putting the feature into an existing project.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; ~10 minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; 4 files changed, 809 insertions(+), 1 deletion(-)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Test 3: Add a Feature to Chatwoot
&lt;/h2&gt;

&lt;p&gt;This is a little different from the other two tests.&lt;/p&gt;

&lt;p&gt;This was more of a test to see how well the model actually understands the codebase and less about generating or working on the UI heavily. The code change isn't going to be huge, but it tests the two on how good they are at understanding the codebase and adding a feature on top.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Far87ngghworzc90e6amb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Far87ngghworzc90e6amb.png" alt="Chatwoot" width="800" height="216"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add a new "Participating" tab to the chat/conversation list in this existing Chatwoot codebase.

The goal is to let users quickly view conversations they are participating in, while keeping the implementation fully aligned with how Chatwoot already handles conversations, tabs, filtering, permissions, and dashboard state.

Constraints:

1. Preserve all existing functionality exactly.
2. Do not break existing conversation list behavior, routing, filters, permissions, or dashboard interactions.
3. Add "Participating" as a proper tab in the existing chat list UI, not as a separate temporary view.
4. Make sure the tab only shows conversations the current user is participating in.
5. Reuse existing backend, frontend, and store patterns wherever possible.
6. Avoid hacks, fake data, or disconnected logic.
7. Keep the implementation production-quality and consistent with the current Chatwoot UI.
8. Ensure permissions and visibility rules continue to work correctly.
9. Make the feature feel like a native part of the product.

Focus areas:

- chat list tab integration
- conversation filtering
- backend query support
- participation-based logic
- store/state updates
- correct tab placement in the UI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Claude Opus 4.6 (with Figma MCP)
&lt;/h3&gt;

&lt;p&gt;Here's the response from Claude Opus 4.6:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/2AsxkzNUZdI"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;You can find the code it generated here: &lt;a href="https://gist.github.com/shricodev/314fa9f6d90859d6146d49d040eecf4e" rel="noopener noreferrer"&gt;Opus 4.6 source Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Opus nailed this one.&lt;/p&gt;

&lt;p&gt;This is exactly the kind of test where you are not making massive frontend changes, but instead need a solid understanding of how the app actually works so the feature fits naturally into the existing codebase.&lt;/p&gt;

&lt;p&gt;The only issue I noticed was some UI flickering. But that looked more like a minor issue than anything wrong with the feature implementation itself.&lt;/p&gt;

&lt;p&gt;Other than that, it worked perfectly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Output token Cost:&lt;/strong&gt; ~$0.02&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; 12 minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output Token Usage:&lt;/strong&gt; ~1.3K&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; 13 files changed, 52 insertions(+), 11 deletions(-)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Kombai
&lt;/h3&gt;

&lt;p&gt;Here's the response from Kombai:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Pmpd3YvHrJk"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;You can find the code it generated here: &lt;a href="https://gist.github.com/shricodev/5f52c2ae2274a734d0eed68fccfffc15" rel="noopener noreferrer"&gt;Kombai source Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There was no flickering here, and Kombai also placed everything where it actually made sense. Claude got the logic right too, but the tab placement felt a bit off. Kombai just tied it together better, and the whole thing felt a bit cleaner.&lt;/p&gt;

&lt;p&gt;So overall, both did well here, but Kombai’s result felt cleaner and better integrated.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; 7 files changed, 30 insertions(+), 9 deletions(-)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; ~8 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fif5cvarx091xh9emzb0l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fif5cvarx091xh9emzb0l.png" alt="Kombai task completion" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Verdict
&lt;/h2&gt;

&lt;p&gt;So, what’s the takeaway?&lt;/p&gt;

&lt;p&gt;After testing both on real tasks in real codebases, Kombai was clearly stronger for frontend work.&lt;/p&gt;

&lt;p&gt;That is not to say Claude Code with Opus 4.6 is bad. Far from it.&lt;/p&gt;

&lt;p&gt;It is one of the strongest coding models available right now, and it can do some serious work. In some cases, especially the Chatwoot test, it understood the codebase really well and shipped something that was genuinely solid.&lt;/p&gt;

&lt;p&gt;Claude Opus lineup is too good for general coding.&lt;/p&gt;

&lt;p&gt;Kombai is a frontend specialized tool.&lt;/p&gt;

&lt;p&gt;And for frontend-heavy work, that specialization really shows.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fym68r25yz6fowfyulu7y.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fym68r25yz6fowfyulu7y.gif" alt="too good gif" width="400" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That said, I do not think you should take my word for it blindly.&lt;/p&gt;

&lt;p&gt;Also, this does not mean Kombai will win every project or every workflow.&lt;/p&gt;

&lt;p&gt;If you are doing backend-heavy work, infra changes, full-stack architecture, or mostly CLI-based development, Claude Code still makes a ton of sense.&lt;/p&gt;

&lt;p&gt;The design side is also getting more interesting now. I did not fully test Design Mode in this post, because that deserves a separate comparison.&lt;/p&gt;

&lt;p&gt;Honestly, the best way to judge tools like this is to try them yourself. A comparison like this can give you a rough idea, but it really clicks only when you use them on your own codebase and see how they actually feel.&lt;/p&gt;

&lt;p&gt;That’s all for this one. Thank you for reading! ✌️&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Claude Code vs. OpenCode without the hype</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Thu, 21 May 2026 13:55:18 +0000</pubDate>
      <link>https://dev.to/composiodev/claude-code-vs-opencode-without-the-hype-j1f</link>
      <guid>https://dev.to/composiodev/claude-code-vs-opencode-without-the-hype-j1f</guid>
      <description>&lt;p&gt;Everyone wants a coding agent now.&lt;/p&gt;

&lt;p&gt;Not a chatbot that explains code.&lt;/p&gt;

&lt;p&gt;An actual agent that can read your repo, edit files, run commands, use tools, and keep moving while you supervise.&lt;/p&gt;

&lt;p&gt;Claude Code and OpenCode are two of the most interesting takes on that idea.&lt;/p&gt;

&lt;p&gt;Claude Code is the polished Anthropic-native route.&lt;/p&gt;

&lt;p&gt;OpenCode is the open-source route for people who want more model choice, more control, and a setup they can tweak.&lt;/p&gt;

&lt;p&gt;And that difference matters more than it looks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5bcj3t5mypqia64m30wr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5bcj3t5mypqia64m30wr.png" alt="distracted man GIF" width="687" height="361"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What is OpenCode
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ Open-source coding agent with model and tool control&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2k02x3jygu2mwj40jz44.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2k02x3jygu2mwj40jz44.png" alt="OpenCode" width="799" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;OpenCode is an open-source coding agent for developers who want more control over their AI coding setup.&lt;/p&gt;

&lt;p&gt;It runs in the terminal, IDE, and desktop, and lets you bring your own model instead of &lt;strong&gt;locking you into one provider&lt;/strong&gt;. Claude, GPT, Gemini, local models, and 75+ other providers are supported. That is probably the biggest reason people care about it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkem0kiznc0fm4g9gyvw9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkem0kiznc0fm4g9gyvw9.png" alt="OpenCode tweet" width="799" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It also comes with the things you expect from a serious coding agent now: LSP support, multi-session workflows, project memory through &lt;code&gt;AGENTS.md&lt;/code&gt;, MCP tools, custom agents, plugins, and editor support, and maybe a bunch more.&lt;/p&gt;

&lt;p&gt;So the pitch is not just “AI in your terminal.”&lt;/p&gt;

&lt;p&gt;That undersells it.&lt;/p&gt;

&lt;p&gt;OpenCode is closer to a &lt;strong&gt;coding-agent workbench&lt;/strong&gt;. You bring the model, the provider, the editor, the agents, and the workflow. OpenCode gives you the open layer that ties it all together.&lt;/p&gt;

&lt;p&gt;Not everyone needs that level of control.&lt;/p&gt;

&lt;p&gt;But some developers absolutely do.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 OpenCode is for developers who want to tweak every single detail of their coding agent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is what makes it interesting next to Claude Code.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Claude Code
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ Anthropic’s polished coding agent for your terminal.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fivw9mx75nhkdloyczjr5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fivw9mx75nhkdloyczjr5.png" alt="Claude Code" width="800" height="208"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude Code is Anthropic’s coding agent that lives in your terminal.&lt;/p&gt;

&lt;p&gt;The idea is pretty same here, it can read your codebase, edit files, run commands, handle Git stuff, and all through prompts.&lt;/p&gt;

&lt;p&gt;The big difference is that Claude Code is built around Claude.&lt;/p&gt;

&lt;p&gt;That sounds obvious, but it matters.&lt;/p&gt;

&lt;p&gt;You are not coming here to mix and match ten different model providers. You are coming here because you trust Anthropic’s models, and you want the cleanest experience around them.&lt;/p&gt;

&lt;p&gt;Claude Code also comes with a lot of serious agent features: project memory through &lt;code&gt;CLAUDE.md&lt;/code&gt;, slash commands, permissions, hooks, MCP, plugins, custom subagents, and IDE integrations.&lt;/p&gt;

&lt;p&gt;Claude Code is closer to a Claude-native coding environment. The model, the agent loop, the tool use, the permissions, and the workflow all come from the same Anthropic-shaped box.&lt;/p&gt;

&lt;p&gt;Less DIY.&lt;/p&gt;

&lt;p&gt;But there is also a small shift happening.&lt;/p&gt;

&lt;p&gt;Some developers are starting to move from Claude Code to OpenCode or OpenAI’s Codex for one simple reason: &lt;strong&gt;usage limits&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi4uxa5o6uxb1s9x87cgw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi4uxa5o6uxb1s9x87cgw.png" alt="Claude Code Usage Limit meme" width="800" height="830"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude Code is great, but when you are deep in a coding session, hitting limits feels brutal. And for heavier users, even the &lt;strong&gt;$200 Claude Max plan&lt;/strong&gt; does not always feel like enough.&lt;/p&gt;

&lt;p&gt;That is why OpenCode and Codex are tempting. Also read: &lt;a href="https://composio.dev/content/claude-code-vs-openai-codex" rel="noopener noreferrer"&gt;Claude Code vs. Codex: Detailed breakdown&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When Claude hits the wall, people still need a way to keep shipping.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 If you're an Anthropic fanboy, and don't care about other models, stick to Claude Code.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  High Level Architecture
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ How both the agents work&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At a high level, both OpenCode and Claude Code follow the same basic agent loop.&lt;/p&gt;

&lt;p&gt;You give it a task.&lt;/p&gt;

&lt;p&gt;It looks at the repo.&lt;/p&gt;

&lt;p&gt;It decides what files, commands, or tools it needs.&lt;/p&gt;

&lt;p&gt;It takes an action.&lt;/p&gt;

&lt;p&gt;Then it reads the result and keeps going.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcv9tejzbqvy4whr7jcj2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcv9tejzbqvy4whr7jcj2.png" alt="Coding Agent architecture" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ This is the highest-level architecture of a coding agent. A few details change from tool to tool.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That loop is the boring part.&lt;/p&gt;

&lt;p&gt;The interesting part is everything around it.&lt;/p&gt;

&lt;p&gt;Here is a tiny example of that loop in practice.&lt;/p&gt;

&lt;p&gt;I gave Claude Code and OpenCode the same small task in a demo word-count repo:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Add a &lt;code&gt;--json&lt;/code&gt; flag to a word-count CLI, update the tests, and run them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The interesting part is not the feature. It is watching both agents go through the same shape: understand the repo, plan the change, edit the files, and run the tests.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/D74fsmbwE98"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Claude Code wraps that loop in Anthropic’s own product system. You get Claude, project memory through &lt;code&gt;CLAUDE.md&lt;/code&gt;, permissions, hooks, MCP, &lt;a href="https://composio.dev/content/top-claude-code-plugins" rel="noopener noreferrer"&gt;plugins&lt;/a&gt;, &lt;a href="https://composio.dev/content/top-claude-skills" rel="noopener noreferrer"&gt;Claude skills&lt;/a&gt;, and subagents in one single setup.&lt;/p&gt;

&lt;p&gt;OpenCode takes a more open route. It gives you the agent runtime, but lets you bring different models, providers, agents, tools, and workflows. Its docs split agents into primary agents and subagents, and let you configure specialized assistants with custom prompts, models, and tool access.&lt;/p&gt;

&lt;p&gt;So architecturally, the difference is not that one is an agent and the other is not.&lt;/p&gt;

&lt;p&gt;They both are.&lt;/p&gt;

&lt;p&gt;The real difference is who controls the harness around the agent.&lt;/p&gt;

&lt;p&gt;Claude Code gives you Anthropic’s harness.&lt;/p&gt;

&lt;p&gt;OpenCode gives you a harness you can inspect, and configure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Context, memory and tool use
&lt;/h2&gt;

&lt;p&gt;Both Claude Code and OpenCode are doing the same basic thing: they build a giant prompt, stuff it with repo context, tool definitions, memory files, recent messages, and tool results, then ask the model what to do next.&lt;/p&gt;

&lt;p&gt;The difference is how much of that system you control.&lt;/p&gt;

&lt;p&gt;Claude Code is more vertically integrated here. It is built around Anthropic models, so it can take advantage of Anthropic-specific stuff like prompt caching, native tool calls, and Claude’s own long-context behavior.&lt;/p&gt;

&lt;p&gt;That matters.&lt;/p&gt;

&lt;p&gt;Tool definitions, system prompts, and &lt;code&gt;CLAUDE.md&lt;/code&gt; can be cached between turns, which makes long coding sessions cheaper and faster than they would be if Claude had to re-read everything from scratch every single time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fphuqzyrqslhkmlnh7ah3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fphuqzyrqslhkmlnh7ah3.jpg" alt="compaction in a coding agent" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;OpenCode takes a different route.&lt;/p&gt;

&lt;p&gt;It does not assume one model or one provider. Instead, it &lt;strong&gt;reads the model’s context limit&lt;/strong&gt; from the provider metadata and builds the session around that. So the same OpenCode setup can run with Claude, GPT, Gemini, Qwen, local models, or whatever else you plug in.&lt;/p&gt;

&lt;p&gt;That flexibility is the whole point.&lt;/p&gt;

&lt;p&gt;But it also means OpenCode has to normalize all the weird provider differences: tool call IDs, cache support, model limits, and tool-calling parts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F75v9hwsvotu1qmz1k8p0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F75v9hwsvotu1qmz1k8p0.png" alt="OpenCode support for multiple providers" width="799" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude Code gets to optimize deeply for Claude.&lt;/p&gt;

&lt;p&gt;OpenCode has to work with everyone.&lt;/p&gt;

&lt;p&gt;Memory works the same way.&lt;/p&gt;

&lt;p&gt;Claude Code uses &lt;code&gt;CLAUDE.md&lt;/code&gt; as the main project memory file. It can also load nested &lt;code&gt;CLAUDE.md&lt;/code&gt; files, user-level memory, and auto-memory. So it feels more like the agent has a built-in memory system.&lt;/p&gt;

&lt;p&gt;OpenCode uses &lt;code&gt;AGENTS.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is more portable. You can commit it to the repo, share it with the team, and use it as a general agent instruction file instead of something tied to one vendor. OpenCode can even fall back to &lt;code&gt;CLAUDE.md&lt;/code&gt;, which makes migration easier.&lt;/p&gt;

&lt;p&gt;At some point, every agent runs out of context.&lt;/p&gt;

&lt;p&gt;Claude Code handles this by compacting the conversation. Older tool outputs are cleared first, then the session gets summarized if needed. That is why Claude Code has commands like &lt;code&gt;/context&lt;/code&gt; and &lt;code&gt;/compact&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;OpenCode is a bit more explicit. It checks whether the session is close to the model’s context limit, keeps a buffer for output, and then prunes old tool outputs before doing a full summary. The important bit is that OpenCode stores the raw history in &lt;strong&gt;SQLite&lt;/strong&gt;, so pruning does not mean the data is gone forever.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7om4vrfngpmqdjsh3phe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7om4vrfngpmqdjsh3phe.png" alt="OpenCode flexibility" width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tool use follows the same pattern.&lt;/p&gt;

&lt;p&gt;Claude Code gives you a polished default toolbelt: read, write, edit, grep, glob, bash, web fetch, todo tracking, MCP, hooks, skills, and subagents.&lt;/p&gt;

&lt;p&gt;OpenCode gives you a smaller but more configurable tool system: read, write, edit, patch, bash, grep, glob, web fetch, task, todo, &lt;a href="https://composio.dev/content/10-best-opencode-skills-that-are-actually-useful-in-2026" rel="noopener noreferrer"&gt;skills&lt;/a&gt;, MCP, custom tools, and experimental LSP support.&lt;/p&gt;

&lt;p&gt;The difference is who controls the tool layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Subagents and task delegation
&lt;/h2&gt;

&lt;p&gt;Subagents are basically how coding agents avoid stuffing everything into one giant conversation.&lt;/p&gt;

&lt;p&gt;Instead of making the main agent do every task itself, it can delegate a smaller job to another agent with its own context window, prompt, tools, and permissions.&lt;/p&gt;

&lt;p&gt;Claude Code and OpenCode both follow the same basic pattern here.&lt;/p&gt;

&lt;p&gt;The parent agent calls a &lt;code&gt;Task&lt;/code&gt; or &lt;code&gt;task&lt;/code&gt; tool.&lt;/p&gt;

&lt;p&gt;A child agent spins up.&lt;/p&gt;

&lt;p&gt;It does the work in isolation.&lt;/p&gt;

&lt;p&gt;Then it returns one final message back to the parent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Main Agent
  |
  | calls Task / task
  v
Subagent
  - own context window
  - own prompt
  - own tools
  - own permissions
  |
  | returns final result only
  v
Main Agent continues...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That part is important. The parent usually does not see the full subagent conversation. It gets the result, not the whole reasoning.&lt;/p&gt;

&lt;p&gt;Claude Code has the more polished version of this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmgfuv1jw1x2rt22d0i57.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmgfuv1jw1x2rt22d0i57.png" alt="Claude Code approach to subagents" width="799" height="269"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It ships with built-in agents like &lt;code&gt;Explore&lt;/code&gt;, &lt;code&gt;Plan&lt;/code&gt;, and &lt;code&gt;general-purpose&lt;/code&gt;. &lt;code&gt;Explore&lt;/code&gt; is mostly read-only and useful for repo research. &lt;code&gt;Plan&lt;/code&gt; helps gather context during planning. &lt;code&gt;general-purpose&lt;/code&gt; is for broader work.&lt;/p&gt;

&lt;p&gt;You can also define custom agents in &lt;code&gt;.claude/agents/&lt;/code&gt; with YAML frontmatter for things like &lt;code&gt;tools&lt;/code&gt;, &lt;code&gt;model&lt;/code&gt;, &lt;code&gt;permissionMode&lt;/code&gt;, &lt;code&gt;maxTurns&lt;/code&gt;, &lt;code&gt;skills&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That means you can do stuff like:&lt;/p&gt;

&lt;p&gt;Use a fast Haiku-style agent for repo search.&lt;/p&gt;

&lt;p&gt;Use a stronger model for code review.&lt;/p&gt;

&lt;p&gt;OpenCode has a similar shape, but it is more transparent.&lt;/p&gt;

&lt;p&gt;It has primary agents and subagents. Primary agents handle the main chat, while subagents are called through the &lt;code&gt;task&lt;/code&gt; tool or &lt;code&gt;@&lt;/code&gt; mentions.&lt;/p&gt;

&lt;p&gt;Custom agents can live in &lt;code&gt;.opencode/agents/*.md&lt;/code&gt; or inside &lt;code&gt;opencode.json&lt;/code&gt;, with fields like &lt;code&gt;mode&lt;/code&gt;, &lt;code&gt;model&lt;/code&gt;, &lt;code&gt;temperature&lt;/code&gt;, &lt;code&gt;steps&lt;/code&gt;, &lt;code&gt;prompt&lt;/code&gt;, and &lt;code&gt;permission&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The interesting part is that OpenCode stores subagents as real child sessions in &lt;strong&gt;SQLite&lt;/strong&gt;. So delegation is not just a hidden prompt trick. It is represented in the session model with its own messages, permissions, and snapshots.&lt;/p&gt;

&lt;p&gt;That fits OpenCode’s whole philosophy.&lt;/p&gt;

&lt;p&gt;Claude Code gives you a cleaner subagent experience.&lt;/p&gt;

&lt;p&gt;OpenCode gives you a more inspectable one.&lt;/p&gt;


&lt;h2&gt;
  
  
  Permissions, safety, and control
&lt;/h2&gt;

&lt;p&gt;This is where the two are very different.&lt;/p&gt;

&lt;p&gt;Claude Code is more conservative by default. It has permission modes, allow/ask/deny rules, hooks, and sandboxing around &lt;strong&gt;Bash&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So you can allow boring commands like tests, deny obvious footguns like .env reads or &lt;code&gt;curl | sh&lt;/code&gt;, and ask before anything risky.&lt;/p&gt;

&lt;p&gt;The important part is that Claude Code has multiple safety layers.&lt;/p&gt;

&lt;p&gt;Permissions decide what Claude is allowed to do.&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Bash(npm run test *)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git status *)"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Read(./.env)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read(./secrets/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(curl *)"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Hooks can intercept tool calls before or after they run and sandboxing gives Bash an OS-level boundary.&lt;/p&gt;

&lt;p&gt;OpenCode is simpler.&lt;/p&gt;

&lt;p&gt;Most of the control lives in one permission object inside &lt;code&gt;opencode.json&lt;/code&gt;. You can set rules for &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;task&lt;/code&gt;, &lt;code&gt;webfetch&lt;/code&gt;, and other tools from the same place.&lt;/p&gt;

&lt;p&gt;That is clean, but OpenCode is also more permissive by default. You are expected to configure the rules yourself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe8i1czi8c1lgv8ujg9l8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe8i1czi8c1lgv8ujg9l8.png" alt="OpenCode permissions" width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permission"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ask"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"bash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ask"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"git status *"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"git push *"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rm *"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deny"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"edit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"packages/web/src/**/*.tsx"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ask"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It does have some smart checks, especially for Bash. OpenCode parses shell commands with tree-sitter (the same thing you have inside NeoVim), so it can detect risky commands like &lt;code&gt;rm&lt;/code&gt;, &lt;code&gt;mv&lt;/code&gt;, &lt;code&gt;chmod&lt;/code&gt;, or paths outside the project more carefully than plain string matching.&lt;/p&gt;

&lt;p&gt;But there is no &lt;a href="https://www.anthropic.com/engineering/claude-code-sandboxing" rel="noopener noreferrer"&gt;native sandbox&lt;/a&gt; like Claude Code.&lt;/p&gt;

&lt;p&gt;The bigger OpenCode power feature is plugins. Plugins can intercept tool execution, add custom tools, and change agent behavior.&lt;/p&gt;

&lt;p&gt;That makes OpenCode way more hackable.&lt;/p&gt;


&lt;h2&gt;
  
  
  What the Claude Code leak tells us
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7h7v5xmi6q45a33xjk2v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7h7v5xmi6q45a33xjk2v.png" alt="Claude Code leak" width="799" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The interesting part of the leak is what it showed about coding agents.&lt;/p&gt;

&lt;p&gt;A lot of Claude magic is in the harness around the model: context management, tool descriptions, prompt caching, permissions, compaction, subagents, and the agent loop.&lt;/p&gt;

&lt;p&gt;OpenCode does pretty much the same. It is not trying to clone some impossible model-level feature. It is trying to build a different harness around similar idea.&lt;/p&gt;

&lt;p&gt;OpenCode’s advantage is that the harness is open, inspectable, and replaceable.&lt;/p&gt;

&lt;p&gt;Another thing that's clear is that the future is not just about better models, but the system around them.&lt;/p&gt;

&lt;p&gt;Better context control.&lt;/p&gt;

&lt;p&gt;Better tool boundaries.&lt;/p&gt;

&lt;p&gt;Better memory.&lt;/p&gt;

&lt;p&gt;Better permissions.&lt;/p&gt;

&lt;p&gt;That is why this comparison is even interesting. Claude Code and OpenCode are not just two CLIs. They are two different answers to the same question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;❓ How much of the agent stack should be final, and how much should developers be able to control?&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  So, which should you pick?
&lt;/h2&gt;

&lt;p&gt;There is no clever answer here.&lt;/p&gt;

&lt;p&gt;Pick &lt;strong&gt;Claude Code&lt;/strong&gt; if you want the cleanest Claude-native coding agent experience.&lt;/p&gt;

&lt;p&gt;Pick &lt;strong&gt;OpenCode&lt;/strong&gt; if you want more control.&lt;/p&gt;

&lt;p&gt;Personally, I still love Claude Code.&lt;/p&gt;

&lt;p&gt;I really do.&lt;/p&gt;

&lt;p&gt;Anthropic models are banger, especially for coding. The problem is that the limits have started to piss me off. When you are deep in a coding session and the limit hits, it completely breaks the flow.&lt;/p&gt;

&lt;p&gt;But there is some relief now.&lt;/p&gt;

&lt;p&gt;On May 6, Anthropic announced a new compute partnership with &lt;strong&gt;SpaceX&lt;/strong&gt; and doubled Claude Code’s 5-hour limits for &lt;strong&gt;Pro&lt;/strong&gt;, &lt;strong&gt;Max&lt;/strong&gt;, &lt;strong&gt;Team&lt;/strong&gt;, and &lt;strong&gt;seat-based Enterprise users&lt;/strong&gt;. They also removed peak-time limits for Pro and Max users.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqosm66pi1fq6pu4k76t0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqosm66pi1fq6pu4k76t0.png" alt="Claude Code increase in usage limit" width="799" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That makes Claude Code a lot easier to recommend again.&lt;/p&gt;

&lt;p&gt;I am personally still mostly stuck with Claude Code because the experience is just that good.&lt;/p&gt;

&lt;p&gt;But I use OpenCode when I want to try newer models like Kimi, OpenAI models, or local models. That is where OpenCode makes more sense to me. And by no means, it is to say that you can't use Anthropic models in OpenCode, you can, and that makes it even better.&lt;/p&gt;


&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Claude Code and OpenCode are both useful, but for different reasons.&lt;/p&gt;

&lt;p&gt;Claude Code is the one I’d pick if I just want the agent to work without thinking too much about setup. It feels cleaner, and better for getting into a repo quickly.&lt;/p&gt;

&lt;p&gt;OpenCode is more for when you want control. Different models, different providers, more ways to shape the workflow around how you actually code.&lt;/p&gt;

&lt;p&gt;I wouldn’t overthink it.&lt;/p&gt;

&lt;p&gt;If you hate setup and love Anthropic, use Claude Code.&lt;/p&gt;

&lt;p&gt;If you want more flexibility and less vendor lock-in, use OpenCode.&lt;/p&gt;

&lt;p&gt;That’s really the whole comparison.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4d09dor9q6cwva8zlg5o.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4d09dor9q6cwva8zlg5o.gif" alt="steve jobs meme" width="422" height="237"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__1127015"&gt;
    &lt;a href="/shricodev" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1127015%2F1c5e48a2-f602-4e7d-8312-3c0322d155c6.jpg" alt="shricodev image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/shricodev"&gt;Shrijal Acharya&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/shricodev"&gt;SDE • GOLD @Microsoft Student Ambassador • Prev Lead Collab and Dev-Team Lead @oppiaorg • Mail for collaboration&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>ai</category>
      <category>agents</category>
      <category>claude</category>
      <category>cli</category>
    </item>
    <item>
      <title>Kimi K2.6 vs. Claude Opus 4.7 in a Weird Game Coding Test ✅</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Tue, 05 May 2026 13:04:03 +0000</pubDate>
      <link>https://dev.to/composiodev/kimi-k26-vs-claude-opus-47-in-a-weird-game-coding-test-2ck3</link>
      <guid>https://dev.to/composiodev/kimi-k26-vs-claude-opus-47-in-a-weird-game-coding-test-2ck3</guid>
      <description>&lt;p&gt;Kimi K2.6 has been getting a lot of love lately, especially from devs who want a strong coding model without paying premium model prices every time they run a big prompt.&lt;/p&gt;

&lt;p&gt;So I wanted to see how good this model actually is. But this time, I wanted to compare it with something much heavier, the developers darling &lt;strong&gt;Claude Opus 4.7&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On paper, Claude Opus 4.7 and Kimi K2.6 are very different models.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9me1tc37trrs3wur8p6o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9me1tc37trrs3wur8p6o.png" alt="Kimi K2.6 and Opus 4.7 benchmarks" width="800" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One is a premium frontier model from Anthropic. The other is Moonshot AI's much cheaper open model for coding and agentic tasks.&lt;/p&gt;

&lt;p&gt;The pricing difference is pretty wild too. Claude Opus 4.7 costs $5/M input tokens and $25/M output tokens. Kimi K2.6 is listed at $0.95/M input tokens and $4/M output tokens, with cached input going even lower at $0.16/M tokens.&lt;/p&gt;

&lt;p&gt;That is a pretty big gap.&lt;/p&gt;

&lt;p&gt;So in this article, we'll see how the cheaper model, Kimi K2.6, does against Claude Opus 4.7.&lt;/p&gt;

&lt;p&gt;For the test, I gave both models the same coding task: build a small Minetest (similar to Minecraft) bounty board with a TypeScript backend, then extend it with Google Sheets logging through Composio.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4zpwug27ph8tc9d8dti6.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4zpwug27ph8tc9d8dti6.gif" alt="Angrybird interacting with a telescope" width="480" height="260"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;If you want the quick take, Claude Opus 4.7 clearly won this test, but it was painfully expensive.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Opus was better at the real task.&lt;/strong&gt; The local build was cleaner, and it was the only one that got the real Google Sheets integration working.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kimi did pretty well in Test 1.&lt;/strong&gt; It got the local bounty board working for way less money, but it needed more debugging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test 2 changed the whole comparison.&lt;/strong&gt; Opus was expensive, but it finished. Kimi just could not put it all together.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cost difference was wild though.&lt;/p&gt;

&lt;p&gt;For the first local bounty board test, Opus cost around &lt;strong&gt;$3.59&lt;/strong&gt;, while Kimi came in at around &lt;strong&gt;$0.39&lt;/strong&gt;. That is a huge gap. For the basic version, Kimi honestly did pretty well for the price.&lt;/p&gt;

&lt;p&gt;But once the task got a little more real, the gap became way more obvious.&lt;/p&gt;

&lt;p&gt;Opus got it working, even though it needed a little back and forth. The Google Sheets sync worked, and the project was modular enough that I could test the whole flow with two &lt;code&gt;curl&lt;/code&gt; requests without even opening the game.&lt;/p&gt;

&lt;p&gt;The painful part is that the Composio run alone cost &lt;strong&gt;$16&lt;/strong&gt; and took around &lt;strong&gt;28min 52sec API time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Kimi, on the other hand, burned &lt;strong&gt;135k+ tokens&lt;/strong&gt;, took around &lt;strong&gt;25 minutes&lt;/strong&gt;, cost around &lt;strong&gt;$5.03&lt;/strong&gt;, and still did not really get any closer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;👀 So yeah, Kimi K2.6 is a usable and interesting cheaper model. But in this test, it could not really come close to Opus 4.7 for real-world coding.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Evaluation
&lt;/h2&gt;

&lt;p&gt;I treated this like a real project, not a benchmark chart. Both models got the same prompts, and I compared the results based on whether it actually worked, how clean the code was, how much debugging it needed, how long it took, and how much it cost. That last one matters a lot here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Same tasks and prompts&lt;/strong&gt; for both models (Test 1: local-only bounty board, Test 2: real Composio Google Sheets sync).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same target architecture&lt;/strong&gt;: Minetest/Luanti Lua mod + TypeScript backend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same success criteria&lt;/strong&gt;: &lt;code&gt;/bounty&lt;/code&gt; flow works in-game, backend APIs behave correctly, and in Test 2 the completion is appended to Google Sheets via Composio.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What I measured
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Functional correctness (most important):&lt;/strong&gt; Did it work end-to-end with real verification?

&lt;ul&gt;
&lt;li&gt;Local run: could a player generate, progress, and complete bounties without breaking state?&lt;/li&gt;
&lt;li&gt;Backend: did &lt;code&gt;/health&lt;/code&gt;, &lt;code&gt;/api/bounty/generate&lt;/code&gt;, &lt;code&gt;/api/bounty/complete&lt;/code&gt;, and &lt;code&gt;/api/leaderboard&lt;/code&gt; return the expected shapes?&lt;/li&gt;
&lt;li&gt;Test 2: did the Google Sheets append succeed, and could I validate it from the API without needing to be in the game?&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Code quality and structure:&lt;/strong&gt; modularity, clarity, and whether the repo was easy to reason about and test.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Debug burden:&lt;/strong&gt; how many follow-ups were needed, how confusing the failure modes were, and whether issues were “real bugs” vs. “misconfiguration traps.”&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Time:&lt;/strong&gt; API time and wall time for each run.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Cost and token usage:&lt;/strong&gt; input, output, cache behavior, and total run cost.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Practical ergonomics:&lt;/strong&gt; whether I could validate quickly (for example, testing the full backend + Composio flow with &lt;code&gt;curl&lt;/code&gt;).&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  How I verified outcomes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test 1:&lt;/strong&gt; ran the backend locally, joined a local Minetest world, used &lt;code&gt;/bounty&lt;/code&gt;, and confirmed task tracking, rewards, and leaderboard persistence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test 2:&lt;/strong&gt; verified the end-to-end sync by generating and completing a bounty via the backend API, and confirming a successful Google Sheets append through Composio.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Scoring approach
&lt;/h3&gt;

&lt;p&gt;This was not a “unit test leaderboard” benchmark. It was a real build-and-ship check.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A model “wins” when the project works with minimal intervention.&lt;/li&gt;
&lt;li&gt;A model “loses” when it cannot reach a working state in reasonable time/cost, even if parts of the code look promising.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Coding Test
&lt;/h2&gt;

&lt;p&gt;For this test, I used the following CLI coding agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Opus 4.7:&lt;/strong&gt; Claude Code, Anthropic's terminal-based agentic coding tool&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvc5bfm8a9ump0ebp5f7o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvc5bfm8a9ump0ebp5f7o.png" alt="Claude Opus 4.7 model in CC" width="800" height="159"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kimi K2.6:&lt;/strong&gt; OpenCode via OpenRouter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxbg1utbhx6wenvgpszln.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxbg1utbhx6wenvgpszln.png" alt="Kimi K2.6 in OpenRouter" width="800" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ This is a practical coding test, so both models get the same prompt. I will compare time taken, code quality, token usage, cost, and all that stuff.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What are we building?
&lt;/h3&gt;

&lt;p&gt;For this test, I wanted something small enough to verify properly, but still weird enough to show how each model handles an unusual idea.&lt;/p&gt;

&lt;p&gt;So, we're building a simple Minetest/Luanti bounty board.&lt;/p&gt;

&lt;p&gt;A player can join a local world, run &lt;code&gt;/bounty&lt;/code&gt;, get a task like mining dirt or placing torches, and receive a reward after completing it.&lt;/p&gt;

&lt;p&gt;After that, the backend records the completion, logs it to Google Sheets through Composio.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I get it, the concept is a little unusual on purpose.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Test Prompts
&lt;/h3&gt;

&lt;p&gt;Both models received the same prompts for each test.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test 1 Prompt:&lt;/strong&gt; &lt;a href="https://gist.github.com/shricodev/a9fa4512142e74c72afc0e8b6b121384" rel="noopener noreferrer"&gt;Local Bounty Board Prompt&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test 2 Prompt:&lt;/strong&gt; &lt;a href="https://gist.github.com/shricodev/5e8f4803f1a9af152403e246b7bcd617" rel="noopener noreferrer"&gt;Real Composio Integration Prompt&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Test 1: Local Bounty Board
&lt;/h3&gt;

&lt;p&gt;This first test is the basic version of the idea.&lt;/p&gt;

&lt;p&gt;No external tools, no Composio. Just the game, the backend, and the local bounty flow working properly.&lt;/p&gt;

&lt;p&gt;The goal was simple. A player runs &lt;code&gt;/bounty&lt;/code&gt;, gets a task, completes it inside the game, and the backend tracks the progress without everything falling apart.&lt;/p&gt;

&lt;h3&gt;
  
  
  Claude Opus 4.7
&lt;/h3&gt;

&lt;p&gt;Claude Opus 4.7 handled the first test really well.&lt;/p&gt;

&lt;p&gt;The local bounty board worked end to end. It built the TypeScript backend, the Minetest/Luanti Lua mod, the command flow, progress tracking, rewards, and leaderboard persistence without needing a bunch of follow-up fixes.&lt;/p&gt;

&lt;p&gt;The file structure also felt nice, which I had specifically asked for in the prompt:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ldggdwrwcrk42jgswb0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1ldggdwrwcrk42jgswb0.png" alt="Claude Opus 4.7 created file structure" width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The backend was built with Express, Zod, and Vitest. It also handled the boring stuff properly, which honestly matters a lot here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;npm test&lt;/code&gt; passed with &lt;strong&gt;11/11 tests&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm run build&lt;/code&gt; passed cleanly&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/health&lt;/code&gt;, &lt;code&gt;/api/bounty/generate&lt;/code&gt;, &lt;code&gt;/api/bounty/complete&lt;/code&gt;, and &lt;code&gt;/api/leaderboard&lt;/code&gt; returned the right response shapes&lt;/li&gt;
&lt;li&gt;incomplete bounty completions returned a clean &lt;code&gt;400&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It created the Lua files cleanly, used &lt;code&gt;minetest.request_http_api()&lt;/code&gt;, handled &lt;code&gt;secure.http_mods&lt;/code&gt;, tracked digging and placing, stored player bounty state, and handled inventory rewards properly.&lt;/p&gt;

&lt;p&gt;The whole run took around &lt;strong&gt;12 minutes of API time&lt;/strong&gt;, with about &lt;strong&gt;23 minutes wall time&lt;/strong&gt;. That is a bit longer than a quick web app build test, but for this kind of cross-stack project, it felt fair.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frwsc4lkcx6f9o43eujev.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frwsc4lkcx6f9o43eujev.png" alt="Claude Opus 4.7 time taken for the build" width="800" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The cost came out to &lt;strong&gt;$3.59&lt;/strong&gt;, which is definitely not cheap. But to be fair, the output was actually useful. It added a lot of code, but most of it was real implementation, not random filler like &lt;code&gt;CONTRIBUTING.md&lt;/code&gt;, &lt;code&gt;INSTALLATION.md&lt;/code&gt;, and all those extra files models sometimes create for no reason.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgj4v5gc4wfmy9bov64qq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgj4v5gc4wfmy9bov64qq.png" alt="Claude Opus 4.7 cost for the build" width="800" height="126"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can find the code it generated here: &lt;a href="https://github.com/shricodev/opus-kimi-minetest-game-mod/tree/04fe23a6f6eb363ef7cfb6af9e0e91993568a718" rel="noopener noreferrer"&gt;Claude Opus 4.7 Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the demo:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/ZAB9fTdTV5Y"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; ~$3.59&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; 12min 3sec API time, 23min 53sec wall time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; +1,688 lines, 0 lines removed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token Usage:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Input:&lt;/strong&gt; 65&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output:&lt;/strong&gt; 54.8k&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache read:&lt;/strong&gt; 2.8M&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache write:&lt;/strong&gt; 129.8k&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick Verdict&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It worked end to end without much tweaking. I only had to configure &lt;code&gt;~/.minetest/minetest.conf&lt;/code&gt; and add this line:&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;secure.http_mods = bountyboard
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Pretty much everything else was smooth. Great quick MVP.&lt;/p&gt;

&lt;p&gt;I noticed one small issue: &lt;code&gt;mine_node&lt;/code&gt; bounties can be farmed by placing and then re-mining the same blocks, because vanilla Minetest does not track who placed a node.&lt;/p&gt;

&lt;p&gt;But that's fine. That is not really a code issue here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Kimi K2.6
&lt;/h3&gt;

&lt;p&gt;The core idea worked. Kimi created the TypeScript backend, the Minetest/Luanti mod, the bounty commands, task tracking, completion flow, rewards, and leaderboard logic.&lt;/p&gt;

&lt;p&gt;The backend side looked solid enough. It used Express, Zod, and Vitest, and the main routes were there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/health&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api/bounty/generate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api/bounty/complete&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/api/leaderboard&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also created the Lua mod files properly and handled the basic &lt;code&gt;/bounty&lt;/code&gt; flow inside Minetest. The code was not bad either. I just felt like it was not as clean or modular as what Opus 4.7 wrote.&lt;/p&gt;

&lt;p&gt;But there was one really irritating issue.&lt;/p&gt;

&lt;p&gt;Somehow, Kimi wrote the global Minetest config in &lt;code&gt;~/.minetest/minetest.conf&lt;/code&gt; with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;secure.http_mods&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;bountykimi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;But then it also created a world config and added a different mod name there.&lt;/p&gt;

&lt;p&gt;So when I loaded the world, Minetest used the world-level config. That basically overrode the global config behavior I was expecting. Because of that, the HTTP API was not enabled for the actual mod that was running.&lt;/p&gt;

&lt;p&gt;This took me more than half an hour to debug.&lt;/p&gt;

&lt;p&gt;And honestly, because I do not have much experience with Minetest config behavior, this was super annoying.&lt;/p&gt;

&lt;p&gt;The run itself was much cheaper and faster than Opus. Kimi used around &lt;strong&gt;52k context tokens&lt;/strong&gt;, took about &lt;strong&gt;9 minutes 27 seconds&lt;/strong&gt;, and cost around &lt;strong&gt;$0.39&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxi3s6ic25sy6620np9wf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxi3s6ic25sy6620np9wf.png" alt="Kimi K2.6 token usage and time for the build" width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That price difference is pretty wild. Opus cost around $3.59 for the first test, while Kimi came in under $0.40.&lt;/p&gt;

&lt;p&gt;You can find the code it generated here: &lt;a href="https://github.com/shricodev/opus-kimi-minetest-game-mod/tree/5c9033f77f2dff7792578b3fa126f7343643b5d1" rel="noopener noreferrer"&gt;Kimi K2.6 Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the demo:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/kpE4EbWdqu4"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; ~$0.39&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; ~9min 27sec&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; +4,671 lines, 0 lines removed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context Used:&lt;/strong&gt; 52,073 tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context Window Used:&lt;/strong&gt; 20%&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick Verdict&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The local bounty board idea worked, the code was usable, and the model clearly understood the Lua + TypeScript setup. But if you notice, Kimi wrote more than twice as much code as Opus 4.7.&lt;/p&gt;

&lt;p&gt;The main problem was the Minetest config mess. It added &lt;code&gt;secure.http_mods = bountykimi&lt;/code&gt; globally, but then created another world-level config with a different mod name, which made debugging way more painful.&lt;/p&gt;

&lt;p&gt;So yeah, Kimi passed the first test, but not as smoothly as Opus.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Test 2: Real Composio Integration
&lt;/h3&gt;

&lt;p&gt;Now this is where the actual test, and the fun, begins.&lt;/p&gt;

&lt;p&gt;The custom mod is ready, so now it is time to integrate Composio and give the game a quick agentic touch.&lt;/p&gt;

&lt;p&gt;The idea is simple. As players progress through the game, their bounty completions get logged into Google Sheets with Composio.&lt;/p&gt;
&lt;h3&gt;
  
  
  Claude Opus 4.7
&lt;/h3&gt;

&lt;p&gt;Claude Opus 4.7 did manage to add the real Composio integration, but this one was not as smooth as Test 1.&lt;/p&gt;

&lt;p&gt;The backend could sync bounty completions to Google Sheets. The nice thing is that I did not even need to open Minetest to test whether it was working. Because the project was structured cleanly, I could test the whole backend flow with just two &lt;code&gt;curl&lt;/code&gt; requests.&lt;/p&gt;

&lt;p&gt;First, generate a bounty:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8787/api/bounty/generate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"player":"singleplayer","availableTasks":["collect_item"]}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;tee&lt;/span&gt; /tmp/b.json | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then complete it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8787/api/bounty/complete &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-nc&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--argjson&lt;/span&gt; b &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq .bounty /tmp/b.json&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--arg&lt;/span&gt; ts &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%Y-%m-%dT%H:%M:%SZ&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="s1"&gt;'{player:"singleplayer", bounty:$b, progress:{current:$b.target.count, required:$b.target.count}, completedAt:$ts}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And if everything is configured correctly, the second response looks like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bounty completed."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"leaderboard"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"player"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"singleplayer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"points"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"completedBounties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sync"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"googleSheets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Google Sheets row appended."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This is one of the things I love about Opus the most. It usually creates a pretty modular setup. The game mod, backend logic, and external sync were separated well enough that I could test the Composio part directly from the API without needing to run around inside the game every time.&lt;/p&gt;

&lt;p&gt;It did run into a dev server issue where the &lt;code&gt;tsx&lt;/code&gt; command was parsing &lt;code&gt;watch&lt;/code&gt; incorrectly and treating it like the entry file.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3mdm9m29o5a7q6mq10oe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3mdm9m29o5a7q6mq10oe.png" alt="Claude Opus 4.7 build error" width="800" height="218"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After a bit of back and forth, it fixed the error. It eventually built a small runtime env loader and adjusted the config import so the backend could read the environment properly before the rest of the app booted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe4e3gju1m8fore381kvx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe4e3gju1m8fore381kvx.png" alt="Claude Opus 4.7 build error fix" width="800" height="203"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After that, the build worked and the Google Sheets sync started working.&lt;/p&gt;

&lt;p&gt;But that cost was painful. It literally cost me around &lt;strong&gt;$16&lt;/strong&gt;. Like, actually :(. If you are not watching usage, this thing can make you broke real fast.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffe8rgfuafxakh1oucp9j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffe8rgfuafxakh1oucp9j.png" alt="Claude Opus 4.7 time and cost" width="800" height="94"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It took &lt;strong&gt;28min 52sec API time&lt;/strong&gt;, and about &lt;strong&gt;1hr 17min wall time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Apart from that, the code did work. But it cost way more than I expected for one run.&lt;/p&gt;

&lt;p&gt;You can find the code it generated here: &lt;a href="https://github.com/shricodev/opus-kimi-minetest-game-mod/tree/0c9c0fa3641796fd97890530c4116e05912b04f4" rel="noopener noreferrer"&gt;Claude Opus 4.7 Code - Composio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the demo:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/HWUmlysy18Q"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; $16.03&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; 28min 52sec API time, 1hr 17min 40sec wall time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; +1,848 lines, 507 lines removed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token Usage:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Input:&lt;/strong&gt; 100.2k with Claude Haiku 4.5, plus Opus usage shown in the session&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output:&lt;/strong&gt; 3.2k with Claude Haiku 4.5, 123.3k with Claude Opus 4.7&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache read:&lt;/strong&gt; 22.3M&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache write:&lt;/strong&gt; 269.3k&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick Verdict:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Claude Opus 4.7 got the real Composio integration working, especially the Google Sheets logging.&lt;/p&gt;

&lt;p&gt;How cool is that? You add a custom agentic feature inside a game. A literal public game.&lt;/p&gt;

&lt;p&gt;So yes, it worked. But $16 for this one run hurt.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Kimi K2.6
&lt;/h3&gt;

&lt;p&gt;Kimi K2.6 did not do well on this test. It was pretty much busted.&lt;/p&gt;

&lt;p&gt;From the start, it ran into a bunch of errors. The dev server broke, tests were failing, and even after a little handholding, it only managed to fix part of the test situation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj3viy07x6cen4f8a6lxh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj3viy07x6cen4f8a6lxh.png" alt="Kimi K2.6 error in the build" width="800" height="193"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It eventually got past some of those failures, but the bigger problem was the actual Composio implementation. It did not seem fully sure how to wire the integration cleanly into the existing backend.&lt;/p&gt;

&lt;p&gt;I had to stop and help again and again, but it still could not make meaningful progress with the build.&lt;/p&gt;

&lt;p&gt;After spending more than &lt;strong&gt;25 minutes&lt;/strong&gt; and burning over &lt;strong&gt;130k tokens&lt;/strong&gt;, there was still no real progress. At that point, I had to stop the run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flpehtobp7tgmpolmyhr4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flpehtobp7tgmpolmyhr4.png" alt="Kimi K2.6 build error" width="800" height="121"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why on earth is it reading a &lt;code&gt;version.txt&lt;/code&gt; file?&lt;/p&gt;

&lt;p&gt;So yeah, I am calling this one a fail for Kimi K2.6.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost:&lt;/strong&gt; ~$5.03&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duration:&lt;/strong&gt; ~25min&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token Usage:&lt;/strong&gt; 135,109+&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Quick Verdict:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Kimi K2.6 failed this test.&lt;/p&gt;

&lt;p&gt;It got stuck around tests, build issues, and the real Composio implementation. Even with manual help, it could not get the integration into a clean working state.&lt;/p&gt;

&lt;p&gt;For the local bounty board, Kimi was surprisingly usable. But once the task moved into real external integration, it struggled a lot more.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Both Claude Opus 4.7 and Kimi K2.6 were pretty solid in this test, at least for the local version.&lt;/p&gt;

&lt;p&gt;The task was not that simple either. It had Lua, TypeScript, SDK docs, backend logic, game commands, and the full flow had to work end to end.&lt;/p&gt;

&lt;p&gt;Plus, the idea itself is not that common. Building an AI agent concept inside a custom game mod is not easy, and it is definitely not a one-shot thing, so props to both models.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu9zolwkatuaoztum6991.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu9zolwkatuaoztum6991.gif" alt="Clap" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Opus 4.7 did better overall. The code was cleaner, and as usual, Anthropic models are pretty good at that.&lt;/p&gt;

&lt;p&gt;The only thing I hate with Anthropic models is the session limit.&lt;/p&gt;

&lt;p&gt;I absolutely hate how little session usage you get. Opus 4.7 especially just eats through it completely in like 3 to 5 prompts.&lt;/p&gt;

&lt;p&gt;Kimi K2.6 is an interesting model. Open models have not always been the best in my experience with real-world projects, but with every new model, my expectations rise a little.&lt;/p&gt;

&lt;p&gt;Let's see where Kimi K2.6 goes from here.&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__1127015"&gt;
    &lt;a href="/shricodev" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1127015%2F1c5e48a2-f602-4e7d-8312-3c0322d155c6.jpg" alt="shricodev image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/shricodev"&gt;Shrijal Acharya&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/shricodev"&gt;SDE • GOLD @Microsoft Student Ambassador • Prev Lead Collab and Dev-Team Lead @oppiaorg • Mail for collaboration&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>ai</category>
      <category>programming</category>
      <category>gamedev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>My speaker broke, so I built a LAN speaker</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Tue, 28 Apr 2026 14:29:00 +0000</pubDate>
      <link>https://dev.to/shricodev/my-speaker-broke-so-i-built-a-lan-speaker-19nc</link>
      <guid>https://dev.to/shricodev/my-speaker-broke-so-i-built-a-lan-speaker-19nc</guid>
      <description>&lt;h2&gt;
  
  
  What's happening?
&lt;/h2&gt;

&lt;p&gt;This was around a year back when I started the project, after a small speaker that I had broke out of nowhere. Won't connect.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd5hyoru6a4xegzrkukgq.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd5hyoru6a4xegzrkukgq.jpeg" alt="Broken speaker" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I used it to listen to music every night before going to sleep (not sure if anybody else does the same, but it's one of my only fixed routines since childhood :D).&lt;/p&gt;




&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;Okay, so what's the idea?&lt;/p&gt;

&lt;p&gt;I had a thought. Why not build something resembling a speaker? Although I had never worked on a project that required working with audio and all that, I knew what I'd use to implement something like that, which was WebSocket (I'll explain why I chose it in a moment).&lt;/p&gt;

&lt;p&gt;And I did start on it.&lt;/p&gt;

&lt;p&gt;The irony is that I had just finished &lt;a href="https://go.dev/tour/" rel="noopener noreferrer"&gt;Tour of Go&lt;/a&gt;, &lt;a href="https://lets-go.alexedwards.net/" rel="noopener noreferrer"&gt;Let's Go&lt;/a&gt;, &lt;a href="https://lets-go-further.alexedwards.net/" rel="noopener noreferrer"&gt;Let's Go Further&lt;/a&gt;, and had just started on 100 Go Mistakes and How to Avoid Them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fadv8xgc17bm3oficmzps.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fadv8xgc17bm3oficmzps.png" alt="LinkedIn post on golang books" width="741" height="730"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So... I had to do this in Golang.&lt;/p&gt;

&lt;p&gt;I had quite a good idea of how to work with Golang, so it had to be done with it, also because I wanted to get away from the JavaScript ecosystem (the same Node.js, React, Next.js, yada yada was just too much).&lt;/p&gt;




&lt;h2&gt;
  
  
  Why WebSocket
&lt;/h2&gt;

&lt;p&gt;So, let's come to the plan. Why WebSocket?&lt;/p&gt;

&lt;p&gt;The main reason is that when I had the idea, it was WebSocket that came to my mind, and it's what I thought of before even starting to code the project.&lt;/p&gt;

&lt;p&gt;And WebSocket kind of makes sense as well. This was my thought process.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Lets you connect multiple connections from one device? &lt;strong&gt;Yes&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Is synchronous enough when streaming over multiple devices in LAN? &lt;strong&gt;Yes&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Something I've worked with a lot? &lt;strong&gt;Totally Yes&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's pretty much the reason I chose WebSocket. It might not be the best option, but it's what I had and something I was confident with when I started.&lt;/p&gt;

&lt;p&gt;One more thing I'll add here: since I'm streaming audio frames sequentially (PCM chunks, one after another), I actually need them to arrive in order. &lt;/p&gt;

&lt;p&gt;WebSocket runs over TCP, so that's handled for me. I don't have to think about it. That alone made it feel like the right call.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not WebRTC or UDP?
&lt;/h3&gt;

&lt;p&gt;Let me tell you something pretty frankly: I never even knew there was something like WebRTC. I came across this term while building the project halfway through it.&lt;/p&gt;

&lt;p&gt;So, why not UDP?&lt;/p&gt;

&lt;p&gt;Okay, so UDP is connectionless, which means there's no guarantee that every packet arrives, and more importantly, no guarantee they arrive in order. For something like video calls or games, that's totally fine. You drop a frame, move on, nobody cares.&lt;/p&gt;

&lt;p&gt;But for audio? If a chunk goes missing or arrives out of order, you either get silence or a glitch.&lt;/p&gt;

&lt;p&gt;And since I'm streaming raw PCM (basically just a stream of bytes that represent sound), every single chunk matters. You can't just skip them.&lt;/p&gt;

&lt;p&gt;So UDP was out as well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpllsruo4m6s36xvej2cs.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpllsruo4m6s36xvej2cs.gif" alt="Out GIF" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And WebRTC? Also, WebRTC is mostly built around browser-to-browser, peer-to-peer stuff. My setup is a server broadcasting to multiple clients over LAN, which isn't really what WebRTC is designed for. So even if I knew about it from day one, I'm not sure it would've been the right fit anyway.&lt;/p&gt;

&lt;p&gt;WebSocket was fine. It worked. And sometimes that's enough. I didn't want to overengineer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;👀 I learned about all these new terms like &lt;strong&gt;PCM&lt;/strong&gt;, &lt;strong&gt;WebRTC&lt;/strong&gt;, and all that stuff during the build. So, I might say something wrong. I'm not really that familiar with them. So, just hit me in the comments if so.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;Okay, so the high level is pretty simple.&lt;/p&gt;

&lt;p&gt;There's a server and there are clients. The server is where the music is stored, and the clients are the devices that play it (could be the same device).&lt;/p&gt;

&lt;p&gt;Here's what actually happens when you press play:&lt;/p&gt;

&lt;p&gt;The server takes the MP3 file, decodes it into raw PCM (basically just bytes of sound data), and starts broadcasting those bytes over WebSocket to every connected client. No client-side decoding. The server does all of that.&lt;/p&gt;

&lt;p&gt;Here's a high level architecture:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foi47bhzovugarytvtmyw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foi47bhzovugarytvtmyw.png" alt="Gophercast architecture" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The tricky part is sync. If you just start streaming, each client will start playing at slightly different times, and it'll sound like a little echo.&lt;/p&gt;

&lt;p&gt;So what I did is, before playback starts, the server sends a timestamp to all clients saying, "Start playing at exactly this moment in time." Every client gets that timestamp, buffers the audio frames, and waits. When the clock hits that time, everyone starts together.&lt;/p&gt;

&lt;p&gt;It works because on a LAN with &lt;strong&gt;NTP&lt;/strong&gt;, all the device clocks are usually within a millisecond or two of each other. Close enough that you can't tell the difference (much).&lt;/p&gt;

&lt;p&gt;That's the whole thing, honestly.&lt;/p&gt;

&lt;p&gt;Server decodes -&amp;gt; broadcasts -&amp;gt; clients sync -&amp;gt; play&lt;/p&gt;




&lt;h2&gt;
  
  
  What I still don't know
&lt;/h2&gt;

&lt;p&gt;Okay, so during the build, I ran into something I didn't fully understand.&lt;/p&gt;

&lt;p&gt;Turns out your speaker doesn't play audio the exact moment you write data to it. There's some time it spends sitting in a system buffer before it actually comes out. And that delay varies by OS and audio system. On Linux, it's something around 50ms, I feel.&lt;/p&gt;

&lt;p&gt;Honestly, I had no idea there was something like this that you need to account for. This thing was completely debugged by &lt;strong&gt;GPT-5.4&lt;/strong&gt;. There's a hardcoded 50ms constant in the code that is counted when clients actually start playing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa7zha09z3aitxrfz1i05.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa7zha09z3aitxrfz1i05.gif" alt="I don't know gif" width="480" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's also something in the code that keeps checking, roughly every second, whether the audio playing on your device is slightly ahead or behind.&lt;/p&gt;

&lt;p&gt;If it is, it quietly adds or removes a tiny bit of audio to bring it back in line. So small you'd never hear it.&lt;/p&gt;

&lt;p&gt;Both of these kind of work. I tested it. I just couldn't tell you exactly why the numbers are what they are.&lt;/p&gt;




&lt;h2&gt;
  
  
  How I usually run it
&lt;/h2&gt;

&lt;p&gt;Since my plan is to use my whole laptop as a "speaker", I usually have the server and client on the same system (my personal laptop).&lt;/p&gt;

&lt;p&gt;And I usually have it like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start the server:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gophercast serve &lt;span class="c"&gt;# now follow the TUI setup....&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Connect clients (each in a tmux split). Usually, I have around 2 clients when running on the same machine. More than 2 kind of distorts the audio quality.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gophercast play &lt;span class="nt"&gt;--host&lt;/span&gt; &amp;lt;ip_from_serve&amp;gt; &lt;span class="nt"&gt;--port&lt;/span&gt; 8080 &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"client 1"&lt;/span&gt;
gophercast play &lt;span class="nt"&gt;--host&lt;/span&gt; &amp;lt;ip_from_serve&amp;gt; &lt;span class="nt"&gt;--port&lt;/span&gt; 8080 &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"client 2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The steps are the same when connecting from multiple machines. Just make sure that all of them are connected to the same LAN.&lt;/p&gt;

&lt;p&gt;Here's a quick demo on a single machine, working as the speaker.&lt;br&gt;
(Ignore the audio quality)&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/cStAJMDMEKw"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;


&lt;h2&gt;
  
  
  My take
&lt;/h2&gt;

&lt;p&gt;I'm pretty happy with how this worked out.&lt;/p&gt;

&lt;p&gt;This was my very first time working with Charm's &lt;a href="https://github.com/charmbracelet/bubbletea" rel="noopener noreferrer"&gt;Bubbletea&lt;/a&gt; and the whole audio stuff.&lt;/p&gt;

&lt;p&gt;I started this project just because I wanted to DIY. It was just the perfect time: my speaker broke, and I had somehow finished learning basic Go programming.&lt;/p&gt;

&lt;p&gt;There's probably still a ton of bugs. I've tried to test most of it. I just picked the tool I knew and figured it out as I went.&lt;/p&gt;

&lt;p&gt;It plays music across multiple devices in sync, or the same one if you connect through multiple terminals. My laptop, another computer, whatever's connected, all playing together.&lt;/p&gt;

&lt;p&gt;That's what I wanted. That's what it does.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftk8fu13vlk1izssz5n7l.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftk8fu13vlk1izssz5n7l.gif" alt="go on GIF" width="480" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(I haven't tested how well it works on Mac, so I can't tell there. Also, I'm not sure Windows will work either due to oto's limitation and our hardcoded 50ms delay.)&lt;/p&gt;

&lt;p&gt;You can find the repo here: &lt;a href="https://github.com/shricodev/gophercast.git" rel="noopener noreferrer"&gt;shricodev/gophercast&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🚀 Want to build such cool stuff? You learn hands on here: &lt;a href="https://app.codecrafters.io/join?via=shricodev" rel="noopener noreferrer"&gt;CodeCrafters&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;div class="ltag__user ltag__user__id__1127015"&gt;
    &lt;a href="/shricodev" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1127015%2F1c5e48a2-f602-4e7d-8312-3c0322d155c6.jpg" alt="shricodev image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/shricodev"&gt;Shrijal Acharya&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/shricodev"&gt;SDE • GOLD @Microsoft Student Ambassador • Prev Lead Collab and Dev-Team Lead @oppiaorg • Mail for collaboration&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>go</category>
      <category>opensource</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Automate Your Slack Workspace with OpenClaw and Composio 🚀</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Thu, 16 Apr 2026 15:19:55 +0000</pubDate>
      <link>https://dev.to/composiodev/how-to-automate-your-slack-workspace-with-openclaw-and-composio-3lhc</link>
      <guid>https://dev.to/composiodev/how-to-automate-your-slack-workspace-with-openclaw-and-composio-3lhc</guid>
      <description>&lt;p&gt;Your team already lives in Slack. Code reviews, project updates and what not, it all happens there.&lt;/p&gt;

&lt;p&gt;But the moment you need to file a GitHub issue, check a Linear ticket, or send a follow-up email, you leave Slack, do the thing, and come back. That context switch adds up.&lt;/p&gt;

&lt;p&gt;What if your Slack workspace had an assistant that could do all of that for you, right in the thread. That too an isolated OpenClaw instance per user with admin control? 🤯&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F34fzwp4wnc4g66m15gp2.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F34fzwp4wnc4g66m15gp2.gif" alt="shocked gif" width="480" height="252"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article, you'll learn how to automate an entire Slack team workspace that connects to your tools, takes actions, without you ever leaving Slack.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Covered
&lt;/h2&gt;

&lt;p&gt;To quickly summarize what we’ll cover in this blog post, here’s what we’ll go through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The idea behind building a Slack bot around OpenClaw.&lt;/li&gt;
&lt;li&gt;How Composio lets each user connect their own tools.&lt;/li&gt;
&lt;li&gt;How OpenClaw powers replies and tool usage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are a few things you'll understand, but there's so many others you'll learn along the way.&lt;/p&gt;

&lt;p&gt;So, if you want to build a Slack-first (though not limited to) AI with personal tool access for each user, this will give you a solid starting point.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we're building
&lt;/h2&gt;

&lt;p&gt;We're building a Slack bot that brings OpenClaw into Slack.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 Not necessarily only for Slack, you can use pretty much the same setup for something like Discord with their SDK or your custom app. The idea remains the same.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Overall the idea is to use OpenClaw and give every user in a workspace their own single instance of it which powers the AI assistant.&lt;/p&gt;

&lt;p&gt;That way, things are isolated per user and the admin can control/limit the toolkits (GitHub, Linear, etc.) the users get access to.&lt;/p&gt;

&lt;p&gt;Each user can connect their own tools with Composio, so the bot can chat, and take actions using the tools they’ve authorized.&lt;/p&gt;

&lt;p&gt;Here's a quick architecture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fszdytuuqr202h16l1vvs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fszdytuuqr202h16l1vvs.png" alt="bot architecture" width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Slack and how to create a Slack App?
&lt;/h2&gt;

&lt;p&gt;No big reason to choose Slack, only because it supports slash commands, and it's mostly where people already work.&lt;/p&gt;

&lt;p&gt;For this, we first need to have a Slack app, if you don't already have one, follow the &lt;a href="https://docs.slack.dev/quickstart/" rel="noopener noreferrer"&gt;quickstart guide&lt;/a&gt; to create one.&lt;/p&gt;

&lt;p&gt;Once your app is created, enable Socket Mode so the bot can receive events without exposing a public webhook URL.&lt;/p&gt;

&lt;p&gt;Then add at least these &lt;strong&gt;Bot Token Scopes&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;app_mentions:read&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chat:write&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;commands&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;im:history&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;im:read&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;users:read&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Subscribe to these &lt;strong&gt;Bot Events&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;app_mention&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;message.im&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And create these &lt;strong&gt;Slash commands&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/connect&lt;/code&gt;: Use it something like &lt;code&gt;/connect &amp;lt;toolkit&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/connections&lt;/code&gt;: User lists active connections&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/help&lt;/code&gt;: Shows usage summary&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/assign&lt;/code&gt;: Admin assigns an OpenClaw instance to a user&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/add-mcp-config&lt;/code&gt;: Admin registers an MCP Config from platform.composio.dev&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/add-auth-config&lt;/code&gt;: Admin links a toolkit to its Composio auth config&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/list-mcp-configs&lt;/code&gt;: Admin lists all registered MCP Configs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally copy these values to your &lt;code&gt;.env&lt;/code&gt; file, which you can find in the app settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;SLACK_BOT_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xoxb-...
&lt;span class="nv"&gt;SLACK_APP_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xapp-...
&lt;span class="nv"&gt;SLACK_SIGNING_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Your &lt;code&gt;SLACK_BOT_TOKEN&lt;/code&gt; is the bot token itself, &lt;code&gt;SLACK_APP_TOKEN&lt;/code&gt; is the Socket Mode app level token and &lt;code&gt;SLACK_SIGNING_SECRET&lt;/code&gt; is used by Slack to verify requests.&lt;/p&gt;


&lt;h2&gt;
  
  
  How to Set Up the Project
&lt;/h2&gt;

&lt;p&gt;It's fairly simple to get this project up and running. Follow these steps:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/shricodev/saas-openclaw-slackbot.git
&lt;span class="nb"&gt;cd &lt;/span&gt;saas-openclaw-slackbot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Next, you install the dependencies:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then set up the environment variables and run the development server:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Slack&lt;/span&gt;
&lt;span class="nv"&gt;SLACK_BOT_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xoxb-...
&lt;span class="nv"&gt;SLACK_APP_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;xapp-...
&lt;span class="nv"&gt;SLACK_SIGNING_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...

&lt;span class="c"&gt;# Database&lt;/span&gt;
&lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...

&lt;span class="c"&gt;# Composio api key (ak...) from https://platform.composio.dev&lt;/span&gt;
&lt;span class="nv"&gt;COMPOSIO_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ak_..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;To get the Composio API key:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log in at &lt;a href="https://platform.composio.dev/" rel="noopener noreferrer"&gt;platform.composio.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Copy your API key (&lt;code&gt;ak_..&lt;/code&gt;) from the Composio dashboard settings, then set it:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjacffrcue8r6sct5ddev.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjacffrcue8r6sct5ddev.png" alt="composio api key" width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Configure Composio Dedicated MCP Server
&lt;/h2&gt;

&lt;p&gt;In this section, we'll go through the process of creating a dedicated MCP server in Composio for each user.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;First, head over to &lt;a href="https://platform.composio.dev" rel="noopener noreferrer"&gt;platform.composio.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Under the MCP Configs tab, create a &lt;strong&gt;Dedicated MCP Server&lt;/strong&gt;. This lets you create MCP servers with specific apps and tools, which is exactly what we want.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo7qlbr4p0vmmtyxkdf3t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo7qlbr4p0vmmtyxkdf3t.png" alt="composio dedicated MCP server creation" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select all the toolkits you plan to assign for the user and create the MCP server.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo218r8j44g593yqlrvkk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo218r8j44g593yqlrvkk.png" alt="composio toolkits" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;For the External User ID, use the user's Slack user ID. To get someone's Slack user ID, head over to their profile, click the three dots, and select &lt;strong&gt;Copy Member ID&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F25u16lvsh5h94z9d4em2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F25u16lvsh5h94z9d4em2.png" alt="slack user id" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Use that as the External User ID.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs98p5tahcxw9ikfogngu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs98p5tahcxw9ikfogngu.png" alt="external user id naming composio" width="800" height="192"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Keep note of the &lt;strong&gt;MCP config name&lt;/strong&gt; and &lt;strong&gt;MCP config ID&lt;/strong&gt;. You will need both when configuring the bot in Slack.&lt;/p&gt;

&lt;p&gt;Upon successful creation, you'll find the URL:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv5kwmuvboz09uitypcw3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv5kwmuvboz09uitypcw3.png" alt="composio dedicated mcp server url" width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can copy this URL directly and add it to the OpenClaw config, which we’ll cover later in the Configure OpenClaw with Composio section. Alternatively, the bot can fetch it for you after you run the &lt;code&gt;/assign&lt;/code&gt; slash command, which we’ll configure later.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You will also need the auth config ID tied to the tools you selected. In the MCP server, head over to the &lt;strong&gt;Manage Config&lt;/strong&gt; tab and click &lt;strong&gt;Manage Auth Config&lt;/strong&gt;. The auth config ID is listed on that page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxmo9o16fcq5tmfezu9ua.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxmo9o16fcq5tmfezu9ua.png" alt="composio auth config" width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Keep note of this as well. You will need it when running &lt;code&gt;/add-auth-config&lt;/code&gt; in Slack.&lt;/p&gt;


&lt;h2&gt;
  
  
  Core Components in the Application
&lt;/h2&gt;

&lt;p&gt;We're not going to code everything from scratch as that'd be too long and impractical. Let's go over some of the core components in the project.&lt;/p&gt;

&lt;p&gt;Before we start with the project core components, here's the project tech stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://slack.dev/bolt-js/" rel="noopener noreferrer"&gt;Slack Bolt&lt;/a&gt;&lt;/strong&gt; - Official Slack bot framework. We use it with Socket Mode, which connects to Slack over a WebSocket without needing a public HTTP endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://openclaw.ai/" rel="noopener noreferrer"&gt;OpenClaw&lt;/a&gt;&lt;/strong&gt; - The agent layer. Exposes an OpenAI-compatible API but acts as a full agentic gateway that plans, calls tools, and reasons over results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://composio.dev/" rel="noopener noreferrer"&gt;Composio&lt;/a&gt;&lt;/strong&gt; - The core of the project. Manages OAuth connections to external apps like GitHub, Linear, and Gmail, and exposes them to the agent via MCP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; - Obvious choice over JavaScript as we get type safe code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL + Prisma&lt;/strong&gt; - Handles user records, connection status, and per-thread conversation history.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Bootstrapping the Bot
&lt;/h3&gt;

&lt;p&gt;This is where everything starts. We initialize the Slack Bolt app with Socket Mode, register all handlers, and start the server.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 👇 app.ts&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SLACK_BOT_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;appToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SLACK_APP_TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;signingSecret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SLACK_SIGNING_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;socketMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;registerMessageHandlers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;registerCommandHandlers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Bot is running on port &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (socket mode)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Instead of exposing a public HTTP endpoint for Slack to POST events to, Socket Mode opens a WebSocket connection. This means you can run the bot anywhere could be your local machine, a private server without a public URL.&lt;/p&gt;

&lt;p&gt;If you've worked with bots before, this should be pretty straight-forward to understand. 👀&lt;/p&gt;
&lt;h3&gt;
  
  
  Handling User Messages
&lt;/h3&gt;

&lt;p&gt;This is the brain of the bot. It handles both direct messages and &lt;code&gt;@mentions&lt;/code&gt; in channels.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 👇 message.handler.ts&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleUserMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;threadTs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;saveMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;slackUserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;slackTeamId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;threadTs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getThreadHistory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;slackUserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;slackTeamId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;threadTs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;priorHistory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;thinkingMsg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;thread_ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;threadTs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;_Thinking..._&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;openclawConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gatewayUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;openclawConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gatewayToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;priorHistory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;sessionKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;saveMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;slackUserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;slackTeamId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;threadTs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;channelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;thinkingMsg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;There's a few things you might notice right up:&lt;/p&gt;

&lt;p&gt;First, we store the user's message to the database before sending it to OpenClaw. But why? This way if the request fails, the history isn't broken. Similar to storing chat history in localhost when creating web chat applications.&lt;/p&gt;

&lt;p&gt;I do not know if there’s a better way to handle this, but right now we just show a &lt;code&gt;Thinking...&lt;/code&gt; message while the AI is generating the full response, and then replace it once the final output is ready.&lt;/p&gt;

&lt;p&gt;A little hacky, maybe, but it gets the job done. There are probably better ways to handle this, like streaming the response, but for now the old-school approach works. 😋&lt;/p&gt;
&lt;h3&gt;
  
  
  Slash Commands
&lt;/h3&gt;

&lt;p&gt;The bot exposes seven slash commands split into two groups: user-facing (&lt;code&gt;/connect&lt;/code&gt;, &lt;code&gt;/connections&lt;/code&gt;, &lt;code&gt;/help&lt;/code&gt;) and admin-only (&lt;code&gt;/assign&lt;/code&gt;, &lt;code&gt;/add-mcp-config&lt;/code&gt;, &lt;code&gt;/add-auth-config&lt;/code&gt;, &lt;code&gt;/list-mcp-configs&lt;/code&gt;).&lt;/p&gt;
&lt;h4&gt;
  
  
  /connect
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;/connect &amp;lt;toolkit&amp;gt;&lt;/code&gt; starts an OAuth flow for a tool like GitHub or Gmail. But unlike the previous version where any user could connect any toolkit, now the bot checks three things before starting a connection:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does this user have an MCP Config assigned?&lt;/li&gt;
&lt;li&gt;Is the requested toolkit in that config?&lt;/li&gt;
&lt;li&gt;Is there an auth config registered for this toolkit?
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 👇 command.handler.ts&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/connect&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;respond&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toolkitSlug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getComposioApiKey&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assignment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mcpConfigAssignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;slackUserId_slackTeamId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;slackUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;slackTeamId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;team_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;mcpConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;assignment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;response_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ephemeral&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You have not been assigned an MCP Config. Ask your admin to run `/assign`.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;assignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mcpConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolkitSlugs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolkitSlug&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;response_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ephemeral&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;This toolkit is not available in your assigned config. &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Your admin controls which toolkits you can access.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toolkitAuth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mcpToolkitAuth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;slackTeamId_toolkitSlug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;slackTeamId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;team_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;toolkitSlug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// check if already connected&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;connectedToolkits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getConnectedToolkits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;connectedToolkits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toolkitSlug&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;response_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ephemeral&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`You're already connected to *&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;toolkitSlug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;*.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redirectUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;initiateConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;toolkitAuth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authConfigId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;response_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ephemeral&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Click here to connect *&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;toolkitSlug&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;*: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;redirectUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The response is only visible to the user who ran the command. That's intentional as OAuth URLs are personal and shouldn't be visible to the whole channel.&lt;/p&gt;
&lt;h4&gt;
  
  
  /assign
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;/assign&lt;/code&gt; is admin-only and lets admins assign a specific OpenClaw gateway to a user. It opens a Slack modal to collect the gateway URL, token and MCP config server ID.&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 👇 command.service.ts&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assign&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_id&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isAdmin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;is_admin&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;userInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;is_owner&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isAdmin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;respond&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;response_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ephemeral&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Only workspace admins can assign OpenClaw instances.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;views&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;trigger_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trigger_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;view&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;assignInstanceModal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// includes gateway URL, token, and MCP Config ID fields&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The user's MCP URL looks something like this: &lt;code&gt;https://backend.composio.dev/v3/mcp/aaa-111/mcp?user_id=&amp;lt;slack_user_id&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It's the key because it's what connects the user's OpenClaw instance to the toolkits the admin selected for them.&lt;/p&gt;
&lt;h4&gt;
  
  
  /add-mcp-config and /add-auth-config
&lt;/h4&gt;

&lt;p&gt;These two admin commands register the Composio resources in the bot's database. Both open modals.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/add-mcp-config&lt;/code&gt; registers an MCP Config by name and server ID:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// On modal submit:&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mcpConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;slackTeamId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;teamId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;composioServerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// the UUID from the MCP URL&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// e.g. "Engineering"&lt;/span&gt;
    &lt;span class="nx"&gt;toolkitSlugs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// e.g. ["github", "linear"]&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;/add-auth-config&lt;/code&gt; links a toolkit slug to its Composio auth config ID. This is what &lt;code&gt;/connect&lt;/code&gt; uses to know which auth config to pass when initiating a connection:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// On modal submit:&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mcpToolkitAuth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;slackTeamId_toolkitSlug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slackTeamId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;teamId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toolkitSlug&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slackTeamId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;teamId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;toolkitSlug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authConfigId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;authConfigId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  /list-mcp-configs
&lt;/h4&gt;

&lt;p&gt;A simple admin command that lists all registered MCP Configs for the workspace:&lt;/p&gt;

&lt;p&gt;"Engineering" - server:  - toolkits: github, linear&lt;br&gt;
"Sales" - server:  - toolkits: gmail, notion&lt;/p&gt;
&lt;h3&gt;
  
  
  Sending Requests to OpenClaw
&lt;/h3&gt;

&lt;p&gt;Up until this point we were working on the Slack side and a bit of Composio setup, but how do we actually send these messages to OpenClaw?&lt;/p&gt;

&lt;p&gt;OpenClaw exposes an OpenAI-compatible &lt;code&gt;/v1/chat/completions&lt;/code&gt; endpoint. Our service code wraps that with a proper system prompt, conversation history, and error handling. Nothing super unknown to most of you.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 👇 openclaw.service.ts&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;gatewayUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;gatewayToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;userMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;sessionKey&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;OpenClawResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChatMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You are a helpful assistant in a Slack workspace. &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You have access to the user's connected tools (GitHub, Linear, Gmail, etc.) through Composio. &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The user's tools are already connected. Do not ask them to connect or authenticate. &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Use the available Composio tools directly to answer questions. &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Be concise. Format responses for Slack (use mrkdwn syntax).&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userMessage&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sendToOpenClaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gatewayUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gatewayToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sessionKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We also wrap the raw fetch in a custom &lt;code&gt;OpenClawError&lt;/code&gt; class with error codes for timeouts, auth failures, and gateway errors. Preferred thing you do in a real-world codebase.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Prefer something built-in like &lt;code&gt;fetch&lt;/code&gt; over third-party tool like &lt;code&gt;axios&lt;/code&gt;. Especially now after the recent compromise of &lt;code&gt;axios&lt;/code&gt; which is used by hundreds and thousands of applications.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Connecting tools with Composio
&lt;/h3&gt;

&lt;p&gt;You might be familiar working with Composio over the SDK &lt;code&gt;@composio/core&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But with Composio, you can also directly talk to it's REST API. We now talk to the Composio REST API on &lt;code&gt;backend.composio.dev&lt;/code&gt; using an API key from &lt;code&gt;platform.composio.dev&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There are only three functions in the service, and each one does exactly what the name suggests 🤌&lt;/p&gt;
&lt;h4&gt;
  
  
  Check what a user has connected:
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 👇 composio.service.ts&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getConnectedToolkits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;slackUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;COMPOSIO_API_BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/connected_accounts?user_id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slackUserId&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-api-key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ACTIVE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolkit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  Initiate a new connection:
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 👇 composio.service.ts&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;initiateConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;authConfigId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;slackUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;COMPOSIO_API_BASE&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/connected_accounts`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-api-key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;auth_config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;authConfigId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;slackUserId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;redirect_url&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  Build the per-user MCP URL:
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 👇 composio.service.ts&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getMcpUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;composioServerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;slackUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`https://backend.composio.dev/v3/mcp/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;composioServerId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/mcp?user_id=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slackUserId&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This last one is the most important. There's no API call, it's pure URL construction. But this URL is what ties everything together: the &lt;code&gt;composioServerId&lt;/code&gt; controls which toolkits are available, and&lt;br&gt;
the &lt;code&gt;user_id&lt;/code&gt; scopes which credentials are used. When &lt;code&gt;/assign&lt;/code&gt; runs, it computes this URL and shows it to the admin so they can configure it in the user's OpenClaw instance.&lt;/p&gt;
&lt;h3&gt;
  
  
  Persisting Users and Conversation
&lt;/h3&gt;

&lt;p&gt;Every Slack user that messages the bot gets a record in our database keyed on (slackUserId, slackTeamId) pair. This is a safety net, as the same Slack user ID could theoretically exist across different workspaces.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 👇 slack-user.service.ts&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;resolveSlackUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;slackUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;slackTeamId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slackUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slackUserId_slackTeamId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slackUserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slackTeamId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slackUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;slackUserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;slackTeamId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;composioEntityId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`slack_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slackTeamId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;slackUserId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Conversation history is stored per-thread using Slack's &lt;code&gt;thread_ts&lt;/code&gt; (the timestamp of the first message in a thread) as the thread id. When the bot receives a message, it fetches the full thread history and passes it to OpenClaw, giving it the memory for the duration of that thread.&lt;/p&gt;


&lt;h2&gt;
  
  
  Configuration Setup
&lt;/h2&gt;

&lt;p&gt;The bot requires per-user OpenClaw instances assigned by an admin. If a user hasn't been assigned an instance, they can't use any features. &lt;code&gt;/connect&lt;/code&gt;, &lt;code&gt;/connections&lt;/code&gt; and chat all require an assignment first.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why not use shared instance?
&lt;/h3&gt;

&lt;p&gt;By shared instance, I mean all the users share the same OpenClaw instance. So why not use it that way? That's how server is supposed to work?&lt;/p&gt;

&lt;p&gt;There's a few reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;By default, OpenClaw is not designed to support multiple users connecting to the same gateway concurrently. In practice, which is likely to be the case for our use case.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;This is already the main reason.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Also, in general, letting multiple users use the same instance with multiple connected accounts is not safe. A prompt injection by one user &lt;strong&gt;could&lt;/strong&gt; access or destroy another user's data.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Even if there's safety measure (which I'm not aware of). Things could always go wrong. So better safe than sorry.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 👇 lib/config.ts&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getUserOpenClawConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;slackUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;slackTeamId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;OpenClawConfig&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slackUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;slackUserId_slackTeamId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;slackUserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;slackTeamId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;select&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;openclawGatewayUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gatewayToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;openclawGatewayUrl&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;gatewayToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;gatewayUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;openclawGatewayUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;gatewayToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gatewayToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No OpenClaw instance assigned. Ask your admin to run /assign.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No assignment, no access. The admin runs &lt;code&gt;/assign&lt;/code&gt; for each user, providing their OpenClaw gateway URL, token, and MCP Config. Until that happens, the bot won't respond to that user.&lt;/p&gt;


&lt;h2&gt;
  
  
  Configure OpenClaw with Composio
&lt;/h2&gt;

&lt;p&gt;Great, now the code part is done. There's one thing that's still left.&lt;/p&gt;

&lt;p&gt;Now, the actual reason to build the bot i.e. to get tools access is not configured &lt;strong&gt;within OpenClaw&lt;/strong&gt; which we do with Composio. It's the most easiest.&lt;/p&gt;

&lt;p&gt;There's multiple ways to configure OpenClaw with Composio. There's standard ways you can find &lt;a href="https://composio.dev/claw" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But, we won't follow the standard way, as by default it uses consumer key, which is a way it's designed by default.&lt;/p&gt;

&lt;p&gt;But we won't work with consumer key, we directly work with the MCP URL.&lt;/p&gt;

&lt;p&gt;Go ahead and modify the OpenClaw config file which lives in the &lt;code&gt;~/.openclaw/openclaw.json&lt;/code&gt; with the following:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;rest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;config...&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"composio"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"...rest"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"entries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"telegram"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"composio"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;put&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;MCP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;URL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;you&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;receive&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;after&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;running&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/assign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;user.&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"mcpUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;p&gt;This sets up &lt;strong&gt;one instance for one user&lt;/strong&gt;. But how do you about configuring multiple instances for multiple users?&lt;/p&gt;
&lt;h3&gt;
  
  
  How do you run it for multiple users?
&lt;/h3&gt;

&lt;p&gt;This only configures one user in the entire workspace. But what about the rest?&lt;/p&gt;

&lt;p&gt;There are a few ways:&lt;/p&gt;
&lt;h4&gt;
  
  
  1. Separate machine or VMs:
&lt;/h4&gt;

&lt;p&gt;Each user's OpenClaw runs on a different machine. Each has its own ~/.openclaw/openclaw.json with its own MCP URL. This is the cleanest but most expensive.&lt;/p&gt;
&lt;h4&gt;
  
  
  2. Use named OpenClaw profiles:
&lt;/h4&gt;

&lt;p&gt;OpenClaw ships with a &lt;code&gt;--profile&lt;/code&gt; flag out of the box:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="nt"&gt;--profile&lt;/span&gt; &amp;lt;name&amp;gt;     Use a named profile &lt;span class="o"&gt;(&lt;/span&gt;isolates OPENCLAW_STATE_DIR/OPENCLAW_CONFIG_PATH under ~/.openclaw-&amp;lt;name&amp;gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You can use a different profile per user. If you name each profile after the user, you get an isolated config for each one on the same machine. Most efficient.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw &lt;span class="nt"&gt;--profile&lt;/span&gt; bob
openclaw &lt;span class="nt"&gt;--profile&lt;/span&gt; shrijal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  3. Separate OS users on one machine:
&lt;/h4&gt;

&lt;p&gt;Somewhat impractical. You'd run one OpenClaw instance per OS user, which means creating a separate system account for each person. Possible, but not a great approach.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There could be hundreds of other ways to do it. These are just the ones I could think of. Do your own research, and you’ll probably find others.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  Slack Workflow
&lt;/h2&gt;

&lt;p&gt;Run these commands in order as an admin before any user can start chatting.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Register your MCP Configs (one per config you created on platform.composio.dev):
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/add-mcp-config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Register auth configs (one per toolkit):
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/add-auth-config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Assign each user their OpenClaw instance and MCP Config:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/assign
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This gives you the user's scoped MCP URL. Configure it in their OpenClaw instance.&lt;/p&gt;

&lt;p&gt;Once assigned, users run:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/connect &amp;lt;toolkit&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That's it. After connecting, they can DM the bot or &lt;code&gt;@mention&lt;/code&gt; it in a channel.&lt;/p&gt;


&lt;h2&gt;
  
  
  Bot in Action
&lt;/h2&gt;

&lt;p&gt;Here's a quick demo of the bot in action:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/CbXwWr4h5LM"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;


&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;So yeah, that's the whole idea.&lt;/p&gt;

&lt;p&gt;A Slack bot on top of OpenClaw, with Composio handling user tool connections, ends up being a really solid setup.&lt;/p&gt;

&lt;p&gt;At this point, you’ve got a good idea of how this bot works with Slack, OpenClaw, and Composio.&lt;/p&gt;

&lt;p&gt;We covered the main flow, how users connect their tools, how everything comes together inside Slack, and why assigning one OpenClaw instance per user helps keep things isolated.&lt;/p&gt;

&lt;p&gt;It keeps the setup clean and gives you a bot that’s actually useful.&lt;/p&gt;

&lt;p&gt;That's all for this one.&lt;/p&gt;

&lt;p&gt;You can find the entire source code here: &lt;a href="https://github.com/shricodev/saas-openclaw-slackbot" rel="noopener noreferrer"&gt;shricodev/saas-openclaw-slackbot&lt;/a&gt;&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__1127015"&gt;
    &lt;a href="/shricodev" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1127015%2F1c5e48a2-f602-4e7d-8312-3c0322d155c6.jpg" alt="shricodev image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/shricodev"&gt;Shrijal Acharya&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/shricodev"&gt;SDE • GOLD @Microsoft Student Ambassador • Prev Lead Collab and Dev-Team Lead @oppiaorg • Mail for collaboration&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>ai</category>
      <category>productivity</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Top 10 CLI Tools to Level-Up Claude Code</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Mon, 06 Apr 2026 12:51:40 +0000</pubDate>
      <link>https://dev.to/composiodev/top-10-cli-tools-to-level-up-claude-code-1kf9</link>
      <guid>https://dev.to/composiodev/top-10-cli-tools-to-level-up-claude-code-1kf9</guid>
      <description>&lt;p&gt;I've been using Claude Code more than any other AI agents recently.&lt;/p&gt;

&lt;p&gt;And when it's the tool you use the most, it just makes sense to make that workflow as productive as possible.&lt;/p&gt;

&lt;p&gt;A lot of the experience comes down to the small tools around it. The ones that help you search, navigate, review diffs, watch system usage, or just keep your workflow clean.&lt;/p&gt;

&lt;p&gt;So this post is a simple list of the CLI tools I think pair really nicely with Claude Code.&lt;/p&gt;

&lt;p&gt;There's an awesome repo with a curated collection of CLI tools for coding agents: &lt;a href="https://github.com/ComposioHQ/awesome-agent-clis" rel="noopener noreferrer"&gt;awesome-agent-clis&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frqdpesfbdf4xrlpgg33w.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frqdpesfbdf4xrlpgg33w.gif" alt="swag gif" width="500" height="284"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What does "tools for Claude Code" actually mean?
&lt;/h2&gt;

&lt;p&gt;Claude Code is already powerful on its own.&lt;/p&gt;

&lt;p&gt;But it gets even better when you pair it with the right terminal tools, especially since you’re already working in the terminal.&lt;/p&gt;

&lt;p&gt;I’m &lt;strong&gt;not talking&lt;/strong&gt; about tools built specifically for Claude Code.&lt;/p&gt;

&lt;p&gt;I mean the CLI tools that make the overall workflow smoother, faster, and easier to manage while Claude is working in your repo.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. &lt;a href="https://cli.github.com/" rel="noopener noreferrer"&gt;GitHub CLI&lt;/a&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ GitHub’s official CLI for working from the terminal.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe2e1eahx78q1aaboalu6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe2e1eahx78q1aaboalu6.png" alt="GitHub cover" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What it is?
&lt;/h3&gt;

&lt;p&gt;GitHub CLI is basically running GitHub from your terminal. You can create repos, check issues, review PRs, manage branches, and handle a bunch of GitHub workflow stuff without leaving your shell.&lt;/p&gt;

&lt;p&gt;It can be as simple as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh repo create
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;for creating a new repository through an interactive prompt, which is one I use the most. And there are tons of other commands you can use.&lt;/p&gt;

&lt;p&gt;Find all the others in the help window.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Why use it with Claude Code?
&lt;/h3&gt;

&lt;p&gt;This one probably will not be for everyone.&lt;/p&gt;

&lt;p&gt;A lot of people do not want to give Claude access to their GitHub repos, and that is totally fair. But if you are comfortable with it, I honestly think it is one of the best tools to pair with Claude Code.&lt;/p&gt;

&lt;p&gt;Or even if you do not want Claude directly using it, GitHub CLI is still great to have beside Claude Code since you can just run the commands yourself and keep moving without leaving the terminal.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. &lt;a href="https://composio.dev" rel="noopener noreferrer"&gt;Composio&lt;/a&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ An MCP server that connects Claude Code to hundreds of external apps.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvlarqn9v88xzclo19eid.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvlarqn9v88xzclo19eid.png" alt="composio cover" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  What it is?
&lt;/h3&gt;

&lt;p&gt;Composio is an MCP server you can add to Claude Code so it can work with 500+ apps.&lt;/p&gt;

&lt;p&gt;You can find the guide on how to connect Composio with Claude Code here: &lt;a href="https://composio.dev/toolkits/composio/framework/claude-code" rel="noopener noreferrer"&gt;Composio Universal CLI&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Why use it with Claude Code?
&lt;/h3&gt;

&lt;p&gt;The main way I use Composio with Claude Code is for email.&lt;/p&gt;

&lt;p&gt;Say I am working on something and need to send a mail to someone.&lt;/p&gt;

&lt;p&gt;Without this, I would usually have to stop, open my mail client, write the message, double check it, and send it myself.&lt;/p&gt;

&lt;p&gt;With Composio set up inside Claude Code, I can just ask Claude to draft the email, or give it the content and the recipient, and it can handle the rest for me.&lt;/p&gt;

&lt;p&gt;And maybe most importantly, no more spelling mistakes in your emails. 😃&lt;/p&gt;

&lt;p&gt;That is the workflow I use the most.&lt;/p&gt;

&lt;p&gt;Since you have 500+ app access, you can already imagine how many other things you could automate from there.&lt;/p&gt;

&lt;p&gt;They recently added CLI support as well, which you can install here: &lt;a href="https://composio.dev/cli" rel="noopener noreferrer"&gt;Composio CLI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For development, Composio provides a playground with test users, execution logs, and real-time trigger streaming so you can iterate on agent behaviour locally before going to production.&lt;/p&gt;


&lt;h2&gt;
  
  
  3. &lt;a href="https://github.com/burntsushi/ripgrep" rel="noopener noreferrer"&gt;ripgrep&lt;/a&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ The fastest way to search through a codebase from the terminal.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx24j0f5r4gi5tiwpdz3m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx24j0f5r4gi5tiwpdz3m.png" alt="ripgrep cover" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  What it is?
&lt;/h3&gt;

&lt;p&gt;It is a ridiculously fast search tool for the terminal.&lt;/p&gt;

&lt;p&gt;It lets you search through files, code, and folders almost instantly.&lt;/p&gt;

&lt;p&gt;If you have ever used &lt;code&gt;grep&lt;/code&gt;, which I assume you have, it's a complete and faster replacement for that in real world repos.&lt;/p&gt;

&lt;p&gt;A simple example:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rg &lt;span class="s2"&gt;"useEffect"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That will search for &lt;code&gt;useEffect&lt;/code&gt; across your entire project and show you where it appears.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why use it with Claude Code?
&lt;/h3&gt;

&lt;p&gt;This is one of the first tools I'd install.&lt;/p&gt;

&lt;p&gt;When working on a real world repo, you are constantly searching for things. Function names, config, and whatnot.&lt;/p&gt;

&lt;p&gt;ripgrep basically makes that fast.&lt;/p&gt;

&lt;p&gt;Even Claude Code defaults to using this tool when searching for things in its workflow. Overall, it is just super handy to have a quick way to move around the repo yourself without digging around manually.&lt;/p&gt;


&lt;h2&gt;
  
  
  4. &lt;a href="https://github.com/tmux/tmux" rel="noopener noreferrer"&gt;tmux&lt;/a&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ A better way to manage terminal sessions.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  What it is?
&lt;/h3&gt;

&lt;p&gt;tmux lets you run multiple terminal sessions inside one terminal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd69oiqggj0gavhw01fve.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd69oiqggj0gavhw01fve.webp" alt="tmux workflow" width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can split panes, open multiple windows, switch between them quickly, and keep everything organized without opening a bunch of separate &lt;strong&gt;terminal tabs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It might feel a little unnecessary at first. But once you get used to it, there is no way back.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why use it with Claude Code?
&lt;/h3&gt;

&lt;p&gt;For me, tmux is one of the most useful tools to pair with Claude Code, and it is actually what is running in my terminal right now as I work on this blog inside Neovim. 👀&lt;/p&gt;

&lt;p&gt;I usually have Claude in a pane, with Neovim or a server running in one window, Lazygit in another, and then some extra panes for running commands.&lt;/p&gt;

&lt;p&gt;If you use Neovim, it gets even better. You can have Claude open in one split and Neovim in another. As Claude makes changes, if you need to edit something, Neovim is right there. And for diffs or Git work, Lazygit is sitting in another window.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkyc3uxa400tcfr35714m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkyc3uxa400tcfr35714m.png" alt="tmux workflow with claude code and lazygit" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How cool is that?&lt;/p&gt;

&lt;p&gt;You are not constantly jumping between tabs or losing track of what is running where.&lt;/p&gt;


&lt;h2&gt;
  
  
  5. &lt;a href="https://github.com/FFmpeg/FFmpeg" rel="noopener noreferrer"&gt;FFmpeg&lt;/a&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ The go-to CLI tool for handling just about any media file.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxi9ut1s6ljrfojjzlr9r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxi9ut1s6ljrfojjzlr9r.png" alt="ffmpeg cover" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  What it is?
&lt;/h3&gt;

&lt;p&gt;Honestly, this is one of the best tools I have added to my workflow recently.&lt;/p&gt;

&lt;p&gt;FFmpeg is a command line tool for working with media files. You can use it to convert images from one format to another, like PNG to JPG, convert video formats, compress files, trim audio, and do all sorts of file processing.&lt;/p&gt;

&lt;p&gt;It supports basically every format you can think of.&lt;/p&gt;

&lt;p&gt;As developers, we end up doing this kind of stuff all the time. And having one tool that handles all of it from the terminal is just super handy.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;FUN FACT:&lt;/strong&gt; Almost all the online media tools that you use on the internet, like online video compressors and similar stuff, are powered by FFmpeg under the hood.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once you have it in your terminal, you really don't need to ever visit such sites.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why use it with Claude Code?
&lt;/h3&gt;

&lt;p&gt;The only catch is that FFmpeg commands are a little complex.&lt;/p&gt;

&lt;p&gt;Even for a simple task, the syntax is just a little too much to understand.&lt;/p&gt;

&lt;p&gt;Here's a quick command to crop a video file:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"crop=1280:720:0:0"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That is exactly where Claude Code becomes useful.&lt;/p&gt;

&lt;p&gt;You can just describe what you want in plain English, and let Claude generate the right FFmpeg command for you.&lt;/p&gt;


&lt;h2&gt;
  
  
  6. &lt;a href="https://github.com/jesseduffield/lazygit" rel="noopener noreferrer"&gt;Lazygit&lt;/a&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ A simple TUI for Git&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0hlcu02qahtbawpce5ay.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0hlcu02qahtbawpce5ay.png" alt="lazygit cover" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  What it is?
&lt;/h3&gt;

&lt;p&gt;Lazygit is a terminal UI for Git.&lt;/p&gt;

&lt;p&gt;It gives you a much nicer way to handle things like commits, branches, stashing, rebasing, and reviewing changes without typing every Git command manually.&lt;/p&gt;

&lt;p&gt;You still stay in the terminal.&lt;/p&gt;

&lt;p&gt;It just makes the whole Git workflow super easy, and you do not need to remember and type out any commands. Just knowing the concepts is enough.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why use it with Claude Code?
&lt;/h3&gt;

&lt;p&gt;This is one I always have open beside Claude Code.&lt;/p&gt;

&lt;p&gt;When Claude makes a lot of changes in a bunch of files, Lazygit makes it easier to review everything, stage only what you want, and manage the overall Git workflow.&lt;/p&gt;

&lt;p&gt;I usually keep Lazygit open in every session inside tmux, in its own window, so I can quickly jump there and handle Git stuff whenever I need to.&lt;/p&gt;

&lt;p&gt;I will talk about tmux a bit later in the list, but this combo works really well.&lt;/p&gt;


&lt;h2&gt;
  
  
  7. &lt;a href="https://github.com/aristocratos/btop" rel="noopener noreferrer"&gt;btop&lt;/a&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ A much better way to monitor your system&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fggie92tumu7etywbvlrx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fggie92tumu7etywbvlrx.png" alt="btop cover" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  What it is?
&lt;/h3&gt;

&lt;p&gt;btop is a system monitor for the terminal.&lt;/p&gt;

&lt;p&gt;It gives you a clean view of CPU, memory, disk, network, and running processes, all in one place.&lt;/p&gt;

&lt;p&gt;There is also htop, which a lot of people already know and use. But personally, I prefer btop.&lt;/p&gt;

&lt;p&gt;It just feels a bit more user friendly, and overall nicer and easier to filter down processes.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why use it with Claude Code?
&lt;/h3&gt;

&lt;p&gt;When you are doing a lot inside the terminal, especially with bigger repos, it is really useful to keep an eye on system usage.&lt;/p&gt;

&lt;p&gt;That might be Claude, any processes it launches with your permission, local servers, or anything else running in the background.&lt;/p&gt;

&lt;p&gt;btop gives you a quick way to see what is eating memory, what is using CPU, and whether your machine is starting to struggle, especially when you're using local models.&lt;/p&gt;


&lt;h2&gt;
  
  
  8. &lt;a href="https://github.com/junegunn/fzf" rel="noopener noreferrer"&gt;fzf&lt;/a&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ The backbone of fuzzy finding in the terminal&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqev3t1z88tdq2vt87u8e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqev3t1z88tdq2vt87u8e.png" alt="fzf cover" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  What it is?
&lt;/h3&gt;

&lt;p&gt;I have probably been using this tool longer than any other in this list.&lt;/p&gt;

&lt;p&gt;fzf is a command line fuzzy finder.&lt;/p&gt;

&lt;p&gt;It lets you search and pick things interactively from the terminal.&lt;/p&gt;

&lt;p&gt;That could be files, directories, Git branches, command history, processes, or really anything you can pipe into it.&lt;/p&gt;

&lt;p&gt;If you haven't heard about this tool or have never used it, you are doing something wrong 😏.&lt;/p&gt;

&lt;p&gt;A simple example:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f | fzf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This gives you a fuzzy searchable list of files in the current directory.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why use it with Claude Code?
&lt;/h3&gt;

&lt;p&gt;This is one of those tools that just makes terminal workflows feel faster.&lt;/p&gt;

&lt;p&gt;Whether I am jumping between files, searching through something, or picking from a long list of options, fzf is usually involved somewhere. I have so many scripts built around fzf.&lt;/p&gt;

&lt;p&gt;And when you are already spending a lot of time in the terminal with Claude Code, that kind of speed matters.&lt;/p&gt;

&lt;p&gt;It is not really a Claude specific tool. It is one of the foundations that make working in the terminal and overall moving between things a lot better.&lt;/p&gt;


&lt;h2&gt;
  
  
  9. (Optional) &lt;a href="https://github.com/AlexsJones/llmfit" rel="noopener noreferrer"&gt;LLMFit&lt;/a&gt;
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ Handy if you are experimenting with local or custom models alongside Claude Code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9agawf7zphwgqvrnqcbp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9agawf7zphwgqvrnqcbp.png" alt="llmfit cover" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  What it is?
&lt;/h3&gt;

&lt;p&gt;LLMFit is a CLI tool that scans your system hardware and tells you which local AI models you can run smoothly on your system.&lt;/p&gt;

&lt;p&gt;If you are planning to run a local model, it is a nice way to avoid downloading something that your system will struggle with. Its whole purpose is to help match models to the machine you have.&lt;/p&gt;

&lt;p&gt;Installing is as simple as:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;llmfit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now, to scan your hardware against models, run this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;llmfit scan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;and it will list out all the models with their metadata and performance scores based on your hardware.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why use it with Claude Code?
&lt;/h3&gt;

&lt;p&gt;This one is definitely more niche.&lt;/p&gt;

&lt;p&gt;But if you are running Claude Code with a local or custom model setup, it can help you figure out what will run well on your machine before you waste time downloading the wrong model.&lt;/p&gt;

&lt;p&gt;It is not something everyone will need, but for people who prefer the Claude Code agent and want to test newer local or custom models from other providers, this is an option as well.&lt;/p&gt;

&lt;p&gt;You can find many guides on doing that. One that I referenced while trying it out is by &lt;a href="https://medium.com/@luongnv89/run-claude-code-on-local-cloud-models-in-5-minutes-ollama-openrouter-llama-cpp-6dfeaee03cda" rel="noopener noreferrer"&gt;Luong NGUYEN&lt;/a&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  A few more nice ones
&lt;/h2&gt;

&lt;p&gt;There are also a few other terminal tools I use a lot that I did not want to give a full section to, but they are still very much part of the overall workflow.&lt;/p&gt;

&lt;p&gt;Things like &lt;strong&gt;fd&lt;/strong&gt;, &lt;strong&gt;zoxide&lt;/strong&gt;, &lt;strong&gt;eza&lt;/strong&gt;, &lt;strong&gt;yazi&lt;/strong&gt;, and &lt;strong&gt;bat&lt;/strong&gt; all make the terminal feel nicer to work in.&lt;/p&gt;

&lt;p&gt;Some help you move around directories faster. Some make listing files, previewing content, or moving around in the filesystem way better than the default.&lt;/p&gt;

&lt;p&gt;I leave it up to you to research these tools.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F860iemg7j7gkx4mp7ehw.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F860iemg7j7gkx4mp7ehw.gif" alt="steve smith playing with magnifying glass" width="490" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;None of these are Claude Code specific.&lt;/p&gt;


&lt;h2&gt;
  
  
  Ones I'd install first
&lt;/h2&gt;

&lt;p&gt;If I had to set this up again from scratch, I’d probably start with &lt;strong&gt;ripgrep&lt;/strong&gt;, &lt;strong&gt;GitHub CLI&lt;/strong&gt;, &lt;strong&gt;tmux&lt;/strong&gt;, and &lt;strong&gt;Lazygit&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That already covers a lot of the core workflow around Claude Code.&lt;/p&gt;

&lt;p&gt;And separately, I’d also set up &lt;strong&gt;Composio&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It’s a bit different from the rest here. It’s not exactly just another CLI tool, but more of an MCP server. Really useful if you want to automate parts of your workflow and connect Claude to external tools in a cleaner way.&lt;/p&gt;


&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;You definitely do not need every tool in this list.&lt;/p&gt;

&lt;p&gt;But a few of them can make working with Claude Code a lot smoother, especially once you start using it more seriously.&lt;/p&gt;

&lt;p&gt;At the end of the day, it’s really just about making the workflow around Claude feel cleaner and easier to manage.&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__1127015"&gt;
    &lt;a href="/shricodev" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1127015%2F1c5e48a2-f602-4e7d-8312-3c0322d155c6.jpg" alt="shricodev image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/shricodev"&gt;Shrijal Acharya&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/shricodev"&gt;SDE • GOLD @Microsoft Student Ambassador • Prev Lead Collab and Dev-Team Lead @oppiaorg • Mail for collaboration&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>productivity</category>
      <category>opensource</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>🚀 How to run a fully-autonomous company with OpenClaw 🦞</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Thu, 02 Apr 2026 14:39:22 +0000</pubDate>
      <link>https://dev.to/composiodev/how-to-run-a-fully-autonomous-company-with-openclaw-ma5</link>
      <guid>https://dev.to/composiodev/how-to-run-a-fully-autonomous-company-with-openclaw-ma5</guid>
      <description>&lt;p&gt;Imagine owning a company with just one human employee, and that too is yourself. The rest? All OpenClaw agents!&lt;/p&gt;

&lt;p&gt;Before OpenClaw, that would have sounded completely silly, but with it, it's possible, &lt;strong&gt;really possible!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can automate your entire company or simulate a fully functioning one with just OpenClaw and your VPS, Mac Mini, or local system for testing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fety6wnq6m91tsqhv27gi.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fety6wnq6m91tsqhv27gi.jpg" alt="obama meme" width="640" height="391"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;In this tutorial, you'll learn how to run an entire company using just yourself and a bunch of &lt;strong&gt;OpenClaw agents&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you will learn: ✨&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What OpenClaw is and how it works&lt;/li&gt;
&lt;li&gt;Why storing API keys locally is a bad idea&lt;/li&gt;
&lt;li&gt;Setting up &lt;strong&gt;Composio&lt;/strong&gt; for secure OAuth-based integrations&lt;/li&gt;
&lt;li&gt;Connecting your first app and getting agents up and running 🚀&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ready to become a one-person company? 👀&lt;/p&gt;




&lt;h2&gt;
  
  
  What's OpenClaw?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcqljwq3cff8femj5e1s0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcqljwq3cff8femj5e1s0.png" alt="OpenClaw Banner" width="800" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 I assume you already know what OpenClaw is. If not, why are you even here? Just kidding... The blog itself is completely beginner friendly. If you already have an idea of what OpenClaw is, just skip this section.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;OpenClaw is a personal AI assistant you run on your own machine or a server you own. It is the thing that actually sits between your model provider (OpenAI, Anthropic, Kimi, etc.) and the stuff you want done, such as messaging, tools, files, and integrations, and this idea is what actually makes the one-person company possible.&lt;/p&gt;

&lt;p&gt;Take this as a mental model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your LLM is the brain (thinks)&lt;/li&gt;
&lt;li&gt;OpenClaw is the body (it can do things)&lt;/li&gt;
&lt;li&gt;The Gateway is the receptionist (routes messages in and results out)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It provides the model with a runtime that can call tools, maintain state, and appear where you already chat (WhatsApp, Telegram, Slack, Discord, etc.). Now, that's just the gist. There's much more to understand. I assume you've already worked with it, so I'm not going any deeper than this in the intro.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fosrayie6pppe2qbj6kan.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fosrayie6pppe2qbj6kan.jpg" alt="OpenClaw architecture" width="800" height="516"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For installation, visit the OpenClaw &lt;a href="https://docs.openclaw.ai/install" rel="noopener noreferrer"&gt;installation guide&lt;/a&gt;, and based on your distro and installation choice, install it on your machine.&lt;/p&gt;

&lt;p&gt;If you just want it running quickly, do the normal installation. If you're even slightly paranoid (which you should be 😮‍💨), use Docker.&lt;/p&gt;

&lt;p&gt;Also, make sure you set up a channel for easier chatting from your phone (preferably Telegram).&lt;/p&gt;

&lt;p&gt;For help setting up a channel, ask OpenClaw itself. It knows itself better than anyone else on the internet.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 If you face issues like &lt;code&gt;OpenClaw: access not configured&lt;/code&gt; when talking with the bot, make sure you run this command:&lt;/p&gt;


&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw pairing approve &amp;lt;telegram/whatsapp/...&amp;gt; &amp;lt;pairing_code&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Just like that, now you have an agent listening on your channel. Message anything, and you should get a reply back.&lt;/p&gt;

&lt;p&gt;From here onwards, I assume you already have OpenClaw running. To make sure everything is working, run this command:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If not, try running &lt;code&gt;openclaw doctor&lt;/code&gt;, which helps debug your gateway or channel issues.&lt;/p&gt;


&lt;h2&gt;
  
  
  Run a whole company?
&lt;/h2&gt;

&lt;p&gt;Yeah, in theory, you can actually automate or run an entire company. Can't guarantee the company will stand long, but with OpenClaw, it's now possible.&lt;/p&gt;

&lt;p&gt;The only human in the process is going to be yourself. All your employees will be &lt;strong&gt;OpenClaw Agents&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3j5c227mzou0xz0n0j38.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3j5c227mzou0xz0n0j38.png" alt="Openclaw running an entire company architecture" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, most day-to-day operations of running a company, such as sales, team meetings, and customer care, can be managed with OpenClaw Agents. And there are many more than just the ones in the image, of course. This is just a quick sketch to give you an idea.&lt;/p&gt;


&lt;h2&gt;
  
  
  Problem with "Just OpenClaw"
&lt;/h2&gt;

&lt;p&gt;By default, OpenClaw works with API keys, and it stores them in a plain text file in the &lt;code&gt;~/.openclaw/&lt;/code&gt; directory for all the services you use, such as Google, Gmail, and so on. This is not a very good practice if you're running this on your local machine. If using something like a VPS or the hyped &lt;strong&gt;Mac Mini&lt;/strong&gt;, it's fine, but still, storing credentials in a local plain text file is never a good idea.&lt;/p&gt;

&lt;p&gt;Especially if you're using smaller models, they are even more prone to prompt injections, and since OpenClaw has whole system access, it might wipe out your entire system without you doing anything.&lt;/p&gt;

&lt;p&gt;What's actually gone wrong in the wild (already):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Malicious skills on ClawHub:&lt;/strong&gt; researchers found hundreds to thousands of skills that were straight-up malware or had critical issues, including credential theft and prompt injection patterns.&lt;/li&gt;
&lt;/ul&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prompt injection turning into installs:&lt;/strong&gt; there's been at least one high-profile incident where a prompt injection was used to push OpenClaw onto machines via an agent workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo215wy0xag7t8z86ogzv.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo215wy0xag7t8z86ogzv.jpg" alt="OpenClaw compromised" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the above reasons, I recommend that you use some hosted service which in my case, &lt;strong&gt;Composio.&lt;/strong&gt; It lets you authenticate using OAuth, which is the most secure option over pasting keys locally.&lt;/p&gt;


&lt;h2&gt;
  
  
  Connecting your first app
&lt;/h2&gt;

&lt;p&gt;Now, it's time to create agents, but first, we need to set up or connect our first app from Composio.&lt;/p&gt;

&lt;p&gt;The agents will mostly revolve around working with those applications from Composio.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Install Composio Plugin
&lt;/h3&gt;

&lt;p&gt;Composio's OpenClaw plugin connects OpenClaw to Composio's MCP endpoint and exposes third-party tools (GitHub, Gmail, Slack, Notion, etc.) through that layer.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw plugins &lt;span class="nb"&gt;install&lt;/span&gt; @composio/openclaw-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  2. Composio Plugin Setup
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Log in at &lt;a href="https://dashboard.composio.dev/" rel="noopener noreferrer"&gt;dashboard.composio.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Choose OpenClaw as the client.&lt;/li&gt;
&lt;li&gt;Copy your consumer key (&lt;code&gt;ck_...&lt;/code&gt;) from the Composio dashboard settings, then set it:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F982ctfxsvbsd8d1dsmmk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F982ctfxsvbsd8d1dsmmk.jpg" alt="Composio OpenClaw setup instructions" width="800" height="214"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw config &lt;span class="nb"&gt;set &lt;/span&gt;plugins.entries.composio.config.consumerKey &lt;span class="s2"&gt;"ck_your_key_here"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now, it's a good idea to restart the gateway:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw gateway restart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  3. Verify the plugin loaded
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw plugins list
openclaw logs &lt;span class="nt"&gt;--follow&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You're looking for something like "Composio loaded" and a "tools registered" message.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fce73szhae07paumt96ae.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fce73szhae07paumt96ae.jpg" alt="OpenClaw successfully loads Composio" width="800" height="219"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the plugin is &lt;strong&gt;"loaded"&lt;/strong&gt;, it means you can now successfully access Composio.&lt;/p&gt;

&lt;p&gt;Here's how it works:&lt;/p&gt;

&lt;p&gt;The plugin connects to Composio's MCP server at &lt;code&gt;https://connect.composio.dev/mcp&lt;/code&gt; and registers all available tools directly into the OpenClaw agent. Tools are called by name — no extra search or execute steps needed.&lt;/p&gt;

&lt;p&gt;If a tool returns an auth error, the agent will prompt you to connect that toolkit at &lt;a href="https://dashboard.composio.dev/" rel="noopener noreferrer"&gt;dashboard.composio.dev&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here's how the configuration looks:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"entries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"composio"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"config"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"consumerKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ck_your_key_here"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You can configure the following options directly from the config file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;enabled&lt;/code&gt;: enable or disable the plugin&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;consumerKey&lt;/code&gt;: your Composio consumer key&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mcpUrl&lt;/code&gt;: the MCP server URL. By default, it's &lt;code&gt;https://connect.composio.dev/mcp&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Previously, you had to configure API keys per integration, but with Composio you don't have to worry about any of that. Just make sure &lt;strong&gt;not to leak&lt;/strong&gt; the consumer key that we generated.&lt;/p&gt;

&lt;p&gt;And it's that simple. Everything works out of the box just as you would use any other OpenClaw plugin!&lt;/p&gt;

&lt;p&gt;Now, to test if it works, head over to the Control UI chat and send a message, something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"List the Composio tools you have available."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fryuiqg0zcs7udhjqn44a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fryuiqg0zcs7udhjqn44a.png" alt="OpenClaw listing composio tools" width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If it asks you to connect the tools, head over to &lt;a href="https://dashboard.composio.dev/" rel="noopener noreferrer"&gt;dashboard.composio.dev&lt;/a&gt; and connect each of the tools you require. It's as simple as clicking &lt;strong&gt;Connect&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fecgu1xvvw1qzz27q5ymt.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fecgu1xvvw1qzz27q5ymt.jpg" alt="Adding integrations in Composio" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All the integrations you use are OAuth-hosted, and only the tools you connect will be available to OpenClaw. Nothing more than that.&lt;/p&gt;


&lt;h2&gt;
  
  
  Setting up a Multi-Agent Team
&lt;/h2&gt;

&lt;p&gt;The idea is pretty clear. Since one single agent wouldn't be enough to handle all sorts of company requirements due to &lt;strong&gt;context window limitations&lt;/strong&gt;, you could have multiple sub-agents for multiple task types.&lt;/p&gt;

&lt;p&gt;Say, one agent AgentA handles marketing, AgentB handles business analysis, AgentC handles something else.&lt;/p&gt;

&lt;p&gt;Each agent has a distinct role, personality, and model optimized for its use case — say, for business analysis, you'd want a more research-oriented model like GPT-5.2.&lt;/p&gt;

&lt;p&gt;And how do you create them? It's simple, just chat with OpenClaw itself, either in the chat window or your configured channel.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Please create a new agent called **Shri**. This agent should be capable of handling tasks such as reading and composing emails, and scheduling Google Meet sessions.

For the model, use **Claude Sonnet 4.6** (`claude-sonnet-4-6`).

Please ensure that the existing main agent remains untouched and unchanged.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd5r5jl6vv59m9a9tv8tb.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd5r5jl6vv59m9a9tv8tb.jpg" alt="Prompt in OpenClaw" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And it will create a new agent, which you can view in the &lt;code&gt;Agents&lt;/code&gt; tab in the OpenClaw dashboard or by running &lt;code&gt;/agents&lt;/code&gt; in the OpenClaw TUI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvbr7ukxn72n1y7fyejgu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvbr7ukxn72n1y7fyejgu.png" alt="OpenClaw agents" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Similarly, do it for all your different work types. Create a separate agent for each type of work.&lt;/p&gt;

&lt;p&gt;The main agent can then delegate work to those specialized agents, each handling one specific task type, which improves response quality because one agent is handling one type of work instead of everything at once.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;TIP:&lt;/strong&gt; This also helps you reduce model usage costs, as you can assign more reasoning-heavy models to complex tasks and smaller, cheaper models to simpler ones.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  What's Missing?
&lt;/h2&gt;

&lt;p&gt;Everything seems good, but there's one thing missing... &lt;strong&gt;autonomy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You still have to message OpenClaw manually to get things done, which isn't ideal when you're planning on using it as an AI employee.&lt;/p&gt;

&lt;p&gt;There are two ways to achieve this:&lt;/p&gt;
&lt;h3&gt;
  
  
  1. If you're a little technical
&lt;/h3&gt;

&lt;p&gt;You must be familiar with cron jobs and their syntax. If so, this is a way to do it directly from the CLI outside of OpenClaw.&lt;/p&gt;

&lt;p&gt;Run the following command:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw cron add &lt;span class="nt"&gt;--schedule&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;cron_syntax&amp;gt;"&lt;/span&gt; &lt;span class="nt"&gt;--message&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;prompt&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Say you want it running every single day at 8 AM:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openclaw cron add &lt;span class="nt"&gt;--schedule&lt;/span&gt; &lt;span class="s2"&gt;"0 9 * * *"&lt;/span&gt; &lt;span class="nt"&gt;--message&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;prompt&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  2. If you're not technical
&lt;/h3&gt;

&lt;p&gt;Similar to how we used a prompt to create a new agent, all you need to do is write a prompt:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Every morning at 9 AM, send me the top news of the day. Also scan my Google Calendar for the day, identify each attendee and their company. Send me two different messages on Telegram: one with the news summary and one with the meeting details.

Use the relevant Agent you have for each purpose.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;💁 There's also a similar concept called Heartbeat, which is another approach for scheduling tasks in OpenClaw. You can check it out here: &lt;a href="https://docs.openclaw.ai/gateway/heartbeat" rel="noopener noreferrer"&gt;OpenClaw Heartbeat&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  Workflow Demo
&lt;/h2&gt;

&lt;p&gt;Okay, time for a demo.&lt;/p&gt;

&lt;p&gt;Showing an entire workflow demo of running a company would be too much work, so for this demo, I will show you one part of the workflow: checking the calendar and messaging a summary with attendees every day at a set time.&lt;/p&gt;

&lt;p&gt;You could have it run every X hours or every single day at a fixed time. After each interval, the model will do as said above (Obviously, the idea is too naive, but it's just for this demo.) The possibilities are endless.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Keep this in mind: “anything that you can do manually on the internet, you can automate with OpenClaw.” So, you get the idea.&lt;/p&gt;

&lt;p&gt;💁 &lt;strong&gt;NOTE:&lt;/strong&gt; If you're serious about this idea, it's better to run this on a VPS or a Mac Mini, because you mostly don't have your personal PC running 24/7.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's the demo:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/3WZ5PkqyCyc"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;


&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;So far, you've learned how to run a fully functioning company with just yourself and a bunch of &lt;strong&gt;OpenClaw agents&lt;/strong&gt;, using &lt;strong&gt;Composio&lt;/strong&gt; as the secure integration layer between OpenClaw and all your third-party apps.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Be sure to give a star to &lt;a href="https://github.com/ComposioHQ/composio" rel="noopener noreferrer"&gt;&lt;strong&gt;Composio&lt;/strong&gt;&lt;/a&gt; and &lt;a href="https://github.com/openclaw/openclaw" rel="noopener noreferrer"&gt;&lt;strong&gt;OpenClaw&lt;/strong&gt;&lt;/a&gt; on their GitHub repositories.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you found this article helpful, drop a like and share your thoughts in the comments below. 👇&lt;/p&gt;

&lt;p&gt;Happy automating! 🥳&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__1127015"&gt;
    &lt;a href="/shricodev" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1127015%2F1c5e48a2-f602-4e7d-8312-3c0322d155c6.jpg" alt="shricodev image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/shricodev"&gt;Shrijal Acharya&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/shricodev"&gt;SDE • GOLD @Microsoft Student Ambassador • Prev Lead Collab and Dev-Team Lead @oppiaorg • Mail for collaboration&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>ai</category>
      <category>productivity</category>
      <category>openclaw</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Everything you need to know about OpenAI GPT-5.4 ✌️</title>
      <dc:creator>Shrijal Acharya</dc:creator>
      <pubDate>Sat, 21 Mar 2026 14:08:05 +0000</pubDate>
      <link>https://dev.to/tensorlake/everything-you-need-to-know-about-openai-gpt-54-3lgm</link>
      <guid>https://dev.to/tensorlake/everything-you-need-to-know-about-openai-gpt-54-3lgm</guid>
      <description>&lt;p&gt;OpenAI’s new GPT-5.4 is here, and on paper at least, it looks like one of their strongest all-rounder models so far.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg2rrzzlrqx2wc2szp0do.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg2rrzzlrqx2wc2szp0do.png" alt="GPT 5.4 release blog" width="799" height="252"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;In this article, we take a quick look at OpenAI GPT-5.4, go through its official benchmarks, and then compare it in one small coding task against Anthropic’s general-purpose model, Claude Sonnet 4.6, to see how it actually performs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We briefly go over what GPT-5.4 is, what OpenAI is claiming with this model, and why it looks like one of their strongest all-rounder releases so far.&lt;/li&gt;
&lt;/ul&gt;



&lt;ul&gt;
&lt;li&gt;We look at the official benchmarks around coding, reasoning, tool use, and computer-use capabilities to get an idea of how strong the model looks on paper.&lt;/li&gt;
&lt;/ul&gt;



&lt;ul&gt;
&lt;li&gt;Instead of relying only on benchmarks, we also compare GPT-5.4 against Claude Sonnet 4.6 in one small, quick coding task (not enough to judge fully, but still...).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Brief on OpenAI GPT-5.4
&lt;/h2&gt;

&lt;p&gt;So, before we jump into the coding test, let me give you a quick brief on GPT-5.4, because this is one of OpenAI’s biggest model releases in a while.&lt;/p&gt;

&lt;p&gt;OpenAI released GPT-5.4 on March 5, 2026, and they are positioning it as their most capable and efficient frontier model for professional work.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgjgdtgurct57gfvfsblk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgjgdtgurct57gfvfsblk.png" alt="OpenAI claiming gpt 5.4 is good at frontend" width="799" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What makes this model interesting is that OpenAI is not selling it as just a coding model, and not just a reasoning model either. They are basically pitching it as an &lt;strong&gt;all-round professional work&lt;/strong&gt; model that combines strong reasoning, strong coding, better tool use, and much better performance on practical work like spreadsheets, presentations, etc.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkfmlidftq907s3rk2c1l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkfmlidftq907s3rk2c1l.png" alt="Sam Altman claiming the model is good at real life tasks like working with spreadsheets" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Honestly, this part matters more than it sounds. A lot of real AI work is not just prompting or writing code, it is dealing with PDFs, spreadsheets, slides, and all kinds of unstructured data. That is also where something like &lt;a href="https://tensorlake.ai" rel="noopener noreferrer"&gt;Tensorlake&lt;/a&gt; makes sense, because it helps turn that mess into something models can actually work with.&lt;/p&gt;

&lt;p&gt;And the specs are also pretty wild. GPT-5.4 supports a &lt;strong&gt;1.05M token&lt;/strong&gt; context window with 128K max output tokens, which is pretty good room to work with. All in all, this helps the model remember things better. Also, a thing to note is that the knowledge cutoff for this model is &lt;strong&gt;August 31, 2025&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Now, let's talk about the part we mostly care about.&lt;/p&gt;

&lt;p&gt;On the official OpenAI benchmarks, &lt;strong&gt;GPT-5.4 scores 57.7% on SWE-Bench Pro (Public)&lt;/strong&gt;, which puts it basically side by side with GPT-5.3-Codex, a coding-focused model, at &lt;strong&gt;56.8%&lt;/strong&gt;. So yes, OpenAI says this general-purpose model is slightly better than GPT-5.3-Codex, a coding-focused model, which I personally have not had the best experience with compared to Claude models, and that is kind of wild to think about.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn6yjg9ftpq3vexntmndl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn6yjg9ftpq3vexntmndl.png" alt="gpt 5.4 benchmark" width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;OpenAI says GPT-5.4 is their &lt;strong&gt;first general-purpose model with native computer-use capabilities&lt;/strong&gt;, which is a pretty big deal. That means it is built not just to generate text or code, but also to operate across software, work from screenshots, and handle more agent-like workflows. On &lt;strong&gt;OSWorld-Verified&lt;/strong&gt;, it scores &lt;strong&gt;75.0%&lt;/strong&gt;, which OpenAI says is above human performance on that benchmark. 🤯&lt;/p&gt;

&lt;p&gt;One thing I also like here is that OpenAI is claiming GPT-5.4 is their &lt;strong&gt;most factual model yet&lt;/strong&gt;. It is said to be 18% less likely to contain any errors compared to GPT-5.2.&lt;/p&gt;

&lt;p&gt;For API developers, pricing matters, of course.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv5667dzgr6es7701tau2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv5667dzgr6es7701tau2.png" alt="gpt 5.4 pricing" width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The standard &lt;strong&gt;GPT-5.4&lt;/strong&gt; model is listed at &lt;strong&gt;$2.50 per 1M input tokens&lt;/strong&gt;, &lt;strong&gt;$0.25 cached input&lt;/strong&gt;, and &lt;strong&gt;$15 per 1M output tokens&lt;/strong&gt;. &lt;strong&gt;GPT-5.4 Pro&lt;/strong&gt; is way more expensive at &lt;strong&gt;$30 input&lt;/strong&gt; and &lt;strong&gt;$180 output per 1M tokens&lt;/strong&gt;, and OpenAI says it can take several minutes on hard tasks, so that one is clearly for cases where you really want the best answer and are okay paying for it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💁 The normal GPT-5.4 model is probably the one most people will actually care about day to day, and that's what I'd prefer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And as always, benchmarks are benchmarks. But on paper at least, GPT-5.4 looks like one of the strongest all-rounder models OpenAI has shipped so far.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Coding Test
&lt;/h2&gt;

&lt;p&gt;As this is a general-purpose model instead of a coding-tuned model, comparing the model's ability solely on coding is just not fair. But as developers, we mostly care about how good the model is at coding anyway, so just to give you an idea of how this model performs, we will do a quick test.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6l1l9xllt29e4rzqakqa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6l1l9xllt29e4rzqakqa.png" alt="gpt 5.4 benchmark compared to 5.3 codex" width="799" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, there's not much difference in SWE-Bench between GPT-5.4 and GPT-5.3-Codex:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GPT-5.4&lt;/strong&gt;: Latency (s): 1,053, Accuracy: 57.7%, Effort: xhigh&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPT-5.3-Codex&lt;/strong&gt;: Latency (s): 1,114, Accuracy: 57.2%, Effort: xhigh&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But to give you an idea of what to expect from this model in coding, I will run one small, quick test.&lt;/p&gt;

&lt;p&gt;Let's take two general models, one from Anthropic, Claude Sonnet 4.6, and one from OpenAI, GPT-5.4, &lt;strong&gt;not pro&lt;/strong&gt;, and compare them against each other to show the difference in their coding skills.&lt;/p&gt;

&lt;p&gt;For the test, we will use the following CLI coding agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Sonnet 4.6:&lt;/strong&gt; Claude Code (Anthropic’s terminal-based agentic coding tool)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI GPT-5.4:&lt;/strong&gt; Codex CLI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As GPT-5.4 is said to be strong in frontend, why not test it on frontend itself?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa3mnuibaxl0c6acx4npd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa3mnuibaxl0c6acx4npd.png" alt="gpt 5.4 frontend claim" width="800" height="180"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Test: Figma Design Clone with MCP
&lt;/h3&gt;

&lt;p&gt;In this test, we'll be comparing both models on a Figma design, a complex dashboard with so many things happening in the UI.&lt;/p&gt;

&lt;p&gt;Here's the Figma design that I'll ask both models to clone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Prompt:

Build a &lt;span class="gs"&gt;**pixel-accurate clone**&lt;/span&gt; of the attached Figma design frame using the &lt;span class="gs"&gt;**provided Next.js project**&lt;/span&gt; as the starting point. Do &lt;span class="gs"&gt;**not**&lt;/span&gt; create a new project. Instead, implement the UI inside the existing codebase.

https://www.figma.com/design/8quNKljV0spv67VAGsA75D/Dashboard-Design-Concept--Community---Copy-?node-id=69-123&amp;amp;t=Tvu2UB7UDMqkvPRb-4

Please match the design as closely as possible, with close attention to layout, spacing, alignment, typography, colors, borders, shadows, corner radius, and overall visual balance.

Requirements:
&lt;span class="p"&gt;
*&lt;/span&gt; use the existing &lt;span class="gs"&gt;**Next.js**&lt;/span&gt; setup
&lt;span class="p"&gt;*&lt;/span&gt; keep the code clean and componentized
&lt;span class="p"&gt;*&lt;/span&gt; make the page responsive without changing the intended design
&lt;span class="p"&gt;*&lt;/span&gt; use semantic HTML where appropriate
&lt;span class="p"&gt;*&lt;/span&gt; avoid adding your own design decisions unless necessary
&lt;span class="p"&gt;*&lt;/span&gt; if any part of the design is unclear, make the most reasonable choice and stay visually consistent

Prioritize &lt;span class="gs"&gt;**design accuracy first**&lt;/span&gt;, then code quality.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h4&gt;
  
  
  GPT-5.4
&lt;/h4&gt;

&lt;p&gt;GPT-5.4 pretty much one-shotted the entire implementation in one go, which was honestly nice to see. It did not need any follow-up prompt, no fixing, nothing. It just took the Figma frame through MCP and started building the whole thing right away.&lt;/p&gt;

&lt;p&gt;The final result actually looked decent. I would not call it pixel-perfect by any means, but compared to Claude Sonnet 4.6, I’d say the implementation looked noticeably better overall. The whole thing feels more like a static picture of the design than an interface you can actually interact with.&lt;/p&gt;

&lt;p&gt;Time-wise, it took roughly &lt;strong&gt;5 minutes&lt;/strong&gt; to get to a working to the working build.&lt;/p&gt;

&lt;p&gt;Here’s the demo:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/4yxzh0qxm5c"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;You can find the code it generated here: &lt;a href="https://gist.github.com/shricodev/f6edd67c32037c0a69def1b10985855d" rel="noopener noreferrer"&gt;GPT-5.4 Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Token usage looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Total Token Usage:&lt;/strong&gt; 166,501&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input Token Usage:&lt;/strong&gt; 151,595&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cached Input Tokens:&lt;/strong&gt; 1,291,776&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output Token Usage:&lt;/strong&gt; 14,906&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning Tokens:&lt;/strong&gt; 1,479&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the following code changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; 3 files changed, 803 insertions(+), 82 deletions(-)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To be honest, I still would not say this is the kind of code implementation you can just ship straight to production and call it done. But for a one-shot frontend clone from a Figma frame, this was a pretty solid attempt.&lt;/p&gt;
&lt;h4&gt;
  
  
  Claude Sonnet 4.6
&lt;/h4&gt;

&lt;p&gt;Claude Sonnet 4.6 went straight into the implementation right away. It did run into an issue at first, not really a build error, but more of one of those annoying &lt;strong&gt;Next.js image gotchas&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0kdi2h66gcdz807p0kom.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0kdi2h66gcdz807p0kom.png" alt="claude sonnet 4.6 image impl error" width="798" height="241"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After that, I gave it a quick follow-up prompt, and almost instantly, it fixed the issue and came back with a decent implementation.&lt;/p&gt;

&lt;p&gt;As you’d expect, it did manage to clone the project structure and get the UI in place. And again, the same issue, there's just no functionality whatsoever. It just feels like a picture with no interactivity.&lt;/p&gt;

&lt;p&gt;Here’s the demo:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/L9l8cGBvC1U"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;You can find the code it generated here: &lt;a href="https://gist.github.com/shricodev/61d485a452f2aab8eb41ceaa31ddd9f9" rel="noopener noreferrer"&gt;Claude Sonnet 4.6 Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Time-wise, it took &lt;strong&gt;9 minutes 56 seconds&lt;/strong&gt; to get to a working result, and the follow-up fix was pretty much instant.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq0ugjnobb005rsla4bnr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq0ugjnobb005rsla4bnr.png" alt="implementation checklist" width="799" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Token usage, based on Claude Code’s model stats, looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Input Token Usage:&lt;/strong&gt; 84&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output Token Usage:&lt;/strong&gt; 35.4K&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgof5gb4r1ufiy1vpwpa4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgof5gb4r1ufiy1vpwpa4.png" alt="token usage" width="800" height="172"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the following code changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code Changes:&lt;/strong&gt; 10 files changed, 1017 insertions(+), 84 deletions(-)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To be honest, I’m not really impressed, but I’m not disappointed either. The result feels pretty neutral overall. It was able to use tools, get fairly close to the UI, and produce something usable for comparison, but the implementation itself feels a bit weird and not all that convincing.&lt;/p&gt;


&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;So, after all the benchmarks, claims, and hype, I think the fairest takeaway is this: GPT-5.4 looks very strong on paper, and for a lot of people it works and is an upgrade, but it still doesn’t seem like it is the best model you can get for coding.&lt;/p&gt;

&lt;p&gt;So yeah, I’d say GPT-5.4 is probably one of the strongest all-rounder models OpenAI has shipped so far, but whether it beats Claude, be it Sonnet or Opus, for coding in real usage is still something you’ll want to judge from your actual hands-on testing, not just benchmarks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5t6hnxig38el1bsey06l.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5t6hnxig38el1bsey06l.gif" alt="slect random gif" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And honestly, that’s the real takeaway here anyway.&lt;/p&gt;

&lt;p&gt;These models keep getting better at a speed that is honestly hard to keep up with. So rather than getting too stuck on who won one benchmark, the better thing to do is probably to keep building, keep testing, and keep learning how to use these models better for your use case.&lt;/p&gt;

&lt;p&gt;What do you think, is GPT-5.4 actually that good, or is Claude still your go-to? 👇&lt;/p&gt;


&lt;div class="ltag__user ltag__user__id__1127015"&gt;
    &lt;a href="/shricodev" class="ltag__user__link profile-image-link"&gt;
      &lt;div class="ltag__user__pic"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=150,height=150,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1127015%2F1c5e48a2-f602-4e7d-8312-3c0322d155c6.jpg" alt="shricodev image"&gt;
      &lt;/div&gt;
    &lt;/a&gt;
  &lt;div class="ltag__user__content"&gt;
    &lt;h2&gt;
&lt;a class="ltag__user__link" href="/shricodev"&gt;Shrijal Acharya&lt;/a&gt;Follow
&lt;/h2&gt;
    &lt;div class="ltag__user__summary"&gt;
      &lt;a class="ltag__user__link" href="/shricodev"&gt;SDE • GOLD @Microsoft Student Ambassador • Prev Lead Collab and Dev-Team Lead @oppiaorg • Mail for collaboration&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
