<?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: Fabio Nonato</title>
    <description>The latest articles on DEV Community by Fabio Nonato (@nonatofabio_28).</description>
    <link>https://dev.to/nonatofabio_28</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%2F694694%2Fdc798bef-dc56-46a4-aa60-dccf7b7d1f8f.jpeg</url>
      <title>DEV Community: Fabio Nonato</title>
      <link>https://dev.to/nonatofabio_28</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nonatofabio_28"/>
    <language>en</language>
    <item>
      <title>A Fruit Fly Brain Plays Doom</title>
      <dc:creator>Fabio Nonato</dc:creator>
      <pubDate>Tue, 22 Sep 2026 16:18:52 +0000</pubDate>
      <link>https://dev.to/nonatofabio_28/a-fruit-fly-brain-plays-doom-4an</link>
      <guid>https://dev.to/nonatofabio_28/a-fruit-fly-brain-plays-doom-4an</guid>
      <description>&lt;p&gt;Imagine I opened this post by telling you I got a fly to play Doom. Would you believe me?&lt;/p&gt;

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

&lt;p&gt;You should not. The thing playing up there is not a fly, but its wiring diagram comes from one. It is a recurrent network built on a real fruit fly connectome: 49,393 neurons and 9 million synapses, all frozen, with one learned gain per synapse. An ordinary convolutional network feeds it the game frames and an ordinary MLP turns its activity into moves, and those two hold more than half of the parameters. So read every result in this post as "a network constrained to the fly connectome" and never as "a fly". Yes, the title breaks that rule. That is the hook, and the rest of the post is the correction.&lt;/p&gt;

&lt;p&gt;Now, here is the thing: I did not write the code, the infrastructure or the first drafts of the docs. All of it came out of the &lt;a href="https://github.com/strands-agents/harness-sdk" rel="noopener noreferrer"&gt;Strands harness&lt;/a&gt;, an open-source coding agent from the Strands Agents team at AWS that you run from the terminal. It keeps its own state on disk between sessions and runs tools, tests and cloud deployments on its own. Here it wrote the pipeline that turns the fly's wiring into a network and the five ordinary Doom agents that served as teachers. It wrote the trainer that taught the fly-wired network to copy them, and the second training stage that tried to push it further. It also wrote the cloud setup that ran a GPU fleet in three AWS regions, and it recorded the footage. Five days, 28 commits, about 3,300 lines of Python and shell, twelve fine-tuning runs and three control runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually typed
&lt;/h2&gt;

&lt;p&gt;The harness keeps its own session state on disk, so I went back and read my side of the transcript. It is short. It started with one prompt, typos and all:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I just saw this: &lt;a href="https://huggingface.co/mlabonne/chessfly" rel="noopener noreferrer"&gt;https://huggingface.co/mlabonne/chessfly&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I want to make the same thing but to play doom! Look at the skills and memory and have a set up ready for us to train it using my fnp3 aws accountl. Also create a infographic style tutorial on how it works, both the conectome model as well as the training. Use this current directory as a infra/cdk for training and training source and data ETL pipeline source. Once we complete all, we should have this is a private github repo. If you find a way to start training while I'm away, go for it!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Across the seven sessions that followed, this is most of what I said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Go for it!&lt;/p&gt;

&lt;p&gt;Can you check how are we doing on the training of doomfly?&lt;/p&gt;

&lt;p&gt;Do you have suggestions to speed up the training process?&lt;/p&gt;

&lt;p&gt;how do I use tb to see all the runs so far? &lt;em&gt;(tb is TensorBoard)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let's stop and revert all this we should not pursue this anymore&lt;/p&gt;

&lt;p&gt;Sorry, wrong session!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"Let's stop and revert all this" was me killing an easter egg I had asked for an hour earlier: a Doom mod with a fly paw for the player's hand and Stan the frog, the Strands mascot, as the enemies. The harness reverted the whole thing, previews and tests included, and went back to the training run. "Sorry, wrong session!" came right after I pasted a request meant for a different agent, about a different project.&lt;/p&gt;

