deepcode-hku 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. cli/__init__.py +18 -0
  2. cli/cli_app.py +296 -0
  3. cli/cli_interface.py +744 -0
  4. cli/cli_launcher.py +155 -0
  5. cli/main_cli.py +243 -0
  6. cli/workflows/__init__.py +11 -0
  7. cli/workflows/cli_workflow_adapter.py +336 -0
  8. deepcode.py +219 -0
  9. deepcode_hku-1.0.1.dist-info/METADATA +695 -0
  10. deepcode_hku-1.0.1.dist-info/RECORD +44 -0
  11. deepcode_hku-1.0.1.dist-info/WHEEL +5 -0
  12. deepcode_hku-1.0.1.dist-info/entry_points.txt +2 -0
  13. deepcode_hku-1.0.1.dist-info/licenses/LICENSE +21 -0
  14. deepcode_hku-1.0.1.dist-info/top_level.txt +6 -0
  15. tools/__init__.py +0 -0
  16. tools/code_implementation_server.py +1045 -0
  17. tools/code_indexer.py +1657 -0
  18. tools/code_reference_indexer.py +486 -0
  19. tools/command_executor.py +324 -0
  20. tools/git_command.py +356 -0
  21. tools/pdf_converter.py +640 -0
  22. tools/pdf_downloader.py +1370 -0
  23. tools/pdf_utils.py +52 -0
  24. ui/__init__.py +43 -0
  25. ui/app.py +13 -0
  26. ui/components.py +1450 -0
  27. ui/handlers.py +773 -0
  28. ui/layout.py +106 -0
  29. ui/streamlit_app.py +38 -0
  30. ui/styles.py +2116 -0
  31. utils/__init__.py +17 -0
  32. utils/cli_interface.py +459 -0
  33. utils/dialogue_logger.py +671 -0
  34. utils/file_processor.py +426 -0
  35. utils/simple_llm_logger.py +198 -0
  36. workflows/__init__.py +31 -0
  37. workflows/agent_orchestration_engine.py +1371 -0
  38. workflows/agents/__init__.py +13 -0
  39. workflows/agents/code_implementation_agent.py +1093 -0
  40. workflows/agents/memory_agent_concise.py +923 -0
  41. workflows/agents/memory_agent_concise_index.py +935 -0
  42. workflows/code_implementation_workflow.py +924 -0
  43. workflows/code_implementation_workflow_index.py +931 -0
  44. workflows/codebase_index_workflow.py +726 -0
