From 4e8b397c8084c86d334d16d02fe3439f2f4393b7 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Tue, 5 May 2026 19:48:38 -0400 Subject: [PATCH] feat(mcp): Add full functional parity for C/C++ tools --- src/mcp_client.py | 228 +++++++++++++++++++++++++++++++ tests/test_mcp_ts_integration.py | 90 ++++++++++++ 2 files changed, 318 insertions(+) diff --git a/src/mcp_client.py b/src/mcp_client.py index ffe3353..1dfd73a 100644 --- a/src/mcp_client.py +++ b/src/mcp_client.py @@ -77,6 +77,8 @@ MUTATING_TOOLS: frozenset[str] = frozenset({ "py_set_signature", "py_set_var_declaration", "edit_file", + "ts_c_update_definition", + "ts_cpp_update_definition", }) # ------------------------------------------------------------------ state @@ -370,6 +372,98 @@ def ts_cpp_get_code_outline(path: str) -> str: except Exception as e: return f"ERROR generating outline for '{path}': {e}" +def ts_c_get_definition(path: str, name: str) -> str: + """Returns the source code for a specific definition in a C file.""" + 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: + from src.file_cache import ASTParser + code = p.read_text(encoding="utf-8") + parser = ASTParser("c") + return parser.get_definition(code, name, path=str(p)) + except Exception as e: + return f"ERROR retrieving definition '{name}' from '{path}': {e}" + +def ts_cpp_get_definition(path: str, name: str) -> str: + """Returns the source code for a specific definition in a C++ file.""" + 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: + from src.file_cache import ASTParser + code = p.read_text(encoding="utf-8") + parser = ASTParser("cpp") + return parser.get_definition(code, name, path=str(p)) + except Exception as e: + return f"ERROR retrieving definition '{name}' from '{path}': {e}" + +def ts_c_get_signature(path: str, name: str) -> str: + """Returns the signature part of a function in a C file.""" + 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: + from src.file_cache import ASTParser + code = p.read_text(encoding="utf-8") + parser = ASTParser("c") + return parser.get_signature(code, name, path=str(p)) + except Exception as e: + return f"ERROR retrieving signature '{name}' from '{path}': {e}" + +def ts_cpp_get_signature(path: str, name: str) -> str: + """Returns the signature part of a function or method in a C++ file.""" + 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: + from src.file_cache import ASTParser + code = p.read_text(encoding="utf-8") + parser = ASTParser("cpp") + return parser.get_signature(code, name, path=str(p)) + except Exception as e: + return f"ERROR retrieving signature '{name}' from '{path}': {e}" + +def ts_c_update_definition(path: str, name: str, new_content: str) -> str: + """Surgically replace the definition of a function in a C file.""" + 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: + from src.file_cache import ASTParser + code = p.read_text(encoding="utf-8") + parser = ASTParser("c") + updated_code = parser.update_definition(code, name, new_content, path=str(p)) + if updated_code.startswith("ERROR:"): + return updated_code + p.write_text(updated_code, encoding="utf-8") + return f"Successfully updated definition '{name}' in {path}" + except Exception as e: + return f"ERROR updating definition '{name}' in '{path}': {e}" + +def ts_cpp_update_definition(path: str, name: str, new_content: str) -> str: + """Surgically replace the definition of a class or function in a C++ file.""" + 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: + from src.file_cache import ASTParser + code = p.read_text(encoding="utf-8") + parser = ASTParser("cpp") + updated_code = parser.update_definition(code, name, new_content, path=str(p)) + if updated_code.startswith("ERROR:"): + return updated_code + p.write_text(updated_code, encoding="utf-8") + return f"Successfully updated definition '{name}' in {path}" + except Exception as e: + return f"ERROR updating definition '{name}' in '{path}': {e}" + def get_file_slice(path: str, start_line: int, end_line: int) -> str: """Return a specific line range from a file.""" p, err = _resolve_and_check(path) @@ -1127,6 +1221,18 @@ def dispatch(tool_name: str, tool_input: dict[str, Any]) -> str: return ts_c_get_code_outline(path) if tool_name == "ts_cpp_get_code_outline": return ts_cpp_get_code_outline(path) + if tool_name == "ts_c_get_definition": + return ts_c_get_definition(path, str(tool_input.get("name", ""))) + if tool_name == "ts_cpp_get_definition": + return ts_cpp_get_definition(path, str(tool_input.get("name", ""))) + if tool_name == "ts_c_get_signature": + return ts_c_get_signature(path, str(tool_input.get("name", ""))) + if tool_name == "ts_cpp_get_signature": + return ts_cpp_get_signature(path, str(tool_input.get("name", ""))) + if tool_name == "ts_c_update_definition": + return ts_c_update_definition(path, str(tool_input.get("name", "")), str(tool_input.get("new_content", ""))) + if tool_name == "ts_cpp_update_definition": + return ts_cpp_update_definition(path, str(tool_input.get("name", "")), str(tool_input.get("new_content", ""))) if tool_name == "py_get_definition": return py_get_definition(path, str(tool_input.get("name", ""))) if tool_name == "py_update_definition": @@ -1394,6 +1500,128 @@ MCP_TOOL_SPECS: list[dict[str, Any]] = [ "required": ["path"], }, }, + { + "name": "ts_c_get_definition", + "description": ( + "Get the full source code of a specific function or struct definition in a C file. " + "This is more efficient than reading the whole file if you know what you're looking for." + ), + "parameters": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Path to the C file.", + }, + "name": { + "type": "string", + "description": "The name of the function or struct to retrieve.", + } + }, + "required": ["path", "name"], + }, + }, + { + "name": "ts_cpp_get_definition", + "description": ( + "Get the full source code of a specific class, function, or method definition in a C++ file. " + "This is more efficient than reading the whole file if you know what you're looking for." + ), + "parameters": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Path to the C++ file.", + }, + "name": { + "type": "string", + "description": "The name of the class or function to retrieve. Use 'ClassName::method_name' for methods.", + } + }, + "required": ["path", "name"], + }, + }, + { + "name": "ts_c_get_signature", + "description": "Get only the signature part of a C function.", + "parameters": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Path to the C file." + }, + "name": { + "type": "string", + "description": "Name of the function." + } + }, + "required": ["path", "name"] + } + }, + { + "name": "ts_cpp_get_signature", + "description": "Get only the signature part of a C++ function or method.", + "parameters": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Path to the C++ file." + }, + "name": { + "type": "string", + "description": "Name of the function/method (e.g. 'ClassName::method_name')." + } + }, + "required": ["path", "name"] + } + }, + { + "name": "ts_c_update_definition", + "description": "Surgically replace the definition of a function in a C file using AST to find line ranges.", + "parameters": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Path to the C file." + }, + "name": { + "type": "string", + "description": "Name of function." + }, + "new_content": { + "type": "string", + "description": "Complete new source for the definition." + } + }, + "required": ["path", "name", "new_content"] + } + }, + { + "name": "ts_cpp_update_definition", + "description": "Surgically replace the definition of a class or function in a C++ file using AST to find line ranges.", + "parameters": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Path to the C++ file." + }, + "name": { + "type": "string", + "description": "Name of class/function/method." + }, + "new_content": { + "type": "string", + "description": "Complete new source for the definition." + } + }, + "required": ["path", "name", "new_content"] + } + }, { "name": "get_file_slice", "description": "Read a specific line range from a file. Useful for reading parts of very large files.", diff --git a/tests/test_mcp_ts_integration.py b/tests/test_mcp_ts_integration.py index da712e2..477a09d 100644 --- a/tests/test_mcp_ts_integration.py +++ b/tests/test_mcp_ts_integration.py @@ -86,3 +86,93 @@ def test_ts_cpp_get_code_outline_dispatch(tmp_path, mock_resolve): mock_instance.get_code_outline.assert_called_once() assert result and len(result) > 0 assert "[Func] main (Lines 1-1)" in result + +def test_ts_c_get_definition_dispatch(tmp_path, mock_resolve): + # Verify ts_c_get_definition via dispatch + c_file = tmp_path / "test.c" + c_file.write_text("void foo() {}") + + with patch("src.file_cache.ASTParser") as mock_parser_cls: + mock_instance = mock_parser_cls.return_value + mock_instance.get_definition.return_value = "void foo() {}" + + result = dispatch("ts_c_get_definition", {"path": str(c_file), "name": "foo"}) + + mock_parser_cls.assert_called_once_with("c") + mock_instance.get_definition.assert_called_once() + assert "void foo() {}" in result + +def test_ts_cpp_get_definition_dispatch(tmp_path, mock_resolve): + # Verify ts_cpp_get_definition via dispatch + cpp_file = tmp_path / "test.cpp" + cpp_file.write_text("void foo() {}") + + with patch("src.file_cache.ASTParser") as mock_parser_cls: + mock_instance = mock_parser_cls.return_value + mock_instance.get_definition.return_value = "void foo() {}" + + result = dispatch("ts_cpp_get_definition", {"path": str(cpp_file), "name": "foo"}) + + mock_parser_cls.assert_called_once_with("cpp") + mock_instance.get_definition.assert_called_once() + assert "void foo() {}" in result + +def test_ts_c_get_signature_dispatch(tmp_path, mock_resolve): + # Verify ts_c_get_signature via dispatch + c_file = tmp_path / "test.c" + c_file.write_text("void foo() {}") + + with patch("src.file_cache.ASTParser") as mock_parser_cls: + mock_instance = mock_parser_cls.return_value + mock_instance.get_signature.return_value = "void foo()" + + result = dispatch("ts_c_get_signature", {"path": str(c_file), "name": "foo"}) + + mock_parser_cls.assert_called_once_with("c") + mock_instance.get_signature.assert_called_once() + assert "void foo()" in result + +def test_ts_cpp_get_signature_dispatch(tmp_path, mock_resolve): + # Verify ts_cpp_get_signature via dispatch + cpp_file = tmp_path / "test.cpp" + cpp_file.write_text("void foo() {}") + + with patch("src.file_cache.ASTParser") as mock_parser_cls: + mock_instance = mock_parser_cls.return_value + mock_instance.get_signature.return_value = "void foo()" + + result = dispatch("ts_cpp_get_signature", {"path": str(cpp_file), "name": "foo"}) + + mock_parser_cls.assert_called_once_with("cpp") + mock_instance.get_signature.assert_called_once() + assert "void foo()" in result + +def test_ts_c_update_definition_dispatch(tmp_path, mock_resolve): + # Verify ts_c_update_definition via dispatch + c_file = tmp_path / "test.c" + c_file.write_text("void foo() {}") + + with patch("src.file_cache.ASTParser") as mock_parser_cls: + mock_instance = mock_parser_cls.return_value + mock_instance.update_definition.return_value = "void foo() { return; }" + + result = dispatch("ts_c_update_definition", {"path": str(c_file), "name": "foo", "new_content": "void foo() { return; }"}) + + mock_parser_cls.assert_called_once_with("c") + mock_instance.update_definition.assert_called_once() + assert "Successfully updated" in result + +def test_ts_cpp_update_definition_dispatch(tmp_path, mock_resolve): + # Verify ts_cpp_update_definition via dispatch + cpp_file = tmp_path / "test.cpp" + cpp_file.write_text("void foo() {}") + + with patch("src.file_cache.ASTParser") as mock_parser_cls: + mock_instance = mock_parser_cls.return_value + mock_instance.update_definition.return_value = "void foo() { return; }" + + result = dispatch("ts_cpp_update_definition", {"path": str(cpp_file), "name": "foo", "new_content": "void foo() { return; }"}) + + mock_parser_cls.assert_called_once_with("cpp") + mock_instance.update_definition.assert_called_once() + assert "Successfully updated" in result