Skip to content

Commit 751e1c3

Browse files
committed
Merge branch 'feature/anthropic-course-units' of https://github.com/zealoushacker/mcp-course into pr/50
2 parents 05c9a75 + bd15457 commit 751e1c3

File tree

10 files changed

+378
-39
lines changed

10 files changed

+378
-39
lines changed

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

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,61 +22,122 @@
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,
27+
working_directory: Optional[str] = None
2628
) -> str:
2729
"""Get the full diff and list of changed files in the current git repository.
2830
2931
Args:
3032
base_branch: Base branch to compare against (default: main)
3133
include_diff: Include the full diff content (default: true)
34+
max_diff_lines: Maximum number of diff lines to include (default: 500)
35+
working_directory: Directory to run git commands in (default: current directory)
3236
"""
3337
try:
38+
# Try to get working directory from roots first
39+
if working_directory is None:
40+
try:
41+
context = mcp.get_context()
42+
roots_result = await context.session.list_roots()
43+
# Get the first root - Claude Code sets this to the CWD
44+
root = roots_result.roots[0]
45+
# FileUrl object has a .path property that gives us the path directly
46+
working_directory = root.uri.path
47+
except Exception as e:
48+
# If we can't get roots, fall back to current directory
49+
pass
50+
51+
# Use provided working directory or current directory
52+
cwd = working_directory if working_directory else os.getcwd()
53+
54+
# Debug output
55+
debug_info = {
56+
"provided_working_directory": working_directory,
57+
"actual_cwd": cwd,
58+
"server_process_cwd": os.getcwd(),
59+
"server_file_location": str(Path(__file__).parent),
60+
"roots_check": None
61+
}
62+
63+
# Add roots debug info
64+
try:
65+
context = mcp.get_context()
66+
roots_result = await context.session.list_roots()
67+
debug_info["roots_check"] = {
68+
"found": True,
69+
"count": len(roots_result.roots),
70+
"roots": [str(root.uri) for root in roots_result.roots]
71+
}
72+
except Exception as e:
73+
debug_info["roots_check"] = {
74+
"found": False,
75+
"error": str(e)
76+
}
77+
3478
# Get list of changed files
3579
files_result = subprocess.run(
3680
["git", "diff", "--name-status", f"{base_branch}...HEAD"],
3781
capture_output=True,
3882
text=True,
39-
check=True
83+
check=True,
84+
cwd=cwd
4085
)
4186

4287
# Get diff statistics
4388
stat_result = subprocess.run(
4489
["git", "diff", "--stat", f"{base_branch}...HEAD"],
4590
capture_output=True,
46-
text=True
91+
text=True,
92+
cwd=cwd
4793
)
4894

4995
# Get the actual diff if requested
5096
diff_content = ""
97+
truncated = False
5198
if include_diff:
5299
diff_result = subprocess.run(
53100
["git", "diff", f"{base_branch}...HEAD"],
54101
capture_output=True,
55-
text=True
102+
text=True,
103+
cwd=cwd
56104
)
57-
diff_content = diff_result.stdout
105+
diff_lines = diff_result.stdout.split('\n')
106+
107+
# Check if we need to truncate
108+
if len(diff_lines) > max_diff_lines:
109+
diff_content = '\n'.join(diff_lines[:max_diff_lines])
110+
diff_content += f"\n\n... Output truncated. Showing {max_diff_lines} of {len(diff_lines)} lines ..."
111+
diff_content += "\n... Use max_diff_lines parameter to see more ..."
112+
truncated = True
113+
else:
114+
diff_content = diff_result.stdout
58115

59116
# Get commit messages for context
60117
commits_result = subprocess.run(
61118
["git", "log", "--oneline", f"{base_branch}..HEAD"],
62119
capture_output=True,
63-
text=True
120+
text=True,
121+
cwd=cwd
64122
)
65123

66124
analysis = {
67125
"base_branch": base_branch,
68126
"files_changed": files_result.stdout,
69127
"statistics": stat_result.stdout,
70128
"commits": commits_result.stdout,
71-
"diff": diff_content if include_diff else "Diff not included (set include_diff=true to see full diff)"
129+
"diff": diff_content if include_diff else "Diff not included (set include_diff=true to see full diff)",
130+
"truncated": truncated,
131+
"total_diff_lines": len(diff_lines) if include_diff else 0,
132+
"_debug": debug_info
72133
}
73134

74135
return json.dumps(analysis, indent=2)
75136

