<?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-05-03T08:38:57+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">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><entry><title type="html">A skill issue</title><link href="https://aakashh242.github.io/blog/2026/02/27/a-skill-issue.html" rel="alternate" type="text/html" title="A skill issue" /><published>2026-02-27T00:00:00+00:00</published><updated>2026-02-27T00:00:00+00:00</updated><id>https://aakashh242.github.io/blog/2026/02/27/a-skill-issue</id><content type="html" xml:base="https://aakashh242.github.io/blog/2026/02/27/a-skill-issue.html"><![CDATA[<blockquote>
  <p>TL;DR: LLMs can accelerate development dramatically, but they also widen the ownership gap if you don’t understand your system deeply. Without discipline, agents tend to duplicate logic, bloat control files, and create subtle technical debt. Tooling like persistent “skills” helps, but clean architecture is still a developer responsibility.</p>
</blockquote>

<p>Do you remember the old days when there were no LLMs and you wrote the entire code by hand? It took time to deliver but,
you had the bragging rights of knowing the entire system inside out. When something broke, you didn’t just troubleshoot
blindly, you already had a suspicion of where the bug could be.</p>

<p>With the advent of the AI bubble, the pace at which code can be spewed out increased drastically! Now, more people can
<em>produce code</em>. It indeed was a revolution - now, non-tech folks could get a taste of the dopamine hits of seeing your
code work and the frustrations when it didn’t. I mean, it got the general person to a terminal, what more can you ask for? 😄</p>

<p>I’m not going to lie - I use LLMs and coding agents in my workflow. While it has helped clean gaps in my architecture
and ship faster, it has also led to increased time in code review and reading through code and refactoring. As this 
<a href="https://dev.to/ismail9k/once-upon-a-time-writing-code-was-fun-62">article</a> puts it,
<strong>The Ownership Gap in Production</strong> becomes a real problem if you do not understand entirely what your codebase does
and I want to avoid it at all costs. I need to know how my systems work.</p>

<h2 id="repeats-and-monoliths">Repeats and Monoliths</h2>

<p>My experience with AI-assisted development has been mixed. Yes, I enjoy the depth I can go to when hardening architecture
and the pace at which I can implement once I have locked on a spec. The rush of laying out the groundwork then, seeing
your feature get implemented is great. Tests work too, awesome! But now, review the additions and changes and compare
with specs - that’s the boring part.</p>

<p>Even if the implementation matches the spec, what I observed was the changes would be limited to a few files and a lot
of logic would be duplicated across them. Be it improving/fixing an existing feature or adding a new one, the few major
files containing control logic would grow into monoliths with huge functions all over, spaghetti-like call stacks and
repetition of same logic that could be generalized. The time I saved in writing the whole thing will now be spent in
cleaning up. I tried prompting to stay DRY and modular but thanks to context compression, in a few turns, the behavior
returned.</p>

<h2 id="agent-skills"><a href="https://agentskills.io/home">Agent Skills</a></h2>

<p>Since prompts didn’t help much, next step was to look at constructs that stay active in the agent’s working context
window. I looked at Agent Skills and found the <a href="https://github.com/ertugrul-dmr/clean-code-skills/">clean-code-skills</a>
that enforce Robert C. Martin’s <em>Clean Code</em> principles. Unlike prompts that get compressed out of context, 
skills remain active in the agent’s working memory, influencing structure across turns.</p>

<p>I found it really useful and experienced the difference first-hand on the code quality and technical debt when using 
it for a fresh project. However, at times, I found that on existing or larger code bases, agents would tend to 
duplicate functionality and turn existing files into giants by cramming as much as possible into them.</p>

<p>I added an extra skill to target specifically this problem - called it <code class="language-plaintext highlighter-rouge">clean-features</code> - and I found it useful. 
I thought of contributing it back and I have put in a pull request with the owner. 🤞</p>

<p>Meanwhile, if you are interested, you can use my <a href="https://github.com/aakashH242/clean-dry-code-skills">fork</a>. 
I shall keep it synced with the owner’s upstream as I use this skill in my workflow.</p>

<h2 id="parting-thoughts">Parting Thoughts</h2>

<p>In the end, this isn’t about AI. It’s about skills. Agents will focus on finishing tasks, not on making sense. 
If you don’t understand modularity, separation of concerns, and long-term maintenance, you will just create technical 
debt more quickly. AI doesn’t replace discipline; it highlights the problems that come from not having it.</p>

<p><strong>PS</strong> - I have also started using <a href="https://github.com/peteromallet/desloppify">desloppify</a> to monitor and maintain
code quality.</p>]]></content><author><name>Aakash</name></author><category term="blog" /><summary type="html"><![CDATA[TL;DR: LLMs can accelerate development dramatically, but they also widen the ownership gap if you don’t understand your system deeply. Without discipline, agents tend to duplicate logic, bloat control files, and create subtle technical debt. Tooling like persistent “skills” helps, but clean architecture is still a developer responsibility.]]></summary></entry><entry><title type="html">The curse of context windows</title><link href="https://aakashh242.github.io/blog/2026/02/20/the-curse-of-context-windows.html" rel="alternate" type="text/html" title="The curse of context windows" /><published>2026-02-20T00:00:00+00:00</published><updated>2026-02-20T00:00:00+00:00</updated><id>https://aakashh242.github.io/blog/2026/02/20/the-curse-of-context-windows</id><content type="html" xml:base="https://aakashh242.github.io/blog/2026/02/20/the-curse-of-context-windows.html"><![CDATA[<blockquote>
  <p><strong>TL;DR</strong>: Large-document extraction with LLMs fails less from “bad reasoning” and more from hard output limits. JSON structured outputs waste tokens on repeated keys and still truncate on big PDFs. Switching to CSV reduces overhead but doesn’t fix truncation—your output can still cut off silently. The reliable fix is chunking the document into page batches, processing chunks asynchronously with strict concurrency limits (semaphores), and stitching results back in order; run summarization as a separate pass.</p>
