<?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: Furkan Köykıran</title>
    <description>The latest articles on DEV Community by Furkan Köykıran (@furkankoykiran).</description>
    <link>https://dev.to/furkankoykiran</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%2F3757458%2F729acfee-83c6-4805-971d-c7d9090d2bed.jpg</url>
      <title>DEV Community: Furkan Köykıran</title>
      <link>https://dev.to/furkankoykiran</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/furkankoykiran"/>
    <language>en</language>
    <item>
      <title>GitHub Actions Self-Hosted Runner on Linux: Setup Guide</title>
      <dc:creator>Furkan Köykıran</dc:creator>
      <pubDate>Wed, 06 May 2026 15:53:45 +0000</pubDate>
      <link>https://dev.to/furkankoykiran/free-ci-minutes-are-gone-setting-up-a-github-actions-self-hosted-runner-on-linux-4a6l</link>
      <guid>https://dev.to/furkankoykiran/free-ci-minutes-are-gone-setting-up-a-github-actions-self-hosted-runner-on-linux-4a6l</guid>
      <description>&lt;p&gt;GitHub Actions gives private repositories a monthly quota of free minutes: 2,000 on the Free plan, 3,000 on Pro and Team. Run a multi-step CI pipeline on every push and every PR and the quota goes faster than you'd expect. Mine ran out in May 2026. Instead of paying for more minutes, I turned a Linux server I already had running into a self-hosted runner. This guide walks through the setup step by step: a dedicated user, checksum verification, a systemd service, labels, and the security cost of a runner that keeps its disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a self-hosted runner
&lt;/h2&gt;

&lt;p&gt;According to GitHub's &lt;a href="https://docs.github.com/en/billing/concepts/product-billing/github-actions" rel="noopener noreferrer"&gt;billing docs&lt;/a&gt;, jobs on self-hosted runners don't consume Actions minutes. Past the quota, a standard Linux runner costs $0.006 a minute. My server was on anyway, so the extra cost was CPU and disk.&lt;/p&gt;

&lt;p&gt;There's a second benefit: the runner's disk persists between jobs. In my project the first run was slow because Poetry installed every dependency from scratch. After that, runs were noticeably faster because the virtualenv was still there. That persistence is also the biggest downside, which I'll come back to in the security section.&lt;/p&gt;

&lt;p&gt;It helps to know how the runner talks to GitHub. It opens an outbound HTTPS connection and waits for work, so you don't need to open any inbound ports on the server:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fen%2Fposts%2Fgithub-actions-self-hosted-runner%2Fimg%2Fself-hosted-runner-flow.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fen%2Fposts%2Fgithub-actions-self-hosted-runner%2Fimg%2Fself-hosted-runner-flow.webp" title="The runner always opens the connection, so no inbound ports are needed" alt="The runner long-polls GitHub over HTTPS, takes a job whose labels match, runs it as github-runner and sends the results back" width="800" height="493"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a dedicated user
&lt;/h2&gt;

&lt;p&gt;Don't run the runner as root; &lt;code&gt;config.sh&lt;/code&gt; refuses to anyway. Create a system user for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;useradd &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; /opt/github-runner &lt;span class="nt"&gt;-s&lt;/span&gt; /bin/bash &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"GitHub Actions Runner"&lt;/span&gt; github-runner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-r&lt;/code&gt; makes it a system account and &lt;code&gt;-m -d&lt;/code&gt; creates its home at &lt;code&gt;/opt/github-runner&lt;/code&gt;. The account has no password, so nobody can log in with one, and it has no sudo rights. I give it &lt;code&gt;/bin/bash&lt;/code&gt; because the next steps run as this user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Download the runner and verify its hash
&lt;/h2&gt;

&lt;p&gt;Do everything from here on as the runner user, so files get the right owner from the start:&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;sudo&lt;/span&gt; &lt;span class="nt"&gt;-iu&lt;/span&gt; github-runner
&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/github-runner

&lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2.338.1
curl &lt;span class="nt"&gt;-fsSLO&lt;/span&gt; &lt;span class="s2"&gt;"https://github.com/actions/runner/releases/download/v&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/actions-runner-linux-x64-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.tar.gz"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"08e9ccb3604d2462c24b5e33604dc30810296ca15cc65f23cffd4317eb144c37  actions-runner-linux-x64-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.tar.gz"&lt;/span&gt; | &lt;span class="nb"&gt;sha256sum&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;

&lt;span class="nb"&gt;tar &lt;/span&gt;xzf &lt;span class="s2"&gt;"actions-runner-linux-x64-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.tar.gz"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The version and hash in the example belong to runner 2.338.1. Every release lists the hash for each package in its notes, right below the Linux x64 install commands:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fposts%2Fgithub-actions-self-hosted-runner-tr%2Fimg%2Frunner-release-linux-x64-sha256.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fposts%2Fgithub-actions-self-hosted-runner-tr%2Fimg%2Frunner-release-linux-x64-sha256.webp" title="Source: GitHub, actions/runner v2.338.1 release notes (quotation)" alt="actions/runner 2.338.1 release notes: the Linux x64 install commands and the package's SHA-256 checksum" width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Use the current version from the &lt;a href="https://github.com/actions/runner/releases" rel="noopener noreferrer"&gt;releases page&lt;/a&gt; and the hash from that release. You're downloading a binary that will run code on your server; don't skip the checksum.&lt;/p&gt;

&lt;h2&gt;
  
  
  Register the runner with your repository
&lt;/h2&gt;

&lt;p&gt;Repository → Settings → Actions → Runners → New self-hosted runner shows a registration token. It's valid for one hour, so have the command ready before you generate it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./config.sh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; https://github.com/YOUR_USER/YOUR_REPO &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--token&lt;/span&gt; YOUR_REGISTRATION_TOKEN &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; prod-server-01 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--labels&lt;/span&gt; prod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--unattended&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runner adds the &lt;code&gt;self-hosted&lt;/code&gt;, &lt;code&gt;linux&lt;/code&gt; and &lt;code&gt;x64&lt;/code&gt; labels on its own, so &lt;code&gt;--labels&lt;/code&gt; only needs your custom ones. &lt;code&gt;--unattended&lt;/code&gt; skips the interactive prompts. On success you'll see something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;√ Connected to GitHub
√ Runner successfully added
√ Settings Saved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;exit&lt;/code&gt; back to your own user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install it as a systemd service
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;svc.sh&lt;/code&gt; in the runner folder generates a systemd unit. Run it as root and tell it which user the service runs as:&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;cd&lt;/span&gt; /opt/github-runner
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./svc.sh &lt;span class="nb"&gt;install &lt;/span&gt;github-runner
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./svc.sh start
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./svc.sh status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Logs go to journald:&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;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s1"&gt;'actions.runner.*'&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 50 &lt;span class="nt"&gt;--no-pager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for &lt;code&gt;Listening for Jobs&lt;/code&gt;. Once that line shows up, the runner is waiting for work, and it appears as "Idle" in the repository's Runners list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Point your workflows at it
&lt;/h2&gt;

&lt;p&gt;Replace &lt;code&gt;runs-on&lt;/code&gt; with the labels:&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="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;self-hosted&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;linux&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;prod&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v7&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;poetry install --no-interaction&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;poetry run ruff check .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A job goes to a runner that has every label in the list. With several servers, labels like &lt;code&gt;prod&lt;/code&gt; and &lt;code&gt;staging&lt;/code&gt; let you choose where each job runs.&lt;/p&gt;

&lt;p&gt;When I opened my first PR after the switch, the Actions page showed &lt;code&gt;Running job: Backend Lint (ruff)&lt;/code&gt;. The job was running on the server in front of me, not on a VM spun up somewhere in Azure. The whole setup, from creating the user to updating the workflow files, took about 20 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security: the cost of a persistent runner
&lt;/h2&gt;

&lt;p&gt;GitHub-hosted runners start every job on a fresh VM. A self-hosted runner doesn't. Anything one job leaves on disk is there for the next one. These are the rules I follow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Don't use self-hosted runners on public repositories. A pull request from a fork can change your workflow and run code on your server, and GitHub's &lt;a href="https://docs.github.com/en/actions/reference/security/secure-use" rel="noopener noreferrer"&gt;secure use guide&lt;/a&gt; says as much. My runner only serves private repos.&lt;/li&gt;
&lt;li&gt;Lock the runner directory to the runner user: &lt;code&gt;sudo chown -R github-runner:github-runner /opt/github-runner&lt;/code&gt; and &lt;code&gt;sudo chmod 700 /opt/github-runner&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Keep the runner user out of sudoers. If a job really needs a root command, write a narrow sudoers rule for that one command.&lt;/li&gt;
&lt;li&gt;Adding the runner user to the &lt;code&gt;docker&lt;/code&gt; group is effectively giving it root. If jobs need Docker, make that choice knowingly or use rootless Docker.&lt;/li&gt;
&lt;li&gt;Avoid steps that write secrets to disk, and delete them at the end of the job if you must. The next job sees the same disk.&lt;/li&gt;
&lt;li&gt;If every job needs a clean start, register the runner with &lt;code&gt;--ephemeral&lt;/code&gt;. It takes one job and then deregisters. The trade-off is that something has to register a new runner for each job, usually a script or Actions Runner Controller.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By default the runner downloads and installs new versions itself. Leave that on: GitHub stops sending jobs to runners that fall too far behind. OS patches are still your job.&lt;/p&gt;

&lt;p&gt;To remove the runner, stop the service first, then delete the registration:&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;cd&lt;/span&gt; /opt/github-runner
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./svc.sh stop
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./svc.sh uninstall
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; github-runner ./config.sh remove &lt;span class="nt"&gt;--token&lt;/span&gt; YOUR_REMOVAL_TOKEN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get the removal token from the runner's menu on the Runners page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one when
&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;GitHub-hosted&lt;/th&gt;
&lt;th&gt;Self-hosted&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Free within quota, then per minute&lt;/td&gt;
&lt;td&gt;No Actions minutes, you pay for the server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Runner and OS updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;Setup and cache download every job&lt;/td&gt;
&lt;td&gt;Fast after the first job thanks to local cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Isolation&lt;/td&gt;
&lt;td&gt;Fresh VM every job&lt;/td&gt;
&lt;td&gt;Jobs share the same disk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;GitHub's network&lt;/td&gt;
&lt;td&gt;Your network, easy access to internal services&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you're under the quota, GitHub-hosted runners are the right default. If you regularly blow through it and have a server sitting idle, a self-hosted runner makes sense as long as you follow the security rules above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;My &lt;a href="https://blog.furkankoykiran.com.tr/en/posts/awesome-trending-repos-web/" rel="noopener noreferrer"&gt;awesome-trending-repos post&lt;/a&gt; shows a cron-driven Actions workflow that deploys to GitHub Pages, and the &lt;a href="https://blog.furkankoykiran.com.tr/en/posts/gitlab-github-jekyll-migration/" rel="noopener noreferrer"&gt;GitLab to GitHub migration story&lt;/a&gt; compares GitHub Actions with GitLab CI from the time I had to use both. For the official steps, &lt;a href="https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/add-runners" rel="noopener noreferrer"&gt;GitHub's guide to adding self-hosted runners&lt;/a&gt; is the most current source.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://blog.furkankoykiran.com.tr/en/posts/github-actions-self-hosted-runner/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;. I write about MCP servers, AI agents, developer tools and Python automation; more at &lt;a href="https://furkankoykiran.com.tr/en/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;furkankoykiran.com.tr&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>cicd</category>
      <category>devops</category>
      <category>linux</category>
    </item>
    <item>
      <title>freqtrade-mcp: Freqtrade Botuma Claude Üzerinden Sormak</title>
      <dc:creator>Furkan Köykıran</dc:creator>
      <pubDate>Sat, 04 Apr 2026 14:14:52 +0000</pubDate>
      <link>https://dev.to/furkankoykiran/freqtrade-mcp-bota-claude-uzerinden-sor-2me8</link>
      <guid>https://dev.to/furkankoykiran/freqtrade-mcp-bota-claude-uzerinden-sor-2me8</guid>
      <description>&lt;p&gt;Bir Freqtrade botu çalıştırıyorum. Strateji hazır, bot işini yapıyor, benim bakmama gerek yok. Teoride. Pratikte gece on birde FreqUI'yı açıp sekmelere tıklıyor, bir sayıya bakıp kapatıyor, on beş dakika sonra yine açıyordum.&lt;/p&gt;

&lt;p&gt;Claude zaten açıktı ve botun düzgün bir REST API'si vardı. Aradaki köprüyü &lt;a href="https://github.com/furkankoykiran/freqtrade-mcp" rel="noopener noreferrer"&gt;freqtrade-mcp&lt;/a&gt; adıyla yazdım: Freqtrade'in REST API'sini MCP araçları olarak sunan bir TypeScript MCP sunucusu. Claude Desktop, Claude Code, Cursor ve Cline gibi &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; konuşan her istemciyle çalışıyor. 1.0.0 sürümünü 4 Nisan 2026'da npm'e yayınladım.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ne işe yarıyor
&lt;/h2&gt;

&lt;p&gt;Botla ilgili sorduğum sorular genelde şunlar:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Bu ay kârda mıyım?"&lt;/p&gt;

&lt;p&gt;"Açık pozisyonlarda zararda olan var mı?"&lt;/p&gt;

&lt;p&gt;"BTC/USDT 4 saatlikte son 50 mum nasıl görünüyor?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude her soruyu ilgili araca çeviriyor, Freqtrade'den gelen JSON'u okuyup özetliyor. Sayıları kendim ayıklamak zorunda kalmıyorum. Araçlar altı gruba ayrılıyor:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Grup&lt;/th&gt;
&lt;th&gt;Araçlar&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hesap&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;get_profit_stats&lt;/code&gt;, &lt;code&gt;get_balance&lt;/code&gt;, &lt;code&gt;get_performance&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;İşlemler&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;get_open_trades&lt;/code&gt;, &lt;code&gt;get_trade&lt;/code&gt;, &lt;code&gt;get_trade_history&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Piyasa verisi&lt;/td&gt;
&lt;td&gt;&lt;code&gt;get_market_data&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Çift listeleri&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;get_whitelist&lt;/code&gt;, &lt;code&gt;get_blacklist&lt;/code&gt;, &lt;code&gt;add_to_blacklist&lt;/code&gt;, &lt;code&gt;remove_from_blacklist&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;İşlem açma/kapama&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;execute_trade&lt;/code&gt;, &lt;code&gt;force_exit_trade&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bot yönetimi&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;start_bot&lt;/code&gt;, &lt;code&gt;stop_bot&lt;/code&gt;, &lt;code&gt;reload_config&lt;/code&gt;, &lt;code&gt;get_bot_info&lt;/code&gt;, &lt;code&gt;get_locks&lt;/code&gt;, &lt;code&gt;delete_lock&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Kodda toplam 19 araç var. README hâlâ "15 araç" diyor, onu da düzeltmem gerekiyor.&lt;/p&gt;

&lt;p&gt;Birkaç pratik not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;get_market_data&lt;/code&gt;, Freqtrade'in &lt;code&gt;/pair_candles&lt;/code&gt; endpoint'ini kullanıyor. Bu endpoint botun kendi analiz ettiği mum verisini döndürüyor, yani pratikte yalnızca botun takip ettiği çiftler için çalışıyor.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_trade_history&lt;/code&gt; ve &lt;code&gt;get_market_data&lt;/code&gt; en fazla 500 kayıt döndürüyor.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_bot_info&lt;/code&gt;, sürüm bilgisini son strateji döngüsünün zamanıyla birlikte veriyor. "Bot ayakta mı, yoksa sessizce takılmış mı?" sorusunun cevabı burada.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Mimari
&lt;/h2&gt;