&lt;p&gt;I ran all of it from the terminal with the &lt;code&gt;strands&lt;/code&gt; command, and I changed the model underneath it as I went. The first sessions ran on Claude Fable 5.1 on Amazon Bedrock. By the last day the same sessions were running on GPT-6 Astra at the highest reasoning effort. The transcript does not care which model is behind it, and neither does the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Between my sentences
&lt;/h2&gt;

&lt;p&gt;Everything else in the transcript is the harness at work, and reading it back showed me how much the Strands harness worked for me.&lt;/p&gt;

&lt;p&gt;Each session persisted to disk, and the next one opened with a summary of the last. Seven sessions over five days read like one conversation, and I could close the laptop at night and pick up in the morning where it left off. Long tool outputs were paged out of the conversation as it grew and fetched back when the agent asked for them. About two hundred of those are still sitting on disk, and without that mechanism the logs from day one alone would have ended the project.&lt;/p&gt;

&lt;p&gt;Background tasks paid off on the last day. While a two-hour evaluation ran on my Mac, the same agent widened the model's output from 22 Doom actions to 23 for the next experiment and wrote nine tests for it. It also confirmed that the widened model played the old scenarios bit-for-bit the same as before. The repo has an &lt;code&gt;AGENTS.md&lt;/code&gt; that says never push, launch, terminate or delete without being told, and the harness asked every time. "Go for it!" was me answering. It also loaded my own &lt;a href="https://github.com/nonatofabio/claude-writing-skills" rel="noopener noreferrer"&gt;writing and verification skills&lt;/a&gt; and the MCP tools I already use, which is why the docs it produced read like mine.&lt;/p&gt;

&lt;p&gt;None of that is exotic on its own. What I had not seen before is all of it in one command, with nothing to wire up. The same agent is two lines of Python if you want it inside a script, and after this week I would not think twice about starting a project that way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I trust
&lt;/h2&gt;

&lt;p&gt;An agent that returns a plausible positive result is hard to trust. This one returned two negative results, with the run prefixes and standard deviations attached, because its rules say that negative results go in &lt;code&gt;docs/&lt;/code&gt; and not in the bin.&lt;/p&gt;

&lt;p&gt;A short map of what the harness did, so the two results make sense. It first trained five ordinary Doom agents with PPO, a standard reinforcement learning method, one per scenario. Those are the teachers. It then distilled the teachers into one fly-wired network, the student, by having the student imitate their moves. The student ended up playing about as well as its teachers. The question the project was built for was whether a second stage of reinforcement learning, GRPO, could push it past that.&lt;/p&gt;

&lt;p&gt;First, it could not. Twelve GRPO runs, one setting changed per run, and not one beat the student by more than the noise in the evaluation. GRPO keeps the new policy close to the student with a penalty. The penalty strength that stopped the policy from wandering into worse play was the same strength that stopped it from improving at all.&lt;/p&gt;

&lt;p&gt;Second, and this one stings, the connectome does nothing measurable. The harness suggested and built two controls. One keeps the same graph but shuffles its edges, so every neuron keeps its degree and its sign and the biology is gone. The other removes the neuron layer entirely.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backbone&lt;/th&gt;
&lt;th&gt;Parameters&lt;/th&gt;
&lt;th&gt;Training steps per second&lt;/th&gt;
&lt;th&gt;Doom scores&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Real connectome&lt;/td&gt;
&lt;td&gt;48.2 M&lt;/td&gt;
&lt;td&gt;646&lt;/td&gt;
&lt;td&gt;baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shuffled edges, two seeds&lt;/td&gt;
&lt;td&gt;48.2 M&lt;/td&gt;
&lt;td&gt;1,454&lt;/td&gt;
&lt;td&gt;same, within noise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No connectome at all&lt;/td&gt;
&lt;td&gt;27.7 M&lt;/td&gt;
&lt;td&gt;14,686&lt;/td&gt;
&lt;td&gt;same, within noise&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One scenario as an example, ten episodes each: on &lt;code&gt;defend_the_center&lt;/code&gt; the real connectome scored 18.6 ± 1.8, the two shuffled graphs 19.0 and 18.4, and the model with no connectome 18.3. The other four scenarios look the same. The fly wiring makes training 22 times slower and buys nothing on these five scenarios. That is why the caption says "wired like a fruit fly's brain" and not "a fruit fly's brain". The harness wrote that vocabulary rule into &lt;code&gt;AGENTS.md&lt;/code&gt; on day two, before either result existed. It would have been a hard rule to write after a result I disliked.&lt;/p&gt;

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

