using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Sozsoft.Mcp.Dtos; using Sozsoft.Mcp.Services; using Sozsoft.Platform; using Sozsoft.Platform.DeveloperKit; using Sozsoft.Platform.Entities; using Sozsoft.Platform.Enums; using Sozsoft.Platform.ListForms; using Sozsoft.Platform.ListForms.Select; using Volo.Abp; using Volo.Abp.Application.Dtos; using Volo.Abp.Domain.Repositories; using Volo.Abp.PermissionManagement; namespace Sozsoft.Mcp; /// /// Ekran ve bilesen gelistirme yuzeyi. Uretim Wizard ve Custom Components servislerinden /// gecer: menu, yetki, dil anahtari, seed dosyasi ve bagimlilik kurallari ekrandaki yolla /// aynidir, yetki kontrolu de o servislerin kendi politikalarindan gelir. Invoke/Write yetkisi /// yalnizca MCP kapisidir, arkadaki yetkinin yerine gecmez. /// [Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Invoke)] public class McpDevelopAppService( IMcpServerConfigProvider serverConfigProvider, IMcpDevGuide devGuide, IMcpTableCreator tableCreator, IListFormWizardAppService wizardAppService, ICustomComponentAppService componentAppService, IListFormSelectAppService listFormSelectAppService, IRepository menuRepository, IRepository permissionRepository, IRepository devRequestRepository) : McpAppService(serverConfigProvider), IMcpDevelopAppService { private const string DefaultGuideDocument = "lowcode"; /// Index cevabinda listelenen en derin baslik; daha derini bolum istenince gorunur. private const int IndexHeadingLevel = 3; /// Custom Components ekraninin tek sayfada dondurebildigi ust sinir. private const int ComponentPageSize = 1000; private const string DevRequestUpdatePermission = PlatformConsts.AppCodes.DevRequests + ".Update"; /// verify_wizard cevabindaki kontrol adlari. private static class VerifyChecks { public const string WizardFile = "wizard_file"; public const string Menu = "menu"; public const string Permission = "permission"; public const string Select = "select"; public const string Component = "component"; public const string SeedClosure = "seed_closure"; } public async Task GetDevGuideAsync(string? document, string? section) { await EnsureToolAllowedAsync(McpToolNames.GetDevGuide); if (string.IsNullOrWhiteSpace(section)) { var headings = await devGuide.GetHeadingsAsync(); if (headings.Count == 0) { throw new UserFriendlyException(L[McpErrorCodes.DevGuideUnavailable, McpDevGuide.ConfigurationKey]); } return new McpDevGuideDto { Sections = [.. headings .Where(h => h.Level <= IndexHeadingLevel) .Where(h => string.IsNullOrWhiteSpace(document) || string.Equals(h.Document, document, StringComparison.OrdinalIgnoreCase)) .Select(ToDto)] }; } var result = await devGuide.GetSectionAsync( string.IsNullOrWhiteSpace(document) ? DefaultGuideDocument : document, section) ?? throw new UserFriendlyException(L[McpErrorCodes.DevGuideSectionNotFound, section]); return new McpDevGuideDto { Heading = ToDto(result.Heading), Content = result.Content, Sections = [.. result.Children.Select(ToDto)] }; } /// /// Tablo tasarimcisiyla ayni yetkiyi ister: ham DDL liste yetkilerini asar, Develop yalnizca /// MCP kapisidir. /// public async Task CreateTableAsync(McpCreateTableRequestDto input) { Check.NotNull(input, nameof(input)); await EnsureToolAllowedAsync(McpToolNames.CreateTable); await AuthorizationService.CheckAsync(PlatformConsts.AppCodes.SqlQueryManagers.Default); var created = await tableCreator.CreateAsync(new McpTableDefinition( input.TableName, [.. (input.Columns ?? []).Select(column => new McpTableColumn( column.Name, column.Type, column.MaxLength, column.IsNullable, column.DefaultValue))], input.FullAudited, input.MultiTenant)); return new McpCreateTableResultDto { TableName = created.TableName, Script = created.Script, SeedFilePath = created.SeedFilePath }; } public async Task> GetWizardsAsync(string? search) { await EnsureToolAllowedAsync(McpToolNames.ListWizards); var files = await wizardAppService.GetFiles(); if (string.IsNullOrWhiteSpace(search)) { return files; } return [.. files.Where(file => Contains(file.FileName, search) || Contains(file.WizardName, search) || Contains(file.ListFormCode, search) || Contains(file.CustomComponentName, search))]; } public async Task GetWizardAsync(string fileName) { await EnsureToolAllowedAsync(McpToolNames.GetWizard); var seed = await wizardAppService.GetFile(fileName); // Model dosyayi Claude Code'un depoda gordugu bicimde gorur; deploy_wizard ayni bicimi // geri kabul eder. using var document = JsonDocument.Parse(seed.ToSeedJson()); return new McpWizardFileDto { FileName = fileName, Seed = document.RootElement.Clone() }; } public async Task DeployWizardAsync(McpWizardDeployRequestDto input) { Check.NotNull(input, nameof(input)); await EnsureToolAllowedAsync(McpToolNames.DeployWizard); var wizard = ParseWizard(input.Wizard); wizard.EditFileName = string.IsNullOrWhiteSpace(input.ReplaceFileName) ? null : input.ReplaceFileName.Trim(); await wizardAppService.Create(wizard); var files = await wizardAppService.GetFiles(); return files.FirstOrDefault(file => string.Equals(file.WizardName, wizard.WizardName?.Trim(), StringComparison.OrdinalIgnoreCase)); } public async Task DeleteWizardAsync(string fileName) { await EnsureToolAllowedAsync(McpToolNames.DeleteWizard); return await wizardAppService.DeleteFile(fileName); } /// /// Ekrani kullanicinin gorecegi yoldan dener: menu kaydi, menunun istedigi yetki, ekranin /// sorgusu (cagiranin yetkisi ve kiraci filtresiyle, tek satir) ve Custom yolunda bilesen. /// Hicbir kontrol veri yazmaz; test satiri eklemek SeedSync acik ekranlarda seed dosyasina /// iz birakirdi. Seed kapanisi engellemez: ekran bu ortamda calisir, eksik dosya yalnizca /// migrate ya da baska bir ortamda ortaya cikar. /// public async Task VerifyWizardAsync(string fileName) { await EnsureToolAllowedAsync(McpToolNames.VerifyWizard); var result = new McpVerifyResultDto { FileName = fileName }; ListFormWizardDto? wizard; try { wizard = (await wizardAppService.GetFile(fileName)).Wizard; } catch (Exception ex) { result.Checks.Add(Failed(VerifyChecks.WizardFile, L[McpErrorCodes.VerifyWizardFileInvalid, fileName, ex.Message])); return result; } if (wizard is null) { result.Checks.Add(Failed(VerifyChecks.WizardFile, L[McpErrorCodes.InvalidWizard])); return result; } var listFormCode = wizard.ComponentKind == WizardComponentKindEnum.Custom ? null : wizard.ResolveListFormCode(); result.ListFormCode = listFormCode; result.MenuUrl = wizard.MenuUrl; result.Checks.Add(new McpVerifyCheckDto { Name = VerifyChecks.WizardFile, Passed = true }); var permissionName = listFormCode; if (wizard.CreateMenu) { var menu = await VerifyMenuAsync(wizard, result.Checks); permissionName = string.IsNullOrWhiteSpace(menu?.RequiredPermissionName) ? permissionName : menu.RequiredPermissionName; } if (!string.IsNullOrWhiteSpace(permissionName)) { var permission = await permissionRepository.FirstOrDefaultAsync(p => p.Name == permissionName); result.Checks.Add(permission is { IsEnabled: true } ? new McpVerifyCheckDto { Name = VerifyChecks.Permission, Passed = true, Message = permissionName } : Failed(VerifyChecks.Permission, L[McpErrorCodes.VerifyPermissionMissing, permissionName])); } result.Checks.Add(listFormCode is null ? await VerifyComponentAsync(wizard) : await VerifySelectAsync(listFormCode)); result.Checks.Add(await VerifySeedClosureAsync(fileName)); result.Passed = result.Checks.TrueForAll(check => check.Passed || !check.Blocking); return result; } /// /// Durum kurali modelin dikkatine birakilmaz: talep Ready to Start degilse reddedilir, uygunsa /// ayni unit of work'te In Progress'e alinir. MCP yetki vermez; ekranin guncelleme yetkisi /// ayrica istenir. /// public async Task StartDevRequestAsync(string request) { await EnsureToolAllowedAsync(McpToolNames.StartDevRequest); await AuthorizationService.CheckAsync(DevRequestUpdatePermission); var devRequest = await FindDevRequestAsync(request); 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]); } devRequest.Status = McpConsts.DevRequestStatuses.InProgress; await devRequestRepository.UpdateAsync(devRequest, autoSave: true); return ToDto(devRequest); } public async Task GetDevRequestAsync(string request) { await EnsureToolAllowedAsync(McpToolNames.GetDevRequest); await AuthorizationService.CheckAsync(PlatformConsts.AppCodes.DevRequests); return ToDto(await FindDevRequestAsync(request)); } /// /// Aciklama base64 gorsel tasir; model onu update_row ile yeniden yazarsa gorselleri geri /// gonderemez ve siler. Ekleme bu yuzden sunucuda, var olan metne dokunmadan yapilir. /// public async Task AppendDevRequestNoteAsync(string request, string html) { await EnsureToolAllowedAsync(McpToolNames.AppendDevRequestNote); await AuthorizationService.CheckAsync(DevRequestUpdatePermission); if (string.IsNullOrWhiteSpace(html)) { throw new UserFriendlyException(L[McpErrorCodes.DevRequestNoteRequired]); } var devRequest = await FindDevRequestAsync(request); devRequest.Description = string.Concat(devRequest.Description, html.Trim()); await devRequestRepository.UpdateAsync(devRequest, autoSave: true); } private static McpDevRequestDto ToDto(DevRequest devRequest) { var (description, images) = McpDevRequestImages.Extract(devRequest.Description); return new McpDevRequestDto { Id = devRequest.Id, Title = devRequest.Title, Status = devRequest.Status, Description = description, Images = images, Priority = devRequest.Priority, Tags = devRequest.Tags, DueDate = devRequest.DueDate, UserName = devRequest.UserName, TenantId = devRequest.TenantId, SubTasks = devRequest.SubTasks }; } /// Id ile ya da tam baslikla bulur; ayni basligi tasiyan birden fazla talep varsa Id istenir. private async Task FindDevRequestAsync(string? request) { var value = request?.Trim() ?? string.Empty; if (Guid.TryParse(value, out var id)) { return await devRequestRepository.FindAsync(id) ?? throw new UserFriendlyException(L[McpErrorCodes.DevRequestNotFound, value]); } var matches = value.Length == 0 ? [] : await devRequestRepository.GetListAsync(r => r.Title == value); return matches.Count switch { 0 => throw new UserFriendlyException(L[McpErrorCodes.DevRequestNotFound, value]), 1 => matches[0], _ => throw new UserFriendlyException(L[McpErrorCodes.DevRequestAmbiguous, value, string.Join(", ", matches.Select(r => r.Id))]) }; } /// /// Kapanis analizi wizard export'unun yetkisini ister; o yetki dogrulamanin kendisini /// dusurmesin diye yoksa kontrol atlanir ve nedeni raporlanir. /// private async Task VerifySeedClosureAsync(string fileName) { var check = new McpVerifyCheckDto { Name = VerifyChecks.SeedClosure, Blocking = false }; if (!await AuthorizationService.IsGrantedAsync(PlatformConsts.AppCodes.Listforms.Wizards.Export)) { check.Message = L[McpErrorCodes.VerifySeedClosureSkipped, PlatformConsts.AppCodes.Listforms.Wizards.Export]; return check; } var warnings = (await wizardAppService.ExportAnalysis(fileName)).Warnings; if (warnings.Count == 0) { check.Passed = true; return check; } check.Message = L[McpErrorCodes.VerifySeedClosureIncomplete, string.Join(", ", warnings)]; return check; } private async Task VerifyMenuAsync(ListFormWizardDto wizard, List checks) { var menuCode = wizard.ResolveMenuCode(); var menu = await menuRepository.FirstOrDefaultAsync(m => m.Code == menuCode); if (menu is null) { checks.Add(Failed(VerifyChecks.Menu, L[McpErrorCodes.VerifyMenuMissing, menuCode])); } else if (menu.IsDisabled) { checks.Add(Failed(VerifyChecks.Menu, L[McpErrorCodes.VerifyMenuDisabled, menuCode])); } else if (!string.Equals(menu.Url, wizard.MenuUrl, StringComparison.OrdinalIgnoreCase)) { checks.Add(Failed(VerifyChecks.Menu, L[McpErrorCodes.VerifyMenuUrlMismatch, menuCode, menu.Url, wizard.MenuUrl])); } else { checks.Add(new McpVerifyCheckDto { Name = VerifyChecks.Menu, Passed = true, Message = menu.Url }); } return menu; } /// /// Ekranin sorgusunu query_list ile ayni servisten calistirir; kolon, veri kaynagi ve yetki /// hatalari burada ortaya cikar. /// private async Task VerifySelectAsync(string listFormCode) { try { var select = await listFormSelectAppService.GetSelectAsync(new SelectRequestDto { ListFormCode = listFormCode, Take = 1, RequireTotalCount = true }); return new McpVerifyCheckDto { Name = VerifyChecks.Select, Passed = true, Message = L[McpErrorCodes.VerifySelectPassed, select.TotalCount] }; } catch (Exception ex) { return Failed(VerifyChecks.Select, L[McpErrorCodes.VerifySelectFailed, ex.Message]); } } private async Task VerifyComponentAsync(ListFormWizardDto wizard) { var component = await FindComponentAsync(wizard.CustomComponentName); if (component is null) { return Failed(VerifyChecks.Component, L[McpErrorCodes.ComponentNotFound, wizard.CustomComponentName]); } if (!component.IsActive) { return Failed(VerifyChecks.Component, L[McpErrorCodes.VerifyComponentInactive, component.Name]); } if (wizard.CreateMenu && !string.Equals(component.RoutePath, wizard.MenuUrl, StringComparison.OrdinalIgnoreCase)) { return Failed(VerifyChecks.Component, L[McpErrorCodes.VerifyComponentRouteMismatch, component.Name, component.RoutePath ?? string.Empty, wizard.MenuUrl]); } return new McpVerifyCheckDto { Name = VerifyChecks.Component, Passed = true, Message = component.RoutePath }; } private static McpVerifyCheckDto Failed(string name, string message) => new() { Name = name, Passed = false, Message = message }; public async Task> GetComponentsAsync(string? search) { await EnsureToolAllowedAsync(McpToolNames.ListComponents); var components = await GetAllComponentsAsync(); return [.. components .Where(component => string.IsNullOrWhiteSpace(search) || Contains(component.Name, search) || Contains(component.RoutePath, search) || Contains(component.Description, search)) .Select(component => new McpComponentSummaryDto { Id = component.Id, Name = component.Name, RoutePath = component.RoutePath, Description = component.Description, IsActive = component.IsActive, IsVisualDesigner = IsVisualDesignerDocument(component.Props) })]; } public async Task GetComponentAsync(string name) { await EnsureToolAllowedAsync(McpToolNames.GetComponent); return await FindComponentAsync(name); } public async Task SaveComponentAsync(McpComponentSaveRequestDto input) { Check.NotNull(input, nameof(input)); await EnsureToolAllowedAsync(McpToolNames.SaveComponent); if (string.IsNullOrWhiteSpace(input.Name)) { throw new UserFriendlyException(L[McpErrorCodes.ComponentNameRequired]); } var existing = await FindComponentAsync(input.Name); var props = input.Props ?? existing?.Props; // Tasarimci belgesinde kod belgeden uretilir; buraya yazilan kod tasarimcidaki ilk // kayitta sessizce ezilirdi. Belge MCP'de uretilemedigi icin bu bilesenler tasarimcida kalir. if (IsVisualDesignerDocument(props)) { throw new UserFriendlyException(L[McpErrorCodes.VisualDesignerComponent, input.Name]); } if (existing is null) { if (string.IsNullOrWhiteSpace(input.Code)) { throw new UserFriendlyException(L[McpErrorCodes.ComponentCodeRequired]); } return await componentAppService.CreateAsync(new CreateUpdateCustomComponentDto { Name = input.Name.Trim(), Code = input.Code, RoutePath = input.RoutePath, Description = input.Description, Props = props, Dependencies = input.Dependencies, IsActive = input.IsActive ?? true }); } return await componentAppService.UpdateAsync(existing.Id, new CreateUpdateCustomComponentDto { Name = existing.Name, Code = string.IsNullOrWhiteSpace(input.Code) ? existing.Code : input.Code, RoutePath = input.RoutePath ?? existing.RoutePath, Description = input.Description ?? existing.Description, Props = props, Dependencies = input.Dependencies ?? existing.Dependencies, IsActive = input.IsActive ?? existing.IsActive }); } public async Task DeleteComponentAsync(string name) { await EnsureToolAllowedAsync(McpToolNames.DeleteComponent); var component = await FindComponentAsync(name) ?? throw new UserFriendlyException(L[McpErrorCodes.ComponentNotFound, name]); await componentAppService.DeleteAsync(component.Id); } /// /// Wizard'i seed dosyasi biciminden ({"Wizard": {...}}) ya da duz cevaplardan okur. /// Iki bicim de dosyanin okuma kuralindan gecer; boylece Menu/Language bloklari ve /// WizardCode/ModuleId adlari dosyadakiyle ayni anlama gelir. /// private ListFormWizardDto ParseWizard(string? json) { JsonObject? root; try { root = string.IsNullOrWhiteSpace(json) ? null : JsonNode.Parse(json) as JsonObject; } catch (JsonException) { root = null; } if (root is null) { throw new UserFriendlyException(L[McpErrorCodes.InvalidWizard]); } var seedJson = root.ContainsKey(nameof(WizardSeedFileDto.Wizard)) ? root.ToJsonString() : new JsonObject { [nameof(WizardSeedFileDto.Wizard)] = root }.ToJsonString(); try { return WizardSeedFileDto.FromSeedJson(seedJson)?.Wizard ?? throw new UserFriendlyException(L[McpErrorCodes.InvalidWizard]); } catch (JsonException) { throw new UserFriendlyException(L[McpErrorCodes.InvalidWizard]); } } private async Task> GetAllComponentsAsync() { var page = await componentAppService.GetListAsync(new PagedAndSortedResultRequestDto { MaxResultCount = ComponentPageSize, Sorting = nameof(CustomComponentDto.Name) }); return page.Items; } private async Task FindComponentAsync(string? name) { if (string.IsNullOrWhiteSpace(name)) { return null; } var components = await GetAllComponentsAsync(); return components.FirstOrDefault(component => string.Equals(component.Name, name.Trim(), StringComparison.OrdinalIgnoreCase)); } /// /// Props icinde tasarimci belgesi varsa ve kip acikca code degilse kod belgeden /// uretilir; tasarimcinin varsayilan kipi visual'dir. /// private static bool IsVisualDesignerDocument(string? props) { if (string.IsNullOrWhiteSpace(props)) { return false; } try { using var document = JsonDocument.Parse(props); if (document.RootElement.ValueKind != JsonValueKind.Object || !document.RootElement.TryGetProperty("visualDesigner", out var designer) || designer.ValueKind != JsonValueKind.Object) { return false; } return !designer.TryGetProperty("sourceMode", out var mode) || mode.ValueKind != JsonValueKind.String || !string.Equals(mode.GetString(), "code", StringComparison.Ordinal); } catch (JsonException) { return false; } } private static bool Contains(string? value, string search) => value?.Contains(search.Trim(), StringComparison.OrdinalIgnoreCase) == true; private static McpDevGuideHeadingDto ToDto(McpDevGuideHeading heading) => new() { Document = heading.Document, Id = heading.Id, Title = heading.Title, Level = heading.Level }; }