<?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: Sheen</title>
    <description>The latest articles on DEV Community by Sheen (@sheen_417f0f3a7f4).</description>
    <link>https://dev.to/sheen_417f0f3a7f4</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%2F3966525%2Fdda3dc1c-510e-4b86-ad3b-5a2f31f1556b.jpg</url>
      <title>DEV Community: Sheen</title>
      <link>https://dev.to/sheen_417f0f3a7f4</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sheen_417f0f3a7f4"/>
    <language>en</language>
    <item>
      <title>Hey Guys!! I am back again after a long break. Just published my latest post, show some love👀🤩</title>
      <dc:creator>Sheen</dc:creator>
      <pubDate>Wed, 21 Oct 2026 12:40:07 +0000</pubDate>
      <link>https://dev.to/sheen_417f0f3a7f4/hey-guys-i-am-back-again-after-a-long-break-just-published-my-latest-post-show-some-love-di6</link>
      <guid>https://dev.to/sheen_417f0f3a7f4/hey-guys-i-am-back-again-after-a-long-break-just-published-my-latest-post-show-some-love-di6</guid>
      <description></description>
    </item>
    <item>
      <title>How I Triage Release Notes Before Claude Code Turns Them Into a Product Video</title>
      <dc:creator>Sheen</dc:creator>
      <pubDate>Wed, 21 Oct 2026 12:35:31 +0000</pubDate>
      <link>https://dev.to/sheen_417f0f3a7f4/how-i-triage-release-notes-before-claude-code-turns-them-into-a-product-video-1h3</link>
      <guid>https://dev.to/sheen_417f0f3a7f4/how-i-triage-release-notes-before-claude-code-turns-them-into-a-product-video-1h3</guid>
      <description>&lt;p&gt;Ask an agent to make a video from &lt;code&gt;CHANGELOG.md&lt;/code&gt; and it will often treat every line as content, including the dependency bump. Nothing looks wrong until the render, where the React upgrade has six seconds of screen time.&lt;/p&gt;

&lt;p&gt;I would rather argue about a cut list than about a finished video. So I wrote a small script that sorts a changelog into keep, cut and "a human should look" before the agent sees it. The sorting turned out to be the boring part. The rows the script refuses to decide are the interesting ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the raw changelog is a bad input
&lt;/h2&gt;

&lt;p&gt;A changelog is written for the team that shipped the release. It records what happened, in the order a maintainer thinks about it, and nothing in the file says which entries a customer cares about.&lt;/p&gt;

&lt;p&gt;That leaves an agent two ways to go wrong. The first is coverage: it can treat every line as content. The second is confidence: when a line is thin, a model tends to fill the gap with a stronger headline than the line supports. A fix for one crash turns into "crashes are gone". Both mistakes are cheap to catch in a table and expensive to catch in a render.&lt;/p&gt;

&lt;h2&gt;
  
  
  The script
&lt;/h2&gt;

&lt;p&gt;It reads a changelog in the &lt;a href="https://keepachangelog.com/en/1.1.0/" rel="noopener noreferrer"&gt;Keep a Changelog&lt;/a&gt; layout, takes the newest dated release, and prints a verdict for every entry. It uses only the standard library.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Draft a keep/cut table for the newest dated release in a CHANGELOG.md.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;RELEASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^##\s+\[?(?P&amp;lt;ver&amp;gt;[^\]\s]+)\]?\s*(?:-\s*(?P&amp;lt;date&amp;gt;\d{4}-\d{2}-\d{2}))?(?P&amp;lt;tail&amp;gt;.*)$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;INTERNAL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b(bump|upgrade[sd]?|dependenc\w+|deps?|refactor\w*|lint\w*|chore|typo|tests?|ci|rename[sd]?)\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;JARGON&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\b(race condition|deadlock|mutex|queue|worker|thread|pool|cache|index|orm|migration script)\b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;newest_release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return (version, date, {group: [entries]}) for the first dated, non-yanked release.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;release&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RELEASE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;release&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yanked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ver&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&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;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;### &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^[-*]\s+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;release&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;release&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; \
                &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yanked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;release&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;found&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^[-*]\s+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;found&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;if&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;triage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Security&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ASK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a person decides if this goes on screen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;INTERNAL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reads as internal work&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&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="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REWRITE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;too vague to turn into a headline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;JARGON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REWRITE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;names a component customers never see&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Deprecated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Removed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SCENE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customers lose something, say it plainly&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SCENE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title card seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--per&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seconds per scene&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--cap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runtime cap in seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--strict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;store_true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;help&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exit 1 if ASK or REWRITE rows remain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;newest_release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No dated, non-yanked release found.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;groups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Release &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ver&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;| Group | Entry | Verdict | Why |&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|---|---|---|---|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;scenes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;open_rows&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="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;why&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;triage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;scenes&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SCENE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="n"&gt;open_rows&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ASK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REWRITE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;| &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; | &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;why&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; |&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;runtime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;scenes&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;per&lt;/span&gt;
    &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &amp;lt;- over the cap&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cap&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;scenes&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; scenes, about &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s of &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cap&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strict&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;open_rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rules are deliberately dumb:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Skips what shouldn't be read.&lt;/strong&gt; It ignores &lt;code&gt;[Unreleased]&lt;/code&gt;, because those lines haven't shipped. It also ignores any release marked &lt;code&gt;[YANKED]&lt;/code&gt;, because that release was pulled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CUT.&lt;/strong&gt; Words like bump, upgrade, refactor, lint and chore suggest internal work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;REWRITE.&lt;/strong&gt; An entry under four words is too thin for a headline. An entry that names a queue, a worker or a deadlock describes something customers never see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ASK.&lt;/strong&gt; Anything under Security goes to a person, every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The runtime budget.&lt;/strong&gt; A title card plus six seconds per scene, checked against a cap. This catches the "we have eight features and 40 seconds" problem before any scene exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A test run on an invented changelog
&lt;/h2&gt;

&lt;p&gt;Here is the file I tested with. The app, Fieldnote, is made up so there is no real customer data in it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## [Unreleased]&lt;/span&gt;

&lt;span class="gu"&gt;### Added&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Shared folders (behind a feature flag).

&lt;span class="gu"&gt;## [2.8.0] - 2026-10-14&lt;/span&gt;

&lt;span class="gu"&gt;### Added&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Offline mode for iOS.
&lt;span class="p"&gt;-&lt;/span&gt; Tag filters in search.

&lt;span class="gu"&gt;### Changed&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Upgrade React to 19.1.
&lt;span class="p"&gt;-&lt;/span&gt; Search results open faster on large libraries.

&lt;span class="gu"&gt;### Removed&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Dropped the legacy Markdown importer.

&lt;span class="gu"&gt;### Fixed&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Fix race condition in sync queue.
&lt;span class="p"&gt;-&lt;/span&gt; Fix crash when opening notes over 10,000 characters.
&lt;span class="p"&gt;-&lt;/span&gt; Misc fixes.

&lt;span class="gu"&gt;### Security&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Sanitize HTML in the note preview.

&lt;span class="gu"&gt;## [2.7.1] - 2026-09-30 [YANKED]&lt;/span&gt;

