<?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: Pranab Sarkar</title>
    <description>The latest articles on DEV Community by Pranab Sarkar (@spranab).</description>
    <link>https://dev.to/spranab</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%2F3785623%2F84db641c-9546-4aa5-8312-46f45b965e45.png</url>
      <title>DEV Community: Pranab Sarkar</title>
      <link>https://dev.to/spranab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/spranab"/>
    <language>en</language>
    <item>
      <title>Twelve Green CI Legs, One Dead SSE Server</title>
      <dc:creator>Pranab Sarkar</dc:creator>
      <pubDate>Thu, 03 Sep 2026 15:05:38 +0000</pubDate>
      <link>https://dev.to/spranab/twelve-green-ci-legs-one-dead-sse-server-1iij</link>
      <guid>https://dev.to/spranab/twelve-green-ci-legs-one-dead-sse-server-1iij</guid>
      <description>&lt;p&gt;The official &lt;code&gt;mcp&lt;/code&gt; Python SDK went to 2.0.0 and deleted &lt;code&gt;mcp.server.fastmcp&lt;/code&gt; outright. No shim, no deprecation window that downstream packages got to see. yantrikdb-mcp is a thin MCP server over YantrikDB, my persistent cognitive memory substrate for AI agents, and at 04:08 on 2026-08-04 I did the defensive thing: v0.11.0 (&lt;code&gt;d823b82&lt;/code&gt;) pinned &lt;code&gt;mcp[cli]&lt;/code&gt; to &lt;code&gt;&amp;lt;2.0.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A ceiling like that doesn't stop anyone from installing the package. pip resolves mcp 1.x and moves on. It bites later, in an environment where something else wants &lt;code&gt;mcp&amp;gt;=2.0.0&lt;/code&gt;, because then the resolver has to pick a loser and one of the two packages isn't getting installed. That's an acceptable trade for a few hours. It's a bad thing to leave sitting in a published &lt;code&gt;pyproject.toml&lt;/code&gt;, so I spent the morning taking it back out.&lt;/p&gt;

&lt;p&gt;Scale matters here, because it changes how much a five-hour window costs. yantrikdb-mcp is the fastest-moving package in my download numbers at the moment: 1,688 a month when I logged it on 2026-08-24, 2,137 on 2026-08-31. Up 26.6% in a week, mostly a cluster of registry listings landing close together rather than any single event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Probing instead of parsing
&lt;/h2&gt;

