How DeusData/codebase-memory-mcp Implements Hybrid LSP Semantic Type Resolution

The codebase-memory-mcp project implements Hybrid LSP semantic type resolution as a lightweight C-based engine that mimics modern language servers (tsserver, pyright, rust-analyzer) without requiring external processes, enabling static analysis of cross-file imports and call graphs across nine language families.

The codebase-memory-mcp repository builds a persistent knowledge graph of codebases by combining tree-sitter parsing with a second pass called Hybrid LSP. This pass runs a native C implementation of type-resolution algorithms immediately after AST generation, producing IDE-grade semantic information that gets stored directly in an SQLite-backed graph.

The Six-Phase Hybrid LSP Pipeline

Each language implementation in internal/cbm/lsp/ follows a standardized six-phase workflow. While specific syntax varies by language, the core pattern remains consistent across all nine supported families.

Phase 1: Import Binding

The py_lsp_bind_imports function in internal/cbm/lsp/py_lsp.c collects all import statements and populates a scoped name-to-type map. This handles both import X and from X import Y patterns in Python, creating the initial symbol table for resolution.

Phase 2: Statement Processing

Top-level statements—including assignments, for loops, with blocks, and class or function definitions—are walked to register variables, fields, and methods in the current scope. In Python, this is implemented by py_process_statement.

Phase 3: Expression Type Evaluation

Individual expressions are evaluated to determine their static types. The py_eval_expr_type function handles literals, binary operations, and function calls at the expression level.

Phase 4: Attribute Lookup

Attribute chains (e.g., obj.attr.method) are resolved using language-specific rules like Python's Method Resolution Order (MRO), falling back to the type registry for built-ins. This is implemented by py_lookup_attribute.

Phase 5: Call Resolution

The system recursively walks the AST, emitting CBMResolvedCall structures for every callable expression. The py_resolve_calls_in function produces fully-qualified callee names with confidence scores, representing the final resolved call edges.

Phase 6: Cross-File Linking

After processing all files, the import maps are used to resolve re-exports, wildcard imports, and intra-package references (Phase 9 in the source), completing the cross-file dependency graph.

Core Data Structures

The Hybrid LSP implementation relies on three shared abstractions defined in internal/cbm/lsp/type_registry.c and related headers:

  • CBMScope: A hierarchical symbol table managed by cbm_scope_* functions that maintains name-to-type bindings across nested scopes.
  • CBMTypeRegistry: Pre-computed tables of standard-library types for each language, stored in generated files under internal/cbm/lsp/generated/..._stdlib_data.c.
  • CBMResolvedCallArray: A dynamic array of resolved call edges that ultimately become RESOLVED_CALLS relationships in the knowledge graph.

Language-Specific Implementations

Each supported language maintains its own implementation file in internal/cbm/lsp/, following the six-phase pattern but adapting to language-specific semantics:

  • py_lsp.c: Python resolution with MRO support and import handling
  • ts_lsp.c: TypeScript/JavaScript with generic substitution and JSX component dispatch
  • go_lsp.c: Go package imports and method set resolution
  • rust_lsp.c: Rust trait method resolution and UFCS (Universal Function Call Syntax)

All implementations generate data compatible with the shared CBMResolvedCall format, ensuring consistent graph representation across languages.

Practical Implementation Examples

Indexing a Single Python File

The following C code demonstrates initializing the Hybrid LSP context for Python and resolving calls:

/* Allocate an arena for temporary allocations */
CBMArena *arena = cbm_arena_new();

/* Load source text (normally read from a file) */
const char *src = "... Python source ...";
int src_len = strlen(src);

/* Registry contains stdlib types for Python (generated data) */
extern const CBMTypeRegistry python_registry;
CBMResolvedCallArray calls = {0};

/* Initialise the LSP context */
PyLSPContext ctx;
py_lsp_init(&ctx, arena, src, src_len,
            &python_registry,
            "__main__", &calls);

