refactor: apply Option B, remove heartbeat and register tools once on startup
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 44s
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 44s
This commit is contained in:
@@ -37,15 +37,19 @@ dependencies {
|
||||
api 'io.lettuce:lettuce-core:6.6.0.RELEASE'
|
||||
|
||||
// MCI XML 전문, JSON Tool Schema/Manifest 처리에 사용합니다.
|
||||
api 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.17.1'
|
||||
api 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.1'
|
||||
api 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
|
||||
api 'com.networknt:json-schema-validator:3.0.0'
|
||||
api 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'
|
||||
api 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml'
|
||||
api 'com.fasterxml.jackson.core:jackson-databind'
|
||||
// MCP Java SDK 2.0.0의 mcp-json-jackson2가 호출하는 Schema API와 반드시 맞춘다.
|
||||
// 3.x는 Schema.validate(JsonNode) 반환 규격이 달라 런타임 NoSuchMethodError가 발생한다.
|
||||
api 'com.networknt:json-schema-validator:2.0.0'
|
||||
|
||||
// Tool Pod의 /mcp Streamable HTTP Server와 MCP SDK를 제공합니다.
|
||||
api 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc'
|
||||
// Boot 3.5.11 위에서 MCP Java SDK 2.0.0의 Server/Client와 Servlet 전송 계층을 사용합니다.
|
||||
// Jackson 2 공급자를 명시해 Boot 3의 Jackson 계열과 맞춥니다.
|
||||
api 'io.modelcontextprotocol.sdk:mcp-core'
|
||||
api 'io.modelcontextprotocol.sdk:mcp-json-jackson2'
|
||||
|
||||
// Spring AI 1.1.x에서 @McpTool, @McpToolParam, metaProvider를 제공합니다.
|
||||
// Boot 3.5 호환 Spring AI 어노테이션 API를 유지합니다.
|
||||
api 'org.springaicommunity:mcp-annotations:0.9.0'
|
||||
|
||||
// 이벤트 기반 확장이 필요한 Tool의 공통 Kafka 연동 기능입니다.
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package io.shinhanlife.dap.lib.mcp;
|
||||
|
||||
import io.modelcontextprotocol.server.McpServer;
|
||||
import io.modelcontextprotocol.server.McpSyncServer;
|
||||
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -28,6 +31,15 @@ public class ToolMcpServerConfiguration {
|
||||
.build();
|
||||
}
|
||||
|
||||
/** Creates the Tool Pod MCP server directly with MCP Java SDK 2.0.0. */
|
||||
@Bean(destroyMethod = "close")
|
||||
public McpSyncServer toolMcpServer(HttpServletStreamableServerTransportProvider transportProvider) {
|
||||
return McpServer.sync(transportProvider)
|
||||
.serverInfo("dap-tool-pod", "2.0.0")
|
||||
.capabilities(McpSchema.ServerCapabilities.builder().tools(true).build())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ServletRegistrationBean<HttpServletStreamableServerTransportProvider> toolMcpServlet(
|
||||
HttpServletStreamableServerTransportProvider transportProvider) {
|
||||
|
||||
@@ -24,6 +24,9 @@ import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
|
||||
import io.shinhanlife.dap.lib.metadata.ToolDefinition;
|
||||
import io.shinhanlife.dap.lib.metadata.ToolDefinitionRepository;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
@@ -110,12 +113,12 @@ public class ToolRegistryHeartbeatSender {
|
||||
String subToolName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty()
|
||||
? mcpProperties.getNamespace() + "_" + rawSubToolName
|
||||
: rawSubToolName;
|
||||
// @GrowToolHint(register = false)인 Tool은 메타데이터 조회에는 남기되,
|
||||
// @GrowToolHint인 Tool은 메타데이터 조회에는 남기되,
|
||||
// Gateway 등록 및 heartbeat 전송 대상에서는 제외합니다.
|
||||
// ToolHint가 없는 기존 Tool은 이전 동작과 동일하게 등록합니다.
|
||||
boolean isRegister = hintAnnotation == null || hintAnnotation.register();
|
||||
if (!isRegister) {
|
||||
log.info(" [HeartbeatSender] '{}' Tool is excluded from Gateway registration because register=false. (tool name: {})",
|
||||
log.info(" [HeartbeatSender] '{}' Tool is excluded from Gateway registration because . (tool name: {})",
|
||||
baseName, subToolName);
|
||||
}
|
||||
|
||||
@@ -223,26 +226,30 @@ public class ToolRegistryHeartbeatSender {
|
||||
meta.setOwnerOrg(definition.ownerOrg());
|
||||
}
|
||||
|
||||
@Scheduled(fixedRate = 30000)
|
||||
public void sendHeartbeats() {
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void registerAllTools() {
|
||||
if (registeredTools.isEmpty()) return;
|
||||
|
||||
for (ToolMetadata tool : registeredTools) {
|
||||
registerTool(tool);
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void deregisterAllTools() {
|
||||
if (registeredTools.isEmpty()) return;
|
||||
|
||||
for (ToolMetadata tool : registeredTools) {
|
||||
try {
|
||||
ResponseEntity<String> response = restClient.post()
|
||||
.uri(gatewayUrl + "/mcp/api/v1/registry/heartbeat")
|
||||
restClient.post()
|
||||
.uri(gatewayUrl + "/mcp/api/v1/registry/deregister")
|
||||
.header("Content-Type", "application/json")
|
||||
|
||||
.body(tool.getUid())
|
||||
.retrieve()
|
||||
.toEntity(String.class);
|
||||
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
log.info(" [HeartbeatSender] 하트비트 전송 성공: {}", tool.getUid());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(" [HeartbeatSender] 하트비트 전송 실패 ({}): {}. 재등록을 시도합니다.", tool.getUid(), e.getMessage());
|
||||
registerTool(tool);
|
||||
.toBodilessEntity();
|
||||
log.info(" [HeartbeatSender] 툴 삭제(Deregister) 성공: {}", tool.getUid());
|
||||
} catch (Exception ex) {
|
||||
log.warn(" [HeartbeatSender] 툴 삭제 실패: {}", ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,13 +337,8 @@ public class PodScaffolder {
|
||||
dockerfile: %s/Dockerfile
|
||||
ports:
|
||||
- "%s:%s"
|
||||
depends_on:
|
||||
- redis
|
||||
environment:
|
||||
- TZ=Asia/Seoul
|
||||
- SPRING_REDIS_HOST=redis
|
||||
- SPRING_REDIS_PORT=6379
|
||||
- SPRING_DATA_REDIS_PORT=6379
|
||||
- AXHUB_GATEWAY_URL=http://gateway:8081
|
||||
- AXHUB_TOOL_URL=http://%s:%s
|
||||
- GLOW_COMMUNICATION_MCI_HOST=http://mci-mock
|
||||
|
||||
@@ -425,9 +425,9 @@ public class ToolScaffolder {
|
||||
if (routingType.trim().isEmpty()) {
|
||||
routingType = "HTTP";
|
||||
}
|
||||
String moduleName = getOrAsk(args, 6, scanner, "7. Target module (default dap-was-oth): ");
|
||||
String moduleName = getOrAsk(args, 6, scanner, "7. Target module (default dap-was-cus): ");
|
||||
if (moduleName.trim().isEmpty()) {
|
||||
moduleName = "dap-was-oth";
|
||||
moduleName = "dap-was-cus";
|
||||
}
|
||||
|
||||
String defaultAuthor = System.getProperty("user.name");
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
package io.shinhanlife.dap.lib.mcp;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import io.modelcontextprotocol.json.schema.jackson2.DefaultJsonSchemaValidator;
|
||||
import io.modelcontextprotocol.spec.McpSchema;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ai.mcp.annotation.McpTool;
|
||||
|
||||
class McpSdk2CompatibilityTest {
|
||||
|
||||
@Test
|
||||
void usesOfficialSpringAiMcpAnnotations() {
|
||||
assertEquals("org.springframework.ai.mcp.annotation", McpTool.class.getPackageName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void usesMcpJavaSdkTwo() {
|
||||
assertEquals("2.0.0", McpSchema.class.getPackage().getImplementationVersion());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validatesJsonSchemaWithTheNetworkntVersionRequiredByMcpSdkTwo() {
|
||||
DefaultJsonSchemaValidator validator = new DefaultJsonSchemaValidator();
|
||||
|
||||
var response = validator.validateSchema(Map.of(
|
||||
"$schema", McpSchema.JSON_SCHEMA_DIALECT_2020_12,
|
||||
"type", "object",
|
||||
"properties", Map.of("employeeId", Map.of("type", "string"))));
|
||||
|
||||
assertTrue(response.valid(), response.errorMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,4 +50,17 @@ class PodScaffolderTest {
|
||||
assertTrue(Files.readString(resources.resolve("application-prod.yml"))
|
||||
.contains("classpath:glow/application-glow-prod.yml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatesRedisOptionalPodComposeConfiguration() throws Exception {
|
||||
Files.writeString(root.resolve("settings.gradle"), "rootProject.name = 'test'\n");
|
||||
Files.writeString(root.resolve("docker-compose.yml"), "services:\n");
|
||||
|
||||
PodScaffolder.scaffoldPod(root, "dap-was-pro", "8085", "pro", "tester", "2026.08.13");
|
||||
|
||||
String compose = Files.readString(root.resolve("docker-compose.yml"));
|
||||
assertTrue(compose.contains("was-pro:"));
|
||||
assertFalse(compose.contains("depends_on:\n - redis"));
|
||||
assertFalse(compose.contains("SPRING_REDIS_HOST=redis"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
|
||||
import io.modelcontextprotocol.server.McpSyncServer;
|
||||
import io.shinhanlife.dap.lib.mcp.ToolMcpServerConfiguration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||
@@ -20,4 +21,14 @@ class ToolMcpServerConfigurationTest {
|
||||
|
||||
assertThat(registration.getUrlMappings()).containsExactlyInAnyOrder("/mcp", "/mcp/message");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsMcpSdkTwoSyncServerWithoutSpringAiAutoConfiguration() {
|
||||
HttpServletStreamableServerTransportProvider transport = configuration.toolMcpTransportProvider();
|
||||
|
||||
McpSyncServer server = configuration.toolMcpServer(transport);
|
||||
|
||||
assertEquals("dap-tool-pod", server.getServerInfo().name());
|
||||
server.close();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user