diff --git a/.github/instructions/lowcode-reference.instructions.md b/.github/instructions/lowcode-reference.instructions.md index 9a6662b2..ddfab24f 100644 --- a/.github/instructions/lowcode-reference.instructions.md +++ b/.github/instructions/lowcode-reference.instructions.md @@ -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 › 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. | -| **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ğı. | | **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. | @@ -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 › 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 › 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`. | | **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` | | "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ı 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 | | "yapay zekâ cevaplasın" | Saas › Definitions › AiBot | Bot tanımı; `/admin/ai` bu tanımı kullanır | diff --git a/.github/instructions/lowcode.instructions.md b/.github/instructions/lowcode.instructions.md index 5a67176c..880d5c9d 100644 --- a/.github/instructions/lowcode.instructions.md +++ b/.github/instructions/lowcode.instructions.md @@ -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 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, -kiracı kullanıcısına hiçbiri açılmaz. +`McpToolNames`'tedir; okuyanlar `App.DeveloperKit.McpServers.Invoke`, değiştirenler `.Write` +ister ve kiracı kullanıcısına hiçbiri açılmaz. | 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 | | `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` | -| `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) | +| `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) | +**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`, `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. +### 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ığı | +| --- | --- | +| ` 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 ` 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": "", "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 `

MCP

