あるとき、Claude Code に 3000 行超の TypeScript ファイルを読ませた。「全部見て、依存関係を教えて」という指示だった。
Claude は丁寧に全文を読んだ。そしてセッションの半分以上のコンテキストを、そのファイル 1 本で消費した。
それ以降「コンテキスト不足」が出るのが早くなった。依存関係を答えてもらう前に、セッションが腐り始めた。これを何度か繰り返して、自分でMCPを作ることにした。
作ったもの: token-guardian MCP
Claude Code のファイル読み取りを 「トークンを意識した読み方」 に変えるMCPサーバー。
ツールは 5 本。
| ツール | 役割 |
|---|---|
read_smart |
閾値を超えたファイルは自動でスケルトン(定義のみ)で返す |
read_fragment |
行範囲を指定してピンポイントで読む |
map_dir_cost |
ディレクトリ全体のトークンコストを一覧表示 |
grep_surgical |
正規表現検索をmax_matches制限付きで実行 |
find_symbol |
関数・クラス・型の定義箇所を直接取得 |
一番使うのは read_smart と map_dir_cost の 2 本。まずこの 2 つで生活が変わった。
read_smart: 1500 トークンを超えたら自動でスケルトンに切り替える
const TOKEN_THRESHOLD = Number(process.env.TOKEN_GUARDIAN_THRESHOLD ?? 1500);
export async function readSmart(input: ReadSmartInput) {
const content = await readFileContent(input.path);
const tokenCount = countTokens(content);
if (input.force_full || tokenCount <= TOKEN_THRESHOLD) {
// フルで返す
return { content: [{ type: "text", text: `[full] ${tokenCount} tokens\n\n${content}` }] };
}
// スケルトンモード: 関数・クラス・型の定義だけ返す
const skeleton = extractSkeleton(input.path, content);
return {
content: [{
type: "text",
text: `[skeleton] ${countTokens(skeleton)} tokens (saved from ${tokenCount})\n\n${skeleton}`
}]
};
}
1500 トークン以下なら全文を返す。超えたら extractSkeleton が定義だけを抽出して返す。
「全部見たい」ときは force_full: true を渡せばいい。Claude はスケルトンを見て「ここを詳しく読みたい」と判断したら read_fragment で行範囲を指定する。自然な 2 ステップになった。
map_dir_cost: 読む前に「どのファイルが重いか」を把握する
[map_dir_cost] /Users/guchi/my-project
Files: 47 | Estimated total: ~185,000 tokens
────────────────────────────────────────────────
src/app/page.tsx 12.3KB ~ 3,000 tok
src/lib/utils.ts 8.1KB ~ 2,000 tok
src/features/auth/auth-service.ts 52.4KB ~ 13,100 tok ⚠️
src/features/data/data-processor.ts 61.2KB ~ 15,300 tok ⚠️
node_modules/... (省略)
⚠️ マークが 1500 トークン超えのファイル。セッション開始時にこれを一発で確認できる。
今まで「なんか重いな」と感覚で感じていたものが、数字で見えるようになった。どのファイルを読ませるべきか、どこから攻めるかが決めやすくなった。
grep_surgical と find_symbol: 広域検索を捨てる
従来の Grep は「キーワードにマッチしたファイルを全部返す」。マッチが多いと数万トークンを軽く消費する。
grep_surgical は max_matches で上限を設定できる。「最初の 5 件だけ見て判断する」という使い方ができる。
find_symbol はさらにシンプル。「getUserById がどこで定義されているか」を知りたいだけなら grep は不要で、シンボル名を渡すだけで定義ブロックが返ってくる。
find_symbol("getUserById") →
// src/lib/user-service.ts:142
export async function getUserById(id: string): Promise<User | null> {
return db.user.findUnique({ where: { id } });
}
これで「定義を確認するためだけに 1 ファイル全部読む」がなくなった。
実装: MCP は意外と 100 行以下で作れる
「MCP サーバーを自作する」と聞くと難しそうに感じた。実際はそうでもなかった。
エントリポイント全体はこれだけ。
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new McpServer({ name: "token-guardian", version: "1.0.0" });
server.tool("read_smart", "...", readSmartSchema.shape, async (input) => readSmart(input));
server.tool("map_dir_cost", "...", mapDirCostSchema.shape, async (input) => mapDirCost(input));
// ...5ツール登録
const transport = new StdioServerTransport();
await server.connect(transport);
@modelcontextprotocol/sdk を入れて、McpServer に server.tool() で登録するだけ。各ツールの実装は「Zod でスキーマ定義 + 非同期関数」の 2 点セット。
特にハマるところはなかった。SDK が整備されていてドキュメント通りに動いた。
導入後: セッションが長持ちするようになった
導入前は、少し複雑な調査をするとすぐにコンテキストが 70〜80% に達していた。読ませすぎに気づかないまま、セッション後半で「コンテキスト不足」が出ることが多かった。
導入後は、同じ調査をしても消費が明らかに落ちた。スケルトンで全体像を掴んで、必要な部分だけ read_fragment で詳しく読む。この流れが定着してから、長いセッションでも終盤まで集中力が続く感覚がある。
「読む量を減らした」のに「理解の質は落ちていない」というのが想定外の発見だった。スケルトンで把握できることは思っていたより多かった。
動作環境
- Claude Code(最新版推奨)
- Node.js 18 以上
- TypeScript ビルド環境(
tscでdist/を生成)
リポジトリが公開され次第、git clone → npm install && npm run build で dist/index.js が生成される。
Claude Code への設定
~/.claude/settings.json に追加する。
{
"mcpServers": {
"token-guardian": {
"command": "node",
"args": ["/path/to/token-guardian-mcp/dist/index.js"]
}
}
}
TOKEN_GUARDIAN_THRESHOLD 環境変数でスケルトンに切り替わる閾値を変えられる。デフォルトは 1500 トークン。小さいファイルが多いプロジェクトなら 2000 くらいに上げても良い。
まとめ
コンテキスト爆発の直接の原因は「Claude がファイルをどう読むか」ではなく、「そのファイルを読ませるかどうかの判断」にある。
token-guardian はその判断を補助するツール。map_dir_cost で重さを確認して、read_smart でコストを抑えながら読む。この 2 ステップを入れるだけで、セッションのコンテキスト消費パターンが変わった。
「でかいファイルを読ませてセッションが終わる」という体験を繰り返している人には試してみてほしい。MCP の実装自体も、思っているより難しくなかった。
リポジトリ
近日公開予定。公開でき次第こちらに追記します。