Skip to content

Commit 4a45f8c

Browse files
committed
major bug fixes and new integration tests
1 parent caf0e11 commit 4a45f8c

45 files changed

Lines changed: 2172 additions & 8880 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CONTRIBUTING.md

Lines changed: 0 additions & 628 deletions
This file was deleted.

DOCS.md

Lines changed: 0 additions & 1799 deletions
This file was deleted.

README.md

Lines changed: 89 additions & 393 deletions
Large diffs are not rendered by default.

build.sh

Lines changed: 0 additions & 31 deletions
This file was deleted.

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ services:
44
codebadger-toolkit:
55
build:
66
context: .
7-
dockerfile: Dockerfile.joern
7+
dockerfile: Dockerfile
88
image: codebadger-toolkit:latest
99
container_name: codebadger-toolkit-server
1010
ports:

examples/integration_test.py

Lines changed: 0 additions & 403 deletions
This file was deleted.

examples/sample_client.py

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Sample Client for CodeBadger Toolkit Server
4+
5+
This client demonstrates basic usage of the CodeBadger Toolkit MCP server:
6+
1. Generate a CPG for a local codebase
7+
2. List methods in the codebase
8+
3. Run a simple CPGQL query
9+
"""
10+
11+
import asyncio
12+
import logging
13+
import sys
14+
import os
15+
16+
# Configure logging
17+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
18+
logger = logging.getLogger(__name__)
19+
20+
try:
21+
from fastmcp import Client
22+
except ImportError:
23+
logger.error("FastMCP not found. Install with: pip install fastmcp")
24+
sys.exit(1)
25+
26+
27+
def extract_tool_result(result):
28+
"""Extract dictionary data from CallToolResult"""
29+
if hasattr(result, 'content') and result.content:
30+
content_text = result.content[0].text
31+
try:
32+
import json
33+
parsed = json.loads(content_text)
34+
35+
# Handle complex results that return Scala output with embedded JSON
36+
if isinstance(parsed, dict) and 'value' in parsed:
37+
value = parsed['value']
38+
if isinstance(value, str):
39+
# Look for embedded JSON in the Scala output
40+
import re
41+
# Match the escaped JSON string between quotes
42+
json_match = re.search(r'val res\d+: String = ("\{.*\}")', value)
43+
if json_match:
44+
try:
45+
# Extract the escaped JSON string and unescape it
46+
escaped_json = json_match.group(1)
47+
# Remove the surrounding quotes and unescape
48+
json_str = escaped_json[1:-1] # Remove quotes
49+
json_str = json_str.replace('\\"', '"') # Unescape quotes
50+
json_str = json_str.replace('\\\\', '\\') # Unescape backslashes
51+
return json.loads(json_str)
52+
except json.JSONDecodeError:
53+
pass
54+
# If no embedded JSON found, return the original parsed result
55+
return parsed
56+
else:
57+
return parsed
58+
else:
59+
return parsed
60+
except json.JSONDecodeError:
61+
return {"error": content_text}
62+
return {}
63+
64+
65+
async def main():
66+
"""Main client function"""
67+
# Server URL - adjust if running on different host/port
68+
server_url = "http://localhost:4242/mcp"
69+
70+
# Path to the codebase - use container path since server runs in Docker
71+
# Host path: playground/codebases/core -> Container path: /app/playground/codebases/core
72+
codebase_path = "/app/playground/codebases/core"
73+
74+
logger.info("="*60)
75+
logger.info("CODEBADGER TOOLKIT SAMPLE CLIENT")
76+
logger.info("="*60)
77+
logger.info(f"Server URL: {server_url}")
78+
logger.info(f"Codebase: {codebase_path}")
79+
80+
try:
81+
async with Client(server_url) as client:
82+
logger.info("\n[1] Testing server connectivity...")
83+
await client.ping()
84+
logger.info("✓ Server is responding")
85+
86+
# ===== GENERATE CPG =====
87+
logger.info("\n[2] Generating CPG for codebase...")
88+
cpg_result = await client.call_tool("generate_cpg", {
89+
"source_type": "local",
90+
"source_path": codebase_path,
91+
"language": "c"
92+
})
93+
94+
cpg_dict = extract_tool_result(cpg_result)
95+
logger.info(f"CPG generation result: {cpg_dict}")
96+
97+
if "codebase_hash" not in cpg_dict:
98+
logger.error("❌ No codebase_hash returned")
99+
return
100+
101+
codebase_hash = cpg_dict["codebase_hash"]
102+
logger.info(f"✓ CPG generation initiated. Hash: {codebase_hash}")
103+
104+
# ===== WAIT FOR CPG TO BE READY =====
105+
logger.info("\n[3] Waiting for CPG to be ready...")
106+
cpg_ready = False
107+
max_attempts = 30
108+
109+
for attempt in range(max_attempts):
110+
await asyncio.sleep(2) # Wait 2 seconds between checks
111+
112+
status_result = await client.call_tool("get_cpg_status", {
113+
"codebase_hash": codebase_hash
114+
})
115+
116+
status_dict = extract_tool_result(status_result)
117+
status = status_dict.get("status")
118+
exists = status_dict.get("exists", False)
119+
120+
logger.info(f" Attempt {attempt + 1}/{max_attempts}: status={status}, exists={exists}")
121+
122+
if status in ["ready", "cached"] and exists:
123+
cpg_ready = True
124+
logger.info("✓ CPG is ready!")
125+
break
126+
127+
if not cpg_ready:
128+
logger.error("❌ CPG not ready after waiting")
129+
return
130+
131+
# ===== LIST METHODS =====
132+
logger.info("\n[4] Listing methods in the codebase...")
133+
methods_result = await client.call_tool("list_methods", {
134+
"codebase_hash": codebase_hash,
135+
"limit": 20
136+
})
137+
138+
methods_dict = extract_tool_result(methods_result)
139+
140+
if methods_dict.get("success"):
141+
methods = methods_dict.get("methods", [])
142+
total = methods_dict.get("total", 0)
143+
logger.info(f"✓ Found {total} methods total, showing up to 20:")
144+
145+
for method in methods[:10]: # Show first 10
146+
logger.info(f" - {method.get('name', 'unknown')} in {method.get('filename', 'unknown')}")
147+
148+
if len(methods) > 10:
149+
logger.info(f" ... and {len(methods) - 10} more methods")
150+
else:
151+
logger.error(f"❌ Failed to list methods: {methods_dict}")
152+
153+
# ===== RUN SIMPLE QUERY - GET CODEBASE SUMMARY =====
154+
logger.info("\n[5] Getting codebase summary...")
155+
156+
summary_result = await client.call_tool("get_codebase_summary", {
157+
"codebase_hash": codebase_hash
158+
})
159+
160+
summary_dict = extract_tool_result(summary_result)
161+
162+
if summary_dict.get("success"):
163+
summary = summary_dict.get("summary", {})
164+
logger.info("✓ Codebase summary retrieved:")
165+
logger.info(f" Language: {summary.get('language')}")
166+
logger.info(f" Files: {summary.get('total_files')}")
167+
logger.info(f" Methods: {summary.get('total_methods')}")
168+
logger.info(f" Calls: {summary.get('total_calls')}")
169+
else:
170+
logger.error(f"❌ Failed to get summary: {summary_dict}")
171+
172+
# ===== RUN ANOTHER QUERY - LIST CALLS =====
173+
logger.info("\n[6] Listing function calls...")
174+
175+
calls_result = await client.call_tool("list_calls", {
176+
"codebase_hash": codebase_hash,
177+
"limit": 10
178+
})
179+
180+
calls_dict = extract_tool_result(calls_result)
181+
182+
if calls_dict.get("success"):
183+
calls = calls_dict.get("calls", [])
184+
total = calls_dict.get("total", 0)
185+
logger.info(f"✓ Found {total} calls total, showing up to 10:")
186+
187+
for call in calls[:5]: # Show first 5
188+
logger.info(f" - {call.get('caller', 'unknown')} -> {call.get('callee', 'unknown')}")
189+
190+
if len(calls) > 5:
191+
logger.info(f" ... and {len(calls) - 5} more calls")
192+
else:
193+
logger.error(f"❌ Failed to list calls: {calls_dict}")
194+
195+
logger.info("\n" + "="*60)
196+
logger.info("SAMPLE CLIENT COMPLETED SUCCESSFULLY!")
197+
logger.info("="*60)
198+
199+
except Exception as e:
200+
logger.error(f"❌ Client error: {e}", exc_info=True)
201+
sys.exit(1)
202+
203+
204+
if __name__ == "__main__":
205+
try:
206+
asyncio.run(main())
207+
except KeyboardInterrupt:
208+
logger.info("\n🛑 Client interrupted by user")
209+
sys.exit(1)

main.py

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@
1818
CodebaseTracker,
1919
GitManager,
2020
CPGGenerator,
21+
JoernServerClient,
22+
JoernServerManager,
23+
PortManager,
2124
QueryExecutor
2225
)
23-
from src.utils import RedisClient, setup_logging
26+
from src.utils import RedisClient, SyncRedisClient, setup_logging
2427
from src.tools import register_tools
2528

2629
# Version information - bump this when releasing new versions
@@ -47,30 +50,40 @@ async def lifespan(mcp: FastMCP):
4750
logger.info("Created required directories")
4851

4952
try:
50-
# Initialize Redis
53+
# Initialize Redis clients
5154
redis_client = RedisClient(config.redis)
5255
await redis_client.connect()
53-
logger.info("Redis client connected")
56+
57+
sync_redis_client = SyncRedisClient(config.redis)
58+
sync_redis_client.connect()
59+
60+
logger.info("Redis clients connected")
5461

5562
# Initialize services
5663
services['config'] = config
5764
services['redis'] = redis_client
58-
services['codebase_tracker'] = CodebaseTracker(redis_client)
65+
services['sync_redis'] = sync_redis_client
66+
services['codebase_tracker'] = CodebaseTracker(sync_redis_client)
5967
services['git_manager'] = GitManager(config.storage.workspace_root)
6068

61-
# Initialize CPG generator (runs Joern CLI directly in container)
62-
services['cpg_generator'] = CPGGenerator(config=config)
63-
# Skip initialize() - no Docker needed
69+
# Initialize port manager for Joern servers
70+
services['port_manager'] = PortManager()
6471

65-
# Initialize query executor (runs Joern servers as local subprocesses)
66-
services['query_executor'] = QueryExecutor(
67-
config.query,
68-
config.joern,
69-
redis_client,
70-
docker_orchestrator=None # Will start Joern servers directly
72+
# Initialize Joern server manager
73+
services['joern_server_manager'] = JoernServerManager(
74+
joern_binary_path=config.joern.binary_path
7175
)
76+
77+
# Initialize CPG generator (runs Joern CLI directly in container)
78+
services['cpg_generator'] = CPGGenerator(config=config, joern_server_manager=services['joern_server_manager'])
7279
# Skip initialize() - no Docker needed
7380

81+
# Initialize query executor with Joern server manager
82+
services['query_executor'] = QueryExecutor(services['joern_server_manager'], config=config.query)
83+
84+
# Register MCP tools now that services are initialized
85+
register_tools(mcp, services)
86+
7487
logger.info("All services initialized")
7588
logger.info("CodeBadger Toolkit Server is ready")
7689

@@ -79,12 +92,9 @@ async def lifespan(mcp: FastMCP):
7992
# Shutdown
8093
logger.info("Shutting down CodeBadger Toolkit Server")
8194

82-
# Cleanup query executor (stops any running Joern server subprocesses)
83-
if 'query_executor' in services:
84-
await services['query_executor'].cleanup()
85-
8695
# Close connections
8796
await redis_client.close()
97+
sync_redis_client.close()
8898

8999
logger.info("CodeBadger Toolkit Server shutdown complete")
90100

@@ -99,8 +109,8 @@ async def lifespan(mcp: FastMCP):
99109
lifespan=lifespan
100110
)
101111

102-
# Register MCP tools
103-
register_tools(mcp, services)
112+
# Note: Tools are registered inside the lifespan function
113+
# register_tools(mcp, services)
104114

105115

106116
# Health check endpoint

run_tests.py

Lines changed: 0 additions & 25 deletions
This file was deleted.

0 commit comments

Comments
 (0)