` 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 | İ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ı**. | | `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. | | `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. | @@ -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) | | 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` | -| 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) | | 5 | Alt Formlar | yalnız List | `SubForms[]` | | 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 | | **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 | -| **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 diff --git a/README.md b/README.md index f37162a2..02839f4d 100644 --- a/README.md +++ b/README.md @@ -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 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`, -`search_schema`, `query_list`, `run_select`. `AllowedTools` bu kümeyi yalnızca daraltabilir; -kiracının `.Write`/`.Develop` ya da başka bir yetkisi olsa da veri yazma, `create_table`, wizard ve -component araçları listelenmez, çağrılırsa `App.Mcp.Error.TenantReadOnly` ile reddedilir. -`list_queries`/`run_query` da dışarıdadır: procedure ve Custom Endpoint çağrısı veri -yazabileceği için okuma olduğu garanti edilemez. Host kullanıcısı için tek sunucu anahtarı +`search_schema`, `query_list`, `run_select`, `insert_row`, `update_row`, `delete_row`. +`AllowedTools` bu kümeyi yalnızca daraltabilir. Satır araçları ekranın C/U/D sözleşmesinden ve +kiracının kendi veritabanından geçer. `create_table`, wizard, component ve geliştirme talebi +araçları kiracının yetkisi ne olursa olsun listelenmez, çağrılırsa `App.Mcp.Error.TenantToolDisabled` +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, 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. | | `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. | -| `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. | | `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ı. | | `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. | -| `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. | -| `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. | +| `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 ` | Mesaj kutusundaki `+` menüsü → `sozsoft` → `process_dev_request`, `request` alanına Id ya da tam 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. | -**Yetki modeli: MCP yetki vermez.** Kullanıcı MCP'de uygulamadaki yetkisini taşır: -- Şema araçları yalnızca kullanıcının okuma yetkisi olan listeleri gösterir (`McpListFormAccessFilter` - — 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 - filtreleri, kullanıcı özelleştirmeleri uygulamadaki gibidir. -- Ham SQL (`run_select`, veritabanı nesneleri) liste yetkilerini aşar; yalnızca uygulamada - `App.SqlQueryManager` yetkisi olana açıktır. Yazma araçları `.Write` + ekranın C/U/D yetkisini ister. -- Geliştirme araçları (`McpToolNames.DevelopTools`) `.Develop` yetkisini ister; sunucu bazında - `AllowedTools` ile kapatılır. `create_table` ham DDL olduğu için tablo tasarımcısıyla aynı - `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 - (`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 - tekrarlanır. Araç → yetki eşlemesi `McpToolNames.RequiredPermissions`'tadır. +**Yetki modeli: MCP yetki vermez.** Kullanıcı MCP'de uygulamadaki yetkisini taşır; MCP'nin iki +yetkisi yalnızca kapıdır. Bir çağrı üç katmandan geçer ve her katman ayrı reddeder: + +1. **Sunucu kaydı** (`AllowedTools`, `Tenants`) — o kiracıda hangi araçların var olduğu. Kiracıdaki + herkes için aynıdır. +2. **MCP kapısı** (`.Invoke`, `.Write`) — kullanıcının MCP'yi okumak / değiştirmek için + kullanabilir olup olmadığı. Rol/kullanıcı bazındadır. +3. **Uygulama yetkisi** — işlemin kendisi: ekranın C/R/U/D yetkisi, Wizard, Custom Components, + SQL Query Manager. Kullanıcı uygulamada yapamadığını MCP'de de yapamaz. + +**MCP yetkileri — ne açar, sınırı ne** + +| Yetki | Kim alabilir | Ne açar | Açmadığı / sını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 `.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. -**Yetkiler.** `App.DeveloperKit.McpServers` (ekran), `.Invoke` (araç çağırma), `.Write` -(veri yazma) ve `.Develop` (tablo/ekran/bileşen geliştirme). `.Invoke` `MultiTenancySide = Both` -olduğu için kiracı kullanıcısına da verilir; `.Write` da kiracıya tanımlanabilir ama MCP'de -kiracı salt okuma kümesiyle sınırlı olduğu için etkisi yoktur. `.Develop` host'tur — -arkasındaki Wizard ve Custom Components yetkileri de yalnızca host'ta tanımlıdır. +**Yetkiler.** Ne açtıkları ve sınırları yukarıdaki "Yetki modeli" tablosundadır. İkisi de +`MultiTenancySide = Both`'tur. Eskiden geliştirme için ayrı olan `.Develop` yetkisi `.Write` ile +birleştirildi; mevcut veritabanında atamalar migrate sırasında `.Write`'a taşınır ve tanım silinir +(`McpDevelopPermissionMergeDataSeedContributor`). + +`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. --- diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpDevelopDtos.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpDevelopDtos.cs index a82e4412..8f18f6fe 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpDevelopDtos.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpDevelopDtos.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text.Json; +using System.Text.Json.Serialization; namespace Sozsoft.Mcp.Dtos; @@ -92,6 +93,84 @@ public class McpCreateTableResultDto public string SeedFilePath { get; set; } = string.Empty; } +/// +/// Deploy edilmis bir wizard'in salt-okur dogrulamasi. yalnizca +/// bir kontrol kaldiginda false olur; engellemeyen +/// kontroller (seed kapanisi gibi) ekrani kullanilamaz yapmaz ama migrate sonrasini etkiler. +/// +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 Checks { get; set; } = []; +} + +public class McpVerifyCheckDto +{ + /// Kontrolun kisa adi: wizard_file, menu, permission, select, component, seed_closure. + public string Name { get; set; } = string.Empty; + + public bool Passed { get; set; } + + public bool Blocking { get; set; } = true; + + public string? Message { get; set; } +} + +/// Ustlenilen gelistirme talebi; uretim icin gereken metni tasir. +public class McpDevRequestDto +{ + public Guid Id { get; set; } + + public string? Title { get; set; } + + public string Status { get; set; } = string.Empty; + + /// + /// HTML. Gomulu gorseller (data:image/...;base64) metinden cikarilir; yerlerinde + /// durur, kendileri 'tadir. + /// + public string? Description { get; set; } + + /// Aciklamadaki gorseller, metinde gectikleri sirayla. + public List 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; } + + /// JSON dizisi metni: { id, title, assignee, dueDate, completed }. + public string? SubTasks { get; set; } +} + +/// +/// Talep aciklamasina gomulu bir gorsel. Veri metin cevabina yazilmaz; MCP araci onu ayri bir +/// gorsel icerigi olarak iletir, model de resmi dogrudan gorur. +/// +public class McpDevRequestImageDto +{ + /// Aciklamada gorselin yerine yazilan isaret, ornegin [image-1]. + public string Placeholder { get; set; } = string.Empty; + + public string MimeType { get; set; } = string.Empty; + + /// Base64 veri. + [JsonIgnore] + public string Data { get; set; } = string.Empty; +} + /// Custom component ozeti; kod tasimaz. public class McpComponentSummaryDto { diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDevelopAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDevelopAppService.cs index 41e5133d..c329c6b6 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDevelopAppService.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDevelopAppService.cs @@ -27,6 +27,21 @@ public interface IMcpDevelopAppService : IApplicationService Task DeleteWizardAsync(string fileName); + /// Deploy edilmis ekrani veri yazmadan dogrular. + Task VerifyWizardAsync(string fileName); + + /// Id ya da basliga gore bulunan talebi, yalnizca Ready to Start durumundaysa In Progress'e alir. + Task StartDevRequestAsync(string request); + + /// Talebi durumuna dokunmadan okur; yarim kalan bir talebe donmek icindir. + Task GetDevRequestAsync(string request); + + /// + /// Aciklamanin sonuna HTML ekler. Aciklama gomulu gorsel tasidigi icin satir guncellemesiyle + /// yeniden yazilamaz; ekleme sunucuda yapilir. + /// + Task AppendDevRequestNoteAsync(string request, string html); + Task> GetComponentsAsync(string? search); Task GetComponentAsync(string name); diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpAppService.cs index 1a92ffdf..a62b55d5 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpAppService.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpAppService.cs @@ -64,7 +64,7 @@ public abstract class McpAppService : ApplicationService 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)) diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDataAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDataAppService.cs index 5b5c6d6c..fc4a89be 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDataAppService.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDataAppService.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authorization; using Sozsoft.Mcp.Dtos; using Sozsoft.Mcp.Services; using Sozsoft.Platform; +using Sozsoft.Platform.Enums; using Sozsoft.Platform.ListForms.Select; using Volo.Abp; @@ -16,15 +17,16 @@ namespace Sozsoft.Mcp; /// sozlesmesinden gecer; ekran yetkisi, tenant filtresi ve seed senkronizasyonu oradaki /// yoldan aynen uygulanir. /// -[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Write)] +[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Invoke)] public class McpDataAppService( IMcpServerConfigProvider serverConfigProvider, - IListFormDataAppService listFormDataAppService) + IListFormDataAppService listFormDataAppService, + IMcpListFormAccessFilter accessFilter) : McpAppService(serverConfigProvider), IMcpDataAppService { public async Task 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); return new McpWriteResultDto { Key = ((object?)key)?.ToString() }; @@ -32,7 +34,7 @@ public class McpDataAppService( public async Task 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); return new McpWriteResultDto { AffectedRows = affected }; @@ -40,7 +42,7 @@ public class McpDataAppService( public async Task 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); return new McpWriteResultDto { AffectedRows = affected }; @@ -53,6 +55,7 @@ public class McpDataAppService( private async Task BuildRequestAsync( McpRowRequestDto input, string toolName, + AuthorizationTypeEnum operation, bool requireKeys, bool requireValues) { @@ -65,6 +68,14 @@ public class McpDataAppService( 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)) { throw new UserFriendlyException(L[McpErrorCodes.ValuesRequired]); diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevRequestImages.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevRequestImages.cs new file mode 100644 index 00000000..c13f7ce2 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevRequestImages.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Sozsoft.Mcp.Dtos; + +namespace Sozsoft.Mcp; + +/// +/// Talep aciklamasina yapistirilan gorseller HtmlEditor'da base64 data: 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. +/// +internal static partial class McpDevRequestImages +{ + private static readonly HashSet SupportedMimeTypes = new(StringComparer.OrdinalIgnoreCase) + { + "image/png", "image/jpeg", "image/gif", "image/webp" + }; + + [GeneratedRegex("""]*?\bsrc\s*=\s*["']data:(?image/[\w.+-]+);base64,(?[^"']+)["'][^>]*>""", RegexOptions.IgnoreCase)] + private static partial Regex EmbeddedImage(); + + public static (string? Html, List Images) Extract(string? html) + { + var images = new List(); + 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 _); +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs index d49aa779..9d6a1314 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs @@ -9,25 +9,34 @@ using Sozsoft.Mcp.Dtos; using Sozsoft.Mcp.Services; using Sozsoft.Platform; using Sozsoft.Platform.DeveloperKit; +using Sozsoft.Platform.Entities; +using Sozsoft.Platform.Enums; using Sozsoft.Platform.ListForms; +using Sozsoft.Platform.ListForms.Select; using Volo.Abp; using Volo.Abp.Application.Dtos; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.PermissionManagement; namespace Sozsoft.Mcp; /// /// Ekran ve bilesen gelistirme yuzeyi. Uretim Wizard ve Custom Components servislerinden /// 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. /// -[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop)] +[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Invoke)] public class McpDevelopAppService( IMcpServerConfigProvider serverConfigProvider, IMcpDevGuide devGuide, IMcpTableCreator tableCreator, IListFormWizardAppService wizardAppService, - ICustomComponentAppService componentAppService) + ICustomComponentAppService componentAppService, + IListFormSelectAppService listFormSelectAppService, + IRepository menuRepository, + IRepository permissionRepository, + IRepository devRequestRepository) : McpAppService(serverConfigProvider), IMcpDevelopAppService { private const string DefaultGuideDocument = "lowcode"; @@ -38,6 +47,19 @@ public class McpDevelopAppService( /// Custom Components ekraninin tek sayfada dondurebildigi ust sinir. private const int ComponentPageSize = 1000; + private const string DevRequestUpdatePermission = PlatformConsts.AppCodes.DevRequests + ".Update"; + + /// verify_wizard cevabindaki kontrol adlari. + 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 GetDevGuideAsync(string? document, string? section) { await EnsureToolAllowedAsync(McpToolNames.GetDevGuide); @@ -147,6 +169,261 @@ public class McpDevelopAppService( return await wizardAppService.DeleteFile(fileName); } + /// + /// 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. + /// + public async Task 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; + } + + /// + /// 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. + /// + public async Task 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 GetDevRequestAsync(string request) + { + await EnsureToolAllowedAsync(McpToolNames.GetDevRequest); + await AuthorizationService.CheckAsync(PlatformConsts.AppCodes.DevRequests); + + return ToDto(await FindDevRequestAsync(request)); + } + + /// + /// 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. + /// + 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 + }; + } + + /// Id ile ya da tam baslikla bulur; ayni basligi tasiyan birden fazla talep varsa Id istenir. + private async Task 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))]) + }; + } + + /// + /// Kapanis analizi wizard export'unun yetkisini ister; o yetki dogrulamanin kendisini + /// dusurmesin diye yoksa kontrol atlanir ve nedeni raporlanir. + /// + private async Task 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 VerifyMenuAsync(ListFormWizardDto wizard, List 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; + } + + /// + /// Ekranin sorgusunu query_list ile ayni servisten calistirir; kolon, veri kaynagi ve yetki + /// hatalari burada ortaya cikar. + /// + private async Task 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 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> GetComponentsAsync(string? search) { await EnsureToolAllowedAsync(McpToolNames.ListComponents); diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpLoadOptions.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpLoadOptions.cs new file mode 100644 index 00000000..a6a5f18c --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpLoadOptions.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using Sozsoft.Platform; + +namespace Sozsoft.Mcp; + +/// +/// query_list'in DevExtreme bicimindeki (JSON) sort/group/summary girdilerini ekranin select +/// ucunun bekledigi metin bicimine (Alan desc|Alan2 asc) cevirir. Arayuzde ayni donusumu +/// ui/src/views/list/Utils.ts yapar; select ucu JSON'u tanimaz ve ORDER BY'a oldugu gibi +/// ekler. JSON olmayan girdi zaten metin bicimindedir, dokunulmaz. Bozuk girdi +/// ya da atar. +/// +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 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; + } +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpQueryAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpQueryAppService.cs index 0958e7d2..f81bc34a 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpQueryAppService.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpQueryAppService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Sozsoft.Mcp.Dtos; @@ -33,7 +34,8 @@ public class McpQueryAppService( IMcpSqlGuard sqlGuard, IMcpQueryExecutor queryExecutor, IRepository customEndpointRepository, - IListFormSelectAppService listFormSelectAppService) + IListFormSelectAppService listFormSelectAppService, + IMcpListFormAccessFilter accessFilter) : McpAppService(serverConfigProvider), IMcpQueryAppService { private const string SqlPermission = PlatformConsts.AppCodes.SqlQueryManagers.Default; @@ -46,14 +48,20 @@ public class McpQueryAppService( var config = await EnsureToolAllowedAsync(McpToolNames.QueryList); 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 { ListFormCode = input.ListFormCode, Filter = NullIfEmpty(input.Filter), - Sort = NullIfEmpty(input.Sort), - Group = NullIfEmpty(input.Group), - GroupSummary = NullIfEmpty(input.GroupSummary), - TotalSummary = NullIfEmpty(input.TotalSummary), + Sort = ToLoadOption(nameof(input.Sort), input.Sort, McpLoadOptions.ToSort), + Group = ToLoadOption(nameof(input.Group), input.Group, McpLoadOptions.ToGroup), + GroupSummary = ToLoadOption(nameof(input.GroupSummary), input.GroupSummary, McpLoadOptions.ToSummary), + TotalSummary = ToLoadOption(nameof(input.TotalSummary), input.TotalSummary, McpLoadOptions.ToSummary), Skip = Math.Max(input.Skip, 0), Take = take, RequireTotalCount = true, @@ -69,6 +77,20 @@ public class McpQueryAppService( Take = take }; } + + /// Bozuk girdi SQL hatasina donusmeden, hangi parametrede oldugu soylenerek reddedilir. + private string? ToLoadOption(string parameter, string? value, Func 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 RunSelectAsync(McpSelectRequestDto input) { Check.NotNull(input, nameof(input)); diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpConsts.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpConsts.cs index f542e4f1..af99641b 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpConsts.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpConsts.cs @@ -90,6 +90,16 @@ public static class McpConsts public const string CustomEndpoint = "customEndpoint"; } + /// + /// Gelistirme talebi (App.DevRequests) durumlari; ekranin Status lookup + /// anahtarlariyla ayni olmak zorundadir. + /// + public static class DevRequestStatuses + { + public const string ReadyToStart = "Ready to Start"; + public const string InProgress = "In Progress"; + } + /// /// Veritabanı katalog sorgusunun dondurdugu nesne turu etiketleri. /// degerlerine eslenir. diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs index 30de78ff..15d51ded 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs @@ -10,7 +10,9 @@ public static class McpErrorCodes public const string NoServerRecord = Prefix + "NoServerRecord"; 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 SingleStatementOnly = Prefix + "SingleStatementOnly"; public const string ReadOnlyStatementRequired = Prefix + "ReadOnlyStatementRequired"; @@ -21,6 +23,7 @@ public static class McpErrorCodes public const string CustomEndpointNotFound = Prefix + "CustomEndpointNotFound"; public const string MissingParameters = Prefix + "MissingParameters"; public const string ListFormCodeRequired = Prefix + "ListFormCodeRequired"; + public const string InvalidLoadOption = Prefix + "InvalidLoadOption"; public const string ValuesRequired = Prefix + "ValuesRequired"; public const string KeysRequired = Prefix + "KeysRequired"; public const string DictionaryNotFound = Prefix + "DictionaryNotFound"; @@ -32,6 +35,21 @@ public static class McpErrorCodes public const string ComponentNameRequired = Prefix + "ComponentNameRequired"; public const string ComponentCodeRequired = Prefix + "ComponentCodeRequired"; 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 TableColumnsRequired = Prefix + "TableColumnsRequired"; public const string UnsupportedColumnType = Prefix + "UnsupportedColumnType"; diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpPromptNames.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpPromptNames.cs new file mode 100644 index 00000000..d6f8a3a9 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpPromptNames.cs @@ -0,0 +1,11 @@ +namespace Sozsoft.Mcp; + +/// +/// /mcp ucunda yayinlanan hazir prompt adlari; istemcide komut olarak gorunur. Prompt yalnizca +/// metindir, yetki tasimaz: calistirdigi araclar kendi kapisindan gecer. +/// +public static class McpPromptNames +{ + public const string ProcessDevRequest = "process_dev_request"; + public const string ProcessReadyRequests = "process_ready_requests"; +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs index d0c25035..fbf822d5 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs @@ -28,14 +28,19 @@ public static class McpToolNames public const string GetWizard = "get_wizard"; public const string DeployWizard = "deploy_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 GetComponent = "get_component"; public const string SaveComponent = "save_component"; public const string DeleteComponent = "delete_component"; /// - /// Ekran ve bilesen gelistirme araclari. Sunucu bazinda AllowedTools ile, kullanici - /// bazinda McpServers.Develop yetkisiyle kapatilir. + /// Ekran ve bilesen gelistirme araclari; kiraci baglaminda hicbiri acilmaz. Okuyanlar + /// McpServers.Invoke ile, degistirenler McpServers.Write ile acilir + /// (). /// public static readonly string[] DevelopTools = [ @@ -45,6 +50,10 @@ public static class McpToolNames GetWizard, DeployWizard, DeleteWizard, + VerifyWizard, + StartDevRequest, + GetDevRequest, + AppendDevRequestNote, ListComponents, GetComponent, SaveComponent, @@ -58,11 +67,12 @@ public static class McpToolNames public static readonly string[] AlwaysAvailableTools = [ServerInfo]; /// - /// Kiraci kullanicisinin gorebildigi araclarin tamami; kiraci baglaminda MCP yalnizca veri - /// okur. AllowedTools bu kumeyi yalnizca daraltabilir, genisletemez. - /// ve disarida kalir: procedure ve Custom Endpoint cagrisi veri - /// yazabilir, okuma oldugu garanti edilemez. ise tek SELECT disindaki - /// her ifadeyi reddettigi icin okumadir. + /// Kiraci kullanicisinin gorebildigi araclarin tamami: veri okuma ve ekranin + /// insert/update/delete sozlesmesinden gecen satir yazma. Yazilim gelistirme araclari + /// () kiraciya acilmaz. AllowedTools bu kumeyi yalnizca + /// daraltabilir, genisletemez. ve disarida + /// kalir: procedure ve Custom Endpoint ekran yetkisinden gecmeden veri yazabilir. + /// ise tek SELECT disindaki her ifadeyi reddettigi icin okumadir. /// public static readonly string[] TenantTools = [ @@ -71,15 +81,18 @@ public static class McpToolNames DescribeList, SearchSchema, QueryList, - RunSelect + RunSelect, + InsertRow, + UpdateRow, + DeleteRow ]; /// - /// Kullanicinin uygulamada da sahip olmasi gereken yetki. MCP yetki vermez: ham SQL'i - /// uygulamada SQL Query Manager'i kullanabilen, veri yazmayi yazma yetkisi olan kullanici - /// gorur. Listede olmayan araclar kendi icinde liste bazli yetkiyle suzulur. Gelistirme - /// araclarinin arkasindaki Wizard ve Custom Components servisleri kendi yetkilerini ayrica - /// ister; Develop yalnizca MCP kapisidir. + /// McpServers.Invoke (/mcp ucunun kendisinde aranir) disinda istenen yetki. Iki + /// turdur: MCP uzerinden her degisiklik McpServers.Write ister; ham SQL liste + /// yetkilerini astigi icin uygulamadaki SQL Query Manager yetkisini ister. Listede olmayan + /// araclar okumadir ve arkadaki servisin kendi yetkisiyle suzulur. MCP yetki vermez: + /// Wizard, Custom Components ve ekran C/U/D yetkileri ayrica aranir. /// public static readonly IReadOnlyDictionary RequiredPermissions = new Dictionary { @@ -87,18 +100,58 @@ public static class McpToolNames [InsertRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, [UpdateRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, [DeleteRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, - [GetDevGuide] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop, - [CreateTable] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop, - [ListWizards] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop, - [GetWizard] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop, - [DeployWizard] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop, - [DeleteWizard] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop, - [ListComponents] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop, - [GetComponent] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop, - [SaveComponent] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop, - [DeleteComponent] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop, + [CreateTable] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, + [DeployWizard] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, + [DeleteWizard] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, + [StartDevRequest] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, + [AppendDevRequestNote] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, + [SaveComponent] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, + [DeleteComponent] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, }; + /// + /// Arkadaki servisin [Authorize] 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, + /// Wizards.Create/Update, Custom Components, App.DevRequests.Update) reddi zaten + /// adiyla bildirdigi icin buraya yazilmaz. + /// + public static readonly IReadOnlyDictionary ServicePermissions = new Dictionary + { + [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], + }; + + /// + /// Bir aracin cagrisinda aranabilecek, adi bilinen yetkiler: uc kapisi (Invoke), MCP kapisi + /// () ve arkadaki servis (). + /// + public static IEnumerable 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; + } + } + } + /// Yayinlanan tum araclar, arac listesindeki siralarina gore. public static readonly string[] All = [ diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Data/McpDevelopPermissionMergeDataSeedContributor.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Data/McpDevelopPermissionMergeDataSeedContributor.cs new file mode 100644 index 00000000..31ec7e21 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Data/McpDevelopPermissionMergeDataSeedContributor.cs @@ -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; + +/// +/// Kaldirilan McpServers.Develop yetkisini McpServers.Write'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. +/// +public class McpDevelopPermissionMergeDataSeedContributor( + IRepository grantRepository, + IRepository 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); + } +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpListFormAccessFilter.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpListFormAccessFilter.cs index 8753e44a..2ebe2603 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpListFormAccessFilter.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpListFormAccessFilter.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Sozsoft.Platform.Entities; +using Sozsoft.Platform.Enums; using Sozsoft.Platform.Permissions; using Volo.Abp.Authorization.Permissions; using Volo.Abp.Domain.Repositories; @@ -19,6 +20,12 @@ namespace Sozsoft.Mcp.Services; public interface IMcpListFormAccessFilter { Task> GetReadableListFormCodesAsync(); + + /// + /// Ekranin bir islem icin istedigi yetkinin adi (PermissionJson); ekran ya da tanim + /// yoksa null. Yetki adiyla kontrol edildiginde reddin nedeni kullaniciya adiyla iletilir. + /// + Task GetPermissionNameAsync(string listFormCode, AuthorizationTypeEnum operation); } public class McpListFormAccessFilter( @@ -66,7 +73,30 @@ public class McpListFormAccessFilter( .ToHashSet(StringComparer.OrdinalIgnoreCase); } + public async Task 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) + { + var permissions = Deserialize(permissionJson); + + return string.IsNullOrWhiteSpace(permissions?.R) ? null : permissions.R; + } + + private static PermissionCrudValueObject? Deserialize(string? permissionJson) { if (string.IsNullOrWhiteSpace(permissionJson)) { @@ -75,9 +105,7 @@ public class McpListFormAccessFilter( try { - var permissions = JsonSerializer.Deserialize(permissionJson); - - return string.IsNullOrWhiteSpace(permissions?.R) ? null : permissions.R; + return JsonSerializer.Deserialize(permissionJson); } catch (JsonException) { diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpHttpApiModule.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpHttpApiModule.cs index 1d3163cf..958b832c 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpHttpApiModule.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpHttpApiModule.cs @@ -46,20 +46,44 @@ public class McpHttpApiModule : AbpModule .WithTools() .WithTools() .WithTools() - .WithRequestFilters(filters => filters.AddListToolsFilter(next => async (requestContext, cancellationToken) => + .WithPrompts(typeof(McpDevelopPrompts)) + .WithRequestFilters(filters => { - var result = await next(requestContext, cancellationToken); + filters.AddListToolsFilter(next => async (requestContext, cancellationToken) => + { + var result = await next(requestContext, cancellationToken); - // Arac listesi kiracinin sunucu kaydina ve kullanicinin uygulamadaki yetkilerine - // gore suzulur. Cagri aninda da ayni kural uygulanir - // (McpAppService.EnsureToolAllowedAsync): istemci listede olmayan bir araci da - // cagirabilir, suzme tek basina bir yetki siniri degildir. - var schemaAppService = requestContext.Services!.GetRequiredService(); - var available = (await schemaAppService.GetAvailableToolsAsync()).ToHashSet(System.StringComparer.Ordinal); + // Arac listesi kiracinin sunucu kaydina ve kullanicinin uygulamadaki yetkilerine + // gore suzulur. Cagri aninda da ayni kural uygulanir + // (McpAppService.EnsureToolAllowedAsync): istemci listede olmayan bir araci da + // cagirabilir, suzme tek basina bir yetki siniri degildir. + var schemaAppService = requestContext.Services!.GetRequiredService(); + try + { + var available = (await schemaAppService.GetAvailableToolsAsync()).ToHashSet(System.StringComparer.Ordinal); + result.Tools = [.. result.Tools.Where(tool => available.Contains(tool.Name))]; + } + catch (System.Exception ex) when (McpToolErrorTranslator.CanTranslate(ex)) + { + throw await requestContext.Services!.GetRequiredService().TranslateAsync(ex, null); + } - result.Tools = [.. result.Tools.Where(tool => available.Contains(tool.Name))]; + 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().TranslateAsync(ex, requestContext.Params?.Name); + } + }); + }); } } diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpServerInfoDefaults.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpServerInfoDefaults.cs index 23786a43..9da57ab8 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpServerInfoDefaults.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpServerInfoDefaults.cs @@ -22,5 +22,9 @@ internal static class McpServerInfoDefaults "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, " + "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."; } diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpToolErrorTranslator.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpToolErrorTranslator.cs new file mode 100644 index 00000000..8e94884e --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpToolErrorTranslator.cs @@ -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; + +/// +/// Arac hatalarini istemcinin gorebilecegi bicime cevirir. SDK yalnizca +/// mesajini iletir; diger her istisna "An error occurred invoking" olarak gider ve model neden +/// reddedildigini bilemez. Yetki reddinde eksik yetkiler adiyla, is kurali hatasinda +/// () zaten yerellestirilmis mesaj iletilir. Diger istisnalar +/// cevrilmez: ic ayrinti (SQL, yigin) istemciye sizmaz. +/// +public class McpToolErrorTranslator( + IPermissionChecker permissionChecker, + IStringLocalizer localizer) : ITransientDependency +{ + public static bool CanTranslate(Exception exception) + => exception is AbpAuthorizationException or UserFriendlyException; + + public async Task TranslateAsync(Exception exception, string? toolName) + => exception is AbpAuthorizationException authorization + ? new McpException(await DescribeMissingPermissionsAsync(authorization, toolName), exception) + : new McpException(exception.Message, exception); + + /// + /// CheckAsync("...") reddi yetki adini tasir; [Authorize] attribute'u tasimaz. Ikinci + /// durumda aracin adi bilinen yetkileri kullanicinin yetkileriyle karsilastirilir. + /// + private async Task DescribeMissingPermissionsAsync(AbpAuthorizationException exception, string? toolName) + { + var candidates = new List(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)]; + } + + /// Yetkinin gorunen adi ve kodu; rol ekraninda aranabilsin diye kod her zaman yazilir. + private string Describe(string permissionName) + { + var displayName = localizer[permissionName]; + + return displayName.ResourceNotFound || displayName.Value == permissionName + ? permissionName + : $"{displayName.Value} ({permissionName})"; + } +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopPrompts.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopPrompts.cs new file mode 100644 index 00000000..b02bc1a2 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopPrompts.cs @@ -0,0 +1,33 @@ +using System.ComponentModel; +using ModelContextProtocol.Server; + +namespace Sozsoft.Mcp.Tools; + +/// +/// 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. +/// +[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."; +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs index 0c78555b..a1f0e33f 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs @@ -1,6 +1,9 @@ +using System; using System.Collections.Generic; using System.ComponentModel; +using System.Text.Json; using System.Threading.Tasks; +using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; using Sozsoft.Mcp.Dtos; using Sozsoft.Platform.DeveloperKit; @@ -16,6 +19,8 @@ namespace Sozsoft.Mcp.Tools; [McpServerToolType] public sealed class McpDevelopTools(IMcpDevelopAppService developAppService) { + private static readonly JsonSerializerOptions TextOptions = new(JsonSerializerDefaults.Web); + [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.")] public async Task GetDevGuideAsync( @@ -59,7 +64,7 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService) => await developAppService.GetWizardAsync(fileName); [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 DeployWizardAsync( [Description("Wizard JSON'u: seed dosyasi bicimi ({\"Wizard\": {...}}, get_wizard cikisi gibi) ya da duz wizard cevaplari. Enum alanlari sayi olarak yazilir.")] string wizard, @@ -74,6 +79,55 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService) string 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 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> 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> 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 (

MCP

+ verify_wizard ozeti) bu aracla ekle.")] + public async Task 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"; + } + + /// + /// Talep metni JSON olarak, gorseller ayri resim icerigi olarak doner; model resmi ancak bu + /// bicimde gorur, base64 metni olarak goremez. + /// + private static List ToContent(McpDevRequestDto devRequest) + { + List 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)] [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> ListComponentsAsync( diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpQueryTools.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpQueryTools.cs index adab58f1..ef086da0 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpQueryTools.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpQueryTools.cs @@ -17,7 +17,7 @@ public sealed class McpQueryTools(IMcpQueryAppService queryAppService) string listFormCode, [Description("Filtre, JSON metni. Ornek: [\"Status\",\"=\",\"Open\"] ya da [[\"Amount\",\">\",100],\"and\",[\"Name\",\"contains\",\"abc\"]]. Operatorler: =, <>, >, >=, <, <=, contains, notcontains, startswith, endswith.")] 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, [Description("Gruplama, JSON metni. Ornek: [{\"selector\":\"CustomerId\",\"desc\":false,\"isExpanded\":false}]. Gruplu sorguda satirlar yerine grup anahtarlari ve ozetleri doner.")] string? group = null, diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs index fcc0c95a..ce5efc4f 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs @@ -77,6 +77,13 @@ public class ListFormWizardDto public bool AllowDetail { get; set; } public bool ConfirmDelete { get; set; } + /// + /// Ekranin verisi kapsam klasorundeki data/{ListFormCode}.json dosyasina aynalanir + /// (SeedFilePath + ekle/guncelle/sil senkronizasyonu). Istekle gelen ornek/demo satirlar + /// ekrandan girildiginde dosyaya yazilir ve migrate ile geri yuklenir. + /// + 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ı); // MCP/prompt ile gelen cevapta bayrak yazılmazsa görünüm sessizce kapanmasın diye. public string DefaultLayout { get; set; } = "grid"; @@ -204,6 +211,10 @@ public class ListFormWizardDto KeyFieldDbSourceType = KeyFieldDbSourceType, DefaultFilter = isDeletedField ? WizardConsts.DefaultFilterJson : 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 @@ -342,6 +353,7 @@ public class ListFormWizardDto IsTenant = listForm.IsTenant; IsBranch = listForm.IsBranch; IsOrganizationUnit = listForm.IsOrganizationUnit; + SeedData = !string.IsNullOrWhiteSpace(listForm.SeedFilePath); var layout = listForm.LayoutDto; DefaultLayout = layout.DefaultLayout; diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs index 541909b7..bacae3dd 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs @@ -70,7 +70,7 @@ public class WizardSeedFileDto [ nameof(ListFormWizardDto.IsTenant), nameof(ListFormWizardDto.IsBranch), nameof(ListFormWizardDto.IsOrganizationUnit), 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.Pivot), nameof(ListFormWizardDto.Tree), nameof(ListFormWizardDto.Chart), nameof(ListFormWizardDto.Gantt), nameof(ListFormWizardDto.Scheduler), nameof(ListFormWizardDto.Todo), diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json index 6298bca1..8c0b9ad8 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json @@ -28557,14 +28557,8 @@ { "resourceName": "Platform", "key": "App.DeveloperKit.McpServers.Write", - "en": "Write Data", - "tr": "Veri Yaz" - }, - { - "resourceName": "Platform", - "key": "App.DeveloperKit.McpServers.Develop", - "en": "Develop", - "tr": "Geliştir" + "en": "Write (data; development on host only)", + "tr": "Yaz (veri; geliştirme yalnızca host)" }, { "resourceName": "Platform", @@ -28604,9 +28598,21 @@ }, { "resourceName": "Platform", - "key": "App.Mcp.Error.TenantReadOnly", - "en": "The '{0}' tool is not available to tenant users; over MCP a tenant can only read data.", - "tr": "'{0}' aracı kiracı kullanıcılarına açık değil; MCP üzerinden kiracı yalnızca veri okuyabilir." + "key": "App.Mcp.Error.TenantToolDisabled", + "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ı 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", @@ -28668,6 +28674,12 @@ "en": "ListFormCode cannot be empty.", "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", "key": "App.Mcp.Error.ValuesRequired", @@ -28716,6 +28728,96 @@ "en": "Component \"{0}\" was not found.", "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", "key": "App.Mcp.Error.VisualDesignerComponent", diff --git a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs index 4153caa1..7b8baa1f 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs @@ -552,10 +552,13 @@ public static class PlatformConsts /// /// MCP sunucu tanimlarinin yetkileri. ekrani gorme - /// (okuma) yetkisidir; ise /mcp ucundan arac - /// cagirma yetkisidir ve kiraci kullanicilarina da verilebilir. - /// /mcp ucundan ekran ve bilesen gelistirme araclarini acar; gelistirme islemi yine - /// Wizard ve Custom Components yetkilerinden gecer. + /// (okuma) yetkisidir. /mcp ucu iki kapi tanir: uca + /// erisim ve tum okuma araclaridir, kiraci kullanicilarina da verilebilir; + /// MCP uzerinden yapilan her degisikliktir: kiracida + /// 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. /// public static class McpServers { @@ -566,7 +569,6 @@ public static class PlatformConsts public const string Delete = McpServer + ".Delete"; public const string Invoke = McpServer + ".Invoke"; public const string Write = McpServer + ".Write"; - public const string Develop = McpServer + ".Develop"; } } diff --git a/api/src/Sozsoft.Platform.Domain.Shared/WizardConsts.cs b/api/src/Sozsoft.Platform.Domain.Shared/WizardConsts.cs index c424b48c..5bcc25a0 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/WizardConsts.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/WizardConsts.cs @@ -29,6 +29,12 @@ public static class WizardConsts public static string LangKeyImport => "App.Platform.Import"; public static string LangKeyNote => "App.Platform.Note"; + /// Liste verisinin aynalandigi seed klasoru; SeedPathResolver ile ayni deger. + public const string SeedDataFolder = "data"; + + /// Ekranin varsayilan veri seed dosyasi, kapsam klasorune gore goreli. + public static string SeedDataFilePath(string listFormCode) => $"{SeedDataFolder}/{listFormCode}.json"; + public static string MenuUrl(string code) => $"/admin/list/{code}"; public static string MenuIcon => "FcList"; diff --git a/api/src/Sozsoft.Platform.Domain/Data/SeedPathResolver.cs b/api/src/Sozsoft.Platform.Domain/Data/SeedPathResolver.cs index d25d6adc..e4d73fbe 100644 --- a/api/src/Sozsoft.Platform.Domain/Data/SeedPathResolver.cs +++ b/api/src/Sozsoft.Platform.Domain/Data/SeedPathResolver.cs @@ -72,12 +72,15 @@ public static class SeedPathResolver public const string CrudFolder = "crud"; /// Liste verisinin aynalandigi seed dosyalari (data/{ListFormCode}.json). - public const string DataFolder = "data"; + public const string DataFolder = WizardConsts.SeedDataFolder; - /// Bir ListForm icin varsayilan veri seed dosyasinin kapsam klasorune gore goreli yolu. + /// + /// 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. + /// public static string GetDataFilePath(string listFormCode) { - return $"{DataFolder}/{listFormCode}.json"; + return WizardConsts.SeedDataFilePath(listFormCode); } /// diff --git a/api/src/Sozsoft.Platform.Domain/Queries/SelectQueryManager.cs b/api/src/Sozsoft.Platform.Domain/Queries/SelectQueryManager.cs index fb0cd40b..07992ed6 100644 --- a/api/src/Sozsoft.Platform.Domain/Queries/SelectQueryManager.cs +++ b/api/src/Sozsoft.Platform.Domain/Queries/SelectQueryManager.cs @@ -635,10 +635,16 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager var sorts = sort.Split(PlatformConsts.MultiValueDelimiter, StringSplitOptions.RemoveEmptyEntries); 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); - 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) - sortFields.Add(sortElem.JoinAsString(" ")); // Gelen istegi su sekle donusturur: [AlanIsmi] desc + if (sortElem.Length is 0 or > 2 || sortElem[0].Contains('"')) + { + 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 diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs index 41ab2ccd..52e5341b 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs @@ -568,9 +568,12 @@ public class PlatformHttpApiHostModule : AbpModule endpoints.MapHub("/notificationhub"); // MCP (Model Context Protocol) ucu. Kimlik dogrulama, kiraci cozumu ve unit of work - // yukaridaki ardisik duzenden gelir; araclar kendi [Authorize] yetkilerini ister. - // Sema acikca bearer'a baglanir: varsayilan cerez semasi token'siz istegi giris - // sayfasina 302 ile yonlendirir, MCP istemcisi ise 401 bekler. + // yukaridaki ardisik duzenden gelir. Uc yalnizca kimlik ister; Invoke her MCP + // servisinin sinif duzeyindeki [Authorize] attribute'unda aranir. Uca konsaydi yetkisiz + // 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 { AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme diff --git a/claude.md b/claude.md index 00d27389..e22df2e6 100644 --- a/claude.md +++ b/claude.md @@ -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 `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ı - bir bayrak yoktur, sınırı kullanıcı yetkisi çizer (`RequiredPermissions`). **Kiracı kullanıcısı - yalnızca okur**: kiracı bağlamında açık araçlar `McpToolNames.TenantTools` ile sınırlıdır ve - `AllowedTools` bu kümeyi yalnızca daraltır. Yeni bir araç veri yazıyor ya da okuma olduğu - garanti değilse (procedure, Custom Endpoint) `TenantTools`'a eklenmez. Veri yazan araçlar - `.Write`, ekran/bileşen geliştirme araçları `DevelopTools`'a eklenip `.Develop` yetkisine bağlanır; - işlem yine Wizard / Custom Components AppService'inden geçer. + bir bayrak yoktur, sınırı kullanıcı yetkisi çizer. **İki MCP yetkisi vardır:** `.Invoke` uca + erişim ve okumadır (MCP servislerinin sınıf düzeyinde aranır); `.Write` MCP üzerinden yapılan her değişikliktir ve + değişiklik yapan her araç `RequiredPermissions`'ta `.Write`'a bağlanır — okuma/yazma ayrımı için + yeni yetki açılmaz. **Kiracı veri okur ve yazar, geliştirme yapamaz**: kiracı bağlamında açık + araçlar `McpToolNames.TenantTools` ile sınırlıdır (şema, `query_list`, `run_select`, + `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` - ç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` 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; @@ -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 ekranın sorgusundan (`IListFormSelectAppService`/`IListFormDataAppService`) geçer; liste 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 (`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`, diff --git a/configs/seeds/host/data/Abp.Identity.Permissions.json b/configs/seeds/host/data/Abp.Identity.Permissions.json index d5970b0a..cbf46213 100644 --- a/configs/seeds/host/data/Abp.Identity.Permissions.json +++ b/configs/seeds/host/data/Abp.Identity.Permissions.json @@ -2764,16 +2764,6 @@ "MultiTenancySide": 3, "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", "Name": "App.Listforms.Wizard", diff --git a/configs/seeds/host/data/App.DeveloperKit.McpServers.json b/configs/seeds/host/data/App.DeveloperKit.McpServers.json index 9b8c657e..d80a620d 100644 --- a/configs/seeds/host/data/App.DeveloperKit.McpServers.json +++ b/configs/seeds/host/data/App.DeveloperKit.McpServers.json @@ -12,7 +12,7 @@ "Tenants": null, "AllowedTools": null, "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)." } ] } diff --git a/ui/src/proxy/admin/wizard/models.ts b/ui/src/proxy/admin/wizard/models.ts index 1c96f4a2..a0deb9ca 100644 --- a/ui/src/proxy/admin/wizard/models.ts +++ b/ui/src/proxy/admin/wizard/models.ts @@ -71,6 +71,11 @@ export interface ListFormWizardDto { allowDeleting: boolean confirmDelete: 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 grid: boolean diff --git a/ui/src/views/admin/listForm/wizard/Wizard.tsx b/ui/src/views/admin/listForm/wizard/Wizard.tsx index 0a0138ef..2313a4ef 100644 --- a/ui/src/views/admin/listForm/wizard/Wizard.tsx +++ b/ui/src/views/admin/listForm/wizard/Wizard.tsx @@ -72,6 +72,7 @@ const initialValues: ListFormWizardDto = { allowDeleting: true, confirmDelete: true, allowDetail: false, + seedData: false, defaultLayout: 'grid', grid: true, card: true, @@ -709,6 +710,7 @@ const Wizard = () => { allowDeleting: w.allowDeleting ?? true, confirmDelete: w.confirmDelete ?? true, allowDetail: w.allowDetail ?? false, + seedData: w.seedData ?? false, defaultLayout: w.defaultLayout ?? 'grid', grid: w.grid ?? initialValues.grid, card: w.card ?? initialValues.card,