Skip to content

Commit 79f5906

Browse files
committed
test: add minimal coverage for completions, async, and context
Move completion fixtures into integration scope and verify them via MCP client completeCompletion. Add STREAMABLE ASYNC callTool integration smoke test, McpApplicationContext unit tests, and CompletionSupport error-path tests. Align generateCode prompt and file://{path} resource templates with completion argument names.
1 parent 4cf702b commit 79f5906

9 files changed

Lines changed: 249 additions & 7 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package com.github.thought2code.mcp.annotated;
2+
3+
import static org.junit.jupiter.api.Assertions.assertFalse;
4+
import static org.junit.jupiter.api.Assertions.assertSame;
5+
import static org.junit.jupiter.api.Assertions.assertThrows;
6+
import static org.junit.jupiter.api.Assertions.assertTrue;
7+
8+
import com.github.thought2code.mcp.annotated.annotation.McpServerApplication;
9+
import com.github.thought2code.mcp.annotated.enums.McpServerError;
10+
import com.github.thought2code.mcp.annotated.exception.McpServerException;
11+
import com.github.thought2code.mcp.annotated.integration.IntegrationMcpApplication;
12+
import com.github.thought2code.mcp.annotated.test.TestMcpTools;
13+
import com.github.thought2code.mcp.context.alpha.AlphaMcpApplication;
14+
import org.junit.jupiter.api.Test;
15+
16+
class McpApplicationContextTest {
17+
18+
@Test
19+
void isInScope_shouldReturnFalseForBlankOrMalformedSourceMethod() {
20+
McpApplicationContext context = McpApplicationContext.from(IntegrationMcpApplication.class);
21+
22+
assertFalse(context.isInScope(""));
23+
assertFalse(context.isInScope(" "));
24+
assertFalse(context.isInScope("NoHashMethod"));
25+
}
26+
27+
@Test
28+
void isInScope_shouldFilterByResolvedBasePackage() {
29+
McpApplicationContext integrationContext =
30+
McpApplicationContext.from(IntegrationMcpApplication.class);
31+
32+
assertTrue(
33+
integrationContext.isInScope(
34+
"com.github.thought2code.mcp.annotated.test.TestMcpTools#toolWithDefaultName()"));
35+
assertFalse(
36+
integrationContext.isInScope(
37+
"com.github.thought2code.mcp.context.alpha.AlphaTools#alphaTool()"));
38+
39+
McpApplicationContext alphaContext = McpApplicationContext.from(AlphaMcpApplication.class);
40+
assertTrue(
41+
alphaContext.isInScope("com.github.thought2code.mcp.context.alpha.AlphaTools#alphaTool()"));
42+
assertFalse(
43+
alphaContext.isInScope(
44+
"com.github.thought2code.mcp.annotated.test.TestMcpTools#toolWithDefaultName()"));
45+
}
46+
47+
@Test
48+
void from_shouldResolveBasePackageFromBasePackageAttribute() {
49+
McpApplicationContext context = McpApplicationContext.from(BetaBasePackageApplication.class);
50+
51+
assertTrue(context.isInScope("com.github.thought2code.mcp.context.beta.BetaTools#betaTool()"));
52+
assertFalse(
53+
context.isInScope("com.github.thought2code.mcp.context.alpha.AlphaTools#alphaTool()"));
54+
}
55+
56+
@Test
57+
void getComponentInstance_shouldCacheSingletonPerClass() {
58+
McpApplicationContext context = McpApplicationContext.from(IntegrationMcpApplication.class);
59+
60+
Object first = context.getComponentInstance(TestMcpTools.class);
61+
Object second = context.getComponentInstance(TestMcpTools.class);
62+
63+
assertSame(first, second);
64+
}
65+
66+
@Test
67+
void getComponentInstance_shouldThrowWhenNoNoArgConstructor() {
68+
McpApplicationContext context = McpApplicationContext.from(IntegrationMcpApplication.class);
69+
70+
McpServerException exception =
71+
assertThrows(
72+
McpServerException.class,
73+
() -> context.getComponentInstance(NoNoArgConstructorFixture.class));
74+
75+
assertTrue(
76+
exception.getMessage().contains(McpServerError.COMPONENT_INSTANCE_CREATE_ERROR.getCode()));
77+
assertTrue(exception.getMessage().contains(NoNoArgConstructorFixture.class.getName()));
78+
}
79+
80+
@McpServerApplication(basePackage = "com.github.thought2code.mcp.context.beta")
81+
static class BetaBasePackageApplication {}
82+
83+
static class NoNoArgConstructorFixture {
84+
NoNoArgConstructorFixture(@SuppressWarnings("unused") String ignored) {}
85+
}
86+
}

