Mcp güncellemesi
This commit is contained in:
parent
42d3c55ae3
commit
6dee1c898b
79 changed files with 4559 additions and 8 deletions
|
|
@ -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
109
README.md
|
|
@ -164,7 +164,8 @@ sozsoft-platform/
|
|||
│ │ ├── Sozsoft.Notifications/ # Bildirim tipleri, kuralları, kullanıcı bildirimleri
|
||||
│ │ ├── Sozsoft.MailQueue/ # Şablonlu mail kuyruğu ve ekleri
|
||||
│ │ ├── Sozsoft.Sender/ # Mail (SES), SMS, WhatsApp, Rocket.Chat göndericileri
|
||||
│ │ └── Sozsoft.SqlQueryManager/ # SQL nesne yönetimi ve sorgu çalıştırma
|
||||
│ │ ├── Sozsoft.SqlQueryManager/ # SQL nesne yönetimi ve sorgu çalıştırma
|
||||
│ │ └── Sozsoft.Mcp/ # MCP sunucusu: /mcp ucu, şema keşfi ve sorgu araçları
|
||||
│ └── test/ # Test projeleri
|
||||
├── ui/ # React uygulaması
|
||||
│ └── src/
|
||||
|
|
@ -930,6 +931,7 @@ içindedir; oradaki kontroller yalnızca butonları gizler, asıl kontrol AppSer
|
|||
| **Sozsoft.MailQueue** | Şablon (Scriban) tabanlı gövde üretimi, ek dosya yaşam döngüsü, kuyruk çalıştırma ve günlükleme. |
|
||||
| **Sozsoft.Sender** | Amazon SES (mail), Posta Güvercini (SMS), WhatsApp Cloud API, Rocket.Chat göndericileri. |
|
||||
| **Sozsoft.SqlQueryManager** | SQL nesne yönetimi, sorgu çalıştırma, tablo/kolon metadata'sı. |
|
||||
| **Sozsoft.Mcp** | MCP (Model Context Protocol) sunucusu: `/mcp` Streamable HTTP ucu, kiracı bazlı araç seti, şema keşfi ve salt-okunur sorgu. Bkz. §13.6. |
|
||||
|
||||
### 10.2. Uygulama servisleri (`api/src/Sozsoft.Platform.Application`)
|
||||
|
||||
|
|
@ -1150,6 +1152,111 @@ kuyruğu arka plan işçisiyle çalıştırır ve sonuçları günlükler.
|
|||
Gemini sohbet modeli bağlanır (`configs/ai/Chat.json`). UI tarafında `/admin/ai` ve header'daki
|
||||
asistan bileşeni bu uç noktayı kullanır.
|
||||
|
||||
### 13.6. MCP sunucusu
|
||||
|
||||
Yapay zekâ istemcileri (Claude Code, Claude Desktop, ChatGPT) platforma **`/mcp`** ucundan
|
||||
bağlanır. Uç, API imajının içinde aynı portta (`8080`) yayınlanır; ayrı bir container yoktur.
|
||||
Modül `api/modules/Sozsoft.Mcp` altında standart ABP katmanlarıyla durur.
|
||||
|
||||
**Kimlik ve kiracı.** Kimlik doğrulama mevcut OpenIddict bearer token'ıdır. Uç,
|
||||
`UseAuthentication` → `UseAbpOpenIddictValidation` → `UseMultiTenancy` → `UseUnitOfWork`
|
||||
ardışık düzeninden sonra bağlanır ve açıkça bearer şemasına bağlıdır (token'sız istek giriş
|
||||
sayfasına 302 ile değil, 401 ile döner); kiracı token talebinden ya da `__tenant` başlığından
|
||||
çözülür. Taşıyıcı **durumsuz** kiptedir
|
||||
(`HttpServerSessionMode.Stateless`): her istek kendi ABP kapsamında koşar, ters vekil
|
||||
arkasında oturum yapışkanlığı gerekmez.
|
||||
|
||||
**Kiracı bazlı sözleşme.** `Sas_H_McpServer` tablosundaki kayıt (ekran:
|
||||
`App.DeveloperKit.McpServers`) hangi araçların yayınlandığını, sorguların hangi veri
|
||||
kaynağında ve hangi satır tavanıyla koşacağını belirler. `Tenants` boşsa kayıt tüm
|
||||
kiracılara açıktır (AiBot ile aynı sözleşme). Araç listesi bu kayda göre süzülür
|
||||
(`AddListToolsFilter`) **ve** çağrı anında aynı kural yeniden uygulanır — süzme tek başına
|
||||
bir yetki sınırı değildir.
|
||||
|
||||
**Araçlar.**
|
||||
|
||||
| Araç | İş |
|
||||
| --- | --- |
|
||||
| `server_info` | Bağlantının kiracısı, açık araçlar, veri sözlüğünün tazelik bilgisi. Her zaman açıktır. |
|
||||
| `list_lists` | Listeler: kod, başlık, açıklama, arkasındaki tablo. |
|
||||
| `describe_list` | Bir listenin sütunları: alan adı, görünen başlık, SQL tipi, lookup ilişkisi, `isSensitive`. |
|
||||
| `search_schema` | Liste ve sütun adları/başlıkları üzerinde aksan ve büyük/küçük harf duyarsız arama. |
|
||||
| `query_list` | Listenin verisini ekranın kendi sorgusuyla (`GetSelectAsync`) okur: filtre, sıralama, gruplama, özet (DevExtreme biçimi). Normal kullanıcının veri okuma yolu. |
|
||||
| `run_select` | Parametreli tek SELECT; çoklu ifade, DDL ve DML reddedilir, satır tavanı sorguya yazılır. Yalnızca SQL Query Manager yetkisiyle. |
|
||||
| `list_queries` | View/procedure/function nesneleri (SQL Query Manager yetkisiyle) + çağrılabilir Custom Endpoint'ler (`App.DeveloperKit.CustomEndpoints.Get` ile). |
|
||||
| `run_query` | Kayıtlı bir nesneyi ya da Custom Endpoint'i parametreleriyle çağırır; yetkiler `list_queries` ile aynı. |
|
||||
| `insert_row` / `update_row` / `delete_row` | ListForm ekranının insert/update/delete sözleşmesinden geçer; `AllowWrite` kapalıysa listelenmez. |
|
||||
|
||||
**Yetki modeli: MCP yetki vermez.** Kullanıcı MCP'de uygulamadaki yetkisini taşır:
|
||||
- Şema araçları yalnızca kullanıcının okuma yetkisi olan listeleri gösterir (`McpListFormAccessFilter`
|
||||
— ListForm `PermissionJson.R`, ekranla aynı karar; tek sorgu + toplu yetki kontrolü).
|
||||
- `query_list` ekranın sorgusundan geçer: liste ve alan (`CanRead`) yetkileri, kiracı/şube
|
||||
filtreleri, kullanıcı özelleştirmeleri uygulamadaki gibidir.
|
||||
- Ham SQL (`run_select`, veritabanı nesneleri) liste yetkilerini aşar; yalnızca uygulamada
|
||||
`App.SqlQueryManager` yetkisi olana açıktır. Yazma araçları `.Write` + ekranın C/U/D yetkisini ister.
|
||||
- Araç listesi bu yetkilere göre süzülür (`GetAvailableToolsAsync`); çağrı anında aynı kontrol
|
||||
tekrarlanır. Araç → yetki eşlemesi `McpToolNames.RequiredPermissions`'tadır.
|
||||
|
||||
**Şema keşfi nereden okur.** Tablo ve sütun bilgisi canlı katalogdan değil,
|
||||
`Sas_H_DbTableColumn` tablosundaki JSON veri sözlüğünden gelir — sözlük dil anahtarlarından
|
||||
çözülmüş başlıkları ve lookup ilişkilerini birlikte taşıdığı için modelin gördüğü adlar
|
||||
kullanıcının ekranda gördüğü adlarla aynı olur. Sözlük
|
||||
`dbo.Sas_H_ListFormFieldsDictionaryJson_SaveDbTableColumns` prosedürüyle kiracı adı başına
|
||||
üretilir; sözlük yoksa araçlar tablo döndürmez ve `server_info` bunu bildirir.
|
||||
|
||||
**OAuth (Claude / ChatGPT connector'ları).** Connector formuna yalnızca ad ve URL girilir;
|
||||
kimlik doğrulamayı istemci MCP OAuth akışıyla kendisi yürütür:
|
||||
|
||||
1. `/mcp` token'sız isteğe 401 + `WWW-Authenticate: Bearer resource_metadata="…"` döner
|
||||
(`McpAuthenticationChallengeMiddleware`).
|
||||
2. İstemci `/.well-known/oauth-protected-resource/mcp` (RFC 9728) belgesinden yetkilendirme
|
||||
sunucusunu (`AuthServer:Authority`) öğrenir, `/.well-known/oauth-authorization-server`
|
||||
(OpenIddict keşif belgesi, `registration_endpoint` eklenmiş) belgesini okur.
|
||||
3. `/connect/register` (RFC 7591, `McpOAuthClientManager`):
|
||||
- İstenen geri dönüş adresleri **bilinen istemcide** (`Platform_Mcp`; Claude, ChatGPT —
|
||||
`DbMigrator` ayarı `OpenIddict:Applications:Platform_Mcp:RedirectUris`) kayıtlıysa onun
|
||||
kimliği döner; onay ekranı çıkmaz.
|
||||
- Değilse (Cursor, VS Code, Claude Code, başka bir AI istemcisi) **dinamik istemci** üretilir:
|
||||
`mcp_` önekli, PKCE zorunlu, **açık onaylı** OpenIddict uygulaması. Aynı adres kümesi aynı
|
||||
uygulamaya bağlanır; tavan `McpConsts.MaxDynamicClients`. Kabul edilen adresler: HTTPS,
|
||||
localhost HTTP (RFC 8252), uygulama şemaları (`cursor://…`); `javascript:` vb. reddedilir.
|
||||
Onay ekranındaki ad istemcinin beyanı + geri dönüş adresinin sahibidir (ad doğrulanamaz).
|
||||
4. Kullanıcı platformun giriş ekranına (`/Account/Login`, kiracı seçimi dahil) yönlenir;
|
||||
authorization code + PKCE ile token alınır. Kiracı ve yetki giriş yapan kullanıcıdan gelir.
|
||||
|
||||
Üç ayrıntı bilinçlidir:
|
||||
- Yayıncı (`issuer`) `AuthServer:Authority`'den sabitlenir; ters vekil arkasında istekten
|
||||
türetilirse `http://` olur ve istemci keşif belgesini reddeder.
|
||||
- MCP istemcisinin gönderdiği RFC 8707 `resource` parametresi yok sayılır
|
||||
(`McpOAuthServerConfiguration`): OpenIddict kayıtsız kaynağı reddeder ya da token kitlesini
|
||||
`/mcp` adresine daraltır, API ise `Platform` kitlesini doğrular.
|
||||
- MCP istemcilerinin (`McpConsts.IsMcpClient`) token'ları kullanıcı oturumuna bağlanmaz
|
||||
(`PlatformTokenController`). Tek oturum kuralı her girişte diğer oturumları sildiği için,
|
||||
bağlansaydı connector her token yenilemesinde kullanıcının web oturumunu düşürürdü.
|
||||
- MCP istemcilerinin yenileme token ömrü uygulama bazında 30 gündür
|
||||
(`McpConsts.RefreshTokenLifetime`, OpenIddict uygulama ayarı) ve her kullanımda yenilenir;
|
||||
web istemcisinin dakikalık ömrü etkilenmez. Kullanıcı 30 gün hiç soru sormazsa bağlantıyı
|
||||
yeniden kurar.
|
||||
|
||||
Bağlantı sunucu gerektirir: connector formu bağlantıyı istemci sağlayıcının sunucularından
|
||||
kurar, `localhost` erişilemez. Forma `https://api.sozsoft.com/mcp` yazılır.
|
||||
|
||||
**Bağlanma (token ile, CLI).**
|
||||
|
||||
```bash
|
||||
# sunucu
|
||||
claude mcp add sozsoft --transport http https://api.sozsoft.com/mcp \n --header "Authorization: Bearer <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
|
||||
|
|
|
|||
|
|
@ -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/"
|
||||
|
|
|
|||
|
|
@ -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/"
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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",">",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; }
|
||||
}
|
||||
|
|
@ -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; } = [];
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
|
|
@ -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
|
||||
""";
|
||||
}
|
||||
|
|
@ -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!)]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
104
api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpConsts.cs
Normal file
104
api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Domain.Shared/McpConsts.cs
Normal 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ı 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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -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))];
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
namespace Sozsoft.Mcp.EntityFrameworkCore;
|
||||
|
||||
public static class McpDbProperties
|
||||
{
|
||||
public const string ConnectionStringName = "Mcp";
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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.";
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 ?? []
|
||||
});
|
||||
}
|
||||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
8
api/modules/Sozsoft.Mcp/common.props
Normal file
8
api/modules/Sozsoft.Mcp/common.props
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<Project>
|
||||
<PropertyGroup>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Version>0.1.0</Version>
|
||||
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||
<AbpProjectType>module</AbpProjectType>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ public enum TableNameEnum
|
|||
CustomEndpoint,
|
||||
CustomComponent,
|
||||
DynamicService,
|
||||
McpServer,
|
||||
ReportCategory,
|
||||
ReportTemplate,
|
||||
IpRestriction,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
@ -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");
|
||||
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@
|
|||
<Folder Include="Dashboard\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\modules\Sozsoft.Mcp\Sozsoft.Mcp.EntityFrameworkCore\Sozsoft.Mcp.EntityFrameworkCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
23
claude.md
23
claude.md
|
|
@ -182,6 +182,29 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
|
|||
Ayrıntı: `dotnet.instructions.md` §5.
|
||||
- **Yeni bir yetki** → `PlatformConsts.AppCodes` + `PermissionsData.json` + (UI'da kullanılacaksa)
|
||||
`permission.constant.ts` + `LanguagesData.json`.
|
||||
- **`/mcp` ucuna yeni bir araç** → üç yer birlikte değişir: `McpToolNames` (ad kayıt defteri),
|
||||
ilgili `Mcp*Tools` sınıfı (`Sozsoft.Mcp.HttpApi`) ve arkasındaki AppService sözleşmesi
|
||||
(`Sozsoft.Mcp.Application.Contracts`). Arac adı `McpToolNames.All` içinde yoksa araç listesi
|
||||
süzgecinden geçmez ve hiç görünmez; MCP Servers ekranındaki `AllowedTools` seçenekleri de
|
||||
`SelectableTools`'dan üretildiği için kendiliğinden güncellenir. Yazma araçları
|
||||
`WriteTools` listesine de eklenir, yoksa `AllowWrite` kapalıyken de yayınlanırlar.
|
||||
Kapı iki yerdedir: `AddListToolsFilter` listeyi süzer, `McpAppService.EnsureToolAllowedAsync`
|
||||
çağrıyı reddeder — süzme tek başına yetki sınırı değildir, ikisi birlikte yazılır.
|
||||
- **MCP şema araçları** → tablo/sütun bilgisi canlı katalogdan değil `Sas_H_DbTableColumn`
|
||||
sözlüğünden okunur (`IMcpSchemaDictionary`). Sözlüğün alan adları
|
||||
`dbo.Sas_H_ListFormFieldsDictionaryJson` prosedürünün ürettiği JSON ile birebir aynıdır;
|
||||
prosedür değişirse `McpDictionaryListForm`/`McpDictionaryField` de değişir.
|
||||
- **MCP OAuth** → bilinen sağlayıcılar (Claude, ChatGPT) seed edilmiş `Platform_Mcp` istemcisini,
|
||||
diğer MCP istemcileri `/connect/register` ile üretilen `mcp_` önekli açık onaylı istemcileri
|
||||
kullanır (`McpOAuthClientManager`; seed `McpOAuthClientDataSeedContributor`). Bilinen sağlayıcı
|
||||
eklemek = DbMigrator'daki `Platform_Mcp:RedirectUris` listesine callback eklemek. MCP
|
||||
istemcisi kararı tek yerdedir (`McpConsts.IsMcpClient`): `resource` parametresi yok sayılır ve
|
||||
token kullanıcı oturumuna bağlanmaz; token ucuna yeni bir oturum yolu eklenirse bu istisna
|
||||
korunur, yoksa connector web oturumunu düşürür.
|
||||
- **MCP yetki vermez** → yeni bir MCP aracı kullanıcının uygulamadaki yetkisini aşamaz: liste verisi
|
||||
ekranın sorgusundan (`IListFormSelectAppService`/`IListFormDataAppService`) geçer; liste
|
||||
yetkisini aşan bir yetenek (ham SQL gibi) `McpToolNames.RequiredPermissions`'a uygulamadaki
|
||||
karşılık gelen yetkiyle eklenir.
|
||||
- **Yeni bir dil anahtarı** → `LanguagesData.json`; eklemeden önce anahtarın zaten var olup
|
||||
olmadığını kontrol et.
|
||||
- **Custom component sınıfları** → bileşen kodu veritabanında yaşar, Tailwind derlemede yalnızca
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
20
configs/seeds/host/data/App.DeveloperKit.McpServers.json
Normal file
20
configs/seeds/host/data/App.DeveloperKit.McpServers.json
Normal 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."
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in a new issue