&lt;span class="gu"&gt;### Fixed&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Fix login redirect.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And what &lt;code&gt;python changelog_triage.py CHANGELOG.md&lt;/code&gt; printed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th&gt;Entry&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Added&lt;/td&gt;
&lt;td&gt;Offline mode for iOS.&lt;/td&gt;
&lt;td&gt;SCENE&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Added&lt;/td&gt;
&lt;td&gt;Tag filters in search.&lt;/td&gt;
&lt;td&gt;SCENE&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Changed&lt;/td&gt;
&lt;td&gt;Upgrade React to 19.1.&lt;/td&gt;
&lt;td&gt;CUT&lt;/td&gt;
&lt;td&gt;reads as internal work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Changed&lt;/td&gt;
&lt;td&gt;Search results open faster on large libraries.&lt;/td&gt;
&lt;td&gt;SCENE&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Removed&lt;/td&gt;
&lt;td&gt;Dropped the legacy Markdown importer.&lt;/td&gt;
&lt;td&gt;SCENE&lt;/td&gt;
&lt;td&gt;customers lose something, say it plainly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed&lt;/td&gt;
&lt;td&gt;Fix race condition in sync queue.&lt;/td&gt;
&lt;td&gt;REWRITE&lt;/td&gt;
&lt;td&gt;names a component customers never see&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed&lt;/td&gt;
&lt;td&gt;Fix crash when opening notes over 10,000 characters.&lt;/td&gt;
&lt;td&gt;SCENE&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed&lt;/td&gt;
&lt;td&gt;Misc fixes.&lt;/td&gt;
&lt;td&gt;REWRITE&lt;/td&gt;
&lt;td&gt;too vague to turn into a headline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;Sanitize HTML in the note preview.&lt;/td&gt;
&lt;td&gt;ASK&lt;/td&gt;
&lt;td&gt;a person decides if this goes on screen&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The footer reads &lt;code&gt;5 scenes, about 33s of 40s&lt;/code&gt;. The shared-folders flag and the yanked 2.7.1 never appear, which is the point.&lt;/p&gt;

&lt;p&gt;Two rows deserve a second look. "Search results open faster on large libraries" got a SCENE verdict, and the script is right to let it through. But a speed claim invites a number, and the note doesn't contain one. If an agent writes "twice as fast" on that scene, it made the number up. I would catch that in the brief, not in the render.&lt;/p&gt;

&lt;p&gt;The "Misc fixes" row is the other. No script can fix it. Someone who knows what shipped has to write the real line, and that is the whole argument for doing this triage before the agent runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hand the table to Claude Code
&lt;/h2&gt;

&lt;p&gt;Setup is the boring part and the docs cover it better than I would. In the CoAnimator app, Settings, General, Command line and agent skill has an install button for each, and the &lt;a href="https://coanimator.com/docs/cli" rel="noopener noreferrer"&gt;CLI docs&lt;/a&gt; cover the command. Claude Code comes from &lt;a href="https://code.claude.com/docs/en/setup" rel="noopener noreferrer"&gt;Anthropic's setup guide&lt;/a&gt;. If you want background on what the agent can do inside a project, &lt;a href="https://coanimator.com/blog/can-claude-code-make-videos" rel="noopener noreferrer"&gt;this post&lt;/a&gt; covers it.&lt;/p&gt;

&lt;p&gt;Then I would send the dated section, the table, and a brief like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Here is a triage table for Fieldnote 2.8.0 and the dated changelog section it came from. Treat the table as a proposal. For each row, confirm or overrule the verdict and give one sentence of reasoning. For REWRITE rows, propose a customer-facing line and quote the source words it came from. Do not state any number, speed or percentage that is not in the notes. Build nothing until I reply.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The last two instructions do most of the work. "Quote the source words" forces every headline to point at its entry, and "build nothing until I reply" turns the first answer into something you can correct for the price of one sentence. Each SCENE row also needs a screenshot or recording to attach, or the scene will be text on a background.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I still check by hand
&lt;/h2&gt;

&lt;p&gt;Once the build is approved, the render step is a single &lt;code&gt;coa render &amp;lt;project&amp;gt;&lt;/code&gt;. For a quick look at the title card, &lt;code&gt;--start 0 --end 6&lt;/code&gt; renders only the first six seconds. The script can't check the rest, so I go through these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Headline strength.&lt;/strong&gt; "Large notes no longer crash the app" matches the crash entry. "Crashes are gone" doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version and date against the tag.&lt;/strong&gt; The card can say 2.8.0 while the repo's latest tag is v2.8.1 after a hotfix. &lt;code&gt;git describe --tags --abbrev=0&lt;/code&gt; prints the latest tag in one second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text outside the scenes.&lt;/strong&gt; Closing cards and subtitles never passed through the table, and that is where stock claims like "faster than ever" show up.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Gate it in CI
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--strict&lt;/code&gt; makes the script exit 1 while any ASK or REWRITE row is unresolved. I tested the exit code locally, and I haven't wired it into a release pipeline yet. The step itself is two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Changelog triage&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python changelog_triage.py CHANGELOG.md --strict&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any Security entry fails strict mode on purpose, so a person has to look at it. Once someone has, run that release without &lt;code&gt;--strict&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The table prints to the job log, so whoever reviews the failed run sees exactly which rows need a person.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the heuristics break
&lt;/h2&gt;

&lt;p&gt;It matches words, not meaning, and it will be wrong in both directions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Add CI status badge to dashboard" gets CUT because of "CI", though customers see that badge.&lt;/li&gt;
&lt;li&gt;"Upgrade your plan from the app" gets CUT because of "upgrade".&lt;/li&gt;
&lt;li&gt;"Add index of recent files" gets REWRITE because of "index".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why the script proposes and the agent and I decide. It also can't tell whether a change is visible to a customer, because it only sees the words.&lt;/p&gt;

&lt;p&gt;One privacy point. The script runs locally, but whatever you paste into an agent can leave your machine for its provider. CoAnimator's &lt;a href="https://coanimator.com/docs/quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; says the same about files and frames sent to an AI provider. Delete embargoed rows from the table before pasting, and crop customer data out of screenshots.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does it work with changelogs that aren't in Keep a Changelog format?&lt;/strong&gt;&lt;br&gt;
It needs &lt;code&gt;## [version] - date&lt;/code&gt; headings and &lt;code&gt;###&lt;/code&gt; groups. GitHub's generated release notes have neither, so the parser would need a small change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the script replace reading the notes?&lt;/strong&gt;&lt;br&gt;
No. It's a first draft of the cut list so the conversation with the agent starts from something concrete.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need CoAnimator for this?&lt;/strong&gt;&lt;br&gt;
No. The table works with any agent or video workflow. I use CoAnimator for the render step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next step: let the PR decide
&lt;/h2&gt;

&lt;p&gt;Regex is a fallback. The better signal is the person who wrote the change. A &lt;code&gt;customer-facing&lt;/code&gt; label on the pull request, or a one-line release-note field in the PR template, would let the table come from labels and leave the regex to catch whatever nobody labeled. Then the triage happens at merge time, when the author still remembers why the change matters.&lt;/p&gt;