&lt;p&gt;Scores on the five scenarios stopped moving, and three of them sit at their ceilings, so the next test is a real level. The student already plays Freedoom II MAP01 zero-shot. With its aiming prior it survives twice as long as random and picks up items. It still dies in eight of ten episodes and never leaves the first two rooms. The wider output and a slot for a sixth scenario are done and tested. Next comes GRPO on that map with all three backbones: real, shuffled and none. If the wiring is ever going to matter, it will be where the policy has to learn rather than imitate. If the network learns anything there, the plan is fly versus fly in a duel.&lt;/p&gt;

&lt;p&gt;The code is public at &lt;a href="https://github.com/nonatofabio/doomfly-rl" rel="noopener noreferrer"&gt;nonatofabio/doomfly-rl&lt;/a&gt;, and the trained checkpoints are on &lt;a href="https://huggingface.co/fabiononato/doomfly-rl" rel="noopener noreferrer"&gt;Hugging Face&lt;/a&gt; with the connectome file they need. If you want to watch a network shaped like a fly play Doom, start there.&lt;/p&gt;

&lt;p&gt;Original post: &lt;a href="https://nonatofabio.github.io/blog/posts/doomfly_autonomous.html" rel="noopener noreferrer"&gt;Fabio's Blog&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>showdev</category>
      <category>python</category>
    </item>
    <item>
      <title>AI Generated pixel art needs a build system, not better prompts</title>
      <dc:creator>Fabio Nonato</dc:creator>
      <pubDate>Mon, 07 Sep 2026 14:37:48 +0000</pubDate>
      <link>https://dev.to/nonatofabio_28/ai-generated-pixel-art-needs-a-build-system-not-better-prompts-280c</link>
      <guid>https://dev.to/nonatofabio_28/ai-generated-pixel-art-needs-a-build-system-not-better-prompts-280c</guid>
      <description>&lt;p&gt;I rebuilt my tiny homage to Warhammer 40k this week. The original, Hive City Rampage, was a Python/Pygame (monstrocity), top-down grimdark shooter that ran but was a prototype. Now we have a sequel, Ashgate Siege, is Godot 4: gothic isometric, two missions, built for  Mac and Android.&lt;/p&gt;

&lt;p&gt;I have a confession though, the 95k lines of GDScript, 8 commits, roughly five hours was written by an AI agent under my direction. The sprites are generated too. I'm disclaiming that up front because the interesting part is what came out of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The easy part was the code
&lt;/h3&gt;

&lt;p&gt;This is the simple finding. An engine port is exactly the shape of coding my agents were very good at: the target is well documented, the semantics known, and correctness is checkable by running the thing. Godot 4 plus GDScript is heavily represented in training data for any model. Zero agent struggle.&lt;/p&gt;

&lt;h3&gt;
  
  
  The hard part was four pictures of the same orc
&lt;/h3&gt;

&lt;p&gt;Here's the hill I'm whiling to die on: &lt;strong&gt;for AI-assisted games, 2D sprite games are harder than 3D.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That sounds backwards, so: in 3D, the engine guarantees coherence. One mesh, one material, one light rig, and every frame of animation is consistent because it's the same object being transformed. The renderer is doing the work.&lt;/p&gt;

&lt;p&gt;In sprite based games there is no shared object. Each sprite is an independent generated map of pixels. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frame 2 of a walk cycle can be a different character than frame 1.&lt;/li&gt;
&lt;li&gt;The light source can move between sprites in the same scene.&lt;/li&gt;
&lt;li&gt;Proportions drift. Your massive size boss shrinks.&lt;/li&gt;
&lt;li&gt;Limbs get cropped at cell edges.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is caught by anything automatically. It just ships, and the game looks odd, like a badly executed collage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prompts as interface specs
&lt;/h3&gt;

