Zero-Overflow Context Windows: BM25 Progressive Discovery and Large Payload Spillover

Large tool catalogs bloat prompts while oversized tool responses crash context windows. Learn how NitroStack prunes tool schemas by 80% with BM25 indexing and automatically spills heavy payloads into virtualized MCP resources.

Hemant Jadhav
Hemant Jadhav
Zero-Overflow Context Windows hero banner with 3D isometric green and blue storage vaults and blueprint grid

The context window of a Large Language Model is its most precious and fragile resource. Every byte of prompt overhead directly impacts model reasoning capacity, inference speed, and token expense.

When deploying Model Context Protocol (MCP) servers in enterprise environments, engineering teams frequently hit the Dual Context Bottleneck:

  1. The Input Bottleneck (Catalog Bloat): When an MCP server registers 60 or more tools, the JSON schemas emitted during tools/list consume upwards of 15,000 prompt tokens. The model spends significant computational attention simply parsing tool definitions it may never invoke.
  2. The Output Bottleneck (Payload Blowups): A single database query, log extraction tool, or document search can easily return hundreds of kilobytes or megabytes of structured data. When injected raw into the conversation history, this payload overwhelms context limits, triggers unexpected compaction, or causes client connection errors.
text
┌─────────────────────────────────────────────────────────────────────────────┐
│                       THE DUAL CONTEXT BOTTLENECK                           │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   Input Dilemma: Monolithic Catalog Ingestion                               │
│   ┌─────────────────────────────────────────────────────────┐               │
│   │ 80 Tool Schemas (~18,000 Tokens)                        │ ◄─── Prompt   │
│   │ Consumes 70% of available agent reasoning space         │      Bloat    │
│   └─────────────────────────────────────────────────────────┘               │
│                                                                             │
│   Output Dilemma: Unbounded Tool Execution Response                         │
│   ┌─────────────────────────────────────────────────────────┐               │
│   │ Database Tool Returns 4,500 Rows (2.8 MB JSON)          │ ◄─── Context  │
│   │ Crashes conversation buffer or triggers API OOM error   │      Overflow │
│   └─────────────────────────────────────────────────────────┘               │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

In NitroStack 1.1, we tackle both sides of this equation simultaneously. Using BM25 Progressive Tool Discovery for the input stream and the Data Spillover Interceptor for the output stream, NitroStack establishes a zero-overflow boundary around agent context windows.


1. Input Protection: Progressive Tool Discovery with BM25

Rather than injecting every registered tool into the LLM system prompt, BM25SearchTransform transforms your server into an on-demand discovery index.

When enabled, NitroStack replaces dozens of tool schemas with two compact meta-tools:

  • search_tools: Discovers tools by relevance matching against their names, descriptions, and parameter schemas.
  • call_tool: Invokes the discovered tool by passing the tool name and arguments.
typescript
import { McpApp, Module, BM25SearchTransform } from '@nitrostack/core';
import { CustomerModule } from './customer/customer.module';
import { OrdersModule } from './orders/orders.module';
import { InventoryModule } from './inventory/inventory.module';

@McpApp({
  module: AppModule,
  server: { name: 'enterprise-retail-hub', version: '1.1.0' },
  transforms: [
    new BM25SearchTransform({
      maxResults: 5,                  // Return top 5 most relevant tools
      bm25K1: 1.2,                    // Term frequency saturation parameter
      bm25B: 0.75,                    // Document length normalization parameter
      alwaysVisible: ['ping', 'help'],// High-frequency tools kept in root catalog
    }),
  ],
})
@Module({
  imports: [CustomerModule, OrdersModule, InventoryModule],
})
export class AppModule {}

How the Okapi BM25 Ranking Engine Works

Unlike naive substring matching, NitroStack uses the Okapi BM25 probabilistic ranking algorithm. When the server boots, NitroStack constructs an in-memory inverted index of all registered tool descriptors:

