Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ MONGODB_URI=mongodb://koin:your-mongo-password@127.0.0.1:27017/koin?authSource=a

# Swagger
SWAGGER_SERVER_URL=http://localhost:8080
KOIN_MCP_ENABLED=false

# AWS SES
AWS_SES_ACCESS_KEY=test
Expand Down
13 changes: 13 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ repositories {
mavenCentral()
}

dependencyManagement {
imports {
mavenBom 'org.springframework.ai:spring-ai-bom:1.1.2'
}
}

dependencies {
// spring boot starters
implementation 'org.springframework.boot:spring-boot-starter-web'
Expand Down Expand Up @@ -84,6 +90,9 @@ dependencies {
// swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'

// mcp
implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc'

// lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
Expand All @@ -108,3 +117,7 @@ tasks.named('bootBuildImage') {
tasks.named('test') {
useJUnitPlatform()
}

tasks.withType(JavaCompile).configureEach {
options.compilerArgs += '-parameters'
}
24 changes: 24 additions & 0 deletions src/main/java/in/koreatech/koin/global/code/Deprecation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package in.koreatech.koin.global.code;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Documented
@Target(METHOD)
@Retention(RUNTIME)
public @interface Deprecation {

String since() default "";

String reason() default "";

String replacedByMethod() default "";

String replacedByPath() default "";

boolean forRemoval() default false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package in.koreatech.koin.global.code;

import java.lang.reflect.Method;
import java.util.Map;

import org.springdoc.core.customizers.OperationCustomizer;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;

import io.swagger.v3.oas.models.Operation;

@Component
public class DeprecationOperationCustomizer implements OperationCustomizer {

public static final String EXTENSION_SINCE = "x-deprecated-since";
public static final String EXTENSION_REASON = "x-deprecated-reason";
public static final String EXTENSION_REPLACED_BY = "x-replaced-by";
public static final String EXTENSION_FOR_REMOVAL = "x-for-removal";

@Override
public Operation customize(Operation operation, HandlerMethod handlerMethod) {
Deprecation deprecation = findDeprecation(handlerMethod);
if (deprecation == null) {
return operation;
}

operation.setDeprecated(true);
addIfNotBlank(operation, EXTENSION_SINCE, deprecation.since());
addIfNotBlank(operation, EXTENSION_REASON, deprecation.reason());
if (!deprecation.replacedByMethod().isBlank() || !deprecation.replacedByPath().isBlank()) {
operation.addExtension(EXTENSION_REPLACED_BY, Map.of(
"method", deprecation.replacedByMethod(),
"path", deprecation.replacedByPath()
));
}
operation.addExtension(EXTENSION_FOR_REMOVAL, deprecation.forRemoval());
return operation;
}

private void addIfNotBlank(Operation operation, String name, String value) {
if (!value.isBlank()) {
operation.addExtension(name, value);
}
}

private Deprecation findDeprecation(HandlerMethod handlerMethod) {
Deprecation deprecation = handlerMethod.getMethodAnnotation(Deprecation.class);
if (deprecation != null) {
return deprecation;
}

Method controllerMethod = handlerMethod.getMethod();
for (Class<?> apiInterface : handlerMethod.getBeanType().getInterfaces()) {
Deprecation interfaceDeprecation = findInterfaceMethodDeprecation(apiInterface, controllerMethod);
if (interfaceDeprecation != null) {
return interfaceDeprecation;
}
}
return null;
}

private Deprecation findInterfaceMethodDeprecation(Class<?> apiInterface, Method controllerMethod) {
for (Method interfaceMethod : apiInterface.getMethods()) {
if (hasSameSignature(interfaceMethod, controllerMethod)) {
return interfaceMethod.getAnnotation(Deprecation.class);
}
}
return null;
}

private boolean hasSameSignature(Method left, Method right) {
if (!left.getName().equals(right.getName())) {
return false;
}
Class<?>[] leftTypes = left.getParameterTypes();
Class<?>[] rightTypes = right.getParameterTypes();
if (leftTypes.length != rightTypes.length) {
return false;
}
for (int i = 0; i < leftTypes.length; i++) {
if (!leftTypes[i].equals(rightTypes[i])) {
return false;
}
}
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,29 @@
import org.springframework.context.annotation.Configuration;

import in.koreatech.koin.global.code.ApiResponseCodesOperationCustomizer;
import in.koreatech.koin.global.code.DeprecationOperationCustomizer;

@Configuration
public class SwaggerGroupConfig {

private final ApiResponseCodesOperationCustomizer customizer;
private final ApiResponseCodesOperationCustomizer apiResponseCodesOperationCustomizer;
private final DeprecationOperationCustomizer deprecationOperationCustomizer;

public SwaggerGroupConfig(ApiResponseCodesOperationCustomizer customizer) {
this.customizer = customizer;
public SwaggerGroupConfig(
ApiResponseCodesOperationCustomizer apiResponseCodesOperationCustomizer,
DeprecationOperationCustomizer deprecationOperationCustomizer
) {
this.apiResponseCodesOperationCustomizer = apiResponseCodesOperationCustomizer;
this.deprecationOperationCustomizer = deprecationOperationCustomizer;
}

@Bean
public GroupedOpenApi loginApi() {
return GroupedOpenApi.builder()
.group("0. Login API")
.pathsToMatch("/**/login")
.addOperationCustomizer(customizer)
.addOperationCustomizer(deprecationOperationCustomizer)
.addOperationCustomizer(apiResponseCodesOperationCustomizer)
.build();
}

Expand Down Expand Up @@ -133,7 +140,8 @@ private GroupedOpenApi createGroupedOpenApi(
return GroupedOpenApi.builder()
.group(groupName)
.packagesToScan(packagesPath)
.addOperationCustomizer(customizer)
.addOperationCustomizer(deprecationOperationCustomizer)
.addOperationCustomizer(apiResponseCodesOperationCustomizer)
.build();
}
}
9 changes: 9 additions & 0 deletions src/main/java/in/koreatech/koin/mcp/McpConstants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package in.koreatech.koin.mcp;

public final class McpConstants {

public static final String SERVER_ENABLED_PROPERTY = "spring.ai.mcp.server.enabled";

private McpConstants() {
}
}
22 changes: 22 additions & 0 deletions src/main/java/in/koreatech/koin/mcp/config/McpToolConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package in.koreatech.koin.mcp.config;

import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import in.koreatech.koin.mcp.McpConstants;
import in.koreatech.koin.mcp.tool.EndpointSpecTools;

@Configuration
@ConditionalOnProperty(name = McpConstants.SERVER_ENABLED_PROPERTY, havingValue = "true")
public class McpToolConfig {

@Bean
public ToolCallbackProvider endpointSpecToolCallbackProvider(EndpointSpecTools endpointSpecTools) {
return MethodToolCallbackProvider.builder()
.toolObjects(endpointSpecTools)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package in.koreatech.koin.mcp.dto.endpoint;

public record EndpointCandidate(
String group,
String method,
String path
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package in.koreatech.koin.mcp.dto.endpoint;

import java.util.List;

public record EndpointDescription(
String group,
String method,
String path,
String operationId,
String summary,
String description,
List<String> tags,
boolean deprecated,
String deprecatedReason,
ReplacedBy replacedBy,
boolean authRequired
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package in.koreatech.koin.mcp.dto.endpoint;

import java.util.List;

public record EndpointSummary(
String group,
String method,
String path,
String operationId,
String summary,
String description,
List<String> tags,
boolean deprecated,
boolean authRequired
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package in.koreatech.koin.mcp.dto.endpoint;

import java.util.List;

public record FindEndpointsResponse(List<EndpointSummary> items) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package in.koreatech.koin.mcp.dto.endpoint;

public record ReplacedBy(
String method,
String path
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package in.koreatech.koin.mcp.dto.endpoint.request;

import in.koreatech.koin.mcp.dto.schema.EndpointSchema;

public record EndpointParameter(
String name,
boolean required,
String description,
EndpointSchema schema
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package in.koreatech.koin.mcp.dto.endpoint.request;

import java.util.List;

public record EndpointParameters(
List<EndpointParameter> path,
List<EndpointParameter> query,
List<EndpointParameter> header
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package in.koreatech.koin.mcp.dto.endpoint.request;

public record EndpointRequestSpec(
String group,
String method,
String path,
EndpointParameters parameters,
RequestBodySpec requestBody
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package in.koreatech.koin.mcp.dto.endpoint.request;

import java.util.List;

import in.koreatech.koin.mcp.dto.schema.EndpointSchema;

public record RequestBodySpec(
boolean required,
List<String> contentTypes,
EndpointSchema schema
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package in.koreatech.koin.mcp.dto.endpoint.response;

import java.util.List;

import com.fasterxml.jackson.annotation.JsonInclude;

import in.koreatech.koin.mcp.dto.schema.EndpointSchema;

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record EndpointResponse(
String status,
String description,
List<String> contentTypes,
EndpointSchema schema
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package in.koreatech.koin.mcp.dto.endpoint.response;

import java.util.List;

public record EndpointResponseSpec(
String group,
String method,
String path,
List<EndpointResponse> responses
) {
}
17 changes: 17 additions & 0 deletions src/main/java/in/koreatech/koin/mcp/dto/error/McpError.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package in.koreatech.koin.mcp.dto.error;

import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.annotation.JsonInclude;

import in.koreatech.koin.mcp.dto.endpoint.EndpointCandidate;

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record McpError(
String code,
String message,
Map<String, String> details,
List<EndpointCandidate> candidates
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package in.koreatech.koin.mcp.dto.error;

import in.koreatech.koin.mcp.exception.EndpointSpecException;

public record McpErrorResponse(McpError error) {

public static McpErrorResponse from(EndpointSpecException exception) {
return new McpErrorResponse(new McpError(
exception.getCode(),
exception.getMessage(),
exception.getDetails(),
exception.getCandidates()
));
}
}
Loading
Loading