@@ -0,0 +1,1045 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Code Implementation MCP Server
4
+
5
+ This MCP server provides core functions needed for paper code reproduction:
6
+ 1. File read/write operations
7
+ 2. Code execution and testing
8
+ 3. Code search and analysis
9
+ 4. Iterative improvement support
10
+
11
+ Usage:
12
+ python tools/code_implementation_server.py
13
+ """
14
+
15
+ import os
16
+ import subprocess
17
+ import json
18
+ import sys
19
+ import io
20
+ from pathlib import Path
21
+ import re
22
+ from typing import Dict, Any
23
+ import tempfile
24
+ import shutil
25
+ import logging
26
+ from datetime import datetime
27
+
28
+ # Set standard output encoding to UTF-8
29
+ if sys.stdout.encoding != "utf-8":
30
+ try:
31
+ if hasattr(sys.stdout, "reconfigure"):
32
+ sys.stdout.reconfigure(encoding="utf-8")
33
+ sys.stderr.reconfigure(encoding="utf-8")
34
+ else:
35
+ sys.stdout = io.TextIOWrapper(sys.stdout.detach(), encoding="utf-8")
36
+ sys.stderr = io.TextIOWrapper(sys.stderr.detach(), encoding="utf-8")
37
+ except Exception as e:
38
+ print(f"Warning: Could not set UTF-8 encoding: {e}")
39
+
40
+ # Import MCP related modules
41
+ from mcp.server.fastmcp import FastMCP
42
+
43
+ # Setup logging
44
+ logging.basicConfig(level=logging.INFO)
45
+ logger = logging.getLogger(__name__)
46
+
47
+ # Create FastMCP server instance
48
+ mcp = FastMCP("code-implementation-server")
49
+
50
+ # Global variables: workspace directory and operation history
51
+ WORKSPACE_DIR = None
52
+ OPERATION_HISTORY = []
53
+ CURRENT_FILES = {}
54
+
55
+
56
+ def initialize_workspace(workspace_dir: str = None):
57
+ """
58
+ Initialize workspace
59
+
60
+ By default, the workspace will be set by the workflow via the set_workspace tool to:
61
+ {plan_file_parent}/generate_code
62
+
63
+ Args:
64
+ workspace_dir: Optional workspace directory path
65
+ """
66
+ global WORKSPACE_DIR
67
+ if workspace_dir is None:
68
+ # Default to generate_code directory under current directory, but don't create immediately
69
+ # This default value will be overridden by workflow via set_workspace tool
70
+ WORKSPACE_DIR = Path.cwd() / "generate_code"
71
+ # logger.info(f"Workspace initialized (default value, will be overridden by workflow): {WORKSPACE_DIR}")
72
+ # logger.info("Note: Actual workspace will be set by workflow via set_workspace tool to {plan_file_parent}/generate_code")
73
+ else:
74
+ WORKSPACE_DIR = Path(workspace_dir).resolve()
75
+ # Only create when explicitly specified
76
+ WORKSPACE_DIR.mkdir(parents=True, exist_ok=True)
77
+ logger.info(f"Workspace initialized: {WORKSPACE_DIR}")
78
+
79
+
80
+ def ensure_workspace_exists():
81
+ """Ensure workspace directory exists"""
82
+ global WORKSPACE_DIR
83
+ if WORKSPACE_DIR is None:
84
+ initialize_workspace()
85
+
86
+ # Create workspace directory (if it doesn't exist)
87
+ if not WORKSPACE_DIR.exists():
88
+ WORKSPACE_DIR.mkdir(parents=True, exist_ok=True)
89
+ logger.info(f"Workspace directory created: {WORKSPACE_DIR}")
90
+
91
+
92
+ def validate_path(path: str) -> Path:
93
+ """Validate if path is within workspace"""
94
+ if WORKSPACE_DIR is None:
95
+ initialize_workspace()
96
+
97
+ full_path = (WORKSPACE_DIR / path).resolve()
98
+ if not str(full_path).startswith(str(WORKSPACE_DIR)):
99
+ raise ValueError(f"Path {path} is outside workspace scope")
100
+ return full_path
101
+
102
+
103
+ def log_operation(action: str, details: Dict[str, Any]):
104
+ """Log operation history"""
105
+ OPERATION_HISTORY.append(
106
+ {"timestamp": datetime.now().isoformat(), "action": action, "details": details}
107
+ )
108
+
109
+
110
+ # ==================== File Operation Tools ====================
111
+
112
+
113
+ @mcp.tool()
114
+ async def read_file(
115
+ file_path: str, start_line: int = None, end_line: int = None
116
+ ) -> str:
117
+ """
118
+ Read file content, supports specifying line number range
119
+
120
+ Args:
121
+ file_path: File path, relative to workspace
122
+ start_line: Starting line number (1-based, optional)
123
+ end_line: Ending line number (1-based, optional)
124
+
125
+ Returns:
126
+ JSON string of file content or error message
127
+ """
128
+ try:
129
+ full_path = validate_path(file_path)
130
+
131
+ if not full_path.exists():
132
+ result = {"status": "error", "message": f"File does not exist: {file_path}"}
133
+ log_operation(
134
+ "read_file_error", {"file_path": file_path, "error": "file_not_found"}
135
+ )
136
+ return json.dumps(result, ensure_ascii=False, indent=2)
137
+
138
+ with open(full_path, "r", encoding="utf-8") as f:
139
+ lines = f.readlines()
140
+
141
+ # 处理行号范围
142
+ if start_line is not None or end_line is not None:
143
+ start_idx = (start_line - 1) if start_line else 0
144
+ end_idx = end_line if end_line else len(lines)
145
+ lines = lines[start_idx:end_idx]
146
+
147
+ content = "".join(lines)
148
+
149
+ result = {
150
+ "status": "success",
151
+ "content": content,
152
+ "file_path": file_path,
153
+ "total_lines": len(lines),
154
+ "size_bytes": len(content.encode("utf-8")),
155
+ }
156
+
157
+ log_operation(
158
+ "read_file",
159
+ {
160
+ "file_path": file_path,
161
+ "start_line": start_line,
162
+ "end_line": end_line,
163
+ "lines_read": len(lines),
164
+ },
165
+ )
166
+
167
+ return json.dumps(result, ensure_ascii=False, indent=2)
168
+
169
+ except Exception as e:
170
+ result = {
171
+ "status": "error",
172
+ "message": f"Failed to read file: {str(e)}",
173
+ "file_path": file_path,
174
+ }
175
+ log_operation("read_file_error", {"file_path": file_path, "error": str(e)})
176
+ return json.dumps(result, ensure_ascii=False, indent=2)
177
+
178
+
179
+ @mcp.tool()
180
+ async def write_file(
181
+ file_path: str, content: str, create_dirs: bool = True, create_backup: bool = False
182
+ ) -> str:
183
+ """
184
+ Write content to file
185
+
186
+ Args:
187
+ file_path: File path, relative to workspace
188
+ content: Content to write to file
189
+ create_dirs: Whether to create directories if they don't exist
190
+ create_backup: Whether to create backup file if file already exists
191
+
192
+ Returns:
193
+ JSON string of operation result
194
+ """
195
+ try:
196
+ full_path = validate_path(file_path)
197
+
198
+ # Create directories (if needed)
199
+ if create_dirs:
200
+ full_path.parent.mkdir(parents=True, exist_ok=True)
201
+
202
+ # Backup existing file (only when explicitly requested)
203
+ backup_created = False
204
+ if full_path.exists() and create_backup:
205
+ backup_path = full_path.with_suffix(full_path.suffix + ".backup")
206
+ shutil.copy2(full_path, backup_path)
207
+ backup_created = True
208
+
209
+ # Write file
210
+ with open(full_path, "w", encoding="utf-8") as f:
211
+ f.write(content)
212
+
213
+ # Update current file record
214
+ CURRENT_FILES[file_path] = {
215
+ "last_modified": datetime.now().isoformat(),
216
+ "size_bytes": len(content.encode("utf-8")),
217
+ "lines": len(content.split("\n")),
218
+ }
219
+
220
+ result = {
221
+ "status": "success",
222
+ "message": f"File written successfully: {file_path}",
223
+ "file_path": file_path,
224
+ "size_bytes": len(content.encode("utf-8")),
225
+ "lines_written": len(content.split("\n")),
226
+ "backup_created": backup_created,
227
+ }
228
+
229
+ log_operation(
230
+ "write_file",
231
+ {
232
+ "file_path": file_path,
233
+ "size_bytes": len(content.encode("utf-8")),
234
+ "lines": len(content.split("\n")),
235
+ "backup_created": backup_created,
236
+ },
237
+ )
238
+
239
+ return json.dumps(result, ensure_ascii=False, indent=2)
240
+
241
+ except Exception as e:
242
+ result = {
243
+ "status": "error",
244
+ "message": f"Failed to write file: {str(e)}",
245
+ "file_path": file_path,
246
+ }
247
+ log_operation("write_file_error", {"file_path": file_path, "error": str(e)})
248
+ return json.dumps(result, ensure_ascii=False, indent=2)
249
+
250
+
251
+ # ==================== Code Execution Tools ====================
252
+
253
+
254
+ @mcp.tool()
255
+ async def execute_python(code: str, timeout: int = 30) -> str:
256
+ """
257
+ Execute Python code and return output
258
+
259
+ Args:
260
+ code: Python code to execute
261
+ timeout: Timeout in seconds
262
+
263
+ Returns:
264
+ JSON string of execution result
265
+ """
266
+ try:
267
+ # Create temporary file
268
+ with tempfile.NamedTemporaryFile(
269
+ mode="w", suffix=".py", delete=False, encoding="utf-8"
270
+ ) as f:
271
+ f.write(code)
272
+ temp_file = f.name
273
+
274
+ try:
275
+ # Ensure workspace directory exists
276
+ ensure_workspace_exists()
277
+
278
+ # Execute Python code
279
+ result = subprocess.run(
280
+ [sys.executable, temp_file],
281
+ cwd=WORKSPACE_DIR,
282
+ capture_output=True,
283
+ text=True,
284
+ timeout=timeout,
285
+ encoding="utf-8",
286
+ )
287
+
288
+ execution_result = {
289
+ "status": "success" if result.returncode == 0 else "error",
290
+ "return_code": result.returncode,
291
+ "stdout": result.stdout,
292
+ "stderr": result.stderr,
293
+ "timeout": timeout,
294
+ }
295
+
296
+ if result.returncode != 0:
297
+ execution_result["message"] = "Python code execution failed"
298
+ else:
299
+ execution_result["message"] = "Python code execution successful"
300
+
301
+ log_operation(
302
+ "execute_python",
303
+ {
304
+ "return_code": result.returncode,
305
+ "stdout_length": len(result.stdout),
306
+ "stderr_length": len(result.stderr),
307
+ },
308
+ )
309
+
310
+ return json.dumps(execution_result, ensure_ascii=False, indent=2)
311
+
312
+ finally:
313
+ # Clean up temporary file
314
+ os.unlink(temp_file)
315
+
316
+ except subprocess.TimeoutExpired:
317
+ result = {
318
+ "status": "error",
319
+ "message": f"Python code execution timeout ({timeout}秒)",
320
+ "timeout": timeout,
321
+ }
322
+ log_operation("execute_python_timeout", {"timeout": timeout})
323
+ return json.dumps(result, ensure_ascii=False, indent=2)
324
+
325
+ except Exception as e:
326
+ result = {
327
+ "status": "error",
328
+ "message": f"Python code execution failed: {str(e)}",
329
+ }
330
+ log_operation("execute_python_error", {"error": str(e)})
331
+ return json.dumps(result, ensure_ascii=False, indent=2)
332
+
333
+
334
+ @mcp.tool()
335
+ async def execute_bash(command: str, timeout: int = 30) -> str:
336
+ """
337
+ Execute bash command
338
+
339
+ Args:
340
+ command: Bash command to execute
341
+ timeout: Timeout in seconds
342
+
343
+ Returns:
344
+ JSON string of execution result
345
+ """
346
+ try:
347
+ # 安全检查:禁止危险命令
348
+ dangerous_commands = ["rm -rf", "sudo", "chmod 777", "mkfs", "dd if="]
349
+ if any(dangerous in command.lower() for dangerous in dangerous_commands):
350
+ result = {
351
+ "status": "error",
352
+ "message": f"Dangerous command execution prohibited: {command}",
353
+ }
354
+ log_operation(
355
+ "execute_bash_blocked",
356
+ {"command": command, "reason": "dangerous_command"},
357
+ )
358
+ return json.dumps(result, ensure_ascii=False, indent=2)
359
+
360
+ # Ensure workspace directory exists
361
+ ensure_workspace_exists()
362
+
363
+ # Execute command
364
+ result = subprocess.run(
365
+ command,
366
+ shell=True,
367
+ cwd=WORKSPACE_DIR,
368
+ capture_output=True,
369
+ text=True,
370
+ timeout=timeout,
371
+ encoding="utf-8",
372
+ )
373
+
374
+ execution_result = {
375
+ "status": "success" if result.returncode == 0 else "error",
376
+ "return_code": result.returncode,
377
+ "stdout": result.stdout,
378
+ "stderr": result.stderr,
379
+ "command": command,
380
+ "timeout": timeout,
381
+ }
382
+
383
+ if result.returncode != 0:
384
+ execution_result["message"] = "Bash command execution failed"
385
+ else:
386
+ execution_result["message"] = "Bash command execution successful"
387
+
388
+ log_operation(
389
+ "execute_bash",
390
+ {
391
+ "command": command,
392
+ "return_code": result.returncode,
393
+ "stdout_length": len(result.stdout),
394
+ "stderr_length": len(result.stderr),
395
+ },
396
+ )
397
+
398
+ return json.dumps(execution_result, ensure_ascii=False, indent=2)
399
+
400
+ except subprocess.TimeoutExpired:
401
+ result = {
402
+ "status": "error",
403
+ "message": f"Bash command execution timeout ({timeout} seconds)",
404
+ "command": command,
405
+ "timeout": timeout,
406
+ }
407
+ log_operation("execute_bash_timeout", {"command": command, "timeout": timeout})
408
+ return json.dumps(result, ensure_ascii=False, indent=2)
409
+
410
+ except Exception as e:
411
+ result = {
412
+ "status": "error",
413
+ "message": f"Failed to execute bash command: {str(e)}",
414
+ "command": command,
415
+ }
416
+ log_operation("execute_bash_error", {"command": command, "error": str(e)})
417
+ return json.dumps(result, ensure_ascii=False, indent=2)
418
+
419
+
420
+ @mcp.tool()
421
+ async def read_code_mem(file_path: str) -> str:
422
+ """
423
+ Check if file summary exists in implement_code_summary.md
424
+
425
+ Args:
426
+ file_path: File path to check for summary information in implement_code_summary.md
427
+
428
+ Returns:
429
+ Summary information if available
430
+ """
431
+ try:
432
+ if not file_path:
433
+ result = {"status": "error", "message": "file_path parameter is required"}
434
+ log_operation("read_code_mem_error", {"error": "missing_file_path"})
435
+ return json.dumps(result, ensure_ascii=False, indent=2)
436
+
437
+ # Ensure workspace exists
438
+ ensure_workspace_exists()
439
+
440
+ # Look for implement_code_summary.md in the workspace
441
+ current_path = Path(WORKSPACE_DIR)
442
+ summary_file_path = current_path.parent / "implement_code_summary.md"
443
+
444
+ if not summary_file_path.exists():
445
+ result = {
446
+ "status": "no_summary",
447
+ "file_path": file_path,
448
+ "message": "No summary file found.",
449
+ # "recommendation": f"read_file(file_path='{file_path}')"
450
+ }
451
+ log_operation(
452
+ "read_code_mem", {"file_path": file_path, "status": "no_summary_file"}
453
+ )
454
+ return json.dumps(result, ensure_ascii=False, indent=2)
455
+
456
+ # Read the summary file
457
+ with open(summary_file_path, "r", encoding="utf-8") as f:
458
+ summary_content = f.read()
459
+
460
+ if not summary_content.strip():
461
+ result = {
462
+ "status": "no_summary",
463
+ "file_path": file_path,
464
+ "message": "Summary file is empty.",
465
+ # "recommendation": f"read_file(file_path='{file_path}')"
466
+ }
467
+ log_operation(
468
+ "read_code_mem", {"file_path": file_path, "status": "empty_summary"}
469
+ )
470
+ return json.dumps(result, ensure_ascii=False, indent=2)
471
+
472
+ # Extract file-specific section from summary
473
+ file_section = _extract_file_section_from_summary(summary_content, file_path)
474
+
475
+ if file_section:
476
+ result = {
477
+ "status": "summary_found",
478
+ "file_path": file_path,
479
+ "summary_content": file_section,
480
+ "message": f"Summary information found for {file_path} in implement_code_summary.md",
481
+ }
482
+ log_operation(
483
+ "read_code_mem",
484
+ {
485
+ "file_path": file_path,
486
+ "status": "summary_found",
487
+ "section_length": len(file_section),
488
+ },
489
+ )
490
+ return json.dumps(result, ensure_ascii=False, indent=2)
491
+ else:
492
+ result = {
493
+ "status": "no_summary",
494
+ "file_path": file_path,
495
+ "message": f"No summary found for {file_path} in implement_code_summary.md",
496
+ # "recommendation": f"Use read_file tool to read the actual file: read_file(file_path='{file_path}')"
497
+ }
498
+ log_operation(
499
+ "read_code_mem", {"file_path": file_path, "status": "no_match"}
500
+ )
501
+ return json.dumps(result, ensure_ascii=False, indent=2)
502
+
503
+ except Exception as e:
504
+ result = {
505
+ "status": "error",
506
+ "message": f"Failed to check code memory: {str(e)}",
507
+ "file_path": file_path,
508
+ # "recommendation": "Use read_file tool instead"
509
+ }
510
+ log_operation("read_code_mem_error", {"file_path": file_path, "error": str(e)})
511
+ return json.dumps(result, ensure_ascii=False, indent=2)
512
+
513
+
514
+ def _extract_file_section_from_summary(
515
+ summary_content: str, target_file_path: str
516
+ ) -> str:
517
+ """
518
+ Extract the specific section for a file from the summary content
519
+
520
+ Args:
521
+ summary_content: Full summary content
522
+ target_file_path: Path of the target file
523
+
524
+ Returns:
525
+ File-specific section or None if not found
526
+ """
527
+ import re
528
+
529
+ # Normalize the target path for comparison
530
+ normalized_target = _normalize_file_path(target_file_path)
531
+
532
+ # Pattern to match implementation sections with separator lines
533
+ section_pattern = r"={80}\s*\n## IMPLEMENTATION File ([^;]+); ROUND \d+\s*\n={80}(.*?)(?=\n={80}|\Z)"
534
+
535
+ matches = re.findall(section_pattern, summary_content, re.DOTALL)
536
+
537
+ for file_path_in_summary, section_content in matches:
538
+ file_path_in_summary = file_path_in_summary.strip()
539
+ section_content = section_content.strip()
540
+
541
+ # Normalize the path from summary for comparison
542
+ normalized_summary_path = _normalize_file_path(file_path_in_summary)
543
+
544
+ # Check if paths match using multiple strategies
545
+ if _paths_match(
546
+ normalized_target,
547
+ normalized_summary_path,
548
+ target_file_path,
549
+ file_path_in_summary,
550
+ ):
551
+ # Return the complete section with proper formatting
552
+ file_section = f"""================================================================================
553
+ ## IMPLEMENTATION File {file_path_in_summary}; ROUND [X]
554
+ ================================================================================
555
+
556
+ {section_content}
557
+
558
+ ---
559
+ *Extracted from implement_code_summary.md*"""
560
+ return file_section
561
+
562
+ # If no section-based match, try alternative parsing method
563
+ return _extract_file_section_alternative(summary_content, target_file_path)
564
+
565
+
566
+ def _normalize_file_path(file_path: str) -> str:
567
+ """Normalize file path for comparison"""
568
+ # Remove leading/trailing slashes and convert to lowercase
569
+ normalized = file_path.strip("/").lower()
570
+ # Replace backslashes with forward slashes
571
+ normalized = normalized.replace("\\", "/")
572
+
573
+ # Remove common prefixes to make matching more flexible
574
+ common_prefixes = ["rice/", "src/", "./rice/", "./src/", "./"]
575
+ for prefix in common_prefixes:
576
+ if normalized.startswith(prefix):
577
+ normalized = normalized[len(prefix) :]
578
+ break
579
+
580
+ return normalized
581
+
582
+
583
+ def _paths_match(
584
+ normalized_target: str,
585
+ normalized_summary: str,
586
+ original_target: str,
587
+ original_summary: str,
588
+ ) -> bool:
589
+ """Check if two file paths match using multiple strategies"""
590
+
591
+ # Strategy 1: Exact normalized match
592
+ if normalized_target == normalized_summary:
593
+ return True
594
+
595
+ # Strategy 2: Basename match (filename only)
596
+ target_basename = os.path.basename(original_target)
597
+ summary_basename = os.path.basename(original_summary)
598
+ if target_basename == summary_basename and len(target_basename) > 4:
599
+ return True
600
+
601
+ # Strategy 3: Suffix match (remove common prefixes and compare)
602
+ target_suffix = _remove_common_prefixes(normalized_target)
603
+ summary_suffix = _remove_common_prefixes(normalized_summary)
604
+ if target_suffix == summary_suffix:
605
+ return True
606
+
607
+ # Strategy 4: Ends with match
608
+ if normalized_target.endswith(normalized_summary) or normalized_summary.endswith(
609
+ normalized_target
610
+ ):
611
+ return True
612
+
613
+ # Strategy 5: Contains match for longer paths
614
+ if len(normalized_target) > 10 and normalized_target in normalized_summary:
615
+ return True
616
+ if len(normalized_summary) > 10 and normalized_summary in normalized_target:
617
+ return True
618
+
619
+ return False
620
+
621
+
622
+ def _remove_common_prefixes(file_path: str) -> str:
623
+ """Remove common prefixes from file path"""
624
+ prefixes_to_remove = ["rice/", "src/", "core/", "./"]
625
+ path = file_path
626
+
627
+ for prefix in prefixes_to_remove:
628
+ if path.startswith(prefix):
629
+ path = path[len(prefix) :]
630
+
631
+ return path
632
+
633
+
634
+ def _extract_file_section_alternative(
635
+ summary_content: str, target_file_path: str
636
+ ) -> str:
637
+ """Alternative method to extract file section using simpler pattern matching"""
638
+
639
+ # Get the basename for fallback matching
640
+ target_basename = os.path.basename(target_file_path)
641
+
642
+ # Split by separator lines to get individual sections
643
+ sections = summary_content.split("=" * 80)
644
+
645
+ for i, section in enumerate(sections):
646
+ if "## IMPLEMENTATION File" in section:
647
+ # Extract the file path from the header
648
+ lines = section.strip().split("\n")
649
+ for line in lines:
650
+ if "## IMPLEMENTATION File" in line:
651
+ # Extract file path between "File " and "; ROUND"
652
+ try:
653
+ file_part = line.split("File ")[1].split("; ROUND")[0].strip()
654
+
655
+ # Check if this matches our target
656
+ if (
657
+ _normalize_file_path(target_file_path)
658
+ == _normalize_file_path(file_part)
659
+ or target_basename == os.path.basename(file_part)
660
+ or target_file_path in file_part
661
+ or file_part.endswith(target_file_path)
662
+ ):
663
+ # Get the next section which contains the content
664
+ if i + 1 < len(sections):
665
+ content_section = sections[i + 1].strip()
666
+ return f"""================================================================================
667
+ ## IMPLEMENTATION File {file_part}
668
+ ================================================================================
669
+
670
+ {content_section}
671
+
672
+ ---
673
+ *Extracted from implement_code_summary.md using alternative method*"""
674
+ except (IndexError, AttributeError):
675
+ continue
676
+
677
+ return None
678
+
679
+
680
+ # ==================== Code Search Tools ====================
681
+
682
+
683
+ @mcp.tool()
684
+ async def search_code(
685
+ pattern: str,
686
+ file_pattern: str = "*.json",
687
+ use_regex: bool = False,
688
+ search_directory: str = None,
689
+ ) -> str:
690
+ """
691
+ Search patterns in code files
692
+
693
+ Args:
694
+ pattern: Search pattern
695
+ file_pattern: File pattern (e.g., '*.py')
696
+ use_regex: Whether to use regular expressions
697
+ search_directory: Specify search directory (optional, uses WORKSPACE_DIR if not specified)
698
+
699
+ Returns:
700
+ JSON string of search results
701
+ """
702
+ try:
703
+ # Determine search directory
704
+ if search_directory:
705
+ # If search directory is specified, use the specified directory
706
+ if os.path.isabs(search_directory):
707
+ search_path = Path(search_directory)
708
+ else:
709
+ # Relative path, relative to current working directory
710
+ search_path = Path.cwd() / search_directory
711
+ else:
712
+ # 如果没有指定Search directory,使用默认的WORKSPACE_DIR
713
+ ensure_workspace_exists()
714
+ search_path = WORKSPACE_DIR
715
+
716
+ # 检查Search directory是否存在
717
+ if not search_path.exists():
718
+ result = {
719
+ "status": "error",
720
+ "message": f"Search directory不存在: {search_path}",
721
+ "pattern": pattern,
722
+ }
723
+ return json.dumps(result, ensure_ascii=False, indent=2)
724
+
725
+ import glob
726
+
727
+ # Get matching files
728
+ file_paths = glob.glob(str(search_path / "**" / file_pattern), recursive=True)
729
+
730
+ matches = []
731
+ total_files_searched = 0
732
+
733
+ for file_path in file_paths:
734
+ try:
735
+ with open(file_path, "r", encoding="utf-8") as f:
736
+ lines = f.readlines()
737
+
738
+ total_files_searched += 1
739
+ relative_path = os.path.relpath(file_path, search_path)
740
+
741
+ for line_num, line in enumerate(lines, 1):
742
+ if use_regex:
743
+ if re.search(pattern, line):
744
+ matches.append(
745
+ {
746
+ "file": relative_path,
747
+ "line_number": line_num,
748
+ "line_content": line.strip(),
749
+ "match_type": "regex",
750
+ }
751
+ )
752
+ else:
753
+ if pattern.lower() in line.lower():
754
+ matches.append(
755
+ {
756
+ "file": relative_path,
757
+ "line_number": line_num,
758
+ "line_content": line.strip(),
759
+ "match_type": "substring",
760
+ }
761
+ )
762
+
763
+ except Exception as e:
764
+ logger.warning(f"Error searching file {file_path}: {e}")
765
+ continue
766
+
767
+ result = {
768
+ "status": "success",
769
+ "pattern": pattern,
770
+ "file_pattern": file_pattern,
771
+ "use_regex": use_regex,
772
+ "search_directory": str(search_path),
773
+ "total_matches": len(matches),
774
+ "total_files_searched": total_files_searched,
775
+ "matches": matches[:50], # 限制返回前50个匹配
776
+ }
777
+
778
+ if len(matches) > 50:
779
+ result["note"] = f"显示前50个匹配,总共找到{len(matches)}个匹配"
780
+
781
+ log_operation(
782
+ "search_code",
783
+ {
784
+ "pattern": pattern,
785
+ "file_pattern": file_pattern,
786
+ "use_regex": use_regex,
787
+ "search_directory": str(search_path),
788
+ "total_matches": len(matches),
789
+ "files_searched": total_files_searched,
790
+ },
791
+ )
792
+
793
+ return json.dumps(result, ensure_ascii=False, indent=2)
794
+
795
+ except Exception as e:
796
+ result = {
797
+ "status": "error",
798
+ "message": f"Code search failed: {str(e)}",
799
+ "pattern": pattern,
800
+ }
801
+ log_operation("search_code_error", {"pattern": pattern, "error": str(e)})
802
+ return json.dumps(result, ensure_ascii=False, indent=2)
803
+
804
+
805
+ # ==================== File Structure Tools ====================
806
+
807
+
808
+ @mcp.tool()
809
+ async def get_file_structure(directory: str = ".", max_depth: int = 5) -> str:
810
+ """
811
+ Get directory file structure
812
+
813
+ Args:
814
+ directory: Directory path, relative to workspace
815
+ max_depth: 最大遍历深度
816
+
817
+ Returns:
818
+ JSON string of file structure
819
+ """
820
+ try:
821
+ ensure_workspace_exists()
822
+
823
+ if directory == ".":
824
+ target_dir = WORKSPACE_DIR
825
+ else:
826
+ target_dir = validate_path(directory)
827
+
828
+ if not target_dir.exists():
829
+ result = {
830
+ "status": "error",
831
+ "message": f"Directory does not exist: {directory}",
832
+ }
833
+ return json.dumps(result, ensure_ascii=False, indent=2)
834
+
835
+ def scan_directory(path: Path, current_depth: int = 0) -> Dict[str, Any]:
836
+ """Recursively scan directory"""
837
+ if current_depth >= max_depth:
838
+ return {"type": "directory", "name": path.name, "truncated": True}
839
+
840
+ items = []
841
+ try:
842
+ for item in sorted(path.iterdir()):
843
+ relative_path = os.path.relpath(item, WORKSPACE_DIR)
844
+
845
+ if item.is_file():
846
+ file_info = {
847
+ "type": "file",
848
+ "name": item.name,
849
+ "path": relative_path,
850
+ "size_bytes": item.stat().st_size,
851
+ "extension": item.suffix,
852
+ }
853
+ items.append(file_info)
854
+ elif item.is_dir() and not item.name.startswith("."):
855
+ dir_info = scan_directory(item, current_depth + 1)
856
+ dir_info["path"] = relative_path
857
+ items.append(dir_info)
858
+ except PermissionError:
859
+ pass
860
+
861
+ return {
862
+ "type": "directory",
863
+ "name": path.name,
864
+ "items": items,
865
+ "item_count": len(items),
866
+ }
867
+
868
+ structure = scan_directory(target_dir)
869
+
870
+ # 统计信息
871
+ def count_items(node):
872
+ if node["type"] == "file":
873
+ return {"files": 1, "directories": 0}
874
+ else:
875
+ counts = {"files": 0, "directories": 1}
876
+ for item in node.get("items", []):
877
+ item_counts = count_items(item)
878
+ counts["files"] += item_counts["files"]
879
+ counts["directories"] += item_counts["directories"]
880
+ return counts
881
+
882
+ counts = count_items(structure)
883
+
884
+ result = {
885
+ "status": "success",
886
+ "directory": directory,
887
+ "max_depth": max_depth,
888
+ "structure": structure,
889
+ "summary": {
890
+ "total_files": counts["files"],
891
+ "total_directories": counts["directories"]
892
+ - 1, # Exclude root directory
893
+ },
894
+ }
895
+
896
+ log_operation(
897
+ "get_file_structure",
898
+ {
899
+ "directory": directory,
900
+ "max_depth": max_depth,
901
+ "total_files": counts["files"],
902
+ "total_directories": counts["directories"] - 1,
903
+ },
904
+ )
905
+
906
+ return json.dumps(result, ensure_ascii=False, indent=2)
907
+
908
+ except Exception as e:
909
+ result = {
910
+ "status": "error",
911
+ "message": f"Failed to get file structure: {str(e)}",
912
+ "directory": directory,
913
+ }
914
+ log_operation(
915
+ "get_file_structure_error", {"directory": directory, "error": str(e)}
916
+ )
917
+ return json.dumps(result, ensure_ascii=False, indent=2)
918
+
919
+
920
+ # ==================== Workspace Management Tools ====================
921
+
922
+
923
+ @mcp.tool()
924
+ async def set_workspace(workspace_path: str) -> str:
925
+ """
926
+ Set workspace directory
927
+
928
+ Called by workflow to set workspace to: {plan_file_parent}/generate_code
929
+ This ensures all file operations are executed relative to the correct project directory
930
+
931
+ Args:
932
+ workspace_path: Workspace path (Usually {plan_file_parent}/generate_code)
933
+
934
+ Returns:
935
+ JSON string of operation result
936
+ """
937
+ try:
938
+ global WORKSPACE_DIR
939
+ new_workspace = Path(workspace_path).resolve()
940
+
941
+ # Create directory (if it does not exist)
942
+ new_workspace.mkdir(parents=True, exist_ok=True)
943
+
944
+ old_workspace = WORKSPACE_DIR
945
+ WORKSPACE_DIR = new_workspace
946
+
947
+ logger.info(f"New Workspace: {WORKSPACE_DIR}")
948
+
949
+ result = {
950
+ "status": "success",
951
+ "message": f"Workspace setup successful: {workspace_path}",
952
+ "new_workspace": str(WORKSPACE_DIR),
953
+ }
954
+
955
+ log_operation(
956
+ "set_workspace",
957
+ {
958
+ "old_workspace": str(old_workspace) if old_workspace else None,
959
+ "new_workspace": str(WORKSPACE_DIR),
960
+ "workspace_alignment": "plan_file_parent/generate_code",
961
+ },
962
+ )
963
+
964
+ return json.dumps(result, ensure_ascii=False, indent=2)
965
+
966
+ except Exception as e:
967
+ result = {
968
+ "status": "error",
969
+ "message": f"Failed to set workspace: {str(e)}",
970
+ "workspace_path": workspace_path,
971
+ }
972
+ log_operation(
973
+ "set_workspace_error", {"workspace_path": workspace_path, "error": str(e)}
974
+ )
975
+ return json.dumps(result, ensure_ascii=False, indent=2)
976
+
977
+
978
+ @mcp.tool()
979
+ async def get_operation_history(last_n: int = 10) -> str:
980
+ """
981
+ Get operation history
982
+
983
+ Args:
984
+ last_n: Return the last N operations
985
+
986
+ Returns:
987
+ JSON string of operation history
988
+ """
989
+ try:
990
+ recent_history = (
991
+ OPERATION_HISTORY[-last_n:] if last_n > 0 else OPERATION_HISTORY
992
+ )
993
+
994
+ result = {
995
+ "status": "success",
996
+ "total_operations": len(OPERATION_HISTORY),
997
+ "returned_operations": len(recent_history),
998
+ "workspace": str(WORKSPACE_DIR) if WORKSPACE_DIR else None,
999
+ "history": recent_history,
1000
+ }
1001
+
1002
+ return json.dumps(result, ensure_ascii=False, indent=2)
1003
+
1004
+ except Exception as e:
1005
+ result = {
1006
+ "status": "error",
1007
+ "message": f"Failed to get operation history: {str(e)}",
1008
+ }
1009
+ return json.dumps(result, ensure_ascii=False, indent=2)
1010
+
1011
+
1012
+ # ==================== Server Initialization ====================
1013
+
1014
+
1015
+ def main():
1016
+ """Start MCP server"""
1017
+ print("🚀 Code Implementation MCP Server")
1018
+ print(
1019
+ "📝 Paper Code Implementation Tool Server / Paper Code Implementation Tool Server"
1020
+ )
1021
+ print("")
1022
+ print("Available tools / Available tools:")
1023
+ # print(" • read_file - Read file contents / Read file contents")
1024
+ print(
1025
+ " • read_code_mem - Read code summary from implement_code_summary.md / Read code summary from implement_code_summary.md"
1026
+ )
1027
+ print(" • write_file - Write file contents / Write file contents")
1028
+ print(" • execute_python - Execute Python code / Execute Python code")
1029
+ print(" • execute_bash - Execute bash command / Execute bash commands")
1030
+ print(" • search_code - Search code patterns / Search code patterns")
1031
+ print(" • get_file_structure - Get file structure / Get file structure")
1032
+ print(" • set_workspace - Set workspace / Set workspace")
1033
+ print(" • get_operation_history - Get operation history / Get operation history")
1034
+ print("")
1035
+ print("🔧 Server starting...")
1036
+
1037
+ # Initialize default workspace
1038
+ initialize_workspace()
1039
+
1040
+ # Start server
1041
+ mcp.run()
1042
+
1043
+
1044
+ if __name__ == "__main__":
1045
+ main()