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.

Maintainers

Package info

github.com/kha333n/laravel-code-graph

pkg:composer/kha333n/laravel-code-graph

Statistics

Installs: 36

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.2 2026-04-08 12:55 UTC

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.

PHP Version Laravel License: MIT

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:

  1. grep for the symbol you mentioned
  2. Read whole files to understand the context
  3. Read more whole files to find the callers
  4. Read the tests for those files
  5. ...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 calls
  • instantiatesnew Foo(...) expressions
  • depends_on — constructor parameter type hints (DI graph)
  • extends / implements / uses_trait — class hierarchies
  • has_relation — Eloquent hasMany/belongsTo/morphMany/etc, with relation type metadata
  • binds — service container bindings inside service providers
  • as_action — classes composing the lorisleiva/laravel-actions AsAction trait
  • route_handlerroutes/*.php → controller@method, with HTTP method + URI
  • filament_model — Filament ResourceModel
  • migrates_table — migrations → table pseudo-FQNs
  • blade_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:

  1. Publishes config/code-graph.php to your project
  2. Builds the initial graph
  3. Registers code-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.sqliteadd .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

  1. Walk the configured scan_paths with Symfony Finder.
  2. For each file: mtime + size fast path — if both match the stored values, skip without reading.
  3. On a mismatch: read + SHA-256. If the hash matches, just update mtime/size and skip.
  4. Otherwise parse with nikic/php-parser and run a pipeline of small extractors over a single AST traversal:
    • SymbolVisitor — class/interface/trait/enum/method/function nodes
    • InheritanceEdgeVisitorextends, implements, uses_trait
    • CallEdgeVisitor — calls, instantiations, constructor DI
    • EloquentRelationVisitorhasMany/belongsTo/etc.
    • ServiceProviderBindingVisitorbind/singleton/scoped/instance
    • AsActionRewriter — detects the AsAction trait and rewrites Foo::run/dispatch/... calls into edges targeting Foo::handle
    • FilamentResourceVisitorprotected static $model = X::class
    • RouteVisitorRoute::get/post/... in routes/*.php
    • MigrationVisitorSchema::create/table in database/migrations/
    • ChunkExtractor — slices symbol bodies for full-text search
  5. 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.
  6. Persist (file row, symbols, edges, chunks) to SQLite atomically per file.
  7. After every file is processed, run a single 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.

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:watch command for active development if you want the indexer running continuously in a separate terminal.

Why this saves tokens

  • A Read of a 500-line file costs ~3000 tokens. A find-symbol call 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-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".
  • 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.