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.ListForms; using Volo.Abp; using Volo.Abp.Application.Dtos; 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. Develop yetkisi /// yalnizca MCP kapisidir, arkadaki yetkinin yerine gecmez. /// [Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Develop)] public class McpDevelopAppService( IMcpServerConfigProvider serverConfigProvider, IMcpDevGuide devGuide, IMcpTableCreator tableCreator, IListFormWizardAppService wizardAppService, ICustomComponentAppService componentAppService) : 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; 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); } 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 }; }