Mcp hataları düzeltildi

This commit is contained in:
Sedat ÖZTÜRK 2026-10-11 01:03:36 +03:00
parent d3d0fa0985
commit dae0a55e16
25 changed files with 927 additions and 508 deletions

View file

@ -1676,19 +1676,23 @@ servisinin sınıf düzeyindeki `[Authorize]` attribute'undadır; `/mcp` ucu yal
ister (ucun kendisinde aranırsa kullanıcı nedeni yazmayan bir 403 alır). Araç listesi aynı ister (ucun kendisinde aranırsa kullanıcı nedeni yazmayan bir 403 alır). Araç listesi aynı
kurallarla süzülür (`GetAvailableToolsAsync`), çağrı anında kontrol tekrarlanır. kurallarla süzülür (`GetAvailableToolsAsync`), çağrı anında kontrol tekrarlanır.
**Yetkisi olmayan çağrı ne görür.** SDK çevrilmemiş istisnayı "An error occurred invoking '…'" **Reddedilen çağrı ne görür.** Ret modelle konuşmadır, istisna değildir: MCP'nin kendi kontrolleri
olarak iletir. `McpToolErrorTranslator` (çağrı ve araç listesi filtresi, `McpHttpApiModule`) iki tür (kiracıya kapalı araç, `AllowedTools` dışı araç, eksik yetki, hatalı girdi, `start_dev_request` durum
hatayı mesaja çevirir; çağrı filtresi mesajı istisna atmadan hata sonucu (`CallToolResult.IsError`) reddi…) `McpProblem` döndürür (`McpOutcome<T>`); araç katmanı onu `McpToolResult.From` ile hata
olarak döner — beklenen ret sunucu logunda yığın izli `ERR` üretmez, model nedeni okuyup devam eder: sonucuna (`CallToolResult.IsError`) çevirir. Debugger durmaz, sunucu logunda yığın izli `ERR` oluşmaz,
model nedeni okuyup girdiyi düzeltir:
- **Yetki reddi** → `App.Mcp.Error.PermissionDenied`: "'deploy_wizard' için gereken yetkiniz yok: - **Yetki reddi** → `App.Mcp.Error.PermissionDenied`: "'deploy_wizard' için gereken yetkiniz yok:
Wizard (App.Listforms.Wizard). Yöneticinizden bu yetkiyi isteyin." Eksik yetkiler aracın bilinen Wizard (App.Listforms.Wizard). Yöneticinizden bu yetkiyi isteyin." MCP yetkiyi
yetkileri (`McpToolNames.KnownPermissionsOf`) ve reddin taşıdığı yetki adı kullanıcının `McpAppService.CheckPermissionAsync` ile adıyla ister; satır ve `query_list` araçları ekranın
yetkileriyle karşılaştırılarak bulunur. Satır ve `query_list` araçları ekranın yetkisini adıyla yetkisini de adıyla önceden ister (`IMcpListFormAccessFilter.GetPermissionNameAsync`).
önceden ister (`IMcpListFormAccessFilter.GetPermissionNameAsync`), bu yüzden ekran yetkisi de - **İş kuralı / girdi reddi** → ilgili `App.Mcp.Error.*` metni.
adıyla görünür. Eksik yetki belirlenemezse `PermissionDeniedUnspecified` döner.
- **İş kuralı hatası** (`UserFriendlyException`: kiracıya kapalı araç, `AllowedTools` dışı araç, Arkadaki platform servisinin (wizard, custom component, ListForm verisi) attığı istisna ve sınıf
`start_dev_request` durum reddi…) → yerelleştirilmiş mesajı olduğu gibi. düzeyindeki `[Authorize]` reddi için `McpToolErrorTranslator` (çağrı ve araç listesi filtresi,
`McpHttpApiModule`) devrededir: yetki reddinde eksik yetkiler aracın bilinen yetkileri
(`McpToolNames.KnownPermissionsOf`) ve reddin taşıdığı yetki adı karşılaştırılarak bulunur (belirlenemezse
`PermissionDeniedUnspecified`), `UserFriendlyException` yerelleştirilmiş mesajıyla döner.
Başka istisnalar çevrilmez; SQL ve yığın ayrıntısı istemciye sızmaz. Tek istisna `run_select`'tir: Başka istisnalar çevrilmez; SQL ve yığın ayrıntısı istemciye sızmaz. Tek istisna `run_select`'tir:
sağlayıcı hatası (`DbException`, örn. tabloda olmayan kolon) `App.Mcp.Error.SelectFailed` ile sağlayıcı hatası (`DbException`, örn. tabloda olmayan kolon) `App.Mcp.Error.SelectFailed` ile

View file

@ -11,9 +11,9 @@ namespace Sozsoft.Mcp;
/// </summary> /// </summary>
public interface IMcpDataAppService : IApplicationService public interface IMcpDataAppService : IApplicationService
{ {
Task<McpWriteResultDto> InsertRowAsync(McpRowRequestDto input); Task<McpOutcome<McpWriteResultDto>> InsertRowAsync(McpRowRequestDto input);
Task<McpWriteResultDto> UpdateRowAsync(McpRowRequestDto input); Task<McpOutcome<McpWriteResultDto>> UpdateRowAsync(McpRowRequestDto input);
Task<McpWriteResultDto> DeleteRowAsync(McpRowRequestDto input); Task<McpOutcome<McpWriteResultDto>> DeleteRowAsync(McpRowRequestDto input);
} }

View file

@ -1,4 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Sozsoft.Mcp.Dtos; using Sozsoft.Mcp.Dtos;
using Sozsoft.Platform.DeveloperKit; using Sozsoft.Platform.DeveloperKit;
@ -14,68 +14,68 @@ namespace Sozsoft.Mcp;
/// </summary> /// </summary>
public interface IMcpDevelopAppService : IApplicationService public interface IMcpDevelopAppService : IApplicationService
{ {
Task<McpDevGuideDto> GetDevGuideAsync(string? document, string? section); Task<McpOutcome<McpDevGuideDto>> GetDevGuideAsync(string? document, string? section);
Task<McpCreateTableResultDto> CreateTableAsync(McpCreateTableRequestDto input); Task<McpOutcome<McpCreateTableResultDto>> CreateTableAsync(McpCreateTableRequestDto input);
/// <summary>View/procedure/function scriptini iki saglayicinin seed klasorune yazar; veritabaninda calistirmaz.</summary> /// <summary>View/procedure/function scriptini iki saglayicinin seed klasorune yazar; veritabaninda calistirmaz.</summary>
Task<McpSqlObjectResultDto> SaveSqlObjectAsync(string sqlServerScript, string postgreSqlScript); Task<McpOutcome<McpSqlObjectResultDto>> SaveSqlObjectAsync(string sqlServerScript, string postgreSqlScript);
/// <summary>Tablo ya da view icin CRUD endpoint'lerini <c>crud/{Entity}.json</c> seed dosyasina yazar; kaydi DB Migrate ekler.</summary> /// <summary>Tablo ya da view icin CRUD endpoint'lerini <c>crud/{Entity}.json</c> seed dosyasina yazar; kaydi DB Migrate ekler.</summary>
Task<McpCrudEndpointsResultDto> GenerateCrudEndpointsAsync(string entityName, string? operationTypes, string? objectType); Task<McpOutcome<McpCrudEndpointsResultDto>> GenerateCrudEndpointsAsync(string entityName, string? operationTypes, string? objectType);
Task<List<WizardFileInfoDto>> GetWizardsAsync(string? search); Task<McpOutcome<List<WizardFileInfoDto>>> GetWizardsAsync(string? search);
Task<McpWizardFileDto> GetWizardAsync(string fileName); Task<McpOutcome<McpWizardFileDto>> GetWizardAsync(string fileName);
/// <summary>Deploy edilen wizard'in ozeti.</summary> /// <summary>Deploy edilen wizard'in ozeti.</summary>
Task<WizardFileInfoDto?> DeployWizardAsync(McpWizardDeployRequestDto input); Task<McpOutcome<WizardFileInfoDto?>> DeployWizardAsync(McpWizardDeployRequestDto input);
/// <summary>Wizard'i siler; <paramref name="dropTables"/> acikken ekranin secilebilir tablosunu da dusurur.</summary> /// <summary>Wizard'i siler; <paramref name="dropTables"/> acikken ekranin secilebilir tablosunu da dusurur.</summary>
Task<WizardDeleteResultDto> DeleteWizardAsync(string fileName, bool dropTables); Task<McpOutcome<WizardDeleteResultDto>> DeleteWizardAsync(string fileName, bool dropTables);
/// <summary>Deploy edilmis ekrani veri yazmadan dogrular.</summary> /// <summary>Deploy edilmis ekrani veri yazmadan dogrular.</summary>
Task<McpVerifyResultDto> VerifyWizardAsync(string fileName); Task<McpOutcome<McpVerifyResultDto>> VerifyWizardAsync(string fileName);
/// <summary>Id ya da basliga gore bulunan talebi, yalnizca Ready to Start durumundaysa In Progress'e alir.</summary> /// <summary>Id ya da basliga gore bulunan talebi, yalnizca Ready to Start durumundaysa In Progress'e alir.</summary>
Task<McpDevRequestDto> StartDevRequestAsync(string request); Task<McpOutcome<McpDevRequestDto>> StartDevRequestAsync(string request);
/// <summary>Talebi durumuna dokunmadan okur; yarim kalan bir talebe donmek icindir.</summary> /// <summary>Talebi durumuna dokunmadan okur; yarim kalan bir talebe donmek icindir.</summary>
Task<McpDevRequestDto> GetDevRequestAsync(string request); Task<McpOutcome<McpDevRequestDto>> GetDevRequestAsync(string request);
/// <summary> /// <summary>
/// Talebin analizini yazar (var olani degistirir); analiz kosulsuz uretime hazirsa talebi /// Talebin analizini yazar (var olani degistirir); analiz kosulsuz uretime hazirsa talebi
/// Ready to Start'a, degilse Analysis durumuna alir. Yalnizca Backlog ya da Analysis durumundaki /// Ready to Start'a, degilse Analysis durumuna alir. Yalnizca Backlog ya da Analysis durumundaki
/// talep analiz edilir. /// talep analiz edilir.
/// </summary> /// </summary>
Task<McpDevRequestDto> SaveDevRequestAnalysisAsync(string request, string html, bool productionReady); Task<McpOutcome<McpDevRequestDto>> SaveDevRequestAnalysisAsync(string request, string html, bool productionReady);
/// <summary> /// <summary>
/// Analizin sonuna HTML ekler. Aciklama talebin yazildigi haliyle kalir; uretim notlari /// Analizin sonuna HTML ekler. Aciklama talebin yazildigi haliyle kalir; uretim notlari
/// analize yazilir. /// analize yazilir.
/// </summary> /// </summary>
Task AppendDevRequestNoteAsync(string request, string html); Task<McpProblem?> AppendDevRequestNoteAsync(string request, string html);
/// <summary> /// <summary>
/// Talebin SubTasks listesine tamamlanmis satirlar ekler; atanan kisi cagiran kullanici, /// Talebin SubTasks listesine tamamlanmis satirlar ekler; atanan kisi cagiran kullanici,
/// bitis tarihi bugundur. /// bitis tarihi bugundur.
/// </summary> /// </summary>
Task AddDevRequestSubTasksAsync(string request, List<string> titles); Task<McpProblem?> AddDevRequestSubTasksAsync(string request, List<string> titles);
/// <summary>Modul dokumanini adiyla ya da ait oldugu ekranin kodu/wizard dosyasiyla okur.</summary> /// <summary>Modul dokumanini adiyla ya da ait oldugu ekranin kodu/wizard dosyasiyla okur.</summary>
Task<ModuleDocDto> GetModuleDocAsync(string name); Task<McpOutcome<ModuleDocDto>> GetModuleDocAsync(string name);
/// <summary> /// <summary>
/// Modul dokumaninin elle yazilan bolumlerini kaydeder; otomatik envanter sunucuda yeniden /// Modul dokumaninin elle yazilan bolumlerini kaydeder; otomatik envanter sunucuda yeniden
/// uretilir. /// uretilir.
/// </summary> /// </summary>
Task<ModuleDocDto> SaveModuleDocAsync(string name, string content); Task<McpOutcome<ModuleDocDto>> SaveModuleDocAsync(string name, string content);
Task<List<McpComponentSummaryDto>> GetComponentsAsync(string? search); Task<McpOutcome<List<McpComponentSummaryDto>>> GetComponentsAsync(string? search);
Task<CustomComponentDto?> GetComponentAsync(string name); Task<McpOutcome<CustomComponentDto?>> GetComponentAsync(string name);
Task<CustomComponentDto> SaveComponentAsync(McpComponentSaveRequestDto input); Task<McpOutcome<CustomComponentDto>> SaveComponentAsync(McpComponentSaveRequestDto input);
Task DeleteComponentAsync(string name); Task<McpProblem?> DeleteComponentAsync(string name);
} }

View file

@ -16,17 +16,17 @@ public interface IMcpQueryAppService : IApplicationService
/// okuma yetkisi, kiraci/sube filtreleri ve kullanici ozellestirmeleri uygulamadaki gibi /// okuma yetkisi, kiraci/sube filtreleri ve kullanici ozellestirmeleri uygulamadaki gibi
/// gecerlidir. Normal kullanicinin veri okuma yolu budur. /// gecerlidir. Normal kullanicinin veri okuma yolu budur.
/// </summary> /// </summary>
Task<McpListQueryResultDto> QueryListAsync(McpListQueryRequestDto input); Task<McpOutcome<McpListQueryResultDto>> QueryListAsync(McpListQueryRequestDto input);
/// <summary> /// <summary>
/// Tek bir SELECT ifadesi kosturur. Coklu ifade, DDL ve DML reddedilir; satir sayisi /// Tek bir SELECT ifadesi kosturur. Coklu ifade, DDL ve DML reddedilir; satir sayisi
/// sunucu kaydindaki tavana kirpilir. /// sunucu kaydindaki tavana kirpilir.
/// </summary> /// </summary>
Task<McpQueryResultDto> RunSelectAsync(McpSelectRequestDto input); Task<McpOutcome<McpQueryResultDto>> RunSelectAsync(McpSelectRequestDto input);
/// <summary>Veri kaynagindaki view/procedure/function nesneleri ve Custom Endpoint'ler.</summary> /// <summary>Veri kaynagindaki view/procedure/function nesneleri ve Custom Endpoint'ler.</summary>
Task<List<McpSavedQueryDto>> GetSavedQueriesAsync(string? search = null); Task<McpOutcome<List<McpSavedQueryDto>>> GetSavedQueriesAsync(string? search = null);
/// <summary>Kayitli bir nesneyi ya da Custom Endpoint'i parametreleriyle cagirir.</summary> /// <summary>Kayitli bir nesneyi ya da Custom Endpoint'i parametreleriyle cagirir.</summary>
Task<McpQueryResultDto> RunSavedQueryAsync(McpSavedQueryRequestDto input); Task<McpOutcome<McpQueryResultDto>> RunSavedQueryAsync(McpSavedQueryRequestDto input);
} }

View file

@ -1,4 +1,4 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Sozsoft.Mcp.Dtos; using Sozsoft.Mcp.Dtos;
using Volo.Abp.Application.Services; using Volo.Abp.Application.Services;
@ -11,16 +11,16 @@ namespace Sozsoft.Mcp;
/// </summary> /// </summary>
public interface IMcpReportAppService : IApplicationService public interface IMcpReportAppService : IApplicationService
{ {
Task<McpReportCatalogDto> GetReportTemplatesAsync(string? search); Task<McpOutcome<McpReportCatalogDto>> GetReportTemplatesAsync(string? search);
/// <summary>Sablonu Id'si ya da adiyla, HTML icerigi ve yer tutuculariyla doner.</summary> /// <summary>Sablonu Id'si ya da adiyla, HTML icerigi ve yer tutuculariyla doner.</summary>
Task<McpReportTemplateDto> GetReportTemplateAsync(string template); Task<McpOutcome<McpReportTemplateDto>> GetReportTemplateAsync(string template);
/// <summary>Kategoriyi ada gore kaydeder: yoksa ekler, varsa gunceller.</summary> /// <summary>Kategoriyi ada gore kaydeder: yoksa ekler, varsa gunceller.</summary>
Task<McpReportCategoryDto> SaveReportCategoryAsync(McpReportCategorySaveRequestDto input); Task<McpOutcome<McpReportCategoryDto>> SaveReportCategoryAsync(McpReportCategorySaveRequestDto input);
/// <summary>Sablonu ada gore kaydeder: yoksa ekler, varsa gunceller.</summary> /// <summary>Sablonu ada gore kaydeder: yoksa ekler, varsa gunceller.</summary>
Task<McpReportTemplateDto> SaveReportTemplateAsync(McpReportTemplateSaveRequestDto input); Task<McpOutcome<McpReportTemplateDto>> SaveReportTemplateAsync(McpReportTemplateSaveRequestDto input);
Task DeleteReportTemplateAsync(string template); Task<McpProblem?> DeleteReportTemplateAsync(string template);
} }

View file

@ -26,13 +26,13 @@ public interface IMcpSchemaAppService : IApplicationService
/// <param name="search">Bos birakilirsa tum listeler doner.</param> /// <param name="search">Bos birakilirsa tum listeler doner.</param>
/// <param name="maxResults">Donecek en fazla liste sayisi.</param> /// <param name="maxResults">Donecek en fazla liste sayisi.</param>
Task<List<McpListSummaryDto>> GetListsAsync(string? search = null, int maxResults = 200); Task<McpOutcome<List<McpListSummaryDto>>> GetListsAsync(string? search = null, int maxResults = 200);
/// <summary>Tek bir listenin sutunlariyla birlikte tanimi; bulunamazsa null.</summary> /// <summary>Tek bir listenin sutunlariyla birlikte tanimi; bulunamazsa null.</summary>
Task<McpListDetailDto?> GetListAsync(string listFormCode); Task<McpOutcome<McpListDetailDto?>> GetListAsync(string listFormCode);
/// <summary> /// <summary>
/// Liste ve sutun adlari ile basliklari uzerinde aksan ve buyuk/kucuk harf duyarsiz arama. /// Liste ve sutun adlari ile basliklari uzerinde aksan ve buyuk/kucuk harf duyarsiz arama.
/// </summary> /// </summary>
Task<List<McpSchemaMatchDto>> SearchAsync(string term, int maxResults = 50); Task<McpOutcome<List<McpSchemaMatchDto>>> SearchAsync(string term, int maxResults = 50);
} }

View file

