<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://aakashh242.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://aakashh242.github.io/" rel="alternate" type="text/html" /><updated>2026-07-23T19:59:23+00:00</updated><id>https://aakashh242.github.io/feed.xml</id><title type="html">Aakash’s Blog</title><subtitle>Personal blog</subtitle><author><name>Aakash</name></author><entry><title type="html">A Surprising Observation</title><link href="https://aakashh242.github.io/2026/07/14/surprising-observation.html" rel="alternate" type="text/html" title="A Surprising Observation" /><published>2026-07-14T00:00:00+00:00</published><updated>2026-07-14T00:00:00+00:00</updated><id>https://aakashh242.github.io/2026/07/14/surprising-observation</id><content type="html" xml:base="https://aakashh242.github.io/2026/07/14/surprising-observation.html"><![CDATA[<blockquote>
  <p><strong>TL;DR:</strong> We expected sending the full prompt to win every time. It did not. On our controlled benchmark, Trimwise Hybrid at 512 tokens matched the full-prompt answer-pass baseline on GPT-5.4 Mini and GPT-5.6 Luna, while beating it slightly on Nano. Trimwise Lexical got very close too, but at roughly 8 ms
instead of Hybrid’s ~52 ms. The bigger finding was not just score: token-level compressors can leave behind broken Markdown, clipped identifiers, damaged JSON, and context fragments that look relevant but are no longer safe to use. Smaller context is useful only
when it is still intact, traceable, and actually usable.</p>
</blockquote>

<p>At <a href="https://github.com/tenwritehq">Tenwrite</a>, we are hard at work writing an internal agent that manages our SEO footprint
by creating and managing content. Naturally, for such a system to be able to learn constantly and stay updated with changing
SEO trends, it will have a lot of blogs to analyze. Now, we just cannot shove full blog content into every prompt. It works
for smaller blogs but, for larger ones, they result in excessive token usage and longer run times. We initially tried
the cursed <code class="language-plaintext highlighter-rouge">first N truncation</code> but the results came back to haunt us so bad we had to revert and think of other solutions.</p>

<p>Our problem was simple - too many huge blogs to process so, condense them into a token budget each with minimal reduction of information
density to hurt performance. The usual methods that we analyzed were LLMLingua, LongLLMLingua and RECOMP. Although their
main use-case is context compression, they still allow query-aware compression. But a few trials revealed a deeper problem - 
these methods remove a lot of tokens which affect information density quite significantly. This was especially visible when
the source texts had institutional knowledge the models had not trained on.</p>

<p>So, we did what any team would do after seeing this mess - we went digging.</p>

<p>The first thing we realized was that “context compression” is a very overloaded phrase. In one place, it means “make this prompt shorter”. In another place, it means “extract the useful passages”. In another place, it effectively
means “delete tokens until the model is okay with the input length”. These are very different things, even though all of them get marketed under roughly the same name.</p>

<p>For our case, we did not want a summary. A summary is useful when a human asks, “what is this blog about?”. But our agent may ask something far more specific later: what was the old recommendation for canonical tags in this particular
article? What example did the author give for internal linking? Was a caveat mentioned near the end? Did the blog say something that contradicts a newer internal document? These are not summary questions. These are retrieval and
evidence questions.</p>

<p>And this is where the usual “compress aggressively and hope” approach becomes dangerous.</p>

<p>A lot of blog content is repetitive. Great, remove repetition. A lot of it is filler. Great, remove filler. But the same blog can have one weird paragraph containing a piece of institutional knowledge, a client-specific exception, an
experiment result, or an old SEO decision that no foundation model has ever seen. Remove that one paragraph and the output can still look very clean, very short, and very wrong. This is the annoying part. The failure does not always
look like a failure. Sometimes the context is still grammatically fine. The agent simply loses the only thing that mattered.</p>

<p>We initially tried the usual family of solutions: LLMLingua, LongLLMLingua, RECOMP, and a few simpler baselines. We also had the classic first N truncation baseline because, well, everyone has to make that mistake at least once before
moving on with their life.</p>

<p>first N is not ideal for obvious reasons. It works suspiciously well when the answer happens to be near the top, which makes you think your benchmark is fine. Then the useful thing is near the end, or split between the beginning and the
middle, and suddenly your agent starts confidently answering a question with half the evidence missing. It is not really a compression strategy. It is a positional assumption pretending to be one.</p>

<p>The existing compressors were more interesting, but they came with a different class of problem.</p>

<p>They can be very aggressive at token removal. On normal prose, that can look okay at first. But on real working material - headings, Markdown, lists, code snippets, JSON, identifiers, links, technical instructions, weird internal
terminology - token-level removal can damage the actual shape of the source. We saw LLMLingua-family outputs where headings became glued fragments, identifiers got clipped, Markdown fences got damaged, and JSON started looking like
punctuation soup. Not “the model missed a sentence” bad. More like “the context is now technically present but no longer a thing you can safely hand to another system” bad.</p>

<p>For example, this kind of output is not useful provenance:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>### 3aching reduces and. latency##
</code></pre></div></div>