&lt;p&gt;The fix was to stop treating generation as commissioning art and start treating it as calling an API with a strict schema. Every prompt pins the geometry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Production game sprite sheet: exactly 4 columns x 3 rows, 1536x1024 canvas, equal cells, solid pure magenta #FF00FF background for color-key import. NO text, shadows, smoke, or border. Every figure entirely within its own cell with ample margin; feet centered at same baseline in each row. All figures face screen RIGHT in three-quarter isometric view [...] Four columns are four coherent walking-cycle poses: left foot forward, passing, right foot forward, passing.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each sentence has to do some work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Magenta &lt;code&gt;#FF00FF&lt;/code&gt;, not transparency.&lt;/strong&gt; Alpha comes back unreliable. A color key is deterministic to strip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit grid and canvas.&lt;/strong&gt; The importer crops on fixed coordinates. If the grid drifts, every sprite is off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"feet centered at same baseline in each row."&lt;/strong&gt; Without it the character bobs while walking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Named poses.&lt;/strong&gt; "Walking animation" returns four unrelated drawings. Naming the four phases is what makes them a cycle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Negative constraints.&lt;/strong&gt; "No text, shadows, smoke, or border" because models add decoration that breaks the silhouette.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The importer is a compiler, and tests are CI
&lt;/h3&gt;

&lt;p&gt;If the prompt is a spec, something has to enforce it. Generated atlases go into &lt;code&gt;art/&lt;/code&gt;, an importer crops and scales them into runtime assets, and a native test suite checks the result: 2,972 reference and combat assertions, 17,112 seam combinations, plus a 600-frame combat simulation. &lt;code&gt;make test&lt;/code&gt; runs it.&lt;/p&gt;

&lt;p&gt;The seam check is the one most important. It takes the belt pixels from the leg sprite, offsets them by the waist socket, and counts how many land on opaque torso pixels. Under 32 and the pose fails. Every view, every clip, every frame, every facing, every gait: 17,112 combinations.&lt;/p&gt;

&lt;p&gt;That catches the failure mode that makes sprites look like a collage, which is a regenerated part that no longer meets the part next to it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Then stress testing the mechanism
&lt;/h3&gt;

&lt;p&gt;It is cheap to claim your tests work. So, I reimplemented the audit standalone, outside the engine, then fed it deliberately corrupted atlases to find out what it actually catches.&lt;/p&gt;

&lt;p&gt;Baseline reproduces: 17,112 combinations, zero failures.&lt;/p&gt;

&lt;p&gt;Then the damage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Character 4% smaller → &lt;strong&gt;FAIL, 352 bad combinations&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Character 8% smaller → &lt;strong&gt;FAIL, 6,728&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Alpha edge eroded 2px → &lt;strong&gt;FAIL, 3,036&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Torso shifted 6px down in the cell → &lt;strong&gt;PASS&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sweeping it: scale drift gets caught at 4% and up, and slips through at 3%. Vertical translation is never caught. I pushed it to 14px and it still passed, because shifting the torso down actually &lt;em&gt;raises&lt;/em&gt; the minimum overlap from 59 to 249. The check gets happier while the sprite gets worse.&lt;/p&gt;

&lt;p&gt;The seam audit measures whether two parts still meet. It says nothing about whether the pair sits correctly in the cell, because the waist socket moves with the torso. Catching that needs a different check, anchored to the cell rather than to the neighbouring sprite.&lt;/p&gt;

&lt;p&gt;I would rather know the shape of the hole in the tests than believe the suite covers everything. The standalone auditor is &lt;code&gt;tools/seam_audit.py&lt;/code&gt; in the repo. Plain Python, PIL and numpy, no Godot required, so you can run the numbers above yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I'd tell you to steal from my repo
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Color-key over alpha. Deterministic beats convenient.&lt;/li&gt;
&lt;li&gt;Pin the grid numerically in the prompt and crop on those exact numbers.&lt;/li&gt;
&lt;li&gt;Name every animation phase. Never say "walk cycle" and hope.&lt;/li&gt;
&lt;li&gt;Baseline your silhouettes and diff them. This is the whole safety net.&lt;/li&gt;
&lt;li&gt;Keep source atlases and prompts in the repo. Regeneration is a build step, so its inputs are source code.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;