&lt;p&gt;Try the script on your last release and see which rows you disagree with. Those are the decisions your changelog never recorded. If you want to turn the approved rows into a video, the &lt;a href="https://coanimator.com/download" rel="noopener noreferrer"&gt;CoAnimator desktop app&lt;/a&gt; is where I'd render them or you can use your existing video workflow.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>python</category>
      <category>video</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Built My Own MCP Client Instead of Using Claude Desktop</title>
      <dc:creator>Sheen</dc:creator>
      <pubDate>Thu, 06 Aug 2026 11:53:30 +0000</pubDate>
      <link>https://dev.to/sheen_417f0f3a7f4/i-built-my-own-mcp-client-instead-of-using-claude-desktop-1e09</link>
      <guid>https://dev.to/sheen_417f0f3a7f4/i-built-my-own-mcp-client-instead-of-using-claude-desktop-1e09</guid>
      <description>&lt;p&gt;Two hours. That's how long I stared at &lt;code&gt;Connection refused&lt;/code&gt; before I gave up trying to debug Claude Desktop and just wrote my own client instead.&lt;/p&gt;

&lt;p&gt;The server was running. I'd verified the config. I'd restarted everything twice. The problem was I had no idea where in the chain it was actually failing. Transport? Handshake? Session init? I was guessing. And I hate guessing.&lt;/p&gt;

&lt;p&gt;Writing my own client didn't fix that specific incident (turned out to be a path issue in a JSON config file, obviously), but it meant I'd never be in that position again. Now when something breaks, I can read the stack trace instead of filing a mental bug report against a black box.&lt;/p&gt;

&lt;p&gt;This is what I built, what I learned, and what the official docs don't tell you.&lt;/p&gt;




&lt;h2&gt;
  
  
  First: What a client actually is
&lt;/h2&gt;

&lt;p&gt;I'll keep this short because most tutorials either skip it entirely or spend three paragraphs on it. MCP has three roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Host&lt;/strong&gt;: your application. A pipeline, a script, a product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server&lt;/strong&gt;: exposes tools, resources, and prompts to the model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client&lt;/strong&gt;: opens exactly one connection to exactly one server, negotiates the protocol, and shuttles tool calls back and forth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The one-to-one constraint is deliberate. A client doesn't fan out to multiple servers. It's scoped to one. If you want multiple servers, you run multiple clients (or use a gateway, more on that later). The reason this matters: it keeps connections sandboxed. A misbehaving server can't reach sideways into another server's session because there's no shared session to reach into.&lt;/p&gt;

&lt;p&gt;The client itself has no capabilities to advertise. All it does is translate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model output → tool call → server → result → back to model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The spec changed in July 2026 and most tutorials haven't caught up
&lt;/h3&gt;

&lt;p&gt;This is the thing I wish I'd known before I started reading examples.&lt;/p&gt;

&lt;p&gt;Tutorials from early 2026 and before show clients that open with an &lt;code&gt;initialize&lt;/code&gt; handshake and then pass an &lt;code&gt;Mcp-Session-Id&lt;/code&gt; header on every request after that. That session ID tied your client to one specific server instance. Fine for a single machine, painful for anything behind a load balancer.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/" rel="noopener noreferrer"&gt;July 28, 2026 spec update&lt;/a&gt; dropped both. No initialize handshake. No session ID. Protocol version and capabilities now travel in &lt;code&gt;_meta&lt;/code&gt; on every request, and remote server discovery moved to a new &lt;code&gt;server/discover&lt;/code&gt; call.&lt;/p&gt;

&lt;p&gt;For local stdio (which is the whole tutorial below), none of this changes your code right now. But if you copy an older tutorial and plan to extend it to Streamable HTTP later, you'll be starting from patterns that are already deprecated. Worth knowing before you build on top of them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why bother when Claude Desktop exists
&lt;/h2&gt;

&lt;p&gt;Fair question. Claude Desktop works. Cursor works. For the use case they're designed for (a human, a chat window, some tools), they're fine.&lt;/p&gt;

&lt;p&gt;I needed something different.&lt;/p&gt;

&lt;p&gt;My specific problem was a data pipeline that runs on a schedule. There's no chat window. No human in the loop. The pipeline needs to call MCP tools, process the output, and move on. You can't shoehorn Claude Desktop into that. The MCP connection has to live inside the pipeline code itself.&lt;/p&gt;

&lt;p&gt;Beyond my specific case, there are a few other situations where building your own client is the only real option:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you need to intercept tool output before it hits the model.&lt;/strong&gt; Prebuilt clients are pass-through by design. If a tool returns a payload with PII, internal metadata, or something oversized for your context budget, you have no way to filter it. A custom client gives you that interception point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you're embedding MCP into a product someone else will use.&lt;/strong&gt; You can't ship a product that requires users to install and configure Claude Desktop. The client logic has to be inside your application.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you need actual error messages.&lt;/strong&gt; This was my original motivation. I wanted a stack trace, not a mystery.&lt;/p&gt;




&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;p&gt;The official MCP SDK covers Python, TypeScript, Java, Kotlin, C#, Ruby, and Rust. I'm going with Python. Lower friction for a tutorial, and the &lt;code&gt;uv&lt;/code&gt; toolchain makes dependency management painless.&lt;/p&gt;

&lt;p&gt;You'll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python (latest stable) and &lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;&lt;code&gt;uv&lt;/code&gt;&lt;/a&gt; installed&lt;/li&gt;
&lt;li&gt;An Anthropic API key. Get one from &lt;a href="https://console.anthropic.com/settings/keys" rel="noopener noreferrer"&gt;console.anthropic.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;An MCP server to point it at. Your own works, Anthropic's weather quickstart server works, or browse &lt;a href="https://mcp360.ai/mcps" rel="noopener noreferrer"&gt;mcp360.ai/mcps&lt;/a&gt; if you want a real production server to test against instead of a demo&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1: Set up the project
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv init mcp-client
&lt;span class="nb"&gt;cd &lt;/span&gt;mcp-client
uv venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
uv add mcp anthropic python-dotenv
&lt;span class="nb"&gt;rm &lt;/span&gt;main.py
&lt;span class="nb"&gt;touch &lt;/span&gt;client.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before you write a single line of application code, deal with the API key:&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;echo&lt;/span&gt; &lt;span class="s2"&gt;"ANTHROPIC_API_KEY=your-key-here"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;".env"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; .gitignore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm putting this first because a leaked key is the most common MCP security failure I've seen discussed in the community. Not clever protocol exploits. Just keys ending up in repos.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: The client class skeleton
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;contextlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AsyncExitStack&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StdioServerParameters&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.client.stdio&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;stdio_client&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Anthropic&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;

&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MCPClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit_stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AsyncExitStack&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;anthropic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one thing here that every tutorial glosses over: &lt;code&gt;AsyncExitStack&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It's not boilerplate. It's the thing that makes cleanup reliable when something goes wrong mid-connection. Without it, failed connections leave hung processes behind that you can't kill without hunting PIDs. I learned this the annoying way.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Connecting to a server
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;connect_to_server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;server_script_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;is_python&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;server_script_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.py&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;is_js&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;server_script_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.js&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_python&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;is_js&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Server script must be a .py or .js file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_python&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;node&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;server_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StdioServerParameters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;server_script_path&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;stdio_transport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit_stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enter_async_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;stdio_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stdio_transport&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit_stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enter_async_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&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="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&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="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list_tools&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Connected. Available tools:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This spins up the server as a subprocess and talks to it over stdio. That's the right call when client and server are on the same machine.&lt;/p&gt;

