定制 datomatic/laravel-database-mcp 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

datomatic/laravel-database-mcp

Composer 安装命令:

composer require datomatic/laravel-database-mcp

包简介

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

README 文档

README

Laravel Database MCP

Laravel Database MCP

CI

A read-only MCP 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:

Tool Purpose
describe_database Discover tables, columns, types and relationships
query_database Read rows with optional joins, filters, ordering, aggregations and pagination

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

Defence is layered. From outermost to innermost:

  1. Authentication + authorization. The route is protected by the configured middleware and an authorization gate (see 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

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

config/database-mcp.php:

Key Default Description
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 tables Tables never exposed
denied_columns secrets Columns stripped from every result

Set a project-specific name so the same package reused across projects stays distinguishable:

MCP_DATABASE_NAME="Acme Database"

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 for step-by-step setup of Sanctum or Passport (OAuth 2.1), for both new and existing applications.

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

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

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

Parameter Type Description
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)
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 string Column to sort by (in aggregate mode: an alias or a group_by column)
order_direction asc | desc Sort direction
limit integer Max rows for a non-paginated query (1 to max_limit, default 50)
page integer Page number (1-based); enables pagination
per_page integer Rows per page (1 to max_limit, default 50)
with_total boolean Include total/last_page (extra COUNT; ignored in aggregate mode)

A filter is { "column", "operator", "value" }. Operators: =, !=, >, >=, <, <=, like (the like value is wrapped in %…% automatically).

Simple query

{
  "table": "orders",
  "columns": ["code", "total"],
  "filters": [ { "column": "status", "operator": "=", "value": "completed" } ],
  "order_by": "created_at",
  "order_direction": "desc",
  "limit": 20
}

Query with a join — "orders with their user"

{
  "table": "orders",
  "columns": ["code", "total"],
  "joins": [
    { "table": "users", "on": "user_id", "columns": ["email", "firstname"] }
  ]
}

A join object accepts:

  • table (required) — related table; must share a foreign key with the base table.
  • on — the foreign key column to join on. Required only when the two tables are linked by more than one relationship.
  • typeleft (default) or inner.
  • columns — columns from the related table; omit for all allowed columns.

The join condition is derived automatically from the foreign key. When a join is present, result keys are prefixed with the table name so columns never collide (orders.code, users.email).

Disambiguating relationships

When two tables are linked by several foreign keys (e.g. orders.user_id and orders.created_by both point at users), joining without on returns an error listing the choices. Pass "on": "user_id" to pick the intended relationship.

Aggregations & grouping

Pass aggregates to compute SUM, COUNT, MIN, MAX or AVG. In aggregate mode the result is grouped: put the non-aggregated columns in group_by (not columns). Aggregate and group columns may reference the base table or any joined table (table.column).

{
  "table": "orders",
  "group_by": ["order_status"],
  "aggregates": [
    { "function": "SUM", "column": "total", "alias": "total_sum" },
    { "function": "COUNT", "column": "*", "alias": "orders_count" }
  ],
  "having": [ { "target": "total_sum", "operator": ">", "value": "1000" } ],
  "order_by": "total_sum",
  "order_direction": "desc"
}

An aggregate object accepts:

  • function (required) — SUM, COUNT, MIN, MAX or AVG (configurable via aggregate_functions).
  • column (required) — column to aggregate (table.column or a base column). Use * only with COUNT.
  • alias (required) — the result key (letters, digits, underscore).
  • distinct — aggregate distinct values, e.g. COUNT(DISTINCT column).

having filters the grouped rows; each target must be an aggregate alias or a group_by column, with the same operators as filters. Sample response:

{
  "table": "orders",
  "group_by": ["order_status"],
  "aggregates": ["total_sum", "orders_count"],
  "rows": [ { "order_status": "completed", "total_sum": 1234.5, "orders_count": 10 } ]
}

Pagination

Pass page (and optionally per_page) for page-based pagination. Add with_total to also get the total count and last page (an extra COUNT query; ignored in aggregate mode).

{
  "table": "orders",
  "columns": ["code", "total"],
  "order_by": "created_at",
  "order_direction": "desc",
  "page": 2,
  "per_page": 20,
  "with_total": true
}

The response carries a pagination block:

{ "pagination": { "page": 2, "per_page": 20, "total": 57, "last_page": 3 } }

Without pagination, use limit for a simple capped result.

Typical AI workflow

  1. describe_database (no arguments) → see available tables and relationships.
  2. describe_database with a table → see columns and foreign keys.
  3. query_database with the correct table, column and on names.

Testing

composer install
vendor/bin/pest

The suite runs against an in-memory SQLite database via Orchestra Testbench.

License

MIT.

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 0
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-07-01

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固