DB MCP Demo
DB MCP Demo
Laravel ilovasining ma'lumotlar bazasini AI agentlarga faqat o'qish uchun ochib beruvchi custom MCP (Model Context Protocol) server.
Agent SELECT yozmaydi. U ruxsat berilgan jadvallar ro'yxatini so'raydi, ustunlar tuzilishini ko'radi va struktur filtrlar orqali qator o'qiydi. Raw SQL umuman qabul qilinmaydi.
Laravel 13 · PHP 8.4 ·
laravel/mcp0.9Ikki rejim: stdio (lokal) va HTTP (domain orqali, Sanctum token bilan)
Mundarija
Tez boshlash
composer install
cp .env.example .env
php artisan key:generate
touch database/database.sqlite
php artisan migrateLokal MCP klient uchun (Claude Code, Cursor va h.k.) loyiha ildizida .mcp.json:
{
"mcpServers": {
"database": {
"command": "php",
"args": ["artisan", "mcp:start", "database"],
"cwd": "/absolute/path/to/db-mcp-demo"
}
}
}Tekshirish:
php artisan mcp:inspector databaseArxitektura
MCP klient (Claude Code / Cursor / ChatGPT connector)
│
│ JSON-RPC 2.0
│
├── stdio ──────► php artisan mcp:start database
│
└── HTTPS POST ─► /mcp/database
auth:sanctum → ability:mcp:use → throttle:mcp
│
▼
DatabaseServer ── #[Instructions] agentga qanday ishlashni aytadi
│
├── list-tables ─┐
├── table-schema ─┼──► DatabaseCatalog ──► config/mcp-database.php
└── query-table ─┘ │ (allowlist, hidden, limit)
▼
Query Builder (bind qilingan)
▼
DatabaseKalit qoida: hech bir tool bazaga to'g'ridan-to'g'ri murojaat qilmaydi. Hammasi DatabaseCatalog orqali o'tadi, u esa config'dagi cheklovlarni majburlaydi.
Fayllar
Fayl | Vazifa |
| Server ro'yxatdan o'tkaziladi: |
| Tool'lar ro'yxati, nom, versiya, agent uchun yo'riqnoma |
|
|
|
|
|
|
| Yagona qo'riqchi: allowlist, yashirin ustunlar, limit |
| Barcha sozlamalar |
|
|
|
|
|
|
| Tool mantiqi testlari |
| HTTP auth testlari |
Sozlash
Hamma narsa config/mcp-database.php da.
return [
'connection' => env('MCP_DATABASE_CONNECTION'),
'tables' => [
'users',
],
'hidden' => [
'*' => ['password', 'remember_token'],
],
'default_limit' => 25,
'max_limit' => 200,
];Kalit | Ma'nosi |
| Qaysi DB ulanishidan o'qiladi. |
| Oq ro'yxat. Faqat shu jadvallar ko'rinadi. Ro'yxatda yo'q jadval AI uchun umuman mavjud emas |
| Har qanday javobdan o'chiriladigan ustunlar. |
| Klient |
| Klient qancha katta son so'rasa ham shu songa qirqiladi |
Misol — bir nechta jadval va jadvalga xos yashirin ustun:
'tables' => ['users', 'orders', 'products'],
'hidden' => [
'*' => ['password', 'remember_token'],
'users' => ['email'],
'orders' => ['card_last_four'],
],Tool'lar
list-tables
Parametrsiz. Ruxsat berilgan jadvallar, ularning qator soni va ko'rinadigan ustunlari.
{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"list-tables","arguments":{}}}{
"connection": "sqlite",
"tables": [
{
"table": "users",
"rows": 5,
"columns": ["id", "name", "email", "email_verified_at", "created_at", "updated_at"]
}
]
}password ro'yxatda yo'q — hidden uni olib tashlagan.
table-schema
Parametr | Turi | Majburiy |
| string | ha |
{"name":"table-schema","arguments":{"table":"users"}}Har bir ustun uchun name, type, nullable qaytadi, hamda yashirilgan ustunlar ro'yxati (hidden_columns) — agent nima ko'rmayotganini bilishi uchun.
query-table
Parametr | Turi | Izoh |
| string | majburiy |
| string[] | qaysi ustunlar. Standart — barcha ko'rinadigan ustunlar |
| object[] | AND bilan birlashadi, maksimum 10 ta |
| string | saralash ustuni |
|
| standart |
| integer |
|
Har bir filter: {"column": "...", "operator": "...", "value": ...}
Ruxsat berilgan operatorlar:
= != < <= > >= like not like in not in null not nullnullvanot null—valuekerak emasinvanot in—valuebo'sh bo'lmagan massiv bo'lishi kerak
Misol:
{
"name": "query-table",
"arguments": {
"table": "users",
"columns": ["id", "name", "email"],
"filters": [
{"column": "email", "operator": "like", "value": "%example.test"}
],
"order_by": "id",
"order_direction": "desc",
"limit": 10
}
}Javob:
{
"table": "users",
"columns": ["id", "name", "email"],
"limit": 10,
"returned": 1,
"total_matching": 1,
"rows": [
{"id": 1, "name": "Demo User", "email": "demo@example.test"}
]
}total_matching — filtrga mos barcha qatorlar soni, returned — limitdan keyin qaytganlari. Agent shu ikkisini taqqoslab, natija qirqilganini tushunadi.
Lokal rejim (stdio)
routes/ai.php:
Mcp::local('database', DatabaseServer::class);Klient jarayonni o'zi ishga tushiradi va stdin/stdout orqali gaplashadi. Auth yo'q — klient allaqachon sizning mashinangizda, sizning huquqlaringiz bilan ishlaydi.
Debug:
php artisan mcp:inspector database
php artisan mcp:start databaseni terminalda qo'lda ishga tushirmang — u stdin kutib osilib qoladi. Faqat klient yoki inspector uni chaqirsin.
Web rejim (domain orqali)
routes/ai.php:
Mcp::web('/mcp/database', DatabaseServer::class)
->middleware(['auth:sanctum', 'ability:mcp:use', 'throttle:mcp']);Uch qatlam:
Middleware | Nima qiladi |
|
|
| Token aynan shu ability'ga ega bo'lishi shart. Aks holda |
| 60 so'rov/daqiqa, foydalanuvchi ID yoki IP bo'yicha |
Token yaratish
php artisan mcp:token demo@example.test --name=laptop --expires=90 INFO Token created. It is shown only once, so copy it now.
1|JVCUiTgZhjV7PvuP42EFsUvNcqVQSorgpHRdEHBj276b3f16
User ....... demo@example.test
Abilities .. mcp:use
Expires .... 2026-12-06 04:39:32Flag | Standart | Izoh |
|
|
|
| yo'q | necha kundan keyin tugaydi. Berilmasa — muddatsiz |
Token faqat mcp:use ability oladi. Ilovaning boshqa API tokenlari bu endpoint'ga o'tolmaydi, va MCP tokeni o'g'irlansa boshqa API'ga kirolmaydi.
Bekor qilish:
php artisan tinker --execute 'App\Models\User::where("email","demo@example.test")->first()->tokens()->delete();'Klient konfiguratsiyasi
{
"mcpServers": {
"database": {
"type": "http",
"url": "https://domain.uz/mcp/database",
"headers": { "Authorization": "Bearer 1|xxxxxxxx" }
}
}
}Deploy tekshiruvi
HTTPS majburiy. Bearer token shifrlanmagan HTTP'da ochiq ketadi.
.envdaTRUSTED_PROXIESga real proxy IP'larini yozing. Bo'sh qoldirsangiz$request->ip()proxy IP'sini qaytaradi va rate limit barcha klientlar uchun umumiy bo'lib qoladi.*qiymati faqat lokal tunnel uchun.Nginx'da streaming (SSE) javoblar uchun:
location /mcp/ { proxy_buffering off; fastcgi_read_timeout 300; }config/mcp-database.phpdagitablesni prod bazasi uchun qayta ko'rib chiqing.php artisan config:cachevaroute:cacheishlaydi —routes/ai.phpavtomatik ro'yxatdan o'tadi, unibootstrap/app.phpga qo'shish shart emas.
ngrok bilan test
Prod'ga chiqarishdan oldin HTTP qatlamini lokal tekshirish.
# 1-terminal
php artisan serve
# 2-terminal
ngrok config add-authtoken SIZNING_TOKENINGIZ
ngrok http 8000.env da lokal test uchun:
TRUSTED_PROXIES=*Foydalanuvchi va token tayyorlang:
php artisan tinker --execute 'App\Models\User::factory()->create(["email" => "demo@example.test"]);'
php artisan mcp:token demo@example.test --expires=1Tekshirish:
URL='https://xxxx.ngrok-free.dev'
TOKEN='1|xxxxxxxx'
# tokensiz → 401 kutiladi
curl -s -o /dev/null -w '%{http_code}\n' -X POST "$URL/mcp/database" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# token bilan
curl -s -X POST "$URL/mcp/database" \
-H "Authorization: Bearer $TOKEN" \
-H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
-H 'ngrok-skip-browser-warning: true' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list-tables","arguments":{}}}'ngrok-skip-browser-warning — ngrok Free rejasining oraliq ogohlantirish sahifasini chetlab o'tish uchun. Haqiqiy domainda kerak emas.
Test tugagach tunnelni yoping va tokenni bekor qiling.
Xavfsizlik modeli
Uchta mustaqil qatlam. Biri buzilsa ham qolgani ushlab qoladi.
1. Raw SQL yo'q
query-table faqat struktur filtr qabul qiladi. operator — yopiq enum. Qiymatlar Query Builder orqali bind qilinadi.
"operator": "; drop table users --"
→ Supported operators are: =, !=, <, <=, >, >=, like, not like, in, not in, null, not null.Xato JSON Schema va validation bosqichida qaytadi — so'rov bazaga umuman yetib bormaydi.
2. Jadval oq ro'yxati
config/mcp-database.php dagi tables da yo'q jadval mavjud emasdek muomala qilinadi.
"table": "sessions"
→ Unknown table [sessions]. Available tables: users.sessions, jobs, password_reset_tokens, personal_access_tokens — hech qachon ko'rinmaydi.
3. Yashirin ustunlar
hidden dagi ustunlar list-tables, table-schema va query-table javoblaridan chiqarib tashlanadi. Agent ularni nomi bilan so'rasa ham:
"columns": ["email", "password"]
→ Unknown or hidden column [password] on table [users].
Available columns: id, name, email, email_verified_at, created_at, updated_at.Qo'shimcha
Barcha tool'lar
#[IsReadOnly]va#[IsIdempotent]bilan belgilangan — klient bularni foydalanuvchiga ko'rsatadi.max_limitbir so'rovda butun jadvalni so'rib olishning oldini oladi.Web rejimda token ability bilan cheklangan va rate limit qo'yilgan.
Nima qilinmagan
Hozir token egasi tables dagi barcha jadvalni ko'radi. Foydalanuvchiga qarab cheklash kerak bo'lsa, tool ichida:
if (! $request->user()->can('read-table', $table)) {
return Response::error('Permission denied.');
}yoki tool'ni butunlay yashirish uchun shouldRegister():
public function shouldRegister(Request $request): bool
{
return $request->user()?->isAdmin() ?? false;
}Testlar
php artisan testFayl | Nimani qoplaydi |
| Tool mantiqi: allowlist, yashirin ustun, filtr, limit qirqilishi, noto'g'ri operator |
| HTTP: |
Faqat bittasini ishga tushirish:
php artisan test --compact tests/Feature/DatabaseServerTest.php
php artisan test --filter='rejects a table that is not allowlisted'Kengaytirish
Yangi jadval ochish
config/mcp-database.php da bitta qator:
'tables' => ['users', 'orders'],Kod o'zgarmaydi. Nozik ustunlar bo'lsa hidden ga qo'shing.
Yangi tool qo'shish
php artisan make:mcp-tool OrderSummaryToolKeyin DatabaseServer::$tools ga qo'shing. Bazaga tegadigan tool DatabaseCatalog ni constructor orqali oling — allowlist va yashirin ustunlar avtomatik amal qiladi:
public function __construct(protected DatabaseCatalog $catalog) {}
public function handle(Request $request): Response|ResponseFactory
{
$rows = $this->catalog->query('orders')->where('status', 'paid')->get();
// ...
}DatabaseCatalog::query() ichida assertTable() chaqiriladi, ya'ni oq ro'yxatdan tashqaridagi jadval istisno tashlaydi.
Tuzoqlar
Bu loyihada allaqachon duch kelingan, .ai/rules/ ga ham yozib qo'yilgan.
1. Tool nomi -tool bilan tugab qoladi
#[Name] atributisiz nom Str::kebab(class_basename($this)) dan olinadi, ya'ni QueryTableTool → query-table-tool. Har doim aniq yozing:
#[Name('query-table')]
#[Title('Query Table')]
class QueryTableTool extends Tool2. Response::structured() Response emas
U ResponseFactory qaytaradi. handle(): Response deb yozsangiz TypeError chiqadi va klientga tool xatosi bo'lib ko'rinadi:
Return value must be of type Laravel\Mcp\Response, Laravel\Mcp\ResponseFactory returnedTo'g'ri turi:
public function handle(Request $request): Response|ResponseFactory3. ability middleware alias avtomatik kelmaydi
Laravel 13'da Sanctum uni ro'yxatdan o'tkazmaydi. bootstrap/app.php da qo'lda:
$middleware->alias([
'abilities' => CheckAbilities::class,
'ability' => CheckForAnyAbility::class,
]);4. throttle:mcp limiter'siz ishlamaydi
AppServiceProvider::boot() da e'lon qilinishi shart, aks holda so'rov paytida istisno tashlanadi.
5. mcp:start ni qo'lda ishga tushirmang
Stdin kutib osilib qoladi. Debug uchun mcp:inspector ishlating.