&lt;p&gt;If your server is remote, the transport changes to Streamable HTTP, not SSE. I see a lot of tutorials still using SSE. The current spec supersedes it, so if you copy SSE-based transport code anywhere, flag it for replacement before you ship anything.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: The part that actually does things
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;

    &lt;span class="n"&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="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list_tools&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;available_tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inputSchema&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&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="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;available_tools&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;final_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;assistant_message_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;final_text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;assistant_message_content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tool_use&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;tool_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
            &lt;span class="n"&gt;tool_args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;

            &lt;span class="n"&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="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;final_text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; called]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;assistant_message_content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;assistant_message_content&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;
                &lt;span class="p"&gt;}]&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;

            &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&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="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;available_tools&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;final_text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&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="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&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="n"&gt;final_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The thing to pay attention to in that loop: the client doesn't decide whether to call a tool. Claude does. All the client does is execute what Claude asked for, package the result as a &lt;code&gt;tool_result&lt;/code&gt; message, and send everything back up for a final response.&lt;/p&gt;

&lt;p&gt;If you find yourself writing client-side logic to override or second-guess Claude's tool choice, that's almost always a sign the tool descriptions are unclear, not that the client logic is wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Chat loop, cleanup, entry point
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chat_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Running. Type &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;quit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; to exit.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Query: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;quit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="n"&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="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;process_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exit_stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aclose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Usage: python client.py &amp;lt;path_to_server_script&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MCPClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect_to_server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;try/except&lt;/code&gt; in the loop isn't optional. MCP tool calls fail for reasons the client has no control over: slow server responses, missing env vars on the server, malformed arguments. Without catching those, one bad query takes out the whole session. You want the session to survive a bad query, not restart from scratch.&lt;/p&gt;

&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run client.py path/to/your/server.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First response will take 20–30 seconds. The server subprocess is cold-starting, and Claude's first completion takes a moment. Subsequent queries are faster.&lt;/p&gt;




&lt;h2&gt;
  
  
  Errors I hit (and what they actually meant)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error&lt;/th&gt;
&lt;th&gt;What I thought&lt;/th&gt;
&lt;th&gt;What it actually was&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FileNotFoundError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Import problem&lt;/td&gt;
&lt;td&gt;Wrong path to the server script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Connection refused&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Port conflict&lt;/td&gt;
&lt;td&gt;Server subprocess wasn't launching at all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Tool execution failed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Client bug&lt;/td&gt;
&lt;td&gt;Server missing an env var&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Timeout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Server crashed&lt;/td&gt;
&lt;td&gt;Server was just slow; raising the timeout fixed it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The one that got me most: I was running the same server through Claude Desktop while testing my client. Claude Desktop has its own config file with its own paths. A stale path in one of them produces a &lt;code&gt;FileNotFoundError&lt;/code&gt; that looks like it's coming from your code. It isn't.&lt;/p&gt;

&lt;p&gt;If you're stuck on connection errors after double-checking paths, the problem is almost always server-side config, not the client code above. The &lt;a href="https://mcp360.ai/docs" rel="noopener noreferrer"&gt;MCP360 docs&lt;/a&gt; have a solid breakdown of the server-side setup issues that client-side debugging won't help you find.&lt;/p&gt;




&lt;h2&gt;
  
  
  The security stuff nobody puts in tutorials
&lt;/h2&gt;

&lt;p&gt;An MCP client hands a language model the ability to trigger real actions on real systems. That's the whole point, and it's also the risk.&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://www.mdpi.com/2624-800X/6/3/84" rel="noopener noreferrer"&gt;threat-modeling paper from May 2026&lt;/a&gt; found prompt injection attacks hiding inside tool &lt;em&gt;descriptions&lt;/em&gt; across seven major clients. Not in tool output. In the description field itself. The attack tells the model to execute something it normally wouldn't, and it works because most clients pass tool metadata straight to the model without any inspection. Across 13,875 servers analyzed, that's a real problem.&lt;/p&gt;

&lt;p&gt;What I actually do:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read tool descriptions before connecting.&lt;/strong&gt; A tool called &lt;code&gt;get_weather&lt;/code&gt; that describes write access to the filesystem doesn't make sense. That's a red flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimum-permission credentials.&lt;/strong&gt; Don't hand a server an API key that can do more than that server needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human confirmation before write/delete/send.&lt;/strong&gt; I add one &lt;code&gt;input("Confirm? y/n: ")&lt;/code&gt; before any tool call that's irreversible. One line. Worth it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.env&lt;/code&gt; in &lt;code&gt;.gitignore&lt;/code&gt; before first commit.&lt;/strong&gt; Not after.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What breaks next and how to fix it
&lt;/h2&gt;

&lt;p&gt;The client above handles one server. The moment you need two, you're copy-pasting the connection block. Three servers, you're doing it again. At some point that becomes its own maintenance problem.&lt;/p&gt;

&lt;p&gt;The pattern most people land on is a gateway: one connection point that sits in front of multiple servers, handles auth centrally, and exposes everything through a single endpoint. Your client code above doesn't change at all. You just point it at the gateway URL instead of individual server scripts. &lt;a href="https://mcp360.ai" rel="noopener noreferrer"&gt;MCP360&lt;/a&gt; is built around that model if you want something you can set up quickly rather than build from scratch.&lt;/p&gt;

&lt;p&gt;The other thing that breaks: config file sprawl. Claude Desktop, Cursor, and Windsurf all store MCP server configs in different paths. If you're running servers across more than one of those clients, keeping configs in sync gets tedious. &lt;a href="https://mcp360.ai/mtarsier" rel="noopener noreferrer"&gt;mTarsier&lt;/a&gt; is an open-source desktop tool that reads all of them from one place. It's from the MCP360 team and it's free.&lt;/p&gt;

&lt;p&gt;For transport, the next step after stdio is Streamable HTTP. When you make that move, strip out any session ID assumptions from your connection logic. The &lt;a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/" rel="noopener noreferrer"&gt;July 2026 spec&lt;/a&gt; removed session pinning entirely, so anything that depends on &lt;code&gt;Mcp-Session-Id&lt;/code&gt; will fail against a compliant server.&lt;/p&gt;




&lt;p&gt;That's the full build. The code above is about 80 lines total and handles the complete client lifecycle. From here the interesting problems aren't in the client. They're in what you connect it to and what you do with the results.&lt;/p&gt;

