Ready to Start ile Mcp Server ile geliştirme

This commit is contained in:
Sedat ÖZTÜRK 2026-09-24 14:58:23 +03:00
parent 9ceacf5d64
commit dd6759f46c
36 changed files with 1364 additions and 136 deletions

View file

@ -1249,7 +1249,7 @@ soneklerini alır.
| **Public › Sales Orders** | `App.Orders.SalesOrders` (alt form Sales Order Item) | `Sas_H_Order` + `Sas_H_OrderItem` | host | **Onay akışı + alt kalem + toplam + diyalog** birlikte kullanılan tam örnek (§12.6). | | **Public › Sales Orders** | `App.Orders.SalesOrders` (alt form Sales Order Item) | `Sas_H_Order` + `Sas_H_OrderItem` | host | **Onay akışı + alt kalem + toplam + diyalog** birlikte kullanılan tam örnek (§12.6). |
| **Public › Blog Category / Posts** | `App.BlogManagement.Category` · `.Posts` | `Sas_H_BlogCategory` · `Sas_H_BlogPost` | host | Çok dilli içerik (`ContentTr`/`ContentEn`), slug, yayın durumu, sayaç alanları; public `/blog` bu tablolardan okur, yayınlanmış yazılar sitemap'e girer. | | **Public › Blog Category / Posts** | `App.BlogManagement.Category` · `.Posts` | `Sas_H_BlogCategory` · `Sas_H_BlogPost` | host | Çok dilli içerik (`ContentTr`/`ContentEn`), slug, yayın durumu, sayaç alanları; public `/blog` bu tablolardan okur, yayınlanmış yazılar sitemap'e girer. |
| **Public › Demos** | `App.Demos` | `Sas_H_Demo` | host | Public `/demo` formundan gelen talepler — public form → tablo akışının örneği. | | **Public › Demos** | `App.Demos` | `Sas_H_Demo` | host | Public `/demo` formundan gelen talepler — public form → tablo akışının örneği. |
| **Dev Requests** | `App.DevRequests` | `Sas_H_DevRequest` | host | **Kanban/Todo düzeninin** referansı (`DefaultLayout: todo`): `Status`, `Priority`, `SortOrder`, `Tags`, `Assignees`, `SubTasks`, `Timesheets`. | | **Dev Requests** | `App.DevRequests` | `Sas_H_DevRequest` | host | **Kanban/Todo düzeninin** referansı (`DefaultLayout: todo`): `Status`, `Priority`, `SortOrder`, `Tags`, `Assignees`, `SubTasks`, `Timesheets`. MCP geliştirme döngüsünün kuyruğudur (`Ready to Start` → `Staging`; yalnızca `start_dev_request` başlatır; açıklamadaki base64 görseller MCP'ye resim içeriği olarak gider, not `append_dev_request_note` ile eklenir; `lowcode.instructions.md` §0.11). |
| **Tenants** | `AbpTenantManagement.Tenants` | `AbpTenants` | host | Kiracı kaydı, bağlantı dizesi diyaloğu (`TenantsConnectionString`), alt form olarak şubeler; yazma `list-form-dynamic-api/tenant-*`. Ekranların `IsTenant` davranışının kaynağı. | | **Tenants** | `AbpTenantManagement.Tenants` | `AbpTenants` | host | Kiracı kaydı, bağlantı dizesi diyaloğu (`TenantsConnectionString`), alt form olarak şubeler; yazma `list-form-dynamic-api/tenant-*`. Ekranların `IsTenant` davranışının kaynağı. |
| **Branches** | `App.Branches` | `Sas_T_Branch` | tenant | Şube listesi. Ekranı `IsBranch = true` yaparsan filtre bu tablodan gelir; `BranchSeed` diyaloğu şube verisini kurar. | | **Branches** | `App.Branches` | `Sas_T_Branch` | tenant | Şube listesi. Ekranı `IsBranch = true` yaparsan filtre bu tablodan gelir; `BranchSeed` diyaloğu şube verisini kurar. |
| **Setting Definitions** | `App.SettingDefinitions` | `Sas_H_SettingDefinition` | host | Yeni uygulama ayarı: `Code`, `MainGroupKey`/`SubGroupKey`, `DataType`, `SelectOptions`, `DefaultValue`, `Providers`, `RequiredPermissionName`, `IsEncrypted`, `IsVisibleToClients`. Hazır tanımlar `data/App.SettingDefinitions.json`'dan gelir. Ayarlanabilir olması gereken değeri koda gömme. | | **Setting Definitions** | `App.SettingDefinitions` | `Sas_H_SettingDefinition` | host | Yeni uygulama ayarı: `Code`, `MainGroupKey`/`SubGroupKey`, `DataType`, `SelectOptions`, `DefaultValue`, `Providers`, `RequiredPermissionName`, `IsEncrypted`, `IsVisibleToClients`. Hazır tanımlar `data/App.SettingDefinitions.json`'dan gelir. Ayarlanabilir olması gereken değeri koda gömme. |
@ -1264,7 +1264,7 @@ soneklerini alır.
| **Developer Kit › Components** | `App.DeveloperKit.CustomComponents` | `Sas_H_CustomComponent` | host | Custom component listesi ve satırdaki **Design** butonuyla **Visual Designer** girişi (§10; `/admin/developerkit/components/design/:id`). Kayıtlar `data/App.DeveloperKit.CustomComponents/{Id}.json` klasörüne satır başına senkronlanır; public sayfalar ile üst/alt bilgi de buradadır. | | **Developer Kit › Components** | `App.DeveloperKit.CustomComponents` | `Sas_H_CustomComponent` | host | Custom component listesi ve satırdaki **Design** butonuyla **Visual Designer** girişi (§10; `/admin/developerkit/components/design/:id`). Kayıtlar `data/App.DeveloperKit.CustomComponents/{Id}.json` klasörüne satır başına senkronlanır; public sayfalar ile üst/alt bilgi de buradadır. |
| **Developer Kit › ListForm** | `App.Listforms.Listform` | `Sas_H_ListForm` · `Sas_H_ListFormField` · `Sas_H_ListFormWorkflow` · `Sas_H_ListFormCustomization` · `Sas_H_ListFormImport` | host | Ekran tanımlarının kendisi. Bu dosyada anlatılan bütün JSON kolonlarının canlı hâli; düzenleyicisi `/admin/listform/edit/{kod}`. | | **Developer Kit › ListForm** | `App.Listforms.Listform` | `Sas_H_ListForm` · `Sas_H_ListFormField` · `Sas_H_ListFormWorkflow` · `Sas_H_ListFormCustomization` · `Sas_H_ListFormImport` | host | Ekran tanımlarının kendisi. Bu dosyada anlatılan bütün JSON kolonlarının canlı hâli; düzenleyicisi `/admin/listform/edit/{kod}`. |
| **Developer Kit › Intranet Widgets** | `App.DeveloperKit.IntranetWidgets` | `Sas_T_IntranetWidget` | tenant | Intranet panosunda hangi widget'ın hangi kolonda (`Column`), hangi sırada (`Order`) ve hangi yetkiyle (`PermissionName`) çizileceği; `SourceType` `Builtin` (koddaki `Code`) ya da `Custom` (`ComponentName` ile bir custom component). Seed: `data/App.DeveloperKit.IntranetWidgets.json`. | | **Developer Kit › Intranet Widgets** | `App.DeveloperKit.IntranetWidgets` | `Sas_T_IntranetWidget` | tenant | Intranet panosunda hangi widget'ın hangi kolonda (`Column`), hangi sırada (`Order`) ve hangi yetkiyle (`PermissionName`) çizileceği; `SourceType` `Builtin` (koddaki `Code`) ya da `Custom` (`ComponentName` ile bir custom component). Seed: `data/App.DeveloperKit.IntranetWidgets.json`. |
| **Developer Kit › MCP Servers** | `App.DeveloperKit.McpServers` | `Sas_H_McpServer` | host | `/mcp` ucunun kiracı bazlı sözleşmesi: `Name`, `Url`, `Tenants`, `AllowedTools` (boşsa tüm araçlar, doluysa yalnızca seçilenler), `MaxRows`. Yetkiler: ekran `App.DeveloperKit.McpServers`, araç çağırma `.Invoke`, veri yazma `.Write`, tablo/ekran/bileşen geliştirme (`McpToolNames.DevelopTools`) `.Develop` — arkasındaki Wizard/Custom Components yetkileri ayrıca istenir. Kiracı kullanıcısı yalnızca okur (`TenantTools`: şema araçları, `query_list`, `run_select`); ham SQL `App.SqlQueryManager` ister; MCP yetki vermez. Kayıtlar `data/App.DeveloperKit.McpServers.json`'a senkronlanır. Ayrıntı: `README.md` §13.9. | | **Developer Kit › MCP Servers** | `App.DeveloperKit.McpServers` | `Sas_H_McpServer` | host | `/mcp` ucunun kiracı bazlı sözleşmesi: `Name`, `Url`, `Tenants`, `AllowedTools` (boşsa tüm araçlar, doluysa yalnızca seçilenler), `MaxRows`. Yetkiler: ekran `App.DeveloperKit.McpServers`, erişim ve okuma `.Invoke`, her değişiklik `.Write` (kiracıda satır yazma, host'ta ayrıca tablo/ekran/bileşen geliştirme) — arkasındaki ekran C/U/D, Wizard ve Custom Components yetkileri ayrıca istenir. Kiracı veri okur ve yazar, geliştirme yapamaz (`TenantTools`: şema araçları, `query_list`, `run_select`, `insert_row`/`update_row`/`delete_row`); ham SQL `App.SqlQueryManager` ister; MCP yetki vermez. Kayıtlar `data/App.DeveloperKit.McpServers.json`'a senkronlanır. Ayrıntı: `README.md` §13.9. |
| **Developer Kit › Wizard Manager** | menü kodu `App.Listforms.WizardManager` · yetki `App.Listforms.Wizard` · `/admin/listform/wizardManager` | `configs/seeds/{kapsam}/wizard/*.json` | host | Wizard seed dosyalarının kart/liste görünümü: menü üst düğümü ve URL, **bağımlılıklar**, düzenleme, export (bağımlılık kapanışı) / import ve **silme** (`WizardRecordCleaner`: ekran ve izlenen kayıtları kaldırılır, başka ekran kullanıyorsa engellenir, sonuç raporlanır — `lowcode` §4.5). Sihirbazın kendisi `/admin/listform/wizard`. | | **Developer Kit › Wizard Manager** | menü kodu `App.Listforms.WizardManager` · yetki `App.Listforms.Wizard` · `/admin/listform/wizardManager` | `configs/seeds/{kapsam}/wizard/*.json` | host | Wizard seed dosyalarının kart/liste görünümü: menü üst düğümü ve URL, **bağımlılıklar**, düzenleme, export (bağımlılık kapanışı) / import ve **silme** (`WizardRecordCleaner`: ekran ve izlenen kayıtları kaldırılır, başka ekran kullanıyorsa engellenir, sonuç raporlanır — `lowcode` §4.5). Sihirbazın kendisi `/admin/listform/wizard`. |
| **Forum Management › Categories / Topics / Posts** | `App.ForumManagement.Categories` · `.Topics` · `.Posts` | `Sas_T_ForumCategory` · `Sas_T_ForumTopic` · `Sas_T_ForumPost` | tenant | Forumun yönetim tarafı: kategori (seed `data/App.ForumManagement.Categories.json`), konu (kategori lookup'ı, sayaçlar, sabitle/kilitle/çözüldü bayrakları), gönderi (konu lookup'ı, kabul edilen yanıt). Kullanıcı tarafı Administration'daki `/admin/forum`. | | **Forum Management › Categories / Topics / Posts** | `App.ForumManagement.Categories` · `.Topics` · `.Posts` | `Sas_T_ForumCategory` · `Sas_T_ForumTopic` · `Sas_T_ForumPost` | tenant | Forumun yönetim tarafı: kategori (seed `data/App.ForumManagement.Categories.json`), konu (kategori lookup'ı, sayaçlar, sabitle/kilitle/çözüldü bayrakları), gönderi (konu lookup'ı, kabul edilen yanıt). Kullanıcı tarafı Administration'daki `/admin/forum`. |
@ -1428,7 +1428,7 @@ alanları da doldurur (`lowcode` §0.9).
| "kim değiştirmiş görelim" | Administration › Identity › Audit Logs | Hazır ekran; kayıt notu için `ShowNote` | | "kim değiştirmiş görelim" | Administration › Identity › Audit Logs | Hazır ekran; kayıt notu için `ShowNote` |
| "menüye eklensin" | Saas › Menus › Manager | Menü kaydı + yetki + route eşlemesi | | "menüye eklensin" | Saas › Menus › Manager | Menü kaydı + yetki + route eşlemesi |
| "yapay zekâ ajanı sorgulasın / veri çeksin" | Saas › DeveloperKit › MCP Servers | Kayıt aç, `AllowedTools`/tenant kapsamını ayarla; yeni endpoint/kod yok | | "yapay zekâ ajanı sorgulasın / veri çeksin" | Saas › DeveloperKit › MCP Servers | Kayıt aç, `AllowedTools`/tenant kapsamını ayarla; yeni endpoint/kod yok |
| "yapay zekâ ajanı ekran/bileşen geliştirsin" | Saas › DeveloperKit › MCP Servers | Yalnızca host kullanıcısı: `App.DeveloperKit.McpServers.Develop` + Wizard / Custom Components yetkilerini ver; geliştirme araçları `AllowedTools` ile açılıp kapatılır. Kiracı MCP'de yalnızca veri okur | | "yapay zekâ ajanı ekran/bileşen geliştirsin" | Saas › DeveloperKit › MCP Servers | Yalnızca host kullanıcısı: `App.DeveloperKit.McpServers.Invoke` + `.Write` + Wizard / Custom Components yetkilerini ver; geliştirme araçları `AllowedTools` ile açılıp kapatılır. Kiracı MCP'de veri okur ve yazar, geliştirme yapamaz |
| "ülke/il/ilçe, para birimi, birim, departman, ünvan" | Saas / Administration › Definitions | Var olan tabloya lookup bağla, yenisini üretme | | "ülke/il/ilçe, para birimi, birim, departman, ünvan" | Saas / Administration › Definitions | Var olan tabloya lookup bağla, yenisini üretme |
| "yapay zekâ cevaplasın" | Saas › Definitions › AiBot | Bot tanımı; `/admin/ai` bu tanımı kullanır | | "yapay zekâ cevaplasın" | Saas › Definitions › AiBot | Bot tanımı; `/admin/ai` bu tanımı kullanır |

View file

@ -495,22 +495,121 @@ JSON-LD belgelerini kurar; `::` ile başlayan değerler orada çevrilir (`resolv
`/mcp` ucuna bağlı bir istemci ekranı dosya yazarak değil, uygulamanın kendi servislerinden geçen `/mcp` ucuna bağlı bir istemci ekranı dosya yazarak değil, uygulamanın kendi servislerinden geçen
araçlarla üretir; sonuç ve seed dosyaları wizard ekranıyla birebir aynıdır. Araç adları araçlarla üretir; sonuç ve seed dosyaları wizard ekranıyla birebir aynıdır. Araç adları
`McpToolNames`'tedir; geliştirme araçlarının hepsi `App.DeveloperKit.McpServers.Develop` ister, `McpToolNames`'tedir; okuyanlar `App.DeveloperKit.McpServers.Invoke`, değiştirenler `.Write`
kiracı kullanıcısına hiçbiri açılmaz. ister ve kiracı kullanıcısına hiçbiri açılmaz.
| Araç | Ne yapar | Not | | Araç | Ne yapar | Not |
| --- | --- | --- | | --- | --- | --- |
| `get_dev_guide` | Bu dosyanın (`lowcode`) ya da `reference`'ın başlık listesini / bir bölümünü döner | Bölüm numarayla istenir (`4.3`); aynı numarayı iki başlık taşımamalıdır | | `get_dev_guide` | Bu dosyanın (`lowcode`) ya da `reference`'ın başlık listesini / bir bölümünü döner | Bölüm numarayla istenir (`4.3`); aynı numarayı iki başlık taşımamalıdır |
| `create_table` | Yeni SQL Server tablosu kurar, `sql/object/{Tablo}.sql` dosyasını yazar | Ek olarak `App.SqlQueryManager` ister; `Id`/`TenantId`/audit kolonlarını kendi ekler (`fullAudited`, `multiTenant` varsayılan açık); var olan tabloyu reddeder. View/SP/fonksiyon kurulamaz | | `create_table` | Yeni SQL Server tablosu kurar, `sql/object/{Tablo}.sql` dosyasını yazar | Ek olarak `App.SqlQueryManager` ister; `Id`/`TenantId`/audit kolonlarını kendi ekler (`fullAudited`, `multiTenant` varsayılan açık); var olan tabloyu reddeder. View/SP/fonksiyon kurulamaz |
| `list_wizards` · `get_wizard` | Wizard dosyalarını listeler (menü yolu, bağımlılar) · dosyayı diskteki hâliyle döner | Var olan menü kökünü/izin grubunu bulmak için önce `list_wizards` | | `list_wizards` · `get_wizard` | Wizard dosyalarını listeler (menü yolu, bağımlılar) · dosyayı diskteki hâliyle döner | Var olan menü kökünü/izin grubunu bulmak için önce `list_wizards` |
| `deploy_wizard` | Wizard cevabından ekranı, menüyü, yetkileri, dil anahtarlarını ve seed dosyasını üretir | Girdi §4.4'teki düz cevap ya da `get_wizard` çıktısıyla aynı dosya biçimi; enum'lar sayı. `replaceFileName` = `EditFileName` (§4.5) | | `deploy_wizard` | Wizard cevabından ekranı, menüyü, yetkileri, dil anahtarlarını ve seed dosyasını üretir | Girdi §4.4'teki düz cevap ya da `get_wizard` çıktısıyla aynı dosya biçimi; enum'lar sayı. `replaceFileName` = `EditFileName` (§4.5). İstek demo veri taşıyorsa `SeedData: true` (§0.10.1) |
| `delete_wizard` | Wizard'ı ve yalnızca onun ürettiği kayıtları siler | Bağımlı wizard varsa reddeder (§4.5) | | `delete_wizard` | Wizard'ı ve yalnızca onun ürettiği kayıtları siler | Bağımlı wizard varsa reddeder (§4.5) |
| `start_dev_request` | `App.DevRequests` talebini üstlenir: `Ready to Start` durumundaysa `In Progress` durumuna alır, değilse reddeder | Ek olarak `App.DevRequests.Update` ister. Açıklamadaki görseller ayrı resim içeriği olarak gelir (§0.11) |
| `get_dev_request` | Talebi durumuna dokunmadan okur (aynı metin + resim içerikleri) | Yarım kalan talebe dönmek için; `App.DevRequests` ister |
| `append_dev_request_note` | Talep açıklamasının sonuna HTML ekler | Sonuç notu için; `Description`'ı `update_row` ile yazma (§0.11) |
| `verify_wizard` | Deploy edilmiş ekranı veri yazmadan doğrular: menü, yetki tanımı, ekran sorgusu (List) ya da bileşen (Custom), seed kapanışı | `passed` yalnızca engelleyici bir kontrol kalırsa `false`; `seed_closure` engellemez ama rapora yazılır (§0.11) |
| `list_components` · `get_component` · `save_component` · `delete_component` | Custom component okuma/yazma (ada göre upsert) | `save_component` visual designer bileşenini reddeder — yalnız Code modu (§7.1.1) | | `list_components` · `get_component` · `save_component` · `delete_component` | Custom component okuma/yazma (ada göre upsert) | `save_component` visual designer bileşenini reddeder — yalnız Code modu (§7.1.1) |
**Yetkiler.** MCP'nin iki yetkisi vardır: `.Invoke` (bağlantı + okuma) ve `.Write` (her
değişiklik; kiracıda yalnızca satır yazma). İkisi de kapıdır, işlemin kendi uygulama yetkisi ayrıca
aranır. Araç başına tam tablo: `README.md` §13.9 › "Yetki modeli". Bir araç yetki yüzünden
reddedilirse cevap eksik yetkiyi adıyla verir ("'deploy_wizard' için gereken yetkiniz yok:
Wizard (App.Listforms.Wizard)…"): bu durumda **başka bir yoldan deneme** — aynı işi farklı bir
araçla yapmaya çalışma, eksik yetkiyi kullanıcıya olduğu gibi bildir ve dur.
#### 0.10.1 Demo veri — `SeedData`
İstek örnek/demo satır taşıyorsa (prompt'taki tablo ya da liste, ekran görüntüsündeki satırlar,
eklenen dosya) bu satırlar ekranla birlikte seed edilir; migrate çalıştığında liste boş gelmez.
1. `deploy_wizard` cevabına `SeedData: true` yaz. Ekranın `SeedFilePath`'i
`data/{ListFormCode}.json` olur, `SeedSyncInsert/Update/Delete` açılır (§9.15).
2. Deploy'dan sonra her satırı `insert_row` ile ekle. Satır ekranın insert sözleşmesinden geçer
ve `ListFormSeedDataSynchronizer` onu `configs/seeds/host/data/{ListFormCode}.json` dosyasına
yazar (kiracı bağlamında `tenants/{TenantId}/data/`).
3. Migrate sırasında wizard seeder ekranı kurar, ardından `ListFormSeedDataApplier` dosyadaki
satırları ekler (anahtarı olmayanı ekler, var olana dokunmaz).
Kurallar:
- Dosyayı elle yazma; satır ekranın sorgusundan okunduğu için anahtar, `TenantId` ve audit
alanları doğru biçimde gelir.
- Görsel yalnızca ekranın şablonu değildir: içinde görünen **her satır** okunur ve demo veri
olarak eklenir (standing default 6'daki "piksel sözleşmesi değildir" kuralı düzen içindir, veri için değil).
- Görselden okunamayan değer uydurulmaz: zorunlu alan boşsa kullanıcıya sor.
- Lookup alanının değeri kaynak tablodaki anahtardır; kaynakta yoksa önce o ekrana satır eklenir
(o ekranın `SeedFilePath`'i yoksa satır yalnızca bu veritabanında kalır — raporda belirt).
- İstek demo veri taşımıyorsa `SeedData` yazılmaz; `verify_wizard` testi için satır eklenmez.
Okuma araçları (`list_lists`, `describe_list`, `search_schema`, `query_list`, `run_select`, Okuma araçları (`list_lists`, `describe_list`, `search_schema`, `query_list`, `run_select`,
`list_queries`, `run_query`) keşif turunda (§0.3) tablo/kolon/lookup kaynağını bulmak içindir; `list_queries`, `run_query`) keşif turunda (§0.3) tablo/kolon/lookup kaynağını bulmak içindir;
kullanıcının uygulamadaki yetkisini aşmaz. kullanıcının uygulamadaki yetkisini aşmaz.
### 0.11 Geliştirme döngüsü — `App.DevRequests` kuyruğu
Talepler `/admin/list/App.DevRequests` ekranında (`Sas_H_DevRequest`) toplanır; kiracılar da
talep girer (`App.DevRequest`). Döngüyü `/mcp`'ye bağlı bir **host** kullanıcısı yürütür —
kiracı bağlamında geliştirme araçları kapalıdır. Gereken yetkiler: `App.DevRequests.Update`,
`App.DeveloperKit.McpServers.Invoke` + `.Write` ve üretimin kendi yetkileri (§0.10).
İki giriş vardır; ikisi de istemcide hazır prompt (komut) olarak görünür (`McpPromptNames`):
| Prompt | Kapsam |
| --- | --- |
| `process_dev_request` (`request`: Id ya da tam başlık) | Yalnızca o talep. Talep başlatılamazsa hiçbir şey üretilmez, başka talebe geçilmez |
| `process_ready_requests` | `Ready to Start` durumundaki bütün talepler, `SortOrder` sırasıyla; başlatılamayan talep atlanır |
Kullanıcı prompt yerine düz metin de yazabilir; aynı anlama gelir:
| Kullanıcı yazar | Karşılığı |
| --- | --- |
| `<Id ya da "başlık"> talebini işle` | `process_dev_request` — yalnızca o talep |
| `Ready to Start durumundaki talepleri sırayla işle` · `Ready olanları işle` | `process_ready_requests` |
| `Ready to Start durumundaki talepleri Id'leriyle listele` | Yalnızca adım 1 (`query_list`); hiçbir şey üstlenilmez, üretilmez |
İstemciye göre komut yazımı (Claude Code `/mcp__sozsoft__process_dev_request <Id>` vb.): `README.md`
› MCP › "Geliştirme taleplerini çalıştırma".
**Durum kuralı sunucudadır:** talep yalnızca `start_dev_request` ile üstlenilir. Araç talebi
`Ready to Start` durumunda değilse reddeder, uygunsa aynı işlemde `In Progress` durumuna alır ve
talebin metnini döndürür. Başlık birden fazla talebe uyuyorsa Id ister. `Status` hiçbir zaman
`update_row` ile `In Progress` yapılmaz. Adımlar:
| Adım | Araç | Durum |
| --- | --- | --- |
| 1. Kuyruğu oku (yalnızca toplu) | `query_list` — `ListFormCode: App.DevRequests`, filtre `["Status","=","Ready to Start"]`, sıralama `SortOrder` | — |
| 2. Talebi üstlen | `start_dev_request` — Id ya da tam başlık. Yarım kalmış talebe dönüşte `get_dev_request` | In Progress |
| 3. Üret | §0.3 keşif → `create_table` / `deploy_wizard` / `save_component`; talep demo veri taşıyorsa `SeedData: true` + `insert_row` (§0.10.1); bu dosyanın kuralları aynen geçerlidir | — |
| 4. Test et | `update_row` → `Testing`, ardından üretilen her wizard için `verify_wizard` | Testing |
| 5a. Geçti | `update_row` → `Staging` + sonuç kaydı | Staging |
| 5b. Kaldı | `update_row` → `In Progress` + sonuç kaydı; düzelt ve 4'e dön ya da bırak | In Progress |
**Sonuç kaydı** şema değiştirmeden mevcut alanlara yazılır:
- `SubTasks` — her üretilen artefakt bir satır: `{ "id": "<guid>", "title": "deploy_wizard: Mrp.Orders.json", "assignee": "", "dueDate": null, "completed": true }`.
Var olan satırları koru, sona ekle (alan JSON dizisi metnidir).
- `Description` — `append_dev_request_note` ile sonuna `<h4>MCP</h4>` başlığıyla `verify_wizard`
özeti: geçen/kalan kontroller, `seed_closure` uyarıları. `Description` hiçbir zaman `update_row`
ile yazılmaz — base64 görselleri taşır ve yeniden yazmak onları siler.
**Görseller.** Talebe yapıştırılan ekran görüntüleri açıklamada base64 durur; araç onları metinden
ayıklar ve ayrı resim içeriği olarak verir, açıklamada yerlerinde `[image-N]` kalır. Kullanıcıdan
görseli yeniden istemezsin. Görsel talebin parçasıdır: ekran düzenini (§0.7 — piksel sözleşmesi
değildir) ve içindeki satırları taşır; satırlar demo veridir (§0.10.1).
**Kurallar**
- Talep §0.6 keşif turunu cevaplayacak kadar net değilse üretme; `Backlog`'a geri al ve eksik
soruları `Description` sonuna yaz. Tabloya yazma hedefi, onaycı ve hesap formülü talepten
okunamıyorsa varsayılmaz.
- Çerçeve dışı bir ihtiyaç (§0.8) çıkarsa talebi `In Progress`'te bırak ve kullanıcıya sor.
- `verify_wizard` veri yazmaz; test için ekrana satır ekleme — SeedSync açık ekranda seed dosyasına iz bırakır. Talebin kendi demo verisi test satırı değildir, §0.10.1 ile eklenir.
- `Staging` son durumdur: ekran bu ortamda o anda kullanılabilir, çünkü deploy canlı veritabanına yazar.
**Migrate** döngünün parçası değildir ve MCP'den tetiklenmez. Parti bitince yetkili host
kullanıcısı `/api/setup/migrate` akışını elle çalıştırır (`Setup.Migrate`; Redis'i temizler ve
bütün kiracıları dolaşır). Gerekli olduğu durumlar: ayrı veritabanı kullanan kiracılara
tablo/script taşımak, seed dosyalarından başka bir ortam ya da sıfırlanmış veritabanı kurmak.
Parti raporunda `seed_closure` uyarısı veren ekranlar ayrıca belirtilir; migrate onları kuramaz.
## 1. Hangi istek → hangi artefakt ## 1. Hangi istek → hangi artefakt
| İstek | Üretilecek artefakt | Bölüm | | İstek | Üretilecek artefakt | Bölüm |
@ -808,6 +907,7 @@ grant eder; `WizardDataSeeder` grant yazmaz — migrate ile gelen ekranın rol y
| --- | --- | | --- | --- |
| `IsTenant` / `IsBranch` / `IsOrganizationUnit` | `[ListForm]` Otomatik kırılım filtresi. Tabloda `TenantId` varsa `IsTenant: true` **olmalı**. | | `IsTenant` / `IsBranch` / `IsOrganizationUnit` | `[ListForm]` Otomatik kırılım filtresi. Tabloda `TenantId` varsa `IsTenant: true` **olmalı**. |
| `AllowAdding` / `AllowUpdating` / `AllowDeleting` / `ConfirmDelete` | `[ListForm]` Toolbar ve satır aksiyonları (`EditingOptionDto`). | | `AllowAdding` / `AllowUpdating` / `AllowDeleting` / `ConfirmDelete` | `[ListForm]` Toolbar ve satır aksiyonları (`EditingOptionDto`). |
| `SeedData` | `[ListForm]` `true` ise `SeedFilePath = data/{ListFormCode}.json` ve `SeedSyncInsert/Update/Delete` açık üretilir; ekrandan girilen satır dosyaya yazılır, migrate geri yükler. Wizard ekranında sorulmaz; demo veri taşıyan MCP üretimi için (§0.10.1). Varsayılan `false`. |
| `AllowDetail` | `[ListForm]` Satırda **Detay** düğmesi çıkar; `/admin/form/{ListFormCode}/{anahtar}` adresini yeni sekmede açar — aynı ekranın tek kayıtlık form görünümüdür. `KeyFieldName` boşsa çalışmaz. | | `AllowDetail` | `[ListForm]` Satırda **Detay** düğmesi çıkar; `/admin/form/{ListFormCode}/{anahtar}` adresini yeni sekmede açar — aynı ekranın tek kayıtlık form görünümüdür. `KeyFieldName` boşsa çalışmaz. |
| `DefaultLayout` | `[ListForm]` `"grid"`, `"card"`, `"pivot"`, `"chart"`, `"tree"`, `"gantt"`, `"scheduler"`, `"todo"`. | | `DefaultLayout` | `[ListForm]` `"grid"`, `"card"`, `"pivot"`, `"chart"`, `"tree"`, `"gantt"`, `"scheduler"`, `"todo"`. |
| `Grid` `Card` `Pivot` `Chart` `Tree` `Gantt` `Scheduler` `Todo` | `[ListForm]` Hangi görünümlerin açık olduğu (`LayoutDto`). **Liste ekranında `Grid`/`Card`/`Pivot`/`Chart` varsayılan `true`**, diğer dördü `false` (§9.8). Açtığın her görünümün option bloğunu da doldur. | | `Grid` `Card` `Pivot` `Chart` `Tree` `Gantt` `Scheduler` `Todo` | `[ListForm]` Hangi görünümlerin açık olduğu (`LayoutDto`). **Liste ekranında `Grid`/`Card`/`Pivot`/`Chart` varsayılan `true`**, diğer dördü `false` (§9.8). Açtığın her görünümün option bloğunu da doldur. |
@ -1716,7 +1816,7 @@ bölümünü doldurur.** Keşif turu (§0.6) soruları da bu adımlarla bire bir
| 0 | Komponent Tipi | her zaman | `ComponentKind` (`0` List · `1` Custom) | | 0 | Komponent Tipi | her zaman | `ComponentKind` (`0` List · `1` Custom) |
| 1 | Custom Component | yalnız Custom yolu | `CustomComponentName`, `MenuUrl` (bileşenin `RoutePath`'i) | | 1 | Custom Component | yalnız Custom yolu | `CustomComponentName`, `MenuUrl` (bileşenin `RoutePath`'i) |
| 2 | Menü | her zaman | `WizardName`, `CreateMenu`, `MenuParentCode`, `MenuCode`, `Language.MenuEn/Tr`, `PermissionGroupName` (+ görünen adlar), `MenuIcon`, `MenuOrder` | | 2 | Menü | her zaman | `WizardName`, `CreateMenu`, `MenuParentCode`, `MenuCode`, `Language.MenuEn/Tr`, `PermissionGroupName` (+ görünen adlar), `MenuIcon`, `MenuOrder` |
| 3 | Ayarlar | yalnız List yolu | `ListFormCode`, `DataSourceCode` (+`DataSourceConnectionString`), `SelectCommandType`, `SelectCommand`, `KeyFieldName`, `KeyFieldDbSourceType`, `IsTenant/IsBranch/IsOrganizationUnit`, `AllowAdding/Updating/Deleting`, `ConfirmDelete`, `AllowDetail`, `DefaultLayout` + 8 görünüm bayrağı, `Language.TitleEn/Tr`, `Language.DescEn/Tr` | | 3 | Ayarlar | yalnız List yolu | `ListFormCode`, `DataSourceCode` (+`DataSourceConnectionString`), `SelectCommandType`, `SelectCommand`, `KeyFieldName`, `KeyFieldDbSourceType`, `IsTenant/IsBranch/IsOrganizationUnit`, `AllowAdding/Updating/Deleting`, `ConfirmDelete`, `AllowDetail`, `DefaultLayout` + 8 görünüm bayrağı (`SeedData` adımda sorulmaz, düzenlemede korunur), `Language.TitleEn/Tr`, `Language.DescEn/Tr` |
| 4 | Alanlar | yalnız List | `Groups[]` (aşağıda) | | 4 | Alanlar | yalnız List | `Groups[]` (aşağıda) |
| 5 | Alt Formlar | yalnız List | `SubForms[]` | | 5 | Alt Formlar | yalnız List | `SubForms[]` |
| 6 | Widget'lar | yalnız List | `Widgets[]` | | 6 | Widget'lar | yalnız List | `Widgets[]` |
@ -3301,7 +3401,7 @@ bağlıdır; hiçbiri "kod yaz" adımının yerine geçmez, hepsi ondan öncedir
| **Components** | `/admin/list/App.DeveloperKit.CustomComponents` | `App.DeveloperKit.CustomComponents` | Custom Component + Visual Designer | 7 | | **Components** | `/admin/list/App.DeveloperKit.CustomComponents` | `App.DeveloperKit.CustomComponents` | Custom Component + Visual Designer | 7 |
| **ListForm** | `/admin/list/App.Listforms.Listform` | `App.Listforms.Listform` | Var olan ekranların ham tanımı | 9.6 | | **ListForm** | `/admin/list/App.Listforms.Listform` | `App.Listforms.Listform` | Var olan ekranların ham tanımı | 9.6 |
| **Wizard Manager** | `/admin/listform/wizardManager` | `App.Listforms.Wizard` | Wizard seed dosyalarının yönetimi (menü yolu, bağımlılık ağacı, silme), export/import | 4, 7.7 | | **Wizard Manager** | `/admin/listform/wizardManager` | `App.Listforms.Wizard` | Wizard seed dosyalarının yönetimi (menü yolu, bağımlılık ağacı, silme), export/import | 4, 7.7 |
| **MCP Servers** | `/admin/list/App.DeveloperKit.McpServers` | `App.DeveloperKit.McpServers` (+ `.Invoke`, `.Write`, `.Develop`) | Dış yapay zekâ istemcisinin `/mcp` erişimi ve açık araçlar (`AllowedTools`) | 0.10 | | **MCP Servers** | `/admin/list/App.DeveloperKit.McpServers` | `App.DeveloperKit.McpServers` (+ `.Invoke`, `.Write`) | Dış yapay zekâ istemcisinin `/mcp` erişimi ve açık araçlar (`AllowedTools`) | 0.10 |
### 9.5 SQL Query Manager — bileşen bileşen ### 9.5 SQL Query Manager — bileşen bileşen

139
README.md
View file

@ -1367,13 +1367,14 @@ kiracılara açıktır (AiBot ile aynı sözleşme). Araç listesi bu kayda gör
(`AddListToolsFilter`) **ve** çağrı anında aynı kural yeniden uygulanır — süzme tek başına (`AddListToolsFilter`) **ve** çağrı anında aynı kural yeniden uygulanır — süzme tek başına
bir yetki sınırı değildir. bir yetki sınırı değildir.
**Kiracı kullanıcısı yalnızca okur.** Kiracı bağlamındaki istekte açık araçlar **Kiracı veri okur ve yazar, geliştirme yapamaz.** Kiracı bağlamındaki istekte açık araçlar
`McpToolNames.TenantTools` kümesiyle sınırlıdır: `server_info`, `list_lists`, `describe_list`, `McpToolNames.TenantTools` kümesiyle sınırlıdır: `server_info`, `list_lists`, `describe_list`,
`search_schema`, `query_list`, `run_select`. `AllowedTools` bu kümeyi yalnızca daraltabilir; `search_schema`, `query_list`, `run_select`, `insert_row`, `update_row`, `delete_row`.
kiracının `.Write`/`.Develop` ya da başka bir yetkisi olsa da veri yazma, `create_table`, wizard ve `AllowedTools` bu kümeyi yalnızca daraltabilir. Satır araçları ekranın C/U/D sözleşmesinden ve
component araçları listelenmez, çağrılırsa `App.Mcp.Error.TenantReadOnly` ile reddedilir. kiracının kendi veritabanından geçer. `create_table`, wizard, component ve geliştirme talebi
`list_queries`/`run_query` da dışarıdadır: procedure ve Custom Endpoint çağrısı veri araçları kiracının yetkisi ne olursa olsun listelenmez, çağrılırsa `App.Mcp.Error.TenantToolDisabled`
yazabileceği için okuma olduğu garanti edilemez. Host kullanıcısı için tek sunucu anahtarı ile reddedilir. `list_queries`/`run_query` da dışarıdadır: procedure ve Custom Endpoint çağrısı
ekran yetkisinden geçmeden veri yazabilir. Host kullanıcısı için tek sunucu anahtarı
`AllowedTools`'tur (boşsa tümü); sınırı yetkiler çizer. Karar `McpToolPolicy.IsAllowed`'dadır, `AllowedTools`'tur (boşsa tümü); sınırı yetkiler çizer. Karar `McpToolPolicy.IsAllowed`'dadır,
liste süzgeci ve çağrı kapısı aynı kuralı kullanır. liste süzgeci ve çağrı kapısı aynı kuralı kullanır.
@ -1385,32 +1386,110 @@ liste süzgeci ve çağrı kapısı aynı kuralı kullanır.
| `list_lists` | Listeler: kod, başlık, açıklama, arkasındaki tablo. | | `list_lists` | Listeler: kod, başlık, açıklama, arkasındaki tablo. |
| `describe_list` | Bir listenin sütunları: alan adı, görünen başlık, SQL tipi, lookup ilişkisi, `isSensitive`. | | `describe_list` | Bir listenin sütunları: alan adı, görünen başlık, SQL tipi, lookup ilişkisi, `isSensitive`. |
| `search_schema` | Liste ve sütun adları/başlıkları üzerinde aksan ve büyük/küçük harf duyarsız arama. | | `search_schema` | Liste ve sütun adları/başlıkları üzerinde aksan ve büyük/küçük harf duyarsız arama. |
| `query_list` | Listenin verisini ekranın kendi sorgusuyla (`GetSelectAsync`) okur: filtre, sıralama, gruplama, özet (DevExtreme biçimi). Normal kullanıcının veri okuma yolu. | | `query_list` | Listenin verisini ekranın kendi sorgusuyla (`GetSelectAsync`) okur: filtre, sıralama, gruplama, özet (DevExtreme biçimi). Sıralama/gruplama/özet JSON'u, arayüzün `Utils.ts`'te yaptığı gibi select ucunun metin biçimine (`Alan desc|Alan2 asc`) `McpLoadOptions` ile çevrilir; select ucu JSON'u tanımaz. Normal kullanıcının veri okuma yolu. |
| `run_select` | Parametreli tek SELECT; çoklu ifade, DDL ve DML reddedilir, satır tavanı sorguya yazılır. Yalnızca SQL Query Manager yetkisiyle. | | `run_select` | Parametreli tek SELECT; çoklu ifade, DDL ve DML reddedilir, satır tavanı sorguya yazılır. Yalnızca SQL Query Manager yetkisiyle. |
| `list_queries` | View/procedure/function nesneleri (SQL Query Manager yetkisiyle) + çağrılabilir Custom Endpoint'ler (`App.DeveloperKit.CustomEndpoints.Get` ile). | | `list_queries` | View/procedure/function nesneleri (SQL Query Manager yetkisiyle) + çağrılabilir Custom Endpoint'ler (`App.DeveloperKit.CustomEndpoints.Get` ile). |
| `run_query` | Kayıtlı bir nesneyi ya da Custom Endpoint'i parametreleriyle çağırır; yetkiler `list_queries` ile aynı. | | `run_query` | Kayıtlı bir nesneyi ya da Custom Endpoint'i parametreleriyle çağırır; yetkiler `list_queries` ile aynı. |
| `insert_row` / `update_row` / `delete_row` | ListForm ekranının insert/update/delete sözleşmesinden geçer. | | `insert_row` / `update_row` / `delete_row` | ListForm ekranının insert/update/delete sözleşmesinden geçer. |
| `get_dev_guide` | Üretim sözleşmesi: `lowcode` / `reference` belgelerinin başlık listesi ya da istenen bölümü (numarayla, örn. `4.2`). Çok büyük bir üst bölüm yalnızca giriş metni + alt başlıklarıyla döner. | | `get_dev_guide` | Üretim sözleşmesi: `lowcode` / `reference` belgelerinin başlık listesi ya da istenen bölümü (numarayla, örn. `4.2`). Çok büyük bir üst bölüm yalnızca giriş metni + alt başlıklarıyla döner. |
| `create_table` | Yeni tabloyu SQL Query Manager tablo tasarımcısıyla aynı script biçimiyle kurar (`McpTableCreator`): `Id` + `TenantId` + tam audit kolonları şablondan gelir, istemci yalnızca iş kolonlarını verir; tip ve varsayılan değer beyaz listeden geçer, istemci SQL yazamaz. Script `{kapsam}/sql/object/{Tablo}.sql` dosyasına yazılır. Yalnızca oluşturur — tablo varsa reddeder; yalnızca SQL Server. `.Develop` + `App.SqlQueryManager` ister. | | `create_table` | Yeni tabloyu SQL Query Manager tablo tasarımcısıyla aynı script biçimiyle kurar (`McpTableCreator`): `Id` + `TenantId` + tam audit kolonları şablondan gelir, istemci yalnızca iş kolonlarını verir; tip ve varsayılan değer beyaz listeden geçer, istemci SQL yazamaz. Script `{kapsam}/sql/object/{Tablo}.sql` dosyasına yazılır. Yalnızca oluşturur — tablo varsa reddeder; yalnızca SQL Server. `.Write` + `App.SqlQueryManager` ister. |
| `list_wizards` / `get_wizard` | Wizard File Manager dosyaları; `get_wizard` seed dosyasını diskteki biçimiyle döndürür. | | `list_wizards` / `get_wizard` | Wizard File Manager dosyaları; `get_wizard` seed dosyasını diskteki biçimiyle döndürür. |
| `deploy_wizard` | Wizard'ı Wizard ekranıyla aynı yoldan (`IListFormWizardAppService.Create`) deploy eder. Girdi seed dosyası biçimi (`{"Wizard": …}`) ya da düz cevaplardır; ikisi de `WizardSeedFileDto.FromSeedJson`'dan geçer. `replaceFileName` doluysa güncellemedir (eski dosya ve ürettiği kayıtlar silinip yeniden üretilir). | | `deploy_wizard` | Wizard'ı Wizard ekranıyla aynı yoldan (`IListFormWizardAppService.Create`) deploy eder. Girdi seed dosyası biçimi (`{"Wizard": …}`) ya da düz cevaplardır; ikisi de `WizardSeedFileDto.FromSeedJson`'dan geçer. `replaceFileName` doluysa güncellemedir (eski dosya ve ürettiği kayıtlar silinip yeniden üretilir). İstek demo veri taşıyorsa (prompt'taki satırlar, ekran görüntüsü) wizard `SeedData: true` ile deploy edilir — ekranın `SeedFilePath`'i `data/{ListFormCode}.json` olur ve `SeedSync*` bayrakları açılır — ardından satırlar `insert_row` ile eklenir; `ListFormSeedDataSynchronizer` onları `configs/seeds/host/data/` altına yazar, migrate ekranla birlikte geri yükler (`lowcode.instructions.md` §0.10.1). |
| `delete_wizard` | Wizard Manager'daki silmeyle aynı: yalnızca `InsertedRecords`, bağımlı wizard varsa reddedilir. | | `delete_wizard` | Wizard Manager'daki silmeyle aynı: yalnızca `InsertedRecords`, bağımlı wizard varsa reddedilir. |
| `verify_wizard` | Deploy edilmiş ekranı veri yazmadan doğrular: menü kaydı ve URL'si, menünün istediği yetki tanımı (`AbpPermissions`), List yolunda ekran sorgusu (`GetSelectAsync`, tek satır, çağıranın yetkisiyle), Custom yolunda bileşen (etkin, rota = menü URL'si) ve seed kapanışı (`ExportAnalysis` uyarıları; engellemez). `App.DevRequests` geliştirme döngüsünün test adımıdır (`lowcode.instructions.md` §0.11). |
| `start_dev_request` | Geliştirme talebini Id ya da tam başlıkla üstlenir: yalnızca `Ready to Start` durumundakini `In Progress` durumuna alır, diğerlerini reddeder; talebin metnini döndürür. Açıklamaya yapıştırılmış base64 görseller (`data:image/png|jpeg|gif|webp`) metinden ayıklanır (`McpDevRequestImages`), yerlerinde `[image-N]` kalır ve her biri ayrı MCP resim içeriği (`ImageContentBlock`) olarak döner — model görseli resim olarak görür, görsel hiçbir yere yüklenmez. `.Write` + `App.DevRequests.Update` ister. |
| `get_dev_request` | Talebi durumuna dokunmadan, `start_dev_request` ile aynı biçimde (metin + resim içerikleri) okur; yarım kalan talebe dönmek için. `App.DevRequests` ister. |
| `append_dev_request_note` | Talep açıklamasının sonuna HTML ekler, var olan metne ve görsellere dokunmaz. Açıklama base64 görsel taşıdığı için `update_row` ile yeniden yazılmaz. `.Write` + `App.DevRequests.Update` ister. |
**Prompt'lar** (`McpPromptNames`, `McpDevelopPrompts`). İstemcide komut olarak görünür; yalnızca
metindir, yetki taşımaz. `process_dev_request` tek talebi, `process_ready_requests` bütün
`Ready to Start` kuyruğunu işler. İkisi de adımları tekrar etmez, `get_dev_guide` ile
`lowcode.instructions.md` §0.11'e yönlendirir.
**Geliştirme taleplerini çalıştırma.** Komutun adı istemcideki sunucu adına göre değişir: aşağıdaki
örnekler `claude mcp add sozsoft ...` ile eklenmiş sunucu içindir (`sozsoft-local` ile eklediysen
`mcp__sozsoft-local__...` yaz). Her durumda talep yalnızca `Ready to Start` durumundaysa işlenir;
değilse sunucu reddeder ve hiçbir şey üretilmez.
| Ne istiyorsun | Claude Code | Claude.ai / Claude Desktop | Düz metin (her istemci) |
| --- | --- | --- | --- |
| Tek talep | `/mcp__sozsoft__process_dev_request <Id>` | Mesaj kutusundaki `+` menüsü → `sozsoft` → `process_dev_request`, `request` alanına Id ya da tam başlık | `<Id ya da "başlık"> talebini işle` |
| Bütün kuyruk | `/mcp__sozsoft__process_ready_requests` | `+` menüsü → `sozsoft` → `process_ready_requests` | `Ready to Start durumundaki talepleri sırayla işle` |
| Önce kuyruğu görmek | — | — | `Ready to Start durumundaki talepleri Id'leriyle listele` |
```text
/mcp__sozsoft__process_dev_request 3f2a9c1e-5b7d-4e2a-9f10-6c8d2b4a1e77
/mcp__sozsoft__process_ready_requests
```
- **Id nereden gelir:** `App.DevRequests` ekranında `Id` kolonu gizlidir. Kolon seçiciden aç ya da
Claude'dan "Id'leriyle listele" ile iste.
- **Başlıkla çağırmak:** Claude Code komut argümanlarını boşluktan böler, bu yüzden boşluklu
başlıkta komut yerine Id ya da düz metin kullan. Başlık birden fazla talebe uyuyorsa sunucu Id ister.
- Düz metin de aynı yoldan geçer: sunucu talimatları modeli `start_dev_request`'e yönlendirir,
durum şartı yine sunucuda uygulanır.
| `list_components` / `get_component` / `save_component` / `delete_component` | Custom component'ler `ICustomComponentAppService` üzerinden; seed senkronu ve `DataSources` çözümü aynen çalışır. `save_component` ada göre ekler/günceller, tasarımcı belgesi (`Props.visualDesigner`, `sourceMode ≠ code`) taşıyan bileşene kod yazmayı reddeder — kod belgeden üretildiği için tasarımcıdaki ilk kayıtta ezilirdi. | | `list_components` / `get_component` / `save_component` / `delete_component` | Custom component'ler `ICustomComponentAppService` üzerinden; seed senkronu ve `DataSources` çözümü aynen çalışır. `save_component` ada göre ekler/günceller, tasarımcı belgesi (`Props.visualDesigner`, `sourceMode ≠ code`) taşıyan bileşene kod yazmayı reddeder — kod belgeden üretildiği için tasarımcıdaki ilk kayıtta ezilirdi. |
**Yetki modeli: MCP yetki vermez.** Kullanıcı MCP'de uygulamadaki yetkisini taşır: **Yetki modeli: MCP yetki vermez.** Kullanıcı MCP'de uygulamadaki yetkisini taşır; MCP'nin iki
- Şema araçları yalnızca kullanıcının okuma yetkisi olan listeleri gösterir (`McpListFormAccessFilter` yetkisi yalnızca kapıdır. Bir çağrı üç katmandan geçer ve her katman ayrı reddeder:
— ListForm `PermissionJson.R`, ekranla aynı karar; tek sorgu + toplu yetki kontrolü).
- `query_list` ekranın sorgusundan geçer: liste ve alan (`CanRead`) yetkileri, kiracı/şube 1. **Sunucu kaydı** (`AllowedTools`, `Tenants`) — o kiracıda hangi araçların var olduğu. Kiracıdaki
filtreleri, kullanıcı özelleştirmeleri uygulamadaki gibidir. herkes için aynıdır.
- Ham SQL (`run_select`, veritabanı nesneleri) liste yetkilerini aşar; yalnızca uygulamada 2. **MCP kapısı** (`.Invoke`, `.Write`) — kullanıcının MCP'yi okumak / değiştirmek için
`App.SqlQueryManager` yetkisi olana açıktır. Yazma araçları `.Write` + ekranın C/U/D yetkisini ister. kullanabilir olup olmadığı. Rol/kullanıcı bazındadır.
- Geliştirme araçları (`McpToolNames.DevelopTools`) `.Develop` yetkisini ister; sunucu bazında 3. **Uygulama yetkisi** — işlemin kendisi: ekranın C/R/U/D yetkisi, Wizard, Custom Components,
`AllowedTools` ile kapatılır. `create_table` ham DDL olduğu için tablo tasarımcısıyla aynı SQL Query Manager. Kullanıcı uygulamada yapamadığını MCP'de de yapamaz.
`App.SqlQueryManager` yetkisini de ister. `.Develop` yalnızca MCP kapısıdır:
işlem Wizard (`App.Listforms.Wizard` + `.Create/.Update/.Delete`) ve Custom Components **MCP yetkileri — ne açar, sınırı ne**
(`App.DeveloperKit.CustomComponents` + `.Create/.Update/.Delete`) yetkilerinden ayrıca geçer.
- Araç listesi bu yetkilere göre süzülür (`GetAvailableToolsAsync`); çağrı anında aynı kontrol | Yetki | Kim alabilir | Ne açar | Açmadığı / sınırı |
tekrarlanır. Araç → yetki eşlemesi `McpToolNames.RequiredPermissions`'tadır. | --- | --- | --- | --- |
| `App.DeveloperKit.McpServers` | Host, kiracı | MCP Servers ekranını görme (`.Create/.Update/.Delete/.Export/.Import` ekran işlemleri) | `/mcp` ucuna erişim vermez |
| `App.DeveloperKit.McpServers.Invoke` | Host, kiracı | `/mcp` bağlantısı ve bütün okuma araçları: `server_info`, `list_lists`, `describe_list`, `search_schema`, `query_list`; host'ta ayrıca `get_dev_guide`, `list_wizards`, `get_wizard`, `verify_wizard`, `list_components`, `get_component` | Hiçbir şey yazmaz. Okuma da ekran yetkisine bağlıdır: `R` yetkisi olmayan liste görünmez, sorgulanamaz. Ham SQL (`run_select`, `list_queries`, `run_query`) ayrıca `App.SqlQueryManager` ister |
| `App.DeveloperKit.McpServers.Write` | Host, kiracı | MCP üzerinden her değişiklik. **Kiracıda:** `insert_row`, `update_row`, `delete_row`. **Host'ta:** bunlara ek olarak `create_table`, `deploy_wizard`, `delete_wizard`, `save_component`, `delete_component`, `start_dev_request`, `append_dev_request_note` | Kendi başına bir şey yazdırmaz: satır araçları ekranın `C`/`U`/`D` yetkisini, geliştirme araçları Wizard / Custom Components / SQL Query Manager / `App.DevRequests.Update` yetkisini ayrıca ister. Kiracıda geliştirme araçlarını **açmaz** — kiracı bağlamında bu araçlar yetkiden bağımsız kapalıdır (`McpToolNames.TenantTools`). `Invoke` olmadan işe yaramaz |
**Araç → gereken yetkiler** (Invoke hepsinde aranır, tabloda tekrar yazılmadı)
| Araç | Kiracı | MCP kapısı | Uygulama yetkisi |
| --- | --- | --- | --- |
| `server_info`, `list_lists`, `describe_list`, `search_schema` | ✓ | — | Listelenen her ekran için `PermissionJson.R` |
| `query_list` | ✓ | — | Ekranın `R` yetkisi |
| `run_select` | ✓ | — | `App.SqlQueryManager` |
| `list_queries`, `run_query` | — | — | Nesne: `App.SqlQueryManager`; Custom Endpoint: `App.DeveloperKit.CustomEndpoints.Get` + endpoint'in kullanıcı/rol listesi |
| `insert_row` / `update_row` / `delete_row` | ✓ | `.Write` | Ekranın `C` / `U` / `D` yetkisi |
| `get_dev_guide` | — | — | — |
| `list_wizards`, `get_wizard` | — | — | `App.Listforms.Wizard` |
| `verify_wizard` | — | — | `App.Listforms.Wizard`; seed kapanışı için `.Export` (yoksa o kontrol atlanır, doğrulama düşmez) |
| `deploy_wizard` | — | `.Write` | `App.Listforms.Wizard` + `.Create` (yeni) / `.Update` (`replaceFileName`) |
| `delete_wizard` | — | `.Write` | `App.Listforms.Wizard` + `.Delete` |
| `create_table` | — | `.Write` | `App.SqlQueryManager` |
| `list_components`, `get_component` | — | — | `App.DeveloperKit.CustomComponents` |
| `save_component` | — | `.Write` | `App.DeveloperKit.CustomComponents.Create` (yeni) / `.Update` |
| `delete_component` | — | `.Write` | `App.DeveloperKit.CustomComponents.Delete` |
| `start_dev_request` | — | `.Write` | `App.DevRequests.Update` |
| `get_dev_request` | — | — | `App.DevRequests` |
| `append_dev_request_note` | — | `.Write` | `App.DevRequests.Update` |
Kaynak: MCP kapısı `McpToolNames.RequiredPermissions`, kiracı kümesi `McpToolNames.TenantTools`,
arkadaki servisin attribute ile istediği yetkiler `McpToolNames.ServicePermissions`. `Invoke` her MCP
servisinin sınıf düzeyindeki `[Authorize]` attribute'undadır; `/mcp` ucu yalnızca kimlik doğrulama
ister (ucun kendisinde aranırsa kullanıcı nedeni yazmayan bir 403 alır). Araç listesi aynı
kurallarla süzülür (`GetAvailableToolsAsync`), çağrı anında kontrol tekrarlanır.
**Yetkisi olmayan çağrı ne görür.** SDK araç hatasının mesajını yalnızca `McpException` ise
iletir; diğer her istisna "An error occurred invoking '…'" olarak gider. `McpToolErrorTranslator`
(çağrı ve araç listesi filtresi, `McpHttpApiModule`) iki tür hatayı çevirir:
- **Yetki reddi** → `App.Mcp.Error.PermissionDenied`: "'deploy_wizard' için gereken yetkiniz yok:
Wizard (App.Listforms.Wizard). Yöneticinizden bu yetkiyi isteyin." Eksik yetkiler aracın bilinen
yetkileri (`McpToolNames.KnownPermissionsOf`) ve reddin taşıdığı yetki adı kullanıcının
yetkileriyle karşılaştırılarak bulunur. Satır ve `query_list` araçları ekranın yetkisini adıyla
önceden ister (`IMcpListFormAccessFilter.GetPermissionNameAsync`), bu yüzden ekran yetkisi de
adıyla görünür. Eksik yetki belirlenemezse `PermissionDeniedUnspecified` döner.
- **İş kuralı hatası** (`UserFriendlyException`: kiracıya kapalı araç, `AllowedTools` dışı araç,
`start_dev_request` durum reddi…) → yerelleştirilmiş mesajı olduğu gibi.
Başka istisnalar çevrilmez; SQL ve yığın ayrıntısı istemciye sızmaz.
**Geliştirme rehberi nereden okur.** `get_dev_guide` depodaki **Geliştirme rehberi nereden okur.** `get_dev_guide` depodaki
`.github/instructions/lowcode*.instructions.md` dosyalarını runtime'da okur; Claude Code ile MCP `.github/instructions/lowcode*.instructions.md` dosyalarını runtime'da okur; Claude Code ile MCP
@ -1501,11 +1580,15 @@ curl -s https://api.sozsoft.com/.well-known/oauth-authorization-server \
Dördü de `https://` olmalıdır. Dördü de `https://` olmalıdır.
**Yetkiler.** `App.DeveloperKit.McpServers` (ekran), `.Invoke` (araç çağırma), `.Write` **Yetkiler.** Ne açtıkları ve sınırları yukarıdaki "Yetki modeli" tablosundadır. İkisi de
(veri yazma) ve `.Develop` (tablo/ekran/bileşen geliştirme). `.Invoke` `MultiTenancySide = Both` `MultiTenancySide = Both`'tur. Eskiden geliştirme için ayrı olan `.Develop` yetkisi `.Write` ile
olduğu için kiracı kullanıcısına da verilir; `.Write` da kiracıya tanımlanabilir ama MCP'de birleştirildi; mevcut veritabanında atamalar migrate sırasında `.Write`'a taşınır ve tanım silinir
kiracı salt okuma kümesiyle sınırlı olduğu için etkisi yoktur. `.Develop` host'tur — (`McpDevelopPermissionMergeDataSeedContributor`).
arkasındaki Wizard ve Custom Components yetkileri de yalnızca host'ta tanımlıdır.
`AllowedTools` ile yetkiler farklı eksenlerdir: sunucu kaydı bir kiracıda **hangi araçların var
olduğunu** belirler ve o kiracının bütün kullanıcıları için aynıdır; yetkiler **kimin
kullanabileceğini** rol/kullanıcı bazında belirler. Örneğin `delete_row` açık bir kayıtta yalnızca
`.Write` verilmiş rol MCP ile silebilir, diğerleri aynı bağlantıyla yalnızca okur.
--- ---

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
namespace Sozsoft.Mcp.Dtos; namespace Sozsoft.Mcp.Dtos;
@ -92,6 +93,84 @@ public class McpCreateTableResultDto
public string SeedFilePath { get; set; } = string.Empty; public string SeedFilePath { get; set; } = string.Empty;
} }
/// <summary>
/// Deploy edilmis bir wizard'in salt-okur dogrulamasi. <see cref="Passed"/> yalnizca
/// <see cref="McpVerifyCheckDto.Blocking"/> bir kontrol kaldiginda false olur; engellemeyen
/// kontroller (seed kapanisi gibi) ekrani kullanilamaz yapmaz ama migrate sonrasini etkiler.
/// </summary>
public class McpVerifyResultDto
{
public string FileName { get; set; } = string.Empty;
public string? ListFormCode { get; set; }
public string? MenuUrl { get; set; }
public bool Passed { get; set; }
public List<McpVerifyCheckDto> Checks { get; set; } = [];
}
public class McpVerifyCheckDto
{
/// <summary>Kontrolun kisa adi: <c>wizard_file</c>, <c>menu</c>, <c>permission</c>, <c>select</c>, <c>component</c>, <c>seed_closure</c>.</summary>
public string Name { get; set; } = string.Empty;
public bool Passed { get; set; }
public bool Blocking { get; set; } = true;
public string? Message { get; set; }
}
/// <summary>Ustlenilen gelistirme talebi; uretim icin gereken metni tasir.</summary>
public class McpDevRequestDto
{
public Guid Id { get; set; }
public string? Title { get; set; }
public string Status { get; set; } = string.Empty;
/// <summary>
/// HTML. Gomulu gorseller (<c>data:image/...;base64</c>) metinden cikarilir; yerlerinde
/// <see cref="McpDevRequestImageDto.Placeholder"/> durur, kendileri <see cref="Images"/>'tadir.
/// </summary>
public string? Description { get; set; }
/// <summary>Aciklamadaki gorseller, metinde gectikleri sirayla.</summary>
public List<McpDevRequestImageDto> Images { get; set; } = [];
public string? Priority { get; set; }
public string? Tags { get; set; }
public DateTime? DueDate { get; set; }
public string UserName { get; set; } = string.Empty;
public Guid? TenantId { get; set; }
/// <summary>JSON dizisi metni: <c>{ id, title, assignee, dueDate, completed }</c>.</summary>
public string? SubTasks { get; set; }
}
/// <summary>
/// Talep aciklamasina gomulu bir gorsel. Veri metin cevabina yazilmaz; MCP araci onu ayri bir
/// gorsel icerigi olarak iletir, model de resmi dogrudan gorur.
/// </summary>
public class McpDevRequestImageDto
{
/// <summary>Aciklamada gorselin yerine yazilan isaret, ornegin <c>[image-1]</c>.</summary>
public string Placeholder { get; set; } = string.Empty;
public string MimeType { get; set; } = string.Empty;
/// <summary>Base64 veri.</summary>
[JsonIgnore]
public string Data { get; set; } = string.Empty;
}
/// <summary>Custom component ozeti; kod tasimaz.</summary> /// <summary>Custom component ozeti; kod tasimaz.</summary>
public class McpComponentSummaryDto public class McpComponentSummaryDto
{ {

View file

@ -27,6 +27,21 @@ public interface IMcpDevelopAppService : IApplicationService
Task<WizardDeleteResultDto> DeleteWizardAsync(string fileName); Task<WizardDeleteResultDto> DeleteWizardAsync(string fileName);
/// <summary>Deploy edilmis ekrani veri yazmadan dogrular.</summary>
Task<McpVerifyResultDto> VerifyWizardAsync(string fileName);
/// <summary>Id ya da basliga gore bulunan talebi, yalnizca Ready to Start durumundaysa In Progress'e alir.</summary>
Task<McpDevRequestDto> StartDevRequestAsync(string request);
/// <summary>Talebi durumuna dokunmadan okur; yarim kalan bir talebe donmek icindir.</summary>
Task<McpDevRequestDto> GetDevRequestAsync(string request);
/// <summary>
/// Aciklamanin sonuna HTML ekler. Aciklama gomulu gorsel tasidigi icin satir guncellemesiyle
/// yeniden yazilamaz; ekleme sunucuda yapilir.
/// </summary>
Task AppendDevRequestNoteAsync(string request, string html);
Task<List<McpComponentSummaryDto>> GetComponentsAsync(string? search); Task<List<McpComponentSummaryDto>> GetComponentsAsync(string? search);
Task<CustomComponentDto?> GetComponentAsync(string name); Task<CustomComponentDto?> GetComponentAsync(string name);

View file

@ -64,7 +64,7 @@ public abstract class McpAppService : ApplicationService
if (CurrentTenant.IsAvailable && !McpToolNames.TenantTools.Contains(toolName, StringComparer.Ordinal)) if (CurrentTenant.IsAvailable && !McpToolNames.TenantTools.Contains(toolName, StringComparer.Ordinal))
{ {
throw new UserFriendlyException(L[McpErrorCodes.TenantReadOnly, toolName]); throw new UserFriendlyException(L[McpErrorCodes.TenantToolDisabled, toolName]);
} }
if (!McpToolPolicy.IsAllowed(true, CurrentTenant.IsAvailable, config.AllowedTools, toolName)) if (!McpToolPolicy.IsAllowed(true, CurrentTenant.IsAvailable, config.AllowedTools, toolName))

View file

@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authorization;
using Sozsoft.Mcp.Dtos; using Sozsoft.Mcp.Dtos;
using Sozsoft.Mcp.Services; using Sozsoft.Mcp.Services;
using Sozsoft.Platform; using Sozsoft.Platform;
using Sozsoft.Platform.Enums;
using Sozsoft.Platform.ListForms.Select; using Sozsoft.Platform.ListForms.Select;
using Volo.Abp; using Volo.Abp;
@ -16,15 +17,16 @@ namespace Sozsoft.Mcp;
/// sozlesmesinden gecer; ekran yetkisi, tenant filtresi ve seed senkronizasyonu oradaki /// sozlesmesinden gecer; ekran yetkisi, tenant filtresi ve seed senkronizasyonu oradaki
/// yoldan aynen uygulanir. /// yoldan aynen uygulanir.
/// </summary> /// </summary>
[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Write)] [Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Invoke)]
public class McpDataAppService( public class McpDataAppService(
IMcpServerConfigProvider serverConfigProvider, IMcpServerConfigProvider serverConfigProvider,
IListFormDataAppService listFormDataAppService) IListFormDataAppService listFormDataAppService,
IMcpListFormAccessFilter accessFilter)
: McpAppService(serverConfigProvider), IMcpDataAppService : McpAppService(serverConfigProvider), IMcpDataAppService
{ {
public async Task<McpWriteResultDto> InsertRowAsync(McpRowRequestDto input) public async Task<McpWriteResultDto> InsertRowAsync(McpRowRequestDto input)
{ {
var request = await BuildRequestAsync(input, McpToolNames.InsertRow, requireKeys: false, requireValues: true); var request = await BuildRequestAsync(input, McpToolNames.InsertRow, AuthorizationTypeEnum.Create, requireKeys: false, requireValues: true);
var key = await listFormDataAppService.PostInsertAsync(request); var key = await listFormDataAppService.PostInsertAsync(request);
return new McpWriteResultDto { Key = ((object?)key)?.ToString() }; return new McpWriteResultDto { Key = ((object?)key)?.ToString() };
@ -32,7 +34,7 @@ public class McpDataAppService(
public async Task<McpWriteResultDto> UpdateRowAsync(McpRowRequestDto input) public async Task<McpWriteResultDto> UpdateRowAsync(McpRowRequestDto input)
{ {
var request = await BuildRequestAsync(input, McpToolNames.UpdateRow, requireKeys: true, requireValues: true); var request = await BuildRequestAsync(input, McpToolNames.UpdateRow, AuthorizationTypeEnum.Update, requireKeys: true, requireValues: true);
var affected = await listFormDataAppService.PostUpdateAsync(request); var affected = await listFormDataAppService.PostUpdateAsync(request);
return new McpWriteResultDto { AffectedRows = affected }; return new McpWriteResultDto { AffectedRows = affected };
@ -40,7 +42,7 @@ public class McpDataAppService(
public async Task<McpWriteResultDto> DeleteRowAsync(McpRowRequestDto input) public async Task<McpWriteResultDto> DeleteRowAsync(McpRowRequestDto input)
{ {
var request = await BuildRequestAsync(input, McpToolNames.DeleteRow, requireKeys: true, requireValues: false); var request = await BuildRequestAsync(input, McpToolNames.DeleteRow, AuthorizationTypeEnum.Delete, requireKeys: true, requireValues: false);
var affected = await listFormDataAppService.PostDeleteAsync(request); var affected = await listFormDataAppService.PostDeleteAsync(request);
return new McpWriteResultDto { AffectedRows = affected }; return new McpWriteResultDto { AffectedRows = affected };
@ -53,6 +55,7 @@ public class McpDataAppService(
private async Task<DataRequestDto> BuildRequestAsync( private async Task<DataRequestDto> BuildRequestAsync(
McpRowRequestDto input, McpRowRequestDto input,
string toolName, string toolName,
AuthorizationTypeEnum operation,
bool requireKeys, bool requireKeys,
bool requireValues) bool requireValues)
{ {
@ -65,6 +68,14 @@ public class McpDataAppService(
throw new UserFriendlyException(L[McpErrorCodes.ListFormCodeRequired]); throw new UserFriendlyException(L[McpErrorCodes.ListFormCodeRequired]);
} }
// Ekranin kendi kontrolu reddi adsiz bir hatayla bildirir; yetki burada adiyla istenir ki
// istemci hangi yetkinin eksik oldugunu gorsun.
var permission = await accessFilter.GetPermissionNameAsync(input.ListFormCode, operation);
if (permission is not null)
{
await AuthorizationService.CheckAsync(permission);
}
if (requireValues && (input.Values is null || input.Values.Count == 0)) if (requireValues && (input.Values is null || input.Values.Count == 0))
{ {
throw new UserFriendlyException(L[McpErrorCodes.ValuesRequired]); throw new UserFriendlyException(L[McpErrorCodes.ValuesRequired]);

View file

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Sozsoft.Mcp.Dtos;
namespace Sozsoft.Mcp;
/// <summary>
/// Talep aciklamasina yapistirilan gorseller HtmlEditor'da base64 <c>data:</c> URI olarak
/// saklanir. Model bu metni resim olarak goremez; gorseller ayiklanip MCP gorsel icerigi olarak
/// iletilir, metinde yerlerine bir isaret kalir. Yalnizca modelin okuyabildigi bicimler ayiklanir;
/// digerleri (svg gibi) metinde oldugu gibi birakilir.
/// </summary>
internal static partial class McpDevRequestImages
{
private static readonly HashSet<string> SupportedMimeTypes = new(StringComparer.OrdinalIgnoreCase)
{
"image/png", "image/jpeg", "image/gif", "image/webp"
};
[GeneratedRegex("""<img\b[^>]*?\bsrc\s*=\s*["']data:(?<mime>image/[\w.+-]+);base64,(?<data>[^"']+)["'][^>]*>""", RegexOptions.IgnoreCase)]
private static partial Regex EmbeddedImage();
public static (string? Html, List<McpDevRequestImageDto> Images) Extract(string? html)
{
var images = new List<McpDevRequestImageDto>();
if (string.IsNullOrEmpty(html))
{
return (html, images);
}
var text = EmbeddedImage().Replace(html, match =>
{
var mimeType = match.Groups["mime"].Value.ToLowerInvariant();
var data = Regex.Replace(match.Groups["data"].Value, @"\s+", string.Empty);
if (!SupportedMimeTypes.Contains(mimeType) || !IsBase64(data))
{
return match.Value;
}
var placeholder = $"[image-{images.Count + 1}]";
images.Add(new McpDevRequestImageDto { Placeholder = placeholder, MimeType = mimeType, Data = data });
return placeholder;
});
return (text, images);
}
private static bool IsBase64(string data)
=> Convert.TryFromBase64String(data, new byte[data.Length * 3 / 4], out _);
}

View file

@ -9,25 +9,34 @@ using Sozsoft.Mcp.Dtos;
using Sozsoft.Mcp.Services; using Sozsoft.Mcp.Services;
using Sozsoft.Platform; using Sozsoft.Platform;
using Sozsoft.Platform.DeveloperKit; using Sozsoft.Platform.DeveloperKit;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums;
using Sozsoft.Platform.ListForms; using Sozsoft.Platform.ListForms;
using Sozsoft.Platform.ListForms.Select;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.PermissionManagement;
namespace Sozsoft.Mcp; namespace Sozsoft.Mcp;
/// <summary> /// <summary>
/// Ekran ve bilesen gelistirme yuzeyi. Uretim Wizard ve Custom Components servislerinden /// Ekran ve bilesen gelistirme yuzeyi. Uretim Wizard ve Custom Components servislerinden
/// gecer: menu, yetki, dil anahtari, seed dosyasi ve bagimlilik kurallari ekrandaki yolla /// gecer: menu, yetki, dil anahtari, seed dosyasi ve bagimlilik kurallari ekrandaki yolla
/// aynidir, yetki kontrolu de o servislerin kendi politikalarindan gelir. Develop yetkisi /// aynidir, yetki kontrolu de o servislerin kendi politikalarindan gelir. Invoke/Write yetkisi
/// yalnizca MCP kapisidir, arkadaki yetkinin yerine gecmez. /// yalnizca MCP kapisidir, arkadaki yetkinin yerine gecmez.
/// </summary> /// </summary>
[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop)] [Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Invoke)]
public class McpDevelopAppService( public class McpDevelopAppService(
IMcpServerConfigProvider serverConfigProvider, IMcpServerConfigProvider serverConfigProvider,
IMcpDevGuide devGuide, IMcpDevGuide devGuide,
IMcpTableCreator tableCreator, IMcpTableCreator tableCreator,
IListFormWizardAppService wizardAppService, IListFormWizardAppService wizardAppService,
ICustomComponentAppService componentAppService) ICustomComponentAppService componentAppService,
IListFormSelectAppService listFormSelectAppService,
IRepository<Menu, Guid> menuRepository,
IRepository<PermissionDefinitionRecord, Guid> permissionRepository,
IRepository<DevRequest, Guid> devRequestRepository)
: McpAppService(serverConfigProvider), IMcpDevelopAppService : McpAppService(serverConfigProvider), IMcpDevelopAppService
{ {
private const string DefaultGuideDocument = "lowcode"; private const string DefaultGuideDocument = "lowcode";
@ -38,6 +47,19 @@ public class McpDevelopAppService(
/// <summary>Custom Components ekraninin tek sayfada dondurebildigi ust sinir.</summary> /// <summary>Custom Components ekraninin tek sayfada dondurebildigi ust sinir.</summary>
private const int ComponentPageSize = 1000; private const int ComponentPageSize = 1000;
private const string DevRequestUpdatePermission = PlatformConsts.AppCodes.DevRequests + ".Update";
/// <summary>verify_wizard cevabindaki kontrol adlari.</summary>
private static class VerifyChecks
{
public const string WizardFile = "wizard_file";
public const string Menu = "menu";
public const string Permission = "permission";
public const string Select = "select";
public const string Component = "component";
public const string SeedClosure = "seed_closure";
}
public async Task<McpDevGuideDto> GetDevGuideAsync(string? document, string? section) public async Task<McpDevGuideDto> GetDevGuideAsync(string? document, string? section)
{ {
await EnsureToolAllowedAsync(McpToolNames.GetDevGuide); await EnsureToolAllowedAsync(McpToolNames.GetDevGuide);
@ -147,6 +169,261 @@ public class McpDevelopAppService(
return await wizardAppService.DeleteFile(fileName); return await wizardAppService.DeleteFile(fileName);
} }
/// <summary>
/// Ekrani kullanicinin gorecegi yoldan dener: menu kaydi, menunun istedigi yetki, ekranin
/// sorgusu (cagiranin yetkisi ve kiraci filtresiyle, tek satir) ve Custom yolunda bilesen.
/// Hicbir kontrol veri yazmaz; test satiri eklemek SeedSync acik ekranlarda seed dosyasina
/// iz birakirdi. Seed kapanisi engellemez: ekran bu ortamda calisir, eksik dosya yalnizca
/// migrate ya da baska bir ortamda ortaya cikar.
/// </summary>
public async Task<McpVerifyResultDto> VerifyWizardAsync(string fileName)
{
await EnsureToolAllowedAsync(McpToolNames.VerifyWizard);
var result = new McpVerifyResultDto { FileName = fileName };
ListFormWizardDto? wizard;
try
{
wizard = (await wizardAppService.GetFile(fileName)).Wizard;
}
catch (Exception ex)
{
result.Checks.Add(Failed(VerifyChecks.WizardFile, L[McpErrorCodes.VerifyWizardFileInvalid, fileName, ex.Message]));
return result;
}
if (wizard is null)
{
result.Checks.Add(Failed(VerifyChecks.WizardFile, L[McpErrorCodes.InvalidWizard]));
return result;
}
var listFormCode = wizard.ComponentKind == WizardComponentKindEnum.Custom ? null : wizard.ResolveListFormCode();
result.ListFormCode = listFormCode;
result.MenuUrl = wizard.MenuUrl;
result.Checks.Add(new McpVerifyCheckDto { Name = VerifyChecks.WizardFile, Passed = true });
var permissionName = listFormCode;
if (wizard.CreateMenu)
{
var menu = await VerifyMenuAsync(wizard, result.Checks);
permissionName = string.IsNullOrWhiteSpace(menu?.RequiredPermissionName) ? permissionName : menu.RequiredPermissionName;
}
if (!string.IsNullOrWhiteSpace(permissionName))
{
var permission = await permissionRepository.FirstOrDefaultAsync(p => p.Name == permissionName);
result.Checks.Add(permission is { IsEnabled: true }
? new McpVerifyCheckDto { Name = VerifyChecks.Permission, Passed = true, Message = permissionName }
: Failed(VerifyChecks.Permission, L[McpErrorCodes.VerifyPermissionMissing, permissionName]));
}
result.Checks.Add(listFormCode is null
? await VerifyComponentAsync(wizard)
: await VerifySelectAsync(listFormCode));
result.Checks.Add(await VerifySeedClosureAsync(fileName));
result.Passed = result.Checks.TrueForAll(check => check.Passed || !check.Blocking);
return result;
}
/// <summary>
/// Durum kurali modelin dikkatine birakilmaz: talep Ready to Start degilse reddedilir, uygunsa
/// ayni unit of work'te In Progress'e alinir. MCP yetki vermez; ekranin guncelleme yetkisi
/// ayrica istenir.
/// </summary>
public async Task<McpDevRequestDto> StartDevRequestAsync(string request)
{
await EnsureToolAllowedAsync(McpToolNames.StartDevRequest);
await AuthorizationService.CheckAsync(DevRequestUpdatePermission);
var devRequest = await FindDevRequestAsync(request);
if (!string.Equals(devRequest.Status, McpConsts.DevRequestStatuses.ReadyToStart, StringComparison.Ordinal))
{
throw new UserFriendlyException(L[McpErrorCodes.DevRequestNotReady, devRequest.Title ?? devRequest.Id.ToString(), devRequest.Status ?? string.Empty]);
}
devRequest.Status = McpConsts.DevRequestStatuses.InProgress;
await devRequestRepository.UpdateAsync(devRequest, autoSave: true);
return ToDto(devRequest);
}
public async Task<McpDevRequestDto> GetDevRequestAsync(string request)
{
await EnsureToolAllowedAsync(McpToolNames.GetDevRequest);
await AuthorizationService.CheckAsync(PlatformConsts.AppCodes.DevRequests);
return ToDto(await FindDevRequestAsync(request));
}
/// <summary>
/// Aciklama base64 gorsel tasir; model onu update_row ile yeniden yazarsa gorselleri geri
/// gonderemez ve siler. Ekleme bu yuzden sunucuda, var olan metne dokunmadan yapilir.
/// </summary>
public async Task AppendDevRequestNoteAsync(string request, string html)
{
await EnsureToolAllowedAsync(McpToolNames.AppendDevRequestNote);
await AuthorizationService.CheckAsync(DevRequestUpdatePermission);
if (string.IsNullOrWhiteSpace(html))
{
throw new UserFriendlyException(L[McpErrorCodes.DevRequestNoteRequired]);
}
var devRequest = await FindDevRequestAsync(request);
devRequest.Description = string.Concat(devRequest.Description, html.Trim());
await devRequestRepository.UpdateAsync(devRequest, autoSave: true);
}
private static McpDevRequestDto ToDto(DevRequest devRequest)
{
var (description, images) = McpDevRequestImages.Extract(devRequest.Description);
return new McpDevRequestDto
{
Id = devRequest.Id,
Title = devRequest.Title,
Status = devRequest.Status,
Description = description,
Images = images,
Priority = devRequest.Priority,
Tags = devRequest.Tags,
DueDate = devRequest.DueDate,
UserName = devRequest.UserName,
TenantId = devRequest.TenantId,
SubTasks = devRequest.SubTasks
};
}
/// <summary>Id ile ya da tam baslikla bulur; ayni basligi tasiyan birden fazla talep varsa Id istenir.</summary>
private async Task<DevRequest> FindDevRequestAsync(string? request)
{
var value = request?.Trim() ?? string.Empty;
if (Guid.TryParse(value, out var id))
{
return await devRequestRepository.FindAsync(id)
?? throw new UserFriendlyException(L[McpErrorCodes.DevRequestNotFound, value]);
}
var matches = value.Length == 0 ? [] : await devRequestRepository.GetListAsync(r => r.Title == value);
return matches.Count switch
{
0 => throw new UserFriendlyException(L[McpErrorCodes.DevRequestNotFound, value]),
1 => matches[0],
_ => throw new UserFriendlyException(L[McpErrorCodes.DevRequestAmbiguous, value, string.Join(", ", matches.Select(r => r.Id))])
};
}
/// <summary>
/// Kapanis analizi wizard export'unun yetkisini ister; o yetki dogrulamanin kendisini
/// dusurmesin diye yoksa kontrol atlanir ve nedeni raporlanir.
/// </summary>
private async Task<McpVerifyCheckDto> VerifySeedClosureAsync(string fileName)
{
var check = new McpVerifyCheckDto { Name = VerifyChecks.SeedClosure, Blocking = false };
if (!await AuthorizationService.IsGrantedAsync(PlatformConsts.AppCodes.Listforms.Wizards.Export))
{
check.Message = L[McpErrorCodes.VerifySeedClosureSkipped, PlatformConsts.AppCodes.Listforms.Wizards.Export];
return check;
}
var warnings = (await wizardAppService.ExportAnalysis(fileName)).Warnings;
if (warnings.Count == 0)
{
check.Passed = true;
return check;
}
check.Message = L[McpErrorCodes.VerifySeedClosureIncomplete, string.Join(", ", warnings)];
return check;
}
private async Task<Menu?> VerifyMenuAsync(ListFormWizardDto wizard, List<McpVerifyCheckDto> checks)
{
var menuCode = wizard.ResolveMenuCode();
var menu = await menuRepository.FirstOrDefaultAsync(m => m.Code == menuCode);
if (menu is null)
{
checks.Add(Failed(VerifyChecks.Menu, L[McpErrorCodes.VerifyMenuMissing, menuCode]));
}
else if (menu.IsDisabled)
{
checks.Add(Failed(VerifyChecks.Menu, L[McpErrorCodes.VerifyMenuDisabled, menuCode]));
}
else if (!string.Equals(menu.Url, wizard.MenuUrl, StringComparison.OrdinalIgnoreCase))
{
checks.Add(Failed(VerifyChecks.Menu, L[McpErrorCodes.VerifyMenuUrlMismatch, menuCode, menu.Url, wizard.MenuUrl]));
}
else
{
checks.Add(new McpVerifyCheckDto { Name = VerifyChecks.Menu, Passed = true, Message = menu.Url });
}
return menu;
}
/// <summary>
/// Ekranin sorgusunu query_list ile ayni servisten calistirir; kolon, veri kaynagi ve yetki
/// hatalari burada ortaya cikar.
/// </summary>
private async Task<McpVerifyCheckDto> VerifySelectAsync(string listFormCode)
{
try
{
var select = await listFormSelectAppService.GetSelectAsync(new SelectRequestDto
{
ListFormCode = listFormCode,
Take = 1,
RequireTotalCount = true
});
return new McpVerifyCheckDto
{
Name = VerifyChecks.Select,
Passed = true,
Message = L[McpErrorCodes.VerifySelectPassed, select.TotalCount]
};
}
catch (Exception ex)
{
return Failed(VerifyChecks.Select, L[McpErrorCodes.VerifySelectFailed, ex.Message]);
}
}
private async Task<McpVerifyCheckDto> VerifyComponentAsync(ListFormWizardDto wizard)
{
var component = await FindComponentAsync(wizard.CustomComponentName);
if (component is null)
{
return Failed(VerifyChecks.Component, L[McpErrorCodes.ComponentNotFound, wizard.CustomComponentName]);
}
if (!component.IsActive)
{
return Failed(VerifyChecks.Component, L[McpErrorCodes.VerifyComponentInactive, component.Name]);
}
if (wizard.CreateMenu && !string.Equals(component.RoutePath, wizard.MenuUrl, StringComparison.OrdinalIgnoreCase))
{
return Failed(VerifyChecks.Component, L[McpErrorCodes.VerifyComponentRouteMismatch, component.Name, component.RoutePath ?? string.Empty, wizard.MenuUrl]);
}
return new McpVerifyCheckDto { Name = VerifyChecks.Component, Passed = true, Message = component.RoutePath };
}
private static McpVerifyCheckDto Failed(string name, string message)
=> new() { Name = name, Passed = false, Message = message };
public async Task<List<McpComponentSummaryDto>> GetComponentsAsync(string? search) public async Task<List<McpComponentSummaryDto>> GetComponentsAsync(string? search)
{ {
await EnsureToolAllowedAsync(McpToolNames.ListComponents); await EnsureToolAllowedAsync(McpToolNames.ListComponents);

View file

@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Sozsoft.Platform;
namespace Sozsoft.Mcp;
/// <summary>
/// query_list'in DevExtreme bicimindeki (JSON) sort/group/summary girdilerini ekranin select
/// ucunun bekledigi metin bicimine (<c>Alan desc|Alan2 asc</c>) cevirir. Arayuzde ayni donusumu
/// <c>ui/src/views/list/Utils.ts</c> yapar; select ucu JSON'u tanimaz ve ORDER BY'a oldugu gibi
/// ekler. JSON olmayan girdi zaten metin bicimindedir, dokunulmaz. Bozuk girdi
/// <see cref="JsonException"/> ya da <see cref="InvalidOperationException"/> atar.
/// </summary>
internal static class McpLoadOptions
{
private static readonly string Delimiter = PlatformConsts.MultiValueDelimiter.ToString();
public static string? ToSort(string? json)
=> Convert(json, item => $"{Selector(item)} {Direction(item)}");
public static string? ToGroup(string? json)
=> Convert(json, item =>
{
var isExpanded = item.TryGetProperty("isExpanded", out var expanded) && expanded.ValueKind == JsonValueKind.True;
var interval = item.TryGetProperty("groupInterval", out var value) && value.ValueKind == JsonValueKind.String
? $" {value.GetString()}"
: string.Empty;
return $"{Selector(item)} {Direction(item)} {(isExpanded ? "true" : "false")}{interval}";
});
public static string? ToSummary(string? json)
=> Convert(json, item => $"{Selector(item)} {Text(item, "summaryType")}");
private static string? Convert(string? json, Func<JsonElement, string> map)
{
var value = json?.Trim();
if (string.IsNullOrEmpty(value))
{
return null;
}
if (!value.StartsWith('['))
{
return value;
}
using var document = JsonDocument.Parse(value);
var parts = document.RootElement.EnumerateArray()
.Where(item => item.ValueKind == JsonValueKind.Object)
.Select(map)
.ToList();
return parts.Count == 0 ? null : string.Join(Delimiter, parts);
}
private static string Selector(JsonElement item) => Text(item, "selector");
private static string Direction(JsonElement item)
=> item.TryGetProperty("desc", out var desc) && desc.ValueKind == JsonValueKind.True ? "desc" : "asc";
private static string Text(JsonElement item, string name)
{
var text = item.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.String ? value.GetString() : null;
// Bosluk ve ayirici metin biciminin sozdizimidir; alan adinda bulunamazlar.
if (string.IsNullOrWhiteSpace(text) || text.Any(c => char.IsWhiteSpace(c) || c == PlatformConsts.MultiValueDelimiter))
{
throw new JsonException(name);
}
return text;
}
}

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Sozsoft.Mcp.Dtos; using Sozsoft.Mcp.Dtos;
@ -33,7 +34,8 @@ public class McpQueryAppService(
IMcpSqlGuard sqlGuard, IMcpSqlGuard sqlGuard,
IMcpQueryExecutor queryExecutor, IMcpQueryExecutor queryExecutor,
IRepository<CustomEndpoint, Guid> customEndpointRepository, IRepository<CustomEndpoint, Guid> customEndpointRepository,
IListFormSelectAppService listFormSelectAppService) IListFormSelectAppService listFormSelectAppService,
IMcpListFormAccessFilter accessFilter)
: McpAppService(serverConfigProvider), IMcpQueryAppService : McpAppService(serverConfigProvider), IMcpQueryAppService
{ {
private const string SqlPermission = PlatformConsts.AppCodes.SqlQueryManagers.Default; private const string SqlPermission = PlatformConsts.AppCodes.SqlQueryManagers.Default;
@ -46,14 +48,20 @@ public class McpQueryAppService(
var config = await EnsureToolAllowedAsync(McpToolNames.QueryList); var config = await EnsureToolAllowedAsync(McpToolNames.QueryList);
var take = ResolveMaxRows(config, input.Take); var take = ResolveMaxRows(config, input.Take);
var permission = await accessFilter.GetPermissionNameAsync(input.ListFormCode, AuthorizationTypeEnum.Read);
if (permission is not null)
{
await AuthorizationService.CheckAsync(permission);
}
var result = await listFormSelectAppService.GetSelectAsync(new SelectRequestDto var result = await listFormSelectAppService.GetSelectAsync(new SelectRequestDto
{ {
ListFormCode = input.ListFormCode, ListFormCode = input.ListFormCode,
Filter = NullIfEmpty(input.Filter), Filter = NullIfEmpty(input.Filter),
Sort = NullIfEmpty(input.Sort), Sort = ToLoadOption(nameof(input.Sort), input.Sort, McpLoadOptions.ToSort),
Group = NullIfEmpty(input.Group), Group = ToLoadOption(nameof(input.Group), input.Group, McpLoadOptions.ToGroup),
GroupSummary = NullIfEmpty(input.GroupSummary), GroupSummary = ToLoadOption(nameof(input.GroupSummary), input.GroupSummary, McpLoadOptions.ToSummary),
TotalSummary = NullIfEmpty(input.TotalSummary), TotalSummary = ToLoadOption(nameof(input.TotalSummary), input.TotalSummary, McpLoadOptions.ToSummary),
Skip = Math.Max(input.Skip, 0), Skip = Math.Max(input.Skip, 0),
Take = take, Take = take,
RequireTotalCount = true, RequireTotalCount = true,
@ -69,6 +77,20 @@ public class McpQueryAppService(
Take = take Take = take
}; };
} }
/// <summary>Bozuk girdi SQL hatasina donusmeden, hangi parametrede oldugu soylenerek reddedilir.</summary>
private string? ToLoadOption(string parameter, string? value, Func<string?, string?> convert)
{
try
{
return convert(value);
}
catch (Exception ex) when (ex is JsonException or InvalidOperationException)
{
throw new UserFriendlyException(L[McpErrorCodes.InvalidLoadOption, JsonNamingPolicy.CamelCase.ConvertName(parameter)]);
}
}
public async Task<McpQueryResultDto> RunSelectAsync(McpSelectRequestDto input) public async Task<McpQueryResultDto> RunSelectAsync(McpSelectRequestDto input)
{ {
Check.NotNull(input, nameof(input)); Check.NotNull(input, nameof(input));

View file

@ -90,6 +90,16 @@ public static class McpConsts
public const string CustomEndpoint = "customEndpoint"; public const string CustomEndpoint = "customEndpoint";
} }
/// <summary>
/// Gelistirme talebi (<c>App.DevRequests</c>) durumlari; ekranin <c>Status</c> lookup
/// anahtarlariyla ayni olmak zorundadir.
/// </summary>
public static class DevRequestStatuses
{
public const string ReadyToStart = "Ready to Start";
public const string InProgress = "In Progress";
}
/// <summary> /// <summary>
/// Veritaban&#305; katalog sorgusunun dondurdugu nesne turu etiketleri. /// Veritaban&#305; katalog sorgusunun dondurdugu nesne turu etiketleri.
/// <see cref="SavedQueryKinds"/> degerlerine eslenir. /// <see cref="SavedQueryKinds"/> degerlerine eslenir.

View file

@ -10,7 +10,9 @@ public static class McpErrorCodes
public const string NoServerRecord = Prefix + "NoServerRecord"; public const string NoServerRecord = Prefix + "NoServerRecord";
public const string ToolDisabled = Prefix + "ToolDisabled"; public const string ToolDisabled = Prefix + "ToolDisabled";
public const string TenantReadOnly = Prefix + "TenantReadOnly"; public const string TenantToolDisabled = Prefix + "TenantToolDisabled";
public const string PermissionDenied = Prefix + "PermissionDenied";
public const string PermissionDeniedUnspecified = Prefix + "PermissionDeniedUnspecified";
public const string EmptyQuery = Prefix + "EmptyQuery"; public const string EmptyQuery = Prefix + "EmptyQuery";
public const string SingleStatementOnly = Prefix + "SingleStatementOnly"; public const string SingleStatementOnly = Prefix + "SingleStatementOnly";
public const string ReadOnlyStatementRequired = Prefix + "ReadOnlyStatementRequired"; public const string ReadOnlyStatementRequired = Prefix + "ReadOnlyStatementRequired";
@ -21,6 +23,7 @@ public static class McpErrorCodes
public const string CustomEndpointNotFound = Prefix + "CustomEndpointNotFound"; public const string CustomEndpointNotFound = Prefix + "CustomEndpointNotFound";
public const string MissingParameters = Prefix + "MissingParameters"; public const string MissingParameters = Prefix + "MissingParameters";
public const string ListFormCodeRequired = Prefix + "ListFormCodeRequired"; public const string ListFormCodeRequired = Prefix + "ListFormCodeRequired";
public const string InvalidLoadOption = Prefix + "InvalidLoadOption";
public const string ValuesRequired = Prefix + "ValuesRequired"; public const string ValuesRequired = Prefix + "ValuesRequired";
public const string KeysRequired = Prefix + "KeysRequired"; public const string KeysRequired = Prefix + "KeysRequired";
public const string DictionaryNotFound = Prefix + "DictionaryNotFound"; public const string DictionaryNotFound = Prefix + "DictionaryNotFound";
@ -32,6 +35,21 @@ public static class McpErrorCodes
public const string ComponentNameRequired = Prefix + "ComponentNameRequired"; public const string ComponentNameRequired = Prefix + "ComponentNameRequired";
public const string ComponentCodeRequired = Prefix + "ComponentCodeRequired"; public const string ComponentCodeRequired = Prefix + "ComponentCodeRequired";
public const string ComponentNotFound = Prefix + "ComponentNotFound"; public const string ComponentNotFound = Prefix + "ComponentNotFound";
public const string VerifyWizardFileInvalid = Prefix + "VerifyWizardFileInvalid";
public const string VerifyMenuMissing = Prefix + "VerifyMenuMissing";
public const string VerifyMenuDisabled = Prefix + "VerifyMenuDisabled";
public const string VerifyMenuUrlMismatch = Prefix + "VerifyMenuUrlMismatch";
public const string VerifyPermissionMissing = Prefix + "VerifyPermissionMissing";
public const string VerifySelectPassed = Prefix + "VerifySelectPassed";
public const string VerifySelectFailed = Prefix + "VerifySelectFailed";
public const string VerifyComponentInactive = Prefix + "VerifyComponentInactive";
public const string VerifyComponentRouteMismatch = Prefix + "VerifyComponentRouteMismatch";
public const string VerifySeedClosureIncomplete = Prefix + "VerifySeedClosureIncomplete";
public const string VerifySeedClosureSkipped = Prefix + "VerifySeedClosureSkipped";
public const string DevRequestNotFound = Prefix + "DevRequestNotFound";
public const string DevRequestAmbiguous = Prefix + "DevRequestAmbiguous";
public const string DevRequestNotReady = Prefix + "DevRequestNotReady";
public const string DevRequestNoteRequired = Prefix + "DevRequestNoteRequired";
public const string VisualDesignerComponent = Prefix + "VisualDesignerComponent"; public const string VisualDesignerComponent = Prefix + "VisualDesignerComponent";
public const string TableColumnsRequired = Prefix + "TableColumnsRequired"; public const string TableColumnsRequired = Prefix + "TableColumnsRequired";
public const string UnsupportedColumnType = Prefix + "UnsupportedColumnType"; public const string UnsupportedColumnType = Prefix + "UnsupportedColumnType";

View file

@ -0,0 +1,11 @@
namespace Sozsoft.Mcp;
/// <summary>
/// /mcp ucunda yayinlanan hazir prompt adlari; istemcide komut olarak gorunur. Prompt yalnizca
/// metindir, yetki tasimaz: calistirdigi araclar kendi kapisindan gecer.
/// </summary>
public static class McpPromptNames
{
public const string ProcessDevRequest = "process_dev_request";
public const string ProcessReadyRequests = "process_ready_requests";
}

View file

@ -28,14 +28,19 @@ public static class McpToolNames
public const string GetWizard = "get_wizard"; public const string GetWizard = "get_wizard";
public const string DeployWizard = "deploy_wizard"; public const string DeployWizard = "deploy_wizard";
public const string DeleteWizard = "delete_wizard"; public const string DeleteWizard = "delete_wizard";
public const string VerifyWizard = "verify_wizard";
public const string StartDevRequest = "start_dev_request";
public const string GetDevRequest = "get_dev_request";
public const string AppendDevRequestNote = "append_dev_request_note";
public const string ListComponents = "list_components"; public const string ListComponents = "list_components";
public const string GetComponent = "get_component"; public const string GetComponent = "get_component";
public const string SaveComponent = "save_component"; public const string SaveComponent = "save_component";
public const string DeleteComponent = "delete_component"; public const string DeleteComponent = "delete_component";
/// <summary> /// <summary>
/// Ekran ve bilesen gelistirme araclari. Sunucu bazinda <c>AllowedTools</c> ile, kullanici /// Ekran ve bilesen gelistirme araclari; kiraci baglaminda hicbiri acilmaz. Okuyanlar
/// bazinda <c>McpServers.Develop</c> yetkisiyle kapatilir. /// <c>McpServers.Invoke</c> ile, degistirenler <c>McpServers.Write</c> ile acilir
/// (<see cref="RequiredPermissions"/>).
/// </summary> /// </summary>
public static readonly string[] DevelopTools = public static readonly string[] DevelopTools =
[ [
@ -45,6 +50,10 @@ public static class McpToolNames
GetWizard, GetWizard,
DeployWizard, DeployWizard,
DeleteWizard, DeleteWizard,
VerifyWizard,
StartDevRequest,
GetDevRequest,
AppendDevRequestNote,
ListComponents, ListComponents,
GetComponent, GetComponent,
SaveComponent, SaveComponent,
@ -58,11 +67,12 @@ public static class McpToolNames
public static readonly string[] AlwaysAvailableTools = [ServerInfo]; public static readonly string[] AlwaysAvailableTools = [ServerInfo];
/// <summary> /// <summary>
/// Kiraci kullanicisinin gorebildigi araclarin tamami; kiraci baglaminda MCP yalnizca veri /// Kiraci kullanicisinin gorebildigi araclarin tamami: veri okuma ve ekranin
/// okur. <c>AllowedTools</c> bu kumeyi yalnizca daraltabilir, genisletemez. <see cref="RunQuery"/> /// insert/update/delete sozlesmesinden gecen satir yazma. Yazilim gelistirme araclari
/// ve <see cref="ListQueries"/> disarida kalir: procedure ve Custom Endpoint cagrisi veri /// (<see cref="DevelopTools"/>) kiraciya acilmaz. <c>AllowedTools</c> bu kumeyi yalnizca
/// yazabilir, okuma oldugu garanti edilemez. <see cref="RunSelect"/> ise tek SELECT disindaki /// daraltabilir, genisletemez. <see cref="RunQuery"/> ve <see cref="ListQueries"/> disarida
/// her ifadeyi reddettigi icin okumadir. /// kalir: procedure ve Custom Endpoint ekran yetkisinden gecmeden veri yazabilir.
/// <see cref="RunSelect"/> ise tek SELECT disindaki her ifadeyi reddettigi icin okumadir.
/// </summary> /// </summary>
public static readonly string[] TenantTools = public static readonly string[] TenantTools =
[ [
@ -71,15 +81,18 @@ public static class McpToolNames
DescribeList, DescribeList,
SearchSchema, SearchSchema,
QueryList, QueryList,
RunSelect RunSelect,
InsertRow,
UpdateRow,
DeleteRow
]; ];
/// <summary> /// <summary>
/// Kullanicinin uygulamada da sahip olmasi gereken yetki. MCP yetki vermez: ham SQL'i /// <c>McpServers.Invoke</c> (/mcp ucunun kendisinde aranir) disinda istenen yetki. Iki
/// uygulamada SQL Query Manager'i kullanabilen, veri yazmayi yazma yetkisi olan kullanici /// turdur: MCP uzerinden her degisiklik <c>McpServers.Write</c> ister; ham SQL liste
/// gorur. Listede olmayan araclar kendi icinde liste bazli yetkiyle suzulur. Gelistirme /// yetkilerini astigi icin uygulamadaki SQL Query Manager yetkisini ister. Listede olmayan
/// araclarinin arkasindaki Wizard ve Custom Components servisleri kendi yetkilerini ayrica /// araclar okumadir ve arkadaki servisin kendi yetkisiyle suzulur. MCP yetki vermez:
/// ister; Develop yalnizca MCP kapisidir. /// Wizard, Custom Components ve ekran C/U/D yetkileri ayrica aranir.
/// </summary> /// </summary>
public static readonly IReadOnlyDictionary<string, string> RequiredPermissions = new Dictionary<string, string> public static readonly IReadOnlyDictionary<string, string> RequiredPermissions = new Dictionary<string, string>
{ {
@ -87,18 +100,58 @@ public static class McpToolNames
[InsertRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, [InsertRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
[UpdateRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, [UpdateRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
[DeleteRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, [DeleteRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
[GetDevGuide] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop, [CreateTable] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
[CreateTable] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop, [DeployWizard] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
[ListWizards] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop, [DeleteWizard] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
[GetWizard] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop, [StartDevRequest] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
[DeployWizard] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop, [AppendDevRequestNote] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
[DeleteWizard] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop, [SaveComponent] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
[ListComponents] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop, [DeleteComponent] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
[GetComponent] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop,
[SaveComponent] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop,
[DeleteComponent] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop,
}; };
/// <summary>
/// Arkadaki servisin <c>[Authorize]</c> attribute'u ile istedigi yetkiler. Attribute ile
/// reddedilen cagri eksik yetkinin adini tasimaz; kullaniciya hangi yetkinin eksik oldugunu
/// soyleyebilmek icin burada tutulur. Kodda adiyla istenen yetkiler (ekran C/R/U/D,
/// <c>Wizards.Create/Update</c>, Custom Components, <c>App.DevRequests.Update</c>) reddi zaten
/// adiyla bildirdigi icin buraya yazilmaz.
/// </summary>
public static readonly IReadOnlyDictionary<string, string[]> ServicePermissions = new Dictionary<string, string[]>
{
[ListWizards] = [PlatformConsts.AppCodes.Listforms.Wizard],
[GetWizard] = [PlatformConsts.AppCodes.Listforms.Wizard],
[VerifyWizard] = [PlatformConsts.AppCodes.Listforms.Wizard],
[DeployWizard] = [PlatformConsts.AppCodes.Listforms.Wizard],
[DeleteWizard] = [PlatformConsts.AppCodes.Listforms.Wizard, PlatformConsts.AppCodes.Listforms.Wizards.Delete],
};
/// <summary>
/// Bir aracin cagrisinda aranabilecek, adi bilinen yetkiler: uc kapisi (Invoke), MCP kapisi
/// (<see cref="RequiredPermissions"/>) ve arkadaki servis (<see cref="ServicePermissions"/>).
/// </summary>
public static IEnumerable<string> KnownPermissionsOf(string? toolName)
{
yield return PlatformConsts.AppCodes.DeveloperKits.McpServers.Invoke;
if (toolName is null)
{
yield break;
}
if (RequiredPermissions.TryGetValue(toolName, out var required))
{
yield return required;
}
if (ServicePermissions.TryGetValue(toolName, out var service))
{
foreach (var permission in service)
{
yield return permission;
}
}
}
/// <summary>Yayinlanan tum araclar, arac listesindeki siralarina gore.</summary> /// <summary>Yayinlanan tum araclar, arac listesindeki siralarina gore.</summary>
public static readonly string[] All = public static readonly string[] All =
[ [

View file

@ -0,0 +1,50 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Sozsoft.Platform;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Guids;
using Volo.Abp.PermissionManagement;
namespace Sozsoft.Mcp.Data;
/// <summary>
/// Kaldirilan <c>McpServers.Develop</c> yetkisini <c>McpServers.Write</c>'a tasir: Develop verilmis
/// rol/kullanicilara Write verilir, sonra Develop'un atamalari ve tanimi silinir. Seed yalnizca
/// ekledigi icin tanim bu adim olmadan yetki ekraninda sahipsiz kalir, o kullanicilar da
/// gelistirme araclarini kaybederdi. Develop kalmadiginda hicbir sey yapmaz; tekrar calismasi guvenlidir.
/// </summary>
public class McpDevelopPermissionMergeDataSeedContributor(
IRepository<PermissionGrant, Guid> grantRepository,
IRepository<PermissionDefinitionRecord, Guid> definitionRepository,
IGuidGenerator guidGenerator) : IDataSeedContributor, ITransientDependency
{
private const string LegacyDevelopPermission = PlatformConsts.AppCodes.DeveloperKits.McpServers.McpServer + ".Develop";
private const string WritePermission = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write;
public async Task SeedAsync(DataSeedContext context)
{
// Develop yalnizca host tarafinda tanimliydi; kiraci turlarinda atamasi olamaz.
if (context.TenantId is not null)
{
return;
}
var developGrants = await grantRepository.GetListAsync(g => g.Name == LegacyDevelopPermission);
if (developGrants.Count > 0)
{
var writeGrants = await grantRepository.GetListAsync(g => g.Name == WritePermission);
var missing = developGrants
.Where(d => !writeGrants.Any(w => w.ProviderName == d.ProviderName && w.ProviderKey == d.ProviderKey))
.Select(d => new PermissionGrant(guidGenerator.Create(), WritePermission, d.ProviderName, d.ProviderKey, d.TenantId))
.ToList();
await grantRepository.InsertManyAsync(missing, autoSave: true);
await grantRepository.DeleteManyAsync(developGrants, autoSave: true);
}
await definitionRepository.DeleteAsync(p => p.Name == LegacyDevelopPermission, autoSave: true);
}
}

View file

@ -4,6 +4,7 @@ using System.Linq;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Sozsoft.Platform.Entities; using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums;
using Sozsoft.Platform.Permissions; using Sozsoft.Platform.Permissions;
using Volo.Abp.Authorization.Permissions; using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
@ -19,6 +20,12 @@ namespace Sozsoft.Mcp.Services;
public interface IMcpListFormAccessFilter public interface IMcpListFormAccessFilter
{ {
Task<IReadOnlySet<string>> GetReadableListFormCodesAsync(); Task<IReadOnlySet<string>> GetReadableListFormCodesAsync();
/// <summary>
/// Ekranin bir islem icin istedigi yetkinin adi (<c>PermissionJson</c>); ekran ya da tanim
/// yoksa null. Yetki adiyla kontrol edildiginde reddin nedeni kullaniciya adiyla iletilir.
/// </summary>
Task<string?> GetPermissionNameAsync(string listFormCode, AuthorizationTypeEnum operation);
} }
public class McpListFormAccessFilter( public class McpListFormAccessFilter(
@ -66,7 +73,30 @@ public class McpListFormAccessFilter(
.ToHashSet(StringComparer.OrdinalIgnoreCase); .ToHashSet(StringComparer.OrdinalIgnoreCase);
} }
public async Task<string?> GetPermissionNameAsync(string listFormCode, AuthorizationTypeEnum operation)
{
var listForm = await listFormRepository.FirstOrDefaultAsync(form => form.ListFormCode == listFormCode);
var permissions = Deserialize(listForm?.PermissionJson);
var name = operation switch
{
AuthorizationTypeEnum.Create => permissions?.C,
AuthorizationTypeEnum.Update => permissions?.U,
AuthorizationTypeEnum.Delete => permissions?.D,
_ => permissions?.R
};
return string.IsNullOrWhiteSpace(name) ? null : name;
}
private static string? ReadPermissionOf(string? permissionJson) private static string? ReadPermissionOf(string? permissionJson)
{
var permissions = Deserialize(permissionJson);
return string.IsNullOrWhiteSpace(permissions?.R) ? null : permissions.R;
}
private static PermissionCrudValueObject? Deserialize(string? permissionJson)
{ {
if (string.IsNullOrWhiteSpace(permissionJson)) if (string.IsNullOrWhiteSpace(permissionJson))
{ {
@ -75,9 +105,7 @@ public class McpListFormAccessFilter(
try try
{ {
var permissions = JsonSerializer.Deserialize<PermissionCrudValueObject>(permissionJson); return JsonSerializer.Deserialize<PermissionCrudValueObject>(permissionJson);
return string.IsNullOrWhiteSpace(permissions?.R) ? null : permissions.R;
} }
catch (JsonException) catch (JsonException)
{ {

View file

@ -46,7 +46,10 @@ public class McpHttpApiModule : AbpModule
.WithTools<McpQueryTools>() .WithTools<McpQueryTools>()
.WithTools<McpDataTools>() .WithTools<McpDataTools>()
.WithTools<McpDevelopTools>() .WithTools<McpDevelopTools>()
.WithRequestFilters(filters => filters.AddListToolsFilter(next => async (requestContext, cancellationToken) => .WithPrompts(typeof(McpDevelopPrompts))
.WithRequestFilters(filters =>
{
filters.AddListToolsFilter(next => async (requestContext, cancellationToken) =>
{ {
var result = await next(requestContext, cancellationToken); var result = await next(requestContext, cancellationToken);
@ -55,11 +58,32 @@ public class McpHttpApiModule : AbpModule
// (McpAppService.EnsureToolAllowedAsync): istemci listede olmayan bir araci da // (McpAppService.EnsureToolAllowedAsync): istemci listede olmayan bir araci da
// cagirabilir, suzme tek basina bir yetki siniri degildir. // cagirabilir, suzme tek basina bir yetki siniri degildir.
var schemaAppService = requestContext.Services!.GetRequiredService<IMcpSchemaAppService>(); var schemaAppService = requestContext.Services!.GetRequiredService<IMcpSchemaAppService>();
try
{
var available = (await schemaAppService.GetAvailableToolsAsync()).ToHashSet(System.StringComparer.Ordinal); var available = (await schemaAppService.GetAvailableToolsAsync()).ToHashSet(System.StringComparer.Ordinal);
result.Tools = [.. result.Tools.Where(tool => available.Contains(tool.Name))]; result.Tools = [.. result.Tools.Where(tool => available.Contains(tool.Name))];
}
catch (System.Exception ex) when (McpToolErrorTranslator.CanTranslate(ex))
{
throw await requestContext.Services!.GetRequiredService<McpToolErrorTranslator>().TranslateAsync(ex, null);
}
return result; return result;
})); });
// SDK arac hatasinin mesajini yalnizca McpException ise iletir; yetki reddi ve is
// kurali hatalari burada cevrilir ki istemci nedenini (eksik yetkiyi) gorsun.
filters.AddCallToolFilter(next => async (requestContext, cancellationToken) =>
{
try
{
return await next(requestContext, cancellationToken);
}
catch (System.Exception ex) when (McpToolErrorTranslator.CanTranslate(ex))
{
throw await requestContext.Services!.GetRequiredService<McpToolErrorTranslator>().TranslateAsync(ex, requestContext.Params?.Name);
}
});
});
} }
} }

View file

@ -22,5 +22,9 @@ internal static class McpServerInfoDefaults
"To build screens or components, read the contract with get_dev_guide first (index, then the " + "To build screens or components, read the contract with get_dev_guide first (index, then the " +
"section you need), inspect what exists with list_wizards/get_wizard and list_components/get_component, " + "section you need), inspect what exists with list_wizards/get_wizard and list_components/get_component, " +
"then produce them with deploy_wizard and save_component; the guide's file-writing steps map to these tools. " + "then produce them with deploy_wizard and save_component; the guide's file-writing steps map to these tools. " +
"Agree on screen type, data source, module and permissions with the user before deploying."; "Agree on screen type, data source, module and permissions with the user before deploying. " +
"Development requests (App.DevRequests) follow get_dev_guide section 0.11: claim each one with " +
"start_dev_request, which only accepts requests in Ready to Start; images pasted into the request arrive as image content and are part of the request (layout and demo rows). " +
"When a tool reports a missing permission, tell the user which permission is missing and stop; " +
"do not try to reach the same result through another tool.";
} }

View file

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Localization;
using ModelContextProtocol;
using Sozsoft.Platform.Localization;
using Volo.Abp;
using Volo.Abp.Authorization;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.DependencyInjection;
namespace Sozsoft.Mcp;
/// <summary>
/// Arac hatalarini istemcinin gorebilecegi bicime cevirir. SDK yalnizca <see cref="McpException"/>
/// mesajini iletir; diger her istisna "An error occurred invoking" olarak gider ve model neden
/// reddedildigini bilemez. Yetki reddinde eksik yetkiler adiyla, is kurali hatasinda
/// (<see cref="UserFriendlyException"/>) zaten yerellestirilmis mesaj iletilir. Diger istisnalar
/// cevrilmez: ic ayrinti (SQL, yigin) istemciye sizmaz.
/// </summary>
public class McpToolErrorTranslator(
IPermissionChecker permissionChecker,
IStringLocalizer<PlatformResource> localizer) : ITransientDependency
{
public static bool CanTranslate(Exception exception)
=> exception is AbpAuthorizationException or UserFriendlyException;
public async Task<McpException> TranslateAsync(Exception exception, string? toolName)
=> exception is AbpAuthorizationException authorization
? new McpException(await DescribeMissingPermissionsAsync(authorization, toolName), exception)
: new McpException(exception.Message, exception);
/// <summary>
/// <c>CheckAsync("...")</c> reddi yetki adini tasir; <c>[Authorize]</c> attribute'u tasimaz. Ikinci
/// durumda aracin adi bilinen yetkileri kullanicinin yetkileriyle karsilastirilir.
/// </summary>
private async Task<string> DescribeMissingPermissionsAsync(AbpAuthorizationException exception, string? toolName)
{
var candidates = new List<string>(McpToolNames.KnownPermissionsOf(toolName));
if (exception.Data["PolicyName"] is string policyName && !string.IsNullOrWhiteSpace(policyName))
{
candidates.Add(policyName);
}
var grants = await permissionChecker.IsGrantedAsync([.. candidates.Distinct(StringComparer.Ordinal)]);
var missing = grants.Result
.Where(result => result.Value != PermissionGrantResult.Granted)
.Select(result => Describe(result.Key))
.ToList();
var target = toolName ?? McpConsts.EndpointPath;
return missing.Count == 0
? localizer[McpErrorCodes.PermissionDeniedUnspecified, target]
: localizer[McpErrorCodes.PermissionDenied, target, string.Join(", ", missing)];
}
/// <summary>Yetkinin gorunen adi ve kodu; rol ekraninda aranabilsin diye kod her zaman yazilir.</summary>
private string Describe(string permissionName)
{
var displayName = localizer[permissionName];
return displayName.ResourceNotFound || displayName.Value == permissionName
? permissionName
: $"{displayName.Value} ({permissionName})";
}
}

View file

@ -0,0 +1,33 @@
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Sozsoft.Mcp.Tools;
/// <summary>
/// Gelistirme dongusunun hazir prompt'lari. Proseduru tekrar etmez, rehberin 0.11 bolumune
/// yonlendirir; boylece adimlar tek yerde (lowcode.instructions.md) degisir. Durum kurali
/// start_dev_request'te sunucu tarafinda uygulanir.
/// </summary>
[McpServerPromptType]
public static class McpDevelopPrompts
{
[McpServerPrompt(Name = McpPromptNames.ProcessDevRequest)]
[Description("Tek bir gelistirme talebini isler. Talep Ready to Start durumunda olmalidir.")]
public static string ProcessDevRequest(
[Description("Talebin Id'si (GUID) ya da tam basligi.")]
string request)
=> $"App.DevRequests kuyrugundaki '{request}' talebini isle; yalnizca bu talebi. " +
"Once get_dev_guide ile lowcode 0.11 bolumunu oku ve oradaki adimlari uygula. " +
"Talebi start_dev_request ile ustlen. Arac reddederse (talep Ready to Start degil, bulunamadi ya da " +
"birden fazla eslesme var) hicbir sey uretme, baska bir talebe gecme: nedenini bildir ve dur. " +
"Sonunda rapor ver: uretilen artefaktlar, verify_wizard sonucu, talebin son durumu.";
[McpServerPrompt(Name = McpPromptNames.ProcessReadyRequests)]
[Description("Ready to Start durumundaki tum gelistirme taleplerini SortOrder sirasiyla isler.")]
public static string ProcessReadyRequests()
=> "App.DevRequests kuyrugundaki Ready to Start taleplerini query_list ile SortOrder sirasiyla oku ve tek tek isle. " +
"Once get_dev_guide ile lowcode 0.11 bolumunu oku ve oradaki adimlari uygula. " +
"Her talebi start_dev_request ile ustlen; reddedilen talebi atla ve rapora yaz. " +
"Sonunda parti raporu ver: talep basina uretilen artefaktlar, verify_wizard sonucu, son durum; " +
"seed_closure uyarisi olan ekranlari ve migrate gerekip gerekmedigini ayrica belirt.";
}

View file

@ -1,6 +1,9 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server; using ModelContextProtocol.Server;
using Sozsoft.Mcp.Dtos; using Sozsoft.Mcp.Dtos;
using Sozsoft.Platform.DeveloperKit; using Sozsoft.Platform.DeveloperKit;
@ -16,6 +19,8 @@ namespace Sozsoft.Mcp.Tools;
[McpServerToolType] [McpServerToolType]
public sealed class McpDevelopTools(IMcpDevelopAppService developAppService) public sealed class McpDevelopTools(IMcpDevelopAppService developAppService)
{ {
private static readonly JsonSerializerOptions TextOptions = new(JsonSerializerDefaults.Web);
[McpServerTool(Name = McpToolNames.GetDevGuide, ReadOnly = true)] [McpServerTool(Name = McpToolNames.GetDevGuide, ReadOnly = true)]
[Description("Ekran ve bilesen uretim sozlesmesini okur: wizard alanlari, enum degerleri, editor/lookup/script sozlesmesi, custom component kurallari. section bos ise baslik listesi doner; once listeyi al, sonra ihtiyac duydugun bolumu numarasiyla iste (ornegin 4.2). Rehberde dosya yazmak anlatilan yerlerde dosya yerine bu sunucunun araclarini kullan.")] [Description("Ekran ve bilesen uretim sozlesmesini okur: wizard alanlari, enum degerleri, editor/lookup/script sozlesmesi, custom component kurallari. section bos ise baslik listesi doner; once listeyi al, sonra ihtiyac duydugun bolumu numarasiyla iste (ornegin 4.2). Rehberde dosya yazmak anlatilan yerlerde dosya yerine bu sunucunun araclarini kullan.")]
public async Task<McpDevGuideDto> GetDevGuideAsync( public async Task<McpDevGuideDto> GetDevGuideAsync(
@ -59,7 +64,7 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService)
=> await developAppService.GetWizardAsync(fileName); => await developAppService.GetWizardAsync(fileName);
[McpServerTool(Name = McpToolNames.DeployWizard, Destructive = true, Idempotent = false)] [McpServerTool(Name = McpToolNames.DeployWizard, Destructive = true, Idempotent = false)]
[Description("Wizard'i deploy eder: ListForm ekrani (ComponentKind 0) ya da bir custom component'i menuye baglayan kayit (ComponentKind 1), yetkileri, dil anahtarlari, menusu ve seed dosyasi birlikte uretilir. Liste ekraninda Grid, Card, Pivot ve Chart gorunumleri varsayilan aciktir (DefaultLayout grid); kullanici bir gorunumu istemedigini soylemedikce bu bayraklari false yazma. Hedef tablo onceden var olmalidir; yoksa once create_table ile olustur. replaceFileName verilirse o wizard ve urettigi kayitlar silinip yeniden uretilir; ekran wizard cevaplarindan yeniden turetildigi icin tasarimcida yapilmis ve wizard'da karsiligi olmayan ayarlar kaybolur. Ne yazacagini get_dev_guide 4. bolumunden ogren.")] [Description("Wizard'i deploy eder: ListForm ekrani (ComponentKind 0) ya da bir custom component'i menuye baglayan kayit (ComponentKind 1), yetkileri, dil anahtarlari, menusu ve seed dosyasi birlikte uretilir. Liste ekraninda Grid, Card, Pivot ve Chart gorunumleri varsayilan aciktir (DefaultLayout grid); kullanici bir gorunumu istemedigini soylemedikce bu bayraklari false yazma. Hedef tablo onceden var olmalidir; yoksa once create_table ile olustur. Istek ornek/demo veri tasiyorsa (prompt'taki tablo/liste ya da ekran goruntusunde gorunen satirlar) SeedData true yaz ve deploy'dan sonra her satiri insert_row ile ekle: satirlar seed klasorundeki data/{ListFormCode}.json dosyasina yazilir ve migrate ile ekranla birlikte geri yuklenir; SeedData olmadan eklenen satir yalnizca bu veritabaninda kalir. replaceFileName verilirse o wizard ve urettigi kayitlar silinip yeniden uretilir; ekran wizard cevaplarindan yeniden turetildigi icin tasarimcida yapilmis ve wizard'da karsiligi olmayan ayarlar kaybolur. Ne yazacagini get_dev_guide 4. bolumunden ogren.")]
public async Task<WizardFileInfoDto?> DeployWizardAsync( public async Task<WizardFileInfoDto?> DeployWizardAsync(
[Description("Wizard JSON'u: seed dosyasi bicimi ({\"Wizard\": {...}}, get_wizard cikisi gibi) ya da duz wizard cevaplari. Enum alanlari sayi olarak yazilir.")] [Description("Wizard JSON'u: seed dosyasi bicimi ({\"Wizard\": {...}}, get_wizard cikisi gibi) ya da duz wizard cevaplari. Enum alanlari sayi olarak yazilir.")]
string wizard, string wizard,
@ -74,6 +79,55 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService)
string fileName) string fileName)
=> await developAppService.DeleteWizardAsync(fileName); => await developAppService.DeleteWizardAsync(fileName);
[McpServerTool(Name = McpToolNames.VerifyWizard, ReadOnly = true)]
[Description("Deploy edilmis bir wizard'i veri yazmadan dogrular: menu kaydi (var mi, etkin mi, URL'si wizard'la ayni mi), menunun istedigi yetki tanimi, liste ekraninda ekranin sorgusu (cagiranin yetkisiyle tek satir), Custom yolunda bilesen (var mi, etkin mi, rotasi menuyle ayni mi) ve seed kapanisi (migrate ya da baska ortamda eksik kalacak bagimlilik). passed yalnizca blocking kontrollerden biri kalirsa false olur; seed_closure engellemez ama raporlanmalidir. deploy_wizard'dan sonra ve bir gelistirme talebini Staging durumuna almadan once cagir.")]
public async Task<McpVerifyResultDto> VerifyWizardAsync(
[Description("Dogrulanacak wizard dosyasinin adi (list_wizards ya da deploy_wizard cikisindaki fileName).")]
string fileName)
=> await developAppService.VerifyWizardAsync(fileName);
[McpServerTool(Name = McpToolNames.StartDevRequest, Destructive = false, Idempotent = false)]
[Description("App.DevRequests kuyrugundaki bir gelistirme talebini ustlenir: talep Ready to Start durumundaysa In Progress'e alir ve talebin metnini (baslik, HTML aciklama, oncelik, etiketler, SubTasks) dondurur; baska bir durumdaysa reddeder. Aciklamaya yapistirilmis gorseller ayri resim icerigi olarak gelir, aciklamada yerlerinde [image-N] isareti durur; gorseldeki ekran duzeni ve satirlar talebin parcasidir (demo veri icin deploy_wizard SeedData). Bir talep uzerinde calismaya bu aracla baslanir, Status'u update_row ile In Progress yapma. Sonraki adimlar icin get_dev_guide lowcode 0.11.")]
public async Task<IList<ContentBlock>> StartDevRequestAsync(
[Description("Talebin Id'si (GUID) ya da tam basligi. Ayni basligi tasiyan birden fazla talep varsa Id ver.")]
string request)
=> ToContent(await developAppService.StartDevRequestAsync(request));
[McpServerTool(Name = McpToolNames.GetDevRequest, ReadOnly = true)]
[Description("Bir gelistirme talebini durumuna dokunmadan okur; start_dev_request ile ayni cevabi (aciklama + ayri resim icerikleri) dondurur. Yarim kalmis (In Progress/Testing) bir talebe donerken ya da gorselleri yeniden gormek icin kullan; talebi ustlenmek icin degil.")]
public async Task<IList<ContentBlock>> GetDevRequestAsync(
[Description("Talebin Id'si (GUID) ya da tam basligi.")]
string request)
=> ToContent(await developAppService.GetDevRequestAsync(request));
[McpServerTool(Name = McpToolNames.AppendDevRequestNote, Destructive = false, Idempotent = false)]
[Description("Talebin HTML aciklamasinin sonuna ekleme yapar; var olan metne ve gorsellere dokunmaz. Aciklama gomulu gorsel tasidigi icin Description'i update_row ile yeniden yazma, sonuc notunu (<h4>MCP</h4> + verify_wizard ozeti) bu aracla ekle.")]
public async Task<string> AppendDevRequestNoteAsync(
[Description("Talebin Id'si (GUID) ya da tam basligi.")]
string request,
[Description("Eklenecek HTML parcasi.")]
string html)
{
await developAppService.AppendDevRequestNoteAsync(request, html);
return "ok";
}
/// <summary>
/// Talep metni JSON olarak, gorseller ayri resim icerigi olarak doner; model resmi ancak bu
/// bicimde gorur, base64 metni olarak goremez.
/// </summary>
private static List<ContentBlock> ToContent(McpDevRequestDto devRequest)
{
List<ContentBlock> content = [new TextContentBlock { Text = JsonSerializer.Serialize(devRequest, TextOptions) }];
foreach (var image in devRequest.Images)
{
content.Add(ImageContentBlock.FromBytes(Convert.FromBase64String(image.Data), image.MimeType));
}
return content;
}
[McpServerTool(Name = McpToolNames.ListComponents, ReadOnly = true)] [McpServerTool(Name = McpToolNames.ListComponents, ReadOnly = true)]
[Description("Custom component'leri ad, rota ve aciklamasiyla listeler; kod dondurmez. isVisualDesigner true olanlarin kodu tasarimci belgesinden uretilir ve MCP ile degistirilemez.")] [Description("Custom component'leri ad, rota ve aciklamasiyla listeler; kod dondurmez. isVisualDesigner true olanlarin kodu tasarimci belgesinden uretilir ve MCP ile degistirilemez.")]
public async Task<List<McpComponentSummaryDto>> ListComponentsAsync( public async Task<List<McpComponentSummaryDto>> ListComponentsAsync(

View file

@ -17,7 +17,7 @@ public sealed class McpQueryTools(IMcpQueryAppService queryAppService)
string listFormCode, string listFormCode,
[Description("Filtre, JSON metni. Ornek: [\"Status\",\"=\",\"Open\"] ya da [[\"Amount\",\">\",100],\"and\",[\"Name\",\"contains\",\"abc\"]]. Operatorler: =, <>, >, >=, <, <=, contains, notcontains, startswith, endswith.")] [Description("Filtre, JSON metni. Ornek: [\"Status\",\"=\",\"Open\"] ya da [[\"Amount\",\">\",100],\"and\",[\"Name\",\"contains\",\"abc\"]]. Operatorler: =, <>, >, >=, <, <=, contains, notcontains, startswith, endswith.")]
string? filter = null, string? filter = null,
[Description("Siralama, JSON metni. Ornek: [{\"selector\":\"CreationTime\",\"desc\":true}].")] [Description("Siralama, JSON metni. Ornek: [{\"selector\":\"CreationTime\",\"desc\":true}]. selector alan adidir (describe_list field); ifade ya da bosluk icermez.")]
string? sort = null, string? sort = null,
[Description("Gruplama, JSON metni. Ornek: [{\"selector\":\"CustomerId\",\"desc\":false,\"isExpanded\":false}]. Gruplu sorguda satirlar yerine grup anahtarlari ve ozetleri doner.")] [Description("Gruplama, JSON metni. Ornek: [{\"selector\":\"CustomerId\",\"desc\":false,\"isExpanded\":false}]. Gruplu sorguda satirlar yerine grup anahtarlari ve ozetleri doner.")]
string? group = null, string? group = null,

View file

@ -77,6 +77,13 @@ public class ListFormWizardDto
public bool AllowDetail { get; set; } public bool AllowDetail { get; set; }
public bool ConfirmDelete { get; set; } public bool ConfirmDelete { get; set; }
/// <summary>
/// Ekranin verisi kapsam klasorundeki <c>data/{ListFormCode}.json</c> dosyasina aynalanir
/// (<c>SeedFilePath</c> + ekle/guncelle/sil senkronizasyonu). Istekle gelen ornek/demo satirlar
/// ekrandan girildiginde dosyaya yazilir ve migrate ile geri yuklenir.
/// </summary>
public bool SeedData { get; set; }
// Liste ekranı varsayılan olarak dört analiz görünümüyle gelir (UI wizard ve LayoutDto ile aynı); // Liste ekranı varsayılan olarak dört analiz görünümüyle gelir (UI wizard ve LayoutDto ile aynı);
// MCP/prompt ile gelen cevapta bayrak yazılmazsa görünüm sessizce kapanmasın diye. // MCP/prompt ile gelen cevapta bayrak yazılmazsa görünüm sessizce kapanmasın diye.
public string DefaultLayout { get; set; } = "grid"; public string DefaultLayout { get; set; } = "grid";
@ -204,6 +211,10 @@ public class ListFormWizardDto
KeyFieldDbSourceType = KeyFieldDbSourceType, KeyFieldDbSourceType = KeyFieldDbSourceType,
DefaultFilter = isDeletedField ? WizardConsts.DefaultFilterJson : null, DefaultFilter = isDeletedField ? WizardConsts.DefaultFilterJson : null,
DeleteCommand = isDeletedField ? WizardConsts.DefaultDeleteCommand(SelectCommand) : null, DeleteCommand = isDeletedField ? WizardConsts.DefaultDeleteCommand(SelectCommand) : null,
SeedFilePath = SeedData ? WizardConsts.SeedDataFilePath(listFormCode) : null,
SeedSyncInsert = SeedData,
SeedSyncUpdate = SeedData,
SeedSyncDelete = SeedData,
}; };
// Varsayilanlar WizardConsts uzerinden JSON olarak kurulur; DTO'nun nesne karsiliklari // Varsayilanlar WizardConsts uzerinden JSON olarak kurulur; DTO'nun nesne karsiliklari
@ -342,6 +353,7 @@ public class ListFormWizardDto
IsTenant = listForm.IsTenant; IsTenant = listForm.IsTenant;
IsBranch = listForm.IsBranch; IsBranch = listForm.IsBranch;
IsOrganizationUnit = listForm.IsOrganizationUnit; IsOrganizationUnit = listForm.IsOrganizationUnit;
SeedData = !string.IsNullOrWhiteSpace(listForm.SeedFilePath);
var layout = listForm.LayoutDto; var layout = listForm.LayoutDto;
DefaultLayout = layout.DefaultLayout; DefaultLayout = layout.DefaultLayout;

View file

@ -70,7 +70,7 @@ public class WizardSeedFileDto
[ [
nameof(ListFormWizardDto.IsTenant), nameof(ListFormWizardDto.IsBranch), nameof(ListFormWizardDto.IsOrganizationUnit), nameof(ListFormWizardDto.IsTenant), nameof(ListFormWizardDto.IsBranch), nameof(ListFormWizardDto.IsOrganizationUnit),
nameof(ListFormWizardDto.AllowAdding), nameof(ListFormWizardDto.AllowUpdating), nameof(ListFormWizardDto.AllowDeleting), nameof(ListFormWizardDto.AllowAdding), nameof(ListFormWizardDto.AllowUpdating), nameof(ListFormWizardDto.AllowDeleting),
nameof(ListFormWizardDto.AllowDetail), nameof(ListFormWizardDto.ConfirmDelete), nameof(ListFormWizardDto.AllowDetail), nameof(ListFormWizardDto.ConfirmDelete), nameof(ListFormWizardDto.SeedData),
nameof(ListFormWizardDto.DefaultLayout), nameof(ListFormWizardDto.Grid), nameof(ListFormWizardDto.Card), nameof(ListFormWizardDto.DefaultLayout), nameof(ListFormWizardDto.Grid), nameof(ListFormWizardDto.Card),
nameof(ListFormWizardDto.Pivot), nameof(ListFormWizardDto.Tree), nameof(ListFormWizardDto.Chart), nameof(ListFormWizardDto.Pivot), nameof(ListFormWizardDto.Tree), nameof(ListFormWizardDto.Chart),
nameof(ListFormWizardDto.Gantt), nameof(ListFormWizardDto.Scheduler), nameof(ListFormWizardDto.Todo), nameof(ListFormWizardDto.Gantt), nameof(ListFormWizardDto.Scheduler), nameof(ListFormWizardDto.Todo),

View file

@ -28557,14 +28557,8 @@
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.DeveloperKit.McpServers.Write", "key": "App.DeveloperKit.McpServers.Write",
"en": "Write Data", "en": "Write (data; development on host only)",
"tr": "Veri Yaz" "tr": "Yaz (veri; geliştirme yalnızca host)"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKit.McpServers.Develop",
"en": "Develop",
"tr": "Geliştir"
}, },
{ {
"resourceName": "Platform", "resourceName": "Platform",
@ -28604,9 +28598,21 @@
}, },
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.Mcp.Error.TenantReadOnly", "key": "App.Mcp.Error.TenantToolDisabled",
"en": "The '{0}' tool is not available to tenant users; over MCP a tenant can only read data.", "en": "The '{0}' tool is not available to tenant users; over MCP a tenant can read and write data but cannot develop.",
"tr": "'{0}' aracı kiracı kullanıcılarına açık değil; MCP üzerinden kiracı yalnızca veri okuyabilir." "tr": "'{0}' aracı kiracı kullanıcılarına açık değil; MCP üzerinden kiracı veri okuyup yazabilir, geliştirme yapamaz."
},
{
"resourceName": "Platform",
"key": "App.Mcp.Error.PermissionDenied",
"en": "You do not have the permission required for '{0}': {1}. Ask your administrator to grant it.",
"tr": "'{0}' için gereken yetkiniz yok: {1}. Yöneticinizden bu yetkiyi isteyin."
},
{
"resourceName": "Platform",
"key": "App.Mcp.Error.PermissionDeniedUnspecified",
"en": "You are not authorized for '{0}'. Ask your administrator which permission is required.",
"tr": "'{0}' için yetkiniz yok. Hangi yetkinin gerektiğini yöneticinize sorun."
}, },
{ {
"resourceName": "Platform", "resourceName": "Platform",
@ -28668,6 +28674,12 @@
"en": "ListFormCode cannot be empty.", "en": "ListFormCode cannot be empty.",
"tr": "ListFormCode boş bırakılamaz." "tr": "ListFormCode boş bırakılamaz."
}, },
{
"resourceName": "Platform",
"key": "App.Mcp.Error.InvalidLoadOption",
"en": "The '{0}' parameter could not be read. Send a JSON array of objects with selector and desc (group) or selector and summaryType (summaries); field names cannot contain spaces or '|'.",
"tr": "'{0}' parametresi okunamadı. selector ve desc (gruplama) ya da selector ve summaryType (özet) alanlı nesnelerden oluşan bir JSON dizisi gönderin; alan adında boşluk ya da '|' bulunamaz."
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.Mcp.Error.ValuesRequired", "key": "App.Mcp.Error.ValuesRequired",
@ -28716,6 +28728,96 @@
"en": "Component \"{0}\" was not found.", "en": "Component \"{0}\" was not found.",
"tr": "\"{0}\" bileşeni bulunamadı." "tr": "\"{0}\" bileşeni bulunamadı."
}, },
{
"resourceName": "Platform",
"key": "App.Mcp.Error.VerifyWizardFileInvalid",
"en": "Wizard file \"{0}\" could not be read: {1}",
"tr": "\"{0}\" wizard dosyası okunamadı: {1}"
},
{
"resourceName": "Platform",
"key": "App.Mcp.Error.VerifyMenuMissing",
"en": "Menu record \"{0}\" was not found.",
"tr": "\"{0}\" menü kaydı bulunamadı."
},
{
"resourceName": "Platform",
"key": "App.Mcp.Error.VerifyMenuDisabled",
"en": "Menu \"{0}\" is disabled.",
"tr": "\"{0}\" menüsü pasif."
},
{
"resourceName": "Platform",
"key": "App.Mcp.Error.VerifyMenuUrlMismatch",
"en": "Menu \"{0}\" points to \"{1}\", the wizard expects \"{2}\".",
"tr": "\"{0}\" menüsü \"{1}\" adresine gidiyor, wizard \"{2}\" bekliyor."
},
{
"resourceName": "Platform",
"key": "App.Mcp.Error.VerifyPermissionMissing",
"en": "Permission \"{0}\" is not defined or is disabled.",
"tr": "\"{0}\" yetkisi tanımlı değil ya da pasif."
},
{
"resourceName": "Platform",
"key": "App.Mcp.Error.VerifySelectPassed",
"en": "The screen query ran; {0} row(s) in total.",
"tr": "Ekran sorgusu çalıştı; toplam {0} satır."
},
{
"resourceName": "Platform",
"key": "App.Mcp.Error.VerifySelectFailed",
"en": "The screen query failed: {0}",
"tr": "Ekran sorgusu başarısız: {0}"
},
{
"resourceName": "Platform",
"key": "App.Mcp.Error.VerifyComponentInactive",
"en": "Component \"{0}\" is inactive.",
"tr": "\"{0}\" bileşeni pasif."
},
{
"resourceName": "Platform",
"key": "App.Mcp.Error.VerifyComponentRouteMismatch",
"en": "Component \"{0}\" route is \"{1}\", the menu points to \"{2}\".",
"tr": "\"{0}\" bileşeninin rotası \"{1}\", menü \"{2}\" adresine gidiyor."
},
{
"resourceName": "Platform",
"key": "App.Mcp.Error.VerifySeedClosureIncomplete",
"en": "Dependencies without a seed file; a migrate or another environment would miss them: {0}",
"tr": "Seed dosyası olmayan bağımlılıklar; migrate ya da başka bir ortam bunları kuramaz: {0}"
},
{
"resourceName": "Platform",
"key": "App.Mcp.Error.VerifySeedClosureSkipped",
"en": "Seed closure was not checked: it needs the {0} permission.",
"tr": "Seed kapanışı kontrol edilmedi: {0} yetkisi gerekiyor."
},
{
"resourceName": "Platform",
"key": "App.Mcp.Error.DevRequestNotFound",
"en": "Development request \"{0}\" was not found.",
"tr": "\"{0}\" geliştirme talebi bulunamadı."
},
{
"resourceName": "Platform",
"key": "App.Mcp.Error.DevRequestAmbiguous",
"en": "More than one development request is titled \"{0}\"; use the Id: {1}",
"tr": "\"{0}\" başlıklı birden fazla geliştirme talebi var; Id kullanın: {1}"
},
{
"resourceName": "Platform",
"key": "App.Mcp.Error.DevRequestNotReady",
"en": "Development request \"{0}\" is in \"{1}\"; only requests in \"Ready to Start\" can be started.",
"tr": "\"{0}\" geliştirme talebi \"{1}\" durumunda; yalnızca \"Ready to Start\" durumundaki talepler başlatılabilir."
},
{
"resourceName": "Platform",
"key": "App.Mcp.Error.DevRequestNoteRequired",
"en": "The note to append cannot be empty.",
"tr": "Eklenecek not boş olamaz."
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.Mcp.Error.VisualDesignerComponent", "key": "App.Mcp.Error.VisualDesignerComponent",

View file

@ -552,10 +552,13 @@ public static class PlatformConsts
/// <summary> /// <summary>
/// MCP sunucu tanimlarinin yetkileri. <see cref="McpServers.McpServer"/> ekrani gorme /// MCP sunucu tanimlarinin yetkileri. <see cref="McpServers.McpServer"/> ekrani gorme
/// (okuma) yetkisidir; <see cref="McpServers.Invoke"/> ise /mcp ucundan arac /// (okuma) yetkisidir. /mcp ucu iki kapi tanir: <see cref="McpServers.Invoke"/> uca
/// cagirma yetkisidir ve kiraci kullanicilarina da verilebilir. <see cref="McpServers.Develop"/> /// erisim ve tum okuma araclaridir, kiraci kullanicilarina da verilebilir;
/// /mcp ucundan ekran ve bilesen gelistirme araclarini acar; gelistirme islemi yine /// <see cref="McpServers.Write"/> MCP uzerinden yapilan her degisikliktir: kiracida
/// Wizard ve Custom Components yetkilerinden gecer. /// yalnizca satir ekleme/guncelleme/silme, host'ta ayrica tablo, ekran, bilesen ve
/// talep gelistirme. Ikisi de yetki vermez: arkadaki islem
/// uygulamadaki kendi yetkisinden (ekran C/U/D, Wizard, Custom Components, SQL Query
/// Manager) ayrica gecer.
/// </summary> /// </summary>
public static class McpServers public static class McpServers
{ {
@ -566,7 +569,6 @@ public static class PlatformConsts
public const string Delete = McpServer + ".Delete"; public const string Delete = McpServer + ".Delete";
public const string Invoke = McpServer + ".Invoke"; public const string Invoke = McpServer + ".Invoke";
public const string Write = McpServer + ".Write"; public const string Write = McpServer + ".Write";
public const string Develop = McpServer + ".Develop";
} }
} }

View file

@ -29,6 +29,12 @@ public static class WizardConsts
public static string LangKeyImport => "App.Platform.Import"; public static string LangKeyImport => "App.Platform.Import";
public static string LangKeyNote => "App.Platform.Note"; public static string LangKeyNote => "App.Platform.Note";
/// <summary>Liste verisinin aynalandigi seed klasoru; SeedPathResolver ile ayni deger.</summary>
public const string SeedDataFolder = "data";
/// <summary>Ekranin varsayilan veri seed dosyasi, kapsam klasorune gore goreli.</summary>
public static string SeedDataFilePath(string listFormCode) => $"{SeedDataFolder}/{listFormCode}.json";
public static string MenuUrl(string code) => $"/admin/list/{code}"; public static string MenuUrl(string code) => $"/admin/list/{code}";
public static string MenuIcon => "FcList"; public static string MenuIcon => "FcList";

View file

@ -72,12 +72,15 @@ public static class SeedPathResolver
public const string CrudFolder = "crud"; public const string CrudFolder = "crud";
/// <summary>Liste verisinin aynalandigi seed dosyalari (<c>data/{ListFormCode}.json</c>).</summary> /// <summary>Liste verisinin aynalandigi seed dosyalari (<c>data/{ListFormCode}.json</c>).</summary>
public const string DataFolder = "data"; public const string DataFolder = WizardConsts.SeedDataFolder;
/// <summary>Bir ListForm icin varsayilan veri seed dosyasinin kapsam klasorune gore goreli yolu.</summary> /// <summary>
/// Bir ListForm icin varsayilan veri seed dosyasinin kapsam klasorune gore goreli yolu. Bicim
/// Domain.Shared'dedir; wizard'in urettigi ekran (Application.Contracts) ayni yolu yazar.
/// </summary>
public static string GetDataFilePath(string listFormCode) public static string GetDataFilePath(string listFormCode)
{ {
return $"{DataFolder}/{listFormCode}.json"; return WizardConsts.SeedDataFilePath(listFormCode);
} }
/// <summary> /// <summary>

View file

@ -635,10 +635,16 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
var sorts = sort.Split(PlatformConsts.MultiValueDelimiter, StringSplitOptions.RemoveEmptyEntries); var sorts = sort.Split(PlatformConsts.MultiValueDelimiter, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < sorts.Length; i++) for (int i = 0; i < sorts.Length; i++)
{ {
// sorts[i] su sekilde gelir: "AlanIsmi desc" // sorts[i] su sekilde gelir: "AlanIsmi desc". Metin istemciden gelir ve ORDER BY'a
// yazilir; alan adi tirnak kiramaz, yon yalnizca asc/desc olabilir.
var sortElem = sorts[i].Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries); var sortElem = sorts[i].Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries);
sortElem[0] = $"\"{sortElem[0].Trim()}\""; // Grid uzerinden gelen siralama istegi icin alan ismini koseli parantes icerisine al (Anahtar isimdeki alanlar icin Bug olusmasini onler, ornegin Key isminde bir alana ait siralama istegi gelebilir) if (sortElem.Length is 0 or > 2 || sortElem[0].Contains('"'))
sortFields.Add(sortElem.JoinAsString(" ")); // Gelen istegi su sekle donusturur: [AlanIsmi] desc {
continue;
}
var direction = sortElem.Length == 2 && sortElem[1].Equals("desc", StringComparison.OrdinalIgnoreCase) ? "desc" : "asc";
sortFields.Add($"\"{sortElem[0]}\" {direction}"); // Anahtar kelime adli alanlar (Key gibi) icin tirnak icinde
} }
} }
else // ekran tanimi ile siralama yapiliyor ise else // ekran tanimi ile siralama yapiliyor ise

View file

@ -568,9 +568,12 @@ public class PlatformHttpApiHostModule : AbpModule
endpoints.MapHub<NotificationHub>("/notificationhub"); endpoints.MapHub<NotificationHub>("/notificationhub");
// MCP (Model Context Protocol) ucu. Kimlik dogrulama, kiraci cozumu ve unit of work // MCP (Model Context Protocol) ucu. Kimlik dogrulama, kiraci cozumu ve unit of work
// yukaridaki ardisik duzenden gelir; araclar kendi [Authorize] yetkilerini ister. // yukaridaki ardisik duzenden gelir. Uc yalnizca kimlik ister; Invoke her MCP
// Sema acikca bearer'a baglanir: varsayilan cerez semasi token'siz istegi giris // servisinin sinif duzeyindeki [Authorize] attribute'unda aranir. Uca konsaydi yetkisiz
// sayfasina 302 ile yonlendirir, MCP istemcisi ise 401 bekler. // kullanici nedeni belirtilmeyen bir 403 alirdi; servis duzeyinde reddedildiginde
// istemci eksik yetkiyi adiyla gorur (McpToolErrorTranslator). Sema acikca bearer'a
// baglanir: varsayilan cerez semasi token'siz istegi giris sayfasina 302 ile
// yonlendirir, MCP istemcisi 401 bekler.
endpoints.MapMcp("/mcp").RequireAuthorization(new AuthorizeAttribute endpoints.MapMcp("/mcp").RequireAuthorization(new AuthorizeAttribute
{ {
AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme

View file

@ -201,14 +201,41 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
süzgecinden geçmez ve hiç görünmez; MCP Servers ekranındaki `AllowedTools` seçenekleri de süzgecinden geçmez ve hiç görünmez; MCP Servers ekranındaki `AllowedTools` seçenekleri de
`SelectableTools`'dan üretildiği için kendiliğinden güncellenir. Sunucu kaydında tek anahtar `SelectableTools`'dan üretildiği için kendiliğinden güncellenir. Sunucu kaydında tek anahtar
`AllowedTools`'tur: boşsa tüm araçlar, doluysa yalnızca seçilenler açıktır; araç tipine göre ayrı `AllowedTools`'tur: boşsa tüm araçlar, doluysa yalnızca seçilenler açıktır; araç tipine göre ayrı
bir bayrak yoktur, sınırı kullanıcı yetkisi çizer (`RequiredPermissions`). **Kiracı kullanıcısı bir bayrak yoktur, sınırı kullanıcı yetkisi çizer. **İki MCP yetkisi vardır:** `.Invoke` uca
yalnızca okur**: kiracı bağlamında açık araçlar `McpToolNames.TenantTools` ile sınırlıdır ve erişim ve okumadır (MCP servislerinin sınıf düzeyinde aranır); `.Write` MCP üzerinden yapılan her değişikliktir ve
`AllowedTools` bu kümeyi yalnızca daraltır. Yeni bir araç veri yazıyor ya da okuma olduğu değişiklik yapan her araç `RequiredPermissions`'ta `.Write`'a bağlanır — okuma/yazma ayrımı için
garanti değilse (procedure, Custom Endpoint) `TenantTools`'a eklenmez. Veri yazan araçlar yeni yetki açılmaz. **Kiracı veri okur ve yazar, geliştirme yapamaz**: kiracı bağlamında açık
`.Write`, ekran/bileşen geliştirme araçları `DevelopTools`'a eklenip `.Develop` yetkisine bağlanır; araçlar `McpToolNames.TenantTools` ile sınırlıdır (şema, `query_list`, `run_select`,
işlem yine Wizard / Custom Components AppService'inden geçer. `insert_row`/`update_row`/`delete_row`) ve `AllowedTools` bu kümeyi yalnızca daraltır. Satır
yazma ekranın C/U/D sözleşmesinden geçtiği için kiracıya açıktır; ekran yetkisinden geçmeden
yazabilen araç (procedure, Custom Endpoint) ve geliştirme araçları (`DevelopTools`) `TenantTools`'a
eklenmez. Geliştirme işlemi yine Wizard / Custom Components AppService'inden geçer.
Kapı iki yerdedir: `AddListToolsFilter` listeyi süzer, `McpAppService.EnsureToolAllowedAsync` Kapı iki yerdedir: `AddListToolsFilter` listeyi süzer, `McpAppService.EnsureToolAllowedAsync`
çağrıyı reddeder — süzme tek başına yetki sınırı değildir, ikisi birlikte yazılır. çağrıyı reddeder — süzme tek başına yetki sınırı değildir, ikisi birlikte yazılır. `Invoke`
uçta değil her MCP servisinin sınıf düzeyindeki `[Authorize]`'dadır; yeni bir MCP AppService'i de
bu attribute'la açılır.
**Hata istemciye yalnızca `McpException` olarak ulaşır**; `McpToolErrorTranslator` (çağrı
filtresi) yetki reddini "şu yetkiniz yok" mesajına, `UserFriendlyException`'ı mesajına çevirir.
Kullanıcının göreceği her hata bu iki türden biriyle atılır. Yeni aracın arkasındaki servis
bir yetkiyi `[Authorize]` attribute'uyla istiyorsa (ret mesajı yetki adını taşımaz) o yetki
`McpToolNames.ServicePermissions`'a eklenir; ekran yetkisi gereken araç yetkiyi
`IMcpListFormAccessFilter.GetPermissionNameAsync` + `CheckAsync` ile adıyla önceden ister.
Araç → yetki tablosu `README.md` §13.9'dadır; araç ya da yetki değişirse tablo da değişir.
Hazır prompt'lar (`McpPromptNames` + `McpDevelopPrompts`) yalnızca metindir; adımları tekrar
etmez, `get_dev_guide` bölümüne yönlendirir. Bir kural bağlayıcıysa prompt'a değil araca yazılır
(örnek: `Ready to Start` şartı `start_dev_request`'tedir).
- **MCP ile demo veri** → geliştirme isteği örnek satır taşıyorsa (prompt'taki tablo/liste, ekran
görüntüsü) ekran `deploy_wizard`'a `SeedData: true` ile gider (`SeedFilePath = data/{ListFormCode}.json`
+ `SeedSync*` açık, `ListFormWizardDto.BuildListForm`), satırlar ardından `insert_row` ile eklenir ve
senkronizasyon onları `configs/seeds/host/data/{ListFormCode}.json` dosyasına yazar; migrate
ekranla birlikte seed eder. Dosya elle yazılmaz, görselden okunamayan değer uydurulmaz.
`SeedData` wizard ekranında sorulmaz ama düzenlemede korunur (`RefreshFrom` + UI modeli).
Ayrıntı: `lowcode.instructions.md` §0.10.1.
- **Talepteki görsel** → `App.DevRequests` açıklamasına yapıştırılan görsel base64 `data:` URI olarak
kalır, hiçbir yere yüklenmez. `start_dev_request` / `get_dev_request` onu metinden ayıklar
(`McpDevRequestImages`) ve MCP resim içeriği (`ImageContentBlock`) olarak döndürür; metinde
`[image-N]` kalır. Açıklama bu yüzden `update_row` ile yeniden yazılmaz, not
`append_dev_request_note` ile eklenir.
- **MCP şema araçları** → tablo/sütun bilgisi canlı katalogdan değil `Sas_H_DbTableColumn` - **MCP şema araçları** → tablo/sütun bilgisi canlı katalogdan değil `Sas_H_DbTableColumn`
sözlüğünden okunur (`IMcpSchemaDictionary`). Sözlüğün alan adları sözlüğünden okunur (`IMcpSchemaDictionary`). Sözlüğün alan adları
`dbo.Sas_H_ListFormFieldsDictionaryJson` prosedürünün ürettiği JSON ile birebir aynıdır; `dbo.Sas_H_ListFormFieldsDictionaryJson` prosedürünün ürettiği JSON ile birebir aynıdır;
@ -223,7 +250,7 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
- **MCP yetki vermez** → yeni bir MCP aracı kullanıcının uygulamadaki yetkisini aşamaz: liste verisi - **MCP yetki vermez** → yeni bir MCP aracı kullanıcının uygulamadaki yetkisini aşamaz: liste verisi
ekranın sorgusundan (`IListFormSelectAppService`/`IListFormDataAppService`) geçer; liste ekranın sorgusundan (`IListFormSelectAppService`/`IListFormDataAppService`) geçer; liste
yetkisini aşan bir yetenek (ham SQL gibi) `McpToolNames.RequiredPermissions`'a uygulamadaki yetkisini aşan bir yetenek (ham SQL gibi) `McpToolNames.RequiredPermissions`'a uygulamadaki
karşılık gelen yetkiyle eklenir. `create_table` bu yüzden `.Develop`'a ek olarak `App.SqlQueryManager` karşılık gelen yetkiyle eklenir. `create_table` bu yüzden `.Write`'a ek olarak `App.SqlQueryManager`
ister; tablo scriptini tasarımcının `generateCreateTableSql` çıktısıyla aynı biçimde üretir ister; tablo scriptini tasarımcının `generateCreateTableSql` çıktısıyla aynı biçimde üretir
(`McpTableCreator`) — tasarımcının şablon kolonları ya da script biçimi değişirse o da değişir. (`McpTableCreator`) — tasarımcının şablon kolonları ya da script biçimi değişirse o da değişir.
- **Bildirim kapsamı** → bildirim tipi, kuralı ve kuyruğu kiracıya aittir (`Adm_T_NotificationType`, - **Bildirim kapsamı** → bildirim tipi, kuralı ve kuyruğu kiracıya aittir (`Adm_T_NotificationType`,

View file

@ -2764,16 +2764,6 @@
"MultiTenancySide": 3, "MultiTenancySide": 3,
"Modules": "Sas|Adm" "Modules": "Sas|Adm"
}, },
{
"GroupName": "App.Saas",
"Name": "App.DeveloperKit.McpServers.Develop",
"ParentName": "App.DeveloperKit.McpServers",
"DisplayName": "App.DeveloperKit.McpServers.Develop",
"Order": 8,
"IsEnabled": true,
"MultiTenancySide": 2,
"Modules": "Sas|Adm"
},
{ {
"GroupName": "App.Saas", "GroupName": "App.Saas",
"Name": "App.Listforms.Wizard", "Name": "App.Listforms.Wizard",

View file

@ -12,7 +12,7 @@
"Tenants": null, "Tenants": null,
"AllowedTools": null, "AllowedTools": null,
"MaxRows": 200, "MaxRows": 200,
"Description": "Default MCP endpoint. Every tool is published while AllowedTools is empty; each call still needs the user's own permission (App.DeveloperKit.McpServers.Invoke, .Write for writing data, .Develop for screen development)." "Description": "Default MCP endpoint. Every tool is published while AllowedTools is empty; each call still needs the user's own permission (App.DeveloperKit.McpServers.Invoke for access and reading, .Write for any change: data, tables, screens, components)."
} }
] ]
} }

View file

@ -71,6 +71,11 @@ export interface ListFormWizardDto {
allowDeleting: boolean allowDeleting: boolean
confirmDelete: boolean confirmDelete: boolean
allowDetail: boolean allowDetail: boolean
/**
* Ekran verisi `data/{ListFormCode}.json` seed dosyasına aynalanır; migrate satırları geri yükler.
* Wizard adımlarında sorulmaz (MCP ile demo veri taşıyan ekranlar açar), düzenlemede korunur.
*/
seedData: boolean
defaultLayout: ListViewLayoutType defaultLayout: ListViewLayoutType
grid: boolean grid: boolean

View file

@ -72,6 +72,7 @@ const initialValues: ListFormWizardDto = {
allowDeleting: true, allowDeleting: true,
confirmDelete: true, confirmDelete: true,
allowDetail: false, allowDetail: false,
seedData: false,
defaultLayout: 'grid', defaultLayout: 'grid',
grid: true, grid: true,
card: true, card: true,
@ -709,6 +710,7 @@ const Wizard = () => {
allowDeleting: w.allowDeleting ?? true, allowDeleting: w.allowDeleting ?? true,
confirmDelete: w.confirmDelete ?? true, confirmDelete: w.confirmDelete ?? true,
allowDetail: w.allowDetail ?? false, allowDetail: w.allowDetail ?? false,
seedData: w.seedData ?? false,
defaultLayout: w.defaultLayout ?? 'grid', defaultLayout: w.defaultLayout ?? 'grid',
grid: w.grid ?? initialValues.grid, grid: w.grid ?? initialValues.grid,
card: w.card ?? initialValues.card, card: w.card ?? initialValues.card,