Mcp güncellemesi

This commit is contained in:
Sedat ÖZTÜRK 2026-09-10 17:40:43 +03:00
parent 42d3c55ae3
commit 6dee1c898b
79 changed files with 4559 additions and 8 deletions

View file

@ -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ı. |

109
README.md
View file

@ -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 <access_token>"
# localhost (dotnet run / IIS Express)
claude mcp add sozsoft-local --transport http https://localhost:44344/mcp \n --header "Authorization: Bearer <access_token>"
```
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

View file

@ -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/"

View file

@ -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/"

View file

@ -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}

View file

@ -0,0 +1,39 @@
using System.Collections.Generic;
namespace Sozsoft.Mcp.Dtos;
/// <summary>Dinamik istemci kaydi (RFC 7591) istegi.</summary>
public class McpClientRegistrationRequestDto
{
public string? ClientName { get; set; }
public List<string> RedirectUris { get; set; } = [];
}
/// <summary>
/// Dinamik istemci kaydinin sonucu. Kayit basarisizsa <see cref="Error"/> RFC 7591 hata kodunu
/// tasir ve diger alanlar bos kalir.
/// </summary>
public class McpClientRegistrationResultDto
{
public string? ClientId { get; set; }
public string? ClientName { get; set; }
public List<string> RedirectUris { get; set; } = [];
public List<string> GrantTypes { get; set; } = [];
public List<string> ResponseTypes { get; set; } = [];
/// <summary>Genel istemci: token ucunda gizli anahtar kullanilmaz, PKCE zorunludur.</summary>
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; }
}

View file

@ -0,0 +1,127 @@
using System.Collections.Generic;
namespace Sozsoft.Mcp.Dtos;
/// <summary>
/// Bir listenin verisini ekranin kendi sorgusuyla okuma istegi. Filtre, siralama ve gruplama
/// DevExtreme veri yukleme bicimindedir; ekranin grid'i de ayni bicimi kullanir.
/// </summary>
public class McpListQueryRequestDto
{
public string ListFormCode { get; set; } = string.Empty;
/// <summary>Ornek: <c>["Status","=","Open"]</c> ya da <c>[["A","&gt;",1],"and",["B","contains","x"]]</c>.</summary>
public string? Filter { get; set; }
/// <summary>Ornek: <c>[{"selector":"CreationTime","desc":true}]</c>.</summary>
public string? Sort { get; set; }
/// <summary>Ornek: <c>[{"selector":"CustomerId","desc":false,"isExpanded":false}]</c>.</summary>
public string? Group { get; set; }
/// <summary>Ornek: <c>[{"selector":"Id","summaryType":"count"}]</c>.</summary>
public string? GroupSummary { get; set; }
/// <summary>Ornek: <c>[{"selector":"Amount","summaryType":"sum"}]</c>.</summary>
public string? TotalSummary { get; set; }
public int Skip { get; set; }
/// <summary>Istenen satir sayisi; sunucu kaydindaki tavana kirpilir.</summary>
public int Take { get; set; }
}
/// <summary>Liste sorgusunun sonucu.</summary>
public class McpListQueryResultDto
{
public object? Data { get; set; }
/// <summary>Filtreye uyan toplam kayit sayisi (sayfalamadan once).</summary>
public int TotalCount { get; set; }
public int GroupCount { get; set; }
public object[]? Summary { get; set; }
/// <summary>Uygulanan satir tavani; <see cref="TotalCount"/> bundan buyukse sonuc sayfalanmistir.</summary>
public int Take { get; set; }
}
/// <summary>Parametreli tek SELECT istegi.</summary>
public class McpSelectRequestDto
{
public string Sql { get; set; } = string.Empty;
/// <summary>Sorgudaki adli parametreler.</summary>
public Dictionary<string, object?> Parameters { get; set; } = [];
/// <summary>Istenen satir sayisi; sunucu kaydindaki tavana kirpilir.</summary>
public int MaxRows { get; set; }
}
/// <summary>
/// Kayitli bir veritabani nesnesi (view, stored procedure, function) ya da Custom Endpoint.
/// </summary>
public class McpSavedQueryDto
{
/// <summary><c>McpConsts.SavedQueryKinds</c> degerlerinden biri.</summary>
public string Kind { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string? SchemaName { get; set; }
public string? Description { get; set; }
/// <summary>Custom Endpoint'te beklenen parametre adlari; veritabani nesnelerinde bostur.</summary>
public List<string> ParameterNames { get; set; } = [];
}
/// <summary>Kayitli bir nesnenin ya da Custom Endpoint'in cagrilmasi.</summary>
public class McpSavedQueryRequestDto
{
/// <summary><see cref="McpSavedQueryDto.Kind"/> ile ayni degerler.</summary>
public string Kind { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
/// <summary>Bos birakilirsa saglayicinin varsayilan semasi kullanilir.</summary>
public string? SchemaName { get; set; }
public Dictionary<string, object?> Parameters { get; set; } = [];
public int MaxRows { get; set; }
}
/// <summary>Sorgu sonucu.</summary>
public class McpQueryResultDto
{
public List<string> Columns { get; set; } = [];
public List<Dictionary<string, object?>> Rows { get; set; } = [];
public int RowCount { get; set; }
/// <summary>Satir sayisi tavana dayandiginda true; sonuc kesilmis demektir.</summary>
public bool Truncated { get; set; }
public long ExecutionTimeMs { get; set; }
}
/// <summary>Bir ListForm ekrani uzerinden kayit yazma/silme istegi.</summary>
public class McpRowRequestDto
{
public string ListFormCode { get; set; } = string.Empty;
/// <summary>Alan adi → deger. Silme isleminde bos birakilir.</summary>
public Dictionary<string, object?> Values { get; set; } = [];
/// <summary>Guncelleme ve silmede anahtar alan degerleri.</summary>
public List<object?> Keys { get; set; } = [];
}
/// <summary>Yazma isleminin sonucu.</summary>
public class McpWriteResultDto
{
/// <summary>Insert'te uretilen anahtar; guncelleme ve silmede bostur.</summary>
public string? Key { get; set; }
/// <summary>Guncelleme ve silmede etkilenen satir sayisi.</summary>
public int AffectedRows { get; set; }
}

View file

@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
namespace Sozsoft.Mcp.Dtos;
/// <summary>
/// Istegin kiracisina dusen MCP sunucu kaydinin etkin hali. /mcp ucu hangi araclari
/// listeleyecegine ve sorgularin hangi satir tavaniyla kosacagina bu kayda bakarak karar verir.
/// </summary>
public class McpServerConfigDto
{
public string Name { get; set; } = string.Empty;
/// <summary>Ekranda gosterilen baglanti adresi; sunucu tarafinda yonlendirme icin kullanilmaz.</summary>
public string? Url { get; set; }
/// <summary>Bos ise istegin kapsamindaki varsayilan veri kaynagi kullanilir.</summary>
public string? DataSourceCode { get; set; }
/// <summary>Bos liste "yazma disindaki tum araclar acik" anlamina gelir.</summary>
public List<string> AllowedTools { get; set; } = [];
public int MaxRows { get; set; }
public bool AllowWrite { get; set; }
public string? Description { get; set; }
/// <summary>Istegin cozumlenen kiraci adi; host baglaminda bostur.</summary>
public string? TenantName { get; set; }
}
/// <summary>
/// Veri sozlugunun tazelik bilgisi. Sozluk <c>Sas_H_DbTableColumn</c> tablosunda, sozluk
/// ureten prosedur tarafindan yazilmis JSON olarak durur.
/// </summary>
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; }
/// <summary>Sozluk bulunamadiginda ne yapilacagini anlatan mesaj.</summary>
public string? Message { get; set; }
}
/// <summary>Bir listenin (ListForm) ozet tanimi.</summary>
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; }
/// <summary>Listenin arkasindaki tablo adi; SELECT tabanli listelerde bostur.</summary>
public string? TableName { get; set; }
/// <summary>Tablo adi ya da SELECT metni — listenin gercek SQL kaynagi.</summary>
public string? SqlSourceName { get; set; }
/// <summary><c>table</c>, <c>selectSql</c> ya da <c>unknown</c>.</summary>
public string? SourceKind { get; set; }
public string? KeyFieldName { get; set; }
public int FieldCount { get; set; }
}
/// <summary>Bir listenin sutun tanimi.</summary>
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; }
/// <summary>Lookup tasiyan sutunda hedef tablo; yoksa bostur.</summary>
public string? LookupTableName { get; set; }
public string? LookupValueColumn { get; set; }
public string? LookupDisplayColumn { get; set; }
/// <summary>Parola/token/anahtar gibi sutunlarda true; sorgu araclari bunlari onermez.</summary>
public bool IsSensitive { get; set; }
/// <summary>Varsayilan SELECT listesine girmesi uygun olan sutunlarda true.</summary>
public bool IsDefaultSelectColumn { get; set; }
}
/// <summary>Bir listenin sutunlariyla birlikte tam tanimi.</summary>
public class McpListDetailDto : McpListSummaryDto
{
public List<McpColumnDto> Columns { get; set; } = [];
}
/// <summary>Serbest metin aramasinin tek sonucu.</summary>
public class McpSchemaMatchDto
{
public string ListFormCode { get; set; } = string.Empty;
public string? DisplayName { get; set; }
public string? TableName { get; set; }
/// <summary>Eslesme yalnizca liste duzeyindeyse bos; sutun duzeyindeyse eslesen sutunlar.</summary>
public List<McpColumnDto> MatchedColumns { get; set; } = [];
}

View file

@ -0,0 +1,19 @@
using System.Threading.Tasks;
using Sozsoft.Mcp.Dtos;
using Volo.Abp.Application.Services;
namespace Sozsoft.Mcp;
/// <summary>
/// MCP veri yazma yuzeyi. Yazma her zaman bir ListForm ekraninin insert/update/delete
/// sozlesmesinden gecer; ekran yetkisi ve tenant filtresi aynen uygulanir. Sunucu kaydinda
/// <c>AllowWrite</c> kapali oldugunda bu araclar hic listelenmez.
/// </summary>
public interface IMcpDataAppService : IApplicationService
{
Task<McpWriteResultDto> InsertRowAsync(McpRowRequestDto input);
Task<McpWriteResultDto> UpdateRowAsync(McpRowRequestDto input);
Task<McpWriteResultDto> DeleteRowAsync(McpRowRequestDto input);
}

View file

@ -0,0 +1,16 @@
using System.Threading.Tasks;
using Sozsoft.Mcp.Dtos;
using Volo.Abp.Application.Services;
namespace Sozsoft.Mcp;
/// <summary>
/// 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.
/// </summary>
public interface IMcpOAuthAppService : IApplicationService
{
Task<McpClientRegistrationResultDto> RegisterClientAsync(McpClientRegistrationRequestDto input);
}

View file

@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Sozsoft.Mcp.Dtos;
using Volo.Abp.Application.Services;
namespace Sozsoft.Mcp;
/// <summary>
/// MCP salt-okunur sorgu yuzeyi. Her sorgu istegin kapsamindaki veri kaynaginda, sunucu
/// kaydindaki satir tavaniyla ve parametreli olarak kosar.
/// </summary>
public interface IMcpQueryAppService : IApplicationService
{
/// <summary>
/// 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.
/// </summary>
Task<McpListQueryResultDto> QueryListAsync(McpListQueryRequestDto input);
/// <summary>
/// Tek bir SELECT ifadesi kosturur. Coklu ifade, DDL ve DML reddedilir; satir sayisi
/// sunucu kaydindaki tavana kirpilir.
/// </summary>
Task<McpQueryResultDto> RunSelectAsync(McpSelectRequestDto input);
/// <summary>Veri kaynagindaki view/procedure/function nesneleri ve Custom Endpoint'ler.</summary>
Task<List<McpSavedQueryDto>> GetSavedQueriesAsync(string? search = null);
/// <summary>Kayitli bir nesneyi ya da Custom Endpoint'i parametreleriyle cagirir.</summary>
Task<McpQueryResultDto> RunSavedQueryAsync(McpSavedQueryRequestDto input);
}

View file

@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Sozsoft.Mcp.Dtos;
using Volo.Abp.Application.Services;
namespace Sozsoft.Mcp;
/// <summary>
/// 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.
/// </summary>
public interface IMcpSchemaAppService : IApplicationService
{
/// <summary>Istegin kiracisina dusen etkin MCP sunucu kaydi; kayit yoksa null.</summary>
Task<McpServerConfigDto?> GetServerConfigAsync();
/// <summary>
/// Bu kullanicinin bu baglantida cagirabilecegi araclar: sunucu kaydinin actiklari ile
/// kullanicinin uygulamadaki yetkilerinin kesisimi.
/// </summary>
Task<List<string>> GetAvailableToolsAsync();
/// <summary>Veri sozlugunun bulunup bulunmadigi ve ne zaman tazelendigi.</summary>
Task<McpSchemaInfoDto> GetSchemaInfoAsync();
/// <param name="search">Bos birakilirsa tum listeler doner.</param>
/// <param name="maxResults">Donecek en fazla liste sayisi.</param>
Task<List<McpListSummaryDto>> GetListsAsync(string? search = null, int maxResults = 200);
/// <summary>Tek bir listenin sutunlariyla birlikte tanimi; bulunamazsa null.</summary>
Task<McpListDetailDto?> GetListAsync(string listFormCode);
/// <summary>
/// Liste ve sutun adlari ile basliklari uzerinde aksan ve buyuk/kucuk harf duyarsiz arama.
/// </summary>
Task<List<McpSchemaMatchDto>> SearchAsync(string term, int maxResults = 50);
}

View file

@ -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
{
}

View file

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\common.props" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<RootNamespace>Sozsoft.Mcp</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.Ddd.Application.Contracts" Version="10.0.0" />
<PackageReference Include="Volo.Abp.Authorization" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Sozsoft.Mcp.Domain.Shared\Sozsoft.Mcp.Domain.Shared.csproj" />
</ItemGroup>
</Project>

View file

@ -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;
/// <summary>
/// MCP uygulama servislerinin ortak tabani. Kullaniciya gorunen metinler platformun
/// <c>PlatformResource</c> kaynagindan okunur; sunucu kaydini cozme ve arac kapisi her
/// serviste ayni davranir.
/// </summary>
public abstract class McpAppService : ApplicationService
{
protected IMcpServerConfigProvider ServerConfigProvider { get; }
protected McpAppService(IMcpServerConfigProvider serverConfigProvider)
{
ServerConfigProvider = serverConfigProvider;
LocalizationResource = typeof(PlatformResource);
}
/// <summary>Etkin sunucu kaydinin DTO hali; kayit yoksa null.</summary>
protected async Task<McpServerConfigDto?> 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
};
}
/// <summary>
/// Kaydi zorunlu kilan islemler icin. Kayit yoksa istemcinin nedenini gorebilecegi bir
/// hata atar.
/// </summary>
protected async Task<McpServerConfigDto> RequireServerConfigAsync()
=> await ResolveServerConfigAsync()
?? throw new UserFriendlyException(L[McpErrorCodes.NoServerRecord]);
/// <summary>
/// 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.
/// </summary>
protected async Task<McpServerConfigDto> 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;
}
/// <summary>Istenen satir sayisini sunucu kaydindaki tavana kirpar.</summary>
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;
}
}

View file

@ -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
{
}

View file

@ -0,0 +1,47 @@
namespace Sozsoft.Mcp;
/// <summary>
/// Kayitli nesneleri listelemek icin kullanilan katalog sorgulari. Parametre almazlar;
/// sabit metin oldugu icin string birlestirme riski yoktur.
/// </summary>
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
""";
}

View file

@ -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;
/// <summary>
/// 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.
/// </summary>
[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Write)]
public class McpDataAppService(
IMcpServerConfigProvider serverConfigProvider,
IListFormDataAppService listFormDataAppService)
: McpAppService(serverConfigProvider), IMcpDataAppService
{
public async Task<McpWriteResultDto> 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<McpWriteResultDto> 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<McpWriteResultDto> 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 };
}
/// <summary>
/// Istegi ekranin bekledigi bicime cevirir. <c>Data</c> JSON metni olarak verilir; ayni
/// bicim ekranin kopyalama yolunda da kullanilir.
/// </summary>
private async Task<DataRequestDto> 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<string, object?>()),
Keys = [.. (input.Keys ?? []).Select(key => key!)]
};
}
}

View file

@ -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;
/// <summary>
/// Dinamik istemci kaydi (RFC 7591). Uc kimlik dogrulamasizdir; istemci ne alirsa alsin token
/// kullanicinin kendi girisiyle ve kendi yetkileriyle uretilir. Kural ve kayit
/// <see cref="IMcpOAuthClientManager"/> icindedir.
/// </summary>
[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<McpClientRegistrationResultDto> 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()
}
};
}
}

View file

@ -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;
/// <summary>
/// Salt-okunur sorgu yuzeyi. MCP kullaniciya yetki vermez, uygulamadaki yetkisini tasir:
/// <list type="bullet">
/// <item><c>query_list</c> ekranin kendi sorgusundan gecer; liste ve alan yetkileri, kiraci ve
/// sube filtreleri aynen uygulanir.</item>
/// <item>Ham SQL ve veritabani nesneleri (view/procedure/function) liste yetkilerini asar; bu
/// yuzden yalnizca uygulamada SQL Query Manager yetkisi olan kullaniciya aciktir.</item>
/// <item>Custom Endpoint'ler endpoint dispatcher'inin yetkisini ve endpoint'in kendi
/// kullanici/rol listesini ister.</item>
/// </list>
/// SQL kurallari <see cref="IMcpSqlGuard"/>, veri kaynagi cozumu ve calistirma
/// <see cref="IMcpQueryExecutor"/> icindedir.
/// </summary>
[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Invoke)]
public class McpQueryAppService(
IMcpServerConfigProvider serverConfigProvider,
IMcpSqlGuard sqlGuard,
IMcpQueryExecutor queryExecutor,
IRepository<CustomEndpoint, Guid> 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<McpListQueryResultDto> 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<McpQueryResultDto> 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<List<McpSavedQueryDto>> GetSavedQueriesAsync(string? search = null)
{
var config = await EnsureToolAllowedAsync(McpToolNames.ListQueries);
var results = new List<McpSavedQueryDto>();
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<List<McpSavedQueryDto>> 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<string, object>(), int.MaxValue);
var results = new List<McpSavedQueryDto>();
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<McpQueryResultDto> 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<McpQueryResultDto> 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<List<McpSavedQueryDto>> 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)]
})];
}
/// <summary>
/// Yalnizca okuma amacli (GET) ve istegi yapan kullaniciya acik Custom Endpoint kayitlari.
/// Yetki sozlesmesi endpoint dispatcher'indaki ile birebir aynidir.
/// </summary>
private async Task<List<CustomEndpoint>> 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<string, object> 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<string, object?> row, string key)
=> row.TryGetValue(key, out var value) ? value?.ToString() : null;
}

View file

@ -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;
/// <summary>
/// Sema kesfini JSON veri sozlugu uzerinden karsilar; sozlugu okuma ve arama normalizasyonu
/// domain servisindedir (<see cref="IMcpSchemaDictionary"/>). Kullanici yalnizca uygulamada
/// okuma yetkisi olan listeleri gorur (<see cref="IMcpListFormAccessFilter"/>).
/// </summary>
[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Invoke)]
public class McpSchemaAppService(
IMcpServerConfigProvider serverConfigProvider,
IMcpSchemaDictionary dictionary,
IMcpListFormAccessFilter accessFilter,
IPermissionChecker permissionChecker)
: McpAppService(serverConfigProvider), IMcpSchemaAppService
{
public async Task<McpServerConfigDto?> GetServerConfigAsync()
=> await ResolveServerConfigAsync();
public async Task<List<string>> 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<string>(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<McpSchemaInfoDto> 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<List<McpListSummaryDto>> 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<McpListDetailDto?> 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<List<McpSchemaMatchDto>> 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<McpSchemaMatchDto>();
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<List<McpDictionaryListForm>> 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;
}

View file

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\common.props" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<RootNamespace>Sozsoft.Mcp</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.Ddd.Application" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Sozsoft.Mcp.Application.Contracts\Sozsoft.Mcp.Application.Contracts.csproj" />
<ProjectReference Include="..\Sozsoft.Mcp.Domain\Sozsoft.Mcp.Domain.csproj" />
<!-- Veri yazma ekranin insert/update/delete sozlesmesinden gecer. -->
<ProjectReference Include="..\..\..\src\Sozsoft.Platform.Application.Contracts\Sozsoft.Platform.Application.Contracts.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,104 @@
namespace Sozsoft.Mcp;
/// <summary>MCP modulunun sabit sozlesme degerleri.</summary>
public static class McpConsts
{
/// <summary>
/// Veri sozlugu satirlarinin turu. <c>Sas_H_DbTableColumn</c> tablosunda ayni deger
/// <c>DictionaryType</c> kolonunda durur.
/// </summary>
public const string DictionaryType = "ListFormJson";
/// <summary>
/// Sozluk ureten prosedurun varsayilan <c>@TenantName</c> degeri. Host baglaminda sozluk
/// bu adla yazilir.
/// </summary>
public const string DefaultDictionaryTenantName = "Sozsoft";
/// <summary>Sunucu kaydinda satir tavani verilmediginde (ya da sifir oldugunda) uygulanan deger.</summary>
public const int DefaultMaxRows = 200;
/// <summary>
/// <c>CustomEndpoint.DataSourceCode</c> bu degeri tasidiginda istegi yapan kullanicinin
/// kiraci veritabani kullanilir.
/// </summary>
public const string TenantDataSourceMarker = "!Tenant";
/// <summary>
/// Sozluk ureten prosedurun adi. Sozluk bulunamadiginda istemciye ne yapacagi bu adla
/// bildirilir.
/// </summary>
public const string DictionaryProcedureName = "dbo.Sas_H_ListFormFieldsDictionaryJson_SaveDbTableColumns";
/// <summary>
/// Uzak MCP istemcilerinin (Claude, ChatGPT connector'lari) OAuth istemci kimligi. OpenIddict
/// uygulamasi DbMigrator'da <c>OpenIddict:Applications:Platform_Mcp</c> ayarindan seed edilir;
/// dinamik istemci kaydi yeni uygulama uretmez, bu kimligi dondurur.
/// </summary>
public const string OAuthClientId = "Platform_Mcp";
/// <summary>
/// Dinamik kayitla (RFC 7591) uretilen istemcilerin kimlik oneki. Bilinen saglayicilarin
/// (Claude, ChatGPT) disindaki MCP istemcileri bu onekle kendi OpenIddict uygulamasini alir.
/// </summary>
public const string DynamicClientIdPrefix = "mcp_";
/// <summary>
/// Dinamik istemci sayisinin tavani. Kayit ucu kimlik dogrulamasizdir; tavan, ucun
/// veritabaninda sinirsiz uygulama biriktirmesini onler.
/// </summary>
public const int MaxDynamicClients = 500;
/// <summary>
/// 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.
/// </summary>
public static readonly System.TimeSpan RefreshTokenLifetime = System.TimeSpan.FromDays(30);
/// <summary>Istemci kimligi bir MCP istemcisine (bilinen ya da dinamik) mi ait.</summary>
public static bool IsMcpClient(string? clientId)
=> clientId is not null
&& (string.Equals(clientId, OAuthClientId, System.StringComparison.Ordinal)
|| clientId.StartsWith(DynamicClientIdPrefix, System.StringComparison.Ordinal));
/// <summary>MCP ucunun sunucu adresine gore yolu.</summary>
public const string EndpointPath = "/mcp";
/// <summary>Dinamik istemci kaydi (RFC 7591) ucunun yolu.</summary>
public const string RegistrationEndpointPath = "/connect/register";
/// <summary>Korumali kaynak meta verisi (RFC 9728) ucunun yolu.</summary>
public const string ProtectedResourceMetadataPath = "/.well-known/oauth-protected-resource";
/// <summary>
/// Istemcinin istemesi gereken kapsamlar. <c>offline_access</c> yenileme token'i icindir;
/// olmadan baglanti her saat yeniden giris ister.
/// </summary>
public static readonly string[] OAuthScopes = [Sozsoft.Platform.PlatformConsts.AppName, "offline_access"];
/// <summary>
/// <c>run_query</c> araciyla cagrilabilen nesne turleri. Degerler istemciye oldugu gibi
/// gosterilir; degistirilirse arac sozlesmesi kirilir.
/// </summary>
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";
}
/// <summary>
/// Veritaban&#305; katalog sorgusunun dondurdugu nesne turu etiketleri.
/// <see cref="SavedQueryKinds"/> degerlerine eslenir.
/// </summary>
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";
}
}

View file

@ -0,0 +1,18 @@
using Volo.Abp.Domain;
using Volo.Abp.Modularity;
using Volo.Abp.Validation;
namespace Sozsoft.Mcp;
/// <summary>
/// Modulun paylasilan sozlesmesi. Kullaniciya gorunen metinler platformun kendi
/// <c>PlatformResource</c> kaynagindan (LanguagesData.json) okunur; modul ikinci bir dil
/// kaynagi acmaz.
/// </summary>
[DependsOn(
typeof(AbpValidationModule),
typeof(AbpDddDomainSharedModule)
)]
public class McpDomainSharedModule : AbpModule
{
}

View file

@ -0,0 +1,28 @@
namespace Sozsoft.Mcp;
/// <summary>
/// MCP hatalarinin dil anahtarlari. Karsiliklari platformun <c>LanguagesData.json</c>
/// dosyasinda EN + TR olarak durur; koda gomulu metin kullanilmaz.
/// </summary>
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";
}

View file

@ -0,0 +1,73 @@
using System.Collections.Generic;
using System.Linq;
using Sozsoft.Platform;
namespace Sozsoft.Mcp;
/// <summary>
/// /mcp ucunda yayinlanan arac adlari. Ayni degerler MCP Servers ekranindaki
/// <c>AllowedTools</c> alaninin secenek listesini besler; buraya bir arac eklenirse ekranin
/// statik lookup listesi de guncellenir.
/// </summary>
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";
/// <summary>
/// Veri yazan araclar. Sunucu kaydinda <c>AllowWrite</c> kapaliysa bunlar arac listesinde
/// hic yer almaz.
/// </summary>
public static readonly string[] WriteTools = [InsertRow, UpdateRow, DeleteRow];
/// <summary>
/// Sunucu kaydi olmasa da acik kalan araclar. Istemcinin baglantisinin neden bos
/// gorundugunu anlayabilmesi icin gereklidir.
/// </summary>
public static readonly string[] AlwaysAvailableTools = [ServerInfo];
/// <summary>
/// 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.
/// </summary>
public static readonly IReadOnlyDictionary<string, string> RequiredPermissions = new Dictionary<string, string>
{
[RunSelect] = PlatformConsts.AppCodes.SqlQueryManagers.Default,
[InsertRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
[UpdateRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
[DeleteRow] = PlatformConsts.AppCodes.DeveloperKits.McpServers.Write,
};
/// <summary>Yayinlanan tum araclar, arac listesindeki siralarina gore.</summary>
public static readonly string[] All =
[
ServerInfo,
ListLists,
DescribeList,
SearchSchema,
QueryList,
RunSelect,
ListQueries,
RunQuery,
InsertRow,
UpdateRow,
DeleteRow
];
/// <summary>
/// <c>AllowedTools</c> alaninda secilebilen araclar. Her zaman acik olanlar secenek
/// olarak sunulmaz; onlari kapatmak mumkun degildir.
/// </summary>
public static IEnumerable<string> SelectableTools
=> All.Where(name => !AlwaysAvailableTools.Contains(name));
}

View file

@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Sozsoft.Mcp;
/// <summary>
/// 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.
/// </summary>
public static class McpToolPolicy
{
/// <param name="hasServerRecord">Istegin kiracisina dusen aktif bir sunucu kaydi var mi.</param>
/// <param name="allowWrite">Sunucu kaydinda veri yazma acik mi.</param>
/// <param name="allowedTools">Acik arac adlari; bos liste "yazma disindaki tumu acik" demektir.</param>
public static bool IsAllowed(
bool hasServerRecord,
bool allowWrite,
IReadOnlyCollection<string> 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<string> VisibleTools(
bool hasServerRecord,
bool allowWrite,
IReadOnlyCollection<string> allowedTools)
=> [.. McpToolNames.All.Where(name => IsAllowed(hasServerRecord, allowWrite, allowedTools, name))];
}

View file

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\common.props" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<RootNamespace>Sozsoft.Mcp</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.Ddd.Domain.Shared" Version="10.0.0" />
<PackageReference Include="Volo.Abp.Validation" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<!-- Yetki adlari ve tablo adi platformun tek kayit defterinde durur
(PlatformConsts.AppCodes, TableNameResolver); modul bunlari kopyalamaz. -->
<ProjectReference Include="..\..\..\src\Sozsoft.Platform.Domain.Shared\Sozsoft.Platform.Domain.Shared.csproj" />
</ItemGroup>
</Project>

View file

@ -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;
/// <summary>
/// Bilinen MCP istemcisini (<see cref="McpConsts.OAuthClientId"/>) seed eder. Geri donus
/// adresleri <c>OpenIddict:Applications:Platform_Mcp:RedirectUris</c> ayarindadir; yeni bir
/// saglayici eklemek bu listeye callback eklemektir.
/// </summary>
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<string[]>();
if (redirectUris is null || redirectUris.Length == 0)
{
return;
}
await clientManager.EnsureKnownClientAsync(redirectUris);
}
}

View file

@ -0,0 +1,49 @@
using System;
using Volo.Abp.Domain.Entities.Auditing;
namespace Sozsoft.Mcp.Entities;
/// <summary>
/// MCP (Model Context Protocol) sunucu tanimi. Kayit host kapsamindadir; hangi kiracilarin
/// kullanabilecegi <see cref="Tenants"/> ile belirlenir. Istegin kiracisina dusen kayit, /mcp
/// ucunda hangi araclarin gorunecegini ve sorgularin hangi veri kaynaginda hangi satir
/// tavaniyla kosacagini belirler.
/// </summary>
public class McpServer : FullAuditedEntity<Guid>
{
public string Name { get; set; } = string.Empty;
public bool IsActive { get; set; } = true;
/// <summary>
/// Istemciye verilen baglanti adresi (ornek: https://api.sozsoft.com/mcp). Yonlendirme icin
/// kullanilmaz; ekranin kopyalanabilir baglanti komutu uretebilmesi icin tutulur.
/// </summary>
public string? Url { get; set; }
/// <summary>
/// Kaydin gecerli oldugu kiraci adlari, <c>PlatformConsts.MultiValueDelimiter</c> ile
/// ayrilir. Bos ise tum kiracilara aciktir.
/// </summary>
public string? Tenants { get; set; }
/// <summary>
/// Sorgu araclarinin kosacagi DataSource kodu. Bos ise istegin kapsamindaki varsayilan
/// veri kaynagi kullanilir.
/// </summary>
public string? DataSourceCode { get; set; }
/// <summary>
/// Acik arac adlari, <c>PlatformConsts.MultiValueDelimiter</c> ile ayrilir. Bos ise yazma
/// disindaki tum araclar aciktir.
/// </summary>
public string? AllowedTools { get; set; }
/// <summary>Tek sorgunun dondurebilecegi en fazla satir sayisi.</summary>
public int MaxRows { get; set; } = McpConsts.DefaultMaxRows;
/// <summary>Kapali oldugunda veri yazma araclari arac listesinde hic yer almaz.</summary>
public bool AllowWrite { get; set; }
public string? Description { get; set; }
}

View file

@ -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
{
}

View file

@ -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;
/// <summary>
/// Kullanicinin okuyabildigi listeleri belirler. Karar ekranin kendisiyle aynidir: ListForm'un
/// <c>PermissionJson.R</c> yetkisi (bkz. <c>ListFormAuthorizationManager</c>). MCP bu kurala
/// ek bir yetki tanimlamaz; uygulamada acamadigin listeyi MCP'de de goremezsin.
/// </summary>
public interface IMcpListFormAccessFilter
{
Task<IReadOnlySet<string>> GetReadableListFormCodesAsync();
}
public class McpListFormAccessFilter(
IRepository<ListForm, Guid> listFormRepository,
IPermissionChecker permissionChecker)
: DomainService, IMcpListFormAccessFilter
{
public async Task<IReadOnlySet<string>> 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<string, string>(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<string>(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<PermissionCrudValueObject>(permissionJson);
return string.IsNullOrWhiteSpace(permissions?.R) ? null : permissions.R;
}
catch (JsonException)
{
// Bozuk yetki tanimi ekranda da erisimi kapatir (CanAccess false doner).
return null;
}
}
}

View file

@ -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;
/// <summary>
/// Uzak MCP istemcilerinin OpenIddict uygulamalari. Iki tur istemci vardir:
/// <list type="bullet">
/// <item><b>Bilinen istemci</b> (<see cref="McpConsts.OAuthClientId"/>): geri donus adresleri
/// DbMigrator ayarinda sabit olan saglayicilar (Claude, ChatGPT). Onay ekrani gostermez.</item>
/// <item><b>Dinamik istemci</b> (<see cref="McpConsts.DynamicClientIdPrefix"/>): baska bir MCP
/// istemcisi (Cursor, VS Code, Claude Code, ...) kayit ucundan kendi uygulamasini alir. Kayit
/// herkese acik oldugu icin bu istemciler <b>acik onay</b> ister: kullanici giris yaptiktan sonra
/// hangi uygulamanin erisim istedigini gorup onaylar.</item>
/// </list>
/// Iki turde de token kullanicinin kendi yetkileriyle uretilir; istemci yetki kazanmaz.
/// </summary>
public interface IMcpOAuthClientManager
{
/// <summary>Bilinen istemciyi olusturur ya da geri donus adreslerini gunceller.</summary>
Task EnsureKnownClientAsync(IReadOnlyCollection<string> redirectUris);
/// <summary>
/// Istemci kaydi. Adresler bilinen istemcide kayitliysa onu, degilse ayni adres kumesine
/// sahip dinamik istemciyi dondurur; yoksa yenisini uretir.
/// </summary>
Task<McpClientRegistration> RegisterAsync(string? clientName, IReadOnlyCollection<string> redirectUris);
}
public class McpOAuthClientManager(
IAbpApplicationManager applicationManager,
IRepository<OpenIddictApplication, Guid> applicationRepository,
ICurrentTenant currentTenant)
: DomainService, IMcpOAuthClientManager
{
private const int MaxClientNameLength = 100;
/// <summary>Tarayici disi calistirilabilir icerik tasiyan, geri donus adresi olamayacak semalar.</summary>
private static readonly string[] ForbiddenSchemes = ["javascript", "data", "file", "vbscript", "about", "blob"];
public async Task EnsureKnownClientAsync(IReadOnlyCollection<string> 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<McpClientRegistration> RegisterAsync(string? clientName, IReadOnlyCollection<string> 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<List<(string? ClientId, IReadOnlyList<string> 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<string> 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;
}
/// <summary>
/// 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: <c>cursor://...</c>). Yerel olmayan duz HTTP reddedilir.
/// </summary>
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);
}
/// <summary>
/// Onay ekraninda gorunen ad. Istemci adi kayit isteginden gelir ve dogrulanamaz (herkes
/// kendine "Claude" diyebilir); bu yuzden token'in gidecegi adresin sahibi de gosterilir.
/// </summary>
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<string> ParseUris(string? json)
=> string.IsNullOrWhiteSpace(json) ? [] : JsonSerializer.Deserialize<List<string>>(json) ?? [];
private static bool SameUriSet(IReadOnlyCollection<string> left, IReadOnlyCollection<string> right)
=> left.Count == right.Count && right.All(uri => left.Any(candidate => SameUri(candidate, uri)));
/// <summary>OpenIddict adresleri <see cref="Uri"/> bicimiyle saklar; karsilastirma ayni bicimle yapilir.</summary>
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
}
/// <summary>Kayit sonucu: basariliysa istemci kimligi, degilse hata ve hataya konu deger.</summary>
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);
}

View file

@ -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;
/// <summary>
/// Sorguyu istegin kapsamindaki veri kaynaginda kosturur ve satir tavanini uygular.
/// Veri kaynagi cozumu tek yerde kalsin diye burada toplanmistir.
/// </summary>
public interface IMcpQueryExecutor
{
/// <param name="dataSourceCode">
/// Sunucu kaydindaki kod. Bos ya da <c>!Tenant</c> ise veri kaynagi istegin kiracisindan
/// cozulur; host baglaminda varsayilan kaynaga duser.
/// </param>
Task<McpDataSourceContext> ResolveDataSourceAsync(string? dataSourceCode);
/// <summary>
/// Sorguyu kosturur. Sonuc kumesi <paramref name="maxRows"/> satirda kesilir; kesilme
/// olduysa sonuc bunu bildirir.
/// </summary>
Task<McpQueryRows> ExecuteAsync(
McpDataSourceContext context,
string sql,
IReadOnlyDictionary<string, object> parameters,
int maxRows);
}
public class McpQueryExecutor(IDynamicDataManager dynamicDataManager) : DomainService, IMcpQueryExecutor
{
public async Task<McpDataSourceContext> 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<McpQueryRows> ExecuteAsync(
McpDataSourceContext context,
string sql,
IReadOnlyDictionary<string, object> 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<string>();
var materialized = new List<Dictionary<string, object?>>();
var truncated = false;
foreach (var row in rows)
{
if (materialized.Count >= maxRows)
{
truncated = true;
break;
}
if (row is not IDictionary<string, object> 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);
}
}
/// <summary>Cozumlenmis veri kaynagi: hangi depo, hangi baglanti, hangi saglayici.</summary>
public sealed record McpDataSourceContext(
IDynamicDataRepository Repository,
string ConnectionString,
DataSourceTypeEnum DataSourceType);
/// <summary>Sorgu sonucunun ham hali.</summary>
public sealed record McpQueryRows(
List<string> Columns,
List<Dictionary<string, object?>> Rows,
bool Truncated,
long ExecutionTimeMs);

View file

@ -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;
/// <summary>
/// Sema bilgisinin tek kaynagi: <c>Sas_H_DbTableColumn</c> 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.
/// </summary>
public interface IMcpSchemaDictionary
{
/// <summary>Sozlugun bulunup bulunmadigi ve ne zaman tazelendigi; yoksa null.</summary>
Task<McpDictionarySnapshot?> GetSnapshotAsync();
/// <summary>Sozlukteki listeler; sozluk yoksa bos liste.</summary>
Task<IReadOnlyList<McpDictionaryListForm>> GetListFormsAsync();
/// <summary>
/// Istegin kapsamina dusen sozluk kiraci adi. Sozluk ureten prosedurun
/// <c>@TenantName</c> parametresiyle ayni degeri tasimasi gerekir.
/// </summary>
string ResolveTenantName();
/// <summary>
/// Aksan ve buyuk/kucuk harf duyarsiz karsilastirma anahtari; "Musteri" aramasi
/// "Müşteri" basligini da bulur.
/// </summary>
string Normalize(string? value);
}
public class McpSchemaDictionary(
IRepository<DbTableColumn, Guid> 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<McpDictionarySnapshot?> 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<IReadOnlyList<McpDictionaryListForm>> GetListFormsAsync()
{
var json = await GetJsonDataAsync();
if (string.IsNullOrWhiteSpace(json))
{
return [];
}
var document = JsonSerializer.Deserialize<DictionaryDocument>(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<string?> 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<IQueryable<DbTableColumn>> 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);
}
/// <summary>Sozluk JSON'unun kok nesnesi.</summary>
private sealed class DictionaryDocument
{
public List<McpDictionaryListForm>? ListForms { get; set; }
}
}
/// <summary>Sozluk satirinin tazelik bilgisi.</summary>
public sealed record McpDictionarySnapshot(
string? CacheKey,
DateTime RefreshedAt,
int FormCount,
int FieldCount,
string? TenantName,
string? DefinitionCultureName,
string? UiCultureName);
/// <summary>
/// Sozlukteki bir liste. Alan adlari <c>dbo.Sas_H_ListFormFieldsDictionaryJson</c> prosedurunun
/// urettigi JSON ile birebir ayni olmak zorundadir.
/// </summary>
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<McpDictionaryField>? Fields { get; set; }
}
/// <summary>Sozlukteki bir sutun.</summary>
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; }
}

View file

@ -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;
/// <summary>
/// Istegin kiracisina dusen etkin sunucu kaydini cozer.
/// </summary>
public interface IMcpServerConfigProvider
{
/// <summary>Etkin kayit; kiraciya acik aktif bir kayit yoksa null.</summary>
Task<McpServer?> GetEffectiveServerAsync();
/// <summary>Coklu deger tasiyan kolonu (kiraci/arac listesi) ogelerine ayirir.</summary>
IReadOnlyList<string> SplitMultiValue(string? value);
}
public class McpServerConfigProvider(
IRepository<McpServer, Guid> serverRepository,
ICurrentTenant currentTenant)
: DomainService, IMcpServerConfigProvider
{
public async Task<McpServer?> 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<McpServer> 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<string> SplitMultiValue(string? value)
=> string.IsNullOrWhiteSpace(value)
? []
: [.. value.Split(
PlatformConsts.MultiValueDelimiter,
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)];
/// <summary>
/// Kaydin kiraci listesi bos ise tum kiracilara aciktir; doluysa kiraci adi listede
/// gecmelidir (AiBot ekraniyla ayni sozlesme).
/// </summary>
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));
}
}

View file

@ -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;
/// <summary>
/// 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.
/// </summary>
public interface IMcpSqlGuard
{
/// <summary>Tek bir okuma ifadesi oldugunu dogrular; degilse anlasilir bir hata atar.</summary>
void EnsureReadOnlyStatement(string sql);
/// <summary>Sema/nesne adini dogrular ve saglayiciya gore tirnaklar.</summary>
string QuoteQualifiedName(string schemaName, string objectName, DataSourceTypeEnum dataSourceType);
/// <summary>Saglayicinin varsayilan semasi.</summary>
string DefaultSchemaName(DataSourceTypeEnum dataSourceType);
/// <summary>
/// Satir tavanini sorgunun kendisine yazar: SQL Server'da <c>TOP (n)</c>, PostgreSQL'de
/// <c>LIMIT n</c>. Sorgu kendi sinirini zaten tasiyorsa dokunulmaz.
/// </summary>
string ApplyRowLimit(string sql, int maxRows, DataSourceTypeEnum dataSourceType);
/// <summary>
/// Istemciden gelen parametre adlarini dogrular ve bastaki @ isaretini duserek Dapper'in
/// bekledigi bicime cevirir.
/// </summary>
Dictionary<string, object> NormalizeParameters(IReadOnlyDictionary<string, object?>? parameters);
}
public class McpSqlGuard(IStringLocalizer<PlatformResource> localizer) : DomainService, IMcpSqlGuard
{
/// <summary>Tek parcali SQL tanimlayicisi; sema ve nesne adlari bu kalibi gecmek zorundadir.</summary>
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);
/// <summary>
/// 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.
/// </summary>
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<string, object> NormalizeParameters(IReadOnlyDictionary<string, object?>? parameters)
{
var normalized = new Dictionary<string, object>(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]);
}
}
}

View file

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\common.props" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<RootNamespace>Sozsoft.Mcp</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.Ddd.Domain" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Sozsoft.Mcp.Domain.Shared\Sozsoft.Mcp.Domain.Shared.csproj" />
<!-- Veri sozlugu (DbTableColumn) ve dinamik veri erisimi platform domain'inde yasar. -->
<ProjectReference Include="..\..\..\src\Sozsoft.Platform.Domain\Sozsoft.Platform.Domain.csproj" />
</ItemGroup>
</Project>

View file

@ -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<McpDbContext> options)
: AbpDbContext<McpDbContext>(options)
{
public DbSet<McpServer> McpServers { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ConfigureMcp();
}
}

View file

@ -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<McpServer>(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");
});
}
}

View file

@ -0,0 +1,6 @@
namespace Sozsoft.Mcp.EntityFrameworkCore;
public static class McpDbProperties
{
public const string ConnectionStringName = "Mcp";
}

View file

@ -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<McpDbContext>(options =>
{
options.AddDefaultRepositories(includeAllEntities: true);
});
}
}

View file

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\common.props" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<RootNamespace>Sozsoft.Mcp</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.EntityFrameworkCore" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Sozsoft.Mcp.Domain\Sozsoft.Mcp.Domain.csproj" />
</ItemGroup>
</Project>

View file

@ -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;
/// <summary>
/// Uzak MCP istemcilerinin baglanti kesfi. Istemci /mcp'den 401 alir, <c>WWW-Authenticate</c>
/// 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.
/// </summary>
[AllowAnonymous]
[IgnoreAntiforgeryToken]
[ApiExplorerSettings(IgnoreApi = true)]
public class McpOAuthController(
IOptions<McpOAuthOptions> options,
IMcpOAuthAppService oAuthAppService) : AbpControllerBase
{
/// <summary>Korumali kaynak meta verisi (RFC 9728).</summary>
[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));
}
/// <summary>Dinamik istemci kaydi (RFC 7591).</summary>
[HttpPost(McpConsts.RegistrationEndpointPath)]
public async Task<IActionResult> 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<string>? 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<string> RedirectUris,
[property: JsonPropertyName("grant_types")] IReadOnlyList<string> GrantTypes,
[property: JsonPropertyName("response_types")] IReadOnlyList<string> 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<string> AuthorizationServers,
[property: JsonPropertyName("scopes_supported")] IReadOnlyList<string> ScopesSupported,
[property: JsonPropertyName("bearer_methods_supported")] IReadOnlyList<string> BearerMethodsSupported,
[property: JsonPropertyName("resource_name")] string ResourceName);
}

View file

@ -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;
/// <summary>
/// /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.
/// </summary>
public class McpAuthenticationChallengeMiddleware(IOptions<McpOAuthOptions> 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);
}
}

View file

@ -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;
/// <summary>
/// /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 <c>MapMcp</c> ile baglanir.
/// </summary>
[DependsOn(
typeof(McpApplicationContractsModule),
typeof(AbpAspNetCoreMvcModule)
)]
public class McpHttpApiModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
Configure<McpOAuthOptions>(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<McpSchemaTools>()
.WithTools<McpQueryTools>()
.WithTools<McpDataTools>()
.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<IMcpSchemaAppService>();
var available = (await schemaAppService.GetAvailableToolsAsync()).ToHashSet(System.StringComparer.Ordinal);
result.Tools = [.. result.Tools.Where(tool => available.Contains(tool.Name))];
return result;
}));
}
}

View file

@ -0,0 +1,22 @@
namespace Sozsoft.Mcp;
/// <summary>
/// /mcp ucunun OAuth adresleri. Degerler ortam yapilandirmasindan gelir (<c>App:SelfUrl</c>,
/// <c>AuthServer:Authority</c>); localhost, dev ve production ayni kodla calisir.
/// </summary>
public class McpOAuthOptions
{
/// <summary>API'nin disaridan erisilen kok adresi, sonunda egik cizgi olmadan.</summary>
public string BaseUrl { get; set; } = string.Empty;
/// <summary>Token'i veren yetkilendirme sunucusu (OpenIddict), sonunda egik cizgi olmadan.</summary>
public string AuthorizationServer { get; set; } = string.Empty;
/// <summary>Korumali kaynagin tanimlayicisi: istemcinin baglandigi /mcp adresi.</summary>
public string ResourceUrl => BaseUrl + McpConsts.EndpointPath;
/// <summary>
/// Kaynaga ozgu meta veri adresi (RFC 9728 §3.1: yol, well-known onekinin arkasina eklenir).
/// </summary>
public string ProtectedResourceMetadataUrl => BaseUrl + McpConsts.ProtectedResourceMetadataPath + McpConsts.EndpointPath;
}

View file

@ -0,0 +1,22 @@
namespace Sozsoft.Mcp;
/// <summary>
/// Istemciye <c>initialize</c> 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.
/// </summary>
internal static class McpServerInfoDefaults
{
public const string Name = "sozsoft-platform";
/// <summary>Connector ekraninda gorunen kaynak adi.</summary>
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.";
}

View file

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\common.props" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<RootNamespace>Sozsoft.Mcp</RootNamespace>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<!-- /mcp ucunun Streamable HTTP tasiyicisi. -->
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="2.2.0" />
<PackageReference Include="Volo.Abp.AspNetCore.Mvc" Version="10.0.0" />
</ItemGroup>
<ItemGroup>
<!-- Sunum katmani: araclar yalnizca uygulama sozlesmesini tanir, Domain'i gormez. -->
<ProjectReference Include="..\Sozsoft.Mcp.Application.Contracts\Sozsoft.Mcp.Application.Contracts.csproj" />
</ItemGroup>
</Project>

View file

@ -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;
/// <summary>
/// Veri yazma araclari. Yazma bir ListForm ekraninin sozlesmesinden gecer; ekran yetkisi ve
/// tenant filtresi aynen uygulanir. Sunucu kaydinda <c>AllowWrite</c> kapaliysa bu araclar
/// arac listesinde hic gorunmez.
/// </summary>
[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<McpWriteResultDto> InsertRowAsync(
[Description("Kaydin eklenecegi listenin kodu.")]
string listFormCode,
[Description("Alan adi -> deger eslemesi.")]
Dictionary<string, object?> 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<McpWriteResultDto> UpdateRowAsync(
[Description("Kaydin bulundugu listenin kodu.")]
string listFormCode,
[Description("Guncellenecek kaydin anahtar alan degeri; describe_list cikisindaki keyFieldName alanina karsilik gelir.")]
List<object?> keys,
[Description("Alan adi -> yeni deger eslemesi.")]
Dictionary<string, object?> 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<McpWriteResultDto> DeleteRowAsync(
[Description("Kaydin bulundugu listenin kodu.")]
string listFormCode,
[Description("Silinecek kaydin anahtar alan degeri.")]
List<object?> keys)
=> await dataAppService.DeleteRowAsync(new McpRowRequestDto
{
ListFormCode = listFormCode,
Keys = keys ?? []
});
}

View file

@ -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;
/// <summary>Salt-okunur sorgu araclari.</summary>
[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<McpListQueryResultDto> 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<McpQueryResultDto> 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<string, object?>? 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<List<McpSavedQueryDto>> 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<McpQueryResultDto> 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<string, object?>? 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
});
}

View file

@ -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;
/// <summary>
/// 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.
/// </summary>
[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<McpServerInfoResult> 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<List<McpListSummaryDto>> 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<McpListDetailDto?> 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<List<McpSchemaMatchDto>> 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);
}
/// <param name="Server">Istegin kiracisina dusen sunucu kaydi; yoksa null.</param>
/// <param name="Schema">Veri sozlugunun tazelik bilgisi.</param>
/// <param name="AvailableTools">Bu baglantida cagrilabilen araclar.</param>
public sealed record McpServerInfoResult(
McpServerConfigDto? Server,
McpSchemaInfoDto Schema,
IReadOnlyList<string> AvailableTools);

View file

@ -0,0 +1,8 @@
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Version>0.1.0</Version>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<AbpProjectType>module</AbpProjectType>
</PropertyGroup>
</Project>

View file

@ -1,11 +1,23 @@
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace Sozsoft.Platform.ListForms.Select;
public interface IListFormDataAppService
/// <summary>
/// Ekran uzerinden veri yazma sozlesmesi. Ekranin insert/update/delete komutlari ve yetki
/// kontrolu burada uygulanir; ayni yol hem grid'den hem /mcp araclarindan kullanilir.
/// </summary>
public interface IListFormDataAppService : IApplicationService
{
Task<int> PostInsertAsync(DataRequestDto input);
/// <returns>Uretilen anahtar degeri.</returns>
Task<dynamic> PostInsertAsync(DataRequestDto input);
/// <returns>Kopyalanan kaydin anahtar degeri.</returns>
Task<dynamic> PostDuplicateAsync(DataRequestDto input);
/// <returns>Etkilenen satir sayisi.</returns>
Task<int> PostUpdateAsync(DataRequestDto input);
/// <returns>Etkilenen satir sayisi.</returns>
Task<int> PostDeleteAsync(DataRequestDto input);
}

View file

@ -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;

View file

@ -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."
}
]
}

View file

@ -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<EditingFormDto>()
{
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))

View file

@ -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",

View file

@ -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",

View file

@ -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"
]
}
}
}

View file

@ -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"
]
}
}
}

View file

@ -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"
]
}
}
}

View file

@ -37,6 +37,7 @@ public enum TableNameEnum
CustomEndpoint,
CustomComponent,
DynamicService,
McpServer,
ReportCategory,
ReportTemplate,
IpRestriction,

View file

@ -513,6 +513,22 @@ public static class PlatformConsts
public const string Publish = DynamicService + ".Publish";
public const string ViewCode = DynamicService + ".ViewCode";
}
/// <summary>
/// MCP sunucu tanimlarinin yetkileri. <see cref="McpServers.McpServer"/> ekrani gorme
/// (okuma) yetkisidir; <see cref="McpServers.Invoke"/> ise /mcp ucundan arac
/// cagirma yetkisidir ve kiraci kullanicilarina da verilebilir.
/// </summary>
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

View file

@ -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

View file

@ -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";

View file

@ -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<AiBot>(b =>

View file

@ -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
{

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Sozsoft.Platform.Migrations
{
[DbContext(typeof(PlatformDbContext))]
[Migration("20260908125150_Initial")]
[Migration("20260910132559_Initial")]
partial class Initial
{
/// <inheritdoc />
@ -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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<bool>("AllowWrite")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<string>("AllowedTools")
.HasMaxLength(1024)
.HasColumnType("nvarchar(1024)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<string>("DataSourceCode")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Description")
.HasMaxLength(1024)
.HasColumnType("nvarchar(1024)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<int>("MaxRows")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(200);
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Tenants")
.HasMaxLength(2048)
.HasColumnType("nvarchar(2048)");
b.Property<string>("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<Guid>("Id")

View file

@ -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<Guid>(type: "uniqueidentifier", nullable: false),
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false),
Url = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
Tenants = table.Column<string>(type: "nvarchar(2048)", maxLength: 2048, nullable: true),
DataSourceCode = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
AllowedTools = table.Column<string>(type: "nvarchar(1024)", maxLength: 1024, nullable: true),
MaxRows = table.Column<int>(type: "int", nullable: false, defaultValue: 200),
AllowWrite = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
Description = table.Column<string>(type: "nvarchar(1024)", maxLength: 1024, nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(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");

View file

@ -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<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<bool>("AllowWrite")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<string>("AllowedTools")
.HasMaxLength(1024)
.HasColumnType("nvarchar(1024)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<string>("DataSourceCode")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Description")
.HasMaxLength(1024)
.HasColumnType("nvarchar(1024)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<int>("MaxRows")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(200);
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Tenants")
.HasMaxLength(2048)
.HasColumnType("nvarchar(2048)");
b.Property<string>("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<Guid>("Id")

View file

@ -54,6 +54,10 @@
<Folder Include="Dashboard\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\modules\Sozsoft.Mcp\Sozsoft.Mcp.EntityFrameworkCore\Sozsoft.Mcp.EntityFrameworkCore.csproj" />
</ItemGroup>
</Project>

View file

@ -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;
/// <summary>
/// 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.
/// </summary>
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<HandleConfigurationRequestContext>(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<ExtractAuthorizationRequestContext>(builder =>
builder.UseInlineHandler(context =>
{
RemoveResourceParameter(context.Request);
return default;
})
.SetOrder(int.MaxValue - 100_000)
.SetType(OpenIddictServerHandlerType.Custom));
server.AddEventHandler<ExtractTokenRequestContext>(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);
}
/// <summary>
/// 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.
/// </summary>
public static bool IsSessionlessClient(string? clientId)
=> McpConsts.IsMcpClient(clientId);
}

View file

@ -278,6 +278,11 @@ Your login code: {twoFactorToken}";
[UnitOfWork]
protected override async Task<IActionResult> 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);

View file

@ -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<OpenIddictServerBuilder>(server => McpOAuthServerConfiguration.Configure(server, configuration));
PreConfigure<IdentityBuilder>(builder =>
{
builder.AddClaimsPrincipalFactory<PlatformUserClaimsPrincipalFactory>();
@ -509,6 +517,7 @@ public class PlatformHttpApiHostModule : AbpModule
await next();
});
app.UseMiddleware<McpAuthenticationChallengeMiddleware>();
app.UseAuthentication();
app.UseAbpOpenIddictValidation();
app.UseMiddleware<CaptchaMiddleware>();
@ -552,6 +561,15 @@ public class PlatformHttpApiHostModule : AbpModule
app.UseConfiguredEndpoints(endpoints =>
{
endpoints.MapHub<MessengerHub>("/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
});
});
}

View file

@ -45,6 +45,8 @@
<ProjectReference Include="..\Sozsoft.Platform.EntityFrameworkCore\Sozsoft.Platform.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\Sozsoft.Platform.HttpApi\Sozsoft.Platform.HttpApi.csproj" />
<ProjectReference Include="..\..\modules\Sozsoft.SqlQueryManager\Sozsoft.SqlQueryManager.Application\Sozsoft.SqlQueryManager.Application.csproj" />
<ProjectReference Include="..\..\modules\Sozsoft.Mcp\Sozsoft.Mcp.Application\Sozsoft.Mcp.Application.csproj" />
<ProjectReference Include="..\..\modules\Sozsoft.Mcp\Sozsoft.Mcp.HttpApi\Sozsoft.Mcp.HttpApi.csproj" />
</ItemGroup>
<ItemGroup>

View file

@ -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

View file

@ -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;

View file

@ -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."
}
]
}