&lt;p&gt;What are you building this for? Curious what use cases people are actually running MCP against.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>mcp</category>
      <category>claude</category>
      <category>python</category>
    </item>
    <item>
      <title>The biggest thing I got wrong about MCP? Thinking it was trying to replace REST APIs. After building with both, I realized it's a new layer for AI, not a replacement for existing APIs.</title>
      <dc:creator>Sheen</dc:creator>
      <pubDate>Wed, 22 Jul 2026 12:01:21 +0000</pubDate>
      <link>https://dev.to/sheen_417f0f3a7f4/the-biggest-thing-i-got-wrong-about-mcp-thinking-it-was-trying-to-replace-rest-apis-after-1l05</link>
      <guid>https://dev.to/sheen_417f0f3a7f4/the-biggest-thing-i-got-wrong-about-mcp-thinking-it-was-trying-to-replace-rest-apis-after-1l05</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/sheen_417f0f3a7f4/stop-comparing-mcp-to-rest-apis-theyre-not-competing-527i" class="crayons-story__hidden-navigation-link"&gt;Stop Comparing MCP to REST APIs! They're Not Competing.&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/sheen_417f0f3a7f4" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F3966525%2Fdda3dc1c-510e-4b86-ad3b-5a2f31f1556b.jpg" alt="sheen_417f0f3a7f4 profile" class="crayons-avatar__image" width="365" height="547"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/sheen_417f0f3a7f4" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Sheen
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Sheen
                
                
              
              &lt;div id="story-author-preview-content-4205900" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/sheen_417f0f3a7f4" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F3966525%2Fdda3dc1c-510e-4b86-ad3b-5a2f31f1556b.jpg" class="crayons-avatar__image" alt="" width="365" height="547"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Sheen&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/sheen_417f0f3a7f4/stop-comparing-mcp-to-rest-apis-theyre-not-competing-527i" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 22&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/sheen_417f0f3a7f4/stop-comparing-mcp-to-rest-apis-theyre-not-competing-527i" id="article-link-4205900"&gt;
          Stop Comparing MCP to REST APIs! They're Not Competing.
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/mcp"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;mcp&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/api"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;api&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/sheen_417f0f3a7f4/stop-comparing-mcp-to-rest-apis-theyre-not-competing-527i" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/sheen_417f0f3a7f4/stop-comparing-mcp-to-rest-apis-theyre-not-competing-527i#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Stop Comparing MCP to REST APIs! They're Not Competing.</title>
      <dc:creator>Sheen</dc:creator>
      <pubDate>Wed, 22 Jul 2026 11:53:20 +0000</pubDate>
      <link>https://dev.to/sheen_417f0f3a7f4/stop-comparing-mcp-to-rest-apis-theyre-not-competing-527i</link>
      <guid>https://dev.to/sheen_417f0f3a7f4/stop-comparing-mcp-to-rest-apis-theyre-not-competing-527i</guid>
      <description>&lt;p&gt;MCP keeps coming up in every AI dev conversation right now. And almost every time, someone asks: "So does this replace REST APIs?"&lt;/p&gt;

&lt;p&gt;No. And the comparison itself reveals a misunderstanding worth clearing up - one I ran into myself when I first started building with MCP.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; REST APIs are for developer-to-system calls. MCP is for model-to-system calls. They're not competing - in most setups, MCP wraps your existing REST APIs. The math shifts from N×M integrations to N+M. That's the whole game.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Problem I Kept Running Into
&lt;/h2&gt;

&lt;p&gt;When I first tried giving an LLM a raw REST API to work with, it kept hallucinating parameter names, calling the wrong endpoints, and mangling request bodies - not because the model was broken, but because raw HTTP endpoints carry zero semantic context.&lt;/p&gt;

&lt;p&gt;I read &lt;code&gt;POST /tickets&lt;/code&gt; and checked the docs. The model couldn't do that reliably at runtime. There's nothing in the endpoint that tells the model what &lt;code&gt;priority&lt;/code&gt; means, which values are valid, or when to use this endpoint versus &lt;code&gt;POST /issues&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;MCP fixes this by replacing endpoints with &lt;strong&gt;named tools&lt;/strong&gt;. Instead of &lt;code&gt;POST /tickets&lt;/code&gt;, the model calls &lt;code&gt;create_support_ticket&lt;/code&gt; with typed, described inputs: &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;priority&lt;/code&gt; (one of: low, medium, high, critical), &lt;code&gt;description&lt;/code&gt;. Every parameter has a description written for the model, not a human developer.&lt;/p&gt;

&lt;p&gt;The model now knows what the tool does, what it expects, and when to use it. That context is what made the difference between reliable tool use and constant hallucination in my own builds.&lt;/p&gt;




&lt;h2&gt;
  
  
  What They Each Actually Do
&lt;/h2&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;REST / GraphQL / gRPC&lt;/th&gt;
&lt;th&gt;MCP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Designed for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Developer-to-system communication&lt;/td&gt;
&lt;td&gt;Model-to-system communication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Caller&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Code you wrote&lt;/td&gt;
&lt;td&gt;An AI agent at runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Discovery&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You read the docs&lt;/td&gt;
&lt;td&gt;Model discovers tools dynamically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None - caller already knows the schema&lt;/td&gt;
&lt;td&gt;Rich - names, descriptions, types per parameter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stable, performance-critical integrations&lt;/td&gt;
&lt;td&gt;Agentic workflows, multi-tool orchestration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auth model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OAuth, API keys, RBAC (mature)&lt;/td&gt;
&lt;td&gt;Still maturing - prompt injection is a real risk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Debugging&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Standard HTTP tooling&lt;/td&gt;
&lt;td&gt;Depends on the client implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Replaces the other?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The right framing isn't "which one" but "which layer." Most production setups I've seen (and built) that use MCP still have REST APIs running underneath - the MCP server is a translation layer that exposes those APIs to models in a format they can actually use reliably.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Quick Code Comparison
&lt;/h2&gt;

&lt;p&gt;Here's what the difference looks like in practice. Same action - creating a support ticket - handled two ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Raw REST call from your agent code:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Agent has to know the endpoint, headers, body shape, and error handling&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.example.com/tickets&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;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;Authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&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="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="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;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;Login broken&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// what does 2 mean? the model doesn't know&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;support&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;     &lt;span class="c1"&gt;// is this required? the model isn't sure&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;strong&gt;MCP tool definition:&lt;/strong&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;"create_support_ticket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Creates a new support ticket. Use when a customer reports a bug or needs help."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputSchema"&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;"title"&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;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Short summary of the issue"&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;"priority"&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;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"enum"&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;"low"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"medium"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"critical"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"How urgently this needs attention"&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;The model calling the MCP tool knows exactly what to pass and why. The model hitting the raw REST endpoint is guessing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The N×M Problem (This Is the Real Insight)
&lt;/h2&gt;

&lt;p&gt;Before I understood MCP properly, I kept rebuilding the same integrations.&lt;/p&gt;

&lt;p&gt;If you have &lt;strong&gt;N models&lt;/strong&gt; and &lt;strong&gt;M tools&lt;/strong&gt;, the traditional approach needs &lt;strong&gt;N×M integrations&lt;/strong&gt;. Each model needs its own connection to each tool: its own auth logic, its own schema handling, its own error handling. Every new tool means N new integrations. Every new model means M new integrations.&lt;/p&gt;

&lt;p&gt;MCP changes the math. Each model implements an MCP client once. Each tool implements an MCP server once. Any client can talk to any server because they share the same protocol.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;N×M becomes N+M.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the actual value proposition - not "MCP vs REST" but "a shared protocol that stops you from rebuilding the same integrations over and over."&lt;/p&gt;




&lt;h2&gt;
  
  
  The Part Most Articles Skip: Dynamic Discovery
&lt;/h2&gt;

&lt;p&gt;REST APIs are static contracts. You define endpoints, the client knows them, done.&lt;/p&gt;

&lt;p&gt;MCP supports &lt;strong&gt;runtime discovery&lt;/strong&gt;. When my agent connects to an MCP server, the server advertises its available tools, their input schemas, and what they return. The agent learns what capabilities exist without prior configuration.&lt;/p&gt;

&lt;p&gt;This matters for agentic workflows where the set of available tools changes. I can spin up a new MCP server, connect it to the gateway, and the model discovers it automatically. No code changes on the client side.&lt;/p&gt;

