<?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: Zayd Mulani</title>
    <description>The latest articles on DEV Community by Zayd Mulani (@zaydmulani09).</description>
    <link>https://dev.to/zaydmulani09</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%2F3939133%2Fa61f4cd3-0e64-4bac-9a1e-00ae898bf88f.png</url>
      <title>DEV Community: Zayd Mulani</title>
      <link>https://dev.to/zaydmulani09</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zaydmulani09"/>
    <language>en</language>
    <item>
      <title>My "secure" sandbox detected an attack and reported success anyway. Here's the marshaling bug that caused it.</title>
      <dc:creator>Zayd Mulani</dc:creator>
      <pubDate>Mon, 24 Aug 2026 21:36:24 +0000</pubDate>
      <link>https://dev.to/zaydmulani09/my-secure-sandbox-detected-an-attack-and-reported-success-anyway-heres-the-marshaling-bug-that-lap</link>
      <guid>https://dev.to/zaydmulani09/my-secure-sandbox-detected-an-attack-and-reported-success-anyway-heres-the-marshaling-bug-that-lap</guid>
      <description>&lt;p&gt;I built a tool (mri) that runs untrusted code snippets inside a sandbox, checked against an execution allowlist derived from a dependency graph. Before I trusted it for anything, I wrote a demo: three attacks, all blocked. Then I stopped testing.&lt;/p&gt;

&lt;p&gt;That was a mistake, and I only found out because I went back and wrote a second benchmark whose entire purpose was to disprove the first one — 19 cases of ordinary code that should just run, and 14 attack cases that weren't in my original three.&lt;/p&gt;

&lt;p&gt;The legitimate-code suite failed first, and badly. 5 of 19 normal patterns got blocked, including a plain function declaration with parameters and a try/catch block. The static scanner registered local bindings for arrow-function params but never for function_declaration params or catch-clause bindings — so those names looked like unresolved external references to the scanner, and unresolved means blocked, by design. A tool that blocks ordinary code isn't safer than one that doesn't. It's the kind of tool that gets a comment added to disable it.&lt;/p&gt;

&lt;p&gt;The attack suite found something worse than a missed attack. One case: a getter that doesn't touch process.env until the return value gets read on the way out of the sandbox. That access triggered the runtime's real violation check — the detection fired correctly. Then the code responsible for serializing that return value back to the host caught the resulting error, converted it to a string, and handed back a clean "executed" result. Nothing in the breach log. The check worked and the report lied about it. I'd rather have a tool that misses something and says so than one that catches something and hides it.&lt;/p&gt;

&lt;p&gt;Also found: the sandbox wasn't a real boundary in the first place. I was using node:vm, and console.log.constructor.constructor (or the same trick through any host-provided reference) reaches the host process's actual Function constructor. This is documented — Node's own docs say vm doesn't provide security isolation — but "documented" and "tested against my own code" are different things, and I hadn't done the second one.&lt;/p&gt;

&lt;p&gt;Fixed the sandbox by moving to isolated-vm, a real separate V8 isolate rather than a shared-realm context. Fixed the marshaling bug by making sure any violation, wherever it's triggered from, produces a blocked verdict before a return value ever gets serialized. Re-ran the full 33-case benchmark after both fixes: zero regressions, both cases now correctly blocked.&lt;/p&gt;

&lt;p&gt;Still true after the fixes: no OS-level sandboxing underneath the isolate, no taint tracking between two resources that are each individually granted, and it only evaluates one snippet at a time — no whole-program analysis.&lt;/p&gt;

&lt;p&gt;Repo, including the benchmark harness and raw per-case results: &lt;a href="https://github.com/zaydmulani09/mri" rel="noopener noreferrer"&gt;https://github.com/zaydmulani09/mri&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I built a static + runtime security scanner for MCP servers (Rust)</title>
      <dc:creator>Zayd Mulani</dc:creator>
      <pubDate>Wed, 19 Aug 2026 04:05:33 +0000</pubDate>
      <link>https://dev.to/zaydmulani09/i-built-a-static-runtime-security-scanner-for-mcp-servers-rust-4498</link>
      <guid>https://dev.to/zaydmulani09/i-built-a-static-runtime-security-scanner-for-mcp-servers-rust-4498</guid>
      <description>&lt;p&gt;I built a static + runtime security scanner for MCP servers (Rust)&lt;/p&gt;

