<?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: Minor Keith</title>
    <description>The latest articles on DEV Community by Minor Keith (@peculiarengineer).</description>
    <link>https://dev.to/peculiarengineer</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%2F3650865%2F3759f803-6cf3-4025-88c5-2130f947ae79.png</url>
      <title>DEV Community: Minor Keith</title>
      <link>https://dev.to/peculiarengineer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/peculiarengineer"/>
    <language>en</language>
    <item>
      <title>Build your first MCP server in Python: give Claude your own notes</title>
      <dc:creator>Minor Keith</dc:creator>
      <pubDate>Wed, 15 Jul 2026 02:40:18 +0000</pubDate>
      <link>https://dev.to/peculiarengineer/build-your-first-mcp-server-in-python-give-claude-your-own-notes-lah</link>
      <guid>https://dev.to/peculiarengineer/build-your-first-mcp-server-in-python-give-claude-your-own-notes-lah</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://peculiarengineer.com/blog/build-your-first-mcp-server-python/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpeculiarengineer.com%2F_astro%2Fog-default.C5dNhgZO.png" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://peculiarengineer.com/blog/build-your-first-mcp-server-python/" rel="noopener noreferrer" class="c-link"&gt;
            Build your first MCP server in Python: give Claude your own notes
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            A beginner guide to the Model Context Protocol. Build a real MCP server in about sixty lines of Python with uv and the official SDK, expose two tools over stdio, and wire it into Claude Code with one command. Includes the stdout footgun that hides behind block buffering and only bites you later.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpeculiarengineer.com%2Ffavicon.svg"&gt;
          peculiarengineer.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;There are 48 posts on this site. I wrote every one of them so I would never have to Google the same thing twice, and it works: when I need the Netplan syntax or the exact &lt;code&gt;ufw&lt;/code&gt; incantation, I come here instead of wading through Stack Overflow.&lt;/p&gt;

&lt;p&gt;The annoying part is that the model I am talking to has not read any of it. It knows the general shape of Netplan, not the version I settled on after the third time it bit me. I can paste a post into the chat window, and I have, plenty of times. But pasting is not a system. It is a thing you do again every session, forever, and it only works when you already know which post you needed.&lt;/p&gt;

&lt;p&gt;What I actually want is a door. Let the model knock when it wants something, and let it read the notes itself.&lt;/p&gt;

&lt;p&gt;That door is the &lt;strong&gt;Model Context Protocol&lt;/strong&gt;, and the useful part is not the protocol. It is that you write the integration once. Without MCP, giving a model access to your notes means a bespoke integration per tool per client: one for Claude Code, another for the desktop app, another for whatever you use next year. With MCP you write one server that knows how to search your notes, and every client that speaks the protocol can call it. The server does not know or care who is asking.&lt;/p&gt;

&lt;p&gt;This post builds that server. It is about sixty lines of Python.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; &lt;code&gt;uv add "mcp[cli]"&lt;/code&gt;, decorate two functions with &lt;code&gt;@mcp.tool()&lt;/code&gt;, end the file with &lt;code&gt;mcp.run(transport="stdio")&lt;/code&gt;, and register it with &lt;code&gt;claude mcp add notes -- uv run --directory /abs/path server.py&lt;/code&gt;. Use an absolute path or it will not connect, and never &lt;code&gt;print()&lt;/code&gt; to stdout, because stdout is the transport.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;You need &lt;a href="https://peculiarengineer.com/blog/install-uv-macos-cheat-sheet/" rel="noopener noreferrer"&gt;uv&lt;/a&gt; and Claude Code. That is the whole list. There is no server to deploy, no Docker, no port to open. A local MCP server is just a program that Claude Code launches as a subprocess and talks to over stdin and stdout.&lt;/p&gt;

&lt;p&gt;That last point is worth sitting with, because it is the thing people expect to be complicated and it is not. MCP defines two transports. &lt;strong&gt;Streamable HTTP&lt;/strong&gt; is for remote servers that many clients connect to over the network, and it comes with the whole authentication story. &lt;strong&gt;stdio&lt;/strong&gt; is for local servers, and it is a pipe. Your process, the client's process, JSON going back and forth. Nothing is listening on a port and nothing is exposed.&lt;/p&gt;

&lt;p&gt;Everything below is stdio. It is the right place to start, and for a server that reads files on your own laptop it may be the right place to stay.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you are building
&lt;/h2&gt;

