feat: MCP 클라이언트 통합 및 TextToSqlController MCP 엔드포인트 추가
- IMcpService 인터페이스 및 McpClient/McpService 구현 추가 - McpClient: Python MCP 서버(localhost:5001)와 JSON-RPC over HTTP 통신 - McpService: McpClient 위임 래퍼, IMcpService 구현 - [JsonPropertyName], PropertyNameCaseInsensitive 적용으로 JSON 역직렬화 수정 - TextToSqlController에 MCP 엔드포인트 5개 추가 - GET /api/text-to-sql/tools - POST /api/text-to-sql/execute-mcp - POST /api/text-to-sql/query-history - GET /api/text-to-sql/tags/search - GET /api/text-to-sql/drawings - HistoryQueryRequestDto 추가 (TextToSqlDtos.cs) - QueryHistoryWithIntervalAsync 올바른 메서드 호출로 수정 (IExperionDbService) - Program.cs: McpClient 싱글톤 등록, AddHttpClient 잘못된 등록 수정 - 빌드 에러 0건, 경고 0건 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,15 @@ public class SqlQueryResultDto
|
|||||||
public int TotalCount { get; set; }
|
public int TotalCount { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>MCP query_pv_history 엔드포인트 요청 DTO</summary>
|
||||||
|
public class HistoryQueryRequestDto
|
||||||
|
{
|
||||||
|
public List<string>? TagNames { get; set; }
|
||||||
|
public string? From { get; set; }
|
||||||
|
public string? To { get; set; }
|
||||||
|
public int? Limit { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class TimeSeriesAnalysisDto
|
public class TimeSeriesAnalysisDto
|
||||||
{
|
{
|
||||||
public bool Success { get; set; }
|
public bool Success { get; set; }
|
||||||
|
|||||||
65
src/Core/Application/Interfaces/IMcpService.cs
Normal file
65
src/Core/Application/Interfaces/IMcpService.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
namespace ExperionCrawler.Core.Application.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP (Model Context Protocol) 서비스 인터페이스
|
||||||
|
/// Python MCP 서버와의 통신 담당
|
||||||
|
/// </summary>
|
||||||
|
public interface IMcpService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// MCP 서버 상태 확인
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> PingAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 도구(tool) 목록 조회
|
||||||
|
/// </summary>
|
||||||
|
Task<List<McpToolDto>> ListToolsAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// run_sql 도구 호출 - 안전한 SQL 실행
|
||||||
|
/// </summary>
|
||||||
|
Task<McpQueryResult> RunSqlAsync(string sql);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// query_pv_history 도구 호출 - 과거 값 히스토리 조회
|
||||||
|
/// </summary>
|
||||||
|
Task<McpQueryResult> QueryPvHistoryAsync(
|
||||||
|
List<string> tagNames,
|
||||||
|
string timeFrom,
|
||||||
|
string timeTo,
|
||||||
|
int limit = 100);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// get_tag_metadata 도구 호출 - 태그 메타데이터 검색
|
||||||
|
/// </summary>
|
||||||
|
Task<McpQueryResult> GetTagMetadataAsync(string query, int limit = 10);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// list_drawings 도구 호출 - 도면 목록 조회
|
||||||
|
/// </summary>
|
||||||
|
Task<McpQueryResult> ListDrawingsAsync(string? unitNo = null);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region DTOs
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP 도구 DTO
|
||||||
|
/// </summary>
|
||||||
|
public class McpToolDto
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP 쿼리 결과 DTO
|
||||||
|
/// </summary>
|
||||||
|
public class McpQueryResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string? Error { get; set; }
|
||||||
|
public string? Data { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
185
src/Infrastructure/Mcp/McpClient.cs
Normal file
185
src/Infrastructure/Mcp/McpClient.cs
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace ExperionCrawler.Infrastructure.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Python FastMCP 서버 (localhost:5001)와 JSON-RPC over HTTP로 통신하는 저수준 클라이언트.
|
||||||
|
/// 모델 클래스(McpResponse 등)도 여기서 단일 관리한다.
|
||||||
|
/// </summary>
|
||||||
|
public class McpClient
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private const string BaseUrl = "http://localhost:5001";
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions _jsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
public McpClient(HttpClient? httpClient = null)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient ?? new HttpClient
|
||||||
|
{
|
||||||
|
BaseAddress = new Uri(BaseUrl),
|
||||||
|
Timeout = TimeSpan.FromSeconds(30)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> PingAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await _httpClient.GetAsync("/health");
|
||||||
|
return response.IsSuccessStatusCode;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<McpTool>> ListToolsAsync()
|
||||||
|
{
|
||||||
|
var request = new
|
||||||
|
{
|
||||||
|
jsonrpc = "2.0",
|
||||||
|
id = Guid.NewGuid().ToString(),
|
||||||
|
method = "tools/list"
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await SendRequestAsync(request);
|
||||||
|
if (response?.result?.tools == null)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return [.. response.result.tools];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string> CallToolAsync(string toolName, Dictionary<string, object> arguments)
|
||||||
|
{
|
||||||
|
var request = new
|
||||||
|
{
|
||||||
|
jsonrpc = "2.0",
|
||||||
|
id = Guid.NewGuid().ToString(),
|
||||||
|
method = "tools/call",
|
||||||
|
@params = new { name = toolName, arguments = arguments }
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await SendRequestAsync(request);
|
||||||
|
var content = response?.result?.content;
|
||||||
|
if (content == null || content.Length == 0)
|
||||||
|
return "호출 결과 없음";
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (var item in content)
|
||||||
|
{
|
||||||
|
if (item.type == "text")
|
||||||
|
sb.AppendLine(item.text);
|
||||||
|
else if (item.type == "image")
|
||||||
|
sb.AppendLine($"[이미지: {item.data ?? "blob"}]");
|
||||||
|
}
|
||||||
|
return sb.Length > 0 ? sb.ToString().TrimEnd() : "호출 결과 없음";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return $"도구 호출 실패: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string> RunSqlAsync(string sql) =>
|
||||||
|
CallToolAsync("run_sql", new Dictionary<string, object> { ["sql"] = sql });
|
||||||
|
|
||||||
|
public Task<string> QueryPvHistoryAsync(
|
||||||
|
List<string> tagNames, string timeFrom, string timeTo, int limit = 100) =>
|
||||||
|
CallToolAsync("query_pv_history", new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["tag_names"] = tagNames,
|
||||||
|
["time_from"] = timeFrom,
|
||||||
|
["time_to"] = timeTo,
|
||||||
|
["limit"] = limit
|
||||||
|
});
|
||||||
|
|
||||||
|
public Task<string> GetTagMetadataAsync(string query, int limit = 10) =>
|
||||||
|
CallToolAsync("get_tag_metadata", new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["query"] = query,
|
||||||
|
["limit"] = limit
|
||||||
|
});
|
||||||
|
|
||||||
|
public Task<string> ListDrawingsAsync(string? unitNo = null)
|
||||||
|
{
|
||||||
|
var args = new Dictionary<string, object>();
|
||||||
|
if (!string.IsNullOrEmpty(unitNo))
|
||||||
|
args["unit_no"] = unitNo;
|
||||||
|
return CallToolAsync("list_drawings", args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<McpResponse?> SendRequestAsync(object request)
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(request);
|
||||||
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
content.Headers.Add("Accept", "application/json");
|
||||||
|
|
||||||
|
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/mcp")
|
||||||
|
{
|
||||||
|
Content = content
|
||||||
|
};
|
||||||
|
httpRequest.Headers.Add("Accept", "application/json");
|
||||||
|
httpRequest.Headers.Add("mcp-protocol-version", "2025-03-26");
|
||||||
|
|
||||||
|
var response = await _httpClient.SendAsync(httpRequest);
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var body = await response.Content.ReadAsStringAsync();
|
||||||
|
return JsonSerializer.Deserialize<McpResponse>(body, _jsonOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region MCP JSON-RPC 모델
|
||||||
|
|
||||||
|
public class McpResponse
|
||||||
|
{
|
||||||
|
public string? jsonrpc { get; set; }
|
||||||
|
public string? id { get; set; }
|
||||||
|
public McpErrorBody? error { get; set; }
|
||||||
|
public McpResult? result { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class McpErrorBody
|
||||||
|
{
|
||||||
|
public int? code { get; set; }
|
||||||
|
public string? message { get; set; }
|
||||||
|
public override string ToString() => message ?? "(오류 메시지 없음)";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class McpResult
|
||||||
|
{
|
||||||
|
public McpTool[]? tools { get; set; }
|
||||||
|
public McpContentItem[]? content { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class McpTool
|
||||||
|
{
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("description")]
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("inputSchema")]
|
||||||
|
public JsonElement? InputSchema { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class McpContentItem
|
||||||
|
{
|
||||||
|
public string type { get; set; } = string.Empty;
|
||||||
|
public string? text { get; set; }
|
||||||
|
public string? data { get; set; }
|
||||||
|
public string? mimeType { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
79
src/Infrastructure/Mcp/McpService.cs
Normal file
79
src/Infrastructure/Mcp/McpService.cs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
using ExperionCrawler.Core.Application.Interfaces;
|
||||||
|
|
||||||
|
namespace ExperionCrawler.Infrastructure.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// IMcpService 구현체 — McpClient를 통해 Python MCP 서버와 통신한다.
|
||||||
|
/// 모든 HTTP/JSON 로직은 McpClient에 위임하고,
|
||||||
|
/// 이 클래스는 McpQueryResult 래핑만 담당한다.
|
||||||
|
/// </summary>
|
||||||
|
public class McpService : IMcpService
|
||||||
|
{
|
||||||
|
private readonly McpClient _client;
|
||||||
|
|
||||||
|
public McpService(McpClient client)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> PingAsync() => _client.PingAsync();
|
||||||
|
|
||||||
|
public async Task<List<McpToolDto>> ListToolsAsync()
|
||||||
|
{
|
||||||
|
var tools = await _client.ListToolsAsync();
|
||||||
|
return tools.Select(t => new McpToolDto { Name = t.Name, Description = t.Description }).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<McpQueryResult> RunSqlAsync(string sql)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = await _client.RunSqlAsync(sql);
|
||||||
|
return new McpQueryResult { Success = true, Data = data };
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new McpQueryResult { Success = false, Error = ex.Message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<McpQueryResult> QueryPvHistoryAsync(
|
||||||
|
List<string> tagNames, string timeFrom, string timeTo, int limit = 100)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = await _client.QueryPvHistoryAsync(tagNames, timeFrom, timeTo, limit);
|
||||||
|
return new McpQueryResult { Success = true, Data = data };
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new McpQueryResult { Success = false, Error = ex.Message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<McpQueryResult> GetTagMetadataAsync(string query, int limit = 10)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = await _client.GetTagMetadataAsync(query, limit);
|
||||||
|
return new McpQueryResult { Success = true, Data = data };
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new McpQueryResult { Success = false, Error = ex.Message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<McpQueryResult> ListDrawingsAsync(string? unitNo = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = await _client.ListDrawingsAsync(unitNo);
|
||||||
|
return new McpQueryResult { Success = true, Data = data };
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new McpQueryResult { Success = false, Error = ex.Message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using ExperionCrawler.Core.Application.DTOs;
|
using ExperionCrawler.Core.Application.DTOs;
|
||||||
using ExperionCrawler.Core.Application.Interfaces;
|
using ExperionCrawler.Core.Application.Interfaces;
|
||||||
|
using ExperionCrawler.Infrastructure.Mcp;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -7,24 +8,28 @@ namespace ExperionCrawler.Web.Controllers;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Text-to-SQL API 컨트롤러
|
/// Text-to-SQL API 컨트롤러
|
||||||
/// 자연어 질의를 SQL로 변환하고 시계열 데이터를 조회합니다.
|
/// 자연어 질의를 파싱하고 시계열 데이터를 조회합니다.
|
||||||
|
/// MCP (Model Context Protocol) 통합을 위한 엔드포인트를 제공합니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/text-to-sql")]
|
[Route("api/text-to-sql")]
|
||||||
public class TextToSqlController : ControllerBase
|
public class TextToSqlController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly ITextToSqlService _textToSqlService;
|
private readonly ITextToSqlService _textToSqlService;
|
||||||
private readonly IExperionDbService _experionDbService;
|
private readonly IExperionDbService _dbService;
|
||||||
|
private readonly IMcpService _mcpService;
|
||||||
private readonly ILogger<TextToSqlController> _logger;
|
private readonly ILogger<TextToSqlController> _logger;
|
||||||
|
|
||||||
public TextToSqlController(
|
public TextToSqlController(
|
||||||
ITextToSqlService textToSqlService,
|
ITextToSqlService textToSqlService,
|
||||||
IExperionDbService experionDbService,
|
IExperionDbService dbService,
|
||||||
|
IMcpService mcpService,
|
||||||
ILogger<TextToSqlController> logger)
|
ILogger<TextToSqlController> logger)
|
||||||
{
|
{
|
||||||
_textToSqlService = textToSqlService;
|
_textToSqlService = textToSqlService;
|
||||||
_experionDbService = experionDbService;
|
_dbService = dbService;
|
||||||
_logger = logger;
|
_mcpService = mcpService;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -45,19 +50,208 @@ public class TextToSqlController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// SQL 쿼리 실행 및 결과 반환
|
/// MCP 도구 목록 조회
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[HttpPost("execute")]
|
[HttpGet("tools")]
|
||||||
public async Task<IActionResult> Execute([FromBody] SqlQueryDto dto)
|
public async Task<IActionResult> ListTools()
|
||||||
{
|
{
|
||||||
var result = await _textToSqlService.ExecuteQueryAsync(dto.Sql, dto.Limit);
|
try
|
||||||
return Ok(new {
|
{
|
||||||
success = result.Success,
|
var tools = await _mcpService.ListToolsAsync();
|
||||||
error = result.Error,
|
return Ok(new { success = true, tools });
|
||||||
columns = result.Columns,
|
}
|
||||||
rows = result.Rows,
|
catch (Exception ex)
|
||||||
totalCount = result.TotalCount
|
{
|
||||||
});
|
_logger.LogError(ex, "[TextToSql] 도구 목록 조회 실패");
|
||||||
|
return Ok(new { success = false, error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP run_sql 도구 호출 - SQL 실행
|
||||||
|
/// Text-to-SQL 엔진으로 생성된 SQL을 안전하게 실행
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("execute-mcp")]
|
||||||
|
public async Task<IActionResult> ExecuteFromMcp([FromBody] SqlQueryDto dto)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(dto.Sql))
|
||||||
|
{
|
||||||
|
return BadRequest(new { success = false, error = "SQL이 비어있음" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// MCP run_sql 도구 호출
|
||||||
|
var result = await _mcpService.RunSqlAsync(dto.Sql);
|
||||||
|
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = result.Error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON 결과 반환 (쿼리 결과)
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var jsonData = System.Text.Json.JsonSerializer.Deserialize<object>(result.Data);
|
||||||
|
return Ok(new { success = true, data = jsonData });
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return Ok(new { success = true, data = result.Data });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "[TextToSql] MCP 실행 실패");
|
||||||
|
return Ok(new { success = false, error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP query_pv_history 도구 호출 - 과거 값 히스토리 조회
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("query-history")]
|
||||||
|
public async Task<IActionResult> QueryHistory([FromBody] HistoryQueryRequestDto dto)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tagNames = dto.TagNames ?? [];
|
||||||
|
var timeFrom = dto.From ?? DateTime.UtcNow.AddDays(-1).ToString("o");
|
||||||
|
var timeTo = dto.To ?? DateTime.UtcNow.ToString("o");
|
||||||
|
var limit = dto.Limit ?? 100;
|
||||||
|
|
||||||
|
var result = await _mcpService.QueryPvHistoryAsync(
|
||||||
|
tagNames,
|
||||||
|
timeFrom,
|
||||||
|
timeTo,
|
||||||
|
limit
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = result.Error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var jsonData = System.Text.Json.JsonSerializer.Deserialize<object>(result.Data);
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
data = jsonData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
data = result.Data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "[TextToSql] History 쿼리 실패");
|
||||||
|
return Ok(new { success = false, error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP get_tag_metadata 도구 호출 - 태그 메타데이터 검색
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("tags/search")]
|
||||||
|
public async Task<IActionResult> SearchTags([FromQuery] string query, [FromQuery] int? limit)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tagLimit = limit ?? 10;
|
||||||
|
var result = await _mcpService.GetTagMetadataAsync(query, tagLimit);
|
||||||
|
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = result.Error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var jsonData = System.Text.Json.JsonSerializer.Deserialize<object>(result.Data);
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
data = jsonData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
data = result.Data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "[TextToSql] 태그 검색 실패");
|
||||||
|
return Ok(new { success = false, error = ex.Message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP list_drawings 도구 호출 - 도면 목록 조회
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("drawings")]
|
||||||
|
public async Task<IActionResult> ListDrawings([FromQuery] string? unitNo)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _mcpService.ListDrawingsAsync(unitNo);
|
||||||
|
|
||||||
|
if (!result.Success)
|
||||||
|
{
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
success = false,
|
||||||
|
error = result.Error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var jsonData = System.Text.Json.JsonSerializer.Deserialize<object>(result.Data);
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
data = jsonData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
success = true,
|
||||||
|
data = result.Data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "[TextToSql] 도면 목록 조회 실패");
|
||||||
|
return Ok(new { success = false, error = ex.Message });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -112,8 +306,8 @@ public class TextToSqlController : ControllerBase
|
|||||||
dto.Interval,
|
dto.Interval,
|
||||||
dto.Limit);
|
dto.Limit);
|
||||||
|
|
||||||
var result = await _experionDbService.QueryHistoryWithIntervalAsync(request);
|
var result = await _dbService.QueryHistoryWithIntervalAsync(request);
|
||||||
|
|
||||||
var response = new
|
var response = new
|
||||||
{
|
{
|
||||||
success = true,
|
success = true,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using ExperionCrawler.Core.Application.Services;
|
|||||||
using ExperionCrawler.Infrastructure.Certificates;
|
using ExperionCrawler.Infrastructure.Certificates;
|
||||||
using ExperionCrawler.Infrastructure.Csv;
|
using ExperionCrawler.Infrastructure.Csv;
|
||||||
using ExperionCrawler.Infrastructure.Database;
|
using ExperionCrawler.Infrastructure.Database;
|
||||||
|
using ExperionCrawler.Infrastructure.Mcp;
|
||||||
using ExperionCrawler.Infrastructure.OpcUa;
|
using ExperionCrawler.Infrastructure.OpcUa;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
@@ -61,6 +62,12 @@ builder.Services.AddHostedService(
|
|||||||
sp => sp.GetRequiredService<ExperionRealtimeService>());
|
sp => sp.GetRequiredService<ExperionRealtimeService>());
|
||||||
builder.Services.AddHostedService<ExperionHistoryService>();
|
builder.Services.AddHostedService<ExperionHistoryService>();
|
||||||
|
|
||||||
|
// ── MCP Service ───────────────────────────────────────────────────────────────
|
||||||
|
// Python MCP 서버 (localhost:5001)와 통신
|
||||||
|
// McpClient: 저수준 HTTP 클라이언트 / McpService: IMcpService 구현 (McpClient 위임)
|
||||||
|
builder.Services.AddSingleton<McpClient>();
|
||||||
|
builder.Services.AddSingleton<IMcpService, McpService>();
|
||||||
|
|
||||||
// ── OPC UA Server BackgroundService ──────────────────────────────────────────
|
// ── OPC UA Server BackgroundService ──────────────────────────────────────────
|
||||||
builder.Services.AddSingleton<ExperionOpcServerService>();
|
builder.Services.AddSingleton<ExperionOpcServerService>();
|
||||||
builder.Services.AddSingleton<IExperionOpcServerService>(
|
builder.Services.AddSingleton<IExperionOpcServerService>(
|
||||||
|
|||||||
Reference in New Issue
Block a user