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;
}