&lt;p&gt;Two tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;search_notes(query)&lt;/code&gt; finds posts containing a phrase and returns their slugs and titles.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_note(slug)&lt;/code&gt; returns one whole post.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That split matters more than it looks. Search returns a small list so the model can pick, then it fetches only what it wants. If &lt;code&gt;search_notes&lt;/code&gt; returned full post bodies, a three word query would dump 40,000 words into the context window and the model would drown before it started.&lt;/p&gt;

&lt;p&gt;I am pointing this at my blog because that is what I have. Point &lt;code&gt;NOTES&lt;/code&gt; at any folder of markdown and it works identically. That is the whole idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set up the project
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;mcp-notes &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;mcp-notes
uv init &lt;span class="nb"&gt;.&lt;/span&gt;
uv add &lt;span class="s2"&gt;"mcp[cli]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;mcp[cli]&lt;/code&gt; is the official Python SDK. The &lt;code&gt;cli&lt;/code&gt; extra brings along the development tooling, which you will want later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The server
&lt;/h2&gt;

&lt;p&gt;Here is the whole thing, &lt;code&gt;server.py&lt;/code&gt;:&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;An MCP server that lets Claude read my blog posts.

Two tools: search_notes finds posts containing a phrase, get_note returns one
whole post. Point NOTES at any folder of markdown and it works the same.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.server.fastmcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;