&lt;p&gt;Mac and Android builds are on GitHub Releases. They're previews: macOS is ad-hoc signed and not notarized, Android uses a debug key, so both will warn you. Source builds with &lt;code&gt;make run&lt;/code&gt; if you have Godot 4.3.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nonatofabio/hive-city-rampage-ii" rel="noopener noreferrer"&gt;https://github.com/nonatofabio/hive-city-rampage-ii&lt;/a&gt;&lt;/p&gt;

</description>
      <category>godot</category>
      <category>gamedev</category>
      <category>ai</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How to give Claude "Long Term Memory" of your local files (No Docker required)</title>
      <dc:creator>Fabio Nonato</dc:creator>
      <pubDate>Wed, 10 Dec 2025 07:29:48 +0000</pubDate>
      <link>https://dev.to/nonatofabio_28/how-to-give-claude-long-term-memory-of-your-local-files-no-docker-required-57o2</link>
      <guid>https://dev.to/nonatofabio_28/how-to-give-claude-long-term-memory-of-your-local-files-no-docker-required-57o2</guid>
      <description>&lt;h3&gt;
  
  
  The Problem: RAG is too hard
&lt;/h3&gt;

&lt;p&gt;I have a folder of 50+ PDFs—technical specs, old logs, and some... &lt;em&gt;interesting&lt;/em&gt; declassified government documents.&lt;/p&gt;

&lt;p&gt;I want to open Claude and ask: &lt;em&gt;"What happened in the 1952 sightings?"&lt;/em&gt; and have it answer using &lt;strong&gt;my&lt;/strong&gt; files.&lt;/p&gt;

&lt;p&gt;The standard advice for this is a nightmare:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Spin up a Docker container (Chroma/Qdrant).&lt;/li&gt;
&lt;li&gt; Write a Python script to parse PDFs (LangChain/LlamaIndex).&lt;/li&gt;
&lt;li&gt; Set up an API server to bridge it to the LLM.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia0.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExZDg3czZtbHQ4ejF0bTQyZnF4bTFqbTJyNGN0djR6ZGRvbWRsa3BqcyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FAPqEbxBsVlkWSuFpth%2Fgiphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia0.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExZDg3czZtbHQ4ejF0bTQyZnF4bTFqbTJyNGN0djR6ZGRvbWRsa3BqcyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FAPqEbxBsVlkWSuFpth%2Fgiphy.gif" width="434" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;People turned "Ctrl+F with semantics" into a distributed systems architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution: &lt;code&gt;local-faiss-mcp&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;I got tired of the bloat, so I built &lt;a href="https://github.com/nonatofabio/local_faiss_mcp" rel="noopener noreferrer"&gt;local-faiss-mcp&lt;/a&gt;. It’s a CLI tool that runs 100% locally.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No Docker:&lt;/strong&gt; Just a Python script.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No API Keys:&lt;/strong&gt; Uses local embeddings (&lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt;) and FAISS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP Native:&lt;/strong&gt; Connects directly to Claude Desktop.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is how to set it up in 5 minutes.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 1: Get some "Cool" Data
&lt;/h3&gt;

&lt;p&gt;To test a RAG system, you need real-world data. Let's use the &lt;strong&gt;ODNI Preliminary Assessment on Unidentified Aerial Phenomena&lt;/strong&gt;. It’s declassified, text-heavy, and full of weird military jargon—perfect for testing semantic search.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Create a folder: &lt;code&gt;mkdir ~/ufo_docs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt; Download this PDF: &lt;a href="https://www.dni.gov/files/ODNI/documents/assessments/Prelimary-Assessment-UAP-20210625.pdf" rel="noopener noreferrer"&gt;ODNI Preliminary Assessment 2021 (PDF)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;(Optional) Throw in the &lt;a href="https://www.404media.co/content/files/2025/02/simplesabotage.pdf" rel="noopener noreferrer"&gt;CIA Simple Sabotage Manual&lt;/a&gt; just for chaos.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 2: Install the Tool
&lt;/h3&gt;

&lt;p&gt;It’s a standard Python package.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;local-faiss-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Index the Docs (The Magic Part)
&lt;/h3&gt;

