Skip to content
Merged
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
21 changes: 21 additions & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: MCP Integration Tests

on:
schedule:
- cron: '0 0 * * *'

jobs:
integration-tests:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6
- name: Set up JDK 25
uses: actions/setup-java@v5
with:
java-version: '25'
distribution: 'temurin'
cache: maven
- name: Run MCP streamable HTTP integration tests
run: mvn -B verify -P integration-tests --file pom.xml
24 changes: 24 additions & 0 deletions data/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,30 @@
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>integration-tests</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
<configuration>
<argLine>--add-reads datamodule=ALL-UNNAMED</argLine>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<dependencyManagement>
<dependencies>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ public Result exec(String... keyCandidates) {
while (dataSource.hasMoreData()) {
String[] row = dataSource.nextRow();
if (finder.found(row)) {
close(dataSource);
return new Result(false, keyCandidates, row);
}
}

Result result = handleSuccessfulCheck(keyCandidates);
close(dataSource);
return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,23 @@
public class Main {

public static void main(String[] args) {
System.setProperty("transport.mode", "stdio");
System.setProperty("spring.main.web-application-type", "none");
String transportMode = resolveTransportMode(args);
System.setProperty("transport.mode", transportMode);
if ("stdio".equals(transportMode)) {
System.setProperty("spring.main.web-application-type", "none");
}
System.setProperty("banner-mode", "off");
SpringApplication.run(Main.class, args);
}

private static String resolveTransportMode(String[] args) {
String prefix = "--transport.mode=";
for (String arg : args) {
if (arg.startsWith(prefix)) {
return arg.substring(prefix.length());
}
}
return "stdio";
}

}
Original file line number Diff line number Diff line change
@@ -1,60 +1,92 @@
package io.github.adamw7.tools.data.uniqueness.mcp;

import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification;
import io.modelcontextprotocol.server.McpSyncServerExchange;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import com.fasterxml.jackson.databind.ObjectMapper;

import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
import io.modelcontextprotocol.server.transport.StdioServerTransportProvider;
import io.modelcontextprotocol.json.jackson2.JacksonMcpJsonMapper;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpServerTransportProvider;

import java.util.function.BiFunction;
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;

@Configuration
public class McpConfiguration {

private final static Logger log = LogManager.getLogger(McpConfiguration.class.getName());
private static final Logger log = LogManager.getLogger(McpConfiguration.class.getName());

@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}

@Bean
@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "stdio", matchIfMissing = true)
public StdioServerTransportProvider stdioServerTransport() {
log.info("Creating StdioServerTransport");
return new StdioServerTransportProvider(new JacksonMcpJsonMapper(objectMapper()));
}
@Bean
@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "stdio", matchIfMissing = true)
public StdioServerTransportProvider stdioServerTransport() {
log.info("Creating StdioServerTransport");
return new StdioServerTransportProvider(new JacksonMcpJsonMapper(objectMapper()));
}

@Bean
@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "streamable-http")
public HttpServletStreamableServerTransportProvider streamableServerTransport() {
log.info("Creating HttpServletStreamableServerTransport");
return HttpServletStreamableServerTransportProvider.builder()
.jsonMapper(new JacksonMcpJsonMapper(objectMapper()))
.mcpEndpoint("/mcp")
.build();
}

@Bean
@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "streamable-http")
public ServletRegistrationBean<HttpServletStreamableServerTransportProvider> streamableServletRegistration(
HttpServletStreamableServerTransportProvider transport) {
ServletRegistrationBean<HttpServletStreamableServerTransportProvider> registration =
new ServletRegistrationBean<>(transport, "/mcp");
registration.setAsyncSupported(true);
return registration;
}

@Bean(destroyMethod = "close")
@ConditionalOnBean(McpServerTransportProvider.class)
public McpSyncServer mcpSyncServer(McpServerTransportProvider transport) {
log.info("Initializing McpSyncServer with transport: {}", transport);
McpSyncServer syncServer = McpServer.sync(transport)
.serverInfo("custom-server", "0.0.1")
.capabilities(McpSchema.ServerCapabilities.builder().tools(true).resources(false, false).prompts(false).build())
.build();
registerTools(syncServer);
return syncServer;
}

McpSyncServer syncServer = McpServer.sync(transport).serverInfo("custom-server", "0.0.1").capabilities(
McpSchema.ServerCapabilities.builder().tools(true).resources(false, false).prompts(false).build())
@Bean(destroyMethod = "close")
@ConditionalOnProperty(prefix = "transport", name = "mode", havingValue = "streamable-http")
public McpSyncServer mcpSyncServerStreamable(McpStreamableServerTransportProvider transport) {
log.info("Initializing McpSyncServer with streamable transport: {}", transport);
McpSyncServer syncServer = McpServer.sync(transport)
.serverInfo("custom-server", "0.0.1")
.capabilities(McpSchema.ServerCapabilities.builder().tools(true).resources(false, false).prompts(false).build())
.build();
registerTools(syncServer);
return syncServer;
}

