updates to tools and mma skills
This commit is contained in:
289
scripts/inject_tools.py
Normal file
289
scripts/inject_tools.py
Normal file
@@ -0,0 +1,289 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
with open('mcp_client.py', 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 1. Add import os if not there
|
||||
if 'import os' not in content:
|
||||
content = content.replace('import summarize', 'import os\nimport summarize')
|
||||
|
||||
# 2. Add the functions before "# ------------------------------------------------------------------ web tools"
|
||||
functions_code = 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 = content.replace('# ------------------------------------------------------------------ web tools', functions_code)
|
||||
|
||||
# 3. Update TOOL_NAMES
|
||||
old_tool_names_match = re.search(r'TOOL_NAMES\s*=\s*\{([^}]*)\}', content)
|
||||
if old_tool_names_match:
|
||||
old_names = old_tool_names_match.group(1)
|
||||
new_names = old_names + ', "py_find_usages", "py_get_imports", "py_check_syntax", "py_get_hierarchy", "py_get_docstring", "get_tree"'
|
||||
content = content.replace(old_tool_names_match.group(0), f'TOOL_NAMES = {{{new_names}}}')
|
||||
|
||||
# 4. Update dispatch
|
||||
dispatch_additions = 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 = re.sub(r' return f"ERROR: unknown MCP tool \'{tool_name}\'"', dispatch_additions.strip(), content)
|
||||
|
||||
# 5. Update MCP_TOOL_SPECS
|
||||
mcp_tool_specs_addition = 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 = 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.")
|
||||
Reference in New Issue
Block a user