sozsoft-platform/api/modules/Sozsoft.Mcp/Sozsoft.Mcp.Application/McpDevelopAppService.cs
2026-09-25 09:39:34 +03:00

693 lines
26 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
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.Entities;
using Sozsoft.Platform.Enums;
using Sozsoft.Platform.ListForms;
using Sozsoft.Platform.ListForms.Select;
using Volo.Abp;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.PermissionManagement;
namespace Sozsoft.Mcp;
/// <summary>
/// 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. Invoke/Write yetkisi
/// yalnizca MCP kapisidir, arkadaki yetkinin yerine gecmez.
/// </summary>
[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Invoke)]
public class McpDevelopAppService(
IMcpServerConfigProvider serverConfigProvider,
IMcpDevGuide devGuide,
IMcpTableCreator tableCreator,
IListFormWizardAppService wizardAppService,
ICustomComponentAppService componentAppService,
IListFormSelectAppService listFormSelectAppService,
IRepository<Menu, Guid> menuRepository,
IRepository<PermissionDefinitionRecord, Guid> permissionRepository,
IRepository<DevRequest, Guid> devRequestRepository)
: McpAppService(serverConfigProvider), IMcpDevelopAppService
{
private const string DefaultGuideDocument = "lowcode";
/// <summary>Index cevabinda listelenen en derin baslik; daha derini bolum istenince gorunur.</summary>
private const int IndexHeadingLevel = 3;
/// <summary>Custom Components ekraninin tek sayfada dondurebildigi ust sinir.</summary>
private const int ComponentPageSize = 1000;
private const string DevRequestUpdatePermission = PlatformConsts.AppCodes.DevRequests + ".Update";
/// <summary>Talep ekranindaki alt gorev tarih kutusunun okudugu bicim.</summary>
private const string SubTaskDateFormat = "yyyy-MM-dd";
/// <summary>verify_wizard cevabindaki kontrol adlari.</summary>
private static class VerifyChecks
{
public const string WizardFile = "wizard_file";
public const string Install = "install";
public const string Menu = "menu";
public const string Permission = "permission";
public const string Select = "select";
public const string Component = "component";
public const string SeedClosure = "seed_closure";
}
public async Task<McpDevGuideDto> 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)]
};
}
/// <summary>
/// Tablo tasarimcisiyla ayni yetkiyi ister: ham DDL liste yetkilerini asar, Develop yalnizca
/// MCP kapisidir.
/// </summary>
public async Task<McpCreateTableResultDto> 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<List<WizardFileInfoDto>> 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<McpWizardFileDto> 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<WizardFileInfoDto?> 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));
}
/// <summary>
/// Ekranin tablosu ancak <paramref name="dropTables"/> acikken ve Wizard Manager'daki onayla ayni
/// kosullarda (secilebilir aday) dusurulur; onayi kullanicidan almak istemcinin isidir.
/// </summary>
public async Task<WizardDeleteResultDto> DeleteWizardAsync(string fileName, bool dropTables)
{
await EnsureToolAllowedAsync(McpToolNames.DeleteWizard);
var input = new WizardDeleteInput { FileName = fileName };
if (dropTables)
{
var analysis = await wizardAppService.DeleteAnalysis(fileName);
input.DropTables = [.. analysis.Tables.Where(t => t.CanDrop).Select(t => t.TableName)];
}
return await wizardAppService.DeleteFile(input);
}
/// <summary>
/// Ekrani kullanicinin gorecegi yoldan dener: menu kaydi, menunun istedigi yetki, ekranin
/// sorgusu (cagiranin yetkisi ve kiraci filtresiyle, tek satir) ve Custom yolunda bilesen.
/// Hicbir kontrol veri yazmaz; test satiri eklemek SeedSync acik ekranlarda seed dosyasina
/// iz birakirdi. Seed kapanisi engellemez: ekran bu ortamda calisir, eksik dosya yalnizca
/// migrate ya da baska bir ortamda ortaya cikar.
/// </summary>
public async Task<McpVerifyResultDto> VerifyWizardAsync(string fileName)
{
await EnsureToolAllowedAsync(McpToolNames.VerifyWizard);
var result = new McpVerifyResultDto { FileName = fileName };
ListFormWizardDto? wizard;
try
{
wizard = (await wizardAppService.GetFile(fileName)).Wizard;
}
catch (Exception ex)
{
result.Checks.Add(Failed(VerifyChecks.WizardFile, L[McpErrorCodes.VerifyWizardFileInvalid, fileName, ex.Message]));
return result;
}
if (wizard is null)
{
result.Checks.Add(Failed(VerifyChecks.WizardFile, L[McpErrorCodes.InvalidWizard]));
return result;
}
var listFormCode = wizard.ComponentKind == WizardComponentKindEnum.Custom ? null : wizard.ResolveListFormCode();
result.ListFormCode = listFormCode;
result.MenuUrl = wizard.MenuUrl;
result.Checks.Add(new McpVerifyCheckDto { Name = VerifyChecks.WizardFile, Passed = true });
// Deploy yalnizca seed dosyalarini yazar; menu, yetki ve ekran DB Migrate'ten sonra vardir.
// Kurulmamis bir wizard'in geri kalan kontrolleri anlamsiz hatalar uretirdi.
var file = (await wizardAppService.GetFiles())
.FirstOrDefault(f => string.Equals(f.FileName, fileName, StringComparison.OrdinalIgnoreCase));
if (file?.IsPending == true)
{
result.Checks.Add(Failed(VerifyChecks.Install, L[McpErrorCodes.VerifyWizardPending, fileName]));
return result;
}
var permissionName = listFormCode;
if (wizard.CreateMenu)
{
var menu = await VerifyMenuAsync(wizard, result.Checks);
permissionName = string.IsNullOrWhiteSpace(menu?.RequiredPermissionName) ? permissionName : menu.RequiredPermissionName;
}
if (!string.IsNullOrWhiteSpace(permissionName))
{
var permission = await permissionRepository.FirstOrDefaultAsync(p => p.Name == permissionName);
result.Checks.Add(permission is { IsEnabled: true }
? new McpVerifyCheckDto { Name = VerifyChecks.Permission, Passed = true, Message = permissionName }
: Failed(VerifyChecks.Permission, L[McpErrorCodes.VerifyPermissionMissing, permissionName]));
}
result.Checks.Add(listFormCode is null
? await VerifyComponentAsync(wizard)
: await VerifySelectAsync(listFormCode));
result.Checks.Add(await VerifySeedClosureAsync(fileName));
result.Passed = result.Checks.TrueForAll(check => check.Passed || !check.Blocking);
return result;
}
/// <summary>
/// Durum kurali modelin dikkatine birakilmaz: talep Ready to Start degilse reddedilir, uygunsa
/// ayni unit of work'te In Progress'e alinir. MCP yetki vermez; ekranin guncelleme yetkisi
/// ayrica istenir.
/// </summary>
public async Task<McpDevRequestDto> StartDevRequestAsync(string request)
{
await EnsureToolAllowedAsync(McpToolNames.StartDevRequest);
await AuthorizationService.CheckAsync(DevRequestUpdatePermission);
var devRequest = await FindDevRequestAsync(request);
if (!string.Equals(devRequest.Status, McpConsts.DevRequestStatuses.ReadyToStart, StringComparison.Ordinal))
{
throw new UserFriendlyException(L[McpErrorCodes.DevRequestNotReady, devRequest.Title ?? devRequest.Id.ToString(), devRequest.Status ?? string.Empty]);
}
devRequest.Status = McpConsts.DevRequestStatuses.InProgress;
await devRequestRepository.UpdateAsync(devRequest, autoSave: true);
return ToDto(devRequest);
}
public async Task<McpDevRequestDto> GetDevRequestAsync(string request)
{
await EnsureToolAllowedAsync(McpToolNames.GetDevRequest);
await AuthorizationService.CheckAsync(PlatformConsts.AppCodes.DevRequests);
return ToDto(await FindDevRequestAsync(request));
}
/// <summary>
/// Aciklama base64 gorsel tasir; model onu update_row ile yeniden yazarsa gorselleri geri
/// gonderemez ve siler. Ekleme bu yuzden sunucuda, var olan metne dokunmadan yapilir.
/// </summary>
public async Task AppendDevRequestNoteAsync(string request, string html)
{
await EnsureToolAllowedAsync(McpToolNames.AppendDevRequestNote);
await AuthorizationService.CheckAsync(DevRequestUpdatePermission);
if (string.IsNullOrWhiteSpace(html))
{
throw new UserFriendlyException(L[McpErrorCodes.DevRequestNoteRequired]);
}
var devRequest = await FindDevRequestAsync(request);
devRequest.Description = string.Concat(devRequest.Description, html.Trim());
await devRequestRepository.UpdateAsync(devRequest, autoSave: true);
}
/// <summary>
/// Atanan kisi ve tarih modelin dikkatine birakilmaz: satiri yazan cagiran kullanicidir
/// (ekranin atanan listesi kullanici adiyla calisir), bitis tarihi yazildigi gundur.
/// Var olan satirlara dokunulmaz.
/// </summary>
public async Task AddDevRequestSubTasksAsync(string request, List<string> titles)
{
await EnsureToolAllowedAsync(McpToolNames.AddDevRequestSubTasks);
await AuthorizationService.CheckAsync(DevRequestUpdatePermission);
var items = (titles ?? []).Select(title => title?.Trim()).Where(title => !string.IsNullOrEmpty(title)).ToList();
if (items.Count == 0)
{
throw new UserFriendlyException(L[McpErrorCodes.DevRequestSubTasksRequired]);
}
var devRequest = await FindDevRequestAsync(request);
JsonArray subTasks;
try
{
subTasks = string.IsNullOrWhiteSpace(devRequest.SubTasks)
? []
: JsonNode.Parse(devRequest.SubTasks) as JsonArray ?? throw new JsonException();
}
catch (JsonException)
{
throw new UserFriendlyException(L[McpErrorCodes.DevRequestSubTasksInvalid, devRequest.Title ?? devRequest.Id.ToString()]);
}
var assignee = CurrentUser.UserName ?? string.Empty;
var dueDate = Clock.Now.ToString(SubTaskDateFormat, CultureInfo.InvariantCulture);
foreach (var title in items)
{
subTasks.Add(new JsonObject
{
["id"] = GuidGenerator.Create().ToString(),
["title"] = title,
["assignee"] = assignee,
["dueDate"] = dueDate,
["completed"] = true
});
}
devRequest.SubTasks = subTasks.ToJsonString();
await devRequestRepository.UpdateAsync(devRequest, autoSave: true);
}
private static McpDevRequestDto ToDto(DevRequest devRequest)
{
var (description, images) = McpDevRequestImages.Extract(devRequest.Description);
return new McpDevRequestDto
{
Id = devRequest.Id,
Title = devRequest.Title,
Status = devRequest.Status,
Description = description,
Images = images,
Priority = devRequest.Priority,
Tags = devRequest.Tags,
DueDate = devRequest.DueDate,
UserName = devRequest.UserName,
TenantId = devRequest.TenantId,
SubTasks = devRequest.SubTasks
};
}
/// <summary>Id ile ya da tam baslikla bulur; ayni basligi tasiyan birden fazla talep varsa Id istenir.</summary>
private async Task<DevRequest> FindDevRequestAsync(string? request)
{
var value = request?.Trim() ?? string.Empty;
if (Guid.TryParse(value, out var id))
{
return await devRequestRepository.FindAsync(id)
?? throw new UserFriendlyException(L[McpErrorCodes.DevRequestNotFound, value]);
}
var matches = value.Length == 0 ? [] : await devRequestRepository.GetListAsync(r => r.Title == value);
return matches.Count switch
{
0 => throw new UserFriendlyException(L[McpErrorCodes.DevRequestNotFound, value]),
1 => matches[0],
_ => throw new UserFriendlyException(L[McpErrorCodes.DevRequestAmbiguous, value, string.Join(", ", matches.Select(r => r.Id))])
};
}
/// <summary>
/// Kapanis analizi wizard export'unun yetkisini ister; o yetki dogrulamanin kendisini
/// dusurmesin diye yoksa kontrol atlanir ve nedeni raporlanir.
/// </summary>
private async Task<McpVerifyCheckDto> VerifySeedClosureAsync(string fileName)
{
var check = new McpVerifyCheckDto { Name = VerifyChecks.SeedClosure, Blocking = false };
if (!await AuthorizationService.IsGrantedAsync(PlatformConsts.AppCodes.Listforms.Wizards.Export))
{
check.Message = L[McpErrorCodes.VerifySeedClosureSkipped, PlatformConsts.AppCodes.Listforms.Wizards.Export];
return check;
}
var warnings = (await wizardAppService.ExportAnalysis(fileName)).Warnings;
if (warnings.Count == 0)
{
check.Passed = true;
return check;
}
check.Message = L[McpErrorCodes.VerifySeedClosureIncomplete, string.Join(", ", warnings)];
return check;
}
private async Task<Menu?> VerifyMenuAsync(ListFormWizardDto wizard, List<McpVerifyCheckDto> checks)
{
var menuCode = wizard.ResolveMenuCode();
var menu = await menuRepository.FirstOrDefaultAsync(m => m.Code == menuCode);
if (menu is null)
{
checks.Add(Failed(VerifyChecks.Menu, L[McpErrorCodes.VerifyMenuMissing, menuCode]));
}
else if (menu.IsDisabled)
{
checks.Add(Failed(VerifyChecks.Menu, L[McpErrorCodes.VerifyMenuDisabled, menuCode]));
}
else if (!string.Equals(menu.Url, wizard.MenuUrl, StringComparison.OrdinalIgnoreCase))
{
checks.Add(Failed(VerifyChecks.Menu, L[McpErrorCodes.VerifyMenuUrlMismatch, menuCode, menu.Url, wizard.MenuUrl]));
}
else
{
checks.Add(new McpVerifyCheckDto { Name = VerifyChecks.Menu, Passed = true, Message = menu.Url });
}
return menu;
}
/// <summary>
/// Ekranin sorgusunu query_list ile ayni servisten calistirir; kolon, veri kaynagi ve yetki
/// hatalari burada ortaya cikar.
/// </summary>
private async Task<McpVerifyCheckDto> VerifySelectAsync(string listFormCode)
{
try
{
var select = await listFormSelectAppService.GetSelectAsync(new SelectRequestDto
{
ListFormCode = listFormCode,
Take = 1,
RequireTotalCount = true
});
return new McpVerifyCheckDto
{
Name = VerifyChecks.Select,
Passed = true,
Message = L[McpErrorCodes.VerifySelectPassed, select.TotalCount]
};
}
catch (Exception ex)
{
return Failed(VerifyChecks.Select, L[McpErrorCodes.VerifySelectFailed, ex.Message]);
}
}
private async Task<McpVerifyCheckDto> VerifyComponentAsync(ListFormWizardDto wizard)
{
var component = await FindComponentAsync(wizard.CustomComponentName);
if (component is null)
{
return Failed(VerifyChecks.Component, L[McpErrorCodes.ComponentNotFound, wizard.CustomComponentName]);
}
if (!component.IsActive)
{
return Failed(VerifyChecks.Component, L[McpErrorCodes.VerifyComponentInactive, component.Name]);
}
if (wizard.CreateMenu && !string.Equals(component.RoutePath, wizard.MenuUrl, StringComparison.OrdinalIgnoreCase))
{
return Failed(VerifyChecks.Component, L[McpErrorCodes.VerifyComponentRouteMismatch, component.Name, component.RoutePath ?? string.Empty, wizard.MenuUrl]);
}
return new McpVerifyCheckDto { Name = VerifyChecks.Component, Passed = true, Message = component.RoutePath };
}
private static McpVerifyCheckDto Failed(string name, string message)
=> new() { Name = name, Passed = false, Message = message };
public async Task<List<McpComponentSummaryDto>> 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<CustomComponentDto?> GetComponentAsync(string name)
{
await EnsureToolAllowedAsync(McpToolNames.GetComponent);
return await FindComponentAsync(name);
}
public async Task<CustomComponentDto> 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);
}
/// <summary>
/// Wizard'i seed dosyasi biciminden (<c>{"Wizard": {...}}</c>) ya da duz cevaplardan okur.
/// Iki bicim de dosyanin okuma kuralindan gecer; boylece Menu/Language bloklari ve
/// WizardCode/ModuleId adlari dosyadakiyle ayni anlama gelir.
/// </summary>
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<IReadOnlyList<CustomComponentDto>> GetAllComponentsAsync()
{
var page = await componentAppService.GetListAsync(new PagedAndSortedResultRequestDto
{
MaxResultCount = ComponentPageSize,
Sorting = nameof(CustomComponentDto.Name)
});
return page.Items;
}
private async Task<CustomComponentDto?> 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));
}
/// <summary>
/// Props icinde tasarimci belgesi varsa ve kip acikca <c>code</c> degilse kod belgeden
/// uretilir; tasarimcinin varsayilan kipi <c>visual</c>'dir.
/// </summary>
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
};
}