directus-safe-mcp
by asukakimya
README.md
# directus-safe-mcp
Schema-aware, dry-run/verify-guarded Directus MCP sidecar.
Directus REST API'yi güvenli, schema-first ve LLM dostu bir MCP (Model Context Protocol) ara katmanıyla saran bağımsız Node.js / TypeScript servisi. Directus resmi remote MCP'deki `items` tool'undaki stringified-JSON serialization bug sınıfını (#26891 / PR #27005) kendi içinde çözer; ek olarak dry-run, verify, after-read doğrulaması, collection allowlist, system collection guard ve batch limit politikaları uygular.
> Bu MCP Directus'un yerine geçmez. Directus API'yi güvenli ve LLM dostu bir ara katmanla kullanır. RBAC bypass etmez — token'ın sahip olduğu izinler neyse MCP de onu kullanır.
Bu MCP **iş kuralı bilmez**. Koleksiyon anlamları (örneğin "şu koleksiyon satın alma kaynağıdır", "bu alan şu vendor'a bağlıdır") MCP'nin değil, bağlanan ajan prompt'larının / skill'lerin / `ai_prompts` kayıtlarının sorumluluğundadır. MCP sadece: schema oku, veri oku, güvenli yaz, validate et, dry-run yap, verify yap, after-read yap.
---
## Özellikler
- **MCP TypeScript SDK** üzerinde `streamable-http` (varsayılan, production) + `stdio` (local debug) transport. Legacy HTTP+SSE transport desteklenmez; Streamable HTTP kendi içinde gerektiğinde `text/event-stream` response kullanabilir (bu, SDK'nın resmi Streamable HTTP davranışıdır).
- **Stateless Streamable HTTP** — `sessionIdGenerator: undefined` ile gerçek stateless mod. Her HTTP request için taze `McpServer` + taze `StreamableHTTPServerTransport` oluşturulur; `mcp-session-id` header üretilmez. Bu sayede `initialize` sonrası `tools/list` tek HTTP bağlantısında çalışır.
- **Endpoint path guard** — sadece `MCP_ENDPOINT_PATH` (default `/mcp`) MCP endpoint. `GET /healthz` liveness probe, diğer path'ler 404.
- **Bearer auth gate** — `MCP_REQUIRE_AUTH=true` (default) ise her HTTP request `Authorization: Bearer <MCP_AUTH_TOKEN>` taşır. Constant-time compare. Stdio exempt (local subprocess).
- **Origin / Host guard** — `MCP_ALLOWED_ORIGINS` ve `MCP_ALLOWED_HOSTS` ile DNS rebinding + CSRF koruması.
- **Verify enforcement** — `MUTATION_REQUIRE_VERIFY=true` (default) iken `directus_update_item` ve `directus_batch_update_items` verify olmadan çalışmaz; `directus_update_items_same_data` tamamen reddedilir (per-key verify desteklemez).
- **All-or-nothing batch apply** — `directus_batch_update_items` ve `directus_create_items` apply (dry_run=false) sırasında önce preflight çalıştırır. Herhangi bir item validation/verify/dedupe hatası verirse **tüm batch abort edilir, sıfır yazma yapılır**. `allow_partial_apply=true` ile eski partial-success davranışına geri dönülebilir.
- **Single-mode read query** — `directus_read_item` artık `limit`/`page`/`offset` parametrelerini reddeder (tek kayıt endpoint'inde anlamsız; LLM karışıklığını gösterir). `fields`/`deep`/`version` hala destekleniyor.
- **Token-bounded `content.text`** — Tüm tool response'larında `content.text` artık sadece kısa bir etiket değil; modelin karar vermesi için gereken gerçek sonucu (şema field listesi, dönen kayıtlar, before/after/diff, hata kodları, batch summary) kompakt formatta içerir. LibreChat `structuredContent`'i modele göstermese bile LLM gerçek sonucu görür. Limitler: `SCHEMA_TEXT_MAX_FIELDS=80`, `READ_TEXT_MAX_ROWS=10`, `READ_TEXT_MAX_CHARS=12000`.
- **Recursive JSON normalisation** — `data`, `query`, `filter`, `deep`, `keys`, `headers`, `verify`, `dedupe`, `items`, `operations` alanları JSON string olarak gelirse otomatik parse.
- **Schema-first validation** — her yazma işleminde field set şemadan doğrulanır; unknown ve readonly field'lar Directus'a gitmeden reddedilir.
- **Read-before, verify, diff, after-read** doğrulama zinciri update'lerde.
- **Dry-run default** — `MUTATION_DRY_RUN_DEFAULT=true` ile yazma işlemleri varsayılan olarak simulate edilir.
- **Collection allowlist + `directus_*` blocklist**.
- **Delete default kapalı** — `DIRECTUS_ALLOW_DELETE=true` + `confirm="DELETE <collection>:<keys>"` gerekli.
- **Batch size limiti** — `MUTATION_MAX_BATCH_SIZE`.
- **Filter operator whitelist** — sadece spec'te listelenen operatorlere izin verilir.
- **Safe default fields** — `fields` verilmezse PK + ilk 10 scalar field döner; `*` wildcard default olarak reddedilir.
- **Partial success reporting** — batch işlemler per-item sonuç döner.
- **Audit log** — her mutation girişimi loglanır.
---
## Hızlı Başlangıç
### 1. Environment
`.env.example`'ı kopyalayıp doldurun:
```bash
cp .env.example .env
# .env içine:
# DIRECTUS_URL=https://your-directus.example.com
# DIRECTUS_TOKEN=replace-with-directus-static-token
# MCP_AUTH_TOKEN=<random 32+ byte string>
```
### 2. Production (Docker, Streamable HTTP)
```bash
docker compose up --build
```
Container `3333` portunda Streamable HTTP MCP sunar. Bearer token ile auth zorunludur (`MCP_REQUIRE_AUTH=true` default).
### 3. Local debug (Node, stdio)
```bash
npm install
npm run build
MCP_TRANSPORT=stdio node dist/index.js
```
Stdio modu local subprocess olduğu için `MCP_REQUIRE_AUTH` yok sayılır — auth parent process'in sorumluluğundadır.
### 4. Local HTTP debug
```bash
MCP_TRANSPORT=streamable-http \
MCP_HTTP_PORT=3333 \
MCP_REQUIRE_AUTH=true \
MCP_AUTH_TOKEN=dev-token \
node dist/index.js
```
---
## Configuration (env)
| Variable | Default | Açıklama |
|---|---|---|
| `DIRECTUS_URL` | (zorunlu) | Directus instance URL'i |
| `DIRECTUS_TOKEN` | (zorunlu) | Directus access token (RBAC'ye tabi) |
| `MCP_TRANSPORT` | `streamable-http` | `streamable-http`, `http` (alias), veya `stdio` |
| `MCP_HTTP_PORT` | `3333` | Streamable HTTP port |
| `MCP_BIND_HOST` | `0.0.0.0` | Network interface (`127.0.0.1` = local-only) |
| `MCP_ENDPOINT_PATH` | `/mcp` | MCP HTTP endpoint path. `/healthz` her zaman açık. |
| `MCP_REQUIRE_AUTH` | `true` | HTTP transport'ta Bearer auth zorunlu mu |
| `MCP_AUTH_TOKEN` | (zorunlu HTTP auth için) | Bearer token; constant-time compare |
| `MCP_ALLOWED_ORIGINS` | (boş = tümü) | Virgülle ayrılmış Origin allowlist (DNS rebinding koruması) |
| `MCP_ALLOWED_HOSTS` | (boş = tümü) | Virgülle ayrılmış Host allowlist. Port-tolerant: `mcp.example.com` allowlist'i hem `mcp.example.com` hem `mcp.example.com:3333` Host header'larını kabul eder. |
| `DIRECTUS_ALLOWED_COLLECTIONS` | (boş = hepsi) | Virgülle ayrılmış allowlist |
| `DIRECTUS_DENIED_COLLECTION_PREFIXES` | `directus_` | Yasaklı prefix'ler |
| `DIRECTUS_ALLOW_DELETE` | `false` | Delete tool'larını açar |
| `DIRECTUS_ALLOW_SCHEMA_WRITE` | `false` | (rezerve) |
| `MUTATION_DRY_RUN_DEFAULT` | `true` | dry-run default |
| `MUTATION_REQUIRE_VERIFY` | `true` | Update'lerde verify zorunlu; `update_items_same_data` reddedilir |
| `MUTATION_MAX_BATCH_SIZE` | `100` | Batch limit |
| `READ_DEFAULT_LIMIT` | `50` | Read default limit |
| `READ_MAX_LIMIT` | `500` | Read maksimum limit (clamp) |
| `ALLOW_WILDCARD_FIELDS` | `false` | `fields: ["*"]` izni |
| `SCHEMA_CACHE_TTL_SECONDS` | `300` | Schema cache TTL |
| `VERIFY_CASE_INSENSITIVE` | `false` | Verify string compare modu |
| `SCHEMA_TEXT_MAX_FIELDS` | `80` | `content.text` için şema detayında maksimum field sayısı |
| `READ_TEXT_MAX_ROWS` | `10` | `content.text` için read/batch sonuçlarında maksimum satır |
| `READ_TEXT_MAX_CHARS` | `12000` | `content.text` toplam karakter limiti (truncation marker dahil) |
| `APPLY_REQUIRES_PLAN` | `true` | Direct `dry_run:false` reddedilir; model önce dry-run yapıp plan almalı, sonra `directus_apply_plan` çağırmalı |
| `PLAN_STORE` | `file` | Plan store backend: `file` (production) veya `memory` (test) |
| `PLAN_STORE_DIR` | `/tmp/directus-safe-mcp-plans` | File-based plan store dizini (0600 izinleri) |
| `PLAN_TTL_SECONDS` | `900` | Plan TTL (saniye, default 15 dk) |
| `PLAN_MAX_BYTES` | `1048576` | Maks plan payload boyutu (byte) |
| `LOG_LEVEL` | `info` | pino log level |
---
## MCP Tool Listesi
20 tool register edilir (hepsi `directus_` prefix'i ile):
| Tool | Açıklama |
|---|---|
| `directus_schema_overview` | Koleksiyon listesi + PK + field count |
| `directus_schema_detail` | Bir veya birden fazla koleksiyonun tam şema detayı |
| `directus_read_items` | Liste okuma (validate edilmiş query ile) |
| `directus_read_item` | Tek item okuma |
| `directus_create_item` | Tek item create (dedupe + dry-run + plan destekli) |
| `directus_create_items` | Batch create (serial, per-item result, all-or-nothing preflight) |
| `directus_update_item` | Tek item update (verify_fields auto-gen destekli) |
| `directus_update_items_same_data` | Bulk PATCH /items/{collection} (aynı data, multi key) |
| `directus_batch_update_items` | Per-item different data update (serial PATCH) |
| `directus_delete_items` | Delete (default kapalı, confirm gerekli) |
| `directus_dry_run_mutation` | Çoklu operasyon planı (dry-run only, planId üretmez) |
| `directus_update_by_query_plan` | **Query ile toplu update planı + bundle** (model kayıt listesi üretmez) |
| `directus_apply_plan` | Tek plan uygular (gerçek yazma + read-back verification) |
| `directus_apply_plans` | Çoklu plan toplu uygular (stop_on_error destekli) |
| `directus_apply_plan_bundle` | **Bundle'ı toplu uygular** (already-applied tolerant + verification) |
| `directus_cancel_plan` | Tek pending plan'ı iptal eder |
| `directus_cancel_plans` | Çoklu pending plan'ı toplu iptal eder |
| `directus_plan_bundle_status` | **Bundle durumunu sorgular** (pending/applied/expired/cancelled) |
| `directus_verify_fields_empty` | Belirli field'ların boş olduğunu doğrular |
| `directus_verify_fields_value` | **Belirli field'ların beklenen değere eşit olduğunu doğrular** (deep equality) |
### Tool input kuralları
- `data`, `items`, `keys`, `query`, `filter`, `deep`, `verify`, `dedupe`, `operations` hem native JSON hem de `<field>_json` formunda kabul edilir.
- **`*_json` alanları string, array VE object kabul eder** (LibreChat uyumluluğu için). Handler otomatik normalize eder.
- LibreChat bazen `keys_json: [1,2,3]` (array) veya `data_json: {tags:[]}` (object) gönderir — bu artık schema validation'da patlamaz, handler'da normalize edilir.
- Update işlemlerinde `verify` field'ları mevcut kayıtla eşleşmezse `VERIFY_FAILED` hatası döner; yazma yapılmaz.
- Unknown field → `UNKNOWN_FIELD` hatası (sessizce drop edilmez).
- Readonly / system audit field'lar (`user_created`, `date_created`, `user_updated`, `date_updated`) → `READONLY_FIELD` hatası.
- Primary key update → `PRIMARY_KEY_UPDATE_DENIED`.
- `directus_*` koleksiyonlarında mutation → `SYSTEM_COLLECTION_DENIED`.
- Filter operator whitelist dışı → `INVALID_FILTER_OPERATOR`.
- Wildcard field default olarak reddedilir (`ALLOW_WILDCARD_FIELDS=false`).
### JSON array alanlarında filtre güvenilirliği uyarısı
**Directus JSON/array alanlarında `_nnull`, `_nempty` filtreleri her zaman güvenilir sonuç vermeyebilir.** JSON kolonlar `"null"`, `"[]"`, `"{}"` veya whitespace string saklayabilir; Directus bunları non-null/non-empty sayabilir.
Güvenli yöntem: `directus_verify_fields_empty` tool'unu kullanın. Bu tool kayıtları okur ve client-side boş/dolu kontrolü yapar:
- `null`, `undefined`, `""`, whitespace-only string, `[]`, `{}` → boş
- diğer her şey → dolu
Örnek:
```json
{
"tool": "directus_verify_fields_empty",
"input": {
"collection": "suppliers",
"fields": ["tags", "products"]
}
}
```
Response'da `nonEmpty` listesi döner — bu kayıtlar tekrar işlenmelidir. Toplu temizlik sonrası "tüm kayıtlar temiz" demeden önce bu tool ile doğrulama yapın.
### Hata formatı
```json
{
"ok": false,
"error": {
"code": "UNKNOWN_FIELD",
"message": "Field 'bogus' does not exist in 'articles'",
"details": { "collection": "articles", "field": "bogus" }
}
}
```
Hata kodları: `CONFIG_ERROR`, `DIRECTUS_API_ERROR`, `COLLECTION_NOT_ALLOWED`, `SYSTEM_COLLECTION_DENIED`, `SCHEMA_NOT_FOUND`, `PRIMARY_KEY_NOT_FOUND`, `UNKNOWN_FIELD`, `READONLY_FIELD`, `PRIMARY_KEY_UPDATE_DENIED`, `REQUIRED_FIELD_MISSING`, `INVALID_QUERY`, `INVALID_FILTER_OPERATOR`, `VERIFY_FAILED`, `VERIFY_REQUIRED`, `ABORTED_BY_PREFLIGHT`, `DUPLICATE_FOUND`, `BATCH_LIMIT_EXCEEDED`, `DELETE_DISABLED`, `CONFIRMATION_REQUIRED`, `INVALID_JSON`, `INVALID_DATA_TYPE`, `DRY_RUN_REQUIRED`, `NOT_FOUND`, `PLAN_NOT_FOUND`, `PLAN_EXPIRED`, `PLAN_ALREADY_APPLIED`, `PLAN_ALREADY_IN_PROGRESS`, `PLAN_CANCELLED`, `PLAN_CHECKSUM_MISMATCH`, `APPLY_REQUIRES_PLAN`, `PLAN_STORE_ERROR`, `PLAN_TOO_LARGE`, `READBACK_MISMATCH`, `CONFIRM_TRUE_REQUIRED`.
---
## Dry-run → Approval → Apply Plan Flow
Bu akış, modelin "uyguladım" deyip gerçekte yazmaması problemini kökten çözer.
### Akış
1. **Mutation tool** `dry_run:true` ile çağrılır (örn. `directus_update_item`).
2. Server dry-run çalıştırır: read-before → verify → validate → diff. **Hiçbir yazma yapmaz.**
3. Server bir `planId` oluşturur ve store eder (file-based, TTL 15 dk).
4. Response'da `planId`, `requiresApplyPlan: true`, `written: false` döner.
5. `content.text`'te açıkça "DRY-RUN ONLY — hiçbir veri yazılmadı" yazar ve `directus_apply_plan` çağrı önerisi verilir.
6. Kullanıcı onay verince model **aynı payload'u tekrar üretmez** — sadece `directus_apply_plan({ plan_id, confirm: true })` çağırır.
7. `directus_apply_plan` planı yükler, checksum doğrular, collection guard + schema + field validation + verify **tekrar** yapar, gerçek yazma yapar, read-back verification yapar, plan'ı `applied` olarak işaretler.
8. Response'da `applied: true`, `written: true`, `dryRun: false`, `readBackOk: true` döner.
9. `content.text`'te "APPLIED — gerçek yazma yapıldı" yazar.
### Kritik kurallar
- `dry_run:true` **asla** veri yazmaz. Kullanıcı onayı sadece apply iznidir; gerçek yazma `directus_apply_plan` çağrısıyla olur.
- `APPLY_REQUIRES_PLAN=true` (default) iken direct `dry_run:false` mutation tool çağrısı `APPLY_REQUIRES_PLAN` hatası alır.
- Her plan sadece bir kez uygulanabilir (idempotency). İkinci apply → `PLAN_ALREADY_APPLIED`.
- **Concurrent apply protection**: plan claim atomik `pending → applying` geçişi yapar. Eşzamanlı iki apply çağrısından ilki claim alır, ikincisi `PLAN_ALREADY_IN_PROGRESS` alır.
- Süresi dolmuş plan → `PLAN_EXPIRED`. Yeni dry-run gerekir.
- Apply sırasında verify koşulları artık geçerli değilse → `VERIFY_FAILED`, yazma yapılmaz (pre-write error, plan cancelled).
- **Read-back mismatch post-write davranışı**: yazma başarılı olursa, plan **mutlaka terminal** duruma geçer. Read-back mismatch olursa `applied_with_warning` statüsü alır ve `warning.code: "READBACK_MISMATCH"` response'da döner — **throw etmez**. Plan tekrar apply edilemez. Bu, "yazıldı ama error döndü, tekrar yazıldı" riskini önler.
- Post-write unexpected error → `failed_after_write` (terminal, tekrar apply edilemez, caller read ile durumu kontrol etmeli).
- Plan store file-based (default), 0600 dosya izinleri, TTL 15 dk, max 1 MB payload.
- Startup + periyodik (5 dk) cleanup: expired/cancelled/applied plan dosyaları temizlenir. `applied_with_warning` ve `failed_after_write` planları korunur (post-mortem inspection için).
- Audit log plan payload'unu loglamaz — sadece `planId`, `operation`, `collection`, `changedFields`.
### `directus_dry_run_mutation` plan üretmez
`directus_dry_run_mutation` tool'u **çoklu işlem planlama/görünüm** içindir. Apply edilebilir `planId` **üretmez**.
Apply edilebilir plan gerekiyorsa spesifik mutation tool `dry_run:true` kullanın:
- Tek kayıt update: `directus_update_item dry_run:true`
- Çoklu farklı update: `directus_batch_update_items dry_run:true`
- Tek/çoklu create: `directus_create_item(s) dry_run:true`
- Delete: `directus_delete_items dry_run:true`
Onay sonrası sadece `directus_apply_plan` çağırın.
### Plan lifecycle durumları
```text
pending → applying → applied (write + readback OK)
→ applied_with_warning (write OK, readback mismatch)
→ failed_after_write (write happened, post-write error)
pending → cancelled (user rejected or pre-write validation fail)
pending/applying → expired (TTL passed)
```
`applied`, `applied_with_warning`, `failed_after_write`, `expired`, `cancelled` hepsi terminal — plan bir daha apply edilemez.
### Örnek akış
```json
// 1. Model: dry-run update
{ "tool": "directus_update_item", "input": {
"collection": "articles", "key": 1,
"verify": { "title": "Intro to MCP" },
"data": { "slug": "intro-to-mcp" },
"dry_run": true
}}
// Response: { "planId": "plan_...", "requiresApplyPlan": true, "written": false, ... }
// 2. Kullanıcı: "onaylıyorum"
// 3. Model: apply plan (payload'u tekrar üretmez!)
{ "tool": "directus_apply_plan", "input": {
"plan_id": "plan_...",
"confirm": true
}}
// Response: { "applied": true, "written": true, "dryRun": false, "readBackOk": true, ... }
```
### Content text örnekleri
Dry-run:
```text
DRY-RUN UPDATE articles — OK (dryRun=true) NOT WRITTEN
Plan ID: plan_...
Plan expires at: 2024-01-01T00:15:00.000Z
Changed fields: slug
Before: {"id":1,"title":"Intro to MCP","slug":null}
After: {"id":1,"title":"Intro to MCP","slug":"intro-to-mcp"}
⚠ DRY-RUN ONLY — hiçbir veri yazılmadı.
Kullanıcı onay verirse şu tool çağrılmalı:
directus_apply_plan({ "plan_id": "plan_...", "confirm": true })
Başarı mesajı ancak directus_apply_plan sonucunda applied:true / written:true görüldükten sonra verilebilir.
```
Apply:
```text
APPLIED UPDATE articles — OK (dryRun=false) written=true
Plan ID: plan_...
Read-back verification: OK
Changed fields: slug
```
### Delete işlemleri
Delete işlemleri de aynı plan akışını kullanır:
1. `directus_delete_items` ile `dry_run:true` → plan oluşturulur (confirm token dahil).
2. `directus_apply_plan` → gerçek delete + read-back (kayıtların artık okunamadığını doğrular).
### Local debug (plan akışı olmadan)
`APPLY_REQUIRES_PLAN=false` ile direct `dry_run:false` çağrılabilir (eski davranış). Sadece local debug için önerilir.
---
## Bundle Flow: Query-Based Toplu Update (düşük parametreli modeller için)
Bu akış, modelin kayıt listesi toplamasını, verify üretmesini, planId'leri takip etmesini engeller. Model sadece niyeti söyler.
### Akış
1. **Model**: `directus_update_by_query_plan` çağırır (query + data + verify_fields)
2. **MCP**: kayıtları okur, verify üretir, chunk'lara böler, planlar oluşturur, **bundle_id** döndürür
3. **Kullanıcı**: onay verir
4. **Model**: `directus_apply_plan_bundle({ bundle_id, confirm: true })` çağırır (payload tekrar üretmez)
5. **MCP**: tüm planları uygular, read-back + bundle-level verification yapar
6. **Response**: `applied: true`, `written: true`, `readBackStatus: 'ok'`, `verification: { ok: true }`
### Örnek
```json
// 1. Model: "Tüm tedarikçilerin tags alanı ['test'] olsun"
{
"tool": "directus_update_by_query_plan",
"input": {
"collection": "suppliers",
"query": { "fields": ["id", "company"], "sort": ["id"], "limit": 100 },
"data": { "tags": ["test"] },
"verify_fields": ["company"],
"dry_run": true,
"chunk_size": 25
}
}
// Response: { "bundleId": "bundle_...", "planIds": [...], "totalMatched": 49, ... }
// 2. Kullanıcı: "onaylıyorum"
// 3. Model: apply bundle (payload tekrar üretmez!)
{
"tool": "directus_apply_plan_bundle",
"input": { "bundle_id": "bundle_...", "confirm": true }
}
// Response: { "applied": true, "written": true, "readBackStatus": "ok", "verification": { "ok": true, "totalChecked": 49 } }
```
### Neden bundle?
- Model planId listesi takip etmez — tek `bundle_id` yeterli
- Model kayıt listesi üretmez — MCP server-side okur
- Model verify üretmez — MCP `verify_fields`'tan otomatik üretir
- Model id tahmini yapamaz — sadece gerçek dönen kayıtlar kullanılır
- `PLAN_ALREADY_APPLIED` durumu otomatik verification ile çözülür
### `verify_fields` auto-generation
`directus_update_item` ve `directus_update_by_query_plan` artık `verify_fields` parametresi destekliyor. `verify` nesnesi vermek yerine field adları verilir; MCP kaydı okur ve verify'yi server-side üretir.
```json
{
"collection": "suppliers",
"key": 61,
"verify_fields": ["company"],
"data": { "ai_info": "# O KIMYA" },
"dry_run": true
}
```
MCP içeride şunu üretir:
```json
{ "verify": { "company": "O KIMYA" } }
```
Bu, modelin `verify: { ai_info: true }` gibi hatalı verify'ler üretmesini engeller.
---
## LibreChat Agent Instructions
Bağlanan ajana şu talimatı verin:
```text
Directus mutation işlemlerinde (create/update/delete) her zaman önce dry_run:true kullan.
Dry-run sonucu planId dönerse, kullanıcı onayı sonrası AYNI PAYLOAD'U TEKRAR ÜRETTME.
Sadece şu çağrıyı yap:
directus_apply_plan({ "plan_id": "<önceki planId>", "confirm": true })
Kullanıcı "onaylıyorum", "uygula", "tamam", "devam et" derse directus_apply_plan çağır.
directus_apply_plan çalışmadan ve çıktıda applied:true / dryRun:false / written:true görmeden
"başarıyla güncellendi", "uygulandı", "kaydedildi" DEME.
Dry-run çıktısı başarı değildir. Dry-run sadece plan üretir.
Eğer planId yoksa veya apply başarısız olursa, kullanıcıya gerçeği söyle.
directus_dry_run_mutation tool'u planId ÜRETMEZ — sadece çoklu işlem görünümü içindir.
Apply edilebilir plan için spesifik mutation tool dry_run:true kullan (update_item, batch_update_items, create_item, delete_items).
Eğer directus_apply_plan çıktısında warning alanı varsa (örn. READBACK_MISMATCH),
yazma gerçekleşmiş ama doğrulama başarısız olmuş demektir. Kullanıcıya gerçeği söyle:
"Yazma yapıldı ama read-back doğrulaması başarısız. Kaydı kontrol edin."
Plan zaten terminal durumdadır, tekrar apply ETME.
```
---
## Client Bağlantı Örnekleri
### 1. Streamable HTTP (production, Docker/LibreChat)
LibreChat veya başka bir MCP client `url` alanını destekliyorsa:
```yaml
mcpServers:
directus-safe:
url: http://directus-safe-mcp:3333/mcp
headers:
Authorization: "Bearer ${MCP_AUTH_TOKEN}"
timeout: 90000
```
Container'ı `MCP_TRANSPORT=streamable-http` + `MCP_AUTH_TOKEN=<token>` ile çalıştırın.
### 2. stdio (local debug, Claude Desktop / Cursor)
```yaml
mcpServers:
directus-safe:
command: docker
args:
- exec
- -i
- directus-safe-mcp
- node
- dist/index.js
env:
DIRECTUS_URL: "${DIRECTUS_URL}"
DIRECTUS_TOKEN: "${DIRECTUS_TOKEN}"
MCP_TRANSPORT: "stdio"
timeout: 90000
initTimeout: 60000
```
Stdio modunda `MCP_REQUIRE_AUTH` yok sayılır (auth parent process'in sorumluluğunda).
---
## Örnek Kullanım
### 1. Schema keşfi
```json
{ "tool": "directus_schema_overview", "input": { "include_system": false } }
{ "tool": "directus_schema_detail", "input": { "collections": ["articles"] } }
```
### 2. Okuma
```json
{
"tool": "directus_read_items",
"input": {
"collection": "articles",
"query": {
"fields": ["id", "title", "slug"],
"filter": { "title": { "_icontains": "MCP" } },
"limit": 10
}
}
}
```
### 3. Dry-run update (önerilen akış)
```json
{
"tool": "directus_batch_update_items",
"input": {
"collection": "articles",
"items": [
{ "key": 1, "verify": { "title": "Intro to MCP" }, "data": { "status": "published" } },
{ "key": 2, "verify": { "title": "Directus Deep Dive" }, "data": { "status": "published" } }
],
"dry_run": true
}
}
```
LLM sonucu kullanıcıya sunar; kullanıcı onaylayınca `dry_run: false` ile gerçek yazma yapılır.
### 4. Stringified JSON regression test
Eğer client `data`'yı string olarak gönderirse (Directus issue #26891):
```json
{
"tool": "directus_update_item",
"input": {
"collection": "articles",
"key": 1,
"data": "{\"status\":\"published\"}",
"dry_run": true
}
}
```
Sidecar bunu otomatik parse eder ve validation'a sokar.
---
## Ajan Kullanım Talimatı
Bağlanan ajana şunu verin:
```text
Directus verisi için resmi Directus remote MCP items tool yerine directus-safe-mcp tool'larını kullan.
Her görevde sıra:
1. directus_schema_detail ile ilgili koleksiyonları incele.
2. directus_read_items veya directus_read_item ile mevcut kayıtları oku.
3. Yazma işleminden önce dry_run=true kullan.
4. Kullanıcı/ana ajan onayı olmadan dry_run=false yapma.
5. Update'lerde verify kullan.
6. Unknown/readonly field hatasında tahminle devam etme.
7. Tool schema hatası alırsan aynı payload'u tekrar etme; data_json veya query_json kullan.
```
İş kuralları (koleksiyon anlamları, vendor mapping'leri, domain-specific davranışlar) MCP'de değil, ajan prompt'larında / skill'lerde / `ai_prompts` kayıtlarında tutulur.
---
## Test
```bash
npm test # vitest run
npm run test:watch # vitest watch
npm run typecheck # tsc --noEmit (strict)
```
Test kapsamı:
- `normalize.test.ts` — recursive JSON normalisation (regression testleri dahil)
- `diff-verify.test.ts` — diff ve verify davranışı
- `permissions.test.ts` — collection / batch guard'lar
- `validators.test.ts` — query validation + field validation
- `mutations.test.ts` — DirectusRestClient + items operations + relation type inference
- `mcp-tools.test.ts` — fetch-mock ile MCP tool entegrasyon testleri (dry-run, verify fail, unknown field, batch partial success, stringified JSON regression)
- `transports.test.ts` — Streamable HTTP auth gate + transport alias parsing
---
## Mimari Notlar
### Stateless Streamable HTTP
`StreamableHTTPServerTransport`'a `sessionIdGenerator: undefined` veriyoruz. Bu, SDK'ya "session üretme, her request self-contained" der. Bu olmadan SDK her initialize'da `mcp-session-id` header üretir; client sonraki request'te bu header'ı göndermek zorundadır; ama server tarafında session map tutmadığımız için ikinci request "Server not initialized" 400 hatası alır. `undefined` ile bu davranış tamamen kapanır.
Her HTTP request için taze `McpServer` + taze transport oluşturulur. Tool'lar zaten stateless — Directus REST çağrısı yapıyorlar, server içinde kullanıcı state'i tutmaya gerek yok.
### Bearer auth
`MCP_REQUIRE_AUTH=true` (default) ise her HTTP request `Authorization: Bearer <MCP_AUTH_TOKEN>` taşır. Token karşılaştırması constant-time yapılır (timing side-channel önlemi). Stdio transport exempt — auth parent process'in sorumluluğunda.
Config yükleme sırasında: eğer `streamable-http` + `MCP_REQUIRE_AUTH=true` + `MCP_AUTH_TOKEN` boşsa server refuse-to-start eder. Yanlışlıkla unauthenticated HTTP açmamak için.
### Transaction garantisi yok (V1)
`directus_batch_update_items` tek tek `PATCH /items/{collection}/{id}` çağrıları yapar. Bir kayıt başarısız olursa diğerleri devam eder (fail_fast=false default). Partial success `summary` alanında raporlanır. All-or-nothing transaction gerekiyorsa Directus API extension içinde `ItemsService` + database transaction kullanmak gerekir (V2 planı).
### Different-data batch update
Resmi Directus Items API "Update Multiple Items" endpoint'i (`PATCH /items/{collection}`) sadece **aynı data**'yı birden fazla key'e uygular (`{ keys, data }` body). Per-key farklı data için sidecar serial `PATCH /items/{collection}/{id}` kullanır.
### RBAC
Sidecar Directus RBAC'yi bypass etmez. Token'ın sahip olduğu permission'lar geçerlidir. 403/401 hataları `DIRECTUS_API_ERROR` olarak sarılır.
### Schema cache
`/collections`, `/fields/{collection}`, `/relations` çağrıları TTL cache'lenir (default 5 dakika). Schema değişikliği olursa servisi restart edin veya cache'i temizleyin.
---
## Dosya Yapısı
```text
directus-safe-mcp/
Dockerfile
docker-compose.yml
.env.example
package.json
tsconfig.json
vitest.config.ts
README.md
scripts/
smoke-stdio.mjs # stdio transport smoke test (initialize + tools/list)
smoke-http.mjs # Streamable HTTP smoke test (healthz, 404, 401, initialize, tools/list)
src/
index.ts # entry: load config → server factory → connect transport
config.ts # env loader + pino logger + transport alias parsing
mcp/
server.ts # ToolContext + buildServer
transports.ts # stdio / stateless streamable-http + Bearer auth
tools.ts # registerAllTools wrapper (normalise + error mapping)
directus/
client.ts # low-level items operations
rest.ts # DirectusRestClient + DirectusApiError
schema.ts # internal schema model + relation inference
schemaService.ts # /collections + /fields + /relations cache
query.ts # read query validation + filter operator whitelist
validators.ts # field validation (unknown, readonly, pk, required)
mutations.ts # read/create/update/delete orchestration
errors.ts # McpUserError + ErrorCode union
safety/
normalize.ts # recursive JSON normaliser
permissions.ts # collection / batch / delete guards
diff.ts # MutationDiff + deepEqual
dryRun.ts # dry-run result shape
verify.ts # verify record vs expectations
audit.ts # in-memory audit log
textFormat.ts # compact content.text formatters (token-bounded)
plans.ts # MutationPlan + PlanStore (file/memory) + checksum
bundles.ts # PlanBundle + BundleStore + status computation
tools/
schemaOverview.ts
schemaDetail.ts
readItems.ts
readItem.ts
createItem.ts
createItems.ts
updateItem.ts
updateItemsSameData.ts
batchUpdateItems.ts
deleteItems.ts
dryRunMutation.ts
applyPlan.ts # single plan apply
applyPlans.ts # batch plan apply (stop_on_error)
applyPlanBundle.ts # bundle apply (already-applied tolerant + verification)
cancelPlan.ts # single plan cancel
cancelPlans.ts # batch plan cancel
planBundleStatus.ts # bundle status query
updateByQueryPlan.ts # query-based batch plan + bundle creation
verifyFieldsEmpty.ts # post-apply verification helper (fields empty)
verifyFieldsValue.ts # post-apply verification helper (fields match expected)
test/
helpers.ts # expectErrorCode test helper
normalize.test.ts
diff-verify.test.ts
permissions.test.ts
validators.test.ts
mutations.test.ts
mcp-tools.test.ts
transports.test.ts
```
---
## Kabul Kriterleri
Tüm kabul kriterleri (spec §25) karşılanır:
1. ✅ Docker Compose ile ayağa kalkar.
2. ✅ `stdio` MCP transport çalışır (+ `streamable-http` production default).
3. ✅ 11 tool register edilir (spec minimum 8).
4. ✅ `data` stringified JSON olarak gelirse native objeye çevrilir.
5. ✅ `query` stringified JSON olarak gelirse native objeye çevrilir.
6. ✅ Update dry-run before/after diff döndürür.
7. ✅ Update apply sonrası after-read doğrulama yapar.
8. ✅ Unknown field update Directus'a gitmeden reddedilir.
9. ✅ Readonly field update Directus'a gitmeden reddedilir.
10. ✅ `directus_*` collection mutation reddedilir.
11. ✅ Delete default kapalıdır.
12. ✅ Batch update partial success raporlar.
13. ✅ Tests pass (`npm test`).
14. ✅ README içinde hem Streamable HTTP hem stdio bağlantı örneği vardır.
15. ✅ Kod TypeScript strict modda compile olur.
16. ✅ Streamable HTTP varsayılan transport, Bearer auth gate zorunlu.
17. ✅ Şirket/koleksiyon sabiti yok — generic Directus ürünü.
18. ✅ Stateless Streamable HTTP: `sessionIdGenerator: undefined` ile `initialize` → `tools/list` tek HTTP bağlantısında çalışır.
19. ✅ Endpoint path guard: sadece `/mcp` MCP endpoint, `/healthz` liveness probe, diğerleri 404.
20. ✅ Origin/Host guard (DNS rebinding + CSRF).
21. ✅ `MUTATION_REQUIRE_VERIFY=true` iken update'lerde verify zorunlu, `update_items_same_data` reddedilir.
22. ✅ Smoke testler proje içinde (`scripts/smoke-stdio.mjs`, `scripts/smoke-http.mjs`) — HTTP smoke test `tools/list`'i de doğrular.
23. ✅ Host allowlist port-tolerant: `mcp.example.com` allowlist'i `mcp.example.com:3333` Host header'ını da kabul eder.
24. ✅ `batch_update_items` ve `create_items` apply sırasında all-or-nothing preflight (`allow_partial_apply` default false). Abort durumunda sıfır yazma yapılır.
25. ✅ `read_item` single-mode: `limit`/`page`/`offset` reddedilir, default limit enjekte edilmez.
26. ✅ `content.text` token-bounded: gerçek sonuç (şema, kayıtlar, diff, hata, summary) kompakt formatta, `SCHEMA_TEXT_MAX_FIELDS` / `READ_TEXT_MAX_ROWS` / `READ_TEXT_MAX_CHARS` limitleriyle.
27. ✅ `directus_apply_plan` tool: dry-run plan'ını uygular, read-back verification yapar, idempotent (plan sadece bir kez apply edilir).
28. ✅ `directus_cancel_plan` tool: pending plan'ı iptal eder.
29. ✅ Dry-run mutation response'ları `planId` döndürür; `content.text` "DRY-RUN ONLY — hiçbir veri yazılmadı" içerir.
30. ✅ `APPLY_REQUIRES_PLAN=true` (default): direct `dry_run:false` reddedilir, model önce plan almalı.
31. ✅ Plan store: file-based (default) / memory, 0600 izinleri, TTL, max bytes, checksum.
32. ✅ Delete işlemleri de plan akışını kullanır (confirm token plan içinde saklanır).
33. ✅ Read-back mismatch post-write: warning olarak döner (throw etmez), plan `applied_with_warning` terminal durumu alır, tekrar apply edilemez.
34. ✅ Concurrent apply protection: atomik `pending → applying` claim, ikinci eşzamanlı apply `PLAN_ALREADY_IN_PROGRESS` alır.
35. ✅ Post-write unexpected error: plan `failed_after_write` terminal durumu alır, tekrar apply edilemez.
36. ✅ `directus_dry_run_mutation` planId üretmez — README ve agent instruction'da belgelendi.
37. ✅ Create işlemleri için gerçek read-back: created id bulunur, kayıt tekrar okunur, field değerleri doğrulanır.
38. ✅ Startup + periyodik (5 dk) plan cleanup: expired/cancelled/applied plan dosyaları temizlenir.
39. ✅ Batch tool `*_json` alanları string + array + object kabul eder (LibreChat uyumluluğu).
40. ✅ `directus_apply_plans` tool: çoklu plan toplu apply + `stop_on_error` desteği.
41. ✅ `directus_cancel_plans` tool: çoklu plan toplu cancel.
42. ✅ `directus_verify_fields_empty` tool: post-apply verification (JSON array field doluluk kontrolü, client-side).
43. ✅ JSON array alanlarında `_nnull`/`_nempty` filtrelerinin güvenilmez olduğu README'de belgelendi.
44. ✅ `directus_update_by_query_plan` tool: query ile toplu update planı + bundle (model kayıt listesi üretmez).
45. ✅ `directus_apply_plan_bundle` tool: bundle toplu apply + already-applied tolerant + verification.
46. ✅ `directus_plan_bundle_status` tool: bundle durum sorgulama.
47. ✅ `directus_verify_fields_value` tool: deep equality ile field değer kontrolü.
48. ✅ `verify_fields` auto-generation: update_item + update_by_query_plan (model verify üretmez).
49. ✅ Bundle sistemi: `bundle_id` ile tek referans (model planId listesi takip etmez).
50. ✅ NEXT ACTION text'leri tüm mutation/apply response'larında.
---
## Kaynaklar
- [Directus API Reference](https://directus.com/docs/api)
- [Directus Items API](https://directus.com/docs/api/items)
- [Directus Filter Rules](https://directus.com/docs/guides/connect/filter-rules)
- [Directus Issue #26891](https://github.com/directus/directus/issues/26891)
- [Directus PR #27005](https://github.com/directus/directus/pull/27005)
- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
- [MCP Transports Specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports)
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues