Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ A Model Context Protocol (MCP) server that provides AI assistants with static co

## Features

- **Multi-Language Support**: Java, C/C++, JavaScript, Python, Go, Kotlin, Swift
- **Multi-Language Support**: Java, C/C++, JavaScript, Python, Go, Kotlin, C#, Ghidra, Jimple, PHP, Ruby, Swift
- **Docker Isolation**: Each analysis session runs in a secure container
- **GitHub Integration**: Analyze repositories directly from GitHub URLs
- **Session-Based**: Persistent CPG sessions with automatic cleanup
Expand Down Expand Up @@ -150,7 +150,7 @@ sessions:

cpg:
generation_timeout: 600 # CPG generation timeout (seconds)
supported_languages: [java, c, cpp, javascript, python, go, kotlin, swift]
supported_languages: [java, c, cpp, javascript, python, go, kotlin, csharp, ghidra, jimple, php, ruby, swift]
```

Environment variables override config file settings (e.g., `MCP_HOST`, `REDIS_HOST`, `SESSION_TTL`).
Expand Down
5 changes: 5 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ cpg:
- python
- go
- kotlin
- csharp
- ghidra
- jimple
- php
- ruby
- swift

query:
Expand Down
15 changes: 15 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[tool:pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
--verbose
--tb=short
--strict-markers
--disable-warnings
--asyncio-mode=auto
markers =
unit: Unit tests
integration: Integration tests
slow: Slow running tests
25 changes: 25 additions & 0 deletions run_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env python3
"""
Test runner for Joern MCP
"""
import sys
import subprocess
from pathlib import Path

def run_tests():
"""Run the test suite"""
project_root = Path(__file__).parent

# Ensure we're in the project root
if not (project_root / "pyproject.toml").exists():
print("Error: Must run from project root directory")
sys.exit(1)

# Run pytest
cmd = [sys.executable, "-m", "pytest", "tests/"]
result = subprocess.run(cmd, cwd=project_root)

sys.exit(result.returncode)

if __name__ == "__main__":
run_tests()
2 changes: 1 addition & 1 deletion src/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ class CPGConfig:
generation_timeout: int = 600 # 10 minutes
max_repo_size_mb: int = 500
supported_languages: List[str] = field(default_factory=lambda: [
"java", "c", "cpp", "javascript", "python", "go", "kotlin"
"java", "c", "cpp", "javascript", "python", "go", "kotlin", "csharp", "ghidra", "jimple", "php", "ruby", "swift"
])


Expand Down
6 changes: 6 additions & 0 deletions src/services/cpg_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ class CPGGenerator:
"python": "pysrc2cpg",
"go": "gosrc2cpg",
"kotlin": "kotlin2cpg",
"csharp": "csharpsrc2cpg",
"ghidra": "ghidra2cpg",
"jimple": "jimple2cpg",
"php": "php2cpg",
"ruby": "rubysrc2cpg",
"swift": "swiftsrc2cpg",
}

def __init__(
Expand Down
22 changes: 13 additions & 9 deletions src/services/query_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ def set_cpg_generator(self, cpg_generator):
async def execute_query_async(
self,
session_id: str,
cpg_path: str,
query: str,
timeout: Optional[int] = None
timeout: Optional[int] = None,
limit: Optional[int] = 150
) -> str:
"""Execute a CPGQL query asynchronously and return query UUID"""
try:
Expand All @@ -85,7 +85,7 @@ async def execute_query_async(
container_cpg_path = "/workspace/cpg.bin"

# Normalize query to ensure JSON output and pipe to file
query_normalized = self._normalize_query_for_json(query.strip())
query_normalized = self._normalize_query_for_json(query.strip(), limit)
output_file = f"/tmp/query_{query_id}.json"
query_with_pipe = f"{query_normalized} #> \"{output_file}\""

Expand All @@ -100,7 +100,7 @@ async def execute_query_async(
}

# Start async execution
asyncio.create_task(self._execute_query_background(query_id, session_id, container_cpg_path, query_with_pipe, timeout))
asyncio.create_task(self._execute_query_background(query_id, session_id, container_cpg_path, query_with_pipe, timeout, limit))

logger.info(f"Started async query {query_id} for session {session_id}")
return query_id
Expand All @@ -113,9 +113,8 @@ async def _execute_query_background(
self,
query_id: str,
session_id: str,
cpg_path: str,
query_with_pipe: str,
timeout: Optional[int]
timeout: Optional[int],
):
"""Execute query in background"""
try:
Expand Down Expand Up @@ -223,7 +222,8 @@ async def execute_query(
session_id: str,
cpg_path: str,
query: str,
timeout: Optional[int] = None
timeout: Optional[int] = None,
limit: Optional[int] = 150
) -> QueryResult:
"""Execute a CPGQL query synchronously (for backwards compatibility)"""
start_time = time.time()
Expand All @@ -233,7 +233,7 @@ async def execute_query(
validate_cpgql_query(query)

# Normalize query to ensure JSON output
query_normalized = self._normalize_query_for_json(query.strip())
query_normalized = self._normalize_query_for_json(query.strip(), limit)

# Check cache if enabled
if self.config.cache_enabled and self.redis:
Expand Down Expand Up @@ -339,7 +339,7 @@ async def cleanup_old_queries(self, max_age_seconds: int = 3600):
if to_cleanup:
logger.info(f"Cleaned up {len(to_cleanup)} old queries")

def _normalize_query_for_json(self, query: str) -> str:
def _normalize_query_for_json(self, query: str, limit: Optional[int] = None) -> str:
"""Normalize query to ensure JSON output"""
# Remove any existing output modifiers
query = query.strip()
Expand All @@ -352,6 +352,10 @@ def _normalize_query_for_json(self, query: str) -> str:
elif query.endswith('.toJsonPretty'):
query = query[:-13]

# Add limit if specified
if limit is not None and limit > 0:
query = f"{query}.take({limit})"

# Add .toJsonPretty for proper JSON output
return query + '.toJsonPretty'

Expand Down
16 changes: 11 additions & 5 deletions src/tools/mcp_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ async def create_cpg_session(
source_type: Either "local" or "github"
source_path: For local: absolute path to source directory
For github: full GitHub URL (e.g., https://github.com/user/repo)
language: Programming language - one of: java, c, cpp, javascript, python, go, kotlin
language: Programming language - one of: java, c, cpp, javascript, python, go, kotlin, csharp, ghidra, jimple, php, ruby, swift
github_token: GitHub Personal Access Token for private repositories (optional)
branch: Specific git branch to checkout (optional, defaults to default branch)

Expand Down Expand Up @@ -332,7 +332,8 @@ async def generate_and_cache():
async def run_cpgql_query_async(
session_id: str,
query: str,
timeout: int = 30
timeout: int = 30,
limit: Optional[int] = 150
) -> Dict[str, Any]:
"""
Executes a CPGQL query asynchronously and returns a query ID for status tracking.
Expand All @@ -345,6 +346,7 @@ async def run_cpgql_query_async(
session_id: The session ID returned from create_cpg_session
query: CPGQL query string (automatically converted to JSON output)
timeout: Maximum execution time in seconds (default: 30)
limit: Maximum number of results to return (default: 150)

Returns:
{
Expand Down Expand Up @@ -381,7 +383,8 @@ async def run_cpgql_query_async(
session_id=session_id,
cpg_path=session.cpg_path,
query=query,
timeout=timeout
timeout=timeout,
limit=limit
)

return {
Expand Down Expand Up @@ -767,7 +770,8 @@ async def cleanup_queries(
async def run_cpgql_query(
session_id: str,
query: str,
timeout: int = 30
timeout: int = 30,
limit: Optional[int] = 150
) -> Dict[str, Any]:
"""
Executes a CPGQL query synchronously on a loaded CPG.
Expand All @@ -780,6 +784,7 @@ async def run_cpgql_query(
session_id: The session ID returned from create_cpg_session
query: CPGQL query string (automatically converted to JSON output)
timeout: Maximum execution time in seconds (default: 30)
limit: Maximum number of results to return (default: 150)

Returns:
{
Expand Down Expand Up @@ -821,7 +826,8 @@ async def run_cpgql_query(
session_id=session_id,
cpg_path=container_cpg_path,
query=query,
timeout=timeout
timeout=timeout,
limit=limit
)

return {
Expand Down
6 changes: 3 additions & 3 deletions src/utils/redis_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import json
import logging
from typing import Optional, Dict, Any, List
import redis.asyncio as aioredis
import redis.asyncio as redis

from ..models import RedisConfig, Session

Expand All @@ -16,12 +16,12 @@ class RedisClient:

def __init__(self, config: RedisConfig):
self.config = config
self.client: Optional[aioredis.Redis] = None
self.client: Optional[redis.Redis] = None

async def connect(self):
"""Establish Redis connection"""
try:
self.client = await aioredis.from_url(
self.client = redis.from_url(
f"redis://{self.config.host}:{self.config.port}/{self.config.db}",
password=self.config.password,
decode_responses=self.config.decode_responses
Expand Down
2 changes: 1 addition & 1 deletion src/utils/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def validate_source_type(source_type: str):

def validate_language(language: str):
"""Validate programming language"""
supported = ["java", "c", "cpp", "javascript", "python", "go", "kotlin"]
supported = ["java", "c", "cpp", "javascript", "python", "go", "kotlin", "csharp", "ghidra", "jimple", "php", "ruby", "swift"]
if language not in supported:
raise ValidationError(
f"Unsupported language '{language}'. Supported: {', '.join(supported)}"
Expand Down
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Test package for Joern MCP
Loading