An MCP tool can return far more text than an agent needs to answer one question. The useful number is not the size of the server response alone: it is what the host actually sends to the model, what the provider reports for every turn, and whether the final answer still matches the records. This guide gives developers who control a local, read-only MCP server a way to measure all three.
The big change
- What changed: The current MCP TypeScript SDK v2 supports a local stdio server and client with validated tool schemas. A host can choose which returned content enters a Claude Messages tool result.
- Why it matters: Filtering, server-side counting, record pagination, and selective host context change what reaches the model. The revision-bound application cache reuses server results; a cache hit alone does not shorten that result. Their combined token effect is an experiment, not a promised saving.
- What to watch: A smaller response can omit a later-page record, lose a caveat, or require extra model turns. Check the answer and the full usage record before calling an optimization successful.
The example below uses the MCP TypeScript SDK v2 server and client and Claude Messages tool calls. BIG CHANGE compiled and ran a local artificial fixture on 21 October 2026. We did not send records to a model, make a paid API call, or measure a token saving. The API comparison is for you to run with records you are allowed to use.
1. Freeze the records and write the answer key
You need Node.js 20 or newer, an approved Claude Messages API model and credential, and a small budget you are willing to spend. Run this in a fresh local directory. This is a local stdio connection; it creates no remote MCP service. The example accepts at most 100 records, each with a unique string id, string revision, and other flat string fields of at most 4,000 characters. Keep secrets and personal data out of the snapshot unless your organization has approved sending those fields to the model. The private run log includes questions, tool arguments, and answers; do not publish it unreviewed.
Create snapshot.json from a reader-authorized, read-only issue or incident export. Preserve its exact bytes for both runs. Give each record a stable ID and revision. In fields.json, write a JSON array of the exact field names the server may return, including id and revision. The baseline and optimized tools use the same allowlist. Use enough records that the later-page question really needs a sixth or later matching result: the application page size here is five. No sample customer records are supplied as reported evidence.
Create tasks.json as a JSON array with exactly three entries. Each entry needs kind, question, expectedAnswer, evidenceIds, and requiredCaveats; the lookup entry also needs detailField, the exact allowed field that must be retrieved after a compact search. The aggregate entry needs aggregateField and aggregateValue; its expectedAnswer is the decimal count as a string. Use lookup, aggregate, and later_page once each. Write the human-checked answer and the exact supporting IDs before the model run. The expectedAnswer must be the exact string you expect in the model's JSON answer; the caveats array contains exact required strings. Make the lookup question require a fact in detailField that a compact find_records result omits. Its keyed ID must appear in that brief result before the exact detail call. The optimized answer passes its tool-path check only when find_records requests brief fields first and a later exact_record call retrieves a keyed ID. Choose an aggregate question expressible as an exact count of one recorded field value. The host independently counts matching snapshot rows before any model call and rejects an incorrect aggregate answer key. For the later-page question, make its evidence fall after the first five matching rows in ID order. The automated check requires an actual continuation call for that case; inspect the full records yourself before accepting its answer.
Record the source system, export time, field permissions, and who checked the keys in your own private notes. The program hashes the snapshot, field allowlist, and task file bytes and stops when any of them change. Hashing proves the two runs used the same file; it does not prove the export was complete or authorized.
2. Install the pinned local example
Create the files in the code sections below under src/, plus package.json and tsconfig.json in the directory root. After copying the code into a fresh directory, npm install --package-lock-only generates a lockfile from these pinned direct dependencies, then npm ci installs it. Commit or retain the lockfile with your private experiment. We checked the installed signatures of MCP client/server 2.3.1 and Zod 4.6.7. The older Zod 4.1.12 did not satisfy this SDK's JSON schema type contract during local compilation.
{"name":"mcp-measured-context-example","private":true,"type":"module","engines":{"node":">=20"},"scripts":{"check":"tsc --noEmit","fixture":"tsx src/fixture-check.ts","run":"tsx src/host.ts"},"dependencies":{"@anthropic-ai/sdk":"0.67.0","@modelcontextprotocol/client":"2.3.1","@modelcontextprotocol/server":"2.3.1","zod":"4.6.7"},"devDependencies":{"@types/node":"24.8.1","tsx":"4.20.6","typescript":"5.9.3"}}{"compilerOptions":{"target":"ES2022","module":"NodeNext","moduleResolution":"NodeNext","strict":true,"esModuleInterop":true,"skipLibCheck":true,"resolveJsonModule":true,"noEmit":true},"include":["src/**/*.ts"]}Run npm install --package-lock-only, npm ci, npm run check, then npm run fixture. The fixture uses artificial T records strictly as software test input. Its PASS line confirms a local MCP handshake, catalog, baseline list, two data pages, a cache hit, an independently counted aggregate, exact lookup, and error returns. It says nothing about your records or Claude's answers. Keep stdout free for MCP protocol traffic in the server; diagnostics go to stderr.
3. Use the frozen server and task-specific tools
The baseline exposes a broad list_records tool plus exact_record. The optimized server exposes find_records, count_by_field, and the same exact lookup. Filtering accepts only field names present in the authorized snapshot. The server returns IDs and revision with projected rows, a total match count, moreAvailable, and a snapshot-bound nextCursor. It binds cursors to the snapshot and exact query, rejecting invalid, stale, or cross-query cursors instead of silently restarting or skipping rows. An aggregate carries its definition and a bounded, pageable audit ID trail, so a count is not detached from its source.
The cache lives in the server process for 60 seconds and keys the exact query, selected fields, cursor, and source revision. The server checks the source and allowlist bytes again on every call and clears the cache if either changed. This is an application result cache, separate from Claude prompt caching. Each question starts a fresh server session and cold application cache. A separate fresh-server warm-cache probe makes the same direct tool query twice and is excluded from model token totals. listTools() pagination concerns the advertised tool catalog; nextCursor below pages record data returned by callTool.
The code for src/data.ts follows.
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
export type Row = { id: string; revision: string; [key: string]: string };
export type Snapshot = { rows: Row[]; hash: string; revision: string; fields: string[] };
export const sha = (v: string | Buffer) => createHash('sha256').update(v).digest('hex');
export function loadSnapshot(path: string): Snapshot {
const bytes = readFileSync(path);
const parsed: unknown = JSON.parse(bytes.toString('utf8'));
if (!Array.isArray(parsed) || parsed.length < 2 || parsed.length > 100) throw Error('snapshot must contain 2..100 records');
const rows = parsed as Row[];
const ids = new Set<string>();
const fields = new Set<string>();
for (const r of rows) {
if (!r || typeof r !== 'object' || typeof r.id !== 'string' || !r.id || typeof r.revision !== 'string' || !r.revision || ids.has(r.id)) throw Error('invalid or duplicate id/revision');
ids.add(r.id);
for (const [k, v] of Object.entries(r)) {
if (typeof v !== 'string' || v.length > 4000 || !/^[a-zA-Z][a-zA-Z0-9_]*$/.test(k)) throw Error('invalid field or value');
fields.add(k);
}
}
rows.sort((a,b) => a.id.localeCompare(b.id));
const hash = sha(bytes);
return { rows, hash, revision: hash.slice(0,16), fields: [...fields].sort() };
}
export function canonical(v: unknown): string { return JSON.stringify(v); }
export function cursor(index: number, hash: string, scope: string): string { return Buffer.from(canonical({index,hash,scope})).toString('base64url'); }
export function decodeCursor(v: string | undefined, hash: string, scope: string, max: number): number {
if (!v) return 0;
let x: any;
try { x = JSON.parse(Buffer.from(v,'base64url').toString('utf8')); } catch { throw Error('INVALID_CURSOR'); }
if (!x || x.hash !== hash) throw Error('STALE_CURSOR');
if (x.scope !== scope) throw Error('CURSOR_QUERY_MISMATCH');
if (!Number.isInteger(x.index) || x.index < 0 || x.index >= max || cursor(x.index,hash,scope) !== v) throw Error('INVALID_CURSOR');
return x.index;
}The code for src/server.ts follows.
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import { z } from 'zod/v4';
import { readFileSync } from 'node:fs';
import { loadSnapshot, cursor, decodeCursor, canonical, sha, type Row } from './data.js';
const path = process.env.SNAPSHOT_PATH;
const mode = process.env.MODE;
if (!path || !['baseline','optimized'].includes(mode ?? '')) throw Error('SNAPSHOT_PATH and MODE required');
const source = loadSnapshot(path);
if (!process.env.FIELDS_PATH) throw Error('FIELDS_PATH required');
const allowedFields: unknown = JSON.parse(readFileSync(process.env.FIELDS_PATH,'utf8'));
const fieldsHash=sha(readFileSync(process.env.FIELDS_PATH));
if (!Array.isArray(allowedFields) || !allowedFields.every(f=>typeof f==='string' && source.fields.includes(f)) || !allowedFields.includes('id') || !allowedFields.includes('revision')) throw Error('INVALID_ALLOWLIST');
const allowed = allowedFields as string[];
if (process.env.EXPECTED_SHA256 && process.env.EXPECTED_SHA256 !== source.hash) throw Error('SOURCE_CHANGED');
if (process.env.EXPECTED_FIELDS_SHA256 && process.env.EXPECTED_FIELDS_SHA256 !== fieldsHash) throw Error('ALLOWLIST_CHANGED');
const pageSize = 5;
const ttlMs = 60_000;
const cache = new Map<string,{ expires: number; value: unknown }>();
const output = (value: unknown) => ({ content: [{ type:'text' as const, text: canonical(value) }], structuredContent: value as Record<string,unknown> });
const failure = (e: unknown) => ({ isError: true, content: [{ type:'text' as const, text: e instanceof Error ? e.message : 'UNKNOWN_ERROR' }] });
function fields(names: string[]) {
if (names.some(n => !allowed.includes(n))) throw Error('INVALID_FIELD');
return [...new Set(['id','revision',...names])];
}
function unchanged() {
if (loadSnapshot(path!).hash !== source.hash) {cache.clear();throw Error('SOURCE_CHANGED');}
if (sha(readFileSync(process.env.FIELDS_PATH!)) !== fieldsHash) {cache.clear();throw Error('ALLOWLIST_CHANGED');}
}
function project(row: Row, names: string[]) { return Object.fromEntries(names.map(n => [n,row[n] ?? null])); }
function memo<T>(key: unknown, fn: () => T): {value:T; cache:'hit'|'miss'} {
unchanged();
const full = canonical({key, revision:source.revision});
const old = cache.get(full);
if (old && old.expires > Date.now()) return { value:old.value as T, cache:'hit' };
const value = fn(); cache.set(full,{expires:Date.now()+ttlMs,value});
return {value,cache:'miss'};
}
serveStdio(() => {
const server = new McpServer({name:'measured-records',version:'1.0.0'}, {capabilities:{tools:{}}});
server.registerTool('exact_record', { description:'Fetch one complete authorized record by exact ID and source revision.', inputSchema:z.object({id:z.string().min(1)}) }, async ({id}) => {
try {unchanged();} catch(e) {return failure(e);}
const row = source.rows.find(r=>r.id===id);
return output({sourceHash:source.hash, sourceRevision:source.revision, found:!!row, row:row ? project(row,allowed) : null});
});
if (mode === 'baseline') {
server.registerTool('list_records', {description:'List every authorized record from the frozen snapshot.',inputSchema:z.object({})}, async () => {try {unchanged();return output({sourceHash:source.hash,sourceRevision:source.revision,total:source.rows.length,rows:source.rows.map(r=>project(r,allowed))});} catch(e) {return failure(e);}});
} else {
server.registerTool('find_records', {description:'Filter authorized records by one exact field value, choose allowed fields, and page by ID. Follow nextCursor until absent.', inputSchema:z.object({matchField:z.string(),matchValue:z.string(),fields:z.array(z.string()).max(20),cursor:z.string().optional()})}, async ({matchField,matchValue,fields:requested,cursor:page}) => {
try {
unchanged();
const allowed = fields([matchField,...requested]);
const found = source.rows.filter(r=>r[matchField]===matchValue);
const scope=sha(canonical({tool:'find',matchField,matchValue,allowed}));
const start = decodeCursor(page,source.hash,scope,found.length);
const {value,cache:disposition} = memo({tool:'find',matchField,matchValue,allowed,cursor:page ?? null},()=>({sourceHash:source.hash,sourceRevision:source.revision,totalMatches:found.length,rows:found.slice(start,start+pageSize).map(r=>project(r,allowed)),moreAvailable:start+pageSize<found.length,nextCursor:start+pageSize<found.length?cursor(start+pageSize,source.hash,scope):null}));
return output({...value,cache:disposition});
} catch(e) { return failure(e); }
});
server.registerTool('count_by_field', {description:'Count all authorized records with one exact field value; returns bounded audit IDs in pages.',inputSchema:z.object({field:z.string(),value:z.string(),cursor:z.string().optional()})}, async ({field,value,cursor:page}) => {
try {
unchanged();
fields([field]); const found=source.rows.filter(r=>r[field]===value);
const scope=sha(canonical({tool:'count',field,value}));
const start=decodeCursor(page,source.hash,scope,found.length);
const {value:result,cache:disposition}=memo({tool:'count',field,value,cursor:page ?? null},()=>({sourceHash:source.hash,sourceRevision:source.revision,definition:{field,value},count:found.length,auditIds:found.slice(start,start+pageSize).map(r=>r.id),moreAvailable:start+pageSize<found.length,nextCursor:start+pageSize<found.length?cursor(start+pageSize,source.hash,scope):null}));
return output({...result,cache:disposition});
} catch(e) { return failure(e); }
});
}
return server;
},{onerror:e=>console.error(e)});4. Run the same three questions through one host
The host advertises only the tools available in that condition, with their schema. It preserves the same model ID, system instruction, max_tokens, snapshot, and three questions across baseline and optimized runs. Each tool_use receives exactly one tool_result with the matching ID, including an error flag if MCP failed. The host appends turns in order and accepts only end_turn; a refusal, max_tokens, timeout, protocol error, missing JSON, or missing answer-key evidence is recorded as failure. The host does not convert an error into a complete answer.
The optimized host removes the server's cache disposition and full source hash from model-facing text but keeps source revision, rows, IDs, audit trail, completeness flag, and cursor. It records raw result.content bytes and hash separately from the text it sends to Claude. The server also returns structuredContent, but this host builds model input and its log from result.content. Returning structuredContent alongside full text does not itself reduce model context. A smaller field projection is only safe when the exact lookup can recover required detail and the answer-key check catches omitted evidence.
The code for src/host.ts follows.
import Anthropic from '@anthropic-ai/sdk';
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
import { readFileSync, mkdirSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { sha, loadSnapshot } from './data.js';
type Kind = 'lookup'|'aggregate'|'later_page';
type Task = {kind:Kind; question:string; expectedAnswer:string; evidenceIds:string[]; requiredCaveats:string[]; detailField?:string; aggregateField?:string; aggregateValue?:string};
const snapshotPath=resolve(process.env.SNAPSHOT_PATH ?? 'snapshot.json');
const fieldsPath=resolve(process.env.FIELDS_PATH ?? 'fields.json');
const tasksPath=resolve(process.env.TASKS_PATH ?? 'tasks.json');
const runsDir=resolve(process.env.RUNS_DIR ?? 'runs');
mkdirSync(runsDir,{recursive:true});
const file=resolve(runsDir,`${new Date().toISOString().replace(/[:.]/g,'-')}.json`);
const run:any={status:'starting',warning:'Private local run; raw records, questions, tool arguments and answers may be sensitive. No credentials are intentionally saved.',results:[],errors:[]};
function persist() {writeFileSync(file,JSON.stringify(run,null,2));}
function errorText(e:unknown) {return e instanceof Error ? e.message : String(e);}
persist();
const maxTokens=1024;
const system='Answer only from the frozen authorized records. Use tools to inspect all necessary pages and exact detail. For an optimized exact lookup, call find_records with brief fields first, then exact_record for the detail. Return JSON only: {"answer":"...","evidenceIds":["..."],"caveats":["..."]}. If evidence is missing or a tool fails, say so; never guess.';
async function main() {
const snapshotBytes=readFileSync(snapshotPath), fieldsBytes=readFileSync(fieldsPath), tasksBytes=readFileSync(tasksPath);
const source=loadSnapshot(snapshotPath);
const inputHashes={snapshot:sha(snapshotBytes),fields:sha(fieldsBytes),tasks:sha(tasksBytes)};
if (source.hash!==inputHashes.snapshot) throw Error('SOURCE_CHANGED_DURING_STARTUP');
const allowedFields:unknown=JSON.parse(fieldsBytes.toString('utf8'));
if (!Array.isArray(allowedFields) || !allowedFields.every(f=>typeof f==='string' && source.fields.includes(f)) || !allowedFields.includes('id') || !allowedFields.includes('revision') || new Set(allowedFields).size!==allowedFields.length) throw Error('INVALID_ALLOWLIST');
const parsed:unknown=JSON.parse(tasksBytes.toString('utf8'));
const ids=new Set(source.rows.map(r=>r.id));
if (!Array.isArray(parsed) || parsed.length!==3 || new Set(parsed.map(t=>t?.kind)).size!==3 || !['lookup','aggregate','later_page'].every(k=>parsed.some(t=>t?.kind===k))) throw Error('TASK_KINDS_REQUIRED');
const tasks=parsed as Task[];
for (const t of tasks) {
if (!t || typeof t!=='object' || typeof t.question!=='string' || !t.question.trim() || typeof t.expectedAnswer!=='string' || !t.expectedAnswer.trim() || !Array.isArray(t.evidenceIds) || !t.evidenceIds.length || !t.evidenceIds.every(id=>typeof id==='string' && ids.has(id)) || !Array.isArray(t.requiredCaveats) || !t.requiredCaveats.every(c=>typeof c==='string' && !!c.trim())) throw Error(`INVALID_TASK_${t?.kind ?? 'unknown'}`);
if (t.kind==='lookup' && (typeof t.detailField!=='string' || !allowedFields.includes(t.detailField) || ['id','revision'].includes(t.detailField))) throw Error('LOOKUP_DETAIL_FIELD_REQUIRED');
if (t.kind==='aggregate') {
if (typeof t.aggregateField!=='string' || !allowedFields.includes(t.aggregateField) || typeof t.aggregateValue!=='string') throw Error('AGGREGATE_DEFINITION_REQUIRED');
if (t.expectedAnswer!==String(source.rows.filter(r=>r[t.aggregateField!]===t.aggregateValue).length)) throw Error('AGGREGATE_KEY_MISMATCH');
}
}
const model=process.env.CLAUDE_MODEL;
if (!model || !process.env.ANTHROPIC_API_KEY) throw Error('Set CLAUDE_MODEL and ANTHROPIC_API_KEY; this command makes billable calls');
const packageVersion=(name:string)=>JSON.parse(readFileSync(resolve('node_modules',name,'package.json'),'utf8')).version as string;
run.inputs={paths:{snapshot:snapshotPath,fields:fieldsPath,tasks:tasksPath},sha256:inputHashes,recordCount:source.rows.length,allowedFields};
run.runtime={node:process.version,mcpClientPackage:packageVersion('@modelcontextprotocol/client'),mcpServerPackage:packageVersion('@modelcontextprotocol/server'),anthropicSdkPackage:packageVersion('@anthropic-ai/sdk'),zodPackage:packageVersion('zod'),lockfileSha256:sha(readFileSync('package-lock.json')),model,maxTokens,systemSha256:sha(system)};
run.status='running';persist();
const anthropic=new Anthropic({timeout:30_000,maxRetries:0});
const assertInputs=()=>{
if (sha(readFileSync(snapshotPath))!==inputHashes.snapshot) throw Error('SOURCE_CHANGED');
if (sha(readFileSync(fieldsPath))!==inputHashes.fields) throw Error('ALLOWLIST_CHANGED');
if (sha(readFileSync(tasksPath))!==inputHashes.tasks) throw Error('TASKS_CHANGED');
};
const open=async (mode:'baseline'|'optimized')=>{
assertInputs();
const transport=new StdioClientTransport({command:process.execPath,args:[resolve('node_modules/tsx/dist/cli.mjs'),resolve('src/server.ts')],cwd:process.cwd(),env:{...process.env,SNAPSHOT_PATH:snapshotPath,FIELDS_PATH:fieldsPath,MODE:mode,EXPECTED_SHA256:inputHashes.snapshot,EXPECTED_FIELDS_SHA256:inputHashes.fields} as Record<string,string>,stderr:'inherit'});
const client=new Client({name:'measured-host',version:'1.0.0'});
try {await client.connect(transport);} catch(e) {await client.close().catch(()=>{});throw e;}
return client;
};
let abort=false;
for (const mode of ['baseline','optimized'] as const) for (const task of tasks) {
const record:any={mode,task:task.kind,question:task.question,status:abort?'skipped':'running',turns:[],sourceHash:source.hash,sourceRevision:source.revision};
if (task.kind==='aggregate') record.independentCount=source.rows.filter(r=>r[task.aggregateField!]===task.aggregateValue).length;
run.results.push(record);persist();
if (abort) {record.failure='PRIOR_FAILURE';persist();continue;}
let client:Client|undefined;
const started=Date.now();
try {
assertInputs();
client=await open(mode); // Fresh server and application cache for every question.
const listing=await client.listTools(); // This pages the catalog, not records.
const tools=listing.tools.filter(t=>mode==='baseline' ? ['list_records','exact_record'].includes(t.name) : ['find_records','count_by_field','exact_record'].includes(t.name)).map(t=>({name:t.name,description:t.description ?? '',input_schema:t.inputSchema}));
record.tools=tools;record.schemaHash=sha(JSON.stringify(tools));record.protocolVersion=client.getNegotiatedProtocolVersion();record.serverApplicationVersion=client.getServerVersion();persist();
const messages:any[]=[{role:'user',content:task.question}];
for (let n=0;n<12;n++) {
assertInputs();
if (process.env.OFFLINE_FAIL_FIRST_TASK==='1' && mode==='baseline' && task.kind===tasks[0].kind) throw Error('OFFLINE_TEST_FAILURE');
const modelStarted=Date.now();
const response:any=await anthropic.messages.create({model,max_tokens:maxTokens,system,messages,tools:tools as any});
const u=response.usage;
const turn:any={modelElapsedMs:Date.now()-modelStarted,stopReason:response.stop_reason,usage:{input_tokens:u.input_tokens,cache_creation_input_tokens:u.cache_creation_input_tokens ?? 0,cache_read_input_tokens:u.cache_read_input_tokens ?? 0,output_tokens:u.output_tokens},tools:[]};
record.turns.push(turn);persist();
assertInputs();
if (response.stop_reason==='end_turn') {
try {record.answer=JSON.parse(response.content.filter((b:any)=>b.type==='text').map((b:any)=>b.text).join(''));}
catch {record.failure='NON_JSON_ANSWER';}
break;
}
if (response.stop_reason!=='tool_use') {record.failure=`STOP_${response.stop_reason ?? 'UNKNOWN'}`;break;}
const uses=response.content.filter((b:any)=>b.type==='tool_use');
if (!uses.length) {record.failure='EMPTY_TOOL_USE';break;}
messages.push({role:'assistant',content:response.content});
const results:any[]=[];
for (const use of uses) {
assertInputs();
const toolStarted=Date.now();
let content='', is_error=false, rawBlocks:unknown[]=[], rawBytes=0, rawHash='';
try {
if (!tools.some(t=>t.name===use.name)) throw Error('UNADVERTISED_TOOL');
const result=await client.callTool({name:use.name,arguments:use.input as Record<string,unknown>},{timeout:10_000});
is_error=!!result.isError;rawBlocks=result.content;rawBytes=Buffer.byteLength(JSON.stringify(result.content));rawHash=sha(JSON.stringify(result.content));
const texts=result.content.filter((b:any)=>b.type==='text').map((b:any)=>b.text);
content=texts.map((text:string)=>{if (mode==='baseline' || result.isError) return text;const {cache,sourceHash,...selected}=JSON.parse(text);return JSON.stringify(selected);}).join('\n');
if (!content) {content='EMPTY_TOOL_CONTENT';is_error=true;}
} catch(e) {content=`TOOL_ERROR: ${errorText(e)}`;is_error=true;}
turn.tools.push({name:use.name,arguments:use.input,elapsedMs:Date.now()-toolStarted,is_error,rawMcpContent:rawBlocks,rawMcpContentBytes:rawBytes,rawMcpContentSha256:rawHash,modelFacingText:content,modelFacingBytes:Buffer.byteLength(content),modelFacingSha256:sha(content)});
results.push({type:'tool_result',tool_use_id:use.id,content,is_error});
persist();
if (is_error) record.failure='TOOL_FAILURE';
}
messages.push({role:'user',content:results});
if (record.failure) break;
}
if (!record.answer && !record.failure) record.failure='TURN_LIMIT';
assertInputs();
const calls=record.turns.flatMap((t:any)=>t.tools);
let pathOkay=mode==='baseline';
if (mode==='optimized' && task.kind==='aggregate') pathOkay=calls.some((c:any)=>c.name==='count_by_field');
if (mode==='optimized' && task.kind==='later_page') pathOkay=calls.some((c:any)=>c.name==='find_records' && c.arguments?.cursor);
if (mode==='optimized' && task.kind==='lookup') {
const brief=calls.findIndex((c:any)=>{
if (c.name!=='find_records' || c.arguments?.matchField===task.detailField || c.arguments?.fields?.includes(task.detailField) || c.is_error) return false;
try {return JSON.parse(c.modelFacingText).rows.some((r:any)=>task.evidenceIds.includes(r.id));} catch {return false;}
});
pathOkay=brief>=0 && calls.slice(brief+1).some((c:any)=>c.name==='exact_record' && task.evidenceIds.includes(c.arguments?.id));
}
const answer=record.answer;
record.quality=!!answer && !record.failure && pathOkay && answer.answer===task.expectedAnswer && Array.isArray(answer.evidenceIds) && task.evidenceIds.every(id=>answer.evidenceIds.includes(id)) && Array.isArray(answer.caveats) && task.requiredCaveats.every(c=>answer.caveats.includes(c));
if (!record.quality && !record.failure) record.failure='ANSWER_KEY_OR_TOOL_PATH_MISMATCH';
} catch(e) {record.failure=`RUNTIME_ERROR: ${errorText(e)}`;record.quality=false;}
finally {
if (client) try {await client.close();} catch(e) {record.failure=[record.failure,`CLOSE_ERROR: ${errorText(e)}`].filter(Boolean).join('; ');record.quality=false;}
record.elapsedMs=Date.now()-started;
record.totalInputTokens=record.turns.reduce((s:number,t:any)=>s+t.usage.input_tokens+t.usage.cache_creation_input_tokens+t.usage.cache_read_input_tokens,0);
record.totalOutputTokens=record.turns.reduce((s:number,t:any)=>s+t.usage.output_tokens,0);
record.status=record.failure?'failed':'passed';persist();
}
if (record.failure) abort=true;
}
if (!abort) {
const probe:any={mode:'optimized',task:'warm_cache_probe_only',status:'running',note:'Separate fresh server; direct tool calls excluded from model comparison.'};run.results.push(probe);persist();
let client:Client|undefined;
try {
assertInputs();client=await open('optimized');
const args={field:'id',value:source.rows[0].id};
const a=await client.callTool({name:'count_by_field',arguments:args});const b=await client.callTool({name:'count_by_field',arguments:args});
const read=(r:any)=>r.isError?{error:r.content}:JSON.parse(r.content[0].text);
probe.first=read(a);probe.repeat=read(b);
if (a.isError || b.isError || probe.first.cache!=='miss' || probe.repeat.cache!=='hit') throw Error('CACHE_PROBE_FAILED');
probe.status='passed';
} catch(e) {probe.status='failed';probe.failure=errorText(e);abort=true;}
finally {if(client) try {await client.close();} catch(e) {probe.status='failed';probe.failure=[probe.failure,`CLOSE_ERROR: ${errorText(e)}`].filter(Boolean).join('; ');abort=true;}persist();}
} else {run.results.push({mode:'optimized',task:'warm_cache_probe_only',status:'skipped',failure:'PRIOR_FAILURE'});persist();}
run.status=abort?'failed':'completed';persist();
}
try {await main();}
catch(e) {run.errors.push({at:new Date().toISOString(),message:errorText(e)});run.status='failed';process.exitCode=1;}
finally {persist();console.log(file);if (run.status!=='completed') process.exitCode=1;}The local check code for src/fixture-check.ts follows. Its artificial values are test inputs only; an injected pre-call failure checks receipt persistence without contacting a model.
import { mkdtempSync, writeFileSync, rmSync, readFileSync, readdirSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
import { loadSnapshot, cursor } from './data.js';
// Artificial software test input only. It is not a reported business result or model run.
const dir=mkdtempSync(join(tmpdir(),'mcp-context-check-'));
const file=join(dir,'rows.json');
const fieldsFile=join(dir,'fields.json');
writeFileSync(file,JSON.stringify(Array.from({length:12},(_,i)=>({id:`T${String(i).padStart(2,'0')}`,revision:'r1',status:i%2?'open':'closed',detail:`test-${i}`}))));
writeFileSync(fieldsFile,JSON.stringify(['id','revision','status','detail']));
const src=loadSnapshot(file);
async function connect(mode:string) {
const transport=new StdioClientTransport({command:process.execPath,args:[resolve('node_modules/tsx/dist/cli.mjs'),resolve('src/server.ts')],cwd:process.cwd(),env:{...process.env,SNAPSHOT_PATH:file,FIELDS_PATH:fieldsFile,MODE:mode} as Record<string,string>,stderr:'inherit'});
const client=new Client({name:'fixture-check',version:'1.0.0'}); await client.connect(transport); return client;
}
try {
const c=await connect('optimized');
try {
const names=(await c.listTools()).tools.map(t=>t.name);
assert.deepEqual(names.sort(),['count_by_field','exact_record','find_records']);
const call=async (name:string,args:Record<string,unknown>)=>await c.callTool({name,arguments:args});
const unpack=(r:any)=>JSON.parse(r.content[0].text);
const first=unpack(await call('find_records',{matchField:'status',matchValue:'open',fields:['detail']}));
assert.equal(first.rows.length,5); assert.equal(first.totalMatches,6); assert.equal(first.moreAvailable,true);
assert.equal(first.rows[0].detail,'test-1');
const second=unpack(await call('find_records',{matchField:'status',matchValue:'open',fields:['detail'],cursor:first.nextCursor}));
assert.equal(second.rows.length,1); assert.equal(second.moreAvailable,false);
const repeat=unpack(await call('find_records',{matchField:'status',matchValue:'open',fields:['detail']})); assert.equal(repeat.cache,'hit');
const count=unpack(await call('count_by_field',{field:'status',value:'open'})); assert.equal(count.count,6); assert.equal(count.auditIds.length,5);
const audit=unpack(await call('count_by_field',{field:'status',value:'open',cursor:count.nextCursor})); assert.equal(audit.auditIds.length,1);
const exact=unpack(await call('exact_record',{id:'T11'})); assert.equal(exact.row.detail,'test-11');
const missing=unpack(await call('exact_record',{id:'absent'})); assert.equal(missing.found,false);
assert.equal((await call('find_records',{matchField:'secret',matchValue:'x',fields:[]})).isError,true);
assert.equal((await call('find_records',{matchField:'status',matchValue:'open',fields:[],cursor:'bad'})).isError,true);
assert.equal((await call('find_records',{matchField:'status',matchValue:'open',fields:[],cursor:cursor(5,'old-hash','old-scope')})).isError,true);
assert.equal((await call('find_records',{matchField:'status',matchValue:'open',fields:[],cursor:first.nextCursor})).isError,true); // selected fields differ
assert.equal(src.rows.filter(r=>r.status==='open').length,count.count);
const original=readFileSync(file);
writeFileSync(file,original.toString('utf8').replace('test-0','test-changed'));
assert.equal((await call('find_records',{matchField:'status',matchValue:'open',fields:[]})).isError,true);
writeFileSync(file,original);
const originalFields=readFileSync(fieldsFile);
writeFileSync(fieldsFile,JSON.stringify(['id','revision','status']));
assert.equal((await call('count_by_field',{field:'status',value:'open'})).isError,true);
writeFileSync(fieldsFile,originalFields);
} finally {await c.close();}
const b=await connect('baseline');
try {assert.deepEqual((await b.listTools()).tools.map(t=>t.name).sort(),['exact_record','list_records']); const r=await b.callTool({name:'list_records',arguments:{}}); assert.equal(JSON.parse((r.content[0] as any).text).rows.length,12);} finally {await b.close();}
const tasksFile=join(dir,'tasks.json'),runsDir=join(dir,'runs');
writeFileSync(tasksFile,JSON.stringify([
{kind:'lookup',question:'test lookup',expectedAnswer:'test',evidenceIds:['T01'],requiredCaveats:[],detailField:'detail'},
{kind:'aggregate',question:'test aggregate',expectedAnswer:'6',aggregateField:'status',aggregateValue:'open',evidenceIds:['T01'],requiredCaveats:[]},
{kind:'later_page',question:'test page',expectedAnswer:'test',evidenceIds:['T11'],requiredCaveats:[]}
]));
const failed=spawnSync(process.execPath,[resolve('node_modules/tsx/dist/cli.mjs'),resolve('src/host.ts')],{cwd:process.cwd(),encoding:'utf8',env:{...process.env,SNAPSHOT_PATH:file,FIELDS_PATH:fieldsFile,TASKS_PATH:tasksFile,RUNS_DIR:runsDir,CLAUDE_MODEL:'offline-test',ANTHROPIC_API_KEY:'offline-test',OFFLINE_FAIL_FIRST_TASK:'1'}});
assert.equal(failed.status,1);
const run=JSON.parse(readFileSync(join(runsDir,readdirSync(runsDir)[0]),'utf8'));
assert.equal(run.status,'failed');assert.equal(run.results.length,7);
assert.match(run.results[0].failure,/OFFLINE_TEST_FAILURE/);
assert.equal(run.results[1].status,'skipped');assert.equal(run.results[6].status,'skipped');
assert.equal(run.runtime.mcpClientPackage,'2.3.1');assert.equal(run.runtime.mcpServerPackage,'2.3.1');
const startupRuns=join(dir,'startup-runs');
const startup=spawnSync(process.execPath,[resolve('node_modules/tsx/dist/cli.mjs'),resolve('src/host.ts')],{cwd:process.cwd(),encoding:'utf8',env:{...process.env,SNAPSHOT_PATH:file,FIELDS_PATH:fieldsFile,TASKS_PATH:tasksFile,RUNS_DIR:startupRuns,CLAUDE_MODEL:'',ANTHROPIC_API_KEY:''}});
assert.equal(startup.status,1);
const startupReceipt=JSON.parse(readFileSync(join(startupRuns,readdirSync(startupRuns)[0]),'utf8'));
assert.equal(startupReceipt.status,'failed');assert.match(startupReceipt.errors[0].message,/CLAUDE_MODEL/);
console.log('PASS: local MCP handshake, catalog, baseline, pagination, cache, aggregate, detail and failures; no model call');
} finally {rmSync(dir,{recursive:true,force:true});}Set SNAPSHOT_PATH, FIELDS_PATH, and TASKS_PATH to your private files, CLAUDE_MODEL to an available model ID, and ANTHROPIC_API_KEY through your shell's secret mechanism. Then run npm run run. The script makes billable calls. Check current Claude pricing, account limits, and rate limits first; the example itself cannot enforce an account spending ceiling. Stop if you cannot bound the experiment acceptably. Never paste the key into the files or the run log.
5. Read the result as a quality check first
The script creates a private JSON file under runs/ before reading inputs and updates it after each stage. A startup, connection, catalog, model, tool, or close failure leaves a failed receipt. When a task fails, remaining tasks are marked skipped; a startup failure is saved before tasks are created. Neither is a completed comparison. For each task and condition, it records the input file hashes, installed client/server package versions and lockfile hash, negotiated MCP protocol version, server application identity, advertised tool schemas, tool arguments, raw MCP content bytes and hash, model-facing bytes and hash, turn count, elapsed time, stop reason, and provider usage. Total input tokens sum input_tokens, cache_creation_input_tokens, and cache_read_input_tokens over every Messages response; output tokens are summed separately. A prompt-cache read may affect price without shrinking the context represented by the request. Compare current account billing if you estimate charges.
First inspect every quality and failure field against the original records. The exact-answer comparison, required IDs, caveat strings, and required tool path are intentionally strict; a harmless wording difference can fail and needs human review, while a deceptively matching short answer can still be incomplete. Examine later-page traversal and the bounded aggregate audit IDs yourself. If any answer loses a fact, hides a contrary record, or gains extra turns, retain that private failed run, repair the query or rubric, and rerun both conditions against the same frozen snapshot. Compare token and byte totals per task and in total only after both answers pass. Savings can be zero or negative.
For recovery, try an invalid field, an invalid or stale cursor, a missing exact ID, and a changed source revision. The local fixture checks these server paths, including a changed test snapshot; repeat the checks against your own export before drawing conclusions about an updated dataset. A model or tool timeout is a failed run, not an empty match. Rebuild the answer key after an authorized source change and begin a new comparison with a new snapshot hash.
If you are also using a ready-made Claude Code context plugin, our Context Mode installation guide covers that separate setup. This experiment measures a reader-owned MCP server and host; neither guide establishes a universal token reduction.
Sources & further reading
- MCP SDK: first server — local server setup and
serveStdiopattern; the filter, cache, and answer rubric here are our application code. - MCP SDK: connect and call tools — client transport, catalog listing, and result semantics. Catalog pagination is distinct from this guide's record cursor.
- MCP SDK: schema libraries — schema support for tool arguments; field authorization still belongs to the application.
- Claude: handle tool calls — the client tool-use and tool-result loop; the local host must enforce its own quality conditions.
- Claude: prompt caching, pricing, and rate limits — usage interpretation and current reader-account cost checks. No paid result is reported here.



