Skip to content

Commit bd15457

Browse files
committed
fix: Handle MCP server working directory issue using roots
- Add roots support to access Claude Code's working directory - Update analyze_file_changes in all modules to use correct cwd - Add documentation about working directory considerations - Include helpful comments in starter code about roots usage
1 parent 0b0c754 commit bd15457

File tree

5 files changed

+154
-16
lines changed

5 files changed

+154
-16
lines changed

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

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,29 +23,73 @@
2323
async def analyze_file_changes(
2424
base_branch: str = "main",
2525
include_diff: bool = True,
26-
max_diff_lines: int = 500
26+
max_diff_lines: int = 500,
27+
working_directory: Optional[str] = None
2728
) -> str:
2829
"""Get the full diff and list of changed files in the current git repository.
2930
3031
Args:
3132
base_branch: Base branch to compare against (default: main)
3233
include_diff: Include the full diff content (default: true)
3334
max_diff_lines: Maximum number of diff lines to include (default: 500)
35+
working_directory: Directory to run git commands in (default: current directory)
3436
"""
3537
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+
3678
# Get list of changed files
3779
files_result = subprocess.run(
3880
["git", "diff", "--name-status", f"{base_branch}...HEAD"],
3981
capture_output=True,
4082
text=True,
41-
check=True
83+
check=True,
84+
cwd=cwd
4285
)
4386

4487
# Get diff statistics
4588
stat_result = subprocess.run(
4689
["git", "diff", "--stat", f"{base_branch}...HEAD"],
4790
capture_output=True,
48-
text=True
91+
text=True,
92+
cwd=cwd
4993
)
5094