&lt;span class="n"&gt;NOTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;home&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;projects/peculiarengineer/src/content/blog&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;notes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_posts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&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;return&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NOTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.mdx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_title&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Frontmatter title, e.g.  title: 'Set up SSH keys for Ubuntu'
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()[:&lt;/span&gt;&lt;span class="mi"&gt;15&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;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeprefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fallback&lt;/span&gt;


&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_notes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Search my blog posts for a word or phrase.

    Matches the literal phrase anywhere in a post, including its frontmatter.
    Returns up to 5 hits with slug and title. There is no ranking: hits come
    back in filename order, so a post that merely mentions the phrase can crowd
    out the post that is about it. Search more than once with different wording.
    Use get_note with a slug to read the full post.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;_posts&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stem&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;_title&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;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No posts mention &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_note&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return the full text of one blog post, by slug.

    The slug is the filename without its extension, as returned by search_notes.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;_posts&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;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stem&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;slug&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;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No post with slug &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt;. Use search_notes to find one.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# stdout is the transport. A stray print() lands in the middle of the
&lt;/span&gt;    &lt;span class="c1"&gt;# JSON-RPC stream, and block buffering means it may not bite until the
&lt;/span&gt;    &lt;span class="c1"&gt;# buffer fills hours later. Diagnostics go to stderr, always.
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;serving &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_posts&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; posts from &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;NOTES&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mcp&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="n"&gt;transport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stdio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a complete MCP server. Three parts are doing the work.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FastMCP("notes")&lt;/code&gt; is the server. The name is what shows up in clients.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@mcp.tool()&lt;/code&gt; is where the magic is, and it is worth understanding what it saves you. The protocol requires each tool to advertise a JSON Schema describing its inputs. FastMCP builds that schema from your &lt;strong&gt;type hints&lt;/strong&gt;, and it takes the tool's description from your &lt;strong&gt;docstring&lt;/strong&gt;. You write a normal Python function and the protocol paperwork is generated for you. This is why the post is short.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mcp.run(transport="stdio")&lt;/code&gt; starts the loop that reads JSON off stdin and writes JSON to stdout.&lt;/p&gt;

&lt;p&gt;Notice the search is a case insensitive substring match. That is not a placeholder I am going to upgrade at the end. It is the point: the protocol is twenty minutes of work, and search quality is a different problem that would eat this entire post. More on where that falls over below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wire it into Claude Code
&lt;/h2&gt;

&lt;p&gt;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 notes &lt;span class="nt"&gt;--&lt;/span&gt; uv run &lt;span class="nt"&gt;--directory&lt;/span&gt; /Users/you/projects/mcp-notes server.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--&lt;/code&gt; matters. Everything before it belongs to &lt;code&gt;claude mcp add&lt;/code&gt;, and everything after it is the literal command Claude Code runs to start your server. Without the separator, &lt;code&gt;claude&lt;/code&gt; tries to parse your command's flags as its own.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--directory&lt;/code&gt; matters more, and this is the first thing that got me. Claude Code runs that command from &lt;strong&gt;whatever directory you launched &lt;code&gt;claude&lt;/code&gt; in&lt;/strong&gt;, not from where your server lives. I registered it with a bare &lt;code&gt;uv run server.py&lt;/code&gt;, started Claude in my blog repo, and got this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;notes: uv run server.py - ✘ Failed to connect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course it failed. There is no &lt;code&gt;server.py&lt;/code&gt; in my blog repo. &lt;code&gt;uv run --directory /abs/path server.py&lt;/code&gt; pins it, and now it does not matter where you start Claude from.&lt;/p&gt;

&lt;p&gt;Check it:&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 list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;notes: uv run --directory /Users/you/projects/mcp-notes server.py - ✔ Connected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;✔ Connected&lt;/code&gt; means Claude Code launched the process, completed the handshake, and got a tool list back. For more detail, &lt;code&gt;claude mcp get notes&lt;/code&gt; shows the scope, the exact command, and any error.&lt;/p&gt;

&lt;h3&gt;
  
  
  A word on scope
&lt;/h3&gt;

&lt;p&gt;By default your server is added with &lt;strong&gt;local&lt;/strong&gt; scope, which means it is private to you and tied to the directory you added it from. This confused me for a solid minute: I added the server while sitting in my blog repo, went over to the server's own directory, ran &lt;code&gt;claude mcp list&lt;/code&gt;, and it was not there. Not broken, not an error, just not listed. Local scope is per project, and I was in a different project.&lt;/p&gt;

&lt;p&gt;Your options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--scope local&lt;/code&gt; (the default) for this project only, stored in &lt;code&gt;~/.claude.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--scope user&lt;/code&gt; for every project you work in. This is what you want for a notes server.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--scope project&lt;/code&gt; writes a &lt;code&gt;.mcp.json&lt;/code&gt; in the repo so anyone who clones it gets the server too.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Use it
&lt;/h2&gt;

&lt;p&gt;Start Claude Code and ask it something that is in your notes and not in its training data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gt"&gt;&amp;gt; What did I decide about Secure Boot when installing NVIDIA drivers?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first time a tool runs, Claude Code asks your permission, once per tool. Approve it and you will not be asked again. The names are namespaced by server, so mine show up as &lt;code&gt;mcp__notes__search_notes&lt;/code&gt; and &lt;code&gt;mcp__notes__get_note&lt;/code&gt;. &lt;code&gt;/mcp&lt;/code&gt; inside a session lists the server and its tools.&lt;/p&gt;

&lt;p&gt;Then it works. It calls &lt;code&gt;search_notes("Secure Boot")&lt;/code&gt;, gets a slug back, calls &lt;code&gt;get_note&lt;/code&gt; on it, and answers out of my own post: if Secure Boot is on, enroll the MOK key when the driver install prompts you, or turn Secure Boot off in the BIOS before you start.&lt;/p&gt;

&lt;p&gt;That is the correct answer, and it is correct because it came from &lt;a href="https://peculiarengineer.com/blog/install-ollama-ubuntu-26-04-nvidia-gpu/" rel="noopener noreferrer"&gt;my own post&lt;/a&gt; rather than from a general impression of how NVIDIA drivers work.&lt;/p&gt;

&lt;p&gt;But the part I did not expect was what came next. Unprompted, it searched again, found my &lt;a href="https://peculiarengineer.com/blog/hardening-ubuntu-26-04-desktop/" rel="noopener noreferrer"&gt;Hardening Ubuntu 26.04 Desktop&lt;/a&gt; post, and pointed out that the two contradict each other: on the desktop I recommend TPM backed full disk encryption, which relies on Secure Boot being on. So "turn Secure Boot off" is advice scoped to a headless GPU box, not a rule, and I had never written that down anywhere because I had never had both posts in my head at the same time.&lt;/p&gt;

&lt;p&gt;That is the moment the door earns its keep. I did not ask it to reconcile two posts written a month apart. It just had access to both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the dumb search falls over
&lt;/h2&gt;

&lt;p&gt;I promised this, and it is more interesting than I expected.&lt;/p&gt;

&lt;p&gt;Ask for something specific and it is great. &lt;code&gt;search_notes("fail2ban")&lt;/code&gt; returns exactly the right three posts. Then try a general word:&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="nf"&gt;search_notes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;firewall&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;create-sudo-user-ubuntu-26-04&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create a Sudo User on Ubuntu 26.04&lt;/span&gt;
&lt;span class="py"&gt;enable-ssh-on-ubuntu-desktop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Enable SSH on an Ubuntu desktop&lt;/span&gt;
&lt;span class="py"&gt;extend-azure-windows-disk-run-command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Extending C: on locked-down Azure Windows VMs&lt;/span&gt;
&lt;span class="py"&gt;hardening-ubuntu-26-04-desktop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Hardening Ubuntu 26.04 Desktop (Resolute Raccoon)&lt;/span&gt;
&lt;span class="py"&gt;hardening-ubuntu-26-04-server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Hardening Ubuntu 26.04 Server (Resolute Raccoon)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of those is a real match. And the actual firewall post, &lt;a href="https://peculiarengineer.com/blog/ufw-firewall-basics-ubuntu/" rel="noopener noreferrer"&gt;UFW Firewall Basics&lt;/a&gt;, is not in the list.&lt;/p&gt;

&lt;p&gt;The substring match did not fail. It found the UFW post fine. The problem is that there is no ranking at all: &lt;code&gt;_posts()&lt;/code&gt; returns files in alphabetical order and I stop at five, so five posts that mention "firewall" once in passing crowded out the post that is entirely about firewalls, purely because &lt;code&gt;u&lt;/code&gt; sorts after &lt;code&gt;c&lt;/code&gt;, &lt;code&gt;e&lt;/code&gt;, and &lt;code&gt;h&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is the real lesson, and it is why I did not paper over it with a better search. &lt;strong&gt;MCP got your notes to the model in twenty minutes. Deciding which notes are the right ones is the actual work,&lt;/strong&gt; and it is the same search problem it always was. The protocol does not help you with it and was never going to.&lt;/p&gt;

&lt;p&gt;There is a cheap fix that costs nothing, though, and it is the most MCP-shaped part of this whole post: I told the model about the limitation. Read the docstring on &lt;code&gt;search_notes&lt;/code&gt; again. It says there is no ranking, and it says to search more than once with different wording. The model reads that and compensates by trying &lt;code&gt;ufw&lt;/code&gt; after &lt;code&gt;firewall&lt;/code&gt; comes back weak. You cannot fix bad search with a comment, but you can stop the model from trusting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas I hit
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never &lt;code&gt;print()&lt;/code&gt; in a stdio server, and do not trust your own testing on this one.&lt;/strong&gt; stdout is the transport. Anything you print goes into the JSON-RPC stream and the client fails to parse it. The catch is that it often looks fine: when stdout is a pipe rather than a terminal, Python block buffers it at around 8 KB, so your debug line sits in the buffer and never interleaves. Add &lt;code&gt;flush=True&lt;/code&gt;, or print enough to fill the buffer, or just run long enough, and it corrupts. Mine failed exactly as advertised the moment I flushed: &lt;code&gt;Failed to parse JSONRPC message from server ... input_value='DEBUG: searching for fail2ban'&lt;/code&gt;. A footgun that works in testing and detonates in week three is worse than one that fails immediately. Send diagnostics to stderr with &lt;code&gt;file=sys.stderr&lt;/code&gt;, which the client ignores and you can still read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relative paths fail.&lt;/strong&gt; The command runs from wherever you started &lt;code&gt;claude&lt;/code&gt;, not where the server lives. Use &lt;code&gt;uv run --directory /abs/path server.py&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local scope is tied to a directory.&lt;/strong&gt; Added from one project, invisible from another, with no error to explain it. Use &lt;code&gt;--scope user&lt;/code&gt; for anything you want everywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The docstring is your API contract.&lt;/strong&gt; It is not a comment. It is the only description of your tool the model ever sees, and it decides how the tool gets called. Mine said "newest matches first" when the order was actually alphabetical, and no compiler was ever going to catch that. A stale docstring is a lying API spec.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;✔ Connected&lt;/code&gt; does not mean the model can call it.&lt;/strong&gt; Connected means the handshake worked. Each tool still needs your approval the first time it runs. This bites hardest in a headless session (&lt;code&gt;claude -p ...&lt;/code&gt;), where there is nobody to approve anything and the call is simply refused. Pre-allow them with &lt;code&gt;--allowedTools "mcp__notes__search_notes,mcp__notes__get_note"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config is read at session start.&lt;/strong&gt; Edit the server and the running session keeps the old one. Restart Claude Code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the command standalone first.&lt;/strong&gt; If &lt;code&gt;uv run --directory /abs/path server.py&lt;/code&gt; does not start on its own, it will not start under Claude Code either, and the error is much easier to read in your terminal.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;New project&lt;/td&gt;
&lt;td&gt;&lt;code&gt;uv init . &amp;amp;&amp;amp; uv add "mcp[cli]"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server object&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mcp = FastMCP("notes")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Define a tool&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@mcp.tool()&lt;/code&gt; above a typed function&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run over stdio&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mcp.run(transport="stdio")&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Log safely&lt;/td&gt;
&lt;td&gt;&lt;code&gt;print(msg, file=sys.stderr)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Register it&lt;/td&gt;
&lt;td&gt;&lt;code&gt;claude mcp add notes -- uv run --directory /abs/path server.py&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Register everywhere&lt;/td&gt;
&lt;td&gt;&lt;code&gt;claude mcp add --scope user notes -- ...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List servers&lt;/td&gt;
&lt;td&gt;&lt;code&gt;claude mcp list&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inspect one&lt;/td&gt;
&lt;td&gt;&lt;code&gt;claude mcp get notes&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manage in session&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/mcp&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pre-allow tools&lt;/td&gt;
&lt;td&gt;&lt;code&gt;claude -p "..." --allowedTools "mcp__notes__search_notes"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remove it&lt;/td&gt;
&lt;td&gt;&lt;code&gt;claude mcp remove notes&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Which LLM should I actually code with? I built a small benchmark to find out</title>
      <dc:creator>Minor Keith</dc:creator>
      <pubDate>Sun, 12 Jul 2026 03:41:18 +0000</pubDate>
      <link>https://dev.to/peculiarengineer/which-llm-should-i-actually-code-with-i-built-a-small-benchmark-to-find-out-3nbg</link>
      <guid>https://dev.to/peculiarengineer/which-llm-should-i-actually-code-with-i-built-a-small-benchmark-to-find-out-3nbg</guid>
      <description>&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://peculiarengineer.com/benchmark/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpeculiarengineer.com%2F_astro%2Fog-default.C5dNhgZO.png" height="400" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://peculiarengineer.com/benchmark/" rel="noopener noreferrer" class="c-link"&gt;
            LLM code benchmark — Peculiar Engineer
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            A small, self-run coding benchmark: 3 models on 14 problems across 3 languages, scored on pass@k, cost, and speed. Last run 11 July 2026.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpeculiarengineer.com%2Ffavicon.svg" width="128" height="128"&gt;
          peculiarengineer.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;I kept going back and forth on which model to reach for in my actual day job. Every "which LLM is best at code" thread turns into vibes and screenshots, and none of it answered the question I had, which is which one to open when I have real work in the languages I use. So I stopped guessing and built a small benchmark to settle it for myself.&lt;/p&gt;

&lt;p&gt;It is deliberately small. 14 problems across Python, C#, and Bash, three models, three attempts each at temperature 0.7 with a 10 second timeout. Every attempt runs in a sandboxed Docker container and gets scored on pass@k, cost, and latency. It is not an authoritative ranking and I am not pretending it splits hairs. It is enough to show the shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing that surprised me
&lt;/h2&gt;

&lt;p&gt;Accuracy is not the differentiator anymore. All three models solved every problem they were allowed to answer, 100% &lt;a href="mailto:pass@3"&gt;pass@3&lt;/a&gt;. If I only looked at pass rates I would have learned nothing, because they all pass.&lt;/p&gt;

&lt;p&gt;The catch hides in "allowed to answer." One model got content filtered out of four problems, so its perfect score covers 10 of the 14, not the whole set. A perfect score on the problems you answered and a perfect score on the whole bench are not the same result, which is why the leaderboard sorts on coverage first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where they actually differ
&lt;/h2&gt;

&lt;p&gt;If they all pass, the decision comes down to what you pay and how long you wait. That is where the spread lives.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cost was close, about 1.3x from cheapest to priciest across the suite.&lt;/li&gt;
&lt;li&gt;Latency was not close. 6.6x between the fastest and the slowest.&lt;/li&gt;
&lt;li&gt;The cheapest run that also covered all 14 problems came in around $0.028 per solved problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the honest summary is boring in the best way. Pick on speed and price, because accuracy already agrees.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotcha worth writing down
&lt;/h2&gt;

&lt;p&gt;One result is not like the others. One model was quick and cheap on Python and C#, a second or two per problem, then fell off a cliff on Bash at 90 to 168 seconds per problem. Four slow Bash problems dragged its average latency up to something that makes it look slow overall when it is really fine everywhere except Bash. If your day is mostly shell, that matters a lot. If it is Python, you would never notice.&lt;/p&gt;

&lt;p&gt;That is why a single "which model is fastest" number is a lie. Fastest at what, in which language, is the only version of the question worth asking.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;p&gt;For my work it came down to speed and cost per language. The benchmark did the one job I wanted. It turned a running argument in my head into a few numbers I can re-run whenever the models change.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>benchmark</category>
      <category>programming</category>
    </item>
    <item>
      <title>Hardening a fresh Ubuntu 26.04 server without locking yourself out</title>
      <dc:creator>Minor Keith</dc:creator>
      <pubDate>Sun, 12 Jul 2026 03:37:42 +0000</pubDate>
      <link>https://dev.to/peculiarengineer/hardening-a-fresh-ubuntu-2604-server-without-locking-yourself-out-40ih</link>
      <guid>https://dev.to/peculiarengineer/hardening-a-fresh-ubuntu-2604-server-without-locking-yourself-out-40ih</guid>
      <description>&lt;p&gt;The hardening pass I run on a fresh Ubuntu 26.04 LTS box before it does anything useful. Keys only SSH, the firewall set before you cut the cord, unattended security updates with a reboot window, fail2ban, free kernel livepatch, and an honest look at what is actually listening.&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://peculiarengineer.com/blog/hardening-ubuntu-26-04-server/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpeculiarengineer.com%2F_astro%2Fubuntu-2604-server-hero.Cl3yGrJg.png" height="400" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://peculiarengineer.com/blog/hardening-ubuntu-26-04-server/" rel="noopener noreferrer" class="c-link"&gt;
            Hardening Ubuntu 26.04 Server (Resolute Raccoon)
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            A practical hardening pass for a fresh Ubuntu 26.04 LTS server: SSH locked down to keys only, the firewall set before you cut yourself off, automatic security updates with a sane reboot window, fail2ban on the front door, kernel livepatch from the free Ubuntu Pro tier, and an honest look at what is actually listening.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpeculiarengineer.com%2Ffavicon.svg" width="128" height="128"&gt;
          peculiarengineer.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>linux</category>
      <category>sysadmin</category>
      <category>ubuntu</category>
      <category>security</category>
    </item>
    <item>
      <title>I built Pong from scratch in LÖVE and Lua (a two part series)</title>
      <dc:creator>Minor Keith</dc:creator>
      <pubDate>Wed, 01 Jul 2026 02:12:12 +0000</pubDate>
      <link>https://dev.to/peculiarengineer/i-built-pong-from-scratch-in-love-and-lua-a-two-part-series-8mn</link>
      <guid>https://dev.to/peculiarengineer/i-built-pong-from-scratch-in-love-and-lua-a-two-part-series-8mn</guid>
      <description>&lt;p&gt;I wrote a two part series on building Pong from scratch in LÖVE and Lua. If you've never made a game before, I think it's a nice place to start.&lt;/p&gt;

&lt;p&gt;Part one is the whole game in two short files: the loop, delta time,&lt;br&gt;
collision, scoring, a title screen, and sound generated in code so there are no audio files to ship. &lt;/p&gt;

&lt;p&gt;Part two adds a one player mode by writing a computer opponent.&lt;/p&gt;

&lt;p&gt;Part 1: &lt;a href="https://peculiarengineer.com/blog/make-pong-with-love2d-and-lua/" rel="noopener noreferrer"&gt;https://peculiarengineer.com/blog/make-pong-with-love2d-and-lua/&lt;/a&gt;&lt;br&gt;
Part 2: &lt;a href="https://peculiarengineer.com/blog/pong-computer-opponent-love2d-lua/" rel="noopener noreferrer"&gt;https://peculiarengineer.com/blog/pong-computer-opponent-love2d-lua/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>lua</category>
      <category>gamedev</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