<p>Neither is this:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>`{-3 "_ ],
</code></pre></div></div>

<p>This matters a lot more than a generic question-answer benchmark will tell you. If your context is only prose, you may get away with it. If it contains rules, code, configuration, exact claims, links, citations, or source material
that needs to be quoted back later, you really cannot.</p>

<p>So we made a list of the things we actually cared about.</p>

<p>We wanted query-aware compression because the agent normally knows what it is trying to do. We wanted a hard output budget because “roughly shorter” is not a useful systems contract. We wanted the retained text to stay source-backed
instead of being quietly rewritten. We wanted the output to preserve source order. We wanted omissions to be visible instead of pretending two distant paragraphs were originally adjacent. And, because this was going into an agent
pipeline, we wanted to know exactly where every retained piece came from.</p>

<p>That last one was important enough that it shaped the API.</p>

<p>Trimwise returns source spans for the retained content. These are Python-string offsets into the original input: inclusive start, exclusive end. If Trimwise keeps two regions from a long blog, the caller gets two source spans in
source order. If Trimwise inserts an omission marker between them, that marker is intentionally not part of either source span.</p>

<p>Why do we care? Because now we can take a trimmed excerpt and still map it back to the original blog, the original document section, or the original repository file. We can keep paths and line references accurate. We can cite the
actual source instead of citing a synthetic compressed blob. And if we want to split a retained block further during final prompt assembly, we can do that without losing provenance.</p>

<p>That is the basic design of Trimwise: do the ranking work, but keep the source relationship intact.</p>

<p>Under the hood, the library starts by segmenting the input into structural units. It tries to respect real boundaries: headings, paragraphs, lists, fenced code blocks, source lines, and smaller fallback units when needed. Then it can
rank those pieces in a few different ways.</p>

<p>There is a structural mode when there is no useful query. There is a lexical mode for query-aware retrieval using term relevance. There is a semantic mode when the caller provides embeddings. And there is a hybrid mode which combines
lexical and semantic signals. We deliberately made embeddings caller-owned. We did not want the library to quietly decide which embedding model gets downloaded, loaded, cached, billed, or trusted inside someone else’s application. If
you already have an embedding stack, use it. If you want a small local model, use that. If you want a stronger remote one, that is your decision too.</p>

<p>The output is still source text. The ranking passages can have extra context to help the scorer understand where a paragraph sits in a document, but the final composition only uses the original slices. This sounds small, but it avoids
a very weird class of bug where a ranking helper accidentally leaks enriched context into the final prompt.</p>

<p>Then, naturally, we benchmarked it. And then naturally, the benchmark became its own project.</p>

<p>At first, we had a dataset that looked reasonable on paper and was absolutely not reasonable once we inspected where the evidence lived. Most answers were near the beginning. This made first N look much better than it deserved to
look. The benchmark was basically rewarding a method for preserving the part of the document we had accidentally made most important. That is not a result. That is us measuring our own dataset bias.</p>

<p>So we fixed it.</p>

<p>We built a position-controlled set of 160 cases:</p>

<ul>
  <li>40 where required evidence appears near the beginning</li>
  <li>40 where it appears in the middle</li>
  <li>40 where it appears near the end</li>
  <li>40 where the answer needs multiple separate regions</li>
</ul>

<p>The cases cover answerable content, instructions, procedures, structured/code-heavy material, adversarial material, and source-backed real content. We kept these tracks separate because one score cannot honestly describe all of them.</p>

<p>A short answer question should be measured differently from “did the required instruction survive?”, which should be measured differently from “did these ordered procedure steps remain in order?”, which should be measured differently
from “is this JSON/code/config block still exact?”. Trying to force all of those through one fuzzy “quality” score is how benchmark dashboards become very pretty and very useless.</p>

<p>We tested 128-token and 512-token output budgets. We compared Trimwise Lexical and Trimwise Hybrid against LLMLingua, LongLLMLingua, and RECOMP. We then took the full contexts and compressed contexts and ran them through three
downstream evaluators: GPT-5.4 Nano, GPT-5.4 Mini, and GPT-5.6 Luna. The full prompt was kept as a reference line, not as a compressor, because obviously it has no compression latency or output budget to compare fairly.</p>

<p>The first result was pleasantly boring: Trimwise Lexical is fast.</p>

<p>On our controlled set, Lexical was around 8 ms median compression latency. Hybrid was around 52 ms because embedding work is not free. At 128 tokens, both were around the same source-retention level: roughly 50% macro case pass across
the different task tracks. At 512 tokens, Hybrid pulled ahead more clearly: about 62.8% macro source pass compared to about 58.4% for Lexical.</p>

<p>So there is a real trade-off here, not magic.</p>

<p>If you need low-latency query-aware compression and your source is mostly normal prose, Lexical is a very serious option. It gets you most of the way there at a fraction of the time. If you can afford the embedding step and care about
semantic matching across larger contexts, Hybrid starts earning its cost as the budget grows.</p>

<p>The downstream answer results were the more interesting part.</p>

<p>At 128 tokens, Trimwise Lexical was already very close to the full-prompt answer-pass baseline for all three evaluators. At 512 tokens, Trimwise Hybrid matched the full-prompt baseline on GPT-5.4 Mini and GPT-5.6 Luna, and slightly
exceeded it on GPT-5.4 Nano in this controlled setup.</p>

<p>That does not mean compression somehow makes a model universally smarter. It means that for these tasks, the compressed context often removed enough irrelevant material that the smaller context was at least as usable as the full one.
This is exactly the type of thing we wanted to know before wiring a compressor into an agent system.</p>

<p>RECOMP was interesting too. It preserves cleaner source pieces than the token-deletion approaches because it is selecting passages rather than aggressively shaving text within them. But in our run it was much slower, around 220 ms,
and it did not retain as much usable source material as Trimwise on the controlled set. This is not a dunk on RECOMP. It is trained around particular retrieval/compression objectives, and that matters. A compressor tuned for one kind
of QA corpus is not automatically the best fit for long SEO material, mixed Markdown, internal documentation, or repository excerpts.</p>

<p>LLMLingua and LongLLMLingua had another issue beyond score: budget compliance.</p>

<p>At the 128-token target, LLMLingua exceeded the budget on around 70% of cases. LongLLMLingua exceeded it on around 86.9% of cases. At 512 tokens, LLMLingua improved a lot, but LongLLMLingua was still over budget on a large chunk of
cases. This makes comparison awkward. If one method gets to use more context than the others, it may get an unfair quality advantage. In our run, it still did not perform well, but the point remains - a token budget should be a
contract, not a suggestion.</p>

<p>We also kept LLMLingua2 out of the main head-to-head chart. Not because it is unimportant, but because it is queryless. Our main question was: if every method receives the same source and the same query, which one gives us the best
context? Giving LLMLingua2 less information and then presenting it beside query-aware methods would not be fair.</p>

<p>We did run it separately. It performed poorly on the diagnostic set, but that result belongs in a queryless comparison, where it can be compared fairly with structural compression, Selective Context, and positional baselines. Mixing
all of those together would make a nice crowded graph and a bad conclusion.</p>

<p>There is another caveat worth saying out loud: the absolute answer-pass baseline was not amazing. Full prompt scored around 41.6% to 44.4% macro answer pass across the three evaluators. That does not automatically mean the models are
bad or that the compressor is bad. It means the benchmark is strict, the tasks are varied, and not every source task is naturally reducible to “produce one short answer matching this gold string”. We therefore keep answer metrics
separate from instruction survival, procedure ordering, and exact structured-source preservation.</p>

<p>This is also why we are not going to publish a graph saying “Trimwise is 2x better”. That would be silly. The interesting result is more specific:</p>

<p>For query-aware context assembly, Trimwise Lexical is extremely fast and preserves a lot of useful source. Trimwise Hybrid costs more but preserves more useful context at larger budgets. Both preserve source shape and provenance. In
our controlled set, they were substantially more usable than the token-level compression baselines we tested, especially when source boundaries mattered.</p>

<p>And source boundaries do matter.</p>

<p>If you are compressing generic English prose before asking a generic question, a slightly damaged sentence may not hurt you. If you are building an agent that works with institutional knowledge, SEO policies, old blog decisions, code,
URLs, configuration, or materials that need to be cited accurately later, you need a stronger guarantee than “the output kind of looks relevant”.</p>

<p>You need to know what stayed, what got omitted, what got damaged, and where the retained text came from.</p>

<p>That is the actual reason we built Trimwise.</p>

<p>Not because long prompts are bad. Long prompts are sometimes exactly what you need. But because when context needs to get smaller, we wanted the process to be explicit, query-aware, budgeted, source-backed, and honest about what it
removed.</p>

<p>No haunted first N truncation. No clean-looking token graveyards. Just a smaller context that still knows where it came from.</p>]]></content><author><name>Aakash</name></author><summary type="html"><![CDATA[While benchmarking Trimwise, we found that a smaller, query-aware context could match and sometimes, outperform, a full prompt without losing source provenance.]]></summary></entry><entry><title type="html">Extending Codex sessions with CtxSift</title><link href="https://aakashh242.github.io/2026/05/24/extending-codex-sessions.html" rel="alternate" type="text/html" title="Extending Codex sessions with CtxSift" /><published>2026-05-24T00:00:00+00:00</published><updated>2026-05-24T00:00:00+00:00</updated><id>https://aakashh242.github.io/2026/05/24/extending-codex-sessions</id><content type="html" xml:base="https://aakashh242.github.io/2026/05/24/extending-codex-sessions.html"><![CDATA[<blockquote>
  <p>TLDR: I didn’t like Codex usages hitting limits in the middle of a session. Was looking for a way to save tokens and extend my usage. Wanted to target the two places where token usage occurs most - command outputs and state recollection. Other opensource solutions either targeted just one problem or was too heavy and complex for my use-case. Inspired by <code class="language-plaintext highlighter-rouge">Distill</code>, CtxSift came into being, extending upon those ideas.</p>
</blockquote>

<p>With AI providers introducing stricter usage limits, I often found myself burning through the usage windows quickly.
Quite often, they would abruptly end in the middle of some work and I would be left writing it out myself while I waited
for the limit to reset.</p>

<p>I use Codex as my primary agent harness and, what I noticed was inconsistent session lengths. Sometimes, a few prompts
would blow through the 5 hour window while at other times, I could get more turns out of it before the limit hit. Upon a bit of
analysis, I found that the amount of tokens the agent sees and outputs contribute to how fast the usage gets over. The
sessions where it had to constantly refresh its recollection and state led to shorter usages while, the ones where it had
didn’t need to perform many recollections went on longer. This recollection loop is noticeable especially after context
compaction events after which, agents mostly start re-reading files and re-running commands to get back to where they were.</p>

<h2 id="what-i-was-looking-for">What I Was Looking For</h2>

<p>Modern LLMs often do not need to view full command outputs to get what they need for a task or to reason.
It became evident that I’d have to control what the agent sees and how much time it spends re-exploring during state recollection.
I started looking at what was already available in opensource and came across a few brilliant projects -
<a href="https://github.com/rtk-ai/rtk">RTK</a>, <a href="https://github.com/mksglu/context-mode">context-mode</a>, <a href="https://github.com/claudioemmanuel/squeez">squeez</a>,
<a href="https://leanctx.com/">LeanCTX</a> and <a href="https://github.com/samuelfaj/distill">Distill</a> to name a few. They all had good approaches
to address the token wastage problem but, I felt that some added more complexity to the agents’ workflow.</p>

<p>I wanted something simple and lightweight - minimal addition to the agents’ workflow, no extra MCP server dependency with multiple tools,
run locally, no complex knowledge graphs and not just heuristics-based compression. I was not looking for a semi or full-fledged memory
system as Codex internally handles its own memory. What I looked for was something to complement this. <code class="language-plaintext highlighter-rouge">Distill</code> was the
closest to what I wanted - a command exposed as a skill - the agent runs a command and, pipes its output to distill
along with an instruction of what it wants from the output. Distill uses a language model to compress the output to only
what was asked for and returns it to the agent.</p>

<p>Distill example:</p>

<p><code class="language-plaintext highlighter-rouge">bash frame="none"
$ pytest -q | distill "Return only the failing tests. No explanations."
tests/api/test_users.py::test_create_user_requires_email
tests/jobs/test_reconcile.py::TestReconcile::test_retries_deadlock
</code></p>

<h2 id="what-was-missing">What Was Missing</h2>

<p>Distill worked well for output compression and I could see limits draining slowly…till a context compression event or when I asked something
unrelated to the current task flow. As the agent was “distilling” command outputs, its working memory did not store raw
details - which is a double-edged sword. On one hand, it keeps the memory small and reduces how much tokens it uses but,
on the other hand, the agent now has to re-read files and re-run commands to recollect and get back to a state from where
it can continue the given task. It shifted the token tax to recollection events! What I saved during distillation got used
during state recollection.</p>

<p>What was remaining was a simple way for the agent to recall its actions and recollect state faster and more efficiently.
I didn’t want a complex knowledge management system or a full-fledged memory layer - they can get noisy fast. Instead, I
opted for a simple, local, workspace-scoped caching system which the agent could access with just one command - no multiple
commands, no chained tool calls - just one command with 3 optional flags. That became by vision for CtxSift - distill
commands when necessary, save those distilled results and let the agent recollect them when it needs context.</p>

<h2 id="building-ctxsift">Building CtxSift</h2>

<p>My goal was to introduce an agent skill that plugs in two simple steps in an agents’ workflow - <code class="language-plaintext highlighter-rouge">compress</code> and <code class="language-plaintext highlighter-rouge">recall</code>.
I wanted it to be local and have a simple caching system. I started building CtxSift as an extension of <code class="language-plaintext highlighter-rouge">Distill</code>’s capabilities
and, after a few iterations, here it is, ready for public use.</p>

<p>In its current state, CtxSift can use local and remote LLMs for compression and works with both CPU and GPU. For caching,
it uses SQLite with FTS5 and SQLite-Vec to store records. A hybrid retrieval pipeline with deterministic scoring and filtering
ensures fresh, grounded context is made available to the agent. It also maintains record freshness and superseded, older
records get marked stale as context updates.</p>

<p>I am really excited to share this with the broader community. Hope it helps you squeeze that extra bit out of your daily
sessions. Happy Sifting!</p>]]></content><author><name>Aakash</name></author><summary type="html"><![CDATA[The problem CtxSift is built to solve in long-running coding-agent sessions.]]></summary></entry><entry><title type="html">Why I am putting TabMate on hold</title><link href="https://aakashh242.github.io/blog/2026/05/15/tabmate-on-hold.html" rel="alternate" type="text/html" title="Why I am putting TabMate on hold" /><published>2026-05-15T00:00:00+00:00</published><updated>2026-05-15T00:00:00+00:00</updated><id>https://aakashh242.github.io/blog/2026/05/15/tabmate-on-hold</id><content type="html" xml:base="https://aakashh242.github.io/blog/2026/05/15/tabmate-on-hold.html"><![CDATA[<p>It has been just over a month or since <a href="https://tabmate.org">TabMate</a>, the browser-based research agent went live. I did
what the rulebook says - work on SEO, submit to search directories, publish in forums where the target users live etc.
Even went out of the rulebook to reach out to early supporters for free use in exchange for feedback and reviews.</p>

<p>The website traffic is okay - over 1k visitors in a month BUT - the main metrics - installs, active users, time spent
on site, CTAs clicked, bounce rate - are sad. I spent a couple of weeks analyzing what is going wrong. And it should
have been obvious from the start (though, I chose to build around it).</p>

<h2 id="the-issues">The Issues</h2>

<ol>
  <li><strong>Privacy</strong> - For a browser extension, privacy is a big factor. TabMate has access to the current page, needs PII to support accounts and saves chats to maintain sessions. This means potentially sensitive data can be passed on to our servers and users are skeptical of sending data to unknown 3rd parties, no matter how good the privacy policy is.</li>
  <li><strong>David vs Goliath</strong> - Big players already exist - Claude for Chrome, and with Gemini being integrated into Chrome, TabMate ends up competing with them. No way can this fight be won.</li>
  <li><strong>Death by generalization</strong> - TabMate can serve many workflows, this advantage of it makes it very less sellable - people have other tools to do them.</li>
</ol>

<p>After a discussion with Rupam and, careful consideration, decided to keep TabMate activities on a pause. Let it exist as
is for now but no need to do further development or SEO or marketing work on it. Let Dib return from his exams, then we
can see.</p>

<p>Meanwhile, before we start hardening work on <a href="https://tenwrite.com">TenWrite</a>, I’ll be working on a skill to reduce token
usage and extend coding sessions. Yeah, it’ll be opensource. Let’s see how it goes.</p>]]></content><author><name>Aakash</name></author><category term="blog" /><summary type="html"><![CDATA[It has been just over a month or since TabMate, the browser-based research agent went live. I did what the rulebook says - work on SEO, submit to search directories, publish in forums where the target users live etc. Even went out of the rulebook to reach out to early supporters for free use in exchange for feedback and reviews.]]></summary></entry><entry><title type="html">From on-prem to the cloud - Lessons Learned</title><link href="https://aakashh242.github.io/blog/2026/05/03/on-prem-to-cloud.html" rel="alternate" type="text/html" title="From on-prem to the cloud - Lessons Learned" /><published>2026-05-03T00:00:00+00:00</published><updated>2026-05-03T00:00:00+00:00</updated><id>https://aakashh242.github.io/blog/2026/05/03/on-prem-to-cloud</id><content type="html" xml:base="https://aakashh242.github.io/blog/2026/05/03/on-prem-to-cloud.html"><![CDATA[<blockquote>
  <p>Moving from on-prem to cloud changed the tooling, speed, and scaling model, but not the core laws of systems design. Capacity, latency, state, failure handling, and topology still matter just as much. Microservices can help, but only when they solve a real scaling or operational problem instead of just moving complexity around.</p>
</blockquote>

<p>A recent conversation with <a href="https://peerlist.io/raymondoyondi">Raymond Oyondi</a> on Peerlist made me rack my memories a bit and reflect on how much software and infrastructure have changed over the years.</p>

<p>I joined the industry back when cloud still felt more like a concept than a default. A lot of systems were still being built and maintained in environments where the infrastructure was very much in your hands. You knew the machines, the network, the limits, the weak points. If something needed scaling, it was not a button click and a dashboard graph. It meant spinning up another server, configuring it, wiring it into the network and load balancer, deploying the application, syncing state, setting up monitoring, and making sure the whole thing did not fall apart under pressure. We had automation in places, of course, but nowhere near the kind of convenience people now take for granted.</p>

<p>A lot has changed since then. But the funny thing is, the biggest lesson for me is that the old principles never really went away.</p>

<p>Cloud changed the speed. It changed the abstractions. It changed how easily we can provision, scale and recover. But it did not change the laws underneath. Capacity still matters. Latency still matters. State still causes pain. Network boundaries still introduce failure. Bad assumptions still come back to collect interest.</p>

<p>That is probably the biggest thing I learned moving from on-prem and bare-metal thinking into cloud-native systems: the tooling changed more than the fundamentals did.</p>

<p>Earlier, a lot of software lived as one big application. One service, one deployment unit, one giant block with hard coupling inside it. It was not always pretty, but it was straightforward in one sense: most of the complexity lived inside the application itself. Since the infrastructure was under our control, nobody really panicked about it. You managed the box, tuned the app, scaled when needed, and kept things moving.</p>

<p>Then cloud became normal, and with it came speed, flexibility, and a different cost model. Suddenly, scaling was easier. You no longer had to treat infrastructure changes like a mini project every single time. But that convenience also exposed something important: a lot of monoliths were expensive in ways people had not fully noticed before.</p>

<p>You would see an application chewing through resources and the default response would be to scale the whole thing. More compute, more memory, more replicas, more money. But when you looked closer, often only certain parts of the application were actually responsible for that load. Maybe one workflow was CPU-heavy. Maybe one module was doing aggressive I/O. Maybe one part had bursty traffic while the rest of the system just sat there minding its own business.</p>

<p>That is where the architectural shift really starts to make sense.</p>

<p>Instead of treating the software like one sealed black box, you begin to see it as a collection of components with different scaling patterns and different operational needs. So you start isolating them. You break out the hot paths. You separate the parts that need to scale from the parts that do not. Pretty soon, what used to be a monolith starts becoming a patchwork of smaller services talking to each other.</p>

<p>And yes, that can absolutely be the right move.</p>

<p>But I also think this is where a lot of people get seduced by architecture diagrams and forget the bill that comes later.</p>

<p>Microservices are not free. They reduce one kind of pain and introduce another. You gain independent scaling, but you also gain more network hops, more deployment surfaces, more observability needs, more operational coordination, more failure modes, and more opportunities for state to become inconsistent. The complexity does not disappear. It just moves.</p>

<p>Earlier, if two parts of the system needed to coordinate, that problem often lived inside one process boundary. Now it may live across services, shared storage, queues, caches, retries, and eventual consistency rules. You may need supporting software to make the architecture work. You may need shared storage. You may need to handle read-write races and stale data. You may need to think much harder about idempotency, ordering, duplicate events, and what “correct” even means in a distributed system.</p>

<p>So for me, the lesson was never “microservices good, monolith bad.” That is too simplistic and honestly a bit lazy.</p>

<p>The real lesson was this: design around the behavior of the system, not around fashionable architecture labels.</p>

<p>If one deployable unit works, keep it one deployable unit. If certain modules clearly have different scaling needs, isolate them. If you are introducing distributed complexity, make sure the benefits are worth the operational cost. Use the minimum supporting software necessary. Every extra moving part is one more thing to monitor, patch, debug, secure and explain at 2 AM.</p>

<p>Another lesson that became much more obvious in the cloud-native world is that deployment topology matters a lot more than many developers initially think. Two services talking to each other on a diagram is easy. The actual topology, where they run, how they communicate, what latency sits between them, how failover behaves, where state lives, and what happens during partial failure, is where reality begins.</p>

<p>I have also come to appreciate observability discipline much more over time. In distributed systems, tracing tools are great and OpenTelemetry has helped a lot, but tooling alone does not save you. If your logs are inconsistent, your labels are exploding in cardinality, your trace attributes are a mess, and every team names the same thing differently, you are not observing a system. You are generating noise. Good observability needs discipline: standard log formats, sensible naming conventions, rules for metrics and labels, and a sampling strategy that matches the criticality of the application. Otherwise, you either drown in telemetry or pay too much to keep it.</p>

<p>So when I think about high availability at scale, my biggest lesson learned is actually a simple one.</p>

<p>Break systems into modules where it genuinely helps. Keep supporting software to a minimum. Be aware of deployment topology. Respect state. And never assume cloud removed the need for sound systems thinking. It did not. It just made it easier to build distributed systems before earning the scars required to run them well.</p>

<p>Cloud is powerful. But it is still someone else’s computer. And the old bare-metal lessons still hold stronger than people think.</p>]]></content><author><name>Aakash</name></author><category term="blog" /><summary type="html"><![CDATA[Moving from on-prem to cloud changed the tooling, speed, and scaling model, but not the core laws of systems design. Capacity, latency, state, failure handling, and topology still matter just as much. Microservices can help, but only when they solve a real scaling or operational problem instead of just moving complexity around.]]></summary></entry><entry><title type="html">From a builder to a founder</title><link href="https://aakashh242.github.io/blog/2026/04/30/builder-to-founder.html" rel="alternate" type="text/html" title="From a builder to a founder" /><published>2026-04-30T00:00:00+00:00</published><updated>2026-04-30T00:00:00+00:00</updated><id>https://aakashh242.github.io/blog/2026/04/30/builder-to-founder</id><content type="html" xml:base="https://aakashh242.github.io/blog/2026/04/30/builder-to-founder.html"><![CDATA[<blockquote>
  <p><strong>TL;DR:</strong> Generic, I know, but it’s the truth. I spent 10 years in TCS, moved into SaaS with my friend Rupam, and learned that building is only one small part of founder life. While helping grow our products, I felt the pain of messy browser research and built TabMate to solve it. Shipping it felt great. Now comes the harder part: marketing, distribution, doubt, and the daily fight to keep going.</p>
</blockquote>

<p>Aloha, back to blogging after a while! So much has happened since!</p>

<p>I quit my job at TCS after serving 10 years to take a foray into the Founder world. I learned a lot
during my time there (I was lucky enough to be in projects that had a mix of dev, devops, infra, platform, security and AI)
and matured as a developer. The only drawbacks were - constraints that would not let me build solutions for common pains
we faced unless the client approved and, not being paid as much for the work I did. Well, in hindsight, it’s just how
service based companies work, and I am not complaining. I wanted to have the freedom to build what I want and be able to
say truthfully that I get paid for doing what I love.</p>

<p>I started my founder journey under an old college friend, Rupam. Since college days, he always had a founder mindset. I 
remember he had built a social networking website for our college - all with plain old HTML, CSS, PHP and SQLite. He is
one of those old-school programmers whose learned the trade through sweat and toil. He, too, was in TCS though, he quit
6 years before me to start his own ventures.</p>

<p>When I joined him, he already had two profitable products in the market and one in the pipeline. I am grateful he agreed
to mentor and guide me, given how differently we saw things when it came to managing the software lifecycle! 
See, I was from a controlled, constrained world where the user base was guaranteed hence, I optimized for longevity with
failsafes, fallbacks, redundancy and best practices. He, on the other hand, optimized for velocity, stable MVPs, 
user-building and feedback driven features - and it makes sense because in the SaaS world, it doesn’t matter how polished or
robust your solution is if no one uses it. I joined him with the project, <a href="https://smartbankstatement.com">Smart Bank Statement</a> where, I learned the basics of
the Founder life - and the hard truth that code is only a small part of it!</p>

<p>With Smart Bank Statement now stable and slowly gaining users, we turned our focus to improving what we have and, if
possible, start on a new project. He started addressing feature requests from one of the products while I started looking
into the other. During this time, my work involved browsing way more than I used to as I was actively learning the part
of the software lifecycle that most devs don’t get to experience - selling it. The workflow was the same most days - 
google search, open up tabs, open a ChatGPT session, go here, copy this, paste that, fight ChatGPT over forgetting stuff,
re-find the same info again - it got annoying, monotnonous and boring pretty soon. I wanted a ChatGPT in my browser which
had context across all my tabs and also, remember where we were when I came back the next day. I basically wanted a 
retrieval agent IN THE BROWSER!</p>

<p>Well, we developers love a challenge. I set out to build it (hence the delay in this post). It initially started out as
a side panel that could just see the text you selected, save memories which you choose to save and used a heuristic 
retrieval system. With time, however, it evolved into an assistant that could remember what you saved across your sessions
and bring them up when necessary! Whenever I found something useful, I would just pin it or save it as a memory. Later,
when I was prepping and needed reference, I would just ask it and that piece of info saved god knows how many tabs and hours
ago just shows up! I named it <a href="https://tabmate.org">TabMate</a> out of love (and because I couldn’t think of anything else).</p>

<p>I pitched this idea to Rupam. My goal was to contribute to the pot. If I daresay, to me, our partnership sounds like the faint
whispers an institute of products in the making. He has already contributed three
products, it’s only fair for me to pitch in and pull my weight. He was skeptical and cross-questioned the idea, just 
like a rigorous co-founder should. One thing he said really stuck with me - “We, devs, build tools for ourselves then, 
we think everyone will find it useful. But most often, that is not the case.” I realized that he was right - the tool 
started out as a dev’s assistant but, dev workflows are pretty niche and varies from dev to dev. I couldn’t pitch a 
generic dev assistant at the browser level, I had to find the proper group of people whose work involves scouring 
across websites and living in “tabland”.</p>

<p>He let me take time to think and build it through and, after a lot of brainstorming and researching across the internet (lol),
I finally managed to build a stable MVP. Yeah, it took longer than usual as I had to iterate and tune the retrieval loops
and user flows. I was finally able to release it publicly on 27th April 2026. Now begins the hard game. The dopamine rush
of building and shipping is over.</p>

<p>As I sit here now, putting into action the marketing strat I have for TabMate (trust me, it adapts every day), so many 
thoughts are playing across my mind. Did I do the right? When will conversion actually start? What else can I do to 
improve its reach and distribution? How do I tune my strategy? Do I give ads? Do I give it all up and go back to
working for someone else? I read a quote on IndieHackers which read - “I built and I failed and I kept building.”
Now, that guy sits at over $30K/month revenue. While stories and quotes are motivating, the human mind is a prison when
left alone to think about and contemplate all possibilities. Mostly, it tends to converge on the negatives. There are
times I think that it’s best to keep TabMate for myself and concentrate on the products we already have. I guess I am
still human. Yet, the thought of having real users for a system I built with my own hands is really seducing and I keep
doing what is necessary - ethical and fair but, necessary.</p>

<p>It’s the early days and yes, there are a lot of uncertainties. TabMate might live for a while then be integrated into
browsers, get shelved and stay as my personal tool or, truly live its potential. I mean, this context switching pain
is something everyone must be feeling, I just have to get to the right kind of people. 
I might pivot and build other ideas - let’s see how it goes. For now, the dopamine rush of building has settled and the
dread of marketing has set in. I have to find ways to get a dopamine rush out of this phase too. Maybe take some
programmatic help? Hmmm, let’s see.</p>]]></content><author><name>Aakash</name></author><category term="blog" /><summary type="html"><![CDATA[TL;DR: Generic, I know, but it’s the truth. I spent 10 years in TCS, moved into SaaS with my friend Rupam, and learned that building is only one small part of founder life. While helping grow our products, I felt the pain of messy browser research and built TabMate to solve it. Shipping it felt great. Now comes the harder part: marketing, distribution, doubt, and the daily fight to keep going.]]></summary></entry><entry><title type="html">How I Got Into Building Smart Bank Statement</title><link href="https://aakashh242.github.io/blog/2026/03/29/smart-bank-statement-start.html" rel="alternate" type="text/html" title="How I Got Into Building Smart Bank Statement" /><published>2026-03-29T00:00:00+00:00</published><updated>2026-03-29T00:00:00+00:00</updated><id>https://aakashh242.github.io/blog/2026/03/29/smart-bank-statement-start</id><content type="html" xml:base="https://aakashh242.github.io/blog/2026/03/29/smart-bank-statement-start.html"><![CDATA[<blockquote>
  <p>TL;DR: This was not my idea originally. Rupert had already started exploring the space when I got involved. Once I joined, we looked harder at the market, the actual workflow pain and (<a href="https://aakashh242.github.io/blog/2026/03/05/remote-mcps-as-local.html">aakashh242.github.io</a>). What started as a broader finance direction became a much narrower product: take messy bank statement PDFs and turn them into structured, usable data. From there, we worked together to get the MVP out.</p>
</blockquote>

<h2 id="how-i-came-into-it">How I came into it</h2>

<p>An old college friend and roommate of mine, Rupert, had already started thinking in this space before I joined. So this was not one of those stories where two people sit down on day one with a blank page and magically arrive at the final product. The motion had already started. I entered after that, and once I did, my role became less about “coming up with the idea” and more about pressure-testing it, sharpening it and helping move it toward something that could become a real product.</p>

<p>At the time, the idea-space was wider. Like many things around finance, it is very easy to drift toward the flashy layer first: dashboards, summaries, personal finance views, spending insights, charts and all the things that look good in a demo. On paper, that feels like the obvious direction. People do want visibility into their money, after all.</p>

<p>The problem is that this part of the market is crowded and, more importantly, the pain is softer. There is a difference between a problem people find interesting and a problem they are willing to pay to make disappear. The more we looked at it, the more it felt like the “analyzer” route sat closer to the first category. Useful, maybe. Attractive, sure. But harder to build a serious business around unless there is a very strong edge.</p>

<h2 id="where-the-idea-started-to-tighten">Where the idea started to tighten</h2>

<p>So we kept looking.</p>

<p>The more practical side of the workflow started standing out. Not the part where somebody wants prettier insights. The part where somebody already has the data locked inside a bank statement PDF and needs it in a usable format for real work.</p>

<p>That was more interesting.</p>

<p>Because on the surface, converting bank statements to Excel sounds solved. It sounds like one of those dull utility problems the internet has already handled ten times over. But once you look at the actual inputs people deal with, the ugliness shows up quickly. Scanned statements, inconsistent layouts, broken table structure, different debit-credit conventions, weird balance columns, low-quality images, multi-page files, sometimes even multiple accounts in the same document. Suddenly this “simple conversion” problem stops being simple.</p>

<p>And that is before the downstream pain even begins.</p>

<p>Getting rows out of a PDF is not the same as getting reliable data. That distinction matters a lot more in accounting and bookkeeping workflows than it does in casual consumer use-cases. If the extraction is only mostly correct, somebody still has to sit there and verify the output line by line. One shifted row, one wrong amount, one broken balance trail and the time savings start collapsing. In these workflows, “almost correct” is not a nice middle ground. It is often just another form of manual work.</p>

<p>That was the point where the product started becoming more real to me.</p>

<h2 id="what-changed-once-i-joined">What changed once I joined</h2>

<p>Once we teamed up, the conversation changed from “what can we build in finance?” to “what painful workflow exists here that people actually need solved?” That is a much better question, because it forces you to stop thinking in vague product language and start looking at where time is genuinely being lost.</p>

<p>We looked at existing players too. There were already tools in the market, obviously. Some looked dated. Some were too broad or enterprise-heavy. Some could handle easy statements but would struggle as soon as the documents became messy. Some could extract data, but still left enough cleanup and checking on the user that the problem was not really solved.</p>

<p>That gap mattered.</p>

<p>To me, the opportunity was never “nobody is doing this.” That is usually the wrong lens anyway. The real opportunity was that the problem was still painful enough, despite existing tools, that there was room for a more focused and more accurate product.</p>

<p>So the idea got narrower.</p>

<p>Not a personal finance dashboard. Not a generic document AI platform. Not a bloated accounting suite. Just a focused workflow: upload a bank statement PDF and get back structured output that is usable enough to save real time.</p>

<p>That kind of narrowing is easy to say and much harder to do.</p>

<h2 id="the-build-and-the-mvp">The build and the MVP</h2>

<p>Once the direction became sharper, the implementation questions also became sharper. You stop thinking only in terms of OCR and start thinking about statement variance, normalization, row structure, balances, reconciliation, scanned versus digital PDFs, error detection and the difference between extraction that merely looks plausible and extraction that can actually be trusted.</p>

<p>That distinction shaped how we approached the product.</p>

<p>The goal could not just be “convert PDF to Excel.” There are too many ways to technically do that while still dumping the messy part back onto the user. The output had to be clean enough that it reduced work, not just moved work to a different stage.</p>

<p>That is what we built the MVP around.</p>

<p>Rupert had the initial seed. I joined once things were already underway. From there, together, we took it through the more difficult but more valuable phase: questioning the original direction, tightening the scope, understanding the market better and actually getting a usable MVP built instead of staying stuck in idea-land.</p>

<h2 id="why-i-like-this-story-more-than-the-polished-version">Why I like this story more than the polished version</h2>

<p>A lot of startup stories get rewritten after the fact to sound cleaner than they were. Two founders see a giant market, spot a perfect gap, align instantly and start executing with full clarity. Real life is usually more uneven than that.</p>

<p>This one certainly was.</p>

<p>The idea was already in motion before I came in. The initial space was broader than where we ended up. The clearer version of the product only emerged after spending more time with the pain, the market and the workflow details.</p>

<p>But honestly, I prefer that kind of story.</p>

<p>It feels more real. Better products often come out of that process: not from trying to sound ambitious from the beginning, but from being honest enough to keep narrowing until the pain becomes sharp and the value becomes obvious.</p>

<p>That is how I got into building Smart Bank Statement.</p>

<p>Not by inventing the idea from zero, but by joining an old friend, helping pressure-test it and then building with Rupert toward something much more grounded than where it began.</p>]]></content><author><name>Aakash</name></author><category term="blog" /><summary type="html"><![CDATA[TL;DR: This was not my idea originally. Rupert had already started exploring the space when I got involved. Once I joined, we looked harder at the market, the actual workflow pain and (aakashh242.github.io). What started as a broader finance direction became a much narrower product: take messy bank statement PDFs and turn them into structured, usable data. From there, we worked together to get the MVP out.]]></summary></entry><entry><title type="html">Dev Blog - Proving the Remote MCP Adapter’s Security Guardrails part 1</title><link href="https://aakashh242.github.io/blog/2026/03/16/dev-blog-securing-remote-mcp-adapter-1-evidences.html" rel="alternate" type="text/html" title="Dev Blog - Proving the Remote MCP Adapter’s Security Guardrails part 1" /><published>2026-03-16T00:00:00+00:00</published><updated>2026-03-16T00:00:00+00:00</updated><id>https://aakashh242.github.io/blog/2026/03/16/dev-blog-securing-remote-mcp-adapter-1-evidences</id><content type="html" xml:base="https://aakashh242.github.io/blog/2026/03/16/dev-blog-securing-remote-mcp-adapter-1-evidences.html"><![CDATA[<blockquote>
  <p><strong>TL;DR:</strong> In the last post, I said v0.3.0 would harden the Remote MCP Adapter against poisoned tool metadata and weak session semantics. This post is the proof. I built a mutable mock MCP server, ran live adapter instances against it, and captured evidence for four security controls: tool-definition pinning, metadata sanitization, description minimization, and session-integrity binding.</p>
</blockquote>

<p>In the <a href="2026-03-16-dev-blog-securing-remote-mcp-adapter-1.md">last post</a>, I wrote about four security issues I wanted to tackle in the <a href="https://github.com/aakashh242/remote-mcp-adapter">Remote MCP Adapter</a>.</p>

<p>That post was about design and intent.</p>

<p>This one is about evidence.</p>

<p>I did not want to stop at unit tests and say “trust me, it works.” So I built a local test harness around a mutable FastMCP server and used it to exercise the adapter end to end. The result is a set of reproducible evidence artifacts showing what the adapter actually does when tool metadata changes, when descriptions are too verbose, and when a session is reused under the wrong authenticated context.</p>

<h2 id="what-i-tested">What I tested</h2>

<p>I ran four live security scenarios:</p>

<ol>
  <li>Tool-definition pinning and drift detection</li>
  <li>Tool metadata sanitization</li>
  <li>Tool description truncation and stripping</li>
  <li>Session integrity hardening for stateful flows</li>
</ol>

<p>Each scenario produced:</p>

<ul>
  <li>the exact adapter config used for the run</li>
  <li>raw upstream tool snapshots</li>
  <li>raw adapter tool snapshots</li>
  <li>error payloads</li>
  <li>process logs</li>
  <li>SQLite state snapshots where relevant</li>
  <li>a per-scenario summary</li>
</ul>

<p>So this was not a mocked “assert function returned true” setup. It was a real adapter process talking to a real mutable MCP server.</p>

<h2 id="the-test-setup">The test setup</h2>

<p>The setup was simple on purpose:</p>

<ul>
  <li>a mutable FastMCP upstream server running locally</li>
  <li>one or more local adapter instances with scenario-specific config</li>
  <li>a runner script that switched upstream revisions, called the adapter, and saved the results</li>
</ul>

<p>The upstream could change its catalog on demand. That made it possible to test:</p>

<ul>
  <li>a benign first tool catalog</li>
  <li>a changed description later in the same session</li>
  <li>dirty metadata</li>
  <li>very long descriptions</li>
  <li>reused sessions after an auth-context change</li>
</ul>

<p>This was exactly the kind of thing I wanted to prove before claiming the adapter had become a safer boundary.</p>

<h2 id="1-tool-definition-pinning">1. Tool-definition pinning</h2>

<p>This was the most important one.</p>

<p>The adapter was configured to:</p>

<ul>
  <li>pin the first visible tool catalog for a session</li>
  <li>block mid-session drift</li>
  <li>invalidate the session when drift is detected</li>
</ul>

<h3 id="what-happened">What happened</h3>

<p>The first <code class="language-plaintext highlighter-rouge">tools/list</code> call established the session baseline.</p>

<p>Then I changed the upstream tool description.</p>

<p>The next <code class="language-plaintext highlighter-rouge">tools/list</code> in the same session failed with a drift message. The adapter invalidated that session and refused to keep using it. Reusing the same session again resulted in <code class="language-plaintext highlighter-rouge">409 Conflict</code>. Starting a fresh session succeeded and picked up the upgraded catalog.</p>

<h3 id="what-that-proves">What that proves</h3>

<p>This closes the “rug pull” path where an upstream server can look safe during the initial review and then quietly mutate the tool surface after trust has already been established.</p>

<p>The trust boundary becomes:</p>

<ul>
  <li>first catalog exposure pins trust for that session</li>
  <li>mid-session tool drift is not silently accepted</li>
  <li>a new session is required to accept upstream changes</li>
</ul>

<p>That is exactly the behavior I wanted.</p>

<h2 id="2-tool-metadata-sanitization">2. Tool metadata sanitization</h2>

<p>The second scenario targeted dirty model-visible metadata.</p>

<p>The mock upstream exposed tool metadata with:</p>

<ul>
  <li>decomposed Unicode</li>
  <li>zero-width characters</li>
  <li>dirty schema descriptions</li>
</ul>

<p>I ran the adapter twice:</p>

<ul>
  <li>once with sanitization enabled</li>
  <li>once with sanitization set to block</li>
</ul>

<h3 id="what-happened-1">What happened</h3>

<p>With sanitization enabled, the adapter cleaned the visible metadata before forwarding it.</p>

<p>The differences were visible in the captured tool snapshots:</p>

<ul>
  <li>dirty title -&gt; normalized title</li>
  <li>dirty description -&gt; normalized description</li>
  <li>dirty schema property description -&gt; normalized schema property description</li>
</ul>

<p>With <code class="language-plaintext highlighter-rouge">block</code> mode enabled, the dirty tool disappeared from the adapter-visible catalog entirely.</p>

<h3 id="what-that-proves-1">What that proves</h3>

<p>The adapter no longer has to behave like a naive tunnel for model-visible metadata.</p>

<p>It can:</p>

<ul>
  <li>clean suspicious text conservatively</li>
  <li>or refuse to forward a tool whose metadata had to be changed</li>
</ul>

<p>That gives operators a real first layer of defense against poisoned tool metadata.</p>

<h2 id="3-tool-description-truncation-and-stripping">3. Tool description truncation and stripping</h2>

<p>The third scenario was about description surface minimization.</p>

<p>This is different from metadata sanitization.</p>

<p>Sanitization cleans obviously suspicious text. Description policy answers a different question:</p>

<blockquote>
  <p>How much tool prose should the model see at all?</p>
</blockquote>

<p>The mock upstream exposed very long tool descriptions and very long nested schema descriptions.</p>

<p>I ran the adapter in two modes:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">truncate</code></li>
  <li><code class="language-plaintext highlighter-rouge">strip</code></li>
</ul>

<h3 id="what-happened-2">What happened</h3>

<p>In truncate mode:</p>

<ul>
  <li>the top-level tool description was shortened to the configured limit</li>
  <li>the nested schema description was also shortened</li>
</ul>

<p>In strip mode:</p>

<ul>
  <li>the top-level tool description was removed</li>
  <li>the nested schema description was removed too</li>
</ul>

<h3 id="what-that-proves-2">What that proves</h3>

<p>This is not just a UI tweak on the top-level tool description.</p>

<p>The policy applies to the model-visible description surface more broadly, including nested schema prose. That matters because otherwise an upstream could just move the same persuasive or poisoned text from the tool description into schema field descriptions.</p>

<p>So this control now works the way it should.</p>

<h2 id="4-session-integrity-hardening">4. Session integrity hardening</h2>

<p>The fourth scenario focused on session integrity.</p>

<p>This one matters because the adapter is stateful. It stores uploads, artifacts, tombstones, and other per-session state. That means session handling is part of the product’s security posture, not just a transport detail.</p>

<p>For this test I:</p>

<ul>
  <li>enabled adapter auth</li>
  <li>used disk-backed state persistence</li>
  <li>established one session with token A</li>
  <li>restarted the adapter against the same persisted state</li>
  <li>tried to reuse the old session with token B</li>
</ul>

<h3 id="what-happened-3">What happened</h3>

<p>The old session had already been bound to the first authenticated context.</p>

<p>When I tried to reuse it under the rotated token, the adapter rejected it with <code class="language-plaintext highlighter-rouge">409 Conflict</code>.</p>

<p>Then I started a fresh session under token B, and that worked.</p>

<p>The persisted SQLite state showed separate trust-context fingerprints for the old and new sessions.</p>

<h3 id="what-that-proves-3">What that proves</h3>

<p>Knowing or reusing an <code class="language-plaintext highlighter-rouge">Mcp-Session-Id</code> is not enough on its own.</p>

<p>When auth is enabled, the adapter now binds the session to the authenticated context that created it. A stale session cannot just be picked up under a different auth context and treated as valid.</p>

<p>That is the right direction for a stateful gateway.</p>

<h2 id="what-the-evidence-bundle-contains">What the evidence bundle contains</h2>

<p>I saved the full evidence bundle and will attach it as a ZIP artifact.</p>

<blockquote>
  <p><strong>Evidence bundle:</strong> <a href="/assets/evidence-pack-remote-mcp-adapter-v0.3.0.zip">Download the full ZIP artifact</a></p>
</blockquote>

<p>It includes:</p>

<ul>
  <li>a top-level report per scenario</li>
  <li>a machine-readable summary</li>
  <li>per-scenario configs</li>
  <li>per-scenario logs</li>
  <li>raw upstream and adapter snapshots</li>
  <li>persisted SQLite state snapshots</li>
</ul>

<p>So anyone interested can inspect the actual evidence instead of relying on screenshots or paraphrases.</p>

<h2 id="one-honest-note">One honest note</h2>

<p>There is one small wrinkle in the captured client behavior.</p>

<p>When a blocked session is retried, the FastMCP client sometimes surfaces the failure as a generic <code class="language-plaintext highlighter-rouge">409 Conflict</code> instead of preserving the full response body each time.</p>

<p>That does not weaken the result, because the evidence still shows:</p>

<ul>
  <li>the initial detailed block message</li>
  <li>the repeated <code class="language-plaintext highlighter-rouge">409</code> response</li>
  <li>the persisted invalidation or trust-binding state</li>
  <li>the fresh-session success path</li>
</ul>

<p>Still, it is worth calling out plainly.</p>

<h2 id="why-this-release-matters">Why this release matters</h2>

<p>The Remote MCP Adapter started as a way to make remote MCP servers more practical by handling uploads, artifacts, and stateful mediation.</p>

<p>That is still true.</p>

<p>But once a gateway starts mediating tool metadata and storing session state, it can no longer pretend it is just a dumb transport wrapper. It is part of the security boundary whether it wants to be or not.</p>

<p>That is what v0.3.0 is really about.</p>

<p>Not security theater.
Not vague “hardened mode” marketing.</p>

<p>Actual controls.
Actual live tests.
Actual evidence.</p>

<h2 id="what-comes-next">What comes next</h2>

<p>I am not done with the security work yet.</p>

<p>But this release crosses an important line: the adapter is now starting to defend the boundary it creates, instead of just expanding it.</p>

<p>That was the goal.</p>]]></content><author><name>Aakash</name></author><category term="blog" /><summary type="html"><![CDATA[TL;DR: In the last post, I said v0.3.0 would harden the Remote MCP Adapter against poisoned tool metadata and weak session semantics. This post is the proof. I built a mutable mock MCP server, ran live adapter instances against it, and captured evidence for four security controls: tool-definition pinning, metadata sanitization, description minimization, and session-integrity binding.]]></summary></entry><entry><title type="html">Dev Blog - Securing the Remote MCP Adapter</title><link href="https://aakashh242.github.io/blog/2026/03/16/dev-blog-securing-remote-mcp-adapter-1.html" rel="alternate" type="text/html" title="Dev Blog - Securing the Remote MCP Adapter" /><published>2026-03-16T00:00:00+00:00</published><updated>2026-03-16T00:00:00+00:00</updated><id>https://aakashh242.github.io/blog/2026/03/16/dev-blog-securing-remote-mcp-adapter-1</id><content type="html" xml:base="https://aakashh242.github.io/blog/2026/03/16/dev-blog-securing-remote-mcp-adapter-1.html"><![CDATA[<blockquote>
  <p><strong>TL;DR:</strong> I built Remote MCP Adapter to solve remote file and artifact handling. Then I realized the same adapter could also be abused unless it actively defends against poisoned tool metadata and session misuse. So v0.3.0 is about turning that middleware into a safer boundary, not just a convenient one.</p>
</blockquote>

<p>After finishing off the core work for the <a href="https://github.com/aakashh242/remote-mcp-adapter">Remote MCP Adapter</a>, I took
a step back and started sharing it in forums to see how others are solving the same issue. You can read more about the
inspiration behind it in <a href="2026-03-05-remote-mcps-as-local.md">this blog</a>.</p>

<p>I read this <a href="https://dev.to/luckypipewrench/your-mcp-servers-tool-descriptions-are-an-attack-surface-37pj">blog on dev.to</a>
that talks about how the MCP protocol has an attack layer via tool descriptions. This issue might not affect folks 
running MCP servers locally but becomes a headache for teams and organizations wanting to host them centrally. An
attacker can essentially poison tool descriptions or manipulate tool arguments to make the Agents perform sinister
stuff! The author of that post has built a tool, <a href="https://github.com/luckyPipewrench/pipelock">Pipelock</a> that acts as
a firewall for AI agents. Do show some love to his work!</p>

<h2 id="the-realization">The realization</h2>

<p>While pondering over the article, I realized that I have created a monster that could easily be used to infiltrate 
systems. And it’s out in the wild! So I’m taking the next logical step - learn from the blog, explore MCP attack
surfaces and provide built-in defense against these attack surfaces.</p>

<p>While writing the v0.3.0 release, I am focusing on addressing four issues -</p>

<ol>
  <li><a href="https://github.com/aakashH242/remote-mcp-adapter/issues/22">Tool definition pinning and drift detection</a> - when enabled, the adapter will baseline tools during the first <code class="language-plaintext highlighter-rouge">list_tools</code> call for a session. Any drifts or changes in tool titles, schemas and descriptions in subsequent calls will be detected and either warned or blocked entirely.</li>
  <li><a href="https://github.com/aakashH242/remote-mcp-adapter/issues/23">Normalize and sanitize tool schemas before forwarding</a> - add a metadata preprocessing feature be able to apply a conservative sanitization step to model-visible tool metadata before that metadata reaches the client or model.</li>
  <li><a href="https://github.com/aakashH242/remote-mcp-adapter/issues/24">Tool description minimization/stripping</a> - allow users to minimize or remove tool descriptions altogether - for those extra-secure environments.</li>
  <li><a href="https://github.com/aakashH242/remote-mcp-adapter/issues/25">Harden adapter-managed session semantics for stateful HTTP/SSE flows</a> - once the adapter stores uploads, artifacts, cancellation state, or other per-session data, session integrity is no longer just an MCP transport concern. It becomes part of the product’s own security posture. The goal is to make sure the adapter’s own stateful features cannot be misused just because a session ID is known.</li>
</ol>

<p>I can see a long night up ahead as I gear up to write and test these guardrails out. I shall publish my test results here
in a new blog once I have them ready.</p>]]></content><author><name>Aakash</name></author><category term="blog" /><summary type="html"><![CDATA[TL;DR: I built Remote MCP Adapter to solve remote file and artifact handling. Then I realized the same adapter could also be abused unless it actively defends against poisoned tool metadata and session misuse. So v0.3.0 is about turning that middleware into a safer boundary, not just a convenient one.]]></summary></entry><entry><title type="html">Crossing limits</title><link href="https://aakashh242.github.io/blog/2026/03/11/crossing-limits.html" rel="alternate" type="text/html" title="Crossing limits" /><published>2026-03-11T00:00:00+00:00</published><updated>2026-03-11T00:00:00+00:00</updated><id>https://aakashh242.github.io/blog/2026/03/11/crossing-limits</id><content type="html" xml:base="https://aakashh242.github.io/blog/2026/03/11/crossing-limits.html"><![CDATA[<blockquote>
  <p><strong>TL;DR:</strong> Too many MCP tools in the context window slow agents down and worsen tool selection. GitHub tackles this with clustering and embedding-guided routing. In <code class="language-plaintext highlighter-rouge">remote-mcp-adapter</code>, Code-Mode avoids the problem by letting agents discover tools progressively instead of loading them all upfront.</p>
</blockquote>

<p>The <a href="https://modelcontextprotocol.io/docs/getting-started/intro">Model Context Protocol</a> has been both - a boon and a
curse for Agentic workflows. Yes, it allows you to connect your agent with diverse systems without needing to write
custom integrations for every one of them. But as your tasks grow in breadth and complexity, the more integrations you 
need hence, the more MCP servers you run. This eventually results in large of number of tools that get shoved into
your agent’s context window. Maybe the agent needs just 4-5 tools to perform the task, but you still end up paying
the price for those extra tokens in the context window.</p>

<h2 id="why-is-it-harmful">Why is it harmful</h2>

<p>If you look at it from the Agent’s perspective, it sees</p>

<ul>
  <li>the system instructions</li>
  <li>task specific instructions</li>
  <li>previous tool call results (if any)</li>
  <li>tool descriptions and schemas</li>
  <li>and finally, your task or the next task at hand</li>
</ul>

<p>Do you see the problem? It sees the task at the end and, a lot of unrelated information beforehand can confuse even the
best of the LLMs. You’ll notice an increased latency, a tendency to choose the wrong tools, losing context in between
and claiming a half-finished task as complete and an overall decrease in quality and consistency.</p>

<p>A simple way to restrict this issue is by limiting the number of tools you can have active in a request. GitHub Copilot
used to restrict to <a href="https://github.com/microsoft/vscode/issues">128 tools</a> per request but after a lot of flak from
the community, they decided to remove that limit. So how did they solve the too-many tools problem? You can read about
it <a href="https://github.blog/ai-and-ml/github-copilot/how-were-making-github-copilot-smarter-with-fewer-tools/">here</a>.
In a nutshell, GitHub improved tool selection in Copilot by grouping tools into clusters and using 
embeddings to pre-select the most relevant ones, so the model doesn’t have to reason over hundreds of tools every time.</p>

<h2 id="a-conscious-trade-off">A conscious trade-off</h2>

<p>While working on my <a href="https://github.com/aakashh242/remote-mcp-adapter">remote-mcp-adapter</a>, I came to the realization
that my adapter will contribute to increased token usage.</p>

<p>One of the adapter’s functionality is to override tools that required file uploads from clients. While
overriding the tool, it also appends instructions on how to perform a staged-upload to the original description.
This is done so that the model knows about the original semantics and constraints but also aligns with the staged-upload 
procedure. This means for every upload-type tool configured, more tokens are sent in a <code class="language-plaintext highlighter-rouge">list_tools</code> call.
Although each upstream has its own MCP mount path, clients configured to connect to all upstreams would eventually get
hit by context bloat.</p>

<p>I did not want to replace the tool description entirely with the upload-staging instruction and risk losing
semantics so, I went for keeping only the first sentence of the upstream tool description trimmed to 50 token. I figured
the savings in tokens should make up for sacrificing a bit of semantics.</p>

<h2 id="enter-code-mode">Enter Code-Mode</h2>

<p>After reading that <a href="https://www.jlowin.dev/blog/fastmcp-3-1-code-mode">FastMCP 3.1.0 brought support for Code-Mode</a>, I
was ecstatic. My adapter uses FastMCP so I could just implement a config toggle to enable code-mode.</p>

<p>Code-Mode allows the Agent to progressively discover the tools it needs without having to bloat the context window with
all tool definitions. Instead of all your 1000 tools, it surfaces 5 tools - <code class="language-plaintext highlighter-rouge">search</code>, <code class="language-plaintext highlighter-rouge">tags</code>, <code class="language-plaintext highlighter-rouge">list_tools</code>, <code class="language-plaintext highlighter-rouge">get_schema</code>
and execute. The Agent searches for certain keywords, discovers tools matching those, decides which ones to use, get 
their schemas then triggers an execute call. The video below will demonstrate Agent behavior without vs with code-mode.</p>

<video controls="">
  <source src="/assets/videos/code-mode-demo.mp4" type="video/mp4" />
</video>

<h2 id="limits-bypassed">Limits bypassed</h2>

<p>With Code-Mode’s progressive discovery integrated into <strong>remote-mcp-adapter</strong>, teams can configure as many upstreams
as they want (within their infra limits, of course). The latest 
<a href="https://github.com/aakashH242/remote-mcp-adapter/releases/tag/v0.2.0">v0.2.0</a> release of 
<a href="https://github.com/aakashH242/remote-mcp-adapter">remote-mcp-adapter</a> now includes Code-Mode. Let’s see how it fares.</p>]]></content><author><name>Aakash</name></author><category term="blog" /><summary type="html"><![CDATA[TL;DR: Too many MCP tools in the context window slow agents down and worsen tool selection. GitHub tackles this with clustering and embedding-guided routing. In remote-mcp-adapter, Code-Mode avoids the problem by letting agents discover tools progressively instead of loading them all upfront.]]></summary></entry><entry><title type="html">Remote MCPs as local</title><link href="https://aakashh242.github.io/blog/2026/03/05/remote-mcps-as-local.html" rel="alternate" type="text/html" title="Remote MCPs as local" /><published>2026-03-05T00:00:00+00:00</published><updated>2026-03-05T00:00:00+00:00</updated><id>https://aakashh242.github.io/blog/2026/03/05/remote-mcps-as-local</id><content type="html" xml:base="https://aakashh242.github.io/blog/2026/03/05/remote-mcps-as-local.html"><![CDATA[<blockquote>
  <p><strong>TLDR;</strong> Check out <a href="https://github.com/aakashh242/remote-mcp-adapter">remote-mcp-adapter</a> which provides stateful proxies for upstream MCP servers and handles the file exchange interaction with “file-touching” tools.</p>
</blockquote>

<p>Recently, a use-case came along where we were tasked with hosting a central MCP platform. The idea was to bring all MCP
servers under a common umbrella, apply guardrails and governance on them and make it the go-to place for all things
MCP in the org. And it made sense, given the dangers unverified MCP servers in the wild pose. If we could provide
most of the tools teams needed and enabled a secure self-service model to add more servers,
we could push teams to use the org-approved MCP platform.</p>

<h2 id="the-problem">The problem</h2>

<p>Although mainly built for local usage, most MCP servers did implement the Streamable HTTP protocol, allowing them to
be hosted remotely. We did not run into many hiccups till we got around to adding servers that work with files -
either consume or produce them. An example is the <a href="https://github.com/microsoft/playwright-mcp">Playwright MCP</a> server
that can produce artifacts in the form of console logs, screenshots, saved PDFs and consume local files for browser
uploads.</p>

<p>While other tools worked as expected, problems arose with the file-touching tools. Since the server and the agent
did not share a filesystem, artifacts generated would never reach the agent and whenever the agent needed to upload
files, the server would not find it.</p>

<h2 id="mcp-constructs-to-the-rescue">MCP constructs to the rescue</h2>

<p>The MCP specs define a construct called <a href="https://modelcontextprotocol.io/specification/2025-06-18/server/resources">resources</a>
to share data that provides context to language models. They are perfect for sharing the server generated artifacts with
the agents. However, file uploads require special handling too as the MCP specification
<a href="https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1306">does not support</a> file uploads via <a href="https://modelcontextprotocol.io/specification/draft/client/elicitation">elicitation</a> yet.</p>

<p>I initially wrote a wrapper that acted as a proxy between the agent and the Playwright MCP server but pretty soon, need
arose to host many more of these types of “file-touching” MCP servers centrally. As a result, instead of writing separate
proxies for each, I wrote the <a href="https://github.com/aakashh242/remote-mcp-adapter">remote-mcp-adapter</a> which provides
stateful proxies for upstream MCP servers and handles the file exchange interaction with “file-touching” tools.</p>

<p>Being a consumer of opensource, I am hopeful it will be beneficial to the community.</p>]]></content><author><name>Aakash</name></author><category term="blog" /><summary type="html"><![CDATA[TLDR; Check out remote-mcp-adapter which provides stateful proxies for upstream MCP servers and handles the file exchange interaction with “file-touching” tools.]]></summary></entry></feed>