5195
# Get the actual diff if requested
@@ -55,7 +99,8 @@ async def analyze_file_changes(
5599
diff_result = subprocess.run(
56100
["git", "diff", f"{base_branch}...HEAD"],
57101
capture_output=True,
58-
text=True
102+
text=True,
103+
cwd=cwd
59104
)
60105
diff_lines = diff_result.stdout.split('\n')
61106

@@ -72,7 +117,8 @@ async def analyze_file_changes(
72117
commits_result = subprocess.run(
73118
["git", "log", "--oneline", f"{base_branch}..HEAD"],
74119
capture_output=True,
75-
text=True
120+
text=True,
121+
cwd=cwd
76122
)
77123

78124
analysis = {
@@ -82,7 +128,8 @@ async def analyze_file_changes(
82128
"commits": commits_result.stdout,
83129
"diff": diff_content if include_diff else "Diff not included (set include_diff=true to see full diff)",
84130
"truncated": truncated,
85-
"total_diff_lines": len(diff_lines) if include_diff else 0
131+
"total_diff_lines": len(diff_lines) if include_diff else 0,
132+
"_debug": debug_info
86133
}
87134

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

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ async def analyze_file_changes(base_branch: str = "main", include_diff: bool = T
4747
# - Adding a max_diff_lines parameter (e.g., 500 lines)
4848
# - Truncating large outputs with a message
4949
# - 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+
5058
return json.dumps({"error": "Not implemented yet", "hint": "Use subprocess to run git commands"})
5159

5260

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

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,29 +28,48 @@
2828
async def analyze_file_changes(
2929
base_branch: str = "main",
3030
include_diff: bool = True,
31-
max_diff_lines: int = 500
31+
max_diff_lines: int = 500,
32+
working_directory: Optional[str] = None
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)
3839
max_diff_lines: Maximum number of diff lines to include (default: 500)
40+
working_directory: Directory to run git commands in (default: current directory)
3941
"""
4042
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()
4158
# Get list of changed files
4259
files_result = subprocess.run(
4360
["git", "diff", "--name-status", f"{base_branch}...HEAD"],
4461
capture_output=True,
4562
text=True,
46-
check=True
63+
check=True,
64+
cwd=cwd
4765
)
4866

4967
# Get diff statistics
5068
stat_result = subprocess.run(
5169
["git", "diff", "--stat", f"{base_branch}...HEAD"],
5270
capture_output=True,
53-
text=True
71+
text=True,
72+
cwd=cwd
5473
)
5574

5675
# Get the actual diff if requested
@@ -60,7 +79,8 @@ async def analyze_file_changes(
6079
diff_result = subprocess.run(
6180
["git", "diff", f"{base_branch}...HEAD"],
6281
capture_output=True,
63-
text=True
82+
text=True,
83+
cwd=cwd
6484
)
6585
diff_lines = diff_result.stdout.split('\n')
6686

@@ -77,7 +97,8 @@ async def analyze_file_changes(
7797
commits_result = subprocess.run(
7898
["git", "log", "--oneline", f"{base_branch}..HEAD"],
7999
capture_output=True,
80-
text=True
100+
text=True,
101+
cwd=cwd
81102
)
82103

83104
analysis = {

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

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,29 +29,48 @@
2929
async def analyze_file_changes(
3030
base_branch: str = "main",
3131
include_diff: bool = True,
32-
max_diff_lines: int = 500
32+
max_diff_lines: int = 500,
33+
working_directory: Optional[str] = None
3334
) -> str:
3435
"""Get the full diff and list of changed files in the current git repository.
3536
3637
Args:
3738
base_branch: Base branch to compare against (default: main)
3839
include_diff: Include the full diff content (default: true)
3940
max_diff_lines: Maximum number of diff lines to include (default: 500)
41+
working_directory: Directory to run git commands in (default: current directory)
4042
"""
4143
try:
44+
# Try to get working directory from roots first
45+
if working_directory is None:
46+
try:
47+
context = mcp.get_context()
48+
roots_result = await context.session.list_roots()
49+
# Get the first root - Claude Code sets this to the CWD
50+
root = roots_result.roots[0]
51+
# FileUrl object has a .path property that gives us the path directly
52+
working_directory = root.uri.path
53+
except Exception as e:
54+
# If we can't get roots, fall back to current directory
55+
pass
56+
57+
# Use provided working directory or current directory
58+
cwd = working_directory if working_directory else os.getcwd()
4259
# Get list of changed files
4360
files_result = subprocess.run(
4461
["git", "diff", "--name-status", f"{base_branch}...HEAD"],
4562
capture_output=True,
4663
text=True,
47-
check=True
64+
check=True,
65+
cwd=cwd
4866
)
4967

5068
# Get diff statistics
5169
stat_result = subprocess.run(
5270
["git", "diff", "--stat", f"{base_branch}...HEAD"],
5371
capture_output=True,
54-
text=True
72+
text=True,
73+
cwd=cwd
5574
)
5675

5776
# Get the actual diff if requested
@@ -61,7 +80,8 @@ async def analyze_file_changes(
6180
diff_result = subprocess.run(
6281
["git", "diff", f"{base_branch}...HEAD"],
6382
capture_output=True,
64-
text=True
83+
text=True,
84+
cwd=cwd
6585
)
6686
diff_lines = diff_result.stdout.split('\n')
6787

@@ -78,7 +98,8 @@ async def analyze_file_changes(
7898
commits_result = subprocess.run(
7999
["git", "log", "--oneline", f"{base_branch}..HEAD"],
80100
capture_output=True,
81-
text=True
101+
text=True,
102+
cwd=cwd
82103
)
83104

84105
analysis = {

units/en/unit3/build-mcp-server.mdx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ This is your first hands-on MCP development experience! Open `server.py` and imp
8888
- ⚠️ **Important**: You'll likely hit a token limit error (25,000 tokens max per response)
8989
- This is a real-world constraint that teaches proper output management
9090
- See the "Handling Large Outputs" section below for the solution
91+
- ⚠️ **Note**: Git commands will run in the MCP server's directory by default. See "Working Directory Considerations" below for details
9192
2. **Implement `get_pr_templates`** to manage and return PR templates
9293
3. **Implement `suggest_template`** to map change types to templates
9394

@@ -254,6 +255,35 @@ async def analyze_file_changes(base_branch: str = "main",
254255
4. **Use progressive disclosure**: Start with high-level info, allow drilling down
255256
5. **Set sensible defaults**: Default to reasonable limits that work for most cases
256257

258+
## Working Directory Considerations
259+
260+
By default, MCP servers run commands in their installation directory, not in Claude's current working directory. This means your git commands might analyze the wrong repository!
261+
262+
To solve this, MCP provides [roots](https://modelcontextprotocol.io/docs/concepts/roots) - a way for clients to inform servers about relevant directories. Claude Code automatically provides its working directory as a root.
263+
264+
Here's how to access it in your tool:
265+
266+
```python
267+
@mcp.tool()
268+
async def analyze_file_changes(...):
269+
# Get Claude's working directory from roots
270+
context = mcp.get_context()
271+
roots_result = await context.session.list_roots()
272+
273+
# Extract the path from the FileUrl object
274+
working_dir = roots_result.roots[0].uri.path
275+
276+
# Use it for all git commands
277+
result = subprocess.run(
278+
["git", "diff", "--name-status"],
279+
capture_output=True,
280+
text=True,
281+
cwd=working_dir # Run in Claude's directory!
282+
)
283+
```
284+
285+
This ensures your tools operate on the repository Claude is actually working with, not the MCP server's installation location.
286+
257287
## Troubleshooting
258288

259289
- **Import errors**: Ensure you've run `uv sync`
@@ -262,6 +292,17 @@ async def analyze_file_changes(base_branch: str = "main",
262292
- **JSON errors**: All tools must return valid JSON strings
263293
- **Token limit exceeded**: This is expected with large diffs! Implement output limiting as shown above
264294
- **"Response too large" errors**: Add `max_diff_lines` parameter or set `include_diff=false`
295+
- **Git commands run in wrong directory**: MCP servers run in their installation directory by default, not Claude's working directory. To fix this, use [MCP roots](https://modelcontextprotocol.io/docs/concepts/roots) to access Claude's current directory:
296+
```python
297+
# Get Claude's working directory from roots
298+
context = mcp.get_context()
299+
roots_result = await context.session.list_roots()
300+
working_dir = roots_result.roots[0].uri.path # FileUrl object has .path property
301+
302+
# Use it in subprocess calls
303+
subprocess.run(["git", "diff"], cwd=working_dir)
304+
```
305+
Claude Code automatically provides its working directory as a root, allowing your MCP server to operate in the correct location.
265306

266307
## Next Steps
267308

0 commit comments

Comments
 (0)