&lt;p&gt;This is borrowed from the Language Server Protocol (LSP) - the same mechanism that lets your editor support a new programming language without being rewritten. Once I made that connection, MCP clicked for me immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Concrete Example From Something I Built
&lt;/h2&gt;

&lt;p&gt;I worked on a support automation agent that needed to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Look up a customer record&lt;/li&gt;
&lt;li&gt;Check their open tickets&lt;/li&gt;
&lt;li&gt;Create a new ticket if none exists&lt;/li&gt;
&lt;li&gt;Notify the customer by email&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;With raw REST:&lt;/strong&gt; My agent code needed to know four endpoints, handle four different auth schemes, parse four response formats, and implement retry logic for each. Every time one of those APIs changed, I updated integration code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With MCP:&lt;/strong&gt; I exposed four tools - &lt;code&gt;get_customer&lt;/code&gt;, &lt;code&gt;list_tickets&lt;/code&gt;, &lt;code&gt;create_ticket&lt;/code&gt;, &lt;code&gt;send_notification&lt;/code&gt; - each with clear descriptions and typed parameters. The agent calls them by name. The MCP server handles the actual HTTP calls. When the underlying API changed, I updated the MCP server, not the agent.&lt;/p&gt;

&lt;p&gt;The agent stayed clean. The tools stayed focused. The model could actually use them reliably.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Use When
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Traditional APIs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connecting two services in code I control&lt;/li&gt;
&lt;li&gt;Performance and latency are the primary constraint&lt;/li&gt;
&lt;li&gt;I need ACID transactions (SOAP/gRPC territory)&lt;/li&gt;
&lt;li&gt;The caller always knows exactly which endpoint to call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;MCP:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An AI agent is the caller&lt;/li&gt;
&lt;li&gt;The set of tools changes or grows over time&lt;/li&gt;
&lt;li&gt;I'm building multi-step agentic workflows&lt;/li&gt;
&lt;li&gt;I want one integration point for multiple AI clients (Claude Desktop, Cursor, custom agents)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Both together (most common in my work):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;My MCP server wraps existing REST APIs - this is almost always the pattern&lt;/li&gt;
&lt;li&gt;REST handles structured service-to-service calls, MCP handles agent-to-tool calls&lt;/li&gt;
&lt;li&gt;I want AI capabilities without rearchitecting my backend&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where I Think This Is Going
&lt;/h2&gt;

&lt;p&gt;REST isn't going anywhere. It handles core business logic where consistency matters. MCP adds a layer on top - one that makes AI agents first-class consumers of existing infrastructure without requiring them to understand API schemas at runtime.&lt;/p&gt;

&lt;p&gt;If you're already running REST APIs internally, converting them to MCP tools is mostly editorial work: decide which operations to expose, write tool names and parameter descriptions that a model can interpret, and test how the model actually calls them before shipping. The backend doesn't change.&lt;/p&gt;

&lt;p&gt;The interesting design question I keep running into isn't whether to use MCP or APIs - it's &lt;em&gt;which operations deserve to be MCP tools&lt;/em&gt;, how precisely to describe their parameters, and how to scope what the agent can and cannot do.&lt;/p&gt;

&lt;p&gt;If you want to go deeper on MCP - tool design patterns, server architecture, real integration examples - I've been writing about all of it over at &lt;a href="https://mcp360.ai" rel="noopener noreferrer"&gt;MCP360&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;💬 &lt;strong&gt;Have you built an MCP server on top of existing REST APIs? What broke first - the tool descriptions, the auth, or something else entirely? Drop it in the comments, I'm genuinely curious what failure modes look like across different stacks.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>api</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Add an AI Chatbot to Shopify: Complete Setup Guide for 2026</title>
      <dc:creator>Sheen</dc:creator>
      <pubDate>Tue, 16 Jun 2026 06:31:20 +0000</pubDate>
      <link>https://dev.to/sheen_417f0f3a7f4/how-to-add-an-ai-chatbot-to-shopify-complete-setup-guide-for-2026-10ag</link>
      <guid>https://dev.to/sheen_417f0f3a7f4/how-to-add-an-ai-chatbot-to-shopify-complete-setup-guide-for-2026-10ag</guid>
      <description>&lt;p&gt;A Shopify AI chatbot is a conversational assistant that has a direct connection to your store’s products, policies, and customer data. AI chatbots can understand customer questions in natural language and respond to them with the information that is available in your Shopify store, unlike traditional chat widgets that rely on pre-written scripts.&lt;/p&gt;

&lt;p&gt;Most Shopify AI chatbots work by combining three sources of information: your product catalog, your support content, and live store data. After connecting to Shopify, the chatbot can access product details, inventory status, shipping policies, FAQs, and order information. When a customer asks a question, the chatbot retrieves the relevant information and generates a response in real time. More sophisticated setups can also trigger actions such as checking an order status, recommending products or handing the conversation over to a support agent.&lt;/p&gt;

&lt;p&gt;For Shopify merchants, the value is practical, not theoretical. Customers can find answers without leaving the product page, track orders without submitting a support ticket, and find the right products faster. Support teams spend less time answering repetitive questions and more time dealing with exceptions needing human judgment. This means a quicker buying experience for customers and less manual work for your team.&lt;/p&gt;

&lt;p&gt;In this guide, you'll learn how to connect YourGPT to Shopify, train it on your store content, setup automated workflow and launch an AI chatbot capable of assisting customers across your storefront and other channels like whatsapp, instagram, etc.&lt;/p&gt;


&lt;div class="crayons-card c-embed"&gt;

  
&lt;h2&gt;
  
  
  &lt;strong&gt;What You'll Need Before You Start&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before setting up your Shopify AI chatbot, make sure you have the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A Shopify store with admin access&lt;/strong&gt; so you can edit your theme files and publish changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A YourGPT account&lt;/strong&gt; to create, train, and manage your chatbot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Help center content or FAQs&lt;/strong&gt; that answer common customer questions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store policies&lt;/strong&gt;, including shipping, returns, refunds, and contact information.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Product information&lt;/strong&gt;, such as descriptions, collections, and sizing details, if you want the chatbot to answer product-specific questions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your support content is incomplete or outdated, update it before training the chatbot. The quality of your chatbot's responses depends on the information you provide.&lt;br&gt;

