A local MCP (Model Context Protocol) server over STDIO focused on dynamic analysis. This build ships with Valgrind Memcheck as the first tool, and the codebase is structured to add more dynamic-analysis tools later.
- Python 3.11+
- MCP server over STDIO or HTTP (JSON-RPC)
- Valgrind Memcheck execution + XML parsing
- ASan / LSan execution paths
- Normalized findings JSON for LLM reasoning
- Per-run artifact storage under
runs/ - Basic tests with pytest
Layers:
- MCP Interface Layer:
src/mcp_dynamic_analysis_server/app.py - Execution Layer:
core/command_builder.py,core/runner.py,core/validators.py - Parsing + Normalization:
core/parser_memcheck.py,core/normalizer.py,core/severity.py - Artifact / Persistence:
core/artifact_store.py
Tools are registered in app.py and can be extended with additional dynamic-analysis tools. Valgrind Memcheck is exposed under valgrind.*; sanitizer execution is exposed through asan.run, lsan.run, and dynamic.run_binary.
- Docker + Docker Compose
- Python 3.11+ (pyenv recommended)
- Valgrind installed locally (Linux)
- A C compiler (
ccorclang) for example binaries
Set WORKSPACE_ROOT to the root directory you want to allow for execution. By default it is the project root. All target_path, cwd, and suppression files must resolve under this directory.
For thesis usage through the root workspace compose, you usually do not need to edit this repository's local .env at all. The top-level docker-compose.thesis-demo.yml already provides the minimal runtime settings used by the UI and orchestrator.
When running mcp-dynamic-analysis-server on its own, the practical split is:
- Required for local standalone usage:
WORKSPACE_ROOT
- Usually enough to keep defaults:
RUNS_DIRARTIFACTS_DIRVALGRIND_BINLOG_LEVELMAX_ARTIFACT_BYTESMAX_ARTIFACT_PREVIEW_BYTES
- Optional, only for remote artifact/object-storage workflows:
- all
R2_*variables
- all
For R2 uploads, configure credentials in .env (see .env.example).
R2_ENDPOINT is used for SDK/presigning, while R2_ALLOW_HOSTS controls which hosts are allowed for downloads. If you use a custom public domain for R2, set R2_ALLOW_HOSTS to that domain (and optionally also include the raw R2 endpoint host as a fallback). Use hostnames only (no scheme).
Optional: enable R2_HEALTHCHECK_ON_STARTUP=true to run a quick head_bucket at startup (timeout via R2_HEALTHCHECK_TIMEOUT_SEC). Failures are logged as warnings and do not stop the server.
pyenv virtualenv 3.12.8 mcp-da
pyenv local mcp-da
pip install -e .[test]On macOS (Homebrew):
brew install valgrindOn Ubuntu/Debian:
sudo apt-get update && sudo apt-get install -y valgrindValgrind is Linux-only. On macOS, use Docker to run the server and tools inside a Linux container.
Build and run the MCP server over HTTP (Valgrind runs inside the container):
cp .env.example .env
docker compose up --buildThe server listens on http://localhost:8080.
Artifacts will be written to runs/ on the host via the volume mount.
The local docker-compose.yml is intended for developing this repository in isolation.
If you are running the full thesis application stack from the workspace root, use the
top-level compose there instead of this repo-local compose.
If running on host (no Docker):
make -C examples/vulnerableFor AddressSanitizer builds:
make -C examples/vulnerable asanIf running in Docker (recommended):
docker compose exec -T mcp-da make -C examples/vulnerableFor AddressSanitizer builds in Docker:
docker compose exec -T mcp-da make -C examples/vulnerable asanOutputs:
examples/vulnerable/bin/invalid_readexamples/vulnerable/bin/leak
mcp-da-serverThe server reads JSON-RPC messages from STDIN and writes responses to STDOUT.
You can also run the server as a simple HTTP JSON-RPC API:
mcp-da-server --transport http --host 0.0.0.0 --port 8080Send JSON-RPC to POST /mcp:
curl -s http://localhost:8080/mcp \\\n -H 'Content-Type: application/json' \\\n -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\":{}}'\n```
Example JSON-RPC (tools/call):
```json
{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "valgrind.analyze_memcheck", "arguments": { "target_path": "examples/vulnerable/bin/invalid_read" } } }Response (result content is JSON text):
{ "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": "{\\n \\\"run_id\\\": \\\"...\\\"\\n}" } ] } }Example host configuration (pseudo):
{
"mcpServers": {
"dynamic-analysis": {
"command": "mcp-da-server",
"args": []
}
}
}Input (example):
{
"target_path": "examples/vulnerable/bin/invalid_read",
"args": [],
"cwd": "examples/vulnerable",
"timeout_sec": 30,
"track_origins": true,
"leak_check": "full",
"show_leak_kinds": "all",
"xml": true,
"suppressions": [],
"env": {},
"stdin": "",
"labels": ["demo"]
}For remote/public servers, you can upload a binary to R2 and pass artifact_id or a target_url:
{
"artifact_id": "abcd1234",
"args": [],
"cwd": "/app/workdir",
"timeout_sec": 60,
"track_origins": true,
"leak_check": "full",
"show_leak_kinds": "all",
"xml": true,
"suppressions": [],
"env": {},
"stdin": ""
}Output (summary):
{
"run_id": "20260313-153012-acde1234",
"status": "completed",
"tool": "memcheck",
"exit_code": 42,
"timed_out": false,
"error_exit_code_triggered": true,
"stats": {
"finding_count": 1,
"high": 1,
"medium": 0,
"low": 0
},
"top_findings": ["..."],
"artifacts": {
"run_dir": "runs/20260313-153012-acde1234",
"report_path": "runs/20260313-153012-acde1234/normalized_report.json",
"xml_path": "runs/20260313-153012-acde1234/valgrind.xml",
"log_path": "runs/20260313-153012-acde1234/valgrind.log",
"stdout_path": "runs/20260313-153012-acde1234/stdout.txt",
"stderr_path": "runs/20260313-153012-acde1234/stderr.txt"
}
}Input:
{ "run_id": "20260313-153012-acde1234" }Output: full normalized report JSON.
Input:
{
"run_id": "20260313-153012-acde1234",
"severity": "high",
"kind": "InvalidRead",
"file": "src/main.c",
"function": "parse_input",
"limit": 20
}Output:
{
"run_id": "20260313-153012-acde1234",
"count": 3,
"findings": []
}Input:
{
"base_run_id": "20260313-153012-acde1234",
"new_run_id": "20260313-153512-acde5678"
}Output:
{
"base_run_id": "20260313-153012-acde1234",
"new_run_id": "20260313-153512-acde5678",
"summary": {"fixed": 0, "new": 1, "persistent": 2},
"fixed_findings": [],
"new_findings": [],
"persistent_findings": []
}Input:
{ "run_id": "20260313-153012-acde1234", "artifact_type": "xml" }Output:
{
"run_id": "20260313-153012-acde1234",
"artifact_type": "xml",
"path": "runs/20260313-153012-acde1234/valgrind.xml",
"content": "...",
"truncated": false,
"size_bytes": 12345
}Run an AddressSanitizer-instrumented binary (built with -fsanitize=address).
Input:
{
"target_path": "examples/vulnerable/bin/leak_asan",
"args": [],
"cwd": "examples/vulnerable",
"timeout_sec": 30,
"asan_options": "detect_leaks=1",
"lsan_options": "",
"env": {},
"stdin": ""
}Output shape matches valgrind.analyze_memcheck (normalized findings).
Run a LeakSanitizer/ASan-instrumented binary with leak detection enabled.
Input:
{
"target_path": "examples/vulnerable/bin/leak_asan",
"args": [],
"cwd": "examples/vulnerable",
"timeout_sec": 30,
"lsan_options": "detect_leaks=1:exitcode=23",
"env": {},
"stdin": "",
"labels": ["demo"]
}Generic binary execution entrypoint for supported analyzers.
Input:
{
"analyzer": "lsan",
"target_path": "examples/vulnerable/bin/leak_asan",
"args": [],
"cwd": "examples/vulnerable",
"timeout_sec": 30,
"labels": ["demo"]
}List stored dynamic analysis runs with optional filters.
Input:
{
"tool": "lsan",
"status": "completed",
"label": "demo",
"limit": 50
}Create a presigned R2 upload URL for binaries.
Input:
{
"filename": "app.bin",
"content_type": "application/octet-stream",
"size_bytes": 123456,
"sha256": "..."
}Output:
{
"artifact_id": "abcd1234",
"upload_url": "https://...presigned-put...",
"download_url": "https://...presigned-get...",
"expires_in_sec": 900,
"key": "uploads/abcd1234/app.bin"
}Each run creates runs/<run_id>/ with:
request.jsoncommand.txtstdout.txtstderr.txtvalgrind.xmlvalgrind.lognormalized_report.jsonsummary.jsonmetadata.json
Resources are exposed under the URI scheme:
artifact://<run_id>/<artifact_type>
Example:
artifact://20260313-153012-acde1234/xml
The server exposes a judge_guidance prompt to help LLMs interpret Memcheck findings.
Run an end-to-end demo (build examples, run Memcheck twice, compare):
python scripts/demo_end_to_end.pypytest- Only Valgrind Memcheck is implemented today, but the registry supports adding more dynamic-analysis tools.
- The JSON-RPC STDIO transport expects one JSON message per line.
- Large artifacts are truncated when read via
valgrind.get_raw_artifactor resource reads.
To add new dynamic-analysis tools:
- Implement a new tool handler in
src/mcp_dynamic_analysis_server/tools/. - Add parser/normalizer modules under
core/as needed. - Register the tool in
app.pywith a new name and JSON schema.
- XML output is enforced for Memcheck to ensure structured parsing.
- The server validates executable paths within
WORKSPACE_ROOT. - Artifacts are isolated per run id.
mcp_dynamic_analysis_server/
pyproject.toml
README.md
.env.example
src/mcp_dynamic_analysis_server/
app.py
config.py
logging_config.py
models/
tools/
core/
resources/
prompts/
runs/
examples/
tests/