76137
except subprocess.CalledProcessError as e:
77-
return f"Error analyzing changes: {e.stderr}"
138+
return json.dumps({"error": f"Git error: {e.stderr}"})
78139
except Exception as e:
79-
return f"Error: {str(e)}"
140+
return json.dumps({"error": str(e)})
80141

81142

82143
@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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,19 @@ 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
50+
51+
# NOTE: Git commands run in the server's directory by default!
52+
# To run in Claude's working directory, use MCP roots:
53+
# context = mcp.get_context()
54+
# roots_result = await context.session.list_roots()
55+
# working_dir = roots_result.roots[0].uri.path
56+
# subprocess.run(["git", "diff"], cwd=working_dir)
57+
4558
return json.dumps({"error": "Not implemented yet", "hint": "Use subprocess to run git commands"})
4659

4760

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

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,66 +22,101 @@
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,
32+
working_directory: Optional[str] = None
3133
) -> str:
3234
"""Get the full diff and list of changed files in the current git repository.
3335
3436
Args:
3537
base_branch: Base branch to compare against (default: main)
3638
include_diff: Include the full diff content (default: true)
39+
max_diff_lines: Maximum number of diff lines to include (default: 500)
40+
working_directory: Directory to run git commands in (default: current directory)
3741
"""
3842
try:
43+
# Try to get working directory from roots first
44+
if working_directory is None:
45+
try:
46+
context = mcp.get_context()
47+
roots_result = await context.session.list_roots()
48+
# Get the first root - Claude Code sets this to the CWD
49+
root = roots_result.roots[0]
50+
# FileUrl object has a .path property that gives us the path directly
51+
working_directory = root.uri.path
52+
except Exception as e:
53+
# If we can't get roots, fall back to current directory
54+
pass
55+
56+
# Use provided working directory or current directory
57+
cwd = working_directory if working_directory else os.getcwd()
3958
# Get list of changed files
4059
files_result = subprocess.run(
4160
["git", "diff", "--name-status", f"{base_branch}...HEAD"],
4261
capture_output=True,
4362
text=True,
44-
check=True
63+
check=True,
64+
cwd=cwd
4565
)
4666

4767
# Get diff statistics
4868
stat_result = subprocess.run(
4969
["git", "diff", "--stat", f"{base_branch}...HEAD"],
5070
capture_output=True,
51-
text=True
71+
text=True,
72+
cwd=cwd
5273
)
5374

5475
# Get the actual diff if requested
5576
diff_content = ""
77+
truncated = False
5678
if include_diff:
5779
diff_result = subprocess.run(
5880
["git", "diff", f"{base_branch}...HEAD"],
5981
capture_output=True,
60-
text=True
82+
text=True,
83+
cwd=cwd
6184
)
62-
diff_content = diff_result.stdout
85+
diff_lines = diff_result.stdout.split('\n')
86+
87+
# Check if we need to truncate
88+
if len(diff_lines) > max_diff_lines:
89+
diff_content = '\n'.join(diff_lines[:max_diff_lines])
90+
diff_content += f"\n\n... Output truncated. Showing {max_diff_lines} of {len(diff_lines)} lines ..."
91+
diff_content += "\n... Use max_diff_lines parameter to see more ..."
92+
truncated = True
93+
else:
94+
diff_content = diff_result.stdout
6395

6496
# Get commit messages for context
6597
commits_result = subprocess.run(
6698
["git", "log", "--oneline", f"{base_branch}..HEAD"],
6799
capture_output=True,
68-
text=True
100+
text=True,
101+
cwd=cwd
69102
)
70103

71104
analysis = {
72105
"base_branch": base_branch,
73106
"files_changed": files_result.stdout,
74107
"statistics": stat_result.stdout,
75108
"commits": commits_result.stdout,
76-
"diff": diff_content if include_diff else "Diff not included (set include_diff=true to see full diff)"
109+
"diff": diff_content if include_diff else "Diff not included (set include_diff=true to see full diff)",
110+
"truncated": truncated,
111+
"total_diff_lines": len(diff_lines) if include_diff else 0
77112
}
78113

79114
return json.dumps(analysis, indent=2)
80115

81116
except subprocess.CalledProcessError as e:
82-
return f"Error analyzing changes: {e.stderr}"
117+
return json.dumps({"error": f"Git error: {e.stderr}"})
83118
except Exception as e:
84-
return f"Error: {str(e)}"
119+
return json.dumps({"error": str(e)})
85120

86121

87122
@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)

0 commit comments

Comments
 (0)