DEV Community

Alessio
Alessio

Posted on Originally published at py-forge.com

Measuring Context Window Overhead in Model Context Protocol Tools

Model Context Protocol (MCP) servers allow LLM agents to interface with external systems by exposing resources, prompts, and tools. When integrating multiple MCP servers into an agent runtime, every tool definition—including its name, description, and parameter JSON Schema—must be injected into the LLM context window on every turn.

If servers define verbose parameter descriptions, deep schema nestings, or redundant utilities, this metadata creates a static baseline of token consumption that repeats on every model request before any user prompt or conversational history is processed.

mcp_tokenmeter_tool_schema_cost_meter.py is an audit utility built entirely with the Python standard library. It connects to MCP servers over standard input/output (stdio), fetches their registered tools, estimates token consumption for schemas and descriptions, identifies duplicate tool declarations, and calculates the resulting financial baseline across various model pricing tiers.


The Problem: Invisible Schema Bloat in MCP Workflows

When an LLM client connects to an MCP server, it negotiates capabilities and requests a tool list using the tools/list protocol method. The server returns an array of tool objects structured like this:

{
  "name": "query_database",
  "description": "Executes SQL queries against the local analytics engine...",
  "inputSchema": {
    "type": "object",
    "properties": {
      "query": {"type": "string", "description": "..."},
      "filters": {"type": "object", "properties": {...}}
    },
    "required": ["query"]
  }
}
Enter fullscreen mode Exit fullscreen mode

Because LLMs cannot call functions without understanding their contract, this entire schema payload must be rendered into tokens within the model's system prompt or tool specification parameters.

This introduces three structural problems:

  1. Repetitive Token Consumption: The schema payload is immutable during a session, yet it is re-sent with every prompt exchange.
  2. Verbose Descriptions and Deep Nesting: Developers often write multi-paragraph guidance in tool descriptions or define heavily nested JSON Schema properties, which inflates the context footprint.
  3. Tool Name Collisions: Running multiple independent MCP servers can result in duplicate tool registrations, confusing agent routing logic while paying double for identical schema footprints.

Without auditing tool schemas, it is difficult to determine how much context window space is consumed by tools alone.


Install & Usage

The tool requires Python 3.8+ and uses only standard library modules (argparse, dataclasses, json, math, os, re, subprocess, sys, and time). No third-party packages are required, so you do not need to run pip install.

Setup

Download the single script file and save it to your working environment:

# Save the script locally as mcp_tokenmeter_tool_schema_cost_meter.py
Enter fullscreen mode Exit fullscreen mode

Check that your Python runtime can execute the file:

python mcp_tokenmeter_tool_schema_cost_meter.py --help
Enter fullscreen mode Exit fullscreen mode

Running an Audit

To inspect an MCP server, pass the command and arguments used to launch the server over stdio. The script launches the process as a subprocess, initializes the MCP handshake, collects tool data, and prints the audit report directly to the terminal.


How It Works

The tool is divided into three functional layers: protocol communication, token heuristic estimation, and schema auditing.

1. MCP Protocol Stdio Client

The script interacts with MCP servers directly over subprocess pipes using standard JSON-RPC 2.0 messages. It does not require external MCP SDK packages.

class MCPStdioClient:
    """Minimal synchronous MCP stdio protocol client."""

    def __init__(self, command: str, args: List[str], env: Optional[Dict[str, str]] = None, timeout: float = 10.0):
        self.cmd = [command] + args
        self.env = os.environ.copy()
        if env:
            self.env.update(env)
        self.timeout = timeout
        self.process: Optional[subprocess.Popen] = None
        self._request_id = 1

    def fetch_tools(self) -> List[Dict[str, Any]]:
        self.start()
        try:
            # 1. Initialize
            self.request(
                "initialize",
                {
                    "protocolVersion": "2024-11-05",
                    "capabilities": {},
                    "clientInfo": {"name": "mcp-tokenmeter", "version": "1.0.0"},
                },
            )
            # 2. Initialized notification
            self._send_msg({"jsonrpc": "2.0", "method": "notifications/initialized"})

            # 3. List tools
            result = self.request("tools/list", {})
            return result.get("tools", [])
        finally:
            self.close()
Enter fullscreen mode Exit fullscreen mode

The client handles the initialization handshake (initialize followed by notifications/initialized), then issues a tools/list request. It blocks until response frames are received or the configurable timeout elapses, terminating the child process cleanly upon completion.

2. Token Estimation Engine

Because running standard tokenizer packages introduces substantial dependencies, the script approximates subword segmentation using regular expressions and length-based subpart heuristics:

