feat(types): Complete strict static analysis and typing track
This commit is contained in:
148
mcp_client.py
148
mcp_client.py
@@ -31,8 +31,10 @@ so the AI doesn't wander outside the project workspace.
|
||||
|
||||
from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import Optional, Callable, Any
|
||||
from typing import Optional, Callable, Any, cast
|
||||
import os
|
||||
import ast
|
||||
import subprocess
|
||||
import summarize
|
||||
import outline_tool
|
||||
import urllib.request
|
||||
@@ -151,7 +153,7 @@ def _resolve_and_check(raw_path: str) -> tuple[Path | None, str]:
|
||||
def read_file(path: str) -> str:
|
||||
"""Return the UTF-8 content of a file, or an error string."""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
if err or p is None:
|
||||
return err
|
||||
if not p.exists():
|
||||
return f"ERROR: file not found: {path}"
|
||||
@@ -165,7 +167,7 @@ def read_file(path: str) -> str:
|
||||
def list_directory(path: str) -> str:
|
||||
"""List entries in a directory. Returns a compact text table."""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
if err or p is None:
|
||||
return err
|
||||
if not p.exists():
|
||||
return f"ERROR: path not found: {path}"
|
||||
@@ -195,7 +197,7 @@ def search_files(path: str, pattern: str) -> str:
|
||||
pattern examples: '*.py', '**/*.toml', 'src/**/*.rs'
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
if err or p is None:
|
||||
return err
|
||||
if not p.is_dir():
|
||||
return f"ERROR: not a directory: {path}"
|
||||
@@ -226,7 +228,7 @@ def get_file_summary(path: str) -> str:
|
||||
For .toml: table keys. For .md: headings. Others: line count + preview.
|
||||
"""
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
if err or p is None:
|
||||
return err
|
||||
if not p.exists():
|
||||
return f"ERROR: file not found: {path}"
|
||||
@@ -245,6 +247,7 @@ def py_get_skeleton(path: str) -> str:
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
if not p.exists():
|
||||
return f"ERROR: file not found: {path}"
|
||||
if not p.is_file() or p.suffix != ".py":
|
||||
@@ -264,6 +267,7 @@ def py_get_code_outline(path: str) -> str:
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
if not p.exists():
|
||||
return f"ERROR: file not found: {path}"
|
||||
if not p.is_file():
|
||||
@@ -279,12 +283,13 @@ def get_file_slice(path: str, start_line: int, end_line: int) -> str:
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
if not p.exists():
|
||||
return f"ERROR: file not found: {path}"
|
||||
try:
|
||||
lines = p.read_text(encoding="utf-8").splitlines(keepends=True)
|
||||
start_idx = int(start_line) - 1
|
||||
end_idx = int(end_line)
|
||||
start_idx = start_line - 1
|
||||
end_idx = end_line
|
||||
return "".join(lines[start_idx:end_idx])
|
||||
except Exception as e:
|
||||
return f"ERROR reading slice from '{path}': {e}"
|
||||
@@ -294,12 +299,13 @@ def set_file_slice(path: str, start_line: int, end_line: int, new_content: str)
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
if not p.exists():
|
||||
return f"ERROR: file not found: {path}"
|
||||
try:
|
||||
lines = p.read_text(encoding="utf-8").splitlines(keepends=True)
|
||||
start_idx = int(start_line) - 1
|
||||
end_idx = int(end_line)
|
||||
start_idx = start_line - 1
|
||||
end_idx = end_line
|
||||
if new_content and not new_content.endswith("\n"):
|
||||
new_content += "\n"
|
||||
new_lines = new_content.splitlines(keepends=True) if new_content else []
|
||||
@@ -309,12 +315,11 @@ def set_file_slice(path: str, start_line: int, end_line: int, new_content: str)
|
||||
except Exception as e:
|
||||
return f"ERROR updating slice in '{path}': {e}"
|
||||
|
||||
def _get_symbol_node(tree: Any, name: str) -> Any:
|
||||
def _get_symbol_node(tree: ast.AST, name: str) -> Optional[ast.AST]:
|
||||
"""Helper to find an AST node by name (Class, Function, or Variable). Supports dot notation."""
|
||||
import ast
|
||||
parts = name.split(".")
|
||||
|
||||
def find_in_scope(scope_node, target_name):
|
||||
def find_in_scope(scope_node: Any, target_name: str) -> Optional[ast.AST]:
|
||||
# scope_node could be Module, ClassDef, or FunctionDef
|
||||
body = getattr(scope_node, "body", [])
|
||||
for node in body:
|
||||
@@ -345,6 +350,7 @@ def py_get_definition(path: str, name: str) -> str:
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
if not p.exists():
|
||||
return f"ERROR: file not found: {path}"
|
||||
if not p.is_file():
|
||||
@@ -352,14 +358,13 @@ def py_get_definition(path: str, name: str) -> str:
|
||||
if p.suffix != ".py":
|
||||
return f"ERROR: py_get_definition currently only supports .py files (unsupported: {p.suffix})"
|
||||
try:
|
||||
import ast
|
||||
code = p.read_text(encoding="utf-8").lstrip(chr(0xFEFF))
|
||||
lines = code.splitlines(keepends=True)
|
||||
tree = ast.parse(code)
|
||||
node = _get_symbol_node(tree, name)
|
||||
if node:
|
||||
start = getattr(node, "lineno") - 1
|
||||
end = getattr(node, "end_lineno")
|
||||
start = cast(int, getattr(node, "lineno")) - 1
|
||||
end = cast(int, getattr(node, "end_lineno"))
|
||||
return "".join(lines[start:end])
|
||||
return f"ERROR: could not find definition '{name}' in {path}"
|
||||
except Exception as e:
|
||||
@@ -370,17 +375,17 @@ def py_update_definition(path: str, name: str, new_content: str) -> str:
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
if not p.exists():
|
||||
return f"ERROR: file not found: {path}"
|
||||
try:
|
||||
import ast
|
||||
code = p.read_text(encoding="utf-8").lstrip(chr(0xFEFF))
|
||||
tree = ast.parse(code)
|
||||
node = _get_symbol_node(tree, name)
|
||||
if not node:
|
||||
return f"ERROR: could not find definition '{name}' in {path}"
|
||||
start = getattr(node, "lineno")
|
||||
end = getattr(node, "end_lineno")
|
||||
start = cast(int, getattr(node, "lineno"))
|
||||
end = cast(int, getattr(node, "end_lineno"))
|
||||
return set_file_slice(path, start, end, new_content)
|
||||
except Exception as e:
|
||||
return f"ERROR updating definition '{name}' in '{path}': {e}"
|
||||
@@ -390,17 +395,17 @@ def py_get_signature(path: str, name: str) -> str:
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
if not p.exists():
|
||||
return f"ERROR: file not found: {path}"
|
||||
try:
|
||||
import ast
|
||||
code = p.read_text(encoding="utf-8").lstrip(chr(0xFEFF))
|
||||
lines = code.splitlines(keepends=True)
|
||||
tree = ast.parse(code)
|
||||
node = _get_symbol_node(tree, name)
|
||||
if not node or not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
return f"ERROR: could not find function/method '{name}' in {path}"
|
||||
start = getattr(node, "lineno") - 1
|
||||
start = node.lineno - 1
|
||||
body_start = node.body[0].lineno - 1
|
||||
sig_lines = lines[start:body_start]
|
||||
sig = "".join(sig_lines).strip()
|
||||
@@ -420,17 +425,17 @@ def py_set_signature(path: str, name: str, new_signature: str) -> str:
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
if not p.exists():
|
||||
return f"ERROR: file not found: {path}"
|
||||
try:
|
||||
import ast
|
||||
code = p.read_text(encoding="utf-8").lstrip(chr(0xFEFF))
|
||||
code.splitlines(keepends=True)
|
||||
tree = ast.parse(code)
|
||||
node = _get_symbol_node(tree, name)
|
||||
if not node or not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
return f"ERROR: could not find function/method '{name}' in {path}"
|
||||
start = getattr(node, "lineno")
|
||||
start = node.lineno
|
||||
body_start_line = node.body[0].lineno
|
||||
# We replace from start until body_start_line - 1
|
||||
# But we must be careful about comments/docstrings between sig and body
|
||||
@@ -445,10 +450,10 @@ def py_get_class_summary(path: str, name: str) -> str:
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
if not p.exists():
|
||||
return f"ERROR: file not found: {path}"
|
||||
try:
|
||||
import ast
|
||||
code = p.read_text(encoding="utf-8").lstrip(chr(0xFEFF))
|
||||
tree = ast.parse(code)
|
||||
node = _get_symbol_node(tree, name)
|
||||
@@ -461,7 +466,7 @@ def py_get_class_summary(path: str, name: str) -> str:
|
||||
summary.append(f" Docstring: {doc}")
|
||||
for body_node in node.body:
|
||||
if isinstance(body_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
start = getattr(body_node, "lineno") - 1
|
||||
start = body_node.lineno - 1
|
||||
body_start = body_node.body[0].lineno - 1
|
||||
sig = "".join(lines[start:body_start]).strip()
|
||||
summary.append(f" - {sig}")
|
||||
@@ -474,18 +479,18 @@ def py_get_var_declaration(path: str, name: str) -> str:
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
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").lstrip(chr(0xFEFF))
|
||||
lines = code.splitlines(keepends=True)
|
||||
tree = ast.parse(code)
|
||||
node = _get_symbol_node(tree, name)
|
||||
if not node or not isinstance(node, (ast.Assign, ast.AnnAssign)):
|
||||
return f"ERROR: could not find variable '{name}' in {path}"
|
||||
start = getattr(node, "lineno") - 1
|
||||
end = getattr(node, "end_lineno")
|
||||
start = cast(int, getattr(node, "lineno")) - 1
|
||||
end = cast(int, getattr(node, "end_lineno"))
|
||||
return "".join(lines[start:end])
|
||||
except Exception as e:
|
||||
return f"ERROR retrieving variable '{name}' from '{path}': {e}"
|
||||
@@ -495,17 +500,17 @@ def py_set_var_declaration(path: str, name: str, new_declaration: str) -> str:
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
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").lstrip(chr(0xFEFF))
|
||||
tree = ast.parse(code)
|
||||
node = _get_symbol_node(tree, name)
|
||||
if not node or not isinstance(node, (ast.Assign, ast.AnnAssign)):
|
||||
return f"ERROR: could not find variable '{name}' in {path}"
|
||||
start = getattr(node, "lineno")
|
||||
end = getattr(node, "end_lineno")
|
||||
start = cast(int, getattr(node, "lineno"))
|
||||
end = cast(int, getattr(node, "end_lineno"))
|
||||
return set_file_slice(path, start, end, new_declaration)
|
||||
except Exception as e:
|
||||
return f"ERROR updating variable '{name}' in '{path}': {e}"
|
||||
@@ -516,10 +521,10 @@ def get_git_diff(path: str, base_rev: str = "HEAD", head_rev: str = "") -> str:
|
||||
base_rev: The base revision (default: HEAD)
|
||||
head_rev: The head revision (optional)
|
||||
"""
|
||||
import subprocess
|
||||
p, err = _resolve_and_check(path)
|
||||
if err:
|
||||
return err
|
||||
assert p is not None
|
||||
cmd = ["git", "diff", base_rev]
|
||||
if head_rev:
|
||||
cmd.append(head_rev)
|
||||
@@ -536,12 +541,13 @@ 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
|
||||
assert p is not None
|
||||
try:
|
||||
import re
|
||||
pattern = re.compile(r"\b" + re.escape(name) + r"\b")
|
||||
results = []
|
||||
|
||||
def _search_file(fp):
|
||||
def _search_file(fp: Path) -> None:
|
||||
if fp.name == "history.toml" or fp.name.endswith("_history.toml"): return
|
||||
if not _is_allowed(fp): return
|
||||
try:
|
||||
@@ -573,9 +579,9 @@ 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
|
||||
assert p is not None
|
||||
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 = []
|
||||
@@ -596,9 +602,9 @@ 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
|
||||
assert p is not None
|
||||
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}"
|
||||
@@ -611,10 +617,10 @@ 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 = []
|
||||
assert p is not None
|
||||
subclasses: list[str] = []
|
||||
|
||||
def _search_file(fp):
|
||||
def _search_file(fp: Path) -> None:
|
||||
if not _is_allowed(fp): return
|
||||
try:
|
||||
code = fp.read_text(encoding="utf-8")
|
||||
@@ -625,7 +631,8 @@ def py_get_hierarchy(path: str, class_name: str) -> str:
|
||||
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})")
|
||||
if isinstance(base.value, ast.Name):
|
||||
subclasses.append(f"{fp.name}: class {node.name}({base.value.id}.{class_name})")
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
@@ -647,9 +654,9 @@ 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
|
||||
assert p is not None
|
||||
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":
|
||||
@@ -657,8 +664,10 @@ def py_get_docstring(path: str, name: str) -> str:
|
||||
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}'."
|
||||
if isinstance(node, (ast.AsyncFunctionDef, ast.FunctionDef, ast.ClassDef, ast.Module)):
|
||||
doc = ast.get_docstring(node)
|
||||
return doc if doc else f"No docstring found for '{name}'."
|
||||
return f"No docstring found for '{name}'."
|
||||
except Exception as e:
|
||||
return f"ERROR getting docstring for '{name}': {e}"
|
||||
|
||||
@@ -666,12 +675,13 @@ 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
|
||||
assert p is not None
|
||||
if not p.is_dir(): return f"ERROR: not a directory: {path}"
|
||||
try:
|
||||
max_depth = int(max_depth)
|
||||
m_depth = max_depth
|
||||
|
||||
def _build_tree(dir_path, current_depth, prefix=""):
|
||||
if current_depth > max_depth: return []
|
||||
def _build_tree(dir_path: Path, current_depth: int, prefix: str = "") -> list[str]:
|
||||
if current_depth > m_depth: return []
|
||||
lines = []
|
||||
try:
|
||||
entries = sorted(dir_path.iterdir(), key=lambda e: (e.is_file(), e.name.lower()))
|
||||
@@ -706,11 +716,11 @@ class _DDGParser(HTMLParser):
|
||||
|
||||
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
||||
attrs_dict = dict(attrs)
|
||||
if tag == "a" and "result__url" in attrs_dict.get("class", ""):
|
||||
self.current_link = attrs_dict.get("href", "")
|
||||
if tag == "a" and "result__snippet" in attrs_dict.get("class", ""):
|
||||
if tag == "a" and "result__url" in cast(str, attrs_dict.get("class", "")):
|
||||
self.current_link = cast(str, attrs_dict.get("href", ""))
|
||||
if tag == "a" and "result__snippet" in cast(str, attrs_dict.get("class", "")):
|
||||
self.in_snippet = True
|
||||
if tag == "h2" and "result__title" in attrs_dict.get("class", ""):
|
||||
if tag == "h2" and "result__title" in cast(str, attrs_dict.get("class", "")):
|
||||
self.in_title = True
|
||||
|
||||
def handle_endtag(self, tag: str) -> None:
|
||||
@@ -777,7 +787,9 @@ def fetch_url(url: str) -> str:
|
||||
"""Fetch a URL and return its text content (stripped of HTML tags)."""
|
||||
# Correct duckduckgo redirect links if passed
|
||||
if url.startswith("//duckduckgo.com/l/?uddg="):
|
||||
url = urllib.parse.unquote(url.split("uddg=")[1].split("&")[0])
|
||||
split_uddg = url.split("uddg=")
|
||||
if len(split_uddg) > 1:
|
||||
url = urllib.parse.unquote(split_uddg[1].split("&")[0])
|
||||
if not url.startswith("http"):
|
||||
url = "https://" + url
|
||||
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'})
|
||||
@@ -819,13 +831,13 @@ def dispatch(tool_name: str, tool_input: dict[str, Any]) -> str:
|
||||
Dispatch an MCP tool call by name. Returns the result as a string.
|
||||
"""
|
||||
# Handle aliases
|
||||
path = tool_input.get("path", tool_input.get("file_path", tool_input.get("dir_path", "")))
|
||||
path = str(tool_input.get("path", tool_input.get("file_path", tool_input.get("dir_path", ""))))
|
||||
if tool_name == "read_file":
|
||||
return read_file(path)
|
||||
if tool_name == "list_directory":
|
||||
return list_directory(path)
|
||||
if tool_name == "search_files":
|
||||
return search_files(path, tool_input.get("pattern", "*"))
|
||||
return search_files(path, str(tool_input.get("pattern", "*")))
|
||||
if tool_name == "get_file_summary":
|
||||
return get_file_summary(path)
|
||||
if tool_name == "py_get_skeleton":
|
||||
@@ -833,47 +845,47 @@ def dispatch(tool_name: str, tool_input: dict[str, Any]) -> str:
|
||||
if tool_name == "py_get_code_outline":
|
||||
return py_get_code_outline(path)
|
||||
if tool_name == "py_get_definition":
|
||||
return py_get_definition(path, tool_input.get("name", ""))
|
||||
return py_get_definition(path, str(tool_input.get("name", "")))
|
||||
if tool_name == "py_update_definition":
|
||||
return py_update_definition(path, tool_input.get("name", ""), tool_input.get("new_content", ""))
|
||||
return py_update_definition(path, str(tool_input.get("name", "")), str(tool_input.get("new_content", "")))
|
||||
if tool_name == "py_get_signature":
|
||||
return py_get_signature(path, tool_input.get("name", ""))
|
||||
return py_get_signature(path, str(tool_input.get("name", "")))
|
||||
if tool_name == "py_set_signature":
|
||||
return py_set_signature(path, tool_input.get("name", ""), tool_input.get("new_signature", ""))
|
||||
return py_set_signature(path, str(tool_input.get("name", "")), str(tool_input.get("new_signature", "")))
|
||||
if tool_name == "py_get_class_summary":
|
||||
return py_get_class_summary(path, tool_input.get("name", ""))
|
||||
return py_get_class_summary(path, str(tool_input.get("name", "")))
|
||||
if tool_name == "py_get_var_declaration":
|
||||
return py_get_var_declaration(path, tool_input.get("name", ""))
|
||||
return py_get_var_declaration(path, str(tool_input.get("name", "")))
|
||||
if tool_name == "py_set_var_declaration":
|
||||
return py_set_var_declaration(path, tool_input.get("name", ""), tool_input.get("new_declaration", ""))
|
||||
return py_set_var_declaration(path, str(tool_input.get("name", "")), str(tool_input.get("new_declaration", "")))
|
||||
if tool_name == "get_file_slice":
|
||||
return get_file_slice(path, tool_input.get("start_line", 1), tool_input.get("end_line", 1))
|
||||
return get_file_slice(path, int(tool_input.get("start_line", 1)), int(tool_input.get("end_line", 1)))
|
||||
if tool_name == "set_file_slice":
|
||||
return set_file_slice(path, tool_input.get("start_line", 1), tool_input.get("end_line", 1), tool_input.get("new_content", ""))
|
||||
return set_file_slice(path, int(tool_input.get("start_line", 1)), int(tool_input.get("end_line", 1)), str(tool_input.get("new_content", "")))
|
||||
if tool_name == "get_git_diff":
|
||||
return get_git_diff(
|
||||
path,
|
||||
tool_input.get("base_rev", "HEAD"),
|
||||
tool_input.get("head_rev", "")
|
||||
str(tool_input.get("base_rev", "HEAD")),
|
||||
str(tool_input.get("head_rev", ""))
|
||||
)
|
||||
if tool_name == "web_search":
|
||||
return web_search(tool_input.get("query", ""))
|
||||
return web_search(str(tool_input.get("query", "")))
|
||||
if tool_name == "fetch_url":
|
||||
return fetch_url(tool_input.get("url", ""))
|
||||
return fetch_url(str(tool_input.get("url", "")))
|
||||
if tool_name == "get_ui_performance":
|
||||
return get_ui_performance()
|
||||
if tool_name == "py_find_usages":
|
||||
return py_find_usages(path, tool_input.get("name", ""))
|
||||
return py_find_usages(path, str(tool_input.get("name", "")))
|
||||
if tool_name == "py_get_imports":
|
||||
return py_get_imports(path)
|
||||
if tool_name == "py_check_syntax":
|
||||
return py_check_syntax(path)
|
||||
if tool_name == "py_get_hierarchy":
|
||||
return py_get_hierarchy(path, tool_input.get("class_name", ""))
|
||||
return py_get_hierarchy(path, str(tool_input.get("class_name", "")))
|
||||
if tool_name == "py_get_docstring":
|
||||
return py_get_docstring(path, tool_input.get("name", ""))
|
||||
return py_get_docstring(path, str(tool_input.get("name", "")))
|
||||
if tool_name == "get_tree":
|
||||
return get_tree(path, tool_input.get("max_depth", 2))
|
||||
return get_tree(path, int(tool_input.get("max_depth", 2)))
|
||||
return f"ERROR: unknown MCP tool '{tool_name}'"
|
||||
# ------------------------------------------------------------------ tool schema helpers
|
||||
# These are imported by ai_client.py to build provider-specific declarations.
|
||||
|
||||
Reference in New Issue
Block a user