src/test/java/com/github/thought2code/mcp/annotated/integration/McpApplicationIntegrationTest.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,34 @@ void sseTransport_shouldServeAllFixtureComponents() {
125125
}
126126
}
127127

128+
@Test
129+
void streamableAsyncTransport_shouldCallToolViaMcpClient() {
130+
int port = new Random().nextInt(8000, 9000);
131+
AnnotatedMcpServer server =
132+
TestMcpServerLifecycle.start(context, TestMcpConfigurations.streamableAsync(port));
133+
try {
134+
HttpClientStreamableHttpTransport transport =
135+
HttpClientStreamableHttpTransport.builder("http://localhost:" + port)
136+
.endpoint("/mcp/message")
137+
.build();
138+
139+
try (McpSyncClient client =
140+
McpClient.sync(transport).requestTimeout(requestTimeout).build()) {
141+
client.initialize();
142+
McpSchema.CallToolRequest request =
143+
McpSchema.CallToolRequest.builder("tool_with_default_name").arguments(Map.of()).build();
144+
McpSchema.CallToolResult result = client.callTool(request);
145+
McpSchema.TextContent content = (McpSchema.TextContent) result.content().get(0);
146+
147+
assertFalse(result.isError());
148+
assertEquals("toolWithDefaultName is called", content.text());
149+
}
150+
} finally {
151+
assert server != null;
152+
server.stop();
153+
}
154+
}
155+
128156
@Test
129157
void streamableTransport_shouldServeAllFixtureComponents() {
130158
int port = new Random().nextInt(8000, 9000);

src/test/java/com/github/thought2code/mcp/annotated/server/component/ComponentProviderE2ETest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import static org.mockito.Mockito.mock;
1111
import static org.mockito.Mockito.verify;
1212

13-
import com.github.thought2code.mcp.annotated.McpApplication;
1413
import com.github.thought2code.mcp.annotated.McpApplicationContext;
1514
import com.github.thought2code.mcp.annotated.integration.IntegrationMcpApplication;
1615
import com.github.thought2code.mcp.annotated.server.component.completion.CompletionSupport;
@@ -82,7 +81,7 @@ void resourceRegistration_shouldRegisterGeneratedResourceViaServiceLoader() {
8281

8382
@Test
8483
void completionSupport_shouldBuildAndInvokeGeneratedCompletionViaServiceLoader() {
85-
McpApplicationContext context = McpApplicationContext.from(McpApplication.class);
84+
McpApplicationContext context = McpApplicationContext.from(IntegrationMcpApplication.class);
8685
List<McpServerFeatures.SyncCompletionSpecification> completions =
8786
CompletionSupport.allSync(context);
8887

src/test/java/com/github/thought2code/mcp/annotated/server/component/completion/CompletionSupportTest.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,69 @@ public List<CompletionDefinition> completions() {
147147
() -> CompletionSupport.allAsync(context, List.of(provider)));
148148
assertTrue(exception.getMessage().contains("resource uri 'resource://duplicate'"));
149149
}
150+
151+
@Test
152+
void allSync_shouldThrowWhenInvocationReturnsError() {
153+
McpApplicationContext context = mock(McpApplicationContext.class);
154+
when(context.isInScope(anyString())).thenReturn(true);
155+
ComponentProvider provider =
156+
new ComponentProvider() {
157+
@Override
158+
public List<CompletionDefinition> completions() {
159+
return List.of(
160+
new CompletionDefinition(
161+
"test.Source#error()",
162+
McpSchema.PromptReference.builder("prompt_name").build(),
163+
(ctx, argument) ->
164+
Invocation.builder().result("failed").isError(true).build()));
165+
}
166+
};
167+
168+
List<McpServerFeatures.SyncCompletionSpecification> specs =
169+
CompletionSupport.allSync(context, List.of(provider));
170+
McpSchema.CompleteRequest request =
171+
new McpSchema.CompleteRequest(
172+
McpSchema.PromptReference.builder("prompt_name").build(),
173+
new McpSchema.CompleteRequest.CompleteArgument("arg", "v"));
174+
175+
McpServerComponentRegistrationException exception =
176+
assertThrows(
177+
McpServerComponentRegistrationException.class,
178+
() -> specs.get(0).completionHandler().apply(null, request));
179+
assertTrue(
180+
exception.getMessage().contains("Completion invocation failed for test.Source#error()"));
181+
}
182+
183+
@Test
184+
void allSync_shouldThrowWhenInvocationReturnsNonCompletionResult() {
185+
McpApplicationContext context = mock(McpApplicationContext.class);
186+
when(context.isInScope(anyString())).thenReturn(true);
187+
ComponentProvider provider =
188+
new ComponentProvider() {
189+
@Override
190+
public List<CompletionDefinition> completions() {
191+
return List.of(
192+
new CompletionDefinition(
193+
"test.Source#wrongReturn()",
194+
McpSchema.PromptReference.builder("prompt_name").build(),
195+
(ctx, argument) -> Invocation.builder().result("not-a-completion").build()));
196+
}
197+
};
198+
199+
List<McpServerFeatures.SyncCompletionSpecification> specs =
200+
CompletionSupport.allSync(context, List.of(provider));
201+
McpSchema.CompleteRequest request =
202+
new McpSchema.CompleteRequest(
203+
McpSchema.PromptReference.builder("prompt_name").build(),
204+
new McpSchema.CompleteRequest.CompleteArgument("arg", "v"));
205+
206+
McpServerComponentRegistrationException exception =
207+
assertThrows(
208+
McpServerComponentRegistrationException.class,
209+
() -> specs.get(0).completionHandler().apply(null, request));
210+
assertTrue(
211+
exception
212+
.getMessage()
213+
.contains("Completion method must return CompletionResult: test.Source#wrongReturn()"));
214+
}
150215
}

src/test/java/com/github/thought2code/mcp/annotated/support/McpClientVerificationSupport.java

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,27 @@ public static void verifyAll(McpSyncClient client) {
2828
verifyPromptsCalled(client);
2929
verifyToolsRegistered(client);
3030
verifyToolsCalled(client);
31+
verifyCompletions(client);
32+
}
33+
34+
public static void verifyCompletions(McpSyncClient client) {
35+
McpSchema.CompleteRequest promptRequest =
36+
new McpSchema.CompleteRequest(
37+
McpSchema.PromptReference.builder("generateCode").build(),
38+
new McpSchema.CompleteRequest.CompleteArgument("language", ""));
39+
McpSchema.CompleteResult promptResult = client.completeCompletion(promptRequest);
40+
assertEquals(List.of("Java", "Python"), promptResult.completion().values());
41+
assertEquals(2, promptResult.completion().total());
42+
assertFalse(promptResult.completion().hasMore());
43+
44+
McpSchema.CompleteRequest resourceRequest =
45+
new McpSchema.CompleteRequest(
46+
new McpSchema.ResourceReference("file://{path}"),
47+
new McpSchema.CompleteRequest.CompleteArgument("path", "file"));
48+
McpSchema.CompleteResult resourceResult = client.completeCompletion(resourceRequest);
49+
assertEquals(List.of("file://a", "file://b"), resourceResult.completion().values());
50+
assertEquals(2, resourceResult.completion().total());
51+
assertTrue(resourceResult.completion().hasMore());
3152
}
3253

3354
public static void verifyServerInfo(McpSyncClient client) {
@@ -39,7 +60,7 @@ public static void verifyServerInfo(McpSyncClient client) {
3960

4061
public static void verifyResourcesRegistered(McpSyncClient client) {
4162
List<McpSchema.Resource> resources = client.listResources().resources();
42-
assertEquals(2, resources.size());
63+
assertEquals(3, resources.size());
4364
verifyResourceRegistered(
4465
resources,
4566
"test://resource1",
@@ -52,6 +73,12 @@ public static void verifyResourcesRegistered(McpSyncClient client) {
5273
"resource2_name",
5374
"resource2_title",
5475
"resource2_description");
76+
verifyResourceRegistered(
77+
resources,
78+
"file://{path}",
79+
"file_resource",
80+
"file_resource",
81+
"File resource for completion integration fixture");
5582
}
5683

5784
private static void verifyResourceRegistered(
@@ -89,7 +116,7 @@ private static void verifyResourceCalled(
89116

90117
public static void verifyPromptsRegistered(McpSyncClient client) {
91118
List<McpSchema.Prompt> prompts = client.listPrompts().prompts();
92-
assertEquals(11, prompts.size());
119+
assertEquals(12, prompts.size());
93120
verifyPromptRegistered(prompts, "prompt_with_default_name", "title", "description", 0);
94121
verifyPromptRegistered(
95122
prompts, "promptWithDefaultTitle", "promptWithDefaultTitle", "description", 0);
@@ -137,6 +164,12 @@ public static void verifyPromptsRegistered(McpSyncClient client) {
137164
"prompt_with_return_null",
138165
"prompt_with_return_null",
139166
0);
167+
verifyPromptRegistered(
168+
prompts,
169+
"generateCode",
170+
"generateCode",
171+
"Prompt used by completion integration fixture",
172+
1);
140173
verifyPromptRegistered(
141174
prompts, "prompt_with_exception", "prompt_with_exception", "prompt_with_exception", 0);
142175
}
@@ -195,6 +228,11 @@ public static void verifyPromptsCalled(McpSyncClient client) {
195228
"prompt_with_return_null",
196229
Map.of(),
197230
"The method call succeeded but the return value is null");
231+
verifyPromptCalled(
232+
client,
233+
"generateCode",
234+
Map.of("language", "Java"),
235+
"generateCode is called with language: Java");
198236
verifyPromptCalled(
199237
client,
200238
"prompt_with_exception",

src/test/java/com/github/thought2code/mcp/annotated/support/TestMcpConfigurations.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,17 @@ public static ServerConfiguration stdio() {
3838
}
3939

4040
public static ServerConfiguration streamable(int port) {
41+
return streamable(port, ServerType.SYNC);
42+
}
43+
44+
public static ServerConfiguration streamableAsync(int port) {
45+
return streamable(port, ServerType.ASYNC);
46+
}
47+
48+
public static ServerConfiguration streamable(int port, ServerType type) {
4149
return baseBuilder()
4250
.mode(ServerMode.STREAMABLE)
51+
.type(type)
4352
.streamable(
4453
ServerStreamable.builder()
4554
.mcpEndpoint("/mcp/message")

src/test/java/com/github/thought2code/mcp/annotated/support/TestMcpCompletions.java renamed to src/test/java/com/github/thought2code/mcp/annotated/test/TestMcpCompletions.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
package com.github.thought2code.mcp.annotated.support;
1+
package com.github.thought2code.mcp.annotated.test;
22

33
import com.github.thought2code.mcp.annotated.annotation.McpPromptCompletion;
44
import com.github.thought2code.mcp.annotated.annotation.McpResourceCompletion;
55
import com.github.thought2code.mcp.annotated.server.component.completion.CompletionResult;
66
import io.modelcontextprotocol.spec.McpSchema;
77
import java.util.List;
88

9-
/** Fixture used to verify MCP completion specification creation and invocation. */
9+
/** Fixture completions registered within the integration test base package scope. */
1010
public class TestMcpCompletions {
1111

1212
@McpPromptCompletion(name = "generateCode", title = "Code languages")
@@ -19,7 +19,7 @@ public CompletionResult completeGenerateCode(
1919
.build();
2020
}
2121

22-
@McpResourceCompletion(uri = "file://")
22+
@McpResourceCompletion(uri = "file://{path}")
2323
public CompletionResult completeFileUri(McpSchema.CompleteRequest.CompleteArgument argument) {
2424
return CompletionResult.builder()
2525
.values(List.of("file://a", "file://b"))

src/test/java/com/github/thought2code/mcp/annotated/test/TestMcpPrompts.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,13 @@ public String promptWithMixedParams(
6868
"promptWithMixedParams is called with params: %s, %s", mcpParam, nonMcpParam);
6969
}
7070

71+
@McpPrompt(name = "generateCode", description = "Prompt used by completion integration fixture")
72+
public String generateCode(
73+
@McpPromptParam(name = "language", description = "Programming language") String language) {
74+
log.debug("calling generateCode with language: {}", language);
75+
return "generateCode is called with language: " + language;
76+
}
77+
7178
@McpPrompt
7279
public String promptWithException() {
7380
throw new IllegalStateException("sensitive prompt failure detail");

src/test/java/com/github/thought2code/mcp/annotated/test/TestMcpResources.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,14 @@ public String resource1() {
1717
log.debug("calling resource1");
1818
return "resource1_content";
1919
}
20+
21+
@McpResource(
22+
uri = "file://{path}",
23+
name = "file_resource",
24+
title = "file_resource",
25+
description = "File resource for completion integration fixture")
26+
public String fileResource() {
27+
log.debug("calling fileResource");
28+
return "file_resource_content";
29+
}
2030
}

0 commit comments

Comments
 (0)