&lt;p&gt;We used to have to write ingestion scripts. Now, we just use the CLI.&lt;/p&gt;

&lt;p&gt;Run this command to recursively index your new UFO folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;local-faiss index &lt;span class="s2"&gt;"~/ufo_docs/**/*.pdf"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll see a progress bar as it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Reads the PDFs (using &lt;code&gt;pypdf&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt; Chunks the text.&lt;/li&gt;
&lt;li&gt; Calculates embeddings on your CPU.&lt;/li&gt;
&lt;li&gt; Saves a &lt;code&gt;.index&lt;/code&gt; file to your disk.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Done. You now have a vector database.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia3.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExa3BwMXJ1OWF3MTNvNGM2bzNlMjI3dHEwZG50azQzZXdwM3FxcTdtciZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2F9r75ILTJtiDACKOKoY%2Fgiphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia3.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExa3BwMXJ1OWF3MTNvNGM2bzNlMjI3dHEwZG50azQzZXdwM3FxcTdtciZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2F9r75ILTJtiDACKOKoY%2Fgiphy.gif" width="275" height="252"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Connect to Claude
&lt;/h3&gt;

&lt;p&gt;Open your Claude Desktop config file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mac:&lt;/strong&gt; &lt;code&gt;~/Library/Application Support/Claude/claude_desktop_config.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows:&lt;/strong&gt; &lt;code&gt;%APPDATA%\Claude\claude_desktop_config.json&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add this block:&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;"local-faiss"&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;"local-faiss-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;"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;"--index-dir"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/ufo_docs"&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. You should see a little "plug" icon indicate two tools are loaded: &lt;code&gt;ingest_document&lt;/code&gt; and &lt;code&gt;query_rag_store&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Or use &lt;code&gt;claude&lt;/code&gt; in the terminal directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Ask Questions
&lt;/h3&gt;

&lt;p&gt;Now for the fun part. Open Claude and ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Using your local faiss knowledge: Does the Pentagon's refusal to answer questions in the UAP Hearing count as 'General Interference with Organizations'?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Call &lt;code&gt;query_rag_store&lt;/code&gt; with your question.&lt;/li&gt;
&lt;li&gt; The CLI will search your FAISS index and return the top N chunks.&lt;/li&gt;
&lt;li&gt; Claude will synthesize the answer with citations.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia3.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExa2g1bXFxZjM1OWVidno0M2Rzd25teWF6enRxbG8xdmdxeDh1M25hMyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FRxumQ0UcdXLX5ntURY%2Fgiphy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmedia3.giphy.com%2Fmedia%2Fv1.Y2lkPTc5MGI3NjExa2g1bXFxZjM1OWVidno0M2Rzd25teWF6enRxbG8xdmdxeDh1M25hMyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw%2FRxumQ0UcdXLX5ntURY%2Fgiphy.gif" width="480" height="274"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Advanced: Enabling "God Mode" (Re-ranking)
&lt;/h3&gt;

&lt;p&gt;If your dataset is huge, standard vector search can sometimes be "fuzzy." You might ask about "Project Mogul" and get results about "Project Grudge."&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;v0.2.0&lt;/strong&gt;, I added &lt;strong&gt;Re-ranking&lt;/strong&gt;. This uses a second, smarter model (CrossEncoder) to double-check the results before giving them to Claude.&lt;/p&gt;

&lt;p&gt;Update your config to enable it:&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;"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;"--index-dir"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"~/ufo_docs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--rerank"&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;Now the accuracy is much better, running entirely on your laptop.&lt;/p&gt;




&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;We don't need Kubernetes to chat with a PDF. We just need a good CLI.&lt;/p&gt;

&lt;p&gt;If you try this out with your own docs (or the UFO files!), let me know how the re-ranking performs for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🔗 Repo:&lt;/strong&gt; &lt;a href="https://github.com/nonatofabio/local_faiss_mcp" rel="noopener noreferrer"&gt;github.com/nonatofabio/local_faiss_mcp&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>mcp</category>
      <category>rag</category>
    </item>
  </channel>
</rss>