&lt;p&gt;TL;DR: MCP (Model Context Protocol) has had 40+ CVEs disclosed just this year, and there wasn't a purpose-built scanner for it, so I built one. sentrymcp does static analysis (path traversal, injection, tool poisoning, missing auth) plus a runtime proxy mode that catches "rug pulls" — servers that silently change a tool's description after you've already approved it. Rust, MIT, Docker one-liner. Repo: &lt;a href="https://github.com/zaydmulani09/sentrymcp" rel="noopener noreferrer"&gt;https://github.com/zaydmulani09/sentrymcp&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this exists
&lt;/h2&gt;

&lt;p&gt;I've been building MCP tooling for a while (a reverse proxy, some agent infra), and at some point I went looking for something to check my own servers for basic security mistakes before shipping them. There wasn't really anything.&lt;/p&gt;

&lt;p&gt;That surprised me, because the CVE numbers aren't small. Endor Labs analyzed over 2,600 real MCP implementations and found 82% use file operations prone to path traversal, 67% use APIs related to code injection, and 34% use APIs susceptible to command injection. Separately, researchers have put the no-auth rate at around 38-40% of scanned servers — meaning close to 2 in 5 MCP servers in the wild have no authentication at all.&lt;/p&gt;

&lt;p&gt;The vulnerability classes aren't exotic either. Anthropic's own reference implementation, &lt;code&gt;mcp-server-git&lt;/code&gt;, shipped a path traversal bug where a &lt;code&gt;repo_path&lt;/code&gt; argument was never validated against a configured boundary — the exact "developer forgot one check" pattern that shows up constantly in these audits.&lt;/p&gt;

&lt;p&gt;So: static analysis for the code-level stuff, and since MCP has an attack class that literally can't exist in source code (a server that changes its tool description mid-session, after the user already approved the original), a runtime mode to catch that too.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;What it detects&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Code vulnerabilities&lt;/td&gt;
&lt;td&gt;Path traversal, command/shell injection, unsafe eval/code-injection sinks&lt;/td&gt;
&lt;td&gt;Unvalidated file path args, string-interpolated &lt;code&gt;exec()&lt;/code&gt; calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool poisoning&lt;/td&gt;
&lt;td&gt;Hidden instructions embedded in tool descriptions, unicode/homoglyph obfuscation&lt;/td&gt;
&lt;td&gt;"ignore previous instructions" phrasing, Cyrillic lookalike characters hiding in description text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth &amp;amp; permissions&lt;/td&gt;
&lt;td&gt;Missing auth on HTTP/SSE transports, hardcoded credentials, credentials leaked via logging&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;api_key = "sk-..."&lt;/code&gt; as a literal, unauthenticated SSE endpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runtime (proxy mode)&lt;/td&gt;
&lt;td&gt;Tool description changes after initial approval ("rug pulls"), unexpected outbound connections&lt;/td&gt;
&lt;td&gt;A tool's description silently changes between the first and second &lt;code&gt;tools/list&lt;/code&gt; call in a session&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The tool-poisoning detection is the one I'm most interested in feedback on. MCP tool descriptions get read directly into the model's context as trusted content, so an attacker who controls a description can hide instructions the LLM will act on while the user only sees a benign-looking label. sentrymcp checks for imperative hidden-instruction phrasing, zero-width/bidi-control unicode, and homoglyph mixing (Latin text with Cyrillic or Greek lookalike characters slipped in).&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;Static scan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mcpaudit scan ./some-mcp-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output is ranked by severity with a CWE or OWASP MCP Top 10 reference and a one-line remediation for each finding, split into three sections — code vulnerabilities, tool poisoning, and auth/permissions.&lt;/p&gt;

&lt;p&gt;Runtime proxy mode, sitting between your MCP client and the real server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sentrymcp proxy &lt;span class="nt"&gt;--&lt;/span&gt; &amp;lt;your-server-command&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This baselines every tool definition it sees on session start and diffs every later &lt;code&gt;tools/list&lt;/code&gt; response against it. Any change gets flagged and logged as a JSON event, with a human-readable summary at session end.&lt;/p&gt;