$$\text{Score}(D, Q) = \sum_{i=1}^{N} \text{IDF}(q_i) \cdot \frac{f(q_i, D) \cdot (k_1 + 1)}{f(q_i, D) + k_1 \cdot \left(1 - b + b \cdot \frac{|D|}{\text{avgdl}}\right)}$$

  • Token Normalization: Field values are lowercased, tokenized, and stripped of common stop words.
  • Weighted Field Indexing: Tool names receive a 3x weight multiplier, tool descriptions receive a 2x multiplier, and property descriptions receive a 1x multiplier.
  • Inverse Document Frequency (IDF): Rare, highly specific technical terms (such as stripeInvoiceId or kubernetesPodNamespace) receive higher match weight than generic words like get or data.

Catalog Browsing Mode: Exploring Without Keywords

A common failure mode in search-based tool discovery occurs when an LLM does not know what keywords to query. If the agent needs to find billing tools but queries "finance", a strict search might return empty results.

In NitroStack 1.1, the query parameter is strictly optional. When an agent invokes search_tools with an empty or omitted query:

json
{
  "name": "search_tools",
  "arguments": {
    "cursor": 0,
    "limit": 10
  }
}

The engine transitions into Catalog Browsing Mode, returning a paginated directory of available tool categories, namespaces, and high-level descriptions. The agent explores the surface area of the server without guessing vocabulary.


2. Output Protection: Large Payload Spillover Interceptor

Even if prompt input is tightly controlled, a single unconstrained tool execution can instantly blow up the agent's context.

Consider an enterprise log query or an inventory scan. A tool like query_order_history might return 2,000 JSON records totaling 1.8 MB. In a standard MCP implementation, this entire payload is returned in the tools/call response content block.

NitroStack's DataSpilloverInterceptor sits as an execution boundary between your controller logic and the MCP serialization layer:

text
┌─────────────────────────────────────────────────────────────────────────────┐
│                     DATA SPILLOVER INTERCEPTOR WORKFLOW                     │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│   Native @Tool Controller Returns Large Array (e.g., 850 KB JSON)           │
│                                  │                                          │
│                                  ▼                                          │
│   DataSpilloverInterceptor (Threshold: 10 KB)                               │
│                                  │                                          │
│         Payload > 10 KB? ────────┴──────── (Yes)                            │
│                 │                                                           │
│                 ▼                                                           │
│   1. Persist Raw Data to Spillover Store (Memory or Filesystem)              │
│      Key: "spill_8f3d12a9-041a-4c22"                                        │
│                                                                             │
│   2. Register MCP Resource URI:                                             │
│      "resource://data-spillover/spill_8f3d12a9-041a-4c22"                   │
│                                                                             │
│   3. Synthesize Compact LLM Preview (schema shape, item count, sample slice)│
│                                  │                                          │
│                                  ▼                                          │
│   Agent Receives Compact Token-Safe Response (<600 Tokens)                  │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Implementing Spillover Protection in Code

You can apply spillover protection to individual controller methods or register it globally across your server:

typescript
import { Controller, Tool, UseInterceptors } from '@nitrostack/core';
import { DataSpilloverInterceptor, FsSpilloverStore } from '@nitrostack/core/interceptors';

@Controller()
export class WarehouseController {
  @Tool({
    name: 'fetch_warehouse_inventory',
    description: 'Retrieves complete inventory counts across regional hubs',
  })
  @UseInterceptors(
    new DataSpilloverInterceptor({
      maxBytes: 10240, // Trigger spillover on payloads exceeding 10 KB
      store: new FsSpilloverStore({
        storageDirectory: '/var/data/nitrostack/spillover',
        ttlMs: 3600000, // Retain payloads for 1 hour
      }),
      sampleRows: 3, // Include 3 representative sample rows in preview
    })
  )
  async getInventory(input: { region: string }) {
    return this.warehouseService.getHeavyStockReport(input.region);
  }
}

What the LLM Sees: Structured Previews

Instead of 850 KB of unformatted JSON, the agent receives a compact, highly structured preview:

json
{
  "_spillover": true,
  "resourceUri": "resource://data-spillover/spill_91a0c84f-42e1",
  "mimeType": "application/json",
  "totalBytes": 870420,
  "totalItems": 1840,
  "schemaPreview": {
    "sku": "string",
    "quantity": "number",
    "binLocation": "string",
    "lastAuditDate": "string"
  },
  "sampleItems": [
    { "sku": "SKU-9921", "quantity": 140, "binLocation": "A-12", "lastAuditDate": "2026-10-01" },
    { "sku": "SKU-9922", "quantity": 12, "binLocation": "B-04", "lastAuditDate": "2026-10-02" },
    { "sku": "SKU-9923", "quantity": 890, "binLocation": "D-88", "lastAuditDate": "2026-09-28" }
  ],
  "instruction": "Full payload exceeds context threshold and has been offloaded to resourceUri. Call resources/read with resourceUri to inspect complete data or use Code Mode to process the collection."
}

The agent learns everything it needs to reason about the data (its schema shape, item count, and representative values) using less than 400 prompt tokens.

If the agent needs the full dataset, it invokes resources/read for the returned URI, or passes the URI directly into CodeModeTransform to process the collection inside WebAssembly without ever loading the raw rows into prompt tokens.


3. Pluggable Spillover Stores

Different deployment environments require different storage characteristics:

text
┌──────────────────────────────┬──────────────────────────────┬──────────────────────────────┐
│ Storage Engine               │ Implementation               │ Recommended Deployment       │
├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤
│ MemorySpilloverStore         │ High-speed in-memory Map     │ Local testing, ephemeral CLI │
│                              │ with LRU eviction            │ instances, desktop agents    │
├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤
│ FsSpilloverStore             │ Atomic filesystem writes     │ Single-node production VMs,  │
│                              │ with automatic TTL sweep     │ containerized MCP sidecars   │
├──────────────────────────────┼──────────────────────────────┼──────────────────────────────┤
│ Distributed Custom Store     │ Implements                   │ High-availability clusters,  │
│ (Redis / S3 / DynamoDB)      │ SpilloverStore interface     │ multi-replica Kubernetes     │
└──────────────────────────────┴──────────────────────────────┴──────────────────────────────┘

Both built-in stores automatically enforce Time-To-Live (TTL) expiration policies, ensuring expired datasets are cleanly pruned without manual administrative intervention.


4. End-to-End Context Benchmark

To evaluate the combined benefit of input pruning and output spillover, we measured a complex customer support diagnosis task across an enterprise service exposing 75 tools:

text
┌─────────────────────────────────────────────────────────────────────────────┐
│                   END-TO-END CONTEXT FOOTPRINT BENCHMARK                    │
├───────────────────────────────┬──────────────────────┬──────────────────────┤
│ Protocol Phase                │ Standard MCP Server  │ NitroStack 1.1       │
├───────────────────────────────┼──────────────────────┼──────────────────────┤
│ Initial Tool Registration     │ 18,200 tokens        │ 1,420 tokens (BM25)  │
│ Tool Discovery Turn           │ 0 tokens             │ 410 tokens           │
│ Heavy Query Return (1.2 MB)   │ 112,000 tokens (OOM) │ 385 tokens (Spilled) │
│ Follow-up Analysis Prompt     │ Failed / Truncated   │ 1,200 tokens         │
├───────────────────────────────┼──────────────────────┼──────────────────────┤
│ Total Token Footprint         │ >130,000 tokens      │ 3,415 tokens         │
│ Context Reduction             │ Baseline             │ 97.4% Reduction      │
└───────────────────────────────┴──────────────────────┴──────────────────────┘

By filtering what goes into the prompt and intercepting what comes out, NitroStack achieves a 97.4% reduction in overall token consumption, transforming fragile workflows into stable, cost-effective production systems.


Next Steps

Zero-overflow context windows solve the data sizing problem. But in enterprise systems, security requires an equally rigorous discipline: fine-grained tool visibility.

In the next article, we dive into VisibilityTransform and SessionVisibilityStore: how to dynamically expose tools based on user roles, authentication stages, and state-machine transitions.

About the Author

Hemant Jadhav

Hemant Jadhav

Author