using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Sozsoft.Mcp.Dtos; using Sozsoft.Mcp.Services; using Sozsoft.Platform; using Sozsoft.Platform.Entities; using Sozsoft.Platform.Enums; using Sozsoft.Platform.ListForms.Select; using Volo.Abp; using Volo.Abp.Domain.Repositories; namespace Sozsoft.Mcp; /// /// Salt-okunur sorgu yuzeyi. MCP kullaniciya yetki vermez, uygulamadaki yetkisini tasir: /// /// query_list ekranin kendi sorgusundan gecer; liste ve alan yetkileri, kiraci ve /// sube filtreleri aynen uygulanir. /// Ham SQL ve veritabani nesneleri (view/procedure/function) liste yetkilerini asar; bu /// yuzden yalnizca uygulamada SQL Query Manager yetkisi olan kullaniciya aciktir. /// Custom Endpoint'ler endpoint dispatcher'inin yetkisini ve endpoint'in kendi /// kullanici/rol listesini ister. /// /// SQL kurallari , veri kaynagi cozumu ve calistirma /// icindedir. /// [Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Invoke)] public class McpQueryAppService( IMcpServerConfigProvider serverConfigProvider, IMcpSqlGuard sqlGuard, IMcpQueryExecutor queryExecutor, IRepository customEndpointRepository, IListFormSelectAppService listFormSelectAppService) : McpAppService(serverConfigProvider), IMcpQueryAppService { private const string SqlPermission = PlatformConsts.AppCodes.SqlQueryManagers.Default; private const string CustomEndpointPermission = PlatformConsts.AppCodes.DeveloperKits.Get; public async Task QueryListAsync(McpListQueryRequestDto input) { Check.NotNull(input, nameof(input)); var config = await EnsureToolAllowedAsync(McpToolNames.QueryList); var take = ResolveMaxRows(config, input.Take); var result = await listFormSelectAppService.GetSelectAsync(new SelectRequestDto { ListFormCode = input.ListFormCode, Filter = NullIfEmpty(input.Filter), Sort = NullIfEmpty(input.Sort), Group = NullIfEmpty(input.Group), GroupSummary = NullIfEmpty(input.GroupSummary), TotalSummary = NullIfEmpty(input.TotalSummary), Skip = Math.Max(input.Skip, 0), Take = take, RequireTotalCount = true, RequireGroupCount = !string.IsNullOrWhiteSpace(input.Group) }); return new McpListQueryResultDto { Data = result.Data, TotalCount = result.TotalCount, GroupCount = result.GroupCount, Summary = result.Summary, Take = take }; } public async Task RunSelectAsync(McpSelectRequestDto input) { Check.NotNull(input, nameof(input)); var config = await EnsureToolAllowedAsync(McpToolNames.RunSelect); var sql = (input.Sql ?? string.Empty).Trim().TrimEnd(';').Trim(); sqlGuard.EnsureReadOnlyStatement(sql); var maxRows = ResolveMaxRows(config, input.MaxRows); var parameters = sqlGuard.NormalizeParameters(input.Parameters); var context = await queryExecutor.ResolveDataSourceAsync(config.DataSourceCode); var limitedSql = sqlGuard.ApplyRowLimit(sql, maxRows, context.DataSourceType); return ToResult(await queryExecutor.ExecuteAsync(context, limitedSql, parameters, maxRows)); } public async Task> GetSavedQueriesAsync(string? search = null) { var config = await EnsureToolAllowedAsync(McpToolNames.ListQueries); var results = new List(); if (await AuthorizationService.IsGrantedAsync(SqlPermission)) { results.AddRange(await GetDatabaseObjectsAsync(config)); } if (await AuthorizationService.IsGrantedAsync(CustomEndpointPermission)) { results.AddRange(await GetCallableCustomEndpointsAsync()); } if (!string.IsNullOrWhiteSpace(search)) { results = [.. results.Where(item => item.Name.Contains(search, StringComparison.OrdinalIgnoreCase) || (item.Description?.Contains(search, StringComparison.OrdinalIgnoreCase) ?? false))]; } return [.. results .OrderBy(item => item.Kind, StringComparer.Ordinal) .ThenBy(item => item.Name, StringComparer.OrdinalIgnoreCase)]; } private async Task> GetDatabaseObjectsAsync(McpServerConfigDto config) { var context = await queryExecutor.ResolveDataSourceAsync(config.DataSourceCode); var catalogSql = context.DataSourceType == DataSourceTypeEnum.Postgresql ? McpCatalogQueries.Postgres : McpCatalogQueries.SqlServer; // Katalog sorgusunun kendi satir siniri yoktur; nesne sayisi tavani asmaz. var catalog = await queryExecutor.ExecuteAsync(context, catalogSql, new Dictionary(), int.MaxValue); var results = new List(); foreach (var row in catalog.Rows) { var kind = MapCatalogKind(ReadString(row, "ObjectKind")); if (kind is null) { continue; } results.Add(new McpSavedQueryDto { Kind = kind, SchemaName = ReadString(row, "SchemaName"), Name = ReadString(row, "ObjectName") ?? string.Empty }); } return results; } public async Task RunSavedQueryAsync(McpSavedQueryRequestDto input) { Check.NotNull(input, nameof(input)); var config = await EnsureToolAllowedAsync(McpToolNames.RunQuery); if (IsKind(input.Kind, McpConsts.SavedQueryKinds.CustomEndpoint)) { await AuthorizationService.CheckAsync(CustomEndpointPermission); return await RunCustomEndpointAsync(input, config); } await AuthorizationService.CheckAsync(SqlPermission); var maxRows = ResolveMaxRows(config, input.MaxRows); var parameters = sqlGuard.NormalizeParameters(input.Parameters); var context = await queryExecutor.ResolveDataSourceAsync(config.DataSourceCode); var schemaName = string.IsNullOrWhiteSpace(input.SchemaName) ? sqlGuard.DefaultSchemaName(context.DataSourceType) : input.SchemaName; var qualifiedName = sqlGuard.QuoteQualifiedName(schemaName, input.Name, context.DataSourceType); var sql = BuildSavedQuerySql(input.Kind, qualifiedName, parameters, maxRows, context.DataSourceType); return ToResult(await queryExecutor.ExecuteAsync(context, sql, parameters, maxRows)); } private async Task RunCustomEndpointAsync( McpSavedQueryRequestDto input, McpServerConfigDto config) { var endpoints = await GetCallableEndpointEntitiesAsync(); var endpoint = endpoints.Find(candidate => string.Equals(candidate.Name, input.Name, StringComparison.OrdinalIgnoreCase)) ?? throw new UserFriendlyException(L[McpErrorCodes.CustomEndpointNotFound, input.Name]); var maxRows = ResolveMaxRows(config, input.MaxRows); var parameters = sqlGuard.NormalizeParameters(input.Parameters); // Eksik parametre Dapper tarafinda belirsiz bir hataya donusmesin diye burada yakalanir. var missing = endpoint.Parameters .Where(parameter => parameter.IsRequired && !parameters.ContainsKey(parameter.Name)) .Select(parameter => parameter.Name) .ToList(); if (missing.Count > 0) { throw new UserFriendlyException(L[McpErrorCodes.MissingParameters, string.Join(", ", missing)]); } var context = await queryExecutor.ResolveDataSourceAsync(endpoint.DataSourceCode); return ToResult(await queryExecutor.ExecuteAsync(context, endpoint.Sql, parameters, maxRows)); } private async Task> GetCallableCustomEndpointsAsync() { var endpoints = await GetCallableEndpointEntitiesAsync(); return [.. endpoints.Select(endpoint => new McpSavedQueryDto { Kind = McpConsts.SavedQueryKinds.CustomEndpoint, Name = endpoint.Name, Description = endpoint.Description, ParameterNames = [.. endpoint.Parameters.Select(parameter => parameter.Name)] })]; } /// /// Yalnizca okuma amacli (GET) ve istegi yapan kullaniciya acik Custom Endpoint kayitlari. /// Yetki sozlesmesi endpoint dispatcher'indaki ile birebir aynidir. /// private async Task> GetCallableEndpointEntitiesAsync() { var query = await customEndpointRepository.GetQueryableAsync(); var candidates = query.Where(endpoint => endpoint.Method == "GET").ToList(); return [.. candidates.Where(endpoint => endpoint.Permissions.Any(permission => permission.ResourceType == "Global" || (permission.ResourceType == "User" && permission.ResourceId == CurrentUser.UserName) || (permission.ResourceType == "Role" && CurrentUser.Roles.Contains(permission.ResourceId))))]; } private string BuildSavedQuerySql( string? kind, string qualifiedName, Dictionary parameters, int maxRows, DataSourceTypeEnum dataSourceType) { var parameterList = string.Join(", ", parameters.Keys.Select(name => $"@{name}")); if (IsKind(kind, McpConsts.SavedQueryKinds.View)) { return sqlGuard.ApplyRowLimit($"SELECT * FROM {qualifiedName}", maxRows, dataSourceType); } if (IsKind(kind, McpConsts.SavedQueryKinds.TableFunction)) { return sqlGuard.ApplyRowLimit($"SELECT * FROM {qualifiedName}({parameterList})", maxRows, dataSourceType); } if (IsKind(kind, McpConsts.SavedQueryKinds.ScalarFunction)) { return $"SELECT {qualifiedName}({parameterList}) AS Value"; } if (IsKind(kind, McpConsts.SavedQueryKinds.Procedure)) { // Prosedur cagrisina satir tavani yazilamaz; sonuc kumesi okunurken kesilir. return dataSourceType == DataSourceTypeEnum.Postgresql ? $"CALL {qualifiedName}({parameterList})" : $"EXEC {qualifiedName} {string.Join(", ", parameters.Keys.Select(name => $"@{name} = @{name}"))}".TrimEnd(); } throw new UserFriendlyException(L[McpErrorCodes.UnsupportedObjectKind, kind ?? string.Empty]); } private static string? NullIfEmpty(string? value) => string.IsNullOrWhiteSpace(value) ? null : value; private static McpQueryResultDto ToResult(McpQueryRows rows) => new() { Columns = rows.Columns, Rows = rows.Rows, RowCount = rows.Rows.Count, Truncated = rows.Truncated, ExecutionTimeMs = rows.ExecutionTimeMs }; private static bool IsKind(string? kind, string expected) => string.Equals(kind, expected, StringComparison.OrdinalIgnoreCase); private static string? MapCatalogKind(string? catalogKind) => catalogKind switch { McpConsts.CatalogKinds.View => McpConsts.SavedQueryKinds.View, McpConsts.CatalogKinds.Procedure => McpConsts.SavedQueryKinds.Procedure, McpConsts.CatalogKinds.TableFunction => McpConsts.SavedQueryKinds.TableFunction, McpConsts.CatalogKinds.ScalarFunction => McpConsts.SavedQueryKinds.ScalarFunction, _ => null }; private static string? ReadString(Dictionary row, string key) => row.TryGetValue(key, out var value) ? value?.ToString() : null; }