</blockquote>

<p>I was working on a problem to extract structured information from large documents on very lean infrastructure.
The input documents were either PDF, CSV or Excel. For CSV and Excel, extraction was pretty straightforward but PDFs
posed a separate challenge of their own.</p>

<p>The PDFs we ingested were mostly a mix of digital and scanned. The moment scanned PDFs come in to the picture, one can
imagine the various edge cases that come with them - image quality, noise, orientation, spillovers etc.</p>

<p>OCR was the obvious option and with so many opensource libraries available, we were spoilt for choices. I 
wanted to use <a href="https://docling-project.github.io/docling/">Docling</a> as my prior experience with it has been good so
far (I shall write a separate blog on those use-cases) but we were constrained by the infra.</p>

<p>Docling uses deep learning to extract structure - something very costly on just CPU. I ran a few tests and it was
evident that we needed another approach - we just cannot wait hours for a document to process.</p>

<h2 id="iteration-1---llm-with-structured-outputs">Iteration 1 - LLM with Structured Outputs</h2>

<p>Latest LLMs are now capable of processing documents, why not use them? After evaluating the major providers and models,
we chose <strong>Gemini 2.5 Flash</strong>. It ticked all the checkboxes we had -</p>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Large context window to support huge PDFs (1 million tokens)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Fast output speed</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Controllable reasoning</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Affordable at scale</li>
</ul>

<p>Our first approach was using structured outputs. The data to extract had to follow a certain schema and also have
a kind of “summary”. Seemed a perfect use-case for outputs following a strict JSON schema.</p>

<p>Well, it worked well for smaller documents. As soon as documents grew beyond a certain size, the output parsing would
fail. Why? Because the output exceeded the model’s maximum allowed output tokens. Setting this to max did not help
either.</p>

<h2 id="iteration-2---why-waste-tokens-on-repeated-extra-characters">Iteration 2 - Why waste tokens on repeated extra characters?</h2>

<p>We realized that a lot of the output tokens were being consumed as JSON field names and characters.
Rows of data, same schema = same identifiers repeated for every row. A real waste, in my view. 
So we took a different route.</p>

<p>Since the extracted data had to follow a certain schema, we took a two-step approach. 
Make the LLM output raw CSV in one pass and make it generate the summary in another parallel pass.
CSV is much condensed than JSON and should solve the parsing problem, right?</p>

<p>Well, it kind of did eliminate the parsing problems and the CSV output would parse successfully too. But on a closer
inspection, it revealed that the problem had not been solved - just masked by the CSV parser. The LLM would return
complete rows but for large documents, rows would suddenly stop. The output was still truncated - we just didn’t catch
it this time as there were no parsing errors.</p>

<h2 id="iteration-3---divide-and-conquer">Iteration 3 - Divide and conquer!</h2>

<p>Taking a leaf from the old-school world of batch automations, we thought - instead of the entire document,
why not pass batches of pages to the LLM? This could enable us to target two birds with one stone:</p>

<ul>
  <li>the issue of truncation</li>
  <li>really slow speeds when processing large documents (the more the LLM has to output, the longer the wait time)</li>
</ul>

<p>So we implemented a pipeline that chunks documents into batches, process
them asynchronously, collect the results and perform order-aware stitching to produce the final output. The separate
summarization pass still stayed.</p>

<p>We tested this on large documents and found that the truncation problem was gone! Now we were able to process large
documents at respectable speeds without the fear of losing data.</p>

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

<ul>
  <li>Structured outputs is good but only when constrained</li>
  <li>For large-scale data extraction using LLMs, not the reasoning capability but the output token limit is your enemy</li>
  <li>Asynchronous processing for I/O heavy tasks can go out of bounds and cause all sorts of issues - from rate-limits to memory spikes. Always have semaphores</li>
  <li>Thinking tokens matter - especially for large documents:
    <ul>
      <li>For one-to-one copy-paste extraction, with a few prompt engineering tricks, you can get the LLM to follow order and output the copy exactly as the source without using thinking tokens</li>
      <li>For extractions which involve copy-paste, transformations and data inference, with vs without thinking tokens produce noticeable differences in the transformed and inferred data</li>
    </ul>
  </li>
</ul>]]></content><author><name>Aakash</name></author><category term="blog" /><summary type="html"><![CDATA[TL;DR: Large-document extraction with LLMs fails less from “bad reasoning” and more from hard output limits. JSON structured outputs waste tokens on repeated keys and still truncate on big PDFs. Switching to CSV reduces overhead but doesn’t fix truncation—your output can still cut off silently. The reliable fix is chunking the document into page batches, processing chunks asynchronously with strict concurrency limits (semaphores), and stitching results back in order; run summarization as a separate pass.]]></summary></entry></feed>