mcp-artisan

mcp-artisan

Provides read-only, structurally safe introspection of a Laravel codebase, letting AI agents inspect routes, models, migrations, jobs, and config without risk of mutation or secret disclosure.

Category
Visit Server

README

mcp-artisan

Point an AI agent at a Laravel codebase and ask it to explain the routes, the data model, the queued jobs, or a config value — without giving it any way to change the code, run a console command, or read a secret. mcp-artisan is a Model Context Protocol server that exposes a Laravel project to an agent read-only. The interesting part is not the list of things it can report; it is that the things it cannot do are enforced structurally, not by convention.

The server runs over stdio, speaks MCP, and needs no framework installed and no LLM API key. It works against a project whether or not PHP is present on the machine.

The problem

An agent is genuinely useful on a large PHP application: "which routes are unauthenticated?", "what does the orders table look like?", "which jobs run on the payments queue?". But the moment you hand a general-purpose agent a shell and a project directory to answer those questions, you have also handed it the ability to run php artisan migrate:fresh, read .env, or edit a controller. On a production or legacy codebase that trade is unacceptable, so the questions go unanswered or a human does the archaeology by hand.

The useful shape is a tool that can only look. mcp-artisan gives an agent a faithful, bounded map of the application and makes mutation impossible by construction — there is no write path and no arbitrary-command path to abuse.

Threat model

The assumed adversary is the agent itself: a capable, possibly-confused, or prompt-injected LLM that will call any tool it is offered with any argument it can construct. The design goal is that no sequence of tool calls can change the project, exfiltrate a secret, or read a file outside the project root.