def estimate_tokens(text: str) -> int:
    """
    Estimate token count of a given string using subword heuristic rules.
    Calibrated against standard LLM tokenizers for JSON/schema payloads.
    """
    if not text:
        return 0

    tokens = _TOKEN_PATTERN.findall(text)
    total_tokens = 0
    for tok in tokens:
        stripped = tok.strip()
        length = len(stripped)
        if length == 0:
            total_tokens += 1
        elif length <= 4:
            total_tokens += 1
        else:
            # Long identifiers, camelCase, snake_case, URLs, or hex strings
            subparts = re.findall(r"[A-Z]?[a-z]+|[A-Z]+(?=[A-Z][a-z]|\b)|\d+|\W+", stripped)
            if len(subparts) > 1:
                total_tokens += sum(max(1, math.ceil(len(p) / 3.8)) for p in subparts)
            else:
                total_tokens += max(1, math.ceil(length / 3.8))
    return max(1, total_tokens)


def schema_tokens(obj: Any) -> int:
    """Return estimated tokens for serialized JSON representation."""
    serialized = json.dumps(obj, indent=None, separators=(",", ":"), ensure_ascii=False)
    return estimate_tokens(serialized)
Enter fullscreen mode Exit fullscreen mode

This approximation handles camelCase, snake_case, punctuation, and compact JSON serialization to mirror how BPE tokenizers split technical schemas.

3. Threshold Analysis and Flagging

Once tool schemas are extracted, the TokenMeterAnalyzer assesses the footprint against standard hygiene limits:

class TokenMeterAnalyzer:
    def __init__(self, desc_token_limit: int = 75, schema_token_limit: int = 350):
        self.desc_token_limit = desc_token_limit
        self.schema_token_limit = schema_token_limit

    def analyze_tool(self, tool_data: Dict[str, Any]) -> ToolMetric:
        name = str(tool_data.get("name", "unnamed_tool"))
        desc = str(tool_data.get("description", ""))
        schema = tool_data.get("inputSchema", {})

        n_tok = estimate_tokens(name)
        d_tok = estimate_tokens(desc)
        s_tok = schema_tokens(schema)

        # Standard tool overhead wrapper: {"name": ..., "description": ..., "parameters": ...}
        wrapper_overhead = 12
        total = n_tok + d_tok + s_tok + wrapper_overhead

        return ToolMetric(
            name=name,
            description=desc,
            input_schema=schema,
            name_tokens=n_tok,
            desc_tokens=d_tok,
            schema_tokens=s_tok,
            total_tokens=total,
            oversized_desc=(d_tok > self.desc_token_limit),
            oversized_schema=(s_tok > self.schema_token_limit),
        )
Enter fullscreen mode Exit fullscreen mode

The analyzer flags:

  • BLOAT_DESC: Descriptions exceeding 75 tokens that could be simplified.
  • BLOAT_SCHEMA: Complex parameter trees exceeding 350 tokens.
  • DUPLICATE: Identical tool names found across more than one configured MCP server.

Limitations

While the tool is portable and lightweight, its architecture has specific limitations:

  • Heuristic Tokenization: The script uses a regular expression subword model rather than an exact BPE vocabulary table. Token counts are approximations.
  • Transport Restrictions: Only standard input/output (stdio) child processes are supported. Servers running on Server-Sent Events (SSE) or HTTP transports cannot be audited by this client.
  • Synchronous I/O: The subprocess reader reads line by line with an internal timeout loop. If a server emits arbitrary debug strings outside of JSON-RPC to standard output instead of standard error, parsing errors can occur.
  • Static Cost Rates: The pricing table (PRICING_TABLE) uses hardcoded token pricing per million input tokens. Any vendor price changes require manual edits to the table dictionary in the source code.

Frequently Asked Questions

Does this tool require third-party libraries installed via pip?

No. The script relies entirely on standard library modules included with standard Python installations.

Why does the tool use regex heuristics instead of an official tokenizer?

Using regex-based subword heuristics avoids compiling native tokenization bindings or downloading large tokenizer vocabulary files, allowing the tool to run instantly as a standalone script.

How does the tool communicate with the MCP server?

It launches the target command via Python's subprocess.Popen, sets up piped input and output streams, and exchanges JSON-RPC 2.0 messages matching the Model Context Protocol specification.


Full version and the rest of the toolkit: MCP TokenMeter tool schema cost meter on PyForge. Product page: https://py-forge.com/products/mcp-tokenmeter-tool-schema-cost-meter

Top comments (2)

Collapse
 
officialmailkr profile image
오피셜메일 •

스키마 크기를 도구별로 분해하고 추정치라는 한계까지 표시한 점이 유용하네요. 비용 보고서에서는 전체 입력 토큰과 실제 청구되는 입력 비용을 구분하면 더 좋겠습니다. 도구 지연 로딩이나 캐시를 사용하는 클라이언트도 있으니, 모든 도구를 매 턴 보내는 가정과 실제 전송량을 나란히 비교할 수 있을까요?

Collapse
 
alessio99xx profile image
Alessio •

Thanks, great point. Right now TokenMeter assumes the worst case: every tool schema is sent on every turn, and the numbers are estimates, not billed tokens. You're right that clients with lazy tool loading or prompt caching send (and pay for) much less. Showing the worst case next to an "actually billed" estimate makes a lot of sense, and I've added it to the list for the next update. 감사합니다!