@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Authorization;
using Sozsoft.Mcp.Dtos; using Sozsoft.Mcp.Dtos;
using Sozsoft.Mcp.Services; using Sozsoft.Mcp.Services;
using Sozsoft.Platform.Localization; using Sozsoft.Platform.Localization;
using Volo.Abp;
using Volo.Abp.Application.Services; using Volo.Abp.Application.Services;
namespace Sozsoft.Mcp; namespace Sozsoft.Mcp;
@ -46,40 +45,49 @@ public abstract class McpAppService : ApplicationService
} }
/// <summary> /// <summary>
/// Kaydi zorunlu kilan islemler icin. Kayit yoksa istemcinin nedenini gorebilecegi bir /// Kapali bir araca yapilan cagriyi reddeder: sunucu kaydi yoksa, arac sunucu kaydinda kapaliysa
/// hata atar. /// ya da kullanicinin uygulamada karsilik gelen yetkisi yoksa. Arac listesini suzmek tek basina
/// </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. /// yeterli degildir — istemci listede olmayan bir araci da cagirabilir.
/// </summary> /// </summary>
protected async Task<McpServerConfigDto> EnsureToolAllowedAsync(string toolName) protected async Task<McpOutcome<McpServerConfigDto>> EnsureToolAllowedAsync(string toolName)
{ {
var config = await RequireServerConfigAsync(); var config = await ResolveServerConfigAsync();
if (config is null)
{
return Problem(McpErrorCodes.NoServerRecord);
}
if (CurrentTenant.IsAvailable && !McpToolNames.TenantTools.Contains(toolName, StringComparer.Ordinal)) if (CurrentTenant.IsAvailable && !McpToolNames.TenantTools.Contains(toolName, StringComparer.Ordinal))
{ {
throw new UserFriendlyException(L[McpErrorCodes.TenantToolDisabled, toolName]); return Problem(McpErrorCodes.TenantToolDisabled, toolName);
} }
if (!McpToolPolicy.IsAllowed(true, CurrentTenant.IsAvailable, config.AllowedTools, toolName)) if (!McpToolPolicy.IsAllowed(true, CurrentTenant.IsAvailable, config.AllowedTools, toolName))
{ {
throw new UserFriendlyException(L[McpErrorCodes.ToolDisabled, toolName, config.Name]); return Problem(McpErrorCodes.ToolDisabled, toolName, config.Name);
} }
if (McpToolNames.RequiredPermissions.TryGetValue(toolName, out var permission)) if (McpToolNames.RequiredPermissions.TryGetValue(toolName, out var permission) &&
await CheckPermissionAsync(toolName, permission) is { } denied)
{ {
await AuthorizationService.CheckAsync(permission); return denied;
} }
return config; return config;
} }
/// <summary>
/// Yetkiyi adiyla ister; eksikse istemcinin rol ekraninda arayabilecegi ad ve kodla reddeder.
/// <c>AuthorizationService.CheckAsync</c> istisna atar, bu yuzden MCP'de kullanilmaz.
/// </summary>
protected async Task<McpProblem?> CheckPermissionAsync(string toolName, string permission)
=> await AuthorizationService.IsGrantedAsync(permission)
? null
: Problem(McpErrorCodes.PermissionDenied, toolName, McpPermissionText.Describe(L, permission));
protected McpProblem Problem(string name, params object[] arguments)
=> McpProblem.From(L, name, arguments);
/// <summary>Istenen satir sayisini sunucu kaydindaki tavana kirpar.</summary> /// <summary>Istenen satir sayisini sunucu kaydindaki tavana kirpar.</summary>
protected static int ResolveMaxRows(McpServerConfigDto config, int requested) protected static int ResolveMaxRows(McpServerConfigDto config, int requested)
{ {

View file

@ -31,28 +31,44 @@ public class McpDataAppService(
private const string DevRequestStatusField = "Status"; private const string DevRequestStatusField = "Status";
private const string DevRequestTestingStatus = "Testing"; private const string DevRequestTestingStatus = "Testing";
public async Task<McpWriteResultDto> InsertRowAsync(McpRowRequestDto input) public async Task<McpOutcome<McpWriteResultDto>> InsertRowAsync(McpRowRequestDto input)
{ {
var request = await BuildRequestAsync(input, McpToolNames.InsertRow, AuthorizationTypeEnum.Create, requireKeys: false, requireValues: true); if (!(await BuildRequestAsync(input, McpToolNames.InsertRow, AuthorizationTypeEnum.Create, requireKeys: false, requireValues: true)).TryGetValue(out var request, out var problem))
{
return problem;
}
var key = await listFormDataAppService.PostInsertAsync(request); var key = await listFormDataAppService.PostInsertAsync(request);
await timesheetRecorder.RecordAsync(McpTimesheetTexts.InsertRow, input.ListFormCode); await timesheetRecorder.RecordAsync(McpTimesheetTexts.InsertRow, input.ListFormCode);
return new McpWriteResultDto { Key = ((object?)key)?.ToString() }; return new McpWriteResultDto { Key = ((object?)key)?.ToString() };
} }
public async Task<McpWriteResultDto> UpdateRowAsync(McpRowRequestDto input) public async Task<McpOutcome<McpWriteResultDto>> UpdateRowAsync(McpRowRequestDto input)
{ {
var request = await BuildRequestAsync(input, McpToolNames.UpdateRow, AuthorizationTypeEnum.Update, requireKeys: true, requireValues: true); if (!(await BuildRequestAsync(input, McpToolNames.UpdateRow, AuthorizationTypeEnum.Update, requireKeys: true, requireValues: true)).TryGetValue(out var request, out var problem))
await EnsureModuleDocsBeforeTestingAsync(input); {
return problem;
}
if (await CheckModuleDocsBeforeTestingAsync(input) is { } incomplete)
{
return incomplete;
}
var affected = await listFormDataAppService.PostUpdateAsync(request); var affected = await listFormDataAppService.PostUpdateAsync(request);
await timesheetRecorder.RecordAsync(McpTimesheetTexts.UpdateRow, input.ListFormCode, string.Join(", ", input.Values.Keys)); await timesheetRecorder.RecordAsync(McpTimesheetTexts.UpdateRow, input.ListFormCode, string.Join(", ", input.Values.Keys));
return new McpWriteResultDto { AffectedRows = affected }; return new McpWriteResultDto { AffectedRows = affected };
} }
public async Task<McpWriteResultDto> DeleteRowAsync(McpRowRequestDto input) public async Task<McpOutcome<McpWriteResultDto>> DeleteRowAsync(McpRowRequestDto input)
{ {
var request = await BuildRequestAsync(input, McpToolNames.DeleteRow, AuthorizationTypeEnum.Delete, requireKeys: true, requireValues: false); if (!(await BuildRequestAsync(input, McpToolNames.DeleteRow, AuthorizationTypeEnum.Delete, requireKeys: true, requireValues: false)).TryGetValue(out var request, out var problem))
{
return problem;
}
var affected = await listFormDataAppService.PostDeleteAsync(request); var affected = await listFormDataAppService.PostDeleteAsync(request);
await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeleteRow, input.ListFormCode); await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeleteRow, input.ListFormCode);
@ -65,17 +81,17 @@ public class McpDataAppService(
/// Talebi Testing'e alan tek yol <c>update_row</c> oldugu icin kural burada uygulanir. Wizard /// Talebi Testing'e alan tek yol <c>update_row</c> oldugu icin kural burada uygulanir. Wizard
/// okuma yetkisi olmayan kullanici wizard uretemeyecegi icin kontrol ona uygulanmaz. /// okuma yetkisi olmayan kullanici wizard uretemeyecegi icin kontrol ona uygulanmaz.
/// </summary> /// </summary>
private async Task EnsureModuleDocsBeforeTestingAsync(McpRowRequestDto input) private async Task<McpProblem?> CheckModuleDocsBeforeTestingAsync(McpRowRequestDto input)
{ {
if (!string.Equals(input.ListFormCode, PlatformConsts.AppCodes.DevRequests, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(input.ListFormCode, PlatformConsts.AppCodes.DevRequests, StringComparison.OrdinalIgnoreCase))
return; return null;
var status = input.Values?.FirstOrDefault(v => string.Equals(v.Key, DevRequestStatusField, StringComparison.OrdinalIgnoreCase)).Value?.ToString(); var status = input.Values?.FirstOrDefault(v => string.Equals(v.Key, DevRequestStatusField, StringComparison.OrdinalIgnoreCase)).Value?.ToString();
if (!string.Equals(status, DevRequestTestingStatus, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(status, DevRequestTestingStatus, StringComparison.OrdinalIgnoreCase))
return; return null;
if (!await AuthorizationService.IsGrantedAsync(PlatformConsts.AppCodes.Listforms.Wizard)) if (!await AuthorizationService.IsGrantedAsync(PlatformConsts.AppCodes.Listforms.Wizard))
return; return null;
foreach (var key in input.Keys ?? []) foreach (var key in input.Keys ?? [])
{ {
@ -88,15 +104,17 @@ public class McpDataAppService(
var detail = string.Join("; ", incomplete.Select(doc => $"{doc.Path} ({string.Join(", ", doc.MissingSections)})")); var detail = string.Join("; ", incomplete.Select(doc => $"{doc.Path} ({string.Join(", ", doc.MissingSections)})"));
throw new UserFriendlyException(L[McpErrorCodes.DevRequestModuleDocsIncomplete, detail]); return Problem(McpErrorCodes.DevRequestModuleDocsIncomplete, detail);
} }
return null;
} }
/// <summary> /// <summary>
/// Istegi ekranin bekledigi bicime cevirir. <c>Data</c> JSON metni olarak verilir; ayni /// Istegi ekranin bekledigi bicime cevirir. <c>Data</c> JSON metni olarak verilir; ayni
/// bicim ekranin kopyalama yolunda da kullanilir. /// bicim ekranin kopyalama yolunda da kullanilir.
/// </summary> /// </summary>
private async Task<DataRequestDto> BuildRequestAsync( private async Task<McpOutcome<DataRequestDto>> BuildRequestAsync(
McpRowRequestDto input, McpRowRequestDto input,
string toolName, string toolName,
AuthorizationTypeEnum operation, AuthorizationTypeEnum operation,
@ -105,29 +123,32 @@ public class McpDataAppService(
{ {
Check.NotNull(input, nameof(input)); Check.NotNull(input, nameof(input));
await EnsureToolAllowedAsync(toolName); if (await EnsureToolAllowedAsync(toolName) is { Problem: { } problem })
{
return problem;
}
if (string.IsNullOrWhiteSpace(input.ListFormCode)) if (string.IsNullOrWhiteSpace(input.ListFormCode))
{ {
throw new UserFriendlyException(L[McpErrorCodes.ListFormCodeRequired]); return Problem(McpErrorCodes.ListFormCodeRequired);
} }
// Ekranin kendi kontrolu reddi adsiz bir hatayla bildirir; yetki burada adiyla istenir ki // Ekranin kendi kontrolu reddi adsiz bir hatayla bildirir; yetki burada adiyla istenir ki
// istemci hangi yetkinin eksik oldugunu gorsun. // istemci hangi yetkinin eksik oldugunu gorsun.
var permission = await accessFilter.GetPermissionNameAsync(input.ListFormCode, operation); var permission = await accessFilter.GetPermissionNameAsync(input.ListFormCode, operation);
if (permission is not null) if (permission is not null && await CheckPermissionAsync(toolName, permission) is { } denied)
{ {
await AuthorizationService.CheckAsync(permission); return denied;
} }
if (requireValues && (input.Values is null || input.Values.Count == 0)) if (requireValues && (input.Values is null || input.Values.Count == 0))
{ {
throw new UserFriendlyException(L[McpErrorCodes.ValuesRequired]); return Problem(McpErrorCodes.ValuesRequired);
} }
if (requireKeys && (input.Keys is null || input.Keys.Count == 0)) if (requireKeys && (input.Keys is null || input.Keys.Count == 0))
{ {
throw new UserFriendlyException(L[McpErrorCodes.KeysRequired]); return Problem(McpErrorCodes.KeysRequired);
} }
return new DataRequestDto return new DataRequestDto

View file

@ -79,16 +79,19 @@ public class McpDevelopAppService(
public const string SeedClosure = "seed_closure"; public const string SeedClosure = "seed_closure";
} }
public async Task<McpDevGuideDto> GetDevGuideAsync(string? document, string? section) public async Task<McpOutcome<McpDevGuideDto>> GetDevGuideAsync(string? document, string? section)
{ {
await EnsureToolAllowedAsync(McpToolNames.GetDevGuide); if (await EnsureToolAllowedAsync(McpToolNames.GetDevGuide) is { Problem: { } problem })
{
return problem;
}
if (string.IsNullOrWhiteSpace(section)) if (string.IsNullOrWhiteSpace(section))
{ {
var headings = await devGuide.GetHeadingsAsync(); var headings = await devGuide.GetHeadingsAsync();
if (headings.Count == 0) if (headings.Count == 0)
{ {
throw new UserFriendlyException(L[McpErrorCodes.DevGuideUnavailable, McpDevGuide.ConfigurationKey]); return Problem(McpErrorCodes.DevGuideUnavailable, McpDevGuide.ConfigurationKey);
} }
return new McpDevGuideDto return new McpDevGuideDto
@ -154,13 +157,20 @@ public class McpDevelopAppService(
/// Tablo tasarimcisiyla ayni yetkiyi ister: ham DDL liste yetkilerini asar, Develop yalnizca /// Tablo tasarimcisiyla ayni yetkiyi ister: ham DDL liste yetkilerini asar, Develop yalnizca
/// MCP kapisidir. /// MCP kapisidir.
/// </summary> /// </summary>
public async Task<McpCreateTableResultDto> CreateTableAsync(McpCreateTableRequestDto input) public async Task<McpOutcome<McpCreateTableResultDto>> CreateTableAsync(McpCreateTableRequestDto input)
{ {
Check.NotNull(input, nameof(input)); Check.NotNull(input, nameof(input));
await EnsureToolAllowedAsync(McpToolNames.CreateTable); if (await EnsureToolAllowedAsync(McpToolNames.CreateTable) is { Problem: { } problem })
await AuthorizationService.CheckAsync(PlatformConsts.AppCodes.SqlQueryManagers.Default); {
return problem;
}
var created = await tableCreator.CreateAsync(new McpTableDefinition( if (await CheckPermissionAsync(McpToolNames.CreateTable, PlatformConsts.AppCodes.SqlQueryManagers.Default) is { } denied)
{
return denied;
}
var outcome = await tableCreator.CreateAsync(new McpTableDefinition(
input.TableName, input.TableName,
[.. (input.Columns ?? []).Select(column => new McpTableColumn( [.. (input.Columns ?? []).Select(column => new McpTableColumn(
column.Name, column.Type, column.MaxLength, column.IsNullable, column.DefaultValue))], column.Name, column.Type, column.MaxLength, column.IsNullable, column.DefaultValue))],
@ -168,6 +178,10 @@ public class McpDevelopAppService(
input.MultiTenant, input.MultiTenant,
[.. (input.Indexes ?? []).Select(index => new McpTableIndex(index.Columns ?? [], index.Unique))], [.. (input.Indexes ?? []).Select(index => new McpTableIndex(index.Columns ?? [], index.Unique))],
[.. (input.ForeignKeys ?? []).Select(fk => new McpTableForeignKey(fk.Column, fk.ReferencedTable, fk.ReferencedColumn, fk.OnDelete))])); [.. (input.ForeignKeys ?? []).Select(fk => new McpTableForeignKey(fk.Column, fk.ReferencedTable, fk.ReferencedColumn, fk.OnDelete))]));
if (!outcome.TryGetValue(out var created, out var invalid))
{
return invalid;
}
await timesheetRecorder.RecordAsync(McpTimesheetTexts.CreateTable, created.TableName); await timesheetRecorder.RecordAsync(McpTimesheetTexts.CreateTable, created.TableName);
@ -183,12 +197,23 @@ public class McpDevelopAppService(
/// Tablo tasarimcisiyla ayni yetkiyi ister: SQL Query Manager'daki view/procedure/function /// Tablo tasarimcisiyla ayni yetkiyi ister: SQL Query Manager'daki view/procedure/function
/// kaydinin karsiligidir. Nesneyi DB Migrate kurar. /// kaydinin karsiligidir. Nesneyi DB Migrate kurar.
/// </summary> /// </summary>
public async Task<McpSqlObjectResultDto> SaveSqlObjectAsync(string sqlServerScript, string postgreSqlScript) public async Task<McpOutcome<McpSqlObjectResultDto>> SaveSqlObjectAsync(string sqlServerScript, string postgreSqlScript)
{ {
await EnsureToolAllowedAsync(McpToolNames.SaveSqlObject); if (await EnsureToolAllowedAsync(McpToolNames.SaveSqlObject) is { Problem: { } problem })
await AuthorizationService.CheckAsync(PlatformConsts.AppCodes.SqlQueryManagers.Default); {
return problem;
}
if (await CheckPermissionAsync(McpToolNames.SaveSqlObject, PlatformConsts.AppCodes.SqlQueryManagers.Default) is { } denied)
{
return denied;
}
if (!sqlObjectWriter.Save(sqlServerScript, postgreSqlScript).TryGetValue(out var saved, out var invalid))
{
return invalid;
}
var saved = sqlObjectWriter.Save(sqlServerScript, postgreSqlScript);
await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveSqlObject, saved.Name); await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveSqlObject, saved.Name);
return new McpSqlObjectResultDto return new McpSqlObjectResultDto
@ -204,16 +229,23 @@ public class McpDevelopAppService(
/// (<see cref="CrudEndpointSeedFileDto.Generate"/>) yalnizca <c>crud/{Entity}.json</c> seed dosyasina /// (<see cref="CrudEndpointSeedFileDto.Generate"/>) yalnizca <c>crud/{Entity}.json</c> seed dosyasina
/// yazar; kayitlari DB Migrate ekler (<c>CrudEndpointSeedApplier</c>). Yetki diyalogunkiyle aynidir. /// yazar; kayitlari DB Migrate ekler (<c>CrudEndpointSeedApplier</c>). Yetki diyalogunkiyle aynidir.
/// </summary> /// </summary>
public async Task<McpCrudEndpointsResultDto> GenerateCrudEndpointsAsync(string entityName, string? operationTypes, string? objectType) public async Task<McpOutcome<McpCrudEndpointsResultDto>> GenerateCrudEndpointsAsync(string entityName, string? operationTypes, string? objectType)
{ {
await EnsureToolAllowedAsync(McpToolNames.GenerateCrudEndpoints); if (await EnsureToolAllowedAsync(McpToolNames.GenerateCrudEndpoints) is { Problem: { } problem })
await AuthorizationService.CheckAsync(PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints); {
return problem;
}
if (await CheckPermissionAsync(McpToolNames.GenerateCrudEndpoints, PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints) is { } denied)
{
return denied;
}
// Tasarimcidaki gibi sema oneki atilir: endpoint yolu yalnizca nesne adini tasir. // Tasarimcidaki gibi sema oneki atilir: endpoint yolu yalnizca nesne adini tasir.
var entity = entityName?.Split('.')[^1].Trim('[', ']', '"', ' ') ?? string.Empty; var entity = entityName?.Split('.')[^1].Trim('[', ']', '"', ' ') ?? string.Empty;
if (CrudEndpointSeedFileDto.SafeFileName(entity) is null) if (CrudEndpointSeedFileDto.SafeFileName(entity) is null)
{ {
throw new UserFriendlyException(L[McpErrorCodes.InvalidIdentifier, entityName ?? string.Empty]); return Problem(McpErrorCodes.InvalidIdentifier, entityName ?? string.Empty);
} }
var seedFile = CrudEndpointSeedFileDto.Generate(entity, operationTypes, objectType, Clock.Now.ToUniversalTime()); var seedFile = CrudEndpointSeedFileDto.Generate(entity, operationTypes, objectType, Clock.Now.ToUniversalTime());
@ -228,9 +260,12 @@ public class McpDevelopAppService(
}; };
} }
public async Task<List<WizardFileInfoDto>> GetWizardsAsync(string? search) public async Task<McpOutcome<List<WizardFileInfoDto>>> GetWizardsAsync(string? search)
{ {
await EnsureToolAllowedAsync(McpToolNames.ListWizards); if (await EnsureToolAllowedAsync(McpToolNames.ListWizards) is { Problem: { } problem })
{
return problem;
}
var files = await wizardAppService.GetFiles(); var files = await wizardAppService.GetFiles();
if (string.IsNullOrWhiteSpace(search)) if (string.IsNullOrWhiteSpace(search))
@ -238,14 +273,17 @@ public class McpDevelopAppService(
return files; return files;
} }
return [.. files.Where(file => return files.Where(file =>
Contains(file.FileName, search) || Contains(file.WizardName, search) || Contains(file.FileName, search) || Contains(file.WizardName, search) ||
Contains(file.ListFormCode, search) || Contains(file.CustomComponentName, search))]; Contains(file.ListFormCode, search) || Contains(file.CustomComponentName, search)).ToList();
} }
public async Task<McpWizardFileDto> GetWizardAsync(string fileName) public async Task<McpOutcome<McpWizardFileDto>> GetWizardAsync(string fileName)
{ {
await EnsureToolAllowedAsync(McpToolNames.GetWizard); if (await EnsureToolAllowedAsync(McpToolNames.GetWizard) is { Problem: { } problem })
{
return problem;
}
var seed = await wizardAppService.GetFile(fileName); var seed = await wizardAppService.GetFile(fileName);
@ -256,14 +294,24 @@ public class McpDevelopAppService(
return new McpWizardFileDto { FileName = fileName, Seed = document.RootElement.Clone() }; return new McpWizardFileDto { FileName = fileName, Seed = document.RootElement.Clone() };
} }
public async Task<WizardFileInfoDto?> DeployWizardAsync(McpWizardDeployRequestDto input) public async Task<McpOutcome<WizardFileInfoDto?>> DeployWizardAsync(McpWizardDeployRequestDto input)
{ {
Check.NotNull(input, nameof(input)); Check.NotNull(input, nameof(input));
await EnsureToolAllowedAsync(McpToolNames.DeployWizard); if (await EnsureToolAllowedAsync(McpToolNames.DeployWizard) is { Problem: { } problem })
{
return problem;
}
if (!ParseWizard(input.Wizard).TryGetValue(out var wizard, out var invalid))
{
return invalid;
}
var wizard = ParseWizard(input.Wizard);
wizard.EditFileName = string.IsNullOrWhiteSpace(input.ReplaceFileName) ? null : input.ReplaceFileName.Trim(); wizard.EditFileName = string.IsNullOrWhiteSpace(input.ReplaceFileName) ? null : input.ReplaceFileName.Trim();
EnsureDetailedDescription(wizard); if (CheckDetailedDescription(wizard) is { } shortDescription)
{
return shortDescription;
}
// Menu sirasi modelin karari degildir: yeni ekran kardeslerinin sonuna eklenir (deploy // Menu sirasi modelin karari degildir: yeni ekran kardeslerinin sonuna eklenir (deploy
// bagimlilik sirasiyla yapildigi icin lookup kaynagi tuketen ekranin ustunde kalir), // bagimlilik sirasiyla yapildigi icin lookup kaynagi tuketen ekranin ustunde kalir),
@ -290,7 +338,10 @@ public class McpDevelopAppService(
editGroups[0].Caption = string.Empty; editGroups[0].Caption = string.Empty;
// Satirlar deploy'dan once dogrulanir; gecersiz girdi yarim bir deploy birakmaz. // Satirlar deploy'dan once dogrulanir; gecersiz girdi yarim bir deploy birakmaz.
var seedRows = await ParseSeedRowsAsync(input.SeedRows, wizard); if (!(await ParseSeedRowsAsync(input.SeedRows, wizard)).TryGetValue(out var seedRows, out invalid))
{
return invalid;
}
// Wizard dosyasi onu ureten talebi tasir; Wizard Manager talebi gosterir ve talebin // Wizard dosyasi onu ureten talebi tasir; Wizard Manager talebi gosterir ve talebin
// butun wizard'larini birlikte export eder. Bag modelin girdisinden degil etkin talepten gelir. // butun wizard'larini birlikte export eder. Bag modelin girdisinden degil etkin talepten gelir.
@ -301,8 +352,8 @@ public class McpDevelopAppService(
var files = await wizardAppService.GetFiles(); var files = await wizardAppService.GetFiles();
var file = files.FirstOrDefault(file => string.Equals(file.WizardName, wizard.WizardName?.Trim(), StringComparison.OrdinalIgnoreCase)); var file = files.FirstOrDefault(file => string.Equals(file.WizardName, wizard.WizardName?.Trim(), StringComparison.OrdinalIgnoreCase));
if (file != null && seedRows.Count > 0) if (file != null && seedRows.Count > 0 && await WriteSeedRowsAsync(file.FileName, seedRows) is { } rowsRejected)
await WriteSeedRowsAsync(file.FileName, seedRows); return rowsRejected;
await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeployWizard, file?.FileName ?? wizard.WizardName, seedRows.Count); await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeployWizard, file?.FileName ?? wizard.WizardName, seedRows.Count);
@ -315,31 +366,32 @@ public class McpDevelopAppService(
/// hesaplar ve iliskili ekranlar (lowcode §4.4.2). Tek satirlik aciklama bu ihtiyaci karsilamadigi /// hesaplar ve iliskili ekranlar (lowcode §4.4.2). Tek satirlik aciklama bu ihtiyaci karsilamadigi
/// icin MCP deploy'u kisa aciklamayi reddeder. /// icin MCP deploy'u kisa aciklamayi reddeder.
/// </summary> /// </summary>
private void EnsureDetailedDescription(ListFormWizardDto wizard) private McpProblem? CheckDetailedDescription(ListFormWizardDto wizard)
{ {
if (wizard.ComponentKind == WizardComponentKindEnum.Custom) if (wizard.ComponentKind == WizardComponentKindEnum.Custom)
return; return null;
static bool IsDetailed(string? text) => static bool IsDetailed(string? text) =>
!string.IsNullOrWhiteSpace(text) && !string.IsNullOrWhiteSpace(text) &&
text.Trim().Length >= MinDescriptionLength && text.Trim().Length >= MinDescriptionLength &&
text.Split('\n').Count(line => !string.IsNullOrWhiteSpace(line)) >= MinDescriptionLines; text.Split('\n').Count(line => !string.IsNullOrWhiteSpace(line)) >= MinDescriptionLines;
if (!IsDetailed(wizard.DescTr) || !IsDetailed(wizard.DescEn)) return IsDetailed(wizard.DescTr) && IsDetailed(wizard.DescEn)
throw new UserFriendlyException(L[McpErrorCodes.WizardDescriptionTooShort, MinDescriptionLines, MinDescriptionLength]); ? null
: Problem(McpErrorCodes.WizardDescriptionTooShort, MinDescriptionLines, MinDescriptionLength);
} }
/// <summary> /// <summary>
/// Demo satirlarini okur ve ekranin alanlarina gore dogrular. Satirlar yalnizca SeedData acik /// Demo satirlarini okur ve ekranin alanlarina gore dogrular. Satirlar yalnizca SeedData acik
/// liste ekraninda kabul edilir: migrate onlari ekranin veri dosyasindan ekler. /// liste ekraninda kabul edilir: migrate onlari ekranin veri dosyasindan ekler.
/// </summary> /// </summary>
private async Task<List<Dictionary<string, JsonElement>>> ParseSeedRowsAsync(string? json, ListFormWizardDto wizard) private async Task<McpOutcome<List<Dictionary<string, JsonElement>>>> ParseSeedRowsAsync(string? json, ListFormWizardDto wizard)
{ {
if (string.IsNullOrWhiteSpace(json)) if (string.IsNullOrWhiteSpace(json))
return []; return new List<Dictionary<string, JsonElement>>();
if (wizard.ComponentKind == WizardComponentKindEnum.Custom || !wizard.SeedData) if (wizard.ComponentKind == WizardComponentKindEnum.Custom || !wizard.SeedData)
throw new UserFriendlyException(L[McpErrorCodes.SeedRowsNotAllowed]); return Problem(McpErrorCodes.SeedRowsNotAllowed);
List<Dictionary<string, JsonElement>>? rows; List<Dictionary<string, JsonElement>>? rows;
try try
@ -352,7 +404,7 @@ public class McpDevelopAppService(
} }
if (rows is null || rows.Any(row => row is null)) if (rows is null || rows.Any(row => row is null))
throw new UserFriendlyException(L[McpErrorCodes.SeedRowsInvalid]); return Problem(McpErrorCodes.SeedRowsInvalid);
var fields = wizard.Groups.SelectMany(group => group.Items).Select(item => item.FieldName) var fields = wizard.Groups.SelectMany(group => group.Items).Select(item => item.FieldName)
.Append(wizard.KeyFieldName) .Append(wizard.KeyFieldName)
@ -367,13 +419,13 @@ public class McpDevelopAppService(
var screenFields = string.Join(", ", fields); var screenFields = string.Join(", ", fields);
var owners = await FindSameTableScreensAsync(wizard, unknown); var owners = await FindSameTableScreensAsync(wizard, unknown);
if (owners.Count == 0) if (owners.Count == 0)
throw new UserFriendlyException(L[McpErrorCodes.SeedRowsUnknownFields, string.Join(", ", unknown), screenFields]); return Problem(McpErrorCodes.SeedRowsUnknownFields, string.Join(", ", unknown), screenFields);
// Ayni tablonun baska ekraninda (cogunlukla alt form sekmesi) alan olan kolon migrate'te // Ayni tablonun baska ekraninda (cogunlukla alt form sekmesi) alan olan kolon migrate'te
// bu ekranin veri dosyasindan yazilmaz; model kolonun nerede durdugunu gorurse alani ekrana // bu ekranin veri dosyasindan yazilmaz; model kolonun nerede durdugunu gorurse alani ekrana
// ekleyip ya da satirdan cikarip tekrar dener. // ekleyip ya da satirdan cikarip tekrar dener.
var placement = string.Join("; ", owners.Select(owner => $"{owner.Key}: {string.Join(", ", owner.Value)}")); var placement = string.Join("; ", owners.Select(owner => $"{owner.Key}: {string.Join(", ", owner.Value)}"));
throw new UserFriendlyException(L[McpErrorCodes.SeedRowsFieldsOnOtherScreens, string.Join(", ", unknown), placement, screenFields]); return Problem(McpErrorCodes.SeedRowsFieldsOnOtherScreens, string.Join(", ", unknown), placement, screenFields);
} }
/// <summary> /// <summary>
@ -417,12 +469,12 @@ public class McpDevelopAppService(
/// burada uretilir: dosya her uygulandiginda ayni kayda karsilik gelir. Hedefte ayni icerikli satir /// burada uretilir: dosya her uygulandiginda ayni kayda karsilik gelir. Hedefte ayni icerikli satir
/// varsa (tekrar deploy) yeniden eklenmez. /// varsa (tekrar deploy) yeniden eklenmez.
/// </summary> /// </summary>
private async Task WriteSeedRowsAsync(string fileName, List<Dictionary<string, JsonElement>> rows) private async Task<McpProblem?> WriteSeedRowsAsync(string fileName, List<Dictionary<string, JsonElement>> rows)
{ {
var listForm = (await wizardAppService.GetFile(fileName)).ListForm; var listForm = (await wizardAppService.GetFile(fileName)).ListForm;
var target = ListFormSeedTarget.Create(SeedPathResolver.GetRootPath(configuration), listForm?.SeedFilePath); var target = ListFormSeedTarget.Create(SeedPathResolver.GetRootPath(configuration), listForm?.SeedFilePath);
if (listForm == null || target == null) if (listForm == null || target == null)
throw new UserFriendlyException(L[McpErrorCodes.SeedRowsNotAllowed]); return Problem(McpErrorCodes.SeedRowsNotAllowed);
var keyFieldName = listForm.KeyFieldName; var keyFieldName = listForm.KeyFieldName;
var existing = (await target.ReadRowsAsync()).Select(row => RowContent(row, keyFieldName)).ToHashSet(StringComparer.Ordinal); var existing = (await target.ReadRowsAsync()).Select(row => RowContent(row, keyFieldName)).ToHashSet(StringComparer.Ordinal);
@ -441,8 +493,9 @@ public class McpDevelopAppService(
} }
// Satir klasorunde her satir anahtariyla adlanan dosyadadir; anahtarsiz satir yazilamaz. // Satir klasorunde her satir anahtariyla adlanan dosyadadir; anahtarsiz satir yazilamaz.
if (added.Count > 0 && (await target.UpsertRowsAsync(listForm.ListFormCode, keyFieldName, added)).Count > 0) return added.Count > 0 && (await target.UpsertRowsAsync(listForm.ListFormCode, keyFieldName, added)).Count > 0
throw new UserFriendlyException(L[McpErrorCodes.SeedRowsInvalid]); ? Problem(McpErrorCodes.SeedRowsInvalid)
: null;
} }
/// <summary>Satirin anahtar disindaki icerigi; tekrar deploy'da ayni satiri tanimak icin.</summary> /// <summary>Satirin anahtar disindaki icerigi; tekrar deploy'da ayni satiri tanimak icin.</summary>
@ -456,9 +509,12 @@ public class McpDevelopAppService(
/// Ekranin tablosu ancak <paramref name="dropTables"/> acikken ve Wizard Manager'daki onayla ayni /// Ekranin tablosu ancak <paramref name="dropTables"/> acikken ve Wizard Manager'daki onayla ayni
/// kosullarda (secilebilir aday) dusurulur; onayi kullanicidan almak istemcinin isidir. /// kosullarda (secilebilir aday) dusurulur; onayi kullanicidan almak istemcinin isidir.
/// </summary> /// </summary>
public async Task<WizardDeleteResultDto> DeleteWizardAsync(string fileName, bool dropTables) public async Task<McpOutcome<WizardDeleteResultDto>> DeleteWizardAsync(string fileName, bool dropTables)
{ {
await EnsureToolAllowedAsync(McpToolNames.DeleteWizard); if (await EnsureToolAllowedAsync(McpToolNames.DeleteWizard) is { Problem: { } problem })
{
return problem;
}
var input = new WizardDeleteInput { FileName = fileName }; var input = new WizardDeleteInput { FileName = fileName };
if (dropTables) if (dropTables)
@ -480,9 +536,12 @@ public class McpDevelopAppService(
/// iz birakirdi. Seed kapanisi engellemez: ekran bu ortamda calisir, eksik dosya yalnizca /// iz birakirdi. Seed kapanisi engellemez: ekran bu ortamda calisir, eksik dosya yalnizca
/// migrate ya da baska bir ortamda ortaya cikar. /// migrate ya da baska bir ortamda ortaya cikar.
/// </summary> /// </summary>
public async Task<McpVerifyResultDto> VerifyWizardAsync(string fileName) public async Task<McpOutcome<McpVerifyResultDto>> VerifyWizardAsync(string fileName)
{ {
await EnsureToolAllowedAsync(McpToolNames.VerifyWizard); if (await EnsureToolAllowedAsync(McpToolNames.VerifyWizard) is { Problem: { } problem })
{
return problem;
}
var result = new McpVerifyResultDto { FileName = fileName }; var result = new McpVerifyResultDto { FileName = fileName };
@ -549,16 +608,26 @@ public class McpDevelopAppService(
/// ayni unit of work'te In Progress'e alinir. MCP yetki vermez; ekranin guncelleme yetkisi /// ayni unit of work'te In Progress'e alinir. MCP yetki vermez; ekranin guncelleme yetkisi
/// ayrica istenir. /// ayrica istenir.
/// </summary> /// </summary>
public async Task<McpDevRequestDto> StartDevRequestAsync(string request) public async Task<McpOutcome<McpDevRequestDto>> StartDevRequestAsync(string request)
{ {
await EnsureToolAllowedAsync(McpToolNames.StartDevRequest); if (await EnsureToolAllowedAsync(McpToolNames.StartDevRequest) is { Problem: { } problem })
await AuthorizationService.CheckAsync(DevRequestUpdatePermission); {
return problem;
}
var devRequest = await FindDevRequestAsync(request); if (await CheckPermissionAsync(McpToolNames.StartDevRequest, DevRequestUpdatePermission) is { } denied)
{
return denied;
}
if (!(await FindDevRequestAsync(request)).TryGetValue(out var devRequest, out var notFound))
{
return notFound;
}
if (!string.Equals(devRequest.Status, McpConsts.DevRequestStatuses.ReadyToStart, StringComparison.Ordinal)) if (!string.Equals(devRequest.Status, McpConsts.DevRequestStatuses.ReadyToStart, StringComparison.Ordinal))
{ {
throw new UserFriendlyException(L[McpErrorCodes.DevRequestNotReady, devRequest.Title ?? devRequest.Id.ToString(), devRequest.Status ?? string.Empty]); return Problem(McpErrorCodes.DevRequestNotReady, devRequest.Title ?? devRequest.Id.ToString(), devRequest.Status ?? string.Empty);
} }
devRequest.Status = McpConsts.DevRequestStatuses.InProgress; devRequest.Status = McpConsts.DevRequestStatuses.InProgress;
@ -568,12 +637,22 @@ public class McpDevelopAppService(
return ToDto(devRequest); return ToDto(devRequest);
} }
public async Task<McpDevRequestDto> GetDevRequestAsync(string request) public async Task<McpOutcome<McpDevRequestDto>> GetDevRequestAsync(string request)
{ {
await EnsureToolAllowedAsync(McpToolNames.GetDevRequest); if (await EnsureToolAllowedAsync(McpToolNames.GetDevRequest) is { Problem: { } problem })
await AuthorizationService.CheckAsync(PlatformConsts.AppCodes.DevRequests); {
return problem;
}
var devRequest = await FindDevRequestAsync(request); if (await CheckPermissionAsync(McpToolNames.GetDevRequest, PlatformConsts.AppCodes.DevRequests) is { } denied)
{
return denied;
}
if (!(await FindDevRequestAsync(request)).TryGetValue(out var devRequest, out var notFound))
{
return notFound;
}
await timesheetRecorder.BeginAsync(devRequest.Id); await timesheetRecorder.BeginAsync(devRequest.Id);
return ToDto(devRequest); return ToDto(devRequest);
@ -585,21 +664,31 @@ public class McpDevelopAppService(
/// talep dogrudan Ready to Start'a gecer; sartli ya da Hayir olan analiz kullanicinin kararini /// talep dogrudan Ready to Start'a gecer; sartli ya da Hayir olan analiz kullanicinin kararini
/// bekledigi icin Analysis'te kalir. /// bekledigi icin Analysis'te kalir.
/// </summary> /// </summary>
public async Task<McpDevRequestDto> SaveDevRequestAnalysisAsync(string request, string html, bool productionReady) public async Task<McpOutcome<McpDevRequestDto>> SaveDevRequestAnalysisAsync(string request, string html, bool productionReady)
{ {
await EnsureToolAllowedAsync(McpToolNames.SaveDevRequestAnalysis); if (await EnsureToolAllowedAsync(McpToolNames.SaveDevRequestAnalysis) is { Problem: { } problem })
await AuthorizationService.CheckAsync(DevRequestUpdatePermission); {
return problem;
}
if (await CheckPermissionAsync(McpToolNames.SaveDevRequestAnalysis, DevRequestUpdatePermission) is { } denied)
{
return denied;
}
if (string.IsNullOrWhiteSpace(html)) if (string.IsNullOrWhiteSpace(html))
{ {
throw new UserFriendlyException(L[McpErrorCodes.DevRequestAnalysisRequired]); return Problem(McpErrorCodes.DevRequestAnalysisRequired);
} }
var devRequest = await FindDevRequestAsync(request); if (!(await FindDevRequestAsync(request)).TryGetValue(out var devRequest, out var notFound))
{
return notFound;
}
if (devRequest.Status is not (McpConsts.DevRequestStatuses.Backlog or McpConsts.DevRequestStatuses.Analysis or McpConsts.DevRequestStatuses.ReadyToStart)) if (devRequest.Status is not (McpConsts.DevRequestStatuses.Backlog or McpConsts.DevRequestStatuses.Analysis or McpConsts.DevRequestStatuses.ReadyToStart))
{ {
throw new UserFriendlyException(L[McpErrorCodes.DevRequestNotAnalyzable, devRequest.Title ?? devRequest.Id.ToString(), devRequest.Status ?? string.Empty]); return Problem(McpErrorCodes.DevRequestNotAnalyzable, devRequest.Title ?? devRequest.Id.ToString(), devRequest.Status ?? string.Empty);
} }
devRequest.Analysis = html.Trim(); devRequest.Analysis = html.Trim();
@ -616,20 +705,33 @@ public class McpDevelopAppService(
/// Aciklama talebin yazildigi haliyle kalir ve base64 gorsel tasir; uretim notlari analize /// Aciklama talebin yazildigi haliyle kalir ve base64 gorsel tasir; uretim notlari analize
/// eklenir. Ekleme sunucuda, var olan metne dokunmadan yapilir. /// eklenir. Ekleme sunucuda, var olan metne dokunmadan yapilir.
/// </summary> /// </summary>
public async Task AppendDevRequestNoteAsync(string request, string html) public async Task<McpProblem?> AppendDevRequestNoteAsync(string request, string html)
{ {
await EnsureToolAllowedAsync(McpToolNames.AppendDevRequestNote); if (await EnsureToolAllowedAsync(McpToolNames.AppendDevRequestNote) is { Problem: { } problem })
await AuthorizationService.CheckAsync(DevRequestUpdatePermission); {
return problem;
}
if (await CheckPermissionAsync(McpToolNames.AppendDevRequestNote, DevRequestUpdatePermission) is { } denied)
{
return denied;
}
if (string.IsNullOrWhiteSpace(html)) if (string.IsNullOrWhiteSpace(html))
{ {
throw new UserFriendlyException(L[McpErrorCodes.DevRequestNoteRequired]); return Problem(McpErrorCodes.DevRequestNoteRequired);
}
if (!(await FindDevRequestAsync(request)).TryGetValue(out var devRequest, out var notFound))
{
return notFound;
} }
var devRequest = await FindDevRequestAsync(request);
devRequest.Analysis = string.Concat(devRequest.Analysis, html.Trim()); devRequest.Analysis = string.Concat(devRequest.Analysis, html.Trim());
await devRequestRepository.UpdateAsync(devRequest, autoSave: true); await devRequestRepository.UpdateAsync(devRequest, autoSave: true);
await timesheetRecorder.RecordAsync(McpTimesheetTexts.AppendNote); await timesheetRecorder.RecordAsync(McpTimesheetTexts.AppendNote);
return null;
} }
/// <summary> /// <summary>
@ -637,29 +739,44 @@ public class McpDevelopAppService(
/// (ekranin atanan listesi kullanici adiyla calisir), bitis tarihi yazildigi gundur. /// (ekranin atanan listesi kullanici adiyla calisir), bitis tarihi yazildigi gundur.
/// Var olan satirlara dokunulmaz. /// Var olan satirlara dokunulmaz.
/// </summary> /// </summary>
public async Task AddDevRequestSubTasksAsync(string request, List<string> titles) public async Task<McpProblem?> AddDevRequestSubTasksAsync(string request, List<string> titles)
{ {
await EnsureToolAllowedAsync(McpToolNames.AddDevRequestSubTasks); if (await EnsureToolAllowedAsync(McpToolNames.AddDevRequestSubTasks) is { Problem: { } problem })
await AuthorizationService.CheckAsync(DevRequestUpdatePermission); {
return problem;
}
if (await CheckPermissionAsync(McpToolNames.AddDevRequestSubTasks, DevRequestUpdatePermission) is { } denied)
{
return denied;
}
var items = (titles ?? []).Select(title => title?.Trim()).Where(title => !string.IsNullOrEmpty(title)).ToList(); var items = (titles ?? []).Select(title => title?.Trim()).Where(title => !string.IsNullOrEmpty(title)).ToList();
if (items.Count == 0) if (items.Count == 0)
{ {
throw new UserFriendlyException(L[McpErrorCodes.DevRequestSubTasksRequired]); return Problem(McpErrorCodes.DevRequestSubTasksRequired);
} }
var devRequest = await FindDevRequestAsync(request); if (!(await FindDevRequestAsync(request)).TryGetValue(out var devRequest, out var notFound))
{
return notFound;
}
JsonArray subTasks; JsonArray? subTasks;
try try
{ {
subTasks = string.IsNullOrWhiteSpace(devRequest.SubTasks) subTasks = string.IsNullOrWhiteSpace(devRequest.SubTasks)
? [] ? new JsonArray()
: JsonNode.Parse(devRequest.SubTasks) as JsonArray ?? throw new JsonException(); : JsonNode.Parse(devRequest.SubTasks) as JsonArray;
} }
catch (JsonException) catch (JsonException)
{ {
throw new UserFriendlyException(L[McpErrorCodes.DevRequestSubTasksInvalid, devRequest.Title ?? devRequest.Id.ToString()]); subTasks = null;
}
if (subTasks is null)
{
return Problem(McpErrorCodes.DevRequestSubTasksInvalid, devRequest.Title ?? devRequest.Id.ToString());
} }
var assignee = CurrentUser.UserName ?? string.Empty; var assignee = CurrentUser.UserName ?? string.Empty;
@ -680,17 +797,26 @@ public class McpDevelopAppService(
devRequest.SubTasks = subTasks.ToJsonString(); devRequest.SubTasks = subTasks.ToJsonString();
await devRequestRepository.UpdateAsync(devRequest, autoSave: true); await devRequestRepository.UpdateAsync(devRequest, autoSave: true);
await timesheetRecorder.RecordAsync(McpTimesheetTexts.AddSubTasks, items.Count); await timesheetRecorder.RecordAsync(McpTimesheetTexts.AddSubTasks, items.Count);
return null;
} }
public async Task<ModuleDocDto> GetModuleDocAsync(string name) public async Task<McpOutcome<ModuleDocDto>> GetModuleDocAsync(string name)
{ {
await EnsureToolAllowedAsync(McpToolNames.GetModuleDoc); if (await EnsureToolAllowedAsync(McpToolNames.GetModuleDoc) is { Problem: { } problem })
{
return problem;
}
return await moduleDocAppService.GetByNameAsync(name); return await moduleDocAppService.GetByNameAsync(name);
} }
public async Task<ModuleDocDto> SaveModuleDocAsync(string name, string content) public async Task<McpOutcome<ModuleDocDto>> SaveModuleDocAsync(string name, string content)
{ {
await EnsureToolAllowedAsync(McpToolNames.SaveModuleDoc); if (await EnsureToolAllowedAsync(McpToolNames.SaveModuleDoc) is { Problem: { } problem })
{
return problem;
}
var doc = await moduleDocAppService.SaveAsync(new ModuleDocSaveInput { Name = name, Content = content }); var doc = await moduleDocAppService.SaveAsync(new ModuleDocSaveInput { Name = name, Content = content });
await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveModuleDoc, doc.Name); await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveModuleDoc, doc.Name);
@ -720,23 +846,23 @@ public class McpDevelopAppService(
} }
/// <summary>Id ile ya da tam baslikla bulur; ayni basligi tasiyan birden fazla talep varsa Id istenir.</summary> /// <summary>Id ile ya da tam baslikla bulur; ayni basligi tasiyan birden fazla talep varsa Id istenir.</summary>
private async Task<DevRequest> FindDevRequestAsync(string? request) private async Task<McpOutcome<DevRequest>> FindDevRequestAsync(string? request)
{ {
var value = request?.Trim() ?? string.Empty; var value = request?.Trim() ?? string.Empty;
if (Guid.TryParse(value, out var id)) if (Guid.TryParse(value, out var id))
{ {
return await devRequestRepository.FindAsync(id) var byId = await devRequestRepository.FindAsync(id);
?? throw new UserFriendlyException(L[McpErrorCodes.DevRequestNotFound, value]); return byId is null ? Problem(McpErrorCodes.DevRequestNotFound, value) : byId;
} }
var matches = value.Length == 0 ? [] : await devRequestRepository.GetListAsync(r => r.Title == value); var matches = value.Length == 0 ? [] : await devRequestRepository.GetListAsync(r => r.Title == value);
return matches.Count switch return matches.Count switch
{ {
0 => throw new UserFriendlyException(L[McpErrorCodes.DevRequestNotFound, value]), 0 => Problem(McpErrorCodes.DevRequestNotFound, value),
1 => matches[0], 1 => matches[0],
_ => throw new UserFriendlyException(L[McpErrorCodes.DevRequestAmbiguous, value, string.Join(", ", matches.Select(r => r.Id))]) _ => Problem(McpErrorCodes.DevRequestAmbiguous, value, string.Join(", ", matches.Select(r => r.Id)))
}; };
} }
@ -843,13 +969,16 @@ public class McpDevelopAppService(
private static McpVerifyCheckDto Failed(string name, string message) private static McpVerifyCheckDto Failed(string name, string message)
=> new() { Name = name, Passed = false, Message = message }; => new() { Name = name, Passed = false, Message = message };
public async Task<List<McpComponentSummaryDto>> GetComponentsAsync(string? search) public async Task<McpOutcome<List<McpComponentSummaryDto>>> GetComponentsAsync(string? search)
{ {
await EnsureToolAllowedAsync(McpToolNames.ListComponents); if (await EnsureToolAllowedAsync(McpToolNames.ListComponents) is { Problem: { } problem })
{
return problem;
}
var components = await GetAllComponentsAsync(); var components = await GetAllComponentsAsync();
return [.. components return components
.Where(component => string.IsNullOrWhiteSpace(search) .Where(component => string.IsNullOrWhiteSpace(search)
|| Contains(component.Name, search) || Contains(component.Name, search)
|| Contains(component.RoutePath, search) || Contains(component.RoutePath, search)
@ -862,24 +991,31 @@ public class McpDevelopAppService(
Description = component.Description, Description = component.Description,
IsActive = component.IsActive, IsActive = component.IsActive,
IsVisualDesigner = IsVisualDesignerDocument(component.Props) IsVisualDesigner = IsVisualDesignerDocument(component.Props)
})]; })
.ToList();
} }
public async Task<CustomComponentDto?> GetComponentAsync(string name) public async Task<McpOutcome<CustomComponentDto?>> GetComponentAsync(string name)
{ {
await EnsureToolAllowedAsync(McpToolNames.GetComponent); if (await EnsureToolAllowedAsync(McpToolNames.GetComponent) is { Problem: { } problem })
{
return problem;
}
return await FindComponentAsync(name); return await FindComponentAsync(name);
} }
public async Task<CustomComponentDto> SaveComponentAsync(McpComponentSaveRequestDto input) public async Task<McpOutcome<CustomComponentDto>> SaveComponentAsync(McpComponentSaveRequestDto input)
{ {
Check.NotNull(input, nameof(input)); Check.NotNull(input, nameof(input));
await EnsureToolAllowedAsync(McpToolNames.SaveComponent); if (await EnsureToolAllowedAsync(McpToolNames.SaveComponent) is { Problem: { } problem })
{
return problem;
}
if (string.IsNullOrWhiteSpace(input.Name)) if (string.IsNullOrWhiteSpace(input.Name))
{ {
throw new UserFriendlyException(L[McpErrorCodes.ComponentNameRequired]); return Problem(McpErrorCodes.ComponentNameRequired);
} }
var existing = await FindComponentAsync(input.Name); var existing = await FindComponentAsync(input.Name);
@ -889,14 +1025,14 @@ public class McpDevelopAppService(
// kayitta sessizce ezilirdi. Belge MCP'de uretilemedigi icin bu bilesenler tasarimcida kalir. // kayitta sessizce ezilirdi. Belge MCP'de uretilemedigi icin bu bilesenler tasarimcida kalir.
if (IsVisualDesignerDocument(props)) if (IsVisualDesignerDocument(props))
{ {
throw new UserFriendlyException(L[McpErrorCodes.VisualDesignerComponent, input.Name]); return Problem(McpErrorCodes.VisualDesignerComponent, input.Name);
} }
if (existing is null) if (existing is null)
{ {
if (string.IsNullOrWhiteSpace(input.Code)) if (string.IsNullOrWhiteSpace(input.Code))
{ {
throw new UserFriendlyException(L[McpErrorCodes.ComponentCodeRequired]); return Problem(McpErrorCodes.ComponentCodeRequired);
} }
var created = await componentAppService.CreateAsync(new CreateUpdateCustomComponentDto var created = await componentAppService.CreateAsync(new CreateUpdateCustomComponentDto
@ -929,15 +1065,23 @@ public class McpDevelopAppService(
return updated; return updated;
} }
public async Task DeleteComponentAsync(string name) public async Task<McpProblem?> DeleteComponentAsync(string name)
{ {
await EnsureToolAllowedAsync(McpToolNames.DeleteComponent); if (await EnsureToolAllowedAsync(McpToolNames.DeleteComponent) is { Problem: { } problem })
{
return problem;
}
var component = await FindComponentAsync(name) var component = await FindComponentAsync(name);
?? throw new UserFriendlyException(L[McpErrorCodes.ComponentNotFound, name]); if (component is null)
{
return Problem(McpErrorCodes.ComponentNotFound, name);
}
await componentAppService.DeleteAsync(component.Id); await componentAppService.DeleteAsync(component.Id);
await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeleteComponent, component.Name); await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeleteComponent, component.Name);
return null;
} }
/// <summary> /// <summary>
@ -946,18 +1090,20 @@ public class McpDevelopAppService(
/// ayni anlama gelir. ListForm/Fields bolumleri ekrana katman olarak uygulanir /// ayni anlama gelir. ListForm/Fields bolumleri ekrana katman olarak uygulanir
/// (<see cref="WizardSeedFileDto.FromDeployJson"/>). /// (<see cref="WizardSeedFileDto.FromDeployJson"/>).
/// </summary> /// </summary>
private ListFormWizardDto ParseWizard(string? json) private McpOutcome<ListFormWizardDto> ParseWizard(string? json)
{ {
ListFormWizardDto? wizard;
try try
{ {
return (string.IsNullOrWhiteSpace(json) ? null : WizardSeedFileDto.FromDeployJson(json)) wizard = string.IsNullOrWhiteSpace(json) ? null : WizardSeedFileDto.FromDeployJson(json);
?? throw new UserFriendlyException(L[McpErrorCodes.InvalidWizard]);
} }
catch (JsonException ex) catch (JsonException ex)
{ {
// Genel mesajla model hangi degerin bozuk oldugunu bilemez ve ayni JSON'u tekrar gonderir. // Genel mesajla model hangi degerin bozuk oldugunu bilemez ve ayni JSON'u tekrar gonderir.
throw new UserFriendlyException(L[McpErrorCodes.InvalidWizardValue, ex.Path ?? "$", ex.Message]); return Problem(McpErrorCodes.InvalidWizardValue, ex.Path ?? "$", ex.Message);
} }
return wizard is null ? Problem(McpErrorCodes.InvalidWizard) : wizard;
} }
private async Task<IReadOnlyList<CustomComponentDto>> GetAllComponentsAsync() private async Task<IReadOnlyList<CustomComponentDto>> GetAllComponentsAsync()

View file

@ -46,27 +46,39 @@ public partial class McpQueryAppService(
private const string SqlPermission = PlatformConsts.AppCodes.SqlQueryManagers.Default; private const string SqlPermission = PlatformConsts.AppCodes.SqlQueryManagers.Default;
private const string CustomEndpointPermission = PlatformConsts.AppCodes.DeveloperKits.Get; private const string CustomEndpointPermission = PlatformConsts.AppCodes.DeveloperKits.Get;
public async Task<McpListQueryResultDto> QueryListAsync(McpListQueryRequestDto input) public async Task<McpOutcome<McpListQueryResultDto>> QueryListAsync(McpListQueryRequestDto input)
{ {
Check.NotNull(input, nameof(input)); Check.NotNull(input, nameof(input));
var config = await EnsureToolAllowedAsync(McpToolNames.QueryList); if (!(await EnsureToolAllowedAsync(McpToolNames.QueryList)).TryGetValue(out var config, out var problem))
{
return problem;
}
var take = ResolveMaxRows(config, input.Take); var take = ResolveMaxRows(config, input.Take);
var permission = await accessFilter.GetPermissionNameAsync(input.ListFormCode, AuthorizationTypeEnum.Read); var permission = await accessFilter.GetPermissionNameAsync(input.ListFormCode, AuthorizationTypeEnum.Read);
if (permission is not null) if (permission is not null && await CheckPermissionAsync(McpToolNames.QueryList, permission) is { } denied)
{ {
await AuthorizationService.CheckAsync(permission); return denied;
}
if (!ToLoadOption(nameof(input.Sort), input.Sort, McpLoadOptions.ToSort).TryGetValue(out var sort, out problem) ||
!ToLoadOption(nameof(input.Group), input.Group, McpLoadOptions.ToGroup).TryGetValue(out var group, out problem) ||
!ToLoadOption(nameof(input.GroupSummary), input.GroupSummary, McpLoadOptions.ToSummary).TryGetValue(out var groupSummary, out problem) ||
!ToLoadOption(nameof(input.TotalSummary), input.TotalSummary, McpLoadOptions.ToSummary).TryGetValue(out var totalSummary, out problem))
{
return problem;
} }
var result = await listFormSelectAppService.GetSelectAsync(new SelectRequestDto var result = await listFormSelectAppService.GetSelectAsync(new SelectRequestDto
{ {
ListFormCode = input.ListFormCode, ListFormCode = input.ListFormCode,
Filter = NullIfEmpty(input.Filter), Filter = NullIfEmpty(input.Filter),
Sort = ToLoadOption(nameof(input.Sort), input.Sort, McpLoadOptions.ToSort), Sort = sort,
Group = ToLoadOption(nameof(input.Group), input.Group, McpLoadOptions.ToGroup), Group = group,
GroupSummary = ToLoadOption(nameof(input.GroupSummary), input.GroupSummary, McpLoadOptions.ToSummary), GroupSummary = groupSummary,
TotalSummary = ToLoadOption(nameof(input.TotalSummary), input.TotalSummary, McpLoadOptions.ToSummary), TotalSummary = totalSummary,
Skip = Math.Max(input.Skip, 0), Skip = Math.Max(input.Skip, 0),
Take = take, Take = take,
RequireTotalCount = true, RequireTotalCount = true,
@ -84,7 +96,7 @@ public partial class McpQueryAppService(
} }
/// <summary>Bozuk girdi SQL hatasina donusmeden, hangi parametrede oldugu soylenerek reddedilir.</summary> /// <summary>Bozuk girdi SQL hatasina donusmeden, hangi parametrede oldugu soylenerek reddedilir.</summary>
private string? ToLoadOption(string parameter, string? value, Func<string?, string?> convert) private McpOutcome<string?> ToLoadOption(string parameter, string? value, Func<string?, string?> convert)
{ {
try try
{ {
@ -92,21 +104,31 @@ public partial class McpQueryAppService(
} }
catch (Exception ex) when (ex is JsonException or InvalidOperationException) catch (Exception ex) when (ex is JsonException or InvalidOperationException)
{ {
throw new UserFriendlyException(L[McpErrorCodes.InvalidLoadOption, JsonNamingPolicy.CamelCase.ConvertName(parameter)]); return Problem(McpErrorCodes.InvalidLoadOption, JsonNamingPolicy.CamelCase.ConvertName(parameter));
} }
} }
public async Task<McpQueryResultDto> RunSelectAsync(McpSelectRequestDto input) public async Task<McpOutcome<McpQueryResultDto>> RunSelectAsync(McpSelectRequestDto input)
{ {
Check.NotNull(input, nameof(input)); Check.NotNull(input, nameof(input));
var config = await EnsureToolAllowedAsync(McpToolNames.RunSelect); if (!(await EnsureToolAllowedAsync(McpToolNames.RunSelect)).TryGetValue(out var config, out var problem))
{
return problem;
}
var sql = (input.Sql ?? string.Empty).Trim().TrimEnd(';').Trim(); var sql = (input.Sql ?? string.Empty).Trim().TrimEnd(';').Trim();
sqlGuard.EnsureReadOnlyStatement(sql); if (sqlGuard.CheckReadOnlyStatement(sql) is { } invalid)
{
return invalid;
}
if (!sqlGuard.NormalizeParameters(input.Parameters).TryGetValue(out var parameters, out problem))
{
return problem;
}
var maxRows = ResolveMaxRows(config, input.MaxRows); var maxRows = ResolveMaxRows(config, input.MaxRows);
var parameters = sqlGuard.NormalizeParameters(input.Parameters);
var context = await queryExecutor.ResolveDataSourceAsync(); var context = await queryExecutor.ResolveDataSourceAsync();
var limitedSql = sqlGuard.ApplyRowLimit(sql, maxRows, context.DataSourceType); var limitedSql = sqlGuard.ApplyRowLimit(sql, maxRows, context.DataSourceType);
@ -121,12 +143,12 @@ public partial class McpQueryAppService(
{ {
var pending = IsUnknownObjectError(ex) ? DescribePendingTables(sql) : null; var pending = IsUnknownObjectError(ex) ? DescribePendingTables(sql) : null;
if (pending is not null) if (pending is not null)
throw new UserFriendlyException(L[McpErrorCodes.SelectTableNotInstalled, ex.Message, pending]); return Problem(McpErrorCodes.SelectTableNotInstalled, ex.Message, pending);
var columns = IsUnknownColumnError(ex) ? await DescribeTableColumnsAsync(context, sql) : null; var columns = IsUnknownColumnError(ex) ? await DescribeTableColumnsAsync(context, sql) : null;
throw new UserFriendlyException(columns is null return columns is null
? L[McpErrorCodes.SelectFailed, ex.Message] ? Problem(McpErrorCodes.SelectFailed, ex.Message)
: L[McpErrorCodes.SelectFailedColumns, ex.Message, columns]); : Problem(McpErrorCodes.SelectFailedColumns, ex.Message, columns);
} }
} }
@ -208,9 +230,13 @@ public partial class McpQueryAppService(
[GeneratedRegex(@"\b(?:FROM|JOIN)\s+(?:[\[""]?\w+[\]""]?\.)?[\[""]?(?<table>\w+)[\]""]?", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)] [GeneratedRegex(@"\b(?:FROM|JOIN)\s+(?:[\[""]?\w+[\]""]?\.)?[\[""]?(?<table>\w+)[\]""]?", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
private static partial Regex TableReferenceRegex(); private static partial Regex TableReferenceRegex();
public async Task<List<McpSavedQueryDto>> GetSavedQueriesAsync(string? search = null) public async Task<McpOutcome<List<McpSavedQueryDto>>> GetSavedQueriesAsync(string? search = null)
{ {
await EnsureToolAllowedAsync(McpToolNames.ListQueries); if (await EnsureToolAllowedAsync(McpToolNames.ListQueries) is { Problem: { } problem })
{
return problem;
}
var results = new List<McpSavedQueryDto>(); var results = new List<McpSavedQueryDto>();
if (await AuthorizationService.IsGrantedAsync(SqlPermission)) if (await AuthorizationService.IsGrantedAsync(SqlPermission))
@ -230,9 +256,10 @@ public partial class McpQueryAppService(
|| (item.Description?.Contains(search, StringComparison.OrdinalIgnoreCase) ?? false))]; || (item.Description?.Contains(search, StringComparison.OrdinalIgnoreCase) ?? false))];
} }
return [.. results return results
.OrderBy(item => item.Kind, StringComparer.Ordinal) .OrderBy(item => item.Kind, StringComparer.Ordinal)
.ThenBy(item => item.Name, StringComparer.OrdinalIgnoreCase)]; .ThenBy(item => item.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
} }
private async Task<List<McpSavedQueryDto>> GetDatabaseObjectsAsync() private async Task<List<McpSavedQueryDto>> GetDatabaseObjectsAsync()
@ -266,44 +293,58 @@ public partial class McpQueryAppService(
return results; return results;
} }
public async Task<McpQueryResultDto> RunSavedQueryAsync(McpSavedQueryRequestDto input) public async Task<McpOutcome<McpQueryResultDto>> RunSavedQueryAsync(McpSavedQueryRequestDto input)
{ {
Check.NotNull(input, nameof(input)); Check.NotNull(input, nameof(input));
var config = await EnsureToolAllowedAsync(McpToolNames.RunQuery); if (!(await EnsureToolAllowedAsync(McpToolNames.RunQuery)).TryGetValue(out var config, out var problem))
if (IsKind(input.Kind, McpConsts.SavedQueryKinds.CustomEndpoint))
{ {
await AuthorizationService.CheckAsync(CustomEndpointPermission); return problem;
return await RunCustomEndpointAsync(input, config);
} }
await AuthorizationService.CheckAsync(SqlPermission); var isCustomEndpoint = IsKind(input.Kind, McpConsts.SavedQueryKinds.CustomEndpoint);
if (await CheckPermissionAsync(McpToolNames.RunQuery, isCustomEndpoint ? CustomEndpointPermission : SqlPermission) is { } denied)
{
return denied;
}
if (!sqlGuard.NormalizeParameters(input.Parameters).TryGetValue(out var parameters, out problem))
{
return problem;
}
var maxRows = ResolveMaxRows(config, input.MaxRows); var maxRows = ResolveMaxRows(config, input.MaxRows);
var parameters = sqlGuard.NormalizeParameters(input.Parameters); if (isCustomEndpoint)
{
return await RunCustomEndpointAsync(input, parameters, maxRows);
}
var context = await queryExecutor.ResolveDataSourceAsync(); var context = await queryExecutor.ResolveDataSourceAsync();
var schemaName = string.IsNullOrWhiteSpace(input.SchemaName) var schemaName = string.IsNullOrWhiteSpace(input.SchemaName)
? sqlGuard.DefaultSchemaName(context.DataSourceType) ? sqlGuard.DefaultSchemaName(context.DataSourceType)
: input.SchemaName; : input.SchemaName;
var qualifiedName = sqlGuard.QuoteQualifiedName(schemaName, input.Name, context.DataSourceType); if (!sqlGuard.QuoteQualifiedName(schemaName, input.Name, context.DataSourceType).TryGetValue(out var qualifiedName, out problem) ||
var sql = BuildSavedQuerySql(input.Kind, qualifiedName, parameters, maxRows, context.DataSourceType); !BuildSavedQuerySql(input.Kind, qualifiedName, parameters, maxRows, context.DataSourceType).TryGetValue(out var sql, out problem))
{
return problem;
}
return ToResult(await queryExecutor.ExecuteAsync(context, sql, parameters, maxRows)); return ToResult(await queryExecutor.ExecuteAsync(context, sql, parameters, maxRows));
} }
private async Task<McpQueryResultDto> RunCustomEndpointAsync( private async Task<McpOutcome<McpQueryResultDto>> RunCustomEndpointAsync(
McpSavedQueryRequestDto input, McpSavedQueryRequestDto input,
McpServerConfigDto config) Dictionary<string, object> parameters,
int maxRows)
{ {
var endpoints = await GetCallableEndpointEntitiesAsync(); var endpoints = await GetCallableEndpointEntitiesAsync();
var endpoint = endpoints.Find(candidate => string.Equals(candidate.Name, input.Name, StringComparison.OrdinalIgnoreCase)) var endpoint = endpoints.Find(candidate => string.Equals(candidate.Name, input.Name, StringComparison.OrdinalIgnoreCase));
?? throw new UserFriendlyException(L[McpErrorCodes.CustomEndpointNotFound, input.Name]); if (endpoint is null)
{
var maxRows = ResolveMaxRows(config, input.MaxRows); return Problem(McpErrorCodes.CustomEndpointNotFound, input.Name);
var parameters = sqlGuard.NormalizeParameters(input.Parameters); }
// Eksik parametre Dapper tarafinda belirsiz bir hataya donusmesin diye burada yakalanir. // Eksik parametre Dapper tarafinda belirsiz bir hataya donusmesin diye burada yakalanir.
var missing = endpoint.Parameters var missing = endpoint.Parameters
@ -313,7 +354,7 @@ public partial class McpQueryAppService(
if (missing.Count > 0) if (missing.Count > 0)
{ {
throw new UserFriendlyException(L[McpErrorCodes.MissingParameters, string.Join(", ", missing)]); return Problem(McpErrorCodes.MissingParameters, string.Join(", ", missing));
} }
var context = await queryExecutor.ResolveDataSourceAsync(endpoint.DataSourceCode); var context = await queryExecutor.ResolveDataSourceAsync(endpoint.DataSourceCode);
@ -349,7 +390,7 @@ public partial class McpQueryAppService(
|| (permission.ResourceType == "Role" && CurrentUser.Roles.Contains(permission.ResourceId))))]; || (permission.ResourceType == "Role" && CurrentUser.Roles.Contains(permission.ResourceId))))];
} }
private string BuildSavedQuerySql( private McpOutcome<string> BuildSavedQuerySql(
string? kind, string? kind,
string qualifiedName, string qualifiedName,
Dictionary<string, object> parameters, Dictionary<string, object> parameters,
@ -381,7 +422,7 @@ public partial class McpQueryAppService(
: $"EXEC {qualifiedName} {string.Join(", ", parameters.Keys.Select(name => $"@{name} = @{name}"))}".TrimEnd(); : $"EXEC {qualifiedName} {string.Join(", ", parameters.Keys.Select(name => $"@{name} = @{name}"))}".TrimEnd();
} }
throw new UserFriendlyException(L[McpErrorCodes.UnsupportedObjectKind, kind ?? string.Empty]); return Problem(McpErrorCodes.UnsupportedObjectKind, kind ?? string.Empty);
} }
private static string? NullIfEmpty(string? value) private static string? NullIfEmpty(string? value)

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.Json; using System.Text.Json;
@ -41,10 +41,12 @@ public partial class McpReportAppService(
[GeneratedRegex(@"\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}")] [GeneratedRegex(@"\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}")]
private static partial Regex PlaceholderRegex(); private static partial Regex PlaceholderRegex();
public async Task<McpReportCatalogDto> GetReportTemplatesAsync(string? search) public async Task<McpOutcome<McpReportCatalogDto>> GetReportTemplatesAsync(string? search)
{ {
await EnsureToolAllowedAsync(McpToolNames.ListReportTemplates); if (await EnsureCanReadAsync(McpToolNames.ListReportTemplates) is { } problem)
await EnsureCanReadAsync(); {
return problem;
}
var categories = await categoryRepository.GetListAsync(); var categories = await categoryRepository.GetListAsync();
var categoryNames = categories.ToDictionary(x => x.Id, x => x.Name); var categoryNames = categories.ToDictionary(x => x.Id, x => x.Name);
@ -70,25 +72,35 @@ public partial class McpReportAppService(
}; };
} }
public async Task<McpReportTemplateDto> GetReportTemplateAsync(string template) public async Task<McpOutcome<McpReportTemplateDto>> GetReportTemplateAsync(string template)
{ {
await EnsureToolAllowedAsync(McpToolNames.GetReportTemplate); if (await EnsureCanReadAsync(McpToolNames.GetReportTemplate) is { } problem)
await EnsureCanReadAsync(); {
return problem;
}
var entity = await FindTemplateAsync(template) var entity = await FindTemplateAsync(template);
?? throw new UserFriendlyException(L[McpErrorCodes.ReportTemplateNotFound, template]); if (entity is null)
{
return Problem(McpErrorCodes.ReportTemplateNotFound, template);
}
return await ToDtoAsync(entity, null, null); return await ToDtoAsync(entity, null, null);
} }
public async Task<McpReportCategoryDto> SaveReportCategoryAsync(McpReportCategorySaveRequestDto input) public async Task<McpOutcome<McpReportCategoryDto>> SaveReportCategoryAsync(McpReportCategorySaveRequestDto input)
{ {
Check.NotNull(input, nameof(input)); Check.NotNull(input, nameof(input));
await EnsureToolAllowedAsync(McpToolNames.SaveReportCategory); const string toolName = McpToolNames.SaveReportCategory;
if (await EnsureToolAllowedAsync(toolName) is { Problem: { } problem })
{
return problem;
}
if (string.IsNullOrWhiteSpace(input.Name)) if (string.IsNullOrWhiteSpace(input.Name))
{ {
throw new UserFriendlyException(L[McpErrorCodes.ReportNameRequired]); return Problem(McpErrorCodes.ReportNameRequired);
} }
var name = input.Name.Trim(); var name = input.Name.Trim();
@ -100,24 +112,33 @@ public partial class McpReportAppService(
["Icon"] = input.Icon ?? existing?.Icon ["Icon"] = input.Icon ?? existing?.Icon
}; };
var id = existing is null var saved = existing is null
? await InsertAsync(CategoriesListFormCode, values) ? await InsertAsync(toolName, CategoriesListFormCode, values)
: await UpdateAsync(CategoriesListFormCode, existing.Id, values); : await UpdateAsync(toolName, CategoriesListFormCode, existing.Id, values);
if (!saved.TryGetValue(out var id, out var rejected))
{
return rejected;
}
await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveReportCategory, name); await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveReportCategory, name);
var saved = await categoryRepository.FirstOrDefaultAsync(x => x.Id == id); var category = await categoryRepository.FirstOrDefaultAsync(x => x.Id == id);
return saved is null ? new McpReportCategoryDto { Id = id, Name = name } : ToDto(saved); return category is null ? new McpReportCategoryDto { Id = id, Name = name } : ToDto(category);
} }
public async Task<McpReportTemplateDto> SaveReportTemplateAsync(McpReportTemplateSaveRequestDto input) public async Task<McpOutcome<McpReportTemplateDto>> SaveReportTemplateAsync(McpReportTemplateSaveRequestDto input)
{ {
Check.NotNull(input, nameof(input)); Check.NotNull(input, nameof(input));
await EnsureToolAllowedAsync(McpToolNames.SaveReportTemplate); const string toolName = McpToolNames.SaveReportTemplate;
if (await EnsureToolAllowedAsync(toolName) is { Problem: { } problem })
{
return problem;
}
if (string.IsNullOrWhiteSpace(input.Name)) if (string.IsNullOrWhiteSpace(input.Name))
{ {
throw new UserFriendlyException(L[McpErrorCodes.ReportNameRequired]); return Problem(McpErrorCodes.ReportNameRequired);
} }
var name = input.Name.Trim(); var name = input.Name.Trim();
@ -125,22 +146,32 @@ public partial class McpReportAppService(
var htmlContent = string.IsNullOrWhiteSpace(input.HtmlContent) ? existing?.HtmlContent : input.HtmlContent; var htmlContent = string.IsNullOrWhiteSpace(input.HtmlContent) ? existing?.HtmlContent : input.HtmlContent;
if (string.IsNullOrWhiteSpace(htmlContent)) if (string.IsNullOrWhiteSpace(htmlContent))
{ {
throw new UserFriendlyException(L[McpErrorCodes.ReportHtmlRequired]); return Problem(McpErrorCodes.ReportHtmlRequired);
}
var categoryId = existing?.CategoryId;
if (!string.IsNullOrWhiteSpace(input.Category))
{
categoryId = (await FindCategoryAsync(input.Category))?.Id;
if (categoryId is null)
{
return Problem(McpErrorCodes.ReportCategoryNotFound, input.Category);
}
} }
var categoryId = string.IsNullOrWhiteSpace(input.Category)
? existing?.CategoryId
: (await FindCategoryAsync(input.Category))?.Id
?? throw new UserFriendlyException(L[McpErrorCodes.ReportCategoryNotFound, input.Category]);
if (categoryId is null) if (categoryId is null)
{ {
throw new UserFriendlyException(L[McpErrorCodes.ReportCategoryRequired]); return Problem(McpErrorCodes.ReportCategoryRequired);
} }
var listForm = await FindListFormAsync(input.ListFormCode); if (!(await FindListFormAsync(input.ListFormCode)).TryGetValue(out var listForm, out var notFound))
if (listForm is not null)
{ {
await EnsurePlaceholdersExistAsync(listForm.ListFormCode, htmlContent); return notFound;
}
if (listForm is not null && await CheckPlaceholdersExistAsync(listForm.ListFormCode, htmlContent) is { } unknown)
{
return unknown;
} }
var values = new Dictionary<string, object?> var values = new Dictionary<string, object?>
@ -152,44 +183,74 @@ public partial class McpReportAppService(
["Status"] = input.Status ?? existing?.Status ?? ActiveStatus ["Status"] = input.Status ?? existing?.Status ?? ActiveStatus
}; };
var id = existing is null var saved = existing is null
? await InsertAsync(TemplatesListFormCode, values) ? await InsertAsync(toolName, TemplatesListFormCode, values)
: await UpdateAsync(TemplatesListFormCode, existing.Id, values); : await UpdateAsync(toolName, TemplatesListFormCode, existing.Id, values);
if (!saved.TryGetValue(out var id, out var rejected))
{
return rejected;
}
await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveReportTemplate, name); await timesheetRecorder.RecordAsync(McpTimesheetTexts.SaveReportTemplate, name);
var saved = await templateRepository.FirstOrDefaultAsync(x => x.Id == id) var template = await templateRepository.FirstOrDefaultAsync(x => x.Id == id);
?? throw new UserFriendlyException(L[McpErrorCodes.ReportTemplateNotFound, name]); if (template is null)
{
return Problem(McpErrorCodes.ReportTemplateNotFound, name);
}
return await ToDtoAsync(saved, listForm, input.ButtonText); return await ToDtoAsync(template, listForm, input.ButtonText);
} }
public async Task DeleteReportTemplateAsync(string template) public async Task<McpProblem?> DeleteReportTemplateAsync(string template)
{ {
await EnsureToolAllowedAsync(McpToolNames.DeleteReportTemplate); const string toolName = McpToolNames.DeleteReportTemplate;
var entity = await FindTemplateAsync(template) if (await EnsureToolAllowedAsync(toolName) is { Problem: { } problem })
?? throw new UserFriendlyException(L[McpErrorCodes.ReportTemplateNotFound, template]); {
return problem;
}
await listFormDataAppService.PostDeleteAsync(await BuildRequestAsync( var entity = await FindTemplateAsync(template);
TemplatesListFormCode, AuthorizationTypeEnum.Delete, null, entity.Id)); if (entity is null)
{
return Problem(McpErrorCodes.ReportTemplateNotFound, template);
}
if (!(await BuildRequestAsync(toolName, TemplatesListFormCode, AuthorizationTypeEnum.Delete, null, entity.Id)).TryGetValue(out var request, out var denied))
{
return denied;
}
await listFormDataAppService.PostDeleteAsync(request);
await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeleteReportTemplate, entity.Name); await timesheetRecorder.RecordAsync(McpTimesheetTexts.DeleteReportTemplate, entity.Name);
return null;
} }
/// <summary> /// <summary>
/// Okuma depodan yapildigi icin ekranin okuma yetkisi burada istenir; MCP kullanicinin /// Okuma depodan yapildigi icin ekranin okuma yetkisi burada istenir; MCP kullanicinin
/// uygulamada goremedigi sablonu gostermez. /// uygulamada goremedigi sablonu gostermez.
/// </summary> /// </summary>
private async Task EnsureCanReadAsync() private async Task<McpProblem?> EnsureCanReadAsync(string toolName)
{ {
if (await EnsureToolAllowedAsync(toolName) is { Problem: { } problem })
{
return problem;
}
var permission = await accessFilter.GetPermissionNameAsync(TemplatesListFormCode, AuthorizationTypeEnum.Read); var permission = await accessFilter.GetPermissionNameAsync(TemplatesListFormCode, AuthorizationTypeEnum.Read);
await AuthorizationService.CheckAsync(permission ?? TemplatesListFormCode); return await CheckPermissionAsync(toolName, permission ?? TemplatesListFormCode);
} }
private async Task<Guid> InsertAsync(string listFormCode, Dictionary<string, object?> values) private async Task<McpOutcome<Guid>> InsertAsync(string toolName, string listFormCode, Dictionary<string, object?> values)
{ {
var key = await listFormDataAppService.PostInsertAsync(await BuildRequestAsync( if (!(await BuildRequestAsync(toolName, listFormCode, AuthorizationTypeEnum.Create, values, null)).TryGetValue(out var request, out var problem))
listFormCode, AuthorizationTypeEnum.Create, values, null)); {
return problem;
}
var key = await listFormDataAppService.PostInsertAsync(request);
// Insert ucu her veri kaynaginda anahtari dondurmez; bu durumda tekil ad uzerinden bulunur. // Insert ucu her veri kaynaginda anahtari dondurmez; bu durumda tekil ad uzerinden bulunur.
if (Guid.TryParse(((object?)key)?.ToString(), out var id)) if (Guid.TryParse(((object?)key)?.ToString(), out var id))
@ -203,10 +264,14 @@ public partial class McpReportAppService(
: (await templateRepository.FirstAsync(x => x.Name == name)).Id; : (await templateRepository.FirstAsync(x => x.Name == name)).Id;
} }
private async Task<Guid> UpdateAsync(string listFormCode, Guid id, Dictionary<string, object?> values) private async Task<McpOutcome<Guid>> UpdateAsync(string toolName, string listFormCode, Guid id, Dictionary<string, object?> values)
{ {
await listFormDataAppService.PostUpdateAsync(await BuildRequestAsync( if (!(await BuildRequestAsync(toolName, listFormCode, AuthorizationTypeEnum.Update, values, id)).TryGetValue(out var request, out var problem))
listFormCode, AuthorizationTypeEnum.Update, values, id)); {
return problem;
}
await listFormDataAppService.PostUpdateAsync(request);
return id; return id;
} }
@ -214,16 +279,17 @@ public partial class McpReportAppService(
/// Ekranin kendi kontrolu reddi adsiz bir hatayla bildirir; yetki burada adiyla istenir ki /// Ekranin kendi kontrolu reddi adsiz bir hatayla bildirir; yetki burada adiyla istenir ki
/// istemci hangi yetkinin eksik oldugunu gorsun. /// istemci hangi yetkinin eksik oldugunu gorsun.
/// </summary> /// </summary>
private async Task<DataRequestDto> BuildRequestAsync( private async Task<McpOutcome<DataRequestDto>> BuildRequestAsync(
string toolName,
string listFormCode, string listFormCode,
AuthorizationTypeEnum operation, AuthorizationTypeEnum operation,
Dictionary<string, object?>? values, Dictionary<string, object?>? values,
Guid? key) Guid? key)
{ {
var permission = await accessFilter.GetPermissionNameAsync(listFormCode, operation); var permission = await accessFilter.GetPermissionNameAsync(listFormCode, operation);
if (permission is not null) if (permission is not null && await CheckPermissionAsync(toolName, permission) is { } denied)
{ {
await AuthorizationService.CheckAsync(permission); return denied;
} }
return new DataRequestDto return new DataRequestDto
@ -255,32 +321,34 @@ public partial class McpReportAppService(
: await categoryRepository.FirstOrDefaultAsync(x => x.Name == value); : await categoryRepository.FirstOrDefaultAsync(x => x.Name == value);
} }
private async Task<ListForm?> FindListFormAsync(string? listFormCode) /// <summary>Kod verilmediyse null; verilen kodun ekrani yoksa sorun.</summary>
private async Task<McpOutcome<ListForm?>> FindListFormAsync(string? listFormCode)
{ {
if (string.IsNullOrWhiteSpace(listFormCode)) if (string.IsNullOrWhiteSpace(listFormCode))
{ {
return null; return default(ListForm);
} }
var code = listFormCode.Trim(); var code = listFormCode.Trim();
return await listFormRepository.FirstOrDefaultAsync(x => x.ListFormCode == code) var listForm = await listFormRepository.FirstOrDefaultAsync(x => x.ListFormCode == code);
?? throw new UserFriendlyException(L[McpErrorCodes.ReportListFormNotFound, code]);
return listForm is null ? Problem(McpErrorCodes.ReportListFormNotFound, code) : listForm;
} }
/// <summary> /// <summary>
/// Bilinmeyen yer tutucu raporda oldugu gibi basilir; sertifikada "{{AdSoyad}}" gorunmesin /// Bilinmeyen yer tutucu raporda oldugu gibi basilir; sertifikada "{{AdSoyad}}" gorunmesin
/// diye kayitta reddedilir. /// diye kayitta reddedilir.
/// </summary> /// </summary>
private async Task EnsurePlaceholdersExistAsync(string listFormCode, string htmlContent) private async Task<McpProblem?> CheckPlaceholdersExistAsync(string listFormCode, string htmlContent)
{ {
var fieldNames = (await listFormFieldRepository.GetListAsync(x => x.ListFormCode == listFormCode)) var fieldNames = (await listFormFieldRepository.GetListAsync(x => x.ListFormCode == listFormCode))
.Select(x => x.FieldName) .Select(x => x.FieldName)
.ToHashSet(StringComparer.OrdinalIgnoreCase); .ToHashSet(StringComparer.OrdinalIgnoreCase);
var unknown = GetPlaceholders(htmlContent).Where(x => !fieldNames.Contains(x)).ToList(); var unknown = GetPlaceholders(htmlContent).Where(x => !fieldNames.Contains(x)).ToList();
if (unknown.Count > 0)
{ return unknown.Count == 0
throw new UserFriendlyException(L[McpErrorCodes.ReportPlaceholdersUnknown, listFormCode, string.Join(", ", unknown)]); ? null
} : Problem(McpErrorCodes.ReportPlaceholdersUnknown, listFormCode, string.Join(", ", unknown));
} }
private static List<string> GetPlaceholders(string htmlContent) private static List<string> GetPlaceholders(string htmlContent)

View file

@ -93,9 +93,12 @@ public class McpSchemaAppService(
}; };
} }
public async Task<List<McpListSummaryDto>> GetListsAsync(string? search = null, int maxResults = 200) public async Task<McpOutcome<List<McpListSummaryDto>>> GetListsAsync(string? search = null, int maxResults = 200)
{ {
await EnsureToolAllowedAsync(McpToolNames.ListLists); if (await EnsureToolAllowedAsync(McpToolNames.ListLists) is { Problem: { } problem })
{
return problem;
}
var forms = await GetReadableListFormsAsync(); var forms = await GetReadableListFormsAsync();
var normalizedSearch = dictionary.Normalize(search); var normalizedSearch = dictionary.Normalize(search);
@ -103,16 +106,19 @@ public class McpSchemaAppService(
var matches = forms.Where(form => normalizedSearch.Length == 0 var matches = forms.Where(form => normalizedSearch.Length == 0
|| dictionary.Normalize(form.SearchText).Contains(normalizedSearch, StringComparison.Ordinal)); || dictionary.Normalize(form.SearchText).Contains(normalizedSearch, StringComparison.Ordinal));
return [.. matches.Take(NormalizeLimit(maxResults, 200)).Select(ToSummary)]; return matches.Take(NormalizeLimit(maxResults, 200)).Select(ToSummary).ToList();
} }
public async Task<McpListDetailDto?> GetListAsync(string listFormCode) public async Task<McpOutcome<McpListDetailDto?>> GetListAsync(string listFormCode)
{ {
await EnsureToolAllowedAsync(McpToolNames.DescribeList); if (await EnsureToolAllowedAsync(McpToolNames.DescribeList) is { Problem: { } problem })
{
return problem;
}
if (string.IsNullOrWhiteSpace(listFormCode)) if (string.IsNullOrWhiteSpace(listFormCode))
{ {
return null; return default(McpListDetailDto);
} }
var forms = await GetReadableListFormsAsync(); var forms = await GetReadableListFormsAsync();
@ -121,7 +127,7 @@ public class McpSchemaAppService(
if (form is null) if (form is null)
{ {
return null; return default(McpListDetailDto);
} }
var detail = new McpListDetailDto(); var detail = new McpListDetailDto();
@ -131,14 +137,17 @@ public class McpSchemaAppService(
return detail; return detail;
} }
public async Task<List<McpSchemaMatchDto>> SearchAsync(string term, int maxResults = 50) public async Task<McpOutcome<List<McpSchemaMatchDto>>> SearchAsync(string term, int maxResults = 50)
{ {
await EnsureToolAllowedAsync(McpToolNames.SearchSchema); if (await EnsureToolAllowedAsync(McpToolNames.SearchSchema) is { Problem: { } problem })
{
return problem;
}
var normalized = dictionary.Normalize(term); var normalized = dictionary.Normalize(term);
if (normalized.Length == 0) if (normalized.Length == 0)
{ {
return []; return new List<McpSchemaMatchDto>();
} }
var limit = NormalizeLimit(maxResults, 50); var limit = NormalizeLimit(maxResults, 50);

View file

@ -0,0 +1,54 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Localization;
namespace Sozsoft.Mcp;
/// <summary>
/// Aracin modelle konusarak cozulecek reddi: eksik ya da hatali girdi, durum kurali, eksik yetki.
/// Istisna olarak atilmaz — bu durum aracin beklenen cevabidir; atilan istisna debugger'i durdurur
/// ve SDK'da hata logu uretir. Istemciye hata sonucu (<c>IsError</c>) olarak gider, model mesaji
/// okuyup girdiyi duzeltir ve tekrar dener.
/// </summary>
public sealed record McpProblem(string Message)
{
public static McpProblem From(IStringLocalizer localizer, string name, params object[] arguments)
=> new(localizer[name, arguments]);
}
/// <summary>Aracin degeri ya da <see cref="McpProblem"/>'i; ikisi birden olmaz.</summary>
public readonly struct McpOutcome<T>
{
private McpOutcome(T? value, McpProblem? problem)
{
Value = value;
Problem = problem;
}
public T? Value { get; }
public McpProblem? Problem { get; }
public bool TryGetValue([MaybeNullWhen(false)] out T value, [NotNullWhen(false)] out McpProblem? problem)
{
value = Value!;
problem = Problem;
return problem is null;
}
public static implicit operator McpOutcome<T>(T value) => new(value, null);
public static implicit operator McpOutcome<T>(McpProblem problem) => new(default, problem);
}
/// <summary>Yetki reddi mesajinda yetkinin gorunen adi ve kodu; rol ekraninda aranabilsin diye kod her zaman yazilir.</summary>
public static class McpPermissionText
{
public static string Describe(IStringLocalizer localizer, string permissionName)
{
var displayName = localizer[permissionName];
return displayName.ResourceNotFound || displayName.Value == permissionName
? permissionName
: $"{displayName.Value} ({permissionName})";
}
}

View file

@ -4,7 +4,6 @@ using System.Text.RegularExpressions;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Sozsoft.Platform.Enums; using Sozsoft.Platform.Enums;
using Sozsoft.Platform.Localization; using Sozsoft.Platform.Localization;
using Volo.Abp;
using Volo.Abp.Domain.Services; using Volo.Abp.Domain.Services;
namespace Sozsoft.Mcp.Services; namespace Sozsoft.Mcp.Services;
@ -16,11 +15,11 @@ namespace Sozsoft.Mcp.Services;
/// </summary> /// </summary>
public interface IMcpSqlGuard public interface IMcpSqlGuard
{ {
/// <summary>Tek bir okuma ifadesi oldugunu dogrular; degilse anlasilir bir hata atar.</summary> /// <summary>Tek bir okuma ifadesi oldugunu dogrular; degilse nedenini dondurur.</summary>
void EnsureReadOnlyStatement(string sql); McpProblem? CheckReadOnlyStatement(string sql);
/// <summary>Sema/nesne adini dogrular ve saglayiciya gore tirnaklar.</summary> /// <summary>Sema/nesne adini dogrular ve saglayiciya gore tirnaklar.</summary>
string QuoteQualifiedName(string schemaName, string objectName, DataSourceTypeEnum dataSourceType); McpOutcome<string> QuoteQualifiedName(string schemaName, string objectName, DataSourceTypeEnum dataSourceType);
/// <summary>Saglayicinin varsayilan semasi.</summary> /// <summary>Saglayicinin varsayilan semasi.</summary>
string DefaultSchemaName(DataSourceTypeEnum dataSourceType); string DefaultSchemaName(DataSourceTypeEnum dataSourceType);
@ -35,7 +34,7 @@ public interface IMcpSqlGuard
/// Istemciden gelen parametre adlarini dogrular ve bastaki @ isaretini duserek Dapper'in /// Istemciden gelen parametre adlarini dogrular ve bastaki @ isaretini duserek Dapper'in
/// bekledigi bicime cevirir. /// bekledigi bicime cevirir.
/// </summary> /// </summary>
Dictionary<string, object> NormalizeParameters(IReadOnlyDictionary<string, object?>? parameters); McpOutcome<Dictionary<string, object>> NormalizeParameters(IReadOnlyDictionary<string, object?>? parameters);
} }
public class McpSqlGuard(IStringLocalizer<PlatformResource> localizer) : DomainService, IMcpSqlGuard public class McpSqlGuard(IStringLocalizer<PlatformResource> localizer) : DomainService, IMcpSqlGuard
@ -78,18 +77,18 @@ public class McpSqlGuard(IStringLocalizer<PlatformResource> localizer) : DomainS
@"\bLIMIT\s+\d+", @"\bLIMIT\s+\d+",
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
public void EnsureReadOnlyStatement(string sql) public McpProblem? CheckReadOnlyStatement(string sql)
{ {
if (string.IsNullOrWhiteSpace(sql)) if (string.IsNullOrWhiteSpace(sql))
{ {
throw new UserFriendlyException(localizer[McpErrorCodes.EmptyQuery]); return McpProblem.From(localizer, McpErrorCodes.EmptyQuery);
} }
var stripped = SqlStringLiteralRegex.Replace(SqlCommentRegex.Replace(sql, " "), "''"); var stripped = SqlStringLiteralRegex.Replace(SqlCommentRegex.Replace(sql, " "), "''");
if (stripped.Contains(';', StringComparison.Ordinal)) if (stripped.Contains(';', StringComparison.Ordinal))
{ {
throw new UserFriendlyException(localizer[McpErrorCodes.SingleStatementOnly]); return McpProblem.From(localizer, McpErrorCodes.SingleStatementOnly);
} }
var trimmed = stripped.TrimStart(); var trimmed = stripped.TrimStart();
@ -98,21 +97,25 @@ public class McpSqlGuard(IStringLocalizer<PlatformResource> localizer) : DomainS
if (!startsWithRead) if (!startsWithRead)
{ {
throw new UserFriendlyException(localizer[McpErrorCodes.ReadOnlyStatementRequired]); return McpProblem.From(localizer, McpErrorCodes.ReadOnlyStatementRequired);
} }
var forbidden = ForbiddenStatementRegex.Match(stripped); var forbidden = ForbiddenStatementRegex.Match(stripped);
if (forbidden.Success)
{ return forbidden.Success
throw new UserFriendlyException( ? McpProblem.From(localizer, McpErrorCodes.ForbiddenKeyword, forbidden.Value.ToUpperInvariant())
localizer[McpErrorCodes.ForbiddenKeyword, forbidden.Value.ToUpperInvariant()]); : null;
}
} }
public string QuoteQualifiedName(string schemaName, string objectName, DataSourceTypeEnum dataSourceType) public McpOutcome<string> QuoteQualifiedName(string schemaName, string objectName, DataSourceTypeEnum dataSourceType)
{ {
EnsureSafeIdentifier(schemaName); foreach (var name in (string?[])[schemaName, objectName])
EnsureSafeIdentifier(objectName); {
if (!SafeIdentifierRegex.IsMatch(name ?? string.Empty))
{
return McpProblem.From(localizer, McpErrorCodes.InvalidIdentifier, name ?? string.Empty);
}
}
return dataSourceType == DataSourceTypeEnum.Postgresql return dataSourceType == DataSourceTypeEnum.Postgresql
? $"\"{schemaName}\".\"{objectName}\"" ? $"\"{schemaName}\".\"{objectName}\""
@ -144,7 +147,7 @@ public class McpSqlGuard(IStringLocalizer<PlatformResource> localizer) : DomainS
: sql; : sql;
} }
public Dictionary<string, object> NormalizeParameters(IReadOnlyDictionary<string, object?>? parameters) public McpOutcome<Dictionary<string, object>> NormalizeParameters(IReadOnlyDictionary<string, object?>? parameters)
{ {
var normalized = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); var normalized = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
if (parameters is null) if (parameters is null)
@ -156,7 +159,7 @@ public class McpSqlGuard(IStringLocalizer<PlatformResource> localizer) : DomainS
{ {
if (key is null || !SafeParameterNameRegex.IsMatch(key)) if (key is null || !SafeParameterNameRegex.IsMatch(key))
{ {
throw new UserFriendlyException(localizer[McpErrorCodes.InvalidParameterName, key ?? string.Empty]); return McpProblem.From(localizer, McpErrorCodes.InvalidParameterName, key ?? string.Empty);
} }
normalized[key.TrimStart('@')] = value!; normalized[key.TrimStart('@')] = value!;
@ -164,12 +167,4 @@ public class McpSqlGuard(IStringLocalizer<PlatformResource> localizer) : DomainS
return normalized; return normalized;
} }
private void EnsureSafeIdentifier(string identifier)
{
if (!SafeIdentifierRegex.IsMatch(identifier ?? string.Empty))
{
throw new UserFriendlyException(localizer[McpErrorCodes.InvalidIdentifier, identifier ?? string.Empty]);
}
}
} }

View file

@ -1,11 +1,10 @@
using System; using System;
using System.IO; using System.IO;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Sozsoft.Platform.Data.Seeds; using Sozsoft.Platform.Data.Seeds;
using Sozsoft.Platform.Localization; using Sozsoft.Platform.Localization;
using Volo.Abp;
using Volo.Abp.Domain.Services; using Volo.Abp.Domain.Services;
namespace Sozsoft.Mcp.Services; namespace Sozsoft.Mcp.Services;
@ -23,7 +22,7 @@ namespace Sozsoft.Mcp.Services;
/// </summary> /// </summary>
public interface IMcpSqlObjectWriter public interface IMcpSqlObjectWriter
{ {
McpSavedSqlObject Save(string sqlServerScript, string postgreSqlScript); McpOutcome<McpSavedSqlObject> Save(string sqlServerScript, string postgreSqlScript);
} }
public partial class McpSqlObjectWriter( public partial class McpSqlObjectWriter(
@ -33,17 +32,19 @@ public partial class McpSqlObjectWriter(
private const string SqlServerProvider = "SQL Server"; private const string SqlServerProvider = "SQL Server";
private const string PostgreSqlProvider = "PostgreSQL"; private const string PostgreSqlProvider = "PostgreSQL";
public McpSavedSqlObject Save(string sqlServerScript, string postgreSqlScript) public McpOutcome<McpSavedSqlObject> Save(string sqlServerScript, string postgreSqlScript)
{ {
var sqlServer = Canonicalize(sqlServerScript, SqlServerHeaderRegex(), "CREATE OR ALTER", SqlServerProvider); var sqlServer = Canonicalize(sqlServerScript, SqlServerHeaderRegex(), "CREATE OR ALTER");
var postgreSql = Canonicalize(postgreSqlScript, PostgreSqlHeaderRegex(), "CREATE OR REPLACE", PostgreSqlProvider); if (sqlServer is null)
return McpProblem.From(localizer, McpErrorCodes.SqlObjectInvalid, SqlServerProvider);
if (!string.Equals(sqlServer.Name, postgreSql.Name, StringComparison.OrdinalIgnoreCase)) var postgreSql = Canonicalize(postgreSqlScript, PostgreSqlHeaderRegex(), "CREATE OR REPLACE");
throw new UserFriendlyException(localizer[McpErrorCodes.SqlObjectInvalid, PostgreSqlProvider]); if (postgreSql is null || !string.Equals(sqlServer.Name, postgreSql.Name, StringComparison.OrdinalIgnoreCase))
return McpProblem.From(localizer, McpErrorCodes.SqlObjectInvalid, PostgreSqlProvider);
// Tablo scriptinin yerine view yazilirsa migrate tabloyu bir daha kuramazdi. // Tablo scriptinin yerine view yazilirsa migrate tabloyu bir daha kuramazdi.
if (PostgreSqlTableScript.ReadSeedColumns(configuration, sqlServer.Name).Count > 0) if (PostgreSqlTableScript.ReadSeedColumns(configuration, sqlServer.Name).Count > 0)
throw new UserFriendlyException(localizer[McpErrorCodes.TableAlreadyExists, sqlServer.Name]); return McpProblem.From(localizer, McpErrorCodes.TableAlreadyExists, sqlServer.Name);
var fileName = $"{sqlServer.Name}.sql"; var fileName = $"{sqlServer.Name}.sql";
var sqlServerPath = Write(SeedPathResolver.SqlFolder, fileName, sqlServer.Script); var sqlServerPath = Write(SeedPathResolver.SqlFolder, fileName, sqlServer.Script);
@ -64,13 +65,13 @@ public partial class McpSqlObjectWriter(
/// <summary> /// <summary>
/// Script tek bir nesne tanimlamalidir; basligi tekrar calismaya dayanikli bicime cevrilir. /// Script tek bir nesne tanimlamalidir; basligi tekrar calismaya dayanikli bicime cevrilir.
/// Yorumlar yalnizca aramada yok sayilir, dosyada kalir. /// Yorumlar yalnizca aramada yok sayilir, dosyada kalir. Tek nesne tanimlamayan script icin null.
/// </summary> /// </summary>
private (string Script, string Name, string Kind) Canonicalize(string script, Regex headerRegex, string createClause, string provider) private static CanonicalScript? Canonicalize(string script, Regex headerRegex, string createClause)
{ {
var headers = string.IsNullOrWhiteSpace(script) ? null : headerRegex.Matches(CommentRegex().Replace(script, " ")); var headers = string.IsNullOrWhiteSpace(script) ? null : headerRegex.Matches(CommentRegex().Replace(script, " "));
if (headers is not { Count: 1 }) if (headers is not { Count: 1 })
throw new UserFriendlyException(localizer[McpErrorCodes.SqlObjectInvalid, provider]); return null;
var name = headers[0].Groups["name"].Value; var name = headers[0].Groups["name"].Value;
var kind = headers[0].Groups["kind"].Value.ToUpperInvariant() switch var kind = headers[0].Groups["kind"].Value.ToUpperInvariant() switch
@ -90,9 +91,11 @@ public partial class McpSqlObjectWriter(
return $"{createClause} {kind} {m.Groups["qualified"].Value}"; return $"{createClause} {kind} {m.Groups["qualified"].Value}";
}); });
return (canonical + Environment.NewLine, name, kind); return new CanonicalScript(canonical + Environment.NewLine, name, kind);
} }
private sealed record CanonicalScript(string Script, string Name, string Kind);
[GeneratedRegex(@"/\*.*?\*/|--[^\r\n]*", RegexOptions.Singleline)] [GeneratedRegex(@"/\*.*?\*/|--[^\r\n]*", RegexOptions.Singleline)]
private static partial Regex CommentRegex(); private static partial Regex CommentRegex();

View file

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -31,7 +31,7 @@ namespace Sozsoft.Mcp.Services;
/// </summary> /// </summary>
public interface IMcpTableCreator public interface IMcpTableCreator
{ {
Task<McpCreatedTable> CreateAsync(McpTableDefinition definition); Task<McpOutcome<McpCreatedTable>> CreateAsync(McpTableDefinition definition);
} }
public class McpTableCreator( public class McpTableCreator(
@ -99,16 +99,22 @@ public class McpTableCreator(
new("DeleterId", "uniqueidentifier", null, true, null) new("DeleterId", "uniqueidentifier", null, true, null)
]; ];
public async Task<McpCreatedTable> CreateAsync(McpTableDefinition definition) public async Task<McpOutcome<McpCreatedTable>> CreateAsync(McpTableDefinition definition)
{ {
Check.NotNull(definition, nameof(definition)); Check.NotNull(definition, nameof(definition));
var tableName = (definition.TableName ?? string.Empty).Trim(); var tableName = (definition.TableName ?? string.Empty).Trim();
EnsureSafeIdentifier(tableName); if (CheckIdentifier(tableName) is { } invalidName)
{
return invalidName;
}
var columns = BuildColumns(definition); if (!BuildColumns(definition).TryGetValue(out var columns, out var problem) ||
var indexes = BuildIndexes(tableName, definition.Indexes, columns); !BuildIndexes(tableName, definition.Indexes, columns).TryGetValue(out var indexes, out problem) ||
var foreignKeys = BuildForeignKeys(definition.ForeignKeys, columns); !BuildForeignKeys(definition.ForeignKeys, columns).TryGetValue(out var foreignKeys, out problem))
{
return problem;
}
var context = await queryExecutor.ResolveDataSourceAsync(); var context = await queryExecutor.ResolveDataSourceAsync();
var isPostgreSql = context.DataSourceType == DataSourceTypeEnum.Postgresql; var isPostgreSql = context.DataSourceType == DataSourceTypeEnum.Postgresql;
@ -127,11 +133,16 @@ public class McpTableCreator(
// scriptinin ustune yazmaz. // scriptinin ustune yazmaz.
if (await TableExistsAsync(tableName) || PostgreSqlTableScript.ReadSeedColumns(configuration, tableName).Count > 0) if (await TableExistsAsync(tableName) || PostgreSqlTableScript.ReadSeedColumns(configuration, tableName).Count > 0)
{ {
throw new UserFriendlyException(localizer[McpErrorCodes.TableAlreadyExists, tableName]); return Problem(McpErrorCodes.TableAlreadyExists, tableName);
} }
foreach (var foreignKey in foreignKeys) foreach (var foreignKey in foreignKeys)
await EnsureReferenceInstalledFirstAsync(tableName, foreignKey, TableExistsAsync); {
if (await CheckReferenceInstalledFirstAsync(tableName, foreignKey, TableExistsAsync) is { } notInstalled)
{
return notInstalled;
}
}
// Dosya biçimi tasarımcınınkiyle aynı T-SQL'dir; PostgreSQL eşi ondan üretilir. // Dosya biçimi tasarımcınınkiyle aynı T-SQL'dir; PostgreSQL eşi ondan üretilir.
var script = BuildScript(tableName, columns, indexes, foreignKeys); var script = BuildScript(tableName, columns, indexes, foreignKeys);
@ -147,22 +158,28 @@ public class McpTableCreator(
return new McpCreatedTable(tableName, script, seedFilePath); return new McpCreatedTable(tableName, script, seedFilePath);
} }
private List<McpTableColumn> BuildColumns(McpTableDefinition definition) private McpOutcome<List<McpTableColumn>> BuildColumns(McpTableDefinition definition)
{ {
if (definition.Columns.Count == 0) if (definition.Columns.Count == 0)
{ {
throw new UserFriendlyException(localizer[McpErrorCodes.TableColumnsRequired]); return Problem(McpErrorCodes.TableColumnsRequired);
} }
var userColumns = definition.Columns.Select(NormalizeColumn).ToList();
var columns = new List<McpTableColumn> { IdColumn }; var columns = new List<McpTableColumn> { IdColumn };
if (definition.MultiTenant) if (definition.MultiTenant)
{ {
columns.Add(TenantColumn); columns.Add(TenantColumn);
} }
columns.AddRange(userColumns); foreach (var column in definition.Columns)
{
if (!NormalizeColumn(column).TryGetValue(out var normalized, out var problem))
{
return problem;
}
columns.Add(normalized);
}
if (definition.FullAudited) if (definition.FullAudited)
{ {
@ -173,21 +190,27 @@ public class McpTableCreator(
.GroupBy(column => column.Name, StringComparer.OrdinalIgnoreCase) .GroupBy(column => column.Name, StringComparer.OrdinalIgnoreCase)
.FirstOrDefault(group => group.Count() > 1); .FirstOrDefault(group => group.Count() > 1);
if (duplicate is not null) return duplicate is null ? columns : Problem(McpErrorCodes.DuplicateColumn, duplicate.Key);
{
throw new UserFriendlyException(localizer[McpErrorCodes.DuplicateColumn, duplicate.Key]);
}
return columns;
} }
/// <summary>Index ve unique kolonlari tablonun kolonlari arasinda olmalidir; ad tasarimcinin kalibindadir.</summary> /// <summary>Index ve unique kolonlari tablonun kolonlari arasinda olmalidir; ad tasarimcinin kalibindadir.</summary>
private List<McpTableIndex> BuildIndexes(string tableName, IReadOnlyList<McpTableIndex> indexes, List<McpTableColumn> columns) private McpOutcome<List<McpTableIndex>> BuildIndexes(string tableName, IReadOnlyList<McpTableIndex> indexes, List<McpTableColumn> columns)
{ {
var result = new List<McpTableIndex>(); var result = new List<McpTableIndex>();
foreach (var index in indexes) foreach (var index in indexes)
{ {
var indexColumns = index.Columns.Select(column => ResolveColumn(column, columns)).Distinct().ToList(); var indexColumns = new List<string>();
foreach (var column in index.Columns)
{
if (!ResolveColumn(column, columns).TryGetValue(out var resolved, out var problem))
{
return problem;
}
if (!indexColumns.Contains(resolved))
indexColumns.Add(resolved);
}
var name = $"{(index.Unique ? "UQ" : "IX")}_{tableName}_{string.Join("_", indexColumns)}"; var name = $"{(index.Unique ? "UQ" : "IX")}_{tableName}_{string.Join("_", indexColumns)}";
// Kolonsuz ya da ayni kolonlarla tekrar eden index uretilecek bir sey tasimaz. // Kolonsuz ya da ayni kolonlarla tekrar eden index uretilecek bir sey tasimaz.
@ -198,16 +221,22 @@ public class McpTableCreator(
return result; return result;
} }
private List<McpTableForeignKey> BuildForeignKeys(IReadOnlyList<McpTableForeignKey> foreignKeys, List<McpTableColumn> columns) private McpOutcome<List<McpTableForeignKey>> BuildForeignKeys(IReadOnlyList<McpTableForeignKey> foreignKeys, List<McpTableColumn> columns)
{ {
var result = new List<McpTableForeignKey>(); var result = new List<McpTableForeignKey>();
foreach (var foreignKey in foreignKeys) foreach (var foreignKey in foreignKeys)
{ {
var column = ResolveColumn(foreignKey.Column, columns); if (!ResolveColumn(foreignKey.Column, columns).TryGetValue(out var column, out var problem))
{
return problem;
}
var referencedTable = (foreignKey.ReferencedTable ?? string.Empty).Trim(); var referencedTable = (foreignKey.ReferencedTable ?? string.Empty).Trim();
var referencedColumn = string.IsNullOrWhiteSpace(foreignKey.ReferencedColumn) ? IdColumn.Name : foreignKey.ReferencedColumn.Trim(); var referencedColumn = string.IsNullOrWhiteSpace(foreignKey.ReferencedColumn) ? IdColumn.Name : foreignKey.ReferencedColumn.Trim();
EnsureSafeIdentifier(referencedTable); if ((CheckIdentifier(referencedTable) ?? CheckIdentifier(referencedColumn)) is { } invalid)
EnsureSafeIdentifier(referencedColumn); {
return invalid;
}
// Tanimadigi davranis (Restrict gibi) SQL Server'in varsayilanina, NO ACTION'a duser. // Tanimadigi davranis (Restrict gibi) SQL Server'in varsayilanina, NO ACTION'a duser.
var action = ReferentialActions.Keys.FirstOrDefault(key => string.Equals(key, foreignKey.OnDelete?.Trim(), StringComparison.OrdinalIgnoreCase)) var action = ReferentialActions.Keys.FirstOrDefault(key => string.Equals(key, foreignKey.OnDelete?.Trim(), StringComparison.OrdinalIgnoreCase))
@ -227,47 +256,55 @@ public class McpTableCreator(
/// scripti yoksa (EF ile kurulan platform tablosu) veritabaninda bulunmalidir. Aksi halde /// scripti yoksa (EF ile kurulan platform tablosu) veritabaninda bulunmalidir. Aksi halde
/// sifirdan kurulumda migrate FK satirinda duserdi. /// sifirdan kurulumda migrate FK satirinda duserdi.
/// </summary> /// </summary>
private async Task EnsureReferenceInstalledFirstAsync(string tableName, McpTableForeignKey foreignKey, Func<string, Task<bool>> tableExistsAsync) private async Task<McpProblem?> CheckReferenceInstalledFirstAsync(string tableName, McpTableForeignKey foreignKey, Func<string, Task<bool>> tableExistsAsync)
{ {
var hasScript = PostgreSqlTableScript.ReadSeedColumns(configuration, foreignKey.ReferencedTable).Count > 0; var hasScript = PostgreSqlTableScript.ReadSeedColumns(configuration, foreignKey.ReferencedTable).Count > 0;
var installed = hasScript var installed = hasScript
? Comparer<string>.Default.Compare($"{foreignKey.ReferencedTable}.sql", $"{tableName}.sql") < 0 ? Comparer<string>.Default.Compare($"{foreignKey.ReferencedTable}.sql", $"{tableName}.sql") < 0
: await tableExistsAsync(foreignKey.ReferencedTable); : await tableExistsAsync(foreignKey.ReferencedTable);
if (!installed) return installed
{ ? null
throw new UserFriendlyException(localizer[McpErrorCodes.ForeignKeyTableNotInstalledFirst, foreignKey.Column, foreignKey.ReferencedTable, tableName]); : Problem(McpErrorCodes.ForeignKeyTableNotInstalledFirst, foreignKey.Column, foreignKey.ReferencedTable, tableName);
}
} }
private string ResolveColumn(string? name, List<McpTableColumn> columns) => private McpOutcome<string> ResolveColumn(string? name, List<McpTableColumn> columns)
columns.FirstOrDefault(column => string.Equals(column.Name, name?.Trim(), StringComparison.OrdinalIgnoreCase))?.Name {
?? throw new UserFriendlyException(localizer[McpErrorCodes.TableColumnNotFound, name ?? string.Empty]); var match = columns.FirstOrDefault(column => string.Equals(column.Name, name?.Trim(), StringComparison.OrdinalIgnoreCase));
private McpTableColumn NormalizeColumn(McpTableColumn column) return match is null ? Problem(McpErrorCodes.TableColumnNotFound, name ?? string.Empty) : match.Name;
}
private McpOutcome<McpTableColumn> NormalizeColumn(McpTableColumn column)
{ {
var name = (column.Name ?? string.Empty).Trim(); var name = (column.Name ?? string.Empty).Trim();
EnsureSafeIdentifier(name); if (CheckIdentifier(name) is { } invalidName)
{
return invalidName;
}
var (type, sizedLength) = ResolveType(column.Type); if (!ResolveType(column.Type).TryGetValue(out var resolved, out var problem))
{
return problem;
}
int? maxLength = null; int? maxLength = null;
if (type == "nvarchar") if (resolved.Type == "nvarchar")
{ {
maxLength = column.MaxLength ?? sizedLength ?? DefaultTextLength; maxLength = column.MaxLength ?? resolved.MaxLength ?? DefaultTextLength;
if (maxLength is < 1 or > MaxTextLength) if (maxLength is < 1 or > MaxTextLength)
{ {
throw new UserFriendlyException(localizer[McpErrorCodes.InvalidColumnLength, name, MaxTextLength]); return Problem(McpErrorCodes.InvalidColumnLength, name, MaxTextLength);
} }
} }
var defaultValue = string.IsNullOrWhiteSpace(column.DefaultValue) ? null : column.DefaultValue.Trim(); var defaultValue = string.IsNullOrWhiteSpace(column.DefaultValue) ? null : column.DefaultValue.Trim();
if (defaultValue is not null && !SafeDefaultRegex.IsMatch(defaultValue)) if (defaultValue is not null && !SafeDefaultRegex.IsMatch(defaultValue))
{ {
throw new UserFriendlyException(localizer[McpErrorCodes.InvalidDefaultValue, name, defaultValue]); return Problem(McpErrorCodes.InvalidDefaultValue, name, defaultValue);
} }
return new McpTableColumn(name, type, maxLength, column.IsNullable, defaultValue); return new McpTableColumn(name, resolved.Type, maxLength, column.IsNullable, defaultValue);
} }
/// <summary> /// <summary>
@ -275,7 +312,7 @@ public class McpTableCreator(
/// <c>nvarchar(MAX)</c> kendi tipidir; decimal yalnizca tasarimcinin kesinligiyle kabul edilir — /// <c>nvarchar(MAX)</c> kendi tipidir; decimal yalnizca tasarimcinin kesinligiyle kabul edilir —
/// baska bir kesinligi 18,4'e cevirmek sessizce veri kaybi riski tasirdi. /// baska bir kesinligi 18,4'e cevirmek sessizce veri kaybi riski tasirdi.
/// </summary> /// </summary>
private (string Type, int? MaxLength) ResolveType(string? rawType) private McpOutcome<ResolvedType> ResolveType(string? rawType)
{ {
var trimmed = rawType?.Trim() ?? string.Empty; var trimmed = rawType?.Trim() ?? string.Empty;
var name = trimmed; var name = trimmed;
@ -305,14 +342,15 @@ public class McpTableCreator(
} }
} }
var type = SupportedTypes.FirstOrDefault(candidate => var type = SupportedTypes.FirstOrDefault(candidate => string.Equals(candidate, name, StringComparison.OrdinalIgnoreCase));
string.Equals(candidate, name, StringComparison.OrdinalIgnoreCase))
?? throw new UserFriendlyException(
localizer[McpErrorCodes.UnsupportedColumnType, trimmed, string.Join(", ", SupportedTypes)]);
return (type, maxLength); return type is null
? Problem(McpErrorCodes.UnsupportedColumnType, trimmed, string.Join(", ", SupportedTypes))
: new ResolvedType(type, maxLength);
} }
private sealed record ResolvedType(string Type, int? MaxLength);
private static string BuildScript(string tableName, List<McpTableColumn> columns, List<McpTableIndex> indexes, List<McpTableForeignKey> foreignKeys) private static string BuildScript(string tableName, List<McpTableColumn> columns, List<McpTableIndex> indexes, List<McpTableForeignKey> foreignKeys)
{ {
var fullTableName = $"[{SchemaName}].[{tableName}]"; var fullTableName = $"[{SchemaName}].[{tableName}]";
@ -388,13 +426,11 @@ public class McpTableCreator(
return $" [{column.Name}] {type} {nullPart}{defaultPart}"; return $" [{column.Name}] {type} {nullPart}{defaultPart}";
} }
private void EnsureSafeIdentifier(string identifier) private McpProblem? CheckIdentifier(string identifier)
{ => SafeIdentifierRegex.IsMatch(identifier) ? null : Problem(McpErrorCodes.InvalidIdentifier, identifier);
if (!SafeIdentifierRegex.IsMatch(identifier))
{ private McpProblem Problem(string name, params object[] arguments)
throw new UserFriendlyException(localizer[McpErrorCodes.InvalidIdentifier, identifier]); => McpProblem.From(localizer, name, arguments);
}
}
} }
/// <param name="FullAudited">Tam audit kolonlari (olusturma/degistirme/silme izi) eklensin mi.</param> /// <param name="FullAudited">Tam audit kolonlari (olusturma/degistirme/silme izi) eklensin mi.</param>

View file

@ -80,9 +80,9 @@ public class McpHttpApiModule : AbpModule
return result; return result;
}); });
// Yetki reddi ve is kurali hatasi aracin beklenen sonucudur: firlatilirsa SDK onu // MCP'nin kendi reddi istisna degildir (McpProblem). Arkadaki platform servisinin yetki
// yigin izli ERR olarak loglar. Hata sonucu (IsError) olarak donulur; model nedeni // reddi ve is kurali hatasi ise istisnadir; firlatilirsa SDK onu yigin izli ERR olarak
// (eksik yetkiyi) okuyup devam eder. // loglar. Hata sonucu (IsError) olarak donulur; model nedeni okuyup devam eder.
filters.AddCallToolFilter(next => async (requestContext, cancellationToken) => filters.AddCallToolFilter(next => async (requestContext, cancellationToken) =>
{ {
try try
@ -92,7 +92,7 @@ public class McpHttpApiModule : AbpModule
catch (System.Exception ex) when (McpToolErrorTranslator.CanTranslate(ex)) catch (System.Exception ex) when (McpToolErrorTranslator.CanTranslate(ex))
{ {
var message = await requestContext.Services!.GetRequiredService<McpToolErrorTranslator>().TranslateAsync(ex, requestContext.Params?.Name); var message = await requestContext.Services!.GetRequiredService<McpToolErrorTranslator>().TranslateAsync(ex, requestContext.Params?.Name);
return new CallToolResult { IsError = true, Content = [new TextContentBlock { Text = message }] }; return McpToolResult.Error(message);
} }
}); });
}); });

View file

@ -12,10 +12,12 @@ using Volo.Abp.DependencyInjection;
namespace Sozsoft.Mcp; namespace Sozsoft.Mcp;
/// <summary> /// <summary>
/// Arac hatalarini istemcinin gorebilecegi mesaja cevirir. SDK cevrilmemis istisnayi /// Aracin arkasindaki platform servislerinden (wizard, custom component, ListForm verisi) ve sinif
/// "An error occurred invoking" olarak iletir ve model neden reddedildigini bilemez. Yetki reddinde eksik yetkiler adiyla, is kurali hatasinda /// duzeyindeki <c>[Authorize]</c>'dan gelen istisnalari istemcinin gorebilecegi mesaja cevirir. MCP'nin
/// (<see cref="UserFriendlyException"/>) zaten yerellestirilmis mesaj iletilir. Diger istisnalar /// kendi reddi istisna degildir, <see cref="McpProblem"/> olarak doner. SDK cevrilmemis istisnayi
/// cevrilmez: ic ayrinti (SQL, yigin) istemciye sizmaz. /// "An error occurred invoking" olarak iletir ve model neden reddedildigini bilemez. Yetki reddinde
/// eksik yetkiler adiyla, is kurali hatasinda (<see cref="UserFriendlyException"/>) zaten
/// yerellestirilmis mesaj iletilir. Diger istisnalar cevrilmez: ic ayrinti (SQL, yigin) istemciye sizmaz.
/// </summary> /// </summary>
public class McpToolErrorTranslator( public class McpToolErrorTranslator(
IPermissionChecker permissionChecker, IPermissionChecker permissionChecker,
@ -44,7 +46,7 @@ public class McpToolErrorTranslator(
var grants = await permissionChecker.IsGrantedAsync([.. candidates.Distinct(StringComparer.Ordinal)]); var grants = await permissionChecker.IsGrantedAsync([.. candidates.Distinct(StringComparer.Ordinal)]);
var missing = grants.Result var missing = grants.Result
.Where(result => result.Value != PermissionGrantResult.Granted) .Where(result => result.Value != PermissionGrantResult.Granted)
.Select(result => Describe(result.Key)) .Select(result => McpPermissionText.Describe(localizer, result.Key))
.ToList(); .ToList();
var target = toolName ?? McpConsts.EndpointPath; var target = toolName ?? McpConsts.EndpointPath;
@ -53,14 +55,4 @@ public class McpToolErrorTranslator(
? localizer[McpErrorCodes.PermissionDeniedUnspecified, target] ? localizer[McpErrorCodes.PermissionDeniedUnspecified, target]
: localizer[McpErrorCodes.PermissionDenied, target, string.Join(", ", missing)]; : localizer[McpErrorCodes.PermissionDenied, target, string.Join(", ", missing)];
} }
/// <summary>Yetkinin gorunen adi ve kodu; rol ekraninda aranabilsin diye kod her zaman yazilir.</summary>
private string Describe(string permissionName)
{
var displayName = localizer[permissionName];
return displayName.ResourceNotFound || displayName.Value == permissionName
? permissionName
: $"{displayName.Value} ({permissionName})";
}
} }

View file

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Threading.Tasks; using System.Threading.Tasks;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server; using ModelContextProtocol.Server;
using Sozsoft.Mcp.Dtos; using Sozsoft.Mcp.Dtos;
@ -16,43 +17,43 @@ public sealed class McpDataTools(IMcpDataAppService dataAppService)
{ {
[McpServerTool(Name = McpToolNames.InsertRow, Destructive = false, Idempotent = false)] [McpServerTool(Name = McpToolNames.InsertRow, Destructive = false, Idempotent = false)]
[Description("Bir listeye yeni kayit ekler. Alan adlari describe_list cikisindaki field degerleridir.")] [Description("Bir listeye yeni kayit ekler. Alan adlari describe_list cikisindaki field degerleridir.")]
public async Task<McpWriteResultDto> InsertRowAsync( public async Task<CallToolResult> InsertRowAsync(
[Description("Kaydin eklenecegi listenin kodu.")] [Description("Kaydin eklenecegi listenin kodu.")]
string listFormCode, string listFormCode,
[Description("Alan adi -> deger eslemesi.")] [Description("Alan adi -> deger eslemesi.")]
Dictionary<string, object?> values) Dictionary<string, object?> values)
=> await dataAppService.InsertRowAsync(new McpRowRequestDto => McpToolResult.From(await dataAppService.InsertRowAsync(new McpRowRequestDto
{ {
ListFormCode = listFormCode, ListFormCode = listFormCode,
Values = values ?? [] Values = values ?? []
}); }));
[McpServerTool(Name = McpToolNames.UpdateRow, Destructive = true, Idempotent = true)] [McpServerTool(Name = McpToolNames.UpdateRow, Destructive = true, Idempotent = true)]
[Description("Bir listedeki kaydi anahtar degerine gore gunceller. Yalnizca verilen alanlar yazilir.")] [Description("Bir listedeki kaydi anahtar degerine gore gunceller. Yalnizca verilen alanlar yazilir.")]
public async Task<McpWriteResultDto> UpdateRowAsync( public async Task<CallToolResult> UpdateRowAsync(
[Description("Kaydin bulundugu listenin kodu.")] [Description("Kaydin bulundugu listenin kodu.")]
string listFormCode, string listFormCode,
[Description("Guncellenecek kaydin anahtar alan degeri; describe_list cikisindaki keyFieldName alanina karsilik gelir.")] [Description("Guncellenecek kaydin anahtar alan degeri; describe_list cikisindaki keyFieldName alanina karsilik gelir.")]
List<object?> keys, List<object?> keys,
[Description("Alan adi -> yeni deger eslemesi.")] [Description("Alan adi -> yeni deger eslemesi.")]
Dictionary<string, object?> values) Dictionary<string, object?> values)
=> await dataAppService.UpdateRowAsync(new McpRowRequestDto => McpToolResult.From(await dataAppService.UpdateRowAsync(new McpRowRequestDto
{ {
ListFormCode = listFormCode, ListFormCode = listFormCode,
Keys = keys ?? [], Keys = keys ?? [],
Values = values ?? [] Values = values ?? []
}); }));
[McpServerTool(Name = McpToolNames.DeleteRow, Destructive = true, Idempotent = true)] [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.")] [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( public async Task<CallToolResult> DeleteRowAsync(
[Description("Kaydin bulundugu listenin kodu.")] [Description("Kaydin bulundugu listenin kodu.")]
string listFormCode, string listFormCode,
[Description("Silinecek kaydin anahtar alan degeri.")] [Description("Silinecek kaydin anahtar alan degeri.")]
List<object?> keys) List<object?> keys)
=> await dataAppService.DeleteRowAsync(new McpRowRequestDto => McpToolResult.From(await dataAppService.DeleteRowAsync(new McpRowRequestDto
{ {
ListFormCode = listFormCode, ListFormCode = listFormCode,
Keys = keys ?? [] Keys = keys ?? []
}); }));
} }

View file

@ -30,16 +30,16 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService, IMc
[McpServerTool(Name = McpToolNames.GetDevGuide, ReadOnly = true)] [McpServerTool(Name = McpToolNames.GetDevGuide, ReadOnly = true)]
[Description("Ekran ve bilesen uretim sozlesmesini okur: wizard alanlari, enum degerleri, editor/lookup/script sozlesmesi, custom component kurallari. section bos ise baslik listesi doner; once listeyi al, sonra ihtiyac duydugun bolumu numarasiyla iste (ornegin 4.2). Rehberde dosya yazmak anlatilan yerlerde dosya yerine bu sunucunun araclarini kullan.")] [Description("Ekran ve bilesen uretim sozlesmesini okur: wizard alanlari, enum degerleri, editor/lookup/script sozlesmesi, custom component kurallari. section bos ise baslik listesi doner; once listeyi al, sonra ihtiyac duydugun bolumu numarasiyla iste (ornegin 4.2). Rehberde dosya yazmak anlatilan yerlerde dosya yerine bu sunucunun araclarini kullan.")]
public async Task<McpDevGuideDto> GetDevGuideAsync( public async Task<CallToolResult> GetDevGuideAsync(
[Description("'lowcode' (uretim protokolu ve artefakt semalari, varsayilan) ya da 'reference' (JSON kolon sozlesmeleri, tasarimci envanteri).")] [Description("'lowcode' (uretim protokolu ve artefakt semalari, varsayilan) ya da 'reference' (JSON kolon sozlesmeleri, tasarimci envanteri).")]
string? document = null, string? document = null,
[Description("Bolum numarasi (4.2), baslik metninin bir parcasi ya da aranacak terim (dxImageUpload). Baslikla eslesmeyen terimde terimin gectigi bolumler listelenir; bos birakilirsa baslik listesi doner.")] [Description("Bolum numarasi (4.2), baslik metninin bir parcasi ya da aranacak terim (dxImageUpload). Baslikla eslesmeyen terimde terimin gectigi bolumler listelenir; bos birakilirsa baslik listesi doner.")]
string? section = null) string? section = null)
=> await developAppService.GetDevGuideAsync(document, section); => McpToolResult.From(await developAppService.GetDevGuideAsync(document, section));
[McpServerTool(Name = McpToolNames.CreateTable, Destructive = false, Idempotent = false)] [McpServerTool(Name = McpToolNames.CreateTable, Destructive = false, Idempotent = false)]
[Description("Ekranin baglanacagi yeni tablonun script'ini SQL Query Manager tablo tasarimcisiyla ayni bicimde uretir ve seed dosyasina (sql/object + postgres/object) yazar; veritabaninda calistirmaz, tabloyu kullanicinin calistirdigi DB Migrate kurar. DB Migrate'i bekleme ve calistirmaya calisma; deploy_wizard kolonlari script'ten okur. Id (uniqueidentifier, PK), TenantId ve tam audit kolonlari (CreationTime, CreatorId, LastModificationTime, LastModifierId, IsDeleted, DeletionTime, DeleterId) otomatik eklenir; columns icine yalnizca is kolonlarini yaz. indexes ile index/unique, foreignKeys ile FK kisitlari ayni scripte yazilir. Yalnizca yeni tablo icindir: tablo ya da script'i varsa reddeder, kolon ekleme/degistirme tablo tasarimcisindan yapilir. View/procedure/function icin save_sql_object kullan. Ardindan deploy_wizard ile ekrani kur.")] [Description("Ekranin baglanacagi yeni tablonun script'ini SQL Query Manager tablo tasarimcisiyla ayni bicimde uretir ve seed dosyasina (sql/object + postgres/object) yazar; veritabaninda calistirmaz, tabloyu kullanicinin calistirdigi DB Migrate kurar. DB Migrate'i bekleme ve calistirmaya calisma; deploy_wizard kolonlari script'ten okur. Id (uniqueidentifier, PK), TenantId ve tam audit kolonlari (CreationTime, CreatorId, LastModificationTime, LastModifierId, IsDeleted, DeletionTime, DeleterId) otomatik eklenir; columns icine yalnizca is kolonlarini yaz. indexes ile index/unique, foreignKeys ile FK kisitlari ayni scripte yazilir. Yalnizca yeni tablo icindir: tablo ya da script'i varsa reddeder, kolon ekleme/degistirme tablo tasarimcisindan yapilir. View/procedure/function icin save_sql_object kullan. Ardindan deploy_wizard ile ekrani kur.")]
public async Task<McpCreateTableResultDto> CreateTableAsync( public async Task<CallToolResult> CreateTableAsync(
[Description("Tablo adi; harf/rakam/alt cizgi. Adlandirma icin get_dev_guide'daki tablo onek kuralina uy (ornegin Adm_T_...).")] [Description("Tablo adi; harf/rakam/alt cizgi. Adlandirma icin get_dev_guide'daki tablo onek kuralina uy (ornegin Adm_T_...).")]
string tableName, string tableName,
[Description("Is kolonlari. type: nvarchar, nvarchar(MAX), int, bigint, decimal (her zaman 18,4 yazilir; boyut verme), float, bit, datetime, datetime2, date, uniqueidentifier, money. maxLength yalnizca nvarchar icin (varsayilan 100). defaultValue: sayi, 'metin' ya da NEWID()/GETDATE()/GETUTCDATE()/SYSUTCDATETIME(). Sabit defaultValue ekranda yeni kaydin form degeri olur (deploy tablodan okur); bu yuzden IsActive bit isNullable false defaultValue \"1\", olcu birimi (UomId) defaultValue \"'Adet'\", para birimi (CurrencyId) defaultValue \"'TRY'\" ve talepteki her sabit varsayilan (Sure = 60 → \"60\") kolona yazilir (get_dev_guide 4.3.5.1).")] [Description("Is kolonlari. type: nvarchar, nvarchar(MAX), int, bigint, decimal (her zaman 18,4 yazilir; boyut verme), float, bit, datetime, datetime2, date, uniqueidentifier, money. maxLength yalnizca nvarchar icin (varsayilan 100). defaultValue: sayi, 'metin' ya da NEWID()/GETDATE()/GETUTCDATE()/SYSUTCDATETIME(). Sabit defaultValue ekranda yeni kaydin form degeri olur (deploy tablodan okur); bu yuzden IsActive bit isNullable false defaultValue \"1\", olcu birimi (UomId) defaultValue \"'Adet'\", para birimi (CurrencyId) defaultValue \"'TRY'\" ve talepteki her sabit varsayilan (Sure = 60 → \"60\") kolona yazilir (get_dev_guide 4.3.5.1).")]
@ -52,7 +52,7 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService, IMc
List<McpTableIndexDto>? indexes = null, List<McpTableIndexDto>? indexes = null,
[Description("FK'ler: [{\"column\": \"UomId\", \"referencedTable\": \"Adm_T_Uom\", \"referencedColumn\": \"Id\", \"onDelete\": \"NoAction\"}]. onDelete: NoAction (varsayilan), Cascade, SetNull. Migrate scriptleri dosya adi sirasiyla calistirdigi icin hedef tablo ya scripti bu tablonunkinden once siralanan bir seed tablosu ya da veritabaninda kurulu bir platform tablosu olmalidir; degilse reddedilir.")] [Description("FK'ler: [{\"column\": \"UomId\", \"referencedTable\": \"Adm_T_Uom\", \"referencedColumn\": \"Id\", \"onDelete\": \"NoAction\"}]. onDelete: NoAction (varsayilan), Cascade, SetNull. Migrate scriptleri dosya adi sirasiyla calistirdigi icin hedef tablo ya scripti bu tablonunkinden once siralanan bir seed tablosu ya da veritabaninda kurulu bir platform tablosu olmalidir; degilse reddedilir.")]
List<McpTableForeignKeyDto>? foreignKeys = null) List<McpTableForeignKeyDto>? foreignKeys = null)
=> await developAppService.CreateTableAsync(new McpCreateTableRequestDto => McpToolResult.From(await developAppService.CreateTableAsync(new McpCreateTableRequestDto
{ {
TableName = tableName, TableName = tableName,
Columns = columns, Columns = columns,
@ -60,134 +60,128 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService, IMc
MultiTenant = multiTenant, MultiTenant = multiTenant,
Indexes = indexes ?? [], Indexes = indexes ?? [],
ForeignKeys = foreignKeys ?? [] ForeignKeys = foreignKeys ?? []
}); }));
[McpServerTool(Name = McpToolNames.SaveSqlObject, Destructive = true, Idempotent = true)] [McpServerTool(Name = McpToolNames.SaveSqlObject, Destructive = true, Idempotent = true)]
[Description("Ekranin veri kaynagi ya da lookup'i olacak view, stored procedure ya da function scriptini SQL Query Manager'in yazdigi yere (sql/object/{Ad}.sql) ve PostgreSQL esine (postgres/object/{Ad}.sql) yazar; ad scriptteki nesne adidir; veritabaninda calistirmaz, nesneyi kullanicinin calistirdigi DB Migrate kurar. Uygulama iki saglayicida calistigi icin iki script de zorunludur ve ayni nesneyi ayni parametre sirasiyla tanimlar (SQL Server: [dbo], PostgreSQL: public, ceviri kurallari get_dev_guide lowcode 8.3.1). Baslik tekrar calismaya dayanikli bicime cevrilir (CREATE OR ALTER / CREATE OR REPLACE); ayni adla tekrar cagrilirsa script guncellenir. Migrate object scriptlerini dosya adi sirasiyla calistirir: view'in okudugu tablolarin adi view'in adindan once siralanmalidir (Inv_T_Product → Inv_V_Product). Birden fazla tablodan beslenen ekranda Query (SelectCommandType 4) yerine bu aracla view yazip ekrani view'e (SelectCommandType 2) bagla. Tablo icin create_table kullan.")] [Description("Ekranin veri kaynagi ya da lookup'i olacak view, stored procedure ya da function scriptini SQL Query Manager'in yazdigi yere (sql/object/{Ad}.sql) ve PostgreSQL esine (postgres/object/{Ad}.sql) yazar; ad scriptteki nesne adidir; veritabaninda calistirmaz, nesneyi kullanicinin calistirdigi DB Migrate kurar. Uygulama iki saglayicida calistigi icin iki script de zorunludur ve ayni nesneyi ayni parametre sirasiyla tanimlar (SQL Server: [dbo], PostgreSQL: public, ceviri kurallari get_dev_guide lowcode 8.3.1). Baslik tekrar calismaya dayanikli bicime cevrilir (CREATE OR ALTER / CREATE OR REPLACE); ayni adla tekrar cagrilirsa script guncellenir. Migrate object scriptlerini dosya adi sirasiyla calistirir: view'in okudugu tablolarin adi view'in adindan once siralanmalidir (Inv_T_Product → Inv_V_Product). Birden fazla tablodan beslenen ekranda Query (SelectCommandType 4) yerine bu aracla view yazip ekrani view'e (SelectCommandType 2) bagla. Tablo icin create_table kullan.")]
public async Task<McpSqlObjectResultDto> SaveSqlObjectAsync( public async Task<CallToolResult> SaveSqlObjectAsync(
[Description("SQL Server scripti: tek bir CREATE [OR ALTER] VIEW/PROCEDURE/FUNCTION [dbo].[Ad] tanimi; GO ile biten batch'ler kullanilabilir. Dosya adi nesnenin adidir.")] [Description("SQL Server scripti: tek bir CREATE [OR ALTER] VIEW/PROCEDURE/FUNCTION [dbo].[Ad] tanimi; GO ile biten batch'ler kullanilabilir. Dosya adi nesnenin adidir.")]
string sqlServerScript, string sqlServerScript,
[Description("PostgreSQL scripti: ayni nesnenin CREATE OR REPLACE VIEW/FUNCTION/PROCEDURE public.\"Ad\" tanimi.")] [Description("PostgreSQL scripti: ayni nesnenin CREATE OR REPLACE VIEW/FUNCTION/PROCEDURE public.\"Ad\" tanimi.")]
string postgreSqlScript) string postgreSqlScript)
=> await developAppService.SaveSqlObjectAsync(sqlServerScript, postgreSqlScript); => McpToolResult.From(await developAppService.SaveSqlObjectAsync(sqlServerScript, postgreSqlScript));
[McpServerTool(Name = McpToolNames.GenerateCrudEndpoints, Destructive = true, Idempotent = true)] [McpServerTool(Name = McpToolNames.GenerateCrudEndpoints, Destructive = true, Idempotent = true)]
[Description("Bir tablo ya da view icin SQL Query Manager'daki CRUD endpoint diyaloguyla ayni REST endpoint'lerini (/api/app/crudendpoint/{entity}: GetList, GetById, Create, Update, Delete) yalnizca crud/{entity}.json seed dosyasina yazar; kayitlari DB Migrate ekler (migrate'i bekleme). Ayni entity icin tekrar cagrilirsa dosya yeniden yazilir. Custom component'in veri okuyup yazacagi tablo icin kullanilir; ListForm ekranlari bu endpoint'lere ihtiyac duymaz. View/procedure/function icin yalnizca GetList ve GetById uretilir. App.SqlQueryManager.CrudEndpoints yetkisi ister.")] [Description("Bir tablo ya da view icin SQL Query Manager'daki CRUD endpoint diyaloguyla ayni REST endpoint'lerini (/api/app/crudendpoint/{entity}: GetList, GetById, Create, Update, Delete) yalnizca crud/{entity}.json seed dosyasina yazar; kayitlari DB Migrate ekler (migrate'i bekleme). Ayni entity icin tekrar cagrilirsa dosya yeniden yazilir. Custom component'in veri okuyup yazacagi tablo icin kullanilir; ListForm ekranlari bu endpoint'lere ihtiyac duymaz. View/procedure/function icin yalnizca GetList ve GetById uretilir. App.SqlQueryManager.CrudEndpoints yetkisi ister.")]
public async Task<McpCrudEndpointsResultDto> GenerateCrudEndpointsAsync( public async Task<CallToolResult> GenerateCrudEndpointsAsync(
[Description("Tablo ya da view adi, ornegin 'Inv_T_Product' (sema oneki atilir).")] [Description("Tablo ya da view adi, ornegin 'Inv_T_Product' (sema oneki atilir).")]
string entityName, string entityName,
[Description("Aktif baslayacak operasyonlar, virgulle: GetList,GetById,Create,Update,Delete. Bos ise hepsi aktif; digerleri pasif kaydedilir.")] [Description("Aktif baslayacak operasyonlar, virgulle: GetList,GetById,Create,Update,Delete. Bos ise hepsi aktif; digerleri pasif kaydedilir.")]
string? operationTypes = null, string? operationTypes = null,
[Description("Nesne turu: table (varsayilan), view, procedure, function.")] [Description("Nesne turu: table (varsayilan), view, procedure, function.")]
string? objectType = null) string? objectType = null)
=> await developAppService.GenerateCrudEndpointsAsync(entityName, operationTypes, objectType); => McpToolResult.From(await developAppService.GenerateCrudEndpointsAsync(entityName, operationTypes, objectType));
[McpServerTool(Name = McpToolNames.ListWizards, ReadOnly = true)] [McpServerTool(Name = McpToolNames.ListWizards, ReadOnly = true)]
[Description("Wizard File Manager'daki wizard dosyalarini listeler: dosya adi, ekran kodu, yol (List/Custom), menu yolu ve bagimli wizard'lar.")] [Description("Wizard File Manager'daki wizard dosyalarini listeler: dosya adi, ekran kodu, yol (List/Custom), menu yolu ve bagimli wizard'lar.")]
public async Task<List<WizardFileInfoDto>> ListWizardsAsync( public async Task<CallToolResult> ListWizardsAsync(
[Description("Dosya adi, wizard adi, ekran kodu ya da bilesen adi icinde aranacak metin. Bos birakilabilir.")] [Description("Dosya adi, wizard adi, ekran kodu ya da bilesen adi icinde aranacak metin. Bos birakilabilir.")]
string? search = null) string? search = null)
=> await developAppService.GetWizardsAsync(search); => McpToolResult.From(await developAppService.GetWizardsAsync(search));
[McpServerTool(Name = McpToolNames.GetWizard, ReadOnly = true)] [McpServerTool(Name = McpToolNames.GetWizard, ReadOnly = true)]
[Description("Bir wizard seed dosyasini diskteki bicimiyle dondurur (Wizard / ListForm / Fields bolumleri). Var olan bir ekrani degistirmeden once oku.")] [Description("Bir wizard seed dosyasini diskteki bicimiyle dondurur (Wizard / ListForm / Fields bolumleri). Var olan bir ekrani degistirmeden once oku.")]
public async Task<McpWizardFileDto> GetWizardAsync( public async Task<CallToolResult> GetWizardAsync(
[Description("list_wizards cikisindaki fileName degeri, ornegin 'Tags.json'.")] [Description("list_wizards cikisindaki fileName degeri, ornegin 'Tags.json'.")]
string fileName) string fileName)
=> await developAppService.GetWizardAsync(fileName); => McpToolResult.From(await developAppService.GetWizardAsync(fileName));
[McpServerTool(Name = McpToolNames.DeployWizard, Destructive = true, Idempotent = false)] [McpServerTool(Name = McpToolNames.DeployWizard, Destructive = true, Idempotent = false)]
[Description("Wizard'i deploy eder: ListForm ekrani (ComponentKind 0) ya da bir custom component'i menuye baglayan kayit (ComponentKind 1), yetkileri, dil anahtarlari ve menusu seed dosyalarina yazilir. Deploy veritabanina yazmaz: ekran, menu ve yetkiler kullanici Wizard Manager'da DB Migrate calistirinca olusur; cikistaki isPending true iken ekran henuz yoktur; migrate'i bekleme, isini bitir ve kullaniciya yalnizca DB Migrate calistirmasini soyle. Eksik modul, ust menu, izin grubu, yetki ve dil satirlari da deploy'da dosyalara yazilir ve migrate onlari dogru sirayla ekler; bunlarin veritabaninda olmamasi beklenen durumdur, uyari olarak raporlama ve kullanicidan elle eklemesini isteme. Ust menu zinciri istenen derinlikte kurulur: MenuParentCode ekranin dogrudan ust menusu, Menu.MenuParentPath onun ustundeki seviyeler (kokten asagi, her biri {Code, Icon, En, Tr}); eksik her seviye bir ustunun altina kurulur, var olana baglanilir. Liste ekraninda Grid, Card, Pivot ve Chart gorunumleri varsayilan aciktir (DefaultLayout grid); kullanici bir gorunumu istemedigini soylemedikce bu bayraklari false yazma. Hedef tablo onceden var olmalidir; yoksa once create_table ile olustur. Istek ornek/demo veri tasiyorsa (prompt'taki tablo/liste ya da ekran goruntusunde gorunen satirlar) SeedData true yaz ve satirlarin hepsini ayni cagrida seedRows ile gonder: satirlar ekranin data/{ListFormCode}.json dosyasina yazilir, migrate ekrani kurduktan sonra ekler. SeedData ekrandan girilen kayitlari dosyaya yansitmaz (SeedSyncInsert/Update/Delete kapali); bunu yalnizca istek acikca isterse SeedSync true yaz. Satirlar icin migrate'i bekleyip insert_row kullanma. replaceFileName verilirse o wizard'in dosyalari yeniden yazilir, onceki kayitlarin yerini migrate'te yenileri alir. Wizard cevaplarinin uretmedigi ekran ayarlari (FormFieldsDefaultValueDto/InsertFieldsDefaultValueDto gibi varsayilan degerler, ValidationRuleDto, Format, Alignment, Width, Visible, BandName, ozet/gruplama, ExtraFilter, Insert/Update/Delete komutlari, PageSize, CommandColumnDto …) ayni JSON'un ListForm ve Fields bolumlerine get_wizard ciktisindaki adlarla yazilir. Yeni kaydin form degeri (Aktif = true, Birim = Adet, Kur = TRY, Sure = 60) ayri yazilmaz: deploy kolonun tablodaki sabit DEFAULT degerini okur; tablodan farkli deger gerekiyorsa alan ogesinde DefaultValue yaz (Groups[].Items[].DefaultValue). StaticData lookup seceneklerinde (RadioGroup, SelectBox, TagBox) name dil anahtaridir (App.StaticLookup.{Key}); EN + TR metni bilinen secenegi Language.Texts'te ver, duz metin yazilirsa deploy onu anahtara cevirip TR'yi metinden yazar (get_dev_guide 4.3.4.1). EditorScript'i elle yazma: Script Builder kurallarini ({v:1, rules:[...]}, get_dev_guide 5.3) ver, deploy kodu Script Builder dialogu ile birebir ayni uretir; govde yazsan da atilir. Onay kutusu alanina setValue text 'true'/'false' (0/1 cevrilir). set ile yazilan alanin kurallari da sirayla calisir (derinlik siniri yok); A->B->A dongusu kuran kural yazma, calisma aninda hata olarak gosterilir. Wizard girdisinde taninmayan anahtar adiyla reddedilir, baska adla tekrar deneme; bolumler kismi olabilir (yalnizca degisen anahtarlar), Fields ogeleri FieldName ile eslesir, *Json adlari (FormFieldsDefaultValueJson) *Dto karsiligina cevrilir. Guncellemede get_wizard ciktisini butunuyle degistirip gonder: tasarimci ayarlari korunur; Wizard bolumunde yazdigin cevap ayni ayarin ListForm'daki eski degerine ustun gelir. Kimlik alanlari (ListFormCode, Name, Title, Description, PermissionDto, FieldName) wizard'dan gelir. Fields'taki her alan Wizard Groups'ta da bulunmalidir; tanınmayan ya da hesaplanan bir anahtar reddedilir. Liste ekraninda DescTr ve DescEn ayrintili olmali (en az 3 satir, 150 karakter): ekran basligindaki bilgi simgesinde gosterilir; ne ise yaradigi, menude nerede ya da hangi ekranin sekmesi oldugu, ana alanlar, otomatik hesaplar ve iliskili ekranlar yazilir (get_dev_guide 4.4.2); kisa aciklama reddedilir. Deploy modulun dokumanini (docs/modules) acar ya da envanterini yeniler; elle yazilan bolumleri save_module_doc ile doldur. Ne yazacagini get_dev_guide 4. bolumunden ogren.")] [Description("Wizard'i deploy eder: ListForm ekrani (ComponentKind 0) ya da bir custom component'i menuye baglayan kayit (ComponentKind 1), yetkileri, dil anahtarlari ve menusu seed dosyalarina yazilir. Deploy veritabanina yazmaz: ekran, menu ve yetkiler kullanici Wizard Manager'da DB Migrate calistirinca olusur; cikistaki isPending true iken ekran henuz yoktur; migrate'i bekleme, isini bitir ve kullaniciya yalnizca DB Migrate calistirmasini soyle. Eksik modul, ust menu, izin grubu, yetki ve dil satirlari da deploy'da dosyalara yazilir ve migrate onlari dogru sirayla ekler; bunlarin veritabaninda olmamasi beklenen durumdur, uyari olarak raporlama ve kullanicidan elle eklemesini isteme. Ust menu zinciri istenen derinlikte kurulur: MenuParentCode ekranin dogrudan ust menusu, Menu.MenuParentPath onun ustundeki seviyeler (kokten asagi, her biri {Code, Icon, En, Tr}); eksik her seviye bir ustunun altina kurulur, var olana baglanilir. Liste ekraninda Grid, Card, Pivot ve Chart gorunumleri varsayilan aciktir (DefaultLayout grid); kullanici bir gorunumu istemedigini soylemedikce bu bayraklari false yazma. Hedef tablo onceden var olmalidir; yoksa once create_table ile olustur. Istek ornek/demo veri tasiyorsa (prompt'taki tablo/liste ya da ekran goruntusunde gorunen satirlar) SeedData true yaz ve satirlarin hepsini ayni cagrida seedRows ile gonder: satirlar ekranin data/{ListFormCode}.json dosyasina yazilir, migrate ekrani kurduktan sonra ekler. SeedData ekrandan girilen kayitlari dosyaya yansitmaz (SeedSyncInsert/Update/Delete kapali); bunu yalnizca istek acikca isterse SeedSync true yaz. Satirlar icin migrate'i bekleyip insert_row kullanma. replaceFileName verilirse o wizard'in dosyalari yeniden yazilir, onceki kayitlarin yerini migrate'te yenileri alir. Wizard cevaplarinin uretmedigi ekran ayarlari (FormFieldsDefaultValueDto/InsertFieldsDefaultValueDto gibi varsayilan degerler, ValidationRuleDto, Format, Alignment, Width, Visible, BandName, ozet/gruplama, ExtraFilter, Insert/Update/Delete komutlari, PageSize, CommandColumnDto …) ayni JSON'un ListForm ve Fields bolumlerine get_wizard ciktisindaki adlarla yazilir. Yeni kaydin form degeri (Aktif = true, Birim = Adet, Kur = TRY, Sure = 60) ayri yazilmaz: deploy kolonun tablodaki sabit DEFAULT degerini okur; tablodan farkli deger gerekiyorsa alan ogesinde DefaultValue yaz (Groups[].Items[].DefaultValue). StaticData lookup seceneklerinde (RadioGroup, SelectBox, TagBox) name dil anahtaridir (App.StaticLookup.{Key}); EN + TR metni bilinen secenegi Language.Texts'te ver, duz metin yazilirsa deploy onu anahtara cevirip TR'yi metinden yazar (get_dev_guide 4.3.4.1). EditorScript'i elle yazma: Script Builder kurallarini ({v:1, rules:[...]}, get_dev_guide 5.3) ver, deploy kodu Script Builder dialogu ile birebir ayni uretir; govde yazsan da atilir. Onay kutusu alanina setValue text 'true'/'false' (0/1 cevrilir). set ile yazilan alanin kurallari da sirayla calisir (derinlik siniri yok); A->B->A dongusu kuran kural yazma, calisma aninda hata olarak gosterilir. Wizard girdisinde taninmayan anahtar adiyla reddedilir, baska adla tekrar deneme; bolumler kismi olabilir (yalnizca degisen anahtarlar), Fields ogeleri FieldName ile eslesir, *Json adlari (FormFieldsDefaultValueJson) *Dto karsiligina cevrilir. Guncellemede get_wizard ciktisini butunuyle degistirip gonder: tasarimci ayarlari korunur; Wizard bolumunde yazdigin cevap ayni ayarin ListForm'daki eski degerine ustun gelir. Kimlik alanlari (ListFormCode, Name, Title, Description, PermissionDto, FieldName) wizard'dan gelir. Fields'taki her alan Wizard Groups'ta da bulunmalidir; tanınmayan ya da hesaplanan bir anahtar reddedilir. Liste ekraninda DescTr ve DescEn ayrintili olmali (en az 3 satir, 150 karakter): ekran basligindaki bilgi simgesinde gosterilir; ne ise yaradigi, menude nerede ya da hangi ekranin sekmesi oldugu, ana alanlar, otomatik hesaplar ve iliskili ekranlar yazilir (get_dev_guide 4.4.2); kisa aciklama reddedilir. Deploy modulun dokumanini (docs/modules) acar ya da envanterini yeniler; elle yazilan bolumleri save_module_doc ile doldur. Ne yazacagini get_dev_guide 4. bolumunden ogren.")]
public async Task<WizardFileInfoDto?> DeployWizardAsync( public async Task<CallToolResult> DeployWizardAsync(
[Description("Wizard JSON'u: seed dosyasi bicimi ({\"Wizard\": {...}, \"ListForm\": {...}, \"Fields\": [...]}, get_wizard cikisi gibi) ya da duz wizard cevaplari. ListForm/Fields istege baglidir ve kismi olabilir. Enum alanlari sayi olarak yazilir.")] [Description("Wizard JSON'u: seed dosyasi bicimi ({\"Wizard\": {...}, \"ListForm\": {...}, \"Fields\": [...]}, get_wizard cikisi gibi) ya da duz wizard cevaplari. ListForm/Fields istege baglidir ve kismi olabilir. Enum alanlari sayi olarak yazilir.")]
string wizard, string wizard,
[Description("Guncellemede yerine gecilecek dosyanin adi (list_wizards cikisindaki fileName). Yeni wizard icin bos birak.")] [Description("Guncellemede yerine gecilecek dosyanin adi (list_wizards cikisindaki fileName). Yeni wizard icin bos birak.")]
string? replaceFileName = null, string? replaceFileName = null,
[Description("Demo satirlar: JSON nesne dizisi, anahtarlar ekranin alan adlari ([{\"Name\": \"...\"}]). Guid anahtar, TenantId ve audit alanlarini yazma; sunucu ve migrate doldurur. Yalnizca SeedData true olan liste ekraninda.")] [Description("Demo satirlar: JSON nesne dizisi, anahtarlar ekranin alan adlari ([{\"Name\": \"...\"}]). Guid anahtar, TenantId ve audit alanlarini yazma; sunucu ve migrate doldurur. Yalnizca SeedData true olan liste ekraninda.")]
string? seedRows = null) string? seedRows = null)
=> await developAppService.DeployWizardAsync(new McpWizardDeployRequestDto { Wizard = wizard, ReplaceFileName = replaceFileName, SeedRows = seedRows }); => McpToolResult.From(await developAppService.DeployWizardAsync(new McpWizardDeployRequestDto { Wizard = wizard, ReplaceFileName = replaceFileName, SeedRows = seedRows }));
[McpServerTool(Name = McpToolNames.DeleteWizard, Destructive = true, Idempotent = true)] [McpServerTool(Name = McpToolNames.DeleteWizard, Destructive = true, Idempotent = true)]
[Description("Bir wizard'i ve yalnizca onun urettigi kayitlari siler; baska yerde kullanilan kayitlar korunur ve raporda gosterilir. Ekranin veri aynasi (data/{ListFormCode}.json) ekranla birlikte gider. Bagimli wizard'i olan bir wizard silinemez, once bagimlilar silinir. Ekranin tablosu varsayilan olarak kalir; dropTables true ise tablo DROP edilir, CRUD endpoint'leri ve sql/object + crud seed dosyalari da silinir. Tablo yalnizca create_table ya da tablo tasarimcisiyla olusturulmussa (sql/object scripti varsa) ve baska ekran, Custom Endpoint ya da bilesen kullanmiyorsa dusurulur; aksi halde raporda neden korundugu yazar. Tablo silmek geri alinamaz ve App.SqlQueryManager yetkisi ister: dropTables true gondermeden once kullaniciya tablonun ve icindeki verinin silinecegini soyle ve acik onay al.")] [Description("Bir wizard'i ve yalnizca onun urettigi kayitlari siler; baska yerde kullanilan kayitlar korunur ve raporda gosterilir. Ekranin veri aynasi (data/{ListFormCode}.json) ekranla birlikte gider. Bagimli wizard'i olan bir wizard silinemez, once bagimlilar silinir. Ekranin tablosu varsayilan olarak kalir; dropTables true ise tablo DROP edilir, CRUD endpoint'leri ve sql/object + crud seed dosyalari da silinir. Tablo yalnizca create_table ya da tablo tasarimcisiyla olusturulmussa (sql/object scripti varsa) ve baska ekran, Custom Endpoint ya da bilesen kullanmiyorsa dusurulur; aksi halde raporda neden korundugu yazar. Tablo silmek geri alinamaz ve App.SqlQueryManager yetkisi ister: dropTables true gondermeden once kullaniciya tablonun ve icindeki verinin silinecegini soyle ve acik onay al.")]
public async Task<WizardDeleteResultDto> DeleteWizardAsync( public async Task<CallToolResult> DeleteWizardAsync(
[Description("Silinecek wizard dosyasinin adi (list_wizards cikisindaki fileName).")] [Description("Silinecek wizard dosyasinin adi (list_wizards cikisindaki fileName).")]
string fileName, string fileName,
[Description("Ekranin tablosu da dusurulsun mu. Yalnizca kullanici acikca onayladiysa true.")] [Description("Ekranin tablosu da dusurulsun mu. Yalnizca kullanici acikca onayladiysa true.")]
bool dropTables = false) bool dropTables = false)
=> await developAppService.DeleteWizardAsync(fileName, dropTables); => McpToolResult.From(await developAppService.DeleteWizardAsync(fileName, dropTables));
[McpServerTool(Name = McpToolNames.VerifyWizard, ReadOnly = true)] [McpServerTool(Name = McpToolNames.VerifyWizard, ReadOnly = true)]
[Description("Deploy edilip DB Migrate ile kurulmus bir wizard'i veri yazmadan dogrular (migrate bekleyen wizard'da install kontrolu basarisiz doner ve diger kontroller yapilmaz): menu kaydi (var mi, etkin mi, URL'si wizard'la ayni mi), menunun istedigi yetki tanimi, liste ekraninda ekranin sorgusu (cagiranin yetkisiyle tek satir), Custom yolunda bilesen (var mi, etkin mi, rotasi menuyle ayni mi) ve seed kapanisi (migrate ya da baska ortamda eksik kalacak bagimlilik). passed yalnizca blocking kontrollerden biri kalirsa false olur; seed_closure engellemez ama raporlanmalidir. Istege baglidir: kullanici migrate sonrasi dogrulama isterse cagir; gelistirme talebi dongusunun adimi degildir.")] [Description("Deploy edilip DB Migrate ile kurulmus bir wizard'i veri yazmadan dogrular (migrate bekleyen wizard'da install kontrolu basarisiz doner ve diger kontroller yapilmaz): menu kaydi (var mi, etkin mi, URL'si wizard'la ayni mi), menunun istedigi yetki tanimi, liste ekraninda ekranin sorgusu (cagiranin yetkisiyle tek satir), Custom yolunda bilesen (var mi, etkin mi, rotasi menuyle ayni mi) ve seed kapanisi (migrate ya da baska ortamda eksik kalacak bagimlilik). passed yalnizca blocking kontrollerden biri kalirsa false olur; seed_closure engellemez ama raporlanmalidir. Istege baglidir: kullanici migrate sonrasi dogrulama isterse cagir; gelistirme talebi dongusunun adimi degildir.")]
public async Task<McpVerifyResultDto> VerifyWizardAsync( public async Task<CallToolResult> VerifyWizardAsync(
[Description("Dogrulanacak wizard dosyasinin adi (list_wizards ya da deploy_wizard cikisindaki fileName).")] [Description("Dogrulanacak wizard dosyasinin adi (list_wizards ya da deploy_wizard cikisindaki fileName).")]
string fileName) string fileName)
=> await developAppService.VerifyWizardAsync(fileName); => McpToolResult.From(await developAppService.VerifyWizardAsync(fileName));
[McpServerTool(Name = McpToolNames.StartDevRequest, Destructive = false, Idempotent = false)] [McpServerTool(Name = McpToolNames.StartDevRequest, Destructive = false, Idempotent = false)]
[Description("App.DevRequests kuyrugundaki bir gelistirme talebini ustlenir: talep Ready to Start durumundaysa In Progress'e alir ve talebin metnini (baslik, HTML aciklama, onaylanmis HTML analiz, oncelik, etiketler, SubTasks) dondurur; baska bir durumdaysa reddeder. Aciklamaya yapistirilmis gorseller ayri resim icerigi olarak gelir, aciklamada yerlerinde [image-N] isareti durur; gorseldeki ekran duzeni ve satirlar talebin parcasidir (demo veri icin deploy_wizard SeedData). Uretim aciklama ile analize birlikte dayanir; analizdeki menu, tablo ve kapsam kararlari onaylanmistir. Bir talep uzerinde calismaya bu aracla baslanir, Status'u update_row ile In Progress yapma. Sonraki adimlar icin get_dev_guide lowcode 0.11.")] [Description("App.DevRequests kuyrugundaki bir gelistirme talebini ustlenir: talep Ready to Start durumundaysa In Progress'e alir ve talebin metnini (baslik, HTML aciklama, onaylanmis HTML analiz, oncelik, etiketler, SubTasks) dondurur; baska bir durumdaysa reddeder. Aciklamaya yapistirilmis gorseller ayri resim icerigi olarak gelir, aciklamada yerlerinde [image-N] isareti durur; gorseldeki ekran duzeni ve satirlar talebin parcasidir (demo veri icin deploy_wizard SeedData). Uretim aciklama ile analize birlikte dayanir; analizdeki menu, tablo ve kapsam kararlari onaylanmistir. Bir talep uzerinde calismaya bu aracla baslanir, Status'u update_row ile In Progress yapma. Sonraki adimlar icin get_dev_guide lowcode 0.11.")]
public async Task<IList<ContentBlock>> StartDevRequestAsync( public async Task<CallToolResult> StartDevRequestAsync(
[Description("Talebin Id'si (GUID) ya da tam basligi. Ayni basligi tasiyan birden fazla talep varsa Id ver.")] [Description("Talebin Id'si (GUID) ya da tam basligi. Ayni basligi tasiyan birden fazla talep varsa Id ver.")]
string request) string request)
=> ToContent(await developAppService.StartDevRequestAsync(request)); => McpToolResult.From(await developAppService.StartDevRequestAsync(request), ToContent);
[McpServerTool(Name = McpToolNames.GetDevRequest, ReadOnly = true)] [McpServerTool(Name = McpToolNames.GetDevRequest, ReadOnly = true)]
[Description("Bir gelistirme talebini durumuna dokunmadan okur; start_dev_request ile ayni cevabi (aciklama + ayri resim icerikleri) dondurur. Analiz oncesi talebi okumak, yarim kalmis (In Progress/Testing) bir talebe donmek ya da gorselleri yeniden gormek icin kullan; talebi ustlenmek icin degil.")] [Description("Bir gelistirme talebini durumuna dokunmadan okur; start_dev_request ile ayni cevabi (aciklama + ayri resim icerikleri) dondurur. Analiz oncesi talebi okumak, yarim kalmis (In Progress/Testing) bir talebe donmek ya da gorselleri yeniden gormek icin kullan; talebi ustlenmek icin degil.")]
public async Task<IList<ContentBlock>> GetDevRequestAsync( public async Task<CallToolResult> GetDevRequestAsync(
[Description("Talebin Id'si (GUID) ya da tam basligi.")] [Description("Talebin Id'si (GUID) ya da tam basligi.")]
string request) string request)
=> ToContent(await developAppService.GetDevRequestAsync(request)); => McpToolResult.From(await developAppService.GetDevRequestAsync(request), ToContent);
[McpServerTool(Name = McpToolNames.SaveDevRequestAnalysis, Destructive = true, Idempotent = true)] [McpServerTool(Name = McpToolNames.SaveDevRequestAnalysis, Destructive = true, Idempotent = true)]
[Description("Backlog, Analysis ya da Ready to Start durumundaki bir talebin analizini (HTML) Analysis alanina yazar; var olan analiz degistirilir, Description'a dokunulmaz. productionReady true ise talep Ready to Start'a, false ise Analysis durumuna alinir. Diger durumlarda reddeder. Once get_dev_request ile talebi ve gorsellerini oku; analizin bolumleri ve standardi get_dev_guide lowcode 0.12'dedir.")] [Description("Backlog, Analysis ya da Ready to Start durumundaki bir talebin analizini (HTML) Analysis alanina yazar; var olan analiz degistirilir, Description'a dokunulmaz. productionReady true ise talep Ready to Start'a, false ise Analysis durumuna alinir. Diger durumlarda reddeder. Once get_dev_request ile talebi ve gorsellerini oku; analizin bolumleri ve standardi get_dev_guide lowcode 0.12'dedir.")]
public async Task<IList<ContentBlock>> SaveDevRequestAnalysisAsync( public async Task<CallToolResult> SaveDevRequestAnalysisAsync(
[Description("Talebin Id'si (GUID) ya da tam basligi.")] [Description("Talebin Id'si (GUID) ya da tam basligi.")]
string request, string request,
[Description("Analizin tamami, HTML.")] [Description("Analizin tamami, HTML.")]
string html, string html,
[Description("Analizin 'Uretime hazir' satiri kosulsuz Evet ise true; sartli (onay bekleyen, 'Evet, ancak …') ya da Hayir ise false. HTML'deki satirla ayni olmak zorundadir.")] [Description("Analizin 'Uretime hazir' satiri kosulsuz Evet ise true; sartli (onay bekleyen, 'Evet, ancak …') ya da Hayir ise false. HTML'deki satirla ayni olmak zorundadir.")]
bool productionReady) bool productionReady)
=> ToContent(await developAppService.SaveDevRequestAnalysisAsync(request, html, productionReady)); => McpToolResult.From(await developAppService.SaveDevRequestAnalysisAsync(request, html, productionReady), ToContent);
[McpServerTool(Name = McpToolNames.AppendDevRequestNote, Destructive = false, Idempotent = false)] [McpServerTool(Name = McpToolNames.AppendDevRequestNote, Destructive = false, Idempotent = false)]
[Description("Talebin Analysis alaninin sonuna HTML ekler; var olan analize ve Description'a dokunmaz. Description talebin yazildigi haliyle kalir; uretim sonuc notunu (<h4>MCP</h4> + uretilenler, varsayimlar ve DB Migrate bekleyen wizard'lar) bu aracla ekle. Description'i ya da Analysis'i update_row ile yazma.")] [Description("Talebin Analysis alaninin sonuna HTML ekler; var olan analize ve Description'a dokunmaz. Description talebin yazildigi haliyle kalir; uretim sonuc notunu (<h4>MCP</h4> + uretilenler, varsayimlar ve DB Migrate bekleyen wizard'lar) bu aracla ekle. Description'i ya da Analysis'i update_row ile yazma.")]
public async Task<string> AppendDevRequestNoteAsync( public async Task<CallToolResult> AppendDevRequestNoteAsync(
[Description("Talebin Id'si (GUID) ya da tam basligi.")] [Description("Talebin Id'si (GUID) ya da tam basligi.")]
string request, string request,
[Description("Eklenecek HTML parcasi.")] [Description("Eklenecek HTML parcasi.")]
string html) string html)
{ => McpToolResult.From(await developAppService.AppendDevRequestNoteAsync(request, html));
await developAppService.AppendDevRequestNoteAsync(request, html);
return "ok";
}
[McpServerTool(Name = McpToolNames.AddDevRequestSubTasks, Destructive = false, Idempotent = false)] [McpServerTool(Name = McpToolNames.AddDevRequestSubTasks, Destructive = false, Idempotent = false)]
[Description("Talebin Sub Tasks listesine uretilen her artefakt icin tamamlanmis bir satir ekler; atanan kisi baglanan kullanici, bitis tarihi bugun olarak sunucuda yazilir, var olan satirlar korunur. SubTasks alanini update_row ile yazma.")] [Description("Talebin Sub Tasks listesine uretilen her artefakt icin tamamlanmis bir satir ekler; atanan kisi baglanan kullanici, bitis tarihi bugun olarak sunucuda yazilir, var olan satirlar korunur. SubTasks alanini update_row ile yazma.")]
public async Task<string> AddDevRequestSubTasksAsync( public async Task<CallToolResult> AddDevRequestSubTasksAsync(
[Description("Talebin Id'si (GUID) ya da tam basligi.")] [Description("Talebin Id'si (GUID) ya da tam basligi.")]
string request, string request,
[Description("Eklenecek satir basliklari, ornegin [\"create_table: Hr_T_Certificate\", \"deploy_wizard: Certificates.json\"].")] [Description("Eklenecek satir basliklari, ornegin [\"create_table: Hr_T_Certificate\", \"deploy_wizard: Certificates.json\"].")]
List<string> titles) List<string> titles)
{ => McpToolResult.From(await developAppService.AddDevRequestSubTasksAsync(request, titles));
await developAppService.AddDevRequestSubTasksAsync(request, titles);
return "ok";
}
[McpServerTool(Name = McpToolNames.GetModuleDoc, ReadOnly = true)] [McpServerTool(Name = McpToolNames.GetModuleDoc, ReadOnly = true)]
[Description("Modul dokumanini (docs/modules/{Menu}.md, seed klasoru; kok menu basina tek dosya, adi kok menu kodu: App.Hr, App.Prc, App.Administration) okur: nasil calistigi, analiz/kararlar, ekranlar, hesaplar, yetkiler ve otomatik 'Ek — Ekran envanteri'. Menunun altindaki her dal dosyada bir bolumdur; ekranin ya da dalin kodu (App.Hr.PerformanceReview, App.Hr.Performance) ya da wizard dosya adi verilirse yalnizca o bolum ve envanteri doner, dosya adi verilirse dosyanin tamami. isComplete false ise missingSections doldurulmamis alt bolumlerdir. 'Bu modul/ekran nasil calisiyor?' sorusunda once bunu ekran koduyla oku; deploy_wizard dosyayi ve dalin bolumunu yoksa kendisi acar. Menude henuz olmayan yeni modulun kodu (App.Inventory, App.Inventory.Stock) hata vermez: exists false ve doldurulacak iskelet doner; save_module_doc ayni kodla yazinca dosya acilir.")] [Description("Modul dokumanini (docs/modules/{Menu}.md, seed klasoru; kok menu basina tek dosya, adi kok menu kodu: App.Hr, App.Prc, App.Administration) okur: nasil calistigi, analiz/kararlar, ekranlar, hesaplar, yetkiler ve otomatik 'Ek — Ekran envanteri'. Menunun altindaki her dal dosyada bir bolumdur; ekranin ya da dalin kodu (App.Hr.PerformanceReview, App.Hr.Performance) ya da wizard dosya adi verilirse yalnizca o bolum ve envanteri doner, dosya adi verilirse dosyanin tamami. isComplete false ise missingSections doldurulmamis alt bolumlerdir. 'Bu modul/ekran nasil calisiyor?' sorusunda once bunu ekran koduyla oku; deploy_wizard dosyayi ve dalin bolumunu yoksa kendisi acar. Menude henuz olmayan yeni modulun kodu (App.Inventory, App.Inventory.Stock) hata vermez: exists false ve doldurulacak iskelet doner; save_module_doc ayni kodla yazinca dosya acilir.")]
public async Task<ModuleDocDto> GetModuleDocAsync( public async Task<CallToolResult> GetModuleDocAsync(
[Description("Ekran ya da dal kodu, wizard dosya adi (o bolum) veya kok menu kodu (App.Hr; dosyanin tamami).")] [Description("Ekran ya da dal kodu, wizard dosya adi (o bolum) veya kok menu kodu (App.Hr; dosyanin tamami).")]
string name) string name)
=> await developAppService.GetModuleDocAsync(name); => McpToolResult.From(await developAppService.GetModuleDocAsync(name));
[McpServerTool(Name = McpToolNames.SaveModuleDoc, Destructive = true, Idempotent = true)] [McpServerTool(Name = McpToolNames.SaveModuleDoc, Destructive = true, Idempotent = true)]
[Description("Modul dokumaninin elle yazilan bolumunu kaydeder (Markdown). Uretimin parcasidir: deploy_wizard'dan sonra get_module_doc ile ekranin bolumunu oku, module-doc:todo isaretli her alt bolumu (Amac ve kapsam, Analiz ve kararlar — onaylanmis analiz ve kararlar, Menu ve ekranlar, Veri modeli, Is kurallari ve hesaplar, Yetki ve gorunurluk, Entegrasyonlar, Akis ornegi, Sik sorulanlar, Dosyalar ve geri alma) doldurup isaretleri kaldirarak ayni ekran ya da dal koduyla gonder: yalnizca o bolum yazilir, diger dallara dokunulmaz. Bos kalacak alt bolume 'Yok.' yaz. 'Ek — Ekran envanteri' bolumunu gonderme, sunucu uretir. Bolumun '<!-- module-doc:section ... claims=... -->' beyan satirini gondermezsen mevcut beyan korunur. Kok menu koduyla (App.Hr) cagrilirsa dosyanin tamami yazilir. Talep, bolumu tamamlanmadan update_row ile Testing'e alinamaz. Bolum standardi get_dev_guide lowcode 0.13.")] [Description("Modul dokumaninin elle yazilan bolumunu kaydeder (Markdown). Uretimin parcasidir: deploy_wizard'dan sonra get_module_doc ile ekranin bolumunu oku, module-doc:todo isaretli her alt bolumu (Amac ve kapsam, Analiz ve kararlar — onaylanmis analiz ve kararlar, Menu ve ekranlar, Veri modeli, Is kurallari ve hesaplar, Yetki ve gorunurluk, Entegrasyonlar, Akis ornegi, Sik sorulanlar, Dosyalar ve geri alma) doldurup isaretleri kaldirarak ayni ekran ya da dal koduyla gonder: yalnizca o bolum yazilir, diger dallara dokunulmaz. Bos kalacak alt bolume 'Yok.' yaz. 'Ek — Ekran envanteri' bolumunu gonderme, sunucu uretir. Bolumun '<!-- module-doc:section ... claims=... -->' beyan satirini gondermezsen mevcut beyan korunur. Kok menu koduyla (App.Hr) cagrilirsa dosyanin tamami yazilir. Talep, bolumu tamamlanmadan update_row ile Testing'e alinamaz. Bolum standardi get_dev_guide lowcode 0.13.")]
public async Task<ModuleDocDto> SaveModuleDocAsync( public async Task<CallToolResult> SaveModuleDocAsync(
[Description("Ekran ya da dal kodu, wizard dosya adi (o bolum) veya kok menu kodu (App.Hr; dosyanin tamami).")] [Description("Ekran ya da dal kodu, wizard dosya adi (o bolum) veya kok menu kodu (App.Hr; dosyanin tamami).")]
string name, string name,
[Description("Bolumun (ya da dosya adi verildiyse dosyanin) tamami, Markdown.")] [Description("Bolumun (ya da dosya adi verildiyse dosyanin) tamami, Markdown.")]
string content) string content)
=> await developAppService.SaveModuleDocAsync(name, content); => McpToolResult.From(await developAppService.SaveModuleDocAsync(name, content));
/// <summary> /// <summary>
/// Talep metni JSON olarak, gorseller ayri resim icerigi olarak doner; model resmi ancak bu /// Talep metni JSON olarak, gorseller ayri resim icerigi olarak doner; model resmi ancak bu
@ -233,21 +227,21 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService, IMc
[McpServerTool(Name = McpToolNames.ListComponents, ReadOnly = true)] [McpServerTool(Name = McpToolNames.ListComponents, ReadOnly = true)]
[Description("Custom component'leri ad, rota ve aciklamasiyla listeler; kod dondurmez. isVisualDesigner true olanlarin kodu tasarimci belgesinden uretilir ve MCP ile degistirilemez.")] [Description("Custom component'leri ad, rota ve aciklamasiyla listeler; kod dondurmez. isVisualDesigner true olanlarin kodu tasarimci belgesinden uretilir ve MCP ile degistirilemez.")]
public async Task<List<McpComponentSummaryDto>> ListComponentsAsync( public async Task<CallToolResult> ListComponentsAsync(
[Description("Ad, rota ya da aciklama icinde aranacak metin. Bos birakilabilir.")] [Description("Ad, rota ya da aciklama icinde aranacak metin. Bos birakilabilir.")]
string? search = null) string? search = null)
=> await developAppService.GetComponentsAsync(search); => McpToolResult.From(await developAppService.GetComponentsAsync(search));
[McpServerTool(Name = McpToolNames.GetComponent, ReadOnly = true)] [McpServerTool(Name = McpToolNames.GetComponent, ReadOnly = true)]
[Description("Bir custom component'in kodunu, props'unu ve bagimliliklarini dondurur. Degistirmeden once oku.")] [Description("Bir custom component'in kodunu, props'unu ve bagimliliklarini dondurur. Degistirmeden once oku.")]
public async Task<CustomComponentDto?> GetComponentAsync( public async Task<CallToolResult> GetComponentAsync(
[Description("Bilesenin adi.")] [Description("Bilesenin adi.")]
string name) string name)
=> await developAppService.GetComponentAsync(name); => McpToolResult.From(await developAppService.GetComponentAsync(name));
[McpServerTool(Name = McpToolNames.SaveComponent, Destructive = true, Idempotent = true)] [McpServerTool(Name = McpToolNames.SaveComponent, Destructive = true, Idempotent = true)]
[Description("Custom component'i ada gore kaydeder: yoksa ekler, varsa gunceller. Guncellemede verilmeyen alanlar korunur. Kod kurallari ve kullanilabilir kutuphaneler icin get_dev_guide 7. bolumu. Bilesen menude gorunecekse ardindan ComponentKind 1 ile deploy_wizard cagir.")] [Description("Custom component'i ada gore kaydeder: yoksa ekler, varsa gunceller. Guncellemede verilmeyen alanlar korunur. Kod kurallari ve kullanilabilir kutuphaneler icin get_dev_guide 7. bolumu. Bilesen menude gorunecekse ardindan ComponentKind 1 ile deploy_wizard cagir.")]
public async Task<CustomComponentDto> SaveComponentAsync( public async Task<CallToolResult> SaveComponentAsync(
[Description("Bilesenin adi; kimligidir.")] [Description("Bilesenin adi; kimligidir.")]
string name, string name,
[Description("Bilesenin React kodu. Yeni bilesende zorunlu, guncellemede bos birakilirsa mevcut kod korunur.")] [Description("Bilesenin React kodu. Yeni bilesende zorunlu, guncellemede bos birakilirsa mevcut kod korunur.")]
@ -262,7 +256,7 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService, IMc
string? dependencies = null, string? dependencies = null,
[Description("Bilesen etkin mi. Yeni bilesende varsayilan true.")] [Description("Bilesen etkin mi. Yeni bilesende varsayilan true.")]
bool? isActive = null) bool? isActive = null)
=> await developAppService.SaveComponentAsync(new McpComponentSaveRequestDto => McpToolResult.From(await developAppService.SaveComponentAsync(new McpComponentSaveRequestDto
{ {
Name = name, Name = name,
Code = code, Code = code,
@ -271,12 +265,12 @@ public sealed class McpDevelopTools(IMcpDevelopAppService developAppService, IMc
Props = props, Props = props,
Dependencies = dependencies, Dependencies = dependencies,
IsActive = isActive IsActive = isActive
}); }));
[McpServerTool(Name = McpToolNames.DeleteComponent, Destructive = true, Idempotent = false)] [McpServerTool(Name = McpToolNames.DeleteComponent, Destructive = true, Idempotent = false)]
[Description("Custom component'i siler. Bilesene bagli bir Custom wizard varsa once delete_wizard ile o silinir.")] [Description("Custom component'i siler. Bilesene bagli bir Custom wizard varsa once delete_wizard ile o silinir.")]
public async Task DeleteComponentAsync( public async Task<CallToolResult> DeleteComponentAsync(
[Description("Bilesenin adi.")] [Description("Bilesenin adi.")]
string name) string name)
=> await developAppService.DeleteComponentAsync(name); => McpToolResult.From(await developAppService.DeleteComponentAsync(name));
} }

View file

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Threading.Tasks; using System.Threading.Tasks;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server; using ModelContextProtocol.Server;
using Sozsoft.Mcp.Dtos; using Sozsoft.Mcp.Dtos;
@ -12,7 +13,7 @@ public sealed class McpQueryTools(IMcpQueryAppService queryAppService)
{ {
[McpServerTool(Name = McpToolNames.QueryList, ReadOnly = true)] [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.")] [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( public async Task<CallToolResult> QueryListAsync(
[Description("Listenin kodu; list_lists cikisindaki listFormCode degeri.")] [Description("Listenin kodu; list_lists cikisindaki listFormCode degeri.")]
string listFormCode, string listFormCode,
[Description("Filtre, JSON metni. Ornek: [\"Status\",\"=\",\"Open\"] ya da [[\"Amount\",\">\",100],\"and\",[\"Name\",\"contains\",\"abc\"]]. Operatorler: =, <>, >, >=, <, <=, contains, notcontains, startswith, endswith.")] [Description("Filtre, JSON metni. Ornek: [\"Status\",\"=\",\"Open\"] ya da [[\"Amount\",\">\",100],\"and\",[\"Name\",\"contains\",\"abc\"]]. Operatorler: =, <>, >, >=, <, <=, contains, notcontains, startswith, endswith.")]
@ -29,7 +30,7 @@ public sealed class McpQueryTools(IMcpQueryAppService queryAppService)
int skip = 0, int skip = 0,
[Description("Istenen kayit sayisi; sunucu kaydindaki tavandan buyuk olamaz.")] [Description("Istenen kayit sayisi; sunucu kaydindaki tavandan buyuk olamaz.")]
int take = 0) int take = 0)
=> await queryAppService.QueryListAsync(new McpListQueryRequestDto => McpToolResult.From(await queryAppService.QueryListAsync(new McpListQueryRequestDto
{ {
ListFormCode = listFormCode, ListFormCode = listFormCode,
Filter = filter, Filter = filter,
@ -39,34 +40,34 @@ public sealed class McpQueryTools(IMcpQueryAppService queryAppService)
TotalSummary = totalSummary, TotalSummary = totalSummary,
Skip = skip, Skip = skip,
Take = take Take = take
}); }));
[McpServerTool(Name = McpToolNames.RunSelect, ReadOnly = true)] [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. Kolonun var oldugunu varsayma: IsDeleted, TenantId, IsActive her tabloda yoktur; emin degilsen once INFORMATION_SCHEMA.COLUMNS (PostgreSQL: information_schema.columns) uzerinden tablonun kolonlarini oku, filtreyi yalnizca var olan kolona yaz.")] [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. Kolonun var oldugunu varsayma: IsDeleted, TenantId, IsActive her tabloda yoktur; emin degilsen once INFORMATION_SCHEMA.COLUMNS (PostgreSQL: information_schema.columns) uzerinden tablonun kolonlarini oku, filtreyi yalnizca var olan kolona yaz.")]
public async Task<McpQueryResultDto> RunSelectAsync( public async Task<CallToolResult> RunSelectAsync(
[Description("Tek bir SELECT (ya da WITH ... SELECT) ifadesi. Degerler @ad bicimindeki parametrelerle verilir.")] [Description("Tek bir SELECT (ya da WITH ... SELECT) ifadesi. Degerler @ad bicimindeki parametrelerle verilir.")]
string sql, string sql,
[Description("Sorgudaki adli parametreler: anahtar parametre adi, deger ise ham JSON degeri.")] [Description("Sorgudaki adli parametreler: anahtar parametre adi, deger ise ham JSON degeri.")]
Dictionary<string, object?>? parameters = null, Dictionary<string, object?>? parameters = null,
[Description("Istenen en fazla satir sayisi; sunucu kaydindaki tavandan buyuk olamaz.")] [Description("Istenen en fazla satir sayisi; sunucu kaydindaki tavandan buyuk olamaz.")]
int maxRows = 0) int maxRows = 0)
=> await queryAppService.RunSelectAsync(new McpSelectRequestDto => McpToolResult.From(await queryAppService.RunSelectAsync(new McpSelectRequestDto
{ {
Sql = sql, Sql = sql,
Parameters = parameters ?? [], Parameters = parameters ?? [],
MaxRows = maxRows MaxRows = maxRows
}); }));
[McpServerTool(Name = McpToolNames.ListQueries, ReadOnly = true)] [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.")] [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( public async Task<CallToolResult> ListQueriesAsync(
[Description("Nesne adi ya da aciklamasi icinde aranacak metin. Bos birakilabilir.")] [Description("Nesne adi ya da aciklamasi icinde aranacak metin. Bos birakilabilir.")]
string? search = null) string? search = null)
=> await queryAppService.GetSavedQueriesAsync(search); => McpToolResult.From(await queryAppService.GetSavedQueriesAsync(search));
[McpServerTool(Name = McpToolNames.RunQuery, ReadOnly = true)] [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.")] [Description("Kayitli bir nesneyi ya da Custom Endpoint'i parametreleriyle cagirir. Nesne turu ve adi list_queries cikisindan alinir.")]
public async Task<McpQueryResultDto> RunQueryAsync( public async Task<CallToolResult> RunQueryAsync(
[Description("Nesne turu: view, procedure, tableFunction, scalarFunction ya da customEndpoint.")] [Description("Nesne turu: view, procedure, tableFunction, scalarFunction ya da customEndpoint.")]
string kind, string kind,
[Description("Nesnenin ya da Custom Endpoint'in adi.")] [Description("Nesnenin ya da Custom Endpoint'in adi.")]
@ -77,12 +78,12 @@ public sealed class McpQueryTools(IMcpQueryAppService queryAppService)
Dictionary<string, object?>? parameters = null, Dictionary<string, object?>? parameters = null,
[Description("Istenen en fazla satir sayisi; sunucu kaydindaki tavandan buyuk olamaz.")] [Description("Istenen en fazla satir sayisi; sunucu kaydindaki tavandan buyuk olamaz.")]
int maxRows = 0) int maxRows = 0)
=> await queryAppService.RunSavedQueryAsync(new McpSavedQueryRequestDto => McpToolResult.From(await queryAppService.RunSavedQueryAsync(new McpSavedQueryRequestDto
{ {
Kind = kind, Kind = kind,
Name = name, Name = name,
SchemaName = schemaName, SchemaName = schemaName,
Parameters = parameters ?? [], Parameters = parameters ?? [],
MaxRows = maxRows MaxRows = maxRows
}); }));
} }

View file

@ -1,5 +1,6 @@
using System.ComponentModel; using System.ComponentModel;
using System.Threading.Tasks; using System.Threading.Tasks;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server; using ModelContextProtocol.Server;
using Sozsoft.Mcp.Dtos; using Sozsoft.Mcp.Dtos;
@ -15,37 +16,37 @@ public sealed class McpReportTools(IMcpReportAppService reportAppService)
{ {
[McpServerTool(Name = McpToolNames.ListReportTemplates, ReadOnly = true)] [McpServerTool(Name = McpToolNames.ListReportTemplates, ReadOnly = true)]
[Description("Rapor kategorilerini ve rapor sablonlarini listeler; HTML dondurmez. Sablon kaydederken kategori adi buradan secilir.")] [Description("Rapor kategorilerini ve rapor sablonlarini listeler; HTML dondurmez. Sablon kaydederken kategori adi buradan secilir.")]
public async Task<McpReportCatalogDto> ListReportTemplatesAsync( public async Task<CallToolResult> ListReportTemplatesAsync(
[Description("Sablon adi ya da aciklamasi icinde aranacak metin. Bos birakilabilir.")] [Description("Sablon adi ya da aciklamasi icinde aranacak metin. Bos birakilabilir.")]
string? search = null) string? search = null)
=> await reportAppService.GetReportTemplatesAsync(search); => McpToolResult.From(await reportAppService.GetReportTemplatesAsync(search));
[McpServerTool(Name = McpToolNames.GetReportTemplate, ReadOnly = true)] [McpServerTool(Name = McpToolNames.GetReportTemplate, ReadOnly = true)]
[Description("Bir rapor sablonunun HTML icerigini, yer tutucularini ve goruntuleme adresini dondurur. Degistirmeden once oku.")] [Description("Bir rapor sablonunun HTML icerigini, yer tutucularini ve goruntuleme adresini dondurur. Degistirmeden once oku.")]
public async Task<McpReportTemplateDto> GetReportTemplateAsync( public async Task<CallToolResult> GetReportTemplateAsync(
[Description("Sablonun Id'si ya da adi.")] [Description("Sablonun Id'si ya da adi.")]
string template) string template)
=> await reportAppService.GetReportTemplateAsync(template); => McpToolResult.From(await reportAppService.GetReportTemplateAsync(template));
[McpServerTool(Name = McpToolNames.SaveReportCategory, Destructive = true, Idempotent = true)] [McpServerTool(Name = McpToolNames.SaveReportCategory, Destructive = true, Idempotent = true)]
[Description("Rapor kategorisini ada gore kaydeder: yoksa ekler, varsa gunceller. Kayit bir ekran kaydindan basilan HTML sablonu ise var olan 'Raporlar' kategorisini kullan; yeni kategori yalnizca istenince acilir.")] [Description("Rapor kategorisini ada gore kaydeder: yoksa ekler, varsa gunceller. Kayit bir ekran kaydindan basilan HTML sablonu ise var olan 'Raporlar' kategorisini kullan; yeni kategori yalnizca istenince acilir.")]
public async Task<McpReportCategoryDto> SaveReportCategoryAsync( public async Task<CallToolResult> SaveReportCategoryAsync(
[Description("Kategorinin adi; kimligidir.")] [Description("Kategorinin adi; kimligidir.")]
string name, string name,
[Description("Aciklama.")] [Description("Aciklama.")]
string? description = null, string? description = null,
[Description("Ikon adi.")] [Description("Ikon adi.")]
string? icon = null) string? icon = null)
=> await reportAppService.SaveReportCategoryAsync(new McpReportCategorySaveRequestDto => McpToolResult.From(await reportAppService.SaveReportCategoryAsync(new McpReportCategorySaveRequestDto
{ {
Name = name, Name = name,
Description = description, Description = description,
Icon = icon Icon = icon
}); }));
[McpServerTool(Name = McpToolNames.SaveReportTemplate, Destructive = true, Idempotent = true)] [McpServerTool(Name = McpToolNames.SaveReportTemplate, Destructive = true, Idempotent = true)]
[Description("DynamicReport sablonunu ada gore kaydeder: yoksa ekler, varsa gunceller; guncellemede verilmeyen alanlar korunur. Sablon A4'e basilan HTML'dir (Standard Taahhutname gibi). listFormCode verilirse HTML icindeki {{AlanAdi}} yer tutuculari o ekranin alanlarina gore dogrulanir ve cevapta rowButton doner: bu butonu ekranin CommandColumnJson listesine ekle (deploy_wizard), VisibleExpression ile kosullandir. Sozlesme icin get_dev_guide 9.12.")] [Description("DynamicReport sablonunu ada gore kaydeder: yoksa ekler, varsa gunceller; guncellemede verilmeyen alanlar korunur. Sablon A4'e basilan HTML'dir (Standard Taahhutname gibi). listFormCode verilirse HTML icindeki {{AlanAdi}} yer tutuculari o ekranin alanlarina gore dogrulanir ve cevapta rowButton doner: bu butonu ekranin CommandColumnJson listesine ekle (deploy_wizard), VisibleExpression ile kosullandir. Sozlesme icin get_dev_guide 9.12.")]
public async Task<McpReportTemplateDto> SaveReportTemplateAsync( public async Task<CallToolResult> SaveReportTemplateAsync(
[Description("Sablonun adi; kimligidir.")] [Description("Sablonun adi; kimligidir.")]
string name, string name,
[Description("Kategori adi ya da Id'si. Yeni sablonda zorunlu; kayit HTML'i icin 'Raporlar'.")] [Description("Kategori adi ya da Id'si. Yeni sablonda zorunlu; kayit HTML'i icin 'Raporlar'.")]
@ -60,7 +61,7 @@ public sealed class McpReportTools(IMcpReportAppService reportAppService)
string? listFormCode = null, string? listFormCode = null,
[Description("Satir butonunun dil anahtari. Verilmezse App.Platform.View.")] [Description("Satir butonunun dil anahtari. Verilmezse App.Platform.View.")]
string? buttonText = null) string? buttonText = null)
=> await reportAppService.SaveReportTemplateAsync(new McpReportTemplateSaveRequestDto => McpToolResult.From(await reportAppService.SaveReportTemplateAsync(new McpReportTemplateSaveRequestDto
{ {
Name = name, Name = name,
Category = category, Category = category,
@ -69,12 +70,12 @@ public sealed class McpReportTools(IMcpReportAppService reportAppService)
Status = status, Status = status,
ListFormCode = listFormCode, ListFormCode = listFormCode,
ButtonText = buttonText ButtonText = buttonText
}); }));
[McpServerTool(Name = McpToolNames.DeleteReportTemplate, Destructive = true, Idempotent = false)] [McpServerTool(Name = McpToolNames.DeleteReportTemplate, Destructive = true, Idempotent = false)]
[Description("Rapor sablonunu siler. Sablonu acan satir butonlari ekranlardan ayrica kaldirilir.")] [Description("Rapor sablonunu siler. Sablonu acan satir butonlari ekranlardan ayrica kaldirilir.")]
public async Task DeleteReportTemplateAsync( public async Task<CallToolResult> DeleteReportTemplateAsync(
[Description("Sablonun Id'si ya da adi.")] [Description("Sablonun Id'si ya da adi.")]
string template) string template)
=> await reportAppService.DeleteReportTemplateAsync(template); => McpToolResult.From(await reportAppService.DeleteReportTemplateAsync(template));
} }

View file

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Threading.Tasks; using System.Threading.Tasks;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server; using ModelContextProtocol.Server;
using Sozsoft.Mcp.Dtos; using Sozsoft.Mcp.Dtos;
using Volo.Abp.MultiTenancy; using Volo.Abp.MultiTenancy;
@ -32,28 +33,28 @@ public sealed class McpSchemaTools(IMcpSchemaAppService schemaAppService, ICurre
[McpServerTool(Name = McpToolNames.ListLists, ReadOnly = true)] [McpServerTool(Name = McpToolNames.ListLists, ReadOnly = true)]
[Description("Kiracinin listelerini (ekranlarini) ad, baslik, aciklama ve arkasindaki tablo ile dondurur. Arama metni verilmezse tumu doner. Gelistiriciye (host, Wizard yetkisi) DB Migrate bekleyen wizard ekranlari (sourceKind pending) ve yalnizca seed scripti olan tablolar (pendingTable) da doner; lookup, widget, view ve Query SQL'ini yazarken kolon adlarini describe_list ile buradan al, tahmin etme.")] [Description("Kiracinin listelerini (ekranlarini) ad, baslik, aciklama ve arkasindaki tablo ile dondurur. Arama metni verilmezse tumu doner. Gelistiriciye (host, Wizard yetkisi) DB Migrate bekleyen wizard ekranlari (sourceKind pending) ve yalnizca seed scripti olan tablolar (pendingTable) da doner; lookup, widget, view ve Query SQL'ini yazarken kolon adlarini describe_list ile buradan al, tahmin etme.")]
public async Task<List<McpListSummaryDto>> ListListsAsync( public async Task<CallToolResult> ListListsAsync(
[Description("Liste adi, basligi, aciklamasi ya da tablo adi icinde aranacak metin. Bos birakilabilir.")] [Description("Liste adi, basligi, aciklamasi ya da tablo adi icinde aranacak metin. Bos birakilabilir.")]
string? search = null, string? search = null,
[Description("Donecek en fazla liste sayisi. Varsayilan 200.")] [Description("Donecek en fazla liste sayisi. Varsayilan 200.")]
int maxResults = 200) int maxResults = 200)
=> await schemaAppService.GetListsAsync(search, maxResults); => McpToolResult.From(await schemaAppService.GetListsAsync(search, maxResults));
[McpServerTool(Name = McpToolNames.DescribeList, ReadOnly = true)] [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.")] [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( public async Task<CallToolResult> DescribeListAsync(
[Description("Listenin kodu; list_lists cikisindaki listFormCode degeri.")] [Description("Listenin kodu; list_lists cikisindaki listFormCode degeri.")]
string listFormCode) string listFormCode)
=> await schemaAppService.GetListAsync(listFormCode); => McpToolResult.From(await schemaAppService.GetListAsync(listFormCode));
[McpServerTool(Name = McpToolNames.SearchSchema, ReadOnly = true)] [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.")] [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( public async Task<CallToolResult> SearchSchemaAsync(
[Description("Aranacak metin. Turkce karakterler ve aksanlar onemsizdir.")] [Description("Aranacak metin. Turkce karakterler ve aksanlar onemsizdir.")]
string term, string term,
[Description("Donecek en fazla eslesme sayisi. Varsayilan 50.")] [Description("Donecek en fazla eslesme sayisi. Varsayilan 50.")]
int maxResults = 50) int maxResults = 50)
=> await schemaAppService.SearchAsync(term, maxResults); => McpToolResult.From(await schemaAppService.SearchAsync(term, maxResults));
} }
/// <param name="Scope">Istegin baglami: <c>host</c> ya da <c>tenant</c>. Gelistirme araclari yalnizca host'ta aciktir.</param> /// <param name="Scope">Istegin baglami: <c>host</c> ya da <c>tenant</c>. Gelistirme araclari yalnizca host'ta aciktir.</param>

View file

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using ModelContextProtocol;
using ModelContextProtocol.Protocol;
namespace Sozsoft.Mcp.Tools;
/// <summary>
/// Servis sonucunu arac sonucuna cevirir. Deger SDK'nin donus degerini yazdigi bicimde
/// (<see cref="McpJsonUtilities.DefaultOptions"/>) JSON metnidir. <see cref="McpProblem"/> hata
/// sonucudur (<c>IsError</c>): istisna atilmadan modele ulasir, model nedeni okuyup girdiyi duzeltir.
/// </summary>
internal static class McpToolResult
{
/// <summary>Degeri olmayan araclarin (not ekleme, silme) basari cevabi.</summary>
private const string Done = "ok";
public static CallToolResult From<T>(McpOutcome<T> outcome)
=> outcome.TryGetValue(out var value, out var problem)
? Text(JsonSerializer.Serialize(value, McpJsonUtilities.DefaultOptions))
: Error(problem.Message);
public static CallToolResult From(McpProblem? problem)
=> problem is null ? Text(Done) : Error(problem.Message);
/// <summary>Degeri metin disinda icerik (resim) tasiyan araclar icin.</summary>
public static CallToolResult From<T>(McpOutcome<T> outcome, Func<T, IList<ContentBlock>> toContent)
=> outcome.TryGetValue(out var value, out var problem)
? new CallToolResult { Content = toContent(value) }
: Error(problem.Message);
public static CallToolResult Error(string message)
=> new() { IsError = true, Content = [new TextContentBlock { Text = message }] };
private static CallToolResult Text(string text)
=> new() { Content = [new TextContentBlock { Text = text }] };
}

View file

@ -341,13 +341,19 @@ Bir çelişki varsa sıra: `ai.instructions.md` → `dotnet.instructions.md` →
çağrıyı reddeder — süzme tek başına yetki sınırı değildir, ikisi birlikte yazılır. `Invoke` çağrıyı reddeder — süzme tek başına yetki sınırı değildir, ikisi birlikte yazılır. `Invoke`
uçta değil her MCP servisinin sınıf düzeyindeki `[Authorize]`'dadır; yeni bir MCP AppService'i de uçta değil her MCP servisinin sınıf düzeyindeki `[Authorize]`'dadır; yeni bir MCP AppService'i de
bu attribute'la açılır. bu attribute'la açılır.
**Hata istemciye hata sonucu (`CallToolResult.IsError`) olarak ulaşır**; `McpToolErrorTranslator` **MCP'nin kendi reddi istisna değildir, modelle konuşmadır**: eksik/hatalı girdi, durum kuralı,
(çağrı filtresi) yetki reddini "şu yetkiniz yok" mesajına, `UserFriendlyException`'ı mesajına kapalı araç ve eksik yetki `McpProblem` olarak döner — MCP AppService'leri `McpOutcome<T>` /
çevirir ve istisna atmadan döner (atılan istisna SDK'da yığın izli `ERR` logudur). `McpProblem?` döndürür, domain servisleri (`McpSqlGuard`, `McpTableCreator`, `McpSqlObjectWriter`)
Kullanıcının göreceği her hata bu iki türden biriyle atılır. Yeni aracın arkasındaki servis de öyle; araç katmanı bunu `McpToolResult.From` ile hata sonucuna (`CallToolResult.IsError`) çevirir,
bir yetkiyi `[Authorize]` attribute'uyla istiyorsa (ret mesajı yetki adını taşımaz) o yetki model mesajı okuyup tekrar dener. `throw new UserFriendlyException` ve `AuthorizationService.CheckAsync`
`McpToolNames.ServicePermissions`'a eklenir; ekran yetkisi gereken araç yetkiyi MCP kodunda kullanılmaz (debugger'ı durdurur, SDK'da yığın izli `ERR` üretir); yetki
`IMcpListFormAccessFilter.GetPermissionNameAsync` + `CheckAsync` ile adıyla önceden ister. `McpAppService.CheckPermissionAsync` ile adıyla istenir. İstisna yalnızca gerçek hatadır (kod hatası,
beklenmeyen durum). Arkadaki platform servisinin (wizard, custom component, ListForm verisi) attığı
`UserFriendlyException`/yetki reddi ile sınıf düzeyindeki `[Authorize]` reddi `McpToolErrorTranslator`
(çağrı filtresi) ile mesaja çevrilir. Yeni aracın arkasındaki servis bir yetkiyi `[Authorize]`
attribute'uyla istiyorsa (ret mesajı yetki adını taşımaz) o yetki `McpToolNames.ServicePermissions`'a
eklenir; ekran yetkisi gereken araç yetkiyi `IMcpListFormAccessFilter.GetPermissionNameAsync` +
`CheckPermissionAsync` ile adıyla önceden ister.
Araç → yetki tablosu `README.md` §13.9'dadır; araç ya da yetki değişirse tablo da değişir. Araç → yetki tablosu `README.md` §13.9'dadır; araç ya da yetki değişirse tablo da değişir.
Hazır prompt'lar (`McpPromptNames` + `McpDevelopPrompts`) yalnızca metindir; adımları tekrar Hazır prompt'lar (`McpPromptNames` + `McpDevelopPrompts`) yalnızca metindir; adımları tekrar
etmez, `get_dev_guide` bölümüne yönlendirir. Bir kural bağlayıcıysa prompt'a değil araca yazılır etmez, `get_dev_guide` bölümüne yönlendirir. Bir kural bağlayıcıysa prompt'a değil araca yazılır