What that means in practice, and where each guarantee lives:

  • No mutation. The server exposes no tool that writes. The static backend only ever opens files for reading; the artisan backend only ever runs verbs from a frozen allowlist (route:list, migrate:status, about), all of which are read-only. The allowlist is checked before a subprocess is created (mcp_artisan/artisan_backend.py), so a request for migrate or tinker never reaches php.
  • No arbitrary commands. Artisan verbs are constants in the code, never built from tool input. The subprocess is invoked with a fixed argument vector (no shell), so there is nothing to inject into, and each call has a hard wall-clock timeout.
  • No secret disclosure. .env is never read. Config values come from config/*.php, and every value is passed through key-based redaction (mcp_artisan/redaction.py): a key matching password, secret, token, key, dsn, and similar returns <redacted>, including nested keys inside a returned subtree. Redaction is deliberately broad — it prefers hiding a harmless value to leaking a credential.
  • No path escape. Every filesystem read is funnelled through one containment check (mcp_artisan/paths.py) that resolves the real path and rejects anything outside the configured project root, including via a crafted config key or a symlink that points out of the tree.

Every one of these is covered by a negative test; see tests/test_paths.py, tests/test_config_redaction.py, tests/test_artisan_backend.py, and tests/test_server_protocol.py.

What is explicitly out of scope: this is not a sandbox for the agent's other tools, and it does not defend against a compromised host or a malicious PHP runtime. It hardens the one surface it owns — introspection of the codebase.

Tools and resources

Tools (all read-only):

Tool Returns
project_summary Counts and versions, in one call, for cheap orientation.
list_routes Every route: methods, URI, name, middleware, controller@action.
describe_route(name_or_uri) One route by name or URI.
list_migrations Migration files on disk, plus applied/pending state when a database is reachable.
list_models Eloquent models: table, $fillable, $casts, relations.
list_jobs_and_queues Queued job classes with $queue, $tries, $timeout.
config_get(key) Dotted config lookup with mandatory secret redaction.
composer_deps Direct requires with versions, plus Laravel and PHP versions.

Resources: laravel://routes, laravel://schema, laravel://composer — the same data as JSON documents, for clients that prefer resources to tool calls.

Two backends, auto-detected

  1. static (default, and the only one used in CI). Pure parsing of composer.json, routes/*.php, database/migrations/*.php, app/Models/*.php, app/Jobs/*.php, and config/*.php. Requires no PHP.
  2. artisan. Used only when php is on PATH and php artisan about succeeds. Routes and migration status then come from route:list --json and migrate:status, which are exact where the static parser is best-effort. Models, jobs, config, and composer data stay static even here — no read-only artisan verb exposes them.

The backend is chosen once at startup and reported in every response so an agent knows how much to trust the data. Set MCP_ARTISAN_STATIC_ONLY=1 to force the static backend even when PHP is available.

Usage

Install and run against a project:

pip install -e .
MCP_ARTISAN_PROJECT=/path/to/laravel-app python -m mcp_artisan

Or register it with an MCP client (the entry point is the mcp-artisan console script):

{
  "mcpServers": {
    "artisan": {
      "command": "mcp-artisan",
      "env": { "MCP_ARTISAN_PROJECT": "/path/to/laravel-app" }
    }
  }
}

The project root defaults to the current working directory if MCP_ARTISAN_PROJECT is unset.

Reproducing the numbers

This repository ships a small hand-built fixture Laravel app under tests/fixtures/laravel-app (no framework was downloaded). Every count quoted below is produced by a command here, not asserted in prose.

python scripts/demo.py

prints, for the fixture: 8 tools exposed, and a project_summary of 6 routes, 2 models, 2 migrations, 2 jobs, 3 direct dependencies, on the static backend — followed by a redacted database password to show the masking.

The artisan allowlist is three verbs:

python -c "from mcp_artisan.artisan_backend import ALLOWED_VERBS; print(sorted(ALLOWED_VERBS))"
# ['about', 'migrate:status', 'route:list']

The full test suite (unit tests for both backends, plus protocol-level tests that drive the server through an in-memory MCP client) runs with:

pip install -e ".[dev]"
pytest

CI runs exactly this on every push (.github/workflows/ci.yml), with no PHP installed and no secrets.

Design notes

Choices made in the interest of a smaller, safer, more predictable server. Where a fuller behaviour was possible, the simpler option was taken and recorded here.

  • The static route parser does not resolve Route::group prefixes/middleware, Route::resource expansion, or closures' internals. It parses top-level Route::<verb> statements and their chained ->name()/->middleware(). This is a best-effort map; where exactness matters, the artisan backend provides the real route table. Closures are reported with an action of Closure.
  • The static backend cannot know which migrations have run — that needs a database connection. It lists the files and reports applied/pending as unknown rather than guessing. The artisan backend fills in real status.
  • The config parser understands a subset of PHP: scalars, arrays (both [...] and array(...)), env() (evaluated as its default argument only, since .env is never read), string concatenation, and Class::class. Anything it cannot evaluate — storage_path(), closures, other calls — resolves to null rather than raising. It never executes PHP.
  • Redaction is keyed on the config key, not the value, because the key is the trustworthy signal. It over-redacts by design: a false positive hides a value the agent could have seen; a false negative leaks a credential.
  • composer_deps reports the require block only, not require-dev, since the target is the application's runtime dependency surface.
  • Default model table names are derived with a small snake-case + pluralisation rule (Userusers, Categorycategories). It covers the common cases, not every English irregular; models with an explicit $table are always exact.
  • Jobs are read from app/Jobs only. Queueable classes elsewhere are not discovered by the static backend.
  • The backend is selected once at startup, not per call, so a project does not flip between backends mid-session.

Layout

mcp_artisan/
  paths.py            project-root resolution and the path-containment check
  redaction.py        key-based secret redaction
  phpparse.py         defensive parser for the PHP subset in config/routes/models
  static_backend.py   source-only parsers (the default backend)
  artisan_backend.py  guarded, allowlisted php-artisan subprocess backend
  service.py          backend selection and the per-tool responses
  server.py           the MCP tool/resource surface over stdio
tests/
  fixtures/laravel-app/   hand-built minimal Laravel app
  test_*.py               unit, protocol, and negative safety tests

Requirements

Python 3.12+. The only runtime dependency is the official mcp SDK. PHP is optional and only enables the artisan backend.

License

MIT.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured