diff --git a/.github/instructions/lowcode-reference.instructions.md b/.github/instructions/lowcode-reference.instructions.md index 934c4260..98437ada 100644 --- a/.github/instructions/lowcode-reference.instructions.md +++ b/.github/instructions/lowcode-reference.instructions.md @@ -1189,6 +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. | | **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ı. | diff --git a/README.md b/README.md index 0345493a..8d203b9a 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,8 @@ sozsoft-platform/ │ │ ├── Sozsoft.Notifications/ # Bildirim tipleri, kuralları, kullanıcı bildirimleri │ │ ├── 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.SqlQueryManager/ # SQL nesne yönetimi ve sorgu çalıştırma +│ │ └── Sozsoft.Mcp/ # MCP sunucusu: /mcp ucu, şema keşfi ve sorgu araçları │ └── test/ # Test projeleri ├── ui/ # React uygulaması │ └── src/ @@ -930,6 +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. | ### 10.2. Uygulama servisleri (`api/src/Sozsoft.Platform.Application`) @@ -1150,6 +1152,111 @@ kuyruğu arka plan işçisiyle çalıştırır ve sonuçları günlükler. Gemini sohbet modeli bağlanır (`configs/ai/Chat.json`). UI tarafında `/admin/ai` ve header'daki asistan bileşeni bu uç noktayı kullanır. +### 13.6. MCP sunucusu + +Yapay zekâ istemcileri (Claude Code, Claude Desktop, ChatGPT) platforma **`/mcp`** ucundan +bağlanır. Uç, API imajının içinde aynı portta (`8080`) yayınlanır; ayrı bir container yoktur. +Modül `api/modules/Sozsoft.Mcp` altında standart ABP katmanlarıyla durur. + +**Kimlik ve kiracı.** Kimlik doğrulama mevcut OpenIddict bearer token'ıdır. Uç, +`UseAuthentication` → `UseAbpOpenIddictValidation` → `UseMultiTenancy` → `UseUnitOfWork` +ardışık düzeninden sonra bağlanır ve açıkça bearer şemasına bağlıdır (token'sız istek giriş +sayfasına 302 ile değil, 401 ile döner); kiracı token talebinden ya da `__tenant` başlığından +çözülür. Taşıyıcı **durumsuz** kiptedir +(`HttpServerSessionMode.Stateless`): her istek kendi ABP kapsamında koşar, ters vekil +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 +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. + +**Araçlar.** + +| Araç | İş | +| --- | --- | +| `server_info` | Bağlantının kiracısı, açık araçlar, veri sözlüğünün tazelik bilgisi. Her zaman açıktır. | +| `list_lists` | Listeler: kod, başlık, açıklama, arkasındaki tablo. | +| `describe_list` | Bir listenin sütunları: alan adı, görünen başlık, SQL tipi, lookup ilişkisi, `isSensitive`. | +| `search_schema` | Liste ve sütun adları/başlıkları üzerinde aksan ve büyük/küçük harf duyarsız arama. | +| `query_list` | Listenin verisini ekranın kendi sorgusuyla (`GetSelectAsync`) okur: filtre, sıralama, gruplama, özet (DevExtreme biçimi). Normal kullanıcının veri okuma yolu. | +| `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. | + +**Yetki modeli: MCP yetki vermez.** Kullanıcı MCP'de uygulamadaki yetkisini taşır: +- Şema araçları yalnızca kullanıcının okuma yetkisi olan listeleri gösterir (`McpListFormAccessFilter` + — ListForm `PermissionJson.R`, ekranla aynı karar; tek sorgu + toplu yetki kontrolü). +- `query_list` ekranın sorgusundan geçer: liste ve alan (`CanRead`) yetkileri, kiracı/şube + filtreleri, kullanıcı özelleştirmeleri uygulamadaki gibidir. +- Ham SQL (`run_select`, veritabanı nesneleri) liste yetkilerini aşar; yalnızca uygulamada + `App.SqlQueryManager` yetkisi olana açıktır. Yazma araçları `.Write` + ekranın C/U/D yetkisini ister. +- 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. + +**Ş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 +kullanıcının ekranda gördüğü adlarla aynı olur. Sözlük +`dbo.Sas_H_ListFormFieldsDictionaryJson_SaveDbTableColumns` prosedürüyle kiracı adı başına +üretilir; sözlük yoksa araçlar tablo döndürmez ve `server_info` bunu bildirir. + +**OAuth (Claude / ChatGPT connector'ları).** Connector formuna yalnızca ad ve URL girilir; +kimlik doğrulamayı istemci MCP OAuth akışıyla kendisi yürütür: + +1. `/mcp` token'sız isteğe 401 + `WWW-Authenticate: Bearer resource_metadata="…"` döner + (`McpAuthenticationChallengeMiddleware`). +2. İstemci `/.well-known/oauth-protected-resource/mcp` (RFC 9728) belgesinden yetkilendirme + sunucusunu (`AuthServer:Authority`) öğrenir, `/.well-known/oauth-authorization-server` + (OpenIddict keşif belgesi, `registration_endpoint` eklenmiş) belgesini okur. +3. `/connect/register` (RFC 7591, `McpOAuthClientManager`): + - İstenen geri dönüş adresleri **bilinen istemcide** (`Platform_Mcp`; Claude, ChatGPT — + `DbMigrator` ayarı `OpenIddict:Applications:Platform_Mcp:RedirectUris`) kayıtlıysa onun + kimliği döner; onay ekranı çıkmaz. + - Değilse (Cursor, VS Code, Claude Code, başka bir AI istemcisi) **dinamik istemci** üretilir: + `mcp_` önekli, PKCE zorunlu, **açık onaylı** OpenIddict uygulaması. Aynı adres kümesi aynı + uygulamaya bağlanır; tavan `McpConsts.MaxDynamicClients`. Kabul edilen adresler: HTTPS, + localhost HTTP (RFC 8252), uygulama şemaları (`cursor://…`); `javascript:` vb. reddedilir. + Onay ekranındaki ad istemcinin beyanı + geri dönüş adresinin sahibidir (ad doğrulanamaz). +4. Kullanıcı platformun giriş ekranına (`/Account/Login`, kiracı seçimi dahil) yönlenir; + authorization code + PKCE ile token alınır. Kiracı ve yetki giriş yapan kullanıcıdan gelir. + +Üç ayrıntı bilinçlidir: +- Yayıncı (`issuer`) `AuthServer:Authority`'den sabitlenir; ters vekil arkasında istekten + türetilirse `http://` olur ve istemci keşif belgesini reddeder. +- MCP istemcisinin gönderdiği RFC 8707 `resource` parametresi yok sayılır + (`McpOAuthServerConfiguration`): OpenIddict kayıtsız kaynağı reddeder ya da token kitlesini + `/mcp` adresine daraltır, API ise `Platform` kitlesini doğrular. +- MCP istemcilerinin (`McpConsts.IsMcpClient`) token'ları kullanıcı oturumuna bağlanmaz + (`PlatformTokenController`). Tek oturum kuralı her girişte diğer oturumları sildiği için, + bağlansaydı connector her token yenilemesinde kullanıcının web oturumunu düşürürdü. +- MCP istemcilerinin yenileme token ömrü uygulama bazında 30 gündür + (`McpConsts.RefreshTokenLifetime`, OpenIddict uygulama ayarı) ve her kullanımda yenilenir; + web istemcisinin dakikalık ömrü etkilenmez. Kullanıcı 30 gün hiç soru sormazsa bağlantıyı + yeniden kurar. + +Bağlantı sunucu gerektirir: connector formu bağlantıyı istemci sağlayıcının sunucularından +kurar, `localhost` erişilemez. Forma `https://api.sozsoft.com/mcp` yazılır. + +**Bağlanma (token ile, CLI).** + +```bash +# sunucu +claude mcp add sozsoft --transport http https://api.sozsoft.com/mcp \n --header "Authorization: Bearer " + +# localhost (dotnet run / IIS Express) +claude mcp add sozsoft-local --transport http https://localhost:44344/mcp \n --header "Authorization: Bearer " +``` + +Ters vekilde `/mcp` ayrı bir blokta karşılanır (`configs/deployment/configs/nginx.conf`): +Streamable HTTP yanıtları SSE akışıdır, `location /` altındaki tamponlama akışı bekletir. + +**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. + --- ## 14. Arka Plan İşleri diff --git a/api/Sozsoft.Platform.DbMigrator.Dockerfile b/api/Sozsoft.Platform.DbMigrator.Dockerfile index 7f35c67d..4bcdce82 100644 --- a/api/Sozsoft.Platform.DbMigrator.Dockerfile +++ b/api/Sozsoft.Platform.DbMigrator.Dockerfile @@ -5,6 +5,7 @@ WORKDIR /app COPY global.json ./ COPY common.props ./ COPY "modules/Sozsoft.Languages/common.props" "modules/Sozsoft.Languages/" +COPY "modules/Sozsoft.Mcp/common.props" "modules/Sozsoft.Mcp/" COPY "modules/Sozsoft.Notifications/common.props" "modules/Sozsoft.Notifications/" COPY "modules/Sozsoft.Settings/common.props" "modules/Sozsoft.Settings/" COPY "modules/Sozsoft.SqlQueryManager/common.props" "modules/Sozsoft.SqlQueryManager/" @@ -13,6 +14,12 @@ COPY "modules/Sozsoft.Languages/Sozsoft.Languages.Domain/Sozsoft.Languages.Domai COPY "modules/Sozsoft.Languages/Sozsoft.Languages.Domain.Shared/Sozsoft.Languages.Domain.Shared.csproj" "modules/Sozsoft.Languages/Sozsoft.Languages.Domain.Shared/" COPY "modules/Sozsoft.Languages/Sozsoft.Languages.EntityFrameworkCore/Sozsoft.Languages.EntityFrameworkCore.csproj" "modules/Sozsoft.Languages/Sozsoft.Languages.EntityFrameworkCore/" COPY "modules/Sozsoft.MailQueue/Sozsoft.MailQueue.csproj" "modules/Sozsoft.MailQueue/" +COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/Sozsoft.Mcp.Application.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/" +COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Sozsoft.Mcp.Application.Contracts.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/" +COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Sozsoft.Mcp.Domain.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/" +COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/Sozsoft.Mcp.Domain.Shared.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/" +COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/Sozsoft.Mcp.EntityFrameworkCore.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/" +COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Sozsoft.Mcp.HttpApi.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/" COPY "modules/Sozsoft.Notifications/Sozsoft.Notifications.Application.Contracts/Sozsoft.Notifications.Application.Contracts.csproj" "modules/Sozsoft.Notifications/Sozsoft.Notifications.Application.Contracts/" COPY "modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain/Sozsoft.Notifications.Domain.csproj" "modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain/" COPY "modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain.Shared/Sozsoft.Notifications.Domain.Shared.csproj" "modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain.Shared/" diff --git a/api/Sozsoft.Platform.HttpApi.Host.Dockerfile b/api/Sozsoft.Platform.HttpApi.Host.Dockerfile index 079974c7..ebd945a4 100644 --- a/api/Sozsoft.Platform.HttpApi.Host.Dockerfile +++ b/api/Sozsoft.Platform.HttpApi.Host.Dockerfile @@ -7,6 +7,7 @@ RUN apk add nodejs~=24 npm~=11 COPY global.json ./ COPY common.props ./ COPY "modules/Sozsoft.Languages/common.props" "modules/Sozsoft.Languages/" +COPY "modules/Sozsoft.Mcp/common.props" "modules/Sozsoft.Mcp/" COPY "modules/Sozsoft.Notifications/common.props" "modules/Sozsoft.Notifications/" COPY "modules/Sozsoft.Settings/common.props" "modules/Sozsoft.Settings/" COPY "modules/Sozsoft.SqlQueryManager/common.props" "modules/Sozsoft.SqlQueryManager/" @@ -26,6 +27,12 @@ COPY "modules/Sozsoft.Languages/Sozsoft.Languages.Domain/Sozsoft.Languages.Domai COPY "modules/Sozsoft.Languages/Sozsoft.Languages.Domain.Shared/Sozsoft.Languages.Domain.Shared.csproj" "modules/Sozsoft.Languages/Sozsoft.Languages.Domain.Shared/" COPY "modules/Sozsoft.Languages/Sozsoft.Languages.EntityFrameworkCore/Sozsoft.Languages.EntityFrameworkCore.csproj" "modules/Sozsoft.Languages/Sozsoft.Languages.EntityFrameworkCore/" COPY "modules/Sozsoft.MailQueue/Sozsoft.MailQueue.csproj" "modules/Sozsoft.MailQueue/" +COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/Sozsoft.Mcp.Application.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/" +COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Sozsoft.Mcp.Application.Contracts.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/" +COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Sozsoft.Mcp.Domain.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/" +COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/Sozsoft.Mcp.Domain.Shared.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/" +COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/Sozsoft.Mcp.EntityFrameworkCore.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/" +COPY "modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Sozsoft.Mcp.HttpApi.csproj" "modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/" COPY "modules/Sozsoft.Notifications/Sozsoft.Notifications.Application/Sozsoft.Notifications.Application.csproj" "modules/Sozsoft.Notifications/Sozsoft.Notifications.Application/" COPY "modules/Sozsoft.Notifications/Sozsoft.Notifications.Application.Contracts/Sozsoft.Notifications.Application.Contracts.csproj" "modules/Sozsoft.Notifications/Sozsoft.Notifications.Application.Contracts/" COPY "modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain/Sozsoft.Notifications.Domain.csproj" "modules/Sozsoft.Notifications/Sozsoft.Notifications.Domain/" diff --git a/api/Sozsoft.Platform.sln b/api/Sozsoft.Platform.sln index adab7fd9..ec6225ab 100644 --- a/api/Sozsoft.Platform.sln +++ b/api/Sozsoft.Platform.sln @@ -92,144 +92,496 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sozsoft.SqlQueryManager.App EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sozsoft.SqlQueryManager.EntityFrameworkCore", "modules\Sozsoft.SqlQueryManager\Sozsoft.SqlQueryManager.EntityFrameworkCore\Sozsoft.SqlQueryManager.EntityFrameworkCore.csproj", "{1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sozsoft.Mcp.Domain.Shared", "modules\Sozsoft.Mcp\Sozsoft.Mcp.Domain.Shared\Sozsoft.Mcp.Domain.Shared.csproj", "{D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sozsoft.Mcp.Domain", "modules\Sozsoft.Mcp\Sozsoft.Mcp.Domain\Sozsoft.Mcp.Domain.csproj", "{E99A894F-1731-4A48-B345-1D14F70EA23F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sozsoft.Mcp.Application.Contracts", "modules\Sozsoft.Mcp\Sozsoft.Mcp.Application.Contracts\Sozsoft.Mcp.Application.Contracts.csproj", "{30F4D3DF-D777-40C2-B434-49173AC06A4B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sozsoft.Mcp.Application", "modules\Sozsoft.Mcp\Sozsoft.Mcp.Application\Sozsoft.Mcp.Application.csproj", "{A4BC1880-5492-41FF-81BA-963AC4C60A02}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sozsoft.Mcp.EntityFrameworkCore", "modules\Sozsoft.Mcp\Sozsoft.Mcp.EntityFrameworkCore\Sozsoft.Mcp.EntityFrameworkCore.csproj", "{BE7FDBA3-58CD-41C0-A748-E636DD11F137}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sozsoft.Mcp.HttpApi", "modules\Sozsoft.Mcp\Sozsoft.Mcp.HttpApi\Sozsoft.Mcp.HttpApi.csproj", "{594F56D4-CDDD-451A-B487-D49EEB9F5B98}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Debug|x64.ActiveCfg = Debug|Any CPU + {554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Debug|x64.Build.0 = Debug|Any CPU + {554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Debug|x86.ActiveCfg = Debug|Any CPU + {554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Debug|x86.Build.0 = Debug|Any CPU {554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Release|Any CPU.ActiveCfg = Release|Any CPU {554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Release|Any CPU.Build.0 = Release|Any CPU + {554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Release|x64.ActiveCfg = Release|Any CPU + {554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Release|x64.Build.0 = Release|Any CPU + {554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Release|x86.ActiveCfg = Release|Any CPU + {554AD327-6DBA-4F8F-96F8-81CE7A0C863F}.Release|x86.Build.0 = Release|Any CPU {1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Debug|x64.ActiveCfg = Debug|Any CPU + {1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Debug|x64.Build.0 = Debug|Any CPU + {1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Debug|x86.ActiveCfg = Debug|Any CPU + {1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Debug|x86.Build.0 = Debug|Any CPU {1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Release|Any CPU.ActiveCfg = Release|Any CPU {1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Release|Any CPU.Build.0 = Release|Any CPU + {1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Release|x64.ActiveCfg = Release|Any CPU + {1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Release|x64.Build.0 = Release|Any CPU + {1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Release|x86.ActiveCfg = Release|Any CPU + {1A94A50E-06DC-43C1-80B5-B662820EC3EB}.Release|x86.Build.0 = Release|Any CPU {C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Debug|x64.ActiveCfg = Debug|Any CPU + {C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Debug|x64.Build.0 = Debug|Any CPU + {C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Debug|x86.ActiveCfg = Debug|Any CPU + {C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Debug|x86.Build.0 = Debug|Any CPU {C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Release|Any CPU.ActiveCfg = Release|Any CPU {C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Release|Any CPU.Build.0 = Release|Any CPU + {C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Release|x64.ActiveCfg = Release|Any CPU + {C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Release|x64.Build.0 = Release|Any CPU + {C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Release|x86.ActiveCfg = Release|Any CPU + {C956DD76-69C8-4A9C-83EA-D17DF83340FD}.Release|x86.Build.0 = Release|Any CPU {42F719ED-8413-4895-B5B4-5AB56079BC66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {42F719ED-8413-4895-B5B4-5AB56079BC66}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42F719ED-8413-4895-B5B4-5AB56079BC66}.Debug|x64.ActiveCfg = Debug|Any CPU + {42F719ED-8413-4895-B5B4-5AB56079BC66}.Debug|x64.Build.0 = Debug|Any CPU + {42F719ED-8413-4895-B5B4-5AB56079BC66}.Debug|x86.ActiveCfg = Debug|Any CPU + {42F719ED-8413-4895-B5B4-5AB56079BC66}.Debug|x86.Build.0 = Debug|Any CPU {42F719ED-8413-4895-B5B4-5AB56079BC66}.Release|Any CPU.ActiveCfg = Release|Any CPU {42F719ED-8413-4895-B5B4-5AB56079BC66}.Release|Any CPU.Build.0 = Release|Any CPU + {42F719ED-8413-4895-B5B4-5AB56079BC66}.Release|x64.ActiveCfg = Release|Any CPU + {42F719ED-8413-4895-B5B4-5AB56079BC66}.Release|x64.Build.0 = Release|Any CPU + {42F719ED-8413-4895-B5B4-5AB56079BC66}.Release|x86.ActiveCfg = Release|Any CPU + {42F719ED-8413-4895-B5B4-5AB56079BC66}.Release|x86.Build.0 = Release|Any CPU {520659C8-C734-4298-A3DA-B539DB9DFC0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {520659C8-C734-4298-A3DA-B539DB9DFC0B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {520659C8-C734-4298-A3DA-B539DB9DFC0B}.Debug|x64.ActiveCfg = Debug|Any CPU + {520659C8-C734-4298-A3DA-B539DB9DFC0B}.Debug|x64.Build.0 = Debug|Any CPU + {520659C8-C734-4298-A3DA-B539DB9DFC0B}.Debug|x86.ActiveCfg = Debug|Any CPU + {520659C8-C734-4298-A3DA-B539DB9DFC0B}.Debug|x86.Build.0 = Debug|Any CPU {520659C8-C734-4298-A3DA-B539DB9DFC0B}.Release|Any CPU.ActiveCfg = Release|Any CPU {520659C8-C734-4298-A3DA-B539DB9DFC0B}.Release|Any CPU.Build.0 = Release|Any CPU + {520659C8-C734-4298-A3DA-B539DB9DFC0B}.Release|x64.ActiveCfg = Release|Any CPU + {520659C8-C734-4298-A3DA-B539DB9DFC0B}.Release|x64.Build.0 = Release|Any CPU + {520659C8-C734-4298-A3DA-B539DB9DFC0B}.Release|x86.ActiveCfg = Release|Any CPU + {520659C8-C734-4298-A3DA-B539DB9DFC0B}.Release|x86.Build.0 = Release|Any CPU {4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Debug|x64.ActiveCfg = Debug|Any CPU + {4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Debug|x64.Build.0 = Debug|Any CPU + {4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Debug|x86.ActiveCfg = Debug|Any CPU + {4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Debug|x86.Build.0 = Debug|Any CPU {4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Release|Any CPU.ActiveCfg = Release|Any CPU {4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Release|Any CPU.Build.0 = Release|Any CPU + {4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Release|x64.ActiveCfg = Release|Any CPU + {4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Release|x64.Build.0 = Release|Any CPU + {4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Release|x86.ActiveCfg = Release|Any CPU + {4164BDF7-F527-4E85-9CE6-E3C2D7426A27}.Release|x86.Build.0 = Release|Any CPU {3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Debug|x64.ActiveCfg = Debug|Any CPU + {3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Debug|x64.Build.0 = Debug|Any CPU + {3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Debug|x86.ActiveCfg = Debug|Any CPU + {3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Debug|x86.Build.0 = Debug|Any CPU {3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Release|Any CPU.ActiveCfg = Release|Any CPU {3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Release|Any CPU.Build.0 = Release|Any CPU + {3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Release|x64.ActiveCfg = Release|Any CPU + {3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Release|x64.Build.0 = Release|Any CPU + {3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Release|x86.ActiveCfg = Release|Any CPU + {3B5A0094-670D-4BB1-BFDD-61B88A8773DC}.Release|x86.Build.0 = Release|Any CPU {AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Debug|x64.ActiveCfg = Debug|Any CPU + {AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Debug|x64.Build.0 = Debug|Any CPU + {AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Debug|x86.ActiveCfg = Debug|Any CPU + {AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Debug|x86.Build.0 = Debug|Any CPU {AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Release|Any CPU.ActiveCfg = Release|Any CPU {AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Release|Any CPU.Build.0 = Release|Any CPU + {AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Release|x64.ActiveCfg = Release|Any CPU + {AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Release|x64.Build.0 = Release|Any CPU + {AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Release|x86.ActiveCfg = Release|Any CPU + {AA94D832-1CCC-4715-95A9-A483F23A1A5D}.Release|x86.Build.0 = Release|Any CPU {748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Debug|x64.ActiveCfg = Debug|Any CPU + {748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Debug|x64.Build.0 = Debug|Any CPU + {748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Debug|x86.ActiveCfg = Debug|Any CPU + {748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Debug|x86.Build.0 = Debug|Any CPU {748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Release|Any CPU.ActiveCfg = Release|Any CPU {748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Release|Any CPU.Build.0 = Release|Any CPU + {748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Release|x64.ActiveCfg = Release|Any CPU + {748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Release|x64.Build.0 = Release|Any CPU + {748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Release|x86.ActiveCfg = Release|Any CPU + {748584B1-BA69-4F6A-81AA-F4BDE6BCE29D}.Release|x86.Build.0 = Release|Any CPU {21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Debug|x64.ActiveCfg = Debug|Any CPU + {21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Debug|x64.Build.0 = Debug|Any CPU + {21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Debug|x86.ActiveCfg = Debug|Any CPU + {21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Debug|x86.Build.0 = Debug|Any CPU {21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Release|Any CPU.ActiveCfg = Release|Any CPU {21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Release|Any CPU.Build.0 = Release|Any CPU + {21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Release|x64.ActiveCfg = Release|Any CPU + {21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Release|x64.Build.0 = Release|Any CPU + {21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Release|x86.ActiveCfg = Release|Any CPU + {21B52B6A-FC09-4EDA-8A73-9516726BF50B}.Release|x86.Build.0 = Release|Any CPU {E9D256F2-B12F-4A90-8CF6-B52724716A94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E9D256F2-B12F-4A90-8CF6-B52724716A94}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9D256F2-B12F-4A90-8CF6-B52724716A94}.Debug|x64.ActiveCfg = Debug|Any CPU + {E9D256F2-B12F-4A90-8CF6-B52724716A94}.Debug|x64.Build.0 = Debug|Any CPU + {E9D256F2-B12F-4A90-8CF6-B52724716A94}.Debug|x86.ActiveCfg = Debug|Any CPU + {E9D256F2-B12F-4A90-8CF6-B52724716A94}.Debug|x86.Build.0 = Debug|Any CPU {E9D256F2-B12F-4A90-8CF6-B52724716A94}.Release|Any CPU.ActiveCfg = Release|Any CPU {E9D256F2-B12F-4A90-8CF6-B52724716A94}.Release|Any CPU.Build.0 = Release|Any CPU + {E9D256F2-B12F-4A90-8CF6-B52724716A94}.Release|x64.ActiveCfg = Release|Any CPU + {E9D256F2-B12F-4A90-8CF6-B52724716A94}.Release|x64.Build.0 = Release|Any CPU + {E9D256F2-B12F-4A90-8CF6-B52724716A94}.Release|x86.ActiveCfg = Release|Any CPU + {E9D256F2-B12F-4A90-8CF6-B52724716A94}.Release|x86.Build.0 = Release|Any CPU {F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Debug|x64.ActiveCfg = Debug|Any CPU + {F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Debug|x64.Build.0 = Debug|Any CPU + {F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Debug|x86.ActiveCfg = Debug|Any CPU + {F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Debug|x86.Build.0 = Debug|Any CPU {F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Release|Any CPU.ActiveCfg = Release|Any CPU {F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Release|Any CPU.Build.0 = Release|Any CPU + {F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Release|x64.ActiveCfg = Release|Any CPU + {F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Release|x64.Build.0 = Release|Any CPU + {F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Release|x86.ActiveCfg = Release|Any CPU + {F1EC497F-D361-4927-AFE6-156A0F7D8A1C}.Release|x86.Build.0 = Release|Any CPU {9020ECD8-81D6-4604-A379-260796918C28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9020ECD8-81D6-4604-A379-260796918C28}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9020ECD8-81D6-4604-A379-260796918C28}.Debug|x64.ActiveCfg = Debug|Any CPU + {9020ECD8-81D6-4604-A379-260796918C28}.Debug|x64.Build.0 = Debug|Any CPU + {9020ECD8-81D6-4604-A379-260796918C28}.Debug|x86.ActiveCfg = Debug|Any CPU + {9020ECD8-81D6-4604-A379-260796918C28}.Debug|x86.Build.0 = Debug|Any CPU {9020ECD8-81D6-4604-A379-260796918C28}.Release|Any CPU.ActiveCfg = Release|Any CPU {9020ECD8-81D6-4604-A379-260796918C28}.Release|Any CPU.Build.0 = Release|Any CPU + {9020ECD8-81D6-4604-A379-260796918C28}.Release|x64.ActiveCfg = Release|Any CPU + {9020ECD8-81D6-4604-A379-260796918C28}.Release|x64.Build.0 = Release|Any CPU + {9020ECD8-81D6-4604-A379-260796918C28}.Release|x86.ActiveCfg = Release|Any CPU + {9020ECD8-81D6-4604-A379-260796918C28}.Release|x86.Build.0 = Release|Any CPU {A3933E06-9DFE-480D-85C4-66F28F076164}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A3933E06-9DFE-480D-85C4-66F28F076164}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A3933E06-9DFE-480D-85C4-66F28F076164}.Debug|x64.ActiveCfg = Debug|Any CPU + {A3933E06-9DFE-480D-85C4-66F28F076164}.Debug|x64.Build.0 = Debug|Any CPU + {A3933E06-9DFE-480D-85C4-66F28F076164}.Debug|x86.ActiveCfg = Debug|Any CPU + {A3933E06-9DFE-480D-85C4-66F28F076164}.Debug|x86.Build.0 = Debug|Any CPU {A3933E06-9DFE-480D-85C4-66F28F076164}.Release|Any CPU.ActiveCfg = Release|Any CPU {A3933E06-9DFE-480D-85C4-66F28F076164}.Release|Any CPU.Build.0 = Release|Any CPU + {A3933E06-9DFE-480D-85C4-66F28F076164}.Release|x64.ActiveCfg = Release|Any CPU + {A3933E06-9DFE-480D-85C4-66F28F076164}.Release|x64.Build.0 = Release|Any CPU + {A3933E06-9DFE-480D-85C4-66F28F076164}.Release|x86.ActiveCfg = Release|Any CPU + {A3933E06-9DFE-480D-85C4-66F28F076164}.Release|x86.Build.0 = Release|Any CPU {556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Debug|x64.ActiveCfg = Debug|Any CPU + {556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Debug|x64.Build.0 = Debug|Any CPU + {556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Debug|x86.ActiveCfg = Debug|Any CPU + {556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Debug|x86.Build.0 = Debug|Any CPU {556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Release|Any CPU.ActiveCfg = Release|Any CPU {556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Release|Any CPU.Build.0 = Release|Any CPU + {556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Release|x64.ActiveCfg = Release|Any CPU + {556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Release|x64.Build.0 = Release|Any CPU + {556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Release|x86.ActiveCfg = Release|Any CPU + {556AC2C2-9765-4B3D-8F38-9EAF3DA61AC5}.Release|x86.Build.0 = Release|Any CPU {5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Debug|x64.ActiveCfg = Debug|Any CPU + {5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Debug|x64.Build.0 = Debug|Any CPU + {5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Debug|x86.ActiveCfg = Debug|Any CPU + {5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Debug|x86.Build.0 = Debug|Any CPU {5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Release|Any CPU.ActiveCfg = Release|Any CPU {5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Release|Any CPU.Build.0 = Release|Any CPU + {5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Release|x64.ActiveCfg = Release|Any CPU + {5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Release|x64.Build.0 = Release|Any CPU + {5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Release|x86.ActiveCfg = Release|Any CPU + {5F288ACC-A9CF-470B-BB68-B1C8B102CDDD}.Release|x86.Build.0 = Release|Any CPU {A926F04F-631B-4A05-853C-6C76555BF26A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A926F04F-631B-4A05-853C-6C76555BF26A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A926F04F-631B-4A05-853C-6C76555BF26A}.Debug|x64.ActiveCfg = Debug|Any CPU + {A926F04F-631B-4A05-853C-6C76555BF26A}.Debug|x64.Build.0 = Debug|Any CPU + {A926F04F-631B-4A05-853C-6C76555BF26A}.Debug|x86.ActiveCfg = Debug|Any CPU + {A926F04F-631B-4A05-853C-6C76555BF26A}.Debug|x86.Build.0 = Debug|Any CPU {A926F04F-631B-4A05-853C-6C76555BF26A}.Release|Any CPU.ActiveCfg = Release|Any CPU {A926F04F-631B-4A05-853C-6C76555BF26A}.Release|Any CPU.Build.0 = Release|Any CPU + {A926F04F-631B-4A05-853C-6C76555BF26A}.Release|x64.ActiveCfg = Release|Any CPU + {A926F04F-631B-4A05-853C-6C76555BF26A}.Release|x64.Build.0 = Release|Any CPU + {A926F04F-631B-4A05-853C-6C76555BF26A}.Release|x86.ActiveCfg = Release|Any CPU + {A926F04F-631B-4A05-853C-6C76555BF26A}.Release|x86.Build.0 = Release|Any CPU {E14E0128-D4FF-462D-B684-7238EA876D69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E14E0128-D4FF-462D-B684-7238EA876D69}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E14E0128-D4FF-462D-B684-7238EA876D69}.Debug|x64.ActiveCfg = Debug|Any CPU + {E14E0128-D4FF-462D-B684-7238EA876D69}.Debug|x64.Build.0 = Debug|Any CPU + {E14E0128-D4FF-462D-B684-7238EA876D69}.Debug|x86.ActiveCfg = Debug|Any CPU + {E14E0128-D4FF-462D-B684-7238EA876D69}.Debug|x86.Build.0 = Debug|Any CPU {E14E0128-D4FF-462D-B684-7238EA876D69}.Release|Any CPU.ActiveCfg = Release|Any CPU {E14E0128-D4FF-462D-B684-7238EA876D69}.Release|Any CPU.Build.0 = Release|Any CPU + {E14E0128-D4FF-462D-B684-7238EA876D69}.Release|x64.ActiveCfg = Release|Any CPU + {E14E0128-D4FF-462D-B684-7238EA876D69}.Release|x64.Build.0 = Release|Any CPU + {E14E0128-D4FF-462D-B684-7238EA876D69}.Release|x86.ActiveCfg = Release|Any CPU + {E14E0128-D4FF-462D-B684-7238EA876D69}.Release|x86.Build.0 = Release|Any CPU {DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Debug|x64.ActiveCfg = Debug|Any CPU + {DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Debug|x64.Build.0 = Debug|Any CPU + {DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Debug|x86.ActiveCfg = Debug|Any CPU + {DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Debug|x86.Build.0 = Debug|Any CPU {DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Release|Any CPU.ActiveCfg = Release|Any CPU {DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Release|Any CPU.Build.0 = Release|Any CPU + {DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Release|x64.ActiveCfg = Release|Any CPU + {DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Release|x64.Build.0 = Release|Any CPU + {DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Release|x86.ActiveCfg = Release|Any CPU + {DBFD0F4B-5592-4E34-8624-04F7540E34B9}.Release|x86.Build.0 = Release|Any CPU {C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Debug|x64.ActiveCfg = Debug|Any CPU + {C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Debug|x64.Build.0 = Debug|Any CPU + {C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Debug|x86.ActiveCfg = Debug|Any CPU + {C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Debug|x86.Build.0 = Debug|Any CPU {C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Release|Any CPU.ActiveCfg = Release|Any CPU {C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Release|Any CPU.Build.0 = Release|Any CPU + {C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Release|x64.ActiveCfg = Release|Any CPU + {C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Release|x64.Build.0 = Release|Any CPU + {C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Release|x86.ActiveCfg = Release|Any CPU + {C2F5630E-A6E2-48D5-B182-5FE9278D3E37}.Release|x86.Build.0 = Release|Any CPU {3E13749E-82D1-434B-B867-F7B094B66065}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3E13749E-82D1-434B-B867-F7B094B66065}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3E13749E-82D1-434B-B867-F7B094B66065}.Debug|x64.ActiveCfg = Debug|Any CPU + {3E13749E-82D1-434B-B867-F7B094B66065}.Debug|x64.Build.0 = Debug|Any CPU + {3E13749E-82D1-434B-B867-F7B094B66065}.Debug|x86.ActiveCfg = Debug|Any CPU + {3E13749E-82D1-434B-B867-F7B094B66065}.Debug|x86.Build.0 = Debug|Any CPU {3E13749E-82D1-434B-B867-F7B094B66065}.Release|Any CPU.ActiveCfg = Release|Any CPU {3E13749E-82D1-434B-B867-F7B094B66065}.Release|Any CPU.Build.0 = Release|Any CPU + {3E13749E-82D1-434B-B867-F7B094B66065}.Release|x64.ActiveCfg = Release|Any CPU + {3E13749E-82D1-434B-B867-F7B094B66065}.Release|x64.Build.0 = Release|Any CPU + {3E13749E-82D1-434B-B867-F7B094B66065}.Release|x86.ActiveCfg = Release|Any CPU + {3E13749E-82D1-434B-B867-F7B094B66065}.Release|x86.Build.0 = Release|Any CPU {620D9F08-EB9F-469F-806A-64E915437154}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {620D9F08-EB9F-469F-806A-64E915437154}.Debug|Any CPU.Build.0 = Debug|Any CPU + {620D9F08-EB9F-469F-806A-64E915437154}.Debug|x64.ActiveCfg = Debug|Any CPU + {620D9F08-EB9F-469F-806A-64E915437154}.Debug|x64.Build.0 = Debug|Any CPU + {620D9F08-EB9F-469F-806A-64E915437154}.Debug|x86.ActiveCfg = Debug|Any CPU + {620D9F08-EB9F-469F-806A-64E915437154}.Debug|x86.Build.0 = Debug|Any CPU {620D9F08-EB9F-469F-806A-64E915437154}.Release|Any CPU.ActiveCfg = Release|Any CPU {620D9F08-EB9F-469F-806A-64E915437154}.Release|Any CPU.Build.0 = Release|Any CPU + {620D9F08-EB9F-469F-806A-64E915437154}.Release|x64.ActiveCfg = Release|Any CPU + {620D9F08-EB9F-469F-806A-64E915437154}.Release|x64.Build.0 = Release|Any CPU + {620D9F08-EB9F-469F-806A-64E915437154}.Release|x86.ActiveCfg = Release|Any CPU + {620D9F08-EB9F-469F-806A-64E915437154}.Release|x86.Build.0 = Release|Any CPU {088B3139-68D3-4A5F-B159-0C8FE94CD969}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {088B3139-68D3-4A5F-B159-0C8FE94CD969}.Debug|Any CPU.Build.0 = Debug|Any CPU + {088B3139-68D3-4A5F-B159-0C8FE94CD969}.Debug|x64.ActiveCfg = Debug|Any CPU + {088B3139-68D3-4A5F-B159-0C8FE94CD969}.Debug|x64.Build.0 = Debug|Any CPU + {088B3139-68D3-4A5F-B159-0C8FE94CD969}.Debug|x86.ActiveCfg = Debug|Any CPU + {088B3139-68D3-4A5F-B159-0C8FE94CD969}.Debug|x86.Build.0 = Debug|Any CPU {088B3139-68D3-4A5F-B159-0C8FE94CD969}.Release|Any CPU.ActiveCfg = Release|Any CPU {088B3139-68D3-4A5F-B159-0C8FE94CD969}.Release|Any CPU.Build.0 = Release|Any CPU + {088B3139-68D3-4A5F-B159-0C8FE94CD969}.Release|x64.ActiveCfg = Release|Any CPU + {088B3139-68D3-4A5F-B159-0C8FE94CD969}.Release|x64.Build.0 = Release|Any CPU + {088B3139-68D3-4A5F-B159-0C8FE94CD969}.Release|x86.ActiveCfg = Release|Any CPU + {088B3139-68D3-4A5F-B159-0C8FE94CD969}.Release|x86.Build.0 = Release|Any CPU {A972109D-D974-4BB2-BA01-72BD845ACBBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A972109D-D974-4BB2-BA01-72BD845ACBBA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A972109D-D974-4BB2-BA01-72BD845ACBBA}.Debug|x64.ActiveCfg = Debug|Any CPU + {A972109D-D974-4BB2-BA01-72BD845ACBBA}.Debug|x64.Build.0 = Debug|Any CPU + {A972109D-D974-4BB2-BA01-72BD845ACBBA}.Debug|x86.ActiveCfg = Debug|Any CPU + {A972109D-D974-4BB2-BA01-72BD845ACBBA}.Debug|x86.Build.0 = Debug|Any CPU {A972109D-D974-4BB2-BA01-72BD845ACBBA}.Release|Any CPU.ActiveCfg = Release|Any CPU {A972109D-D974-4BB2-BA01-72BD845ACBBA}.Release|Any CPU.Build.0 = Release|Any CPU + {A972109D-D974-4BB2-BA01-72BD845ACBBA}.Release|x64.ActiveCfg = Release|Any CPU + {A972109D-D974-4BB2-BA01-72BD845ACBBA}.Release|x64.Build.0 = Release|Any CPU + {A972109D-D974-4BB2-BA01-72BD845ACBBA}.Release|x86.ActiveCfg = Release|Any CPU + {A972109D-D974-4BB2-BA01-72BD845ACBBA}.Release|x86.Build.0 = Release|Any CPU {297B7268-DEF3-4669-98DF-78FA08619EDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {297B7268-DEF3-4669-98DF-78FA08619EDF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {297B7268-DEF3-4669-98DF-78FA08619EDF}.Debug|x64.ActiveCfg = Debug|Any CPU + {297B7268-DEF3-4669-98DF-78FA08619EDF}.Debug|x64.Build.0 = Debug|Any CPU + {297B7268-DEF3-4669-98DF-78FA08619EDF}.Debug|x86.ActiveCfg = Debug|Any CPU + {297B7268-DEF3-4669-98DF-78FA08619EDF}.Debug|x86.Build.0 = Debug|Any CPU {297B7268-DEF3-4669-98DF-78FA08619EDF}.Release|Any CPU.ActiveCfg = Release|Any CPU {297B7268-DEF3-4669-98DF-78FA08619EDF}.Release|Any CPU.Build.0 = Release|Any CPU + {297B7268-DEF3-4669-98DF-78FA08619EDF}.Release|x64.ActiveCfg = Release|Any CPU + {297B7268-DEF3-4669-98DF-78FA08619EDF}.Release|x64.Build.0 = Release|Any CPU + {297B7268-DEF3-4669-98DF-78FA08619EDF}.Release|x86.ActiveCfg = Release|Any CPU + {297B7268-DEF3-4669-98DF-78FA08619EDF}.Release|x86.Build.0 = Release|Any CPU {631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Debug|x64.ActiveCfg = Debug|Any CPU + {631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Debug|x64.Build.0 = Debug|Any CPU + {631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Debug|x86.ActiveCfg = Debug|Any CPU + {631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Debug|x86.Build.0 = Debug|Any CPU {631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Release|Any CPU.ActiveCfg = Release|Any CPU {631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Release|Any CPU.Build.0 = Release|Any CPU + {631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Release|x64.ActiveCfg = Release|Any CPU + {631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Release|x64.Build.0 = Release|Any CPU + {631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Release|x86.ActiveCfg = Release|Any CPU + {631092C7-B59D-4EA7-92D0-5E181AB4F9F6}.Release|x86.Build.0 = Release|Any CPU {23659070-58F7-403B-8973-B2E20B5E9BE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {23659070-58F7-403B-8973-B2E20B5E9BE1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23659070-58F7-403B-8973-B2E20B5E9BE1}.Debug|x64.ActiveCfg = Debug|Any CPU + {23659070-58F7-403B-8973-B2E20B5E9BE1}.Debug|x64.Build.0 = Debug|Any CPU + {23659070-58F7-403B-8973-B2E20B5E9BE1}.Debug|x86.ActiveCfg = Debug|Any CPU + {23659070-58F7-403B-8973-B2E20B5E9BE1}.Debug|x86.Build.0 = Debug|Any CPU {23659070-58F7-403B-8973-B2E20B5E9BE1}.Release|Any CPU.ActiveCfg = Release|Any CPU {23659070-58F7-403B-8973-B2E20B5E9BE1}.Release|Any CPU.Build.0 = Release|Any CPU + {23659070-58F7-403B-8973-B2E20B5E9BE1}.Release|x64.ActiveCfg = Release|Any CPU + {23659070-58F7-403B-8973-B2E20B5E9BE1}.Release|x64.Build.0 = Release|Any CPU + {23659070-58F7-403B-8973-B2E20B5E9BE1}.Release|x86.ActiveCfg = Release|Any CPU + {23659070-58F7-403B-8973-B2E20B5E9BE1}.Release|x86.Build.0 = Release|Any CPU {D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Debug|x64.ActiveCfg = Debug|Any CPU + {D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Debug|x64.Build.0 = Debug|Any CPU + {D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Debug|x86.ActiveCfg = Debug|Any CPU + {D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Debug|x86.Build.0 = Debug|Any CPU {D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Release|Any CPU.ActiveCfg = Release|Any CPU {D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Release|Any CPU.Build.0 = Release|Any CPU + {D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Release|x64.ActiveCfg = Release|Any CPU + {D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Release|x64.Build.0 = Release|Any CPU + {D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Release|x86.ActiveCfg = Release|Any CPU + {D9E0D333-60F3-493F-A5B2-5758ACA42A17}.Release|x86.Build.0 = Release|Any CPU {10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Debug|Any CPU.Build.0 = Debug|Any CPU + {10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Debug|x64.ActiveCfg = Debug|Any CPU + {10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Debug|x64.Build.0 = Debug|Any CPU + {10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Debug|x86.ActiveCfg = Debug|Any CPU + {10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Debug|x86.Build.0 = Debug|Any CPU {10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Release|Any CPU.ActiveCfg = Release|Any CPU {10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Release|Any CPU.Build.0 = Release|Any CPU + {10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Release|x64.ActiveCfg = Release|Any CPU + {10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Release|x64.Build.0 = Release|Any CPU + {10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Release|x86.ActiveCfg = Release|Any CPU + {10D71F44-C9FD-41F2-8F1A-D93FAE3CE696}.Release|x86.Build.0 = Release|Any CPU {8730045F-91F5-4438-8772-C6E31E89AACC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8730045F-91F5-4438-8772-C6E31E89AACC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8730045F-91F5-4438-8772-C6E31E89AACC}.Debug|x64.ActiveCfg = Debug|Any CPU + {8730045F-91F5-4438-8772-C6E31E89AACC}.Debug|x64.Build.0 = Debug|Any CPU + {8730045F-91F5-4438-8772-C6E31E89AACC}.Debug|x86.ActiveCfg = Debug|Any CPU + {8730045F-91F5-4438-8772-C6E31E89AACC}.Debug|x86.Build.0 = Debug|Any CPU {8730045F-91F5-4438-8772-C6E31E89AACC}.Release|Any CPU.ActiveCfg = Release|Any CPU {8730045F-91F5-4438-8772-C6E31E89AACC}.Release|Any CPU.Build.0 = Release|Any CPU + {8730045F-91F5-4438-8772-C6E31E89AACC}.Release|x64.ActiveCfg = Release|Any CPU + {8730045F-91F5-4438-8772-C6E31E89AACC}.Release|x64.Build.0 = Release|Any CPU + {8730045F-91F5-4438-8772-C6E31E89AACC}.Release|x86.ActiveCfg = Release|Any CPU + {8730045F-91F5-4438-8772-C6E31E89AACC}.Release|x86.Build.0 = Release|Any CPU {B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Debug|x64.ActiveCfg = Debug|Any CPU + {B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Debug|x64.Build.0 = Debug|Any CPU + {B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Debug|x86.ActiveCfg = Debug|Any CPU + {B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Debug|x86.Build.0 = Debug|Any CPU {B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Release|Any CPU.ActiveCfg = Release|Any CPU {B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Release|Any CPU.Build.0 = Release|Any CPU + {B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Release|x64.ActiveCfg = Release|Any CPU + {B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Release|x64.Build.0 = Release|Any CPU + {B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Release|x86.ActiveCfg = Release|Any CPU + {B45A3E8B-286B-4A74-9602-FC192ACEE8C4}.Release|x86.Build.0 = Release|Any CPU {ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Debug|x64.ActiveCfg = Debug|Any CPU + {ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Debug|x64.Build.0 = Debug|Any CPU + {ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Debug|x86.ActiveCfg = Debug|Any CPU + {ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Debug|x86.Build.0 = Debug|Any CPU {ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Release|Any CPU.ActiveCfg = Release|Any CPU {ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Release|Any CPU.Build.0 = Release|Any CPU + {ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Release|x64.ActiveCfg = Release|Any CPU + {ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Release|x64.Build.0 = Release|Any CPU + {ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Release|x86.ActiveCfg = Release|Any CPU + {ED9C639A-A706-4ECB-9638-A15B3681BDEC}.Release|x86.Build.0 = Release|Any CPU {1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Debug|x64.ActiveCfg = Debug|Any CPU + {1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Debug|x64.Build.0 = Debug|Any CPU + {1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Debug|x86.ActiveCfg = Debug|Any CPU + {1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Debug|x86.Build.0 = Debug|Any CPU {1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Release|Any CPU.ActiveCfg = Release|Any CPU {1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Release|Any CPU.Build.0 = Release|Any CPU + {1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Release|x64.ActiveCfg = Release|Any CPU + {1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Release|x64.Build.0 = Release|Any CPU + {1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Release|x86.ActiveCfg = Release|Any CPU + {1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6}.Release|x86.Build.0 = Release|Any CPU + {D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Debug|x64.ActiveCfg = Debug|Any CPU + {D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Debug|x64.Build.0 = Debug|Any CPU + {D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Debug|x86.ActiveCfg = Debug|Any CPU + {D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Debug|x86.Build.0 = Debug|Any CPU + {D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Release|Any CPU.Build.0 = Release|Any CPU + {D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Release|x64.ActiveCfg = Release|Any CPU + {D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Release|x64.Build.0 = Release|Any CPU + {D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Release|x86.ActiveCfg = Release|Any CPU + {D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36}.Release|x86.Build.0 = Release|Any CPU + {E99A894F-1731-4A48-B345-1D14F70EA23F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E99A894F-1731-4A48-B345-1D14F70EA23F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E99A894F-1731-4A48-B345-1D14F70EA23F}.Debug|x64.ActiveCfg = Debug|Any CPU + {E99A894F-1731-4A48-B345-1D14F70EA23F}.Debug|x64.Build.0 = Debug|Any CPU + {E99A894F-1731-4A48-B345-1D14F70EA23F}.Debug|x86.ActiveCfg = Debug|Any CPU + {E99A894F-1731-4A48-B345-1D14F70EA23F}.Debug|x86.Build.0 = Debug|Any CPU + {E99A894F-1731-4A48-B345-1D14F70EA23F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E99A894F-1731-4A48-B345-1D14F70EA23F}.Release|Any CPU.Build.0 = Release|Any CPU + {E99A894F-1731-4A48-B345-1D14F70EA23F}.Release|x64.ActiveCfg = Release|Any CPU + {E99A894F-1731-4A48-B345-1D14F70EA23F}.Release|x64.Build.0 = Release|Any CPU + {E99A894F-1731-4A48-B345-1D14F70EA23F}.Release|x86.ActiveCfg = Release|Any CPU + {E99A894F-1731-4A48-B345-1D14F70EA23F}.Release|x86.Build.0 = Release|Any CPU + {30F4D3DF-D777-40C2-B434-49173AC06A4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {30F4D3DF-D777-40C2-B434-49173AC06A4B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {30F4D3DF-D777-40C2-B434-49173AC06A4B}.Debug|x64.ActiveCfg = Debug|Any CPU + {30F4D3DF-D777-40C2-B434-49173AC06A4B}.Debug|x64.Build.0 = Debug|Any CPU + {30F4D3DF-D777-40C2-B434-49173AC06A4B}.Debug|x86.ActiveCfg = Debug|Any CPU + {30F4D3DF-D777-40C2-B434-49173AC06A4B}.Debug|x86.Build.0 = Debug|Any CPU + {30F4D3DF-D777-40C2-B434-49173AC06A4B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {30F4D3DF-D777-40C2-B434-49173AC06A4B}.Release|Any CPU.Build.0 = Release|Any CPU + {30F4D3DF-D777-40C2-B434-49173AC06A4B}.Release|x64.ActiveCfg = Release|Any CPU + {30F4D3DF-D777-40C2-B434-49173AC06A4B}.Release|x64.Build.0 = Release|Any CPU + {30F4D3DF-D777-40C2-B434-49173AC06A4B}.Release|x86.ActiveCfg = Release|Any CPU + {30F4D3DF-D777-40C2-B434-49173AC06A4B}.Release|x86.Build.0 = Release|Any CPU + {A4BC1880-5492-41FF-81BA-963AC4C60A02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4BC1880-5492-41FF-81BA-963AC4C60A02}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4BC1880-5492-41FF-81BA-963AC4C60A02}.Debug|x64.ActiveCfg = Debug|Any CPU + {A4BC1880-5492-41FF-81BA-963AC4C60A02}.Debug|x64.Build.0 = Debug|Any CPU + {A4BC1880-5492-41FF-81BA-963AC4C60A02}.Debug|x86.ActiveCfg = Debug|Any CPU + {A4BC1880-5492-41FF-81BA-963AC4C60A02}.Debug|x86.Build.0 = Debug|Any CPU + {A4BC1880-5492-41FF-81BA-963AC4C60A02}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4BC1880-5492-41FF-81BA-963AC4C60A02}.Release|Any CPU.Build.0 = Release|Any CPU + {A4BC1880-5492-41FF-81BA-963AC4C60A02}.Release|x64.ActiveCfg = Release|Any CPU + {A4BC1880-5492-41FF-81BA-963AC4C60A02}.Release|x64.Build.0 = Release|Any CPU + {A4BC1880-5492-41FF-81BA-963AC4C60A02}.Release|x86.ActiveCfg = Release|Any CPU + {A4BC1880-5492-41FF-81BA-963AC4C60A02}.Release|x86.Build.0 = Release|Any CPU + {BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Debug|x64.ActiveCfg = Debug|Any CPU + {BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Debug|x64.Build.0 = Debug|Any CPU + {BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Debug|x86.ActiveCfg = Debug|Any CPU + {BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Debug|x86.Build.0 = Debug|Any CPU + {BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Release|Any CPU.Build.0 = Release|Any CPU + {BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Release|x64.ActiveCfg = Release|Any CPU + {BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Release|x64.Build.0 = Release|Any CPU + {BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Release|x86.ActiveCfg = Release|Any CPU + {BE7FDBA3-58CD-41C0-A748-E636DD11F137}.Release|x86.Build.0 = Release|Any CPU + {594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Debug|Any CPU.Build.0 = Debug|Any CPU + {594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Debug|x64.ActiveCfg = Debug|Any CPU + {594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Debug|x64.Build.0 = Debug|Any CPU + {594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Debug|x86.ActiveCfg = Debug|Any CPU + {594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Debug|x86.Build.0 = Debug|Any CPU + {594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Release|Any CPU.ActiveCfg = Release|Any CPU + {594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Release|Any CPU.Build.0 = Release|Any CPU + {594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Release|x64.ActiveCfg = Release|Any CPU + {594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Release|x64.Build.0 = Release|Any CPU + {594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Release|x86.ActiveCfg = Release|Any CPU + {594F56D4-CDDD-451A-B487-D49EEB9F5B98}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -274,6 +626,12 @@ Global {B45A3E8B-286B-4A74-9602-FC192ACEE8C4} = {2889482E-64CA-4A25-91D8-5B963D83681B} {ED9C639A-A706-4ECB-9638-A15B3681BDEC} = {2889482E-64CA-4A25-91D8-5B963D83681B} {1DA666D8-DBFE-40F7-8EBF-95CC892E4EB6} = {2889482E-64CA-4A25-91D8-5B963D83681B} + {D6DE6894-DF5E-4F59-A4DD-145FBEFBDD36} = {03E1C8DA-035E-4882-AF81-F392139FCF38} + {E99A894F-1731-4A48-B345-1D14F70EA23F} = {03E1C8DA-035E-4882-AF81-F392139FCF38} + {30F4D3DF-D777-40C2-B434-49173AC06A4B} = {03E1C8DA-035E-4882-AF81-F392139FCF38} + {A4BC1880-5492-41FF-81BA-963AC4C60A02} = {03E1C8DA-035E-4882-AF81-F392139FCF38} + {BE7FDBA3-58CD-41C0-A748-E636DD11F137} = {03E1C8DA-035E-4882-AF81-F392139FCF38} + {594F56D4-CDDD-451A-B487-D49EEB9F5B98} = {03E1C8DA-035E-4882-AF81-F392139FCF38} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {28315BFD-90E7-4E14-A2EA-F3D23AF4126F} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpOAuthDtos.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpOAuthDtos.cs new file mode 100644 index 00000000..6d92eca3 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpOAuthDtos.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; + +namespace Sozsoft.Mcp.Dtos; + +/// Dinamik istemci kaydi (RFC 7591) istegi. +public class McpClientRegistrationRequestDto +{ + public string? ClientName { get; set; } + + public List RedirectUris { get; set; } = []; +} + +/// +/// Dinamik istemci kaydinin sonucu. Kayit basarisizsa RFC 7591 hata kodunu +/// tasir ve diger alanlar bos kalir. +/// +public class McpClientRegistrationResultDto +{ + public string? ClientId { get; set; } + + public string? ClientName { get; set; } + + public List RedirectUris { get; set; } = []; + + public List GrantTypes { get; set; } = []; + + public List ResponseTypes { get; set; } = []; + + /// Genel istemci: token ucunda gizli anahtar kullanilmaz, PKCE zorunludur. + public string TokenEndpointAuthMethod { get; set; } = "none"; + + public string? Scope { get; set; } + + public long ClientIdIssuedAt { get; set; } + + public string? Error { get; set; } + + public string? ErrorDescription { get; set; } +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpQueryDtos.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpQueryDtos.cs new file mode 100644 index 00000000..c99fe2fd --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpQueryDtos.cs @@ -0,0 +1,127 @@ +using System.Collections.Generic; + +namespace Sozsoft.Mcp.Dtos; + +/// +/// Bir listenin verisini ekranin kendi sorgusuyla okuma istegi. Filtre, siralama ve gruplama +/// DevExtreme veri yukleme bicimindedir; ekranin grid'i de ayni bicimi kullanir. +/// +public class McpListQueryRequestDto +{ + public string ListFormCode { get; set; } = string.Empty; + + /// Ornek: ["Status","=","Open"] ya da [["A",">",1],"and",["B","contains","x"]]. + public string? Filter { get; set; } + + /// Ornek: [{"selector":"CreationTime","desc":true}]. + public string? Sort { get; set; } + + /// Ornek: [{"selector":"CustomerId","desc":false,"isExpanded":false}]. + public string? Group { get; set; } + + /// Ornek: [{"selector":"Id","summaryType":"count"}]. + public string? GroupSummary { get; set; } + + /// Ornek: [{"selector":"Amount","summaryType":"sum"}]. + public string? TotalSummary { get; set; } + + public int Skip { get; set; } + + /// Istenen satir sayisi; sunucu kaydindaki tavana kirpilir. + public int Take { get; set; } +} + +/// Liste sorgusunun sonucu. +public class McpListQueryResultDto +{ + public object? Data { get; set; } + + /// Filtreye uyan toplam kayit sayisi (sayfalamadan once). + public int TotalCount { get; set; } + + public int GroupCount { get; set; } + + public object[]? Summary { get; set; } + + /// Uygulanan satir tavani; bundan buyukse sonuc sayfalanmistir. + public int Take { get; set; } +} + +/// Parametreli tek SELECT istegi. +public class McpSelectRequestDto +{ + public string Sql { get; set; } = string.Empty; + + /// Sorgudaki adli parametreler. + public Dictionary Parameters { get; set; } = []; + + /// Istenen satir sayisi; sunucu kaydindaki tavana kirpilir. + public int MaxRows { get; set; } +} + +/// +/// Kayitli bir veritabani nesnesi (view, stored procedure, function) ya da Custom Endpoint. +/// +public class McpSavedQueryDto +{ + /// McpConsts.SavedQueryKinds degerlerinden biri. + public string Kind { get; set; } = string.Empty; + + public string Name { get; set; } = string.Empty; + public string? SchemaName { get; set; } + public string? Description { get; set; } + + /// Custom Endpoint'te beklenen parametre adlari; veritabani nesnelerinde bostur. + public List ParameterNames { get; set; } = []; +} + +/// Kayitli bir nesnenin ya da Custom Endpoint'in cagrilmasi. +public class McpSavedQueryRequestDto +{ + /// ile ayni degerler. + public string Kind { get; set; } = string.Empty; + + public string Name { get; set; } = string.Empty; + + /// Bos birakilirsa saglayicinin varsayilan semasi kullanilir. + public string? SchemaName { get; set; } + + public Dictionary Parameters { get; set; } = []; + + public int MaxRows { get; set; } +} + +/// Sorgu sonucu. +public class McpQueryResultDto +{ + public List Columns { get; set; } = []; + public List> Rows { get; set; } = []; + public int RowCount { get; set; } + + /// Satir sayisi tavana dayandiginda true; sonuc kesilmis demektir. + public bool Truncated { get; set; } + + public long ExecutionTimeMs { get; set; } +} + +/// Bir ListForm ekrani uzerinden kayit yazma/silme istegi. +public class McpRowRequestDto +{ + public string ListFormCode { get; set; } = string.Empty; + + /// Alan adi → deger. Silme isleminde bos birakilir. + public Dictionary Values { get; set; } = []; + + /// Guncelleme ve silmede anahtar alan degerleri. + public List Keys { get; set; } = []; +} + +/// Yazma isleminin sonucu. +public class McpWriteResultDto +{ + /// Insert'te uretilen anahtar; guncelleme ve silmede bostur. + public string? Key { get; set; } + + /// Guncelleme ve silmede etkilenen satir sayisi. + public int AffectedRows { 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 new file mode 100644 index 00000000..fc50faeb --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Dtos/McpSchemaDtos.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; + +namespace Sozsoft.Mcp.Dtos; + +/// +/// Istegin kiracisina dusen MCP sunucu kaydinin etkin hali. /mcp ucu hangi araclari +/// listeleyecegine ve sorgularin hangi satir tavaniyla kosacagina bu kayda bakarak karar verir. +/// +public class McpServerConfigDto +{ + public string Name { get; set; } = string.Empty; + + /// 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. + 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. + public string? TenantName { get; set; } +} + +/// +/// Veri sozlugunun tazelik bilgisi. Sozluk Sas_H_DbTableColumn tablosunda, sozluk +/// ureten prosedur tarafindan yazilmis JSON olarak durur. +/// +public class McpSchemaInfoDto +{ + public bool Available { get; set; } + public string? CacheKey { get; set; } + public DateTime? RefreshedAt { get; set; } + public int FormCount { get; set; } + public int FieldCount { get; set; } + public string? TenantName { get; set; } + public string? DefinitionCultureName { get; set; } + public string? UiCultureName { get; set; } + + /// Sozluk bulunamadiginda ne yapilacagini anlatan mesaj. + public string? Message { get; set; } +} + +/// Bir listenin (ListForm) ozet tanimi. +public class McpListSummaryDto +{ + public string ListFormCode { get; set; } = string.Empty; + public string? DisplayName { get; set; } + public string? Title { get; set; } + public string? Description { get; set; } + + /// Listenin arkasindaki tablo adi; SELECT tabanli listelerde bostur. + public string? TableName { get; set; } + + /// Tablo adi ya da SELECT metni — listenin gercek SQL kaynagi. + public string? SqlSourceName { get; set; } + + /// table, selectSql ya da unknown. + public string? SourceKind { get; set; } + + public string? KeyFieldName { get; set; } + public int FieldCount { get; set; } +} + +/// Bir listenin sutun tanimi. +public class McpColumnDto +{ + public string Field { get; set; } = string.Empty; + public string? DisplayName { get; set; } + public string? SqlDataType { get; set; } + public bool IsVisible { get; set; } + public bool IsBooleanColumn { get; set; } + + /// Lookup tasiyan sutunda hedef tablo; yoksa bostur. + public string? LookupTableName { get; set; } + public string? LookupValueColumn { get; set; } + public string? LookupDisplayColumn { get; set; } + + /// Parola/token/anahtar gibi sutunlarda true; sorgu araclari bunlari onermez. + public bool IsSensitive { get; set; } + + /// Varsayilan SELECT listesine girmesi uygun olan sutunlarda true. + public bool IsDefaultSelectColumn { get; set; } +} + +/// Bir listenin sutunlariyla birlikte tam tanimi. +public class McpListDetailDto : McpListSummaryDto +{ + public List Columns { get; set; } = []; +} + +/// Serbest metin aramasinin tek sonucu. +public class McpSchemaMatchDto +{ + public string ListFormCode { get; set; } = string.Empty; + public string? DisplayName { get; set; } + public string? TableName { get; set; } + + /// Eslesme yalnizca liste duzeyindeyse bos; sutun duzeyindeyse eslesen sutunlar. + public List MatchedColumns { get; set; } = []; +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDataAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDataAppService.cs new file mode 100644 index 00000000..40e0b496 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpDataAppService.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using Sozsoft.Mcp.Dtos; +using Volo.Abp.Application.Services; + +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. +/// +public interface IMcpDataAppService : IApplicationService +{ + Task InsertRowAsync(McpRowRequestDto input); + + Task UpdateRowAsync(McpRowRequestDto input); + + Task DeleteRowAsync(McpRowRequestDto input); +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpOAuthAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpOAuthAppService.cs new file mode 100644 index 00000000..773ed847 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpOAuthAppService.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using Sozsoft.Mcp.Dtos; +using Volo.Abp.Application.Services; + +namespace Sozsoft.Mcp; + +/// +/// Uzak MCP istemcilerinin (Claude, ChatGPT connector'lari) OAuth kaydi. Kayit yeni bir +/// OpenIddict uygulamasi uretmez: istek, seed edilmis tek MCP istemcisinin izinli geri donus +/// adresleriyle dogrulanir ve o istemcinin kimligi dondurulur. Boylece herkese acik kayit ucu +/// veritabaninda denetimsiz uygulama biriktiremez. +/// +public interface IMcpOAuthAppService : IApplicationService +{ + Task RegisterClientAsync(McpClientRegistrationRequestDto input); +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpQueryAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpQueryAppService.cs new file mode 100644 index 00000000..b5a5a8e9 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpQueryAppService.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Sozsoft.Mcp.Dtos; +using Volo.Abp.Application.Services; + +namespace Sozsoft.Mcp; + +/// +/// MCP salt-okunur sorgu yuzeyi. Her sorgu istegin kapsamindaki veri kaynaginda, sunucu +/// kaydindaki satir tavaniyla ve parametreli olarak kosar. +/// +public interface IMcpQueryAppService : IApplicationService +{ + /// + /// Bir listenin verisini ekranin kendi sorgusuyla okur: liste okuma yetkisi, alan bazli + /// okuma yetkisi, kiraci/sube filtreleri ve kullanici ozellestirmeleri uygulamadaki gibi + /// gecerlidir. Normal kullanicinin veri okuma yolu budur. + /// + Task QueryListAsync(McpListQueryRequestDto input); + + /// + /// Tek bir SELECT ifadesi kosturur. Coklu ifade, DDL ve DML reddedilir; satir sayisi + /// sunucu kaydindaki tavana kirpilir. + /// + Task RunSelectAsync(McpSelectRequestDto input); + + /// Veri kaynagindaki view/procedure/function nesneleri ve Custom Endpoint'ler. + Task> GetSavedQueriesAsync(string? search = null); + + /// Kayitli bir nesneyi ya da Custom Endpoint'i parametreleriyle cagirir. + Task RunSavedQueryAsync(McpSavedQueryRequestDto input); +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpSchemaAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpSchemaAppService.cs new file mode 100644 index 00000000..67613054 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/IMcpSchemaAppService.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Sozsoft.Mcp.Dtos; +using Volo.Abp.Application.Services; + +namespace Sozsoft.Mcp; + +/// +/// MCP sema kesfi. Tablo ve sutun bilgisi canli katalogdan degil, platformun ekran +/// tanimlarindan uretilen JSON veri sozlugunden gelir; boylece modelin gordugu adlar +/// kullanicinin ekranda gordugu adlarla ayni olur. +/// +public interface IMcpSchemaAppService : IApplicationService +{ + /// Istegin kiracisina dusen etkin MCP sunucu kaydi; kayit yoksa null. + Task GetServerConfigAsync(); + + /// + /// Bu kullanicinin bu baglantida cagirabilecegi araclar: sunucu kaydinin actiklari ile + /// kullanicinin uygulamadaki yetkilerinin kesisimi. + /// + Task> GetAvailableToolsAsync(); + + /// Veri sozlugunun bulunup bulunmadigi ve ne zaman tazelendigi. + Task GetSchemaInfoAsync(); + + /// Bos birakilirsa tum listeler doner. + /// Donecek en fazla liste sayisi. + Task> GetListsAsync(string? search = null, int maxResults = 200); + + /// Tek bir listenin sutunlariyla birlikte tanimi; bulunamazsa null. + Task GetListAsync(string listFormCode); + + /// + /// Liste ve sutun adlari ile basliklari uzerinde aksan ve buyuk/kucuk harf duyarsiz arama. + /// + Task> SearchAsync(string term, int maxResults = 50); +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/McpApplicationContractsModule.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/McpApplicationContractsModule.cs new file mode 100644 index 00000000..c6490648 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/McpApplicationContractsModule.cs @@ -0,0 +1,14 @@ +using Volo.Abp.Application; +using Volo.Abp.Authorization; +using Volo.Abp.Modularity; + +namespace Sozsoft.Mcp; + +[DependsOn( + typeof(McpDomainSharedModule), + typeof(AbpDddApplicationContractsModule), + typeof(AbpAuthorizationModule) +)] +public class McpApplicationContractsModule : AbpModule +{ +} 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 new file mode 100644 index 00000000..5b219c2c --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application.Contracts/Sozsoft.Mcp.Application.Contracts.csproj @@ -0,0 +1,20 @@ + + + + + + net10.0 + enable + Sozsoft.Mcp + + + + + + + + + + + + diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpAppService.cs new file mode 100644 index 00000000..3f259ab7 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpAppService.cs @@ -0,0 +1,86 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Sozsoft.Mcp.Dtos; +using Sozsoft.Mcp.Services; +using Sozsoft.Platform.Localization; +using Volo.Abp; +using Volo.Abp.Application.Services; + +namespace Sozsoft.Mcp; + +/// +/// MCP uygulama servislerinin ortak tabani. Kullaniciya gorunen metinler platformun +/// PlatformResource kaynagindan okunur; sunucu kaydini cozme ve arac kapisi her +/// serviste ayni davranir. +/// +public abstract class McpAppService : ApplicationService +{ + protected IMcpServerConfigProvider ServerConfigProvider { get; } + + protected McpAppService(IMcpServerConfigProvider serverConfigProvider) + { + ServerConfigProvider = serverConfigProvider; + LocalizationResource = typeof(PlatformResource); + } + + /// Etkin sunucu kaydinin DTO hali; kayit yoksa null. + protected async Task ResolveServerConfigAsync() + { + var server = await ServerConfigProvider.GetEffectiveServerAsync(); + if (server is null) + { + return null; + } + + return new McpServerConfigDto + { + 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 + }; + } + + /// + /// Kaydi zorunlu kilan islemler icin. Kayit yoksa istemcinin nedenini gorebilecegi bir + /// hata atar. + /// + protected async Task RequireServerConfigAsync() + => await ResolveServerConfigAsync() + ?? throw new UserFriendlyException(L[McpErrorCodes.NoServerRecord]); + + /// + /// Kapali bir araca yapilan cagriyi reddeder: arac sunucu kaydinda kapaliysa ya da + /// kullanicinin uygulamada karsilik gelen yetkisi yoksa. Arac listesini suzmek tek basina + /// yeterli degildir — istemci listede olmayan bir araci da cagirabilir. + /// + protected async Task EnsureToolAllowedAsync(string toolName) + { + var config = await RequireServerConfigAsync(); + + if (!McpToolPolicy.IsAllowed(true, config.AllowWrite, config.AllowedTools, toolName)) + { + throw new UserFriendlyException(L[McpErrorCodes.ToolDisabled, toolName, config.Name]); + } + + if (McpToolNames.RequiredPermissions.TryGetValue(toolName, out var permission)) + { + await AuthorizationService.CheckAsync(permission); + } + + return config; + } + + /// Istenen satir sayisini sunucu kaydindaki tavana kirpar. + protected static int ResolveMaxRows(McpServerConfigDto config, int requested) + { + var ceiling = config.MaxRows > 0 ? config.MaxRows : McpConsts.DefaultMaxRows; + + return requested > 0 ? Math.Min(requested, ceiling) : ceiling; + } +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpApplicationModule.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpApplicationModule.cs new file mode 100644 index 00000000..d228d291 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpApplicationModule.cs @@ -0,0 +1,13 @@ +using Volo.Abp.Application; +using Volo.Abp.Modularity; + +namespace Sozsoft.Mcp; + +[DependsOn( + typeof(McpDomainModule), + typeof(McpApplicationContractsModule), + typeof(AbpDddApplicationModule) +)] +public class McpApplicationModule : AbpModule +{ +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpCatalogQueries.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpCatalogQueries.cs new file mode 100644 index 00000000..429008f6 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpCatalogQueries.cs @@ -0,0 +1,47 @@ +namespace Sozsoft.Mcp; + +/// +/// Kayitli nesneleri listelemek icin kullanilan katalog sorgulari. Parametre almazlar; +/// sabit metin oldugu icin string birlestirme riski yoktur. +/// +internal static class McpCatalogQueries +{ + public const string SqlServer = """ + SELECT + SCHEMA_NAME(o.schema_id) AS SchemaName, + o.name AS ObjectName, + CASE o.type + WHEN 'V' THEN 'VIEW' + WHEN 'P' THEN 'PROCEDURE' + WHEN 'IF' THEN 'TABLE_FUNCTION' + WHEN 'TF' THEN 'TABLE_FUNCTION' + WHEN 'FN' THEN 'SCALAR_FUNCTION' + END AS ObjectKind + FROM sys.objects o + WHERE o.is_ms_shipped = 0 + AND o.type IN ('V', 'P', 'IF', 'TF', 'FN') + ORDER BY SchemaName, ObjectName + """; + + public const string Postgres = """ + SELECT n.nspname AS "SchemaName", + c.relname AS "ObjectName", + 'VIEW' AS "ObjectKind" + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('v', 'm') + AND n.nspname NOT IN ('pg_catalog', 'information_schema') + UNION ALL + SELECT n.nspname AS "SchemaName", + p.proname AS "ObjectName", + CASE + WHEN p.prokind = 'p' THEN 'PROCEDURE' + WHEN p.proretset THEN 'TABLE_FUNCTION' + ELSE 'SCALAR_FUNCTION' + END AS "ObjectKind" + FROM pg_proc p + JOIN pg_namespace n ON n.oid = p.pronamespace + WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') + ORDER BY 1, 2 + """; +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDataAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDataAppService.cs new file mode 100644 index 00000000..2ce3cbd3 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDataAppService.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Sozsoft.Mcp.Dtos; +using Sozsoft.Mcp.Services; +using Sozsoft.Platform; +using Sozsoft.Platform.ListForms.Select; +using Volo.Abp; + +namespace Sozsoft.Mcp; + +/// +/// Veri yazma yuzeyi. Yazma her zaman bir ListForm ekraninin insert/update/delete +/// sozlesmesinden gecer; ekran yetkisi, tenant filtresi ve seed senkronizasyonu oradaki +/// yoldan aynen uygulanir. +/// +[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Write)] +public class McpDataAppService( + IMcpServerConfigProvider serverConfigProvider, + IListFormDataAppService listFormDataAppService) + : McpAppService(serverConfigProvider), IMcpDataAppService +{ + public async Task InsertRowAsync(McpRowRequestDto input) + { + var request = await BuildRequestAsync(input, McpToolNames.InsertRow, requireKeys: false, requireValues: true); + var key = await listFormDataAppService.PostInsertAsync(request); + + return new McpWriteResultDto { Key = ((object?)key)?.ToString() }; + } + + public async Task UpdateRowAsync(McpRowRequestDto input) + { + var request = await BuildRequestAsync(input, McpToolNames.UpdateRow, requireKeys: true, requireValues: true); + var affected = await listFormDataAppService.PostUpdateAsync(request); + + return new McpWriteResultDto { AffectedRows = affected }; + } + + public async Task DeleteRowAsync(McpRowRequestDto input) + { + var request = await BuildRequestAsync(input, McpToolNames.DeleteRow, requireKeys: true, requireValues: false); + var affected = await listFormDataAppService.PostDeleteAsync(request); + + return new McpWriteResultDto { AffectedRows = affected }; + } + + /// + /// Istegi ekranin bekledigi bicime cevirir. Data JSON metni olarak verilir; ayni + /// bicim ekranin kopyalama yolunda da kullanilir. + /// + private async Task BuildRequestAsync( + McpRowRequestDto input, + string toolName, + bool requireKeys, + bool requireValues) + { + 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)) + { + throw new UserFriendlyException(L[McpErrorCodes.ListFormCodeRequired]); + } + + if (requireValues && (input.Values is null || input.Values.Count == 0)) + { + throw new UserFriendlyException(L[McpErrorCodes.ValuesRequired]); + } + + if (requireKeys && (input.Keys is null || input.Keys.Count == 0)) + { + throw new UserFriendlyException(L[McpErrorCodes.KeysRequired]); + } + + return new DataRequestDto + { + ListFormCode = input.ListFormCode, + Data = JsonSerializer.Serialize(input.Values ?? new Dictionary()), + Keys = [.. (input.Keys ?? []).Select(key => key!)] + }; + } +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpOAuthAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpOAuthAppService.cs new file mode 100644 index 00000000..0239c19c --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpOAuthAppService.cs @@ -0,0 +1,65 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Sozsoft.Mcp.Dtos; +using Sozsoft.Mcp.Services; +using Sozsoft.Platform.Localization; +using Volo.Abp; +using Volo.Abp.Application.Services; + +namespace Sozsoft.Mcp; + +/// +/// Dinamik istemci kaydi (RFC 7591). Uc kimlik dogrulamasizdir; istemci ne alirsa alsin token +/// kullanicinin kendi girisiyle ve kendi yetkileriyle uretilir. Kural ve kayit +/// icindedir. +/// +[AllowAnonymous] +public class McpOAuthAppService : ApplicationService, IMcpOAuthAppService +{ + private const string AuthorizationCodeGrant = "authorization_code"; + private const string RefreshTokenGrant = "refresh_token"; + + private readonly IMcpOAuthClientManager clientManager; + + public McpOAuthAppService(IMcpOAuthClientManager clientManager) + { + this.clientManager = clientManager; + LocalizationResource = typeof(PlatformResource); + } + + public async Task RegisterClientAsync(McpClientRegistrationRequestDto input) + { + Check.NotNull(input, nameof(input)); + + var redirectUris = (input.RedirectUris ?? []).Distinct(StringComparer.Ordinal).ToList(); + var registration = await clientManager.RegisterAsync(input.ClientName, redirectUris); + + return registration.Error switch + { + McpClientRegistrationError.InvalidRedirectUri => new McpClientRegistrationResultDto + { + Error = "invalid_redirect_uri", + ErrorDescription = L[McpErrorCodes.RedirectUriNotAllowed, registration.ErrorValue ?? string.Empty] + }, + McpClientRegistrationError.LimitReached => new McpClientRegistrationResultDto + { + Error = "invalid_client_metadata", + ErrorDescription = L[McpErrorCodes.RegistrationLimitReached, registration.ErrorValue ?? string.Empty] + }, + _ => new McpClientRegistrationResultDto + { + ClientId = registration.ClientId, + ClientName = input.ClientName, + RedirectUris = redirectUris, + // Istemci hangi akislari isterse istesin gecerli olanlar bunlardir; RFC 7591 + // sunucunun istenen degerleri daraltmasina izin verir. + GrantTypes = [AuthorizationCodeGrant, RefreshTokenGrant], + ResponseTypes = ["code"], + Scope = string.Join(' ', McpConsts.OAuthScopes), + ClientIdIssuedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + } + }; + } +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpQueryAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpQueryAppService.cs new file mode 100644 index 00000000..d6356d2e --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpQueryAppService.cs @@ -0,0 +1,291 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Sozsoft.Mcp.Dtos; +using Sozsoft.Mcp.Services; +using Sozsoft.Platform; +using Sozsoft.Platform.Entities; +using Sozsoft.Platform.Enums; +using Sozsoft.Platform.ListForms.Select; +using Volo.Abp; +using Volo.Abp.Domain.Repositories; + +namespace Sozsoft.Mcp; + +/// +/// Salt-okunur sorgu yuzeyi. MCP kullaniciya yetki vermez, uygulamadaki yetkisini tasir: +/// +/// query_list ekranin kendi sorgusundan gecer; liste ve alan yetkileri, kiraci ve +/// sube filtreleri aynen uygulanir. +/// Ham SQL ve veritabani nesneleri (view/procedure/function) liste yetkilerini asar; bu +/// yuzden yalnizca uygulamada SQL Query Manager yetkisi olan kullaniciya aciktir. +/// Custom Endpoint'ler endpoint dispatcher'inin yetkisini ve endpoint'in kendi +/// kullanici/rol listesini ister. +/// +/// SQL kurallari , veri kaynagi cozumu ve calistirma +/// icindedir. +/// +[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Invoke)] +public class McpQueryAppService( + IMcpServerConfigProvider serverConfigProvider, + IMcpSqlGuard sqlGuard, + IMcpQueryExecutor queryExecutor, + IRepository customEndpointRepository, + IListFormSelectAppService listFormSelectAppService) + : McpAppService(serverConfigProvider), IMcpQueryAppService +{ + private const string SqlPermission = PlatformConsts.AppCodes.SqlQueryManagers.Default; + private const string CustomEndpointPermission = PlatformConsts.AppCodes.DeveloperKits.Get; + + public async Task QueryListAsync(McpListQueryRequestDto input) + { + Check.NotNull(input, nameof(input)); + + var config = await EnsureToolAllowedAsync(McpToolNames.QueryList); + var take = ResolveMaxRows(config, input.Take); + + var result = await listFormSelectAppService.GetSelectAsync(new SelectRequestDto + { + ListFormCode = input.ListFormCode, + Filter = NullIfEmpty(input.Filter), + Sort = NullIfEmpty(input.Sort), + Group = NullIfEmpty(input.Group), + GroupSummary = NullIfEmpty(input.GroupSummary), + TotalSummary = NullIfEmpty(input.TotalSummary), + Skip = Math.Max(input.Skip, 0), + Take = take, + RequireTotalCount = true, + RequireGroupCount = !string.IsNullOrWhiteSpace(input.Group) + }); + + return new McpListQueryResultDto + { + Data = result.Data, + TotalCount = result.TotalCount, + GroupCount = result.GroupCount, + Summary = result.Summary, + Take = take + }; + } + public async Task RunSelectAsync(McpSelectRequestDto input) + { + Check.NotNull(input, nameof(input)); + + var config = await EnsureToolAllowedAsync(McpToolNames.RunSelect); + + var sql = (input.Sql ?? string.Empty).Trim().TrimEnd(';').Trim(); + sqlGuard.EnsureReadOnlyStatement(sql); + + var maxRows = ResolveMaxRows(config, input.MaxRows); + var parameters = sqlGuard.NormalizeParameters(input.Parameters); + var context = await queryExecutor.ResolveDataSourceAsync(config.DataSourceCode); + var limitedSql = sqlGuard.ApplyRowLimit(sql, maxRows, context.DataSourceType); + + return ToResult(await queryExecutor.ExecuteAsync(context, limitedSql, parameters, maxRows)); + } + + public async Task> GetSavedQueriesAsync(string? search = null) + { + var config = await EnsureToolAllowedAsync(McpToolNames.ListQueries); + var results = new List(); + + if (await AuthorizationService.IsGrantedAsync(SqlPermission)) + { + results.AddRange(await GetDatabaseObjectsAsync(config)); + } + + if (await AuthorizationService.IsGrantedAsync(CustomEndpointPermission)) + { + results.AddRange(await GetCallableCustomEndpointsAsync()); + } + + if (!string.IsNullOrWhiteSpace(search)) + { + results = [.. results.Where(item => + item.Name.Contains(search, StringComparison.OrdinalIgnoreCase) + || (item.Description?.Contains(search, StringComparison.OrdinalIgnoreCase) ?? false))]; + } + + return [.. results + .OrderBy(item => item.Kind, StringComparer.Ordinal) + .ThenBy(item => item.Name, StringComparer.OrdinalIgnoreCase)]; + } + + private async Task> GetDatabaseObjectsAsync(McpServerConfigDto config) + { + var context = await queryExecutor.ResolveDataSourceAsync(config.DataSourceCode); + var catalogSql = context.DataSourceType == DataSourceTypeEnum.Postgresql + ? McpCatalogQueries.Postgres + : McpCatalogQueries.SqlServer; + + // Katalog sorgusunun kendi satir siniri yoktur; nesne sayisi tavani asmaz. + var catalog = await queryExecutor.ExecuteAsync(context, catalogSql, new Dictionary(), int.MaxValue); + + var results = new List(); + + foreach (var row in catalog.Rows) + { + var kind = MapCatalogKind(ReadString(row, "ObjectKind")); + if (kind is null) + { + continue; + } + + results.Add(new McpSavedQueryDto + { + Kind = kind, + SchemaName = ReadString(row, "SchemaName"), + Name = ReadString(row, "ObjectName") ?? string.Empty + }); + } + + return results; + } + + public async Task RunSavedQueryAsync(McpSavedQueryRequestDto input) + { + Check.NotNull(input, nameof(input)); + + var config = await EnsureToolAllowedAsync(McpToolNames.RunQuery); + + if (IsKind(input.Kind, McpConsts.SavedQueryKinds.CustomEndpoint)) + { + await AuthorizationService.CheckAsync(CustomEndpointPermission); + return await RunCustomEndpointAsync(input, config); + } + + await AuthorizationService.CheckAsync(SqlPermission); + + var maxRows = ResolveMaxRows(config, input.MaxRows); + var parameters = sqlGuard.NormalizeParameters(input.Parameters); + var context = await queryExecutor.ResolveDataSourceAsync(config.DataSourceCode); + + var schemaName = string.IsNullOrWhiteSpace(input.SchemaName) + ? sqlGuard.DefaultSchemaName(context.DataSourceType) + : input.SchemaName; + + var qualifiedName = sqlGuard.QuoteQualifiedName(schemaName, input.Name, context.DataSourceType); + var sql = BuildSavedQuerySql(input.Kind, qualifiedName, parameters, maxRows, context.DataSourceType); + + return ToResult(await queryExecutor.ExecuteAsync(context, sql, parameters, maxRows)); + } + + private async Task RunCustomEndpointAsync( + McpSavedQueryRequestDto input, + McpServerConfigDto config) + { + var endpoints = await GetCallableEndpointEntitiesAsync(); + var endpoint = endpoints.Find(candidate => string.Equals(candidate.Name, input.Name, StringComparison.OrdinalIgnoreCase)) + ?? throw new UserFriendlyException(L[McpErrorCodes.CustomEndpointNotFound, input.Name]); + + var maxRows = ResolveMaxRows(config, input.MaxRows); + var parameters = sqlGuard.NormalizeParameters(input.Parameters); + + // Eksik parametre Dapper tarafinda belirsiz bir hataya donusmesin diye burada yakalanir. + var missing = endpoint.Parameters + .Where(parameter => parameter.IsRequired && !parameters.ContainsKey(parameter.Name)) + .Select(parameter => parameter.Name) + .ToList(); + + if (missing.Count > 0) + { + throw new UserFriendlyException(L[McpErrorCodes.MissingParameters, string.Join(", ", missing)]); + } + + var context = await queryExecutor.ResolveDataSourceAsync(endpoint.DataSourceCode); + + return ToResult(await queryExecutor.ExecuteAsync(context, endpoint.Sql, parameters, maxRows)); + } + + private async Task> GetCallableCustomEndpointsAsync() + { + var endpoints = await GetCallableEndpointEntitiesAsync(); + + return [.. endpoints.Select(endpoint => new McpSavedQueryDto + { + Kind = McpConsts.SavedQueryKinds.CustomEndpoint, + Name = endpoint.Name, + Description = endpoint.Description, + ParameterNames = [.. endpoint.Parameters.Select(parameter => parameter.Name)] + })]; + } + + /// + /// Yalnizca okuma amacli (GET) ve istegi yapan kullaniciya acik Custom Endpoint kayitlari. + /// Yetki sozlesmesi endpoint dispatcher'indaki ile birebir aynidir. + /// + private async Task> GetCallableEndpointEntitiesAsync() + { + var query = await customEndpointRepository.GetQueryableAsync(); + var candidates = query.Where(endpoint => endpoint.Method == "GET").ToList(); + + return [.. candidates.Where(endpoint => endpoint.Permissions.Any(permission => + permission.ResourceType == "Global" + || (permission.ResourceType == "User" && permission.ResourceId == CurrentUser.UserName) + || (permission.ResourceType == "Role" && CurrentUser.Roles.Contains(permission.ResourceId))))]; + } + + private string BuildSavedQuerySql( + string? kind, + string qualifiedName, + Dictionary parameters, + int maxRows, + DataSourceTypeEnum dataSourceType) + { + var parameterList = string.Join(", ", parameters.Keys.Select(name => $"@{name}")); + + if (IsKind(kind, McpConsts.SavedQueryKinds.View)) + { + return sqlGuard.ApplyRowLimit($"SELECT * FROM {qualifiedName}", maxRows, dataSourceType); + } + + if (IsKind(kind, McpConsts.SavedQueryKinds.TableFunction)) + { + return sqlGuard.ApplyRowLimit($"SELECT * FROM {qualifiedName}({parameterList})", maxRows, dataSourceType); + } + + if (IsKind(kind, McpConsts.SavedQueryKinds.ScalarFunction)) + { + return $"SELECT {qualifiedName}({parameterList}) AS Value"; + } + + if (IsKind(kind, McpConsts.SavedQueryKinds.Procedure)) + { + // Prosedur cagrisina satir tavani yazilamaz; sonuc kumesi okunurken kesilir. + return dataSourceType == DataSourceTypeEnum.Postgresql + ? $"CALL {qualifiedName}({parameterList})" + : $"EXEC {qualifiedName} {string.Join(", ", parameters.Keys.Select(name => $"@{name} = @{name}"))}".TrimEnd(); + } + + throw new UserFriendlyException(L[McpErrorCodes.UnsupportedObjectKind, kind ?? string.Empty]); + } + + private static string? NullIfEmpty(string? value) + => string.IsNullOrWhiteSpace(value) ? null : value; + + private static McpQueryResultDto ToResult(McpQueryRows rows) => new() + { + Columns = rows.Columns, + Rows = rows.Rows, + RowCount = rows.Rows.Count, + Truncated = rows.Truncated, + ExecutionTimeMs = rows.ExecutionTimeMs + }; + + private static bool IsKind(string? kind, string expected) + => string.Equals(kind, expected, StringComparison.OrdinalIgnoreCase); + + private static string? MapCatalogKind(string? catalogKind) => catalogKind switch + { + McpConsts.CatalogKinds.View => McpConsts.SavedQueryKinds.View, + McpConsts.CatalogKinds.Procedure => McpConsts.SavedQueryKinds.Procedure, + McpConsts.CatalogKinds.TableFunction => McpConsts.SavedQueryKinds.TableFunction, + McpConsts.CatalogKinds.ScalarFunction => McpConsts.SavedQueryKinds.ScalarFunction, + _ => null + }; + + private static string? ReadString(Dictionary row, string key) + => row.TryGetValue(key, out var value) ? value?.ToString() : null; +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpSchemaAppService.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpSchemaAppService.cs new file mode 100644 index 00000000..fc1efea9 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpSchemaAppService.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Sozsoft.Mcp.Dtos; +using Sozsoft.Mcp.Services; +using Sozsoft.Platform; +using Volo.Abp.Authorization.Permissions; + +namespace Sozsoft.Mcp; + +/// +/// Sema kesfini JSON veri sozlugu uzerinden karsilar; sozlugu okuma ve arama normalizasyonu +/// domain servisindedir (). Kullanici yalnizca uygulamada +/// okuma yetkisi olan listeleri gorur (). +/// +[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Invoke)] +public class McpSchemaAppService( + IMcpServerConfigProvider serverConfigProvider, + IMcpSchemaDictionary dictionary, + IMcpListFormAccessFilter accessFilter, + IPermissionChecker permissionChecker) + : McpAppService(serverConfigProvider), IMcpSchemaAppService +{ + public async Task GetServerConfigAsync() + => await ResolveServerConfigAsync(); + + public async Task> GetAvailableToolsAsync() + { + var config = await ResolveServerConfigAsync(); + var candidates = McpToolPolicy.VisibleTools( + config is not null, + config?.AllowWrite ?? false, + config?.AllowedTools ?? []); + + var required = candidates + .Where(McpToolNames.RequiredPermissions.ContainsKey) + .Select(tool => McpToolNames.RequiredPermissions[tool]) + .Distinct(StringComparer.Ordinal) + .ToArray(); + + var granted = new HashSet(StringComparer.Ordinal); + if (required.Length > 0) + { + var grants = await permissionChecker.IsGrantedAsync(required); + granted.UnionWith(grants.Result + .Where(result => result.Value == PermissionGrantResult.Granted) + .Select(result => result.Key)); + } + + return [.. candidates.Where(tool => + !McpToolNames.RequiredPermissions.TryGetValue(tool, out var permission) || granted.Contains(permission))]; + } + + public async Task GetSchemaInfoAsync() + { + var snapshot = await dictionary.GetSnapshotAsync(); + if (snapshot is null) + { + return new McpSchemaInfoDto + { + Available = false, + TenantName = dictionary.ResolveTenantName(), + Message = L[McpErrorCodes.DictionaryNotFound, McpConsts.DictionaryProcedureName] + }; + } + + return new McpSchemaInfoDto + { + Available = true, + CacheKey = snapshot.CacheKey, + RefreshedAt = snapshot.RefreshedAt, + FormCount = snapshot.FormCount, + FieldCount = snapshot.FieldCount, + TenantName = snapshot.TenantName, + DefinitionCultureName = snapshot.DefinitionCultureName, + UiCultureName = snapshot.UiCultureName + }; + } + + public async Task> GetListsAsync(string? search = null, int maxResults = 200) + { + await EnsureToolAllowedAsync(McpToolNames.ListLists); + + var forms = await GetReadableListFormsAsync(); + var normalizedSearch = dictionary.Normalize(search); + + var matches = forms.Where(form => normalizedSearch.Length == 0 + || dictionary.Normalize(form.SearchText).Contains(normalizedSearch, StringComparison.Ordinal)); + + return [.. matches.Take(NormalizeLimit(maxResults, 200)).Select(ToSummary)]; + } + + public async Task GetListAsync(string listFormCode) + { + await EnsureToolAllowedAsync(McpToolNames.DescribeList); + + if (string.IsNullOrWhiteSpace(listFormCode)) + { + return null; + } + + var forms = await GetReadableListFormsAsync(); + var form = forms.FirstOrDefault(candidate => + string.Equals(candidate.ListFormCode, listFormCode, StringComparison.OrdinalIgnoreCase)); + + if (form is null) + { + return null; + } + + var detail = new McpListDetailDto(); + CopySummary(form, detail); + detail.Columns = [.. (form.Fields ?? []).Select(ToColumn)]; + + return detail; + } + + public async Task> SearchAsync(string term, int maxResults = 50) + { + await EnsureToolAllowedAsync(McpToolNames.SearchSchema); + + var normalized = dictionary.Normalize(term); + if (normalized.Length == 0) + { + return []; + } + + var limit = NormalizeLimit(maxResults, 50); + var forms = await GetReadableListFormsAsync(); + var results = new List(); + + foreach (var form in forms) + { + var formMatches = dictionary.Normalize(form.SearchText).Contains(normalized, StringComparison.Ordinal); + + var columnMatches = (form.Fields ?? []) + .Where(field => dictionary.Normalize(field.Field).Contains(normalized, StringComparison.Ordinal) + || dictionary.Normalize(field.DisplayName).Contains(normalized, StringComparison.Ordinal)) + .Select(ToColumn) + .ToList(); + + if (!formMatches && columnMatches.Count == 0) + { + continue; + } + + results.Add(new McpSchemaMatchDto + { + ListFormCode = form.ListFormCode ?? string.Empty, + DisplayName = form.DisplayName, + TableName = NullIfEmpty(form.TableName), + MatchedColumns = columnMatches + }); + + if (results.Count >= limit) + { + break; + } + } + + return results; + } + + private async Task> GetReadableListFormsAsync() + { + var forms = await dictionary.GetListFormsAsync(); + var readable = await accessFilter.GetReadableListFormCodesAsync(); + + return [.. forms.Where(form => form.ListFormCode is not null && readable.Contains(form.ListFormCode))]; + } + + private static McpListSummaryDto ToSummary(McpDictionaryListForm form) + { + var summary = new McpListSummaryDto(); + CopySummary(form, summary); + + return summary; + } + + private static void CopySummary(McpDictionaryListForm form, McpListSummaryDto target) + { + target.ListFormCode = form.ListFormCode ?? string.Empty; + target.DisplayName = NullIfEmpty(form.DisplayName); + target.Title = NullIfEmpty(form.Title); + target.Description = NullIfEmpty(form.Description); + target.TableName = NullIfEmpty(form.TableName); + target.SqlSourceName = NullIfEmpty(form.SqlSourceName); + target.SourceKind = NullIfEmpty(form.SourceKind); + target.KeyFieldName = NullIfEmpty(form.KeyFieldName); + target.FieldCount = form.Fields?.Count ?? 0; + } + + private static McpColumnDto ToColumn(McpDictionaryField field) => new() + { + Field = field.Field ?? string.Empty, + DisplayName = NullIfEmpty(field.DisplayName), + SqlDataType = NullIfEmpty(field.SqlDataType), + IsVisible = field.IsVisible, + IsBooleanColumn = field.IsBooleanColumn, + LookupTableName = NullIfEmpty(field.LookupTableName), + LookupValueColumn = NullIfEmpty(field.LookupValueColumn), + LookupDisplayColumn = NullIfEmpty(field.LookupDisplayColumn), + IsSensitive = field.IsSensitive, + IsDefaultSelectColumn = field.IsDefaultSelectColumn + }; + + private static int NormalizeLimit(int requested, int fallback) + => requested <= 0 ? fallback : Math.Min(requested, fallback * 5); + + private static string? NullIfEmpty(string? value) + => string.IsNullOrWhiteSpace(value) ? null : value; +} 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 new file mode 100644 index 00000000..9e6c0163 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/Sozsoft.Mcp.Application.csproj @@ -0,0 +1,22 @@ + + + + + + net10.0 + enable + Sozsoft.Mcp + + + + + + + + + + + + + + diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpConsts.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpConsts.cs new file mode 100644 index 00000000..f542e4f1 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpConsts.cs @@ -0,0 +1,104 @@ +namespace Sozsoft.Mcp; + +/// MCP modulunun sabit sozlesme degerleri. +public static class McpConsts +{ + /// + /// Veri sozlugu satirlarinin turu. Sas_H_DbTableColumn tablosunda ayni deger + /// DictionaryType kolonunda durur. + /// + public const string DictionaryType = "ListFormJson"; + + /// + /// Sozluk ureten prosedurun varsayilan @TenantName degeri. Host baglaminda sozluk + /// bu adla yazilir. + /// + public const string DefaultDictionaryTenantName = "Sozsoft"; + + /// Sunucu kaydinda satir tavani verilmediginde (ya da sifir oldugunda) uygulanan deger. + public const int DefaultMaxRows = 200; + + /// + /// CustomEndpoint.DataSourceCode bu degeri tasidiginda istegi yapan kullanicinin + /// kiraci veritabani kullanilir. + /// + public const string TenantDataSourceMarker = "!Tenant"; + + /// + /// Sozluk ureten prosedurun adi. Sozluk bulunamadiginda istemciye ne yapacagi bu adla + /// bildirilir. + /// + public const string DictionaryProcedureName = "dbo.Sas_H_ListFormFieldsDictionaryJson_SaveDbTableColumns"; + + /// + /// Uzak MCP istemcilerinin (Claude, ChatGPT connector'lari) OAuth istemci kimligi. OpenIddict + /// uygulamasi DbMigrator'da OpenIddict:Applications:Platform_Mcp ayarindan seed edilir; + /// dinamik istemci kaydi yeni uygulama uretmez, bu kimligi dondurur. + /// + public const string OAuthClientId = "Platform_Mcp"; + + /// + /// Dinamik kayitla (RFC 7591) uretilen istemcilerin kimlik oneki. Bilinen saglayicilarin + /// (Claude, ChatGPT) disindaki MCP istemcileri bu onekle kendi OpenIddict uygulamasini alir. + /// + public const string DynamicClientIdPrefix = "mcp_"; + + /// + /// Dinamik istemci sayisinin tavani. Kayit ucu kimlik dogrulamasizdir; tavan, ucun + /// veritabaninda sinirsiz uygulama biriktirmesini onler. + /// + public const int MaxDynamicClients = 500; + + /// + /// MCP istemcilerinin yenileme token omru. Web istemcisinin kisa omru (dakikalar) connector + /// icin uygun degil: kullanici birkac saat soru sormadiginda baglanti kopardi. Yenileme + /// token'i her kullanimda yenilenir; omur, son kullanimdan itibaren sayilir. + /// + public static readonly System.TimeSpan RefreshTokenLifetime = System.TimeSpan.FromDays(30); + + /// Istemci kimligi bir MCP istemcisine (bilinen ya da dinamik) mi ait. + public static bool IsMcpClient(string? clientId) + => clientId is not null + && (string.Equals(clientId, OAuthClientId, System.StringComparison.Ordinal) + || clientId.StartsWith(DynamicClientIdPrefix, System.StringComparison.Ordinal)); + + /// MCP ucunun sunucu adresine gore yolu. + public const string EndpointPath = "/mcp"; + + /// Dinamik istemci kaydi (RFC 7591) ucunun yolu. + public const string RegistrationEndpointPath = "/connect/register"; + + /// Korumali kaynak meta verisi (RFC 9728) ucunun yolu. + public const string ProtectedResourceMetadataPath = "/.well-known/oauth-protected-resource"; + + /// + /// Istemcinin istemesi gereken kapsamlar. offline_access yenileme token'i icindir; + /// olmadan baglanti her saat yeniden giris ister. + /// + public static readonly string[] OAuthScopes = [Sozsoft.Platform.PlatformConsts.AppName, "offline_access"]; + + /// + /// run_query araciyla cagrilabilen nesne turleri. Degerler istemciye oldugu gibi + /// gosterilir; degistirilirse arac sozlesmesi kirilir. + /// + public static class SavedQueryKinds + { + public const string View = "view"; + public const string Procedure = "procedure"; + public const string TableFunction = "tableFunction"; + public const string ScalarFunction = "scalarFunction"; + public const string CustomEndpoint = "customEndpoint"; + } + + /// + /// Veritabanı katalog sorgusunun dondurdugu nesne turu etiketleri. + /// degerlerine eslenir. + /// + public static class CatalogKinds + { + public const string View = "VIEW"; + public const string Procedure = "PROCEDURE"; + public const string TableFunction = "TABLE_FUNCTION"; + public const string ScalarFunction = "SCALAR_FUNCTION"; + } +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpDomainSharedModule.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpDomainSharedModule.cs new file mode 100644 index 00000000..815b39b5 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpDomainSharedModule.cs @@ -0,0 +1,18 @@ +using Volo.Abp.Domain; +using Volo.Abp.Modularity; +using Volo.Abp.Validation; + +namespace Sozsoft.Mcp; + +/// +/// Modulun paylasilan sozlesmesi. Kullaniciya gorunen metinler platformun kendi +/// PlatformResource kaynagindan (LanguagesData.json) okunur; modul ikinci bir dil +/// kaynagi acmaz. +/// +[DependsOn( + typeof(AbpValidationModule), + typeof(AbpDddDomainSharedModule) +)] +public class McpDomainSharedModule : AbpModule +{ +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs new file mode 100644 index 00000000..43f73334 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpErrorCodes.cs @@ -0,0 +1,28 @@ +namespace Sozsoft.Mcp; + +/// +/// MCP hatalarinin dil anahtarlari. Karsiliklari platformun LanguagesData.json +/// dosyasinda EN + TR olarak durur; koda gomulu metin kullanilmaz. +/// +public static class McpErrorCodes +{ + private const string Prefix = "App.Mcp.Error."; + + public const string NoServerRecord = Prefix + "NoServerRecord"; + public const string ToolDisabled = Prefix + "ToolDisabled"; + public const string EmptyQuery = Prefix + "EmptyQuery"; + public const string SingleStatementOnly = Prefix + "SingleStatementOnly"; + public const string ReadOnlyStatementRequired = Prefix + "ReadOnlyStatementRequired"; + public const string ForbiddenKeyword = Prefix + "ForbiddenKeyword"; + public const string InvalidIdentifier = Prefix + "InvalidIdentifier"; + public const string InvalidParameterName = Prefix + "InvalidParameterName"; + public const string UnsupportedObjectKind = Prefix + "UnsupportedObjectKind"; + public const string CustomEndpointNotFound = Prefix + "CustomEndpointNotFound"; + public const string MissingParameters = Prefix + "MissingParameters"; + public const string ListFormCodeRequired = Prefix + "ListFormCodeRequired"; + public const string ValuesRequired = Prefix + "ValuesRequired"; + public const string KeysRequired = Prefix + "KeysRequired"; + public const string DictionaryNotFound = Prefix + "DictionaryNotFound"; + public const string RegistrationLimitReached = Prefix + "RegistrationLimitReached"; + public const string RedirectUriNotAllowed = Prefix + "RedirectUriNotAllowed"; +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs new file mode 100644 index 00000000..2c6f77e1 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolNames.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.Linq; +using Sozsoft.Platform; + +namespace Sozsoft.Mcp; + +/// +/// /mcp ucunda yayinlanan arac adlari. Ayni degerler MCP Servers ekranindaki +/// AllowedTools alaninin secenek listesini besler; buraya bir arac eklenirse ekranin +/// statik lookup listesi de guncellenir. +/// +public static class McpToolNames +{ + public const string ServerInfo = "server_info"; + public const string ListLists = "list_lists"; + public const string DescribeList = "describe_list"; + public const string SearchSchema = "search_schema"; + public const string QueryList = "query_list"; + public const string RunSelect = "run_select"; + public const string ListQueries = "list_queries"; + public const string RunQuery = "run_query"; + public const string InsertRow = "insert_row"; + public const string UpdateRow = "update_row"; + public const string DeleteRow = "delete_row"; + + /// + /// Veri yazan araclar. Sunucu kaydinda AllowWrite kapaliysa bunlar arac listesinde + /// hic yer almaz. + /// + public static readonly string[] WriteTools = [InsertRow, UpdateRow, DeleteRow]; + + /// + /// Sunucu kaydi olmasa da acik kalan araclar. Istemcinin baglantisinin neden bos + /// gorundugunu anlayabilmesi icin gereklidir. + /// + public static readonly string[] AlwaysAvailableTools = [ServerInfo]; + + /// + /// 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. + /// + public static readonly IReadOnlyDictionary RequiredPermissions = new Dictionary + { + [RunSelect] = PlatformConsts.AppCodes.SqlQueryManagers.Default, + [InsertRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, + [UpdateRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, + [DeleteRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write, + }; + + /// Yayinlanan tum araclar, arac listesindeki siralarina gore. + public static readonly string[] All = + [ + ServerInfo, + ListLists, + DescribeList, + SearchSchema, + QueryList, + RunSelect, + ListQueries, + RunQuery, + InsertRow, + UpdateRow, + DeleteRow + ]; + + /// + /// AllowedTools alaninda secilebilen araclar. Her zaman acik olanlar secenek + /// olarak sunulmaz; onlari kapatmak mumkun degildir. + /// + public static IEnumerable SelectableTools + => All.Where(name => !AlwaysAvailableTools.Contains(name)); +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolPolicy.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolPolicy.cs new file mode 100644 index 00000000..73861c20 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpToolPolicy.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Sozsoft.Mcp; + +/// +/// Bir aracin bir sunucu kaydina gore acik olup olmadigina karar veren kural. Karar iki yerde +/// kullanilir: arac listesi suzulurken ve arac cagrilirken. Listeyi suzmek tek basina bir +/// yetki siniri degildir — istemci listede olmayan bir araci da cagirabilir. +/// +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. + public static bool IsAllowed( + bool hasServerRecord, + bool allowWrite, + IReadOnlyCollection allowedTools, + string toolName) + { + if (McpToolNames.AlwaysAvailableTools.Contains(toolName, StringComparer.Ordinal)) + { + return true; + } + + if (!hasServerRecord) + { + return false; + } + + if (!allowWrite && McpToolNames.WriteTools.Contains(toolName, StringComparer.Ordinal)) + { + return false; + } + + if (allowedTools.Count == 0) + { + return true; + } + + return allowedTools.Contains(toolName, StringComparer.OrdinalIgnoreCase); + } + + public static IReadOnlyList VisibleTools( + bool hasServerRecord, + bool allowWrite, + IReadOnlyCollection allowedTools) + => [.. McpToolNames.All.Where(name => IsAllowed(hasServerRecord, allowWrite, allowedTools, name))]; +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/Sozsoft.Mcp.Domain.Shared.csproj b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/Sozsoft.Mcp.Domain.Shared.csproj new file mode 100644 index 00000000..e9dc83d7 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/Sozsoft.Mcp.Domain.Shared.csproj @@ -0,0 +1,22 @@ + + + + + + net10.0 + enable + Sozsoft.Mcp + + + + + + + + + + + + + diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Data/McpOAuthClientDataSeedContributor.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Data/McpOAuthClientDataSeedContributor.cs new file mode 100644 index 00000000..74114c52 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Data/McpOAuthClientDataSeedContributor.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Sozsoft.Mcp.Services; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; + +namespace Sozsoft.Mcp.Data; + +/// +/// Bilinen MCP istemcisini () seed eder. Geri donus +/// adresleri OpenIddict:Applications:Platform_Mcp:RedirectUris ayarindadir; yeni bir +/// saglayici eklemek bu listeye callback eklemektir. +/// +public class McpOAuthClientDataSeedContributor( + IConfiguration configuration, + IMcpOAuthClientManager clientManager) : IDataSeedContributor, ITransientDependency +{ + private const string RedirectUrisKey = "OpenIddict:Applications:Platform_Mcp:RedirectUris"; + + public async Task SeedAsync(DataSeedContext context) + { + // OpenIddict uygulamalari host kapsamindadir; kiraci seed turlarinda tekrarlanmaz. + if (context.TenantId is not null) + { + return; + } + + var redirectUris = configuration.GetSection(RedirectUrisKey).Get(); + if (redirectUris is null || redirectUris.Length == 0) + { + return; + } + + await clientManager.EnsureKnownClientAsync(redirectUris); + } +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Entities/McpServer.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Entities/McpServer.cs new file mode 100644 index 00000000..be26c671 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Entities/McpServer.cs @@ -0,0 +1,49 @@ +using System; +using Volo.Abp.Domain.Entities.Auditing; + +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. +/// +public class McpServer : FullAuditedEntity +{ + public string Name { get; set; } = string.Empty; + + public bool IsActive { get; set; } = true; + + /// + /// Istemciye verilen baglanti adresi (ornek: https://api.sozsoft.com/mcp). Yonlendirme icin + /// kullanilmaz; ekranin kopyalanabilir baglanti komutu uretebilmesi icin tutulur. + /// + public string? Url { get; set; } + + /// + /// Kaydin gecerli oldugu kiraci adlari, PlatformConsts.MultiValueDelimiter ile + /// ayrilir. Bos ise tum kiracilara aciktir. + /// + 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. + /// + 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/McpDomainModule.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/McpDomainModule.cs new file mode 100644 index 00000000..7e17812d --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/McpDomainModule.cs @@ -0,0 +1,14 @@ +using Volo.Abp.Domain; +using Volo.Abp.Modularity; +using Volo.Abp.OpenIddict; + +namespace Sozsoft.Mcp; + +[DependsOn( + typeof(AbpDddDomainModule), + typeof(AbpOpenIddictDomainModule), + typeof(McpDomainSharedModule) +)] +public class McpDomainModule : AbpModule +{ +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpListFormAccessFilter.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpListFormAccessFilter.cs new file mode 100644 index 00000000..8753e44a --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpListFormAccessFilter.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Sozsoft.Platform.Entities; +using Sozsoft.Platform.Permissions; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Domain.Services; + +namespace Sozsoft.Mcp.Services; + +/// +/// Kullanicinin okuyabildigi listeleri belirler. Karar ekranin kendisiyle aynidir: ListForm'un +/// PermissionJson.R yetkisi (bkz. ListFormAuthorizationManager). MCP bu kurala +/// ek bir yetki tanimlamaz; uygulamada acamadigin listeyi MCP'de de goremezsin. +/// +public interface IMcpListFormAccessFilter +{ + Task> GetReadableListFormCodesAsync(); +} + +public class McpListFormAccessFilter( + IRepository listFormRepository, + IPermissionChecker permissionChecker) + : DomainService, IMcpListFormAccessFilter +{ + public async Task> GetReadableListFormCodesAsync() + { + // ListFormAuthorizationManager.CanAccess liste basina bir sorgu ve bir yetki kontrolu + // yapar; sozlukteki yuzlerce liste icin bu tek sorgu + tek toplu kontrole indirilir. + var query = await listFormRepository.GetQueryableAsync(); + var definitions = query + .Select(listForm => new { listForm.ListFormCode, listForm.PermissionJson }) + .ToList() + .GroupBy(definition => definition.ListFormCode, StringComparer.OrdinalIgnoreCase) + .Select(group => group.First()) + .ToList(); + + var readPermissions = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var definition in definitions) + { + var permission = ReadPermissionOf(definition.PermissionJson); + if (permission is not null) + { + readPermissions[definition.ListFormCode] = permission; + } + } + + var distinctPermissions = readPermissions.Values.Distinct(StringComparer.Ordinal).ToArray(); + if (distinctPermissions.Length == 0) + { + return new HashSet(StringComparer.OrdinalIgnoreCase); + } + + var grants = await permissionChecker.IsGrantedAsync(distinctPermissions); + var granted = grants.Result + .Where(result => result.Value == PermissionGrantResult.Granted) + .Select(result => result.Key) + .ToHashSet(StringComparer.Ordinal); + + return readPermissions + .Where(entry => granted.Contains(entry.Value)) + .Select(entry => entry.Key) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + } + + private static string? ReadPermissionOf(string? permissionJson) + { + if (string.IsNullOrWhiteSpace(permissionJson)) + { + return null; + } + + try + { + var permissions = JsonSerializer.Deserialize(permissionJson); + + return string.IsNullOrWhiteSpace(permissions?.R) ? null : permissions.R; + } + catch (JsonException) + { + // Bozuk yetki tanimi ekranda da erisimi kapatir (CanAccess false doner). + return null; + } + } +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpOAuthClientManager.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpOAuthClientManager.cs new file mode 100644 index 00000000..a5c6cee7 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpOAuthClientManager.cs @@ -0,0 +1,246 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using OpenIddict.Abstractions; +using Sozsoft.Platform; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Domain.Services; +using Volo.Abp.MultiTenancy; +using Volo.Abp.OpenIddict.Applications; + +namespace Sozsoft.Mcp.Services; + +/// +/// Uzak MCP istemcilerinin OpenIddict uygulamalari. Iki tur istemci vardir: +/// +/// Bilinen istemci (): geri donus adresleri +/// DbMigrator ayarinda sabit olan saglayicilar (Claude, ChatGPT). Onay ekrani gostermez. +/// Dinamik istemci (): baska bir MCP +/// istemcisi (Cursor, VS Code, Claude Code, ...) kayit ucundan kendi uygulamasini alir. Kayit +/// herkese acik oldugu icin bu istemciler acik onay ister: kullanici giris yaptiktan sonra +/// hangi uygulamanin erisim istedigini gorup onaylar. +/// +/// Iki turde de token kullanicinin kendi yetkileriyle uretilir; istemci yetki kazanmaz. +/// +public interface IMcpOAuthClientManager +{ + /// Bilinen istemciyi olusturur ya da geri donus adreslerini gunceller. + Task EnsureKnownClientAsync(IReadOnlyCollection redirectUris); + + /// + /// Istemci kaydi. Adresler bilinen istemcide kayitliysa onu, degilse ayni adres kumesine + /// sahip dinamik istemciyi dondurur; yoksa yenisini uretir. + /// + Task RegisterAsync(string? clientName, IReadOnlyCollection redirectUris); +} + +public class McpOAuthClientManager( + IAbpApplicationManager applicationManager, + IRepository applicationRepository, + ICurrentTenant currentTenant) + : DomainService, IMcpOAuthClientManager +{ + private const int MaxClientNameLength = 100; + + /// Tarayici disi calistirilabilir icerik tasiyan, geri donus adresi olamayacak semalar. + private static readonly string[] ForbiddenSchemes = ["javascript", "data", "file", "vbscript", "about", "blob"]; + + public async Task EnsureKnownClientAsync(IReadOnlyCollection redirectUris) + { + // OpenIddict uygulamalari host kapsamindadir. + using (currentTenant.Change(null)) + { + var existing = await applicationManager.FindByClientIdAsync(McpConsts.OAuthClientId); + var descriptor = BuildDescriptor( + McpConsts.OAuthClientId, + "MCP Connectors", + redirectUris, + OpenIddictConstants.ConsentTypes.Implicit); + + if (existing is null) + { + await applicationManager.CreateAsync(descriptor); + return; + } + + // Tanim tamamen dosyadan gelir; ayar degistiyse (yeni saglayici callback'i, token omru) + // kayit yeniden yazilir. Kimlik ve istemci turu degismez. + await applicationManager.PopulateAsync(existing, descriptor); + await applicationManager.UpdateAsync(existing); + } + } + + public async Task RegisterAsync(string? clientName, IReadOnlyCollection redirectUris) + { + if (redirectUris.Count == 0) + { + return McpClientRegistration.Rejected(McpClientRegistrationError.InvalidRedirectUri, string.Empty); + } + + var invalid = redirectUris.FirstOrDefault(uri => !IsAcceptableRedirectUri(uri)); + if (invalid is not null) + { + return McpClientRegistration.Rejected(McpClientRegistrationError.InvalidRedirectUri, invalid); + } + + using (currentTenant.Change(null)) + { + var known = await applicationManager.FindByClientIdAsync(McpConsts.OAuthClientId); + if (known is not null) + { + var knownUris = await applicationManager.GetRedirectUrisAsync(known); + if (redirectUris.All(uri => knownUris.Any(registered => SameUri(registered, uri)))) + { + return McpClientRegistration.Accepted(McpConsts.OAuthClientId); + } + } + + var dynamicClients = await GetDynamicClientsAsync(); + + // Ayni istemci her baglantida yeniden kayit olabilir; ayni adres kumesi tek uygulamaya + // baglanir, tablo buyumez. + var match = dynamicClients.FirstOrDefault(client => SameUriSet(client.RedirectUris, redirectUris)); + if (match.ClientId is not null) + { + return McpClientRegistration.Accepted(match.ClientId); + } + + if (dynamicClients.Count >= McpConsts.MaxDynamicClients) + { + return McpClientRegistration.Rejected(McpClientRegistrationError.LimitReached, McpConsts.MaxDynamicClients.ToString(CultureInfo.InvariantCulture)); + } + + var clientId = McpConsts.DynamicClientIdPrefix + GuidGenerator.Create().ToString("N"); + await applicationManager.CreateAsync(BuildDescriptor( + clientId, + DisplayNameOf(clientName, redirectUris.First()), + redirectUris, + OpenIddictConstants.ConsentTypes.Explicit)); + + return McpClientRegistration.Accepted(clientId); + } + } + + private async Task RedirectUris)>> GetDynamicClientsAsync() + { + var query = await applicationRepository.GetQueryableAsync(); + var rows = query + .Where(application => application.ClientId!.StartsWith(McpConsts.DynamicClientIdPrefix)) + .Select(application => new { application.ClientId, application.RedirectUris }) + .ToList(); + + return [.. rows.Select(row => ((string?)row.ClientId, ParseUris(row.RedirectUris)))]; + } + + private static AbpApplicationDescriptor BuildDescriptor( + string clientId, + string displayName, + IEnumerable redirectUris, + string consentType) + { + var descriptor = new AbpApplicationDescriptor + { + ClientId = clientId, + ClientType = OpenIddictConstants.ClientTypes.Public, + ConsentType = consentType, + DisplayName = displayName + }; + + descriptor.Permissions.UnionWith( + [ + OpenIddictConstants.Permissions.Endpoints.Authorization, + OpenIddictConstants.Permissions.Endpoints.Token, + OpenIddictConstants.Permissions.Endpoints.Revocation, + OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode, + OpenIddictConstants.Permissions.GrantTypes.RefreshToken, + OpenIddictConstants.Permissions.ResponseTypes.Code, + OpenIddictConstants.Permissions.Prefixes.Scope + PlatformConsts.AppName + ]); + + // Genel istemci gizli anahtar tasiyamaz; yetkilendirme kodunun calinmasina karsi PKCE + // zorunludur. + descriptor.Requirements.Add(OpenIddictConstants.Requirements.Features.ProofKeyForCodeExchange); + + descriptor.Settings[OpenIddictConstants.Settings.TokenLifetimes.RefreshToken] = + McpConsts.RefreshTokenLifetime.ToString("c", CultureInfo.InvariantCulture); + + foreach (var redirectUri in redirectUris) + { + descriptor.RedirectUris.Add(new Uri(redirectUri, UriKind.Absolute)); + } + + return descriptor; + } + + /// + /// Kabul edilen geri donus adresleri: HTTPS, yerel makineye donen HTTP (RFC 8252 §7.3 — + /// masaustu istemciler rastgele portla dinler) ve masaustu uygulamalarin ozel semalari + /// (RFC 8252 §7.1, ornek: cursor://...). Yerel olmayan duz HTTP reddedilir. + /// + private static bool IsAcceptableRedirectUri(string value) + { + if (!Uri.TryCreate(value, UriKind.Absolute, out var uri) || !string.IsNullOrEmpty(uri.Fragment)) + { + return false; + } + + if (uri.Scheme == Uri.UriSchemeHttps) + { + return true; + } + + if (uri.Scheme == Uri.UriSchemeHttp) + { + return uri.IsLoopback; + } + + return !ForbiddenSchemes.Contains(uri.Scheme, StringComparer.OrdinalIgnoreCase); + } + + /// + /// Onay ekraninda gorunen ad. Istemci adi kayit isteginden gelir ve dogrulanamaz (herkes + /// kendine "Claude" diyebilir); bu yuzden token'in gidecegi adresin sahibi de gosterilir. + /// + private static string DisplayNameOf(string? clientName, string redirectUri) + { + var name = string.IsNullOrWhiteSpace(clientName) ? "MCP Client" : clientName.Trim(); + if (name.Length > MaxClientNameLength) + { + name = name[..MaxClientNameLength]; + } + + var uri = new Uri(redirectUri, UriKind.Absolute); + var owner = uri.IsLoopback ? "localhost" : string.IsNullOrEmpty(uri.Host) ? uri.Scheme : uri.Host; + + return $"{name} ({owner})"; + } + + private static IReadOnlyList ParseUris(string? json) + => string.IsNullOrWhiteSpace(json) ? [] : JsonSerializer.Deserialize>(json) ?? []; + + private static bool SameUriSet(IReadOnlyCollection left, IReadOnlyCollection right) + => left.Count == right.Count && right.All(uri => left.Any(candidate => SameUri(candidate, uri))); + + /// OpenIddict adresleri bicimiyle saklar; karsilastirma ayni bicimle yapilir. + private static bool SameUri(string left, string right) + => Uri.TryCreate(left, UriKind.Absolute, out var a) + && Uri.TryCreate(right, UriKind.Absolute, out var b) + && string.Equals(a.AbsoluteUri, b.AbsoluteUri, StringComparison.Ordinal); +} + +public enum McpClientRegistrationError +{ + InvalidRedirectUri, + LimitReached +} + +/// Kayit sonucu: basariliysa istemci kimligi, degilse hata ve hataya konu deger. +public sealed record McpClientRegistration(string? ClientId, McpClientRegistrationError? Error, string? ErrorValue) +{ + public static McpClientRegistration Accepted(string clientId) => new(clientId, null, null); + + public static McpClientRegistration Rejected(McpClientRegistrationError error, string value) => new(null, error, value); +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpQueryExecutor.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpQueryExecutor.cs new file mode 100644 index 00000000..a4861b18 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpQueryExecutor.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Sozsoft.Platform.Data.Seeds; +using Sozsoft.Platform.DynamicData; +using Sozsoft.Platform.Enums; +using Volo.Abp.Domain.Services; + +namespace Sozsoft.Mcp.Services; + +/// +/// Sorguyu istegin kapsamindaki veri kaynaginda kosturur ve satir tavanini uygular. +/// Veri kaynagi cozumu tek yerde kalsin diye burada toplanmistir. +/// +public interface IMcpQueryExecutor +{ + /// + /// Sunucu kaydindaki kod. Bos ya da !Tenant ise veri kaynagi istegin kiracisindan + /// cozulur; host baglaminda varsayilan kaynaga duser. + /// + Task ResolveDataSourceAsync(string? dataSourceCode); + + /// + /// Sorguyu kosturur. Sonuc kumesi satirda kesilir; kesilme + /// olduysa sonuc bunu bildirir. + /// + Task ExecuteAsync( + McpDataSourceContext context, + string sql, + IReadOnlyDictionary parameters, + int maxRows); +} + +public class McpQueryExecutor(IDynamicDataManager dynamicDataManager) : DomainService, IMcpQueryExecutor +{ + public async Task ResolveDataSourceAsync(string? dataSourceCode) + { + var useTenantScope = string.IsNullOrWhiteSpace(dataSourceCode) + || dataSourceCode == McpConsts.TenantDataSourceMarker; + + var resolvedCode = useTenantScope + ? SeedConsts.DataSources.DefaultCode + : dataSourceCode!; + + var (repository, connectionString, dataSourceType) = + await dynamicDataManager.GetAsync(useTenantScope, resolvedCode); + + return new McpDataSourceContext(repository, connectionString, dataSourceType); + } + + public async Task ExecuteAsync( + McpDataSourceContext context, + string sql, + IReadOnlyDictionary parameters, + int maxRows) + { + var stopwatch = Stopwatch.StartNew(); + var rows = await context.Repository.QueryAsync( + sql, + context.ConnectionString, + parameters.ToDictionary(entry => entry.Key, entry => entry.Value)); + stopwatch.Stop(); + + var columns = new List(); + var materialized = new List>(); + var truncated = false; + + foreach (var row in rows) + { + if (materialized.Count >= maxRows) + { + truncated = true; + break; + } + + if (row is not IDictionary values) + { + continue; + } + + if (columns.Count == 0) + { + columns = [.. values.Keys]; + } + + materialized.Add(values.ToDictionary(entry => entry.Key, entry => (object?)entry.Value)); + } + + return new McpQueryRows(columns, materialized, truncated, stopwatch.ElapsedMilliseconds); + } +} + +/// Cozumlenmis veri kaynagi: hangi depo, hangi baglanti, hangi saglayici. +public sealed record McpDataSourceContext( + IDynamicDataRepository Repository, + string ConnectionString, + DataSourceTypeEnum DataSourceType); + +/// Sorgu sonucunun ham hali. +public sealed record McpQueryRows( + List Columns, + List> Rows, + bool Truncated, + long ExecutionTimeMs); diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpSchemaDictionary.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpSchemaDictionary.cs new file mode 100644 index 00000000..461fca90 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpSchemaDictionary.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Sozsoft.Platform.Entities; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Domain.Services; +using Volo.Abp.MultiTenancy; + +namespace Sozsoft.Mcp.Services; + +/// +/// Sema bilgisinin tek kaynagi: Sas_H_DbTableColumn tablosundaki JSON veri sozlugu. +/// Sozluk kullaniciya gorunen basliklari ve lookup iliskilerini birlikte tasidigi icin canli +/// katalog yerine bu kullanilir — modelin gordugu adlar ekranda gorunen adlarla ayni olur. +/// +public interface IMcpSchemaDictionary +{ + /// Sozlugun bulunup bulunmadigi ve ne zaman tazelendigi; yoksa null. + Task GetSnapshotAsync(); + + /// Sozlukteki listeler; sozluk yoksa bos liste. + Task> GetListFormsAsync(); + + /// + /// Istegin kapsamina dusen sozluk kiraci adi. Sozluk ureten prosedurun + /// @TenantName parametresiyle ayni degeri tasimasi gerekir. + /// + string ResolveTenantName(); + + /// + /// Aksan ve buyuk/kucuk harf duyarsiz karsilastirma anahtari; "Musteri" aramasi + /// "Müşteri" basligini da bulur. + /// + string Normalize(string? value); +} + +public class McpSchemaDictionary( + IRepository dictionaryRepository, + ICurrentTenant currentTenant) + : DomainService, IMcpSchemaDictionary +{ + private static readonly JsonSerializerOptions DictionaryJsonOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + private static readonly CultureInfo TurkishCulture = CultureInfo.GetCultureInfo("tr-TR"); + + public string ResolveTenantName() + => currentTenant.Name ?? McpConsts.DefaultDictionaryTenantName; + + public async Task GetSnapshotAsync() + { + // Tazelik bilgisi icin JsonData (buyuk sozluklerde megabaytlar) okunmaz. + using (currentTenant.Change(null)) + { + var query = await ActiveRowsAsync(ResolveTenantName()); + + return query + .Select(row => new McpDictionarySnapshot( + row.CacheKey, + row.RefreshedAt, + row.FormCount, + row.FieldCount, + row.TenantName, + row.DefinitionCultureName, + row.UiCultureName)) + .FirstOrDefault(); + } + } + + public async Task> GetListFormsAsync() + { + var json = await GetJsonDataAsync(); + if (string.IsNullOrWhiteSpace(json)) + { + return []; + } + + var document = JsonSerializer.Deserialize(json, DictionaryJsonOptions); + + return document?.ListForms ?? []; + } + + public string Normalize(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return string.Empty; + } + + var decomposed = value.ToLower(TurkishCulture).Normalize(NormalizationForm.FormD); + var builder = new StringBuilder(decomposed.Length); + + foreach (var character in decomposed) + { + if (CharUnicodeInfo.GetUnicodeCategory(character) == UnicodeCategory.NonSpacingMark) + { + continue; + } + + builder.Append(character switch + { + 'ı' => 'i', + 'ğ' => 'g', + 'ş' => 's', + _ => character + }); + } + + return builder.ToString(); + } + + private async Task GetJsonDataAsync() + { + // Sozluk host kapsamli bir tabloda tutulur; kiraci ayrimi TenantName kolonundadir. + using (currentTenant.Change(null)) + { + var query = await ActiveRowsAsync(ResolveTenantName()); + + return query.Select(row => row.JsonData).FirstOrDefault(); + } + } + + private async Task> ActiveRowsAsync(string tenantName) + { + var query = await dictionaryRepository.GetQueryableAsync(); + + return query + .Where(row => row.DictionaryType == McpConsts.DictionaryType + && row.IsActive + && row.TenantName == tenantName) + .OrderByDescending(row => row.RefreshedAt); + } + + /// Sozluk JSON'unun kok nesnesi. + private sealed class DictionaryDocument + { + public List? ListForms { get; set; } + } +} + +/// Sozluk satirinin tazelik bilgisi. +public sealed record McpDictionarySnapshot( + string? CacheKey, + DateTime RefreshedAt, + int FormCount, + int FieldCount, + string? TenantName, + string? DefinitionCultureName, + string? UiCultureName); + +/// +/// Sozlukteki bir liste. Alan adlari dbo.Sas_H_ListFormFieldsDictionaryJson prosedurunun +/// urettigi JSON ile birebir ayni olmak zorundadir. +/// +public class McpDictionaryListForm +{ + public string? ListFormCode { get; set; } + public string? DisplayName { get; set; } + public string? Title { get; set; } + public string? Description { get; set; } + public string? TableName { get; set; } + public string? SqlSourceName { get; set; } + public string? SourceKind { get; set; } + public string? KeyFieldName { get; set; } + public string? SearchText { get; set; } + public List? Fields { get; set; } +} + +/// Sozlukteki bir sutun. +public class McpDictionaryField +{ + public string? Field { get; set; } + public string? DisplayName { get; set; } + public string? SqlDataType { get; set; } + public bool IsVisible { get; set; } + public bool IsBooleanColumn { get; set; } + public string? LookupTableName { get; set; } + public string? LookupValueColumn { get; set; } + public string? LookupDisplayColumn { get; set; } + public bool IsSensitive { get; set; } + public bool IsDefaultSelectColumn { get; set; } +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpServerConfigProvider.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpServerConfigProvider.cs new file mode 100644 index 00000000..9210734f --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpServerConfigProvider.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Sozsoft.Mcp.Entities; +using Sozsoft.Platform; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Domain.Services; +using Volo.Abp.MultiTenancy; + +namespace Sozsoft.Mcp.Services; + +/// +/// Istegin kiracisina dusen etkin sunucu kaydini cozer. +/// +public interface IMcpServerConfigProvider +{ + /// Etkin kayit; kiraciya acik aktif bir kayit yoksa null. + Task GetEffectiveServerAsync(); + + /// Coklu deger tasiyan kolonu (kiraci/arac listesi) ogelerine ayirir. + IReadOnlyList SplitMultiValue(string? value); +} + +public class McpServerConfigProvider( + IRepository serverRepository, + ICurrentTenant currentTenant) + : DomainService, IMcpServerConfigProvider +{ + public async Task GetEffectiveServerAsync() + { + var tenantName = currentTenant.Name; + + // Sunucu tanimlari host kapsaminda yasar; kiraci baglamindaki istek de ayni tabloyu + // okur, hangi kiracilara acik oldugu Tenants kolonunda durur. + List servers; + using (currentTenant.Change(null)) + { + var query = await serverRepository.GetQueryableAsync(); + servers = [.. query.Where(server => server.IsActive).OrderBy(server => server.Name)]; + } + + return servers.Find(server => IsAvailableForTenant(server.Tenants, tenantName)); + } + + public IReadOnlyList SplitMultiValue(string? value) + => string.IsNullOrWhiteSpace(value) + ? [] + : [.. value.Split( + PlatformConsts.MultiValueDelimiter, + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)]; + + /// + /// Kaydin kiraci listesi bos ise tum kiracilara aciktir; doluysa kiraci adi listede + /// gecmelidir (AiBot ekraniyla ayni sozlesme). + /// + private bool IsAvailableForTenant(string? tenants, string? tenantName) + { + var allowed = SplitMultiValue(tenants); + if (allowed.Count == 0) + { + return true; + } + + return tenantName is not null + && allowed.Any(name => string.Equals(name, tenantName, StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpSqlGuard.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpSqlGuard.cs new file mode 100644 index 00000000..33d7a83e --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Services/McpSqlGuard.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Microsoft.Extensions.Localization; +using Sozsoft.Platform.Enums; +using Sozsoft.Platform.Localization; +using Volo.Abp; +using Volo.Abp.Domain.Services; + +namespace Sozsoft.Mcp.Services; + +/// +/// Salt-okunur sorgu kurali. Hem serbest SELECT hem kayitli nesne cagrisi buradan gecer: +/// ifadenin gercekten okuma oldugu dogrulanir, tanimlayici ve parametre adlari kalibi gecer, +/// satir tavani sorguya yazilir. +/// +public interface IMcpSqlGuard +{ + /// Tek bir okuma ifadesi oldugunu dogrular; degilse anlasilir bir hata atar. + void EnsureReadOnlyStatement(string sql); + + /// Sema/nesne adini dogrular ve saglayiciya gore tirnaklar. + string QuoteQualifiedName(string schemaName, string objectName, DataSourceTypeEnum dataSourceType); + + /// Saglayicinin varsayilan semasi. + string DefaultSchemaName(DataSourceTypeEnum dataSourceType); + + /// + /// Satir tavanini sorgunun kendisine yazar: SQL Server'da TOP (n), PostgreSQL'de + /// LIMIT n. Sorgu kendi sinirini zaten tasiyorsa dokunulmaz. + /// + string ApplyRowLimit(string sql, int maxRows, DataSourceTypeEnum dataSourceType); + + /// + /// Istemciden gelen parametre adlarini dogrular ve bastaki @ isaretini duserek Dapper'in + /// bekledigi bicime cevirir. + /// + Dictionary NormalizeParameters(IReadOnlyDictionary? parameters); +} + +public class McpSqlGuard(IStringLocalizer localizer) : DomainService, IMcpSqlGuard +{ + /// Tek parcali SQL tanimlayicisi; sema ve nesne adlari bu kalibi gecmek zorundadir. + private static readonly Regex SafeIdentifierRegex = new( + @"^[A-Za-z_][A-Za-z0-9_]{0,127}$", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + private static readonly Regex SafeParameterNameRegex = new( + @"^@?[A-Za-z_][A-Za-z0-9_]{0,127}$", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + private static readonly Regex SqlCommentRegex = new( + @"--[^\r\n]*|/\*[\s\S]*?\*/", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + private static readonly Regex SqlStringLiteralRegex = new( + @"'(?:[^']|'')*'", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + /// + /// Okuma sozde kalmasi gereken ifadelerde yasakli anahtar kelimeler. Kontrol yorumlar ve + /// metin sabitleri cikarildiktan sonra yapilir; aksi halde bir yorum blogu ya da tirnak + /// icindeki metin yasakli bir kelimeyi gizleyebilir. + /// + private static readonly Regex ForbiddenStatementRegex = new( + @"\b(INSERT|UPDATE|DELETE|MERGE|UPSERT|CREATE|ALTER|DROP|TRUNCATE|GRANT|REVOKE|DENY|BACKUP|RESTORE|SHUTDOWN|RECONFIGURE|EXEC|EXECUTE|WAITFOR|OPENROWSET|OPENDATASOURCE|OPENQUERY|BULK|INTO)\b", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + private static readonly Regex SelectPrefixRegex = new( + @"^\s*SELECT\s+(?:(?:DISTINCT|ALL)\s+)?", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + private static readonly Regex ExistingTopRegex = new( + @"^\s*SELECT\s+(?:(?:DISTINCT|ALL)\s+)?TOP\s*[(\s]", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + private static readonly Regex ExistingLimitRegex = new( + @"\bLIMIT\s+\d+", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + public void EnsureReadOnlyStatement(string sql) + { + if (string.IsNullOrWhiteSpace(sql)) + { + throw new UserFriendlyException(localizer[McpErrorCodes.EmptyQuery]); + } + + var stripped = SqlStringLiteralRegex.Replace(SqlCommentRegex.Replace(sql, " "), "''"); + + if (stripped.Contains(';', StringComparison.Ordinal)) + { + throw new UserFriendlyException(localizer[McpErrorCodes.SingleStatementOnly]); + } + + var trimmed = stripped.TrimStart(); + var startsWithRead = trimmed.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase) + || trimmed.StartsWith("WITH", StringComparison.OrdinalIgnoreCase); + + if (!startsWithRead) + { + throw new UserFriendlyException(localizer[McpErrorCodes.ReadOnlyStatementRequired]); + } + + var forbidden = ForbiddenStatementRegex.Match(stripped); + if (forbidden.Success) + { + throw new UserFriendlyException( + localizer[McpErrorCodes.ForbiddenKeyword, forbidden.Value.ToUpperInvariant()]); + } + } + + public string QuoteQualifiedName(string schemaName, string objectName, DataSourceTypeEnum dataSourceType) + { + EnsureSafeIdentifier(schemaName); + EnsureSafeIdentifier(objectName); + + return dataSourceType == DataSourceTypeEnum.Postgresql + ? $"\"{schemaName}\".\"{objectName}\"" + : $"[{schemaName}].[{objectName}]"; + } + + public string DefaultSchemaName(DataSourceTypeEnum dataSourceType) + => dataSourceType == DataSourceTypeEnum.Postgresql ? "public" : "dbo"; + + public string ApplyRowLimit(string sql, int maxRows, DataSourceTypeEnum dataSourceType) + { + if (dataSourceType == DataSourceTypeEnum.Postgresql) + { + return ExistingLimitRegex.IsMatch(sql) ? sql : $"{sql} LIMIT {maxRows}"; + } + + if (ExistingTopRegex.IsMatch(sql)) + { + return sql; + } + + var match = SelectPrefixRegex.Match(sql); + + // WITH ile baslayan ya da beklenmeyen bicimdeki sorgularda sarmalama yapilmaz: SQL + // Server'da ORDER BY tasiyan bir alt sorgu gecersizdir. Sonuc kumesi okunurken yine + // maxRows satirda kesilir. + return match.Success + ? string.Concat(sql.AsSpan(0, match.Length), $"TOP ({maxRows}) ", sql.AsSpan(match.Length)) + : sql; + } + + public Dictionary NormalizeParameters(IReadOnlyDictionary? parameters) + { + var normalized = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (parameters is null) + { + return normalized; + } + + foreach (var (key, value) in parameters) + { + if (key is null || !SafeParameterNameRegex.IsMatch(key)) + { + throw new UserFriendlyException(localizer[McpErrorCodes.InvalidParameterName, key ?? string.Empty]); + } + + normalized[key.TrimStart('@')] = value!; + } + + return normalized; + } + + private void EnsureSafeIdentifier(string identifier) + { + if (!SafeIdentifierRegex.IsMatch(identifier ?? string.Empty)) + { + throw new UserFriendlyException(localizer[McpErrorCodes.InvalidIdentifier, identifier ?? string.Empty]); + } + } +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Sozsoft.Mcp.Domain.csproj b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Sozsoft.Mcp.Domain.csproj new file mode 100644 index 00000000..e463563b --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain/Sozsoft.Mcp.Domain.csproj @@ -0,0 +1,21 @@ + + + + + + net10.0 + enable + Sozsoft.Mcp + + + + + + + + + + + + + diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/EntityFrameworkCore/McpDbContext.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/EntityFrameworkCore/McpDbContext.cs new file mode 100644 index 00000000..5089e254 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/EntityFrameworkCore/McpDbContext.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using Sozsoft.Mcp.Entities; +using Volo.Abp.Data; +using Volo.Abp.EntityFrameworkCore; + +namespace Sozsoft.Mcp.EntityFrameworkCore; + +[ConnectionStringName(McpDbProperties.ConnectionStringName)] +public class McpDbContext(DbContextOptions options) + : AbpDbContext(options) +{ + public DbSet McpServers { get; set; } = null!; + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder.ConfigureMcp(); + } +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/EntityFrameworkCore/McpDbContextModelCreatingExtensions.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/EntityFrameworkCore/McpDbContextModelCreatingExtensions.cs new file mode 100644 index 00000000..40714282 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/EntityFrameworkCore/McpDbContextModelCreatingExtensions.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore; +using Sozsoft.Mcp.Entities; +using Sozsoft.Platform.Enums; +using Volo.Abp; +using Volo.Abp.EntityFrameworkCore.Modeling; +using static Sozsoft.Platform.PlatformConsts; + +namespace Sozsoft.Mcp.EntityFrameworkCore; + +public static class McpDbContextModelCreatingExtensions +{ + public static void ConfigureMcp(this ModelBuilder builder) + { + Check.NotNull(builder, nameof(builder)); + + builder.Entity(b => + { + // Tablo adi platformun tek kayit defterinden gelir (TableNameEnum.McpServer); + // modul ikinci bir adlandirma kurali tanimlamaz. + b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.McpServer)), Prefix.DbSchema); + b.ConfigureByConvention(); + + b.Property(x => x.Name).IsRequired().HasMaxLength(128); + b.Property(x => x.Url).HasMaxLength(512); + // Kiraci ve arac listeleri tek kolonda MultiValueDelimiter ile saklanir; ekranda + // 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.EntityFrameworkCore/EntityFrameworkCore/McpDbProperties.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/EntityFrameworkCore/McpDbProperties.cs new file mode 100644 index 00000000..d5f14a88 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/EntityFrameworkCore/McpDbProperties.cs @@ -0,0 +1,6 @@ +namespace Sozsoft.Mcp.EntityFrameworkCore; + +public static class McpDbProperties +{ + public const string ConnectionStringName = "Mcp"; +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/EntityFrameworkCore/McpEntityFrameworkCoreModule.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/EntityFrameworkCore/McpEntityFrameworkCoreModule.cs new file mode 100644 index 00000000..a1261267 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/EntityFrameworkCore/McpEntityFrameworkCoreModule.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.EntityFrameworkCore; +using Volo.Abp.Modularity; + +namespace Sozsoft.Mcp.EntityFrameworkCore; + +[DependsOn( + typeof(McpDomainModule), + typeof(AbpEntityFrameworkCoreModule) +)] +public class McpEntityFrameworkCoreModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddAbpDbContext(options => + { + options.AddDefaultRepositories(includeAllEntities: true); + }); + } +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/Sozsoft.Mcp.EntityFrameworkCore.csproj b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/Sozsoft.Mcp.EntityFrameworkCore.csproj new file mode 100644 index 00000000..e59f1bad --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.EntityFrameworkCore/Sozsoft.Mcp.EntityFrameworkCore.csproj @@ -0,0 +1,19 @@ + + + + + + net10.0 + enable + Sozsoft.Mcp + + + + + + + + + + + diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Controllers/McpOAuthController.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Controllers/McpOAuthController.cs new file mode 100644 index 00000000..8d8918a1 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Controllers/McpOAuthController.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Sozsoft.Mcp.Dtos; +using Volo.Abp.AspNetCore.Mvc; + +namespace Sozsoft.Mcp.Controllers; + +/// +/// Uzak MCP istemcilerinin baglanti kesfi. Istemci /mcp'den 401 alir, WWW-Authenticate +/// basligindaki meta veri adresinden yetkilendirme sunucusunu ogrenir, kayit ucundan istemci +/// kimligini alir ve kullaniciyi platformun giris ekranina yonlendirir. +/// Adresler ve yanit bicimi RFC'lerle sabittir; bu yuzden ABP'nin konvansiyonel uclari yerine +/// acik rotali bir controller kullanilir. +/// +[AllowAnonymous] +[IgnoreAntiforgeryToken] +[ApiExplorerSettings(IgnoreApi = true)] +public class McpOAuthController( + IOptions options, + IMcpOAuthAppService oAuthAppService) : AbpControllerBase +{ + /// Korumali kaynak meta verisi (RFC 9728). + [HttpGet(McpConsts.ProtectedResourceMetadataPath)] + [HttpGet(McpConsts.ProtectedResourceMetadataPath + McpConsts.EndpointPath)] + public IActionResult GetProtectedResourceMetadata() + { + var value = options.Value; + + return new JsonResult(new ProtectedResourceMetadata( + value.ResourceUrl, + [value.AuthorizationServer], + McpConsts.OAuthScopes, + ["header"], + McpServerInfoDefaults.DisplayName)); + } + + /// Dinamik istemci kaydi (RFC 7591). + [HttpPost(McpConsts.RegistrationEndpointPath)] + public async Task RegisterAsync([FromBody] ClientRegistrationRequest request) + { + var result = await oAuthAppService.RegisterClientAsync(new McpClientRegistrationRequestDto + { + ClientName = request.ClientName, + RedirectUris = request.RedirectUris ?? [] + }); + + if (result.Error is not null) + { + return new JsonResult(new ClientRegistrationError(result.Error, result.ErrorDescription)) + { + StatusCode = StatusCodes.Status400BadRequest + }; + } + + return new JsonResult(new ClientRegistrationResponse( + result.ClientId!, + result.ClientIdIssuedAt, + result.ClientName, + result.RedirectUris, + result.GrantTypes, + result.ResponseTypes, + result.TokenEndpointAuthMethod, + result.Scope)) + { + StatusCode = StatusCodes.Status201Created + }; + } + + // Protokol sozlesmeleri snake_case alan adlariyla sabittir; ABP'nin camelCase JSON + // politikasina birakilmaz. + + public sealed record ClientRegistrationRequest( + [property: JsonPropertyName("client_name")] string? ClientName, + [property: JsonPropertyName("redirect_uris")] List? RedirectUris); + + private sealed record ClientRegistrationResponse( + [property: JsonPropertyName("client_id")] string ClientId, + [property: JsonPropertyName("client_id_issued_at")] long ClientIdIssuedAt, + [property: JsonPropertyName("client_name")] string? ClientName, + [property: JsonPropertyName("redirect_uris")] IReadOnlyList RedirectUris, + [property: JsonPropertyName("grant_types")] IReadOnlyList GrantTypes, + [property: JsonPropertyName("response_types")] IReadOnlyList ResponseTypes, + [property: JsonPropertyName("token_endpoint_auth_method")] string TokenEndpointAuthMethod, + [property: JsonPropertyName("scope")] string? Scope); + + private sealed record ClientRegistrationError( + [property: JsonPropertyName("error")] string Error, + [property: JsonPropertyName("error_description")] string? ErrorDescription); + + private sealed record ProtectedResourceMetadata( + [property: JsonPropertyName("resource")] string Resource, + [property: JsonPropertyName("authorization_servers")] IReadOnlyList AuthorizationServers, + [property: JsonPropertyName("scopes_supported")] IReadOnlyList ScopesSupported, + [property: JsonPropertyName("bearer_methods_supported")] IReadOnlyList BearerMethodsSupported, + [property: JsonPropertyName("resource_name")] string ResourceName); +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpAuthenticationChallengeMiddleware.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpAuthenticationChallengeMiddleware.cs new file mode 100644 index 00000000..41fac43e --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpAuthenticationChallengeMiddleware.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using Volo.Abp.DependencyInjection; + +namespace Sozsoft.Mcp; + +/// +/// /mcp'den donen 401 yanitina korumali kaynak meta verisinin adresini ekler (RFC 9728 §5.1). +/// Uzak MCP istemcisi OAuth akisini bu basliktan baslatir; baslik yoksa baglanti formu +/// "kimlik dogrulama desteklenmiyor" diye durur. Kimlik dogrulamadan once calismalidir: +/// basligi yanit baslamadan hemen once yazar, 401'i hangi katman uretmis olursa olsun. +/// +public class McpAuthenticationChallengeMiddleware(IOptions options) + : IMiddleware, ITransientDependency +{ + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + if (context.Request.Path.StartsWithSegments(McpConsts.EndpointPath)) + { + context.Response.OnStarting(() => + { + if (context.Response.StatusCode == StatusCodes.Status401Unauthorized) + { + context.Response.Headers[HeaderNames.WWWAuthenticate] = + $"Bearer resource_metadata=\"{options.Value.ProtectedResourceMetadataUrl}\", " + + $"scope=\"{string.Join(' ', McpConsts.OAuthScopes)}\""; + } + + return Task.CompletedTask; + }); + } + + await next(context); + } +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpHttpApiModule.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpHttpApiModule.cs new file mode 100644 index 00000000..69ef42d3 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpHttpApiModule.cs @@ -0,0 +1,64 @@ +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using ModelContextProtocol.AspNetCore; +using Sozsoft.Mcp.Tools; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.Modularity; + +namespace Sozsoft.Mcp; + +/// +/// /mcp ucunun sunum katmani. Araclar yalnizca uygulama sozlesmesini tanir; kimlik dogrulama, +/// kiraci cozumu, unit of work ve yetki kontrolu istegin gectigi ABP ardisik duzeninden gelir. +/// Ucun kendisi host tarafinda MapMcp ile baglanir. +/// +[DependsOn( + typeof(McpApplicationContractsModule), + typeof(AbpAspNetCoreMvcModule) +)] +public class McpHttpApiModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + var configuration = context.Services.GetConfiguration(); + + Configure(options => + { + options.BaseUrl = (configuration["App:SelfUrl"] ?? string.Empty).TrimEnd('/'); + options.AuthorizationServer = (configuration["AuthServer:Authority"] ?? string.Empty).TrimEnd('/'); + }); + + context.Services + .AddMcpServer(options => + { + options.ServerInfo = new() { Name = McpServerInfoDefaults.Name, Version = McpServerInfoDefaults.Version }; + options.ServerInstructions = McpServerInfoDefaults.Instructions; + }) + .WithHttpTransport(options => + { + // Durumsuz kip: her istek kendi ABP kapsaminda kosar, boylece kimlik, kiraci + // cozumu ve unit of work istekle birlikte dogru kurulur. Durumlu kipte oturum + // kapsami initialize istegine baglanir ve sonraki cagrilar yanlis kiracinin + // baglaminda kalabilir; ayrica ters vekil arkasinda oturum yapisakligi gerekmez. + options.SessionMode = HttpServerSessionMode.Stateless; + }) + .WithTools() + .WithTools() + .WithTools() + .WithRequestFilters(filters => filters.AddListToolsFilter(next => async (requestContext, cancellationToken) => + { + var result = await next(requestContext, cancellationToken); + + // Arac listesi kiracinin sunucu kaydina ve kullanicinin uygulamadaki yetkilerine + // gore suzulur. Cagri aninda da ayni kural uygulanir + // (McpAppService.EnsureToolAllowedAsync): istemci listede olmayan bir araci da + // cagirabilir, suzme tek basina bir yetki siniri degildir. + var schemaAppService = requestContext.Services!.GetRequiredService(); + var available = (await schemaAppService.GetAvailableToolsAsync()).ToHashSet(System.StringComparer.Ordinal); + + result.Tools = [.. result.Tools.Where(tool => available.Contains(tool.Name))]; + + return result; + })); + } +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpOAuthOptions.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpOAuthOptions.cs new file mode 100644 index 00000000..5b50eebe --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpOAuthOptions.cs @@ -0,0 +1,22 @@ +namespace Sozsoft.Mcp; + +/// +/// /mcp ucunun OAuth adresleri. Degerler ortam yapilandirmasindan gelir (App:SelfUrl, +/// AuthServer:Authority); localhost, dev ve production ayni kodla calisir. +/// +public class McpOAuthOptions +{ + /// API'nin disaridan erisilen kok adresi, sonunda egik cizgi olmadan. + public string BaseUrl { get; set; } = string.Empty; + + /// Token'i veren yetkilendirme sunucusu (OpenIddict), sonunda egik cizgi olmadan. + public string AuthorizationServer { get; set; } = string.Empty; + + /// Korumali kaynagin tanimlayicisi: istemcinin baglandigi /mcp adresi. + public string ResourceUrl => BaseUrl + McpConsts.EndpointPath; + + /// + /// Kaynaga ozgu meta veri adresi (RFC 9728 §3.1: yol, well-known onekinin arkasina eklenir). + /// + public string ProtectedResourceMetadataUrl => BaseUrl + McpConsts.ProtectedResourceMetadataPath + McpConsts.EndpointPath; +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpServerInfoDefaults.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpServerInfoDefaults.cs new file mode 100644 index 00000000..6da4b77a --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/McpServerInfoDefaults.cs @@ -0,0 +1,22 @@ +namespace Sozsoft.Mcp; + +/// +/// Istemciye initialize yanitinda bildirilen sunucu kimligi ve yonerge metni. +/// Yonerge modele hitap eder, son kullaniciya degil; bu yuzden dil anahtari uzerinden +/// gecmez — protokol metnidir, arayuz metni degil. +/// +internal static class McpServerInfoDefaults +{ + public const string Name = "sozsoft-platform"; + + /// Connector ekraninda gorunen kaynak adi. + public const string DisplayName = "Sozsoft Platform"; + + public const string Version = "1.0.0"; + + public const string Instructions = + "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."; +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Sozsoft.Mcp.HttpApi.csproj b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Sozsoft.Mcp.HttpApi.csproj new file mode 100644 index 00000000..4ca31577 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Sozsoft.Mcp.HttpApi.csproj @@ -0,0 +1,26 @@ + + + + + + net10.0 + enable + Sozsoft.Mcp + + + + + + + + + + + + + + + + + + diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDataTools.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDataTools.cs new file mode 100644 index 00000000..b60d95ef --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpDataTools.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Sozsoft.Mcp.Dtos; + +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. +/// +[McpServerToolType] +public sealed class McpDataTools(IMcpDataAppService dataAppService) +{ + [McpServerTool(Name = McpToolNames.InsertRow, Destructive = false, Idempotent = false)] + [Description("Bir listeye yeni kayit ekler. Alan adlari describe_list cikisindaki field degerleridir.")] + public async Task InsertRowAsync( + [Description("Kaydin eklenecegi listenin kodu.")] + string listFormCode, + [Description("Alan adi -> deger eslemesi.")] + Dictionary values) + => await dataAppService.InsertRowAsync(new McpRowRequestDto + { + ListFormCode = listFormCode, + Values = values ?? [] + }); + + [McpServerTool(Name = McpToolNames.UpdateRow, Destructive = true, Idempotent = true)] + [Description("Bir listedeki kaydi anahtar degerine gore gunceller. Yalnizca verilen alanlar yazilir.")] + public async Task UpdateRowAsync( + [Description("Kaydin bulundugu listenin kodu.")] + string listFormCode, + [Description("Guncellenecek kaydin anahtar alan degeri; describe_list cikisindaki keyFieldName alanina karsilik gelir.")] + List keys, + [Description("Alan adi -> yeni deger eslemesi.")] + Dictionary values) + => await dataAppService.UpdateRowAsync(new McpRowRequestDto + { + ListFormCode = listFormCode, + Keys = keys ?? [], + Values = values ?? [] + }); + + [McpServerTool(Name = McpToolNames.DeleteRow, Destructive = true, Idempotent = true)] + [Description("Bir listedeki kaydi anahtar degerine gore siler. Ekranin kendi silme komutu kullanilir; soft delete tasiyan tablolarda kayit pasife cekilir.")] + public async Task DeleteRowAsync( + [Description("Kaydin bulundugu listenin kodu.")] + string listFormCode, + [Description("Silinecek kaydin anahtar alan degeri.")] + List keys) + => await dataAppService.DeleteRowAsync(new McpRowRequestDto + { + ListFormCode = listFormCode, + Keys = keys ?? [] + }); +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpQueryTools.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpQueryTools.cs new file mode 100644 index 00000000..adab58f1 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpQueryTools.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Sozsoft.Mcp.Dtos; + +namespace Sozsoft.Mcp.Tools; + +/// Salt-okunur sorgu araclari. +[McpServerToolType] +public sealed class McpQueryTools(IMcpQueryAppService queryAppService) +{ + [McpServerTool(Name = McpToolNames.QueryList, ReadOnly = true)] + [Description("Bir listenin verisini uygulamadaki ekranla ayni sorguyla okur; kullanicinin liste ve alan yetkileri, kiraci ve sube filtreleri gecerlidir. Veri okumak icin once bunu kullan. Alan adlari describe_list cikisindaki field degerleridir. Filtre/siralama/gruplama DevExtreme bicimindedir.")] + public async Task QueryListAsync( + [Description("Listenin kodu; list_lists cikisindaki listFormCode degeri.")] + string listFormCode, + [Description("Filtre, JSON metni. Ornek: [\"Status\",\"=\",\"Open\"] ya da [[\"Amount\",\">\",100],\"and\",[\"Name\",\"contains\",\"abc\"]]. Operatorler: =, <>, >, >=, <, <=, contains, notcontains, startswith, endswith.")] + string? filter = null, + [Description("Siralama, JSON metni. Ornek: [{\"selector\":\"CreationTime\",\"desc\":true}].")] + string? sort = null, + [Description("Gruplama, JSON metni. Ornek: [{\"selector\":\"CustomerId\",\"desc\":false,\"isExpanded\":false}]. Gruplu sorguda satirlar yerine grup anahtarlari ve ozetleri doner.")] + string? group = null, + [Description("Grup ozetleri, JSON metni. Ornek: [{\"selector\":\"Id\",\"summaryType\":\"count\"},{\"selector\":\"Amount\",\"summaryType\":\"sum\"}]. Tipler: count, sum, min, max, avg.")] + string? groupSummary = null, + [Description("Tum sonuc icin ozet, JSON metni. Ornek: [{\"selector\":\"Amount\",\"summaryType\":\"sum\"}].")] + string? totalSummary = null, + [Description("Atlanacak kayit sayisi (sayfalama).")] + int skip = 0, + [Description("Istenen kayit sayisi; sunucu kaydindaki tavandan buyuk olamaz.")] + int take = 0) + => await queryAppService.QueryListAsync(new McpListQueryRequestDto + { + ListFormCode = listFormCode, + Filter = filter, + Sort = sort, + Group = group, + GroupSummary = groupSummary, + TotalSummary = totalSummary, + Skip = skip, + Take = take + }); + + [McpServerTool(Name = McpToolNames.RunSelect, ReadOnly = true)] + [Description("Yalnizca uygulamada SQL Query Manager yetkisi olan kullanicilar icin: tek bir parametreli SELECT ifadesi kosturur. Liste verisi icin query_list tercih edilir. Coklu ifade, DDL ve DML reddedilir; satir sayisi sunucu kaydindaki tavana kirpilir. Sorguya deger gomme, @ad parametresi kullan.")] + public async Task RunSelectAsync( + [Description("Tek bir SELECT (ya da WITH ... SELECT) ifadesi. Degerler @ad bicimindeki parametrelerle verilir.")] + string sql, + [Description("Sorgudaki adli parametreler: anahtar parametre adi, deger ise ham JSON degeri.")] + Dictionary? parameters = null, + [Description("Istenen en fazla satir sayisi; sunucu kaydindaki tavandan buyuk olamaz.")] + int maxRows = 0) + => await queryAppService.RunSelectAsync(new McpSelectRequestDto + { + Sql = sql, + Parameters = parameters ?? [], + MaxRows = maxRows + }); + + [McpServerTool(Name = McpToolNames.ListQueries, ReadOnly = true)] + [Description("Veri kaynagindaki kayitli nesneleri (view, stored procedure, function) ve cagrilabilir Custom Endpoint'leri dondurur. run_query bunlardan birini cagirir.")] + public async Task> ListQueriesAsync( + [Description("Nesne adi ya da aciklamasi icinde aranacak metin. Bos birakilabilir.")] + string? search = null) + => await queryAppService.GetSavedQueriesAsync(search); + + [McpServerTool(Name = McpToolNames.RunQuery, ReadOnly = true)] + [Description("Kayitli bir nesneyi ya da Custom Endpoint'i parametreleriyle cagirir. Nesne turu ve adi list_queries cikisindan alinir.")] + public async Task RunQueryAsync( + [Description("Nesne turu: view, procedure, tableFunction, scalarFunction ya da customEndpoint.")] + string kind, + [Description("Nesnenin ya da Custom Endpoint'in adi.")] + string name, + [Description("Nesnenin semasi. Bos birakilirsa SQL Server'da dbo, PostgreSQL'de public kullanilir.")] + string? schemaName = null, + [Description("Adli parametreler: anahtar parametre adi, deger ise ham JSON degeri.")] + Dictionary? parameters = null, + [Description("Istenen en fazla satir sayisi; sunucu kaydindaki tavandan buyuk olamaz.")] + int maxRows = 0) + => await queryAppService.RunSavedQueryAsync(new McpSavedQueryRequestDto + { + Kind = kind, + Name = name, + SchemaName = schemaName, + Parameters = parameters ?? [], + MaxRows = maxRows + }); +} diff --git a/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpSchemaTools.cs b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpSchemaTools.cs new file mode 100644 index 00000000..ea2cecb3 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.HttpApi/Tools/McpSchemaTools.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading.Tasks; +using ModelContextProtocol.Server; +using Sozsoft.Mcp.Dtos; + +namespace Sozsoft.Mcp.Tools; + +/// +/// Sema kesfi araclari. Tablo ve sutun bilgisi canli katalogdan degil, platformun ekran +/// tanimlarindan uretilen veri sozlugunden gelir; boylece modelin gordugu adlar kullanicinin +/// ekranda gordugu adlarla ayni olur. +/// +[McpServerToolType] +public sealed class McpSchemaTools(IMcpSchemaAppService schemaAppService) +{ + [McpServerTool(Name = McpToolNames.ServerInfo, ReadOnly = true)] + [Description("Bu baglantinin kiracisini, acik araclari ve veri sozlugunun tazelik bilgisini dondurur. Baska bir arac beklendigi gibi calismiyorsa once bunu cagir.")] + public async Task ServerInfoAsync() + { + var config = await schemaAppService.GetServerConfigAsync(); + var schema = await schemaAppService.GetSchemaInfoAsync(); + var availableTools = await schemaAppService.GetAvailableToolsAsync(); + + return new McpServerInfoResult(config, schema, availableTools); + } + + [McpServerTool(Name = McpToolNames.ListLists, ReadOnly = true)] + [Description("Kiracinin listelerini (ekranlarini) ad, baslik, aciklama ve arkasindaki tablo ile dondurur. Arama metni verilmezse tumu doner.")] + public async Task> ListListsAsync( + [Description("Liste adi, basligi, aciklamasi ya da tablo adi icinde aranacak metin. Bos birakilabilir.")] + string? search = null, + [Description("Donecek en fazla liste sayisi. Varsayilan 200.")] + int maxResults = 200) + => await schemaAppService.GetListsAsync(search, maxResults); + + [McpServerTool(Name = McpToolNames.DescribeList, ReadOnly = true)] + [Description("Tek bir listenin sutunlarini dondurur: alan adi, kullaniciya gorunen basligi, SQL tipi, lookup iliskisi ve sutunun hassas olup olmadigi.")] + public async Task DescribeListAsync( + [Description("Listenin kodu; list_lists cikisindaki listFormCode degeri.")] + string listFormCode) + => await schemaAppService.GetListAsync(listFormCode); + + [McpServerTool(Name = McpToolNames.SearchSchema, ReadOnly = true)] + [Description("Liste ve sutun adlari ile basliklari uzerinde aksan ve buyuk/kucuk harf duyarsiz arama yapar; hangi listenin hangi sutununun arandigini bulmak icin kullanilir.")] + public async Task> SearchSchemaAsync( + [Description("Aranacak metin. Turkce karakterler ve aksanlar onemsizdir.")] + string term, + [Description("Donecek en fazla eslesme sayisi. Varsayilan 50.")] + int maxResults = 50) + => await schemaAppService.SearchAsync(term, maxResults); +} + +/// Istegin kiracisina dusen sunucu kaydi; yoksa null. +/// Veri sozlugunun tazelik bilgisi. +/// Bu baglantida cagrilabilen araclar. +public sealed record McpServerInfoResult( + McpServerConfigDto? Server, + McpSchemaInfoDto Schema, + IReadOnlyList AvailableTools); diff --git a/api/modules/Sozsoft.Mcp/common.props b/api/modules/Sozsoft.Mcp/common.props new file mode 100644 index 00000000..4fce1403 --- /dev/null +++ b/api/modules/Sozsoft.Mcp/common.props @@ -0,0 +1,8 @@ + + + latest + 0.1.0 + $(NoWarn);CS1591 + module + + diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/IListFormDataAppService.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/IListFormDataAppService.cs index fc56ae05..082f7778 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/IListFormDataAppService.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/IListFormDataAppService.cs @@ -1,11 +1,23 @@ using System.Threading.Tasks; +using Volo.Abp.Application.Services; namespace Sozsoft.Platform.ListForms.Select; -public interface IListFormDataAppService +/// +/// Ekran uzerinden veri yazma sozlesmesi. Ekranin insert/update/delete komutlari ve yetki +/// kontrolu burada uygulanir; ayni yol hem grid'den hem /mcp araclarindan kullanilir. +/// +public interface IListFormDataAppService : IApplicationService { - Task PostInsertAsync(DataRequestDto input); + /// Uretilen anahtar degeri. + Task PostInsertAsync(DataRequestDto input); + + /// Kopyalanan kaydin anahtar degeri. + Task PostDuplicateAsync(DataRequestDto input); + + /// Etkilenen satir sayisi. Task PostUpdateAsync(DataRequestDto input); + + /// Etkilenen satir sayisi. Task PostDeleteAsync(DataRequestDto input); } - diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormDataAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormDataAppService.cs index 93bf3067..2f6a528d 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormDataAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormDataAppService.cs @@ -11,7 +11,7 @@ using static Sozsoft.Platform.PlatformConsts; namespace Sozsoft.Platform.ListForms.Select; -public class ListFormDataAppService : PlatformAppService +public class ListFormDataAppService : PlatformAppService, IListFormDataAppService { private readonly IListFormAuthorizationManager authManager; private readonly IQueryManager qManager; diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json index 11b97b17..d8057814 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json @@ -28295,6 +28295,156 @@ "key": "App.StaticLookup.Right", "en": "Right", "tr": "Sağ" + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKit.McpServers", + "en": "MCP Servers", + "tr": "MCP Sunucuları" + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKit.McpServers.Invoke", + "en": "Call MCP tools", + "tr": "MCP araçlarını çağır" + }, + { + "resourceName": "Platform", + "key": "App.DeveloperKit.McpServers.Write", + "en": "Write data over MCP", + "tr": "MCP üzerinden veri yaz" + }, + { + "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." + }, + { + "resourceName": "Platform", + "key": "App.Listform.ListformField.Url", + "en": "Url", + "tr": "Adres" + }, + { + "resourceName": "Platform", + "key": "App.Listform.ListformField.AllowedTools", + "en": "Allowed Tools", + "tr": "İzinli Araçlar" + }, + { + "resourceName": "Platform", + "key": "App.Listform.ListformField.MaxRows", + "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", + "en": "No active MCP server record is defined for this tenant.", + "tr": "Bu kiracı için tanımlı ve aktif bir MCP sunucu kaydı yok." + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.ToolDisabled", + "en": "The '{0}' tool is disabled on the '{1}' MCP server.", + "tr": "'{0}' aracı '{1}' MCP sunucusunda kapalı." + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.EmptyQuery", + "en": "The query text is empty.", + "tr": "Sorgu metni boş." + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.SingleStatementOnly", + "en": "Only a single SELECT statement may be run at a time.", + "tr": "Tek seferde yalnızca bir SELECT ifadesi koşturulabilir." + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.ReadOnlyStatementRequired", + "en": "The query must start with SELECT or WITH.", + "tr": "Sorgu SELECT ya da WITH ile başlamak zorunda." + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.ForbiddenKeyword", + "en": "Expression not allowed in a read-only query: {0}", + "tr": "Salt-okunur sorguda kullanılamayan ifade: {0}" + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.InvalidIdentifier", + "en": "Invalid object or schema name: {0}", + "tr": "Geçersiz nesne ya da şema adı: {0}" + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.InvalidParameterName", + "en": "Invalid parameter name: {0}", + "tr": "Geçersiz parametre adı: {0}" + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.UnsupportedObjectKind", + "en": "Unsupported object kind: {0}", + "tr": "Desteklenmeyen nesne türü: {0}" + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.CustomEndpointNotFound", + "en": "Custom Endpoint not found or not permitted: {0}", + "tr": "Custom Endpoint bulunamadı ya da yetki yok: {0}" + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.MissingParameters", + "en": "Required parameters are missing: {0}", + "tr": "Zorunlu parametreler eksik: {0}" + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.ListFormCodeRequired", + "en": "ListFormCode cannot be empty.", + "tr": "ListFormCode boş bırakılamaz." + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.ValuesRequired", + "en": "No field value was given to write.", + "tr": "Yazılacak alan değeri verilmedi." + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.KeysRequired", + "en": "No key field value was given.", + "tr": "Anahtar alan değeri verilmedi." + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.DictionaryNotFound", + "en": "The data dictionary was not found. Run the {0} procedure with the tenant name.", + "tr": "Veri sözlüğü bulunamadı. {0} prosedürünü kiracı adıyla çalıştırın." + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.RedirectUriNotAllowed", + "en": "The redirect address is not accepted. Use HTTPS, a localhost address or an application scheme: {0}", + "tr": "Geri dönüş adresi kabul edilmiyor. HTTPS, localhost ya da uygulama şeması kullanın: {0}" + }, + { + "resourceName": "Platform", + "key": "App.Mcp.Error.RegistrationLimitReached", + "en": "The limit of {0} dynamically registered MCP clients has been reached. Remove unused clients from the OpenIddict applications.", + "tr": "Dinamik kayıtlı MCP istemcisi tavanına ({0}) ulaşıldı. Kullanılmayan istemcileri OpenIddict uygulamalarından silin." } ] } diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs b/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs index f973db5f..62680073 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; using System.Data; +using System.Linq; using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Localization; using Sozsoft.Languages.Languages; +using Sozsoft.Mcp; using Sozsoft.Platform.Editors; using Sozsoft.Platform.Entities; using Sozsoft.Platform.Enums; @@ -7846,6 +7848,292 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency } #endregion + #region MCP Servers + listFormName = AppCodes.DeveloperKits.McpServers; + if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName)) + { + // Kayit /mcp ucunun kiraci bazli sozlesmesini tutar: hangi araclar acik, sorgular + // hangi veri kaynaginda hangi satir tavaniyla kosar. + var mcpServerTable = TableNameResolver.GetFullTableName(nameof(TableNameEnum.McpServer)); + + var listForm = await _listFormRepository.InsertAsync( + new ListForm() + { + ListFormType = ListFormTypeEnum.List, + PageSize = 10, + ExportJson = DefaultExportJson, + IsSubForm = false, + ShowNote = false, + LayoutJson = DefaultLayoutJson(), + CultureName = LanguageCodes.En, + ListFormCode = listFormName, + Name = listFormName, + Title = listFormName, + DataSourceCode = SeedConsts.DataSources.DefaultCode, + IsTenant = false, + IsBranch = false, + IsOrganizationUnit = false, + Description = $"{listFormName}.Description", + SelectCommandType = SelectCommandTypeEnum.Table, + SelectCommand = mcpServerTable, + SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName), + SeedSyncInsert = true, + SeedSyncUpdate = true, + SeedSyncDelete = true, + KeyFieldName = "Id", + KeyFieldDbSourceType = DbType.Guid, + DefaultFilter = DefaultFilterJson, + SortMode = GridOptions.SortModeSingle, + FilterRowJson = DefaultFilterRowJson(), + HeaderFilterJson = DefaultHeaderFilterJson(), + SearchPanelJson = DefaultSearchPanelJson(), + GroupPanelJson = DefaultGroupPanelJson(), + SelectionJson = DefaultSelectionSingleJson, + ColumnOptionJson = DefaultColumnOptionJson(), + PermissionJson = DefaultPermissionJson(listFormName), + PagerOptionJson = DefaultPagerOptionJson, + EditingOptionJson = DefaultEditingOptionJson(listFormName, 700, 600, true, true, true, true, false), + EditingFormJson = JsonSerializer.Serialize(new List() + { + new() { Order = 1, ColCount = 2, ColSpan = 1, ItemType = "group" }, + }), + InsertFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] { + new() { FieldName = "Id", FieldDbType = DbType.Guid, Value = "@NEWID", CustomValueType = FieldCustomValueTypeEnum.CustomKey }, + new() { FieldName = "CreationTime", FieldDbType = DbType.DateTimeOffset, Value = "@NOW", CustomValueType = FieldCustomValueTypeEnum.CustomKey }, + new() { FieldName = "CreatorId", FieldDbType = DbType.Guid, Value = "@USERID", CustomValueType = FieldCustomValueTypeEnum.CustomKey }, + }), + 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)), + DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(), + } + ); + + #region MCP Servers Fields + await _listFormFieldRepository.InsertManyAsync([ + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.Guid, + FieldName = "Id", + CaptionName = "App.Listform.ListformField.Id", + Width = 0, + ListOrderNo = 1, + Visible = false, + IsActive = true, + + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "Name", + EditGroupOrderNo = 1, + EditOrderNo = 1, + EditorType2 = EditorTypes.dxTextBox, + ColSpan = 1, + CaptionName = "App.Listform.ListformField.Name", + Width = 0, + ListOrderNo = 2, + Visible = true, + IsActive = true, + + AllowSearch = true, + ValidationRuleJson = DefaultValidationRuleRequiredJson, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.Boolean, + FieldName = "IsActive", + EditGroupOrderNo = 1, + EditOrderNo = 2, + EditorType2 = EditorTypes.dxCheckBox, + ColSpan = 1, + CaptionName = "App.Listform.ListformField.IsActive", + Width = 0, + ListOrderNo = 3, + 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 = "Url", + EditGroupOrderNo = 1, + EditOrderNo = 3, + EditorType2 = EditorTypes.dxTextBox, + ColSpan = 2, + CaptionName = "App.Listform.ListformField.Url", + Width = 0, + ListOrderNo = 4, + 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 = "Tenants", + EditGroupOrderNo = 1, + EditOrderNo = 4, + EditorType2 = EditorTypes.dxTagBox, + ColSpan = 2, + CaptionName = "App.Listform.ListformField.Tenants", + Width = 0, + ListOrderNo = 5, + Visible = true, + IsActive = true, + + AllowSearch = true, + LookupJson = JsonSerializer.Serialize(new LookupDto { + DataSourceType = UiLookupDataSourceTypeEnum.Query, + DisplayExpr = "Name", + ValueExpr = "Key", + LookupQuery = LookupQueryValues.TenantValues(true), + }), + ColumnCustomizationJson = DefaultColumnCustomizationJson, + 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, + EditorType2 = EditorTypes.dxNumberBox, + EditorOptions = EditorOptions.Number(0), + ColSpan = 1, + CaptionName = "App.Listform.ListformField.MaxRows", + Width = 0, + ListOrderNo = 7, + Visible = true, + IsActive = true, + + AllowSearch = true, + ValidationRuleJson = DefaultValidationRuleRequiredJson, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + new() { + ListFormCode = listForm.ListFormCode, + CultureName = LanguageCodes.En, + SourceDbType = DbType.String, + FieldName = "AllowedTools", + EditGroupOrderNo = 1, + EditOrderNo = 7, + EditorType2 = EditorTypes.dxTagBox, + ColSpan = 2, + CaptionName = "App.Listform.ListformField.AllowedTools", + Width = 0, + ListOrderNo = 8, + Visible = true, + IsActive = true, + + AllowSearch = true, + // Secenek listesi modulun arac kayit defterinden uretilir; yeni bir arac + // eklendiginde ekran kendiliginden guncel olur. + LookupJson = JsonSerializer.Serialize(new LookupDto { + DataSourceType = UiLookupDataSourceTypeEnum.StaticData, + DisplayExpr = "name", + ValueExpr = "key", + LookupQuery = JsonSerializer.Serialize( + McpToolNames.SelectableTools.Select(tool => new LookupDataDto { Key = tool, Name = tool }).ToArray()), + }), + ColumnCustomizationJson = DefaultColumnCustomizationJson, + 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, + EditorType2 = EditorTypes.dxTextArea, + EditorOptions = EditorOptions.Multiline(60), + ColSpan = 2, + CaptionName = "App.Listform.ListformField.Description", + Width = 0, + ListOrderNo = 10, + Visible = false, + IsActive = true, + + AllowSearch = true, + ColumnCustomizationJson = DefaultColumnCustomizationJson, + PermissionJson = DefaultFieldPermissionJson(listForm.Name), + PivotSettingsJson = DefaultPivotSettingsJson + }, + ]); + #endregion + } + #endregion + #region Products listFormName = AppCodes.Orders.Products; if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName)) diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/MenusData.json b/api/src/Sozsoft.Platform.DbMigrator/Migrations/MenusData.json index 157d4014..4e441ffe 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/MenusData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/MenusData.json @@ -926,6 +926,17 @@ "IsDisabled": false, "ModuleId": "Sas" }, + { + "ParentCode": "App.DeveloperKit", + "Code": "App.DeveloperKit.McpServers", + "DisplayName": "App.DeveloperKit.McpServers", + "Order": 9, + "Url": "/admin/list/App.DeveloperKit.McpServers", + "Icon": "FcElectroDevices", + "RequiredPermissionName": "App.DeveloperKit.McpServers", + "IsDisabled": false, + "ModuleId": "Sas" + }, { "ParentCode": "App.Saas", "Code": "App.ForumManagement", diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/PermissionsData.json b/api/src/Sozsoft.Platform.DbMigrator/Migrations/PermissionsData.json index 9fe4a711..547f8c0a 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/PermissionsData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/PermissionsData.json @@ -2505,6 +2505,78 @@ "MultiTenancySide": 2, "Modules": "Sas|Adm" }, + { + "GroupName": "App.Saas", + "Name": "App.DeveloperKit.McpServers", + "ParentName": "App.DeveloperKit", + "DisplayName": "App.DeveloperKit.McpServers", + "IsEnabled": true, + "MultiTenancySide": 2, + "Modules": "Sas|Adm" + }, + { + "GroupName": "App.Saas", + "Name": "App.DeveloperKit.McpServers.Create", + "ParentName": "App.DeveloperKit.McpServers", + "DisplayName": "App.Platform.Create", + "IsEnabled": true, + "MultiTenancySide": 2, + "Modules": "Sas|Adm" + }, + { + "GroupName": "App.Saas", + "Name": "App.DeveloperKit.McpServers.Update", + "ParentName": "App.DeveloperKit.McpServers", + "DisplayName": "App.Platform.Update", + "IsEnabled": true, + "MultiTenancySide": 2, + "Modules": "Sas|Adm" + }, + { + "GroupName": "App.Saas", + "Name": "App.DeveloperKit.McpServers.Delete", + "ParentName": "App.DeveloperKit.McpServers", + "DisplayName": "App.Platform.Delete", + "IsEnabled": true, + "MultiTenancySide": 2, + "Modules": "Sas|Adm" + }, + { + "GroupName": "App.Saas", + "Name": "App.DeveloperKit.McpServers.Export", + "ParentName": "App.DeveloperKit.McpServers", + "DisplayName": "Export", + "IsEnabled": true, + "MultiTenancySide": 2, + "Modules": "Sas|Adm" + }, + { + "GroupName": "App.Saas", + "Name": "App.DeveloperKit.McpServers.Import", + "ParentName": "App.DeveloperKit.McpServers", + "DisplayName": "Import", + "IsEnabled": true, + "MultiTenancySide": 2, + "Modules": "Sas|Adm" + }, + { + "GroupName": "App.Saas", + "Name": "App.DeveloperKit.McpServers.Invoke", + "ParentName": "App.DeveloperKit.McpServers", + "DisplayName": "App.DeveloperKit.McpServers.Invoke", + "IsEnabled": true, + "MultiTenancySide": 3, + "Modules": "Sas|Adm" + }, + { + "GroupName": "App.Saas", + "Name": "App.DeveloperKit.McpServers.Write", + "ParentName": "App.DeveloperKit.McpServers", + "DisplayName": "App.DeveloperKit.McpServers.Write", + "IsEnabled": true, + "MultiTenancySide": 3, + "Modules": "Sas|Adm" + }, { "GroupName": "App.Saas", "Name": "App.Listforms.Listform", diff --git a/api/src/Sozsoft.Platform.DbMigrator/appsettings.Dev.json b/api/src/Sozsoft.Platform.DbMigrator/appsettings.Dev.json index 08e2b014..2c336d91 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/appsettings.Dev.json +++ b/api/src/Sozsoft.Platform.DbMigrator/appsettings.Dev.json @@ -29,6 +29,13 @@ "Platform_PublicApi": { "ClientId": "Platform_PublicApi", "RootUrl": "https://dev.sozsoft.com" + }, + "Platform_Mcp": { + "RedirectUris": [ + "https://claude.ai/api/mcp/auth_callback", + "https://claude.com/api/mcp/auth_callback", + "https://chatgpt.com/connector_platform_oauth_redirect" + ] } } } diff --git a/api/src/Sozsoft.Platform.DbMigrator/appsettings.Production.json b/api/src/Sozsoft.Platform.DbMigrator/appsettings.Production.json index 22ec8b3f..7352e7a2 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/appsettings.Production.json +++ b/api/src/Sozsoft.Platform.DbMigrator/appsettings.Production.json @@ -29,6 +29,13 @@ "Platform_PublicApi": { "ClientId": "Platform_PublicApi", "RootUrl": "https://sozsoft.com" + }, + "Platform_Mcp": { + "RedirectUris": [ + "https://claude.ai/api/mcp/auth_callback", + "https://claude.com/api/mcp/auth_callback", + "https://chatgpt.com/connector_platform_oauth_redirect" + ] } } } diff --git a/api/src/Sozsoft.Platform.DbMigrator/appsettings.json b/api/src/Sozsoft.Platform.DbMigrator/appsettings.json index 0443fdf6..ff3ac536 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/appsettings.json +++ b/api/src/Sozsoft.Platform.DbMigrator/appsettings.json @@ -29,6 +29,13 @@ "Platform_PublicApi": { "ClientId": "Platform_PublicApi", "RootUrl": "http://localhost:3000" + }, + "Platform_Mcp": { + "RedirectUris": [ + "https://claude.ai/api/mcp/auth_callback", + "https://claude.com/api/mcp/auth_callback", + "https://chatgpt.com/connector_platform_oauth_redirect" + ] } } } diff --git a/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs b/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs index 2c7eb4f8..c0c4eb27 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs @@ -37,6 +37,7 @@ public enum TableNameEnum CustomEndpoint, CustomComponent, DynamicService, + McpServer, ReportCategory, ReportTemplate, IpRestriction, diff --git a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs index 2a6b1119..76103bc5 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs @@ -513,6 +513,22 @@ public static class PlatformConsts public const string Publish = DynamicService + ".Publish"; public const string ViewCode = DynamicService + ".ViewCode"; } + + /// + /// MCP sunucu tanimlarinin yetkileri. ekrani gorme + /// (okuma) yetkisidir; ise /mcp ucundan arac + /// cagirma yetkisidir ve kiraci kullanicilarina da verilebilir. + /// + public static class McpServers + { + public const string McpServer = Default + ".McpServers"; + + public const string Create = McpServer + ".Create"; + public const string Update = McpServer + ".Update"; + public const string Delete = McpServer + ".Delete"; + public const string Invoke = McpServer + ".Invoke"; + public const string Write = McpServer + ".Write"; + } } public static class SqlQueryManagers diff --git a/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs b/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs index d080e8f3..90a0deaf 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs @@ -68,6 +68,7 @@ public static class TableNameResolver { nameof(TableNameEnum.CustomEndpoint), (TablePrefix.PlatformByName, MenuPrefix.Sas) }, { nameof(TableNameEnum.CustomComponent), (TablePrefix.PlatformByName, MenuPrefix.Sas) }, { nameof(TableNameEnum.DynamicService), (TablePrefix.PlatformByName, MenuPrefix.Sas) }, + { nameof(TableNameEnum.McpServer), (TablePrefix.PlatformByName, MenuPrefix.Sas) }, { nameof(TableNameEnum.DevRequest), (TablePrefix.PlatformByName, MenuPrefix.Sas) }, // 🔹 ADMINISTRATION TABLOLARI diff --git a/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs b/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs index 11b207e8..135437ac 100644 --- a/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs +++ b/api/src/Sozsoft.Platform.Domain/Data/SeedConsts.cs @@ -460,6 +460,7 @@ public static class SeedConsts public const string DynamicServices = Default + ".DynamicServices"; public const string CustomComponents = Default + ".CustomComponents"; + public const string McpServers = Default + ".McpServers"; } public const string Forum = Prefix.App + ".Forum"; diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs index a9466fce..c7dd7079 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Sozsoft.Languages.EntityFrameworkCore; using Sozsoft.MailQueue.EntityFrameworkCore; +using Sozsoft.Mcp.EntityFrameworkCore; using Sozsoft.Notifications.EntityFrameworkCore; using Sozsoft.Platform.Entities; using Sozsoft.Platform.Enums; @@ -185,6 +186,7 @@ public class PlatformDbContext : builder.ConfigureMailQueue(); builder.ConfigureNotification(); builder.ConfigureSqlQueryManager(); + builder.ConfigureMcp(); //Saas builder.Entity(b => diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformEntityFrameworkCoreModule.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformEntityFrameworkCoreModule.cs index da5ba5d0..ee062f31 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformEntityFrameworkCoreModule.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformEntityFrameworkCoreModule.cs @@ -1,6 +1,7 @@ using System; using Microsoft.Extensions.DependencyInjection; using Sozsoft.Languages.EntityFrameworkCore; +using Sozsoft.Mcp.EntityFrameworkCore; using Sozsoft.Notifications.EntityFrameworkCore; using Sozsoft.Platform.Domain.DeveloperKit; using Sozsoft.Settings.EntityFrameworkCore; @@ -36,7 +37,8 @@ namespace Sozsoft.Platform.EntityFrameworkCore; typeof(LanguagesEntityFrameworkCoreModule), typeof(SettingsEntityFrameworkCoreModule), typeof(NotificationEntityFrameworkCoreModule), - typeof(SqlQueryManagerEntityFrameworkCoreModule) + typeof(SqlQueryManagerEntityFrameworkCoreModule), + typeof(McpEntityFrameworkCoreModule) )] public class PlatformEntityFrameworkCoreModule : AbpModule { diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260908125150_Initial.Designer.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260910132559_Initial.Designer.cs similarity index 99% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260908125150_Initial.Designer.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260910132559_Initial.Designer.cs index 4ad9e28d..ecb9f20e 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260908125150_Initial.Designer.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260910132559_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace Sozsoft.Platform.Migrations { [DbContext(typeof(PlatformDbContext))] - [Migration("20260908125150_Initial")] + [Migration("20260910132559_Initial")] partial class Initial { /// @@ -437,6 +437,89 @@ namespace Sozsoft.Platform.Migrations b.ToTable("Sas_H_BackgroundWorker_MailQueueTableFormat", (string)null); }); + modelBuilder.Entity("Sozsoft.Mcp.Entities.McpServer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AllowWrite") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("AllowedTools") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DataSourceCode") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("MaxRows") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(200); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Tenants") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("Url") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("Sas_H_McpServer", (string)null); + }); + modelBuilder.Entity("Sozsoft.Notifications.Entities.Notification", b => { b.Property("Id") diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260908125150_Initial.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260910132559_Initial.cs similarity index 99% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260908125150_Initial.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260910132559_Initial.cs index d880ff02..2da1203b 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260908125150_Initial.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260910132559_Initial.cs @@ -1491,6 +1491,33 @@ namespace Sozsoft.Platform.Migrations table.PrimaryKey("PK_Sas_H_LogEntry", x => x.Id); }); + migrationBuilder.CreateTable( + name: "Sas_H_McpServer", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + 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), + LastModificationTime = table.Column(type: "datetime2", nullable: true), + LastModifierId = table.Column(type: "uniqueidentifier", nullable: true), + IsDeleted = table.Column(type: "bit", nullable: false, defaultValue: false), + DeleterId = table.Column(type: "uniqueidentifier", nullable: true), + DeletionTime = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Sas_H_McpServer", x => x.Id); + }); + migrationBuilder.CreateTable( name: "Sas_H_Module", columns: table => new @@ -4156,6 +4183,13 @@ namespace Sozsoft.Platform.Migrations table: "Sas_H_ListFormWorkflow", column: "ListFormCode"); + migrationBuilder.CreateIndex( + name: "IX_Sas_H_McpServer_Name", + table: "Sas_H_McpServer", + column: "Name", + unique: true, + filter: "[IsDeleted] = 0"); + migrationBuilder.CreateIndex( name: "IX_Sas_H_Menu_Code", table: "Sas_H_Menu", @@ -4523,6 +4557,9 @@ namespace Sozsoft.Platform.Migrations migrationBuilder.DropTable( name: "Sas_H_LogEntry"); + migrationBuilder.DropTable( + name: "Sas_H_McpServer"); + migrationBuilder.DropTable( name: "Sas_H_Menu"); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs index b192088a..3dafbca9 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs @@ -434,6 +434,89 @@ namespace Sozsoft.Platform.Migrations b.ToTable("Sas_H_BackgroundWorker_MailQueueTableFormat", (string)null); }); + modelBuilder.Entity("Sozsoft.Mcp.Entities.McpServer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AllowWrite") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("AllowedTools") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DataSourceCode") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("MaxRows") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(200); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Tenants") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("Url") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("Sas_H_McpServer", (string)null); + }); + modelBuilder.Entity("Sozsoft.Notifications.Entities.Notification", b => { b.Property("Id") diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Sozsoft.Platform.EntityFrameworkCore.csproj b/api/src/Sozsoft.Platform.EntityFrameworkCore/Sozsoft.Platform.EntityFrameworkCore.csproj index 698c0e33..5a0d94f5 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Sozsoft.Platform.EntityFrameworkCore.csproj +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Sozsoft.Platform.EntityFrameworkCore.csproj @@ -54,6 +54,10 @@ + + + + diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/Identity/McpOAuthServerConfiguration.cs b/api/src/Sozsoft.Platform.HttpApi.Host/Identity/McpOAuthServerConfiguration.cs new file mode 100644 index 00000000..8bd075a9 --- /dev/null +++ b/api/src/Sozsoft.Platform.HttpApi.Host/Identity/McpOAuthServerConfiguration.cs @@ -0,0 +1,90 @@ +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using OpenIddict.Abstractions; +using OpenIddict.Server; +using Sozsoft.Mcp; +using static OpenIddict.Server.OpenIddictServerEvents; + +namespace Sozsoft.Platform.Identity; + +/// +/// OpenIddict'i uzak MCP istemcilerinin (Claude, ChatGPT connector'lari) bekledigi OAuth +/// sozlesmesine uyarlar. Yalnizca MCP istemcisini ilgilendiren davranislar istemci kimligine +/// gore sinirlanir; web ve mobil istemcilerin akisina dokunulmaz. +/// +public static class McpOAuthServerConfiguration +{ + public static void Configure(OpenIddictServerBuilder server, IConfiguration configuration) + { + var authority = configuration["AuthServer:Authority"]; + + // Ters vekil arkasinda istek HTTP olarak gelir; yayinci istekten turetilirse + // http://... olur ve istemci kesif belgesindeki issuer'i kaynak meta verisindeki + // yetkilendirme sunucusuyla eslestiremez. Yayinci ortam ayarindan sabitlenir. + if (!string.IsNullOrWhiteSpace(authority)) + { + server.SetIssuer(new Uri(authority)); + } + + // MCP istemcileri once RFC 8414 adresini dener; ayni belge iki adresten de sunulur. + server.SetConfigurationEndpointUris( + ".well-known/openid-configuration", + ".well-known/oauth-authorization-server"); + + server.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + if (!string.IsNullOrWhiteSpace(authority)) + { + context.Metadata["registration_endpoint"] = + authority.TrimEnd('/') + McpConsts.RegistrationEndpointPath; + } + + return default; + }) + .SetType(OpenIddictServerHandlerType.Custom)); + + // MCP istemcileri her istekte RFC 8707 "resource" parametresini (/mcp adresi) gonderir. + // OpenIddict kayitsiz kaynaklari reddeder ve kabul etse bile token'in hedef kitlesini o + // adrese daraltir; API ise "Platform" kitlesini dogrular. RFC 8707 sunucunun parametreyi + // yok saymasina izin verir: MCP istemcisinin istegindeki parametre cikarilir, token + // diger istemcilerle ayni kitleyle uretilir. + server.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + RemoveResourceParameter(context.Request); + return default; + }) + .SetOrder(int.MaxValue - 100_000) + .SetType(OpenIddictServerHandlerType.Custom)); + + server.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + RemoveResourceParameter(context.Request); + return default; + }) + .SetOrder(int.MaxValue - 100_000) + .SetType(OpenIddictServerHandlerType.Custom)); + } + + private static void RemoveResourceParameter(OpenIddictRequest? request) + { + if (request is null || !McpConsts.IsMcpClient(request.ClientId)) + { + return; + } + + request.RemoveParameter(OpenIddictConstants.Parameters.Resource); + } + + /// + /// Token uretiminde MCP istemcisinin kullanici oturumuna baglanmamasi gerektigini soyler. + /// Platform tek oturum kuralini uygular: her giris kullanicinin diger oturumlarini siler. + /// Connector token'i bir oturum acarsa her yenilemede kullanicinin web oturumunu duser, + /// kullanici web'e girdiginde de connector baglantisi kopar. + /// + public static bool IsSessionlessClient(string? clientId) + => McpConsts.IsMcpClient(clientId); +} diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformTokenController.cs b/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformTokenController.cs index ee1f41d8..244aa8b7 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformTokenController.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/Identity/PlatformTokenController.cs @@ -278,6 +278,11 @@ Your login code: {twoFactorToken}"; [UnitOfWork] protected override async Task HandleRefreshTokenAsync(OpenIddictRequest request) { + if (McpOAuthServerConfiguration.IsSessionlessClient(request.ClientId)) + { + return await base.HandleRefreshTokenAsync(request); + } + var info = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); var tenantId = await ResolveRefreshTenantIdAsync(info); var userGuid = await ResolveRefreshUserIdAsync(request, info, tenantId); diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs index a6af7989..cc9afdc7 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs @@ -8,6 +8,7 @@ using DevExpress.XtraReports.Web.Extensions; using Hangfire; using Hangfire.PostgreSql; using Hangfire.SqlServer; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Extensions.DependencyInjection; @@ -24,6 +25,7 @@ using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; using OpenIddict.Server.AspNetCore; using OpenIddict.Validation.AspNetCore; +using Sozsoft.Mcp; using Sozsoft.Languages; using Sozsoft.MailQueue; using Sozsoft.Notifications.Application; @@ -77,7 +79,9 @@ namespace Sozsoft.Platform; typeof(AbpAspNetCoreSerilogModule), typeof(AbpSwashbuckleModule), typeof(AbpBackgroundWorkersHangfireModule), - typeof(SqlQueryManagerApplicationModule) + typeof(SqlQueryManagerApplicationModule), + typeof(McpApplicationModule), + typeof(McpHttpApiModule) )] public class PlatformHttpApiHostModule : AbpModule { @@ -103,6 +107,10 @@ public class PlatformHttpApiHostModule : AbpModule }); }); + // Uzak MCP istemcilerinin OAuth sozlesmesi. Uc adresleri ABP'nin kendi sunucu + // ayarlarindan sonra uygulanmali; bu yuzden OpenIddictServerBuilder uzerinden verilir. + PreConfigure(server => McpOAuthServerConfiguration.Configure(server, configuration)); + PreConfigure(builder => { builder.AddClaimsPrincipalFactory(); @@ -509,6 +517,7 @@ public class PlatformHttpApiHostModule : AbpModule await next(); }); + app.UseMiddleware(); app.UseAuthentication(); app.UseAbpOpenIddictValidation(); app.UseMiddleware(); @@ -552,6 +561,15 @@ public class PlatformHttpApiHostModule : AbpModule app.UseConfiguredEndpoints(endpoints => { endpoints.MapHub("/messengerhub"); + + // MCP (Model Context Protocol) ucu. Kimlik dogrulama, kiraci cozumu ve unit of work + // yukaridaki ardisik duzenden gelir; araclar kendi [Authorize] yetkilerini ister. + // Sema acikca bearer'a baglanir: varsayilan cerez semasi token'siz istegi giris + // sayfasina 302 ile yonlendirir, MCP istemcisi ise 401 bekler. + endpoints.MapMcp("/mcp").RequireAuthorization(new AuthorizeAttribute + { + AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme + }); }); } diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/Sozsoft.Platform.HttpApi.Host.csproj b/api/src/Sozsoft.Platform.HttpApi.Host/Sozsoft.Platform.HttpApi.Host.csproj index 617dab1f..64b33d50 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/Sozsoft.Platform.HttpApi.Host.csproj +++ b/api/src/Sozsoft.Platform.HttpApi.Host/Sozsoft.Platform.HttpApi.Host.csproj @@ -45,6 +45,8 @@ + + diff --git a/claude.md b/claude.md index abd3d71f..194eeebc 100644 --- a/claude.md +++ b/claude.md @@ -182,6 +182,29 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` → Ayrıntı: `dotnet.instructions.md` §5. - **Yeni bir yetki** → `PlatformConsts.AppCodes` + `PermissionsData.json` + (UI'da kullanılacaksa) `permission.constant.ts` + `LanguagesData.json`. +- **`/mcp` ucuna yeni bir araç** → üç yer birlikte değişir: `McpToolNames` (ad kayıt defteri), + 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. + 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` + sözlüğünden okunur (`IMcpSchemaDictionary`). Sözlüğün alan adları + `dbo.Sas_H_ListFormFieldsDictionaryJson` prosedürünün ürettiği JSON ile birebir aynıdır; + prosedür değişirse `McpDictionaryListForm`/`McpDictionaryField` de değişir. +- **MCP OAuth** → bilinen sağlayıcılar (Claude, ChatGPT) seed edilmiş `Platform_Mcp` istemcisini, + diğer MCP istemcileri `/connect/register` ile üretilen `mcp_` önekli açık onaylı istemcileri + kullanır (`McpOAuthClientManager`; seed `McpOAuthClientDataSeedContributor`). Bilinen sağlayıcı + eklemek = DbMigrator'daki `Platform_Mcp:RedirectUris` listesine callback eklemek. MCP + istemcisi kararı tek yerdedir (`McpConsts.IsMcpClient`): `resource` parametresi yok sayılır ve + token kullanıcı oturumuna bağlanmaz; token ucuna yeni bir oturum yolu eklenirse bu istisna + korunur, yoksa connector web oturumunu düşürür. +- **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. - **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/configs/nginx.conf b/configs/deployment/configs/nginx.conf index f55f6449..6ebbd3de 100644 --- a/configs/deployment/configs/nginx.conf +++ b/configs/deployment/configs/nginx.conf @@ -105,6 +105,22 @@ server { proxy_send_timeout 3600; } + # MCP (Model Context Protocol) ucu. Streamable HTTP yanitlari SSE akisidir: location / + # altindaki tamponlama akisi bekletip istemcinin zaman asimina dusmesine yol acar, + # bu yuzden ayri bir blokta tamponlama kapatilir. + location /mcp { + proxy_pass http://127.0.0.1:8080/mcp; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header __tenant $http___tenant; + proxy_set_header Connection ""; + proxy_buffering off; + proxy_cache off; + chunked_transfer_encoding on; + proxy_read_timeout 3600; + proxy_send_timeout 3600; + } + location / { proxy_pass http://127.0.0.1:8080/; include /etc/nginx/proxy_params; diff --git a/configs/seeds/host/data/App.DeveloperKit.McpServers.json b/configs/seeds/host/data/App.DeveloperKit.McpServers.json new file mode 100644 index 00000000..5d7df705 --- /dev/null +++ b/configs/seeds/host/data/App.DeveloperKit.McpServers.json @@ -0,0 +1,20 @@ +{ + "ListFormCode": "App.DeveloperKit.McpServers", + "KeyFieldName": "Id", + "GeneratedAt": "2026-09-10T00:00:00Z", + "Order": 0, + "Rows": [ + { + "Id": "8f3a41d2-6b1c-4e57-9a0d-2c7f5e84b310", + "Name": "Sozsoft Platform", + "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." + } + ] +}