A persistent code graph for Laravel projects. Indexes your codebase into a local SQLite graph and exposes it to AI assistants over MCP, so they get token-efficient, structurally accurate context instead of re-reading files.
kha333n/laravel-code-graph is a Laravel package for a persistent code graph for laravel projects. indexes your codebase into a local sqlite graph and exposes it to ai assistants over mcp, so they get token-efficient, structurally accurate context instead of re-reading files..
It currently has 0 GitHub stars and 403 downloads on Packagist (latest version v0.1.2).
Install it with composer require kha333n/laravel-code-graph.
Discover more Laravel packages by kha333n
or browse all Laravel packages to compare alternatives.
Last updated
A persistent, structural map of your Laravel codebase, exposed to AI assistants over MCP — so they get token-efficient, accurate context instead of re-reading whole files.
If it works, don't touch it ✨When you ask an AI assistant (Claude Code, Cursor, Windsurf, Zed, Continue) to make a change in a Laravel project, it typically does this on every turn:
grep for the symbol you mentionedRead whole files to understand the contextRead more whole files to find the callersRead the tests for those filesThat's a lot of tokens spent re-discovering structure that doesn't change between turns. On a real Laravel codebase it adds up to half or more of every conversation.
Laravel Code Graph parses your project once into a local SQLite graph, keeps it up to date incrementally on every file change, and exposes it to AI assistants via the Model Context Protocol. Instead of grepping and reading whole files, the assistant asks the graph "where is ScoringService::score?" and gets back:
• [method] App\Services\Matching\ScoringService::score
app/Services/Matching/ScoringService.php:42-58
public function score(Profile $a, Profile $b): float
Then it can Read exactly those 16 lines instead of the whole 800-line file.
A single SQLite file in .code-graph/index.sqlite containing:
Symbols
Edges (directed relationships)
calls — method, static, function callsinstantiates — new Foo(...) expressionsdepends_on — constructor parameter type hints (DI graph)extends / implements / uses_trait — class hierarchieshas_relation — Eloquent hasMany/belongsTo/morphMany/etc, with relation type metadatabinds — service container bindings inside service providersas_action — classes composing the lorisleiva/laravel-actions AsAction traitroute_handler — routes/*.php → controller@method, with HTTP method + URIfilament_model — Filament Resource → Modelmigrates_table — migrations → table pseudo-FQNsblade_includes — Blade @include, <x-foo> components, etc.livewire_renders — <livewire:foo> and @livewire('foo') referencesChunks
Requires PHP 8.3+, Laravel 11 / 12 / 13, and the pdo_sqlite extension.
composer require kha333n/laravel-code-graph --dev
The package auto-discovers via Laravel's package discovery — no service provider registration needed.
# Publish config, run the first build, and register the MCP server
php artisan code-graph:install
That's it. The install command:
config/code-graph.php to your projectcode-graph in your project's .mcp.json so AI clients can connect (auto-detects Windows + WSL vs native)After the install you'll see something like:
Files scanned .......................................... 412
Changed (re-parsed) .................................... 412
Skipped (unchanged) ...................................... 0
Symbols emitted ........................................ 2847
Edges emitted .......................................... 5912
Chunks emitted ......................................... 2114
Duration .............................................. 1.84s
The index lives at .code-graph/index.sqlite — add .code-graph/ to your .gitignore.
Subsequent runs only re-parse files that actually changed:
Files scanned .......................................... 412
Changed (re-parsed) ...................................... 3
Skipped (unchanged) .................................... 409
Duration .............................................. 0.01s
code-graph:install does this automatically — it writes a code-graph entry into your project's .mcp.json (or creates the file if it doesn't exist) and auto-detects whether you're on a native Linux/macOS install or inside WSL on Windows. The resulting entry looks like one of:
// native Linux / macOS
{
"mcpServers": {
"code-graph": {
"command": "/usr/bin/php",
"args": ["/abs/path/to/artisan", "mcp:start", "code-graph"]
}
}
}
// Windows + WSL
{
"mcpServers": {
"code-graph": {
"command": "wsl.exe",
"args": ["/usr/bin/php8.3", "/home/.../artisan", "mcp:start", "code-graph"]
}
}
}
After installing, restart your AI assistant (Claude Code, Cursor, Zed, ...) so it picks up the new MCP server. Verify with the MCP Inspector if needed:
php artisan mcp:inspector code-graph
If you need to register or re-register the MCP server later (or remove it):
php artisan code-graph:install-mcp # add or update the entry
php artisan code-graph:install-mcp --force-wsl # force the WSL command pattern
php artisan code-graph:install-mcp --no-wsl # force the native pattern
php artisan code-graph:install-mcp --remove # take it out
Existing entries for other servers (like laravel-boost) are preserved.
The graph auto-refreshes on every tool call (mtime fast path keeps it under a few milliseconds), so the assistant always sees fresh data even right after you save a file.
Every tool returns exact file:start-end line ranges so the calling assistant can Read only the slice it needs.
find-symbol-toolLook up a symbol by name or fully qualified name. Falls back to FTS5 prefix matching if there's no exact hit. By default, also returns the file's use statements so you can author a new file from the same template without an extra Read.
{ "name": "ScoringService" }
{ "name": "score", "kind": "method" }
{ "name": "App\\Services\\Matching\\ScoringService" }
{ "name": "migration::create_profiles_table" }
{ "name": "config::profile_visibility" }
{ "name": "Profile", "with_imports": false } // skip imports if you don't need them
search-code-toolKeyword and intent search across symbol-level chunks (method bodies, small classes, Blade views) ranked by BM25. Use this when you don't know the exact name but you know what you're looking for.
{ "query": "rate limiting middleware" }
{ "query": "send notification" }
{ "query": "Eloquent profile model save" }
get-callers-toolFind every callsite that calls a function, method, or constructor. Pass a class FQN to get callers of any method on that class, or a method FQN for the exact callers.
{ "fqn": "App\\Services\\Matching\\ScoringService::score" }
{ "fqn": "App\\Models\\Profile" }
{ "fqn": "?::save" } // any method named "save"
Method calls on dynamic receivers (when the static type isn't resolvable) are recorded with a wildcard receiver ?::methodName.
get-callees-toolThe inverse: what does this function/method/class call?
{ "fqn": "App\\Actions\\MatchProfiles::handle" }
{ "fqn": "App\\Services\\Entitlements\\EntitlementService" }
get-blast-radius-toolRecursive BFS over the call graph going backwards from a root symbol — every caller, every caller's caller, up to the requested depth. Use this before changing a method to see what might break.
{ "fqn": "App\\Models\\Profile::save", "depth": 3 }
Results are depth-stratified so you can see which symbols are most directly affected.
get-relations-toolList Eloquent relations (hasMany, belongsTo, morphMany, etc.) declared on a Model. Returns the relation type, the related model FQN, and the file/line where each relation lives.
{ "model": "App\\Models\\Profile" }
{ "model": "App\\Models\\Profile::user" } // single relation
get-implementations-toolFind every concrete class that extends, implements, or uses a given class, interface, or trait. The inverse of get-callers for inheritance.
{ "fqn": "App\\Contracts\\Scorer" }
{ "fqn": "Lorisleiva\\Actions\\Concerns\\AsAction" }
report-issue-tool (opt-in)Lets the AI assistant log a case where one of the other tools gave a wrong, missing, incomplete, or misleading result. The package writes the entry to a local markdown file (.code-graph/feedback.md by default). Disabled by default — enable it in config/code-graph.php if you want to gather a feedback log over time. Local-only; nothing is sent over the network.
{
"tool": "find-symbol-tool",
"query": "ScoringService",
"issue": "not_found",
"expected": "A class symbol",
"workaround": "grep + Read",
"notes": "Probably indexed but under a different namespace."
}
| Command | Description |
|---|---|
| php artisan code-graph:install | Full setup: publish config, run the first build, and register the MCP server in .mcp.json. Pass --force to overwrite an existing config or --skip-mcp to keep .mcp.json untouched. |
| php artisan code-graph:install-mcp | Just the .mcp.json step — register, update, or --remove the code-graph server entry. Auto-detects Windows/WSL vs native. |
| php artisan code-graph:build | Re-index the project. Pass --fresh to nuke the existing graph and rebuild from scratch. |
| php artisan code-graph:watch | Long-running incremental indexer for active development. Polls on a 1-second interval; the mtime+size fast path keeps each tick negligible. |
| php artisan code-graph:wire-claude-md | Insert (or update) an instructions block in your project's CLAUDE.md so AI assistants know to prefer code-graph tools over grep/Read. Idempotent. Pass --remove to take it out again. |
| php artisan mcp:start code-graph | Start the local MCP server. Used by AI clients via stdio — you don't normally run this by hand. |
| php artisan mcp:inspector code-graph | Launch the MCP Inspector for interactive testing of the tools. |
The published config lives at config/code-graph.php:
return [
// Where the SQLite graph file lives.
'database' => env('CODE_GRAPH_DB', base_path('.code-graph/index.sqlite')),
// Directories to scan, relative to base_path().
'scan_paths' => [
'app',
'config',
'database',
'routes',
'resources/views',
'tests',
],
// Substrings of paths to exclude.
'exclude' => [
'vendor',
'storage',
'bootstrap/cache',
'node_modules',
'.code-graph',
],
// File extensions to index.
'extensions' => ['php'],
// Optional feedback / analytics layer. Disabled by default. When
// enabled, the report-issue-tool MCP tool becomes functional and
// appends entries to the path below. Local-only, never sent over
// the network.
'feedback' => [
'enabled' => env('CODE_GRAPH_FEEDBACK', false),
'path' => env('CODE_GRAPH_FEEDBACK_PATH', base_path('.code-graph/feedback.md')),
],
];
You can override the database location with the CODE_GRAPH_DB env var if you want the index somewhere other than .code-graph/. Set CODE_GRAPH_FEEDBACK=true to enable the feedback log.
scan_paths with Symfony Finder.nikic/php-parser and run a pipeline of small extractors over a single AST traversal:
SymbolVisitor — class/interface/trait/enum/method/function nodesInheritanceEdgeVisitor — extends, implements, uses_traitCallEdgeVisitor — calls, instantiations, constructor DIEloquentRelationVisitor — hasMany/belongsTo/etc.ServiceProviderBindingVisitor — bind/singleton/scoped/instanceAsActionRewriter — detects the AsAction trait and rewrites Foo::run/dispatch/... calls into edges targeting Foo::handleFilamentResourceVisitor — protected static $model = X::classRouteVisitor — Route::get/post/... in routes/*.phpMigrationVisitor — Schema::create/table in database/migrations/ChunkExtractor — slices symbol bodies for full-text search.blade.php) are handled by a separate extractor that uses regex over the raw text — Blade isn't valid PHP, so nikic's parser doesn't apply.(file row, symbols, edges, chunks) to SQLite atomically per file.resolveEdges() pass that joins source/destination FQNs against symbols.fqn to fill in numeric symbol IDs — this lets a visitor in file A emit an edge to a symbol that lives in file B without waiting for B to be parsed.Five tables in a single .code-graph/index.sqlite file:
| Table | Purpose |
|---|---|
| files | One row per indexed file with sha256 + size + mtime, for incremental change detection |
| symbols | Class/interface/trait/enum/method/function nodes |
| edges | Directed relationships between symbols, with optional resolved symbol IDs |
| chunks | Symbol bodies for the search layer |
| symbols_fts / chunks_fts | FTS5 mirrors for fast text search |
The package guarantees that AI tools always see fresh data:
code-graph:watch command for active development if you want the indexer running continuously in a separate terminal.Read of a 500-line file costs ~3000 tokens. A find-symbol call returning a 6-line summary with file:line ranges costs ~50.search-code-tool returns whole, callable units instead of arbitrary line windows — no more "you read lines 100-200 but the function actually starts at line 95".If you use Laravel Boost (the official Laravel MCP server), the two are complementary, not overlapping:
| Layer | Source of truth | Tool |
|---|---|---|
| Versioned package docs (Laravel, Livewire, Filament, Pest, Spatie...) | composer.lock | Boost search-docs |
| Runtime (DB schema, last error, logs, browser logs) | Live app | Boost database-*, last-error, browser-logs |
| Code structure (symbols, edges, blast radius, callers, search) | Your codebase | laravel-code-graph |
Boost owns docs and runtime. laravel-code-graph owns structure. Run both side-by-side in your .mcp.json and your AI assistant gets the full picture.
Issues and pull requests are welcome on GitHub. Before opening a PR, please open an issue first to discuss the change — especially if you're touching the storage schema or the MCP tool surface.
To work on the package locally:
git clone https://github.com/kha333n/laravel-code-graph.git
cd laravel-code-graph
composer install
vendor/bin/pest
The test suite uses orchestra/testbench to boot a minimal Laravel app and exercises the indexer end-to-end against fixture files in a temp directory.
MIT. See LICENSE.