kha333n / laravel-code-graph
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.
Requires
- php: ^8.3
- ext-pdo_sqlite: *
- illuminate/console: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
- laravel/mcp: >=0.1 <1.0
- nikic/php-parser: ^5.0
- symfony/finder: ^7.0
Requires (Dev)
- larastan/larastan: ^3.0
- orchestra/testbench: ^9.0|^10.0
- pestphp/pest: ^3.0
This package is auto-updated.
Last update: 2026-04-08 12:57:20 UTC
README
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.
By the AI, For the AI
Only AI and God knows how it works, but it works.
As per the rule of If it works, don't touch it ✨
Why this exists
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:
grepfor the symbol you mentionedReadwhole files to understand the contextReadmore whole files to find the callersReadthe tests for those files- ...and only then start editing
That'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.
What it indexes
A single SQLite file in .code-graph/index.sqlite containing:
Symbols
- Classes, interfaces, traits, enums, methods, functions
- Each with its FQN, file path, exact line range, signature, and docblock
- Plus a symbol per Blade view file
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— EloquenthasMany/belongsTo/morphMany/etc, with relation type metadatabinds— service container bindings inside service providersas_action— classes composing thelorisleiva/laravel-actionsAsActiontraitroute_handler—routes/*.php→ controller@method, with HTTP method + URIfilament_model— FilamentResource→Modelmigrates_table— migrations → table pseudo-FQNsblade_includes— Blade@include,<x-foo>components, etc.livewire_renders—<livewire:foo>and@livewire('foo')references
Chunks
- Symbol-level source slices (one per method, function, small class, or Blade view)
- Mirrored into FTS5 for full-text search by intent or keyword
Installation
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.
Quickstart
# Publish config, run the first build, and register the MCP server
php artisan code-graph:install
That's it. The install command:
- Publishes
config/code-graph.phpto your project - Builds the initial graph
- Registers
code-graphin your project's.mcp.jsonso 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
Connecting to your AI assistant
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.
The MCP tools
Every tool returns exact file:start-end line ranges so the calling assistant can Read only the slice it needs.
find-symbol-tool
Look 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-tool
Keyword 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-tool
Find 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-tool
The inverse: what does this function/method/class call?
{ "fqn": "App\\Actions\\MatchProfiles::handle" }
{ "fqn": "App\\Services\\Entitlements\\EntitlementService" }
get-blast-radius-tool
Recursive 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-tool
List 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-tool
Find 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."
}
Commands
| 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. |
Configuration
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.
How it works
Indexing pipeline
- Walk the configured
scan_pathswith Symfony Finder. - For each file: mtime + size fast path — if both match the stored values, skip without reading.
- On a mismatch: read + SHA-256. If the hash matches, just update mtime/size and skip.
- Otherwise parse with
nikic/php-parserand 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 theAsActiontrait and rewritesFoo::run/dispatch/...calls into edges targetingFoo::handleFilamentResourceVisitor—protected static $model = X::classRouteVisitor—Route::get/post/...inroutes/*.phpMigrationVisitor—Schema::create/tableindatabase/migrations/ChunkExtractor— slices symbol bodies for full-text search
- Blade files (
.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. - Persist
(file row, symbols, edges, chunks)to SQLite atomically per file. - After every file is processed, run a single
resolveEdges()pass that joins source/destination FQNs againstsymbols.fqnto 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.
Storage
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 |
Freshness
The package guarantees that AI tools always see fresh data:
- Auto-refresh on every MCP call. Each tool runs an incremental index pass before answering. Thanks to the mtime + size fast path, the no-op case takes ~1ms even on hundreds of files.
- In-memory throttle. If a tool was called less than 250ms ago, the next call reuses the previous freshen — handles the case where an AI client batches several queries in one turn.
code-graph:watchcommand for active development if you want the indexer running continuously in a separate terminal.
Why this saves tokens
- A
Readof a 500-line file costs ~3000 tokens. Afind-symbolcall returning a 6-line summary with file:line ranges costs ~50. - For multi-step tasks (find symbol → find callers → check tests → make change), the graph collapses 4 file reads to 1 grep + 3 line-range reads.
- The chunked search layer means
search-code-toolreturns whole, callable units instead of arbitrary line windows — no more "you read lines 100-200 but the function actually starts at line 95". - Auto-refresh means the graph stays in sync without per-turn re-indexing prompts cluttering the conversation.
Working with Laravel Boost
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.
Contributing
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.
License
MIT. See LICENSE.