/* Register imports that were previously extracted by the tree‑sitter pass */
py_lsp_add_import(&ctx, "os", "os");
py_lsp_add_import(&ctx, "np", "numpy");

/* Bind imports into the scope */
py_lsp_bind_imports(&ctx);

/* Walk the AST and emit resolved calls */
TSNode root = ts_tree_root_node(ctx.tree);
py_resolve_calls_in(&ctx, root);

/* `calls` now contains fully‑qualified callee names and confidence scores */
for (size_t i = 0; i < calls.len; ++i) {
    printf("call %zu: %s (conf %.2f)\n",
           i, calls.items[i].callee_qn, calls.items[i].confidence);
}

Querying the Persisted Graph

Once indexed, resolved calls are stored as RESOLVED_CALLS edges in the SQLite graph:


# After indexing the whole repository:

codebase-memory-mcp query_graph '
MATCH (f:Function)-[:RESOLVED_CALLS]->(g:Function)
WHERE f.file = "src/main.py"
RETURN f.name, g.name, g.module
' | jq .

This returns the same call edges emitted during the Hybrid LSP pass, now queryable without a live language server.

Adding TypeScript Support

New languages follow the same structural pattern. The TypeScript entry points in internal/cbm/lsp/ts_lsp.c mirror the Python API:

void ts_lsp_init(TsLSPContext *ctx, …);
void ts_lsp_add_import(TsLSPContext *ctx, const char *local, const char *module_qn);
void ts_lsp_bind_imports(TsLSPContext *ctx);
void ts_resolve_calls_in(TsLSPContext *ctx, TSNode node);

This consistency ensures that extending support to additional languages requires only implementing language-specific rules (e.g., generic substitution for TypeScript, trait dispatch for Rust) while reusing the shared infrastructure.

Summary

  • Hybrid LSP semantic type resolution in codebase-memory-mcp combines tree-sitter ASTs with a native C type engine to resolve symbols without external LSP processes.
  • The implementation follows a six-phase pipeline: import binding, statement processing, expression evaluation, attribute lookup, call resolution, and cross-file linking.
  • Core data structures (CBMScope, CBMTypeRegistry, CBMResolvedCallArray) are shared across nine language implementations located in internal/cbm/lsp/.
  • Resolved calls are persisted as graph edges (RESOLVED_CALLS), enabling fast structural queries for AI coding agents.
  • The system supports Python, TypeScript/JavaScript/JSX/TSX, PHP, C#, Go, C/C++, Java, Kotlin, and Rust with language-specific semantics handled in dedicated source files.

Frequently Asked Questions

What is the difference between Hybrid LSP and a standard language server?

Standard language servers like pyright or tsserver run as persistent processes that analyze code on demand. Hybrid LSP semantic type resolution implements similar algorithms in a lightweight C library that runs once during indexing, producing static data stored in SQLite. This eliminates runtime dependencies on external LSP processes while maintaining IDE-grade accuracy for import resolution and type inference.

How does the system handle complex Python imports like from package import *?

The import binding phase in py_lsp_bind_imports handles both explicit imports and wildcard patterns. During the cross-file linking phase (Phase 6), the system resolves wildcard imports and re-exports by consulting the pre-computed import maps from all indexed files, ensuring that symbols imported via wildcard are correctly linked to their origins in the knowledge graph.

Can the Hybrid LSP resolve generic types in TypeScript?

Yes. The TypeScript implementation in internal/cbm/lsp/ts_lsp.c includes specific logic for generic substitution and JSX component dispatch. When resolving expression types, the system substitutes concrete types for generic parameters, allowing accurate resolution of calls like Array<string>.map by tracking the type arguments through the scope chain.

Where are the standard library type definitions stored?

Standard library types for each language are stored in auto-generated C files under internal/cbm/lsp/generated/. For example, generated/python_stdlib_data.c contains pre-computed tables of Python built-in types. These are loaded into CBMTypeRegistry structures at runtime to provide fallback type information during attribute lookup and call resolution.

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 →