Add your own detectors without modifying the core. The server auto-registers
everything in src/tools/custom_tools.py on start.
- Write a Scala query template in
src/tools/queries/<name>.scala. - Register a Python tool in
src/tools/custom_tools.py. - Restart - the tool appears in every connected MCP client.
flowchart LR
Q[queries/<name>.scala<br/>CPGQL + placeholders] --> L[QueryLoader.load]
L --> T[Python @mcp.tool<br/>in custom_tools.py]
T --> R[_run_query] --> J[Joern server]
J --> O[<codebadger_result> text]
Templates are Scala blocks. Variables use {{double_braces}}, substituted at
runtime; user values are sanitized against template injection. Wrap output in
<codebadger_result> tags so the parser extracts it cleanly.
{
import io.shiftleft.codepropertygraph.generated.nodes._
import io.shiftleft.semanticcpg.language._
val myPattern = "{{my_pattern}}" // string - keep the quotes
val maxResults = {{max_results}} // numeric - no quotes
val output = new StringBuilder()
val results = cpg.call.name(myPattern).take(maxResults).l
if (results.isEmpty) output.append("No findings.\n")
else results.zipWithIndex.foreach { case (c, i) =>
output.append(s"--- Finding ${i + 1} ---\n")
output.append(s"${c.location.filename}:${c.location.lineNumber.getOrElse(-1)} ${c.code}\n")
}
"<codebadger_result>\n" + output.toString() + "</codebadger_result>"
}| Variable kind | Scala | Python call |
|---|---|---|
| String | val x = "{{x}}" |
QueryLoader.load("q", x="value") |
| Integer | val n = {{n}} |
QueryLoader.load("q", n=50) |
| Long (node ID) | val id = {{id}}L |
QueryLoader.load("q", id=12345) |
To filter by file, anchor to a path boundary so "parser.c" matches /src/parser.c
but not /src/myparser.c:
def pathBoundaryRegex(f: String) = "(^|.*/)" + java.util.regex.Pattern.quote(f) + "$"Add inside register_custom_tools() in src/tools/custom_tools.py:
@mcp.tool(
description="""One-line summary shown in client listings.
Args:
codebase_hash: Hash returned by generate_cpg.
my_param: What this controls (default "value").
Returns:
Text report with findings and locations.
""",
tags={"security", "CWE-NNN"},
)
def my_tool(
codebase_hash: Annotated[str, Field(description="Codebase hash from generate_cpg")],
my_param: Annotated[str, Field(description="Detection pattern")] = "default",
max_results: Annotated[int, Field(description="Max findings", ge=1, le=500)] = 50,
) -> str:
try:
info = _get_codebase(services, codebase_hash)
query = QueryLoader.load("my_tool", my_pattern=my_param, max_results=max_results)
return _run_query(
services, codebase_hash, info.cpg_path, query,
timeout=60, tool_name="my_tool",
cache_params={"my_param": my_param, "max_results": max_results},
)
except (ValueError, RuntimeError) as e:
return f"Error: {e}"
except Exception as e:
logger.error(f"my_tool: {e}", exc_info=True)
return f"Internal Error: {e}"Then docker compose restart codebadger (or restart main.py).
_get_codebase(services, hash) → CodebaseInfo- validates the hash; raisesValueErrorif unknown. Fields:.cpg_path,.language,.source_path,.metadata._run_query(services, hash, cpg_path, query, *, timeout, tool_name, cache_params) → str- renders + executes the query, extracts
<codebadger_result>. Passingtool_name+cache_paramscaches the result (TTLquery.cache_ttl); omit both to always run fresh. RaisesRuntimeErroron failure.
- renders + executes the query, extracts
QueryLoader.load(name, **kwargs) → str- loadsqueries/<name>.scala, substitutes{{key}}placeholders, caches the template in memory.
The services dict also exposes query_executor, codebase_tracker,
db_manager, and config for cases the helpers don't cover.
Use tags so clients/agents can discover tools: "security", "code-quality",
"taint", "memory-safety", "injection", "attack-surface", "CWE-NNN".
- Prototype the CPGQL with the built-in
run_cpgql_querytool first; move it to a.scalafile once stable. find_command_injection_sinksis the reference implementation - copy it.
- Queries live in
.scalafiles, not inline Python. Each piece is editable independently;QueryLoadercaches templates so there's no per-query I/O. <codebadger_result>wrapping over.toJsonPretty. Analysis tools produce readable multi-section text reports;.toJsonPrettyis reserved for simple collection traversals that need raw JSON.- Template-injection sanitization. Any
{{inside a supplied value is escaped, so a crafted value can't overwrite another template variable. - Tools return
str, notDict. Consistent type so clients display results without unwrapping. _get_codebase/_run_queryare thin helpers, not abstractions - they remove boilerplate but don't hideservices[...].