&lt;p&gt;Sunucu, istemciyle stdio üzerinden konuşan tek bir Node.js süreci. Freqtrade'e HTTP Basic Auth ile &lt;code&gt;/api/v1&lt;/code&gt; altından bağlanıyor:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fposts%2Ffreqtrade-mcp-server-tr%2Fimg%2Ffreqtrade-mcp-request-flow.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fposts%2Ffreqtrade-mcp-server-tr%2Fimg%2Ffreqtrade-mcp-request-flow.webp" title="Sohbetteki soru bir MCP aracı çağrısına ve bir REST isteğine dönüşüyor" alt="Üç adım: sohbetteki soru, Claude'un get_open_trades aracını stdio ile çağırması ve Freqtrade REST API'nin /api/v1 altında yanıt vermesi" width="800" height="552"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Kod üç katmanlı:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;src/client.ts&lt;/code&gt;: Freqtrade API'si için tipli bir HTTP istemcisi. Node 18+ ile gelen &lt;code&gt;fetch&lt;/code&gt;'i kullanıyor, her isteği &lt;code&gt;AbortController&lt;/code&gt; ile 10 saniyede kesiyor ve 2xx dışı yanıtlarda Freqtrade'in &lt;code&gt;detail&lt;/code&gt; alanını hata mesajına ekliyor.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/tools/*.ts&lt;/code&gt;: Her grup için bir dosya. Her araç parametrelerini bir zod şemasıyla tanımlıyor.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/index.ts&lt;/code&gt;: İstemciyi ortam değişkenlerinden kurup araçları kaydediyor ve &lt;code&gt;StdioServerTransport&lt;/code&gt; ile bağlanıyor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repo bu yüzden küçük: &lt;code&gt;src/&lt;/code&gt; altında kod, yanında bir &lt;code&gt;.env.example&lt;/code&gt; ve CI ile npm yayını için &lt;code&gt;.github/&lt;/code&gt; klasörü.&lt;/p&gt;

&lt;p&gt;Bir aracın tamamı şu kadar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_trade&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="s1"&gt;Retrieve detailed information about a specific trade by its ID, including full history, fees, and profit/loss.&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;trade_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;positive&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The numeric trade ID to look up.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;trade_id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTrade&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trade_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="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="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&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="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="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="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Error fetching trade &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;trade_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="kd"&gt;as &lt;/span&gt;&lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="na"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="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;Hataları fırlatmak yerine &lt;code&gt;isError: true&lt;/code&gt; ile döndürüyorum. Model böylece "trade 42 bulunamadı" mesajını görüp kullanıcıya düzgün bir cevap verebiliyor ve oturum düşmüyor. Modelin aracı ne zaman seçeceğini de açıklama metinleri belirliyor. &lt;code&gt;execute_trade&lt;/code&gt; açıklamasına "100 USDT'lik BTC al" gibi doğal dil örneklerini ve bunların hangi parametrelere karşılık geldiğini yazdım.&lt;/p&gt;

&lt;p&gt;Bugün yeniden yazsam tek bir şeyi değiştirirdim. MCP TypeScript SDK'nın yeni sürümleri &lt;code&gt;server.tool()&lt;/code&gt; yerine &lt;code&gt;registerTool()&lt;/code&gt; kullanmayı öneriyor. Sunucu 1.x SDK ile sorunsuz çalışıyor, ama yeni bir projeye &lt;code&gt;registerTool()&lt;/code&gt; ile başlardım.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kurulum
&lt;/h2&gt;

&lt;p&gt;Önce Freqtrade'in REST API'sini açın, varsayılan olarak kapalı geliyor. &lt;code&gt;config.json&lt;/code&gt; içine şunu ekleyin:&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="nl"&gt;"api_server"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"listen_ip_address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"listen_port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verbosity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"enable_openapi"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jwt_secret_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rastgele-uzun-bir-deger"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Freqtrader"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GucluBirSifre"&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;&lt;code&gt;listen_ip_address&lt;/code&gt; değerini &lt;code&gt;127.0.0.1&lt;/code&gt; bırakın. API'yi internete açarsanız botunuz ve borsa hesabınız tek bir şifreye kalır. Freqtrade dokümantasyonu da aynı uyarıyı yapıyor:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fposts%2Ffreqtrade-mcp-server-tr%2Fimg%2Ffreqtrade-api-security-warning.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fposts%2Ffreqtrade-mcp-server-tr%2Fimg%2Ffreqtrade-api-security-warning.webp" title="Kaynak: Freqtrade dokümantasyonu, REST API sayfası (alıntı)" alt="Freqtrade dokümantasyonundaki güvenlik uyarısı: API'yi internete açmayın ve güçlü, benzersiz bir şifre seçin" width="800" height="133"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sonra Claude Desktop yapılandırmasına (&lt;code&gt;claude_desktop_config.json&lt;/code&gt;) sunucuyu ekleyin:&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;"mcpServers"&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;"freqtrade"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@furkankoykiran/freqtrade-mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"FREQTRADE_API_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://127.0.0.1:8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FREQTRADE_USERNAME"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Freqtrader"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FREQTRADE_PASSWORD"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GucluBirSifre"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Desktop'ı yeniden başlatınca araçlar listede görünüyor. Claude Code kullanıyorsanız aynı şeyi tek komutla yapabilirsiniz:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add freqtrade &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;FREQTRADE_API_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://127.0.0.1:8080 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;FREQTRADE_USERNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Freqtrader &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;FREQTRADE_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;GucluBirSifre &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; @furkankoykiran/freqtrade-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Araçları bir istemciye bağlamadan önce denemek isterseniz MCP Inspector işinizi görür: &lt;code&gt;npx @modelcontextprotocol/inspector npx -y @furkankoykiran/freqtrade-mcp&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Canlı işlemde dikkat edilecekler
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;execute_trade&lt;/code&gt; ve &lt;code&gt;force_exit_trade&lt;/code&gt; stratejiyi atlayıp doğrudan Freqtrade'in &lt;code&gt;/forceenter&lt;/code&gt; ve &lt;code&gt;/forceexit&lt;/code&gt; endpoint'lerini çağırıyor. Sunucuda onay adımı yok.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt;&lt;br&gt;
Bot canlı moddaysa bu araçlar gerçek borsada, gerçek parayla, geri alınamayan bir emir veriyor.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Güvenmeden önce şunları bilmeniz gerekiyor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Freqtrade &lt;code&gt;/forceenter&lt;/code&gt; çağrılarını yalnızca config'te &lt;code&gt;force_entry_enable: true&lt;/code&gt; varsa kabul ediyor. Bu ayar kapalıyken &lt;code&gt;execute_trade&lt;/code&gt; hata döndürüyor. Ayarı bilerek açmanız gerekiyor.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;side: "short"&lt;/code&gt; yalnızca futures ya da margin modunda çalışan botlarda anlamlı. Spot modda short açamazsınız.&lt;/li&gt;
&lt;li&gt;Önce &lt;code&gt;dry_run: true&lt;/code&gt; ile deneyin. Freqtrade bu modda emirleri simüle ediyor, araçların davranışını risksiz görürsünüz.&lt;/li&gt;
&lt;li&gt;Claude Desktop ve Claude Code araç çağrılarından önce izin istiyor. İşlem açan araçlar için "her zaman izin ver" seçeneğini hiç kullanmıyorum.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Kodu ve sonraki adımlar
&lt;/h2&gt;

&lt;p&gt;Çözmek istediğim şey basitti: FreqUI'yı açmadan bota bakabilmek. REST API zaten oradaydı, MCP sunucusu onu sohbete taşıdı. "Acaba bir şey değişti mi" hissi geçmedi, ama sormak artık tek bir cümle.&lt;/p&gt;

&lt;p&gt;Kod MIT lisanslı ve &lt;a href="https://github.com/furkankoykiran/freqtrade-mcp" rel="noopener noreferrer"&gt;GitHub'da&lt;/a&gt;, projenin özeti de &lt;a href="https://furkankoykiran.com.tr/projeler/freqtrade-mcp/" rel="noopener noreferrer"&gt;proje sayfamda&lt;/a&gt;. Kendi botunuza bağlamadan önce bir hafta &lt;code&gt;dry_run: true&lt;/code&gt; ile çalıştırıp &lt;code&gt;get_profit_stats&lt;/code&gt; ve &lt;code&gt;get_open_trades&lt;/code&gt; sonuçlarını FreqUI ile karşılaştırın. Python tarafında bot geliştirmeyle ilgileniyorsanız &lt;a href="https://blog.furkankoykiran.com.tr/posts/python-telegram-trading-bot/" rel="noopener noreferrer"&gt;Python ile Telegram trading botu&lt;/a&gt; ve &lt;a href="https://blog.furkankoykiran.com.tr/posts/yapay-zeka-destekli-trading-stratejileri/" rel="noopener noreferrer"&gt;yapay zekâ destekli trading stratejileri&lt;/a&gt; yazılarına, başka bir MCP sunucusunu nasıl sardığımı görmek için de &lt;a href="https://blog.furkankoykiran.com.tr/posts/cli-anything-browser-automation-katkim/" rel="noopener noreferrer"&gt;CLI-Anything katkıma&lt;/a&gt; bakabilirsiniz.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://blog.furkankoykiran.com.tr/posts/freqtrade-mcp-server-tr/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;. I write about MCP servers, AI agents, developer tools and Python automation; more at &lt;a href="https://furkankoykiran.com.tr/en/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;furkankoykiran.com.tr&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>trading</category>
      <category>typescript</category>
      <category>claude</category>
    </item>
    <item>
      <title>freqtrade-mcp: An MCP Server for Your Freqtrade Bot</title>
      <dc:creator>Furkan Köykıran</dc:creator>
      <pubDate>Sat, 04 Apr 2026 14:14:38 +0000</pubDate>
      <link>https://dev.to/furkankoykiran/freqtrade-mcp-ask-claude-to-check-your-trades-44hj</link>
      <guid>https://dev.to/furkankoykiran/freqtrade-mcp-ask-claude-to-check-your-trades-44hj</guid>
      <description>&lt;p&gt;I run a Freqtrade bot. Like most people with a trading bot, I tell myself I don't check it all the time, and then I check it all the time: open FreqUI, click through tabs, look at one number, close the tab, open it again fifteen minutes later.&lt;/p&gt;

&lt;p&gt;Claude was already open, and the bot already had a perfectly good REST API. So I wrote &lt;a href="https://github.com/furkankoykiran/freqtrade-mcp" rel="noopener noreferrer"&gt;freqtrade-mcp&lt;/a&gt;, a TypeScript MCP server that exposes Freqtrade's REST API as MCP tools. It works with Claude Desktop, Claude Code, Cursor, Cline and anything else that speaks the &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt;. Version 1.0.0 went to npm on April 4, 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;The questions I actually ask look like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Am I up this month?"&lt;/p&gt;

&lt;p&gt;"Are any open trades in the red?"&lt;/p&gt;

&lt;p&gt;"What do the last 50 BTC/USDT candles on the 4h look like?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude maps each question to a tool, reads the JSON that comes back from Freqtrade and summarizes it, so I don't have to parse numbers at 11pm. The tools fall into six groups:&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;Tools&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Account&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;get_profit_stats&lt;/code&gt;, &lt;code&gt;get_balance&lt;/code&gt;, &lt;code&gt;get_performance&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trades&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;get_open_trades&lt;/code&gt;, &lt;code&gt;get_trade&lt;/code&gt;, &lt;code&gt;get_trade_history&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Market data&lt;/td&gt;
&lt;td&gt;&lt;code&gt;get_market_data&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pair lists&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;get_whitelist&lt;/code&gt;, &lt;code&gt;get_blacklist&lt;/code&gt;, &lt;code&gt;add_to_blacklist&lt;/code&gt;, &lt;code&gt;remove_from_blacklist&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;execute_trade&lt;/code&gt;, &lt;code&gt;force_exit_trade&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lifecycle&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;start_bot&lt;/code&gt;, &lt;code&gt;stop_bot&lt;/code&gt;, &lt;code&gt;reload_config&lt;/code&gt;, &lt;code&gt;get_bot_info&lt;/code&gt;, &lt;code&gt;get_locks&lt;/code&gt;, &lt;code&gt;delete_lock&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's 19 tools in the code. The README still says 15, which is on my list to fix.&lt;/p&gt;

&lt;p&gt;A few practical notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;get_market_data&lt;/code&gt; uses Freqtrade's &lt;code&gt;/pair_candles&lt;/code&gt; endpoint, which returns the candle data the bot has analyzed. In practice that means pairs the bot is tracking, not any pair on the exchange.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_trade_history&lt;/code&gt; and &lt;code&gt;get_market_data&lt;/code&gt; return at most 500 rows.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_bot_info&lt;/code&gt; returns the bot version together with the time of the last strategy cycle. That answers "is the bot running, or has it been silently stuck?"&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The server is a single Node.js process that talks stdio to the client and HTTP Basic Auth to Freqtrade under &lt;code&gt;/api/v1&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fen%2Fposts%2Ffreqtrade-mcp-server%2Fimg%2Ffreqtrade-mcp-request-flow.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fen%2Fposts%2Ffreqtrade-mcp-server%2Fimg%2Ffreqtrade-mcp-request-flow.webp" title="A chat question becomes one MCP tool call and one REST request" alt="Three steps: a chat question, Claude calling the get_open_trades tool over stdio, and the Freqtrade REST API answering under /api/v1" width="800" height="544"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The code has three layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;src/client.ts&lt;/code&gt; is a typed HTTP client for the Freqtrade API. It uses the &lt;code&gt;fetch&lt;/code&gt; built into Node 18+, aborts each request after 10 seconds with an &lt;code&gt;AbortController&lt;/code&gt;, and puts Freqtrade's &lt;code&gt;detail&lt;/code&gt; field into the error message on non-2xx responses.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/tools/*.ts&lt;/code&gt; has one file per group, and each tool declares its parameters with a zod schema.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/index.ts&lt;/code&gt; builds the client from environment variables, registers the tools and connects a &lt;code&gt;StdioServerTransport&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That keeps the repo small: the code under &lt;code&gt;src/&lt;/code&gt;, a &lt;code&gt;.env.example&lt;/code&gt;, and a &lt;code&gt;.github/&lt;/code&gt; folder for CI and the npm release.&lt;/p&gt;

&lt;p&gt;A complete tool is this short:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;get_trade&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="s1"&gt;Retrieve detailed information about a specific trade by its ID, including full history, fees, and profit/loss.&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;trade_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;positive&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The numeric trade ID to look up.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;trade_id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTrade&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trade_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="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="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&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="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="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="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Error fetching trade &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;trade_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="kd"&gt;as &lt;/span&gt;&lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="na"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="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;I return errors with &lt;code&gt;isError: true&lt;/code&gt; instead of throwing them. The model sees "trade 42 not found", can tell the user something sensible, and the session stays up. The descriptions matter too, because they decide when the model picks a tool. For &lt;code&gt;execute_trade&lt;/code&gt; I put natural-language examples like "buy 100 USDT of BTC" in the description, along with the parameters each one maps to.&lt;/p&gt;

&lt;p&gt;If I started it today I'd change one thing: newer releases of the MCP TypeScript SDK recommend &lt;code&gt;registerTool()&lt;/code&gt; over &lt;code&gt;server.tool()&lt;/code&gt;. The server works fine on the 1.x SDK, but a new project should start with &lt;code&gt;registerTool()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;First, turn on Freqtrade's REST API, which is off by default. In &lt;code&gt;config.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"api_server"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"listen_ip_address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"listen_port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verbosity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"enable_openapi"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jwt_secret_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"some-long-random-value"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Freqtrader"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YourStrongPassword"&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;Keep &lt;code&gt;listen_ip_address&lt;/code&gt; at &lt;code&gt;127.0.0.1&lt;/code&gt;. Exposing the API to the internet puts your bot and your exchange account behind a single password. The Freqtrade docs say the same:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fposts%2Ffreqtrade-mcp-server-tr%2Fimg%2Ffreqtrade-api-security-warning.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fposts%2Ffreqtrade-mcp-server-tr%2Fimg%2Ffreqtrade-api-security-warning.webp" title="Source: Freqtrade documentation, REST API page (quotation)" alt="Security warning from the Freqtrade docs: don't expose the API to the internet and pick a strong, unique password" width="800" height="133"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then add the server to Claude Desktop's &lt;code&gt;claude_desktop_config.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&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;"freqtrade"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@furkankoykiran/freqtrade-mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"FREQTRADE_API_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://127.0.0.1:8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FREQTRADE_USERNAME"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Freqtrader"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FREQTRADE_PASSWORD"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YourStrongPassword"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart Claude Desktop and the tools show up. With Claude Code it's one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add freqtrade &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;FREQTRADE_API_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://127.0.0.1:8080 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;FREQTRADE_USERNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Freqtrader &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;FREQTRADE_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YourStrongPassword &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--&lt;/span&gt; npx &lt;span class="nt"&gt;-y&lt;/span&gt; @furkankoykiran/freqtrade-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To poke at the tools before wiring them into a client, use the MCP Inspector: &lt;code&gt;npx @modelcontextprotocol/inspector npx -y @furkankoykiran/freqtrade-mcp&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live trading caveats
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;execute_trade&lt;/code&gt; and &lt;code&gt;force_exit_trade&lt;/code&gt; skip the strategy and call Freqtrade's &lt;code&gt;/forceenter&lt;/code&gt; and &lt;code&gt;/forceexit&lt;/code&gt; endpoints directly. The server has no confirmation step.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Warning:&lt;/strong&gt;&lt;br&gt;
If the bot is live, these tools place a real order on a real exchange, and you can't take it back.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Things to know before you rely on it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Freqtrade only accepts &lt;code&gt;/forceenter&lt;/code&gt; when the config has &lt;code&gt;force_entry_enable: true&lt;/code&gt;. Without it, &lt;code&gt;execute_trade&lt;/code&gt; returns an error. Turning that on should be a deliberate choice.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;side: "short"&lt;/code&gt; only makes sense for bots running in futures or margin mode. You can't short in spot mode.&lt;/li&gt;
&lt;li&gt;Try everything with &lt;code&gt;dry_run: true&lt;/code&gt; first. Freqtrade simulates the orders, so you can see how the tools behave without risk.&lt;/li&gt;
&lt;li&gt;Claude Desktop and Claude Code ask for permission before tool calls. I never pick "always allow" for the tools that place orders.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where the code is
&lt;/h2&gt;

&lt;p&gt;What I wanted was simple: check the bot without opening FreqUI. The REST API was already there; the MCP server moved it into the chat. I still wonder whether something changed while I wasn't looking, but now finding out takes one sentence.&lt;/p&gt;

&lt;p&gt;The code is MIT licensed and &lt;a href="https://github.com/furkankoykiran/freqtrade-mcp" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;, with a summary on my &lt;a href="https://furkankoykiran.com.tr/en/projects/freqtrade-mcp/" rel="noopener noreferrer"&gt;project page&lt;/a&gt;. Before you point it at your own bot, run it for a week with &lt;code&gt;dry_run: true&lt;/code&gt; and compare what &lt;code&gt;get_profit_stats&lt;/code&gt; and &lt;code&gt;get_open_trades&lt;/code&gt; report with FreqUI. To see a different MCP integration, where I wrapped someone else's MCP server as a CLI, read &lt;a href="https://blog.furkankoykiran.com.tr/en/posts/cli-anything-browser-automation-contribution/" rel="noopener noreferrer"&gt;my CLI-Anything contribution&lt;/a&gt;. If your Python services lean on async I/O the way trading bots do, the &lt;a href="https://blog.furkankoykiran.com.tr/en/posts/python-asyncio-concurrent-processing/" rel="noopener noreferrer"&gt;asyncio guide&lt;/a&gt; is a good next read.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://blog.furkankoykiran.com.tr/en/posts/freqtrade-mcp-server/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;. I write about MCP servers, AI agents, developer tools and Python automation; more at &lt;a href="https://furkankoykiran.com.tr/en/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;furkankoykiran.com.tr&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>trading</category>
      <category>typescript</category>
      <category>claude</category>
    </item>
    <item>
      <title>awesome-trending-repos: Tracking GitHub Trending Daily</title>
      <dc:creator>Furkan Köykıran</dc:creator>
      <pubDate>Fri, 27 Mar 2026 20:12:20 +0000</pubDate>
      <link>https://dev.to/furkankoykiran/awesome-trending-repos-modern-web-interface-for-github-trending-26b5</link>
      <guid>https://dev.to/furkankoykiran/awesome-trending-repos-modern-web-interface-for-github-trending-26b5</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/furkankoykiran/awesome-trending-repos" rel="noopener noreferrer"&gt;awesome-trending-repos&lt;/a&gt; is a small project of mine that tracks GitHub Trending on its own. Every night at 00:00 UTC a GitHub Actions job scrapes the trending list, compares it with the previous day, commits the result as JSON and redeploys a React dashboard to GitHub Pages. It has run without me touching it since March 2026. This post walks through both versions of the project, the data pipeline, and the mistakes I made along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;I looked at the trending page most days but couldn't remember what was on it yesterday. Seeing which repos were new, which were climbing and which languages were hot meant keeping history somewhere. GitHub doesn't keep it for you, so I started keeping it myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 1: a script that wrote the README
&lt;/h2&gt;

&lt;p&gt;The March 17, 2026 release was plain Node.js. &lt;code&gt;scripts/update.js&lt;/code&gt; scraped the trending page and wrote everything straight into &lt;code&gt;README.md&lt;/code&gt;: language breakdowns, rank changes, ASCII charts, the fastest risers. The last 7 days of data lived in &lt;code&gt;.data/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It worked, but a README is a poor data UI. No filters, no search, and the charts were text. Ten days later I split the interface out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Version 2: JSON data, React front end
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/furkankoykiran/awesome-trending-repos/pull/5" rel="noopener noreferrer"&gt;PR #5&lt;/a&gt;, merged on March 27, split the project in two. The script no longer touches the README; it only writes &lt;code&gt;public/data/trending.json&lt;/code&gt; and &lt;code&gt;public/data/feed.json&lt;/code&gt;, and the README became a static landing page. The front end is a single-page app that reads that JSON:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React 19 and Vite 6&lt;/li&gt;
&lt;li&gt;Tailwind CSS v4 through the &lt;code&gt;@tailwindcss/vite&lt;/code&gt; plugin&lt;/li&gt;
&lt;li&gt;Framer Motion for transitions, Recharts for the language distribution chart&lt;/li&gt;
&lt;li&gt;Language filter, instant search across name, owner and description, and a feed that lists each update&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the top of the live site. The Trending, Stats and Feed tabs, the language chips and the search box all read the same JSON:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fposts%2Fawesome-trending-repos-web%2Fimg%2Ftrending-dashboard.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fposts%2Fawesome-trending-repos-web%2Fimg%2Ftrending-dashboard.webp" title="The React front end at furkankoykiran.com.tr/awesome-trending-repos" alt="The live awesome-trending-repos site: Trending, Stats and Feed tabs, language filters, a search box and repo cards" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With 25 repos, filtering doesn't need anything clever:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchQuery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filteredRepos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;repos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matchesLang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;All&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matchesSearch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;matchesLang&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;matchesSearch&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;h2&gt;
  
  
  The data pipeline
&lt;/h2&gt;

&lt;p&gt;The real work happens in &lt;code&gt;scripts/update.js&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It fetches &lt;code&gt;github.com/trending/&amp;lt;language&amp;gt;?since=daily&lt;/code&gt; for 15 languages with axios and parses the HTML with cheerio. The all-languages page changed structure at some point, and going language by language turned out to be more reliable.&lt;/li&gt;
&lt;li&gt;If that yields fewer than 25 repos, the GitHub Search API fills the gap.&lt;/li&gt;
&lt;li&gt;Invalid entries and anything outside a list of popular languages are dropped.&lt;/li&gt;
&lt;li&gt;If the list is still short, repos from the last 7 days of snapshots are added.&lt;/li&gt;
&lt;li&gt;The result is compared with yesterday's snapshot: new entries, risers, fallers and a "rising star".&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.data/history.json&lt;/code&gt; keeps at most 7 snapshots, and older snapshot files are deleted.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two numbers in there are guesses. The Search API doesn't tell you how many stars a repo got today, so for repos that come from search I estimate daily stars as 1% of total stars. Repos pulled back in from history get half of their old daily count. Both are rough fillers, not measurements. Flagging them in the UI is on my list.&lt;/p&gt;

&lt;h2&gt;
  
  
  The GitHub Actions workflow
&lt;/h2&gt;

&lt;p&gt;The workflow runs once a day on a cron, on manual dispatch, and on pushes that touch &lt;code&gt;src/&lt;/code&gt; or &lt;code&gt;scripts/&lt;/code&gt;. It updates the data, builds, commits any changes and deploys to Pages. Here is a trimmed version of the one in the repo:&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="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update and Deploy Trends&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;src/**'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;scripts/**'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;package.json'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;package-lock.json'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;pages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pages&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;update-and-build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v7&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;22'&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;npm run update&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;npm run build&lt;/span&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;Commit data changes&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;if [ -z "$(git status --porcelain .data/ public/data/)" ]; then exit 0; fi&lt;/span&gt;
          &lt;span class="s"&gt;git config user.name 'github-actions[bot]'&lt;/span&gt;
          &lt;span class="s"&gt;git config user.email 'github-actions[bot]@users.noreply.github.com'&lt;/span&gt;
          &lt;span class="s"&gt;git add .data/ public/data/&lt;/span&gt;
          &lt;span class="s"&gt;git commit -m "chore: update trending repositories [skip ci]"&lt;/span&gt;
          &lt;span class="s"&gt;git push&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-pages-artifact@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./dist&lt;/span&gt;

  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;update-and-build&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&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;github-pages&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.deployment.outputs.page_url }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deployment&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/deploy-pages@v5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file in the repo still pins &lt;code&gt;checkout@v4&lt;/code&gt;, &lt;code&gt;setup-node@v4&lt;/code&gt;, &lt;code&gt;upload-pages-artifact@v3&lt;/code&gt; and Node 20, which reached end of life in April 2026. If you're starting fresh, use the versions above.&lt;/p&gt;

&lt;p&gt;A few details that pull their weight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;[skip ci]&lt;/code&gt; keeps the bot's commit from kicking off another run. Pushes made with &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; don't start new workflow runs anyway, but the marker costs nothing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;concurrency&lt;/code&gt; stops two deploys from racing each other.&lt;/li&gt;
&lt;li&gt;The build runs after the update. Vite copies &lt;code&gt;public/&lt;/code&gt; into &lt;code&gt;dist/&lt;/code&gt;, so the site always ships with that day's JSON.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My commit step above adds the whole &lt;code&gt;public/data/&lt;/code&gt; folder. The workflow in the repo only commits &lt;code&gt;public/data/trending.json&lt;/code&gt; and leaves &lt;code&gt;feed.json&lt;/code&gt; out. So each run reads the stale feed from the repo and appends one entry, and the live feed always shows the March 27 entry plus today's. The fix is the one line shown above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vite on GitHub Pages: the base path
&lt;/h2&gt;

&lt;p&gt;The site is a project page served under &lt;code&gt;/awesome-trending-repos/&lt;/code&gt;. If Vite doesn't know that, the built asset URLs point at the domain root and everything 404s in production:&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;// vite.config.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;react&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vitejs/plugin-react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;tailwindcss&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tailwindcss/vite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;react&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;tailwindcss&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
  &lt;span class="na"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/awesome-trending-repos/&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vite already adds content hashes to file names in production builds, so you don't need extra &lt;code&gt;rollupOptions&lt;/code&gt; for cache busting. The app loads its data with &lt;code&gt;fetch('./data/trending.json')&lt;/code&gt;, a relative path that works with any &lt;code&gt;base&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The custom domain mistake
&lt;/h2&gt;

&lt;p&gt;Putting the root domain in a project's &lt;code&gt;public/CNAME&lt;/code&gt; is a common mistake, and this repo had exactly that file. &lt;code&gt;furkankoykiran.com.tr&lt;/code&gt; belongs to my user site (&lt;code&gt;furkankoykiran.github.io&lt;/code&gt;). Project pages inherit that domain automatically and are served at &lt;code&gt;furkankoykiran.com.tr/awesome-trending-repos/&lt;/code&gt;. A project repo with the root domain in its own CNAME is trying to claim the same domain as the user site.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/furkankoykiran/awesome-trending-repos/pull/7" rel="noopener noreferrer"&gt;PR #7&lt;/a&gt; removed &lt;code&gt;public/CNAME&lt;/code&gt; and &lt;code&gt;dist/CNAME&lt;/code&gt;. With &lt;code&gt;actions/deploy-pages&lt;/code&gt;, a CNAME file inside the artifact is ignored anyway; the domain comes from Settings → Pages → Custom domain, and for a project page that field should stay empty.&lt;/p&gt;

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

&lt;p&gt;The project still runs every day. Next on the list: fix the feed bug, mark estimated star counts in the UI, and bump the workflow's action versions.&lt;/p&gt;

&lt;p&gt;The live site is at &lt;a href="https://furkankoykiran.com.tr/awesome-trending-repos/" rel="noopener noreferrer"&gt;furkankoykiran.com.tr/awesome-trending-repos&lt;/a&gt;, and there's a summary on the &lt;a href="https://furkankoykiran.com.tr/en/projects/awesome-trending-repos/" rel="noopener noreferrer"&gt;project page&lt;/a&gt;. If your Actions minutes are running low, my &lt;a href="https://blog.furkankoykiran.com.tr/en/posts/github-actions-self-hosted-runner/" rel="noopener noreferrer"&gt;self-hosted runner post&lt;/a&gt; shows how I moved CI onto my own server. The story of how this blog ended up on GitHub Pages is in &lt;a href="https://blog.furkankoykiran.com.tr/en/posts/gitlab-github-jekyll-migration/" rel="noopener noreferrer"&gt;my GitLab and GitHub migration post&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://blog.furkankoykiran.com.tr/en/posts/awesome-trending-repos-web/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;. I write about MCP servers, AI agents, developer tools and Python automation; more at &lt;a href="https://furkankoykiran.com.tr/en/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;furkankoykiran.com.tr&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>automation</category>
      <category>cicd</category>
      <category>web</category>
    </item>
    <item>
      <title>awesome-trending-repos: GitHub Trending'i Her Gün İzlemek</title>
      <dc:creator>Furkan Köykıran</dc:creator>
      <pubDate>Fri, 27 Mar 2026 20:12:18 +0000</pubDate>
      <link>https://dev.to/furkankoykiran/awesome-trending-repos-github-trending-icin-modern-web-arayuzu-m7n</link>
      <guid>https://dev.to/furkankoykiran/awesome-trending-repos-github-trending-icin-modern-web-arayuzu-m7n</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/furkankoykiran/awesome-trending-repos" rel="noopener noreferrer"&gt;awesome-trending-repos&lt;/a&gt;, GitHub Trending sayfasını her gün kendiliğinden takip eden küçük bir projem. Her gece UTC 00:00'da bir GitHub Actions işi trending listesini çekiyor, bir önceki günle karşılaştırıyor, sonucu JSON olarak repoya yazıyor ve React ile yazdığım arayüzü GitHub Pages'e yeniden yayınlıyor. Mart 2026'dan beri elle dokunmadan çalışıyor. Bu yazıda projenin iki sürümünü, veri hattını ve yolda yaptığım hataları anlatıyorum.&lt;/p&gt;

&lt;h2&gt;
  
  
  Neden yazdım
&lt;/h2&gt;

&lt;p&gt;Trending sayfasına her gün bakıyordum ama dünkü listeyi hatırlamıyordum. Hangi repo yeni girdi, hangisi yükseliyor, hangi dil öne çıkıyor, bunları görmek için geçmişi bir yerde tutmak gerekiyordu. GitHub bunu sunmuyor, bu yüzden kendim tutmaya karar verdim.&lt;/p&gt;

&lt;h2&gt;
  
  
  Birinci sürüm: README'ye yazan bir script
&lt;/h2&gt;

&lt;p&gt;17 Mart 2026'daki ilk sürüm tamamen Node.js'ti. &lt;code&gt;scripts/update.js&lt;/code&gt; trending sayfasını kazıyıp sonucu doğrudan &lt;code&gt;README.md&lt;/code&gt; dosyasına yazıyordu: dil kırılımları, sıralama değişiklikleri, ASCII grafikler, en hızlı yükselen repolar. Son 7 günün verisi &lt;code&gt;.data/&lt;/code&gt; klasöründe tutuluyordu.&lt;/p&gt;

&lt;p&gt;Çalışıyordu ama README bir veri arayüzü olarak sınırlı. Filtre yok, arama yok, grafikler metin. On gün sonra arayüzü ayırmaya karar verdim.&lt;/p&gt;

&lt;h2&gt;
  
  
  İkinci sürüm: JSON veri, React arayüz
&lt;/h2&gt;

&lt;p&gt;27 Mart'ta merge ettiğim &lt;a href="https://github.com/furkankoykiran/awesome-trending-repos/pull/5" rel="noopener noreferrer"&gt;PR #5&lt;/a&gt; projeyi ikiye böldü. Script artık README'ye dokunmuyor, yalnızca &lt;code&gt;public/data/trending.json&lt;/code&gt; ve &lt;code&gt;public/data/feed.json&lt;/code&gt; dosyalarını üretiyor. README statik bir tanıtım sayfasına dönüştü. Arayüz ise bu JSON'u okuyan tek sayfalık bir uygulama:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React 19 ve Vite 6&lt;/li&gt;
&lt;li&gt;Tailwind CSS v4 (&lt;code&gt;@tailwindcss/vite&lt;/code&gt; eklentisiyle)&lt;/li&gt;
&lt;li&gt;Framer Motion ile geçişler, Recharts ile dil dağılımı grafiği&lt;/li&gt;
&lt;li&gt;Dile göre filtreleme, isim/sahip/açıklamada anlık arama ve her güncellemenin listelendiği bir akış (feed)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Canlı sitenin üst kısmı aşağıda. Trending, Stats ve Feed sekmeleri, dil filtreleri ve arama kutusu aynı JSON'dan besleniyor:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fposts%2Fawesome-trending-repos-web%2Fimg%2Ftrending-dashboard.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fposts%2Fawesome-trending-repos-web%2Fimg%2Ftrending-dashboard.webp" title="furkankoykiran.com.tr/awesome-trending-repos üzerindeki React arayüzü" alt="awesome-trending-repos canlı sitesi: Trending, Stats ve Feed sekmeleri, dil filtreleri, arama kutusu ve repo kartları" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Filtreleme kodu basit, 25 repo için başka bir şeye gerek yok:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchQuery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filteredRepos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;repos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matchesLang&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;All&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;language&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matchesSearch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;matchesLang&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;matchesSearch&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;h2&gt;
  
  
  Veri hattı
&lt;/h2&gt;

&lt;p&gt;Asıl iş &lt;code&gt;scripts/update.js&lt;/code&gt; içinde. Adımları sırasıyla:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;15 dil için &lt;code&gt;github.com/trending/&amp;lt;dil&amp;gt;?since=daily&lt;/code&gt; sayfaları axios ile çekilip cheerio ile ayrıştırılıyor. Dilsiz genel sayfa yapısı değiştiği için dil dil gitmek daha sağlam çıktı.&lt;/li&gt;
&lt;li&gt;25 repoya ulaşılamazsa GitHub Search API devreye giriyor.&lt;/li&gt;
&lt;li&gt;Geçersiz kayıtlar ve popüler dil listesi dışındakiler eleniyor.&lt;/li&gt;
&lt;li&gt;Hâlâ eksik varsa son 7 günün snapshot'larından repo ekleniyor.&lt;/li&gt;
&lt;li&gt;Sonuç dünkü snapshot'la karşılaştırılıyor: yeni girenler, yükselenler, düşenler, günün yıldızı.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.data/history.json&lt;/code&gt; en fazla 7 snapshot tutuyor, eski snapshot dosyaları siliniyor.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;İki sayı ölçüm değil, tahmin. Search API "bugün kaç yıldız aldı" bilgisini vermiyor, bu yüzden oradan gelen repolar için günlük yıldızı toplam yıldızın %1'i olarak tahmin ediyorum. Geçmişten eklenen repolarda da eski günlük yıldızın yarısını kullanıyorum. İkisi de listeyi doldurmak için kaba tahminler, gerçek veri değil. Arayüzde bunları ayrı işaretlemek yapılacaklar listemde.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Actions workflow'u
&lt;/h2&gt;

&lt;p&gt;Workflow günde bir kez cron ile, istendiğinde elle ve &lt;code&gt;src/&lt;/code&gt; ya da &lt;code&gt;scripts/&lt;/code&gt; değiştiğinde push ile çalışıyor. Önce veriyi güncelliyor, sonra build alıyor, değişiklik varsa commit atıyor ve Pages'e yayınlıyor. Repodaki halinin kısaltılmışı:&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="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update and Deploy Trends&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;src/**'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;scripts/**'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;package.json'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;package-lock.json'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;pages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

&lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pages&lt;/span&gt;
  &lt;span class="na"&gt;cancel-in-progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;update-and-build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v7&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;22'&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;npm ci&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;npm run update&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&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;npm run build&lt;/span&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;Commit data changes&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;if [ -z "$(git status --porcelain .data/ public/data/)" ]; then exit 0; fi&lt;/span&gt;
          &lt;span class="s"&gt;git config user.name 'github-actions[bot]'&lt;/span&gt;
          &lt;span class="s"&gt;git config user.email 'github-actions[bot]@users.noreply.github.com'&lt;/span&gt;
          &lt;span class="s"&gt;git add .data/ public/data/&lt;/span&gt;
          &lt;span class="s"&gt;git commit -m "chore: update trending repositories [skip ci]"&lt;/span&gt;
          &lt;span class="s"&gt;git push&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-pages-artifact@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./dist&lt;/span&gt;

  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;update-and-build&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&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;github-pages&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.deployment.outputs.page_url }}&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deployment&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/deploy-pages@v5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repodaki dosya hâlâ &lt;code&gt;checkout@v4&lt;/code&gt;, &lt;code&gt;setup-node@v4&lt;/code&gt;, &lt;code&gt;upload-pages-artifact@v3&lt;/code&gt; ve Node 20 kullanıyor. Node 20'nin desteği Nisan 2026'da bitti. Sıfırdan kuruyorsanız yukarıdaki sürümleri kullanın.&lt;/p&gt;

&lt;p&gt;Birkaç ayrıntı işe yarıyor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;[skip ci]&lt;/code&gt; olmadan botun commit'i workflow'u yeniden tetikleyebilir. &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; ile atılan push'lar zaten yeni workflow başlatmıyor ama işareti bırakmak zararsız.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;concurrency&lt;/code&gt; aynı anda iki deploy'un çakışmasını önlüyor.&lt;/li&gt;
&lt;li&gt;Build, update'ten sonra çalışıyor. Vite &lt;code&gt;public/&lt;/code&gt; içeriğini &lt;code&gt;dist/&lt;/code&gt; içine kopyaladığı için site her zaman günün JSON'uyla yayınlanıyor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Yukarıdaki commit adımında &lt;code&gt;public/data/&lt;/code&gt; klasörünün tamamını ekliyorum. Repodaki workflow ise yalnızca &lt;code&gt;public/data/trending.json&lt;/code&gt; dosyasını commit ediyor ve &lt;code&gt;feed.json&lt;/code&gt; dışarıda kalıyor. Sonuç: her çalıştırma repodaki eski feed'i okuyup üstüne tek bir satır ekliyor. Canlı sitedeki akış bu yüzden hep 27 Mart kaydı ile günün kaydından oluşuyor. Düzeltmesi yukarıdaki gibi tek satır.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vite ve GitHub Pages: base yolu
&lt;/h2&gt;

&lt;p&gt;Site bir proje sayfası olarak &lt;code&gt;/awesome-trending-repos/&lt;/code&gt; altında yayınlanıyor. Vite'a bunu söylemezseniz build'deki asset yolları kökü gösterir ve production'da her şey 404 verir:&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;// vite.config.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;react&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vitejs/plugin-react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;tailwindcss&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tailwindcss/vite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;react&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;tailwindcss&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
  &lt;span class="na"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/awesome-trending-repos/&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vite production build'de dosya adlarına zaten içerik hash'i ekliyor, cache kırmak için ek bir &lt;code&gt;rollupOptions&lt;/code&gt; ayarı gerekmiyor. Arayüz JSON'u &lt;code&gt;fetch('./data/trending.json')&lt;/code&gt; ile göreli yoldan okuyor, bu da &lt;code&gt;base&lt;/code&gt; ile uyumlu.&lt;/p&gt;

&lt;h2&gt;
  
  
  Özel alan adı hatası
&lt;/h2&gt;

&lt;p&gt;Proje sayfasında &lt;code&gt;public/CNAME&lt;/code&gt; dosyasına kök alan adını yazmak sık yapılan bir hata ve bu repoda da öyle bir dosya vardı. &lt;code&gt;furkankoykiran.com.tr&lt;/code&gt; kullanıcı sitemin (&lt;code&gt;furkankoykiran.github.io&lt;/code&gt;) alan adı. Proje sayfaları bu alan adını kendiliğinden devralıyor ve &lt;code&gt;furkankoykiran.com.tr/awesome-trending-repos/&lt;/code&gt; adresinden yayınlanıyor. Projenin kendi CNAME dosyasında kök alan adını taşıması, kullanıcı sitesiyle aynı adı sahiplenmeye çalışması demek.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/furkankoykiran/awesome-trending-repos/pull/7" rel="noopener noreferrer"&gt;PR #7&lt;/a&gt; ile &lt;code&gt;public/CNAME&lt;/code&gt; ve &lt;code&gt;dist/CNAME&lt;/code&gt; dosyalarını sildim. &lt;code&gt;actions/deploy-pages&lt;/code&gt; kullanıyorsanız artifact içindeki CNAME dosyası zaten yok sayılıyor. Alan adını repo ayarlarındaki Settings → Pages → Custom domain alanı belirliyor. Proje sayfası için bu alan boş kalmalı.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sonraki adımlar
&lt;/h2&gt;

&lt;p&gt;Proje her gün çalışmaya devam ediyor. Sıradaki işler: feed hatasını kapatmak, tahmini yıldız sayılarını arayüzde işaretlemek ve workflow sürümlerini güncellemek.&lt;/p&gt;

&lt;p&gt;Canlı siteye &lt;a href="https://furkankoykiran.com.tr/awesome-trending-repos/" rel="noopener noreferrer"&gt;furkankoykiran.com.tr/awesome-trending-repos&lt;/a&gt; adresinden, projenin özetine &lt;a href="https://furkankoykiran.com.tr/projeler/awesome-trending-repos/" rel="noopener noreferrer"&gt;proje sayfasından&lt;/a&gt; bakabilirsiniz. GitHub Actions dakikalarınız bitiyorsa &lt;a href="https://blog.furkankoykiran.com.tr/posts/github-actions-self-hosted-runner-tr/" rel="noopener noreferrer"&gt;self-hosted runner yazım&lt;/a&gt;, daha büyük pipeline'lar için &lt;a href="https://blog.furkankoykiran.com.tr/posts/github-actions-cicd-pipeline/" rel="noopener noreferrer"&gt;GitHub Actions CI/CD yazısı&lt;/a&gt; iyi bir devam olur. Bu blogun GitHub Pages yolculuğunu da &lt;a href="https://blog.furkankoykiran.com.tr/posts/gitlab-github-macerasi-jekyll-altyapisi/" rel="noopener noreferrer"&gt;GitLab ve GitHub macerası&lt;/a&gt; yazısında anlattım.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://blog.furkankoykiran.com.tr/posts/awesome-trending-repos-web/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;. I write about MCP servers, AI agents, developer tools and Python automation; more at &lt;a href="https://furkankoykiran.com.tr/en/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;furkankoykiran.com.tr&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>automation</category>
      <category>cicd</category>
      <category>web</category>
    </item>
    <item>
      <title>CLI-Anything Browser Harness: My First MCP Backend PR</title>
      <dc:creator>Furkan Köykıran</dc:creator>
      <pubDate>Sun, 22 Mar 2026 22:40:08 +0000</pubDate>
      <link>https://dev.to/furkankoykiran/adding-browser-automation-to-cli-anything-first-mcp-backend-pattern-4bb3</link>
      <guid>https://dev.to/furkankoykiran/adding-browser-automation-to-cli-anything-first-mcp-backend-pattern-4bb3</guid>
      <description>&lt;p&gt;In March 2026 I sent a pull request that added browser automation to &lt;a href="https://github.com/HKUDS/CLI-Anything" rel="noopener noreferrer"&gt;CLI-Anything&lt;/a&gt;. &lt;a href="https://github.com/HKUDS/CLI-Anything/pull/118" rel="noopener noreferrer"&gt;PR #118&lt;/a&gt; became the project's first harness that talks to an MCP server instead of calling an application directly. This post covers how the harness is built, what came up in review, and the bugs that only showed up after it was merged.&lt;/p&gt;

&lt;p&gt;CLI-Anything's premise is that if you wrap software in a clean command-line interface, AI agents can drive it. The repo has "harnesses" for GIMP, Blender, LibreOffice and dozens of other tools. Browsers were missing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue #90: browsing a page like a filesystem
&lt;/h2&gt;

&lt;p&gt;On March 16, &lt;a href="https://github.com/apireno" rel="noopener noreferrer"&gt;@apireno&lt;/a&gt;, the author of &lt;a href="https://github.com/apireno/DOMShell" rel="noopener noreferrer"&gt;DOMShell&lt;/a&gt;, opened &lt;a href="https://github.com/HKUDS/CLI-Anything/issues/90" rel="noopener noreferrer"&gt;issue #90&lt;/a&gt;. DOMShell is a Chrome extension plus MCP server that exposes Chrome's Accessibility Tree as a virtual filesystem, so an agent moves through a page with &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cd&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt; and &lt;code&gt;click&lt;/code&gt;. In apireno's benchmarks with Claude, this cut API calls by half compared with screenshot-based browsing. That number is his; I didn't measure it myself.&lt;/p&gt;

&lt;p&gt;The Accessibility Tree is the simplified tree the browser derives from the DOM for screen readers. Each node has a role such as button, link or textbox, and the CSS classes and wrapper &lt;code&gt;div&lt;/code&gt;s are gone. That makes it less brittle than CSS selectors when a site's markup changes, and the page → section → element hierarchy maps cleanly onto directories.&lt;/p&gt;

&lt;p&gt;In the issue thread, &lt;a href="https://github.com/omerarslan0" rel="noopener noreferrer"&gt;@omerarslan0&lt;/a&gt; sketched an integration: DOMShell's MCP server as the backend, command groups like &lt;code&gt;page&lt;/code&gt; on the CLI side, and state (URL, working directory) kept by the CLI. apireno asked to keep the filesystem-style commands rather than switch to selectors, and recommended the official &lt;code&gt;mcp&lt;/code&gt; Python SDK over stdio. I built on that discussion and opened the PR on March 21.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the harness is built
&lt;/h2&gt;

&lt;p&gt;Most harnesses shell out to an application or call its API. This one puts an MCP client in the middle:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fen%2Fposts%2Fcli-anything-browser-automation-contribution%2Fimg%2Fharness-steps.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fen%2Fposts%2Fcli-anything-browser-automation-contribution%2Fimg%2Fharness-steps.webp" title="From command to Chrome, with an MCP client in between" alt="Click commands call domshell_backend.py, an MCP client that talks over stdio to DOMShell, which exposes Chrome's Accessibility Tree as a filesystem" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are four command groups:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;page&lt;/code&gt;: open, reload, back, forward, info&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fs&lt;/code&gt;: ls, cd, cat, grep, pwd&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;act&lt;/code&gt;: click, type&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;session&lt;/code&gt;: status, daemon-start, daemon-stop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cli-anything-browser page open https://example.com
cli-anything-browser fs &lt;span class="nb"&gt;ls&lt;/span&gt; /
cli-anything-browser fs &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Login"&lt;/span&gt;
cli-anything-browser act click /main/button[0]
cli-anything-browser &lt;span class="nt"&gt;--json&lt;/span&gt; fs &lt;span class="nb"&gt;ls&lt;/span&gt; /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one-shot command starts a fresh session. Stateful flows, like &lt;code&gt;cd&lt;/code&gt; followed by a bare &lt;code&gt;ls&lt;/code&gt;, belong in the REPL, which opens when you run &lt;code&gt;cli-anything-browser&lt;/code&gt; with no arguments.&lt;/p&gt;

&lt;h3&gt;
  
  
  The MCP call in Python
&lt;/h3&gt;

&lt;p&gt;When I wrote the PR, DOMShell exposed one MCP tool per command (&lt;code&gt;domshell_ls&lt;/code&gt;, &lt;code&gt;domshell_click&lt;/code&gt; and so on). DOMShell 2.0 collapsed those into a single &lt;code&gt;domshell_execute&lt;/code&gt; tool, and the harness was updated for it. A minimal client for DOMShell 2.x looks like this:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&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="n"&gt;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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npx&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="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-p&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;@apireno/domshell&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;domshell-proxy&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;--port&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DOMSHELL_PORT&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;3001&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;--token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DOMSHELL_TOKEN&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="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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&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;with&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;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;as &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;read&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="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&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;read&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="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&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;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;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;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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domshell_execute&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;command&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;ls&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;group_id&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;new&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="nf"&gt;print&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="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="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;&lt;code&gt;domshell-proxy&lt;/code&gt; is a stdio bridge to a DOMShell server that is already running, and &lt;code&gt;DOMSHELL_TOKEN&lt;/code&gt; is the token that server prints at startup. &lt;code&gt;group_id: "new"&lt;/code&gt; asks DOMShell 2.x for an isolated tab group for the agent. None of this was in my original PR, and the section after the review explains why.&lt;/p&gt;

&lt;h3&gt;
  
  
  A sync CLI on top of an async SDK
&lt;/h3&gt;

&lt;p&gt;The MCP Python SDK is fully async and Click commands are sync, so each command opens a short session inside &lt;code&gt;asyncio.run()&lt;/code&gt; and closes it. The cost is spawning an &lt;code&gt;npx&lt;/code&gt; subprocess on every command. To avoid that during interactive use I added a daemon mode: while the REPL runs, one MCP connection stays open and every command reuses it. It only works inside a single process. Separate CLI invocations don't share it, and the README says so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests
&lt;/h2&gt;

&lt;p&gt;Two layers. The 31 unit tests in &lt;code&gt;test_core.py&lt;/code&gt; mock MCP responses and cover path resolution and session state, with no Chrome needed. The end-to-end tests in &lt;code&gt;test_full_e2e.py&lt;/code&gt; drive a real Chrome with the DOMShell extension and only run when you ask for them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest cli_anything/browser/tests/test_core.py &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;span class="nv"&gt;DOMSHELL_E2E&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 pytest cli_anything/browser/tests/test_full_e2e.py &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;DOMSHELL_E2E&lt;/code&gt; gate came out of a Copilot comment. Before it, pytest could try to run &lt;code&gt;npx&lt;/code&gt; during test collection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review
&lt;/h2&gt;

&lt;p&gt;Copilot reviewed the PR twice, with 10 comments and then 9. The ones that mattered most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A context manager leak in daemon mode. &lt;code&gt;_start_daemon()&lt;/code&gt; called &lt;code&gt;__aenter__&lt;/code&gt; on &lt;code&gt;stdio_client(...)&lt;/code&gt; without keeping a reference, and &lt;code&gt;_stop_daemon()&lt;/code&gt; called &lt;code&gt;__aexit__&lt;/code&gt; on a brand-new client, so the original subprocess never shut down. I now keep the reference in a module-level &lt;code&gt;_daemon_client_context&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;page back&lt;/code&gt; and &lt;code&gt;page forward&lt;/code&gt; replayed navigation from a local history list, which is always empty in one-shot mode. They now call DOMShell's own back and forward tools.&lt;/li&gt;
&lt;li&gt;The REPL split quoted arguments. &lt;code&gt;act type /path "Hello, World!"&lt;/code&gt; now goes through &lt;code&gt;shlex.split()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;act type&lt;/code&gt; echoed the typed text, which leaves passwords in terminal scrollback. It no longer does.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fs cd&lt;/code&gt; only updates the working directory when the backend reports success.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;omerarslan0 listed 8 issues in his first review, all of which earlier commits had already fixed. I replied with the commit for each one, and he checked and approved. &lt;a href="https://github.com/yuh-yang" rel="noopener noreferrer"&gt;@yuh-yang&lt;/a&gt; asked me to fix the contributor field in &lt;code&gt;registry.json&lt;/code&gt;, then approved and merged on March 22. Final size: 15 commits, 21 files, 3,095 lines added:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fposts%2Fcli-anything-browser-automation-katkim%2Fimg%2Fcli-anything-pr-118-header.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fposts%2Fcli-anything-browser-automation-katkim%2Fimg%2Fcli-anything-pr-118-header.webp" title="CLI-Anything PR #118. Source: GitHub" alt="Header of CLI-Anything PR #118: merged by yuh-yang, 15 commits, +3,095 −1" width="800" height="100"&gt;&lt;/a&gt; I also added an "MCP Backend Pattern" section to &lt;code&gt;cli-anything-plugin/HARNESS.md&lt;/code&gt; so the next MCP integration has something to follow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke after the merge
&lt;/h2&gt;

&lt;p&gt;I learned the most from this PR after it was merged. Right after the merge, apireno tested the harness against a real DOMShell server. All 31 unit tests passed and 9 of 10 end-to-end tests passed, but several commands failed when he used them by hand:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In one-shot mode every command started a new &lt;code&gt;npx @apireno/domshell&lt;/code&gt; process, which tried to bind ports 9876 and 3001. If another client such as Claude Desktop was already running DOMShell, every command failed.&lt;/li&gt;
&lt;li&gt;I was sending the wrong parameter names to some tools (&lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;click&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;DOMShell didn't support &lt;code&gt;--version&lt;/code&gt;, which my availability check relied on. apireno fixed that on the DOMShell side.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;apireno fixed the port and parameter problems in &lt;a href="https://github.com/HKUDS/CLI-Anything/pull/135" rel="noopener noreferrer"&gt;PR #135&lt;/a&gt;, merged on March 24, which moved the connection to the &lt;code&gt;domshell-proxy&lt;/code&gt; bridge with token auth. My unit tests passed because they mocked a server interface that didn't match the real one. Pulling the tool schemas from the server with &lt;code&gt;list_tools&lt;/code&gt; and building the mocks from them would have caught this before review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security hardening in PR #156
&lt;/h2&gt;

&lt;p&gt;After I shared the contribution on LinkedIn, Ilhan Goktas pointed out several attack surfaces: SSRF through arbitrary navigation, prompt injection hidden in ARIA labels, and escaping the virtual filesystem. I followed up with &lt;a href="https://github.com/HKUDS/CLI-Anything/pull/156" rel="noopener noreferrer"&gt;PR #156&lt;/a&gt;, which omerarslan0 merged on April 7.&lt;/p&gt;

&lt;p&gt;It added:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;URL validation that blocks &lt;code&gt;file://&lt;/code&gt;, &lt;code&gt;javascript:&lt;/code&gt;, &lt;code&gt;data:&lt;/code&gt; and &lt;code&gt;vbscript:&lt;/code&gt; schemes&lt;/li&gt;
&lt;li&gt;Optional blocking of private network addresses, switched on with an environment variable&lt;/li&gt;
&lt;li&gt;A basic sanitizer for known prompt injection patterns in page content&lt;/li&gt;
&lt;li&gt;42 security tests, plus security notes for agents in &lt;code&gt;SKILL.md&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sanitizer catches obvious patterns and nothing more. If you give an agent a browser, treat every piece of page text as untrusted input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the code went next
&lt;/h2&gt;

&lt;p&gt;I wrote the harness, and most of the code that runs now has been through other hands since. apireno did the DOMShell 2.0 migration and the fixes after it, and other contributors followed. That's normal for open source, and it's a good reason to write code that's easy for the next person to change.&lt;/p&gt;

&lt;p&gt;What I took away from this PR:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When you wrap an MCP server, run at least one end-to-end test against the real server before asking for a merge.&lt;/li&gt;
&lt;li&gt;When you bridge an async SDK into a sync CLI, keep the connection lifecycle in one place. That's where the leaks come from. I go deeper into the asyncio side in my &lt;a href="https://blog.furkankoykiran.com.tr/en/posts/python-asyncio-concurrent-processing/" rel="noopener noreferrer"&gt;asyncio guide&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Answer review comments with the commit hash for each fix. It makes the reviewer's job much easier.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;The code is in &lt;a href="https://github.com/HKUDS/CLI-Anything/tree/main/browser/agent-harness" rel="noopener noreferrer"&gt;browser/agent-harness&lt;/a&gt;. For an MCP server I built from scratch, read the &lt;a href="https://blog.furkankoykiran.com.tr/en/posts/freqtrade-mcp-server/" rel="noopener noreferrer"&gt;freqtrade-mcp post&lt;/a&gt;. For another open source contribution, with a very different codebase, see &lt;a href="https://blog.furkankoykiran.com.tr/en/posts/my-open-source-contribution-keploy/" rel="noopener noreferrer"&gt;my Keploy contribution&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://blog.furkankoykiran.com.tr/en/posts/cli-anything-browser-automation-contribution/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;. I write about MCP servers, AI agents, developer tools and Python automation; more at &lt;a href="https://furkankoykiran.com.tr/en/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;furkankoykiran.com.tr&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>CLI-Anything'e Tarayıcı Desteği: İlk MCP Backend Katkım</title>
      <dc:creator>Furkan Köykıran</dc:creator>
      <pubDate>Sun, 22 Mar 2026 22:40:06 +0000</pubDate>
      <link>https://dev.to/furkankoykiran/cli-anythinge-tarayici-otomasyon-destegi-mcp-backend-pattern-ilk-uygulama-221d</link>
      <guid>https://dev.to/furkankoykiran/cli-anythinge-tarayici-otomasyon-destegi-mcp-backend-pattern-ilk-uygulama-221d</guid>
      <description>&lt;p&gt;Mart 2026'da &lt;a href="https://github.com/HKUDS/CLI-Anything" rel="noopener noreferrer"&gt;CLI-Anything&lt;/a&gt; projesine tarayıcı otomasyonu ekleyen bir PR gönderdim. &lt;a href="https://github.com/HKUDS/CLI-Anything/pull/118" rel="noopener noreferrer"&gt;PR #118&lt;/a&gt;, projede bir MCP sunucusunu backend olarak kullanan ilk "harness" oldu. Bu yazıda neyi neden öyle kurduğumu, review'da neler çıktığını ve merge'den sonra ortaya çıkan, benim kaçırdığım hataları anlatıyorum.&lt;/p&gt;

&lt;p&gt;CLI-Anything'in fikri basit: bir yazılımı komut satırından yönetilebilir hale getirirsen, AI ajanları da onu kullanabilir. Projede GIMP, Blender, LibreOffice gibi uygulamalar için "harness" denen CLI sarmalayıcıları var. Tarayıcı listede yoktu.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue #90: tarayıcı neden dosya sistemi gibi gezilsin?
&lt;/h2&gt;

&lt;p&gt;16 Mart 2026'da DOMShell'in geliştiricisi &lt;a href="https://github.com/apireno" rel="noopener noreferrer"&gt;@apireno&lt;/a&gt;, &lt;a href="https://github.com/HKUDS/CLI-Anything/issues/90" rel="noopener noreferrer"&gt;issue #90&lt;/a&gt;'ı açtı. &lt;a href="https://github.com/apireno/DOMShell" rel="noopener noreferrer"&gt;DOMShell&lt;/a&gt;, Chrome'un Accessibility Tree'sini sanal bir dosya sistemi olarak sunan bir Chrome eklentisi ve MCP sunucusu. Ajan sayfada &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cd&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;click&lt;/code&gt; gibi komutlarla dolaşıyor. apireno'nun Claude ile yaptığı ölçümlere göre bu yaklaşım, ekran görüntüsü tabanlı gezinmeye kıyasla API çağrılarını yarıya indiriyordu. Bu rakam onun benchmark'ı, ben ayrıca ölçmedim.&lt;/p&gt;

&lt;p&gt;Accessibility Tree, tarayıcının ekran okuyucular için DOM'dan çıkardığı sadeleştirilmiş ağaç. Her öğe bir rol taşıyor: button, link, textbox. CSS sınıfları ve iç içe geçmiş &lt;code&gt;div&lt;/code&gt;'ler burada yok, bu yüzden sayfa tasarımı değiştiğinde selector'lara göre daha az kırılıyor. Sayfa → bölümler → öğeler hiyerarşisi de bir dizin yapısına doğal olarak oturuyor.&lt;/p&gt;

&lt;p&gt;Issue altında &lt;a href="https://github.com/omerarslan0" rel="noopener noreferrer"&gt;@omerarslan0&lt;/a&gt; bir entegrasyon taslağı önerdi: DOMShell'in MCP sunucusu backend olacak, CLI tarafı &lt;code&gt;page&lt;/code&gt; gibi komut grupları sunacak, durum (URL, çalışma dizini) CLI'da tutulacak. apireno da selector tabanlı komutlar yerine dosya sistemi komutlarının korunmasını ve resmi &lt;code&gt;mcp&lt;/code&gt; Python SDK'sının stdio transport'uyla kullanılmasını önerdi. Ben bu konuşmanın üzerine implementasyonu yazıp 21 Mart'ta PR'ı açtım.&lt;/p&gt;

&lt;h2&gt;
  
  
  Harness'ın yapısı
&lt;/h2&gt;

&lt;p&gt;Diğer harness'lar genelde uygulamanın kendi API'sini ya da CLI'ını çağırıyor. Burada araya bir MCP istemcisi giriyor:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fposts%2Fcli-anything-browser-automation-katkim%2Fimg%2Fharness-steps.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fposts%2Fcli-anything-browser-automation-katkim%2Fimg%2Fharness-steps.webp" title="Komuttan Chrome'a: araya giren MCP istemcisi" alt="Click komutları domshell_backend.py MCP istemcisini çağırıyor; istemci stdio ile DOMShell'e bağlanıyor, DOMShell de Chrome'un Accessibility Tree'sini sunuyor" width="800" height="519"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Komutlar dört grupta toplandı:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;page&lt;/code&gt;: open, reload, back, forward, info&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fs&lt;/code&gt;: ls, cd, cat, grep, pwd&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;act&lt;/code&gt;: click, type&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;session&lt;/code&gt;: status, daemon-start, daemon-stop&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kullanımı şöyle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cli-anything-browser page open https://example.com
cli-anything-browser fs &lt;span class="nb"&gt;ls&lt;/span&gt; /
cli-anything-browser fs &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Login"&lt;/span&gt;
cli-anything-browser act click /main/button[0]
cli-anything-browser &lt;span class="nt"&gt;--json&lt;/span&gt; fs &lt;span class="nb"&gt;ls&lt;/span&gt; /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Her tek seferlik komut yeni bir oturumla başlıyor. &lt;code&gt;cd&lt;/code&gt; yapıp ardından yolsuz &lt;code&gt;ls&lt;/code&gt; çalıştırmak gibi durum gerektiren akışlar için REPL var (&lt;code&gt;cli-anything-browser&lt;/code&gt; komutunu argümansız çalıştırınca açılıyor).&lt;/p&gt;

&lt;h3&gt;
  
  
  MCP çağrısı Python'da nasıl görünüyor
&lt;/h3&gt;

&lt;p&gt;PR'ı yazdığım günlerde DOMShell her komut için ayrı bir MCP aracı sunuyordu (&lt;code&gt;domshell_ls&lt;/code&gt;, &lt;code&gt;domshell_click&lt;/code&gt; gibi). DOMShell 2.0 bunları tek bir &lt;code&gt;domshell_execute&lt;/code&gt; aracında topladı ve harness da buna göre güncellendi. DOMShell 2.x ile çalışan minimal bir istemci şöyle:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&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="n"&gt;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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;npx&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="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-p&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;@apireno/domshell&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;domshell-proxy&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;--port&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DOMSHELL_PORT&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;3001&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;--token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DOMSHELL_TOKEN&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="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="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&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;with&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;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;as &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;read&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="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&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;read&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="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&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;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;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;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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domshell_execute&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;command&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;ls&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;group_id&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;new&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="nf"&gt;print&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="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="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;&lt;code&gt;domshell-proxy&lt;/code&gt;, zaten çalışan bir DOMShell sunucusuna stdio köprüsü kuruyor. &lt;code&gt;DOMSHELL_TOKEN&lt;/code&gt; o sunucunun açılışta verdiği token. &lt;code&gt;group_id: "new"&lt;/code&gt; ise DOMShell 2.x'te ajana ayrı bir sekme grubu açıyor. Bu ayrıntıların hiçbiri benim ilk PR'ımda yoktu, aşağıda nedenini anlatıyorum.&lt;/p&gt;

&lt;h3&gt;
  
  
  Senkron CLI, asenkron SDK
&lt;/h3&gt;

&lt;p&gt;MCP Python SDK tamamen async. Click komutları ise senkron. Her komut kendi içinde &lt;code&gt;asyncio.run()&lt;/code&gt; ile kısa bir oturum açıp kapatıyor. Bunun bedeli, her komutta &lt;code&gt;npx&lt;/code&gt; ile alt süreç başlatmak. Bu yüzden bir de daemon modu ekledim: REPL açıkken tek bir MCP bağlantısı açık kalıyor ve bütün komutlar onu kullanıyor. Daemon modu yalnızca tek bir süreç içinde çalışıyor. Ayrı CLI çağrıları arasında bağlantı taşınmıyor, bunu README'ye de açıkça yazdım.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testler
&lt;/h2&gt;

&lt;p&gt;İki katman vardı. &lt;code&gt;test_core.py&lt;/code&gt; içindeki 31 unit test MCP yanıtlarını mock'luyor, yol çözümlemeyi ve oturum durumunu test ediyordu. Chrome gerekmiyordu. &lt;code&gt;test_full_e2e.py&lt;/code&gt; içindeki E2E testler ise gerçek Chrome ve DOMShell eklentisiyle çalışıyor ve yalnızca açıkça istendiğinde koşuyordu:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest cli_anything/browser/tests/test_core.py &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;span class="nv"&gt;DOMSHELL_E2E&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 pytest cli_anything/browser/tests/test_full_e2e.py &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DOMSHELL_E2E&lt;/code&gt; bayrağını Copilot'un bir yorumundan sonra ekledim. Öncesinde test toplama aşaması bile &lt;code&gt;npx&lt;/code&gt; çalıştırmaya kalkabiliyordu.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review süreci
&lt;/h2&gt;

&lt;p&gt;PR'a Copilot iki tur review yaptı: ilk turda 10, ikinci turda 9 yorum. Birkaçı gerçekten önemliydi:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Daemon modunda context manager sızıntısı. &lt;code&gt;_start_daemon()&lt;/code&gt; içinde &lt;code&gt;stdio_client(...)&lt;/code&gt; için &lt;code&gt;__aenter__&lt;/code&gt; çağrılıyor ama referans saklanmıyordu, &lt;code&gt;_stop_daemon()&lt;/code&gt; ise yepyeni bir istemcide &lt;code&gt;__aexit__&lt;/code&gt; çağırıyordu. Asıl alt süreç hiç kapanmıyordu. Referansı &lt;code&gt;_daemon_client_context&lt;/code&gt; adlı modül değişkeninde tutarak düzelttim.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;page back&lt;/code&gt; ve &lt;code&gt;page forward&lt;/code&gt; CLI içindeki yerel geçmişten yeniden navigasyon yapıyordu. Tek seferlik kullanımda geçmiş hep boş olduğu için çalışmıyordu. DOMShell'in kendi geri/ileri araçlarını çağıracak şekilde değiştirdim.&lt;/li&gt;
&lt;li&gt;REPL'de tırnaklı argümanlar bölünüyordu. &lt;code&gt;act type /path "Hello, World!"&lt;/code&gt; artık &lt;code&gt;shlex.split()&lt;/code&gt; ile doğru ayrıştırılıyor.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;act type&lt;/code&gt; yazılan metni ekrana basıyordu. Şifre yazdırırsanız terminal geçmişinde kalıyor. Artık basmıyor.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fs cd&lt;/code&gt; yalnızca backend başarılı yanıt verince çalışma dizinini değiştiriyor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;omerarslan0 ilk review'unda 8 madde sıraladı. Hepsi önceki commit'lerde düzeltilmişti, commit'leri tek tek eşleştirip cevap yazdım, o da kontrol edip onayladı. &lt;a href="https://github.com/yuh-yang" rel="noopener noreferrer"&gt;@yuh-yang&lt;/a&gt; &lt;code&gt;registry.json&lt;/code&gt;'daki contributor alanının düzeltilmesini istedi, sonra PR'ı onaylayıp 22 Mart'ta merge etti. Toplam: 15 commit, 21 dosya, 3.095 satır ekleme:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fposts%2Fcli-anything-browser-automation-katkim%2Fimg%2Fcli-anything-pr-118-header.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fposts%2Fcli-anything-browser-automation-katkim%2Fimg%2Fcli-anything-pr-118-header.webp" title="CLI-Anything PR #118. Kaynak: GitHub" alt="CLI-Anything PR #118'in başlığı: yuh-yang tarafından merge edildi, 15 commit, +3.095 −1" width="800" height="100"&gt;&lt;/a&gt; Projenin &lt;code&gt;cli-anything-plugin/HARNESS.md&lt;/code&gt; dosyasına da diğer MCP entegrasyonları için bir "MCP Backend Pattern" bölümü ekledim.&lt;/p&gt;

&lt;h2&gt;
  
  
  Merge'den sonra ne bozuldu
&lt;/h2&gt;

&lt;p&gt;Bu PR'dan en çok şeyi merge'den sonra öğrendim. Merge'den hemen sonra apireno harness'ı gerçek DOMShell sunucusuna karşı test etti. 31 unit testin hepsi geçti, E2E'de 10 testten 9'u geçti. Elle denemelerde ise birkaç komut çalışmadı:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tek seferlik modda her komut yeni bir &lt;code&gt;npx @apireno/domshell&lt;/code&gt; süreci başlatıyordu ve bu süreç 9876 ile 3001 portlarını açmaya çalışıyordu. Claude Desktop gibi başka bir istemci DOMShell'i zaten çalıştırıyorsa bütün komutlar düşüyordu.&lt;/li&gt;
&lt;li&gt;Bazı MCP araçlarına yanlış parametre adları gönderiyordum (&lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;click&lt;/code&gt; için).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--version&lt;/code&gt; desteği DOMShell'de yoktu, bunu apireno kendi tarafında düzeltti.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Port ve parametre sorunlarını apireno &lt;a href="https://github.com/HKUDS/CLI-Anything/pull/135" rel="noopener noreferrer"&gt;PR #135&lt;/a&gt; ile düzeltti (24 Mart'ta merge edildi). Bağlantı modeli &lt;code&gt;domshell-proxy&lt;/code&gt; köprüsüne ve token tabanlı kimlik doğrulamaya geçti. Yani benim unit testlerim mock'larla geçiyordu ama mock'lar sunucunun gerçek arayüzünü yansıtmıyordu. Bir MCP sunucusunu sararken araç şemalarını sunucudan &lt;code&gt;list_tools&lt;/code&gt; ile çekip testleri ona göre kurmak bu hatayı baştan yakalardı.&lt;/p&gt;

&lt;h2&gt;
  
  
  Güvenlik sertleştirmesi: PR #156
&lt;/h2&gt;

&lt;p&gt;LinkedIn'de bu katkıyı paylaştıktan sonra Ilhan Goktas birkaç saldırı yüzeyine dikkat çekti: rastgele URL açtırarak SSRF, ARIA etiketlerine gizlenmiş prompt injection, sanal dosya sisteminden kaçış. Bunun üzerine &lt;a href="https://github.com/HKUDS/CLI-Anything/pull/156" rel="noopener noreferrer"&gt;PR #156&lt;/a&gt;'yı açtım. omerarslan0 onu 7 Nisan'da merge etti.&lt;/p&gt;

&lt;p&gt;PR şunları ekledi:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;file://&lt;/code&gt;, &lt;code&gt;javascript:&lt;/code&gt;, &lt;code&gt;data:&lt;/code&gt; ve &lt;code&gt;vbscript:&lt;/code&gt; şemalarını engelleyen URL doğrulaması&lt;/li&gt;
&lt;li&gt;Bir ortam değişkeniyle açılabilen özel ağ engellemesi&lt;/li&gt;
&lt;li&gt;Sayfa içeriğinde bilinen prompt injection kalıpları için basit bir temizleme katmanı&lt;/li&gt;
&lt;li&gt;42 yeni güvenlik testi ve ajanlar için &lt;code&gt;SKILL.md&lt;/code&gt; içine güvenlik notları&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Temizleme katmanı yalnızca bariz kalıpları yakalıyor, prompt injection'ı tek başına çözmüyor. Bir ajana tarayıcı veriyorsanız sayfadaki her metni güvenilmeyen girdi olarak görmek zorundasınız.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kodun sonraki hayatı
&lt;/h2&gt;

&lt;p&gt;Harness'ı ilk ben yazdım, çalışan kodun büyük kısmı ise sonradan başkalarının elinden geçti. DOMShell 2.0 geçişini ve sonraki düzeltmeleri apireno yaptı, ardından başka katkıcılar da dokundu. Açık kaynakta bu olağan, bu yüzden bir sonraki kişinin kolayca değiştirebileceği kod yazmaya değer.&lt;/p&gt;

&lt;p&gt;Bu PR'dan aklımda kalanlar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bir MCP sunucusunu sarıyorsanız gerçek sunucuya karşı en az bir uçtan uca test koşmadan merge istemeyin.&lt;/li&gt;
&lt;li&gt;Async SDK'yı senkron CLI'a bağlarken bağlantının yaşam döngüsünü tek bir yerde tutun. Sızıntıların çoğu oradan çıkıyor. asyncio tarafını &lt;a href="https://blog.furkankoykiran.com.tr/posts/python-asyncio-paralel-islem/" rel="noopener noreferrer"&gt;asyncio rehberimde&lt;/a&gt; daha ayrıntılı anlattım.&lt;/li&gt;
&lt;li&gt;Review yorumlarına her düzeltmenin commit hash'iyle cevap verin. Reviewer'ın kontrol etmesi çok kolaylaşıyor.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sırada ne var
&lt;/h2&gt;

&lt;p&gt;Kodu incelemek isterseniz &lt;a href="https://github.com/HKUDS/CLI-Anything/tree/main/browser/agent-harness" rel="noopener noreferrer"&gt;browser/agent-harness&lt;/a&gt; dizininden başlayın. Kendi yazdığım MCP sunucularından biri için &lt;a href="https://blog.furkankoykiran.com.tr/posts/freqtrade-mcp-server-tr/" rel="noopener noreferrer"&gt;freqtrade-mcp yazısına&lt;/a&gt;, diğer açık kaynak katkılarım için &lt;a href="https://blog.furkankoykiran.com.tr/posts/mcp-ai-contributions/" rel="noopener noreferrer"&gt;MCP ve AI platformlarına katkılarım&lt;/a&gt; ve &lt;a href="https://blog.furkankoykiran.com.tr/posts/keploy-acik-kaynak-katkim/" rel="noopener noreferrer"&gt;Keploy katkım&lt;/a&gt; yazılarına bakabilirsiniz.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://blog.furkankoykiran.com.tr/posts/cli-anything-browser-automation-katkim/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;. I write about MCP servers, AI agents, developer tools and Python automation; more at &lt;a href="https://furkankoykiran.com.tr/en/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;furkankoykiran.com.tr&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Keploy Contribution: Go defer and HTTP Body Leaks</title>
      <dc:creator>Furkan Köykıran</dc:creator>
      <pubDate>Sat, 21 Mar 2026 00:27:26 +0000</pubDate>
      <link>https://dev.to/furkankoykiran/my-keploy-contribution-resource-management-in-go-and-open-source-journey-2139</link>
      <guid>https://dev.to/furkankoykiran/my-keploy-contribution-resource-management-in-go-and-open-source-journey-2139</guid>
      <description>&lt;p&gt;While following GitHub trending with &lt;a href="https://github.com/furkankoykiran/awesome-trending-repos" rel="noopener noreferrer"&gt;awesome-trending-repos&lt;/a&gt;, I came across &lt;a href="https://github.com/keploy/keploy" rel="noopener noreferrer"&gt;Keploy&lt;/a&gt;, an API testing tool written in Go. Its issue tracker had two resource management bugs, and I sent a PR for each. This post covers that Keploy contribution: using &lt;code&gt;defer&lt;/code&gt; correctly in Go, and why an HTTP response body needs to be both drained and closed. Both PRs were merged on March 20, 2026. They were my first contributions to Keploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Keploy does
&lt;/h2&gt;

&lt;p&gt;Keploy captures your application's real traffic with eBPF and turns it into API tests and mocks for external dependencies. When the tests run again, database and third-party calls are answered from those mocks.&lt;/p&gt;

&lt;p&gt;The two bugs I fixed sit at the edges of this flow: one where Keploy detects the application's language, the other in the HTTP client Keploy uses to talk to its own agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  PR #3927: reading from a closed file
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The bug
&lt;/h3&gt;

&lt;p&gt;Issue &lt;a href="https://github.com/keploy/keploy/issues/3821" rel="noopener noreferrer"&gt;#3821&lt;/a&gt; pointed at &lt;code&gt;isGoBinary&lt;/code&gt; in &lt;code&gt;utils/utils.go&lt;/code&gt;. The function opened an ELF file, closed it right away, and then tried to read sections from the closed file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;elf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filePath&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to open file %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;zap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&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;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c"&gt;// the file is closed here&lt;/span&gt;
    &lt;span class="n"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"failed to close file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;zap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;sections&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;".go.buildinfo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;".gopclntab"&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;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;section&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;sections&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;sect&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;sect&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c"&gt;// ...and used here&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DetectLanguage&lt;/code&gt; relies on &lt;code&gt;isGoBinary&lt;/code&gt;, so the bug sat right where Keploy decides whether an application is written in Go.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;I moved &lt;code&gt;Close()&lt;/code&gt; into a &lt;code&gt;defer&lt;/code&gt;. A deferred call runs when the function returns, whichever way it exits: an early &lt;code&gt;return&lt;/code&gt;, the normal path, or a panic. I also removed a leftover debug &lt;code&gt;fmt.Println(section)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The PR's first commit used a plain &lt;code&gt;defer f.Close()&lt;/code&gt;. Copilot's automated review pointed out that this swallows the error from &lt;code&gt;Close()&lt;/code&gt;, while the rest of the file logs close errors, and that it would probably trip &lt;code&gt;golangci-lint&lt;/code&gt;'s &lt;code&gt;errcheck&lt;/code&gt;. The final version matches the other call sites:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;elf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filePath&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to open file %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;zap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"failed to close file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;zap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}()&lt;/span&gt;

&lt;span class="n"&gt;sections&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;".go.buildinfo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;".gopclntab"&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;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;section&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;sections&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;sect&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;sect&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/keploy/keploy/pull/3927" rel="noopener noreferrer"&gt;PR #3927&lt;/a&gt; was a +5/−4 change. AkashKumar7902 merged it on March 20.&lt;/p&gt;

&lt;h2&gt;
  
  
  PR #3932: response bodies that were never closed
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The bug
&lt;/h3&gt;

&lt;p&gt;Issue &lt;a href="https://github.com/keploy/keploy/issues/3854" rel="noopener noreferrer"&gt;#3854&lt;/a&gt; pointed at &lt;code&gt;MockOutgoing&lt;/code&gt; and &lt;code&gt;UpdateMockParams&lt;/code&gt; in &lt;code&gt;pkg/platform/http/agent.go&lt;/code&gt;. Both sent a request to the agent and never closed &lt;code&gt;res.Body&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to send request for mockOutgoing: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;// res.Body is never closed&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mockResp&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AgentResp&lt;/span&gt;
&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewDecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;mockResp&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;net/http&lt;/code&gt; docs are explicit: the caller must close the body. An unclosed body holds on to its TCP connection, so the connection can't go back to the pool and every request opens a new one. In a long-running process that can end in exhausted file descriptors or ports.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix: drain, then close
&lt;/h3&gt;

&lt;p&gt;Side by side, the whole change is one deferred function:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fen%2Fposts%2Fmy-open-source-contribution-keploy%2Fimg%2Fkeploy-body-close-fix.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fen%2Fposts%2Fmy-open-source-contribution-keploy%2Fimg%2Fkeploy-body-close-fix.webp" title="The fix from PR #3932, shortened" alt="Before: res.Body is never closed. After: a deferred function drains the body with io.Copy(io.Discard, res.Body), then closes it" width="800" height="524"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to send request for mockOutgoing: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Discard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&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;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;utils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"failed to close response body for mockOutgoing"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}()&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mockResp&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AgentResp&lt;/span&gt;
&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewDecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;mockResp&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;io.Copy(io.Discard, res.Body)&lt;/code&gt; line was the most useful thing I learned from this PR. The maintainer, AkashKumar7902, asked me to address the Copilot comments, and one of them explained it: &lt;code&gt;json.Decoder.Decode&lt;/code&gt; doesn't promise to read the body to the end. If the body is closed before it reaches EOF, the &lt;code&gt;Transport&lt;/code&gt; can't reuse the connection. Draining it first is what makes keep-alive actually work.&lt;/p&gt;

&lt;p&gt;One caveat: if the body can be large, an unbounded &lt;code&gt;io.Copy&lt;/code&gt; may pull megabytes you don't need. In that case, cap the drain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Discard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LimitReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c"&gt;// at most 64 KiB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keploy's agent responses are small, so the unbounded version was fine there. &lt;a href="https://github.com/keploy/keploy/pull/3932" rel="noopener noreferrer"&gt;PR #3932&lt;/a&gt; was merged with two commits and 12 added lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rules I now follow with defer
&lt;/h2&gt;

&lt;p&gt;Since these two PRs I stick to a few rules with &lt;code&gt;defer&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Put the &lt;code&gt;defer&lt;/code&gt; right after you acquire the resource and after the error check. If &lt;code&gt;os.Open&lt;/code&gt; fails, &lt;code&gt;f&lt;/code&gt; is nil, and a &lt;code&gt;defer f.Close()&lt;/code&gt; placed before the check will panic.&lt;/p&gt;

&lt;p&gt;Don't &lt;code&gt;defer&lt;/code&gt; inside a loop. Deferred calls run when the function returns, not at the end of each iteration, so thousands of files can stay open. Move the loop body into its own function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;countLines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paths&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;countFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// the defer runs when countFile returns&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;n&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;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;countFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;scanner&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;bufio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewScanner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&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;scanner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scan&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;++&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;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scanner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&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;Multiple defers run in reverse order (LIFO). Their arguments are evaluated when the &lt;code&gt;defer&lt;/code&gt; statement runs, not when the function returns, so use a closure if you need a variable's final value.&lt;/p&gt;

&lt;p&gt;For a file you only read, the &lt;code&gt;Close()&lt;/code&gt; error rarely matters. For a file you write, it does: that's where you learn buffered data didn't make it to disk. In that case, return the error from the function instead of only logging it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;The two PRs added 17 lines in total, and I only learned the body-draining detail from the review comments. In your own Go code, grep for &lt;code&gt;client.Do&lt;/code&gt; and check that every response body is drained and closed in a &lt;code&gt;defer&lt;/code&gt;. For more, see the Go docs for &lt;a href="https://pkg.go.dev/net/http#Client.Do" rel="noopener noreferrer"&gt;net/http Client.Do&lt;/a&gt; and the &lt;a href="https://go.dev/wiki/CommonMistakes" rel="noopener noreferrer"&gt;CommonMistakes page on the Go wiki&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The tool I found Keploy with, &lt;a href="https://furkankoykiran.com.tr/en/projects/awesome-trending-repos/" rel="noopener noreferrer"&gt;awesome-trending-repos&lt;/a&gt;, has &lt;a href="https://blog.furkankoykiran.com.tr/en/posts/awesome-trending-repos-web/" rel="noopener noreferrer"&gt;its own post&lt;/a&gt;. My other open source work is in &lt;a href="https://blog.furkankoykiran.com.tr/en/posts/mcp-ai-open-source-contributions/" rel="noopener noreferrer"&gt;MCP open source contributions&lt;/a&gt; and &lt;a href="https://blog.furkankoykiran.com.tr/en/posts/security-and-api-fixes/" rel="noopener noreferrer"&gt;security and API fixes&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://blog.furkankoykiran.com.tr/en/posts/my-open-source-contribution-keploy/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;. I write about MCP servers, AI agents, developer tools and Python automation; more at &lt;a href="https://furkankoykiran.com.tr/en/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;furkankoykiran.com.tr&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>opensource</category>
      <category>testing</category>
      <category>api</category>
    </item>
    <item>
      <title>Keploy Katkım: Go'da defer ve HTTP Body Sızıntısı</title>
      <dc:creator>Furkan Köykıran</dc:creator>
      <pubDate>Sat, 21 Mar 2026 00:27:24 +0000</pubDate>
      <link>https://dev.to/furkankoykiran/keploya-katkilarim-goda-resource-management-ve-open-source-seruveni-27lb</link>
      <guid>https://dev.to/furkankoykiran/keploya-katkilarim-goda-resource-management-ve-open-source-seruveni-27lb</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/furkankoykiran/awesome-trending-repos" rel="noopener noreferrer"&gt;awesome-trending-repos&lt;/a&gt; ile GitHub trending listesini takip ederken Go ile yazılmış bir API test aracı olan &lt;a href="https://github.com/keploy/keploy" rel="noopener noreferrer"&gt;Keploy&lt;/a&gt; gözüme çarptı. Issue listesinde iki kaynak yönetimi hatası buldum ve ikisi için de PR açtım. Bu yazı o Keploy katkısını anlatıyor: Go'da &lt;code&gt;defer&lt;/code&gt;'in doğru kullanımı ve HTTP response body'lerinin neden hem okunup hem kapatılması gerektiği. İki PR da 20 Mart 2026'da merge edildi. Keploy'a ilk katkımdı.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keploy ne yapıyor
&lt;/h2&gt;

&lt;p&gt;Keploy, uygulamanızın gerçek trafiğini eBPF ile yakalayıp bundan API testleri ve dış bağımlılıklar için mock'lar üretiyor. Testleri yeniden çalıştırırken veritabanı ve dış servis çağrıları bu mock'lardan cevaplanıyor.&lt;/p&gt;

&lt;p&gt;Benim düzelttiğim iki hata bu akışın kenarlarında duruyor: biri Keploy'un uygulamanın dilini tespit ettiği yerde, diğeri Keploy'un kendi agent'ıyla konuştuğu HTTP istemcisinde.&lt;/p&gt;

&lt;h2&gt;
  
  
  PR #3927: kapatılmış dosyadan okuma
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Hata
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/keploy/keploy/issues/3821" rel="noopener noreferrer"&gt;#3821&lt;/a&gt; numaralı issue, &lt;code&gt;utils/utils.go&lt;/code&gt; içindeki &lt;code&gt;isGoBinary&lt;/code&gt; fonksiyonunu gösteriyordu. Fonksiyon bir ELF dosyasını açıyor, hemen kapatıyor, sonra kapatılmış dosyanın section'larını okumaya çalışıyordu:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;elf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filePath&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to open file %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;zap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&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;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c"&gt;// dosya burada kapanıyor&lt;/span&gt;
    &lt;span class="n"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"failed to close file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;zap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;sections&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;".go.buildinfo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;".gopclntab"&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;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;section&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;sections&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;sect&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;sect&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c"&gt;// kapalı dosya üzerinde çalışıyor&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;isGoBinary&lt;/code&gt;, &lt;code&gt;DetectLanguage&lt;/code&gt; tarafından kullanılıyor. Yani hata, Keploy'un bir uygulamanın Go ile yazılıp yazılmadığına karar verdiği yeri etkiliyordu.&lt;/p&gt;

&lt;h3&gt;
  
  
  Düzeltme
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Close()&lt;/code&gt; çağrısını &lt;code&gt;defer&lt;/code&gt; ile fonksiyonun sonuna taşıdım. &lt;code&gt;defer&lt;/code&gt; edilen çağrı, fonksiyon hangi yoldan çıkarsa çıksın (erken &lt;code&gt;return&lt;/code&gt;, normal dönüş ya da panic) en sonda çalışıyor. Bir de debug'dan kalmış &lt;code&gt;fmt.Println(section)&lt;/code&gt; satırını sildim.&lt;/p&gt;

&lt;p&gt;PR'ın ilk commit'inde düz &lt;code&gt;defer f.Close()&lt;/code&gt; yazmıştım. Copilot'un otomatik review'u iki şeye dikkat çekti: bu yazım &lt;code&gt;Close()&lt;/code&gt; hatasını yutuyor, ayrıca dosyanın geri kalanında kapatma hataları loglanıyor. Muhtemelen &lt;code&gt;golangci-lint&lt;/code&gt;'in &lt;code&gt;errcheck&lt;/code&gt; kuralına da takılacaktı. Son hâli dosyadaki diğer kullanımlarla aynı:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;elf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filePath&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to open file %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;zap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"failed to close file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;zap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}()&lt;/span&gt;

&lt;span class="n"&gt;sections&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;".go.buildinfo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;".gopclntab"&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;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;section&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;sections&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;sect&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;section&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;sect&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/keploy/keploy/pull/3927" rel="noopener noreferrer"&gt;PR #3927&lt;/a&gt; +5/−4 satırlık bir değişiklikti. AkashKumar7902 onu 20 Mart'ta merge etti.&lt;/p&gt;

&lt;h2&gt;
  
  
  PR #3932: kapatılmayan response body'ler
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Hata
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/keploy/keploy/issues/3854" rel="noopener noreferrer"&gt;#3854&lt;/a&gt; numaralı issue, &lt;code&gt;pkg/platform/http/agent.go&lt;/code&gt; içindeki &lt;code&gt;MockOutgoing&lt;/code&gt; ve &lt;code&gt;UpdateMockParams&lt;/code&gt; fonksiyonlarını gösteriyordu. İkisi de agent'a istek atıyor ama &lt;code&gt;res.Body&lt;/code&gt;'yi hiç kapatmıyordu:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to send request for mockOutgoing: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;// res.Body hiç kapatılmıyor&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mockResp&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AgentResp&lt;/span&gt;
&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewDecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;mockResp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;net/http&lt;/code&gt; dokümantasyonu açık: body'yi kapatmak çağıranın sorumluluğunda. Kapatılmayan her body, altındaki TCP bağlantısını da elinde tutuyor. Bağlantı havuza geri dönemiyor, her istek yeni bir bağlantı açıyor. Uzun çalışan bir süreçte bu, dosya tanımlayıcılarının ve portların tükenmesine kadar gidebiliyor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Düzeltme: kapat, ama önce sonuna kadar oku
&lt;/h3&gt;

&lt;p&gt;Yan yana koyunca değişikliğin tamamı tek bir defer fonksiyonu:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fposts%2Fkeploy-acik-kaynak-katkim%2Fimg%2Fkeploy-body-close-fix.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fblog.furkankoykiran.com.tr%2Fposts%2Fkeploy-acik-kaynak-katkim%2Fimg%2Fkeploy-body-close-fix.webp" title="PR #3932'deki düzeltme, kısaltılmış hâliyle" alt="Önce: res.Body hiç kapatılmıyor. Sonra: defer edilen fonksiyon body'yi io.Copy(io.Discard, res.Body) ile sonuna kadar okuyup kapatıyor" width="800" height="562"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to send request for mockOutgoing: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Discard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&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;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;utils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"failed to close response body for mockOutgoing"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}()&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mockResp&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AgentResp&lt;/span&gt;
&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewDecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;mockResp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;io.Copy(io.Discard, res.Body)&lt;/code&gt; satırı bu PR'dan öğrendiğim en faydalı şey oldu. Bakımcı AkashKumar7902 Copilot yorumlarına bakmamı istedi ve yorumlardan biri tam da bunu söylüyordu: &lt;code&gt;json.Decoder.Decode&lt;/code&gt; body'nin sonuna kadar okumayı garanti etmiyor. JSON nesnesi bittikten sonra body EOF'a ulaşmadan kapatılırsa, &lt;code&gt;Transport&lt;/code&gt; bağlantıyı yeniden kullanamıyor. Body'yi önce EOF'a kadar tüketmek, keep-alive'ın gerçekten çalışmasını sağlıyor.&lt;/p&gt;

&lt;p&gt;Bir uyarı: body çok büyük olabiliyorsa sınırsız &lt;code&gt;io.Copy&lt;/code&gt; gereksiz yere megabaytlarca veri indirebilir. Böyle durumlarda okumayı sınırlamak daha iyi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Discard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LimitReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;64&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c"&gt;// en fazla 64 KiB&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keploy'daki agent yanıtları küçük olduğu için sınırsız hâli yeterliydi. &lt;a href="https://github.com/keploy/keploy/pull/3932" rel="noopener noreferrer"&gt;PR #3932&lt;/a&gt; iki commit ve 12 satırla merge edildi.&lt;/p&gt;

&lt;h2&gt;
  
  
  defer ile ilgili akılda tutulacaklar
&lt;/h2&gt;

&lt;p&gt;Bu iki PR'dan sonra kendi Go kodumda da şu kurallara dikkat ediyorum.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;defer&lt;/code&gt; kaynağı aldıktan hemen sonra ve hata kontrolünden sonra yazılır. &lt;code&gt;os.Open&lt;/code&gt; hata döndürdüyse &lt;code&gt;f&lt;/code&gt; nil'dir, &lt;code&gt;defer f.Close()&lt;/code&gt; hata kontrolünden önce yazılırsa panic'e gider.&lt;/p&gt;

&lt;p&gt;Döngü içinde &lt;code&gt;defer&lt;/code&gt; kullanılmaz. Ertelenen çağrılar döngü bitince değil fonksiyon bitince çalışır, binlerce dosya açık kalabilir. Çözüm, döngünün gövdesini ayrı bir fonksiyona almak:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;countLines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paths&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;paths&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;countFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// defer, countFile bitince çalışır&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;n&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;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;countFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;scanner&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;bufio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewScanner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&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;scanner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Scan&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;++&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;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scanner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&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;Birden fazla &lt;code&gt;defer&lt;/code&gt; ters sırada (LIFO) çalışır. Argümanlar ise &lt;code&gt;defer&lt;/code&gt; satırına gelindiği anda değerlendirilir, fonksiyon bittiğinde değil. Değişkenin son değerini istiyorsanız closure kullanın.&lt;/p&gt;

&lt;p&gt;Sadece okunan bir dosyada &lt;code&gt;Close()&lt;/code&gt; hatası nadiren önemlidir. Yazılan bir dosyada ise önemlidir, çünkü tamponlanmış verinin diske yazılamadığını ancak orada öğrenirsiniz. O durumda hatayı loglamak yerine fonksiyonun döndürdüğü hataya eklemek daha doğru olur.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sırada ne var
&lt;/h2&gt;

&lt;p&gt;İki PR toplamda 17 satır ekledi. Body'yi boşaltma ayrıntısını review yorumları sayesinde öğrendim. Kendi Go kodunuzda &lt;code&gt;client.Do&lt;/code&gt; çağrılarını tarayıp her birinin arkasında &lt;code&gt;defer&lt;/code&gt; ile kapatılan ve boşaltılan bir body olup olmadığına bakın. Daha fazlası için Go'nun &lt;a href="https://pkg.go.dev/net/http#Client.Do" rel="noopener noreferrer"&gt;net/http Client.Do dokümantasyonuna&lt;/a&gt; ve &lt;a href="https://go.dev/wiki/CommonMistakes" rel="noopener noreferrer"&gt;Go wiki'deki yaygın hatalar sayfasına&lt;/a&gt; bakabilirsiniz.&lt;/p&gt;

&lt;p&gt;Keploy'u bulduğum araç olan &lt;a href="https://furkankoykiran.com.tr/projeler/awesome-trending-repos/" rel="noopener noreferrer"&gt;awesome-trending-repos&lt;/a&gt; için &lt;a href="https://blog.furkankoykiran.com.tr/posts/awesome-trending-repos-web/" rel="noopener noreferrer"&gt;ayrı bir yazı&lt;/a&gt; var. Diğer açık kaynak katkılarım &lt;a href="https://blog.furkankoykiran.com.tr/posts/mcp-ai-contributions/" rel="noopener noreferrer"&gt;MCP ekosistemi&lt;/a&gt;, &lt;a href="https://blog.furkankoykiran.com.tr/posts/github-mcp-server-acik-kaynak-katki/" rel="noopener noreferrer"&gt;GitHub MCP Server&lt;/a&gt; ve &lt;a href="https://blog.furkankoykiran.com.tr/posts/security-api-fixes/" rel="noopener noreferrer"&gt;güvenlik ve API düzeltmeleri&lt;/a&gt; yazılarında.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://blog.furkankoykiran.com.tr/posts/keploy-acik-kaynak-katkim/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;my blog&lt;/a&gt;. I write about MCP servers, AI agents, developer tools and Python automation; more at &lt;a href="https://furkankoykiran.com.tr/en/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=crosspost" rel="noopener noreferrer"&gt;furkankoykiran.com.tr&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>opensource</category>
      <category>testing</category>
      <category>api</category>
    </item>
    <item>
      <title>Building DevTo-MCP: Bridging Forem API and AI Assistants with Google Gemini</title>
      <dc:creator>Furkan Köykıran</dc:creator>
      <pubDate>Fri, 27 Feb 2026 14:41:39 +0000</pubDate>
      <link>https://dev.to/furkankoykiran/building-devto-mcp-bridging-forem-api-and-ai-assistants-with-google-gemini-4a58</link>
      <guid>https://dev.to/furkankoykiran/building-devto-mcp-bridging-forem-api-and-ai-assistants-with-google-gemini-4a58</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/devteam/join-the-built-with-google-gemini-writing-challenge-presented-by-major-league-hacking-mlh-win-17pk"&gt;Built with Google Gemini: Writing Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As software developers, DEV Community (dev.to) and the wider Forem ecosystem have increasingly become our digital home. It's the place we go for "React best practices", solutions to obscure bugs, and career advice. But as our workflows become more tightly integrated with AI assistants, a gap has emerged: why can't our AI tools natively interact with this vibrant community?&lt;/p&gt;

&lt;p&gt;Why couldn't I tell my AI assistant to "list popular Rust articles on DEV," or "take this markdown draft and publish it directly to my DEV account"?&lt;/p&gt;

&lt;p&gt;To solve this, I built &lt;strong&gt;DevTo-MCP&lt;/strong&gt;. It's an open-source Model Context Protocol (MCP) server that acts as a secure, type-safe bridge between the Forem API and AI assistants. And throughout this journey of architectural design, debugging, and implementation, &lt;strong&gt;Google Gemini&lt;/strong&gt; was my constant pair-programming companion.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built with Google Gemini
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;DevTo-MCP&lt;/strong&gt; is essentially a translation layer. The Model Context Protocol (MCP) is an open standard that lets AI models use external tools and resources securely. DevTo-MCP takes the complex world of Forem API endpoints and turns them into simple, standardized functions that AI assistants (like Claude Desktop, Cursor, and custom Gemini agents) can seamlessly trigger.&lt;/p&gt;

&lt;p&gt;During the development process, Google Gemini played several pivotal roles:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Architectural Guidance and Type Safety
&lt;/h3&gt;

&lt;p&gt;Building an MCP server requires strict data validation. AI models can sometimes hallucinate parameters, so your server's defense must be ironclad. I decided to build DevTo-MCP using &lt;strong&gt;TypeScript&lt;/strong&gt;, &lt;strong&gt;Node.js&lt;/strong&gt;, and &lt;strong&gt;Zod&lt;/strong&gt; for runtime validation.&lt;/p&gt;

&lt;p&gt;Gemini helped me quickly map out the Forem API responses to Zod schemas. For example, when defining the &lt;code&gt;create_article&lt;/code&gt; tool, Gemini assisted in generating the complete schema configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;create_article&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;Create a new article on DEV Community.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;z.string().describe(&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Title of the article&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;),&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="na"&gt;body_markdown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Article content in markdown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;published&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Whether to publish immediately&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;max&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;optional&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;List of tags (max 4)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;canonical_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;url&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;organization_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Implementation logic&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;Thanks to Gemini's suggestions, added strict &lt;code&gt;.max(4)&lt;/code&gt; constraints and &lt;code&gt;.url()&lt;/code&gt; validations to ensure we never send malformed data to the Forem API.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Self-Healing API Client
&lt;/h3&gt;

&lt;p&gt;Forem's API deals with millions of requests, meaning rate limits and occasional network blips are part of the game. I wanted my MCP server to be resilient. I tasked Gemini with helping me write a self-healing HTTP client. &lt;/p&gt;

&lt;p&gt;Gemini generated the core logic for a robust &lt;code&gt;fetch&lt;/code&gt; wrapper that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reads &lt;code&gt;x-ratelimit-remaining&lt;/code&gt; headers.&lt;/li&gt;
&lt;li&gt;Implements an exponential backoff strategy for 429 (Too Many Requests) and 500 errors.&lt;/li&gt;
&lt;li&gt;Provides type-safe generic responses.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A simplified look at the retry logic we implemented&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(...):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&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="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_RETRIES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="o"&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;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isRetryable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRetryDelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
             &lt;span class="c1"&gt;// Handle network level errors&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. CI/CD and NPM "Trusted Publishing"
&lt;/h3&gt;

&lt;p&gt;A crucial goal was to automate the publishing of this package to NPM using GitHub Actions. I wanted to use modern &lt;strong&gt;OpenID Connect (OIDC)&lt;/strong&gt; (NPM's Trusted Publishing) to avoid hardcoding &lt;code&gt;NPM_TOKEN&lt;/code&gt; secrets. &lt;/p&gt;

&lt;p&gt;I was stuck with persistent 404 errors during the pipeline execution. I pasted my YAML into Gemini, and it instantly spotted the missing piece: my workflow was missing the &lt;code&gt;environment: production&lt;/code&gt; declaration required to establish trust.&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="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;  &lt;span class="c1"&gt;# &amp;lt;-- The magic line Gemini pointed out!&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;      
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;The project is fully open-source and available here:&lt;br&gt;
&lt;a href="https://github.com/furkankoykiran/DevTo-MCP" rel="noopener noreferrer"&gt;&lt;strong&gt;GitHub: furkankoykiran/DevTo-MCP&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can install and use it in any MCP-compatible client (like Claude Desktop) by simply adding it to your configuration:&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;"mcpServers"&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;"devto"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@furkankoykiran/devto-mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"DEVTO_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_API_KEY"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once installed, your AI assistant can fetch your latest followers, create article drafts, and analyze your reading list tags dynamically!&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;Building this project taught me several invaluable lessons:&lt;/p&gt;

&lt;h3&gt;
  
  
  The Art of "Feature Cutting"
&lt;/h3&gt;

&lt;p&gt;Initially, I wanted to include a tool that allowed users to add reactions (like, unicorn, fire) to articles via the AI. However, I consistently encountered unreliable 401 errors from the API side. As tempting as it was to keep debugging it, I learned that in building AI tools, reliability is paramount. If an AI thinks it successfully reacted to a post but didn't, it breaks the chain of trust. I had to learn the difficult engineering skill of "Feature Cutting" and disabled the tool for the MVP.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fallback Mechanisms for UX
&lt;/h3&gt;

&lt;p&gt;When the AI tried to fetch a "Draft" article using the standard &lt;code&gt;get_article_by_id&lt;/code&gt; endpoint, Forem would return a 404. Initially, the AI would give up and respond, "This article doesn't exist." &lt;br&gt;
I learned to build fallback loops. Now, if the server receives a 404, it automatically makes a secondary request to &lt;code&gt;/articles/me/all&lt;/code&gt;, searches for the ID locally, and returns the draft. This completely changed how smoothly the AI interacted with unpublished content.&lt;/p&gt;

&lt;h3&gt;
  
  
  Modern Release Pipelines
&lt;/h3&gt;

&lt;p&gt;I leveled up my Devops skills by successfully setting up an automated, secret-less NPM release pipeline using OIDC and GitHub Actions, understanding exactly when and why workflows trigger.&lt;/p&gt;




&lt;h2&gt;
  
  
  Google Gemini Feedback
&lt;/h2&gt;

&lt;p&gt;My experience with Google Gemini was incredibly positive, with a few notable learning curves.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Worked Well:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Boilerplate and Structure:&lt;/strong&gt; Gemini was fantastic at instantly generating the foundational boilerplate for a TypeScript Node project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error Decoding:&lt;/strong&gt; When I encountered cryptic TypeScript errors regarding complex generic types for my API wrapper, Gemini was able to decipher them and explain the exact configuration changes needed in my &lt;code&gt;tsconfig.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regex and Parsing:&lt;/strong&gt; Writing the logic to safely sanitize markdown payloads before sending them to the API was made trivial thanks to Gemini's rapid regex suggestions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where I Encountered Friction (The Candid Truth):
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API Hallucinations:&lt;/strong&gt; Because the Forem API has changed over time, Gemini occasionally hallucinated endpoints or parameter names that didn't exist or were deprecated. For example, it suggested sending data in a structure that the current API rejected. This actually reinforced my decision to use &lt;strong&gt;Zod&lt;/strong&gt;—building strict validation layers meant I caught these AI hallucinations in my test environment immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Context Window Management:&lt;/strong&gt; When working across multiple files (&lt;code&gt;api-client.ts&lt;/code&gt;, &lt;code&gt;server.ts&lt;/code&gt;, and the Zod schema files), I sometimes found Gemini losing the thread of the specific generic types we had agreed upon earlier in the conversation. Reminding it of the interface definitions occasionally disrupted the flow state.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Look Forward
&lt;/h2&gt;

&lt;p&gt;The future of software engineering lies in AI assistants that have secure, predictable access to our development ecosystems. DevTo-MCP is my contribution to ensuring the DEV community isn't left behind in this AI revolution.&lt;/p&gt;

&lt;p&gt;I plan to expand DevTo-MCP to include organization-level analytics, more robust comment threading capabilities, and eventually figuring out a secure way to re-enable that elusive "Reactions" tool!&lt;/p&gt;

&lt;p&gt;Thank you to MLH and Google for hosting this challenge, it was the perfect catalyst to reflect on this build journey. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;(P.S. As a special proof-of-concept, this very article was drafted, formatted, and published autonomously to DEV.to by my AI Assistant (Gemini) using the DevTo-MCP server I built! A little meta-magic to wrap things up! 🪄)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>geminireflections</category>
      <category>gemini</category>
      <category>mcp</category>
    </item>
  </channel>
</rss>