private void registerTools(McpSyncServer server) {
UniquenessTool uniquenessTool = new UniquenessTool();

SyncToolSpecification.Builder tool = SyncToolSpecification.builder().tool(uniquenessTool.getToolDefinition());
tool.callHandler(new BiFunction<McpSyncServerExchange, McpSchema.CallToolRequest, McpSchema.CallToolResult>() {
@Override
public McpSchema.CallToolResult apply(McpSyncServerExchange mcpSyncServerExchange, McpSchema.CallToolRequest callToolRequest) {
return uniquenessTool.apply(callToolRequest.arguments());
}
});
syncServer.addTool(tool.build());

return syncServer;
SyncToolSpecification toolSpec = SyncToolSpecification.builder()
.tool(uniquenessTool.getToolDefinition())
.callHandler((exchange, request) -> uniquenessTool.apply(request.arguments()))
.build();
server.addTool(toolSpec);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,64 @@ void negativeDuplicatesInInputArray(Class<AbstractUniqueness> uniquenessClass, I
assertEquals("Duplicate in input: year1", thrown.getMessage());
}

@Test
public void notUniquePathClosesDataSource() throws Exception {
ClosingSpyDataSource source = new ClosingSpyDataSource(
Utils.createDataSource(Utils.getHouseholdFile(), COLUMNS_ROW));
NoMemoryUniquenessCheck uniqueness = new NoMemoryUniquenessCheck();
uniqueness.setDataSource(source);

Result result = uniqueness.exec(NOT_UNIQUE_COLUMNS);

assertFalse(result.isUnique());
assertTrue(source.isClosed(), "Data source must be closed when a duplicate is found");
}

private static final class ClosingSpyDataSource implements IterableDataSource {

private final IterableDataSource delegate;
private boolean closed = false;

ClosingSpyDataSource(IterableDataSource delegate) {
this.delegate = delegate;
}

boolean isClosed() {
return closed;
}

@Override
public void close() throws java.io.IOException {
closed = true;
delegate.close();
}

@Override
public String[] getColumnNames() {
return delegate.getColumnNames();
}

@Override
public void open() {
delegate.open();
}

@Override
public String[] nextRow() {
return delegate.nextRow();
}

@Override
public boolean hasMoreData() {
return delegate.hasMoreData();
}

@Override
public void reset() {
delegate.reset();
}
}

@Test
public void negativeInMemorySourceVsNoMemoryCheck() {
AbstractUniqueness uniqueness = new InMemoryUniquenessCheck();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package io.github.adamw7.tools.data.uniqueness.mcp;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.json.jackson2.JacksonMcpJsonMapper;
import io.modelcontextprotocol.server.McpSyncServer;
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
import io.modelcontextprotocol.server.transport.StdioServerTransportProvider;

public class McpConfigurationTest {
Expand All @@ -25,4 +27,26 @@ public void stdioTransportIsNotNull() {
McpConfiguration config = new McpConfiguration();
assertFalse(config.stdioServerTransport() == null);
}

@Test
public void streamableTransportIsNotNull() {
McpConfiguration config = new McpConfiguration();
assertNotNull(config.streamableServerTransport());
}

@Test
public void streamableServletRegistrationIsNotNull() {
McpConfiguration config = new McpConfiguration();
HttpServletStreamableServerTransportProvider transport = config.streamableServerTransport();
assertNotNull(config.streamableServletRegistration(transport));
}

@Test
public void mcpSyncServerStreamableHasTools() {
McpConfiguration config = new McpConfiguration();
HttpServletStreamableServerTransportProvider transport = config.streamableServerTransport();
McpSyncServer server = config.mcpSyncServerStreamable(transport);
assertNotNull(server.getServerCapabilities().tools());
server.close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package io.github.adamw7.tools.data.uniqueness.mcp;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

import java.util.Map;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;

import io.github.adamw7.tools.data.Utils;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
import io.modelcontextprotocol.spec.McpSchema;

@SpringBootTest(
classes = Main.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = { "transport.mode=streamable-http", "spring.main.banner-mode=off" })
public class McpStreamableHttpIT {

@LocalServerPort
private int port;

private McpSyncClient client;

@BeforeEach
void setUp() {
HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport
.builder("http://localhost:" + port)
.build();
client = McpClient.sync(transport)
.clientInfo(McpSchema.Implementation.builder("integration-test-client", "1.0").build())
.build();
client.initialize();
}

@AfterEach
void tearDown() {
client.close();
}

@Test
void nonUniqueColumnReturnsFalse() {
McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder("uniqueness_check")
.arguments(Map.of("file", Utils.getHouseholdFile(), "columns_row", 1, "columns_name", "year1"))
.build();

McpSchema.CallToolResult result = client.callTool(request);

assertFalse(result.isError());
McpSchema.TextContent content = (McpSchema.TextContent) result.content().getFirst();
assertEquals("false", content.text());
}

@Test
void uniqueColumnReturnsTrue() {
McpSchema.CallToolRequest request = McpSchema.CallToolRequest.builder("uniqueness_check")
.arguments(Map.of("file", Utils.getHouseholdFile(), "columns_row", 1, "columns_name", "income"))
.build();

McpSchema.CallToolResult result = client.callTool(request);

assertFalse(result.isError());
McpSchema.TextContent content = (McpSchema.TextContent) result.content().getFirst();
assertEquals("true", content.text());
}
}
Loading