&lt;/p&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  &lt;strong&gt;How to Set Up an AI Chatbot for Shopify&lt;/strong&gt;
&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%2Fuwy8bddzu0pcwhjc5b66.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%2Fuwy8bddzu0pcwhjc5b66.png" alt="screenshot of dashboard of yourgpt on the Shopify integration page" width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Following are the steps to add an AI Chatbot to your Shopify store using YourGPT:&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create Your YourGPT Workspace
&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%2Ffjwrd38zvb40rykaoyfv.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%2Ffjwrd38zvb40rykaoyfv.png" alt="screenshot of dashboard of yourgpt" width="800" height="396"&gt;&lt;/a&gt;&lt;br&gt;
Go to &lt;a href="https://yourgpt.ai/" rel="noopener noreferrer"&gt;YourGPT&lt;/a&gt; and create an account. Once you're signed in, you'll have access to the chatbot builder, training tools, and integrations dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Train Your Chatbot with Store Content
&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%2Fzh6zexwk2gaaq85ghzwj.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%2Fzh6zexwk2gaaq85ghzwj.png" alt="screenshot of dashboard of yourgpt while training the chatbot " width="800" height="465"&gt;&lt;/a&gt;&lt;br&gt;
Add the information your chatbot will use to answer customer questions.&lt;br&gt;
Import your help center articles, FAQs, product descriptions, shipping details, return policies, and other customer-facing content. You can upload documents, add website links, or connect your sitemap.&lt;br&gt;
The chatbot uses this content to generate responses, so review your information before importing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Customize the Chatbot
&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%2Fqeppbnx1zruvq49w4ib7.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%2Fqeppbnx1zruvq49w4ib7.png" alt="screenshot of dashboard of yourgpt of customising the chatbot window" width="800" height="465"&gt;&lt;/a&gt;&lt;br&gt;
Adjust the chatbot to match your store.&lt;br&gt;
You can update the chatbot name, welcome message, and brand colors. If needed, add quick replies or lead capture fields through the visual editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Generate and Copy the Chatbot Script
&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%2F4n6wxurx3kqephmno0pq.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%2F4n6wxurx3kqephmno0pq.png" alt="The YourGPT integration dashboard showing the copyable widget script block" width="799" height="447"&gt;&lt;/a&gt;&lt;br&gt;
Open the &lt;strong&gt;Integrations&lt;/strong&gt; section in your YourGPT dashboard and copy the widget script for your chatbot.&lt;br&gt;
You'll add this code to your Shopify theme in the next steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Access Your Shopify Theme Code
&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%2Fce708tltriw5zrh6kdxv.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%2Fce708tltriw5zrh6kdxv.png" alt="screenshot of dashboard of accessing the shopify theme code" width="800" height="468"&gt;&lt;/a&gt;&lt;br&gt;
In your Shopify admin panel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to &lt;strong&gt;Online Store → Themes&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Find your active theme.&lt;/li&gt;
&lt;li&gt;Open the options menu next to the theme.&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Edit code&lt;/strong&gt;.&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%2F4oqdw7qad5dlerehx5ol.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%2F4oqdw7qad5dlerehx5ol.png" alt="screenshot of dashboard of editing the theme code" width="799" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Locate the theme.liquid File
&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%2F4n01nlgp0e46pdqrkej1.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%2F4n01nlgp0e46pdqrkej1.png" alt="screenshot of the dashboard of the layout folder locating the theme. liquid file" width="800" height="455"&gt;&lt;/a&gt;&lt;br&gt;
In the code editor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open the &lt;strong&gt;Layout folder&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;&lt;code&gt;theme.liquid&lt;/code&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Scroll to the bottom of the file.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 7: Paste the Chatbot Script
&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%2Fmb65jacf9ibmhfzgl2th.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%2Fmb65jacf9ibmhfzgl2th.png" alt="screenshot of dashboard of pasting the chatbot script" width="800" height="457"&gt;&lt;/a&gt;&lt;br&gt;
Paste the YourGPT widget script directly above the closing &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&gt; tag.&lt;br&gt;
Double-check that the entire script has been added without changing any existing code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: Save Changes and Test Your Chatbot
&lt;/h2&gt;

&lt;p&gt;Click &lt;strong&gt;Save&lt;/strong&gt; in the top-right corner of the editor.&lt;br&gt;
Return to your storefront and refresh the page. Your chatbot should now appear and be ready to answer customer questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Real Use Cases: What Your Shopify AI Chatbot Can Handle&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Following are the real use cases your shopify AI chatbot can handle:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product Recommendations&lt;/strong&gt;&lt;br&gt;
Customers often have trouble deciding between similar products, or figuring out the right size or comparing features. Your chatbot can answer questions about your product and recommend suitable products based on your customer’s needs, budget or preferences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Order Tracking and Shipping Updates&lt;/strong&gt;&lt;br&gt;
Customers shouldn't have to contact support to check an order status. When combined with information from your store, your bot can provide shipping info, estimated delivery dates, and links to tracking pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Returns and Refund Requests&lt;/strong&gt;&lt;br&gt;
It’s normal to have questions about returns, especially once you’ve bought something. Your chatbot can explain your return policy, guide you through the process for refunds, and direct customers to the right support channel when an issue requires manual review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cart Recovery&lt;/strong&gt;&lt;br&gt;
Some shoppers leave because they can’t find the information they want. Your chatbot is able to answer last-minute questions about pricing, shipping costs, delivery times or product details that could otherwise prevent a purchase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common Customer Support Questions&lt;/strong&gt;&lt;br&gt;
Your chatbot will be able to answer frequently asked questions about store policies, payment methods, dimensions, contact details, business hours and account management. If a request requires human assistance, the chatbot can gather key information and pass the conversation to your team.&lt;/p&gt;

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

&lt;p&gt;Adding an AI &lt;a href="https://chatbot.yourgpt.ai/dashboard" rel="noopener noreferrer"&gt;chatbot&lt;/a&gt; to your Shopify store does not require custom development or a complex setup process. With YourGPT, you can train a chatbot on your existing content, add it to your Shopify theme, and start assisting customers directly on your storefront.&lt;/p&gt;

&lt;p&gt;The quality of your chatbot depends on the information you feed. Keep your FAQs, product details, and store policies up to date, and review customer conversations regularly to identify gaps or outdated responses.&lt;/p&gt;

&lt;p&gt;Once your chatbot is live, start with the questions your support team answers most often. This helps you improve response quality over time and gives customers faster access to the information they need.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;FAQ's&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I need coding skills to add an AI chatbot to Shopify?&lt;/strong&gt;&lt;br&gt;
No. You only need to copy the chatbot script from YourGPT and paste it into your Shopify theme file. No custom development or app installation is required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I train a Shopify AI chatbot?&lt;/strong&gt;&lt;br&gt;
Train your chatbot using the content your customers already rely on, including FAQs, help center articles, product descriptions, shipping information, and return policies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use my existing FAQs and help center content?&lt;/strong&gt;&lt;br&gt;
Yes. YourGPT can use your existing support content, so you do not need to create a separate knowledge base from scratch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What content sources and file formats does YourGPT support?&lt;/strong&gt;&lt;br&gt;
You can train YourGPT using website URLs, sitemaps, PDFs, documents, and direct text input. This makes it easy to import content from your help center, product pages, and policy documents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can a Shopify AI chatbot track orders?&lt;/strong&gt;&lt;br&gt;
Yes. YourGPT can access Shopify order data to help customers check order status, delivery updates, and tracking information without contacting your support team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can the chatbot answer product questions?&lt;/strong&gt;&lt;br&gt;
Yes. After training, the chatbot can answer questions about product features, sizing, pricing, availability, and store policies based on the content you provide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where do I add the chatbot script in Shopify?&lt;/strong&gt;&lt;br&gt;
Paste the widget script in the theme.liquid file, directly above the closing  tag.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>shopify</category>
      <category>agents</category>
      <category>automation</category>
    </item>
    <item>
      <title>How to Convert Any REST API into a MCP Server</title>
      <dc:creator>Sheen</dc:creator>
      <pubDate>Wed, 03 Jun 2026 13:25:25 +0000</pubDate>
      <link>https://dev.to/sheen_417f0f3a7f4/how-to-convert-any-rest-api-into-an-mcp-server-2ilm</link>
      <guid>https://dev.to/sheen_417f0f3a7f4/how-to-convert-any-rest-api-into-an-mcp-server-2ilm</guid>
      <description>&lt;p&gt;APIs were built for developers, not AI agents. A developer reads the docs, figures out which endpoint to call, and knows what to put in the request body. An LLM does not have that luxury. Give it a raw REST API and it will hallucinate parameter names, call the wrong endpoint, or format the request body incorrectly, not because the model is bad, but because raw HTTP endpoints carry zero semantic context.&lt;/p&gt;

