From d21e87c26f41641efb3f574fc9bd28518adf3831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Fri, 18 Sep 2026 16:47:23 +0300 Subject: [PATCH] MCP Servers Develop eklendi --- .github/instructions/ai.instructions.md | 4 +- .../lowcode-reference.instructions.md | 5 +- .github/instructions/lowcode.instructions.md | 2 +- README.md | 45 ++- .../Dtos/McpDevelopDtos.cs | 133 +++++++ .../Dtos/McpSchemaDtos.cs | 7 +- .../IMcpDataAppService.cs | 2 +- .../IMcpDevelopAppService.cs | 37 ++ .../Sozsoft.Mcp.Application.Contracts.csproj | 3 + .../Sozsoft.Mcp.Application/McpAppService.cs | 10 +- .../McpDataAppService.cs | 2 - .../McpDevelopAppService.cs | 341 ++++++++++++++++++ .../McpQueryAppService.cs | 12 +- .../McpSchemaAppService.cs | 2 +- .../Sozsoft.Mcp.Application.csproj | 2 - .../McpErrorCodes.cs | 15 + .../Sozsoft.Mcp.Domain.Shared/McpToolNames.cs | 62 +++- .../McpToolPolicy.cs | 14 +- .../Sozsoft.Mcp.Domain/Entities/McpServer.cs | 17 +- .../Services/McpDevGuide.cs | 177 +++++++++ .../Services/McpQueryExecutor.cs | 8 +- .../Services/McpTableCreator.cs | 262 ++++++++++++++ .../McpDbContextModelCreatingExtensions.cs | 2 - .../Sozsoft.Mcp.HttpApi/McpHttpApiModule.cs | 1 + .../McpServerInfoDefaults.cs | 6 +- .../Sozsoft.Mcp.HttpApi/Tools/McpDataTools.cs | 4 +- .../Tools/McpDevelopTools.cs | 125 +++++++ .../Migrations/LanguagesData.json | 114 +++++- .../Migrations/ListFormSeeder_Saas.cs | 54 +-- .../PlatformConsts.cs | 5 +- ....cs => 20260918122519_Initial.Designer.cs} | 11 +- ...9_Initial.cs => 20260918122519_Initial.cs} | 2 - .../PlatformDbContextModelSnapshot.cs | 9 - .../appsettings.Dev.json | 1 + .../appsettings.Production.json | 1 + claude.md | 14 +- configs/deployment/docker-compose-app.yml | 3 + .../host/data/Abp.Identity.Permissions.json | 9 + .../data/App.DeveloperKit.McpServers.json | 4 +- 39 files changed, 1373 insertions(+), 154 deletions(-) create mode 100644 api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpDevelopDtos.cs create mode 100644 api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDevelopAppService.cs create mode 100644 api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs create mode 100644 api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpDevGuide.cs create mode 100644 api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpTableCreator.cs create mode 100644 api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs rename api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/{20260910132559_Initial.Designer.cs => 20260918122519_Initial.Designer.cs} (99%) rename api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/{20260910132559_Initial.cs => 20260918122519_Initial.cs} (99%) diff --git a/.github/instructions/ai.instructions.md b/.github/instructions/ai.instructions.md index 3313614f..ef876035 100644 --- a/.github/instructions/ai.instructions.md +++ b/.github/instructions/ai.instructions.md @@ -363,7 +363,9 @@ AI workflow integration: MCP (`/mcp`, `Sozsoft.Mcp` module) — expose ListForm data/queries to an external AI agent (Claude, ChatGPT, or an OAuth-registered MCP client) without new code: configure a server row on the **MCP Servers** screen (`App.DeveloperKit.McpServers`, §12.2 of -`lowcode-reference.instructions.md`) with its allowed tools, datasource and tenant scope. A +`lowcode-reference.instructions.md`) with its allowed tools and tenant scope; queries always run +on the calling tenant's own database. Tenant users are read-only over MCP (schema tools, +`query_list`, `run_select`); writing and development tools are host-only. A request to "let an agent query/update X" is a Decision Flow §1 (ListForm) request first — MCP only gates which of the existing screens/queries an already-authorized client may reach; it never grants access beyond the calling user's own ListForm/query permissions. Ayrıntı: diff --git a/.github/instructions/lowcode-reference.instructions.md b/.github/instructions/lowcode-reference.instructions.md index a78ea592..cf4ba750 100644 --- a/.github/instructions/lowcode-reference.instructions.md +++ b/.github/instructions/lowcode-reference.instructions.md @@ -1189,7 +1189,7 @@ soneklerini alır. | **Developer Kit › Components** | `App.DeveloperKit.CustomComponents` · `/admin/list/App.DeveloperKit.CustomComponents` | `Sas_H_CustomComponent` | host | Custom component listesi (ListForm) ve satırdaki **Design** butonuyla **Visual Designer** girişi (§10). Kayıtlar `data/App.DeveloperKit.CustomComponents.json` dosyasına senkronlanı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 › Wizard Manager** | menü kodu `App.Listforms.WizardManager` · yetki `App.Listforms.Wizard` · `/admin/listform/wizardManager` | — | host | Wizard seed dosyalarının listesi, düzenlemesi, silinmesi, export/import'u. Sihirbazın kendisi ayrı bir rotadadır: `/admin/listform/wizard`. | -| **Developer Kit › MCP Servers** | `App.DeveloperKit.McpServers` · `/admin/list/App.DeveloperKit.McpServers` | `Sas_H_McpServer` | host | `/mcp` ucunun kiracı bazlı sözleşmesi: `Name`, `Url`, `Tenants`, `DataSourceCode`, `AllowedTools`, `MaxRows`, `AllowWrite`. Araç listesi bu kayda göre süzülür, çağrı anında aynı kural yeniden uygulanır. Yetkiler: ekran için `App.DeveloperKit.McpServers`, araç çağırma için `.Invoke`, veri yazma için `.Write` (son ikisi kiracı kullanıcılarına da verilebilir). MCP yetki vermez: kullanıcı yalnızca uygulamada okuyabildiği listeleri görür ve sorgular; ham SQL `App.SqlQueryManager` ister. Kayıtlar `data/App.DeveloperKit.McpServers.json` dosyasına senkronlanır. Araçlar ve şema keşfinin kaynağı: `README.md` §13.6. | +| **Developer Kit › MCP Servers** | `App.DeveloperKit.McpServers` · `/admin/list/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 açık), `MaxRows`. Araç listesi bu kayda göre süzülür, çağrı anında aynı kural yeniden uygulanır. Yetkiler: ekran için `App.DeveloperKit.McpServers`, araç çağırma için `.Invoke`, veri yazma için `.Write`, prompt ile tablo/ekran/bileşen geliştirme (`create_table`, wizard deploy, custom component kaydı) için `.Develop` (host; Wizard ve Custom Components yetkilerini ayrıca ister). Kiracı kullanıcısı yetkisi ne olursa olsun yalnızca okur (`McpToolNames.TenantTools`: şema araçları, `query_list`, `run_select`); yazma, `create_table`, wizard, component ve `run_query` kiracıya kapalıdır. Host için tek sunucu anahtarı `AllowedTools`. MCP yetki vermez: kullanıcı yalnızca uygulamada okuyabildiği listeleri görür ve sorgular; ham SQL `App.SqlQueryManager` ister. Kayıtlar `data/App.DeveloperKit.McpServers.json` dosyasına senkronlanır. Araçlar ve şema keşfinin kaynağı: `README.md` §13.6. | | **Forum Management** | `App.ForumManagement` (grup menüsü) | `Sas_T_ForumCategory` · `Sas_T_ForumTopic` · `Sas_T_ForumPost` | tenant | Altında Categories / Topics / Posts ListForm ekranları; yayın tarafı Administration'daki `/admin/forum`. | | **Forum Categories** | `App.ForumManagement.Categories` · `/admin/list/App.ForumManagement.Categories` | `Sas_T_ForumCategory` | tenant | Forum kategorilerinin ListForm ekranı; veri `configs/seeds/host/data/App.ForumManagement.Categories.json` dosyasından seed edilir. | | **Forum Topics** | `App.ForumManagement.Topics` · `/admin/list/App.ForumManagement.Topics` | `Sas_T_ForumTopic` | tenant | Forum konuları; kategori lookup'ı, sayaçlar ve sabitle/kilitle/çözüldü bayrakları. | @@ -1326,7 +1326,8 @@ yazma, oradan yeni bir Custom Component üret. `Services` sayfası yoktur. | "mesai dışında girilemesin" / "sadece ofis IP'si" | Administration › Restrictions | Work Hour · IP Restriction kaydı | | "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`/`DataSourceCode`/tenant kapsamını ayarla; yeni endpoint/kod yok | +| "yapay zekâ ajanı sorgulasın / veri çeksin" | Saas › DeveloperKit › MCP Servers | Kayıt aç, `AllowedTools`/tenant kapsamını ayarla; yeni endpoint/kod yok | +| "yapay zekâ ajanı ekran/bileşen geliştirsin" | Saas › DeveloperKit › MCP Servers | Yalnızca host kullanıcısı: `App.DeveloperKit.McpServers.Develop` + Wizard / Custom Components yetkilerini ver; geliştirme araçları `AllowedTools` ile açılıp kapatılır. Kiracı MCP'de yalnızca veri okur | | "ü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 d7984297..4c1a0f41 100644 --- a/.github/instructions/lowcode.instructions.md +++ b/.github/instructions/lowcode.instructions.md @@ -64,7 +64,7 @@ Tenant kapsamı isteniyorsa `host/` yerine `tenants/{tenantId}/`; iç düzen ayn | # | Adım | Dosya | Neden bu sırada | | --- | --- | --- | --- | -| 1 | Tablo / view | `sql/object/…` | Ekranın bağlanacağı nesne önce var olmalı | +| 1 | Tablo / view | `sql/object/…` | Ekranın bağlanacağı nesne önce var olmalı. MCP'de yeni tablo `create_table` ile kurulur (script ve dosya tasarımcıyla aynı); view MCP'den kurulamaz | | 2 | CRUD uçları (gerekiyorsa) | `crud/{Entity}.json` | Custom Component'in data source'ları buna bakar | | 3 | Custom Component (gerekiyorsa) | `data/App.DeveloperKit.CustomComponents.json` | Wizard `Custom` yolunda bileşen adını arar | | 4 | Wizard | `wizard/{Ad}.json` | Menü, yetki, dil anahtarı ve ListForm'u üretir | diff --git a/README.md b/README.md index d271f329..b6a166df 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,7 @@ sozsoft-platform/ │ │ ├── Sozsoft.MailQueue/ # Şablonlu mail kuyruğu ve ekleri │ │ ├── Sozsoft.Sender/ # Mail (SES), SMS, WhatsApp, Rocket.Chat göndericileri │ │ ├── Sozsoft.SqlQueryManager/ # SQL nesne yönetimi ve sorgu çalıştırma -│ │ └── Sozsoft.Mcp/ # MCP sunucusu: /mcp ucu, şema keşfi ve sorgu araçları +│ │ └── Sozsoft.Mcp/ # MCP sunucusu: /mcp ucu, şema keşfi, sorgu ve geliştirme araçları │ └── test/ # Test projeleri ├── ui/ # React uygulaması │ └── src/ @@ -931,7 +931,7 @@ içindedir; oradaki kontroller yalnızca butonları gizler, asıl kontrol AppSer | **Sozsoft.MailQueue** | Şablon (Scriban) tabanlı gövde üretimi, ek dosya yaşam döngüsü, kuyruk çalıştırma ve günlükleme. | | **Sozsoft.Sender** | Amazon SES (mail), Posta Güvercini (SMS), WhatsApp Cloud API, Rocket.Chat göndericileri. | | **Sozsoft.SqlQueryManager** | SQL nesne yönetimi, sorgu çalıştırma, tablo/kolon metadata'sı. | -| **Sozsoft.Mcp** | MCP (Model Context Protocol) sunucusu: `/mcp` Streamable HTTP ucu, kiracı bazlı araç seti, şema keşfi ve salt-okunur sorgu. Bkz. §13.6. | +| **Sozsoft.Mcp** | MCP (Model Context Protocol) sunucusu: `/mcp` Streamable HTTP ucu, kiracı bazlı araç seti, şema keşfi, sorgu, veri yazma ve (host) tablo/ekran/bileşen geliştirme. Kiracı kullanıcısı yalnızca veri okur. Bkz. §13.6. | ### 10.2. Uygulama servisleri (`api/src/Sozsoft.Platform.Application`) @@ -1175,12 +1175,22 @@ sayfasına 302 ile değil, 401 ile döner); kiracı token talebinden ya da `__te arkasında oturum yapışkanlığı gerekmez. **Kiracı bazlı sözleşme.** `Sas_H_McpServer` tablosundaki kayıt (ekran: -`App.DeveloperKit.McpServers`) hangi araçların yayınlandığını, sorguların hangi veri -kaynağında ve hangi satır tavanıyla koşacağını belirler. `Tenants` boşsa kayıt tüm +`App.DeveloperKit.McpServers`) hangi araçların yayınlandığını ve sorguların hangi satır tavanıyla koşacağını +belirler; sorgular her zaman isteğin kiracısının veritabanında koşar. `Tenants` boşsa kayıt tüm kiracılara açıktır (AiBot ile aynı sözleşme). Araç listesi bu kayda göre süzülü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 +`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ı +`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. + **Araçlar.** | Araç | İş | @@ -1193,7 +1203,13 @@ bir yetki sınırı değildir. | `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; `AllowWrite` kapalıysa listelenmez. | +| `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. | +| `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). | +| `delete_wizard` | Wizard Manager'daki silmeyle aynı: yalnızca `InsertedRecords`, bağımlı wizard varsa reddedilir. | +| `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` @@ -1202,9 +1218,21 @@ bir yetki sınırı değildir. 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. +**Geliştirme rehberi nereden okur.** `get_dev_guide` depodaki +`.github/instructions/lowcode*.instructions.md` dosyalarını runtime'da okur; Claude Code ile MCP +istemcisi aynı sözleşmeyi görür. Klasör `App:DevGuidePath` ayarından (konteynerde +`/etc/api/dev-guide`, compose'da `.github/instructions` salt okunur mount), boşsa çalışma +dizininden yukarı `.github/instructions` aranarak bulunur. İmaja gömülmez: API build bağlamı +`api/` olduğu için `.github` imaja girmez, rehber değiştiğinde imaj derlenmez. + **Şema keşfi nereden okur.** Tablo ve sütun bilgisi canlı katalogdan değil, `Sas_H_DbTableColumn` tablosundaki JSON veri sözlüğünden gelir — sözlük dil anahtarlarından çözülmüş başlıkları ve lookup ilişkilerini birlikte taşıdığı için modelin gördüğü adlar @@ -1286,8 +1314,11 @@ 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) ve `.Write` -(veri yazma). Son ikisi `MultiTenancySide = Both`, yani kiracı kullanıcılarına da verilebilir. +**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. --- 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 new file mode 100644 index 00000000..a82e4412 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpDevelopDtos.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; + +namespace Sozsoft.Mcp.Dtos; + +/// Gelistirme rehberinin bir basligi. +public class McpDevGuideHeadingDto +{ + /// lowcode (uretim protokolu, artefakt semalari) ya da reference (JSON kolon sozlesmeleri). + public string Document { get; set; } = string.Empty; + + /// Bolum numarasi (4.3); get_dev_guide'a bu deger verilir. + public string Id { get; set; } = string.Empty; + + public string Title { get; set; } = string.Empty; + + public int Level { get; set; } +} + +/// +/// Rehber cevabi. Bolum istenmediyse yalnizca doludur; istendiyse +/// bolumun metnini, alt basliklarini tasir. +/// +public class McpDevGuideDto +{ + public McpDevGuideHeadingDto? Heading { get; set; } + + public string? Content { get; set; } + + public List Sections { get; set; } = []; +} + +/// Bir wizard seed dosyasi; dosyanin diskteki bicimidir. +public class McpWizardFileDto +{ + public string FileName { get; set; } = string.Empty; + + public JsonElement Seed { get; set; } +} + +/// Wizard'i ilk kez ya da yeniden deploy etme istegi. +public class McpWizardDeployRequestDto +{ + /// + /// Seed dosyasi bicimindeki JSON ({"Wizard": {...}}) ya da duz wizard cevaplari. + /// + public string Wizard { get; set; } = string.Empty; + + /// Doluysa guncellemedir: bu dosya ve urettigi kayitlar silinip yeniden uretilir. + public string? ReplaceFileName { get; set; } +} + +/// +/// Yeni tablo istegi. Id, TenantId ve audit kolonlari sablondan gelir; burada +/// yalnizca is kolonlari yazilir. +/// +public class McpCreateTableRequestDto +{ + public string TableName { get; set; } = string.Empty; + + public List Columns { get; set; } = []; + + public bool FullAudited { get; set; } = true; + + public bool MultiTenant { get; set; } = true; +} + +public class McpTableColumnDto +{ + public string Name { get; set; } = string.Empty; + + /// Tablo tasarimcisinin tip listesinden biri (nvarchar, int, decimal, bit ...). + public string Type { get; set; } = string.Empty; + + /// Yalnizca nvarchar icin. + public int? MaxLength { get; set; } + + public bool IsNullable { get; set; } = true; + + /// Sayi, metin sabiti ('...') ya da NEWID()/GETDATE()/GETUTCDATE()/SYSUTCDATETIME(). + public string? DefaultValue { get; set; } +} + +public class McpCreateTableResultDto +{ + public string TableName { get; set; } = string.Empty; + + public string Script { get; set; } = string.Empty; + + /// Script'in yazildigi seed dosyasi, seed kokune gore goreli. + public string SeedFilePath { get; set; } = string.Empty; +} + +/// Custom component ozeti; kod tasimaz. +public class McpComponentSummaryDto +{ + public Guid Id { get; set; } + + public string Name { get; set; } = string.Empty; + + public string? RoutePath { get; set; } + + public string? Description { get; set; } + + public bool IsActive { get; set; } + + /// Kodu tasarimci belgesinden uretilen bilesen; MCP uzerinden kodu yazilamaz. + public bool IsVisualDesigner { get; set; } +} + +/// +/// Custom component kaydetme istegi; ada gore varsa gunceller, yoksa ekler. Guncellemede null +/// birakilan alanlar mevcut degerini korur. +/// +public class McpComponentSaveRequestDto +{ + public string Name { get; set; } = string.Empty; + + public string? Code { get; set; } + + /// Bos dize rotayi kaldirir (rotasiz bilesen); null mevcut rotayi korur. + public string? RoutePath { get; set; } + + public string? Description { get; set; } + + public string? Props { get; set; } + + /// Bagimli bilesen adlarinin JSON dizisi. + public string? Dependencies { get; set; } + + public bool? IsActive { get; set; } +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpSchemaDtos.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpSchemaDtos.cs index fc50faeb..5dd7884f 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpSchemaDtos.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpSchemaDtos.cs @@ -14,16 +14,11 @@ public class McpServerConfigDto /// Ekranda gosterilen baglanti adresi; sunucu tarafinda yonlendirme icin kullanilmaz. public string? Url { get; set; } - /// Bos ise istegin kapsamindaki varsayilan veri kaynagi kullanilir. - public string? DataSourceCode { get; set; } - - /// Bos liste "yazma disindaki tum araclar acik" anlamina gelir. + /// Bos liste "tum araclar acik" anlamina gelir. public List AllowedTools { get; set; } = []; public int MaxRows { get; set; } - public bool AllowWrite { get; set; } - public string? Description { get; set; } /// Istegin cozumlenen kiraci adi; host baglaminda bostur. diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDataAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDataAppService.cs index 40e0b496..541dcbb6 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDataAppService.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDataAppService.cs @@ -7,7 +7,7 @@ namespace Sozsoft.Mcp; /// /// MCP veri yazma yuzeyi. Yazma her zaman bir ListForm ekraninin insert/update/delete /// sozlesmesinden gecer; ekran yetkisi ve tenant filtresi aynen uygulanir. Sunucu kaydinda -/// AllowWrite kapali oldugunda bu araclar hic listelenmez. +/// AllowedTools dolu ve bu araclar secili degilse hic listelenmez. /// public interface IMcpDataAppService : IApplicationService { diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDevelopAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDevelopAppService.cs new file mode 100644 index 00000000..41e5133d --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDevelopAppService.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Sozsoft.Mcp.Dtos; +using Sozsoft.Platform.DeveloperKit; +using Sozsoft.Platform.ListForms; +using Volo.Abp.Application.Services; + +namespace Sozsoft.Mcp; + +/// +/// MCP gelistirme yuzeyi: tablo, ekran (wizard) ve custom component uretimi. Her islem uygulamadaki +/// Wizard ve Custom Components servislerinden gecer; MCP yalnizca kapidir, ekranin +/// kendi yetkilerini, seed dosyasi yazimini ve bagimlilik kurallarini atlamaz. +/// +public interface IMcpDevelopAppService : IApplicationService +{ + Task GetDevGuideAsync(string? document, string? section); + + Task CreateTableAsync(McpCreateTableRequestDto input); + + Task> GetWizardsAsync(string? search); + + Task GetWizardAsync(string fileName); + + /// Deploy edilen wizard'in ozeti. + Task DeployWizardAsync(McpWizardDeployRequestDto input); + + Task DeleteWizardAsync(string fileName); + + Task> GetComponentsAsync(string? search); + + Task GetComponentAsync(string name); + + Task SaveComponentAsync(McpComponentSaveRequestDto input); + + Task DeleteComponentAsync(string name); +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Sozsoft.Mcp.Application.Contracts.csproj b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Sozsoft.Mcp.Application.Contracts.csproj index 5b219c2c..d49ddda1 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Sozsoft.Mcp.Application.Contracts.csproj +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Sozsoft.Mcp.Application.Contracts.csproj @@ -15,6 +15,9 @@ + + diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpAppService.cs index 3f259ab7..1a92ffdf 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpAppService.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpAppService.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Sozsoft.Mcp.Dtos; @@ -37,10 +38,8 @@ public abstract class McpAppService : ApplicationService { Name = server.Name, Url = server.Url, - DataSourceCode = server.DataSourceCode, AllowedTools = [.. ServerConfigProvider.SplitMultiValue(server.AllowedTools)], MaxRows = server.MaxRows, - AllowWrite = server.AllowWrite, Description = server.Description, TenantName = CurrentTenant.Name }; @@ -63,7 +62,12 @@ public abstract class McpAppService : ApplicationService { var config = await RequireServerConfigAsync(); - if (!McpToolPolicy.IsAllowed(true, config.AllowWrite, config.AllowedTools, toolName)) + if (CurrentTenant.IsAvailable && !McpToolNames.TenantTools.Contains(toolName, StringComparer.Ordinal)) + { + throw new UserFriendlyException(L[McpErrorCodes.TenantReadOnly, toolName]); + } + + if (!McpToolPolicy.IsAllowed(true, CurrentTenant.IsAvailable, config.AllowedTools, toolName)) { throw new UserFriendlyException(L[McpErrorCodes.ToolDisabled, toolName, config.Name]); } diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDataAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDataAppService.cs index 2ce3cbd3..5b5c6d6c 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDataAppService.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDataAppService.cs @@ -58,8 +58,6 @@ public class McpDataAppService( { Check.NotNull(input, nameof(input)); - // AllowWrite kapaliysa yazma araclari McpToolPolicy tarafindan zaten reddedilir; - // burada ayri bir kontrol yok. await EnsureToolAllowedAsync(toolName); if (string.IsNullOrWhiteSpace(input.ListFormCode)) diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs new file mode 100644 index 00000000..d49aa779 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs @@ -0,0 +1,341 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Sozsoft.Mcp.Dtos; +using Sozsoft.Mcp.Services; +using Sozsoft.Platform; +using Sozsoft.Platform.DeveloperKit; +using Sozsoft.Platform.ListForms; +using Volo.Abp; +using Volo.Abp.Application.Dtos; + +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 +/// yalnizca MCP kapisidir, arkadaki yetkinin yerine gecmez. +/// +[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop)] +public class McpDevelopAppService( + IMcpServerConfigProvider serverConfigProvider, + IMcpDevGuide devGuide, + IMcpTableCreator tableCreator, + IListFormWizardAppService wizardAppService, + ICustomComponentAppService componentAppService) + : McpAppService(serverConfigProvider), IMcpDevelopAppService +{ + private const string DefaultGuideDocument = "lowcode"; + + /// Index cevabinda listelenen en derin baslik; daha derini bolum istenince gorunur. + private const int IndexHeadingLevel = 3; + + /// Custom Components ekraninin tek sayfada dondurebildigi ust sinir. + private const int ComponentPageSize = 1000; + + public async Task GetDevGuideAsync(string? document, string? section) + { + await EnsureToolAllowedAsync(McpToolNames.GetDevGuide); + + if (string.IsNullOrWhiteSpace(section)) + { + var headings = await devGuide.GetHeadingsAsync(); + if (headings.Count == 0) + { + throw new UserFriendlyException(L[McpErrorCodes.DevGuideUnavailable, McpDevGuide.ConfigurationKey]); + } + + return new McpDevGuideDto + { + Sections = [.. headings + .Where(h => h.Level <= IndexHeadingLevel) + .Where(h => string.IsNullOrWhiteSpace(document) || string.Equals(h.Document, document, StringComparison.OrdinalIgnoreCase)) + .Select(ToDto)] + }; + } + + var result = await devGuide.GetSectionAsync( + string.IsNullOrWhiteSpace(document) ? DefaultGuideDocument : document, + section) + ?? throw new UserFriendlyException(L[McpErrorCodes.DevGuideSectionNotFound, section]); + + return new McpDevGuideDto + { + Heading = ToDto(result.Heading), + Content = result.Content, + Sections = [.. result.Children.Select(ToDto)] + }; + } + + /// + /// Tablo tasarimcisiyla ayni yetkiyi ister: ham DDL liste yetkilerini asar, Develop yalnizca + /// MCP kapisidir. + /// + public async Task CreateTableAsync(McpCreateTableRequestDto input) + { + Check.NotNull(input, nameof(input)); + await EnsureToolAllowedAsync(McpToolNames.CreateTable); + await AuthorizationService.CheckAsync(PlatformConsts.AppCodes.SqlQueryManagers.Default); + + var created = await tableCreator.CreateAsync(new McpTableDefinition( + input.TableName, + [.. (input.Columns ?? []).Select(column => new McpTableColumn( + column.Name, column.Type, column.MaxLength, column.IsNullable, column.DefaultValue))], + input.FullAudited, + input.MultiTenant)); + + return new McpCreateTableResultDto + { + TableName = created.TableName, + Script = created.Script, + SeedFilePath = created.SeedFilePath + }; + } + + public async Task> GetWizardsAsync(string? search) + { + await EnsureToolAllowedAsync(McpToolNames.ListWizards); + + var files = await wizardAppService.GetFiles(); + if (string.IsNullOrWhiteSpace(search)) + { + return files; + } + + return [.. files.Where(file => + Contains(file.FileName, search) || Contains(file.WizardName, search) || + Contains(file.ListFormCode, search) || Contains(file.CustomComponentName, search))]; + } + + public async Task GetWizardAsync(string fileName) + { + await EnsureToolAllowedAsync(McpToolNames.GetWizard); + + var seed = await wizardAppService.GetFile(fileName); + + // Model dosyayi Claude Code'un depoda gordugu bicimde gorur; deploy_wizard ayni bicimi + // geri kabul eder. + using var document = JsonDocument.Parse(seed.ToSeedJson()); + + return new McpWizardFileDto { FileName = fileName, Seed = document.RootElement.Clone() }; + } + + public async Task DeployWizardAsync(McpWizardDeployRequestDto input) + { + Check.NotNull(input, nameof(input)); + await EnsureToolAllowedAsync(McpToolNames.DeployWizard); + + var wizard = ParseWizard(input.Wizard); + wizard.EditFileName = string.IsNullOrWhiteSpace(input.ReplaceFileName) ? null : input.ReplaceFileName.Trim(); + + await wizardAppService.Create(wizard); + + var files = await wizardAppService.GetFiles(); + + return files.FirstOrDefault(file => string.Equals(file.WizardName, wizard.WizardName?.Trim(), StringComparison.OrdinalIgnoreCase)); + } + + public async Task DeleteWizardAsync(string fileName) + { + await EnsureToolAllowedAsync(McpToolNames.DeleteWizard); + + return await wizardAppService.DeleteFile(fileName); + } + + public async Task> GetComponentsAsync(string? search) + { + await EnsureToolAllowedAsync(McpToolNames.ListComponents); + + var components = await GetAllComponentsAsync(); + + return [.. components + .Where(component => string.IsNullOrWhiteSpace(search) + || Contains(component.Name, search) + || Contains(component.RoutePath, search) + || Contains(component.Description, search)) + .Select(component => new McpComponentSummaryDto + { + Id = component.Id, + Name = component.Name, + RoutePath = component.RoutePath, + Description = component.Description, + IsActive = component.IsActive, + IsVisualDesigner = IsVisualDesignerDocument(component.Props) + })]; + } + + public async Task GetComponentAsync(string name) + { + await EnsureToolAllowedAsync(McpToolNames.GetComponent); + + return await FindComponentAsync(name); + } + + public async Task SaveComponentAsync(McpComponentSaveRequestDto input) + { + Check.NotNull(input, nameof(input)); + await EnsureToolAllowedAsync(McpToolNames.SaveComponent); + + if (string.IsNullOrWhiteSpace(input.Name)) + { + throw new UserFriendlyException(L[McpErrorCodes.ComponentNameRequired]); + } + + var existing = await FindComponentAsync(input.Name); + var props = input.Props ?? existing?.Props; + + // Tasarimci belgesinde kod belgeden uretilir; buraya yazilan kod tasarimcidaki ilk + // kayitta sessizce ezilirdi. Belge MCP'de uretilemedigi icin bu bilesenler tasarimcida kalir. + if (IsVisualDesignerDocument(props)) + { + throw new UserFriendlyException(L[McpErrorCodes.VisualDesignerComponent, input.Name]); + } + + if (existing is null) + { + if (string.IsNullOrWhiteSpace(input.Code)) + { + throw new UserFriendlyException(L[McpErrorCodes.ComponentCodeRequired]); + } + + return await componentAppService.CreateAsync(new CreateUpdateCustomComponentDto + { + Name = input.Name.Trim(), + Code = input.Code, + RoutePath = input.RoutePath, + Description = input.Description, + Props = props, + Dependencies = input.Dependencies, + IsActive = input.IsActive ?? true + }); + } + + return await componentAppService.UpdateAsync(existing.Id, new CreateUpdateCustomComponentDto + { + Name = existing.Name, + Code = string.IsNullOrWhiteSpace(input.Code) ? existing.Code : input.Code, + RoutePath = input.RoutePath ?? existing.RoutePath, + Description = input.Description ?? existing.Description, + Props = props, + Dependencies = input.Dependencies ?? existing.Dependencies, + IsActive = input.IsActive ?? existing.IsActive + }); + } + + public async Task DeleteComponentAsync(string name) + { + await EnsureToolAllowedAsync(McpToolNames.DeleteComponent); + + var component = await FindComponentAsync(name) + ?? throw new UserFriendlyException(L[McpErrorCodes.ComponentNotFound, name]); + + await componentAppService.DeleteAsync(component.Id); + } + + /// + /// Wizard'i seed dosyasi biciminden ({"Wizard": {...}}) ya da duz cevaplardan okur. + /// Iki bicim de dosyanin okuma kuralindan gecer; boylece Menu/Language bloklari ve + /// WizardCode/ModuleId adlari dosyadakiyle ayni anlama gelir. + /// + private ListFormWizardDto ParseWizard(string? json) + { + JsonObject? root; + try + { + root = string.IsNullOrWhiteSpace(json) ? null : JsonNode.Parse(json) as JsonObject; + } + catch (JsonException) + { + root = null; + } + + if (root is null) + { + throw new UserFriendlyException(L[McpErrorCodes.InvalidWizard]); + } + + var seedJson = root.ContainsKey(nameof(WizardSeedFileDto.Wizard)) + ? root.ToJsonString() + : new JsonObject { [nameof(WizardSeedFileDto.Wizard)] = root }.ToJsonString(); + + try + { + return WizardSeedFileDto.FromSeedJson(seedJson)?.Wizard + ?? throw new UserFriendlyException(L[McpErrorCodes.InvalidWizard]); + } + catch (JsonException) + { + throw new UserFriendlyException(L[McpErrorCodes.InvalidWizard]); + } + } + + private async Task> GetAllComponentsAsync() + { + var page = await componentAppService.GetListAsync(new PagedAndSortedResultRequestDto + { + MaxResultCount = ComponentPageSize, + Sorting = nameof(CustomComponentDto.Name) + }); + + return page.Items; + } + + private async Task FindComponentAsync(string? name) + { + if (string.IsNullOrWhiteSpace(name)) + { + return null; + } + + var components = await GetAllComponentsAsync(); + + return components.FirstOrDefault(component => string.Equals(component.Name, name.Trim(), StringComparison.OrdinalIgnoreCase)); + } + + /// + /// Props icinde tasarimci belgesi varsa ve kip acikca code degilse kod belgeden + /// uretilir; tasarimcinin varsayilan kipi visual'dir. + /// + private static bool IsVisualDesignerDocument(string? props) + { + if (string.IsNullOrWhiteSpace(props)) + { + return false; + } + + try + { + using var document = JsonDocument.Parse(props); + if (document.RootElement.ValueKind != JsonValueKind.Object || + !document.RootElement.TryGetProperty("visualDesigner", out var designer) || + designer.ValueKind != JsonValueKind.Object) + { + return false; + } + + return !designer.TryGetProperty("sourceMode", out var mode) || + mode.ValueKind != JsonValueKind.String || + !string.Equals(mode.GetString(), "code", StringComparison.Ordinal); + } + catch (JsonException) + { + return false; + } + } + + private static bool Contains(string? value, string search) + => value?.Contains(search.Trim(), StringComparison.OrdinalIgnoreCase) == true; + + private static McpDevGuideHeadingDto ToDto(McpDevGuideHeading heading) => new() + { + Document = heading.Document, + Id = heading.Id, + Title = heading.Title, + Level = heading.Level + }; +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpQueryAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpQueryAppService.cs index d6356d2e..0958e7d2 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpQueryAppService.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpQueryAppService.cs @@ -80,7 +80,7 @@ public class McpQueryAppService( var maxRows = ResolveMaxRows(config, input.MaxRows); var parameters = sqlGuard.NormalizeParameters(input.Parameters); - var context = await queryExecutor.ResolveDataSourceAsync(config.DataSourceCode); + var context = await queryExecutor.ResolveDataSourceAsync(); var limitedSql = sqlGuard.ApplyRowLimit(sql, maxRows, context.DataSourceType); return ToResult(await queryExecutor.ExecuteAsync(context, limitedSql, parameters, maxRows)); @@ -88,12 +88,12 @@ public class McpQueryAppService( public async Task> GetSavedQueriesAsync(string? search = null) { - var config = await EnsureToolAllowedAsync(McpToolNames.ListQueries); + await EnsureToolAllowedAsync(McpToolNames.ListQueries); var results = new List(); if (await AuthorizationService.IsGrantedAsync(SqlPermission)) { - results.AddRange(await GetDatabaseObjectsAsync(config)); + results.AddRange(await GetDatabaseObjectsAsync()); } if (await AuthorizationService.IsGrantedAsync(CustomEndpointPermission)) @@ -113,9 +113,9 @@ public class McpQueryAppService( .ThenBy(item => item.Name, StringComparer.OrdinalIgnoreCase)]; } - private async Task> GetDatabaseObjectsAsync(McpServerConfigDto config) + private async Task> GetDatabaseObjectsAsync() { - var context = await queryExecutor.ResolveDataSourceAsync(config.DataSourceCode); + var context = await queryExecutor.ResolveDataSourceAsync(); var catalogSql = context.DataSourceType == DataSourceTypeEnum.Postgresql ? McpCatalogQueries.Postgres : McpCatalogQueries.SqlServer; @@ -160,7 +160,7 @@ public class McpQueryAppService( var maxRows = ResolveMaxRows(config, input.MaxRows); var parameters = sqlGuard.NormalizeParameters(input.Parameters); - var context = await queryExecutor.ResolveDataSourceAsync(config.DataSourceCode); + var context = await queryExecutor.ResolveDataSourceAsync(); var schemaName = string.IsNullOrWhiteSpace(input.SchemaName) ? sqlGuard.DefaultSchemaName(context.DataSourceType) diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpSchemaAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpSchemaAppService.cs index fc1efea9..a0a88a9f 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpSchemaAppService.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpSchemaAppService.cs @@ -31,7 +31,7 @@ public class McpSchemaAppService( var config = await ResolveServerConfigAsync(); var candidates = McpToolPolicy.VisibleTools( config is not null, - config?.AllowWrite ?? false, + CurrentTenant.IsAvailable, config?.AllowedTools ?? []); var required = candidates diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/Sozsoft.Mcp.Application.csproj b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/Sozsoft.Mcp.Application.csproj index 9e6c0163..13eb9d28 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/Sozsoft.Mcp.Application.csproj +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/Sozsoft.Mcp.Application.csproj @@ -15,8 +15,6 @@ - - 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 43f73334..30de78ff 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs @@ -10,6 +10,7 @@ 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 EmptyQuery = Prefix + "EmptyQuery"; public const string SingleStatementOnly = Prefix + "SingleStatementOnly"; public const string ReadOnlyStatementRequired = Prefix + "ReadOnlyStatementRequired"; @@ -25,4 +26,18 @@ public static class McpErrorCodes public const string DictionaryNotFound = Prefix + "DictionaryNotFound"; public const string RegistrationLimitReached = Prefix + "RegistrationLimitReached"; public const string RedirectUriNotAllowed = Prefix + "RedirectUriNotAllowed"; + public const string DevGuideUnavailable = Prefix + "DevGuideUnavailable"; + public const string DevGuideSectionNotFound = Prefix + "DevGuideSectionNotFound"; + public const string InvalidWizard = Prefix + "InvalidWizard"; + public const string ComponentNameRequired = Prefix + "ComponentNameRequired"; + public const string ComponentCodeRequired = Prefix + "ComponentCodeRequired"; + public const string ComponentNotFound = Prefix + "ComponentNotFound"; + public const string VisualDesignerComponent = Prefix + "VisualDesignerComponent"; + public const string TableColumnsRequired = Prefix + "TableColumnsRequired"; + public const string UnsupportedColumnType = Prefix + "UnsupportedColumnType"; + public const string InvalidColumnLength = Prefix + "InvalidColumnLength"; + public const string InvalidDefaultValue = Prefix + "InvalidDefaultValue"; + public const string DuplicateColumn = Prefix + "DuplicateColumn"; + public const string TableAlreadyExists = Prefix + "TableAlreadyExists"; + public const string TableProviderNotSupported = Prefix + "TableProviderNotSupported"; } 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 2c6f77e1..d0c25035 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs @@ -22,12 +22,34 @@ public static class McpToolNames public const string InsertRow = "insert_row"; public const string UpdateRow = "update_row"; public const string DeleteRow = "delete_row"; + public const string GetDevGuide = "get_dev_guide"; + public const string CreateTable = "create_table"; + public const string ListWizards = "list_wizards"; + public const string GetWizard = "get_wizard"; + public const string DeployWizard = "deploy_wizard"; + public const string DeleteWizard = "delete_wizard"; + public const string ListComponents = "list_components"; + public const string GetComponent = "get_component"; + public const string SaveComponent = "save_component"; + public const string DeleteComponent = "delete_component"; /// - /// Veri yazan araclar. Sunucu kaydinda AllowWrite kapaliysa bunlar arac listesinde - /// hic yer almaz. + /// Ekran ve bilesen gelistirme araclari. Sunucu bazinda AllowedTools ile, kullanici + /// bazinda McpServers.Develop yetkisiyle kapatilir. /// - public static readonly string[] WriteTools = [InsertRow, UpdateRow, DeleteRow]; + public static readonly string[] DevelopTools = + [ + GetDevGuide, + CreateTable, + ListWizards, + GetWizard, + DeployWizard, + DeleteWizard, + ListComponents, + GetComponent, + SaveComponent, + DeleteComponent + ]; /// /// Sunucu kaydi olmasa da acik kalan araclar. Istemcinin baglantisinin neden bos @@ -35,10 +57,29 @@ 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. + /// + public static readonly string[] TenantTools = + [ + ServerInfo, + ListLists, + DescribeList, + SearchSchema, + QueryList, + RunSelect + ]; + /// /// 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. + /// 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. /// public static readonly IReadOnlyDictionary RequiredPermissions = new Dictionary { @@ -46,6 +87,16 @@ 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, }; /// Yayinlanan tum araclar, arac listesindeki siralarina gore. @@ -61,7 +112,8 @@ public static class McpToolNames RunQuery, InsertRow, UpdateRow, - DeleteRow + DeleteRow, + .. DevelopTools ]; /// diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolPolicy.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolPolicy.cs index 73861c20..6d1fe8c8 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolPolicy.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolPolicy.cs @@ -12,11 +12,11 @@ namespace Sozsoft.Mcp; public static class McpToolPolicy { /// Istegin kiracisina dusen aktif bir sunucu kaydi var mi. - /// Sunucu kaydinda veri yazma acik mi. - /// Acik arac adlari; bos liste "yazma disindaki tumu acik" demektir. + /// Istek bir kiraci kullanicisindan mi geliyor; o zaman yalnizca okuma araclari acilir. + /// Acik arac adlari; bos liste "tumu acik" demektir. public static bool IsAllowed( bool hasServerRecord, - bool allowWrite, + bool isTenant, IReadOnlyCollection allowedTools, string toolName) { @@ -25,12 +25,12 @@ public static class McpToolPolicy return true; } - if (!hasServerRecord) + if (isTenant && !McpToolNames.TenantTools.Contains(toolName, StringComparer.Ordinal)) { return false; } - if (!allowWrite && McpToolNames.WriteTools.Contains(toolName, StringComparer.Ordinal)) + if (!hasServerRecord) { return false; } @@ -45,7 +45,7 @@ public static class McpToolPolicy public static IReadOnlyList VisibleTools( bool hasServerRecord, - bool allowWrite, + bool isTenant, IReadOnlyCollection allowedTools) - => [.. McpToolNames.All.Where(name => IsAllowed(hasServerRecord, allowWrite, allowedTools, name))]; + => [.. McpToolNames.All.Where(name => IsAllowed(hasServerRecord, isTenant, allowedTools, name))]; } diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Entities/McpServer.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Entities/McpServer.cs index be26c671..482273ca 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Entities/McpServer.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Entities/McpServer.cs @@ -6,8 +6,8 @@ namespace Sozsoft.Mcp.Entities; /// /// MCP (Model Context Protocol) sunucu tanimi. Kayit host kapsamindadir; hangi kiracilarin /// kullanabilecegi ile belirlenir. Istegin kiracisina dusen kayit, /mcp -/// ucunda hangi araclarin gorunecegini ve sorgularin hangi veri kaynaginda hangi satir -/// tavaniyla kosacagini belirler. +/// ucunda hangi araclarin gorunecegini ve sorgularin hangi satir tavaniyla kosacagini belirler. +/// Sorgular her zaman istegin kiracisinin veritabaninda kosar. /// public class McpServer : FullAuditedEntity { @@ -28,22 +28,13 @@ public class McpServer : FullAuditedEntity public string? Tenants { get; set; } /// - /// Sorgu araclarinin kosacagi DataSource kodu. Bos ise istegin kapsamindaki varsayilan - /// veri kaynagi kullanilir. - /// - public string? DataSourceCode { get; set; } - - /// - /// Acik arac adlari, PlatformConsts.MultiValueDelimiter ile ayrilir. Bos ise yazma - /// disindaki tum araclar aciktir. + /// Acik arac adlari, PlatformConsts.MultiValueDelimiter ile ayrilir. Bos ise tum + /// araclar aciktir. /// public string? AllowedTools { get; set; } /// Tek sorgunun dondurebilecegi en fazla satir sayisi. public int MaxRows { get; set; } = McpConsts.DefaultMaxRows; - /// Kapali oldugunda veri yazma araclari arac listesinde hic yer almaz. - public bool AllowWrite { get; set; } - public string? Description { get; set; } } diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpDevGuide.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpDevGuide.cs new file mode 100644 index 00000000..5feabc3f --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpDevGuide.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Volo.Abp.Domain.Services; + +namespace Sozsoft.Mcp.Services; + +/// +/// Modelin ekran ve bilesen uretirken okudugu sozlesme: depodaki +/// .github/instructions/lowcode*.instructions.md dosyalari. Claude Code ayni dosyalari +/// depodan okur; MCP istemcisi ise bu servisten bolum bolum alir, boylece iki yolun bildigi +/// sozlesme ayrisamaz. +/// +public interface IMcpDevGuide +{ + /// Okunabilen belgelerin baslik agaci; klasor bulunamazsa bos. + Task> GetHeadingsAsync(); + + /// + /// Bir bolumun metni. bolum numarasi (4.3) ya da baslik + /// metninin bir parcasidir; bulunamazsa null. + /// + Task GetSectionAsync(string document, string section); +} + +/// Belgenin kisa adi (). +/// Bolum numarasi; numarasiz basliklarda baslik metni. +public sealed record McpDevGuideHeading(string Document, string Id, string Title, int Level); + +public sealed record McpDevGuideSection(McpDevGuideHeading Heading, string Content, IReadOnlyList Children); + +public partial class McpDevGuide(IConfiguration configuration) : DomainService, IMcpDevGuide +{ + /// Konteynerde mount noktasi; bos ise depo klasoru calisma dizininden yukari aranir. + public const string ConfigurationKey = "App:DevGuidePath"; + + /// Kisa ad → dosya. Yalnizca uretim sozlesmesi sunulur; kod standardi ve karar kurallari degil. + public static readonly IReadOnlyDictionary Documents = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["lowcode"] = "lowcode.instructions.md", + ["reference"] = "lowcode-reference.instructions.md", + }; + + /// + /// Tek bir yanitta donen metnin tavani. Ust seviye bolumlerin bazilari istemcilerin tek + /// arac cevabi icin kabul ettigi boyutu asar; bu durumda alt basliklar ayrica istenir. + /// + private const int MaxSectionLength = 60_000; + + [GeneratedRegex(@"^(#{2,4})\s+(?:(\d+(?:\.\d+)*)\.?\s+)?(.+?)\s*$")] + private static partial Regex HeadingPattern(); + + public async Task> GetHeadingsAsync() + { + var headings = new List(); + foreach (var document in Documents.Keys) + { + var lines = await ReadLinesAsync(document); + headings.AddRange(ParseHeadings(document, lines).Select(heading => heading.Heading)); + } + + return headings; + } + + public async Task GetSectionAsync(string document, string section) + { + var lines = await ReadLinesAsync(document); + var headings = ParseHeadings(document, lines); + var wanted = section.Trim().TrimEnd('.'); + + var index = headings.FindIndex(h => string.Equals(h.Heading.Id, wanted, StringComparison.OrdinalIgnoreCase)); + if (index < 0) + { + index = headings.FindIndex(h => h.Heading.Title.Contains(wanted, StringComparison.OrdinalIgnoreCase)); + } + + if (index < 0) + { + return null; + } + + var match = headings[index]; + var next = headings.FindIndex(index + 1, h => h.Heading.Level <= match.Heading.Level); + var end = next < 0 ? lines.Length : headings[next].Line; + var children = headings + .Skip(index + 1) + .TakeWhile(h => h.Line < end) + .Where(h => h.Heading.Level == match.Heading.Level + 1) + .ToList(); + + var content = string.Join('\n', lines[match.Line..end]); + if (content.Length > MaxSectionLength && children.Count > 0) + { + // Bolumun giris metni tek basina anlamlidir; geri kalani alt baslik olarak istenir. + content = string.Join('\n', lines[match.Line..children[0].Line]); + } + + return new McpDevGuideSection(match.Heading, content, [.. children.Select(child => child.Heading)]); + } + + private async Task ReadLinesAsync(string document) + { + if (!Documents.TryGetValue(document, out var fileName)) + { + return []; + } + + var directory = ResolveDirectory(); + var path = directory is null ? null : Path.Combine(directory, fileName); + if (path is null || !File.Exists(path)) + { + return []; + } + + return (await File.ReadAllTextAsync(path)).Replace("\r\n", "\n").Split('\n'); + } + + private string? ResolveDirectory() + { + var configured = configuration[ConfigurationKey]; + if (!string.IsNullOrWhiteSpace(configured)) + { + return Path.GetFullPath(configured); + } + + for (var dir = new DirectoryInfo(Directory.GetCurrentDirectory()); dir != null; dir = dir.Parent) + { + var candidate = Path.Combine(dir.FullName, ".github", "instructions"); + if (Directory.Exists(candidate)) + { + return candidate; + } + } + + return null; + } + + /// + /// Kod bloklari icindeki ## ile baslayan satirlar (ornegin SQL yorumlari) baslik + /// sayilmaz; aksi halde bolum sinirlari ornek kodun ortasinda kesilirdi. + /// + private static List<(McpDevGuideHeading Heading, int Line)> ParseHeadings(string document, string[] lines) + { + var headings = new List<(McpDevGuideHeading, int)>(); + var inFence = false; + + for (var i = 0; i < lines.Length; i++) + { + if (lines[i].TrimStart().StartsWith("```", StringComparison.Ordinal)) + { + inFence = !inFence; + continue; + } + + if (inFence) + { + continue; + } + + var match = HeadingPattern().Match(lines[i]); + if (!match.Success) + { + continue; + } + + var title = match.Groups[3].Value; + var id = match.Groups[2].Success ? match.Groups[2].Value : title; + headings.Add((new McpDevGuideHeading(document, id, title, match.Groups[1].Length), i)); + } + + return headings; + } +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpQueryExecutor.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpQueryExecutor.cs index a4861b18..43adbac4 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpQueryExecutor.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpQueryExecutor.cs @@ -16,10 +16,10 @@ namespace Sozsoft.Mcp.Services; public interface IMcpQueryExecutor { /// - /// Sunucu kaydindaki kod. Bos ya da !Tenant ise veri kaynagi istegin kiracisindan - /// cozulur; host baglaminda varsayilan kaynaga duser. + /// Custom Endpoint'in kendi veri kaynagi kodu. Bos ya da !Tenant ise veri kaynagi + /// istegin kiracisindan cozulur; host baglaminda varsayilan kaynaga duser. /// - Task ResolveDataSourceAsync(string? dataSourceCode); + Task ResolveDataSourceAsync(string? dataSourceCode = null); /// /// Sorguyu kosturur. Sonuc kumesi satirda kesilir; kesilme @@ -34,7 +34,7 @@ public interface IMcpQueryExecutor public class McpQueryExecutor(IDynamicDataManager dynamicDataManager) : DomainService, IMcpQueryExecutor { - public async Task ResolveDataSourceAsync(string? dataSourceCode) + public async Task ResolveDataSourceAsync(string? dataSourceCode = null) { var useTenantScope = string.IsNullOrWhiteSpace(dataSourceCode) || dataSourceCode == McpConsts.TenantDataSourceMarker; diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpTableCreator.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpTableCreator.cs new file mode 100644 index 00000000..30839e55 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpTableCreator.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Localization; +using Sozsoft.Platform.Data.Seeds; +using Sozsoft.Platform.Enums; +using Sozsoft.Platform.Localization; +using Volo.Abp; +using Volo.Abp.Domain.Services; + +namespace Sozsoft.Mcp.Services; + +/// +/// Yeni tabloyu SQL Query Manager'daki tablo tasarimcisiyla ayni sozlesmeyle kurar: script +/// tasarimcinin generateCreateTableSql ciktisiyla ayni bicimdedir ve tasarimcinin deploy +/// sonrasi yazdigi yere ({kapsam}/sql/object/{Tablo}.sql) yazilir; boylece tablo +/// tasarimcida acilip duzenlenebilir ve veritabani sifirlandiginda seed ile geri gelir. +/// +/// Yalnizca olusturma yapar. Var olan tabloya dokunulmaz — kolon ekleme/degistirme tasarimcinin +/// isidir ve sema degisikligi kurallarina (kayipsiz ekleme, degistirme/dusurme yok) tabidir. +/// Kolon tipi ve varsayilan deger serbest metin degil, beyaz listeden gecer; istemci SQL yazamaz. +/// +/// +public interface IMcpTableCreator +{ + Task CreateAsync(McpTableDefinition definition); +} + +public class McpTableCreator( + IMcpQueryExecutor queryExecutor, + IConfiguration configuration, + IStringLocalizer localizer) : DomainService, IMcpTableCreator +{ + private const string SchemaName = "dbo"; + private const int DefaultTextLength = 100; + private const int MaxTextLength = 4000; + + private static readonly Regex SafeIdentifierRegex = new( + @"^[A-Za-z_][A-Za-z0-9_]{0,127}$", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + /// + /// Varsayilan deger olarak kabul edilenler: sayi, metin sabiti ve tasarimcinin kendi + /// sablonlarinda kullandigi fonksiyonlar. Baska bir ifade script'e hic girmez. + /// + private static readonly Regex SafeDefaultRegex = new( + @"^(?:-?\d{1,18}(?:\.\d{1,18})?|N?'(?:[^']|'')*'|NEWID\(\)|GETDATE\(\)|GETUTCDATE\(\)|SYSUTCDATETIME\(\))$", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + /// Tasarimcinin tip listesi (DATA_TYPES) ile aynidir. + private static readonly string[] SupportedTypes = + [ + "nvarchar", "nvarchar(MAX)", "int", "bigint", "decimal", "float", "bit", + "datetime", "datetime2", "date", "uniqueidentifier", "money" + ]; + + // Sablon kolonlari tasarimcinin FULL_AUDIT_COLUMNS / TENANT_COLUMN tanimlariyla aynidir. + private static readonly McpTableColumn IdColumn = new("Id", "uniqueidentifier", null, false, "NEWID()"); + + private static readonly McpTableColumn TenantColumn = new("TenantId", "uniqueidentifier", null, true, null); + + private static readonly McpTableColumn[] AuditColumns = + [ + new("CreationTime", "datetime2", null, false, "GETUTCDATE()"), + new("CreatorId", "uniqueidentifier", null, true, null), + new("LastModificationTime", "datetime2", null, true, null), + new("LastModifierId", "uniqueidentifier", null, true, null), + new("IsDeleted", "bit", null, false, "0"), + new("DeletionTime", "datetime2", null, true, null), + new("DeleterId", "uniqueidentifier", null, true, null) + ]; + + public async Task CreateAsync(McpTableDefinition definition) + { + Check.NotNull(definition, nameof(definition)); + + var tableName = (definition.TableName ?? string.Empty).Trim(); + EnsureSafeIdentifier(tableName); + + var columns = BuildColumns(definition); + + var context = await queryExecutor.ResolveDataSourceAsync(); + if (context.DataSourceType != DataSourceTypeEnum.Mssql) + { + throw new UserFriendlyException(localizer[McpErrorCodes.TableProviderNotSupported, context.DataSourceType.ToString()]); + } + + var existing = await context.Repository.ExecuteScalarAsync( + "SELECT OBJECT_ID(@name, 'U')", + context.ConnectionString, + new Dictionary { ["name"] = $"{SchemaName}.{tableName}" }); + + if (existing is not null) + { + throw new UserFriendlyException(localizer[McpErrorCodes.TableAlreadyExists, tableName]); + } + + var script = BuildScript(tableName, columns); + + // GO yalnizca SSMS ayiricidir; tek batch oldugu icin calistirirken atilir, dosyada kalir. + await context.Repository.ExecuteAsync( + string.Join('\n', script.Split('\n').Where(line => line.Trim() != "GO")), + context.ConnectionString); + + var seedFilePath = SaveSeedScript(tableName, script); + + return new McpCreatedTable(tableName, script, seedFilePath); + } + + private List BuildColumns(McpTableDefinition definition) + { + if (definition.Columns.Count == 0) + { + throw new UserFriendlyException(localizer[McpErrorCodes.TableColumnsRequired]); + } + + var userColumns = definition.Columns.Select(NormalizeColumn).ToList(); + + var columns = new List { IdColumn }; + if (definition.MultiTenant) + { + columns.Add(TenantColumn); + } + + columns.AddRange(userColumns); + + if (definition.FullAudited) + { + columns.AddRange(AuditColumns); + } + + var duplicate = columns + .GroupBy(column => column.Name, StringComparer.OrdinalIgnoreCase) + .FirstOrDefault(group => group.Count() > 1); + + if (duplicate is not null) + { + throw new UserFriendlyException(localizer[McpErrorCodes.DuplicateColumn, duplicate.Key]); + } + + return columns; + } + + private McpTableColumn NormalizeColumn(McpTableColumn column) + { + var name = (column.Name ?? string.Empty).Trim(); + EnsureSafeIdentifier(name); + + var type = SupportedTypes.FirstOrDefault(candidate => + string.Equals(candidate, column.Type?.Trim(), StringComparison.OrdinalIgnoreCase)) + ?? throw new UserFriendlyException( + localizer[McpErrorCodes.UnsupportedColumnType, column.Type ?? string.Empty, string.Join(", ", SupportedTypes)]); + + int? maxLength = null; + if (type == "nvarchar") + { + maxLength = column.MaxLength ?? DefaultTextLength; + if (maxLength is < 1 or > MaxTextLength) + { + throw new UserFriendlyException(localizer[McpErrorCodes.InvalidColumnLength, name, MaxTextLength]); + } + } + + var defaultValue = string.IsNullOrWhiteSpace(column.DefaultValue) ? null : column.DefaultValue.Trim(); + if (defaultValue is not null && !SafeDefaultRegex.IsMatch(defaultValue)) + { + throw new UserFriendlyException(localizer[McpErrorCodes.InvalidDefaultValue, name, defaultValue]); + } + + return new McpTableColumn(name, type, maxLength, column.IsNullable, defaultValue); + } + + private static string BuildScript(string tableName, List columns) + { + var fullTableName = $"[{SchemaName}].[{tableName}]"; + + var lines = new List + { + $"IF OBJECT_ID(N'{fullTableName}', 'U') IS NULL", + "BEGIN", + $" CREATE TABLE {fullTableName}", + " (" + }; + + lines.AddRange(columns.Select(column => ToColumnLine(column) + ",")); + lines.Add($" CONSTRAINT [PK_{tableName}] PRIMARY KEY NONCLUSTERED"); + lines.Add(" ("); + lines.Add(" [Id] ASC"); + lines.Add(" )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]"); + lines.Add(" ) ON [PRIMARY]"); + lines.Add("END"); + lines.Add("GO"); + + return string.Join('\n', lines); + } + + private static string ToColumnLine(McpTableColumn column) + { + var type = column.Type switch + { + "nvarchar" => $"nvarchar({column.MaxLength})", + "decimal" => "decimal(18, 4)", + _ => column.Type + }; + + var nullPart = column.IsNullable ? "NULL" : "NOT NULL"; + var defaultPart = column.DefaultValue is null ? string.Empty : $" DEFAULT {column.DefaultValue}"; + + return $" [{column.Name}] {type} {nullPart}{defaultPart}"; + } + + /// + /// Tasarimcinin SaveTableScriptAsync ile yazdigi yol: kapsam istegin kiracisini izler. + /// Dosyasiz tablo veritabani sifirlandiginda geri gelmezdi; yazilamazsa islem hata verir. + /// + private string SaveSeedScript(string tableName, string script) + { + var outputPath = SeedPathResolver.GetScopePath( + configuration, + CurrentTenant.Id, + SeedPathResolver.GetProviderFolderName(isPostgreSql: false), + SeedPathResolver.ObjectFolder); + + Directory.CreateDirectory(outputPath); + + var filePath = Path.Combine(outputPath, $"{tableName}.sql"); + File.WriteAllText(filePath, script); + + return $"{SeedPathResolver.GetScopeFolderName(CurrentTenant.Id)}/{SeedPathResolver.SqlFolder}/{SeedPathResolver.ObjectFolder}/{tableName}.sql"; + } + + private void EnsureSafeIdentifier(string identifier) + { + if (!SafeIdentifierRegex.IsMatch(identifier)) + { + throw new UserFriendlyException(localizer[McpErrorCodes.InvalidIdentifier, identifier]); + } + } +} + +/// Tam audit kolonlari (olusturma/degistirme/silme izi) eklensin mi. +/// TenantId kolonu eklensin mi. +public sealed record McpTableDefinition( + string TableName, + IReadOnlyList Columns, + bool FullAudited, + bool MultiTenant); + +/// Yalnizca nvarchar icin; bos ise tasarimcinin varsayilani (100). +public sealed record McpTableColumn( + string Name, + string Type, + int? MaxLength, + bool IsNullable, + string? DefaultValue); + +/// Seed kokune gore goreli yol. +public sealed record McpCreatedTable(string TableName, string Script, string SeedFilePath); diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/EntityFrameworkCore/McpDbContextModelCreatingExtensions.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/EntityFrameworkCore/McpDbContextModelCreatingExtensions.cs index 40714282..fdd9c452 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/EntityFrameworkCore/McpDbContextModelCreatingExtensions.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/EntityFrameworkCore/McpDbContextModelCreatingExtensions.cs @@ -26,9 +26,7 @@ public static class McpDbContextModelCreatingExtensions // dxTagBox ile doldurulur. b.Property(x => x.Tenants).HasMaxLength(2048); b.Property(x => x.AllowedTools).HasMaxLength(1024); - b.Property(x => x.DataSourceCode).HasMaxLength(64); b.Property(x => x.MaxRows).HasDefaultValue(McpConsts.DefaultMaxRows); - b.Property(x => x.AllowWrite).HasDefaultValue(false); b.Property(x => x.Description).HasMaxLength(1024); b.HasIndex(x => new { x.Name }).IsUnique().HasFilter("[IsDeleted] = 0"); diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpHttpApiModule.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpHttpApiModule.cs index 69ef42d3..1d3163cf 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpHttpApiModule.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpHttpApiModule.cs @@ -45,6 +45,7 @@ public class McpHttpApiModule : AbpModule .WithTools() .WithTools() .WithTools() + .WithTools() .WithRequestFilters(filters => filters.AddListToolsFilter(next => async (requestContext, cancellationToken) => { var result = await next(requestContext, cancellationToken); diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpServerInfoDefaults.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpServerInfoDefaults.cs index 6da4b77a..23786a43 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpServerInfoDefaults.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpServerInfoDefaults.cs @@ -18,5 +18,9 @@ internal static class McpServerInfoDefaults "Sozsoft Platform data access. Do not guess table or column names: first find the list with " + "list_lists or search_schema, read its columns with describe_list, then query it with run_select " + "using named @parameters. Never inline literal values into SQL. Columns marked isSensitive must " + - "not be selected. Call server_info first when a tool behaves unexpectedly."; + "not be selected. Call server_info first when a tool behaves unexpectedly. " + + "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."; } diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDataTools.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDataTools.cs index b60d95ef..559d440e 100644 --- a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDataTools.cs +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDataTools.cs @@ -8,8 +8,8 @@ namespace Sozsoft.Mcp.Tools; /// /// Veri yazma araclari. Yazma bir ListForm ekraninin sozlesmesinden gecer; ekran yetkisi ve -/// tenant filtresi aynen uygulanir. Sunucu kaydinda AllowWrite kapaliysa bu araclar -/// arac listesinde hic gorunmez. +/// tenant filtresi aynen uygulanir. Sunucu kaydinda AllowedTools dolu ve bu araclar +/// secili degilse arac listesinde hic gorunmez. /// [McpServerToolType] public sealed class McpDataTools(IMcpDataAppService dataAppService) diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs new file mode 100644 index 00000000..b8f713c5 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDevelopTools.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Sozsoft.Mcp.Dtos; +using Sozsoft.Platform.DeveloperKit; +using Sozsoft.Platform.ListForms; + +namespace Sozsoft.Mcp.Tools; + +/// +/// Ekran (wizard) ve custom component gelistirme araclari. Uretim uygulamadaki Wizard ve +/// Custom Components servislerinden gecer; menu, yetki, dil anahtari ve seed dosyasi ekrandaki +/// yolla ayni sekilde olusur. +/// +[McpServerToolType] +public sealed class McpDevelopTools(IMcpDevelopAppService developAppService) +{ + [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( + [Description("'lowcode' (uretim protokolu ve artefakt semalari, varsayilan) ya da 'reference' (JSON kolon sozlesmeleri, tasarimci envanteri).")] + string? document = null, + [Description("Bolum numarasi (4.2) ya da baslik metninin bir parcasi. Bos birakilirsa baslik listesi doner.")] + string? section = null) + => await developAppService.GetDevGuideAsync(document, section); + + [McpServerTool(Name = McpToolNames.CreateTable, Destructive = false, Idempotent = false)] + [Description("Ekranin baglanacagi yeni tabloyu SQL Query Manager tablo tasarimcisiyla ayni bicimde olusturur ve script'i seed dosyasina (sql/object) yazar. Id (uniqueidentifier, PK), TenantId ve tam audit kolonlari (CreationTime, CreatorId, LastModificationTime, LastModifierId, IsDeleted, DeletionTime, DeleterId) otomatik eklenir; columns icine yalnizca is kolonlarini yaz. Yalnizca yeni tablo olusturur: tablo varsa reddeder, kolon ekleme/degistirme tablo tasarimcisindan yapilir. SQL Server veri kaynaginda calisir. Ardindan deploy_wizard ile ekrani kur.")] + public async Task CreateTableAsync( + [Description("Tablo adi; harf/rakam/alt cizgi. Adlandirma icin get_dev_guide'daki tablo onek kuralina uy (ornegin Adm_T_...).")] + string tableName, + [Description("Is kolonlari. type: nvarchar, nvarchar(MAX), int, bigint, decimal (18,4), float, bit, datetime, datetime2, date, uniqueidentifier, money. maxLength yalnizca nvarchar icin (varsayilan 100). defaultValue: sayi, 'metin' ya da NEWID()/GETDATE()/GETUTCDATE()/SYSUTCDATETIME().")] + List columns, + [Description("Tam audit kolonlari eklensin mi. Kullanici acikca istemedikce true birak.")] + bool fullAudited = true, + [Description("TenantId kolonu eklensin mi. Kullanici acikca istemedikce true birak.")] + bool multiTenant = true) + => await developAppService.CreateTableAsync(new McpCreateTableRequestDto + { + TableName = tableName, + Columns = columns, + FullAudited = fullAudited, + MultiTenant = multiTenant + }); + + [McpServerTool(Name = McpToolNames.ListWizards, ReadOnly = true)] + [Description("Wizard File Manager'daki wizard dosyalarini listeler: dosya adi, ekran kodu, yol (List/Custom), menu yolu ve bagimli wizard'lar.")] + public async Task> ListWizardsAsync( + [Description("Dosya adi, wizard adi, ekran kodu ya da bilesen adi icinde aranacak metin. Bos birakilabilir.")] + string? search = null) + => await developAppService.GetWizardsAsync(search); + + [McpServerTool(Name = McpToolNames.GetWizard, ReadOnly = true)] + [Description("Bir wizard seed dosyasini diskteki bicimiyle dondurur (Wizard / ListForm / Fields bolumleri). Var olan bir ekrani degistirmeden once oku.")] + public async Task GetWizardAsync( + [Description("list_wizards cikisindaki fileName degeri, ornegin 'Tags.json'.")] + string fileName) + => 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. 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.")] + 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, + [Description("Guncellemede yerine gecilecek dosyanin adi (list_wizards cikisindaki fileName). Yeni wizard icin bos birak.")] + string? replaceFileName = null) + => await developAppService.DeployWizardAsync(new McpWizardDeployRequestDto { Wizard = wizard, ReplaceFileName = replaceFileName }); + + [McpServerTool(Name = McpToolNames.DeleteWizard, Destructive = true, Idempotent = true)] + [Description("Bir wizard'i ve yalnizca onun urettigi kayitlari siler; baska yerde kullanilan kayitlar korunur ve raporda gosterilir. Bagimli wizard'i olan bir wizard silinemez, once bagimlilar silinir.")] + public async Task DeleteWizardAsync( + [Description("Silinecek wizard dosyasinin adi (list_wizards cikisindaki fileName).")] + string fileName) + => await developAppService.DeleteWizardAsync(fileName); + + [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( + [Description("Ad, rota ya da aciklama icinde aranacak metin. Bos birakilabilir.")] + string? search = null) + => await developAppService.GetComponentsAsync(search); + + [McpServerTool(Name = McpToolNames.GetComponent, ReadOnly = true)] + [Description("Bir custom component'in kodunu, props'unu ve bagimliliklarini dondurur. Degistirmeden once oku.")] + public async Task GetComponentAsync( + [Description("Bilesenin adi.")] + string name) + => await developAppService.GetComponentAsync(name); + + [McpServerTool(Name = McpToolNames.SaveComponent, Destructive = true, Idempotent = true)] + [Description("Custom component'i ada gore kaydeder: yoksa ekler, varsa gunceller. Guncellemede verilmeyen alanlar korunur. Kod kurallari ve kullanilabilir kutuphaneler icin get_dev_guide 7. bolumu. Bilesen menude gorunecekse ardindan ComponentKind 1 ile deploy_wizard cagir.")] + public async Task SaveComponentAsync( + [Description("Bilesenin adi; kimligidir.")] + string name, + [Description("Bilesenin React kodu. Yeni bilesende zorunlu, guncellemede bos birakilirsa mevcut kod korunur.")] + string? code = null, + [Description("'/' ile baslayan rota. '/admin/' ile baslamayan rota public sayfa olur. Bos dize rotayi kaldirir, verilmezse mevcut rota korunur.")] + string? routePath = null, + [Description("Aciklama.")] + string? description = null, + [Description("Props JSON metni. Tasarimci belgesi (visualDesigner) iceremez.")] + string? props = null, + [Description("Bagimli bilesen adlarinin JSON dizisi, ornegin [\"PublicHeader\"].")] + string? dependencies = null, + [Description("Bilesen etkin mi. Yeni bilesende varsayilan true.")] + bool? isActive = null) + => await developAppService.SaveComponentAsync(new McpComponentSaveRequestDto + { + Name = name, + Code = code, + RoutePath = routePath, + Description = description, + Props = props, + Dependencies = dependencies, + IsActive = isActive + }); + + [McpServerTool(Name = McpToolNames.DeleteComponent, Destructive = true, Idempotent = false)] + [Description("Custom component'i siler. Bilesene bagli bir Custom wizard varsa once delete_wizard ile o silinir.")] + public async Task DeleteComponentAsync( + [Description("Bilesenin adi.")] + string name) + => await developAppService.DeleteComponentAsync(name); +} diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json index ad99dfa5..7a330679 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json @@ -28455,20 +28455,26 @@ { "resourceName": "Platform", "key": "App.DeveloperKit.McpServers.Invoke", - "en": "Call MCP tools", - "tr": "MCP araçlarını çağır" + "en": "Invoke", + "tr": "Çağır" }, { "resourceName": "Platform", "key": "App.DeveloperKit.McpServers.Write", - "en": "Write data over MCP", - "tr": "MCP üzerinden veri yaz" + "en": "Write Data", + "tr": "Veri Yaz" + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKit.McpServers.Develop", + "en": "Develop", + "tr": "Geliştir" }, { "resourceName": "Platform", "key": "App.DeveloperKit.McpServers.Description", - "en": "MCP Servers — the Model Context Protocol endpoint definitions. Table: Sas_H_McpServer · host scope · list screen.\nUsage: an AI client (Claude Code, Claude Desktop, ChatGPT) connects to /mcp with the user's own access token; the record matching the request's tenant decides which tools are published, on which data source they run and with which row ceiling.\nFields:\n• Name → the server name shown in the tool list\n• Url → the address handed to the client (https://api.sozsoft.com/mcp in production, https://localhost:44344/mcp locally); the server does not route on this value\n• Tenants → which tenants may use it; when empty it is open to all\n• DataSourceCode → the data source the query tools run on; when empty the request's own scope is used\n• AllowedTools → the open tools; when empty every tool except the writing ones is open\n• MaxRows → the largest number of rows a single query may return\n• AllowWrite → while off, insert_row, update_row and delete_row are not even listed\n• IsActive → a passive record publishes no tools\nCaution: schema discovery reads the JSON data dictionary in Sas_H_DbTableColumn. When the dictionary is missing or stale the tools return no tables — run dbo.Sas_H_ListFormFieldsDictionaryJson_SaveDbTableColumns with the tenant name.", - "tr": "MCP Sunucuları — Model Context Protocol uç tanımları. Tablo: Sas_H_McpServer · host kapsamlı · liste ekranı.\nKullanım: bir yapay zekâ istemcisi (Claude Code, Claude Desktop, ChatGPT) kullanıcının kendi erişim token'ı ile /mcp ucuna bağlanır; isteğin kiracısına düşen kayıt hangi araçların yayınlandığını, sorguların hangi veri kaynağında ve hangi satır tavanıyla koşacağını belirler.\nAlanlar:\n• Name → araç listesinde görünen sunucu adı\n• Url → istemciye verilen adres (sunucuda https://api.sozsoft.com/mcp, yerelde https://localhost:44344/mcp); sunucu bu değere göre yönlendirme yapmaz\n• Tenants → hangi kiracıların kullanabileceği; boş bırakılırsa tümüne açıktır\n• DataSourceCode → sorgu araçlarının koşacağı veri kaynağı; boş ise isteğin kendi kapsamı kullanılır\n• AllowedTools → açık araçlar; boş bırakılırsa yazma dışındaki tüm araçlar açıktır\n• MaxRows → tek sorgunun döndürebileceği en fazla satır sayısı\n• AllowWrite → kapalıyken insert_row, update_row ve delete_row araç listesinde hiç yer almaz\n• IsActive → pasif kayıt hiçbir araç yayınlamaz\nDikkat: şema keşfi Sas_H_DbTableColumn tablosundaki JSON veri sözlüğünü okur. Sözlük yok ya da bayatsa araçlar tablo döndürmez — dbo.Sas_H_ListFormFieldsDictionaryJson_SaveDbTableColumns prosedürünü kiracı adıyla çalıştırın." + "en": "MCP Servers — the Model Context Protocol endpoint definitions. Table: Sas_H_McpServer · host scope · list screen.\nUsage: an AI client (Claude Code, Claude Desktop, ChatGPT) connects to /mcp with the user's own access token; the record matching the request's tenant decides which tools are published and with which row ceiling they run; queries always run on the requesting tenant's own database.\nFields:\n• Name → the server name shown in the tool list\n• Url → the address handed to the client (https://api.sozsoft.com/mcp in production, https://localhost:44344/mcp locally); the server does not route on this value\n• Tenants → which tenants may use it; when empty it is open to all\n• AllowedTools → the open tools; when empty every tool is open, when filled only the selected ones are. Tenant users only ever get the read tools (schema tools, query_list, run_select); this field can narrow that set but never widen it\n• MaxRows → the largest number of rows a single query may return\n• IsActive → a passive record publishes no tools\nCaution: schema discovery reads the JSON data dictionary in Sas_H_DbTableColumn. When the dictionary is missing or stale the tools return no tables — run dbo.Sas_H_ListFormFieldsDictionaryJson_SaveDbTableColumns with the tenant name.", + "tr": "MCP Sunucuları — Model Context Protocol uç tanımları. Tablo: Sas_H_McpServer · host kapsamlı · liste ekranı.\nKullanım: bir yapay zekâ istemcisi (Claude Code, Claude Desktop, ChatGPT) kullanıcının kendi erişim token'ı ile /mcp ucuna bağlanır; isteğin kiracısına düşen kayıt hangi araçların yayınlandığını, sorguların hangi satır tavanıyla koşacağını belirler; sorgular her zaman isteğin kiracısının kendi veritabanında koşar.\nAlanlar:\n• Name → araç listesinde görünen sunucu adı\n• Url → istemciye verilen adres (sunucuda https://api.sozsoft.com/mcp, yerelde https://localhost:44344/mcp); sunucu bu değere göre yönlendirme yapmaz\n• Tenants → hangi kiracıların kullanabileceği; boş bırakılırsa tümüne açıktır\n• AllowedTools → açık araçlar; boş bırakılırsa tüm araçlar açıktır, doluysa yalnızca seçilenler açıktır. Kiracı kullanıcısı yalnızca okuma araçlarını alır (şema araçları, query_list, run_select); bu alan o kümeyi daraltabilir, genişletemez\n• MaxRows → tek sorgunun döndürebileceği en fazla satır sayısı\n• IsActive → pasif kayıt hiçbir araç yayınlamaz\nDikkat: şema keşfi Sas_H_DbTableColumn tablosundaki JSON veri sözlüğünü okur. Sözlük yok ya da bayatsa araçlar tablo döndürmez — dbo.Sas_H_ListFormFieldsDictionaryJson_SaveDbTableColumns prosedürünü kiracı adıyla çalıştırın." }, { "resourceName": "Platform", @@ -28488,12 +28494,6 @@ "en": "Max Rows", "tr": "En Fazla Satır" }, - { - "resourceName": "Platform", - "key": "App.Listform.ListformField.AllowWrite", - "en": "Allow Write", - "tr": "Yazmaya İzin Ver" - }, { "resourceName": "Platform", "key": "App.Mcp.Error.NoServerRecord", @@ -28506,6 +28506,12 @@ "en": "The '{0}' tool is disabled on the '{1}' MCP server.", "tr": "'{0}' aracı '{1}' MCP sunucusunda kapalı." }, + { + "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." + }, { "resourceName": "Platform", "key": "App.Mcp.Error.EmptyQuery", @@ -28578,6 +28584,90 @@ "en": "No key field value was given.", "tr": "Anahtar alan değeri verilmedi." }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.DevGuideUnavailable", + "en": "The development guide could not be found on the server. Check the {0} setting.", + "tr": "Geliştirme rehberi sunucuda bulunamadı. {0} ayarını kontrol edin." + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.DevGuideSectionNotFound", + "en": "Section \"{0}\" was not found in the development guide. Call get_dev_guide without a section to see the headings.", + "tr": "Geliştirme rehberinde \"{0}\" bölümü bulunamadı. Başlıkları görmek için get_dev_guide aracını bölüm vermeden çağırın." + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.InvalidWizard", + "en": "The wizard JSON could not be read. Send the seed file format ({\"Wizard\": {...}}) or the flat wizard answers.", + "tr": "Wizard JSON okunamadı. Seed dosyası biçimini ({\"Wizard\": {...}}) ya da düz wizard cevaplarını gönderin." + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.ComponentNameRequired", + "en": "Component name is required.", + "tr": "Bileşen adı zorunludur." + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.ComponentCodeRequired", + "en": "Code is required for a new component.", + "tr": "Yeni bileşen için kod zorunludur." + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.ComponentNotFound", + "en": "Component \"{0}\" was not found.", + "tr": "\"{0}\" bileşeni bulunamadı." + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.VisualDesignerComponent", + "en": "The code of component \"{0}\" is generated from its Visual Designer document and cannot be written over MCP. Edit it in the Visual Designer.", + "tr": "\"{0}\" bileşeninin kodu Visual Designer belgesinden üretiliyor, MCP üzerinden yazılamaz. Bileşeni Visual Designer'da düzenleyin." + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.TableColumnsRequired", + "en": "At least one column is required to create a table.", + "tr": "Tablo oluşturmak için en az bir kolon gerekir." + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.UnsupportedColumnType", + "en": "Unsupported column type: {0}. Supported types: {1}", + "tr": "Desteklenmeyen kolon tipi: {0}. Desteklenen tipler: {1}" + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.InvalidColumnLength", + "en": "The length of column \"{0}\" must be between 1 and {1}.", + "tr": "\"{0}\" kolonunun uzunluğu 1 ile {1} arasında olmalıdır." + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.InvalidDefaultValue", + "en": "Invalid default value for column \"{0}\": {1}. Use a number, a quoted text or NEWID()/GETDATE()/GETUTCDATE()/SYSUTCDATETIME().", + "tr": "\"{0}\" kolonu için geçersiz varsayılan değer: {1}. Sayı, tırnaklı metin ya da NEWID()/GETDATE()/GETUTCDATE()/SYSUTCDATETIME() kullanın." + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.DuplicateColumn", + "en": "Column \"{0}\" is defined more than once. Id, TenantId and audit columns are added automatically.", + "tr": "\"{0}\" kolonu birden fazla kez tanımlanmış. Id, TenantId ve audit kolonları otomatik eklenir." + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.TableAlreadyExists", + "en": "Table \"{0}\" already exists. Change existing tables in the SQL Query Manager table designer.", + "tr": "\"{0}\" tablosu zaten var. Var olan tabloları SQL Query Manager tablo tasarımcısından değiştirin." + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.TableProviderNotSupported", + "en": "Creating tables over MCP is supported only on SQL Server data sources (current: {0}).", + "tr": "MCP üzerinden tablo oluşturma yalnızca SQL Server veri kaynaklarında desteklenir (mevcut: {0})." + }, { "resourceName": "Platform", "key": "App.Mcp.Error.DictionaryNotFound", diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs b/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs index 53dbd282..7790dca8 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs @@ -7969,7 +7969,6 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency }), FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] { new() { FieldName = "IsActive", FieldDbType = DbType.Boolean, Value = "true", CustomValueType = FieldCustomValueTypeEnum.Value }, - new() { FieldName = "AllowWrite", FieldDbType = DbType.Boolean, Value = "false", CustomValueType = FieldCustomValueTypeEnum.Value }, new() { FieldName = "MaxRows", FieldDbType = DbType.Int32, Value = McpConsts.DefaultMaxRows.ToString(), CustomValueType = FieldCustomValueTypeEnum.Value }, }), DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.McpServer)), @@ -8081,40 +8080,19 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency PermissionJson = DefaultFieldPermissionJson(listForm.Name), PivotSettingsJson = DefaultPivotSettingsJson }, - new() { - ListFormCode = listForm.ListFormCode, - CultureName = LanguageCodes.En, - SourceDbType = DbType.String, - FieldName = "DataSourceCode", - EditGroupOrderNo = 1, - EditOrderNo = 5, - EditorType2 = EditorTypes.dxSelectBox, - ColSpan = 1, - CaptionName = "App.Listform.ListformField.DataSourceCode", - Width = 0, - ListOrderNo = 6, - Visible = true, - IsActive = true, - - AllowSearch = true, - LookupJson = LookupQueryValues.DefaultLookupQueryJson(nameof(TableNameEnum.DataSource), "Code", "Code"), - ColumnCustomizationJson = DefaultColumnCustomizationJson, - PermissionJson = DefaultFieldPermissionJson(listForm.Name), - PivotSettingsJson = DefaultPivotSettingsJson - }, new() { ListFormCode = listForm.ListFormCode, CultureName = LanguageCodes.En, SourceDbType = DbType.Int32, FieldName = "MaxRows", EditGroupOrderNo = 1, - EditOrderNo = 6, + EditOrderNo = 5, EditorType2 = EditorTypes.dxNumberBox, EditorOptions = EditorOptions.Number(0), ColSpan = 1, CaptionName = "App.Listform.ListformField.MaxRows", Width = 0, - ListOrderNo = 7, + ListOrderNo = 6, Visible = true, IsActive = true, @@ -8130,12 +8108,12 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency SourceDbType = DbType.String, FieldName = "AllowedTools", EditGroupOrderNo = 1, - EditOrderNo = 7, + EditOrderNo = 6, EditorType2 = EditorTypes.dxTagBox, ColSpan = 2, CaptionName = "App.Listform.ListformField.AllowedTools", Width = 0, - ListOrderNo = 8, + ListOrderNo = 7, Visible = true, IsActive = true, @@ -8153,39 +8131,19 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency PermissionJson = DefaultFieldPermissionJson(listForm.Name), PivotSettingsJson = DefaultPivotSettingsJson }, - new() { - ListFormCode = listForm.ListFormCode, - CultureName = LanguageCodes.En, - SourceDbType = DbType.Boolean, - FieldName = "AllowWrite", - EditGroupOrderNo = 1, - EditOrderNo = 8, - EditorType2 = EditorTypes.dxCheckBox, - ColSpan = 1, - CaptionName = "App.Listform.ListformField.AllowWrite", - Width = 0, - ListOrderNo = 9, - Visible = true, - IsActive = true, - - AllowSearch = true, - ColumnCustomizationJson = DefaultColumnCustomizationJson, - PermissionJson = DefaultFieldPermissionJson(listForm.Name), - PivotSettingsJson = DefaultPivotSettingsJson - }, new() { ListFormCode = listForm.ListFormCode, CultureName = LanguageCodes.En, SourceDbType = DbType.String, FieldName = "Description", EditGroupOrderNo = 1, - EditOrderNo = 9, + EditOrderNo = 7, EditorType2 = EditorTypes.dxTextArea, EditorOptions = EditorOptions.Multiline(60), ColSpan = 2, CaptionName = "App.Listform.ListformField.Description", Width = 0, - ListOrderNo = 10, + ListOrderNo = 8, Visible = false, IsActive = true, diff --git a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs index 76103bc5..6887f2c5 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs @@ -517,7 +517,9 @@ public static class PlatformConsts /// /// MCP sunucu tanimlarinin yetkileri. ekrani gorme /// (okuma) yetkisidir; ise /mcp ucundan arac - /// cagirma yetkisidir ve kiraci kullanicilarina da verilebilir. + /// cagirma yetkisidir ve kiraci kullanicilarina da verilebilir. + /// /mcp ucundan ekran ve bilesen gelistirme araclarini acar; gelistirme islemi yine + /// Wizard ve Custom Components yetkilerinden gecer. /// public static class McpServers { @@ -528,6 +530,7 @@ 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.EntityFrameworkCore/Migrations/20260910132559_Initial.Designer.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260918122519_Initial.Designer.cs similarity index 99% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260910132559_Initial.Designer.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260918122519_Initial.Designer.cs index ecb9f20e..c1185765 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260910132559_Initial.Designer.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260918122519_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace Sozsoft.Platform.Migrations { [DbContext(typeof(PlatformDbContext))] - [Migration("20260910132559_Initial")] + [Migration("20260918122519_Initial")] partial class Initial { /// @@ -443,11 +443,6 @@ namespace Sozsoft.Platform.Migrations .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); - b.Property("AllowWrite") - .ValueGeneratedOnAdd() - .HasColumnType("bit") - .HasDefaultValue(false); - b.Property("AllowedTools") .HasMaxLength(1024) .HasColumnType("nvarchar(1024)"); @@ -460,10 +455,6 @@ namespace Sozsoft.Platform.Migrations .HasColumnType("uniqueidentifier") .HasColumnName("CreatorId"); - b.Property("DataSourceCode") - .HasMaxLength(64) - .HasColumnType("nvarchar(64)"); - b.Property("DeleterId") .HasColumnType("uniqueidentifier") .HasColumnName("DeleterId"); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260910132559_Initial.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260918122519_Initial.cs similarity index 99% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260910132559_Initial.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260918122519_Initial.cs index 2da1203b..f79e372c 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260910132559_Initial.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260918122519_Initial.cs @@ -1500,10 +1500,8 @@ namespace Sozsoft.Platform.Migrations IsActive = table.Column(type: "bit", nullable: false), Url = table.Column(type: "nvarchar(512)", maxLength: 512, nullable: true), Tenants = table.Column(type: "nvarchar(2048)", maxLength: 2048, nullable: true), - DataSourceCode = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: true), AllowedTools = table.Column(type: "nvarchar(1024)", maxLength: 1024, nullable: true), MaxRows = table.Column(type: "int", nullable: false, defaultValue: 200), - AllowWrite = table.Column(type: "bit", nullable: false, defaultValue: false), Description = table.Column(type: "nvarchar(1024)", maxLength: 1024, nullable: true), CreationTime = table.Column(type: "datetime2", nullable: false), CreatorId = table.Column(type: "uniqueidentifier", nullable: true), diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs index 3dafbca9..468c4476 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs @@ -440,11 +440,6 @@ namespace Sozsoft.Platform.Migrations .ValueGeneratedOnAdd() .HasColumnType("uniqueidentifier"); - b.Property("AllowWrite") - .ValueGeneratedOnAdd() - .HasColumnType("bit") - .HasDefaultValue(false); - b.Property("AllowedTools") .HasMaxLength(1024) .HasColumnType("nvarchar(1024)"); @@ -457,10 +452,6 @@ namespace Sozsoft.Platform.Migrations .HasColumnType("uniqueidentifier") .HasColumnName("CreatorId"); - b.Property("DataSourceCode") - .HasMaxLength(64) - .HasColumnType("nvarchar(64)"); - b.Property("DeleterId") .HasColumnType("uniqueidentifier") .HasColumnName("DeleterId"); diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/appsettings.Dev.json b/api/src/Sozsoft.Platform.HttpApi.Host/appsettings.Dev.json index 18bcb4ee..92f824e2 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/appsettings.Dev.json +++ b/api/src/Sozsoft.Platform.HttpApi.Host/appsettings.Dev.json @@ -8,6 +8,7 @@ "CdnUrl": "https://dev-cdn.sozsoft.com", "CdnPath": "/etc/api/cdn", "SeedsPath": "/etc/api/seeds", + "DevGuidePath": "/etc/api/dev-guide", "BaseDomain": "sozsoft.com", "BackupPath": "/var/opt/mssql/backup" }, diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/appsettings.Production.json b/api/src/Sozsoft.Platform.HttpApi.Host/appsettings.Production.json index 83939862..b0f06cf5 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/appsettings.Production.json +++ b/api/src/Sozsoft.Platform.HttpApi.Host/appsettings.Production.json @@ -8,6 +8,7 @@ "CdnUrl": "https://cdn.sozsoft.com", "CdnPath": "/etc/api/cdn", "SeedsPath": "/etc/api/seeds", + "DevGuidePath": "/etc/api/dev-guide", "BaseDomain": "sozsoft.com", "BackupPath": "/var/opt/mssql/backup" }, diff --git a/claude.md b/claude.md index d30b0902..935aca81 100644 --- a/claude.md +++ b/claude.md @@ -186,8 +186,14 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` → ilgili `Mcp*Tools` sınıfı (`Sozsoft.Mcp.HttpApi`) ve arkasındaki AppService sözleşmesi (`Sozsoft.Mcp.Application.Contracts`). Arac adı `McpToolNames.All` içinde yoksa araç listesi 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. Yazma araçları - `WriteTools` listesine de eklenir, yoksa `AllowWrite` kapalıyken de yayınlanırlar. + `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. 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. - **MCP şema araçları** → tablo/sütun bilgisi canlı katalogdan değil `Sas_H_DbTableColumn` @@ -204,7 +210,9 @@ 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. + karşılık gelen yetkiyle eklenir. `create_table` bu yüzden `.Develop`'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. - **Yeni bir dil anahtarı** → `LanguagesData.json`; eklemeden önce anahtarın zaten var olup olmadığını kontrol et. - **Custom component sınıfları** → bileşen kodu veritabanında yaşar, Tailwind derlemede yalnızca diff --git a/configs/deployment/docker-compose-app.yml b/configs/deployment/docker-compose-app.yml index 4f6e575f..ac2b8f3d 100644 --- a/configs/deployment/docker-compose-app.yml +++ b/configs/deployment/docker-compose-app.yml @@ -56,6 +56,9 @@ services: # buradan okunur (ChangeLogStartupService). Boylece yeni tag API imajini # yeniden derletmez. - ../change-logs:/etc/api/change-logs:ro + # MCP get_dev_guide araci uretim sozlesmesini depodaki lowcode*.instructions.md + # dosyalarindan okur (App:DevGuidePath); rehber guncellendiginde imaj derlenmez. + - ../../.github/instructions:/etc/api/dev-guide:ro networks: - db - default diff --git a/configs/seeds/host/data/Abp.Identity.Permissions.json b/configs/seeds/host/data/Abp.Identity.Permissions.json index 2da15d2a..21e01f2c 100644 --- a/configs/seeds/host/data/Abp.Identity.Permissions.json +++ b/configs/seeds/host/data/Abp.Identity.Permissions.json @@ -2569,6 +2569,15 @@ "MultiTenancySide": 3, "Modules": "Sas|Adm" }, + { + "GroupName": "App.Saas", + "Name": "App.DeveloperKit.McpServers.Develop", + "ParentName": "App.DeveloperKit.McpServers", + "DisplayName": "App.DeveloperKit.McpServers.Develop", + "IsEnabled": true, + "MultiTenancySide": 2, + "Modules": "Sas|Adm" + }, { "GroupName": "App.Saas", "Name": "App.Listforms.Listform", diff --git a/configs/seeds/host/data/App.DeveloperKit.McpServers.json b/configs/seeds/host/data/App.DeveloperKit.McpServers.json index 5d7df705..9b8c657e 100644 --- a/configs/seeds/host/data/App.DeveloperKit.McpServers.json +++ b/configs/seeds/host/data/App.DeveloperKit.McpServers.json @@ -10,11 +10,9 @@ "IsActive": true, "Url": "https://api.sozsoft.com/mcp", "Tenants": null, - "DataSourceCode": null, "AllowedTools": null, "MaxRows": 200, - "AllowWrite": false, - "Description": "Default read-only MCP endpoint. Tools are gated by the App.DeveloperKit.McpServers.Invoke permission; turn AllowWrite on only for tenants that should write data through MCP." + "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)." } ] }