import os import re with open('mcp_client.py', 'r', encoding='utf-8') as f: content: str = f.read() # 1. Add import os if not there if 'import os' not in content: content: str = content.replace('import summarize', 'import os\nimport summarize') # 2. Add the functions before "# ------------------------------------------------------------------ web tools" functions_code: str = r''' def py_find_usages(path: str, name: str) -> str: """Finds exact string matches of a symbol in a given file or directory.""" p, err = _resolve_and_check(path) if err: return err try: import re pattern = re.compile(r"\b" + re.escape(name) + r"\b") results = [] def _search_file(fp): if fp.name == "history.toml" or fp.name.endswith("_history.toml"): return if not _is_allowed(fp): return try: text = fp.read_text(encoding="utf-8") lines = text.splitlines() for i, line in enumerate(lines, 1): if pattern.search(line): rel = fp.relative_to(_primary_base_dir if _primary_base_dir else Path.cwd()) results.append(f"{rel}:{i}: {line.strip()[:100]}") except Exception: pass if p.is_file(): _search_file(p) else: for root, dirs, files in os.walk(p): dirs[:] = [d for d in dirs if not d.startswith('.') and d not in ('__pycache__', 'venv', 'env')] for file in files: if file.endswith(('.py', '.md', '.toml', '.txt', '.json')): _search_file(Path(root) / file) if not results: return f"No usages found for '{name}' in {p}" if len(results) > 100: return "\n".join(results[:100]) + f"\n... (and {len(results)-100} more)" return "\n".join(results) except Exception as e: return f"ERROR finding usages for '{name}': {e}" def py_get_imports(path: str) -> str: """Parses a file's AST and returns a strict list of its dependencies.""" p, err = _resolve_and_check(path) if err: return err if not p.is_file() or p.suffix != ".py": return f"ERROR: not a python file: {path}" try: import ast code = p.read_text(encoding="utf-8") tree = ast.parse(code) imports = [] for node in tree.body: if isinstance(node, ast.Import): for alias in node.names: imports.append(alias.name) elif isinstance(node, ast.ImportFrom): module = node.module or "" for alias in node.names: imports.append(f"{module}.{alias.name}" if module else alias.name) if not imports: return "No imports found." return "Imports:\n" + "\n".join(f" - {i}" for i in imports) except Exception as e: return f"ERROR getting imports for '{path}': {e}" def py_check_syntax(path: str) -> str: """Runs a quick syntax check on a Python file.""" p, err = _resolve_and_check(path) if err: return err if not p.is_file() or p.suffix != ".py": return f"ERROR: not a python file: {path}" try: import ast code = p.read_text(encoding="utf-8") ast.parse(code) return f"Syntax OK: {path}" except SyntaxError as e: return f"SyntaxError in {path} at line {e.lineno}, offset {e.offset}: {e.msg}\n{e.text}" except Exception as e: return f"ERROR checking syntax for '{path}': {e}" def py_get_hierarchy(path: str, class_name: str) -> str: """Scans the project to find subclasses of a given class.""" p, err = _resolve_and_check(path) if err: return err import ast subclasses = [] def _search_file(fp): if not _is_allowed(fp): return try: code = fp.read_text(encoding="utf-8") tree = ast.parse(code) for node in ast.walk(tree): if isinstance(node, ast.ClassDef): for base in node.bases: if isinstance(base, ast.Name) and base.id == class_name: subclasses.append(f"{fp.name}: class {node.name}({class_name})") elif isinstance(base, ast.Attribute) and base.attr == class_name: subclasses.append(f"{fp.name}: class {node.name}({base.value.id}.{class_name})") except Exception: pass try: if p.is_file(): _search_file(p) else: for root, dirs, files in os.walk(p): dirs[:] = [d for d in dirs if not d.startswith('.') and d not in ('__pycache__', 'venv', 'env')] for file in files: if file.endswith('.py'): _search_file(Path(root) / file) if not subclasses: return f"No subclasses of '{class_name}' found in {p}" return f"Subclasses of '{class_name}':\n" + "\n".join(f" - {s}" for s in subclasses) except Exception as e: return f"ERROR finding subclasses of '{class_name}': {e}" def py_get_docstring(path: str, name: str) -> str: """Extracts the docstring for a specific module, class, or function.""" p, err = _resolve_and_check(path) if err: return err if not p.is_file() or p.suffix != ".py": return f"ERROR: not a python file: {path}" try: import ast code = p.read_text(encoding="utf-8") tree = ast.parse(code) if not name or name == "module": doc = ast.get_docstring(tree) return doc if doc else "No module docstring found." node = _get_symbol_node(tree, name) if not node: return f"ERROR: could not find symbol '{name}' in {path}" doc = ast.get_docstring(node) return doc if doc else f"No docstring found for '{name}'." except Exception as e: return f"ERROR getting docstring for '{name}': {e}" def get_tree(path: str, max_depth: int = 2) -> str: """Returns a directory structure up to a max depth.""" p, err = _resolve_and_check(path) if err: return err if not p.is_dir(): return f"ERROR: not a directory: {path}" try: max_depth = int(max_depth) def _build_tree(dir_path, current_depth, prefix=""): if current_depth > max_depth: return [] lines = [] try: entries = sorted(dir_path.iterdir(), key=lambda e: (e.is_file(), e.name.lower())) except PermissionError: return [] # Filter entries = [e for e in entries if not e.name.startswith('.') and e.name not in ('__pycache__', 'venv', 'env') and e.name != "history.toml" and not e.name.endswith("_history.toml")] for i, entry in enumerate(entries): is_last = (i == len(entries) - 1) connector = "└── " if is_last else "├── " lines.append(f"{prefix}{connector}{entry.name}") if entry.is_dir(): extension = " " if is_last else "│ " lines.extend(_build_tree(entry, current_depth + 1, prefix + extension)) return lines tree_lines = [f"{p.name}/"] + _build_tree(p, 1) return "\n".join(tree_lines) except Exception as e: return f"ERROR generating tree for '{path}': {e}" # ------------------------------------------------------------------ web tools''' content: str = content.replace('# ------------------------------------------------------------------ web tools', functions_code) # 3. Update TOOL_NAMES old_tool_names_match: re.Match | None = re.search(r'TOOL_NAMES\s*=\s*\{([^}]*)\}', content) if old_tool_names_match: old_names: str = old_tool_names_match.group(1) new_names: str = old_names + ', "py_find_usages", "py_get_imports", "py_check_syntax", "py_get_hierarchy", "py_get_docstring", "get_tree"' content: str = content.replace(old_tool_names_match.group(0), f'TOOL_NAMES = {{{new_names}}}') # 4. Update dispatch dispatch_additions: str = r''' if tool_name == "py_find_usages": return py_find_usages(tool_input.get("path", ""), tool_input.get("name", "")) if tool_name == "py_get_imports": return py_get_imports(tool_input.get("path", "")) if tool_name == "py_check_syntax": return py_check_syntax(tool_input.get("path", "")) if tool_name == "py_get_hierarchy": return py_get_hierarchy(tool_input.get("path", ""), tool_input.get("class_name", "")) if tool_name == "py_get_docstring": return py_get_docstring(tool_input.get("path", ""), tool_input.get("name", "")) if tool_name == "get_tree": return get_tree(tool_input.get("path", ""), tool_input.get("max_depth", 2)) return f"ERROR: unknown MCP tool '{tool_name}'" ''' content: str = re.sub( r' return f"ERROR: unknown MCP tool \'{tool_name}\'"', dispatch_additions.strip(), content) # 5. Update MCP_TOOL_SPECS mcp_tool_specs_addition: str = r''' { "name": "py_find_usages", "description": "Finds exact string matches of a symbol in a given file or directory.", "parameters": { "type": "object", "properties": { "path": { "type": "string", "description": "Path to file or directory to search." }, "name": { "type": "string", "description": "The symbol/string to search for." } }, "required": ["path", "name"] } }, { "name": "py_get_imports", "description": "Parses a file's AST and returns a strict list of its dependencies.", "parameters": { "type": "object", "properties": { "path": { "type": "string", "description": "Path to the .py file." } }, "required": ["path"] } }, { "name": "py_check_syntax", "description": "Runs a quick syntax check on a Python file.", "parameters": { "type": "object", "properties": { "path": { "type": "string", "description": "Path to the .py file." } }, "required": ["path"] } }, { "name": "py_get_hierarchy", "description": "Scans the project to find subclasses of a given class.", "parameters": { "type": "object", "properties": { "path": { "type": "string", "description": "Directory path to search in." }, "class_name": { "type": "string", "description": "Name of the base class." } }, "required": ["path", "class_name"] } }, { "name": "py_get_docstring", "description": "Extracts the docstring for a specific module, class, or function.", "parameters": { "type": "object", "properties": { "path": { "type": "string", "description": "Path to the .py file." }, "name": { "type": "string", "description": "Name of symbol or 'module' for the file docstring." } }, "required": ["path", "name"] } }, { "name": "get_tree", "description": "Returns a directory structure up to a max depth.", "parameters": { "type": "object", "properties": { "path": { "type": "string", "description": "Directory path." }, "max_depth": { "type": "integer", "description": "Maximum depth to recurse (default 2)." } }, "required": ["path"] } } ] ''' content: str = re.sub( r'\]\s*$', mcp_tool_specs_addition.strip(), content) with open('mcp_client.py', 'w', encoding='utf-8') as f: f.write(content) print("Injected new tools.")