How the Cypher Query Engine Works in Codebase-Memory-MCP: OpenCypher Support and Implementation

The Cypher query engine in DeusData/codebase-memory-mcp implements a lightweight OpenCypher subset that lexes, parses, and compiles graph queries into parameterized SQLite SQL, executing them against an internal knowledge graph with enforced safety ceilings.

The codebase-memory-mcp repository provides a Model Context Protocol (MCP) server that stores codebases as knowledge graphs. Its Cypher query engine translates OpenCypher syntax into executable SQL, enabling semantic code search while preventing resource exhaustion through built-in limits and authorisation checks.

Four-Stage Query Processing Pipeline

The engine processes every Cypher query through a tightly-coupled pipeline implemented primarily in src/cypher/cypher.c and src/store/store.c.

Lexing and Tokenization

The first stage uses a hand-written lexer to scan raw query strings into tokens. Located in src/cypher/cypher.c (lines 45–120), the lexer handles string escaping, number literals, comments, and two-character operators such as != and ... Key functions include lex_push for token emission, lex_string_literal for quote handling, and lex_try_number for numeric parsing.

Recursive-Descent Parsing

Next, a recursive-descent parser constructs an abstract syntax tree (AST). The parser recognises node patterns, relationship patterns, optional matches, WHERE clauses, and return items while explicitly rejecting unsupported constructs. Core entry points include parse_node, parse_rel, and parse_where, with expression builders like parse_or_expr and parse_and_expr handling boolean logic (see src/cypher/cypher.c, lines 445–575). If the parser encounters unsupported clauses such as CREATE, MERGE, or CALL, it returns a clear error via unsupported_clause_error (lines 813–838) before reaching the SQL layer.

AST-to-SQL Translation

During the planning stage, the AST walker generates parameterised SQLite queries. Each MATCH pattern becomes a series of joins on the node and edge tables. Labels and relationship-type alternations compile to IN predicates, while optional matches render as LEFT OUTER JOINs. The planner also injects pagination (LIMIT, SKIP) and aggregates (COUNT, SUM). Variable-length hop ranges parsed by parse_hop_range (lines 998–1020) are translated into recursive or bounded join patterns depending on the specified depth.

Execution with SQLite Safety Controls

The final stage executes the generated SQL in src/store/store.c. The cbm_query_graph function (lines 350–420) runs the query through SQLite, while a custom authoriser installed in cbm_db_open (line 639) blocks disallowed statements such as ATTACH or DROP. Result rows are marshalled into JSON for the client, ensuring the underlying database cannot be abused by malformed Cypher patterns.

Supported OpenCypher Features and Limitations

The engine supports a read-only subset of OpenCypher optimised for graph traversal and property filtering.

Supported constructs include:

  • MATCH clauses with node and relationship patterns
  • Variable-length paths using * and range operators (e.g., [:CALL*1..3])
  • WHERE clauses with boolean expressions and property comparisons
  • RETURN with aggregates (COUNT, SUM) and pagination (LIMIT, SKIP)
  • Label and relationship type filtering

Explicitly unsupported constructs:

The parser rejects write operations and procedural extensions. Any query containing CREATE, MERGE, DELETE, SET, REMOVE, or CALL triggers the unsupported_clause_error handler (lines 813–838), returning an immediate error to the CLI without touching the SQL layer.

Safety Mechanisms for Open Queries

The engine treats "open" queries—those without explicit termination or with unbounded variable-length hops—through multiple protective layers.

Variable-Length Hop Ceilings

When parsing hops like [:CALL*], the parse_hop_range function (lines 998–1020) extracts minimum and maximum bounds. If the upper bound is omitted, the planner emits an unbounded join but enforces a hard-coded safety ceiling of 100,000 rows to prevent runaway traversals.

Default LIMIT Injection

If a query omits the LIMIT clause, the planner automatically appends a default ceiling of 100,000 rows to the generated SQL. This limit is documented in the CLI usage text (src/cli/cli.c, lines 590–595) and enforced via WHERE rowid < 100000 predicates in the final query.

SQL Injection Prevention

All generated SQL passes through a custom SQLite authoriser registered in cbm_db_open (line 639). This authoriser rejects any attempt to execute ATTACH, DROP, or other schema-modifying statements, ensuring that even syntactically valid Cypher cannot be exploited to escape the intended query shape.

Query Examples

Execute these via the query_graph tool handled by handle_query_graph in src/mcp/mcp.c (line 2912):


# Simple open query without LIMIT (automatically capped at 100k rows)

codebase-memory-mcp query_graph '{"project":"demo","query":"MATCH (n) RETURN n"}'

# Variable-length hop with unbounded depth (capped at 100k rows)

codebase-memory-mcp query_graph '{"project":"demo","query":"MATCH (a)-[:CALL*]->(b) RETURN a,b"}'

# Explicit LIMIT to override the default ceiling

codebase-memory-mcp query_graph '{"project":"demo","query":"MATCH (n) RETURN n LIMIT 50"}'

Summary

  • The Cypher query engine compiles OpenCypher into SQLite SQL through lexing, parsing, AST translation, and execution stages.
  • The lexer and parser in src/cypher/cypher.c handle identifiers, literals, patterns, and expressions while rejecting CREATE, MERGE, and CALL.
  • The planner generates parameterised joins on node and edge tables, encoding labels as IN predicates and optional matches as LEFT OUTER JOINs.
  • Safety ceilings of 100,000 rows protect against unbounded variable-length hops (*) and missing LIMIT clauses.
  • A custom SQLite authoriser in src/store/store.c prevents SQL injection by blocking ATTACH, DROP, and other dangerous statements.

Frequently Asked Questions

What happens if I use CREATE or MERGE in a query?

The parser explicitly rejects write operations. When CREATE, MERGE, DELETE, or SET clauses are detected, the unsupported_clause_error function (lines 813–838 in src/cypher/cypher.c) returns an error string immediately, preventing the query from reaching the SQL execution layer.

How does the engine handle infinite-length path queries like [:REL*]?

The parse_hop_range function recognises the unbounded * operator and passes it to the planner, which generates a recursive join pattern. However, the execution layer enforces a hard limit of 100,000 rows to prevent resource exhaustion, effectively capping the traversal depth regardless of the open-ended syntax.

Can I perform SQL injection through the Cypher query interface?

No. Even if the Cypher parser accepts unusual syntax, all generated SQL executes through SQLite with a custom authoriser installed in cbm_db_open (line 639 of src/store/store.c). This authoriser blocks ATTACH, DROP, ALTER, and other schema-modifying commands, ensuring the query remains confined to read-only operations on the knowledge graph tables.

Where is the default row limit of 100,000 defined?

The default ceiling is hard-coded in the query planner and documented in the CLI help text located in src/cli/cli.c (lines 590–595). When no LIMIT clause is present, the planner injects WHERE rowid < 100000 into the generated SQL to protect the server from runaway queries.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →