Skip to content

Commit 0b0c754

Browse files
committed
fix: Add output limiting to prevent MCP token limit errors
- Module 1: Added comprehensive lesson on handling 25k token limit - Implemented max_diff_lines parameter (default 500) - Added truncation logic with clear messaging - Included test for output limiting - Framed as important real-world learning opportunity - Modules 2 & 3: Pre-included fix so learners can focus on new concepts - Updated all starter and solution code - Added notes that output limiting is already handled - Prevents repetitive debugging of same issue This ensures learners encounter and solve the token limit problem once in Module 1 as a valuable lesson, then can focus on webhooks (Module 2) and Slack integration (Module 3) without distraction. Fixes the issue where large git diffs cause "MCP tool response exceeds maximum allowed tokens (25000)" errors.
1 parent 59dab07 commit 0b0c754

File tree

10 files changed

+228
-27
lines changed

10 files changed

+228
-27
lines changed

projects/unit3/build-mcp-server/solution/server.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@
2222
@mcp.tool()
2323
async def analyze_file_changes(
2424
base_branch: str = "main",
25-
include_diff: bool = True
25+
include_diff: bool = True,
26+
max_diff_lines: int = 500
2627
) -> str:
2728
"""Get the full diff and list of changed files in the current git repository.
2829
2930
Args:
3031
base_branch: Base branch to compare against (default: main)
3132
include_diff: Include the full diff content (default: true)
33+
max_diff_lines: Maximum number of diff lines to include (default: 500)
3234
"""
3335
try:
3436
# Get list of changed files
@@ -48,13 +50,23 @@ async def analyze_file_changes(
4850

4951
# Get the actual diff if requested
5052
diff_content = ""
53+
truncated = False
5154
if include_diff:
5255
diff_result = subprocess.run(
5356
["git", "diff", f"{base_branch}...HEAD"],
5457
capture_output=True,
5558
text=True
5659
)
57-
diff_content = diff_result.stdout
60+
diff_lines = diff_result.stdout.split('\n')
61+
62+
# Check if we need to truncate
63+
if len(diff_lines) > max_diff_lines:
64+
diff_content = '\n'.join(diff_lines[:max_diff_lines])
65+
diff_content += f"\n\n... Output truncated. Showing {max_diff_lines} of {len(diff_lines)} lines ..."
66+
diff_content += "\n... Use max_diff_lines parameter to see more ..."
67+
truncated = True
68+
else:
69+
diff_content = diff_result.stdout
5870

5971
# Get commit messages for context
6072
commits_result = subprocess.run(
@@ -68,15 +80,17 @@ async def analyze_file_changes(
6880
"files_changed": files_result.stdout,
6981
"statistics": stat_result.stdout,
7082
"commits": commits_result.stdout,
71-
"diff": diff_content if include_diff else "Diff not included (set include_diff=true to see full diff)"
83+
"diff": diff_content if include_diff else "Diff not included (set include_diff=true to see full diff)",
84+
"truncated": truncated,
85+
"total_diff_lines": len(diff_lines) if include_diff else 0
7286
}
7387

7488
return json.dumps(analysis, indent=2)
7589

7690
except subprocess.CalledProcessError as e:
77-
return f"Error analyzing changes: {e.stderr}"
91+
return json.dumps({"error": f"Git error: {e.stderr}"})
7892
except Exception as e:
79-
return f"Error: {str(e)}"
93+
return json.dumps({"error": str(e)})
8094

8195

8296
@mcp.tool()

projects/unit3/build-mcp-server/solution/test_server.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,40 @@ async def test_includes_required_fields(self):
7171
else:
7272
# Starter code - just verify it returns something structured
7373
assert isinstance(data, dict), "Should return a JSON object even if not implemented"
74+
75+
@pytest.mark.asyncio
76+
async def test_output_limiting(self):
77+
"""Test that large diffs are properly truncated."""
78+
with patch('subprocess.run') as mock_run:
79+
# Create a mock diff with many lines
80+
large_diff = "\n".join([f"+ line {i}" for i in range(1000)])
81+
82+
# Set up mock responses
83+
mock_run.side_effect = [
84+
MagicMock(stdout="M\tfile1.py\n", stderr=""), # files changed
85+
MagicMock(stdout="1 file changed, 1000 insertions(+)", stderr=""), # stats
86+
MagicMock(stdout=large_diff, stderr=""), # diff
87+
MagicMock(stdout="abc123 Initial commit", stderr="") # commits
88+
]
89+
90+
# Test with default limit (500 lines)
91+
result = await analyze_file_changes(include_diff=True)
92+
data = json.loads(result)
93+
94+
# Check if it's implemented
95+
if "error" not in data or "Not implemented" not in str(data.get("error", "")):
96+
if "diff" in data and data["diff"] != "Diff not included (set include_diff=true to see full diff)":
97+
diff_lines = data["diff"].split('\n')
98+
# Should be truncated to around 500 lines plus truncation message
99+
assert len(diff_lines) < 600, "Large diffs should be truncated"
100+
101+
# Check for truncation indicator
102+
if "truncated" in data:
103+
assert data["truncated"] == True, "Should indicate truncation"
104+
105+
# Should have truncation message
106+
assert "truncated" in data["diff"].lower() or "..." in data["diff"], \
107+
"Should indicate diff was truncated"
74108

75109

76110
@pytest.mark.skipif(not IMPORTS_SUCCESSFUL, reason="Imports failed")

projects/unit3/build-mcp-server/starter/server.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ async def analyze_file_changes(base_branch: str = "main", include_diff: bool = T
4242
include_diff: Include the full diff content (default: true)
4343
"""
4444
# TODO: Implement this tool
45+
# IMPORTANT: MCP tools have a 25,000 token response limit!
46+
# Large diffs can easily exceed this. Consider:
47+
# - Adding a max_diff_lines parameter (e.g., 500 lines)
48+
# - Truncating large outputs with a message
49+
# - Returning summary statistics alongside limited diffs
4550
return json.dumps({"error": "Not implemented yet", "hint": "Use subprocess to run git commands"})
4651

4752

projects/unit3/github-actions-integration/solution/server.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,20 @@
2222
EVENTS_FILE = Path(__file__).parent / "github_events.json"
2323

2424

25-
# ===== Original Tools from Module 1 =====
25+
# ===== Original Tools from Module 1 (with output limiting) =====
2626

2727
@mcp.tool()
2828
async def analyze_file_changes(
2929
base_branch: str = "main",
30-
include_diff: bool = True
30+
include_diff: bool = True,
31+
max_diff_lines: int = 500
3132
) -> str:
3233
"""Get the full diff and list of changed files in the current git repository.
3334
3435
Args:
3536
base_branch: Base branch to compare against (default: main)
3637
include_diff: Include the full diff content (default: true)
38+
max_diff_lines: Maximum number of diff lines to include (default: 500)
3739
"""
3840
try:
3941
# Get list of changed files
@@ -53,13 +55,23 @@ async def analyze_file_changes(
5355

5456
# Get the actual diff if requested
5557
diff_content = ""
58+
truncated = False
5659
if include_diff:
5760
diff_result = subprocess.run(
5861
["git", "diff", f"{base_branch}...HEAD"],
5962
capture_output=True,
6063
text=True
6164
)
62-
diff_content = diff_result.stdout
65+
diff_lines = diff_result.stdout.split('\n')
66+
67+
# Check if we need to truncate
68+
if len(diff_lines) > max_diff_lines:
69+
diff_content = '\n'.join(diff_lines[:max_diff_lines])
70+
diff_content += f"\n\n... Output truncated. Showing {max_diff_lines} of {len(diff_lines)} lines ..."
71+
diff_content += "\n... Use max_diff_lines parameter to see more ..."
72+
truncated = True
73+
else:
74+
diff_content = diff_result.stdout
6375

6476
# Get commit messages for context
6577
commits_result = subprocess.run(
@@ -73,15 +85,17 @@ async def analyze_file_changes(
7385
"files_changed": files_result.stdout,
7486
"statistics": stat_result.stdout,
7587
"commits": commits_result.stdout,
76-
"diff": diff_content if include_diff else "Diff not included (set include_diff=true to see full diff)"
88+
"diff": diff_content if include_diff else "Diff not included (set include_diff=true to see full diff)",
89+
"truncated": truncated,
90+
"total_diff_lines": len(diff_lines) if include_diff else 0
7791
}
7892

7993
return json.dumps(analysis, indent=2)
8094

8195
except subprocess.CalledProcessError as e:
82-
return f"Error analyzing changes: {e.stderr}"
96+
return json.dumps({"error": f"Git error: {e.stderr}"})
8397
except Exception as e:
84-
return f"Error: {str(e)}"
98+
return json.dumps({"error": str(e)})
8599

86100

87101
@mcp.tool()

projects/unit3/github-actions-integration/starter/server.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,20 @@
2323
# Hint: EVENTS_FILE = Path(__file__).parent / "github_events.json"
2424

2525

26-
# ===== Module 1 Tools (Keep these as-is) =====
26+
# ===== Module 1 Tools (Already includes output limiting fix from Module 1) =====
2727

2828
@mcp.tool()
2929
async def analyze_file_changes(
3030
base_branch: str = "main",
31-
include_diff: bool = True
31+
include_diff: bool = True,
32+
max_diff_lines: int = 500
3233
) -> str:
3334
"""Get the full diff and list of changed files in the current git repository.
3435
3536
Args:
3637
base_branch: Base branch to compare against (default: main)
3738
include_diff: Include the full diff content (default: true)
39+
max_diff_lines: Maximum number of diff lines to include (default: 500)
3840
"""
3941
try:
4042
# Get list of changed files
@@ -54,13 +56,23 @@ async def analyze_file_changes(
5456

5557
# Get the actual diff if requested
5658
diff_content = ""
59+
truncated = False
5760
if include_diff:
5861
diff_result = subprocess.run(
5962
["git", "diff", f"{base_branch}...HEAD"],
6063
capture_output=True,
6164
text=True
6265
)
63-
diff_content = diff_result.stdout
66+
diff_lines = diff_result.stdout.split('\n')
67+
68+
# Check if we need to truncate (learned from Module 1)
69+
if len(diff_lines) > max_diff_lines:
70+
diff_content = '\n'.join(diff_lines[:max_diff_lines])
71+
diff_content += f"\n\n... Output truncated. Showing {max_diff_lines} of {len(diff_lines)} lines ..."
72+
diff_content += "\n... Use max_diff_lines parameter to see more ..."
73+
truncated = True
74+
else:
75+
diff_content = diff_result.stdout
6476

6577
# Get commit messages for context
6678
commits_result = subprocess.run(
@@ -74,7 +86,9 @@ async def analyze_file_changes(
7486
"files_changed": files_result.stdout,
7587
"statistics": stat_result.stdout,
7688
"commits": commits_result.stdout,
77-
"diff": diff_content if include_diff else "Diff not included (set include_diff=true to see full diff)"
89+
"diff": diff_content if include_diff else "Diff not included (set include_diff=true to see full diff)",
90+
"truncated": truncated,
91+
"total_diff_lines": len(diff_lines) if include_diff else 0
7892
}
7993

8094
return json.dumps(analysis, indent=2)

projects/unit3/slack-notification/solution/server.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,20 @@
2323
EVENTS_FILE = Path(__file__).parent / "github_events.json"
2424

2525

26-
# ===== Tools from Modules 1 & 2 (Complete) =====
26+
# ===== Tools from Modules 1 & 2 (Complete with output limiting) =====
2727

2828
@mcp.tool()
2929
async def analyze_file_changes(
3030
base_branch: str = "main",
31-
include_diff: bool = True
31+
include_diff: bool = True,
32+
max_diff_lines: int = 500
3233
) -> str:
3334
"""Get the full diff and list of changed files in the current git repository.
3435
3536
Args:
3637
base_branch: Base branch to compare against (default: main)
3738
include_diff: Include the full diff content (default: true)
39+
max_diff_lines: Maximum number of diff lines to include (default: 500)
3840
"""
3941
try:
4042
# Get list of changed files
@@ -54,13 +56,23 @@ async def analyze_file_changes(
5456

5557
# Get the actual diff if requested
5658
diff_content = ""
59+
truncated = False
5760
if include_diff:
5861
diff_result = subprocess.run(
5962
["git", "diff", f"{base_branch}...HEAD"],
6063
capture_output=True,
6164
text=True
6265
)
63-
diff_content = diff_result.stdout
66+
diff_lines = diff_result.stdout.split('\n')
67+
68+
# Check if we need to truncate
69+
if len(diff_lines) > max_diff_lines:
70+
diff_content = '\n'.join(diff_lines[:max_diff_lines])
71+
diff_content += f"\n\n... Output truncated. Showing {max_diff_lines} of {len(diff_lines)} lines ..."
72+
diff_content += "\n... Use max_diff_lines parameter to see more ..."
73+
truncated = True
74+
else:
75+
diff_content = diff_result.stdout
6476

6577
# Get commit messages for context
6678
commits_result = subprocess.run(
@@ -74,15 +86,17 @@ async def analyze_file_changes(
7486
"files_changed": files_result.stdout,
7587
"statistics": stat_result.stdout,
7688
"commits": commits_result.stdout,
77-
"diff": diff_content if include_diff else "Diff not included (set include_diff=true to see full diff)"
89+
"diff": diff_content if include_diff else "Diff not included (set include_diff=true to see full diff)",
90+
"truncated": truncated,
91+
"total_diff_lines": len(diff_lines) if include_diff else 0
7892
}
7993

8094
return json.dumps(analysis, indent=2)
8195

8296
except subprocess.CalledProcessError as e:
83-
return f"Error analyzing changes: {e.stderr}"
97+
return json.dumps({"error": f"Git error: {e.stderr}"})
8498
except Exception as e:
85-
return f"Error: {str(e)}"
99+
return json.dumps({"error": str(e)})
86100

87101

88102
@mcp.tool()

projects/unit3/slack-notification/starter/server.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,20 @@
2222
EVENTS_FILE = Path(__file__).parent / "github_events.json"
2323

2424

25-
# ===== Tools from Modules 1 & 2 (Complete) =====
25+
# ===== Tools from Modules 1 & 2 (Complete with output limiting) =====
2626

2727
@mcp.tool()
2828
async def analyze_file_changes(
2929
base_branch: str = "main",
30-
include_diff: bool = True
30+
include_diff: bool = True,
31+
max_diff_lines: int = 500
3132
) -> str:
3233
"""Get the full diff and list of changed files in the current git repository.
3334
3435
Args:
3536
base_branch: Base branch to compare against (default: main)
3637
include_diff: Include the full diff content (default: true)
38+
max_diff_lines: Maximum number of diff lines to include (default: 500)
3739
"""
3840
try:
3941
# Get list of changed files
@@ -53,13 +55,23 @@ async def analyze_file_changes(
5355

5456
# Get the actual diff if requested
5557
diff_content = ""
58+
truncated = False
5659
if include_diff:
5760
diff_result = subprocess.run(
5861
["git", "diff", f"{base_branch}...HEAD"],
5962
capture_output=True,
6063
text=True
6164
)
62-
diff_content = diff_result.stdout
65+
diff_lines = diff_result.stdout.split('\n')
66+
67+
# Check if we need to truncate
68+
if len(diff_lines) > max_diff_lines:
69+
diff_content = '\n'.join(diff_lines[:max_diff_lines])
70+
diff_content += f"\n\n... Output truncated. Showing {max_diff_lines} of {len(diff_lines)} lines ..."
71+
diff_content += "\n... Use max_diff_lines parameter to see more ..."
72+
truncated = True
73+
else:
74+
diff_content = diff_result.stdout
6375

6476
# Get commit messages for context
6577
commits_result = subprocess.run(
@@ -73,15 +85,17 @@ async def analyze_file_changes(
7385
"files_changed": files_result.stdout,
7486
"statistics": stat_result.stdout,
7587
"commits": commits_result.stdout,
76-
"diff": diff_content if include_diff else "Diff not included (set include_diff=true to see full diff)"
88+
"diff": diff_content if include_diff else "Diff not included (set include_diff=true to see full diff)",
89+
"truncated": truncated,
90+
"total_diff_lines": len(diff_lines) if include_diff else 0
7791
}
7892

7993
return json.dumps(analysis, indent=2)
8094

8195
except subprocess.CalledProcessError as e:
82-
return f"Error analyzing changes: {e.stderr}"
96+
return json.dumps({"error": f"Git error: {e.stderr}"})
8397
except Exception as e:
84-
return f"Error: {str(e)}"
98+
return json.dumps({"error": str(e)})
8599

86100

87101
@mcp.tool()

0 commit comments

Comments
 (0)