Add testing and update README for JSON & testing
This commit is contained in:
parent
cbe5dcc1f3
commit
1e737ed44b
51
README.md
51
README.md
@ -254,6 +254,57 @@ View result from get_function from ghydra (local){
|
||||
Based on this analysis, I can see these binaries communicate using a simple protocol where...
|
||||
```
|
||||
|
||||
# JSON Communication
|
||||
|
||||
GhydraMCP uses structured JSON for all communication between the Python bridge and Java plugin. This ensures consistent and reliable data exchange.
|
||||
|
||||
## Response Format
|
||||
|
||||
All responses follow a standard format:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"result": "...",
|
||||
"timestamp": 1712159482123,
|
||||
"port": 8192,
|
||||
"instanceType": "base"
|
||||
}
|
||||
```
|
||||
|
||||
Error responses include additional information:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "Error message",
|
||||
"status_code": 404,
|
||||
"timestamp": 1712159482123
|
||||
}
|
||||
```
|
||||
|
||||
This structured approach makes the communication more reliable and easier to debug.
|
||||
|
||||
# Testing
|
||||
|
||||
GhydraMCP includes comprehensive test suites for both the HTTP API and MCP bridge. See [TESTING.md](TESTING.md) for details on running the tests.
|
||||
|
||||
## HTTP API Tests
|
||||
|
||||
Tests the HTTP endpoints exposed by the Java plugin:
|
||||
- Response format and structure
|
||||
- JSON structure consistency
|
||||
- Required fields in responses
|
||||
- Error handling
|
||||
|
||||
## MCP Bridge Tests
|
||||
|
||||
Tests the MCP bridge functionality:
|
||||
- MCP protocol communication
|
||||
- Tool availability and structure
|
||||
- Response format and structure
|
||||
- JSON structure consistency
|
||||
|
||||
# Building from Source
|
||||
|
||||
You can build different artifacts with Maven:
|
||||
|
||||
154
TESTING.md
Normal file
154
TESTING.md
Normal file
@ -0,0 +1,154 @@
|
||||
# Testing GhydraMCP
|
||||
|
||||
This document describes how to test the GhydraMCP plugin and bridge.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.11 or higher
|
||||
- Ghidra with the GhydraMCP plugin installed and running
|
||||
- The `requests` Python package (`pip install requests`)
|
||||
|
||||
## Running All Tests
|
||||
|
||||
The easiest way to run all tests is to use the test runner script:
|
||||
|
||||
```bash
|
||||
python run_tests.py
|
||||
```
|
||||
|
||||
This will run both the HTTP API tests and the MCP bridge tests and provide a summary of the results.
|
||||
|
||||
You can also run specific test suites:
|
||||
|
||||
```bash
|
||||
# Run only the HTTP API tests
|
||||
python run_tests.py --http
|
||||
|
||||
# Run only the MCP bridge tests
|
||||
python run_tests.py --mcp
|
||||
```
|
||||
|
||||
## HTTP API Tests
|
||||
|
||||
The `test_http_api.py` script tests the HTTP API exposed by the Java plugin. It verifies that the endpoints return the expected JSON structure and that the response format is consistent.
|
||||
|
||||
### Running the HTTP API Tests
|
||||
|
||||
1. Make sure Ghidra is running with the GhydraMCP plugin loaded
|
||||
2. Run the tests:
|
||||
|
||||
```bash
|
||||
python test_http_api.py
|
||||
```
|
||||
|
||||
The tests will automatically skip if Ghidra is not running or if the plugin is not responding.
|
||||
|
||||
### What's Being Tested
|
||||
|
||||
- Basic connectivity to the plugin
|
||||
- Response format and structure
|
||||
- JSON structure consistency
|
||||
- Required fields in responses
|
||||
- Error handling
|
||||
|
||||
## MCP Bridge Tests
|
||||
|
||||
The `test_mcp_client.py` script tests the MCP bridge functionality using the MCP client library. It verifies that the bridge responds correctly to MCP requests and that the response format is consistent.
|
||||
|
||||
### Running the MCP Bridge Tests
|
||||
|
||||
1. Make sure Ghidra is running with the GhydraMCP plugin loaded
|
||||
2. Run the tests:
|
||||
|
||||
```bash
|
||||
python test_mcp_client.py
|
||||
```
|
||||
|
||||
The test script will:
|
||||
1. Connect to the bridge using the MCP client
|
||||
2. Initialize the session
|
||||
3. List the available tools
|
||||
4. Call the list_instances tool
|
||||
5. Call the discover_instances tool
|
||||
6. Call the list_functions tool
|
||||
|
||||
### What's Being Tested
|
||||
|
||||
- MCP protocol communication
|
||||
- Tool availability and structure
|
||||
- Response format and structure
|
||||
- JSON structure consistency
|
||||
- Required fields in responses
|
||||
- Proper initialization of the MCP session
|
||||
- Ability to call tools and receive responses
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### HTTP API Tests
|
||||
|
||||
- If tests are skipped with "Ghidra server not running or not accessible", make sure Ghidra is running and the GhydraMCP plugin is loaded.
|
||||
- If tests fail with connection errors, check that the plugin is listening on the expected port (default: 8192).
|
||||
|
||||
### MCP Bridge Tests
|
||||
|
||||
- If tests are skipped with "Failed to start MCP bridge process", check that the bridge script is executable and that all dependencies are installed.
|
||||
- If tests fail with JSON parsing errors, check that the bridge is responding with valid JSON.
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
### HTTP API Tests
|
||||
|
||||
To add a new test for an HTTP endpoint:
|
||||
|
||||
1. Add a new test method to the `GhydraMCPHttpApiTests` class
|
||||
2. Use the `requests` library to make HTTP requests to the endpoint
|
||||
3. Verify the response using assertions
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
def test_new_endpoint(self):
|
||||
"""Test the /new_endpoint endpoint"""
|
||||
response = requests.get(f"{BASE_URL}/new_endpoint")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify response is valid JSON
|
||||
data = response.json()
|
||||
|
||||
# Check required fields in the standard response format
|
||||
self.assertIn("success", data)
|
||||
self.assertTrue(data["success"])
|
||||
self.assertIn("timestamp", data)
|
||||
self.assertIn("port", data)
|
||||
```
|
||||
|
||||
### MCP Bridge Tests
|
||||
|
||||
To add a new test for an MCP tool:
|
||||
|
||||
1. Add a new test method to the `MCPBridgeTests` class
|
||||
2. Use the `send_mcp_request` method to send an MCP request to the bridge
|
||||
3. Verify the response using assertions
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
def test_new_tool(self):
|
||||
"""Test the new_tool tool"""
|
||||
response = self.send_mcp_request("call_tool", {
|
||||
"name": "new_tool",
|
||||
"arguments": {
|
||||
"param1": "value1",
|
||||
"param2": "value2"
|
||||
}
|
||||
})
|
||||
|
||||
# Check basic response structure
|
||||
self.assertIn("result", response)
|
||||
self.assertIn("content", response["result"])
|
||||
|
||||
# Parse the content
|
||||
content = response["result"]["content"]
|
||||
self.assertIsInstance(content, list)
|
||||
self.assertGreaterEqual(len(content), 1)
|
||||
```
|
||||
116
run_tests.py
Normal file
116
run_tests.py
Normal file
@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test runner for GhydraMCP tests.
|
||||
This script runs both the HTTP API tests and the MCP bridge tests.
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
import time
|
||||
|
||||
def print_header(text):
|
||||
"""Print a header with the given text"""
|
||||
print("\n" + "=" * 80)
|
||||
print(f" {text} ".center(80, "="))
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
def run_http_api_tests():
|
||||
"""Run the HTTP API tests"""
|
||||
print_header("Running HTTP API Tests")
|
||||
|
||||
# Import and run the tests
|
||||
try:
|
||||
from test_http_api import GhydraMCPHttpApiTests
|
||||
|
||||
# Create a test suite with all tests from GhydraMCPHttpApiTests
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(GhydraMCPHttpApiTests)
|
||||
|
||||
# Run the tests
|
||||
result = unittest.TextTestRunner(verbosity=2).run(suite)
|
||||
|
||||
return result.wasSuccessful()
|
||||
except ImportError:
|
||||
print("Error: Could not import test_http_api.py")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Error running HTTP API tests: {str(e)}")
|
||||
return False
|
||||
|
||||
def run_mcp_bridge_tests():
|
||||
"""Run the MCP bridge tests using the MCP client"""
|
||||
print_header("Running MCP Bridge Tests")
|
||||
|
||||
try:
|
||||
# Run the MCP client test script
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
print("Running MCP client test script...")
|
||||
result = subprocess.run(
|
||||
[sys.executable, "test_mcp_client.py"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
# Print the output
|
||||
if result.stdout:
|
||||
print("STDOUT:")
|
||||
print(result.stdout)
|
||||
|
||||
if result.stderr:
|
||||
print("STDERR:")
|
||||
print(result.stderr)
|
||||
|
||||
# Return True if the process exited with code 0
|
||||
return result.returncode == 0
|
||||
except Exception as e:
|
||||
print(f"Error running MCP bridge tests: {str(e)}")
|
||||
return False
|
||||
|
||||
def run_all_tests():
|
||||
"""Run all tests"""
|
||||
print_header("GhydraMCP Test Suite")
|
||||
|
||||
# Run the HTTP API tests
|
||||
http_api_success = run_http_api_tests()
|
||||
|
||||
# Run the MCP bridge tests
|
||||
mcp_bridge_success = run_mcp_bridge_tests()
|
||||
|
||||
# Print a summary
|
||||
print_header("Test Summary")
|
||||
print(f"HTTP API Tests: {'PASSED' if http_api_success else 'FAILED'}")
|
||||
print(f"MCP Bridge Tests: {'PASSED' if mcp_bridge_success else 'FAILED'}")
|
||||
print(f"Overall: {'PASSED' if http_api_success and mcp_bridge_success else 'FAILED'}")
|
||||
|
||||
# Return True if all tests passed, False otherwise
|
||||
return http_api_success and mcp_bridge_success
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Check if we have the required dependencies
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("Error: The 'requests' package is required to run the tests.")
|
||||
print("Please install it with 'pip install requests'")
|
||||
sys.exit(1)
|
||||
|
||||
# Parse command line arguments
|
||||
if len(sys.argv) > 1:
|
||||
if sys.argv[1] == "--http":
|
||||
# Run only the HTTP API tests
|
||||
success = run_http_api_tests()
|
||||
elif sys.argv[1] == "--mcp":
|
||||
# Run only the MCP bridge tests
|
||||
success = run_mcp_bridge_tests()
|
||||
else:
|
||||
print(f"Unknown argument: {sys.argv[1]}")
|
||||
print("Usage: python run_tests.py [--http|--mcp]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
# Run all tests
|
||||
success = run_all_tests()
|
||||
|
||||
# Exit with the appropriate status code
|
||||
sys.exit(0 if success else 1)
|
||||
153
test_http_api.py
Normal file
153
test_http_api.py
Normal file
@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for the GhydraMCP HTTP API.
|
||||
This script tests the HTTP endpoints of the Java plugin.
|
||||
"""
|
||||
import json
|
||||
import requests
|
||||
import time
|
||||
import unittest
|
||||
|
||||
# Default Ghidra server port
|
||||
DEFAULT_PORT = 8192
|
||||
BASE_URL = f"http://localhost:{DEFAULT_PORT}"
|
||||
|
||||
class GhydraMCPHttpApiTests(unittest.TestCase):
|
||||
"""Test cases for the GhydraMCP HTTP API"""
|
||||
|
||||
def setUp(self):
|
||||
"""Setup before each test"""
|
||||
# Check if the server is running
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/info", timeout=2)
|
||||
if response.status_code != 200:
|
||||
self.skipTest("Ghidra server not running or not responding")
|
||||
except requests.exceptions.RequestException:
|
||||
self.skipTest("Ghidra server not running or not accessible")
|
||||
|
||||
def test_info_endpoint(self):
|
||||
"""Test the /info endpoint"""
|
||||
response = requests.get(f"{BASE_URL}/info")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify response is valid JSON
|
||||
data = response.json()
|
||||
|
||||
# Check required fields
|
||||
self.assertIn("port", data)
|
||||
self.assertIn("isBaseInstance", data)
|
||||
self.assertIn("project", data)
|
||||
self.assertIn("file", data)
|
||||
|
||||
def test_root_endpoint(self):
|
||||
"""Test the / endpoint"""
|
||||
response = requests.get(BASE_URL)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify response is valid JSON
|
||||
data = response.json()
|
||||
|
||||
# Check required fields
|
||||
self.assertIn("port", data)
|
||||
self.assertIn("isBaseInstance", data)
|
||||
self.assertIn("project", data)
|
||||
self.assertIn("file", data)
|
||||
|
||||
def test_instances_endpoint(self):
|
||||
"""Test the /instances endpoint"""
|
||||
response = requests.get(f"{BASE_URL}/instances")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify response is valid JSON
|
||||
data = response.json()
|
||||
|
||||
# Check required fields in the standard response format
|
||||
self.assertIn("success", data)
|
||||
self.assertTrue(data["success"])
|
||||
self.assertIn("timestamp", data)
|
||||
self.assertIn("port", data)
|
||||
|
||||
# Check that we have either result or data
|
||||
self.assertTrue("result" in data or "data" in data)
|
||||
|
||||
def test_functions_endpoint(self):
|
||||
"""Test the /functions endpoint"""
|
||||
response = requests.get(f"{BASE_URL}/functions")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify response is valid JSON
|
||||
data = response.json()
|
||||
|
||||
# Check required fields in the standard response format
|
||||
self.assertIn("success", data)
|
||||
self.assertTrue(data["success"])
|
||||
self.assertIn("timestamp", data)
|
||||
self.assertIn("port", data)
|
||||
|
||||
# Check that we have either result or data
|
||||
self.assertTrue("result" in data or "data" in data)
|
||||
|
||||
def test_functions_with_pagination(self):
|
||||
"""Test the /functions endpoint with pagination"""
|
||||
response = requests.get(f"{BASE_URL}/functions?offset=0&limit=5")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify response is valid JSON
|
||||
data = response.json()
|
||||
|
||||
# Check required fields in the standard response format
|
||||
self.assertIn("success", data)
|
||||
self.assertTrue(data["success"])
|
||||
self.assertIn("timestamp", data)
|
||||
self.assertIn("port", data)
|
||||
|
||||
def test_classes_endpoint(self):
|
||||
"""Test the /classes endpoint"""
|
||||
response = requests.get(f"{BASE_URL}/classes")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify response is valid JSON
|
||||
data = response.json()
|
||||
|
||||
# Check required fields in the standard response format
|
||||
self.assertIn("success", data)
|
||||
self.assertTrue(data["success"])
|
||||
self.assertIn("timestamp", data)
|
||||
self.assertIn("port", data)
|
||||
|
||||
def test_segments_endpoint(self):
|
||||
"""Test the /segments endpoint"""
|
||||
response = requests.get(f"{BASE_URL}/segments")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify response is valid JSON
|
||||
data = response.json()
|
||||
|
||||
# Check required fields in the standard response format
|
||||
self.assertIn("success", data)
|
||||
self.assertTrue(data["success"])
|
||||
self.assertIn("timestamp", data)
|
||||
self.assertIn("port", data)
|
||||
|
||||
def test_variables_endpoint(self):
|
||||
"""Test the /variables endpoint"""
|
||||
response = requests.get(f"{BASE_URL}/variables")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify response is valid JSON
|
||||
data = response.json()
|
||||
|
||||
# Check required fields in the standard response format
|
||||
self.assertIn("success", data)
|
||||
self.assertTrue(data["success"])
|
||||
self.assertIn("timestamp", data)
|
||||
self.assertIn("port", data)
|
||||
|
||||
def test_error_handling(self):
|
||||
"""Test error handling for non-existent endpoints"""
|
||||
response = requests.get(f"{BASE_URL}/nonexistent_endpoint")
|
||||
# This should return 404, but some servers might return other codes
|
||||
self.assertNotEqual(response.status_code, 200)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
71
test_mcp_client.py
Normal file
71
test_mcp_client.py
Normal file
@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for the GhydraMCP bridge using the MCP client.
|
||||
This script tests the bridge by sending MCP requests and handling responses.
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import anyio
|
||||
|
||||
from mcp.client.session import ClientSession
|
||||
from mcp.client.stdio import StdioServerParameters, stdio_client
|
||||
|
||||
# Set up logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger("mcp_client_test")
|
||||
|
||||
async def test_bridge():
|
||||
"""Test the bridge using the MCP client"""
|
||||
# Configure the server parameters
|
||||
server_parameters = StdioServerParameters(
|
||||
command=sys.executable,
|
||||
args=["bridge_mcp_hydra.py"],
|
||||
)
|
||||
|
||||
# Connect to the bridge
|
||||
logger.info("Connecting to bridge...")
|
||||
async with stdio_client(server_parameters) as (read_stream, write_stream):
|
||||
# Create a session
|
||||
logger.info("Creating session...")
|
||||
async with ClientSession(read_stream, write_stream) as session:
|
||||
# Initialize the session
|
||||
logger.info("Initializing session...")
|
||||
init_result = await session.initialize()
|
||||
logger.info(f"Initialization result: {init_result}")
|
||||
|
||||
# List tools
|
||||
logger.info("Listing tools...")
|
||||
tools_result = await session.list_tools()
|
||||
logger.info(f"Tools result: {tools_result}")
|
||||
|
||||
# Call the list_instances tool
|
||||
logger.info("Calling list_instances tool...")
|
||||
list_instances_result = await session.call_tool("list_instances")
|
||||
logger.info(f"List instances result: {list_instances_result}")
|
||||
|
||||
# Call the discover_instances tool
|
||||
logger.info("Calling discover_instances tool...")
|
||||
discover_instances_result = await session.call_tool("discover_instances")
|
||||
logger.info(f"Discover instances result: {discover_instances_result}")
|
||||
|
||||
# Call the list_functions tool
|
||||
logger.info("Calling list_functions tool...")
|
||||
list_functions_result = await session.call_tool(
|
||||
"list_functions",
|
||||
arguments={"port": 8192, "offset": 0, "limit": 5}
|
||||
)
|
||||
logger.info(f"List functions result: {list_functions_result}")
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
try:
|
||||
anyio.run(test_bridge)
|
||||
except Exception as e:
|
||||
logger.error(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
x
Reference in New Issue
Block a user