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; }
|
||||
}
|
||||
|
||||
/// <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 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.Interfaces;
|
||||
using ExperionCrawler.Infrastructure.Mcp;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -7,24 +8,28 @@ namespace ExperionCrawler.Web.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Text-to-SQL API 컨트롤러
|
||||
/// 자연어 질의를 SQL로 변환하고 시계열 데이터를 조회합니다.
|
||||
/// 자연어 질의를 파싱하고 시계열 데이터를 조회합니다.
|
||||
/// MCP (Model Context Protocol) 통합을 위한 엔드포인트를 제공합니다.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/text-to-sql")]
|
||||
public class TextToSqlController : ControllerBase
|
||||
{
|
||||
private readonly ITextToSqlService _textToSqlService;
|
||||
private readonly IExperionDbService _experionDbService;
|
||||
private readonly IExperionDbService _dbService;
|
||||
private readonly IMcpService _mcpService;
|
||||
private readonly ILogger<TextToSqlController> _logger;
|
||||
|
||||
public TextToSqlController(
|
||||
ITextToSqlService textToSqlService,
|
||||
IExperionDbService experionDbService,
|
||||
IExperionDbService dbService,
|
||||
IMcpService mcpService,
|
||||
ILogger<TextToSqlController> logger)
|
||||
{
|
||||
_textToSqlService = textToSqlService;
|
||||
_experionDbService = experionDbService;
|
||||
_logger = logger;
|
||||
_textToSqlService = textToSqlService;
|
||||
_dbService = dbService;
|
||||
_mcpService = mcpService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -45,19 +50,208 @@ public class TextToSqlController : ControllerBase
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SQL 쿼리 실행 및 결과 반환
|
||||
/// MCP 도구 목록 조회
|
||||
/// </summary>
|
||||
[HttpPost("execute")]
|
||||
public async Task<IActionResult> Execute([FromBody] SqlQueryDto dto)
|
||||
[HttpGet("tools")]
|
||||
public async Task<IActionResult> ListTools()
|
||||
{
|
||||
var result = await _textToSqlService.ExecuteQueryAsync(dto.Sql, dto.Limit);
|
||||
return Ok(new {
|
||||
success = result.Success,
|
||||
error = result.Error,
|
||||
columns = result.Columns,
|
||||
rows = result.Rows,
|
||||
totalCount = result.TotalCount
|
||||
});
|
||||
try
|
||||
{
|
||||
var tools = await _mcpService.ListToolsAsync();
|
||||
return Ok(new { success = true, tools });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_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>
|
||||
@@ -112,8 +306,8 @@ public class TextToSqlController : ControllerBase
|
||||
dto.Interval,
|
||||
dto.Limit);
|
||||
|
||||
var result = await _experionDbService.QueryHistoryWithIntervalAsync(request);
|
||||
|
||||
var result = await _dbService.QueryHistoryWithIntervalAsync(request);
|
||||
|
||||
var response = new
|
||||
{
|
||||
success = true,
|
||||
|
||||
@@ -3,6 +3,7 @@ using ExperionCrawler.Core.Application.Services;
|
||||
using ExperionCrawler.Infrastructure.Certificates;
|
||||
using ExperionCrawler.Infrastructure.Csv;
|
||||
using ExperionCrawler.Infrastructure.Database;
|
||||
using ExperionCrawler.Infrastructure.Mcp;
|
||||
using ExperionCrawler.Infrastructure.OpcUa;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@@ -61,6 +62,12 @@ builder.Services.AddHostedService(
|
||||
sp => sp.GetRequiredService<ExperionRealtimeService>());
|
||||
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 ──────────────────────────────────────────
|
||||
builder.Services.AddSingleton<ExperionOpcServerService>();
|
||||
builder.Services.AddSingleton<IExperionOpcServerService>(
|
||||
|
||||
Reference in New Issue
Block a user