&lt;p&gt;Docker, no local Rust toolchain needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;docker build -t sentrymcp .
docker run --rm -v $(pwd):/scan sentrymcp scan /scan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How it's built
&lt;/h2&gt;

&lt;p&gt;Rust workspace, four crates: a core scan engine, a rules crate that loads detection patterns from TOML files (so adding a new check doesn't require recompiling), a CLI, and the proxy. Rule-driven design was a deliberate choice — path traversal, injection, and tool-poisoning heuristics are all just declarative pattern definitions with a severity, a reference (CWE or OWASP MCP Top 10 id), and remediation text, which makes the ruleset easy to extend as new attack patterns show up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations (being upfront about these)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Detection is regex/pattern-based, not AST or taint-tracking. It'll miss vulnerabilities that flow through an intermediate variable before hitting a sink — this is a known, documented gap, and proper taint analysis is the natural next step.&lt;/li&gt;
&lt;li&gt;Description extraction for tool-poisoning checks is targeted, not a full parser — it handles the common registration patterns in Python/TS/JS but isn't exhaustive.&lt;/li&gt;
&lt;li&gt;The runtime proxy's network-connection monitoring is currently Windows-only and best-effort (polls &lt;code&gt;netstat&lt;/code&gt;, not a real sandbox).&lt;/li&gt;
&lt;li&gt;Heuristic findings (like "no visible auth pattern anywhere in the file") are marked with a confidence flag separate from severity, since absence-of-pattern checks are inherently noisier than presence-based ones.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it / feedback wanted
&lt;/h2&gt;

&lt;p&gt;Repo's here: &lt;a href="https://github.com/zaydmulani09/sentrymcp" rel="noopener noreferrer"&gt;https://github.com/zaydmulani09/sentrymcp&lt;/a&gt; — MIT licensed, contributions welcome. If you run MCP servers, I'd genuinely appreciate people trying the scanner against their own and telling me what it misses or false-positives on. The corpus of test cases is still small and real-world feedback is the fastest way to make the ruleset actually useful instead of just theoretically correct.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>rust</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I built a local LLM that runs entirely in your browser. No install, no GPU, no server</title>
      <dc:creator>Zayd Mulani</dc:creator>
      <pubDate>Mon, 27 Jul 2026 18:34:05 +0000</pubDate>
      <link>https://dev.to/zaydmulani09/i-built-a-local-llm-that-runs-entirely-in-your-browser-no-install-no-gpu-no-server-3c3c</link>
      <guid>https://dev.to/zaydmulani09/i-built-a-local-llm-that-runs-entirely-in-your-browser-no-install-no-gpu-no-server-3c3c</guid>
      <description>&lt;p&gt;A few months ago I got obsessed with a question: can you run a real LLM entirely inside a browser tab, with zero backend, zero GPU, and zero install?&lt;/p&gt;

&lt;p&gt;The answer is yes. Here's what I built.&lt;/p&gt;

&lt;p&gt;ghost is a single HTML file that downloads a quantized language model into your browser's cache on first visit, then runs inference locally in WebAssembly forever after. Fully offline after that first download. No API key. No npm. No build step. Open the file, pick a model, chat.&lt;/p&gt;

&lt;p&gt;How it works&lt;/p&gt;

&lt;p&gt;The inference engine is wllama — a WebAssembly binding for llama.cpp. It runs GGUF quantized models directly in the browser using WASM SIMD. I pin it to a specific version so the JS and WASM files always match (learned this the hard way after a fun debugging session involving mismatched memory imports).&lt;/p&gt;

&lt;p&gt;Models are downloaded from HuggingFace on first load and cached via the browser's Cache API. On every subsequent visit they load instantly from cache, no network needed.&lt;/p&gt;

&lt;p&gt;Features&lt;/p&gt;

&lt;p&gt;Three models: Qwen2.5 1.5B (smart), Qwen2 0.5B (fast), TinyLlama (lightweight)&lt;br&gt;
Markdown rendering from scratch — no library, just regex transforms&lt;br&gt;
RAG: drag a .txt or .pdf onto the chat window. It chunks the text, embeds each chunk using wllama's embedding API, stores vectors in memory, and retrieves the top-3 relevant chunks on each message. Fully local, fully offline&lt;br&gt;
Voice input via the Web Speech API — mic button auto-sends on silence&lt;br&gt;
Multi-turn conversation memory capped at 10 turns&lt;br&gt;
PWA installable — works on mobile home screen too&lt;/p&gt;

&lt;p&gt;The hard parts&lt;/p&gt;

&lt;p&gt;Getting wllama to load from a cached model was genuinely tricky. Blob URLs created in the main thread aren't accessible from wllama's internal Web Worker. IndexedDB chunk reconstruction hit a 2GB ArrayBuffer limit on Windows Chrome. The final solution was using wllama's built-in loadModelFromHF with useCache: true which handles everything internally.&lt;/p&gt;

&lt;p&gt;The embeddings API requires toggling a flag (embeddings: true) that conflicts with normal chat completion — so I toggle it on before each embedding call and back off in a finally block.&lt;/p&gt;

&lt;p&gt;Try it&lt;/p&gt;

&lt;p&gt;Live: &lt;a href="https://zaydmulani09.github.io/ghost/ghost.html" rel="noopener noreferrer"&gt;https://zaydmulani09.github.io/ghost/ghost.html&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/zaydmulani09/ghost" rel="noopener noreferrer"&gt;https://github.com/zaydmulani09/ghost&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Speed on CPU is ~3-8 tokens/sec depending on model. Not fast, but it works, and every token stays on your machine.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I built a local-first coverage-guided fuzzer in Rust with LLM seed bootstrap</title>
      <dc:creator>Zayd Mulani</dc:creator>
      <pubDate>Tue, 14 Jul 2026 13:27:34 +0000</pubDate>
      <link>https://dev.to/zaydmulani09/i-built-a-local-first-coverage-guided-fuzzer-in-rust-with-llm-seed-bootstrap-4c4a</link>
      <guid>https://dev.to/zaydmulani09/i-built-a-local-first-coverage-guided-fuzzer-in-rust-with-llm-seed-bootstrap-4c4a</guid>
      <description>&lt;p&gt;I built a local-first coverage-guided fuzzer in Rust with LLM seed bootstrap&lt;/p&gt;

&lt;h1&gt;
  
  
  rust #opensource #security #fuzzing
&lt;/h1&gt;




&lt;p&gt;The problem that kept bothering me: I wanted to fuzz a custom binary protocol parser. Not something with a public corpus of real traffic. Just a service my team built that speaks a length-prefixed binary format over TCP.&lt;/p&gt;

&lt;p&gt;AFL++ requires a C harness. cargo-fuzz wraps libFuzzer but you still need to write the harness and you're starting from random bytes. Neither tool helps you when the first thing the parser does is check a 4-byte magic header and exit if it doesn't match. You spend hours getting past that check before you find anything real.&lt;/p&gt;

&lt;p&gt;So I built Phaedra.&lt;/p&gt;

&lt;h2&gt;
  
  
  The LLM seeding idea
&lt;/h2&gt;

&lt;p&gt;The core insight is that you don't need random bytes to start. You need bytes that look plausible. If you describe what your target parses in plain English, a language model can generate a structured initial corpus.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;phaedra fuzz &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target&lt;/span&gt; ./my_parser &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--description&lt;/span&gt; &lt;span class="s2"&gt;"TLV binary protocol with PHDR magic header, u8 type, u16be length, payload bytes"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Phaedra sends that description to a local Ollama instance and gets back hex-encoded seed inputs. No API key, no data leaving your machine. OpenAI and Anthropic work as drop-in backends if you want them.&lt;/p&gt;

&lt;p&gt;The seed generation prompt asks for variety: valid inputs, boundary values, truncated records, inputs where the length field exceeds the payload. The parser gets past the magic check immediately because the seeds include valid magic bytes. Campaign starts finding new coverage on the first execution instead of the hundredth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coverage via SanCov edge bitmaps
&lt;/h2&gt;

&lt;p&gt;Once you have seeds, you need coverage feedback. I chose SanCov edge bitmaps over shared memory rather than LLVM &lt;code&gt;.profraw&lt;/code&gt; files for one reason: speed. Reading a profraw file after each execution means a file write, a file read, and a parse. Shared memory is a single &lt;code&gt;shmat&lt;/code&gt; call and a memcpy.&lt;/p&gt;

&lt;p&gt;The setup: Phaedra allocates a 65536-slot edge bitmap in POSIX shared memory, passes the segment ID to the target via &lt;code&gt;__PHAEDRA_SHM_ID&lt;/code&gt;, and the target's SanCov runtime shim writes edge hit counts directly into that segment on every execution. After the process exits, Phaedra reads the bitmap, XORs against the global "seen" bitmap, and if new edges fired, the input goes into the corpus.&lt;/p&gt;

&lt;p&gt;The shim is a small C file that gets compiled into the target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;__sanitizer_cov_trace_pc_guard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;guard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!*&lt;/span&gt;&lt;span class="n"&gt;guard&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;phaedra_map&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="n"&gt;phaedra_map&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;guard&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;PHAEDRA_MAP_SIZE&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Schema-aware mutation
&lt;/h2&gt;

&lt;p&gt;Random byte mutation gets past the magic check once you have good seeds. But it still struggles with structured fields. If your protocol has a u16be length prefix followed by exactly that many payload bytes, random mutation almost never produces a valid record because it corrupts the length without adjusting the payload.&lt;/p&gt;

&lt;p&gt;The schema DSL solves this. You describe your protocol in TOML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my_protocol"&lt;/span&gt;

&lt;span class="nn"&gt;[[fields]]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"magic"&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"magic"&lt;/span&gt;
&lt;span class="py"&gt;length&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="py"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"50484452"&lt;/span&gt;
&lt;span class="py"&gt;mutable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="nn"&gt;[[fields]]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"length"&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"u16_be"&lt;/span&gt;

&lt;span class="nn"&gt;[[fields]]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"payload"&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"lp_bytes16_be"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;30% of mutations operate at the field level: corrupting the length prefix specifically, setting it to 0, to max value, or to a value larger than the actual payload. The other 70% are still raw byte mutations. The split keeps the campaign from getting stuck in schema-valid inputs only.&lt;/p&gt;

&lt;p&gt;If you have a corpus but no schema, &lt;code&gt;phaedra infer&lt;/code&gt; analyzes the samples with six heuristics and writes a starting schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;phaedra infer &lt;span class="nt"&gt;--corpus-db&lt;/span&gt; ./phaedra-corpus/corpus.db &lt;span class="nt"&gt;--name&lt;/span&gt; my_proto
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Crash triage
&lt;/h2&gt;

&lt;p&gt;When Phaedra finds a crash it deduplicates by signal number and input fingerprint. SIGSEGV and SIGABRT are CRITICAL. SIGILL and SIGFPE are HIGH. No-signal crashes are LOW. Each unique signature gets one entry in SQLite with a hit counter so if the same bug triggers 500 times you see one entry with hit_count=500, not 500 files in your crash directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;phaedra crashes

ID  SEVERITY   HITS  SIGNATURE     STATUS
1   LOW        47    crash_3f2a1b  Crash { signal: None }
2   LOW        12    crash_7e4d2c  Crash { signal: None }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;phaedra minimize&lt;/code&gt; runs delta minimization to reduce a crashing input to the smallest bytes that still trigger it. Found a 200-byte crash? It will usually get it down to under 10.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install &lt;/span&gt;phaedra
phaedra fuzz &lt;span class="nt"&gt;--demo&lt;/span&gt; tlv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The TLV demo target has a real bounds-check bug. Phaedra finds it within the first few seconds.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/zaydmulani09/phaedra" rel="noopener noreferrer"&gt;https://github.com/zaydmulani09/phaedra&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>opensource</category>
      <category>security</category>
      <category>testing</category>
    </item>
    <item>
      <title>I built a local token cost tracker for Claude Code in Rust</title>
      <dc:creator>Zayd Mulani</dc:creator>
      <pubDate>Mon, 13 Jul 2026 21:58:53 +0000</pubDate>
      <link>https://dev.to/zaydmulani09/i-built-a-local-token-cost-tracker-for-claude-code-in-rust-500h</link>
      <guid>https://dev.to/zaydmulani09/i-built-a-local-token-cost-tracker-for-claude-code-in-rust-500h</guid>
      <description>&lt;p&gt;After one too many surprise Claude Code bills, I built agentwatch.&lt;br&gt;
It's a local HTTP proxy written in Rust. Point your agent at localhost:7878, and it intercepts every request, parses the token usage, and shows a live Ratatui TUI dashboard of what you're spending — in real time.&lt;br&gt;
Everything stays local. SQLite database, zero signup, single binary via cargo install agentwatch.&lt;br&gt;
Supports Claude Code, Codex, and Gemini CLI.&lt;br&gt;
github.com/zaydmulani09/agentwatch&lt;/p&gt;

</description>
      <category>rust</category>
      <category>ai</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I built a self-hosted reverse proxy for MCP servers in Rust</title>
      <dc:creator>Zayd Mulani</dc:creator>
      <pubDate>Thu, 02 Jul 2026 18:52:40 +0000</pubDate>
      <link>https://dev.to/zaydmulani09/i-built-a-self-hosted-reverse-proxy-for-mcp-servers-in-rust-45a1</link>
      <guid>https://dev.to/zaydmulani09/i-built-a-self-hosted-reverse-proxy-for-mcp-servers-in-rust-45a1</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Every AI tool that talks to MCP servers (Claude, Cursor, etc.) &lt;br&gt;
connects directly — no auth, no rate limiting, no observability. &lt;br&gt;
You have no idea what's hitting your servers or how often.&lt;/p&gt;

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

&lt;p&gt;MCP Gateway sits in front of all your MCP servers and handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Routing&lt;/strong&gt; — /mcp/server-name/* proxies to the right backend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth&lt;/strong&gt; — per-server API keys encrypted AES-256-GCM in memory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting&lt;/strong&gt; — token bucket per client, 429 + Retry-After&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging&lt;/strong&gt; — SQLite log of every request with latency + status&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usage tracking&lt;/strong&gt; — token counts from Anthropic/OpenAI responses&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLI&lt;/strong&gt; — mcpgw server add/list/remove, mcpgw logs show, mcpgw stats&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quickstart
&lt;/h2&gt;

&lt;p&gt;MCPGW_MASTER_SECRET=your-secret docker compose up --build&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;p&gt;Rust, Axum, SQLite (rusqlite), DashMap, AES-GCM&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/zaydmulani09/mcp-gateway" rel="noopener noreferrer"&gt;https://github.com/zaydmulani09/mcp-gateway&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>mcp</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I built a local-first AI memory layer for LLMs in Rust (no cloud, no API keys)</title>
      <dc:creator>Zayd Mulani</dc:creator>
      <pubDate>Wed, 03 Jun 2026 20:33:22 +0000</pubDate>
      <link>https://dev.to/zaydmulani09/i-built-a-local-first-ai-memory-layer-for-llms-in-rust-no-cloud-no-api-keys-1eh8</link>
      <guid>https://dev.to/zaydmulani09/i-built-a-local-first-ai-memory-layer-for-llms-in-rust-no-cloud-no-api-keys-1eh8</guid>
      <description>&lt;p&gt;Every LLM app has the same problem — the model forgets everything between &lt;br&gt;
conversations. Cloud solutions like Mem0 exist but they send your data &lt;br&gt;
to their servers. I built mnemo to solve this locally.&lt;/p&gt;

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

&lt;p&gt;mnemo runs as a sidecar process next to your app. You POST text to it, &lt;br&gt;
it extracts named entities and relationships using a local LLM (Ollama), &lt;br&gt;
builds a persistent knowledge graph, and injects relevant context back &lt;br&gt;
into your prompts automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rust&lt;/strong&gt; — core engine, 4 crates (mnemo-core, mnemo-api, mnemo-cli, mnemo-bench)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite + WAL mode&lt;/strong&gt; — persistent storage, survives restarts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;petgraph&lt;/strong&gt; — in-memory knowledge graph with BFS traversal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Axum&lt;/strong&gt; — REST API sidecar any app can call&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ollama&lt;/strong&gt; — fully local LLM, zero API costs&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Fully free by default
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;mnemo-ollama ollama pull llama3
curl http://localhost:8080/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works with OpenAI or Anthropic too if you bring your own key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Python SDK
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mnemo&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MnemoClient&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MnemoClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ingest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;m building a Rust vector database called vecdb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;what am I working on?&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;h2&gt;
  
  
  Numbers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;122 Rust tests, 21 Python SDK tests&lt;/li&gt;
&lt;li&gt;Sub-millisecond entity lookup&lt;/li&gt;
&lt;li&gt;~4ms full retrieval pipeline (debug build)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/zaydmulani09/mnemo" rel="noopener noreferrer"&gt;https://github.com/zaydmulani09/mnemo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Would love feedback, especially on the retrieval scoring and graph &lt;br&gt;
traversal approach.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>ai</category>
      <category>opensource</category>
      <category>llm</category>
    </item>
    <item>
      <title>I built an open-source dependency intelligence platform in TypeScript — here's how it works</title>
      <dc:creator>Zayd Mulani</dc:creator>
      <pubDate>Fri, 29 May 2026 12:13:27 +0000</pubDate>
      <link>https://dev.to/zaydmulani09/i-built-an-open-source-dependency-intelligence-platform-in-typescript-heres-how-it-works-1ofi</link>
      <guid>https://dev.to/zaydmulani09/i-built-an-open-source-dependency-intelligence-platform-in-typescript-heres-how-it-works-1ofi</guid>
      <description>&lt;p&gt;Most teams find out their dependencies are risky after something breaks. A maintainer disappears, a vulnerability sits unpatched for months, or a single package with one contributor becomes a hidden outage waiting to happen. I wanted a tool that told you this stuff before it became a problem.&lt;br&gt;
So I built depgraph — an open-source dependency intelligence platform that crawls npm, PyPI, and Cargo registries, builds a live risk-scored dependency graph, and tells you exactly which packages in your supply chain are about to cause you pain.&lt;br&gt;
What it does&lt;br&gt;
Risk scoring across 6 dimensions&lt;br&gt;
Every package gets scored on security (open advisories, CVSS scores), maintenance (commit recency, release cadence), compatibility (semver violation rate), concentration (bus factor, single-maintainer risk), blast radius (how many downstream packages break if this one changes), and operational health (issue response latency, PR merge time).&lt;br&gt;
These combine into a single composite score with full explanations — not just a number, but "35% because maintainer activity fell 80% in 120 days."&lt;br&gt;
Interactive dependency graph&lt;br&gt;
Built with Cytoscape.js. Click any package to see its full transitive dependency tree, blast radius stats, and which packages it would take down if it disappeared. Chokepoint detection highlights the packages that are structurally too central to ignore.&lt;br&gt;
Policy engine with CI gate&lt;br&gt;
Define rules like "block packages with one maintainer and no release in 180 days" or "require approval for anything with 500+ downstream dependents." A GitHub Action runs the policy check on every PR and fails the build on violations — with a formatted comment showing exactly what triggered and how to fix it.&lt;br&gt;
Abandonment detection&lt;br&gt;
Time-series signals track commit frequency trends, maintainer count decay, and bus factor over time. A weighted model produces an abandonment probability score per package so you can see which ones are quietly dying before they become your problem.&lt;br&gt;
Historical snapshot diffs&lt;br&gt;
Every scan creates a full risk snapshot. Compare any two snapshots to see which packages degraded, which improved, what new vulnerability chains appeared, and how your overall supply chain health changed over time.&lt;br&gt;
Tech stack&lt;br&gt;
TypeScript monorepo with pnpm workspaces. Hono API, React + Vite frontend, PostgreSQL + Drizzle ORM, BullMQ workers, MinIO for raw storage. Fully self-hostable — one docker compose up and you're running. Zero paid services required.&lt;br&gt;
Supports npm, PyPI, and Cargo with a shared normalization layer that maps all three ecosystems to a canonical data model.&lt;br&gt;
Running it locally&lt;br&gt;
bashgit clone &lt;a href="https://github.com/zaydmulani09/depgraph" rel="noopener noreferrer"&gt;https://github.com/zaydmulani09/depgraph&lt;/a&gt;&lt;br&gt;
cd depgraph&lt;br&gt;
pnpm install --ignore-scripts&lt;br&gt;
cp .env.example .env&lt;br&gt;
docker compose up -d&lt;br&gt;
pnpm db:migrate&lt;br&gt;
pnpm --filter @depgraph/api dev &amp;amp;&lt;br&gt;
pnpm --filter @depgraph/crawler dev &amp;amp;&lt;br&gt;
pnpm --filter @depgraph/ui dev&lt;br&gt;
Open localhost:5173. The crawler seeds 10 packages immediately and starts processing — within a few minutes you'll see risk scores, graphs, and explanations populating in the UI.&lt;br&gt;
What I learned building this&lt;br&gt;
The hardest part wasn't the risk scoring or the UI — it was maintaining a living graph. Getting data once is easy. Keeping it fresh, detecting drift, diffing snapshots, and making all of that fast enough to be useful is where the real complexity lives.&lt;br&gt;
The second hardest part was normalization. npm, PyPI, and Cargo all have wildly different versioning semantics, dependency specifier formats, and registry API shapes. Building a canonical model that works cleanly across all three took longer than any other single component.&lt;/p&gt;

&lt;p&gt;Check it out, open issues, and PRs are welcome.&lt;br&gt;
GitHub: &lt;a href="https://github.com/zaydmulani09/depgraph" rel="noopener noreferrer"&gt;https://github.com/zaydmulani09/depgraph&lt;/a&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>opensource</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built a local-first hybrid vector database in Rust from scratch</title>
      <dc:creator>Zayd Mulani</dc:creator>
      <pubDate>Tue, 19 May 2026 01:29:22 +0000</pubDate>
      <link>https://dev.to/zaydmulani09/i-built-a-local-first-hybrid-vector-database-in-rust-from-scratch-3fd3</link>
      <guid>https://dev.to/zaydmulani09/i-built-a-local-first-hybrid-vector-database-in-rust-from-scratch-3fd3</guid>
      <description>&lt;p&gt;A few months ago I started building vecdb — a vector database that &lt;br&gt;
runs entirely on your own machine. No cloud, no API keys, no subscription.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Most vector databases make you choose — semantic search OR keyword search.&lt;br&gt;
Semantic search finds meaning but misses exact keywords. Keyword search &lt;br&gt;
finds exact matches but misses meaning.&lt;/p&gt;

&lt;p&gt;vecdb combines both in a two-stage pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;HNSW dense index retrieves candidates by meaning&lt;/li&gt;
&lt;li&gt;BM25 sparse index re-scores by keyword relevance
&lt;/li&gt;
&lt;li&gt;A fusion function combines both scores&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What it can do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Hybrid HNSW + BM25 retrieval&lt;/li&gt;
&lt;li&gt;SQL-like query language with VECTOR_SIM predicate&lt;/li&gt;
&lt;li&gt;Python and TypeScript SDKs&lt;/li&gt;
&lt;li&gt;Single binary, Docker support&lt;/li&gt;
&lt;li&gt;187 tests&lt;/li&gt;
&lt;li&gt;MIT license&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Example query
&lt;/h2&gt;

&lt;p&gt;SELECT * FROM documents&lt;br&gt;
WHERE VECTOR_SIM(vec, [0.1, 0.2, 0.3]) &amp;gt; 0.75&lt;br&gt;
AND payload-&amp;gt;&amp;gt;'region' = 'US'&lt;br&gt;
LIMIT 10;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/zaydmulani09/vecdb" rel="noopener noreferrer"&gt;https://github.com/zaydmulani09/vecdb&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Would love feedback from the community — especially on the &lt;br&gt;
architecture and what to tackle in v0.2.0.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rust</category>
      <category>opensource</category>
      <category>database</category>
    </item>
  </channel>
</rss>