&lt;p&gt;This is the problem MCP solves. Instead of pointing an AI client at endpoints, you expose named tools with explicit input definitions. The model knows what each tool does, what it expects, and what it returns. No guessing.&lt;/p&gt;

&lt;p&gt;This guide walks through converting an existing REST API into an MCP server using MCP360, without touching the backend.&lt;/p&gt;


&lt;div class="crayons-card c-embed"&gt;

  
&lt;h2&gt;
  
  
  What MCP Actually Changes
&lt;/h2&gt;

&lt;p&gt;The API itself does not change. The backend stays exactly as it is.&lt;br&gt;
What MCP adds is a translation layer. Instead of POST /tickets, the AI client calls a tool named create_support_ticket with typed inputs: title, priority, and description. The tool definition tells the model what each field means, what is required, and what format they expect.&lt;br&gt;
That context is what makes the difference. Without it, the model is guessing. With it, the model can reliably select the right tool and populate it correctly.&lt;br&gt;
The work of converting a REST API to MCP is mostly editorial: decide which operations to expose, write clear tool names and descriptions, and define inputs tightly enough that there is no ambiguity.&lt;br&gt;

&lt;/p&gt;
&lt;/div&gt;


&lt;h3&gt;
  
  
  &lt;strong&gt;Step 1: Create a New MCP Server in MCP360&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Log in to &lt;a href="https://dashboard.mcp360.ai/login" rel="noopener noreferrer"&gt;MCP360&lt;/a&gt;, open the MCP Servers section, and click Create New Server.&lt;/li&gt;
&lt;li&gt;Select &lt;a href="https://mcp360.ai/docs" rel="noopener noreferrer"&gt;Custom MCP Server&lt;/a&gt;, fill in the name and description, and click Create MCP.&lt;/li&gt;
&lt;li&gt;MCP360 generates your MCP endpoint and opens the configuration screen.&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%2Fo57o1ftssowqllfybg98.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%2Fo57o1ftssowqllfybg98.png" alt="Screenshot of the MCP360 dashboard showing the " width="799" height="347"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Step 2: Go to the Tools Section&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Inside the MCP configuration page, navigate to Tools and click Import Tools.&lt;/li&gt;
&lt;li&gt;This is where REST endpoints get mapped to MCP tools.
&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%2F94ulr4e2qlhfivby5p38.png" alt="Screenshot of the MCP360 dashboard showing the custom mcp tools section" width="799" height="535"&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Step 3: Select Custom API Tool&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;In the Import Tools window, choose Custom API Tool.&lt;/li&gt;
&lt;li&gt;This lets you point MCP360 at any REST endpoint and define how it should be exposed as a 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%2Fm7fum3msqm8ayf5ufrym.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%2Fm7fum3msqm8ayf5ufrym.png" alt="Screenshot of the MCP360 dashboard showing Import Tools window" width="800" height="700"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Step 4: Configure the Tool&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Fill in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tool name : use something descriptive, like get_customer_orders not get_data&lt;/li&gt;
&lt;li&gt;Tool description : Write this for the model, not for a human developer. Be explicit about what the tool does and when to use it&lt;/li&gt;
&lt;li&gt;Request method : GET, POST, etc.&lt;/li&gt;
&lt;li&gt;Endpoint URL : the full API endpoint&lt;/li&gt;
&lt;li&gt;If the API needs authentication, add the required headers here.&lt;/li&gt;
&lt;li&gt;Then configure AI parameters. These are the input fields the model will populate when it calls the tool. For each parameter, set a clear name and description. This is the part most people rush through and then wonder why the model keeps sending wrong values.&lt;/li&gt;
&lt;li&gt;A parameter named id with no description is useless to a model. A parameter named customer_id described as "the unique identifier for the customer, found in the user profile response," is not.
&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%2Fnxi4m4hiiv8nhnx7lbrs.png" alt="Screenshot of the MCP360 dashboard showing Tool configuration window" width="800" height="608"&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Step 5: Import the Tool&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Review everything, then click Import Tool.&lt;/li&gt;
&lt;li&gt;MCP360 creates the tool from your configuration. It will show up in the Tools section once imported.&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%2Fgqhxgea5tiz0pw84tv62.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%2Fgqhxgea5tiz0pw84tv62.png" alt="Screenshot of the MCP360 dashboard showing Tool import window" width="800" height="515"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Step 6: Test Before You Connect Anything&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Do not connect an AI client until you have tested the tool manually.&lt;/li&gt;
&lt;li&gt;Open the Tools section, find your tool, and click Test API.&lt;/li&gt;
&lt;li&gt;This fires the actual HTTP request and shows you exactly what comes back. It is much faster to debug a bad header or wrong parameter here than after hooking it up to a model.&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%2Fw8thit5wqdhek283r11n.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%2Fw8thit5wqdhek283r11n.png" alt="Screenshot of the MCP360 dashboard showing Tools section and test api window" width="800" height="521"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Step 7: Check the Test Response&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Request status: 200 OK&lt;/li&gt;
&lt;li&gt;Response data returned in the panel&lt;/li&gt;
&lt;li&gt;Valid JSON output&lt;/li&gt;
&lt;li&gt;If all three check out, the MCP tool is communicating with the API correctly and is ready to use.&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%2Fxvw8zqddaf6jqjqwoptj.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%2Fxvw8zqddaf6jqjqwoptj.png" alt="Screenshot of the MCP360 dashboard showing the tool working window showing test api successful" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Common Errors and how to fix them&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Auth failures:&lt;/strong&gt;&lt;br&gt;
Check the API key, the Authorization header format, and whether the API allows requests from external sources. Most auth issues come down to a missing Bearer prefix or a header key typed incorrectly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Wrong endpoint config:&lt;/strong&gt;&lt;br&gt;
Confirm the URL, verify the HTTP method matches what the API actually expects, and make sure the API returns JSON. A GET configured as POST will fail silently in some cases, which makes it harder to diagnose.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Parameter mismatches:&lt;/strong&gt;&lt;br&gt;
If the tool runs but returns garbage, the AI parameters are probably mapped to the wrong fields. Walk through each one and confirm it points to the right key in the request body or query string. Also check whether any parameters are hardcoded that should be dynamic; if the model is expected to supply a value, it cannot be fixed in the config.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The actual conversion work here is not technical. The backend does not change. What takes judgment is deciding which endpoints to expose, how to name and describe tools clearly, and how to define parameters in a way the model can use without ambiguity.&lt;br&gt;
Start with two or three high-value operations. Test them properly. See how the model actually calls them before adding more. A small set of well-defined tools is significantly more reliable than a large set of poorly defined ones.&lt;br&gt;
MCP360 handles the infrastructure so you are not writing server code from scratch, but the quality of the tools you get out depends entirely on how precisely you define them going in.&lt;/p&gt;

</description>
      <category>api</category>
      <category>llm</category>
      <category>mcp</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
