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.
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 formigrateortinkernever reachesphp. - 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.
.envis never read. Config values come fromconfig/*.php, and every value is passed through key-based redaction (mcp_artisan/redaction.py): a key matchingpassword,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
- 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, andconfig/*.php. Requires no PHP. - artisan. Used only when
phpis onPATHandphp artisan aboutsucceeds. Routes and migration status then come fromroute:list --jsonandmigrate: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::groupprefixes/middleware,Route::resourceexpansion, or closures' internals. It parses top-levelRoute::<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 ofClosure. - The static backend cannot know which migrations have run — that needs a
database connection. It lists the files and reports
applied/pendingas unknown rather than guessing. The artisan backend fills in real status. - The config parser understands a subset of PHP: scalars, arrays (both
[...]andarray(...)),env()(evaluated as its default argument only, since.envis never read), string concatenation, andClass::class. Anything it cannot evaluate —storage_path(), closures, other calls — resolves tonullrather 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_depsreports therequireblock only, notrequire-dev, since the target is the application's runtime dependency surface.- Default model table names are derived with a small snake-case +
pluralisation rule (
User→users,Category→categories). It covers the common cases, not every English irregular; models with an explicit$tableare always exact. - Jobs are read from
app/Jobsonly. 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
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
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.
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.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
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.
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.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
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.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.