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 required = candidates
.Where(McpToolNames.RequiredPermissions.ContainsKey)
.Select(tool => McpToolNames.RequiredPermissions[tool])
.Distinct(StringComparer.Ordinal)
.ToArray();
var granted = new HashSet(StringComparer.Ordinal);
if (required.Length > 0)
{
var grants = await permissionChecker.IsGrantedAsync(required);
granted.UnionWith(grants.Result
.Where(result => result.Value == PermissionGrantResult.Granted)
.Select(result => result.Key));
}
return [.. candidates.Where(tool =>
!McpToolNames.RequiredPermissions.TryGetValue(tool, out var permission) || granted.Contains(permission))];
}
public async Task 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)
{
await EnsureToolAllowedAsync(McpToolNames.ListLists);
var forms = await GetReadableListFormsAsync();
var normalizedSearch = dictionary.Normalize(search);
var matches = forms.Where(form => normalizedSearch.Length == 0
|| dictionary.Normalize(form.SearchText).Contains(normalizedSearch, StringComparison.Ordinal));
return [.. matches.Take(NormalizeLimit(maxResults, 200)).Select(ToSummary)];
}
public async Task GetListAsync(string listFormCode)
{
await EnsureToolAllowedAsync(McpToolNames.DescribeList);
if (string.IsNullOrWhiteSpace(listFormCode))
{
return null;
}
var forms = await GetReadableListFormsAsync();
var form = forms.FirstOrDefault(candidate =>
string.Equals(candidate.ListFormCode, listFormCode, StringComparison.OrdinalIgnoreCase));
if (form is null)
{
return null;
}
var detail = new McpListDetailDto();
CopySummary(form, detail);
detail.Columns = [.. (form.Fields ?? []).Select(ToColumn)];
return detail;
}
public async Task> SearchAsync(string term, int maxResults = 50)
{
await EnsureToolAllowedAsync(McpToolNames.SearchSchema);
var normalized = dictionary.Normalize(term);
if (normalized.Length == 0)
{
return [];
}
var limit = NormalizeLimit(maxResults, 50);
var forms = await GetReadableListFormsAsync();
var results = new List();
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;
}