using System; using System.Collections.Generic; using System.Data; using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Configuration; using Sozsoft.Mcp.Dtos; using Sozsoft.Mcp.Services; using Sozsoft.Platform; using Sozsoft.Platform.Data.Seeds; using Sozsoft.Platform.Enums; using Sozsoft.Platform.ListForms; using Volo.Abp.Authorization.Permissions; namespace Sozsoft.Mcp; /// /// Sema kesfini JSON veri sozlugu uzerinden karsilar; sozlugu okuma ve arama normalizasyonu /// domain servisindedir (). Kullanici yalnizca uygulamada /// okuma yetkisi olan listeleri gorur (). /// [Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Invoke)] public class McpSchemaAppService( IMcpServerConfigProvider serverConfigProvider, IMcpSchemaDictionary dictionary, IMcpListFormAccessFilter accessFilter, IPermissionChecker permissionChecker, IListFormWizardAppService wizardAppService, IConfiguration configuration) : McpAppService(serverConfigProvider), IMcpSchemaAppService { /// Wizard dosyasi olan ama sozlukte olmayan ekran (DB Migrate bekliyor). private const string PendingSourceKind = "pending"; /// Yalnizca seed scripti olan, ekrani olmayan tablo. private const string PendingTableSourceKind = "pendingTable"; public async Task GetServerConfigAsync() => await ResolveServerConfigAsync(); public async Task> GetAvailableToolsAsync() { var config = await ResolveServerConfigAsync(); var candidates = McpToolPolicy.VisibleTools( config is not null, CurrentTenant.IsAvailable, config?.AllowedTools ?? []); var granted = await permissionChecker.GetGrantedNamesAsync(candidates .Where(McpToolNames.RequiredPermissions.ContainsKey) .Select(tool => McpToolNames.RequiredPermissions[tool])); return [.. candidates.Where(tool => !McpToolNames.RequiredPermissions.TryGetValue(tool, out var permission) || granted.Contains(permission))]; } public async Task GetSchemaInfoAsync() { var snapshot = await dictionary.GetSnapshotAsync(); if (snapshot is null) { return new McpSchemaInfoDto { Available = false, DictionaryKey = dictionary.ResolveTenantName(), Message = L[McpErrorCodes.DictionaryNotFound, McpConsts.DictionaryProcedureName] }; } return new McpSchemaInfoDto { Available = true, CacheKey = snapshot.CacheKey, RefreshedAt = snapshot.RefreshedAt, FormCount = snapshot.FormCount, FieldCount = snapshot.FieldCount, DictionaryKey = snapshot.TenantName, DefinitionCultureName = snapshot.DefinitionCultureName, UiCultureName = snapshot.UiCultureName }; } public async Task>> GetListsAsync(string? search = null, int maxResults = 200) { if (await EnsureToolAllowedAsync(McpToolNames.ListLists) is { Problem: { } problem }) { return problem; } var forms = await GetReadableListFormsAsync(); var normalizedSearch = dictionary.Normalize(search); var matches = forms.Where(form => normalizedSearch.Length == 0 || dictionary.Normalize(form.SearchText).Contains(normalizedSearch, StringComparison.Ordinal)); return matches.Take(NormalizeLimit(maxResults, 200)).Select(ToSummary).ToList(); } public async Task> GetListAsync(string listFormCode) { if (await EnsureToolAllowedAsync(McpToolNames.DescribeList) is { Problem: { } problem }) { return problem; } if (string.IsNullOrWhiteSpace(listFormCode)) { return default(McpListDetailDto); } var forms = await GetReadableListFormsAsync(); var form = forms.FirstOrDefault(candidate => string.Equals(candidate.ListFormCode, listFormCode, StringComparison.OrdinalIgnoreCase)); if (form is null) { return default(McpListDetailDto); } var detail = new McpListDetailDto(); CopySummary(form, detail); detail.Columns = [.. (form.Fields ?? []).Select(ToColumn)]; return detail; } public async Task>> SearchAsync(string term, int maxResults = 50) { if (await EnsureToolAllowedAsync(McpToolNames.SearchSchema) is { Problem: { } problem }) { return problem; } var normalized = dictionary.Normalize(term); if (normalized.Length == 0) { return new List(); } var limit = NormalizeLimit(maxResults, 50); var forms = await GetReadableListFormsAsync(); var results = new List(); foreach (var form in forms) { var formMatches = dictionary.Normalize(form.SearchText).Contains(normalized, StringComparison.Ordinal); var columnMatches = (form.Fields ?? []) .Where(field => dictionary.Normalize(field.Field).Contains(normalized, StringComparison.Ordinal) || dictionary.Normalize(field.DisplayName).Contains(normalized, StringComparison.Ordinal)) .Select(ToColumn) .ToList(); if (!formMatches && columnMatches.Count == 0) { continue; } results.Add(new McpSchemaMatchDto { ListFormCode = form.ListFormCode ?? string.Empty, DisplayName = form.DisplayName, TableName = NullIfEmpty(form.TableName), MatchedColumns = columnMatches }); if (results.Count >= limit) { break; } } return results; } private async Task> GetReadableListFormsAsync() { var forms = await dictionary.GetListFormsAsync(); var readable = await accessFilter.GetReadableListFormCodesAsync(); List result = [.. forms.Where(form => form.ListFormCode is not null && readable.Contains(form.ListFormCode))]; // Gelistirici, sozlukte henuz olmayan (DB Migrate bekleyen ya da sozluk tazelenmemis) ekran ve // tablolari da gorur; lookup, widget ve view SQL'i bunlarin gercek kolon adlariyla yazilir. if (!CurrentTenant.IsAvailable && await AuthorizationService.IsGrantedAsync(PlatformConsts.AppCodes.Listforms.Wizard)) result.AddRange(await GetSeedOnlySourcesAsync(forms)); return result; } /// /// Seed klasorunde olup sozlukte olmayan kaynaklar: wizard dosyalarinin ekranlari (alanlari /// Fields bolumunden) ve yalnizca sql/object scripti olan tablolar (kolonlari scriptten). /// pending / pendingTable olarak isaretlenir: /// kolon adlari dogrudur ama query_list migrate'ten once calismaz. /// private async Task> GetSeedOnlySourcesAsync(IReadOnlyList known) { var codes = known.Select(form => form.ListFormCode).OfType().ToHashSet(StringComparer.OrdinalIgnoreCase); var tables = known.Select(form => form.TableName).OfType().ToHashSet(StringComparer.OrdinalIgnoreCase); var result = new List(); foreach (var file in await wizardAppService.GetFiles()) { if (file.ComponentKind == WizardComponentKindEnum.Custom || codes.Contains(file.ListFormCode ?? string.Empty)) continue; var seed = await wizardAppService.GetFile(file.FileName); if (seed.ListForm is not { } listForm) continue; var table = listForm.SelectCommandType == SelectCommandTypeEnum.Query ? null : listForm.SelectCommand?.Split('.')[^1].Trim('[', ']', '"'); if (table is not null) tables.Add(table); result.Add(new McpDictionaryListForm { ListFormCode = listForm.ListFormCode, DisplayName = seed.Wizard?.TitleTr ?? seed.Wizard?.TitleEn, Title = seed.Wizard?.TitleEn, Description = seed.Wizard?.DescTr, TableName = table, SqlSourceName = listForm.SelectCommand, SourceKind = PendingSourceKind, KeyFieldName = listForm.KeyFieldName, SearchText = string.Join(' ', listForm.ListFormCode, seed.Wizard?.TitleTr, seed.Wizard?.TitleEn, table), Fields = [.. seed.Fields.Select(field => new McpDictionaryField { Field = field.FieldName, DisplayName = field.CaptionName, SqlDataType = field.SourceDbType.ToString(), IsVisible = field.Visible, IsBooleanColumn = field.SourceDbType == DbType.Boolean, LookupValueColumn = string.IsNullOrWhiteSpace(field.LookupJson) ? null : field.LookupDto.ValueExpr, LookupDisplayColumn = string.IsNullOrWhiteSpace(field.LookupJson) ? null : field.LookupDto.DisplayExpr, IsDefaultSelectColumn = field.Visible })] }); } var scriptFolder = SeedPathResolver.GetPath(configuration, SeedPathResolver.SqlFolder, SeedPathResolver.ObjectFolder); foreach (var path in Directory.Exists(scriptFolder) ? Directory.GetFiles(scriptFolder, "*.sql") : []) { var table = Path.GetFileNameWithoutExtension(path); if (tables.Contains(table)) continue; var columns = PostgreSqlTableScript.ReadSeedColumns(configuration, table); if (columns.Count == 0) continue; result.Add(new McpDictionaryListForm { ListFormCode = table, TableName = table, SqlSourceName = table, SourceKind = PendingTableSourceKind, KeyFieldName = columns.ContainsKey("Id") ? "Id" : null, SearchText = table, Fields = [.. columns.Keys.Select(column => new McpDictionaryField { Field = column, IsVisible = true, IsDefaultSelectColumn = true })] }); } return result; } private static McpListSummaryDto ToSummary(McpDictionaryListForm form) { var summary = new McpListSummaryDto(); CopySummary(form, summary); return summary; } private static void CopySummary(McpDictionaryListForm form, McpListSummaryDto target) { target.ListFormCode = form.ListFormCode ?? string.Empty; target.DisplayName = NullIfEmpty(form.DisplayName); target.Title = NullIfEmpty(form.Title); target.Description = NullIfEmpty(form.Description); target.TableName = NullIfEmpty(form.TableName); target.SqlSourceName = NullIfEmpty(form.SqlSourceName); target.SourceKind = NullIfEmpty(form.SourceKind); target.KeyFieldName = NullIfEmpty(form.KeyFieldName); target.FieldCount = form.Fields?.Count ?? 0; } private static McpColumnDto ToColumn(McpDictionaryField field) => new() { Field = field.Field ?? string.Empty, DisplayName = NullIfEmpty(field.DisplayName), SqlDataType = NullIfEmpty(field.SqlDataType), IsVisible = field.IsVisible, IsBooleanColumn = field.IsBooleanColumn, LookupTableName = NullIfEmpty(field.LookupTableName), LookupValueColumn = NullIfEmpty(field.LookupValueColumn), LookupDisplayColumn = NullIfEmpty(field.LookupDisplayColumn), IsSensitive = field.IsSensitive, IsDefaultSelectColumn = field.IsDefaultSelectColumn }; private static int NormalizeLimit(int requested, int fallback) => requested <= 0 ? fallback : Math.Min(requested, fallback * 5); private static string? NullIfEmpty(string? value) => string.IsNullOrWhiteSpace(value) ? null : value; }