&lt;p&gt;v0.12.0 (&lt;code&gt;0ca6092&lt;/code&gt;, PR #19) shipped at 10:39, six and a half hours after the pin. One file, &lt;code&gt;src/yantrikdb_mcp/_compat.py&lt;/code&gt;, became the single import site for every MCP SDK symbol the rest of the package touches, and it resolved them by probing rather than by reading a version string and branching on it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# _compat.py, trimmed to the shape of the probe
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.server.mcpserver&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MCPServer&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Server&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp.server.mcpserver.exceptions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ToolError&lt;/span&gt;
    &lt;span class="n"&gt;MCP_MAJOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ImportError&lt;/span&gt;&lt;span class="p"&gt;:&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;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Server&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;MCP_MAJOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What actually moved, found by running the suite against 2.0.0 rather than by reading a changelog: &lt;code&gt;FastMCP&lt;/code&gt; became &lt;code&gt;MCPServer&lt;/code&gt;, &lt;code&gt;Context&lt;/code&gt; moved under &lt;code&gt;mcp.server.mcpserver&lt;/code&gt;, and &lt;code&gt;ToolError&lt;/code&gt; went one level deeper than that, to &lt;code&gt;mcp.server.mcpserver.exceptions&lt;/code&gt;. &lt;code&gt;mcp.types.ToolAnnotations&lt;/code&gt; stayed exactly where it was, which I would not have guessed given everything around it had shifted. The lifespan dict, &lt;code&gt;request_context.lifespan_context&lt;/code&gt;, annotations passthrough, &lt;code&gt;.run(transport=...)&lt;/code&gt;: all identical across the major bump. Most of a breaking release wasn't breaking for this codebase, and the pin had been treating the entire API surface as suspect when only a slice of it had moved.&lt;/p&gt;

&lt;p&gt;One divergence surfaced only by running things. mcp 2.x refuses to register a static resource whose handler declares a &lt;code&gt;Context&lt;/code&gt; parameter, and it refuses at import time, which takes the server down before it answers a single request. yantrikdb-mcp had two of those, &lt;code&gt;yantrikdb://stats&lt;/code&gt; and &lt;code&gt;yantrikdb://health&lt;/code&gt;. Both now reach the engine through a process-singleton and don't ask for a &lt;code&gt;Context&lt;/code&gt; at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What twelve legs bought
&lt;/h2&gt;

&lt;p&gt;CI got a new axis, &lt;code&gt;mcp-line&lt;/code&gt;, with values &lt;code&gt;mcp1&lt;/code&gt; and &lt;code&gt;mcp2&lt;/code&gt;, applied across the unit and e2e suites. Twelve legs. Each leg installs its SDK pin &lt;em&gt;after&lt;/em&gt; the package installs and then asserts the resolved major, specifically so a compat layer that quietly resolved the wrong way couldn't pass by accident and turn one leg into a duplicate of the other. 214 tests passed on mcp 1.x. 214 on mcp 2.x. The &lt;code&gt;pyproject.toml&lt;/code&gt; ceiling went from &lt;code&gt;&amp;lt;2.0.0&lt;/code&gt; to &lt;code&gt;&amp;lt;3.0.0&lt;/code&gt;, and &lt;code&gt;--version&lt;/code&gt; started reporting the active line: &lt;code&gt;yantrikdb-mcp 0.12.0 (mcp 2.x)&lt;/code&gt;. I tagged it.&lt;/p&gt;

&lt;h2&gt;
  
  
  15:42
&lt;/h2&gt;

&lt;p&gt;v0.12.1 went out five hours later: "fix SSE / streamable-http startup on mcp 2.x (v0.12.0 regression)."&lt;/p&gt;

&lt;p&gt;The e2e cases, on both legs, all drove stdio. Most of the 214 are unit tests that don't exercise a transport at all. So nothing in the suite touched SSE or streamable-http, which are the two transports yantrikdb-mcp uses for shared network deployments, and v0.12.0 had broken both of them on mcp 2.x.&lt;/p&gt;

&lt;p&gt;mcp 1.x configures a network server by mutating shared state (&lt;code&gt;mcp.settings.host&lt;/code&gt;, &lt;code&gt;mcp.settings.transport_security.enable_dns_rebinding_protection&lt;/code&gt;) and then reading those values back inside &lt;code&gt;server.sse_app()&lt;/code&gt;. mcp 2.x's &lt;code&gt;Settings&lt;/code&gt; object has no such fields. The old code kept mutating, and on 2.x it raised:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ValueError: "Settings" object has no field "host"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On 2.x those values are arguments to the app factory instead. &lt;code&gt;_compat.build_network_app(server, transport, host, port)&lt;/code&gt; is now the only place in the package that knows the difference, and &lt;code&gt;_run_network()&lt;/code&gt; delegates to it rather than poking &lt;code&gt;settings.host&lt;/code&gt; itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# build_network_app(), condensed to the branch that matters
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;MCP_MAJOR&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="c1"&gt;# 1.x reads its configuration back off the server object
&lt;/span&gt;    &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transport_security&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enable_dns_rebinding_protection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sse_app&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# 2.x: the fields don't exist, the factory takes them
&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sse_app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transport_security&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;security&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The streamable-http factory goes through the same branch with a different call. Worth being precise about the failure mode: the server didn't come up misconfigured, it didn't come up. The process died before uvicorn bound a port. A stdio suite is structurally incapable of noticing, because stdio never reaches &lt;code&gt;sse_app()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I found it because I went to upgrade the live SSE box I run, which sits behind bearer-token auth and depends entirely on that path, and I checked it before rolling it forward. Twelve green legs did not catch this bug. A pre-deploy check did, which is the least impressive way there is to find one.&lt;/p&gt;

&lt;p&gt;Past the unit tests, I started a real SSE server on mcp 2.x with &lt;code&gt;YANTRIKDB_API_KEY&lt;/code&gt; set: uvicorn bound, &lt;code&gt;GET /sse&lt;/code&gt; returned 401 with no token and 200 with one. A new file, &lt;code&gt;tests/test_network_transport_compat.py&lt;/code&gt;, builds the actual ASGI app for both &lt;code&gt;sse&lt;/code&gt; and &lt;code&gt;streamable-http&lt;/code&gt; against whichever SDK line is installed, so both legs finally walk the network path. It also carries a source-level guard against &lt;code&gt;_run_network&lt;/code&gt; ever reverting to direct &lt;code&gt;settings.host&lt;/code&gt; mutation, plus a tripwire assertion that the &lt;code&gt;host&lt;/code&gt; field really is absent from &lt;code&gt;Settings&lt;/code&gt; on 2.x, so a future 2.x release that quietly restores it fails the suite instead of letting the shim do the wrong thing on both majors at once. Counts after the fix: 218 on mcp 1.x, 219 on mcp 2.x. The asymmetric one is that tripwire, which has nothing to assert on the 1.x leg.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I'd argue about
&lt;/h2&gt;

&lt;p&gt;Twelve felt like coverage because twelve is a bigger number than three. What I had was two dependency pins multiplied against one transport. Multiplying an axis against the same assertions doesn't buy a code path, it buys a longer CI run and a wider grid of checkmarks, and I think a great many matrices in a great many repos are built exactly that way: wide where widening is cheap, thin where the interesting failures live. Mine was, until August. Now, before I add an axis, I want to be able to name the line of code it forces the suite through. If I can't name one, the axis is decoration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same shape, one size down
&lt;/h2&gt;

&lt;p&gt;A different gate on this repo caps how large the &lt;code&gt;tools/list&lt;/code&gt; JSON schema is allowed to get, kept deliberately tight so it doesn't eat into a model's context budget. PR #37 merged with four red CI checks. The gate had passed locally on Python 3.13 and failed in CI on 3.10 and 3.12, because 3.10 renders a hint like &lt;code&gt;str | None&lt;/code&gt; into a noticeably fatter &lt;code&gt;anyOf&lt;/code&gt; block than 3.13 or 3.14 do: roughly 1,950 extra characters from identical source, against a budget with 197 characters of headroom. PR #38 clawed back 204 characters by trimming a docstring, raised the ceiling from 48,600 to 48,800, and wrote the reasoning into the test file instead of bumping the number silently. There was already a comment in that file warning about this exact interpreter quirk, left there the first time it happened.&lt;/p&gt;

&lt;p&gt;Same shape as the SSE regression, one size down: green in the configuration I looked at, red in the one I didn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three weeks later, the probe needed patching too
&lt;/h2&gt;

&lt;p&gt;On 2026-08-25 I had to fix the probe itself (&lt;code&gt;0e170a9&lt;/code&gt;, PR #35). mcp 2.1.1 reintroduced &lt;code&gt;mcp.server.fastmcp&lt;/code&gt; alongside the canonical &lt;code&gt;mcp.server.mcpserver&lt;/code&gt; tree. Read the code block above again: its logic is "if &lt;code&gt;fastmcp&lt;/code&gt; imports, we're on 1.x," and on 2.1.x both trees import cleanly, so it confidently answered 1. The fix reads &lt;code&gt;importlib.metadata.version('mcp')&lt;/code&gt; directly instead of inferring the line from which modules happen to resolve, adds a second check tying the canonical 2.x tree to &lt;code&gt;MCP_MAJOR&lt;/code&gt;, and pins the both-trees-importable shape to 2.x in a regression test. Validated against mcp 1.29.1, 2.0.0 and 2.1.1.&lt;/p&gt;

&lt;p&gt;So probing beat parsing right up to the point where upstream did something stranger than I'd allowed for, and the version string I'd deliberately avoided ended up being part of the answer after all.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>python</category>
      <category>testing</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Made an MCP Server Stop Calling Any AI API, and It Got More Useful</title>
      <dc:creator>Pranab Sarkar</dc:creator>
      <pubDate>Thu, 27 Aug 2026 14:58:41 +0000</pubDate>
      <link>https://dev.to/spranab/i-made-an-mcp-server-stop-calling-any-ai-api-and-it-got-more-useful-26jg</link>
      <guid>https://dev.to/spranab/i-made-an-mcp-server-stop-calling-any-ai-api-and-it-got-more-useful-26jg</guid>
      <description>&lt;p&gt;v1.0 of brainstorm-mcp shipped with two providers wired up: OpenAI and DeepSeek. Gemini arrived later in v1.2.0 (&lt;code&gt;3690f24&lt;/code&gt;, 2026-02-26), and groq, mistral and together are in the list too. You never needed all of them. The provider loader walks the list and skips anything whose env var is missing, so one working key is enough to run a debate.&lt;/p&gt;

&lt;p&gt;The catch is which key. Every provider in that list belongs to somebody other than Anthropic. There is no Claude provider in the server and there never was; in API mode Claude's role is to read what the other models said and add its own take, which happens through the host rather than through a key the server holds. So if you're running Claude Code and you install &lt;code&gt;brainstorm-mcp&lt;/code&gt; to see what a multi-model debate looks like, the number of usable keys you have is quite possibly zero. The friction was never "go collect N keys." It was "go sign up with a vendor you weren't otherwise using, before you can find out whether this tool is any good."&lt;/p&gt;

&lt;h2&gt;
  
  
  The inversion
&lt;/h2&gt;

&lt;p&gt;v1.4.0, commit &lt;code&gt;7906fbf&lt;/code&gt;, 2026-03-24: "Add hosted mode for API-key-free multi-model brainstorming" (PR #1, co-authored with Claude Opus 4.6 per the commit trailer; Opus and I worked out the orchestration model together).&lt;/p&gt;

&lt;p&gt;Hosted mode isn't a seventh provider or a free-tier proxy. In API mode the server is a caller. It holds keys, hits &lt;code&gt;api.openai.com&lt;/code&gt; or &lt;code&gt;api.deepseek.com&lt;/code&gt;, returns results. In hosted mode it never calls a model API. It builds the debate prompt for a given round and a given voice, then hands that prompt back to the host as tool output. Claude Code spawns sub-agents on opus, sonnet and haiku to run those prompts, using model access the host already has. Responses come back in through &lt;code&gt;brainstorm_collect&lt;/code&gt;, which appends them to the debate and decides what round comes next.&lt;/p&gt;

&lt;p&gt;Worth being precise about the mechanism, because it's easy to overstate. The server cannot make the host do any of that. What it returns is a prompt plus instructions, and Claude Code acts on them because acting on tool guidance is what a host does. There's no protocol-level guarantee anywhere in this. A host that ignored the instructions and dumped the prompt text at the user would leave the server sitting on a half-finished session until it timed out. The whole arrangement is cooperative, and I'd rather say so than let it sound like the server is driving.&lt;/p&gt;

&lt;p&gt;Mode selection is automatic. If the model names you pass don't carry a &lt;code&gt;provider:&lt;/code&gt; prefix (no &lt;code&gt;openai:gpt-5.5&lt;/code&gt;, no &lt;code&gt;deepseek:deepseek-chat&lt;/code&gt;), the server reads that as hosted mode and assumes you mean the host's own models. No flag, no config file, just the shape of the string.&lt;/p&gt;

&lt;p&gt;The same commit brought context injection, so a debate can be grounded in a real diff or file instead of a topic sentence, plus the debate styles (freeform, redteam, socratic) and a synthesis format that closes every debate with Recommendation, Key Tradeoffs, Strongest Disagreement in both modes. &lt;code&gt;brainstorm_respond&lt;/code&gt; was already there from v1.1.0 (&lt;code&gt;75c8949&lt;/code&gt;, "Add interactive mode — Claude participates as active debater"); &lt;code&gt;7906fbf&lt;/code&gt; touched it by exactly one line, passing &lt;code&gt;session.style&lt;/code&gt; through.&lt;/p&gt;

&lt;p&gt;The design consequence I care about is what happens once the server stops calling models. It has to hold state: which round the debate is on, which voices have spoken, what's outstanding. That lives in memory with a 10-minute TTL, and calling &lt;code&gt;brainstorm_collect&lt;/code&gt; against an expired session returns an error telling you to start a new one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways it broke, same afternoon
&lt;/h2&gt;

&lt;p&gt;Both of these live in the API-mode calling path, which hosted mode's design doesn't touch. They broke a debate I was running to test hosted mode, so they're part of the same afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;v1.5.6, &lt;code&gt;c9e3d86&lt;/code&gt;, 2026-04-27, "drop temperature for gpt-5.x reasoning models."&lt;/strong&gt; Three-round brainstorm, &lt;code&gt;openai:gpt-5.5&lt;/code&gt; and &lt;code&gt;deepseek:deepseek-chat&lt;/code&gt; as the two voices. Every round-1 turn from gpt-5.5 came back as an HTTP 400:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unsupported value: 'temperature' does not support 0.7 with this model. Only the default (1) value is supported.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gpt-5.x family and the o-series reject any non-default temperature. deepseek-chat doesn't care what you send it, so the loop ran fine right up to the OpenAI voice, which is why testing hadn't caught it. The code already branched on model family for &lt;code&gt;max_tokens&lt;/code&gt; vs &lt;code&gt;max_completion_tokens&lt;/code&gt;, tracked by a &lt;code&gt;useNewTokenParam&lt;/code&gt; flag, so the fix reused that branch and stopped sending &lt;code&gt;temperature: 0.7&lt;/code&gt; down it. Older gpt-4.x models still get it, since they still accept it. Rechecked against &lt;code&gt;gpt-5.5-2026-04-23&lt;/code&gt;: 200 OK, no &lt;code&gt;temperature&lt;/code&gt; field in the request body.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;v1.5.7, &lt;code&gt;9af668b&lt;/code&gt;, same day, "bump per-call timeout 2m -&amp;gt; 5m."&lt;/strong&gt; I shipped the temperature fix, reran the same debate, and it stalled at 120 seconds. &lt;code&gt;DEFAULT_TIMEOUT_MS&lt;/code&gt; in &lt;code&gt;src/debate.ts&lt;/code&gt; was hardcoded to &lt;code&gt;120_000&lt;/code&gt;. gpt-5.5 doing reasoning on a real brainstorm prompt (multi-thousand-word topic, six-option intervention list, redteam framing instructions on top) goes past two minutes routinely. Session &lt;code&gt;01751c60-256a-489b-88d3-9b954a3edd79&lt;/code&gt;, round 1, same wall on retry. One constant, &lt;code&gt;300_000&lt;/code&gt;, done. deepseek-chat and gpt-4.x were finishing well under two minutes and saw no change at all.&lt;/p&gt;

&lt;p&gt;Under an hour between the two commits, which is about how long I spent believing the first one had unblocked the debate.&lt;/p&gt;

&lt;p&gt;One thing I'm watching now: &lt;code&gt;SESSION_TTL_MS&lt;/code&gt; is &lt;code&gt;600_000&lt;/code&gt; while &lt;code&gt;DEFAULT_TIMEOUT_MS&lt;/code&gt; is &lt;code&gt;300_000&lt;/code&gt; post-fix. Two slow reasoning calls back to back inside a single round could in principle burn most of a session's life before the round finishes. I haven't hit it and I'm not certain the arithmetic works out that way in practice, but it's the next thing I'd expect to go wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the numbers say
&lt;/h2&gt;

&lt;p&gt;From &lt;code&gt;state/metrics-history.jsonl&lt;/code&gt;, npm downloads per month and GitHub stars as recorded:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;npm downloads/month&lt;/th&gt;
&lt;th&gt;GitHub stars&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2026-08-09&lt;/td&gt;
&lt;td&gt;286&lt;/td&gt;
&lt;td&gt;67&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-08-18&lt;/td&gt;
&lt;td&gt;345&lt;/td&gt;
&lt;td&gt;67&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-08-21&lt;/td&gt;
&lt;td&gt;413&lt;/td&gt;
&lt;td&gt;67&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2026-08-24&lt;/td&gt;
&lt;td&gt;427&lt;/td&gt;
&lt;td&gt;67&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Downloads up about 49% across those 15 days. Stars sat at 67 on every check.&lt;/p&gt;

&lt;p&gt;I read that as a property of how this kind of tool gets installed rather than as a problem. You add &lt;code&gt;brainstorm-mcp&lt;/code&gt; to an MCP client config and start calling &lt;code&gt;brainstorm_quick&lt;/code&gt;. Nothing in that path requires opening the repo. Starring costs a page visit plus a small public declaration of interest; downloading costs wanting to run the thing. For a mechanical utility with a narrow use case, I'd weight the download curve over the star count.&lt;/p&gt;

&lt;p&gt;Plenty of people will call that motivated reasoning from someone whose star count isn't moving, and four data points over two weeks isn't evidence of anything. It's four numbers. What it did change is what I bother logging.&lt;/p&gt;




&lt;p&gt;Pranab Sarkar, Independent Researcher&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>opensource</category>
      <category>llm</category>
    </item>
    <item>
      <title>The Model Reading My Benchmark Mattered More Than the Memory System Did</title>
      <dc:creator>Pranab Sarkar</dc:creator>
      <pubDate>Tue, 25 Aug 2026 20:40:17 +0000</pubDate>
      <link>https://dev.to/spranab/the-model-reading-my-benchmark-mattered-more-than-the-memory-system-did-44l4</link>
      <guid>https://dev.to/spranab/the-model-reading-my-benchmark-mattered-more-than-the-memory-system-did-44l4</guid>
      <description>&lt;p&gt;On 2026-08-11 I reran a benchmark I'd already published and changed exactly one thing: the model that reads the retrieved memory and writes the answer. Same memory system, same retrieved context, same judge, same rubric. YantrikDB's own score moved about 9 points on that swap.&lt;/p&gt;

&lt;p&gt;Grounding, if you haven't heard of the thing being measured: YantrikDB is a persistent memory system for AI agents — it stores what happened in earlier sessions and hands the relevant parts back on demand. It makes no LLM call at ingest, which is where the 241-second ingest and 80ms retrieval numbers further down come from.&lt;/p&gt;

&lt;p&gt;Two weeks before that rerun, Hindsight — a competing agent-memory system — had published a score on the same benchmark 25 points above mine. Nine of those 25 came from a model swap that has nothing to do with either memory architecture. More than a third of a published architectural gap, produced by changing the plumbing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comparison that got thrown out
&lt;/h2&gt;

&lt;p&gt;The first version was the lazy one. BEAM-100K, an agent-memory benchmark, rubric-scored: YantrikDB at 0.611, Hindsight's published number at 0.862. My answerer was deepseek-v4-flash, theirs was gemini-3.1-pro-preview, and I filed that as a footnote.&lt;/p&gt;

&lt;p&gt;Two adversarial reviews came back on it on 2026-08-11, one from a Codex-family model (gpt-5.6-sol) and one from Qwen (qwen3.8-max), both rejecting it for the same reason. An end-to-end agent-memory score has at least three things stacked inside it: how good retrieval is, whatever an ingest-time LLM does to the data on the way in, and how well the reading model uses what came back. YantrikDB and Hindsight differ in architecture. Hindsight runs an LLM fact-extraction pass at ingest and YantrikDB doesn't. The answerer models were different. Three variables moved together and I'd assigned all 25 points to the one I happen to build.&lt;/p&gt;

&lt;p&gt;My first reaction was that this was reviewer pedantry — the reader model is plumbing, the memory system is the interesting part. Then I ran the swap to prove it, got the 9 points, and spent the next two days rewriting the paper instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replaying their own context strings
&lt;/h2&gt;

&lt;p&gt;Hindsight publishes, per query, the literal context string it injects into its answerer — one for a RAG configuration, one for a single-query configuration. Not a description of what its retrieval does; the string itself. So I took those strings unmodified and ran them through YantrikDB's own fixed answerer and judge: deepseek-v4-flash:0731 at temperature 0, the identical pair already scoring YantrikDB's own retrieved contexts. Nothing here calls Hindsight's live system; it's their published artifacts replayed through my scoring pipeline, so the only thing varying between rows is which memory system wrote the context.&lt;/p&gt;

&lt;p&gt;Worth being blunt about one detail: the answerer and the judge here are the same model. deepseek-v4-flash:0731 writes the answer and then grades it, and self-grading has an obvious bias risk that I'm not going to dress up. What makes it usable is the frozen-context design — the same reader and the same judge process both systems' contexts under the same rubric, so whatever that model is inclined to reward, it rewards equally in both columns. The residual worry attaches to the absolute score level; the gap between the columns is what gets reported.&lt;/p&gt;

&lt;p&gt;400 queries nested inside 20 conversations, all from BEAM-100K. All 400 query_ids matched across systems, so there's no partial-overlap story to explain this away.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Binary accuracy&lt;/th&gt;
&lt;th&gt;Rubric score&lt;/th&gt;
&lt;th&gt;Context tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;YantrikDB&lt;/td&gt;
&lt;td&gt;RAG&lt;/td&gt;
&lt;td&gt;72.2%&lt;/td&gt;
&lt;td&gt;0.607&lt;/td&gt;
&lt;td&gt;13,673&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hindsight&lt;/td&gt;
&lt;td&gt;RAG&lt;/td&gt;
&lt;td&gt;71.5%&lt;/td&gt;
&lt;td&gt;0.592&lt;/td&gt;
&lt;td&gt;23,689&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hindsight&lt;/td&gt;
&lt;td&gt;single-query&lt;/td&gt;
&lt;td&gt;65.0%&lt;/td&gt;
&lt;td&gt;0.563&lt;/td&gt;
&lt;td&gt;17,655&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The significance test needs a note, because the obvious test is the wrong one. Those 400 queries aren't 400 independent draws — they sit inside 20 conversations, and queries from the same conversation tend to succeed and fail together, so a per-query t-test would count 400 samples where there are really 20. The test therefore works at conversation level: flip the sign of each conversation's mean difference, enumerate every possible assignment, which with 20 conversations is 2^20 of them, a bit over a million, and p comes out at 0.438. The interval is a separate procedure — 4,000 conversation-clustered bootstrap resamples, giving [-0.021, +0.052]. A +0.015 rubric difference with an interval that comfortably straddles zero is a tie, and the paper's own word for it is "equivalent". Against Hindsight's single-query configuration the difference is +0.044, p=0.025, CI [+0.009, +0.077] — real, but single-query is the weaker of their two published configurations, so leading with it would be picking the easy target.&lt;/p&gt;

&lt;p&gt;What did survive is cheaper than a win. YantrikDB reached that equivalent rubric score on 13,673 tokens of context against Hindsight's 23,689 — 42% fewer. The paper is careful here and I want to stay careful: part of that 42% is the cost of not carrying event time. Hindsight's contexts carry a median of 117 dates each. YantrikDB's carry a median of 5. "Not a free efficiency" is the paper's phrase, and it's the right call: that's a difference in what the two memory layers choose to emit. YantrikDB's two weakest categories in this eval are event ordering at 0.298 and temporal reasoning at 0.425. Those numbers and the token saving are one fact seen from two sides: a context with fewer dates in it is cheaper to read and worse at time.&lt;/p&gt;

&lt;p&gt;Ingest and retrieval move the same direction, for a design reason and not a tuning one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Ingest time&lt;/th&gt;
&lt;th&gt;Retrieval (mean)&lt;/th&gt;
&lt;th&gt;LLM call at ingest&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;YantrikDB&lt;/td&gt;
&lt;td&gt;241s&lt;/td&gt;
&lt;td&gt;80ms&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hindsight (RAG)&lt;/td&gt;
&lt;td&gt;404s&lt;/td&gt;
&lt;td&gt;2,565ms&lt;/td&gt;
&lt;td&gt;fact extraction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hindsight (single-query)&lt;/td&gt;
&lt;td&gt;404s&lt;/td&gt;
&lt;td&gt;6,379ms&lt;/td&gt;
&lt;td&gt;fact extraction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No LLM call at ingest is the choice; 80ms against 2,565ms is what the choice buys.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part worth stealing
&lt;/h2&gt;

&lt;p&gt;Back to the 9 points, the finding I'd defend hardest. Holding retrieval completely fixed and changing only the answerer moved YantrikDB's own score by more than a third of the entire published gap between two different memory systems. The paper puts it flatly: "The published gap is therefore overwhelmingly a property of the reading model, not of the memory systems." Any end-to-end "our agent memory beats X" number that doesn't name its answerer model and its judge model isn't a comparable number, and may be describing nothing more interesting than a stronger LLM reading the same context better. That's a complaint about how the category reports results, not about Hindsight, whose habit of publishing raw context strings is the only reason this experiment was possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  A different reader, a different answer
&lt;/h2&gt;

&lt;p&gt;There's a later experiment in the same paper, and leaving it out here would be editing rather than summarising. On 2026-08-15 I ran the rag-mode 2×2 again under an equalized frontier-class reader: moonshotai/kimi-k2.6 answering, meta-llama/llama-4-maverick judging — a different reader/judge pair from the deepseek one above. On 393 paired queries, YantrikDB scored 0.6331 rubric against Hindsight RAG's 0.5982: +3.5 percentage points, bootstrap 95% CI [+0.7, +6.2]pp, excluding zero.&lt;/p&gt;

&lt;p&gt;That doesn't overturn the tie, and I'd rather be tedious about it than let anyone read it that way. A different reader model is a different question rather than a rerun of the same one, and the RAG-vs-RAG comparison under the shared deepseek reader stays the honest headline: equivalent. What the frontier run supports is narrower — under one stronger reader, on a slightly smaller paired set, a small edge that clears its own confidence interval. It is not the sentence "YantrikDB beats Hindsight," and writing that sentence would be doing the exact thing the previous section complains about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrong twice, and what's still open
&lt;/h2&gt;

&lt;p&gt;The reader-model confound was the big retraction. There's a second one, smaller, the same shape. I'd had a hypothesis that Hindsight's ingest-time extraction step was quietly discarding evidence later questions needed — an information-bottleneck story, and the one result that would have made YantrikDB structurally better instead of just cheaper. Category by category against their single-query configuration, it looked real. Against their RAG configuration it mostly evaporated. The paper's line: "We are withdrawing the information-bottleneck claim as unsupported by this experiment." That paragraph took longer to write than the whole results section.&lt;/p&gt;

&lt;p&gt;Smaller things, in no order. The results page briefly said engine version 0.14.0; the version actually installed in the benchmark's virtualenv, per dist-info at 21:05 on 2026-08-11, was 0.13.4. That number turned out to matter slightly — a 2026-08-16 rerun of the same comparison on engine 0.15.0 scored 72.5% / 0.6375 where 0.13.4 scored 71.5% / 0.6107, a +0.027 rubric delta, bootstrap CI [+0.007, +0.045], small and clear of zero. The deepseek 2×2 is incomplete: YantrikDB's contexts were never run through Hindsight's answerer, so three of the four cells exist. Context length was never controlled either, so the token-efficiency and accuracy-equivalence findings stay two claims sitting next to each other rather than one. The local harness is a fork of vectorize-io/agent-memory-benchmark, which carries no license and can't be redistributed, so the paper's "Reproduce" link points at the upstream repo.&lt;/p&gt;

&lt;p&gt;The paper went up 2026-08-12 at yantrikdb.com/papers/beam-frozen-context/ and until this post it hadn't been anywhere but its own domain. Take the boring thing from it: when you publish agent-memory numbers, name the model that read the context and name the model that graded the answer. Without those two names the number doesn't mean much, including when the person publishing it is me.&lt;/p&gt;




&lt;p&gt;Pranab Sarkar, Independent Researcher&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>benchmarking</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Relicensing yantrikdb took 57 minutes and I got two things wrong</title>
      <dc:creator>Pranab Sarkar</dc:creator>
      <pubDate>Tue, 18 Aug 2026 23:56:18 +0000</pubDate>
      <link>https://dev.to/spranab/relicensing-yantrikdb-took-57-minutes-and-i-got-two-things-wrong-12ad</link>
      <guid>https://dev.to/spranab/relicensing-yantrikdb-took-57-minutes-and-i-got-two-things-wrong-12ad</guid>
      <description>&lt;p&gt;On 2026-08-18 I moved yantrikdb from AGPL-3.0 to Apache-2.0. The engine commit landed at 22:32:31 UTC. The server pull request merged at 23:29:28. Fifty-seven minutes end to end.&lt;/p&gt;

&lt;p&gt;Almost none of that was writing. It was finding the places where the old licence had been declared and changing the declaration.&lt;/p&gt;

&lt;p&gt;I got two of them wrong on the first pass. There is a third thing I still cannot account for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you touch a file
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'%an'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If more than one name comes back, the flip is not yours to make alone. Copyright in a contribution belongs to whoever wrote it, and relicensing the project relicenses their work too.&lt;/p&gt;

&lt;p&gt;The engine repo is 438 commits, all mine, across a few spellings of my own name. The server repo has exactly one commit that isn't mine: PR #11, RFC 006 Phase 5, claims wire opcodes plus a new &lt;code&gt;GET /v1/claims&lt;/code&gt;, +219/-5 across six files, merged 2026-04-18, from an outside contributor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why
&lt;/h2&gt;

&lt;p&gt;From my own commit message: "The moat is the ecosystem — engine, server, MCP, plugin, packs, web — not the licence."&lt;/p&gt;

&lt;p&gt;AGPL bought protection against someone standing up a hosted service on top of my work, and cost every commercial evaluation that ends at legal review. The adoption evidence says the cost is the larger number. A comparable competitor six weeks younger than yantrikdb sits at 2,608 stars against yantrikdb's 48, MIT-licensed and installable in one line. Stars are not a controlled experiment.&lt;/p&gt;

&lt;p&gt;Apache-2.0 rather than MIT because a patent has been filed on part of this work. Apache section 3 grants patent rights explicitly, and its retaliation clause terminates the licence of anyone who sues over patents. MIT is silent on patents, which leaves an implied-licence question that corporate reviewers have to resolve themselves. That is the same friction, relocated.&lt;/p&gt;

&lt;p&gt;Also from that commit: "This is irreversible: every version published under Apache-2.0 stays available under it permanently."&lt;/p&gt;

&lt;h2&gt;
  
  
  Eight files
&lt;/h2&gt;

&lt;p&gt;Engine commit 4411244 touched LICENSE, a new NOTICE, README.md, pyproject.toml, crates/yantrikdb-python/pyproject.toml, and the Cargo.toml in each of yantrikdb-core, yantrikdb-python and yantrikdb-wasm.&lt;/p&gt;

&lt;p&gt;Five manifests. Two toolchains, since both pyprojects use &lt;code&gt;build-backend = "maturin"&lt;/code&gt;. Each pyproject needed two edits rather than one, a &lt;code&gt;license&lt;/code&gt; field and a separate trove classifier, and nothing checks that the two agree.&lt;/p&gt;

&lt;p&gt;The README change was three lines. Two were the badge and the licence line. The third:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-The [MCP server](...) is MIT-licensed — using the engine via the MCP server does not trigger AGPL obligations on your code.
&lt;/span&gt;&lt;span class="gi"&gt;+The [MCP server](...) is MIT-licensed.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  letterpress
&lt;/h2&gt;

&lt;p&gt;Commit 4411244's message carved out one directory by name:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;NOT included: packs/letterpress, which is AGPL-3.0-or-later and references third-party photographs under licences that require attribution. Its terms are a separate decision and are not mine to flip mechanically.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Twenty-one minutes later, at 22:53:06, commit cea13b3 flipped it anyway. Two files, packs/letterpress/README.md and packs/letterpress/pyproject.toml. The reason I gave: the pack declared &lt;code&gt;license = { text = "AGPL-3.0-or-later" }&lt;/code&gt; in its own metadata while carrying no LICENSE file of its own, so it inherited the newly-Apache repo root and contradicted itself.&lt;/p&gt;

&lt;p&gt;That reasoning does not answer the reason I gave twenty-one minutes earlier. The photo attribution question is about the assets. The pyproject licence field is about the code. I collapsed the two together at 17:53 local time because the contradiction was the thing in front of me. I would keep them apart if I did it again.&lt;/p&gt;

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

&lt;p&gt;PR #94, sixteen files. The LICENSE diff was +202/-661, Apache's text being much shorter than AGPL's. LICENSE, NOTICE, README.md, DESIGN.md, SERVER_README.md, Cargo.lock, pyproject.toml, deny.toml, four crate Cargo.tomls, four crate READMEs.&lt;/p&gt;

&lt;p&gt;The root pyproject.toml still said &lt;code&gt;AGPL-3.0-only&lt;/code&gt; after the first commit at 22:45:23. I caught it 36 seconds later and fixed it in SHA 96053e10. Live package metadata that would have published an AGPL classifier sitting under an Apache LICENSE file.&lt;/p&gt;

&lt;p&gt;I opened it as a PR, and said so in the body, "so the required status checks run". Seven jobs ran: format, clippy, supply-chain, chaos-gate, and build-and-test on ubuntu, macos and windows. All seven passed on the second run. Then I went and checked: &lt;code&gt;GET /repos/yantrikos/yantrikdb-server/branches/main/protection&lt;/code&gt; returns 404, "Branch not protected". The repo's only ruleset is called "default", with &lt;code&gt;enforcement: disabled&lt;/code&gt;. Zero required checks. Nothing would have stopped a red merge. I open PRs out of habit, and the habit is what caught the next problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  cargo-deny
&lt;/h2&gt;

&lt;p&gt;The allow list in deny.toml had this entry, and I deleted it:&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;AGPL-3.0-only&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;# our own license
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The comment above the list went from "Our own code is AGPL-3.0." to "Our own code is Apache-2.0."&lt;/p&gt;

&lt;p&gt;CI run 32194878789 started at 22:54:53 on SHA 96053e10. The supply-chain job runs &lt;code&gt;cargo deny check licenses bans sources&lt;/code&gt;. Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error[rejected]: failed to satisfy license requirements
  ┌─ /home/runner/.cargo/git/checkouts/yantrikdb-1317bbf12f5451d8/42dad7f/crates/yantrikdb-core/Cargo.toml:5:12
  │
5 │ license = "AGPL-3.0-only"
  │            ━━━━━━━━━━━━━
  │            │
  │            rejected: license is not explicitly allowed
  │
  ├ AGPL-3.0-only - GNU Affero General Public License v3.0 only:
  ├   - OSI approved
  ├   - FSF Free/Libre
  ├   - Copyleft
  ├ yantrikdb v0.15.3
    └── yantrikdb-server v0.16.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;bans ok, licenses FAILED, sources ok&lt;/code&gt;, and &lt;code&gt;Process completed with exit code 4&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Read the file path in that error. &lt;code&gt;.cargo/git/checkouts/&lt;/code&gt;. The server does not pull the engine from a registry at all. Line 35 of crates/yantrikdb-server/Cargo.toml, on the failing SHA, was:&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;yantrikdb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.15.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;git&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://github.com/yantrikos/yantrikdb.git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;tag&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"v0.15.3"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;42dad7f is the commit that tag v0.15.3 pointed at, the last one before the relicense. cargo-deny read the licence straight out of that git checkout. crates.io was never in the server's dependency path.&lt;/p&gt;

&lt;p&gt;The unblock was a tag, not a config edit. Engine commit 473b7ae at 23:03:14, "release: v0.15.4 — Apache-2.0 relicense + embedder-identity guard", carries tag v0.15.4. Then line 35 moved:&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;yantrikdb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.15.4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;git&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://github.com/yantrikos/yantrikdb.git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;tag&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"v0.15.4"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That landed as server SHA 2d770017 at 23:13:12, and CI run 32196261107 started 28 seconds after it. A new run on a new head SHA, not a re-run of the failed one.&lt;/p&gt;

&lt;p&gt;There was another way. cargo-deny supports &lt;code&gt;[licenses.exceptions]&lt;/code&gt;, and I could have scoped an AGPL exception to our own crate and left the allow list intact. I didn't. An exception would have held the gate green while the tree was genuinely mixed, and telling me the tree is mixed is the whole job of the gate. Deleting the entry made the build fail for a true reason.&lt;/p&gt;

&lt;p&gt;You can disagree with that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the registries know
&lt;/h2&gt;

&lt;p&gt;None of that involved crates.io, but crates.io matters for a separate reason: a registry only learns your licence changed when you cut a release. crates.io still serves yantrikdb 0.15.3 as AGPL-3.0-only, published 2026-08-18T05:04:17Z. 0.15.4 at 23:11:19Z is the first Apache-2.0 one. Every version before it stays AGPL on crates.io permanently, for everyone who pins one.&lt;/p&gt;

&lt;p&gt;PyPI got there first, by about a minute. Wheels uploaded 23:10:13 through 23:10:25, carrying the classifier &lt;code&gt;License :: OSI Approved :: Apache Software License&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where else the licence lived
&lt;/h2&gt;

&lt;p&gt;yantrikdb-mcp's README has a competitor comparison table. The License row read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;| License | MIT (engine AGPL-3.0) | Apache 2.0 | Apache 2.0 | Apache 2.0 | MIT |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three of the four alternatives Apache, one MIT, our own cell qualifying itself. A shorter table higher up the same file had &lt;code&gt;| **License** | MIT (engine: AGPL-3.0) |&lt;/code&gt;. CONTRIBUTING.md there has a License section naming the engine's licence too.&lt;/p&gt;

&lt;p&gt;Then the GitHub repo description, which is a string in GitHub's database that never touches git. The engine's already said Apache-2.0. The server's still read "Rust, AGPL, ships as library / MCP server / HTTP cluster" after PR #94 merged. I found it while writing this, in the same hour, and fixed it with &lt;code&gt;gh repo edit yantrikos/yantrikdb-server --description "..."&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One concrete unblock: yantrikdb-mcp was being held back from the Docker MCP Registry, which takes permissive licences only. It is eligible now.&lt;/p&gt;

&lt;p&gt;The engine repo had 48 stars at the time of the commit and has 50 now. There is no data yet on whether any of this worked and there won't be for a while.&lt;/p&gt;

&lt;h2&gt;
  
  
  The linter
&lt;/h2&gt;

&lt;p&gt;scripts/consistency_check.py is 108 lines and runs twice a day. Its file mtime is 23:06Z on 2026-08-18, which is to say I wrote it in the middle of all this. Its docstring lists the motivating examples: "yantrikdb-mcp's comparison table said AGPL after the Apache relicense", and "PyPI classifiers contradicting the repo LICENSE".&lt;/p&gt;

&lt;p&gt;It queries PyPI and npm. There is no crates.io code path in it, so it could not catch a crates.io licence mismatch. For repo descriptions it checks that one exists, not what it says. Both holes are mine and I know they are there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one I left alone
&lt;/h2&gt;

&lt;p&gt;docs/papers/ still says AGPL. Section 13.3 of skill-substrate-experience-report-v4.md reads, in full:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;YantrikDB is AGPL-3.0. Non-trivial barrier for proprietary commercial adoption; appropriate for research-and-substrate positioning. Dual licensing on inquiry. I do not present AGPL as an unambiguous advantage.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That text exists at two paths, one a Zenodo bundle. It is dated and published. Rewriting it to match tonight would make it wrong about the day it carries. The repo keeps a CORRECTIONS.md, and that is where this goes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lockfile
&lt;/h2&gt;

&lt;p&gt;Here is the part I have not worked out. The Cargo.lock sitting on the server's main branch right now records:&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;"yantrikdb"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.15.3"&lt;/span&gt;
&lt;span class="py"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"git+https://github.com/yantrikos/yantrikdb.git?tag&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;v&lt;/span&gt;&lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="c"&gt;#42dad7f19c0b7b43b54e1af1c5c492a5c4ad10ef"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tag v0.15.4 points at 473b7ae. 42dad7f is the pre-relicense commit. The lockfile names the new tag against the old commit and still calls it 0.15.3. That same lockfile was on the SHA where all seven checks went green, cargo-deny included. I noticed it while writing this and I have not chased it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Pranab Sarkar, Independent Researcher. Building yantrikdb, saga-mcp, brainstorm-mcp, and icantmarket.com.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>licensing</category>
      <category>rust</category>
      <category>devops</category>
    </item>
    <item>
      <title>My Agent Runs My Marketing (I Mostly Just Hit Approve)</title>
      <dc:creator>Pranab Sarkar</dc:creator>
      <pubDate>Thu, 13 Aug 2026 15:08:13 +0000</pubDate>
      <link>https://dev.to/spranab/my-agent-runs-my-marketing-i-mostly-just-hit-approve-4d81</link>
      <guid>https://dev.to/spranab/my-agent-runs-my-marketing-i-mostly-just-hit-approve-4d81</guid>
      <description>&lt;p&gt;I have 16 open-source projects, 15,386 downloads per month across PyPI and npm, and no ability to make myself post on forums consistently. I've known this about myself for years. So on 2026-08-09, I built a system to do the parts I won't do.&lt;/p&gt;

&lt;p&gt;Here's what actually happened in the first four days.&lt;/p&gt;




&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The system lives in a folder called &lt;code&gt;c:\Users\sync\codes\marketing&lt;/code&gt;. A &lt;code&gt;PLAYBOOK.md&lt;/code&gt; holds the voice rules and guardrails. A &lt;code&gt;routines/&lt;/code&gt; folder has markdown files that each describe one agent prompt. A &lt;code&gt;state/&lt;/code&gt; folder of JSON and markdown files coordinates between routines. And a Python stdlib server at &lt;code&gt;localhost:8781&lt;/code&gt; handles the one thing that still needs a human.&lt;/p&gt;

&lt;p&gt;The routines run on cron via Claude Code headless. &lt;code&gt;weekly-metrics&lt;/code&gt; on Mondays. &lt;code&gt;registry-sweep&lt;/code&gt;, &lt;code&gt;repo-seo&lt;/code&gt;, and &lt;code&gt;content-writer&lt;/code&gt; weekly. &lt;code&gt;opportunity-scout&lt;/code&gt; daily (bumped from twice a week on launch day because Hacker News threads go cold in hours). &lt;code&gt;issue-responder&lt;/code&gt; at 7:26am and 7:26pm.&lt;/p&gt;

&lt;p&gt;The important architectural decision is the tier split. Tier 1 is fully autonomous: MCP registries, directory listings, repo SEO, release notes, dev.to articles. The system writes, reviews through a multi-stage pipeline, and publishes without asking me. The article you're reading went through that pipeline.&lt;/p&gt;

&lt;p&gt;Tier 2 is draft-only, always. Hacker News, Reddit, X. The system writes ready-to-paste drafts, queues them in &lt;code&gt;state/drafts/&lt;/code&gt;, and stops. The portal (&lt;code&gt;scripts/portal.py&lt;/code&gt;) parses each draft file by splitting on a &lt;code&gt;---&lt;/code&gt; separator -- metadata above, paste-ready body below -- extracts a "Reply to:" URL via regex, and serves it at &lt;code&gt;localhost:8781&lt;/code&gt; with Copy, Open, and Archive buttons. Archiving moves the file to &lt;code&gt;state/drafts/archive/&lt;/code&gt;. That's the whole approval flow. It takes about 10 seconds.&lt;/p&gt;

&lt;p&gt;The reason Tier 2 exists is written into the system's own docs: "Those platforms shadowban automated self-promotion and the reputational downside is permanent." The PLAYBOOK.md voice rules reinforce it from the other direction: "Never fake community enthusiasm, never astroturf, never post the same text to two places." I'm not planning to graduate Tier 2 channels to full autonomy. The communities that make HN and Reddit worth posting to are the ones that will correctly identify and punish automated engagement. That 10-second friction is doing real work.&lt;/p&gt;

&lt;p&gt;There's also an owned channel -- bothn.com, my platform for agents. The autopilot has a disclosed agent account (&lt;code&gt;yantrikos-autopilot&lt;/code&gt;). It posts as itself, not as me.&lt;/p&gt;




&lt;h2&gt;
  
  
  The first registry sweep
&lt;/h2&gt;

&lt;p&gt;The first sweep ran on 2026-08-09 and hit a mix of successes and walls.&lt;/p&gt;

&lt;p&gt;It opened PRs on &lt;code&gt;punkpeye/awesome-mcp-servers&lt;/code&gt; (#11771) and &lt;code&gt;tolkonepiu/best-of-mcp-servers&lt;/code&gt; (#349 and #350). It submitted yantrikdb-mcp and saga-mcp to mcp.directory via web form, with screenshot receipts saved to &lt;code&gt;state/logs/&lt;/code&gt;. It found mcpserverfinder.com, which only accepts submissions by email, and sent one from &lt;a href="mailto:yantrikdb@gmail.com"&gt;yantrikdb@gmail.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Then the dead ends.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;appcypher/awesome-mcp-servers&lt;/code&gt; -- archived and read-only. The target list had stale data; the repo hasn't accepted PRs in months. A branch was prepared, then deleted unopened.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mcphunt.com&lt;/code&gt; -- parked domain, for sale. Not a live directory.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;wong2/awesome-mcp-servers&lt;/code&gt; -- the README states: "We do not accept PRs. Please submit your MCP on the website: &lt;a href="https://mcpservers.org/submit" rel="noopener noreferrer"&gt;https://mcpservers.org/submit&lt;/a&gt;" The automation checked the README before opening anything and stopped.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;smithery.ai&lt;/code&gt; -- needs my GitHub login, and the browser automation profile's session was dead. The system logged "NO SESSION" and fell back to drafts.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docker/mcp-registry&lt;/code&gt; -- requires my GitHub identity per the run instructions. Skipped entirely; no session check attempted, just a note in submissions.json and it moved on.&lt;/p&gt;

&lt;p&gt;A safety rail in PLAYBOOK.md caps outbound PRs: "Max 3 outbound PRs to third-party repos per sweep (avoid looking like a bot wave)." The sweep opened 3 PRs (two to tolkonepiu, one to punkpeye) and stopped there.&lt;/p&gt;

&lt;p&gt;The autopilot also posted a summary of this run to bothn.com (post #99, from &lt;code&gt;yantrikos-autopilot&lt;/code&gt;). It reported the actual results: 8 answered issues, what it counted as 4 listing submissions, one new directory discovered and submitted to. Then it asked other agents where they'd found agent-native distribution channels.&lt;/p&gt;




&lt;h2&gt;
  
  
  What broke
&lt;/h2&gt;

&lt;p&gt;The day after launch, the dashboard showed portfolio PyPI downloads had dropped from 14,389/month to 130. Overnight. That's the number in &lt;code&gt;state/metrics-history.jsonl&lt;/code&gt;, line 2 -- it's still there.&lt;/p&gt;

&lt;p&gt;Here's what &lt;code&gt;scripts/collect_metrics.py&lt;/code&gt; actually does: it loops over &lt;code&gt;portfolio.json&lt;/code&gt; projects, calling &lt;code&gt;pypistats.org/api/packages/{package}/recent&lt;/code&gt; and &lt;code&gt;api.npmjs.org/downloads/point/last-month/{package}&lt;/code&gt; for each one. When a call fails, the handler runs &lt;code&gt;print(f"  warn: {url} -&amp;gt; {e}", file=sys.stderr)&lt;/code&gt; and returns None. The package is silently excluded from the sum. No retry. No alert. The totals line just sums whatever succeeded.&lt;/p&gt;

&lt;p&gt;So on 2026-08-10, most PyPI calls apparently failed -- probably a pypistats.org outage, possibly rate limiting -- and the dashboard reported 130 downloads/month because that's what the one successful call returned (yantrikdb-client, 130 downloads). The error went to stderr and nowhere else. I didn't notice for twenty minutes, and even then only because the number looked absurd on the dashboard. A more insidious failure -- say, 30% of packages failing instead of 95% -- would have produced a plausible-looking number and I wouldn't have caught it at all.&lt;/p&gt;

&lt;p&gt;The obvious fix: compare against the previous snapshot's total, flag anything that drops more than 40% as suspicious before recording it. I haven't written that yet.&lt;/p&gt;

&lt;p&gt;The Bun runtime also hard-crashed twice -- during the opportunity-scout on 2026-08-11 at 18:13, and during the issue-responder this morning at 07:26. Bun v1.3.10, Windows x64, identical panic both times: &lt;code&gt;panic(main thread): switch on corrupt value&lt;/code&gt; / "Bun has crashed. This indicates a bug in Bun, not your code." Different routines, different days. Haven't filed the bug report yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  Baseline numbers
&lt;/h2&gt;

&lt;p&gt;As of 2026-08-09: 15,386 downloads/month (14,389 PyPI + 997 npm), 483 GitHub stars across roughly 16 tracked projects. yantrikdb-hermes-plugin pulls 1,831 downloads/month -- second-biggest PyPI package in the portfolio -- without a single announcement. It's a pluggable memory provider inside NousResearch/hermes-agent, and people find it there. No launch post, no marketing. That data point is why the next phase focuses on embedding into frameworks rather than posting into voids.&lt;/p&gt;

&lt;p&gt;90-day targets (set 2026-08-09, target 2026-11-07): downloads to 31,000/month, stars to 800, live directory listings from 9 to 16+, and 12 dev.to articles published. This is article #1 -- the &lt;code&gt;content-log.json&lt;/code&gt; articles array was empty before this run.&lt;/p&gt;

&lt;p&gt;The longer-term executor is meant to be yantrik-mind, my own agent stack running on a home server, not Claude Code headless. Migration is one routine at a time, risk order, once shadow runs match production for four consecutive weeks. No routine has graduated yet. The shadow metrics run fires Mondays at 08:20 and its diffs land in &lt;code&gt;state/shadow-diff.md&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;This system is what icantmarket.com is meant to be for other technical founders who built something real and can't make themselves promote it. Full automation where the platforms accept it. A 10-second interface where they don't.&lt;/p&gt;

&lt;p&gt;I built the system on its own portfolio first because I didn't want to sell it before I knew where it breaks. Now I know some of the places.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Pranab Sarkar, Independent Researcher. Building yantrikdb, saga-mcp, brainstorm-mcp, and icantmarket.com.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>automation</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>I Gave Claude Code a "Phone a Friend" Lifeline — It Calls GPT, Gemini, and DeepSeek</title>
      <dc:creator>Pranab Sarkar</dc:creator>
      <pubDate>Thu, 05 Mar 2026 21:05:56 +0000</pubDate>
      <link>https://dev.to/spranab/i-gave-claude-code-a-phone-a-friend-lifeline-it-calls-gpt-gemini-and-deepseek-3gjo</link>
      <guid>https://dev.to/spranab/i-gave-claude-code-a-phone-a-friend-lifeline-it-calls-gpt-gemini-and-deepseek-3gjo</guid>
      <description>&lt;p&gt;Last week Claude Code was stuck on an architecture decision. Instead of going back and forth with one model, I thought — what if it could phone a friend? Or three?&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/spranab/brainstorm-mcp" rel="noopener noreferrer"&gt;brainstorm-mcp&lt;/a&gt;, an MCP server that gives your coding agent a brainstorming team. Claude poses the question, GPT-5, Gemini, DeepSeek, and others each bring their perspective, then they build on each other's ideas across multiple rounds. A synthesizer distills everything into a consolidated recommendation.&lt;/p&gt;

&lt;p&gt;It's not about picking a winner — it's about getting perspectives you'd never get from a single model.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;You give it a topic: &lt;em&gt;"Design the architecture for a next-gen AI-powered code review tool"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;All models respond independently in Round 1 — diverse, unbiased first takes&lt;/li&gt;
&lt;li&gt;In Round 2, they see each other's ideas and build on them, challenge assumptions, or offer alternatives&lt;/li&gt;
&lt;li&gt;A synthesizer distills the best ideas into a final recommendation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Claude participates as a brainstormer with full context of your codebase, so the discussion is grounded in your actual code — not abstract advice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real example: AI code review architecture
&lt;/h2&gt;

&lt;p&gt;I asked three models to design an AI-powered code review tool that goes beyond linting. Here's what each one brought to the table:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GPT-5.2&lt;/strong&gt; went deep on systems architecture — a full Temporal-orchestrated pipeline with a Repo Knowledge Graph, multi-pass LLM review (intent inference → local correctness → architectural reasoning → team alignment), and a feedback loop that learns from accepted/rejected suggestions. Detailed tech stack: Tree-sitter for parsing, Neo4j for the graph layer, OPA for policy-as-code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DeepSeek&lt;/strong&gt; focused on the data flow and learning loop — how the system should ingest PRs, build context packs from the dependency neighborhood, and gradually learn team conventions. Simpler architecture, but pragmatic choices about what to build first vs. defer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude&lt;/strong&gt; (with codebase context from my actual project) grounded the discussion — pointed out which components I already had, where existing CI pipelines could be reused, and which parts of the architecture were overkill for my scale.&lt;/p&gt;

&lt;p&gt;The synthesis combined GPT's comprehensive systems design with DeepSeek's pragmatic sequencing and Claude's "here's what you actually need right now" reality check. &lt;strong&gt;2 rounds, 75 seconds, ~11k tokens, $0.07.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.github.com/spranab/c1770d0bfdff409c33cc9f98504318e3" rel="noopener noreferrer"&gt;Full debate output&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What surprised me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Each model has a genuine personality.&lt;/strong&gt; GPT goes wide and comprehensive — you'll get a 6-section architecture doc. DeepSeek favors elegant simplicity. Claude with codebase context catches things the others miss entirely. The value isn't in any single response — it's in the combination.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Round 2 is where the magic happens.&lt;/strong&gt; Round 1 gives you breadth — independent first takes. But Round 2, where models first see each other's thinking and decide what to build on or challenge, is where the most creative solutions emerge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cheaper than you'd think.&lt;/strong&gt; A full debate costs $0.02-0.07. Less than a single complex prompt to a reasoning model, but you get 3x the perspectives refined over multiple rounds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local models add real value.&lt;/strong&gt; Running Llama via Ollama alongside cloud models adds diversity. They think differently — less polished but more willing to suggest unconventional approaches.&lt;/p&gt;

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

&lt;p&gt;It's an MCP server, so if you use Claude Code, Claude Desktop, or any MCP client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx brainstorm-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add your API keys and you're done. Works with OpenAI, Gemini, DeepSeek, Groq, Ollama, Mistral — anything with an OpenAI-compatible API.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more thing
&lt;/h2&gt;

&lt;p&gt;While building brainstorm-mcp, I kept losing track of decisions and tasks across sessions. Claude would forget what we'd discussed yesterday. So I built &lt;a href="https://github.com/spranab/saga-mcp" rel="noopener noreferrer"&gt;saga-mcp&lt;/a&gt; — a Jira-like project tracker that runs as an MCP server. SQLite-backed, 31 tools, full hierarchy from Projects down to Subtasks. One &lt;code&gt;tracker_dashboard&lt;/code&gt; call gives your AI agent full context to pick up where it left off.&lt;/p&gt;

&lt;p&gt;Both are free, open source, MIT licensed.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;brainstorm-mcp:&lt;/strong&gt; &lt;a href="https://github.com/spranab/brainstorm-mcp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | &lt;a href="https://www.npmjs.com/package/brainstorm-mcp" rel="noopener noreferrer"&gt;npm&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;saga-mcp:&lt;/strong&gt; &lt;a href="https://github.com/spranab/saga-mcp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | &lt;a href="https://www.npmjs.com/package/saga-mcp" rel="noopener noreferrer"&gt;npm&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>mcp</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why AI Agents Keep Forgetting Your Project (And How I Fixed It)</title>
      <dc:creator>Pranab Sarkar</dc:creator>
      <pubDate>Mon, 23 Feb 2026 00:36:47 +0000</pubDate>
      <link>https://dev.to/spranab/why-ai-agents-keep-forgetting-your-project-and-how-i-fixed-it-2j4d</link>
      <guid>https://dev.to/spranab/why-ai-agents-keep-forgetting-your-project-and-how-i-fixed-it-2j4d</guid>
      <description>&lt;p&gt;Every time I start a new session with an AI coding agent, the same thing happens: it has no idea what I was working on yesterday.&lt;/p&gt;

&lt;p&gt;It doesn't know which tasks are done. It doesn't know we decided to use Redis for caching. It doesn't remember that the auth module is blocked waiting on a dependency upgrade. It just... starts fresh.&lt;/p&gt;

&lt;p&gt;So I'd do what most people do — maintain a &lt;code&gt;PROGRESS.md&lt;/code&gt; file. After every session, I'd ask the agent to update it. And at the start of the next session, the agent would read the file and try to pick up where it left off.&lt;/p&gt;

&lt;p&gt;This worked fine for about a week.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Markdown Problem
&lt;/h2&gt;

&lt;p&gt;As the project grew, so did the file. 50 lines became 200. Status updates from three weeks ago sat next to current blockers. The agent would read the whole thing, burn 3,000+ tokens on context, and still miss that one task I'd marked as blocked because it was buried between two old progress notes.&lt;/p&gt;

&lt;p&gt;The fundamental issue: &lt;strong&gt;I was using a text file as a database.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I needed queries ("what's blocked?"), not full-file reads. I needed structure (projects → epics → tasks), not flat bullet points. And I needed an audit trail that wouldn't bloat the context window.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building a Project Tracker for AI Agents
&lt;/h2&gt;

&lt;p&gt;So I built one. &lt;strong&gt;Saga&lt;/strong&gt; is an MCP server that gives AI agents a local SQLite database for project tracking — think Jira, but designed for the way agents actually work.&lt;/p&gt;

&lt;p&gt;If you're not familiar with MCP (Model Context Protocol) — it's a standard from Anthropic that lets AI tools talk to external services through typed tool calls. Claude Code, Claude Desktop, Cursor, and Windsurf all support it. Your agent discovers available tools at startup and calls them as needed during conversation.&lt;/p&gt;

&lt;p&gt;Saga exposes 23 tools through MCP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CRUD for a full hierarchy:&lt;/strong&gt; Projects → Epics → Tasks → Subtasks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A notes system:&lt;/strong&gt; For decisions, context, meeting notes, blockers — all typed and searchable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A dashboard:&lt;/strong&gt; One call returns your entire project status — completion percentages, blocked tasks, recent activity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An activity log:&lt;/strong&gt; Every change is automatically recorded with old/new values&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A session diff:&lt;/strong&gt; "Show me what changed since yesterday" — in one call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything lives in a single &lt;code&gt;.tracker.db&lt;/code&gt; SQLite file. No servers, no API keys, no accounts.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Actually Works
&lt;/h2&gt;

&lt;p&gt;Here's what a typical session looks like now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Starting a new project:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Me: "Set up tracking for the e-commerce API"

Agent calls: tracker_init → epic_create (Auth) → epic_create (Catalog)
           → task_create (JWT auth) → subtask_create ([setup lib, create endpoint, add middleware])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five tool calls. Project is structured with epics, tasks, and subtasks. All persisted to SQLite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resuming the next day:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Me: "What's the status?"

Agent calls: tracker_dashboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One call. Returns:&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;"stats"&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;"total_tasks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tasks_done"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"tasks_blocked"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"completion_pct"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;33.3&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;"blocked_tasks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Add rate limiting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"epic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Authentication"&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;"recent_activity"&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;"Task 'JWT auth' status: in_progress → done"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent immediately knows: 33% done, one task is blocked, auth epic is ahead of catalog. It can prioritize without me having to explain anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recording a decision:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Me: "We're going with Redis for caching. Mark the research tasks as done."

Agent calls: note_save (decision: Redis for caching, reasons, trade-offs)
           → task_batch_update (mark tasks 8, 9 as done)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The decision is stored as a typed note, linked to the relevant epic. Next session, if the agent needs to understand why we chose Redis, it can search for it instead of me re-explaining.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Token Math
&lt;/h2&gt;

&lt;p&gt;This is the part that surprised me.&lt;/p&gt;

&lt;p&gt;Saga's 23 tool definitions cost about &lt;strong&gt;1,500 tokens&lt;/strong&gt; in the system prompt. That's fixed — it doesn't grow with your project.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;tracker_dashboard&lt;/code&gt; call returns ~800 tokens of structured data. A filtered query like "show me blocked tasks" returns ~200 tokens.&lt;/p&gt;

&lt;p&gt;Compare that to a &lt;code&gt;PROGRESS.md&lt;/code&gt; file for a medium project: 3,000–5,000 tokens, loaded in full every session, growing over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The crossover happens at about 15–20 tasks.&lt;/strong&gt; Beyond that, the structured approach scales better because the agent only retrieves what it asks for, not everything.&lt;/p&gt;

&lt;p&gt;And unlike a markdown file, the data is queryable. "What did we decide about caching?" is a &lt;code&gt;note_search&lt;/code&gt; call, not a full-file scan.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Under the Hood
&lt;/h2&gt;

&lt;p&gt;For the technically curious:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQLite with WAL mode&lt;/strong&gt; — concurrent reads during writes, busy timeout of 5 seconds for lock contention&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Foreign keys enforced&lt;/strong&gt; — no orphaned tasks when you delete an epic (cascading deletes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Append-only activity log&lt;/strong&gt; — every create, update, and delete is recorded with field-level granularity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parameterized queries with column allowlists&lt;/strong&gt; — no SQL injection surface&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP safety annotations on every tool&lt;/strong&gt; — clients know which tools are read-only, destructive, or idempotent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole thing is ~1,400 lines of TypeScript. Two dependencies: the MCP SDK and better-sqlite3.&lt;/p&gt;




&lt;h2&gt;
  
  
  Session Diff: The Feature I Didn't Plan
&lt;/h2&gt;

&lt;p&gt;After launching, someone on Reddit asked: &lt;em&gt;"Can it show what changed between sessions?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Good idea. The activity log already captured everything — it just needed an aggregation layer.&lt;/p&gt;

&lt;p&gt;So I added &lt;code&gt;tracker_session_diff&lt;/code&gt;. You give it a timestamp, and it returns:&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;"total_changes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&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;"created"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status_changed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"updated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"deleted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&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;"highlights"&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="s2"&gt;"Task 'Fix auth bug' status: in_progress → done"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Created epic 'API v2'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Note 'Sprint retro' deleted"&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;An agent calling this at the start of a session gets a structured changelog of everything that happened since it last checked. No parsing markdown diffs. No re-reading files.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Add this to your project's &lt;code&gt;.mcp.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"saga"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"saga-mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"DB_PATH"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/absolute/path/to/your/project/.tracker.db"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Works with Claude Code, Claude Desktop, or any MCP-compatible client. The database file is created automatically on first use.&lt;/p&gt;




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

&lt;p&gt;Building Saga taught me something about how agents actually consume information: &lt;strong&gt;they're better with structure than with prose.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A markdown file is optimized for humans scanning a document. A typed tool call returning filtered JSON is optimized for an LLM deciding what to do next. The agent doesn't need to "read" your project status — it needs to &lt;em&gt;query&lt;/em&gt; it.&lt;/p&gt;

&lt;p&gt;MCP makes this practical. The protocol handles tool discovery, typed schemas, and transport. All I had to do was put a database behind it.&lt;/p&gt;

&lt;p&gt;If you're building multi-session agent workflows and finding yourself maintaining growing context files, consider whether that context should be a database instead.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Saga is open-source (MIT) and available on npm:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/spranab/saga-mcp" rel="noopener noreferrer"&gt;https://github.com/spranab/saga-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Install: &lt;code&gt;npx -y saga-mcp&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>opensource</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
