Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/mcp_server_git/config/token_limits.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ class TokenLimitSettings:
# Core token limits
llm_token_limit: int = 20000
human_token_limit: int = 0 # 0 = unlimited
unknown_token_limit: int = 25000
# kept below client MAX_MCP_OUTPUT_TOKENS so the offloader spills before the client truncates
# (~42 KB at 3.5 ch/token, under the ~60 KB client cap)
unknown_token_limit: int = 12000

# Feature toggles
enable_content_optimization: bool = True
Expand Down Expand Up @@ -105,14 +107,14 @@ def from_profile(cls, profile: TokenLimitProfile) -> "TokenLimitSettings":
profiles = {
TokenLimitProfile.CONSERVATIVE: cls(
llm_token_limit=15000,
unknown_token_limit=18000,
unknown_token_limit=8000,
enable_content_optimization=True,
enable_intelligent_truncation=True,
add_truncation_warnings=True,
),
TokenLimitProfile.BALANCED: cls(
llm_token_limit=20000,
unknown_token_limit=25000,
unknown_token_limit=12000,
enable_content_optimization=True,
enable_intelligent_truncation=True,
),
Expand Down
125 changes: 125 additions & 0 deletions tests/unit/lean/test_offloader_token_limit.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import pytest

import mcp_server_git.config.token_limits as _token_limits_mod
from mcp_server_git.config.token_limits import TokenLimitProfile
from mcp_server_git.lean.token_limiter import MCPTokenLimiter

# A payload length that sits between the old 2000-token default and the
Expand Down Expand Up @@ -82,3 +83,127 @@ def test_git_lean_interface_token_limiter_uses_configured_limit(
f"Expected default_limit={_CONFIGURED_LIMIT}, "
f"got {iface.token_limiter.default_limit} (hard-wired 2000 bug still present)"
)


# ---------------------------------------------------------------------------
# Regression: unknown_token_limit default must be below the client output cap
# ---------------------------------------------------------------------------

# The old default (25 000 tokens ≈ 88 KB at 3.5 ch/tok) sat above the Claude
# Code client MAX_MCP_OUTPUT_TOKENS cap (~60 KB for git-hash-heavy text).
# Outputs in the 60-88 KB range were returned inline and silently truncated by
# the client. The new default (12 000 tokens ≈ 42 KB) ensures the server
# offloader triggers first.
_OLD_OFFLOADER_DEFAULT = 25_000
_NEW_OFFLOADER_DEFAULT = 12_000
_CHARS_PER_TOKEN_ESTIMATE = 3.5
_CLIENT_CAP_APPROX_TOKENS = int(60 * 1024 / _CHARS_PER_TOKEN_ESTIMATE)


class TestOffloaderDefaultBelowClientCap:
"""Guard the 25000→12000 reduction in unknown_token_limit."""

def test_unknown_token_limit_default_is_12000(self) -> None:
"""TokenLimitSettings.unknown_token_limit default must be 12000.

Regression guard: do not restore 25000 — that value sits above the
client MAX_MCP_OUTPUT_TOKENS (~60 KB) and causes silent truncation.
"""
settings = _token_limits_mod.TokenLimitSettings()
assert settings.unknown_token_limit == _NEW_OFFLOADER_DEFAULT, (
f"unknown_token_limit default regressed: expected {_NEW_OFFLOADER_DEFAULT}, "
f"got {settings.unknown_token_limit}. The old value ({_OLD_OFFLOADER_DEFAULT}) "
f"exceeds the client cap and must not be restored."
)

def test_unknown_token_limit_default_is_below_client_cap(self) -> None:
"""Default unknown_token_limit must be strictly below the client output cap."""
settings = _token_limits_mod.TokenLimitSettings()
assert settings.unknown_token_limit < _CLIENT_CAP_APPROX_TOKENS, (
f"unknown_token_limit ({settings.unknown_token_limit}) must be below the "
f"approximate client cap ({_CLIENT_CAP_APPROX_TOKENS} tokens ≈ 60 KB) so the "
f"server offloader spills before the client truncates."
)

def test_env_override_raises_limit_above_default(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""MCP_GIT_UNKNOWN_TOKEN_LIMIT env var must still override the default upward."""
monkeypatch.setenv("MCP_GIT_UNKNOWN_TOKEN_LIMIT", "30000")
manager = _token_limits_mod.TokenLimitConfigManager()
settings = manager.load_configuration()
assert settings.unknown_token_limit == 30_000

def test_limiter_at_new_default_truncates_50kb_output(self) -> None:
"""A ~50 KB payload must be truncated by a limiter at the new 12 000-token default.

50 KB / 3.5 ch per token ≈ 14 629 tokens — above 12 000, so the server
offloader must trigger before the ~60 KB client cap is reached.
"""
# ~50 KB dict value — representative of a large git log / diff output
fifty_kb_chars = int(50 * 1024)
large_payload = {"output": "a" * fifty_kb_chars}

limiter = MCPTokenLimiter(default_limit=_NEW_OFFLOADER_DEFAULT)
assert limiter.would_truncate(large_payload, "git_log"), (
f"A ~50 KB payload (~{fifty_kb_chars // int(_CHARS_PER_TOKEN_ESTIMATE)} tokens) "
f"must exceed the new {_NEW_OFFLOADER_DEFAULT}-token default so the offloader "
f"triggers before the client cap."
)

def test_limiter_at_old_default_passes_50kb_output(self) -> None:
"""Regression anchor: the old 25 000-token default did NOT truncate ~50 KB output.

This confirms the old default was the root cause of client-side truncation.
"""
fifty_kb_chars = int(50 * 1024)
large_payload = {"output": "a" * fifty_kb_chars}

old_limiter = MCPTokenLimiter(default_limit=_OLD_OFFLOADER_DEFAULT)
assert not old_limiter.would_truncate(large_payload, "git_log"), (
"Sanity-check failed: old 25 000-token limiter should NOT truncate a "
"~50 KB payload, confirming the regression was real."
)


# ---------------------------------------------------------------------------
# Profile coverage: safe profiles must stay below the client output cap
# ---------------------------------------------------------------------------

class TestProfilesBelowClientCap:
"""Guard that CONSERVATIVE and BALANCED profiles stay below the client cap."""

def test_conservative_unknown_token_limit_is_below_default_and_client_cap(
self,
) -> None:
"""CONSERVATIVE profile unknown_token_limit must be below the new default (12000)
and strictly below the client cap — a 'conservative' profile must be the most
restrictive of the safe profiles.
"""
settings = _token_limits_mod.TokenLimitSettings.from_profile(
TokenLimitProfile.CONSERVATIVE
)
assert settings.unknown_token_limit <= _NEW_OFFLOADER_DEFAULT, (
f"CONSERVATIVE unknown_token_limit ({settings.unknown_token_limit}) must be "
f"<= the new default ({_NEW_OFFLOADER_DEFAULT})."
)
assert settings.unknown_token_limit < _CLIENT_CAP_APPROX_TOKENS, (
f"CONSERVATIVE unknown_token_limit ({settings.unknown_token_limit}) must be "
f"< client cap ({_CLIENT_CAP_APPROX_TOKENS} tokens ≈ 60 KB)."
)

def test_balanced_unknown_token_limit_equals_new_default_and_is_below_client_cap(
self,
) -> None:
"""BALANCED profile unknown_token_limit must equal 12000 and be below the client cap."""
settings = _token_limits_mod.TokenLimitSettings.from_profile(
TokenLimitProfile.BALANCED
)
assert settings.unknown_token_limit == _NEW_OFFLOADER_DEFAULT, (
f"BALANCED unknown_token_limit ({settings.unknown_token_limit}) must equal "
f"{_NEW_OFFLOADER_DEFAULT}."
)
assert settings.unknown_token_limit < _CLIENT_CAP_APPROX_TOKENS, (
f"BALANCED unknown_token_limit ({settings.unknown_token_limit}) must be "
f"< client cap ({_CLIENT_CAP_APPROX_TOKENS} tokens ≈ 60 KB)."
)
Loading