Skip to main content
Glama
azizdevfull

DB MCP Demo

by azizdevfull
README.md
# 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/mcp` 0.9
- Ikki rejim: stdio (lokal) va HTTP (domain orqali, Sanctum token bilan)

---

## Mundarija

1. [Tez boshlash](#tez-boshlash)
2. [Arxitektura](#arxitektura)
3. [Sozlash](#sozlash)
4. [Tool'lar](#toollar)
5. [Lokal rejim (stdio)](#lokal-rejim-stdio)
6. [Web rejim (domain orqali)](#web-rejim-domain-orqali)
7. [ngrok bilan test](#ngrok-bilan-test)
8. [Xavfsizlik modeli](#xavfsizlik-modeli)
9. [Testlar](#testlar)
10. [Kengaytirish](#kengaytirish)
11. [Tuzoqlar](#tuzoqlar)

---

## Tez boshlash

```bash
composer install
cp .env.example .env
php artisan key:generate
touch database/database.sqlite
php artisan migrate
```

Lokal MCP klient uchun (Claude Code, Cursor va h.k.) loyiha ildizida `.mcp.json`:

```json
{
  "mcpServers": {
    "database": {
      "command": "php",
      "args": ["artisan", "mcp:start", "database"],
      "cwd": "/absolute/path/to/db-mcp-demo"
    }
  }
}
```

Tekshirish:

```bash
php artisan mcp:inspector database
```

---

## Arxitektura

```
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)
                                      ▼
                                  Database
```

**Kalit 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 |
|---|---|
| `routes/ai.php` | Server ro'yxatdan o'tkaziladi: `Mcp::local()` va `Mcp::web()` |
| `app/Mcp/Servers/DatabaseServer.php` | Tool'lar ro'yxati, nom, versiya, agent uchun yo'riqnoma |
| `app/Mcp/Tools/ListTablesTool.php` | `list-tables` |
| `app/Mcp/Tools/TableSchemaTool.php` | `table-schema` |
| `app/Mcp/Tools/QueryTableTool.php` | `query-table` |
| `app/Mcp/Support/DatabaseCatalog.php` | Yagona qo'riqchi: allowlist, yashirin ustunlar, limit |
| `config/mcp-database.php` | Barcha sozlamalar |
| `app/Console/Commands/McpTokenCommand.php` | `php artisan mcp:token` |
| `app/Providers/AppServiceProvider.php` | `mcp` rate limiter |
| `bootstrap/app.php` | `ability` alias, trusted proxies |
| `tests/Feature/DatabaseServerTest.php` | Tool mantiqi testlari |
| `tests/Feature/DatabaseServerHttpTest.php` | HTTP auth testlari |

---

## Sozlash

Hamma narsa `config/mcp-database.php` da.

```php
return [
    'connection' => env('MCP_DATABASE_CONNECTION'),

    'tables' => [
        'users',
    ],

    'hidden' => [
        '*' => ['password', 'remember_token'],
    ],

    'default_limit' => 25,
    'max_limit' => 200,
];
```

| Kalit | Ma'nosi |
|---|---|
| `connection` | Qaysi DB ulanishidan o'qiladi. `null` → ilovaning standart ulanishi |
| `tables` | **Oq ro'yxat.** Faqat shu jadvallar ko'rinadi. Ro'yxatda yo'q jadval AI uchun umuman mavjud emas |
| `hidden` | Har qanday javobdan o'chiriladigan ustunlar. `'*'` → barcha jadvallar uchun. Jadval nomi kalit sifatida ham ishlaydi |
| `default_limit` | Klient `limit` bermasa nechta qator qaytadi |
| `max_limit` | Klient qancha katta son so'rasa ham shu songa qirqiladi |

Misol — bir nechta jadval va jadvalga xos yashirin ustun:

```php
'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.

```json
{"jsonrpc":"2.0","id":1,"method":"tools/call",
 "params":{"name":"list-tables","arguments":{}}}
```

```json
{
  "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 |
|---|---|---|
| `table` | string | ha |

```json
{"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 |
|---|---|---|
| `table` | string | majburiy |
| `columns` | string[] | qaysi ustunlar. Standart — barcha ko'rinadigan ustunlar |
| `filters` | object[] | AND bilan birlashadi, maksimum 10 ta |
| `order_by` | string | saralash ustuni |
| `order_direction` | `asc` \| `desc` | standart `asc` |
| `limit` | integer | `max_limit` gacha qirqiladi |

Har bir filter: `{"column": "...", "operator": "...", "value": ...}`

Ruxsat berilgan operatorlar:

```
=  !=  <  <=  >  >=  like  not like  in  not in  null  not null
```

- `null` va `not null` — `value` kerak emas
- `in` va `not in` — `value` bo'sh bo'lmagan massiv bo'lishi kerak

Misol:

```json
{
  "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:

```json
{
  "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`:

```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:

```bash
php artisan mcp:inspector database
```

> `php artisan mcp:start database` ni terminalda qo'lda ishga tushirmang — u stdin kutib osilib qoladi. Faqat klient yoki inspector uni chaqirsin.

---

## Web rejim (domain orqali)

`routes/ai.php`:

```php
Mcp::web('/mcp/database', DatabaseServer::class)
    ->middleware(['auth:sanctum', 'ability:mcp:use', 'throttle:mcp']);
```

Uch qatlam:

| Middleware | Nima qiladi |
|---|---|
| `auth:sanctum` | `Authorization: Bearer <token>` tekshiradi. Yo'q bo'lsa `401` |
| `ability:mcp:use` | Token aynan shu ability'ga ega bo'lishi shart. Aks holda `403` |
| `throttle:mcp` | 60 so'rov/daqiqa, foydalanuvchi ID yoki IP bo'yicha |

### Token yaratish

```bash
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:32
```

| Flag | Standart | Izoh |
|---|---|---|
| `--name=` | `mcp` | `personal_access_tokens` jadvalidagi yorliq |
| `--expires=` | 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:

```bash
php artisan tinker --execute 'App\Models\User::where("email","demo@example.test")->first()->tokens()->delete();'
```

### Klient konfiguratsiyasi

```json
{
  "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.
- `.env` da `TRUSTED_PROXIES` ga 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:

  ```nginx
  location /mcp/ {
      proxy_buffering off;
      fastcgi_read_timeout 300;
  }
  ```

- `config/mcp-database.php` dagi `tables` ni prod bazasi uchun qayta ko'rib chiqing.
- `php artisan config:cache` va `route:cache` ishlaydi — `routes/ai.php` avtomatik ro'yxatdan o'tadi, uni `bootstrap/app.php` ga qo'shish **shart emas**.

---

## ngrok bilan test

Prod'ga chiqarishdan oldin HTTP qatlamini lokal tekshirish.

```bash
# 1-terminal
php artisan serve

# 2-terminal
ngrok config add-authtoken SIZNING_TOKENINGIZ
ngrok http 8000
```

`.env` da lokal test uchun:

```env
TRUSTED_PROXIES=*
```

Foydalanuvchi va token tayyorlang:

```bash
php artisan tinker --execute 'App\Models\User::factory()->create(["email" => "demo@example.test"]);'
php artisan mcp:token demo@example.test --expires=1
```

Tekshirish:

```bash
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_limit` bir 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:

```php
if (! $request->user()->can('read-table', $table)) {
    return Response::error('Permission denied.');
}
```

yoki tool'ni butunlay yashirish uchun `shouldRegister()`:

```php
public function shouldRegister(Request $request): bool
{
    return $request->user()?->isAdmin() ?? false;
}
```

---

## Testlar

```bash
php artisan test
```

| Fayl | Nimani qoplaydi |
|---|---|
| `DatabaseServerTest.php` | Tool mantiqi: allowlist, yashirin ustun, filtr, limit qirqilishi, noto'g'ri operator |
| `DatabaseServerHttpTest.php` | HTTP: `401` tokensiz, `403` noto'g'ri ability, `200` to'g'ri token, real qator o'qish, `mcp:token` komandasi |

Faqat bittasini ishga tushirish:

```bash
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:

```php
'tables' => ['users', 'orders'],
```

Kod o'zgarmaydi. Nozik ustunlar bo'lsa `hidden` ga qo'shing.

### Yangi tool qo'shish

```bash
php artisan make:mcp-tool OrderSummaryTool
```

Keyin `DatabaseServer::$tools` ga qo'shing. Bazaga tegadigan tool `DatabaseCatalog` ni constructor orqali oling — allowlist va yashirin ustunlar avtomatik amal qiladi:

```php
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:

```php
#[Name('query-table')]
#[Title('Query Table')]
class QueryTableTool extends Tool
```

**2. `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 returned
```

To'g'ri turi:

```php
public function handle(Request $request): Response|ResponseFactory
```

**3. `ability` middleware alias avtomatik kelmaydi**

Laravel 13'da Sanctum uni ro'yxatdan o'tkazmaydi. `bootstrap/app.php` da qo'lda:

```php
$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.