PHPackages                             datomatic/laravel-database-mcp - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. datomatic/laravel-database-mcp

ActiveLibrary[Database &amp; ORM](/categories/database)

datomatic/laravel-database-mcp
==============================

Read-only MCP server to let AI assistants safely explore and query a Laravel application's database.

v0.0.1(today)37↑2471.4%MITPHPPHP ^8.3CI passing

Since Jul 1Pushed today1 watchersCompare

[ Source](https://github.com/datomatic/laravel-database-mcp)[ Packagist](https://packagist.org/packages/datomatic/laravel-database-mcp)[ RSS](/packages/datomatic-laravel-database-mcp/feed)WikiDiscussions main Synced today

READMEChangelog (1)Dependencies (11)Versions (2)Used By (0)

    ![Laravel Database MCP](branding/logo-light.jpg)

Laravel Database MCP
====================

[](#laravel-database-mcp)

[![CI](https://github.com/datomatic/laravel-database-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/datomatic/laravel-database-mcp/actions/workflows/ci.yml)

A read-only [MCP](https://modelcontextprotocol.io) server that lets an AI assistant (Claude Code, Cursor, …) explore and query a Laravel application's database through safe, structured parameters — never raw SQL.

It exposes two tools:

ToolPurpose`describe_database`Discover tables, columns, types and relationships`query_database`Read rows with optional joins, filters, ordering, aggregations and paginationInstallation
------------

[](#installation)

```
composer require datomatic/laravel-database-mcp
```

The service provider is auto-discovered. Optionally publish the config:

```
php artisan vendor:publish --tag=database-mcp-config
```

Security model
--------------

[](#security-model)

Defence is layered. From outermost to innermost:

1. **Authentication + authorization.** The route is protected by the configured middleware and an authorization gate (see [Authorization](#authorization)).
2. **Read-only database connection.** All reads run through `config('database-mcp.connection')`. Point it at a database user with `SELECT`-only grants and the assistant physically cannot write — the only guarantee that does not rely on application logic.
3. **No raw SQL.** Tools accept structured parameters only; nothing is interpolated into SQL.
4. **Identifiers validated against the live schema.** Every table and column must exist and survive the deny lists, or the request is rejected before a query runs.
5. **Table deny list.** Auth tokens, sessions, jobs, cache and migrations are never exposed (configurable via `denied_tables`).
6. **Column deny list.** `password`, `remember_token`, `two_factor_*`, `api_token` are stripped from every result and description, even on a wildcard select (configurable via `denied_columns`).
7. **Row cap.** Results are limited (`max_limit`, default 100).

On MySQL the tools only expose tables belonging to the connection's own database, even if the user can see other schemas.

### Setting up a read-only database user

[](#setting-up-a-read-only-database-user)

```
CREATE USER 'app_readonly'@'%' IDENTIFIED BY 'a-strong-password';
GRANT SELECT ON your_database.* TO 'app_readonly'@'%';
FLUSH PRIVILEGES;
```

Define a dedicated connection in `config/database.php`:

```
'mysql_readonly' => [
    ...config('database.connections.mysql'),
    'username' => env('DB_READONLY_USERNAME'),
    'password' => env('DB_READONLY_PASSWORD'),
],
```

Then point the package at it:

```
DATABASE_MCP_CONNECTION=mysql_readonly
DB_READONLY_USERNAME=app_readonly
DB_READONLY_PASSWORD=a-strong-password
```

When `connection` is `null` the application's default connection is used — which is **not**read-only. Always configure the dedicated user in any shared or production environment.

Configuration
-------------

[](#configuration)

`config/database-mcp.php`:

KeyDefaultDescription`connection``env('DATABASE_MCP_CONNECTION')`Connection to read from (null = default)`register_route``true`Auto-register the HTTP route`path``database-mcp`URL path of the server`middleware``['auth:sanctum']`Middleware applied to the route`gate``access-database-mcp`Ability checked as `can:` middleware (null disables)`name``"{APP_NAME} Database"`Name advertised to MCP clients`instructions`(workflow text)Guidance the assistant reads on connect`max_limit``100`Maximum rows per query`denied_tables`auth/infra tablesTables never exposed`denied_columns`secretsColumns stripped from every resultSet a project-specific name so the same package reused across projects stays distinguishable:

```
MCP_DATABASE_NAME="Acme Database"
```

### Authentication guard

[](#authentication-guard)

The route is authenticated with **Laravel Sanctum** (`auth:sanctum`) by default. If your API uses a different guard — for example **Laravel Passport** (`auth:api`) — override `middleware` in your own `config/database-mcp.php`. Only the keys you set override the package defaults:

```
// config/database-mcp.php
return [
    'middleware' => ['auth:api'], // Passport guard
];
```

See the [authentication guide](docs/authentication.md) for step-by-step setup of Sanctum or Passport (OAuth 2.1), for both new and existing applications.

Authorization
-------------

[](#authorization)

The route is guarded by a gate named in `config('database-mcp.gate')` (default `access-database-mcp`), applied as `can:` middleware. Define it in your own service provider to decide who may access the server:

```
use Illuminate\Support\Facades\Gate;

Gate::define('access-database-mcp', fn ($user) => $user->isSuperAdmin());
```

If you never define the gate, the package falls back to allowing **local environments only**(everyone else gets a `403`). Set `gate` to `null` in the config to disable the check entirely.

Registration
------------

[](#registration)

By default the package registers the server over HTTP at `config('database-mcp.path')` with the configured middleware. To register it yourself, set `register_route` to `false` and add it to `routes/ai.php`:

```
use Datomatic\LaravelDatabaseMcp\Servers\DatabaseServer;
use Laravel\Mcp\Facades\Mcp;

Mcp::web('database-mcp', DatabaseServer::class)
    ->middleware(['auth:sanctum', 'can:access-database-mcp']);
```

Register it with your MCP client using a project-specific connector name:

```
claude mcp add acme-db --transport http https://acme.test/database-mcp
```

Usage
-----

[](#usage)

### `describe_database`

[](#describe_database)

Call with no arguments to list allowed tables and their outgoing foreign keys:

```
{
  "tables": [
    { "table": "orders", "references": [ { "column": "user_id", "references": "users.id" } ] }
  ]
}
```

Call with a `table` to get its columns and relationships in both directions:

```
{
  "table": "orders",
  "columns": [
    { "name": "id", "type": "bigint", "nullable": false, "default": null }
  ],
  "references":   [ { "column": "user_id", "references": "users.id" } ],
  "referenced_by": [ { "table": "order_product", "column": "order_id" } ]
}
```

Relationships come from the database foreign keys, not Eloquent — they reflect the actual constraints. Foreign keys pointing at denied tables are filtered out.

### `query_database`

[](#query_database)

ParameterTypeDescription`table`string (required)Base table`columns`string\[\]Columns to select; omit for all allowed columns. Not allowed with `aggregates``joins`object\[\]Related tables to join`aggregates`object\[\]Aggregate expressions (see [Aggregations](#aggregations--grouping))`group_by`string\[\]Columns to group by (`table.column` or a base column)`having`object\[\]Conditions on grouped results (only with `aggregates`)`filters`object\[\]`WHERE` conditions, ANDed together`order_by`stringColumn to sort by (in aggregate mode: an alias or a group\_by column)`order_direction``asc` | `desc`Sort direction`limit`integerMax rows for a non-paginated query (1 to `max_limit`, default 50)`page`integerPage number (1-based); enables pagination`per_page`integerRows per page (1 to `max_limit`, default 50)`with_total`booleanInclude `total`/`last_page` (extra COUNT; ignored in aggregate mode)A filter is `{ "column", "operator", "value" }`. Operators: `=`, `!=`, `>`, `>=`, `
